mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
505 lines
13 KiB
C++
505 lines
13 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
// SAGA Image resource management routines
|
|
|
|
#include "saga/saga.h"
|
|
#include "common/compression/powerpacker.h"
|
|
|
|
namespace Saga {
|
|
|
|
static int granulate(int value, int granularity) {
|
|
int remainder;
|
|
|
|
if (value == 0)
|
|
return 0;
|
|
|
|
if (granularity == 0)
|
|
return 0;
|
|
|
|
remainder = value % granularity;
|
|
|
|
if (remainder == 0) {
|
|
return value;
|
|
} else {
|
|
return (granularity - remainder + value);
|
|
}
|
|
}
|
|
|
|
static bool unbankAmiga(ByteArray& outputBuffer, const byte *banked, uint len, uint16 height, uint16 width, uint bitnum) {
|
|
uint planePitch = (width + 15) & ~15;
|
|
uint linePitch = bitnum == 8 ? planePitch : (planePitch * 5 / 8);
|
|
if (len != linePitch * height)
|
|
return false;
|
|
outputBuffer.resize(height * width);
|
|
memset(outputBuffer.getBuffer(), 0, width * height);
|
|
for (uint y = 0; y < height; y++)
|
|
for (uint x = 0; x < width; x++)
|
|
for (unsigned bit = 0; bit < bitnum; bit++) {
|
|
int inXbit = x + bit * planePitch;
|
|
outputBuffer[y * width + x] |= ((banked[y * linePitch + inXbit / 8] >> (7 - inXbit % 8)) & 1) << bit;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
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;
|
|
ByteArray decodeBuffer;
|
|
|
|
if (imageData.size() <= SAGA_IMAGE_DATA_OFFSET) {
|
|
error("decodeBGImage() Image size is way too small (%d)", (int)imageData.size());
|
|
}
|
|
|
|
ByteArrayReadStreamEndian readS(imageData, isBigEndian());
|
|
|
|
hdr.width = readS.readUint16();
|
|
hdr.height = readS.readUint16();
|
|
// The next four bytes of the image header aren't used.
|
|
readS.readUint16();
|
|
readS.readUint16();
|
|
|
|
if (isAGA() || isECS()) {
|
|
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;
|
|
|
|
if (getFeatures() & GF_POWERPACK_GFX) {
|
|
int aligned_width = (hdr.width + 15) & ~15;
|
|
int pitch = isAGA() ? aligned_width : (aligned_width * 5 / 8);
|
|
if (RLE_data_len < 12) {
|
|
warning("Compressed size too short");
|
|
return false;
|
|
}
|
|
if (READ_BE_UINT32 (RLE_data_ptr) != RLE_data_len - 4) {
|
|
warning("Compressed size mismatch: %d vs %d", READ_BE_UINT32 (RLE_data_ptr), (int)RLE_data_len - 4);
|
|
return false;
|
|
}
|
|
if (READ_BE_UINT32 (RLE_data_ptr + 4) != MKTAG('P', 'A', 'C', 'K')) {
|
|
warning("Compressed magic mismatch: 0x%08x vs %08x", READ_BE_UINT32 (RLE_data_ptr + 4), MKTAG('P', 'A', 'C', 'K'));
|
|
return false;
|
|
}
|
|
uint32 uncompressed_len = 0;
|
|
byte *uncompressed = Common::PowerPackerStream::unpackBuffer(RLE_data_ptr + 4, RLE_data_len - 4, uncompressed_len);
|
|
if (uncompressed == nullptr || (int) uncompressed_len != pitch * hdr.height) {
|
|
warning("Uncompressed size mismatch: %d vs %d", uncompressed_len, pitch * hdr.height);
|
|
delete[] uncompressed;
|
|
return false;
|
|
}
|
|
if (isAGA() && pitch == hdr.width) {
|
|
// TODO: Use some kind of move semantics
|
|
outputBuffer = ByteArray(uncompressed, uncompressed_len);
|
|
} else if (isAGA()) {
|
|
outputBuffer.resize(hdr.height * hdr.width);
|
|
for (int y = 0; y < hdr.height; y++)
|
|
memcpy(outputBuffer.getBuffer() + y * hdr.width, uncompressed + y * pitch, hdr.width);
|
|
} else {
|
|
if (!unbankAmiga(outputBuffer, uncompressed, uncompressed_len, hdr.height, hdr.width, bitnum)) {
|
|
delete[] uncompressed;
|
|
return false;
|
|
}
|
|
}
|
|
delete[] uncompressed;
|
|
} else {
|
|
if (!unbankAmiga(outputBuffer, RLE_data_ptr, RLE_data_len, hdr.height, hdr.width, bitnum))
|
|
return false;
|
|
}
|
|
} 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;
|
|
}
|
|
outputBuffer.resize(hdr.width * hdr.height);
|
|
unbankBGImage(outputBuffer.getBuffer(), decodeBuffer.getBuffer(), hdr.width, hdr.height);
|
|
}
|
|
|
|
// For some reason bg images in IHNM are upside down
|
|
if (getGameId() == GID_IHNM && !flip) {
|
|
flipImage(outputBuffer.getBuffer(), hdr.width, hdr.height);
|
|
}
|
|
|
|
*w = hdr.width;
|
|
*h = hdr.height;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SagaEngine::decodeBGImageRLE(const byte *inbuf, size_t inbuf_len, ByteArray &outbuf) {
|
|
const byte *inbuf_ptr;
|
|
byte *outbuf_ptr;
|
|
byte *outbuf_start;
|
|
uint32 inbuf_remain;
|
|
|
|
const byte *inbuf_end;
|
|
byte *outbuf_end;
|
|
uint32 outbuf_remain;
|
|
|
|
byte mark_byte;
|
|
int test_byte;
|
|
|
|
uint32 runcount;
|
|
|
|
byte bitfield;
|
|
byte bitfield_byte1;
|
|
byte bitfield_byte2;
|
|
|
|
byte *backtrack_ptr;
|
|
int backtrack_amount;
|
|
|
|
uint16 c, b;
|
|
|
|
int decode_err = 0;
|
|
|
|
inbuf_ptr = inbuf;
|
|
inbuf_remain = inbuf_len;
|
|
|
|
outbuf_start = outbuf_ptr = outbuf.getBuffer();
|
|
outbuf_remain = outbuf.size();
|
|
outbuf_end = (outbuf_start + outbuf_remain) - 1;
|
|
memset(outbuf_start, 0, outbuf_remain);
|
|
|
|
inbuf_end = (inbuf + inbuf_len) - 1;
|
|
|
|
|
|
while ((inbuf_remain > 1) && (outbuf_remain > 0) && !decode_err) {
|
|
|
|
if ((inbuf_ptr > inbuf_end) || (outbuf_ptr > outbuf_end)) {
|
|
return false;
|
|
}
|
|
|
|
mark_byte = *inbuf_ptr++;
|
|
inbuf_remain--;
|
|
|
|
test_byte = mark_byte & 0xC0; // Mask all but two high order bits
|
|
|
|
switch (test_byte) {
|
|
case 0xC0: // 1100 0000
|
|
// Uncompressed run follows: Max runlength 63
|
|
runcount = mark_byte & 0x3f;
|
|
if ((inbuf_remain < runcount) || (outbuf_remain < runcount)) {
|
|
return false;
|
|
}
|
|
|
|
for (c = 0; c < runcount; c++) {
|
|
*outbuf_ptr++ = *inbuf_ptr++;
|
|
}
|
|
|
|
inbuf_remain -= runcount;
|
|
outbuf_remain -= runcount;
|
|
continue;
|
|
break;
|
|
case 0x80: // 1000 0000
|
|
// Compressed run follows: Max runlength 63
|
|
runcount = (mark_byte & 0x3f) + 3;
|
|
if (!inbuf_remain || (outbuf_remain < runcount)) {
|
|
return false;
|
|
}
|
|
|
|
for (c = 0; c < runcount; c++) {
|
|
*outbuf_ptr++ = *inbuf_ptr;
|
|
}
|
|
|
|
inbuf_ptr++;
|
|
inbuf_remain--;
|
|
outbuf_remain -= runcount;
|
|
continue;
|
|
|
|
break;
|
|
|
|
case 0x40: // 0100 0000
|
|
// Repeat decoded sequence from output stream:
|
|
// Max runlength 10
|
|
|
|
runcount = ((mark_byte >> 3) & 0x07U) + 3;
|
|
backtrack_amount = *inbuf_ptr;
|
|
|
|
if (!inbuf_remain || (backtrack_amount > (outbuf_ptr - outbuf_start)) || (runcount > outbuf_remain)) {
|
|
return false;
|
|
}
|
|
|
|
inbuf_ptr++;
|
|
inbuf_remain--;
|
|
|
|
backtrack_ptr = outbuf_ptr - backtrack_amount;
|
|
|
|
for (c = 0; c < runcount; c++) {
|
|
*outbuf_ptr++ = *backtrack_ptr++;
|
|
}
|
|
|
|
outbuf_remain -= runcount;
|
|
continue;
|
|
break;
|
|
default: // 0000 0000
|
|
break;
|
|
}
|
|
|
|
// Mask all but the third and fourth highest order bits
|
|
test_byte = mark_byte & 0x30;
|
|
|
|
switch (test_byte) {
|
|
|
|
case 0x30: // 0011 0000
|
|
// Bitfield compression
|
|
runcount = (mark_byte & 0x0F) + 1;
|
|
|
|
if ((inbuf_remain < (runcount + 2)) || (outbuf_remain < (runcount * 8))) {
|
|
return false;
|
|
}
|
|
|
|
bitfield_byte1 = *inbuf_ptr++;
|
|
bitfield_byte2 = *inbuf_ptr++;
|
|
|
|
for (c = 0; c < runcount; c++) {
|
|
bitfield = *inbuf_ptr;
|
|
for (b = 0; b < 8; b++) {
|
|
if (bitfield & 0x80) {
|
|
*outbuf_ptr = bitfield_byte2;
|
|
} else {
|
|
*outbuf_ptr = bitfield_byte1;
|
|
}
|
|
bitfield <<= 1;
|
|
outbuf_ptr++;
|
|
}
|
|
inbuf_ptr++;
|
|
}
|
|
|
|
inbuf_remain -= (runcount + 2);
|
|
outbuf_remain -= (runcount * 8);
|
|
continue;
|
|
break;
|
|
case 0x20: // 0010 0000
|
|
// Uncompressed run follows
|
|
runcount = ((mark_byte & 0x0F) << 8) + *inbuf_ptr;
|
|
if ((inbuf_remain < (runcount + 1)) || (outbuf_remain < runcount)) {
|
|
return false;
|
|
}
|
|
|
|
inbuf_ptr++;
|
|
|
|
for (c = 0; c < runcount; c++) {
|
|
*outbuf_ptr++ = *inbuf_ptr++;
|
|
}
|
|
|
|
inbuf_remain -= (runcount + 1);
|
|
outbuf_remain -= runcount;
|
|
continue;
|
|
|
|
break;
|
|
|
|
case 0x10: // 0001 0000
|
|
// Repeat decoded sequence from output stream
|
|
backtrack_amount = ((mark_byte & 0x0F) << 8) + *inbuf_ptr;
|
|
if (inbuf_remain < 2) {
|
|
return false;
|
|
}
|
|
|
|
inbuf_ptr++;
|
|
runcount = *inbuf_ptr++;
|
|
|
|
if ((backtrack_amount > (outbuf_ptr - outbuf_start)) || (outbuf_remain < runcount)) {
|
|
return false;
|
|
}
|
|
|
|
backtrack_ptr = outbuf_ptr - backtrack_amount;
|
|
|
|
for (c = 0; c < runcount; c++) {
|
|
*outbuf_ptr++ = *backtrack_ptr++;
|
|
}
|
|
|
|
inbuf_remain -= 2;
|
|
outbuf_remain -= runcount;
|
|
continue;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SagaEngine::flipImage(byte *imageBuffer, int columns, int scanlines) {
|
|
int line;
|
|
ByteArray tmp_scan;
|
|
|
|
byte *flip_p1;
|
|
byte *flip_p2;
|
|
byte *flip_tmp;
|
|
|
|
int flipcount = scanlines / 2;
|
|
|
|
tmp_scan.resize(columns);
|
|
flip_tmp = tmp_scan.getBuffer();
|
|
if (flip_tmp == nullptr) {
|
|
return;
|
|
}
|
|
|
|
flip_p1 = imageBuffer;
|
|
flip_p2 = imageBuffer + (columns * (scanlines - 1));
|
|
|
|
for (line = 0; line < flipcount; line++) {
|
|
memcpy(flip_tmp, flip_p1, columns);
|
|
memcpy(flip_p1, flip_p2, columns);
|
|
memcpy(flip_p2, flip_tmp, columns);
|
|
flip_p1 += columns;
|
|
flip_p2 -= columns;
|
|
}
|
|
}
|
|
|
|
void SagaEngine::unbankBGImage(byte *dst_buf, const byte *src_buf, int columns, int scanlines) {
|
|
int x, y;
|
|
int temp;
|
|
int quadruple_rows;
|
|
int remain_rows;
|
|
int rowjump_src;
|
|
int rowjump_dest;
|
|
const byte *src_p;
|
|
const byte *srcptr1, *srcptr2, *srcptr3, *srcptr4;
|
|
byte *dstptr1, *dstptr2, *dstptr3, *dstptr4;
|
|
|
|
quadruple_rows = scanlines - (scanlines % 4);
|
|
remain_rows = scanlines - quadruple_rows;
|
|
|
|
assert(scanlines > 0);
|
|
|
|
src_p = src_buf;
|
|
|
|
srcptr1 = src_p;
|
|
srcptr2 = src_p + 1;
|
|
srcptr3 = src_p + 2;
|
|
srcptr4 = src_p + 3;
|
|
|
|
dstptr1 = dst_buf;
|
|
dstptr2 = dst_buf + columns;
|
|
dstptr3 = dst_buf + columns * 2;
|
|
dstptr4 = dst_buf + columns * 3;
|
|
|
|
rowjump_src = columns * 4;
|
|
rowjump_dest = columns * 4;
|
|
|
|
// Unbank groups of 4 first
|
|
for (y = 0; y < quadruple_rows; y += 4) {
|
|
for (x = 0; x < columns; x++) {
|
|
temp = x * 4;
|
|
dstptr1[x] = srcptr1[temp];
|
|
dstptr2[x] = srcptr2[temp];
|
|
dstptr3[x] = srcptr3[temp];
|
|
dstptr4[x] = srcptr4[temp];
|
|
}
|
|
|
|
// This is to avoid generating invalid pointers -
|
|
// usually innocuous, but undefined
|
|
if (y < quadruple_rows - 4) {
|
|
dstptr1 += rowjump_dest;
|
|
dstptr2 += rowjump_dest;
|
|
dstptr3 += rowjump_dest;
|
|
dstptr4 += rowjump_dest;
|
|
srcptr1 += rowjump_src;
|
|
srcptr2 += rowjump_src;
|
|
srcptr3 += rowjump_src;
|
|
srcptr4 += rowjump_src;
|
|
}
|
|
}
|
|
|
|
// Unbank rows remaining
|
|
switch (remain_rows) {
|
|
case 1:
|
|
dstptr1 += rowjump_dest;
|
|
srcptr1 += rowjump_src;
|
|
for (x = 0; x < columns; x++) {
|
|
temp = x * 4;
|
|
dstptr1[x] = srcptr1[temp];
|
|
}
|
|
break;
|
|
case 2:
|
|
dstptr1 += rowjump_dest;
|
|
dstptr2 += rowjump_dest;
|
|
srcptr1 += rowjump_src;
|
|
srcptr2 += rowjump_src;
|
|
for (x = 0; x < columns; x++) {
|
|
temp = x * 4;
|
|
dstptr1[x] = srcptr1[temp];
|
|
dstptr2[x] = srcptr2[temp];
|
|
}
|
|
break;
|
|
case 3:
|
|
dstptr1 += rowjump_dest;
|
|
dstptr2 += rowjump_dest;
|
|
dstptr3 += rowjump_dest;
|
|
srcptr1 += rowjump_src;
|
|
srcptr2 += rowjump_src;
|
|
srcptr3 += rowjump_src;
|
|
for (x = 0; x < columns; x++) {
|
|
temp = x * 4;
|
|
dstptr1[x] = srcptr1[temp];
|
|
dstptr2[x] = srcptr2[temp];
|
|
dstptr3[x] = srcptr3[temp];
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // End of namespace Saga
|