Merge pull request #1 from dwsJason/develop

Develop
This commit is contained in:
Jason Andersen 2020-07-22 16:27:41 -04:00 committed by GitHub
commit 2714f80f6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2427 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*.user
*.vpj
*.vpw
*.vpwhist
*.vtg
vcxproj/.vs/
vcxproj/x64/

View File

@ -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

48
source/bctypes.h Normal file
View File

@ -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

143
source/c2_file.cpp Normal file
View File

@ -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 <stdio.h>
// 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<unsigned char> 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;
}
}
//------------------------------------------------------------------------------

73
source/c2_file.h Normal file
View File

@ -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 <vector>
#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<unsigned char*>& GetPixelMaps() { return m_pC1PixelMaps; }
private:
int m_widthPixels; // Width of image in pixels
int m_heightPixels; // Height of image in pixels
std::vector<unsigned char*> m_pC1PixelMaps;
};
#endif // C2_FILE_H

606
source/gsla_file.cpp Normal file
View File

@ -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, its 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 dont 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 frames 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 <stdio.h>
// 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<unsigned char> 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<unsigned char*>& 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<unsigned char> 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);
}
//------------------------------------------------------------------------------

150
source/gsla_file.h Normal file
View File

@ -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 <vector>
#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<unsigned char*>& 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<unsigned char*>& 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<unsigned char*> m_pC1PixelMaps;
};
#endif // C2_FILE_H

986
source/lzb.cpp Normal file
View File

@ -0,0 +1,986 @@
//
// LZB Encode / Decode
//
#include "lzb.h"
#include <stdio.h>
#include <string.h>
#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