SAGA: Support decoding AGA and ECS graphics

This commit is contained in:
Vladimir Serbinenko 2022-11-12 06:06:52 +01:00 committed by Eugene Sandulenko
parent 8ff65b22cc
commit ee8151e007
8 changed files with 317 additions and 22 deletions

View File

@ -389,7 +389,7 @@ void Anim::load(uint16 animId, const ByteArray &resourceData) {
} else
anim = _animations[animId] = new AnimationData();
ByteArrayReadStreamEndian headerReadS(resourceData, _vm->isBigEndian());
ByteArrayReadStreamEndian headerReadS(resourceData, _vm->isBigEndian() && !_vm->isAGA() && !_vm->isECS());
anim->magic = headerReadS.readUint16LE(); // cause ALWAYS LE
anim->screenWidth = headerReadS.readUint16();
anim->screenHeight = headerReadS.readUint16();
@ -689,6 +689,70 @@ void Anim::decodeFrame(AnimationData *anim, size_t frameOffset, byte *buf, size_
#define VALIDATE_WRITE_POINTER
#endif
if (_vm->isAGA() || _vm->isECS()) {
int curY = 0, curX = 0;
unsigned realY;
unsigned outbit;
// TODO: Check if we want to use tempaltes instead to optimize AGA case
unsigned int pixelSize = _vm->isAGA() ? 8 : 5;
while (1) {
markByte = readS.readByte();
if (markByte == SAGA_FRAME_AMIGA_END)
break;
if (markByte == SAGA_FRAME_AMIGA_START) {
xStart = readS.readByte();
yStart = readS.readUint16BE();
/* int xPos = */ readS.readUint16BE();
/* int yPos = */ readS.readUint16BE();
/* int width = */ readS.readUint16BE();
/*int height = */ readS.readUint16BE();
curX = xStart;
curY = yStart;
realY = curY / pixelSize;
outbit = curY % pixelSize;
continue;
}
uint8 param = markByte & SAGA_FRAME_AMIGA_PARAM_MASK;
switch (markByte & SAGA_FRAME_AMIGA_OPCODE_MASK) {
case SAGA_FRAME_AMIGA_OPCODE_LITERAL:
for (i = 0; i < param + 1; i++, curX++) {
byte b = readS.readByte();
for (unsigned inbit = 0;inbit < 8; inbit++) {
unsigned realX = (curX << 3) + 7 - inbit;
if (realX < screenWidth && realY < screenHeight) {
buf[realX + realY * screenWidth] =
(buf[realX + realY * screenWidth] & (~(1 << outbit))) | (((b >> inbit) & 1) << outbit);
}
}
}
continue;
case SAGA_FRAME_AMIGA_OPCODE_TRANSPARENT:
curX += param;
continue;
case SAGA_FRAME_AMIGA_OPCODE_NEWLINE:
curY++;
curX = param;
outbit++;
if (outbit >= pixelSize) {
outbit -= pixelSize;
realY++;
}
continue;
case SAGA_FRAME_AMIGA_OPCODE_REPOSITION:
curY = readS.readUint16BE();
realY = curY / pixelSize;
outbit = curY % pixelSize;
curX = param;
continue;
}
}
return;
}
// Begin RLE decompression to output buffer
do {
@ -815,6 +879,44 @@ int Anim::fillFrameOffsets(AnimationData *anim, bool reallyFill) {
Common::MemoryReadStreamEndian readS(&anim->resourceData.front(), anim->resourceData.size(), !_vm->isBigEndian()); // RLE has inversion BE<>LE
if (_vm->isAGA() || _vm->isECS()) {
while (readS.pos() != readS.size()) {
if (reallyFill) {
anim->frameOffsets[currentFrame] = readS.pos();
if (currentFrame == anim->maxFrame) {
break;
}
}
currentFrame++;
while (1) {
markByte = readS.readByte();
// debug(7, "_pos=%X currentFrame=%i markByte=%X", (int) readS.pos(), (int) currentFrame, (int) markByte);
if (markByte == SAGA_FRAME_AMIGA_END)
break;
if (markByte == SAGA_FRAME_AMIGA_START) {
readS.seek(11, SEEK_CUR);
continue;
}
switch (markByte & SAGA_FRAME_AMIGA_OPCODE_MASK) {
case SAGA_FRAME_AMIGA_OPCODE_LITERAL:
readS.seek((markByte & SAGA_FRAME_AMIGA_PARAM_MASK) + 1, SEEK_CUR);
continue;
case SAGA_FRAME_AMIGA_OPCODE_TRANSPARENT:
case SAGA_FRAME_AMIGA_OPCODE_NEWLINE:
continue;
case SAGA_FRAME_AMIGA_OPCODE_REPOSITION:
readS.readSint16BE();
continue;
}
}
}
return currentFrame;
}
while (readS.pos() != readS.size()) {
if (reallyFill) {
anim->frameOffsets[currentFrame] = readS.pos();

View File

@ -40,6 +40,15 @@ namespace Saga {
#define SAGA_FRAME_UNCOMPRESSED_RUN 0x40
#define SAGA_FRAME_EMPTY_RUN 0xC0
#define SAGA_FRAME_AMIGA_OPCODE_REPOSITION 0x00
#define SAGA_FRAME_AMIGA_OPCODE_LITERAL 0x40
#define SAGA_FRAME_AMIGA_OPCODE_TRANSPARENT 0xC0
#define SAGA_FRAME_AMIGA_OPCODE_NEWLINE 0x80
#define SAGA_FRAME_AMIGA_OPCODE_MASK 0xC0
#define SAGA_FRAME_AMIGA_PARAM_MASK 0x3F
#define SAGA_FRAME_AMIGA_END 0x3F
#define SAGA_FRAME_AMIGA_START 0x3E
enum AnimationState {
ANIM_PLAYING = 0x01,
ANIM_PAUSE = 0x02,

View File

@ -43,11 +43,45 @@ static int granulate(int value, int granularity) {
}
}
bool SagaEngine::decodeBGImageMask(const ByteArray &imageData, ByteArray &outputBuffer, int *w, int *h, bool flip) {
if (isAGA() || isECS()) {
if (imageData.size() < 160 * 137 + 64)
return false;
*w = 320;
*h = 137;
outputBuffer.resize(320*137);
// First read types
for (int i = 0; i < 160*137; i++) {
outputBuffer[2 * i] = (imageData[i] << 4) | 0xf;
outputBuffer[2 * i + 1] = (imageData[i] << 4) | 0xf;
}
// Now instead of storing depth amiga variant stores precomputed mask for every
// depth. Obviously not every set of precomputed masks is valid but we assume
// that it is. So far it has always been the case. If ever it isn't then we'll
// get a minor graphical glitch
for (int depth = 15; depth > 0; depth--) {
uint32 off = READ_BE_UINT32(&imageData[160 * 137 + 4 * (15 - depth)]);
if (off == 0)
continue;
off += 160 * 137;
if (imageData.size() < off + 137 * 40)
return false;
for (int y = 0; y < 137; y++)
for (int x = 0; x < 320; x++)
if ((imageData[y * 40 + (x / 8) + off] << (x % 8)) & 0x80)
outputBuffer[y * 320 + x] = (outputBuffer[y * 320 + x] & 0xf0) | (depth - 1);
}
return true;
}
return decodeBGImage(imageData, outputBuffer, w, h, flip);
}
bool SagaEngine::decodeBGImage(const ByteArray &imageData, ByteArray &outputBuffer, int *w, int *h, bool flip) {
ImageHeader hdr;
int modex_height;
const byte *RLE_data_ptr;
size_t RLE_data_len;
ByteArray decodeBuffer;
if (imageData.size() <= SAGA_IMAGE_DATA_OFFSET) {
@ -62,20 +96,38 @@ bool SagaEngine::decodeBGImage(const ByteArray &imageData, ByteArray &outputBuff
readS.readUint16();
readS.readUint16();
RLE_data_ptr = &imageData.front() + SAGA_IMAGE_DATA_OFFSET;
RLE_data_len = imageData.size() - SAGA_IMAGE_DATA_OFFSET;
modex_height = granulate(hdr.height, 4);
decodeBuffer.resize(hdr.width * modex_height);
outputBuffer.resize(hdr.width * hdr.height);
if (!decodeBGImageRLE(RLE_data_ptr, RLE_data_len, decodeBuffer)) {
return false;
}
if (isAGA() || isECS()) {
int planePitch = (hdr.width + 15) & ~15;
int linePitch = isAGA() ? planePitch : (planePitch * 5 / 8);
unsigned bitnum = isAGA() ? 8 : 5;
int headerSize = 8 + (3 << bitnum);
const byte *RLE_data_ptr = &imageData.front() + headerSize;
size_t RLE_data_len = imageData.size() - headerSize;
unbankBGImage(outputBuffer.getBuffer(), decodeBuffer.getBuffer(), hdr.width, hdr.height);
if (RLE_data_len != (size_t) linePitch * hdr.height)
return false;
memset(outputBuffer.getBuffer(), 0, hdr.width * hdr.height);
for (int y = 0; y < hdr.height; y++)
for (int x = 0; x < hdr.width; x++)
for (unsigned bit = 0; bit < bitnum; bit++) {
int inXbit = x + bit * planePitch;
outputBuffer[y * hdr.width + x] |= ((RLE_data_ptr[y * linePitch + inXbit / 8] >> (7 - inXbit % 8)) & 1) << bit;
}
} else {
int modex_height = granulate(hdr.height, 4);
const byte *RLE_data_ptr;
size_t RLE_data_len;
decodeBuffer.resize(hdr.width * modex_height);
RLE_data_ptr = &imageData.front() + SAGA_IMAGE_DATA_OFFSET;
RLE_data_len = imageData.size() - SAGA_IMAGE_DATA_OFFSET;
if (!decodeBGImageRLE(RLE_data_ptr, RLE_data_len, decodeBuffer)) {
return false;
}
unbankBGImage(outputBuffer.getBuffer(), decodeBuffer.getBuffer(), hdr.width, hdr.height);
}
// For some reason bg images in IHNM are upside down
if (getGameId() == GID_IHNM && !flip) {

View File

@ -124,10 +124,11 @@ void IsoMap::loadImages(const ByteArray &resourceData) {
error("IsoMap::loadImages wrong resourceLength");
}
bool longOffset = _vm->isAGA() || _vm->isECS();
ByteArrayReadStreamEndian readS(resourceData, _vm->isBigEndian());
readS.readUint16(); // skip
i = readS.readUint16();
i = longOffset ? readS.readUint32() : readS.readUint16();
i = i / SAGA_ISOTILEDATA_LEN;
_tilesTable.resize(i);
Common::Array<size_t> tempOffsets;
@ -139,7 +140,7 @@ void IsoMap::loadImages(const ByteArray &resourceData) {
tileData = &_tilesTable[i];
tileData->height = readS.readByte();
tileData->attributes = readS.readSByte();
tempOffsets[i] = readS.readUint16();
tempOffsets[i] = longOffset ? readS.readUint32() : readS.readUint16();
tileData->terrainMask = readS.readUint16();
tileData->FGDBGDAttr = readS.readByte();
readS.readByte(); //skip
@ -152,6 +153,7 @@ void IsoMap::loadImages(const ByteArray &resourceData) {
for (i = 0; i < _tilesTable.size(); i++) {
_tilesTable[i].tilePointer = _tileData.getBuffer() + tempOffsets[i] - offsetDiff;
_tilesTable[i].tileSize = i + 1 == (int)_tilesTable.size() ? resourceData.size() - tempOffsets[i] : tempOffsets[i+1] - tempOffsets[i];
}
}
@ -819,6 +821,40 @@ void IsoMap::drawTile(uint16 tileIndex, const Point &point, const Location *loca
readPointer = tilePointer;
lowBound = MIN((int)(drawPoint.y + height), (int)_tileClip.bottom);
if (_vm->isAGA() || _vm->isECS()) {
if (height == 0)
return;
int pitchLine = _tilesTable[tileIndex].tileSize / height;
int pitchPlane = _vm->isAGA() ? pitchLine / 8 : pitchLine / 5;
int width = pitchPlane * 8;
int bitnum = _vm->isAGA() ? 8 : 5;
Common::Rect bounds(
MAX((int)drawPoint.x, (int)_tileClip.left),
MAX((int)drawPoint.y, (int)_tileClip.top),
MIN((int)drawPoint.x + width, (int)_tileClip.right),
MIN((int)drawPoint.y + height, (int)_tileClip.bottom)
);
for (row = bounds.top; row < bounds.bottom; row++)
for (col = bounds.left; col < bounds.right; col++) {
int sourceX = (col - drawPoint.x);
int sourceY = (row - drawPoint.y);
byte res = 0;
for (int bit = 0; bit < bitnum; bit++) {
unsigned inOff = sourceY * pitchLine + sourceX / 8 + pitchPlane * bit;
if (inOff >= _tilesTable[tileIndex].tileSize) {
//warning("Overrun sourceX=%d, sourceY=%d, pitch=%d, height=%d", sourceX, sourceY, pitchLine, height);
continue;
}
res |= ((tilePointer[inOff] >> (7 - sourceX % 8)) & 1) << bit;
}
if (res != 0)
_vm->_gfx->getBackBufferPixels()[col + (row * _vm->_gfx->getBackBufferPitch())] = res;
}
return;
}
for (row = drawPoint.y; row < lowBound; row++) {
widthCount = 0;
if (row >= _tileClip.top) {

View File

@ -90,6 +90,7 @@ enum TileMapEdgeType {
struct IsoTileData {
byte height;
size_t tileSize;
int8 attributes;
byte *tilePointer;
uint16 terrainMask;

View File

@ -475,6 +475,7 @@ private:
public:
bool decodeBGImage(const ByteArray &imageData, ByteArray &outputBuffer, int *w, int *h, bool flip = false);
bool decodeBGImageMask(const ByteArray &imageData, ByteArray &outputBuffer, int *w, int *h, bool flip = false);
const byte *getImagePal(const ByteArray &imageData) {
if (imageData.size() <= SAGA_IMAGE_HEADER_LEN) {
return NULL;

View File

@ -971,7 +971,10 @@ void Scene::processSceneResources(SceneResourceDataArray &resourceList, SceneLoa
error("Scene::ProcessSceneResources(): Duplicate background mask resource encountered");
debug(3, "Loading BACKGROUND MASK resource.");
_vm->decodeBGImage(resourceData, _bgMask.buffer, &_bgMask.w, &_bgMask.h, true);
if (flags & kLoadBgMaskIsImage)
_vm->decodeBGImage(resourceData, _bgMask.buffer, &_bgMask.w, &_bgMask.h, true);
else
_vm->decodeBGImageMask(resourceData, _bgMask.buffer, &_bgMask.w, &_bgMask.h, true);
_bgMask.loaded = true;
// At least in ITE the mask needs to be clipped.
@ -1053,15 +1056,21 @@ void Scene::processSceneResources(SceneResourceDataArray &resourceList, SceneLoa
{
PalEntry pal[PAL_ENTRIES];
byte *palPtr = resourceData.getBuffer();
uint16 c;
if (resourceData.size() < 3 * PAL_ENTRIES)
if (resourceData.size() < 3 * _vm->getPalNumEntries())
error("Too small scene palette %i", (int)resourceData.size());
for (uint16 c = 0; c < PAL_ENTRIES; c++) {
for (c = 0; c < _vm->getPalNumEntries(); c++) {
pal[c].red = *palPtr++;
pal[c].green = *palPtr++;
pal[c].blue = *palPtr++;
}
for (; c < PAL_ENTRIES; c++) {
pal[c].red = 0;
pal[c].green = 0;
pal[c].blue = 0;
}
_vm->_gfx->setPalette(pal);
}
break;

View File

@ -33,6 +33,83 @@
namespace Saga {
namespace {
template<int bitsPerPixel, int bitsPerEntry>
bool blitAmigaSprite(byte *outBuf, int outPitch, const byte *inputBuffer, size_t inLength, int width, int height) {
byte *outPointer = outBuf;
int c;
int widthAligned = (width + 15) & ~15;
Common::MemoryReadStream readS(inputBuffer, inLength);
const byte *ptr = inputBuffer, *end = inputBuffer + inLength;
for (int bitOut = 0; bitOut < bitsPerPixel; bitOut++)
for (int x = 0; x < widthAligned; x += bitsPerEntry) {
for (int y = 0; y < height; ) {
int bg_runcount = bitsPerEntry == 16 ? READ_BE_UINT16(ptr) : *ptr;
ptr += bitsPerEntry / 8;
if (ptr >= end)
return true;
// Transparent
y += bg_runcount;
if (y > height) {
warning("Sprite height overrun in transparent run: coord=%d+%dx%d, size=%dx%d, pos=%d",
x, bitOut, y, width, height, (int)readS.pos());
return true;
}
if (y == height) {
continue;
}
int fg_runcount = bitsPerEntry == 16 ? READ_BE_UINT16(ptr) : *ptr;
ptr += bitsPerEntry / 8;
if (ptr >= end)
return true;
for (c = 0; c < fg_runcount && ptr < end; c++, y++) {
uint16 val = bitsPerEntry == 16 ? READ_BE_UINT16(ptr) : *ptr;
ptr += bitsPerEntry / 8;
if (y >= height) {
warning("Sprite height overrun in opaque run: coord=%d+%dx%d, size=%dx%d, pos=%d",
x, bitOut, y, width, height, (int)readS.pos());
return false;
}
for (int bitIn = 0; bitIn < bitsPerEntry; bitIn++) {
int realX = x + (bitsPerEntry - 1 - bitIn);
if (realX >= width) {
continue;
}
outPointer[y * outPitch + realX] =
(outPointer[y * outPitch + realX] & ~(1 << bitOut)) | (((val >> bitIn) & 1) << bitOut);
}
}
if (bitsPerEntry < 8) {
byte mask = (byte) ~(0xff << bitsPerEntry);
for (c = 0, y -= fg_runcount; c < fg_runcount; c++, y++) {
for (int z = 0; z < bitsPerEntry; z++) {
int realX = x + z;
if (realX >= width) {
continue;
}
outPointer[y * outPitch + realX] &= mask;
}
}
}
if (fg_runcount == 0 && bg_runcount == 0) {
warning("Sprite zero-sized run: coord=%d+%dx%d, size=%dx%d, pos=%d",
x, bitOut, y, width, height, (int)readS.pos());
return false;
}
}
}
return true;
}
}
#define RID_IHNM_ARROW_SPRITES 13
#define RID_IHNM_SAVEREMINDER_SPRITES 14
#define RID_IHNMDEMO_ARROW_SPRITES 8
@ -92,7 +169,7 @@ void Sprite::loadList(int resourceId, SpriteList &spriteList) {
for (uint i = oldSpriteCount; i < spriteList.size(); i++) {
uint32 offset;
if (bigHeader)
if (bigHeader || _vm->isITEAmiga())
offset = readS.readUint32();
else
offset = readS.readUint16();
@ -134,7 +211,15 @@ void Sprite::loadList(int resourceId, SpriteList &spriteList) {
int inputLength = spriteListData.size() - (spriteDataPointer - spriteListData.getBuffer());
spriteInfo->decodedBuffer.resize(outputLength);
if (outputLength > 0) {
decodeRLEBuffer(spriteDataPointer, inputLength, outputLength);
if (_vm->isAGA() || _vm->isECS()) {
_decodeBuf.resize(spriteInfo->width * spriteInfo->height);
memset(&_decodeBuf.front(), 0, _decodeBuf.size());
if (_vm->isAGA())
blitAmigaSprite<8, 16>(&_decodeBuf.front(), spriteInfo->width, spriteDataPointer, inputLength, spriteInfo->width, spriteInfo->height);
else
blitAmigaSprite<4, 8>(&_decodeBuf.front(), spriteInfo->width, spriteDataPointer, inputLength, spriteInfo->width, spriteInfo->height);
} else
decodeRLEBuffer(spriteDataPointer, inputLength, outputLength);
byte *dst = &spriteInfo->decodedBuffer.front();
#ifdef ENABLE_IHNM
// IHNM sprites are upside-down, for reasons which i can only