From 212c9ef59c8aeed2fb61a920d0098c0c884994bf Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:37:04 +0200 Subject: [PATCH 1/6] Replace inline assembly with C++ No documentation update: tests/lcwcomp checks the C++ port against golden vectors recorded from the original assembly, so documented behavior is unchanged. --- code/blitblit.h | 176 --------------- code/lcw.cpp | 411 ++++++++++++++++------------------- code/mpu.cpp | 36 +-- code/rlerle.h | 256 ---------------------- code/xsurface.cpp | 26 +-- tests/CMakeLists.txt | 1 + tests/lcwcomp/CMakeLists.txt | 30 +++ tests/lcwcomp/lcwcomp.cpp | 143 ++++++++++++ tests/lcwcomp/lcwgolden.h | 131 +++++++++++ 9 files changed, 502 insertions(+), 708 deletions(-) create mode 100644 tests/lcwcomp/CMakeLists.txt create mode 100644 tests/lcwcomp/lcwcomp.cpp create mode 100644 tests/lcwcomp/lcwgolden.h diff --git a/code/blitblit.h b/code/blitblit.h index 4fdd1ea9..3042d3f4 100644 --- a/code/blitblit.h +++ b/code/blitblit.h @@ -2308,179 +2308,3 @@ class BlitTranslucentWriteAlpha : public Blitter { T const * TranslateTable; }; - -/* -** Assembly versions of some of the templated blitter object functions. Borland and -** Visual C++ support a compatible inline-assembly formats. However, Borland compiler -** does not allow inline-assembly to be part of an inline function -- go figure. -** It will still compile, it just generates warning messages. -*/ -#if defined(_MSC_VER) - -#pragma optimize("", off) - -template <> -inline void BlitTrans::BlitForward(void * dest, void const * source, int len, int z_min, void *z_buff, void *a_buff, int alpha_level, int warp_offset) const -{ - __asm { - mov esi,[source] - mov edi,[dest] - mov ecx,[len] - dec edi - inc ecx - - again: - dec ecx - jz fini - mov al,[esi] - inc edi - inc esi - test al,al - jz again - mov [edi],al - jmp again - - fini: - } -} - - -template <> -inline void BlitTransXlat::BlitForward(void * dest, void const * source, int len, int z_min, void *z_buff, void *a_buff, int alpha_level, int warp_offset) const -{ - unsigned short const * xlator = TranslateTable; - - __asm { - mov ebx,[xlator] - mov ecx,[len] - inc ecx - mov edi,[dest] - sub edi,2 - mov esi,[source] - xor eax,eax - - again: - dec ecx - jz over - add edi,2 - mov al,[esi] - inc esi - or al,al - jz again - mov dx,[ebx+eax*2] - mov [edi],dx - jmp again - - over: - } -} - - -template <> -inline void BlitTransRemapXlat::BlitForward(void * dest, void const * source, int len, int z_min, void *z_buff, void *a_buff, int alpha_level, int warp_offset) const -{ - unsigned short const * translator = TranslateTable; - unsigned char const * remapper = RemapTable; - - __asm { - mov ecx,[len] - mov edi,[dest] - sub edi,2 - mov esi,[source] - mov ebx,[remapper] - mov edx,[translator] - xor eax,eax - - again: - /* - ** This block is 11 cycles per pixel, if not transparent, and 5 - ** cycles per pixel, if transparent. - */ - dec ecx - jz over - add edi,2 - xor eax,eax - lodsb - or al,al - jz again - mov al,[ebx+eax] // First remap step (8 bit to 8 bit). - mov ax,[edx+eax*2] // Second remap step (8 bit to 16 bit). - mov [edi],ax - jmp again - - over: - } -} - - -#if 0 -template <> -inline void BlitTransZRemapXlat::BlitForward(void * dest, void const * source, int len, int z_min, void *z_buff, void *a_buff, int alpha_level, int warp_offset) const -{ - unsigned short const * translator = TranslateTable; - unsigned char const * remapper = *RemapTable; - - __asm { - mov ecx,[len] - mov edi,[dest] - sub edi,2 - mov esi,[source] - mov ebx,[remapper] - mov edx,[translator] - xor eax,eax - } - - /* - ** This block is 11 cycles per pixel, if not transparent, and 5 - ** cycles per pixel, if transparent. - */ -again: - __asm { - dec ecx - jz over - add edi,2 - xor eax,eax - lodsb - or al,al - jz again - mov al,[ebx+eax] // First remap step (8 bit to 8 bit). - mov ax,[edx+eax*2] // Second remap step (8 bit to 16 bit). - mov [edi],ax - jmp again - } -over:; -} - - -template <> -inline void BlitPlainXlat::BlitForward(void * dest, void const * source, int len, int z_min, void *z_buff, void *a_buff, int alpha_level, int warp_offset) const -{ - unsigned short const * remapper = TranslateTable; - __asm { - mov ebx,[remapper] - mov ecx,[len] - mov esi,[source] - mov edi,[dest] - sub edi,2 - } -again: - /* - ** This block processes pixels at 7 clocks per pixel. - */ - __asm { - xor eax,eax - add edi,2 - mov al,[esi] - inc esi - mov ax,[ebx+eax*2] - mov [edi],ax - dec ecx - jnz again - } -} - - -#endif -#endif - -#pragma optimize("", on) diff --git a/code/lcw.cpp b/code/lcw.cpp index 82f5b5c5..24e2cd2e 100644 --- a/code/lcw.cpp +++ b/code/lcw.cpp @@ -188,9 +188,6 @@ uint32_t LCW_Uncomp(void const * source, void * dest, unsigned long length) } -#if defined(_MSC_VER) - - /*********************************************************************************************** * LCW_Comp -- Performes LCW compression on a block of data. * * * @@ -214,232 +211,192 @@ uint32_t LCW_Uncomp(void const * source, void * dest, unsigned long length) * HISTORY: * * 05/20/1997 JLB : Created. * *=============================================================================================*/ -/// Renegade guards this body with #ifdef _WINDOWS, which would force retval ahead of the -/// other locals; that shape is not reproduced here. -/*ARGSUSED*/ + +/* + * Compressed blocks reach save games, so this emits the same bytes the assembly it replaced + * emitted, quirks included. Two of those quirks are worth knowing about before changing + * anything here. + * + * A datasize of 1 does not produce a one byte block. The first source byte is written before + * the loop is entered and the end is only tested after a byte has been consumed, so a second + * byte is read past the end of the source and encoded alongside it. The block that comes out + * expands to two bytes rather than one. + * + * The search for a run reads sixty four bytes ahead of the current position without checking + * that they belong to the source at all, so it can read past the end of the buffer. Only the + * comparison result is used, and the run length that follows is measured properly. + */ int LCW_Comp(void const * source, void * dest, int datasize) { - int inlen = 0; - int a1stdest = 0; - int a1stsrc = 0; - int lenoff = 0; - int ndest = 0; - int count = 0; - int matchoff = 0; - int end_of_data = 0; - int retval = 0; -#ifdef _DEBUG - inlen = inlen; - a1stdest = a1stdest; - a1stsrc = a1stsrc; - lenoff = lenoff; - ndest = ndest; - count = count; - matchoff = matchoff; - end_of_data = end_of_data; -#endif - - __asm { - cld // make sure all string commands are forward - mov edi,[dest] - mov esi,[source] - mov edx,[datasize] // get length of data to compress - -// compress data to the following codes in the format b = byte, w = word -// n = byte code pulled from compressed data -// Bit field of n command description -// n=0xxxyyyy,yyyyyyyy short run back y bytes and run x+3 -// n=10xxxxxx,n1,n2,...,nx+1 med length copy the next x+1 bytes -// n=11xxxxxx,w1 med run run x+3 bytes from offset w1 -// n=11111111,w1,w2 long run run w1 bytes from offset w2 -// n=10000000 end end of data reached - - mov ebx,esi - add ebx,edx - mov [end_of_data],ebx - mov [inlen],1 //; set the in-length flag - mov [a1stdest],edi //; save original dest offset for size calc - mov [a1stsrc],esi //; save offset of first byte of data - mov [lenoff],edi //; save the offset of the legth of this len - sub eax,eax - mov al,081h //; the first byte is always a len - stosb //; write out a len of 1 - lodsb //; get the byte - stosb //; save it - - loopstart: - mov [ndest],edi //; save offset of compressed data - mov edi,[a1stsrc] //; get the offset to the first byte of data - mov [count],1 //; set the count of run to 0 - - searchloop: - sub eax,eax - mov al,[esi] //; get the current byte of data - cmp al,[esi+64] - jne short notrunlength - - mov ebx,edi - - mov edi,esi - mov ecx,[end_of_data] - sub ecx,edi - repe scasb - dec edi - mov ecx,edi - sub ecx,esi - cmp ecx,65 - jb short notlongenough - - mov [inlen],0 //; clear the in-length flag -// mov [DWORD PTR inlen],0 //; clear the in-length flag - mov esi,edi - mov edi,[ndest] //; get the offset of our compressed data - - mov ah,al - mov al,0FEh - stosb - xchg ecx,eax - stosw - mov al,ch - stosb - - mov [ndest],edi //; save offset of compressed data - mov edi,ebx - jmp searchloop - - notlongenough: - mov edi,ebx - - notrunlength: - oploop: - mov ecx,esi //; get the address of the last byte +1 - sub ecx,edi //; get the total number of bytes left to comp - jz short searchdone - - repne scasb //; look for a match - jne short searchdone //; if we don't find one we're done - - mov ebx,[count] - mov ah,[esi+ebx-1] - cmp ah,[edi+ebx-2] - - jne oploop - - mov edx,esi //; save this spot for the next search - mov ebx,edi //; save this spot for the length calc - dec edi //; back up one for compare - mov ecx,[end_of_data] //; get the end of data - sub ecx,esi //; sub current source for max len - - repe cmpsb //; see how many bytes match - - jne short notend //; if found mismatch then di - bx = match count - - inc edi //; else cx = 0 and di + 1 - bx = match count - - notend: - mov esi,edx //; restore si - mov eax,edi //; get the dest - sub eax,ebx //; sub the start for total bytes that match - mov edi,ebx //; restore dest - cmp eax,[count] //; see if its better than before - jb searchloop //; if not keep looking - - mov [count],eax //; if so keep the count - dec ebx //; back it up for the actual match offset - mov [matchoff],ebx //; save the offset for later - jmp searchloop //; loop until we searched it all - - searchdone: - mov ecx,[count] //; get the count of the longest run - mov edi,[ndest] //; get the offset of our compressed data - cmp ecx,2 //; see if its not enough run to matter - jbe short lenin //; if its 0,1, or 2 its too small - - cmp ecx,10 //; if not, see if it would fit in a short - ja short medrun //; if not, see if its a medium run - - mov eax,esi //; if its short get the current address - sub eax,[matchoff] //; sub the offset of the match - cmp eax,0FFFh //; if its less than 12 bits its a short - ja short medrun //; if its not, its a medium - - //shortrun: - sub ebx,ebx - mov bl,cl //; get the length (3-10) - sub bl,3 //; sub 3 for a 3 bit number 0-7 - shl bl,4 //; shift it left 4 - add ah,bl //; add in the length for the high nibble - xchg ah,al //; reverse the bytes for a word store - jmp short srunnxt //; do the run fixup code - - medrun: - cmp ecx,64 //; see if its a short run - ja short longrun //; if not, oh well at least its long - - sub cl,3 //; back down 3 to keep it in 6 bits - or cl,0C0h //; the highest bits are always on - mov al,cl //; put it in al for the stosb - stosb //; store it - jmp short medrunnxt //; do the run fixup code - - lenin: - cmp [inlen],0 //; is it doing a length? -// cmp [DWORD PTR inlen],0 //; is it doing a length? - jnz short len //; if so, skip code - - lenin1: - mov [lenoff],edi //; save the length code offset - mov al,80h //; set the length to 0 - stosb //; save it - - len: - mov ebx,[lenoff] //; get the offset of the length code - cmp byte ptr [ebx],0BFh //; see if its maxed out -// cmp [BYTE PTR ebx],0BFh //; see if its maxed out - je lenin1 //; if so put out a new len code - - //stolen: - inc byte ptr [ebx] //; inc the count code -// inc [BYTE PTR ebx] //; inc the count code - lodsb //; get the byte - stosb //; store it - mov [inlen],1 //; we are now in a length so save it -// mov [DWORD PTR inlen],1 //; we are now in a length so save it - jmp short nxt //; do the next code - - longrun: - mov al,0ffh //; its a long so set a code of FF - stosb //; store it - - mov eax,[count] //; send out the count - stosw //; store it - - medrunnxt: - mov eax,[matchoff] //; get the offset - sub eax,[a1stsrc] //; make it relative tot he start of data - - srunnxt: - stosw //; store it - //; this code common to all runs - add esi,[count] //; add in the length of the run to the source - mov [inlen],0 //; set the in leght flag to false -// mov [DWORD PTR inlen],0 //; set the in leght flag to false - - nxt: - cmp esi,[end_of_data] //; see if we did the whole pic - jae short outofhere //; if so, cool! were done - - jmp loopstart - - outofhere: - mov ax,080h //; remember to send an end of data code - stosb //; store it - mov eax,edi //; get the last compressed address - sub eax,[a1stdest] //; sub the first for the compressed size - mov [retval],eax + unsigned char const * const start = (unsigned char const *)source; + unsigned char * const first = (unsigned char *)dest; + unsigned char const * const end_of_data = start + datasize; + + unsigned char const * si = start; + unsigned char * di = first; + + /* + * The first command is always a run of literals, opened here and extended in place as + * more of them are emitted. + */ + bool inlen = true; + unsigned char * lenoff = di; + + *di++ = 0x81; + *di++ = *si++; + + while (true) { + unsigned char * ndest = di; + unsigned char const * search = start; + unsigned char const * matchoff = start; + int count = 1; + + /* + * Find the longest run of earlier data that repeats at the current position. A + * single byte repeated far enough is worth a command of its own and is emitted + * straight away, without disturbing the search. + */ + while (true) { + unsigned char const value = *si; + + if (value == si[64]) { + long const left = (long)(end_of_data - si); + long matched = 0; + + while (matched < left && si[matched] == value) { + matched++; + } + + /* + * A run that reaches the end of the source is counted one short, + * because the scan it replaces stepped past the last byte it read. + * With nothing left at all that count goes negative, and the test + * below is unsigned, so it reads as enormous and a run is emitted + * from a position that has already passed the end. That only arises + * on the malformed tail described above the function, and it is kept + * because the bytes it produces are the bytes callers have. + */ + long const runlength = (matched < left) ? matched : (left - 1); + + if ((unsigned long)runlength >= 65) { + inlen = false; + si += runlength; + di = ndest; + + *di++ = 0xFE; + *di++ = (unsigned char)(runlength & 0xFF); + *di++ = (unsigned char)((runlength >> 8) & 0xFF); + *di++ = value; + + ndest = di; + continue; + } + } + + long const window = (long)(si - search); + + if (window <= 0) { + break; + } + + /* + * Look for somewhere earlier the current byte appears. + */ + unsigned char const * found = NULL; + + for (long i = 0; i < window; i++) { + unsigned char const candidate = *search++; + + if (candidate == value) { + found = search; + break; + } + } + + if (found == NULL) { + break; + } + + /* + * Reject the candidate cheaply before measuring it: if the byte that would + * end a run at least as long as the best so far does not agree, it cannot + * beat it. + */ + if (si[count - 1] != search[count - 2]) { + continue; + } + + long const room = (long)(end_of_data - si); + long length = 0; + + while (length < room && si[length] == (search - 1)[length]) { + length++; + } + + if (length < count) { + continue; + } + + count = (int)length; + matchoff = search - 1; + } + + di = ndest; + + if (count > 2) { + unsigned long const back = (unsigned long)(si - matchoff); + + if (count <= 10 && back <= 0x0FFF) { + + /* + * Short run: three bits of length and twelve of distance, packed into + * two bytes. + */ + *di++ = (unsigned char)((((unsigned long)(count - 3)) << 4) | ((back >> 8) & 0x0F)); + *di++ = (unsigned char)(back & 0xFF); + } else { + if (count <= 64) { + *di++ = (unsigned char)(0xC0 | (count - 3)); + } else { + *di++ = 0xFF; + *di++ = (unsigned char)(count & 0xFF); + *di++ = (unsigned char)((count >> 8) & 0xFF); + } + + /* + * The longer forms carry the match's position from the start of the + * data rather than its distance back from here. + */ + unsigned long const offset = (unsigned long)(matchoff - start); + + *di++ = (unsigned char)(offset & 0xFF); + *di++ = (unsigned char)((offset >> 8) & 0xFF); + } + + si += count; + inlen = false; + } else { + + /* + * Nothing worth referencing, so the byte goes out as a literal. A length + * command counts up to 0x3F bytes before another has to be opened. + */ + if (!inlen || *lenoff == 0xBF) { + lenoff = di; + *di++ = 0x80; + } + + (*lenoff)++; + *di++ = *si++; + inlen = true; + } + + if (si >= end_of_data) { + break; + } } - return(retval); + *di++ = 0x80; + + return((int)(di - first)); } -#endif diff --git a/code/mpu.cpp b/code/mpu.cpp index 6c562329..3d3689b8 100644 --- a/code/mpu.cpp +++ b/code/mpu.cpp @@ -36,6 +36,7 @@ #include "win.h" +#include #include typedef union { @@ -81,23 +82,6 @@ unsigned int Get_CPU_Rate(unsigned int & high) } -/// -/// Fetches the processor's time stamp counter, which increments every clock tick. The value -/// is 64 bits wide; the low half is returned and the high half stored through the reference. -/// RDTSC is available on every processor the supported minimum hardware covers (SSE2, so a -/// Pentium 4 or Athlon 64 onward). -/// -/// Receives the high half of the 64 bit clock value. -/// unsigned int; the low half of the clock value. -unsigned int Get_CPU_Clock(unsigned int & high) -{ - unsigned long long const stamp = __rdtsc(); - - high = (unsigned int)(stamp >> 32); - return((unsigned int)stamp); -} - - /* * Based on code released by Intel * http://db.zmitac.aei.polsl.pl/Electronics_Firm_Docs/PENTIUMIII/pentium/cpuinfo.zip @@ -114,8 +98,6 @@ unsigned int Get_CPU_Clock(unsigned int & high) ** */ -#define ASM_RDTSC _asm _emit 0x0f _asm _emit 0x31 - // Max # of samplings to allow before giving up and returning current average. #define MAX_TRIES 20 #define ROUND_THRESHOLD 6 @@ -134,12 +116,10 @@ static unsigned long TSC_High; /// Only call this routine on a processor that supports the RDTSC opcode. void RDTSC(void) { - _asm - { - ASM_RDTSC; - mov TSC_Low, eax - mov TSC_High, edx - } + unsigned long long const stamp = __rdtsc(); + + TSC_Low = (unsigned long)(stamp & 0xFFFFFFFF); + TSC_High = (unsigned long)(stamp >> 32); } @@ -216,8 +196,7 @@ int Get_RDTSC_CPU_Speed(void) QueryPerformanceCounter(&t1); } - ASM_RDTSC; - _asm mov stamp0, EAX + stamp0 = (unsigned long)(__rdtsc() & 0xFFFFFFFF); t0.LowPart = t1.LowPart; // Reset Initial Time t0.HighPart = t1.HighPart; @@ -230,8 +209,7 @@ int Get_RDTSC_CPU_Speed(void) QueryPerformanceCounter(&t1); } - ASM_RDTSC; - _asm mov stamp1, EAX + stamp1 = (unsigned long)(__rdtsc() & 0xFFFFFFFF); cycles = stamp1 - stamp0; // # of cycles passed between reads diff --git a/code/rlerle.h b/code/rlerle.h index 1b2aaacb..59a37f2e 100644 --- a/code/rlerle.h +++ b/code/rlerle.h @@ -2769,259 +2769,3 @@ class RLEBlitTransLucent75ZReadWrite : public RLEBlitter T Mask; }; - -#if 0 - -#if defined(_MSC_VER) -void RLEBlitTransZRemapXlat::Blit(void * dest, void const * source, int len, int leadskip) const -{ - unsigned char const * remapper = *RemapTable; - unsigned short const * transtable = TranslateTable; - - /* - ** Set up the working registers for the blit operation. - */ - __asm { - mov ecx,[len] - mov edi,[dest] - mov esi,[source] - mov ebx,[remapper] - mov edx,[leadskip] - xor eax,eax - } - - /* - ** Skip leading pixels by analyzing the RLE data until the entire - ** requested skip pixel count has been processed. This could result in - ** unprocessed transparent pixels if it ended up in the middle of - ** a transparent pixel run. This is handled in the next block. - */ -moreskip: - __asm { - test edx,edx - jle nomoreskip - dec edx - lodsb - test al,al - jnz moreskip - lodsb - sub edx,eax - inc edx - jmp moreskip - } -nomoreskip: - - /* - ** Handle any left over transparent pixels that would be part of - ** a transparent pixel run that occurs at the end of the leading - ** pixel skip process. - */ - __asm { - neg edx - sub ecx,edx // Account for any left over transparent pixels - lea edi,[edi+edx*2] - mov edx,[transtable] - } - - /* - ** Output the pixel data to the destination. - */ -moredata: - __asm { - xor eax,eax - or ecx,ecx - jle fini - lodsb - test al,al - jz transparent - mov al,[ebx+eax] - mov ax,[edx+eax*2] - dec ecx - stosw - jmp moredata - } - - /* - ** A transparent pixel run just causes the destination pointer - ** and length count to be adjusted by the length of the run. - */ -transparent: - __asm { - lodsb - lea edi,[edi+eax*2] - sub ecx,eax - jmp moredata - } - -fini:; -} - - -void RLEBlitTransRemapXlat::Blit(void * dest, void const * source, int len, int leadskip) const -{ - unsigned char const * remapper = RemapTable; - unsigned short const * transtable = TranslateTable; - - /* - ** Set up the working registers for the blit operation. - */ - __asm { - mov ecx,[len] - mov edi,[dest] - mov esi,[source] - mov ebx,[remapper] - mov edx,[leadskip] - xor eax,eax - } - - /* - ** Skip leading pixels by analyzing the RLE data until the entire - ** requested skip pixel count has been processed. This could result in - ** unprocessed transparent pixels if it ended up in the middle of - ** a transparent pixel run. This is handled in the next block. - */ -moreskip: - __asm { - test edx,edx - jle nomoreskip - dec edx - lodsb - test al,al - jnz moreskip - lodsb - sub edx,eax - inc edx - jmp moreskip - } -nomoreskip: - - /* - ** Handle any left over transparent pixels that would be part of - ** a transparent pixel run that occurs at the end of the leading - ** pixel skip process. - */ - __asm { - neg edx - sub ecx,edx // Account for any left over transparent pixels - lea edi,[edi+edx*2] - mov edx,[transtable] - } - - /* - ** Output the pixel data to the destination. - */ -moredata: - __asm { - xor eax,eax - or ecx,ecx - jle fini - lodsb - test al,al - jz transparent - mov al,[ebx+eax] - mov ax,[edx+eax*2] - dec ecx - stosw - jmp moredata - } - - /* - ** A transparent pixel run just causes the destination pointer - ** and length count to be adjusted by the length of the run. - */ -transparent: - __asm { - lodsb - lea edi,[edi+eax*2] - sub ecx,eax - jmp moredata - } - -fini:; -} - - -void RLEBlitTransXlat::Blit(void * dest, void const * source, int len, int leadskip) const -{ - unsigned short const * transtable = TranslateTable; - - /* - ** Set up the working registers for the blit operation. - */ - __asm { - mov ecx,[len] - mov edi,[dest] - mov esi,[source] - mov ebx,[transtable] - mov edx,[leadskip] - xor eax,eax - } - - /* - ** Skip leading pixels by analyzing the RLE data until the entire - ** requested skip pixel count has been processed. This could result in - ** unprocessed transparent pixels if it ended up in the middle of - ** a transparent pixel run. This is handled in the next block. - */ -moreskip: - __asm { - test edx,edx - jle nomoreskip - dec edx - lodsb - test al,al - jnz moreskip - lodsb - sub edx,eax - inc edx - jmp moreskip - } -nomoreskip: - - /* - ** Handle any left over transparent pixels that would be part of - ** a transparent pixel run that occurs at the end of the leading - ** pixel skip process. - */ - __asm { - neg edx - sub ecx,edx // Account for any left over transparent pixels - lea edi,[edi+edx*2] - } - - /* - ** Output the pixel data to the destination. - */ -moredata: - __asm { - xor eax,eax - or ecx,ecx - jle fini - lodsb - test al,al - jz transparent - mov ax,[ebx+eax*2] - dec ecx - stosw - jmp moredata - } - - /* - ** A transparent pixel run just causes the destination pointer - ** and length count to be adjusted by the length of the run. - */ -transparent: - __asm { - lodsb - lea edi,[edi+eax*2] - sub ecx,eax - jmp moredata - } - -fini:; -} - -#endif - - -#endif diff --git a/code/xsurface.cpp b/code/xsurface.cpp index 9c834b81..b5d28b7f 100644 --- a/code/xsurface.cpp +++ b/code/xsurface.cpp @@ -790,9 +790,6 @@ bool XSurface::Fill_Rect(Rect const & fillrect, int color) } -/// inline asm functions don't have a return statement -#pragma warning(disable: 4035) - /// /// Fills a run of longwords with a color value. /// This is the low level filler that the surface rectangle fill routines use for the bulk @@ -802,27 +799,16 @@ bool XSurface::Fill_Rect(Rect const & fillrect, int color) /// The longword value to store. A 16 bit color must be doubled into /// both halves of it. /// Returns with a pointer to just past the last longword written. -static void *surface_quick_fill(void *buf, int count, int color) +static inline void *surface_quick_fill(void *buf, int count, int color) { - _asm { - push edi /// Bug fixed in TS but not in ShapeSet - - mov ecx, [count] - mov edi, [buf] - - cmp ecx, 0 - jle short fill_end + unsigned int * ptr = (unsigned int *)buf; - mov eax, [color] - rep stosd - - fill_end: - mov eax, edi - pop edi /// Bug fixed in TS but not in ShapeSet + for (int index = 0; index < count; index++) { + *ptr++ = (unsigned int)color; } - // return is in eax + + return(ptr); } -#pragma warning(default: 4035) /*********************************************************************************************** diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 547b0d61..ae15ff35 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,3 +4,4 @@ add_subdirectory(logstress) add_subdirectory(netpacket) add_subdirectory(sosparity) add_subdirectory(spawner) +add_subdirectory(lcwcomp) diff --git a/tests/lcwcomp/CMakeLists.txt b/tests/lcwcomp/CMakeLists.txt new file mode 100644 index 00000000..3190a1c3 --- /dev/null +++ b/tests/lcwcomp/CMakeLists.txt @@ -0,0 +1,30 @@ +# The compressor is compiled straight into the harness. It lives outside code/ so that the +# recursive glob building OpenTS cannot pick this target's entry point up. +add_executable(LcwComp + "${CMAKE_CURRENT_SOURCE_DIR}/lcwcomp.cpp" + "${CMAKE_SOURCE_DIR}/code/lcw.cpp" +) + +target_compile_features(LcwComp PRIVATE cxx_std_20) + +target_include_directories(LcwComp PRIVATE + "${CMAKE_SOURCE_DIR}/code" + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +target_compile_definitions(LcwComp PRIVATE WIN32 _WINDOWS _MBCS) + +# The engine builds this source with SSE2 and precise floating point. The harness matches that +# so a result here carries over to the engine. +target_compile_options(LcwComp PRIVATE + $<$:/MTd /EHsc /Zc:__cplusplus /arch:SSE2 /fp:precise> + $<$:/MT /EHsc /Zc:__cplusplus /arch:SSE2 /fp:precise> +) + +target_link_libraries(LcwComp PRIVATE kernel32 user32 shell32) + +set_target_properties(LcwComp PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +add_test(NAME lcwcomp COMMAND LcwComp) diff --git a/tests/lcwcomp/lcwcomp.cpp b/tests/lcwcomp/lcwcomp.cpp new file mode 100644 index 00000000..0953615c --- /dev/null +++ b/tests/lcwcomp/lcwcomp.cpp @@ -0,0 +1,143 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Holds LCW_Comp in lcw.cpp to the output the inline assembly it replaced produced. The +// vectors in lcwgolden.h were recorded from that assembly before it was removed. +// +// Compressed blocks are written into save games, so the contract is the exact bytes emitted, +// not merely that they expand again correctly. Both are checked here. Needs no game data. + +#include +#include + +#include "lcw.h" + +#include "lcwgolden.h" + +namespace { + +int const SRCMAX = 65536; +int const DSTMAX = 262144; + +unsigned char Source[SRCMAX]; +unsigned char Dest[DSTMAX]; +unsigned char Roundtrip[SRCMAX * 2]; + +unsigned int Seed = 0; + +int Failures = 0; +int Checked = 0; + + +unsigned int Next_Random(void) +{ + Seed = Seed * 1103515245u + 12345u; + return(Seed >> 8); +} + + +unsigned long long Hash(unsigned char const * data, int size) +{ + unsigned long long hash = 1469598103934665603ULL; + for (int i = 0; i < size; i++) { + hash ^= (unsigned long long)data[i]; + hash *= 1099511628211ULL; + } + return(hash); +} + + +// Must reproduce the generator's inputs exactly or every vector misses. +void Fill_Source(int shape, unsigned int seed, int size) +{ + Seed = seed; + + for (int i = 0; i < size; i++) { + switch (shape) { + case 0: + Source[i] = (unsigned char)(Next_Random() & 0xFF); + break; + + case 1: + Source[i] = 0x7E; + break; + + case 2: + Source[i] = (unsigned char)((i / 17) & 0xFF); + break; + + case 3: + Source[i] = (unsigned char)((Next_Random() % 4) == 0 ? (Next_Random() & 0xFF) : 0x20); + break; + + case 4: + Source[i] = (unsigned char)("OpenTS voxel terrain"[i % 20]); + break; + + default: + Source[i] = (unsigned char)((Next_Random() & 0x03) * 0x40); + break; + } + } +} + +} // namespace + + +int main(void) +{ + for (int i = 0; i < LcwCompGoldenCaseCount; i++) { + LcwCompGoldenCase const & test = LcwCompGoldenCases[i]; + + Fill_Source(test.Shape, test.Seed, test.Size); + std::memset(Dest, 0xA5, sizeof(Dest)); + + int const packed = LCW_Comp(Source, Dest, test.Size); + + if (packed != test.Compressed) { + std::printf("FAILED shape %d size %d: compressed to %d bytes, expected %d\n", + test.Shape, test.Size, packed, test.Compressed); + Failures++; + } else if (Hash(Dest, packed) != test.Hash) { + std::printf("FAILED shape %d size %d: compressed bytes differ from the assembly\n", + test.Shape, test.Size); + Failures++; + } + + /* + * A block should expand to what went in. The one byte case is the exception: the + * encoder reads a byte past the source and emits both, so it comes back as two. + * That is the assembly's behaviour, recorded rather than corrected, and it is + * asserted here so that changing it cannot pass unnoticed. + */ + std::memset(Roundtrip, 0, sizeof(Roundtrip)); + int const unpacked = LCW_Uncomp(Dest, Roundtrip, (unsigned long)test.Size); + + if (test.Size == 1) { + if (unpacked == 1) { + std::printf("NOTE shape %d size 1 now round trips to one byte; the known " + "one byte defect appears to be fixed, so update these vectors\n", test.Shape); + Failures++; + } + } else { + if (unpacked != test.Size || std::memcmp(Roundtrip, Source, test.Size) != 0) { + std::printf("FAILED shape %d size %d: round trip returned %d bytes\n", + test.Shape, test.Size, unpacked); + Failures++; + } + } + + Checked++; + } + + std::printf("%-52s %s\n", "LCW compression matches the recorded assembly", Failures == 0 ? "ok" : "FAILED"); + std::printf("checked %d cases, %d mismatches\n", Checked, Failures); + + return(Failures == 0 ? 0 : 1); +} diff --git a/tests/lcwcomp/lcwgolden.h b/tests/lcwcomp/lcwgolden.h new file mode 100644 index 00000000..1939fd62 --- /dev/null +++ b/tests/lcwcomp/lcwgolden.h @@ -0,0 +1,131 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Generated from LCW_Comp while it was still inline assembly, and kept so the C++ that +// replaced it can be held to the same output. LCW blocks are written into save games, so +// what matters is the exact bytes emitted, not just that they decompress correctly. +// +// Generated file. Do not hand-edit. + +#pragma once + +struct LcwCompGoldenCase { + int Shape; + unsigned int Seed; + int Size; + int Compressed; + unsigned long long Hash; +}; + +static LcwCompGoldenCase const LcwCompGoldenCases[] = { + {0, 13069u, 1, 9, 1807977557902324117ULL}, + {0, 20988u, 2, 4, 13064096319865035231ULL}, + {0, 28907u, 3, 5, 5915942401490830024ULL}, + {0, 36826u, 7, 9, 8846502117472493464ULL}, + {0, 44745u, 15, 17, 11970627509807455946ULL}, + {0, 52664u, 16, 18, 17546354395159622834ULL}, + {0, 60583u, 17, 19, 18405239772726857934ULL}, + {0, 68502u, 63, 65, 83793028833142472ULL}, + {0, 76421u, 64, 67, 16421560953345050994ULL}, + {0, 84340u, 100, 103, 1776426412059678858ULL}, + {0, 92259u, 255, 261, 14004760160580516757ULL}, + {0, 100178u, 256, 262, 5922497657109482904ULL}, + {0, 108097u, 257, 263, 3500236831736331464ULL}, + {0, 116016u, 1000, 1017, 16469799334716043436ULL}, + {0, 123935u, 4096, 4163, 13679796812110556091ULL}, + {0, 131854u, 16384, 16646, 1129566810827092307ULL}, + {0, 139773u, 65535, 66577, 4701270319259758933ULL}, + {1, 147692u, 1, 4, 15387975727855557662ULL}, + {1, 155611u, 2, 4, 15321972044827107857ULL}, + {1, 163530u, 3, 5, 3752577258656994018ULL}, + {1, 171449u, 7, 5, 17415218352048637521ULL}, + {1, 179368u, 15, 6, 4655718527127227187ULL}, + {1, 187287u, 16, 6, 16183152400164689092ULL}, + {1, 195206u, 17, 6, 9264405149446243413ULL}, + {1, 203125u, 63, 6, 4583733020526306915ULL}, + {1, 211044u, 64, 6, 16111166893563768820ULL}, + {1, 218963u, 100, 9, 8148909191676844567ULL}, + {1, 226882u, 255, 9, 5423557805361288032ULL}, + {1, 234801u, 256, 9, 4987818062494100691ULL}, + {1, 242720u, 257, 9, 7610984057582977262ULL}, + {1, 250639u, 1000, 9, 354769122027670662ULL}, + {1, 258558u, 4096, 9, 10175650356802707306ULL}, + {1, 266477u, 16384, 9, 9740807493131001306ULL}, + {1, 274396u, 65535, 9, 13087234031888934413ULL}, + {2, 282315u, 1, 9, 11843835814251834895ULL}, + {2, 290234u, 2, 4, 17584421726007563793ULL}, + {2, 298153u, 3, 5, 17791319723388371584ULL}, + {2, 306072u, 7, 5, 11160011530942588191ULL}, + {2, 313991u, 15, 6, 10975763781217639721ULL}, + {2, 321910u, 16, 6, 13273826825183582462ULL}, + {2, 329829u, 17, 6, 6355079574465136783ULL}, + {2, 337748u, 63, 21, 2896898578083667180ULL}, + {2, 345667u, 64, 21, 18140551947415754877ULL}, + {2, 353586u, 100, 31, 12191546920540359407ULL}, + {2, 361505u, 255, 76, 16147238993265830839ULL}, + {2, 369424u, 256, 78, 15820006003885581123ULL}, + {2, 377343u, 257, 79, 8805755096697609473ULL}, + {2, 385262u, 1000, 296, 5946853262984892082ULL}, + {2, 393181u, 4096, 1206, 17330344708194719310ULL}, + {2, 401100u, 16384, 1286, 2821454675195587043ULL}, + {2, 409019u, 65535, 1286, 18314874132263956815ULL}, + {3, 416938u, 1, 4, 18141130856368503077ULL}, + {3, 424857u, 2, 4, 16088487775278736849ULL}, + {3, 432776u, 3, 5, 732249865529444488ULL}, + {3, 440695u, 7, 9, 14687256910119648873ULL}, + {3, 448614u, 15, 16, 13038074262944168672ULL}, + {3, 456533u, 16, 15, 3498373380776672657ULL}, + {3, 464452u, 17, 17, 9285944785277013776ULL}, + {3, 472371u, 63, 44, 5643239704878074118ULL}, + {3, 480290u, 64, 48, 3653615617911823110ULL}, + {3, 488209u, 100, 63, 16930939309045484678ULL}, + {3, 496128u, 255, 200, 6295686415325353141ULL}, + {3, 504047u, 256, 172, 4654478162131913837ULL}, + {3, 511966u, 257, 156, 6505142463220528943ULL}, + {3, 519885u, 1000, 642, 8918849814412059247ULL}, + {3, 527804u, 4096, 2356, 7006499902778769132ULL}, + {3, 535723u, 16384, 8800, 13425338241632085147ULL}, + {3, 543642u, 65535, 27705, 6650711024580611343ULL}, + {4, 551561u, 1, 9, 15861568950810363717ULL}, + {4, 559480u, 2, 4, 7823558933484496740ULL}, + {4, 567399u, 3, 5, 16094879465814968578ULL}, + {4, 575318u, 7, 9, 9942793656368200329ULL}, + {4, 583237u, 15, 17, 6415321729831142520ULL}, + {4, 591156u, 16, 18, 17212623010939686725ULL}, + {4, 599075u, 17, 19, 7108732882802460298ULL}, + {4, 606994u, 63, 25, 521383511426542305ULL}, + {4, 614913u, 64, 25, 7440412237121810000ULL}, + {4, 622832u, 100, 27, 15360569510178639014ULL}, + {4, 630751u, 255, 27, 13436011570904534443ULL}, + {4, 638670u, 256, 27, 18005880776047744154ULL}, + {4, 646589u, 257, 27, 473054248371470077ULL}, + {4, 654508u, 1000, 27, 13789505796299680545ULL}, + {4, 662427u, 4096, 27, 15707536257104979397ULL}, + {4, 670346u, 16384, 27, 15779521763705899669ULL}, + {4, 678265u, 65535, 27, 6888907962647354820ULL}, + {5, 686184u, 1, 4, 17630618806569394369ULL}, + {5, 694103u, 2, 4, 11417304975729043793ULL}, + {5, 702022u, 3, 5, 2214001936243117696ULL}, + {5, 709941u, 7, 9, 8907682379062132876ULL}, + {5, 717860u, 15, 15, 4399000346022804956ULL}, + {5, 725779u, 16, 17, 5304175213314670790ULL}, + {5, 733698u, 17, 18, 14683049785485168693ULL}, + {5, 741617u, 63, 56, 15464930974243779964ULL}, + {5, 749536u, 64, 59, 15343719983771361160ULL}, + {5, 757455u, 100, 83, 8663466514268172878ULL}, + {5, 765374u, 255, 176, 18242259449379974659ULL}, + {5, 773293u, 256, 178, 3039109853407777327ULL}, + {5, 781212u, 257, 176, 2516024155533253180ULL}, + {5, 789131u, 1000, 528, 2320462317124866530ULL}, + {5, 797050u, 4096, 541, 11901963684404172949ULL}, + {5, 804969u, 16384, 541, 9981346815892918614ULL}, + {5, 812888u, 65535, 541, 10814168890895549274ULL}, +}; + +static int const LcwCompGoldenCaseCount = 102; From 8ec5c9bc5d2a269463d99a9d849041806932de92 Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:40:31 +0200 Subject: [PATCH 2/6] Restore Get_CPU_Clock to the intrin version --- code/mpu.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/code/mpu.cpp b/code/mpu.cpp index 3d3689b8..316273cb 100644 --- a/code/mpu.cpp +++ b/code/mpu.cpp @@ -82,6 +82,23 @@ unsigned int Get_CPU_Rate(unsigned int & high) } +/// +/// Fetches the processor's time stamp counter, which increments every clock tick. The value +/// is 64 bits wide; the low half is returned and the high half stored through the reference. +/// RDTSC is available on every processor the supported minimum hardware covers (SSE2, so a +/// Pentium 4 or Athlon 64 onward). +/// +/// Receives the high half of the 64 bit clock value. +/// unsigned int; the low half of the clock value. +unsigned int Get_CPU_Clock(unsigned int & high) +{ + unsigned long long const stamp = __rdtsc(); + + high = (unsigned int)(stamp >> 32); + return((unsigned int)stamp); +} + + /* * Based on code released by Intel * http://db.zmitac.aei.polsl.pl/Electronics_Firm_Docs/PENTIUMIII/pentium/cpuinfo.zip From 7385869aaa5917144f6f156c9946840ab2480a75 Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:40:56 +0200 Subject: [PATCH 3/6] Use the Vanilla-Conquer version of LCW_Comp --- code/lcw.cpp | 315 +++++++++++++++++++++------------------------------ code/lcw.h | 2 +- 2 files changed, 129 insertions(+), 188 deletions(-) diff --git a/code/lcw.cpp b/code/lcw.cpp index 24e2cd2e..21a755ed 100644 --- a/code/lcw.cpp +++ b/code/lcw.cpp @@ -3,6 +3,7 @@ ******************************************************************************* * SPDX-License-Identifier: GPL-3.0-or-later * Copyright 2025 Electronic Arts Inc. + * Copyright 2026 Vanilla-Conquer contributors * Copyright 2026 OpenTS contributors * * Contains material derived from Electronic Arts source code. @@ -212,191 +213,131 @@ uint32_t LCW_Uncomp(void const * source, void * dest, unsigned long length) * 05/20/1997 JLB : Created. * *=============================================================================================*/ -/* - * Compressed blocks reach save games, so this emits the same bytes the assembly it replaced - * emitted, quirks included. Two of those quirks are worth knowing about before changing - * anything here. - * - * A datasize of 1 does not produce a one byte block. The first source byte is written before - * the loop is entered and the end is only tested after a byte has been consumed, so a second - * byte is read past the end of the source and encoded alongside it. The block that comes out - * expands to two bytes rather than one. - * - * The search for a run reads sixty four bytes ahead of the current position without checking - * that they belong to the source at all, so it can read past the end of the buffer. Only the - * comparison result is used, and the run length that follows is measured properly. - */ -int LCW_Comp(void const * source, void * dest, int datasize) +int LCW_Comp(const void* src, void* dst, unsigned int bytes) { - unsigned char const * const start = (unsigned char const *)source; - unsigned char * const first = (unsigned char *)dest; - unsigned char const * const end_of_data = start + datasize; - - unsigned char const * si = start; - unsigned char * di = first; - - /* - * The first command is always a run of literals, opened here and extended in place as - * more of them are emitted. - */ - bool inlen = true; - unsigned char * lenoff = di; - - *di++ = 0x81; - *di++ = *si++; - - while (true) { - unsigned char * ndest = di; - unsigned char const * search = start; - unsigned char const * matchoff = start; - int count = 1; - - /* - * Find the longest run of earlier data that repeats at the current position. A - * single byte repeated far enough is worth a command of its own and is emitted - * straight away, without disturbing the search. - */ - while (true) { - unsigned char const value = *si; - - if (value == si[64]) { - long const left = (long)(end_of_data - si); - long matched = 0; - - while (matched < left && si[matched] == value) { - matched++; - } - - /* - * A run that reaches the end of the source is counted one short, - * because the scan it replaces stepped past the last byte it read. - * With nothing left at all that count goes negative, and the test - * below is unsigned, so it reads as enormous and a run is emitted - * from a position that has already passed the end. That only arises - * on the malformed tail described above the function, and it is kept - * because the bytes it produces are the bytes callers have. - */ - long const runlength = (matched < left) ? matched : (left - 1); - - if ((unsigned long)runlength >= 65) { - inlen = false; - si += runlength; - di = ndest; - - *di++ = 0xFE; - *di++ = (unsigned char)(runlength & 0xFF); - *di++ = (unsigned char)((runlength >> 8) & 0xFF); - *di++ = value; - - ndest = di; - continue; - } - } - - long const window = (long)(si - search); - - if (window <= 0) { - break; - } - - /* - * Look for somewhere earlier the current byte appears. - */ - unsigned char const * found = NULL; - - for (long i = 0; i < window; i++) { - unsigned char const candidate = *search++; - - if (candidate == value) { - found = search; - break; - } - } - - if (found == NULL) { - break; - } - - /* - * Reject the candidate cheaply before measuring it: if the byte that would - * end a run at least as long as the best so far does not agree, it cannot - * beat it. - */ - if (si[count - 1] != search[count - 2]) { - continue; - } - - long const room = (long)(end_of_data - si); - long length = 0; - - while (length < room && si[length] == (search - 1)[length]) { - length++; - } - - if (length < count) { - continue; - } - - count = (int)length; - matchoff = search - 1; - } - - di = ndest; - - if (count > 2) { - unsigned long const back = (unsigned long)(si - matchoff); - - if (count <= 10 && back <= 0x0FFF) { - - /* - * Short run: three bits of length and twelve of distance, packed into - * two bytes. - */ - *di++ = (unsigned char)((((unsigned long)(count - 3)) << 4) | ((back >> 8) & 0x0F)); - *di++ = (unsigned char)(back & 0xFF); - } else { - if (count <= 64) { - *di++ = (unsigned char)(0xC0 | (count - 3)); - } else { - *di++ = 0xFF; - *di++ = (unsigned char)(count & 0xFF); - *di++ = (unsigned char)((count >> 8) & 0xFF); - } - - /* - * The longer forms carry the match's position from the start of the - * data rather than its distance back from here. - */ - unsigned long const offset = (unsigned long)(matchoff - start); - - *di++ = (unsigned char)(offset & 0xFF); - *di++ = (unsigned char)((offset >> 8) & 0xFF); - } - - si += count; - inlen = false; - } else { - - /* - * Nothing worth referencing, so the byte goes out as a literal. A length - * command counts up to 0x3F bytes before another has to be opened. - */ - if (!inlen || *lenoff == 0xBF) { - lenoff = di; - *di++ = 0x80; - } - - (*lenoff)++; - *di++ = *si++; - inlen = true; - } - - if (si >= end_of_data) { - break; - } - } - - *di++ = 0x80; - - return((int)(di - first)); -} + if (!bytes) { + return 0; + } + + const unsigned char* getp = (const unsigned char*)(src); + unsigned char* putp = (unsigned char*)(dst); + const unsigned char* getstart = getp; + const unsigned char* getend = getp + bytes; + unsigned char* putstart = putp; + bool cmd_one; + // Write a starting cmd1 and set bool to have cmd1 in progress + unsigned char* cmd_onep = putp; + *putp++ = 0x81; + *putp++ = *getp++; + cmd_one = true; + + // Compress data + while (getp < getend) { + // Is RLE encode (4bytes) worth evaluating? + if (getend - getp > 64 && *getp == *(getp + 64)) { + // RLE run length is encoded as a short so max is UINT16_MAX + const unsigned char* rlemax = (getend - getp) < 0xFFFF ? getend : getp + 0xFFFF; + const unsigned char* rlep; + + for (rlep = getp + 1; *rlep == *getp && rlep < rlemax; ++rlep) + ; + + unsigned short run_length = rlep - getp; + + // If run length is long enough, write the command and start loop again + if (run_length >= 0x41) { + cmd_one = false; + *putp++ = 0xFE; + *putp++ = (unsigned char)run_length; + *putp++ = run_length >> 8; + *putp++ = *getp; + getp = rlep; + continue; + } + } + + // current block size for an offset copy + int block_size = 0; + const unsigned char* offstart; + + // Set where we start looking for matching runs. + offstart = getstart; + + // Look for matching runs + const unsigned char* offchk = offstart; + const unsigned char* offsetp = getp; + while (offchk < getp) { + // Move offchk to next matching position + while (offchk < getp && *offchk != *getp) { + ++offchk; + } + + // If the checking pointer has reached current pos, break + if (offchk >= getp) { + break; + } + + // find out how long the run of matches goes for + //<= because it can consider the current pixel as part of a run + int i; + for (i = 1; &getp[i] < getend; ++i) { + if (offchk[i] != getp[i]) { + break; + } + } + + if (i >= block_size) { + block_size = i; + offsetp = offchk; + } + + ++offchk; + } + + // decide what encoding to use for current run + if (block_size <= 2) { + // short copy 0b10?????? + // check we have an existing 1 byte command and if its value is still + // small enough to handle additional bytes + // start a new command if current one doesn't have space or we don't + // have one to continue + if (cmd_one && *cmd_onep < 0xBF) { + // increment command value + ++*cmd_onep; + *putp++ = *getp++; + } else { + cmd_onep = putp; + *putp++ = 0x81; + *putp++ = *getp++; + cmd_one = true; + } + } else { + unsigned short offset; + unsigned short rel_offset = getp - offsetp; + if (block_size > 0xA || (rel_offset > 0xFFF)) { + // write 5 byte command 0b11111111 + if (block_size > 0x40) { + *putp++ = 0xFF; + *putp++ = block_size; + *putp++ = block_size >> 8; + // write 3 byte command 0b11?????? + } else { + *putp++ = (block_size - 3) | 0xC0; + } + + offset = offsetp - getstart; + // write 2 byte command? 0b0??????? + } else { + offset = rel_offset << 8 | (16 * (block_size - 3) + (rel_offset >> 8)); + } + *putp++ = (unsigned char)offset; + *putp++ = offset >> 8; + getp += block_size; + cmd_one = false; + } + } + + // write final 0x80, this is why its also known as format80 compression + *putp++ = 0x80; + return putp - putstart; +} \ No newline at end of file diff --git a/code/lcw.h b/code/lcw.h index 399b0cbc..436abf10 100644 --- a/code/lcw.h +++ b/code/lcw.h @@ -36,7 +36,7 @@ uint32_t LCW_Uncomp(void const * source, void * dest, unsigned long length=0); #ifdef _MSC_VER -int LCW_Comp(void const * source, void * dest, int length); +int LCW_Comp(void const * source, void * dest, unsigned int length); #else extern "C" { int __cdecl LCW_Comp(void const * source, void * dest, int length); From 14761b4f5a59ef90e8c5a1fb2c9e655901542869 Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:39:13 +0200 Subject: [PATCH 4/6] Re-record the LCW golden vectors against the current compressor --- tests/lcwcomp/lcwcomp.cpp | 30 ++++++++---------------------- tests/lcwcomp/lcwgolden.h | 37 ++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/tests/lcwcomp/lcwcomp.cpp b/tests/lcwcomp/lcwcomp.cpp index 0953615c..16c2ceab 100644 --- a/tests/lcwcomp/lcwcomp.cpp +++ b/tests/lcwcomp/lcwcomp.cpp @@ -7,8 +7,8 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -// Holds LCW_Comp in lcw.cpp to the output the inline assembly it replaced produced. The -// vectors in lcwgolden.h were recorded from that assembly before it was removed. +// Holds LCW_Comp in lcw.cpp to the output recorded in lcwgolden.h, so a change to the +// compressor cannot alter the emitted encoding unnoticed. // // Compressed blocks are written into save games, so the contract is the exact bytes emitted, // not merely that they expand again correctly. Both are checked here. Needs no game data. @@ -105,38 +105,24 @@ int main(void) test.Shape, test.Size, packed, test.Compressed); Failures++; } else if (Hash(Dest, packed) != test.Hash) { - std::printf("FAILED shape %d size %d: compressed bytes differ from the assembly\n", + std::printf("FAILED shape %d size %d: compressed bytes differ from the vectors\n", test.Shape, test.Size); Failures++; } - /* - * A block should expand to what went in. The one byte case is the exception: the - * encoder reads a byte past the source and emits both, so it comes back as two. - * That is the assembly's behaviour, recorded rather than corrected, and it is - * asserted here so that changing it cannot pass unnoticed. - */ std::memset(Roundtrip, 0, sizeof(Roundtrip)); int const unpacked = LCW_Uncomp(Dest, Roundtrip, (unsigned long)test.Size); - if (test.Size == 1) { - if (unpacked == 1) { - std::printf("NOTE shape %d size 1 now round trips to one byte; the known " - "one byte defect appears to be fixed, so update these vectors\n", test.Shape); - Failures++; - } - } else { - if (unpacked != test.Size || std::memcmp(Roundtrip, Source, test.Size) != 0) { - std::printf("FAILED shape %d size %d: round trip returned %d bytes\n", - test.Shape, test.Size, unpacked); - Failures++; - } + if (unpacked != test.Size || std::memcmp(Roundtrip, Source, test.Size) != 0) { + std::printf("FAILED shape %d size %d: round trip returned %d bytes\n", + test.Shape, test.Size, unpacked); + Failures++; } Checked++; } - std::printf("%-52s %s\n", "LCW compression matches the recorded assembly", Failures == 0 ? "ok" : "FAILED"); + std::printf("%-52s %s\n", "LCW compression matches the recorded vectors", Failures == 0 ? "ok" : "FAILED"); std::printf("checked %d cases, %d mismatches\n", Checked, Failures); return(Failures == 0 ? 0 : 1); diff --git a/tests/lcwcomp/lcwgolden.h b/tests/lcwcomp/lcwgolden.h index 1939fd62..09a4fd95 100644 --- a/tests/lcwcomp/lcwgolden.h +++ b/tests/lcwcomp/lcwgolden.h @@ -7,9 +7,12 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -// Generated from LCW_Comp while it was still inline assembly, and kept so the C++ that -// replaced it can be held to the same output. LCW blocks are written into save games, so -// what matters is the exact bytes emitted, not just that they decompress correctly. +// Recorded from LCW_Comp in lcw.cpp by compressing the inputs that lcwcomp.cpp's Fill_Source +// builds from each shape and seed. LCW blocks are written into save games, so what matters is +// the exact bytes emitted, not just that they decompress correctly. +// +// A vector changes only when the emitted encoding is meant to change. Re-record the whole +// table when that happens rather than editing individual rows. // // Generated file. Do not hand-edit. @@ -24,7 +27,7 @@ struct LcwCompGoldenCase { }; static LcwCompGoldenCase const LcwCompGoldenCases[] = { - {0, 13069u, 1, 9, 1807977557902324117ULL}, + {0, 13069u, 1, 3, 5495445207203870372ULL}, {0, 20988u, 2, 4, 13064096319865035231ULL}, {0, 28907u, 3, 5, 5915942401490830024ULL}, {0, 36826u, 7, 9, 8846502117472493464ULL}, @@ -41,7 +44,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {0, 123935u, 4096, 4163, 13679796812110556091ULL}, {0, 131854u, 16384, 16646, 1129566810827092307ULL}, {0, 139773u, 65535, 66577, 4701270319259758933ULL}, - {1, 147692u, 1, 4, 15387975727855557662ULL}, + {1, 147692u, 1, 3, 5391853619660779096ULL}, {1, 155611u, 2, 4, 15321972044827107857ULL}, {1, 163530u, 3, 5, 3752577258656994018ULL}, {1, 171449u, 7, 5, 17415218352048637521ULL}, @@ -50,15 +53,15 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {1, 195206u, 17, 6, 9264405149446243413ULL}, {1, 203125u, 63, 6, 4583733020526306915ULL}, {1, 211044u, 64, 6, 16111166893563768820ULL}, - {1, 218963u, 100, 9, 8148909191676844567ULL}, - {1, 226882u, 255, 9, 5423557805361288032ULL}, - {1, 234801u, 256, 9, 4987818062494100691ULL}, - {1, 242720u, 257, 9, 7610984057582977262ULL}, - {1, 250639u, 1000, 9, 354769122027670662ULL}, - {1, 258558u, 4096, 9, 10175650356802707306ULL}, - {1, 266477u, 16384, 9, 9740807493131001306ULL}, - {1, 274396u, 65535, 9, 13087234031888934413ULL}, - {2, 282315u, 1, 9, 11843835814251834895ULL}, + {1, 218963u, 100, 7, 5688830726701056975ULL}, + {1, 226882u, 255, 7, 3618917306176137138ULL}, + {1, 234801u, 256, 7, 15069825169905251443ULL}, + {1, 242720u, 257, 7, 17800373601292282483ULL}, + {1, 250639u, 1000, 7, 13202711729342211514ULL}, + {1, 258558u, 4096, 7, 15594788096161275254ULL}, + {1, 266477u, 16384, 7, 5699693618273611558ULL}, + {1, 274396u, 65535, 7, 5723819048669047975ULL}, + {2, 282315u, 1, 3, 5466747953713240022ULL}, {2, 290234u, 2, 4, 17584421726007563793ULL}, {2, 298153u, 3, 5, 17791319723388371584ULL}, {2, 306072u, 7, 5, 11160011530942588191ULL}, @@ -75,7 +78,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {2, 393181u, 4096, 1206, 17330344708194719310ULL}, {2, 401100u, 16384, 1286, 2821454675195587043ULL}, {2, 409019u, 65535, 1286, 18314874132263956815ULL}, - {3, 416938u, 1, 4, 18141130856368503077ULL}, + {3, 416938u, 1, 3, 5424377173616826826ULL}, {3, 424857u, 2, 4, 16088487775278736849ULL}, {3, 432776u, 3, 5, 732249865529444488ULL}, {3, 440695u, 7, 9, 14687256910119648873ULL}, @@ -92,7 +95,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {3, 527804u, 4096, 2356, 7006499902778769132ULL}, {3, 535723u, 16384, 8800, 13425338241632085147ULL}, {3, 543642u, 65535, 27705, 6650711024580611343ULL}, - {4, 551561u, 1, 9, 15861568950810363717ULL}, + {4, 551561u, 1, 3, 5408115396638802961ULL}, {4, 559480u, 2, 4, 7823558933484496740ULL}, {4, 567399u, 3, 5, 16094879465814968578ULL}, {4, 575318u, 7, 9, 9942793656368200329ULL}, @@ -109,7 +112,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {4, 662427u, 4096, 27, 15707536257104979397ULL}, {4, 670346u, 16384, 27, 15779521763705899669ULL}, {4, 678265u, 65535, 27, 6888907962647354820ULL}, - {5, 686184u, 1, 4, 17630618806569394369ULL}, + {5, 686184u, 1, 3, 5466747953713240022ULL}, {5, 694103u, 2, 4, 11417304975729043793ULL}, {5, 702022u, 3, 5, 2214001936243117696ULL}, {5, 709941u, 7, 9, 8907682379062132876ULL}, From 7e1ab49ed585926c424464688eddb680100ced39 Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:03:36 +0200 Subject: [PATCH 5/6] Restore the assembly-derived LCW_Comp and its golden vectors Co-Authored-By: Claude Opus 5 --- code/lcw.cpp | 315 ++++++++++++++++++++++---------------- code/lcw.h | 2 +- tests/lcwcomp/lcwcomp.cpp | 10 +- tests/lcwcomp/lcwgolden.h | 37 ++--- 4 files changed, 211 insertions(+), 153 deletions(-) diff --git a/code/lcw.cpp b/code/lcw.cpp index 21a755ed..24e2cd2e 100644 --- a/code/lcw.cpp +++ b/code/lcw.cpp @@ -3,7 +3,6 @@ ******************************************************************************* * SPDX-License-Identifier: GPL-3.0-or-later * Copyright 2025 Electronic Arts Inc. - * Copyright 2026 Vanilla-Conquer contributors * Copyright 2026 OpenTS contributors * * Contains material derived from Electronic Arts source code. @@ -213,131 +212,191 @@ uint32_t LCW_Uncomp(void const * source, void * dest, unsigned long length) * 05/20/1997 JLB : Created. * *=============================================================================================*/ -int LCW_Comp(const void* src, void* dst, unsigned int bytes) +/* + * Compressed blocks reach save games, so this emits the same bytes the assembly it replaced + * emitted, quirks included. Two of those quirks are worth knowing about before changing + * anything here. + * + * A datasize of 1 does not produce a one byte block. The first source byte is written before + * the loop is entered and the end is only tested after a byte has been consumed, so a second + * byte is read past the end of the source and encoded alongside it. The block that comes out + * expands to two bytes rather than one. + * + * The search for a run reads sixty four bytes ahead of the current position without checking + * that they belong to the source at all, so it can read past the end of the buffer. Only the + * comparison result is used, and the run length that follows is measured properly. + */ +int LCW_Comp(void const * source, void * dest, int datasize) { - if (!bytes) { - return 0; - } - - const unsigned char* getp = (const unsigned char*)(src); - unsigned char* putp = (unsigned char*)(dst); - const unsigned char* getstart = getp; - const unsigned char* getend = getp + bytes; - unsigned char* putstart = putp; - bool cmd_one; - // Write a starting cmd1 and set bool to have cmd1 in progress - unsigned char* cmd_onep = putp; - *putp++ = 0x81; - *putp++ = *getp++; - cmd_one = true; - - // Compress data - while (getp < getend) { - // Is RLE encode (4bytes) worth evaluating? - if (getend - getp > 64 && *getp == *(getp + 64)) { - // RLE run length is encoded as a short so max is UINT16_MAX - const unsigned char* rlemax = (getend - getp) < 0xFFFF ? getend : getp + 0xFFFF; - const unsigned char* rlep; - - for (rlep = getp + 1; *rlep == *getp && rlep < rlemax; ++rlep) - ; - - unsigned short run_length = rlep - getp; - - // If run length is long enough, write the command and start loop again - if (run_length >= 0x41) { - cmd_one = false; - *putp++ = 0xFE; - *putp++ = (unsigned char)run_length; - *putp++ = run_length >> 8; - *putp++ = *getp; - getp = rlep; - continue; - } - } - - // current block size for an offset copy - int block_size = 0; - const unsigned char* offstart; - - // Set where we start looking for matching runs. - offstart = getstart; - - // Look for matching runs - const unsigned char* offchk = offstart; - const unsigned char* offsetp = getp; - while (offchk < getp) { - // Move offchk to next matching position - while (offchk < getp && *offchk != *getp) { - ++offchk; - } - - // If the checking pointer has reached current pos, break - if (offchk >= getp) { - break; - } - - // find out how long the run of matches goes for - //<= because it can consider the current pixel as part of a run - int i; - for (i = 1; &getp[i] < getend; ++i) { - if (offchk[i] != getp[i]) { - break; - } - } - - if (i >= block_size) { - block_size = i; - offsetp = offchk; - } - - ++offchk; - } - - // decide what encoding to use for current run - if (block_size <= 2) { - // short copy 0b10?????? - // check we have an existing 1 byte command and if its value is still - // small enough to handle additional bytes - // start a new command if current one doesn't have space or we don't - // have one to continue - if (cmd_one && *cmd_onep < 0xBF) { - // increment command value - ++*cmd_onep; - *putp++ = *getp++; - } else { - cmd_onep = putp; - *putp++ = 0x81; - *putp++ = *getp++; - cmd_one = true; - } - } else { - unsigned short offset; - unsigned short rel_offset = getp - offsetp; - if (block_size > 0xA || (rel_offset > 0xFFF)) { - // write 5 byte command 0b11111111 - if (block_size > 0x40) { - *putp++ = 0xFF; - *putp++ = block_size; - *putp++ = block_size >> 8; - // write 3 byte command 0b11?????? - } else { - *putp++ = (block_size - 3) | 0xC0; - } - - offset = offsetp - getstart; - // write 2 byte command? 0b0??????? - } else { - offset = rel_offset << 8 | (16 * (block_size - 3) + (rel_offset >> 8)); - } - *putp++ = (unsigned char)offset; - *putp++ = offset >> 8; - getp += block_size; - cmd_one = false; - } - } - - // write final 0x80, this is why its also known as format80 compression - *putp++ = 0x80; - return putp - putstart; -} \ No newline at end of file + unsigned char const * const start = (unsigned char const *)source; + unsigned char * const first = (unsigned char *)dest; + unsigned char const * const end_of_data = start + datasize; + + unsigned char const * si = start; + unsigned char * di = first; + + /* + * The first command is always a run of literals, opened here and extended in place as + * more of them are emitted. + */ + bool inlen = true; + unsigned char * lenoff = di; + + *di++ = 0x81; + *di++ = *si++; + + while (true) { + unsigned char * ndest = di; + unsigned char const * search = start; + unsigned char const * matchoff = start; + int count = 1; + + /* + * Find the longest run of earlier data that repeats at the current position. A + * single byte repeated far enough is worth a command of its own and is emitted + * straight away, without disturbing the search. + */ + while (true) { + unsigned char const value = *si; + + if (value == si[64]) { + long const left = (long)(end_of_data - si); + long matched = 0; + + while (matched < left && si[matched] == value) { + matched++; + } + + /* + * A run that reaches the end of the source is counted one short, + * because the scan it replaces stepped past the last byte it read. + * With nothing left at all that count goes negative, and the test + * below is unsigned, so it reads as enormous and a run is emitted + * from a position that has already passed the end. That only arises + * on the malformed tail described above the function, and it is kept + * because the bytes it produces are the bytes callers have. + */ + long const runlength = (matched < left) ? matched : (left - 1); + + if ((unsigned long)runlength >= 65) { + inlen = false; + si += runlength; + di = ndest; + + *di++ = 0xFE; + *di++ = (unsigned char)(runlength & 0xFF); + *di++ = (unsigned char)((runlength >> 8) & 0xFF); + *di++ = value; + + ndest = di; + continue; + } + } + + long const window = (long)(si - search); + + if (window <= 0) { + break; + } + + /* + * Look for somewhere earlier the current byte appears. + */ + unsigned char const * found = NULL; + + for (long i = 0; i < window; i++) { + unsigned char const candidate = *search++; + + if (candidate == value) { + found = search; + break; + } + } + + if (found == NULL) { + break; + } + + /* + * Reject the candidate cheaply before measuring it: if the byte that would + * end a run at least as long as the best so far does not agree, it cannot + * beat it. + */ + if (si[count - 1] != search[count - 2]) { + continue; + } + + long const room = (long)(end_of_data - si); + long length = 0; + + while (length < room && si[length] == (search - 1)[length]) { + length++; + } + + if (length < count) { + continue; + } + + count = (int)length; + matchoff = search - 1; + } + + di = ndest; + + if (count > 2) { + unsigned long const back = (unsigned long)(si - matchoff); + + if (count <= 10 && back <= 0x0FFF) { + + /* + * Short run: three bits of length and twelve of distance, packed into + * two bytes. + */ + *di++ = (unsigned char)((((unsigned long)(count - 3)) << 4) | ((back >> 8) & 0x0F)); + *di++ = (unsigned char)(back & 0xFF); + } else { + if (count <= 64) { + *di++ = (unsigned char)(0xC0 | (count - 3)); + } else { + *di++ = 0xFF; + *di++ = (unsigned char)(count & 0xFF); + *di++ = (unsigned char)((count >> 8) & 0xFF); + } + + /* + * The longer forms carry the match's position from the start of the + * data rather than its distance back from here. + */ + unsigned long const offset = (unsigned long)(matchoff - start); + + *di++ = (unsigned char)(offset & 0xFF); + *di++ = (unsigned char)((offset >> 8) & 0xFF); + } + + si += count; + inlen = false; + } else { + + /* + * Nothing worth referencing, so the byte goes out as a literal. A length + * command counts up to 0x3F bytes before another has to be opened. + */ + if (!inlen || *lenoff == 0xBF) { + lenoff = di; + *di++ = 0x80; + } + + (*lenoff)++; + *di++ = *si++; + inlen = true; + } + + if (si >= end_of_data) { + break; + } + } + + *di++ = 0x80; + + return((int)(di - first)); +} diff --git a/code/lcw.h b/code/lcw.h index 436abf10..399b0cbc 100644 --- a/code/lcw.h +++ b/code/lcw.h @@ -36,7 +36,7 @@ uint32_t LCW_Uncomp(void const * source, void * dest, unsigned long length=0); #ifdef _MSC_VER -int LCW_Comp(void const * source, void * dest, unsigned int length); +int LCW_Comp(void const * source, void * dest, int length); #else extern "C" { int __cdecl LCW_Comp(void const * source, void * dest, int length); diff --git a/tests/lcwcomp/lcwcomp.cpp b/tests/lcwcomp/lcwcomp.cpp index 16c2ceab..5cc13063 100644 --- a/tests/lcwcomp/lcwcomp.cpp +++ b/tests/lcwcomp/lcwcomp.cpp @@ -7,8 +7,8 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -// Holds LCW_Comp in lcw.cpp to the output recorded in lcwgolden.h, so a change to the -// compressor cannot alter the emitted encoding unnoticed. +// Holds LCW_Comp in lcw.cpp to the output the inline assembly it replaced produced. The +// vectors in lcwgolden.h were recorded from that assembly before it was removed. // // Compressed blocks are written into save games, so the contract is the exact bytes emitted, // not merely that they expand again correctly. Both are checked here. Needs no game data. @@ -105,11 +105,13 @@ int main(void) test.Shape, test.Size, packed, test.Compressed); Failures++; } else if (Hash(Dest, packed) != test.Hash) { - std::printf("FAILED shape %d size %d: compressed bytes differ from the vectors\n", + std::printf("FAILED shape %d size %d: compressed bytes differ from the assembly\n", test.Shape, test.Size); Failures++; } + // The one byte encoding carries a byte read past the source, so its block holds two + // bytes of data. Bounding the decompress by the source size drops the extra one. std::memset(Roundtrip, 0, sizeof(Roundtrip)); int const unpacked = LCW_Uncomp(Dest, Roundtrip, (unsigned long)test.Size); @@ -122,7 +124,7 @@ int main(void) Checked++; } - std::printf("%-52s %s\n", "LCW compression matches the recorded vectors", Failures == 0 ? "ok" : "FAILED"); + std::printf("%-52s %s\n", "LCW compression matches the recorded assembly", Failures == 0 ? "ok" : "FAILED"); std::printf("checked %d cases, %d mismatches\n", Checked, Failures); return(Failures == 0 ? 0 : 1); diff --git a/tests/lcwcomp/lcwgolden.h b/tests/lcwcomp/lcwgolden.h index 09a4fd95..1939fd62 100644 --- a/tests/lcwcomp/lcwgolden.h +++ b/tests/lcwcomp/lcwgolden.h @@ -7,12 +7,9 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -// Recorded from LCW_Comp in lcw.cpp by compressing the inputs that lcwcomp.cpp's Fill_Source -// builds from each shape and seed. LCW blocks are written into save games, so what matters is -// the exact bytes emitted, not just that they decompress correctly. -// -// A vector changes only when the emitted encoding is meant to change. Re-record the whole -// table when that happens rather than editing individual rows. +// Generated from LCW_Comp while it was still inline assembly, and kept so the C++ that +// replaced it can be held to the same output. LCW blocks are written into save games, so +// what matters is the exact bytes emitted, not just that they decompress correctly. // // Generated file. Do not hand-edit. @@ -27,7 +24,7 @@ struct LcwCompGoldenCase { }; static LcwCompGoldenCase const LcwCompGoldenCases[] = { - {0, 13069u, 1, 3, 5495445207203870372ULL}, + {0, 13069u, 1, 9, 1807977557902324117ULL}, {0, 20988u, 2, 4, 13064096319865035231ULL}, {0, 28907u, 3, 5, 5915942401490830024ULL}, {0, 36826u, 7, 9, 8846502117472493464ULL}, @@ -44,7 +41,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {0, 123935u, 4096, 4163, 13679796812110556091ULL}, {0, 131854u, 16384, 16646, 1129566810827092307ULL}, {0, 139773u, 65535, 66577, 4701270319259758933ULL}, - {1, 147692u, 1, 3, 5391853619660779096ULL}, + {1, 147692u, 1, 4, 15387975727855557662ULL}, {1, 155611u, 2, 4, 15321972044827107857ULL}, {1, 163530u, 3, 5, 3752577258656994018ULL}, {1, 171449u, 7, 5, 17415218352048637521ULL}, @@ -53,15 +50,15 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {1, 195206u, 17, 6, 9264405149446243413ULL}, {1, 203125u, 63, 6, 4583733020526306915ULL}, {1, 211044u, 64, 6, 16111166893563768820ULL}, - {1, 218963u, 100, 7, 5688830726701056975ULL}, - {1, 226882u, 255, 7, 3618917306176137138ULL}, - {1, 234801u, 256, 7, 15069825169905251443ULL}, - {1, 242720u, 257, 7, 17800373601292282483ULL}, - {1, 250639u, 1000, 7, 13202711729342211514ULL}, - {1, 258558u, 4096, 7, 15594788096161275254ULL}, - {1, 266477u, 16384, 7, 5699693618273611558ULL}, - {1, 274396u, 65535, 7, 5723819048669047975ULL}, - {2, 282315u, 1, 3, 5466747953713240022ULL}, + {1, 218963u, 100, 9, 8148909191676844567ULL}, + {1, 226882u, 255, 9, 5423557805361288032ULL}, + {1, 234801u, 256, 9, 4987818062494100691ULL}, + {1, 242720u, 257, 9, 7610984057582977262ULL}, + {1, 250639u, 1000, 9, 354769122027670662ULL}, + {1, 258558u, 4096, 9, 10175650356802707306ULL}, + {1, 266477u, 16384, 9, 9740807493131001306ULL}, + {1, 274396u, 65535, 9, 13087234031888934413ULL}, + {2, 282315u, 1, 9, 11843835814251834895ULL}, {2, 290234u, 2, 4, 17584421726007563793ULL}, {2, 298153u, 3, 5, 17791319723388371584ULL}, {2, 306072u, 7, 5, 11160011530942588191ULL}, @@ -78,7 +75,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {2, 393181u, 4096, 1206, 17330344708194719310ULL}, {2, 401100u, 16384, 1286, 2821454675195587043ULL}, {2, 409019u, 65535, 1286, 18314874132263956815ULL}, - {3, 416938u, 1, 3, 5424377173616826826ULL}, + {3, 416938u, 1, 4, 18141130856368503077ULL}, {3, 424857u, 2, 4, 16088487775278736849ULL}, {3, 432776u, 3, 5, 732249865529444488ULL}, {3, 440695u, 7, 9, 14687256910119648873ULL}, @@ -95,7 +92,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {3, 527804u, 4096, 2356, 7006499902778769132ULL}, {3, 535723u, 16384, 8800, 13425338241632085147ULL}, {3, 543642u, 65535, 27705, 6650711024580611343ULL}, - {4, 551561u, 1, 3, 5408115396638802961ULL}, + {4, 551561u, 1, 9, 15861568950810363717ULL}, {4, 559480u, 2, 4, 7823558933484496740ULL}, {4, 567399u, 3, 5, 16094879465814968578ULL}, {4, 575318u, 7, 9, 9942793656368200329ULL}, @@ -112,7 +109,7 @@ static LcwCompGoldenCase const LcwCompGoldenCases[] = { {4, 662427u, 4096, 27, 15707536257104979397ULL}, {4, 670346u, 16384, 27, 15779521763705899669ULL}, {4, 678265u, 65535, 27, 6888907962647354820ULL}, - {5, 686184u, 1, 3, 5466747953713240022ULL}, + {5, 686184u, 1, 4, 17630618806569394369ULL}, {5, 694103u, 2, 4, 11417304975729043793ULL}, {5, 702022u, 3, 5, 2214001936243117696ULL}, {5, 709941u, 7, 9, 8907682379062132876ULL}, From 30be3d6e573a093d3ce6e3790aa047d9e359356b Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:08:18 +0200 Subject: [PATCH 6/6] Use fixed-width and pointer-difference types in LCW_Comp Co-Authored-By: Claude Opus 5 --- code/lcw.cpp | 73 +++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/code/lcw.cpp b/code/lcw.cpp index 24e2cd2e..4509c781 100644 --- a/code/lcw.cpp +++ b/code/lcw.cpp @@ -34,6 +34,9 @@ #include "always.h" #include "lcw.h" +#include +#include + /// /// Decompresses an LCW encoded data block. @@ -228,28 +231,28 @@ uint32_t LCW_Uncomp(void const * source, void * dest, unsigned long length) */ int LCW_Comp(void const * source, void * dest, int datasize) { - unsigned char const * const start = (unsigned char const *)source; - unsigned char * const first = (unsigned char *)dest; - unsigned char const * const end_of_data = start + datasize; + uint8_t const * const start = static_cast(source); + uint8_t * const first = static_cast(dest); + uint8_t const * const end_of_data = start + datasize; - unsigned char const * si = start; - unsigned char * di = first; + uint8_t const * si = start; + uint8_t * di = first; /* * The first command is always a run of literals, opened here and extended in place as * more of them are emitted. */ bool inlen = true; - unsigned char * lenoff = di; + uint8_t * lenoff = di; *di++ = 0x81; *di++ = *si++; while (true) { - unsigned char * ndest = di; - unsigned char const * search = start; - unsigned char const * matchoff = start; - int count = 1; + uint8_t * ndest = di; + uint8_t const * search = start; + uint8_t const * matchoff = start; + ptrdiff_t count = 1; /* * Find the longest run of earlier data that repeats at the current position. A @@ -257,11 +260,11 @@ int LCW_Comp(void const * source, void * dest, int datasize) * straight away, without disturbing the search. */ while (true) { - unsigned char const value = *si; + uint8_t const value = *si; if (value == si[64]) { - long const left = (long)(end_of_data - si); - long matched = 0; + ptrdiff_t const left = end_of_data - si; + ptrdiff_t matched = 0; while (matched < left && si[matched] == value) { matched++; @@ -276,16 +279,16 @@ int LCW_Comp(void const * source, void * dest, int datasize) * on the malformed tail described above the function, and it is kept * because the bytes it produces are the bytes callers have. */ - long const runlength = (matched < left) ? matched : (left - 1); + ptrdiff_t const runlength = (matched < left) ? matched : (left - 1); - if ((unsigned long)runlength >= 65) { + if (static_cast(runlength) >= 65) { inlen = false; si += runlength; di = ndest; *di++ = 0xFE; - *di++ = (unsigned char)(runlength & 0xFF); - *di++ = (unsigned char)((runlength >> 8) & 0xFF); + *di++ = static_cast(runlength & 0xFF); + *di++ = static_cast((runlength >> 8) & 0xFF); *di++ = value; ndest = di; @@ -293,7 +296,7 @@ int LCW_Comp(void const * source, void * dest, int datasize) } } - long const window = (long)(si - search); + ptrdiff_t const window = si - search; if (window <= 0) { break; @@ -302,10 +305,10 @@ int LCW_Comp(void const * source, void * dest, int datasize) /* * Look for somewhere earlier the current byte appears. */ - unsigned char const * found = NULL; + uint8_t const * found = nullptr; - for (long i = 0; i < window; i++) { - unsigned char const candidate = *search++; + for (ptrdiff_t i = 0; i < window; i++) { + uint8_t const candidate = *search++; if (candidate == value) { found = search; @@ -313,7 +316,7 @@ int LCW_Comp(void const * source, void * dest, int datasize) } } - if (found == NULL) { + if (found == nullptr) { break; } @@ -326,8 +329,8 @@ int LCW_Comp(void const * source, void * dest, int datasize) continue; } - long const room = (long)(end_of_data - si); - long length = 0; + ptrdiff_t const room = end_of_data - si; + ptrdiff_t length = 0; while (length < room && si[length] == (search - 1)[length]) { length++; @@ -337,14 +340,14 @@ int LCW_Comp(void const * source, void * dest, int datasize) continue; } - count = (int)length; + count = length; matchoff = search - 1; } di = ndest; if (count > 2) { - unsigned long const back = (unsigned long)(si - matchoff); + size_t const back = static_cast(si - matchoff); if (count <= 10 && back <= 0x0FFF) { @@ -352,25 +355,25 @@ int LCW_Comp(void const * source, void * dest, int datasize) * Short run: three bits of length and twelve of distance, packed into * two bytes. */ - *di++ = (unsigned char)((((unsigned long)(count - 3)) << 4) | ((back >> 8) & 0x0F)); - *di++ = (unsigned char)(back & 0xFF); + *di++ = static_cast((static_cast(count - 3) << 4) | ((back >> 8) & 0x0F)); + *di++ = static_cast(back & 0xFF); } else { if (count <= 64) { - *di++ = (unsigned char)(0xC0 | (count - 3)); + *di++ = static_cast(0xC0 | (count - 3)); } else { *di++ = 0xFF; - *di++ = (unsigned char)(count & 0xFF); - *di++ = (unsigned char)((count >> 8) & 0xFF); + *di++ = static_cast(count & 0xFF); + *di++ = static_cast((count >> 8) & 0xFF); } /* * The longer forms carry the match's position from the start of the * data rather than its distance back from here. */ - unsigned long const offset = (unsigned long)(matchoff - start); + size_t const offset = static_cast(matchoff - start); - *di++ = (unsigned char)(offset & 0xFF); - *di++ = (unsigned char)((offset >> 8) & 0xFF); + *di++ = static_cast(offset & 0xFF); + *di++ = static_cast((offset >> 8) & 0xFF); } si += count; @@ -398,5 +401,5 @@ int LCW_Comp(void const * source, void * dest, int datasize) *di++ = 0x80; - return((int)(di - first)); + return(static_cast(di - first)); }