diff --git a/compressors/gfxcomp_sonic2.cpp b/compressors/gfxcomp_sonic2.cpp new file mode 100644 index 0000000..4e7f66c --- /dev/null +++ b/compressors/gfxcomp_sonic2.cpp @@ -0,0 +1,199 @@ +#include +#include +#include +#include + +extern "C" __declspec(dllexport) const char* getName() +{ + // A pretty name for this compression type + // Generally, the name of the game it was REd from + return "Sonic 2"; +} + +extern "C" __declspec(dllexport) const char* getExt() +{ + // A string suitable for use as a file extension + // ReSharper disable once StringLiteralTypo + return "sonic2compr"; +} + +void writeWord(std::vector& buffer, const uint16_t word) +{ + buffer.push_back((word >> 0) & 0xff); + buffer.push_back((word >> 8) & 0xff); +} + +bool allZero(const std::vector& tile) +{ + return std::ranges::all_of( + tile, + [](auto x) + { + return x == 0; + }); +} + +bool shouldCompress(const std::vector& tile) +{ + int count = 0; + for (const uint8_t value : tile) + { + if (value == 0) + { + ++count; + } + } + return count > 4; +} + +std::vector xorTile(const std::vector& tile) +{ + std::vector copy(tile); + for (int i = 0; i < 7; ++i) + { + copy[i + 2] ^= copy[i + 0]; + copy[i + 3] ^= copy[i + 1]; + copy[i + 18] ^= copy[i + 16]; + copy[i + 19] ^= copy[i + 17]; + } + return copy; +} + + +std::vector compress(const std::vector& tile) +{ + std::vector result; + // Emit bitmask for zero bits + uint32_t bitmask = 0; + for (const uint8_t value : tile) + { + bitmask <<= 1; + if (value != 0) + { + bitmask &= 1; + } + } + result.push_back((bitmask >> 0) & 0xff); + result.push_back((bitmask >> 8) & 0xff); + result.push_back((bitmask >> 16) & 0xff); + result.push_back((bitmask >> 24) & 0xff); + // Then the non-zero bytes + for (const uint8_t value : tile) + { + if (value != 0) + { + result.push_back(value); + } + } + return result; +} + +extern "C" __declspec(dllexport) int compressTiles( + const uint8_t* pSource, + const uint32_t numTiles, + uint8_t* pDestination, + const uint32_t destinationLength) +{ + if (numTiles > 0xffff) + { + return -1; // error + } + std::vector destination; // the output + destination.reserve(destinationLength); // avoid reallocation + + // Format is: + // dw 0001 ; meaningless header (little-endian) + // dw TileCount ; Tile count (little-endian) + // dw OffsetToBitStream ; Offset of stream 2, relative to start of data + // dsb n Data ; stream 1 + // dsb n CompressionData ; stream 2 + // Compression data holds 2 bits per tile, right-aligned in CompressionData: + // 00 = all zeroes + // 01 = raw, copy 32 bytes from Data + // 02 = compressed tile, see below + // 03 = XORed compressed data, see below + // Compressed data then consists of a run of bytes in Data: + // 4B: 32 bits, 1 = emit a byte from Data, 0 = emit a 0 + // So it makes sense to use this is more than 4 bytes of the 32 are 0. + // XOR compressed data is the same except after decoding, the bytes are XORed against each other within bitplanes. + // So we need to pre-process the same way to check if it yields some 0s. + + // First we make the header + writeWord(destination, 0x0001); + writeWord(destination, static_cast(numTiles)); + writeWord(destination, 0x0000); // Offset to fill in later + + // Then we make a buffer for the compression bitstream. This will hold a byte per tile which we'll squash later. + std::vector bitStream; + bitStream.reserve(numTiles); + + // Then for each tile... + std::vector tile(32); + for (auto i = 0u; i < numTiles; ++i) + { + // Copy to temp buffer + std::copy_n(pSource, 32, tile.begin()); + + // If all 0, it's easy + if (allZero(tile)) + { + bitStream.push_back(0); + } + else if (shouldCompress(tile)) + { + bitStream.push_back(2); + const auto& compressed = compress(tile); + std::ranges::copy(compressed, destination.end()); + } + else + { + if (const auto& xored = xorTile(tile); shouldCompress(xored)) + { + bitStream.push_back(3); + const auto& compressed = compress(xored); + std::ranges::copy(compressed, destination.end()); + } + else + { + // Uncompressed + bitStream.push_back(1); + std::ranges::copy(tile, destination.end()); + } + } + } + + // Update the offset + const auto offsetOfBitstream = static_cast(destination.size()); + destination[4] = (offsetOfBitstream >> 0) & 0xff; + destination[5] = (offsetOfBitstream >> 8) & 0xff; + + // And then append it + int nibbleCount = 0; + uint8_t accumulator = 0; + for (const uint8_t b : bitStream) + { + accumulator <<= 2; + accumulator |= b; + if (++nibbleCount == 4) + { + destination.push_back(accumulator); + accumulator = 0; + nibbleCount = 0; + } + } + if (nibbleCount != 0) + { + // Left over bits in accumulator + destination.push_back(accumulator); + } + + // check length + if (destination.size() > destinationLength) + { + return 0; + } + // copy to dest + memcpy_s(pDestination, destinationLength, destination.data(), destination.size()); + // return length + return static_cast(destination.size()); +} diff --git a/compressors/gfxcomp_sonic2.vcxproj b/compressors/gfxcomp_sonic2.vcxproj new file mode 100644 index 0000000..b56b013 --- /dev/null +++ b/compressors/gfxcomp_sonic2.vcxproj @@ -0,0 +1,84 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + + + + {279B63D9-4BFE-406C-A6C2-756D3082BB37} + Win32Proj + 10.0 + + + + DynamicLibrary + v143 + true + + + DynamicLibrary + v143 + + + + + + + <_ProjectFileVersion>10.0.40219.1 + $(SolutionDir)$(Configuration)\ + $(SolutionDir)$(Configuration)\$(ProjectName)\ + true + $(SolutionDir)$(Configuration)\ + $(SolutionDir)$(Configuration)\$(ProjectName)\ + + + + + + + + + Level4 + true + Disabled + MultiThreadedDebugDLL + stdcpp20 + + + $(IntermediateOutputPath)$(ProjectName).lib + + + + + + + + + + Level4 + true + true + true + stdcpp20 + + + $(IntermediateOutputPath)$(ProjectName).lib + %(AdditionalDependencies) + Windows + UseFastLinkTimeCodeGeneration + true + + + + + + \ No newline at end of file diff --git a/compressors/tilecompressordlls.sln b/compressors/tilecompressordlls.sln index 7e3610f..8c8786b 100644 --- a/compressors/tilecompressordlls.sln +++ b/compressors/tilecompressordlls.sln @@ -54,6 +54,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gfxcomp_stc0", "gfxcomp_stc0.vcxproj", "{EB4E5423-CA2C-4020-8796-438E218FF0E0}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gfxcomp_sonic2", "gfxcomp_sonic2.vcxproj", "{279B63D9-4BFE-406C-A6C2-756D3082BB37}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x86 = Debug|x86 @@ -148,6 +150,10 @@ Global {EB4E5423-CA2C-4020-8796-438E218FF0E0}.Debug|x86.Build.0 = Debug|Win32 {EB4E5423-CA2C-4020-8796-438E218FF0E0}.Release|x86.ActiveCfg = Release|Win32 {EB4E5423-CA2C-4020-8796-438E218FF0E0}.Release|x86.Build.0 = Release|Win32 + {279B63D9-4BFE-406C-A6C2-756D3082BB37}.Debug|x86.ActiveCfg = Debug|Win32 + {279B63D9-4BFE-406C-A6C2-756D3082BB37}.Debug|x86.Build.0 = Debug|Win32 + {279B63D9-4BFE-406C-A6C2-756D3082BB37}.Release|x86.ActiveCfg = Release|Win32 + {279B63D9-4BFE-406C-A6C2-756D3082BB37}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/compressors/tilecompressordlls.sln.DotSettings b/compressors/tilecompressordlls.sln.DotSettings index c83cc03..9d40d13 100644 --- a/compressors/tilecompressordlls.sln.DotSettings +++ b/compressors/tilecompressordlls.sln.DotSettings @@ -7,6 +7,7 @@ True True True + True True True True diff --git a/decompressors/Sonic 2 decompressor.asm b/decompressors/Sonic 2 decompressor.asm new file mode 100644 index 0000000..2b64a33 --- /dev/null +++ b/decompressors/Sonic 2 decompressor.asm @@ -0,0 +1,227 @@ +; Sonic 2 tile decompressor +; +; Needs 32 bytes of RAM for temporary storage. Define Sonic2TileLoaderMemory as the start address of the RAM to use. + +.block "TileLoaderSonic2" +.section "Tile loader (Sonic 2)" free + +; RAM usage +.enum Sonic2TileLoaderMemory export +Sonic2TileLoader_TileCount dw +Sonic2TileLoader_DataPointer dw +Sonic2TileLoader_BitStreamPointer dw +Sonic2TileLoader_TileBuffer dsb 32 +Sonic2TileLoader_DecompressedData dsb 32 +.ende + +; A definition +.define SMS_VDP_DATA $be + +Sonic2TileLoader_Decompress: + push hl + ; Skip header + inc hl + inc hl + ; Read tile count + ld a, (hl) + ld (Sonic2TileLoader_TileCount), a + inc hl + ld a, (hl) + ld (Sonic2TileLoader_TileCount+1), a + inc hl + ; Read bitstream pointer + ld e, (hl) + inc hl + ld d, (hl) + inc hl + ; Data starts here + ld (Sonic2TileLoader_DataPointer), hl + pop hl + add hl, de + ld (Sonic2TileLoader_BitStreamPointer), hl + + ; Zero the buffer + ld hl, Sonic2TileLoader_TileBuffer + ld de, Sonic2TileLoader_TileBuffer + 1 + ld bc, 31 + ld (hl), 0 + ldir + + ; Reset the consumed tile counter + xor a + ld (Sonic2TileLoader_BitstreamTileCounter), a + +-:call _getTileType + cp 0 + jr nz, + + ; Type 0 = all zero + call _emitBuffer + jr ++ + ++:cp $02 + jr nz, + + ; Type 2 = compressed + call _decompress + call _emitBuffer2 + jr ++ + ++:cp $03 + jr nz, + + ; Type 3 = compressed + XOR + call _decompress + call _xor + call _emitBuffer2 + jr ++ + ++:; Type 1 = raw + call _raw + call _emitBuffer2 + ; fall through + +++: + ; Count down the tile count until done + ld hl, (Sonic2TileLoader_TileCount) + dec hl + ld (Sonic2TileLoader_TileCount), hl + ld a, l + or h + jr nz, - + ret + +_raw: + ; Copy 32 bytes + ld bc, 32 + ld hl, (Sonic2TileLoader_DataPointer) + ld de, Sonic2TileLoader_DecompressedData + ldir + ld (Sonic2TileLoader_DataPointer), hl + ret + +_decompress: + ; Output pointer + ld ix, Sonic2TileLoader_DecompressedData + ; Get 32 bits of flags + ld hl, (Sonic2TileLoader_DataPointer) + ld e, (hl) + inc hl + ld d, (hl) + inc hl + ld c, (hl) + inc hl + ld b, (hl) + inc hl + ld a, 32 ; bit counter +-: + push af + ; Rotate a bit out + rr b + rr c + rr d + rr e + jr c, + + ; 0 => emit a 0 + ld (ix+0), 0 + jr ++ + ++: ; 1 => emit a byte + ld a, (hl) + ld (ix+0), a + inc hl + +++: inc ix + pop af + ; Repeat 32 times + dec a + jr nz, - + ld (Sonic2TileLoader_DataPointer), hl + ret + +_xor: + ld ix, Sonic2TileLoader_DecompressedData + ld b, 7 +-:; XOR bytes against each other across bitplanes on a few rows + ld a, (ix+0) + xor (ix+2) + ld (ix+2), a + ld a, (ix+1) + xor (ix+3) + ld (ix+3), a + ld a, (ix+16) + xor (ix+18) + ld (ix+18), a + ld a, (ix+17) + xor (ix+19) + ld (ix+19), a + inc ix + inc ix + djnz - + ret + +_getTileType: + ld a, (Sonic2TileLoader_BitstreamTileCounter) + cp 4 + jr nz, + + ; Read next byte from bitstream + ld hl, (Sonic2TileLoader_BitStreamPointer) + inc hl + ld (Sonic2TileLoader_BitStreamPointer), hl + xor a + ld (Sonic2TileLoader_BitstreamTileCounter), a + ++:; + ld b, a + ld hl, (Sonic2TileLoader_BitStreamPointer) + ld a, (hl) + ; Get the right two bits in the low two according to the value of Sonic2TileLoader_BitstreamTileCounter +-:dec b + jp m, + + rrca + rrca + jp - + ++:and %11 ; MAsk to two bits + push af + ld a, (Sonic2TileLoader_BitstreamTileCounter) + inc a + ld (Sonic2TileLoader_BitstreamTileCounter), a + pop af + ret + +_emitBuffer: + ld hl, _RAM_D320_ + ld de, Sonic2TileLoader_DecompressedData + ld bc, $0020 + ldir + ; fall through +_emitBuffer2: + ld a, (_RAM_D34C_) + or a + jp nz, + + ld hl, Sonic2TileLoader_DecompressedData + ld b, 32 +-:ld a, (hl) + out (Port_VDPData), a + push hl ; delay + pop hl + inc hl + djnz - + ret + ++: + ld hl, Sonic2TileLoader_DecompressedData + ld b, $20 +-: + ld e, (hl) + ld d, $01 ; Index into table at $100 for tile flipping + ld a, (de) + out (Port_VDPData), a + push hl + pop hl + inc hl + djnz - + ret +.ends +.endb + + +