diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b45cb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.user +*.vpj +*.vpw +*.vpwhist +*.vtg +vcxproj/.vs/ +vcxproj/x64/ diff --git a/README.md b/README.md index b2c49e6..4005672 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # gsla Apple IIgs animation tool, alternative to Paintworks/$C2 animation format + +This is a C++ command line tool, that will convert a C2/Paintworks Animation +file into the more efficient GS Lzb Animation file format. + +See https://github.com/dwsJason/gslaplay, for a GSOS Sample Application +that can play these animations + + diff --git a/source/bctypes.h b/source/bctypes.h new file mode 100644 index 0000000..7a55f9d --- /dev/null +++ b/source/bctypes.h @@ -0,0 +1,48 @@ +/* + bctypes.h + + Because I need Standard Types +*/ + +#ifndef _bctypes_h +#define _bctypes_h + +typedef signed char i8; +typedef unsigned char u8; +typedef signed short i16; +typedef unsigned short u16; +typedef signed long i32; +typedef unsigned long u32; + +typedef signed long long i64; +typedef unsigned long long u64; + +// If we're using C, I still like having a bool around +#ifndef __cplusplus +typedef i32 bool; +#define false (0) +#define true (!false) +#define nullptr 0 +#endif + +typedef float f32; +typedef float r32; +typedef double f64; +typedef double r64; + + +#define null (0) + +// Odd Types +typedef union { +// u128 ul128; + u64 ul64[2]; + u32 ui32[4]; +} QWdata; + + +#endif // _bctypes_h + +// EOF - bctypes.h + + diff --git a/source/c2_file.cpp b/source/c2_file.cpp new file mode 100644 index 0000000..6dab4b6 --- /dev/null +++ b/source/c2_file.cpp @@ -0,0 +1,143 @@ +// +// C++ Encoder/Decoder +// For C2, Paintworks Animation File Format +// +// File Format summary here from Brutal Deluxe +// +//0000.7fff first pic +//8000.8003 length of frame data - 8008 +//8004.8007 timing +//8008.800b length of first frame data +//800c.800d first frame index +//800e.800f first frame data +// +// Offset 0, Value FFFF indicates the end of a frame +// 00 00 FF FF +// +#include "c2_file.h" +#include + +// If these structs are the wrong size, there's an issue with type sizes, and +// your compiler +static_assert(sizeof(C2File_Header)==0x800C, "C2File_Header is supposed to be 0x800C bytes"); + +//------------------------------------------------------------------------------ +// Load in a FanFile constructor +// +C2File::C2File(const char *pFilePath) + : m_widthPixels(320) + , m_heightPixels(200) +{ + LoadFromFile(pFilePath); +} +//------------------------------------------------------------------------------ + +C2File::~C2File() +{ + // Free Up the memory + for (int idx = 0; idx < m_pC1PixelMaps.size(); ++idx) + { + delete[] m_pC1PixelMaps[idx]; + m_pC1PixelMaps[ idx ] = nullptr; + } +} + +//------------------------------------------------------------------------------ + +void C2File::LoadFromFile(const char* pFilePath) +{ + // Free Up the memory + for (int idx = 0; idx < m_pC1PixelMaps.size(); ++idx) + { + delete[] m_pC1PixelMaps[idx]; + m_pC1PixelMaps[ idx ] = nullptr; + } + + m_pC1PixelMaps.clear(); + //-------------------------------------------------------------------------- + + std::vector bytes; + + //-------------------------------------------------------------------------- + // Read the file into memory + FILE* pFile = nullptr; + errno_t err = fopen_s(&pFile, pFilePath, "rb"); + + if (0==err) + { + fseek(pFile, 0, SEEK_END); + size_t length = ftell(pFile); // get file size + fseek(pFile, 0, SEEK_SET); + + bytes.resize( length ); // make sure buffer is large enough + + // Read in the file + fread(&bytes[0], sizeof(unsigned char), bytes.size(), pFile); + fclose(pFile); + } + + if (bytes.size()) + { + size_t file_offset = 0; // File Cursor + + // Bytes are in the buffer, so let's start looking at what we have + C2File_Header* pHeader = (C2File_Header*) &bytes[0]; + + // Early out if things don't look right + if (!pHeader->IsValid((unsigned int)bytes.size())) + return; + + // Grab Initial Frame, and put it in the list + + unsigned char* pFrame = new unsigned char[ 0x8000 ]; + unsigned char* pCanvas = new unsigned char[ 0x8001 ]; // each frame changes the canvas + m_pC1PixelMaps.push_back(pFrame); + memcpy(pFrame, &bytes[0], 0x8000); + memcpy(pCanvas, &bytes[0], 0x8000); + + //---------------------------------------------------------------------- + // Process Frames as we encounter them + file_offset += sizeof(C2File_Header); + + // Since we always pull 4 bytes + // let's keep us from pulling bytes outside our buffer + size_t eof_size = bytes.size() - 4; + + // While we're not at the end of the file + unsigned short offset = 0; + //unsigned short prev_offset = 0; + unsigned short data = 0xFFFF; + while (file_offset <= eof_size) + { + //prev_offset = offset; + + offset = (unsigned short)bytes[ file_offset++ ]; + offset |= ((unsigned short)bytes[ file_offset++ ])<<8; + + data = (unsigned short)bytes[ file_offset++ ]; + data |= ((unsigned short)bytes[ file_offset++ ])<<8; + + //if (((offset == 0)&&(data == 0xFFFF))||(offset < prev_offset)) + if (0 == offset) + { + // End of Frame, capture a copy + pFrame = new unsigned char[ 0x8000 ]; + memcpy(pFrame, pCanvas, 0x8000); + m_pC1PixelMaps.push_back(pFrame); + } + else + { + // Apply Change to the Canvas + offset &= 0x7FFF; // force the offset into the Canvas + // probably should ignore offsets outside the canvas + pCanvas[ offset ] = data & 0xFF; + pCanvas[ offset+1 ] = (data >> 8) & 0xFF; + } + } + + delete[] pCanvas; + } +} + +//------------------------------------------------------------------------------ + diff --git a/source/c2_file.h b/source/c2_file.h new file mode 100644 index 0000000..954321f --- /dev/null +++ b/source/c2_file.h @@ -0,0 +1,73 @@ +// +// C++ Encoder/Decoder +// For C2, Paintworks Animation File Format +// +// File Format summary here from Brutal Deluxe +// +//0000.7fff first pic +//8000.8003 length of frame data - 8008 +//8004.8007 timing +//8008.800b length of first frame data +//800c.800d first frame index +//800e.800f first frame data +// +// Offset 0, Value FFFF indicates the end of a frame +// 00 00 FF FF +// +#ifndef C2_FILE_H +#define C2_FILE_H + +#include + +#pragma pack(push, 1) + +typedef struct C2File_Header +{ + char image[0x8000]; // C1 Initial Image + + unsigned int file_length; // length of frame_data (file length - 8008) + unsigned int timing; + unsigned int frame_length; // first encoded frame length + +//------------------------------------------------------------------------------ +// If you're doing C, just get rid of these methods + bool IsValid(unsigned int fileLength) + { + if ((file_length+0x8008) != fileLength) + return false; // size isn't right + + return true; + } + +} C2File_Header; + +#pragma pack(pop) + +class C2File +{ +public: + // Load in a C2 File + C2File(const char *pFilePath); + + ~C2File(); + + // Retrieval + void LoadFromFile(const char* pFilePath); + int GetFrameCount() { return (int)m_pC1PixelMaps.size(); } + int GetWidth() { return m_widthPixels; } + int GetHeight() { return m_heightPixels; } + + const std::vector& GetPixelMaps() { return m_pC1PixelMaps; } + +private: + + int m_widthPixels; // Width of image in pixels + int m_heightPixels; // Height of image in pixels + + std::vector m_pC1PixelMaps; + +}; + + +#endif // C2_FILE_H + diff --git a/source/gsla_file.cpp b/source/gsla_file.cpp new file mode 100644 index 0000000..4aa4d2c --- /dev/null +++ b/source/gsla_file.cpp @@ -0,0 +1,606 @@ +// +// C++ Encoder/Decoder +// For GSLA, GS Lzb Animation File Format +// +// +// Care is taken in the encoder, to make sure the 65816 does not have to cross +// bank boundaries during any copy. This is so we can use the MVN instruction, +// and so we can reduce the number of bank checks in the code. We will have an +// opcode, that says “source data bank has changed” +// +// The file will be laid out such that you load the file in at a 64K memory +// boundary +// +// Goals include a good balance between file size, and playback performance +// (since one often makes a trade off with the other). +// +// The file is defined as a byte stream, loaded on a 64K Bank Boundary +// +// +// file-offset: the thing that is at the offset +// +// Header of the File is 20 bytes as follows +// +//File Offset Data Commentary +//------------------------------------------------------------------ +//0 0x47 ; ‘G’ Graphics +//1 0x53 ; ‘S’ +//2 0x4C ; ‘L’ LZB +//3 0x41 ; ‘A’ Animation +// +// File Length, is the total length of the file +//4 FileLengthLow ; Low byte, 32-bit file length +//5 LengthLowHigh ; High byte of low word +//6 LengthHighLow ; Low byte, of high word +//7 LengthHighHigh ; High Byte, of high word +// +// 16 bit word with version # +//8 VL ; Version # of the file format, currently only version 0 exists +//9 VH ; Version High Byte +// ; %RVVV_VVVV_VVVV_VVVV +// ; V is a version #, 0 for now +// ; R is the MSB, R = 0 no ring frame +// ; R = 1, there is a ring frame +// ; A Ring Frame is a frame that will delta from the last +// ; frame of the animation, back to the first, for smoother +// ; playback looping , If a ring frame exists, it’s also +// ; included in the frame count +// +// next is a word, width in bytes (likely 160 for now) +//0xA WL ; Display Width in bytes low byte +//0xB WH ; Display Width in bytes high byte +// +// next is a word, height (likely 200 for now) +//0xC HL ; Display Height in bytes, low byte +//0xD HH ; Display Height in bytes, high byte +// 2 bytes, Frame Size in Bytes, since a “Frame” may contain more than just the +// width * height, worth of pixels, for now this is $8000, or 32768 +//0xE FBL ; Frame Buffer Length Low +//0xF FBH ; Frame Buffer Length High +// +// 4 byte, 32-bit, Frame Count (includes total frame count, so if there is a ring frame, this is included in the total) +//0x10 FrameCountLow +//0x11 FrameCountLowHigh +//0x12 FrameCountHighLow +//0x13 FrameCountHigh +// +// +// After this comes AIFF style chunks of data, basically a 4 byte chunk name, +// followed by a 4 byte length (inclusive of the chunk size). The idea is that +// you can skip chunks you don’t understand. +// +//File Offset: +//0x14 First Chunk (followed by more Chunks, until end of file) +// +//Chunk Definitions +//Name: ‘INIT’ - Initial Frame Chunk, this is the data used to first initialize the playback buffer +//0: 0x49 ‘I’ +//1: 0x4E ‘N’ +//2: 0x49 ‘I’ +//3: 0x54 ‘T’ +// 32 bit long, length, little endian, including the 8 byte header +//4: length low low +//5: length low high +//6: length high low +//7: length high high +// +//8: …. This is a single frame of data, that decodes/decompresses into frame +// sized bytes (right now 0x8000) +// This data stream includes, an end of animation opcode, so that the normal +// animation decompressor, can be called on this data, and it will emit the +// initial frame onto the screen +// +//Name: ‘ANIM’ - Frames +//0: 0x41 ‘A’ +//1: 0x4E ‘N’ +//2: 0x49 ‘I’ +//3: 0x4D ‘M’ +// 32 bit long, length, little endian, including chunk header +//4: length low low +//5: length low high +//6: length high low +//7: length high high +// +// This is followed by the frames, with the intention of decompressing them at +// 60FPS, which is why no play speed is included, if you need a play-rate +// slower than this, blank frame’s should be inserted into the animation data +// +// Every attempt is made to delta encode the image, meaning we just encode +// information about what changed each frame. We attempt to make the size +// efficient by supporting dictionary copies (where the dictionary is made up +// of existing pixels in the frame buffer). +// +//Command Word, encoded low-high, what the bits mean: +// +// xxx_xxxx_xxxx_xxx is the number of bytes 1-16384 to follow (0 == 1 byte) +// +//%0xxx_xxxx_xxxx_xxx1 - Copy Bytes - straight copy bytes +//%1xxx_xxxx_xxxx_xxx1 - Skip Bytes - skip bytes / move the cursor +//%1xxx_xxxx_xxxx_xxx0 - Dictionary Copy Bytes from frame buffer to frame buffer +// +//%0000_0000_0000_0000- Source Skip -> Source pointer skips to next bank of data +//%0000_0000_0000_0010- End of Frame - end of frame +//%0000_0000_0000_0110- End of Animation / End of File / no more frames +// +// +// other remaining codes, are reserved for future expansion + +#include "gsla_file.h" + +#include "lzb.h" + +#include + +// If these structs are the wrong size, there's an issue with type sizes, and +// your compiler +static_assert(sizeof(GSLA_Header)==20, "GSLA_Header is supposed to be 20 bytes"); +static_assert(sizeof(GSLA_INIT)==8, "GSLA_INIT is supposed to be 8 bytes"); +static_assert(sizeof(GSLA_ANIM)==8, "GSLA_ANIM is supposed to be 8 bytes"); +static_assert(sizeof(GSLA_CHUNK)==8, "GSLA_CHUNK is supposed to be 8 bytes"); + +//------------------------------------------------------------------------------ +// Load in a FanFile constructor +// +GSLAFile::GSLAFile(const char *pFilePath) + : m_widthPixels(320) + , m_heightPixels(200) +{ + LoadFromFile(pFilePath); +} + +//------------------------------------------------------------------------------ + +GSLAFile::GSLAFile(int iWidthPixels, int iHeightPixels, int iFrameSizeBytes ) + : m_widthPixels(iWidthPixels) + , m_heightPixels(iHeightPixels) + , m_frameSize( iFrameSizeBytes ) +{ + +} + +//------------------------------------------------------------------------------ + +GSLAFile::~GSLAFile() +{ + // Free Up the memory + for (int idx = 0; idx < m_pC1PixelMaps.size(); ++idx) + { + delete[] m_pC1PixelMaps[idx]; + m_pC1PixelMaps[ idx ] = nullptr; + } +} + +//------------------------------------------------------------------------------ + +void GSLAFile::LoadFromFile(const char* pFilePath) +{ + // Free Up the memory + for (int idx = 0; idx < m_pC1PixelMaps.size(); ++idx) + { + delete[] m_pC1PixelMaps[idx]; + m_pC1PixelMaps[ idx ] = nullptr; + } + + m_pC1PixelMaps.clear(); + //-------------------------------------------------------------------------- + + std::vector bytes; + + //-------------------------------------------------------------------------- + // Read the file into memory + FILE* pFile = nullptr; + errno_t err = fopen_s(&pFile, pFilePath, "rb"); + + if (0==err) + { + fseek(pFile, 0, SEEK_END); + size_t length = ftell(pFile); // get file size + fseek(pFile, 0, SEEK_SET); + + bytes.resize( length ); // make sure buffer is large enough + + // Read in the file + fread(&bytes[0], sizeof(unsigned char), bytes.size(), pFile); + fclose(pFile); + } + + if (bytes.size()) + { + size_t file_offset = 0; // File Cursor + + // Bytes are in the buffer, so let's start looking at what we have + GSLA_Header* pHeader = (GSLA_Header*) &bytes[0]; + + // Early out if things don't look right + if (!pHeader->IsValid((unsigned int)bytes.size())) + return; + + // Size in bytes for each frame in this animation + m_frameSize = pHeader->frame_size; + + // pre-allocate all the frames + for (unsigned int idx = 0; idx < pHeader->frame_count; ++idx) + { + m_pC1PixelMaps.push_back(new unsigned char[ m_frameSize ]); + } + + //---------------------------------------------------------------------- + // Process Chunks as we encounter them + file_offset += sizeof(GSLA_Header); + + // While we're not at the end of the file + while (file_offset < bytes.size()) + { + // This is pretty dumb, just get it done + // These are the types I understand + // every chunk is supposed to contain a value chunk_length + // at offset +4, so that we can ignore ones we don't understand + GSLA_INIT* pINIT = (GSLA_INIT*)&bytes[ file_offset ]; + GSLA_ANIM* pANIM = (GSLA_ANIM*)&bytes[ file_offset ]; + GSLA_CHUNK* pCHUNK = (GSLA_CHUNK*)&bytes[ file_offset ]; + + if (pINIT->IsValid()) + { + // We have an initial frame chunk + UnpackInitialFrame(pINIT, pHeader); + } + else if (pANIM->IsValid()) + { + // We have a packed animation frames chunk + UnpackAnimation(pANIM, pHeader); + } + + file_offset += pCHUNK->chunk_length; + + } + } +} + +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// +// Unpack the initial frame, that's been packed with an empty initial dictionary +// So every byte of the buffer will be written out (no skip opcodes) +// +void GSLAFile::UnpackInitialFrame(GSLA_INIT* pINIT, GSLA_Header* pHeader) +{ + unsigned char* pData = ((unsigned char*)pINIT) + sizeof(GSLA_INIT); + + unsigned char* pTargetBuffer = m_pC1PixelMaps[ 0 ]; // Data needs to be pre allocated + + DecompressFrame(pTargetBuffer, pData, (unsigned char*)pHeader); +} + +//------------------------------------------------------------------------------ +// +// Unpack the animation frame, assuming that the initial frame already exists +// +void GSLAFile::UnpackAnimation(GSLA_ANIM* pANIM, GSLA_Header* pHeader) +{ + unsigned char* pData = ((unsigned char*)pANIM) + sizeof(GSLA_ANIM); + + unsigned char *pCanvas = new unsigned char[m_frameSize]; + + // Initialize the Canvas with the first frame + memcpy(pCanvas, m_pC1PixelMaps[0], m_frameSize); + + for (int idx = 1; idx < m_pC1PixelMaps.size(); ++idx) + { + // Apply Changes to the Canvas + pData += DecompressFrame(pCanvas, pData, (unsigned char*) pHeader); + + // Capture the Canvas + memcpy(m_pC1PixelMaps[idx], pCanvas, m_frameSize); + } +} + +//------------------------------------------------------------------------------ +// +// Append a copy of raw image data into the class +// +void GSLAFile::AddImages( const std::vector& pFrameBytes ) +{ + for (int idx = 0; idx < pFrameBytes.size(); ++idx) + { + unsigned char* pPixels = new unsigned char[ m_frameSize ]; + memcpy(pPixels, pFrameBytes[ idx ], m_frameSize ); + m_pC1PixelMaps.push_back( pPixels ); + } +} + +//------------------------------------------------------------------------------ +// +// Compress / Serialize a new GSLA File +// +void GSLAFile::SaveToFile(const char* pFilenamePath) +{ + // We're not going to even try encoding an empty file + if (m_pC1PixelMaps.size() < 1) + { + return; + } + + // serialize to memory, then save that to a file + std::vector bytes; + + //-------------------------------------------------------------------------- + // Add the header + bytes.resize( bytes.size() + sizeof(GSLA_Header) ); + + //$$JGA Remember, you have to set the pointer, before every access + //$$JGA to the header data, because vector is going to change out + //$$JGA memory addresses from underneath you + GSLA_Header* pHeader = (GSLA_Header*)&bytes[0]; + + pHeader->G = 'G'; pHeader->S = 'S'; pHeader->L = 'L'; pHeader->A = 'A'; + + pHeader->file_length = 0; // Temp File Length + + pHeader->version = 0x8000; // Version 0, with a Ring/Loop Frame at the end + + pHeader->width = m_widthPixels >> 1; + pHeader->height = m_heightPixels; + + pHeader->frame_size = m_frameSize; + + pHeader->frame_count = (unsigned int)m_pC1PixelMaps.size() + 1; // + 1 for the ring frame + + //-------------------------------------------------------------------------- + // Add the INITial frame chunk + // + // If there's only an initial frame, I guess this becomes a picture + // + + size_t init_offset = bytes.size(); + + // Add space for the INIT header + bytes.resize( bytes.size() + sizeof(GSLA_INIT) ); + GSLA_INIT* pINIT = (GSLA_INIT*) &bytes[ init_offset ]; + + pINIT->I = 'I'; pINIT->N = 'N'; pINIT->i = 'I'; pINIT->T = 'T'; + pINIT->chunk_length = 0; // temp chunk size + + printf("Save Initial Frame\n"); + + // Need a place to put compressed data, in theory it could be bigger + // than the original data, I think if that happens, the image was probably + // designed to break this, anyway, give double theoretical max + unsigned char* pWorkBuffer = new unsigned char[ m_frameSize * 2 ]; + + unsigned char* pInitialFrame = m_pC1PixelMaps[ 0 ]; + + // We're not worried about bank wrap on the first frame, and we don't have a pre-populated + // dictionary - Also use the best compression we can get here + int compressedSize = Old_LZB_Compress(pWorkBuffer, pInitialFrame, m_frameSize); + + printf("frameSize = %d\n", compressedSize); + + for (int compressedIndex = 0; compressedIndex < compressedSize; ++compressedIndex) + { + bytes.push_back(pWorkBuffer[ compressedIndex ]); + } + + // Insert EOF/ End of Animation Done opcode + bytes.push_back( 0x06 ); + bytes.push_back( 0x00 ); + + + // Reset pointer to the pINIT (as the baggage may have shifted) + pINIT = (GSLA_INIT*) &bytes[ init_offset ]; + pINIT->chunk_length = (unsigned int) (bytes.size() - init_offset); + + //-------------------------------------------------------------------------- + // Add the ANIMation frames chunk + // + // We always add this, because we always add a Ring/Loop frame, we always + // end up with at least 2 frames + // + + size_t anim_offset = bytes.size(); + + // Add Space for the ANIM Header + bytes.resize( bytes.size() + sizeof(GSLA_ANIM) ); + GSLA_ANIM* pANIM = (GSLA_ANIM*) &bytes[ anim_offset ]; + + pANIM->A = 'A'; pANIM->N = 'N'; pANIM->I ='I'; pANIM->M = 'M'; + pANIM->chunk_length = 0; // temporary chunk size + + // Initialize the Canvas with the initial frame (we alread exported this) + unsigned char *pCanvas = new unsigned char[ m_frameSize ]; + memcpy(pCanvas, m_pC1PixelMaps[0], m_frameSize); + + // Let's encode some frames buddy + for (int frameIndex = 1; frameIndex < m_pC1PixelMaps.size(); ++frameIndex) + { + printf("Save Frame %d\n", frameIndex+1); + + // I don't want random data in the bank gaps, so initialize this + // buffer with zero + //memset(pWorkBuffer, 0xEA, m_frameSize * 2); + + int frameSize = LZBA_Compress(pWorkBuffer, m_pC1PixelMaps[ frameIndex ], + m_frameSize, pWorkBuffer-bytes.size(), + pCanvas, m_frameSize ); + + //int canvasDiff = memcmp(pCanvas, m_pC1PixelMaps[ frameIndex], m_frameSize); + //if (canvasDiff) + //{ + // printf("Canvas is not correct - %d\n", canvasDiff); + //} + printf("frameSize = %d\n", frameSize); + + + for (int frameIndex = 0; frameIndex < frameSize; ++frameIndex) + { + bytes.push_back(pWorkBuffer[ frameIndex ]); + } + } + + // Add the RING Frame + //memset(pWorkBuffer, 0xAB, m_frameSize * 2); + + printf("Save Ring Frame\n"); + + int ringSize = LZBA_Compress(pWorkBuffer, m_pC1PixelMaps[ 0 ], + m_frameSize, pWorkBuffer-bytes.size(), + pCanvas, m_frameSize ); + + printf("Ring Size %d\n", ringSize); + + for (int ringIndex = 0; ringIndex < ringSize; ++ringIndex) + { + bytes.push_back(pWorkBuffer[ ringIndex ]); + } + + delete[] pCanvas; pCanvas = nullptr; + + // Insert End of file/ End of Animation Done opcode + // -- There has to be room for this, or there wouldn't be room to insert + // -- a source bank skip opcode + bytes.push_back( 0x06 ); + bytes.push_back( 0x00 ); + + // Update the chunk length + pANIM = (GSLA_ANIM*)&bytes[ anim_offset ]; + pANIM->chunk_length = (unsigned int) (bytes.size() - anim_offset); + + // Update the header + pHeader = (GSLA_Header*)&bytes[0]; // Required + pHeader->file_length = (unsigned int)bytes.size(); // get some valid data in there + + // Try not to leak memory, even though we probably do + delete[] pWorkBuffer; + + //-------------------------------------------------------------------------- + // Create the file and write it + FILE* pFile = nullptr; + errno_t err = fopen_s(&pFile, pFilenamePath, "wb"); + + if (0==err) + { + fwrite(&bytes[0], sizeof(unsigned char), bytes.size(), pFile); + fclose(pFile); + } +} + +//------------------------------------------------------------------------------ +// +// Std C memcpy seems to be stopping the copy from happening, when I overlap +// the buffer to get a pattern run copy (overlapped buffers) +// +static void my_memcpy(unsigned char* pDest, unsigned char* pSrc, int length) +{ + while (length-- > 0) + { + *pDest++ = *pSrc++; + } +} + +//------------------------------------------------------------------------------ +// +// pTarget is the Target Frame Buffer +// pData is the source data for a Frame +// +// pDataBaseAddress, is the base address wheret the animation file was loaded +// this is used so we can properly interpret bank-skip opcodes (data is broken +// into 64K chunks for the IIgs/65816) +// +// returns the number of bytes that have been processed in the pData +// +int GSLAFile::DecompressFrame(unsigned char* pTarget, unsigned char* pData, unsigned char* pDataBaseAddress) +{ + unsigned char *pDataStart = pData; + + int cursorPosition = 0; + unsigned short opcode; + + bool bDoWork = true; + + while (bDoWork) + { + opcode = pData[0]; + opcode |= (((unsigned short)pData[1])<<8); + + if (opcode & 0x8000) + { + if (opcode & 0x0001) + { + // Cursor Skip Forward + opcode = (opcode>>1) & 0x3FFF; + cursorPosition += (opcode+1); + pData+=2; + } + else + { + // Dictionary Copy + unsigned short dictionaryPosition = pData[2]; + dictionaryPosition |= (((unsigned short)pData[3])<<8); + + dictionaryPosition -= 0x2000; // it's like this to to help the + // GS decode it quicker + + unsigned short length = ((opcode>>1) & 0x3FFF)+1; + + my_memcpy(pTarget + cursorPosition, pTarget + dictionaryPosition, (int) length ); + + pData += 4; + cursorPosition += length; + } + } + else + { + if (opcode & 0x0001) + { + // Literal Copy Bytes + pData += 2; + unsigned short length = ((opcode>>1) & 0x3FFF)+1; + + my_memcpy(pTarget + cursorPosition, pData, (int) length); + + pData += length; + cursorPosition += length; + } + else + { + opcode = ((opcode>>1)) & 3; + + switch (opcode) + { + case 0: // Source bank Skip + { + int offset = (int)(pData - pDataBaseAddress); + + offset &= 0xFFFF0000; + offset += 0x00010000; + + pData = pDataBaseAddress + offset; + } + break; + case 1: // End of frame + pData+=2; + bDoWork = false; + break; + + case 3: // End of Animation + // Intentionally, leave cursor alone here + bDoWork = false; + break; + + default: + // Reserved / Illegal + bDoWork = false; + break; + } + + } + } + + + } + + return (int)(pData - pDataStart); +} + +//------------------------------------------------------------------------------ + diff --git a/source/gsla_file.h b/source/gsla_file.h new file mode 100644 index 0000000..d51e345 --- /dev/null +++ b/source/gsla_file.h @@ -0,0 +1,150 @@ +// +// C++ Encoder/Decoder +// For GSLA, GS Lzb Animation File Format +// +// +// Care is taken in the encoder, to make sure the 65816 does not have to cross +// bank boundaries during any copy. This is so we can use the MVN instruction, +// and so we can reduce the number of bank checks in the code. We will have an +// opcode, that says “source data bank has changed” +// +// The file will be laid out such that you load the file in at a 64K memory +// boundary +// +// Please see the gsla_file.cpp, for documentation on the actual +// format of the file +// +#ifndef GSLA_FILE_H +#define GSLA_FILE_H + +#include + +#pragma pack(push, 1) + +// Data Stream Header +typedef struct GSLA_Header +{ + char G,S,L,A; + unsigned int file_length; // length of the file, including this header + unsigned short version; + unsigned short width; // data width in bytes + unsigned short height; // data height in bytes + unsigned short frame_size; // frame size in bytes (0 means 64K) + unsigned int frame_count; // total number of frames in the file + +//------------------------------------------------------------------------------ +// If you're doing C, just get rid of these methods + bool IsValid(unsigned int fileLength) + { + if ((G!='G')||(S!='S')||(L!='L')||(A!='A')) + return false; // signature is not right + + if (file_length != fileLength) + return false; // size isn't right + + if (0x8000 != frame_size) + return false; + + if (160 != width) + return false; + + if (200 != height) + return false; + + return true; + } + +} GSLA_Header; + +// INITial Frame Chunk +typedef struct GSLA_INIT +{ + char I,N,i,T; // 'I','N','I','T' + unsigned int chunk_length; // in bytes, including the 9 bytes header of this chunk + + // Commands Coded Data Follows +//------------------------------------------------------------------------------ +// If you're doing C, just get rid of these methods + bool IsValid() + { + if ((I!='I')||(N!='N')||(i!='I')||(T!='T')) + return false; // signature is not right + + return true; + } + + +} GSLA_INIT; + +// Animated Frames Chunk +typedef struct GSLA_ANIM +{ + char A,N,I,M; // 'A','N','I','M' + unsigned int chunk_length; // in bytes, including the 8 bytes header of this chunk + + // Commands Coded Data Follows + +//------------------------------------------------------------------------------ +// If you're doing C, just get rid of these methods + bool IsValid() + { + if ((A!='A')||(N!='N')||(I!='I')||(M!='M')) + return false; // signature is not right + + return true; + } + +} GSLA_ANIM; + +// Generic Unknown Chunk +typedef struct GSLA_CHUNK +{ + char id0,id1,id2,id3; + unsigned int chunk_length; + +} GSLA_CHUNK; + + +#pragma pack(pop) + +class GSLAFile +{ +public: + // Load in a GSLA File + GSLAFile(const char *pFilePath); + ~GSLAFile(); + + // Creation + GSLAFile(int iWidthPixels, int iHeightPixels, int iFrameSizeBytes); + void AddImages( const std::vector& pFrameBytes ); + void SaveToFile(const char* pFilenamePath); + + // Retrieval + void LoadFromFile(const char* pFilePath); + int GetFrameCount() { return (int)m_pC1PixelMaps.size(); } + int GetWidth() { return m_widthPixels; } + int GetHeight() { return m_heightPixels; } + int GetFrameSize() { return m_frameSize; } + + const std::vector& GetPixelMaps() { return m_pC1PixelMaps; } + + int DecompressFrame(unsigned char* pTarget, unsigned char* pData, unsigned char* pDataBaseAddress); + +private: + + void UnpackInitialFrame(GSLA_INIT* pINIT, GSLA_Header* pHeader); + void UnpackAnimation(GSLA_ANIM* pANIM, GSLA_Header* pHeader); + + + int m_frameSize; // frame buffer size in bytes + + int m_widthPixels; // Width of image in pixels + int m_heightPixels; // Height of image in pixels + + std::vector m_pC1PixelMaps; + +}; + + +#endif // C2_FILE_H + diff --git a/source/lzb.cpp b/source/lzb.cpp new file mode 100644 index 0000000..4ce97cf --- /dev/null +++ b/source/lzb.cpp @@ -0,0 +1,986 @@ +// +// LZB Encode / Decode +// +#include "lzb.h" + +#include +#include + +#include "bctypes.h" + +#include "assert.h" + +// +// This is written specifically for the GSLA, so opcodes emitted are designed +// to work with our version of a run/skip/dump +// + +// +//Command Word, encoded low-high, what the bits mean: +// +// xxx_xxxx_xxxx_xxx is the number of bytes 1-16384 to follow (0 == 1 byte) +// +//%0xxx_xxxx_xxxx_xxx1 - Copy Bytes - straight copy bytes +//%1xxx_xxxx_xxxx_xxx1 - Skip Bytes - skip bytes / move the cursor +//%1xxx_xxxx_xxxx_xxx0 - Dictionary Copy Bytes from frame buffer to frame buffer +// +//%0000_0000_0000_0000- Source Skip -> Source pointer skips to next bank of data +//%0000_0000_0000_0010- End of Frame - end of frame +//%0000_0000_0000_0110- End of Animation / End of File / no more frames +// + +#define MAX_DICTIONARY_SIZE (32 * 1024) +#define MAX_STRING_SIZE (16384) +// +// Yes This is a 32K Buffer, of bytes, with no structure to it +// +static unsigned char *pGlobalDictionary = nullptr; + +struct DataString { + // Information about the data we're trying to match + int size; + unsigned char *pData; +}; + +static int AddDictionary(const DataString& data, int dictionarySize); +static int EmitLiteral(unsigned char *pDest, DataString& data); +static int ConcatLiteral(unsigned char *pDest, DataString& data); +static int EmitReference(unsigned char *pDest, int dictionaryOffset, DataString& data); +static int DictionaryMatch(const DataString& data, int dictionarySize); + +// Stuff I need for a faster version +static DataString LongestMatch(const DataString& data, const DataString& dictionary); +static DataString LongestMatch(const DataString& data, const DataString& dictionary, int cursorPosition); + +// +// New Version, still Brute Force, but not as many times +// +int LZB_Compress(unsigned char* pDest, unsigned char* pSource, int sourceSize) +{ + //printf("LZB Compress %d bytes\n", sourceSize); + + unsigned char *pOriginalDest = pDest; + + DataString sourceData; + DataString dictionaryData; + DataString candidateData; + + // Source Data Stream - will compress until the size is zero + sourceData.pData = pSource; + sourceData.size = sourceSize; + + // Remember, this eventually will point at the frame buffer + pGlobalDictionary = pSource; + dictionaryData.pData = pSource; + dictionaryData.size = 0; + + // dumb last emit is a literal stuff + bool bLastEmitIsLiteral = false; + unsigned char* pLastLiteralDest = nullptr; + + while (sourceData.size > 0) + { + candidateData = LongestMatch(sourceData, dictionaryData); + + // If no match, or the match is too small, then take the next byte + // and emit as literal + if ((0 == candidateData.size)) // || (candidateData.size < 4)) + { + candidateData.size = 1; + candidateData.pData = sourceData.pData; + } + + // Adjust source stream + sourceData.pData += candidateData.size; + sourceData.size -= candidateData.size; + + dictionaryData.size = AddDictionary(candidateData, dictionaryData.size); + + if (candidateData.size > 3) + { + // Emit a dictionary reference + pDest += (int)EmitReference(pDest, (int)(candidateData.pData - dictionaryData.pData), candidateData); + bLastEmitIsLiteral = false; + } + else if (bLastEmitIsLiteral) + { + // Concatenate this literal onto the previous literal + pDest += ConcatLiteral(pLastLiteralDest, candidateData); + } + else + { + // Emit a new literal + pLastLiteralDest = pDest; + bLastEmitIsLiteral = true; + pDest += EmitLiteral(pDest, candidateData); + } + } + + return (int)(pDest - pOriginalDest); +} + + +// +// This works, but it's stupidly slow, because it uses brute force, and +// because the brute force starts over everytime I grow the data string +// +int Old_LZB_Compress(unsigned char* pDest, unsigned char* pSource, int sourceSize) +{ + //printf("LZB_Compress %d bytes\n", sourceSize); + + // Initialize Dictionary + int bytesInDictionary = 0; // eventually add the ability to start with the dictionary filled + pGlobalDictionary = pSource; + + int processedBytes = 0; + int bytesEmitted = 0; + + // dumb last emit is a literal stuff + bool bLastEmitIsLiteral = false; + int lastEmittedLiteralOffset = 0; + + DataString candidate_data; + candidate_data.pData = pSource; + candidate_data.size = 0; + + int MatchOffset = -1; + int PreviousMatchOffset = -1; + + while (processedBytes < sourceSize) + { + // Add a byte to the candidate_data, also tally number of processed + processedBytes++; + candidate_data.size++; + + // Basic Flow Idea Here + // If there's a match, then add to the candidate data, and see if + // there's a bigger match (use previous result to speed up search) + // else + // if there's a previous match, and it's large enough, emit that + // else emit what we have as a literal + + + // (KMP is probably the last planned optmization here) + PreviousMatchOffset = MatchOffset; + + MatchOffset = DictionaryMatch(candidate_data, bytesInDictionary); + + // The dictionary only contains bytes that have been emitted, so we + // can't add this byte until we've emitted it? + if (MatchOffset < 0) + { + // Was there a dictionary match + + // Previous Data, we can't get here with candidate_data.size == 0 + // this is an opportunity to use an assert + candidate_data.size--; + + MatchOffset = PreviousMatchOffset; //DictionaryMatch(candidate_data, bytesInDictionary); + + if ((MatchOffset >= 0) && candidate_data.size > 3) + { + processedBytes--; + bytesInDictionary = AddDictionary(candidate_data, bytesInDictionary); + bytesEmitted += EmitReference(pDest + bytesEmitted, MatchOffset, candidate_data); + bLastEmitIsLiteral = false; + } + else + { + if (0 == candidate_data.size) + { + candidate_data.size++; + } + else + { + processedBytes--; + + //if (candidate_data.size > 1) + //{ + // processedBytes -= (candidate_data.size - 1); + // candidate_data.size = 1; + //} + } + + + // Add Dictionary + bytesInDictionary = AddDictionary(candidate_data, bytesInDictionary); + + if (bLastEmitIsLiteral) + { + // If the last emit was a literal, I want to concatenate + // this literal into the previous opcode, to save space + bytesEmitted += ConcatLiteral(pDest + lastEmittedLiteralOffset, candidate_data); + } + else + { + lastEmittedLiteralOffset = bytesEmitted; + bytesEmitted += EmitLiteral(pDest + bytesEmitted, candidate_data); + } + bLastEmitIsLiteral = true; + //MatchOffset = -1; + } + } + } + + if (candidate_data.size > 0) + { + + int MatchOffset = DictionaryMatch(candidate_data, bytesInDictionary); + + if ((MatchOffset >=0) && candidate_data.size > 2) + { + bytesInDictionary = AddDictionary(candidate_data, bytesInDictionary); + bytesEmitted += EmitReference(pDest + bytesEmitted, MatchOffset, candidate_data); + } + else + { + // Add Dictionary + bytesInDictionary = AddDictionary(candidate_data, bytesInDictionary); + + if (bLastEmitIsLiteral) + { + // If the last emit was a literal, I want to concatenate + // this literal into the previous opcode, to save space + bytesEmitted += ConcatLiteral(pDest + lastEmittedLiteralOffset, candidate_data); + } + else + { + bytesEmitted += EmitLiteral(pDest + bytesEmitted, candidate_data); + } + } + } + + return bytesEmitted; +} + +//------------------------------------------------------------------------------ +// Return new dictionarySize +static int AddDictionary(const DataString& data, int dictionarySize) +{ + int dataIndex = 0; + while (dataIndex < data.size) + { + pGlobalDictionary[ dictionarySize++ ] = data.pData[ dataIndex++ ]; + } + + //dictionarySize += data.size; + + return dictionarySize; +} + +//------------------------------------------------------------------------------ +// +// Return longest match of data, in dictionary +// + +DataString LongestMatch(const DataString& data, const DataString& dictionary) +{ + DataString result; + result.pData = nullptr; + result.size = 0; + + // Find the longest matching data in the dictionary + if ((dictionary.size > 0) && (data.size > 0)) + { + DataString candidate; + candidate.pData = data.pData; + candidate.size = 0; + + // First Check for a pattern / run-length style match + // Check the end of the dictionary, to see if this data could be a + // pattern "run" (where we can repeat a pattern for X many times for free + // using the memcpy with overlapping source/dest buffers) + // (This is a dictionary based pattern run/length) + { + // Check for pattern sizes, start small + int max_pattern_size = 4096; + if (dictionary.size < max_pattern_size) max_pattern_size = dictionary.size; + if (data.size < max_pattern_size) max_pattern_size = data.size; + + for (int pattern_size = 1; pattern_size <= max_pattern_size; ++pattern_size) + { + int pattern_start = dictionary.size - pattern_size; + + for (int dataIndex = 0; dataIndex < data.size; ++dataIndex) + { + if (data.pData[ dataIndex ] == dictionary.pData[ pattern_start + (dataIndex % pattern_size) ]) + { + candidate.pData = dictionary.pData + pattern_start; + candidate.size = dataIndex+1; + continue; + } + + break; + } + + //if (candidate.size < pattern_size) + // break; + + if (candidate.size > result.size) + { + result = candidate; + } + } + } + + // As an optimization + int dictionarySize = dictionary.size; // - 1; // This last string has already been checked by, the + // run-length matcher above + + // As the size grows, we're missing potential matches in here + // I think the best way to counter this is to attempt somthing + // like KMP + + if (dictionarySize > candidate.size) + { + // Check the dictionary for a match, brute force + for (int dictionaryIndex = 0; dictionaryIndex <= (dictionarySize-candidate.size); ++dictionaryIndex) + { + int sizeAvailable = dictionarySize - dictionaryIndex; + + if (sizeAvailable > data.size) sizeAvailable = data.size; + + // this could index off the end of the dictionary!!! FIX ME + for (int dataIndex = 0; dataIndex < sizeAvailable; ++dataIndex) + { + if (data.pData[ dataIndex ] == dictionary.pData[ dictionaryIndex + dataIndex ]) + { + if (dataIndex >= candidate.size) + { + candidate.pData = dictionary.pData + dictionaryIndex; + candidate.size = dataIndex + 1; + } + continue; + } + + break; + } + + if (candidate.size > result.size) + { + result = candidate; + //dictionaryIndex = -1; + break; + } + } + } + } + + return result; +} +//------------------------------------------------------------------------------ +DataString LongestMatch(const DataString& data, const DataString& dictionary, int cursorPosition) +{ + DataString result; + result.pData = nullptr; + result.size = 0; + + // Find the longest matching data in the dictionary + if ((dictionary.size > 0) && (data.size > 0)) + { + DataString candidate; + candidate.pData = data.pData; + candidate.size = 0; + + // First Check for a pattern / run-length style match + // Check the end of the dictionary, to see if this data could be a + // pattern "run" (where we can repeat a pattern for X many times for free + // using the memcpy with overlapping source/dest buffers) + // (This is a dictionary based pattern run/length) + { + // Check for pattern sizes, start small + int max_pattern_size = 4096; + if (cursorPosition < max_pattern_size) max_pattern_size = cursorPosition; + if (data.size < max_pattern_size) max_pattern_size = data.size; + + for (int pattern_size = 1; pattern_size <= max_pattern_size; ++pattern_size) + { + int pattern_start = cursorPosition - pattern_size; + + for (int dataIndex = 0; dataIndex < data.size; ++dataIndex) + { + if (data.pData[ dataIndex ] == dictionary.pData[ pattern_start + (dataIndex % pattern_size) ]) + { + candidate.pData = dictionary.pData + pattern_start; + candidate.size = dataIndex+1; + continue; + } + + break; + } + + if (candidate.size > result.size) + { + result = candidate; + } + } + } + + // Not getting better than this + if (result.size == data.size) + return result; + + // This will keep us from finding matches that we can't use + + int dictionarySize = cursorPosition; + + // As the size grows, we're missing potential matches in here + // I think the best way to counter this is to attempt somthing + // like KMP + + if (dictionarySize > candidate.size) + { + // Check the dictionary for a match, brute force + for (int dictionaryIndex = 0; dictionaryIndex <= (dictionarySize-candidate.size); ++dictionaryIndex) + { + int sizeAvailable = dictionarySize - dictionaryIndex; + + if (sizeAvailable > data.size) sizeAvailable = data.size; + + // this could index off the end of the dictionary!!! FIX ME + for (int dataIndex = 0; dataIndex < sizeAvailable; ++dataIndex) + { + if (data.pData[ dataIndex ] == dictionary.pData[ dictionaryIndex + dataIndex ]) + { + if (dataIndex >= candidate.size) + { + candidate.pData = dictionary.pData + dictionaryIndex; + candidate.size = dataIndex + 1; + } + continue; + } + + break; + } + + if (candidate.size > result.size) + { + result = candidate; + //dictionaryIndex = -1; + break; + } + } + } + + // Not getting better than this + if (result.size == data.size) + return result; + + + #if 1 + // Look for matches beyond the cursor + dictionarySize = dictionary.size; + + if ((dictionarySize-cursorPosition) > candidate.size) + { + // Check the dictionary for a match, brute force + for (int dictionaryIndex = cursorPosition+3; dictionaryIndex <= (dictionarySize-candidate.size); ++dictionaryIndex) + { + int sizeAvailable = dictionarySize - dictionaryIndex; + + if (sizeAvailable > data.size) sizeAvailable = data.size; + + // this could index off the end of the dictionary!!! FIX ME + for (int dataIndex = 0; dataIndex < sizeAvailable; ++dataIndex) + { + if (data.pData[ dataIndex ] == dictionary.pData[ dictionaryIndex + dataIndex ]) + { + if (dataIndex >= candidate.size) + { + candidate.pData = dictionary.pData + dictionaryIndex; + candidate.size = dataIndex + 1; + } + continue; + } + + break; + } + + if (candidate.size > result.size) + { + result = candidate; + break; + } + } + } + #endif + } + + return result; +} + +//------------------------------------------------------------------------------ +// +// Return offset into dictionary where the string matches +// +// -1 means, no match +// +static int DictionaryMatch(const DataString& data, int dictionarySize) +{ + if( (0 == dictionarySize ) || + (0 == data.size) || + (data.size > MAX_STRING_SIZE) ) // 16384 is largest string copy we can encode + { + return -1; + } + + // Check the end of the dictionary, to see if this data could be a + // pattern "run" (where we can repeat a pattern for X many times for free + // using the memcpy with overlapping source/dest buffers) + // (This is a dictionary based pattern run/length) + + { + // Check for pattern sizes, start small + int max_pattern_size = 256; + if (dictionarySize < max_pattern_size) max_pattern_size = dictionarySize; + if (data.size < max_pattern_size) max_pattern_size = data.size; + + for (int pattern_size = 1; pattern_size <= max_pattern_size; ++pattern_size) + { + bool bMatch = true; + int pattern_start = dictionarySize - pattern_size; + + for (int dataIndex = 0; dataIndex < data.size; ++dataIndex) + { + if (data.pData[ dataIndex ] == pGlobalDictionary[ pattern_start + (dataIndex % pattern_size) ]) + continue; + + bMatch = false; + break; + } + + if (bMatch) + { + // Return a RLE Style match result + return pattern_start; + } + } + } + + // As an optimization + dictionarySize -= 1; // This last string has already been checked by, the + // run-length matcher above + + if (dictionarySize < data.size) + { + return -1; + } + + int result = -1; + + // Check the dictionary for a match, brute force + for (int idx = 0; idx <= (dictionarySize-data.size); ++idx) + { + bool bMatch = true; + for (int dataIdx = 0; dataIdx < data.size; ++dataIdx) + { + if (data.pData[ dataIdx ] == pGlobalDictionary[ idx + dataIdx ]) + continue; + + bMatch = false; + break; + } + + if (bMatch) + { + result = idx; + break; + } + } + + return result; +} + +//------------------------------------------------------------------------------ +// +// Emit a literal, that appends itself to an existing literal +// +static int ConcatLiteral(unsigned char *pDest, DataString& data) +{ + // Return Size + int outSize = (int)data.size; + + int opCode = pDest[0]; + opCode |= (int)(((pDest[1])&0x7F)<<8); + + opCode>>=1; + opCode+=1; + // opCode contains the length of the literal that's already encoded + + int skip = opCode; + opCode += outSize; + + // Opcode + opCode -= 1; + opCode <<=1; + opCode |= 1; + + *pDest++ = (unsigned char)(opCode & 0xFF); + *pDest++ = (unsigned char)((opCode >> 8) & 0x7F); + + pDest += skip; + + // Literal Data + for (int idx = 0; idx < data.size; ++idx) + { + *pDest++ = data.pData[ idx ]; + } + + // Clear + data.pData += data.size; + data.size = 0; + + return outSize; +} + +//------------------------------------------------------------------------------ + +static int EmitLiteral(unsigned char *pDest, DataString& data) +{ + // Return Size + int outSize = 2 + (int)data.size; + + unsigned short length = (unsigned short)data.size; + length -= 1; + + assert(length < MAX_STRING_SIZE); + + unsigned short opcode = length<<1; + opcode |= 0x0001; + + // Opcode out + *pDest++ = (unsigned char)( opcode & 0xFF ); + *pDest++ = (unsigned char)(( opcode>>8)&0xFF); + + // Literal Data + for (int idx = 0; idx < data.size; ++idx) + { + *pDest++ = data.pData[ idx ]; + } + + // Clear + data.pData += data.size; + data.size = 0; + + return outSize; +} + +//------------------------------------------------------------------------------ + +static int EmitReference(unsigned char *pDest, int dictionaryOffset, DataString& data) +{ + // Return Size + int outSize = 2 + 2; + + unsigned short length = (unsigned short)data.size; + length -= 1; + + assert(length < MAX_STRING_SIZE); + + unsigned short opcode = length<<1; + opcode |= 0x8000; + + // Opcode out + *pDest++ = (unsigned char)( opcode & 0xFF ); + *pDest++ = (unsigned char)(( opcode>>8)&0xFF); + + // Destination Address out + unsigned short address = (unsigned short)dictionaryOffset; + address += 0x2000; // So we don't have to add $2000 in the animation player + + *pDest++ = (unsigned char)(address & 0xFF); + *pDest++ = (unsigned char)((address>>8)&0xFF); + + // Clear + data.pData += data.size; + data.size = 0; + + return outSize; +} + +//------------------------------------------------------------------------------ +// +// Std C memcpy seems to be stopping the copy from happening, when I overlap +// the buffer to get a pattern run copy (overlapped buffers) +// +static void my_memcpy(u8* pDest, u8* pSrc, int length) +{ + while (length-- > 0) + { + *pDest++ = *pSrc++; + } +} + +//------------------------------------------------------------------------------ +// +// Emit one or more Cursor Skip forward opcode +// +int EmitSkip(unsigned char* pDest, int skipSize) +{ + int outSize = 0; + int thisSkip = 0; + + while (skipSize > 0) + { + outSize+=2; + + thisSkip = skipSize; + if (thisSkip > MAX_STRING_SIZE) + { + thisSkip = MAX_STRING_SIZE; + } + skipSize -= thisSkip; + + + unsigned short length = (unsigned short)thisSkip; + length -= 1; + + assert(length < MAX_STRING_SIZE); + + unsigned short opcode = length<<1; + opcode |= 0x8001; + // Opcode out + *pDest++ = (unsigned char)( opcode & 0xFF ); + *pDest++ = (unsigned char)(( opcode>>8)&0xFF); + } + + return outSize; +} + +//------------------------------------------------------------------------------ +// +// Forcibly Emit a source Skip Opcode +// +// return space_left_in_Bank +// +int EmitSourceSkip(unsigned char*& pDest, int space_left_in_bank) +{ + assert(space_left_in_bank >= 2); + + *pDest++ = 0; + *pDest++ = 0; + space_left_in_bank-=2; + + while (space_left_in_bank) + { + space_left_in_bank--; + *pDest++ = 0; + } + + return 0x10000; +} + +//------------------------------------------------------------------------------ +// +// Conditionally shit out the Source Bank Skip +// +int CheckEmitSourceSkip(int checkSpace, unsigned char*& pDest, int space_left_in_bank) +{ + if ((checkSpace+2) > space_left_in_bank) + { + return EmitSourceSkip(pDest, space_left_in_bank); + } + + space_left_in_bank -= checkSpace; + + return space_left_in_bank; +} + +//------------------------------------------------------------------------------ +// +// Compress a Frame in the GSLA LZB Format +// +// The dictionary is also the canvas, so when we're finished the dictionary +// buffer will match the original pSource buffer +// +// If they both match to begin with, we just crap out an End of Frame opcode +// +int LZBA_Compress(unsigned char* pDest, unsigned char* pSource, int sourceSize, + unsigned char* pDataStart, unsigned char* pDictionary, + int dictionarySize) +{ +// printf("LZBA Compress %d bytes\n", sourceSize); + + pGlobalDictionary = pDictionary; + + // Used for bank skip opcode emission + int bankOffset = (int)((pDest - pDataStart) & 0xFFFF); + + // So we can track how big our compressed data ends up being + unsigned char *pOriginalDest = pDest; + + DataString sourceData; + DataString dictionaryData; + DataString candidateData; + + // Source Data Stream - will compress until the size is zero + sourceData.pData = pSource; + sourceData.size = sourceSize; + + // Dictionary is the Frame Buffer + dictionaryData.pData = pDictionary; + dictionaryData.size = dictionarySize; + + // dumb last emit is a literal stuff + bool bLastEmitIsLiteral = false; + unsigned char* pLastLiteralDest = nullptr; + + int lastEmittedCursorPosition = 0; // This is the default for each frame + + int space_left_in_bank = (int)0x10000 - (int)((pDest - pDataStart)&0xFFFF); + + space_left_in_bank = CheckEmitSourceSkip(0, pDest, space_left_in_bank); + + for (int cursorPosition = 0; cursorPosition < dictionarySize;) + { + if (pSource[ cursorPosition ] != pDictionary[ cursorPosition ]) + { + // Here is some data that has to be processed, so let's decide + // how large of a chunk of data we're looking at here + + // Do we need to emit a Skip opcode?, compare cursor to last emit + // and emit a skip command if we need it (I'm going want a gap of + // at least 3 bytes? before we call it the end + int skipSize = cursorPosition - lastEmittedCursorPosition; + + if (skipSize) + { + int numSkips = (skipSize / MAX_STRING_SIZE) + 1; + + space_left_in_bank = CheckEmitSourceSkip(2 * numSkips, pDest, space_left_in_bank); + + // We need to Skip + pDest += EmitSkip(pDest, skipSize); + bLastEmitIsLiteral = false; + lastEmittedCursorPosition = cursorPosition; + } + + int tempCursorPosition = cursorPosition; + int gapCount = 0; + for (; tempCursorPosition < dictionarySize; ++tempCursorPosition) + { + if (pSource[ tempCursorPosition ] != pDictionary[ tempCursorPosition ]) + { + gapCount = 0; + } + else + { + // if there's a small amount of matching data, let's include + // it in the clump (try and reduce opcode emissions) + if (gapCount >= 3) + break; + gapCount++; + } + } + + tempCursorPosition -= gapCount; + + // Now we know from cursorPosition to tempCursorPosition is data + // that we want to encode, we either literally copy it, or look + // to see if this data is already in the dictionary (so we can copy + // it from one part of the frame buffer to another part) + + sourceData.pData = &pSource[ cursorPosition ]; + sourceData.size = tempCursorPosition - cursorPosition; + + #if 0 // This Works + //-------------------------- Dump, so skip dump only + space_left_in_bank = CheckEmitSourceSkip(2+sourceData.size, pDest, space_left_in_bank); + + cursorPosition = AddDictionary(sourceData, cursorPosition); + + pDest += EmitLiteral(pDest, sourceData); + lastEmittedCursorPosition = cursorPosition; + #endif + + while (sourceData.size > 0) + { + candidateData = LongestMatch(sourceData, dictionaryData, cursorPosition); + + // If no match, or the match is too small, then take the next byte + // and emit as literal + if ((0 == candidateData.size)) // || (candidateData.size < 4)) + { + candidateData.size = 1; + candidateData.pData = sourceData.pData; + } + + // Adjust source stream + sourceData.pData += candidateData.size; + sourceData.size -= candidateData.size; + + // Modify the dictionary + cursorPosition = AddDictionary(candidateData, cursorPosition); + lastEmittedCursorPosition = cursorPosition; + + if (candidateData.size > 3) + { + space_left_in_bank = CheckEmitSourceSkip(4, pDest, space_left_in_bank); + + // Emit a dictionary reference + pDest += (int)EmitReference(pDest, (int)(candidateData.pData - dictionaryData.pData), candidateData); + bLastEmitIsLiteral = false; + + } + else if (bLastEmitIsLiteral) + { + // This is a problem for the source bank skip, we can't + // concatenate if we end up injecting a source bank skip opcode + // into the stream... what to do???, if insert, we will need to + // do a "normal" literal emission, ugly + + int space = CheckEmitSourceSkip(candidateData.size, pDest, space_left_in_bank); + + if (space != (space_left_in_bank - candidateData.size)) + { + space_left_in_bank = space-2; + + // Emit a new literal + pLastLiteralDest = pDest; + pDest += EmitLiteral(pDest, candidateData); + } + else + { + // Concatenate this literal onto the previous literal + space_left_in_bank = space; + pDest += ConcatLiteral(pLastLiteralDest, candidateData); + } + } + else + { + space_left_in_bank = CheckEmitSourceSkip(2 + candidateData.size, pDest, space_left_in_bank); + + // Emit a new literal + pLastLiteralDest = pDest; + bLastEmitIsLiteral = true; + pDest += EmitLiteral(pDest, candidateData); + } + } + } + else + { + // no change + cursorPosition++; + } + } + + space_left_in_bank = CheckEmitSourceSkip(2, pDest, space_left_in_bank); + + // Emit the End of Frame Opcode + *pDest++ = 0x02; + *pDest++ = 0x00; + + for (int idx = 0; idx < dictionarySize; ++idx) + { + if (pSource[ idx ] != pDictionary[ idx ]) + { + assert(0); + } + } + + return (int)(pDest - pOriginalDest); + +} + +//------------------------------------------------------------------------------ + diff --git a/source/lzb.h b/source/lzb.h new file mode 100644 index 0000000..8f43e20 --- /dev/null +++ b/source/lzb.h @@ -0,0 +1,21 @@ +// +// LZB Encode +// +#ifndef LZB_H +#define LZB_H + +// +// returns the size of data saved into the pDest Buffer +// +int LZB_Compress(unsigned char* pDest, unsigned char* pSource, int sourceSize); +int Old_LZB_Compress(unsigned char* pDest, unsigned char* pSource, int sourceSize); + +// +// LZB Compressor that uses GSLA Opcodes while encoding +// +int LZBA_Compress(unsigned char* pDest, unsigned char* pSource, int sourceSize, + unsigned char* pDataStart, unsigned char* pDictionary, + int dictionarySize); + +#endif // LZB_H + diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..e780553 --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,157 @@ +// +// GSLA - GS LZB Animation Tool +// +// Look in gsla_file.cpp/h for more information about the file format +// + +#include +#include +#include + +#include "c2_file.h" +#include "gsla_file.h" + +//------------------------------------------------------------------------------ +static void helpText() +{ + printf("GSLA - v1.0\n"); + printf("--------------\n"); + printf("GS Lzb Animation Creation Tool\n"); + printf("\n"); + printf("\ngsla [options] \n"); + printf("\n\n There are no [options] yet\n"); + printf("Converts from C2 to GSLA\n"); + + exit(-1); +} +//------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------ +// Local helper functions + +static std::string toLower(const std::string s) +{ + std::string result = s; + + for (int idx = 0; idx < result.size(); ++idx) + { + result[ idx ] = (char)tolower(result[idx]); + } + + return result; +} + +// Case Insensitive +static bool endsWith(const std::string& S, const std::string& SUFFIX) +{ + bool bResult = false; + + std::string s = toLower(S); + std::string suffix = toLower(SUFFIX); + + bResult = s.rfind(suffix) == (s.size()-suffix.size()); + + return bResult; +} +//------------------------------------------------------------------------------ + + +int main(int argc, char* argv[]) +{ + char* pInfilePath = nullptr; + char* pOutfilePath = nullptr; + + + // index 0 is the executable name + + if (argc < 2) helpText(); + + for (int idx = 1; idx < argc; ++idx ) + { + char* arg = argv[ idx ]; + + if ('-' == arg[0]) + { + // Parse as an option + // Currently I have no options, so I'll just skip + } + else if (nullptr == pInfilePath) + { + // Assume the first non-option is an input file path + pInfilePath = argv[ idx ]; + } + else if (nullptr == pOutfilePath) + { + // Assume second non-option is an output file path + pOutfilePath = argv[ idx ]; + } + else + { + // Oh Crap, we have a non-option, but we don't know what to do with + // it + printf("ERROR: Invalid option, Arg %d = %s\n\n", idx, argv[ idx ]); + helpText(); + } + } + + if (pInfilePath) + { + // See what we can do with the input file path + // could be a .gsla file, for a .c2 file, or maybe a series of .c1 files + if (endsWith(pInfilePath, ".c2") || endsWith(pInfilePath, "#c20000")) + { + // It's a C2 file + + printf("Loading C2 File %s\n", pInfilePath); + + C2File c2data( pInfilePath ); + + int frameCount = c2data.GetFrameCount(); + + if (frameCount < 1) + { + // c2 file can't be valid, if there are no frames + printf("C2 File seems invalid.\n"); + helpText(); + } + + if (pOutfilePath) + { + const std::vector& c1Datas = c2data.GetPixelMaps(); + + printf("Saving %s with %d frames\n", pOutfilePath, (int)c1Datas.size()); + + GSLAFile anim(320,200, 0x8000); + + anim.AddImages(c1Datas); + + anim.SaveToFile(pOutfilePath); + + #if 1 + { + // Verify the conversion is good + // Load the file back in + GSLAFile verify(pOutfilePath); + + const std::vector &frames = verify.GetPixelMaps(); + + for (int idx = 0; idx < frames.size(); ++idx) + { + int result = memcmp(c1Datas[idx % c1Datas.size()], frames[idx], verify.GetFrameSize()); + printf("Verify Frame %d - %s\n", idx, result ? "Failed" : "Good"); + } + } + #endif + } + } + + } + else + { + helpText(); + } + + + return 0; +} + diff --git a/vcxproj/gsla.sln b/vcxproj/gsla.sln new file mode 100644 index 0000000..8656bea --- /dev/null +++ b/vcxproj/gsla.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30225.117 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gsla", "gsla.vcxproj", "{08214015-2B09-4ED2-ACD4-93031A939CF1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Debug|x64.ActiveCfg = Debug|x64 + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Debug|x64.Build.0 = Debug|x64 + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Debug|x86.ActiveCfg = Debug|Win32 + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Debug|x86.Build.0 = Debug|Win32 + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Release|x64.ActiveCfg = Release|x64 + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Release|x64.Build.0 = Release|x64 + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Release|x86.ActiveCfg = Release|Win32 + {08214015-2B09-4ED2-ACD4-93031A939CF1}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {922EAE98-7805-4172-A57D-9B441077B035} + EndGlobalSection +EndGlobal diff --git a/vcxproj/gsla.vcxproj b/vcxproj/gsla.vcxproj new file mode 100644 index 0000000..24ac719 --- /dev/null +++ b/vcxproj/gsla.vcxproj @@ -0,0 +1,156 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {08214015-2b09-4ed2-acd4-93031a939cf1} + gsla + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + + + false + + + true + + + false + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vcxproj/gsla.vcxproj.filters b/vcxproj/gsla.vcxproj.filters new file mode 100644 index 0000000..70b137e --- /dev/null +++ b/vcxproj/gsla.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + + + Header Files + + + Header Files + + + Header Files + + + Source Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file