; ============================================================
; Retro Tool PBI Header
; Datei: Formats/module_format_rfxl.pbi
; SPDX-License-Identifier: MIT
; Copyright (c) 2026 Jörg Burbach, joerg-burbach.de
; Lizenz: Siehe Formats/LICENSE_MIT.txt
; Version: RFXL-26
;
; Funktion:
;   RFXL memory-only image codec with small decoder and encoder-side
;   palette/planar/raster/sparse candidate search.
;
;   Changelog:
;   1.11 - Production hardening without bitstream changes: decoded U32
;          stream and sparse-payload sizes are validated before arithmetic;
;          sparse mask/index bounds use remaining-byte checks to avoid signed
;          overflow. Public LZSS/Rice helpers reject empty inputs before
;          allocating or dereferencing. Focused corruption and stride tests
;          cover the public API.
;   1.10 - Decoder aliases an unpacked non-palette stream directly when
;          predictor=0, avoiding one full-size allocation and copy. Other
;          predictor paths are unchanged; no bitstream change.
;   1.9 - Decoder-only palette hot-loop cleanup: non-MTF palette streams
;         now dispatch by index bit-depth once and run a 8/4/2/1-bit
;         specific unpack+lookup loop, instead of testing palBits for
;         every pixel. No bitstream change.
;   1.8 - Decoder-only hot-loop cleanup: RFXDecodePixelStream now
;         dispatches on the pixel format once per image and runs a
;         format-specific loop, instead of doing Select format inside
;         every pixel. Same reconstruction, no bitstream change.
;   1.7 - Adds one encoder-only candidate for lossy qualities: after the
;         normal quantization step, the encoder also tries storing those
;         already-quantized pixels exactly as RGB/RGBA raster data and lets
;         the existing candidate race keep it only when it is smaller than
;         the tier's compact pixel layout. No decoder or bitstream feature
;         was added; it reuses the existing raster candidate builder. This
;         measured unchanged on image.png, but improved TEST24rle.tga High
;         82700 -> 76173 bytes (~7.9%) and Mid 52909 -> 52352 bytes (~1.1%)
;         at identical PSNR.
;   1.6 - New Crappy quality tier (codec-stack spec section G/A), below
;         the previous floor tier Low - the RFXLQuality enum was
;         renumbered (Crappy=0..Lossless=4, was Low=0..Lossless=3) so
;         Crappy sits genuinely below Low for the existing ">="-style
;         comparisons elsewhere (RFXL never stores Quality in its own
;         bitstream, so this has no decode-time consequence for RFXL
;         files themselves - see the enum's own comment). No new pixel
;         format or codec: QuantizeByte gained a Crappy case (shift 6,
;         one step past Low's shift 5) and RFXFormatForQuality now maps
;         Crappy to the SAME format 7 (RGB332) Low already uses -
;         reuses the existing aggressive path exactly as the spec asked
;         ("vorhandenen RGB332-Pfad verwenden, keinen neuen Bildcodec
;         bauen"). Verified smaller than Low on real content (192,710
;         vs 311,732 bytes on a 1024x1024 photo) and decodes correctly
;         through the unmodified existing decoder.
;   1.5 - Fixed Low-quality color corruption: format 7's single-byte
;         pixel packing was labeled RGB333 but actually shifted 3+3+3=9
;         bits of color into an 8-bit byte (r>>5 << 6, i.e. red's top
;         bit sat at bit 8 and was silently dropped by the & $FF store).
;         Any pixel with r>=128 lost that bit on encode, decode then
;         reconstructed it as 0 - suppressing red and giving Low-quality
;         output a systematic blue/green tint on real photos (reported
;         by user on IMG_0723.jpg; not visible on the earlier synthetic
;         test image, whose reds happened to stay under 128). Repacked
;         as true RGB332 (3 bits R, 3 bits G, 2 bits B - fits exactly in
;         8 bits, no overflow) on both encode and decode. Bitstream
;         change for format 7 only: existing Low-quality .rfxl files
;         must be re-encoded from source, since old and new decoders
;         disagree on this format's byte layout.
;   1.4 - Fixed a crash: the encoder's internal CreateThread() calls (top-
;         level 4-way task split, and the 5-way predictor-mode split
;         inside it) ran unconditionally, regardless of whether the HOST
;         program was compiled with PureBasic's -t flag. PB's allocator
;         isn't thread-safe without -t, so any caller not built with -t
;         (which was every caller before this fix) risked intermittent
;         memory corruption/crashes under real use - a race, which is why
;         it didn't show up in repeated automated round-trip testing but
;         did hit a real user. Both CreateThread() sites are now guarded
;         by CompilerIf #PB_Compiler_Thread; when off, thread(i)/
;         pthread(pred) stay 0 and the existing WaitThread fallback runs
;         the task function directly, i.e. clean serial execution instead
;         of undefined behavior. No bitstream/output change either way.
;   1.3 - Rice unary run now written/read in byte chunks instead of
;         bit-by-bit; LZSS hash-chain search depth capped at 2048 hops
;         (bounds worst case, same size on all tested images); LZSS
;         encode is now one-step lazy instead of strictly greedy. All
;         byte-identical to the old decoder.
;   1.2 - RFXUnpredict: border pixels handled once instead of per-pixel
;         bounds checks; mode dispatched once per row, not per pixel.
;         Decoder only, no bitstream change. 1.64x-1.67x faster decode.
;   1.1 - Predictor mode 4 (MED / Median Edge Detector, as in JPEG-LS).
;         -0.9% to -6.2% on photographic content, 0% on palette-heavy
;         images (palette path already wins there). Not backwards-
;         compatible with a pre-1.1 decoder.
;
; Unterstuetzte Formate:
;   RGB24/RGBA32 input and output, optional alpha, Crappy/Low/Mid/High/Lossless.
;
; Dateiformat:
;   RFXL header plus compact pixel payload; RFXL-26 stream.
;
; DLL-Faehigkeit:
;   Ja; standalone decoder/encoder, no file/UI/image dependencies.
;
; Formatstatus:
;   lesen=ja | schreiben=ja | memory=ja | optimiert=ja | vollstaendig=RFXL-26
; ============================================================

CompilerIf Not Defined(WORM0_Media, #PB_Module)

DeclareModule WORM0_Media
  EnableExplicit
  #rp_image_plugin_rfxl = $5246584C ; "RFXL"

  ; Five-tier ladder shared (by name) across RAU/RFXL/RFXA - section A of
  ; the codec-stack spec. Crappy=0 is the new floor tier (v1.6): renumbered
  ; from the old Low=0..Lossless=3 so Crappy sits genuinely BELOW Low for
  ; the ">="-style comparisons elsewhere in this file and in RFXLAnim
  ; (e.g. "skip palette reduction at Lossless or above"). Quality is never
  ; stored in RFXL's own bitstream (see EncodeRFXLMemory), so this
  ; renumbering has no decode-time consequence for RFXL files themselves.
  Enumeration RFXLQuality
    #Quality_Crappy = 0
    #Quality_Low = 1
    #Quality_Mid = 2
    #Quality_High = 3
    #Quality_Lossless = 4
    #Quality_Best = 4
  EndEnumeration

  Declare.i DetectMemory(*Memory, MemorySize.i)
  Declare.s GetInfoText_RFXL(*Memory, MemorySize.i, Source.s = "")
  Declare.i DecodeRFXLToRGB24(*Memory, MemorySize.i, *OutWidth.Integer = #Null, *OutHeight.Integer = #Null, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl)
  Declare.i DecodeRFXLToRGBA32(*Memory, MemorySize.i, *OutWidth.Integer = #Null, *OutHeight.Integer = #Null, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl)
  Declare.i EncodeRFXLFromRGB24(*RGB, Width.i, Height.i, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl, Quality.i = #Quality_Lossless, SourceStride.i = 0, SourcePixelBytes.i = 3)
  Declare.i EncodeRFXLFromRGBA32(*RGBA, Width.i, Height.i, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl, Quality.i = #Quality_Lossless, SourceStride.i = 0, SourcePixelBytes.i = 4)
  Declare.i CountColorsRGBA32(*RGBA, Pixels.i, MaxColors.i = 257)
  Declare.i RFXLZSSEncode(*src, size.i, *OutSize.Integer)
  Declare.i RFXLZSSDecode(*src, size.i, expected.i, *OutSize.Integer)
  Declare.i RFXRicePackEncode(*src, size.i, *OutSize.Integer)
  Declare.i RFXRicePackDecode(*src, size.i, expected.i, *OutSize.Integer)
  Declare.s GetGeneral_Image_RFXLInfo()
EndDeclareModule

Module WORM0_Media
  EnableExplicit
  #RFXL_HeaderSize = 11
  #RFXL_MaxWidth = 8192
  #RFXL_MaxHeight = 8192
  #RFXL_MaxPixels = 16777216
  #RFXL_MaxMemorySize = #RFXL_MaxPixels * 4 + 4096

  Declare.i ReadU16Mem(*mem, offset.i)
  Declare.i EncodeRFXLMemory(*RGBA, Width.i, Height.i, *OutSize.Integer, Quality.i, SourceStride.i = 0, SourcePixelBytes.i = 4)
  Declare.i CatchRFXLToRGBA(*Memory, MemorySize.i, *OutSize.Integer)

  Structure MemWriter
    *memory
    size.i
    capacity.i
  EndStructure

  Procedure.s GetGeneral_Image_RFXLInfo()
    ProcedureReturn "Module: RFXL-26; File: module_format_rfxl.pbi; memory-only image codec"
  EndProcedure

  Procedure.i RFXLDimensionsOK(width.i, height.i)
    If width <= 0 Or height <= 0 Or width > #RFXL_MaxWidth Or height > #RFXL_MaxHeight : ProcedureReturn #False : EndIf
    ProcedureReturn Bool(width <= #RFXL_MaxPixels / height)
  EndProcedure

  Procedure.i RFXCopySourceToRGBA(*SourceRGBA, Width.i, Height.i, SourceStride.i, SourcePixelBytes.i)
    Protected *rgba, x.i, y.i, srcOff.i, dstOff.i
    If *SourceRGBA = 0 Or SourcePixelBytes < 3 Or RFXLDimensionsOK(Width, Height) = #False : ProcedureReturn 0 : EndIf
    If SourceStride <= 0 : SourceStride = Width * SourcePixelBytes : EndIf
    If SourceStride < Width * SourcePixelBytes : ProcedureReturn 0 : EndIf
    *rgba = AllocateMemory(Width * Height * 4)
    If *rgba = 0 : ProcedureReturn 0 : EndIf
    For y = 0 To Height - 1
      For x = 0 To Width - 1
        srcOff = y * SourceStride + x * SourcePixelBytes
        dstOff = (y * Width + x) * 4
        PokeA(*rgba + dstOff + 0, PeekA(*SourceRGBA + srcOff + 0) & $FF)
        PokeA(*rgba + dstOff + 1, PeekA(*SourceRGBA + srcOff + 1) & $FF)
        PokeA(*rgba + dstOff + 2, PeekA(*SourceRGBA + srcOff + 2) & $FF)
        If SourcePixelBytes >= 4
          PokeA(*rgba + dstOff + 3, PeekA(*SourceRGBA + srcOff + 3) & $FF)
        Else
          PokeA(*rgba + dstOff + 3, 255)
        EndIf
      Next
    Next
    ProcedureReturn *rgba
  EndProcedure

  Procedure.i DetectMemory(*Memory, MemorySize.i)
    Protected top.i
    If *Memory = 0 Or MemorySize <= #RFXL_HeaderSize Or MemorySize > #RFXL_MaxMemorySize : ProcedureReturn #False : EndIf
    If PeekS(*Memory, 4, #PB_Ascii) <> "RFXL" : ProcedureReturn #False : EndIf
    If (PeekA(*Memory + 4) & $FF) <> 26 : ProcedureReturn #False : EndIf
    If RFXLDimensionsOK(ReadU16Mem(*Memory, 5), ReadU16Mem(*Memory, 7)) = #False : ProcedureReturn #False : EndIf
    top = PeekA(*Memory + 10) & $FF
    ProcedureReturn Bool(((PeekA(*Memory + 9) & $FF) = 3 Or (PeekA(*Memory + 9) & $FF) = 4) And (top = 0 Or top = 4))
  EndProcedure

  Procedure.s GetInfoText_RFXL(*Memory, MemorySize.i, Source.s = "")
    Protected width.i, height.i, channels.i, top.i, text.s
    If Source <> ""
      text = "Source: " + Source + #LF$
    EndIf
    text + "Bytes: " + Str(MemorySize) + #LF$
    If DetectMemory(*Memory, MemorySize) = #False
      text + "RFXL: invalid header" + #LF$
      ProcedureReturn text
    EndIf
    width = ReadU16Mem(*Memory, 5)
    height = ReadU16Mem(*Memory, 7)
    channels = PeekA(*Memory + 9) & $FF
    top = PeekA(*Memory + 10) & $FF
    text + "RFXL magic: RFXL" + #LF$
    text + "RFXL version: 26" + #LF$
    text + "RFXL width: " + Str(width) + #LF$
    text + "RFXL height: " + Str(height) + #LF$
    text + "RFXL channels: " + Str(channels) + #LF$
    text + "RFXL top: " + Str(top) + #LF$
    ProcedureReturn text
  EndProcedure

  Procedure.i DecodeRFXLToRGBA32(*Memory, MemorySize.i, *OutWidth.Integer = #Null, *OutHeight.Integer = #Null, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl)
    Protected rgbaSize.Integer, *rgba
    If *OutWidth : *OutWidth\i = 0 : EndIf
    If *OutHeight : *OutHeight\i = 0 : EndIf
    If *OutSize : *OutSize\i = 0 : EndIf
    If DetectMemory(*Memory, MemorySize) = #False : ProcedureReturn 0 : EndIf
    *rgba = CatchRFXLToRGBA(*Memory, MemorySize, @rgbaSize)
    If *rgba = 0 : ProcedureReturn 0 : EndIf
    If *OutWidth : *OutWidth\i = ReadU16Mem(*Memory, 5) : EndIf
    If *OutHeight : *OutHeight\i = ReadU16Mem(*Memory, 7) : EndIf
    If *OutSize : *OutSize\i = rgbaSize\i : EndIf
    ProcedureReturn *rgba
  EndProcedure

  Procedure.i DecodeRFXLToRGB24(*Memory, MemorySize.i, *OutWidth.Integer = #Null, *OutHeight.Integer = #Null, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl)
    Protected width.Integer, height.Integer, rgbaSize.Integer, *rgba, *rgb, pixels.i, i.i, src.i, dst.i
    *rgba = DecodeRFXLToRGBA32(*Memory, MemorySize, @width, @height, @rgbaSize, Plugin)
    If *rgba = 0 : ProcedureReturn 0 : EndIf
    pixels = width\i * height\i
    *rgb = AllocateMemory(pixels * 3)
    If *rgb = 0
      FreeMemory(*rgba) : ProcedureReturn 0
    EndIf
    For i = 0 To pixels - 1
      PokeA(*rgb + dst + 0, PeekA(*rgba + src + 0) & $FF)
      PokeA(*rgb + dst + 1, PeekA(*rgba + src + 1) & $FF)
      PokeA(*rgb + dst + 2, PeekA(*rgba + src + 2) & $FF)
      src + 4
      dst + 3
    Next
    FreeMemory(*rgba)
    If *OutWidth : *OutWidth\i = width\i : EndIf
    If *OutHeight : *OutHeight\i = height\i : EndIf
    If *OutSize : *OutSize\i = pixels * 3 : EndIf
    ProcedureReturn *rgb
  EndProcedure

  Procedure.i EncodeRFXLFromRGBA32(*RGBA, Width.i, Height.i, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl, Quality.i = #Quality_Lossless, SourceStride.i = 0, SourcePixelBytes.i = 4)
    ProcedureReturn EncodeRFXLMemory(*RGBA, Width, Height, *OutSize, Quality, SourceStride, SourcePixelBytes)
  EndProcedure

  Procedure.i EncodeRFXLFromRGB24(*RGB, Width.i, Height.i, *OutSize.Integer = #Null, Plugin.i = #rp_image_plugin_rfxl, Quality.i = #Quality_Lossless, SourceStride.i = 0, SourcePixelBytes.i = 3)
    ProcedureReturn EncodeRFXLFromRGBA32(*RGB, Width, Height, *OutSize, Plugin, Quality, SourceStride, SourcePixelBytes)
  EndProcedure

  Procedure WriteU16Mem(*mem, offset.i, value.i)
    PokeA(*mem + offset + 0, value & $FF)
    PokeA(*mem + offset + 1, (value >> 8) & $FF)
  EndProcedure

  Procedure.i ReadU16Mem(*mem, offset.i)
    ProcedureReturn (PeekA(*mem + offset) & $FF) | ((PeekA(*mem + offset + 1) & $FF) << 8)
  EndProcedure

  Procedure.i ReadU32Mem(*mem, offset.i)
    ProcedureReturn (PeekA(*mem + offset) & $FF) | ((PeekA(*mem + offset + 1) & $FF) << 8) | ((PeekA(*mem + offset + 2) & $FF) << 16) | ((PeekA(*mem + offset + 3) & $FF) << 24)
  EndProcedure

  Procedure WriteU32Mem(*mem, offset.i, value.q)
    PokeA(*mem + offset + 0, value & $FF)
    PokeA(*mem + offset + 1, (value >> 8) & $FF)
    PokeA(*mem + offset + 2, (value >> 16) & $FF)
    PokeA(*mem + offset + 3, (value >> 24) & $FF)
  EndProcedure

  Procedure.i MW_Init(*mw.MemWriter, capacity.i)
    *mw\memory = AllocateMemory(capacity)
    If *mw\memory = 0 : ProcedureReturn #False : EndIf
    *mw\size = 0
    *mw\capacity = capacity
    ProcedureReturn #True
  EndProcedure

  Procedure.i MW_Reserve(*mw.MemWriter, addBytes.i)
    Protected newCapacity.i, *newMemory
    If *mw\size + addBytes <= *mw\capacity : ProcedureReturn #True : EndIf
    newCapacity = *mw\capacity * 2
    If newCapacity < *mw\size + addBytes : newCapacity = *mw\size + addBytes + 4096 : EndIf
    *newMemory = ReAllocateMemory(*mw\memory, newCapacity)
    If *newMemory = 0 : ProcedureReturn #False : EndIf
    *mw\memory = *newMemory
    *mw\capacity = newCapacity
    ProcedureReturn #True
  EndProcedure

  Procedure.i MW_Byte(*mw.MemWriter, value.i)
    If MW_Reserve(*mw, 1) = #False : ProcedureReturn #False : EndIf
    PokeA(*mw\memory + *mw\size, value & $FF)
    *mw\size + 1
    ProcedureReturn #True
  EndProcedure

  Procedure.i MW_U16(*mw.MemWriter, value.i)
    If MW_Reserve(*mw, 2) = #False : ProcedureReturn #False : EndIf
    WriteU16Mem(*mw\memory, *mw\size, value)
    *mw\size + 2
    ProcedureReturn #True
  EndProcedure

  Procedure.i MW_U32(*mw.MemWriter, value.q)
    If MW_Reserve(*mw, 4) = #False : ProcedureReturn #False : EndIf
    WriteU32Mem(*mw\memory, *mw\size, value)
    *mw\size + 4
    ProcedureReturn #True
  EndProcedure

  Procedure.i MW_Data(*mw.MemWriter, *data, bytes.i)
    If bytes <= 0 : ProcedureReturn #True : EndIf
    If MW_Reserve(*mw, bytes) = #False : ProcedureReturn #False : EndIf
    CopyMemory(*data, *mw\memory + *mw\size, bytes)
    *mw\size + bytes
    ProcedureReturn #True
  EndProcedure

  Procedure.i RFXEmitHeaderMem(*mw.MemWriter, width.i, height.i, top.i, channels.i = 4)
    If MW_Byte(*mw, 'R') = #False : ProcedureReturn #False : EndIf
    If MW_Byte(*mw, 'F') = #False : ProcedureReturn #False : EndIf
    If MW_Byte(*mw, 'X') = #False : ProcedureReturn #False : EndIf
    If MW_Byte(*mw, 'L') = #False : ProcedureReturn #False : EndIf
    If MW_Byte(*mw, 26) = #False : ProcedureReturn #False : EndIf
    If MW_U16(*mw, width) = #False : ProcedureReturn #False : EndIf
    If MW_U16(*mw, height) = #False : ProcedureReturn #False : EndIf
    If MW_Byte(*mw, channels) = #False : ProcedureReturn #False : EndIf
    If MW_Byte(*mw, top) = #False : ProcedureReturn #False : EndIf
    ProcedureReturn #True
  EndProcedure

  Procedure.i QuantizeByte(v.i, Quality.i)
    Select Quality
      Case #Quality_Crappy
        ProcedureReturn (v >> 6) << 6
      Case #Quality_Low
        ProcedureReturn (v >> 5) << 5
      Case #Quality_Mid
        ProcedureReturn (v >> 4) << 4
      Case #Quality_High
        ProcedureReturn (v >> 3) << 3
    EndSelect
    ProcedureReturn v
  EndProcedure

  Procedure QuantizeRGBA(*rgba, size.i, Quality.i)
    Protected i.i
    If Quality = #Quality_Lossless : ProcedureReturn : EndIf
    For i = 0 To size - 1 Step 4
      PokeA(*rgba + i + 0, QuantizeByte(PeekA(*rgba + i + 0) & $FF, Quality))
      PokeA(*rgba + i + 1, QuantizeByte(PeekA(*rgba + i + 1) & $FF, Quality))
      PokeA(*rgba + i + 2, QuantizeByte(PeekA(*rgba + i + 2) & $FF, Quality))
    Next
  EndProcedure

  Procedure.i RFXHasAlpha(*rgba, pixels.i)
    Protected i.i
    For i = 0 To pixels - 1
      If (PeekA(*rgba + i * 4 + 3) & $FF) <> 255 : ProcedureReturn #True : EndIf
    Next
    ProcedureReturn #False
  EndProcedure

  Procedure.i RFXPaletteColorCount(*rgba, pixels.i, maxColors.i)
    Protected i.i, count.i, color.l, h.i, probe.i, slot.i
    Protected Dim used.b(1023)
    Protected Dim key.l(1023)
    If *rgba = 0 Or pixels <= 0 Or maxColors <= 0 : ProcedureReturn 0 : EndIf
    For i = 0 To pixels - 1
      color = PeekL(*rgba + i * 4)
      h = (color ! (color >> 8) ! (color >> 16) ! (color >> 24)) & 1023
      For probe = 0 To 1023
        slot = (h + probe) & 1023
        If used(slot) = 0
          used(slot) = 1
          key(slot) = color
          count + 1
          If count > maxColors : ProcedureReturn count : EndIf
          Break
        ElseIf key(slot) = color
          Break
        EndIf
      Next
    Next
    ProcedureReturn count
  EndProcedure

  Procedure.i CountColorsRGBA32(*RGBA, Pixels.i, MaxColors.i = 257)
    ProcedureReturn RFXPaletteColorCount(*RGBA, Pixels, MaxColors)
  EndProcedure

  Procedure.i RFXBytesPerPixel(format.i)
    Select format
      Case 7
        ProcedureReturn 1 ; RGB332
      Case 2, 3
        ProcedureReturn 2 ; RGB444A4 / RGB565
      Case 9, 10
        ProcedureReturn 3 ; RGB888 / planar RGB888
    EndSelect
    ProcedureReturn 4 ; RGBA8888
  EndProcedure

  Procedure.i RFXFormatForQuality(Quality.i, hasAlpha.i)
    Select Quality
      Case #Quality_Crappy, #Quality_Low
        ProcedureReturn 7 ; reuse the existing RGB332 path - no new pixel format for Crappy
      Case #Quality_Mid
        ProcedureReturn 2
      Case #Quality_High
        ProcedureReturn 3
    EndSelect
    If hasAlpha = #False : ProcedureReturn 9 : EndIf
    ProcedureReturn 0
  EndProcedure

  Procedure.i RFXBuildPixelStream(*rgba, pixels.i, Quality.i, hasAlpha.i, *OutSize.Integer, *OutFormat.Integer)
    Protected format.i = RFXFormatForQuality(Quality, hasAlpha)
    Protected bpp.i, *stream
    Protected i.i, r.i, g.i, b.i, a.i, v.i
    bpp = RFXBytesPerPixel(format)
    *stream = AllocateMemory(pixels * bpp)
    If *stream = 0 : ProcedureReturn 0 : EndIf
    For i = 0 To pixels - 1
      r = PeekA(*rgba + i * 4 + 0) & $FF
      g = PeekA(*rgba + i * 4 + 1) & $FF
      b = PeekA(*rgba + i * 4 + 2) & $FF
      a = PeekA(*rgba + i * 4 + 3) & $FF
      Select format
        Case 7
          ; True RGB332 (3+3+2 = 8 bits, fits exactly in one byte). The
          ; previous packing used <<6/<<3/<<5 as if it were RGB333 (9
          ; bits of color crammed into 8), so red's top bit silently
          ; fell off the & $FF truncation for any r>=128 - corrupting
          ; the red channel on bright/warm content and giving Low-
          ; quality output a systematic blue/green tint. 2 bits for blue
          ; instead of 3 is still a real quality loss vs the old (buggy)
          ; scheme, but a correct one instead of wraparound corruption.
          v = ((r >> 5) << 5) | ((g >> 5) << 2) | (b >> 6)
          PokeA(*stream + i, v & $FF)
        Case 2
          PokeA(*stream + i * 2 + 0, ((r & $F0) | (g >> 4)) & $FF)
          PokeA(*stream + i * 2 + 1, ((b & $F0) | (a >> 4)) & $FF)
        Case 3
          v = ((r & $F8) << 8) | ((g & $FC) << 3) | (b >> 3)
          PokeA(*stream + i * 2 + 0, v & $FF)
          PokeA(*stream + i * 2 + 1, (v >> 8) & $FF)
        Case 9
          PokeA(*stream + i * 3 + 0, r)
          PokeA(*stream + i * 3 + 1, g)
          PokeA(*stream + i * 3 + 2, b)
        Default
          PokeA(*stream + i * 4 + 0, r)
          PokeA(*stream + i * 4 + 1, g)
          PokeA(*stream + i * 4 + 2, b)
          PokeA(*stream + i * 4 + 3, a)
      EndSelect
    Next
    If *OutSize : *OutSize\i = pixels * bpp : EndIf
    If *OutFormat : *OutFormat\i = format : EndIf
    ProcedureReturn *stream
  EndProcedure

  Procedure.i RFXBuildPlanarStream(*rgba, pixels.i, hasAlpha.i, *OutSize.Integer, *OutFormat.Integer)
    Protected planes.i = 3 + Bool(hasAlpha)
    Protected *stream = AllocateMemory(pixels * planes)
    Protected i.i
    If *stream = 0 : ProcedureReturn 0 : EndIf
    For i = 0 To pixels - 1
      PokeA(*stream + i, PeekA(*rgba + i * 4 + 0) & $FF)
      PokeA(*stream + pixels + i, PeekA(*rgba + i * 4 + 1) & $FF)
      PokeA(*stream + pixels * 2 + i, PeekA(*rgba + i * 4 + 2) & $FF)
      If hasAlpha : PokeA(*stream + pixels * 3 + i, PeekA(*rgba + i * 4 + 3) & $FF) : EndIf
    Next
    If *OutSize : *OutSize\i = pixels * planes : EndIf
    If *OutFormat : *OutFormat\i = 8 + Bool(hasAlpha = #False) * 2 : EndIf
    ProcedureReturn *stream
  EndProcedure

  Procedure RFXDecodePixelStream(*stream, pixels.i, format.i, *rgba)
    Protected i.i, v.i, r.i, g.i, b.i, a.i
    Select format
      Case 7
        For i = 0 To pixels - 1
          v = PeekA(*stream + i) & $FF
          PokeA(*rgba + i * 4 + 0, (((v >> 5) & 7) * 255) / 7)
          PokeA(*rgba + i * 4 + 1, (((v >> 2) & 7) * 255) / 7)
          PokeA(*rgba + i * 4 + 2, ((v & 3) * 255) / 3)
          PokeA(*rgba + i * 4 + 3, 255)
        Next
      Case 2
        For i = 0 To pixels - 1
          v = PeekA(*stream + i * 2 + 0) & $FF
          r = v & $F0
          g = (v << 4) & $F0
          v = PeekA(*stream + i * 2 + 1) & $FF
          b = v & $F0
          a = (v << 4) & $F0
          If a = 240 : a = 255 : EndIf
          PokeA(*rgba + i * 4 + 0, r)
          PokeA(*rgba + i * 4 + 1, g)
          PokeA(*rgba + i * 4 + 2, b)
          PokeA(*rgba + i * 4 + 3, a)
        Next
      Case 3
        For i = 0 To pixels - 1
          v = (PeekA(*stream + i * 2 + 0) & $FF) | ((PeekA(*stream + i * 2 + 1) & $FF) << 8)
          PokeA(*rgba + i * 4 + 0, (v >> 8) & $F8)
          PokeA(*rgba + i * 4 + 1, (v >> 3) & $FC)
          PokeA(*rgba + i * 4 + 2, (v << 3) & $F8)
          PokeA(*rgba + i * 4 + 3, 255)
        Next
      Case 8
        For i = 0 To pixels - 1
          PokeA(*rgba + i * 4 + 0, PeekA(*stream + i) & $FF)
          PokeA(*rgba + i * 4 + 1, PeekA(*stream + pixels + i) & $FF)
          PokeA(*rgba + i * 4 + 2, PeekA(*stream + pixels * 2 + i) & $FF)
          PokeA(*rgba + i * 4 + 3, PeekA(*stream + pixels * 3 + i) & $FF)
        Next
      Case 9
        For i = 0 To pixels - 1
          PokeA(*rgba + i * 4 + 0, PeekA(*stream + i * 3 + 0) & $FF)
          PokeA(*rgba + i * 4 + 1, PeekA(*stream + i * 3 + 1) & $FF)
          PokeA(*rgba + i * 4 + 2, PeekA(*stream + i * 3 + 2) & $FF)
          PokeA(*rgba + i * 4 + 3, 255)
        Next
      Case 10
        For i = 0 To pixels - 1
          PokeA(*rgba + i * 4 + 0, PeekA(*stream + i) & $FF)
          PokeA(*rgba + i * 4 + 1, PeekA(*stream + pixels + i) & $FF)
          PokeA(*rgba + i * 4 + 2, PeekA(*stream + pixels * 2 + i) & $FF)
          PokeA(*rgba + i * 4 + 3, 255)
        Next
      Default
        For i = 0 To pixels - 1
          PokeA(*rgba + i * 4 + 0, PeekA(*stream + i * 4 + 0) & $FF)
          PokeA(*rgba + i * 4 + 1, PeekA(*stream + i * 4 + 1) & $FF)
          PokeA(*rgba + i * 4 + 2, PeekA(*stream + i * 4 + 2) & $FF)
          PokeA(*rgba + i * 4 + 3, PeekA(*stream + i * 4 + 3) & $FF)
        Next
    EndSelect
  EndProcedure

  Procedure RFXPredict(*src, *dst, w.i, h.i, bpp.i, mode.i)
    Protected x.i, y.i, c.i, idx.i, p.i, l.i, u.i, ul.i
    For y = 0 To h - 1
      For x = 0 To w - 1
        For c = 0 To bpp - 1
          idx = (y * w + x) * bpp + c
          l = 0 : u = 0 : ul = 0
          If x > 0 : l = PeekA(*src + idx - bpp) & $FF : EndIf
          If y > 0 : u = PeekA(*src + idx - w * bpp) & $FF : EndIf
          If x > 0 And y > 0 : ul = PeekA(*src + idx - w * bpp - bpp) & $FF : EndIf
          Select mode
            Case 1 : p = l
            Case 2 : p = u
            Case 3 : p = l + u - ul
            Case 4
              If ul >= l And ul >= u
                p = l : If u < p : p = u : EndIf
              ElseIf ul <= l And ul <= u
                p = l : If u > p : p = u : EndIf
              Else
                p = l + u - ul
              EndIf
            Default : p = 0
          EndSelect
          PokeA(*dst + idx, ((PeekA(*src + idx) & $FF) - p) & $FF)
        Next
      Next
    Next
  EndProcedure

  Procedure RFXUnpredict(*src, *dst, w.i, h.i, bpp.i, mode.i)
    Protected x.i, y.i, c.i, idx.i, p.i, l.i, u.i, ul.i

    ; Row 0 has no U/UL neighbor (u=0, ul=0 always) - fold that into the
    ; per-mode formula once instead of checking "If y>0" on every pixel.
    ; (Verified per mode: with u=0,ul=0 the general formula collapses to
    ; p=l for modes 1/3/4, p=0 for mode 2/default - mode 4's min/max
    ; selection reduces to l for every l>=0 when u=ul=0.)
    For x = 0 To w - 1
      For c = 0 To bpp - 1
        idx = x * bpp + c
        l = 0 : If x > 0 : l = PeekA(*dst + idx - bpp) & $FF : EndIf
        Select mode
          Case 1 : p = l
          Case 2 : p = 0
          Case 3 : p = l
          Case 4 : p = l
          Default : p = 0
        EndSelect
        PokeA(*dst + idx, ((PeekA(*src + idx) & $FF) + p) & $FF)
      Next
    Next

    ; Rows 1..h-1: column 0 has no L/UL neighbor (l=0, ul=0) - same fold,
    ; collapses to p=u for modes 2/3/4, p=0 for mode 1/default. Columns
    ; 1..w-1 then never touch an out-of-bounds neighbor, so the interior
    ; loop drops all three boundary checks entirely.
    For y = 1 To h - 1
      idx = (y * w) * bpp
      For c = 0 To bpp - 1
        u = PeekA(*dst + idx + c - w * bpp) & $FF
        Select mode
          Case 1 : p = 0
          Case 2 : p = u
          Case 3 : p = u
          Case 4 : p = u
          Default : p = 0
        EndSelect
        PokeA(*dst + idx + c, ((PeekA(*src + idx + c) & $FF) + p) & $FF)
      Next
      ; mode is the same for every pixel in the whole call, so dispatch it
      ; once per row instead of once per pixel-channel.
      Select mode
        Case 1
          For x = 1 To w - 1
            idx = (y * w + x) * bpp
            For c = 0 To bpp - 1
              PokeA(*dst + idx + c, ((PeekA(*src + idx + c) & $FF) + (PeekA(*dst + idx + c - bpp) & $FF)) & $FF)
            Next
          Next
        Case 2
          For x = 1 To w - 1
            idx = (y * w + x) * bpp
            For c = 0 To bpp - 1
              PokeA(*dst + idx + c, ((PeekA(*src + idx + c) & $FF) + (PeekA(*dst + idx + c - w * bpp) & $FF)) & $FF)
            Next
          Next
        Case 3
          For x = 1 To w - 1
            idx = (y * w + x) * bpp
            For c = 0 To bpp - 1
              l = PeekA(*dst + idx + c - bpp) & $FF
              u = PeekA(*dst + idx + c - w * bpp) & $FF
              ul = PeekA(*dst + idx + c - w * bpp - bpp) & $FF
              PokeA(*dst + idx + c, ((PeekA(*src + idx + c) & $FF) + (l + u - ul)) & $FF)
            Next
          Next
        Case 4
          For x = 1 To w - 1
            idx = (y * w + x) * bpp
            For c = 0 To bpp - 1
              l = PeekA(*dst + idx + c - bpp) & $FF
              u = PeekA(*dst + idx + c - w * bpp) & $FF
              ul = PeekA(*dst + idx + c - w * bpp - bpp) & $FF
              If ul >= l And ul >= u
                p = l : If u < p : p = u : EndIf
              ElseIf ul <= l And ul <= u
                p = l : If u > p : p = u : EndIf
              Else
                p = l + u - ul
              EndIf
              PokeA(*dst + idx + c, ((PeekA(*src + idx + c) & $FF) + p) & $FF)
            Next
          Next
        Default
          For x = 1 To w - 1
            idx = (y * w + x) * bpp
            For c = 0 To bpp - 1
              PokeA(*dst + idx + c, PeekA(*src + idx + c) & $FF)
            Next
          Next
      EndSelect
    Next
  EndProcedure

  Procedure.i RFXRLEEncode(*src, size.i, *OutSize.Integer)
    Protected *dst = AllocateMemory(size + size / 128 + 256)
    Protected in.i, out.i, run.i, litStart.i, litCount.i
    If *dst = 0 : ProcedureReturn 0 : EndIf
    While in < size
      run = 1
      While in + run < size And run < 127 And PeekA(*src + in + run) = PeekA(*src + in)
        run + 1
      Wend
      If run >= 4
        PokeA(*dst + out, $80 | run) : out + 1
        PokeA(*dst + out, PeekA(*src + in) & $FF) : out + 1
        in + run
      Else
        litStart = in
        litCount = 0
        Repeat
          in + 1 : litCount + 1
          run = 1
          While in + run < size And run < 127 And PeekA(*src + in + run) = PeekA(*src + in)
            run + 1
          Wend
        Until in >= size Or litCount = 127 Or run >= 4
        PokeA(*dst + out, litCount) : out + 1
        CopyMemory(*src + litStart, *dst + out, litCount) : out + litCount
      EndIf
    Wend
    If *OutSize : *OutSize\i = out : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure.i RFXRLEDecode(*src, size.i, expected.i, *OutSize.Integer)
    Protected *dst = AllocateMemory(expected)
    Protected in.i, out.i, token.i, count.i, value.i
    If *dst = 0 : ProcedureReturn 0 : EndIf
    While in < size And out < expected
      token = PeekA(*src + in) & $FF : in + 1
      If token & $80
        count = token & $7F
        If in >= size Or out + count > expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
        value = PeekA(*src + in) & $FF : in + 1
        FillMemory(*dst + out, count, value) : out + count
      Else
        count = token
        If count <= 0 Or in + count > size Or out + count > expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
        CopyMemory(*src + in, *dst + out, count)
        in + count : out + count
      EndIf
    Wend
    If out <> expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
    If *OutSize : *OutSize\i = out : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure.i RFXSmallResidualByte(v.i)
    ProcedureReturn Bool(v <= 7 Or v >= 248)
  EndProcedure

  Procedure.i RFXBytePackEncode(*src, size.i, *OutSize.Integer)
    Protected *dst = AllocateMemory(size * 2 + 256)
    Protected in.i, out.i, run.i, count.i, litStart.i, probe.i
    Protected nib.i, v.i, packed.i
    If *dst = 0 : ProcedureReturn 0 : EndIf
    While in < size
      run = 1
      While in + run < size And run < 64 And PeekA(*src + in + run) = PeekA(*src + in)
        run + 1
      Wend
      If PeekA(*src + in) = 0 And run >= 3
        PokeA(*dst + out, $40 | (run - 1)) : out + 1
        in + run
      ElseIf run >= 4
        PokeA(*dst + out, $80 | (run - 1)) : out + 1
        PokeA(*dst + out, PeekA(*src + in) & $FF) : out + 1
        in + run
      Else
        count = 0
        While in + count < size And count < 64 And RFXSmallResidualByte(PeekA(*src + in + count) & $FF)
          count + 1
        Wend
        If count >= 4
          PokeA(*dst + out, $C0 | (count - 1)) : out + 1
          For nib = 0 To count - 1 Step 2
            v = PeekA(*src + in + nib) & $FF
            If v >= 248 : v - 256 : EndIf
            packed = v & $0F
            If nib + 1 < count
              v = PeekA(*src + in + nib + 1) & $FF
              If v >= 248 : v - 256 : EndIf
              packed | ((v & $0F) << 4)
            EndIf
            PokeA(*dst + out, packed) : out + 1
          Next
          in + count
        Else
          litStart = in
          count = 0
          Repeat
            in + 1 : count + 1
            run = 1
            While in + run < size And run < 64 And PeekA(*src + in + run) = PeekA(*src + in)
              run + 1
            Wend
            probe = 0
            While in + probe < size And probe < 64 And RFXSmallResidualByte(PeekA(*src + in + probe) & $FF)
              probe + 1
            Wend
          Until in >= size Or count = 64 Or (PeekA(*src + in) = 0 And run >= 3) Or run >= 4 Or probe >= 4
          PokeA(*dst + out, count - 1) : out + 1
          CopyMemory(*src + litStart, *dst + out, count) : out + count
        EndIf
      EndIf
    Wend
    If *OutSize : *OutSize\i = out : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure.i RFXBytePackDecode(*src, size.i, expected.i, *OutSize.Integer)
    Protected *dst = AllocateMemory(expected)
    Protected in.i, out.i, token.i, count.i, kind.i, i.i, packed.i, nib.i, v.i
    If *dst = 0 : ProcedureReturn 0 : EndIf
    While in < size And out < expected
      token = PeekA(*src + in) & $FF : in + 1
      kind = token & $C0
      count = (token & $3F) + 1
      Select kind
        Case $00
          If in + count > size Or out + count > expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
          CopyMemory(*src + in, *dst + out, count)
          in + count : out + count
        Case $40
          If out + count > expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
          FillMemory(*dst + out, count, 0) : out + count
        Case $80
          If in >= size Or out + count > expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
          v = PeekA(*src + in) & $FF : in + 1
          FillMemory(*dst + out, count, v) : out + count
        Case $C0
          If in + ((count + 1) / 2) > size Or out + count > expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
          For i = 0 To count - 1
            packed = PeekA(*src + in + i / 2) & $FF
            If i & 1
              nib = (packed >> 4) & $0F
            Else
              nib = packed & $0F
            EndIf
            If nib >= 8
              v = nib - 16
            Else
              v = nib
            EndIf
            PokeA(*dst + out, v & $FF) : out + 1
          Next
          in + ((count + 1) / 2)
      EndSelect
    Wend
    If out <> expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
    If *OutSize : *OutSize\i = out : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure.i RFXLZSSHash(*src)
    ProcedureReturn (((PeekA(*src) & $FF) << 4) ! ((PeekA(*src + 1) & $FF) << 2) ! (PeekA(*src + 2) & $FF)) & 4095
  EndProcedure

  Procedure RFXLZSSInsert(*src, size.i, pos.i, *head, *prev)
    Protected h.i
    If pos + 2 >= size : ProcedureReturn : EndIf
    h = RFXLZSSHash(*src + pos)
    PokeL(*prev + pos * 4, PeekL(*head + h * 4))
    PokeL(*head + h * 4, pos)
  EndProcedure

  ; Chain-search depth cap: without this, pathological/repetitive input
  ; (e.g. many positions sharing the same 3-byte hash bucket, as in flat
  ; icon/pixel-art blocks) can make the prev-linked hash chain at a single
  ; position walk deep into the 4096-byte window, and lazy matching
  ; doubles that cost (it searches both `pos` and `pos+1`).
  ; Re-tuned after the length escape below made matches unbounded: a
  ; deep chain search matters much less now, since even a candidate found
  ; early in a short scan typically extends far via the chunked compare
  ; in RFXLZSSBestMatch, so a much smaller cap barely costs any
  ; compression. Measured on an adversarial 800x800 hash-collision image
  ; (every 4-byte group shares one hash bucket, real match length stays
  ; short so the "found the longest possible match" early-exit rarely
  ; fires): 2048 hops cost ~6.4s to encode vs the pre-length-escape
  ; baseline's 4.1s (+55%); 256 hops brings that to ~4.6s (+14%) while
  ; only costing 27 bytes total across the whole 8-image/4-quality-level
  ; pipeline suite (185506 vs 185479 - 0.015%). Smaller still (64/128)
  ; recovers more of the timing gap for a slightly larger compression
  ; cost; 256 was the best balance found.
  #RFXLZSS_MaxChainHops = 256

  ; See RFXLZSSEncode's match-commit branch for why this exists (only
  ; needed once match length became unbounded via the length escape).
  #RFXLZSS_MaxInsertsPerMatch = 128

  ; Match length is no longer capped at 19: RFXLZSSEncode/Decode use a
  ; length-escape (nibble 15 = "keep reading LZ4-style continuation
  ; bytes") so a run can be arbitrarily long, letting one token cover an
  ; entire flat/repeated block instead of forcing it into ceil(n/19)
  ; separate 19-byte tokens. Offset is still capped at the 4096-byte
  ; window (unrelated to length), so maxLen only needs to stay within
  ; the remaining buffer.
  Procedure.i RFXLZSSBestMatch(*src, size.i, pos.i, *head, *prev, *BestOff.Integer)
    Protected searchStart.i = pos - 4096, p.i, l.i, maxLen.i, bestLen.i, h.i, chunk.i
    Protected hops.i = #RFXLZSS_MaxChainHops
    If *BestOff : *BestOff\i = 0 : EndIf
    If searchStart < 0 : searchStart = 0 : EndIf
    maxLen = size - pos
    If maxLen < 4 : ProcedureReturn 0 : EndIf
    h = RFXLZSSHash(*src + pos)
    p = PeekL(*head + h * 4)
    While p >= searchStart And p >= 0 And hops > 0
      hops - 1
      If PeekA(*src + p) = PeekA(*src + pos) And PeekA(*src + p + 1) = PeekA(*src + pos + 1) And PeekA(*src + p + 2) = PeekA(*src + pos + 2)
        l = 3
        ; Match length is unbounded now, so a genuinely long run (a solid
        ; fill, a big flat block) can extend for millions of bytes - a
        ; byte-by-byte PeekA loop out there means millions of individual
        ; Peek calls for a single candidate. Grow the compare window
        ; (doubling, capped) and confirm each chunk with one CompareMemory
        ; call instead; only the final short remainder falls back to
        ; byte-by-byte to pin down the exact boundary.
        chunk = 32
        While l + chunk <= maxLen And CompareMemory(*src + p + l, *src + pos + l, chunk)
          l + chunk
          If chunk < 8192 : chunk * 2 : EndIf
        Wend
        While l < maxLen And PeekA(*src + p + l) = PeekA(*src + pos + l)
          l + 1
        Wend
        If l > bestLen
          bestLen = l
          If *BestOff : *BestOff\i = pos - p : EndIf
          If bestLen = maxLen : Break : EndIf
        EndIf
      EndIf
      p = PeekL(*prev + p * 4)
    Wend
    ProcedureReturn bestLen
  EndProcedure

  Procedure.i RFXLZSSEncode(*src, size.i, *OutSize.Integer)
    Protected *dst
    Protected *head, *prev
    Protected in.i, out.i, flagPos.i, flags.i, flagBit.i, p.i
    Protected bestLen.i, bestOff.Integer, off.i, nib.i, extra.i
    Protected nextLen.i, nextOff.Integer
    ; One-step lazy matching: before committing a match found at `in`, also
    ; check `in+1`. Every match token here costs a fixed 2 bytes regardless
    ; of length, so "longer" alone doesn't mean "cheaper" - deferring to
    ; in+1 costs 1 literal byte + the in+1 match's 2 bytes for (1+nextLen)
    ; source bytes, versus 2 bytes for bestLen source bytes now. Compare
    ; bytes-per-source-byte (3*bestLen vs 2*(1+nextLen)) rather than raw
    ; length, otherwise a marginally longer in+1 match at large bestLen
    ; (e.g. bestLen=10, nextLen=11) gets picked even though it's the worse
    ; deal. Bitstream/decoder are untouched: this only changes which
    ; (already-legal) tokens the encoder picks.
    Protected pendingValid.i, pendingLen.i, pendingOff.Integer, pendingPos.i
    If *OutSize : *OutSize\i = 0 : EndIf
    If *src = 0 Or size <= 0 : ProcedureReturn 0 : EndIf
    *dst = AllocateMemory(size + size / 8 + 512)
    If *dst = 0 : ProcedureReturn 0 : EndIf
    *head = AllocateMemory(4096 * 4)
    *prev = AllocateMemory(size * 4)
    If *head = 0 Or *prev = 0
      If *head : FreeMemory(*head) : EndIf
      If *prev : FreeMemory(*prev) : EndIf
      FreeMemory(*dst) : ProcedureReturn 0
    EndIf
    FillMemory(*head, 4096 * 4, $FF)
    FillMemory(*prev, size * 4, $FF)
    While in < size
      flagPos = out : out + 1 : flags = 0 : flagBit = 0
      While flagBit < 8 And in < size
        If pendingValid And pendingPos = in
          bestLen = pendingLen
          bestOff\i = pendingOff\i
        Else
          bestLen = RFXLZSSBestMatch(*src, size, in, *head, *prev, @bestOff)
        EndIf
        pendingValid = #False
        If bestLen >= 4
          RFXLZSSInsert(*src, size, in, *head, *prev)
          If in + 1 < size
            nextLen = RFXLZSSBestMatch(*src, size, in + 1, *head, *prev, @nextOff)
          Else
            nextLen = 0
          EndIf
          If nextLen >= 4 And 2 * (1 + nextLen) > 3 * bestLen
            PokeA(*dst + out, PeekA(*src + in) & $FF) : out + 1
            in + 1
            pendingValid = #True : pendingLen = nextLen : pendingOff\i = nextOff\i : pendingPos = in
          Else
            flags | (1 << flagBit)
            off = bestOff\i - 1
            ; nibble 0-14 => length 4-18 directly. nibble 15 => length
            ; escape: length is 19 plus LZ4-style continuation bytes (each
            ; 255 means "add 255, more follow"; the first byte <255 ends
            ; it, so length 19 itself needs one continuation byte of 0).
            If bestLen <= 18
              nib = bestLen - 4
            Else
              nib = 15
            EndIf
            PokeA(*dst + out, (nib << 4) | ((off >> 8) & $0F)) : out + 1
            PokeA(*dst + out, off & $FF) : out + 1
            If nib = 15
              extra = bestLen - 19
              While extra >= 255
                PokeA(*dst + out, 255) : out + 1
                extra - 255
              Wend
              PokeA(*dst + out, extra) : out + 1
            EndIf
            ; Match length is now unbounded (length-escape above), so a
            ; single match inside a large uniform/repeated region (a solid
            ; fill, a big flat icon block) can span millions of bytes.
            ; Inserting every one of those positions into the hash chain
            ; is wasted work once `in` jumps straight past all of them -
            ; none of them will ever be queried again as a search position
            ; before the next literal/match starts. Cap it (standard
            ; long-match technique, same idea as zlib's deflate): insert
            ; only the first #RFXLZSS_MaxInsertsPerMatch positions, still
            ; giving future matches into this region something to find.
            For p = in + 1 To in + bestLen - 1
              If p - in > #RFXLZSS_MaxInsertsPerMatch : Break : EndIf
              RFXLZSSInsert(*src, size, p, *head, *prev)
            Next
            in + bestLen
          EndIf
        Else
          PokeA(*dst + out, PeekA(*src + in) & $FF) : out + 1
          RFXLZSSInsert(*src, size, in, *head, *prev)
          in + 1
        EndIf
        flagBit + 1
      Wend
      PokeA(*dst + flagPos, flags)
    Wend
    FreeMemory(*head)
    FreeMemory(*prev)
    If *OutSize : *OutSize\i = out : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure.i RFXLZSSDecode(*src, size.i, expected.i, *OutSize.Integer)
    Protected *dst
    Protected in.i, out.i, flags.i, flagBit.i, b1.i, b2.i, count.i, off.i, srcPos.i, i.i, nib.i, extraByte.i
    If *OutSize : *OutSize\i = 0 : EndIf
    If *src = 0 Or size <= 0 Or expected <= 0 : ProcedureReturn 0 : EndIf
    *dst = AllocateMemory(expected)
    If *dst = 0 : ProcedureReturn 0 : EndIf
    While in < size And out < expected
      flags = PeekA(*src + in) & $FF : in + 1
      For flagBit = 0 To 7
        If out >= expected : Break : EndIf
        If flags & (1 << flagBit)
          If in + 2 > size : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
          b1 = PeekA(*src + in) & $FF : b2 = PeekA(*src + in + 1) & $FF : in + 2
          nib = (b1 >> 4) & $0F
          If nib = 15
            count = 19
            Repeat
              If in >= size : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
              extraByte = PeekA(*src + in) & $FF : in + 1
              count + extraByte
            Until extraByte <> 255
          Else
            count = nib + 4
          EndIf
          off = (((b1 & $0F) << 8) | b2) + 1
          srcPos = out - off
          If srcPos < 0 Or out + count > expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
          For i = 0 To count - 1
            PokeA(*dst + out, PeekA(*dst + srcPos + i) & $FF)
            out + 1
          Next
        Else
          If in >= size : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
          PokeA(*dst + out, PeekA(*src + in) & $FF) : in + 1 : out + 1
        EndIf
      Next
    Wend
    If out <> expected : FreeMemory(*dst) : ProcedureReturn 0 : EndIf
    If *OutSize : *OutSize\i = out : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure RFXMemWriteBits(*dst, *OutByte.Integer, *OutBit.Integer, value.q, count.i)
    Protected i.i
    For i = 0 To count - 1
      If (value >> i) & 1
        PokeA(*dst + *OutByte\i, (PeekA(*dst + *OutByte\i) & $FF) | (1 << *OutBit\i))
      EndIf
      *OutBit\i + 1
      If *OutBit\i = 8
        *OutBit\i = 0
        *OutByte\i + 1
      EndIf
    Next
  EndProcedure

  Procedure.q RFXMemReadBits(*src, size.i, *InByte.Integer, *InBit.Integer, count.i, *Ok.Integer)
    Protected i.i, result.q
    If *Ok = 0 Or *Ok\i = 0 : ProcedureReturn 0 : EndIf
    For i = 0 To count - 1
      If *InByte\i >= size
        *Ok\i = 0
        ProcedureReturn result
      EndIf
      If (PeekA(*src + *InByte\i) & $FF) & (1 << *InBit\i)
        result | (1 << i)
      EndIf
      *InBit\i + 1
      If *InBit\i = 8
        *InBit\i = 0
        *InByte\i + 1
      EndIf
    Next
    ProcedureReturn result
  EndProcedure

  Procedure.i RFXSignedByte(v.i)
    v & $FF
    If v >= 128 : v - 256 : EndIf
    ProcedureReturn v
  EndProcedure

  Procedure.i RFXZZEncode8(v.i)
    ProcedureReturn ((v << 1) ! (v >> 31)) & $FF
  EndProcedure

  Procedure.i RFXZZDecode8(z.i)
    If z & 1
      ProcedureReturn -((z >> 1) + 1)
    EndIf
    ProcedureReturn z >> 1
  EndProcedure

  ; Rice outlier escape (same rationale as RAU's #RiceEscQ): once the unary
  ; quotient reaches #RFXRiceEscQ, both writer and estimator switch to a
  ; fixed #RFXRiceEscBits-wide raw zigzag byte instead of continuing the
  ; unary run - bounds the cost of a single large residual (e.g. a hard
  ; edge) without punishing the rest of the block's chosen k. z is always
  ; an 8-bit zigzag value here (RFXZZEncode8 masks to $FF), so 8 raw bits
  ; are always enough. Flat threshold (not k-dependent) keeps decoding a
  ; single "did the unary run reach the cap" check.
  ; #RFXRiceEscQ=24 was RAU's #RiceEscQ=48 halved as a placeholder - but
  ; RAU escapes 16bit+ PCM samples while RFXL always escapes an 8-bit
  ; zigzag byte (max 255), a much narrower domain, so the same value
  ; isn't actually tuned for this codec. Re-measured directly against
  ; RFXRicePackEncode/Decode (salt-and-pepper outliers over near-zero
  ; residuals, rates 1/1000 to 1/20, escape-aware estimator already
  ; active): #RFXRiceEscQ=4 wins outright at every rate tried, e.g.
  ; 1/20 rate: 32296 bytes at 4 vs 37529 at 24 (-14%); 1/1000 rate:
  ; 28805 vs 28906. A pure no-outlier moderate-variance case (no outliers
  ; at all) is byte-identical at 4 vs 24 (the estimator's own k-selection
  ; already keeps genuine non-outlier q small regardless), and the 8
  ; image / 4 quality-level pipeline suite is unaffected either way
  ; (none of those synthetic images have big enough outliers to trigger
  ; escape at any threshold tried). Values below 4 (1-3) are measurably
  ; worse - too aggressive, taxing ordinary small residuals.
  #RFXRiceEscQ = 4
  #RFXRiceEscBits = 8

  Procedure.i RFXRiceBestKBytes(*src, count.i)
    Protected k.i, i.i, v.i, z.i, q.i, bits.q, bestBits.q = $7FFFFFFFFFFFFFFF, bestK.i
    For k = 0 To 7
      bits = 0
      For i = 0 To count - 1
        v = RFXSignedByte(PeekA(*src + i) & $FF)
        z = RFXZZEncode8(v)
        q = z >> k
        If q >= #RFXRiceEscQ
          bits + #RFXRiceEscQ + 1 + #RFXRiceEscBits
        Else
          bits + q + 1 + k
        EndIf
        If bits >= bestBits : Break : EndIf
      Next
      If bits < bestBits
        bestBits = bits
        bestK = k
      EndIf
    Next
    ProcedureReturn bestK
  EndProcedure

  Procedure.i RFXRiceBlockSize(mode.i)
    Select mode
      Case 0 : ProcedureReturn 16
      Case 1 : ProcedureReturn 32
      Case 2 : ProcedureReturn 64
    EndSelect
    ProcedureReturn 128
  EndProcedure

  Procedure.q RFXRiceEstimateBits(*src, size.i, blockSize.i)
    Protected in.i, n.i, k.i, i.i, v.i, z.i, q.i, bits.q
    While in < size
      n = size - in
      If n > blockSize : n = blockSize : EndIf
      k = RFXRiceBestKBytes(*src + in, n)
      bits + 4
      For i = 0 To n - 1
        v = RFXSignedByte(PeekA(*src + in + i) & $FF)
        z = RFXZZEncode8(v)
        q = z >> k
        If q >= #RFXRiceEscQ
          bits + #RFXRiceEscQ + 1 + #RFXRiceEscBits
        Else
          bits + q + 1 + k
        EndIf
      Next
      in + n
    Wend
    ProcedureReturn bits
  EndProcedure

  ; Fast unary run I/O: writes/reads a run of `q` one-bits followed by a
  ; terminating zero-bit, byte-batched instead of one RFXMemWriteBits/
  ; RFXMemReadBits call per single bit. Bit-exact match to the old
  ; "While q>0: WriteBits(1,1): q-1: Wend: WriteBits(0,1)" /
  ; "Repeat: bit=ReadBits(1): Until bit=0" pattern - same stream position
  ; semantics, same bytes out, just without a function-call-plus-bounds-
  ; check per single bit (the dominant cost for low-k blocks/outliers,
  ; where q can run into the hundreds for one residual).
  Procedure RFXWriteUnaryOnes(*dst, *OutByte.Integer, *OutBit.Integer, q.i)
    Protected obyte.i = *OutByte\i, obit.i = *OutBit\i
    Protected avail.i, chunk.i
    If obit > 0
      avail = 8 - obit
      chunk = avail : If chunk > q : chunk = q : EndIf
      PokeA(*dst + obyte, (PeekA(*dst + obyte) & $FF) | (((1 << chunk) - 1) << obit))
      obit + chunk
      q - chunk
      If obit = 8 : obit = 0 : obyte + 1 : EndIf
    EndIf
    While q >= 8
      PokeA(*dst + obyte, $FF)
      obyte + 1
      q - 8
    Wend
    If q > 0
      PokeA(*dst + obyte, (PeekA(*dst + obyte) & $FF) | ((1 << q) - 1))
      obit = q
    EndIf
    obit + 1 ; terminating zero bit (dst already zero there via AllocateMemory)
    If obit = 8 : obit = 0 : obyte + 1 : EndIf
    *OutByte\i = obyte
    *OutBit\i = obit
  EndProcedure

  Procedure.i RFXReadUnaryOnes(*src, size.i, *InByte.Integer, *InBit.Integer, *Ok.Integer)
    Protected ibyte.i = *InByte\i, ibit.i = *InBit\i
    Protected q.i, b.i, avail.i, cur.i, localCount.i, found.i
    Repeat
      If ibyte >= size
        *Ok\i = 0
        *InByte\i = ibyte : *InBit\i = ibit
        ProcedureReturn 0
      EndIf
      b = PeekA(*src + ibyte) & $FF
      avail = 8 - ibit
      cur = b >> ibit ; only bits 0..avail-1 are meaningful (b is a single byte)
      found = #False : localCount = 0
      While localCount < avail
        If ((cur >> localCount) & 1) = 0
          found = #True
          Break
        EndIf
        localCount + 1
      Wend
      q + localCount
      If found
        ibit + localCount + 1
        If ibit >= 8 : ibit - 8 : ibyte + 1 : EndIf
        *InByte\i = ibyte : *InBit\i = ibit
        ProcedureReturn q
      Else
        ibyte + 1
        ibit = 0
      EndIf
    ForEver
  EndProcedure

  Procedure.i RFXRicePackEncode(*src, size.i, *OutSize.Integer)
    Protected *dst
    Protected in.i, outByte.Integer, outBit.Integer
    Protected n.i, k.i, i.i, v.i, z.i, q.i, mode.i, bestMode.i, blockSize.i, bits.q, bestBits.q = $7FFFFFFFFFFFFFFF
    If *OutSize : *OutSize\i = 0 : EndIf
    If *src = 0 Or size <= 0 : ProcedureReturn 0 : EndIf
    *dst = AllocateMemory(size * 2 + 256)
    If *dst = 0 : ProcedureReturn 0 : EndIf
    For mode = 0 To 3
      bits = RFXRiceEstimateBits(*src, size, RFXRiceBlockSize(mode))
      If bits < bestBits
        bestBits = bits
        bestMode = mode
      EndIf
    Next
    PokeA(*dst, bestMode)
    outByte\i = 1
    blockSize = RFXRiceBlockSize(bestMode)
    While in < size
      n = size - in
      If n > blockSize : n = blockSize : EndIf
      k = RFXRiceBestKBytes(*src + in, n)
      RFXMemWriteBits(*dst, @outByte, @outBit, k, 4)
      For i = 0 To n - 1
        v = RFXSignedByte(PeekA(*src + in + i) & $FF)
        z = RFXZZEncode8(v)
        q = z >> k
        If q >= #RFXRiceEscQ
          RFXWriteUnaryOnes(*dst, @outByte, @outBit, #RFXRiceEscQ)
          RFXMemWriteBits(*dst, @outByte, @outBit, z, #RFXRiceEscBits)
        Else
          RFXWriteUnaryOnes(*dst, @outByte, @outBit, q)
          If k > 0
            RFXMemWriteBits(*dst, @outByte, @outBit, z & ((1 << k) - 1), k)
          EndIf
        EndIf
      Next
      in + n
    Wend
    If outBit\i > 0 : outByte\i + 1 : EndIf
    If *OutSize : *OutSize\i = outByte\i : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure.i RFXRicePackDecode(*src, size.i, expected.i, *OutSize.Integer)
    Protected *dst
    Protected out.i, inByte.Integer, inBit.Integer, ok.Integer
    Protected n.i, i.i, k.i, q.i, bit.i, rest.i, z.i, v.i, blockSize.i
    If *OutSize : *OutSize\i = 0 : EndIf
    If *src = 0 Or size <= 0 Or expected <= 0 : ProcedureReturn 0 : EndIf
    *dst = AllocateMemory(expected)
    If *dst = 0 : ProcedureReturn 0 : EndIf
    blockSize = RFXRiceBlockSize(PeekA(*src) & 3)
    inByte\i = 1
    ok\i = 1
    While out < expected And ok\i
      n = expected - out
      If n > blockSize : n = blockSize : EndIf
      k = RFXMemReadBits(*src, size, @inByte, @inBit, 4, @ok)
      If ok\i = 0 : Break : EndIf
      For i = 0 To n - 1
        q = RFXReadUnaryOnes(*src, size, @inByte, @inBit, @ok)
        If ok\i = 0 : Break : EndIf
        If q >= #RFXRiceEscQ
          z = RFXMemReadBits(*src, size, @inByte, @inBit, #RFXRiceEscBits, @ok)
          If ok\i = 0 : Break : EndIf
        Else
          rest = 0
          If k > 0
            rest = RFXMemReadBits(*src, size, @inByte, @inBit, k, @ok)
            If ok\i = 0 : Break : EndIf
          EndIf
          z = (q << k) | rest
        EndIf
        v = RFXZZDecode8(z)
        PokeA(*dst + out, v & $FF)
        out + 1
      Next
    Wend
    If ok\i = 0 Or out <> expected
      FreeMemory(*dst)
      ProcedureReturn 0
    EndIf
    If *OutSize : *OutSize\i = out : EndIf
    ProcedureReturn *dst
  EndProcedure

  Procedure.i RFXBuildRasterCandidate(*stream, streamSize.i, width.i, height.i, channels.i, format.i, predictor.i, codec.i, *OutSize.Integer)
    Protected mw.MemWriter, *pred, *payload, payloadSize.Integer, bpp.i = RFXBytesPerPixel(format)
    If *OutSize : *OutSize\i = 0 : EndIf
    *pred = AllocateMemory(streamSize)
    If *pred = 0 : ProcedureReturn #False : EndIf
    If format = 8
      RFXPredict(*stream, *pred, width, height * 4, 1, predictor)
    ElseIf format = 10
      RFXPredict(*stream, *pred, width, height * 3, 1, predictor)
    Else
      RFXPredict(*stream, *pred, width, height, bpp, predictor)
    EndIf
    Select codec
      Case 0
        *payload = *pred
        payloadSize\i = streamSize
      Case 1
        *payload = RFXRLEEncode(*pred, streamSize, @payloadSize)
      Case 2
        *payload = RFXBytePackEncode(*pred, streamSize, @payloadSize)
      Case 3
        *payload = RFXRicePackEncode(*pred, streamSize, @payloadSize)
      Case 4
        *payload = RFXLZSSEncode(*pred, streamSize, @payloadSize)
      Default
        FreeMemory(*pred) : ProcedureReturn #False
    EndSelect
    If *payload = 0
      FreeMemory(*pred) : ProcedureReturn #False
    EndIf
    If MW_Init(@mw, 18 + payloadSize\i) = #False
      If codec <> 0 : FreeMemory(*payload) : EndIf
      FreeMemory(*pred) : ProcedureReturn 0
    EndIf
    If RFXEmitHeaderMem(@mw, width, height, 0, channels) = #False Or MW_Byte(@mw, format) = #False Or MW_Byte(@mw, predictor) = #False Or MW_Byte(@mw, codec) = #False Or MW_U32(@mw, streamSize) = #False Or MW_Data(@mw, *payload, payloadSize\i) = #False
      If codec <> 0 : FreeMemory(*payload) : EndIf
      FreeMemory(*pred) : FreeMemory(mw\memory) : ProcedureReturn 0
    EndIf
    If codec <> 0 : FreeMemory(*payload) : EndIf
    FreeMemory(*pred)
    If *OutSize : *OutSize\i = mw\size : EndIf
    ProcedureReturn mw\memory
  EndProcedure

  Procedure.i RFXPaletteSortKey(color.l)
    Protected r.i = color & $FF, g.i = (color >> 8) & $FF, b.i = (color >> 16) & $FF
    Protected y.i = (r * 30 + g * 59 + b * 11) / 100
    ProcedureReturn (y << 16) | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3)
  EndProcedure

  Procedure.l RFXColorRGBA(r.i, g.i, b.i, a.i)
    ProcedureReturn (r & $FF) | ((g & $FF) << 8) | ((b & $FF) << 16) | ((a & $FF) << 24)
  EndProcedure

  Procedure SortPaletteByKey(Array palette.l(1), Array counts.i(1), count.i)
    Protected i.i, j.i, keyI.i, keyJ.i, tmpColor.l, tmpCount.i
    For i = 1 To count - 1
      tmpColor = palette(i)
      tmpCount = counts(i)
      keyI = RFXPaletteSortKey(tmpColor)
      j = i - 1
      While j >= 0
        keyJ = RFXPaletteSortKey(palette(j))
        If keyJ <= keyI : Break : EndIf
        palette(j + 1) = palette(j)
        counts(j + 1) = counts(j)
        j - 1
      Wend
      palette(j + 1) = tmpColor
      counts(j + 1) = tmpCount
    Next
  EndProcedure

  Procedure SortPaletteByCount(Array palette.l(1), Array counts.i(1), count.i)
    Protected i.i, j.i, keyI.i, keyJ.i, tmpColor.l, tmpCount.i
    For i = 1 To count - 1
      tmpColor = palette(i)
      tmpCount = counts(i)
      keyI = RFXPaletteSortKey(tmpColor)
      j = i - 1
      While j >= 0
        keyJ = RFXPaletteSortKey(palette(j))
        If counts(j) > tmpCount Or (counts(j) = tmpCount And keyJ <= keyI) : Break : EndIf
        palette(j + 1) = palette(j)
        counts(j + 1) = counts(j)
        j - 1
      Wend
      palette(j + 1) = tmpColor
      counts(j + 1) = tmpCount
    Next
  EndProcedure

  ; Move-to-front rank transform for the palette index stream, tried only
  ; for narrow (<=16 color) palettes: instead of the raw palette index,
  ; store its rank in a recency list (most-recently-seen color is always
  ; rank 0). Complements orderMode=2's global frequency sort - MTF
  ; captures LOCAL clustering (a color used recently, not necessarily
  ; globally common) that a fixed frequency order can't. Array-based, so
  ; worst case is O(maxColors) per pixel - bounded at 16 by the <=16-
  ; color restriction, matching the concern the original proposal raised
  ; about this being too costly for large (up to 256-color) palettes.
  ; Measured only helping when paired with Rice coding specifically (its
  ; unary code rewards long runs of small values, which MTF produces for
  ; recently-reused colors); MTF actively hurt BytePack/RLE in testing.
  ; This is fine: it's tried as one more candidate among the existing
  ; codec/orderMode combinations, so a losing pairing just doesn't win,
  ; it never regresses what was already achievable.
  Procedure RFXMTFEncode(*indices, pixels.i, maxColors.i)
    Protected Dim table.b(maxColors - 1)
    Protected i.i, v.i, rank.i
    For i = 0 To maxColors - 1 : table(i) = i : Next
    For i = 0 To pixels - 1
      v = PeekA(*indices + i) & $FF
      For rank = 0 To maxColors - 1
        If table(rank) = v : Break : EndIf
      Next
      PokeA(*indices + i, rank)
      While rank > 0
        table(rank) = table(rank - 1)
        rank - 1
      Wend
      table(0) = v
    Next
  EndProcedure

  Procedure RFXMTFDecode(*indices, pixels.i, maxColors.i)
    Protected Dim table.b(maxColors - 1)
    Protected i.i, v.i, rank.i
    For i = 0 To maxColors - 1 : table(i) = i : Next
    For i = 0 To pixels - 1
      rank = PeekA(*indices + i) & $FF
      v = table(rank) & $FF
      PokeA(*indices + i, v)
      While rank > 0
        table(rank) = table(rank - 1)
        rank - 1
      Wend
      table(0) = v
    Next
  EndProcedure

  Procedure.i RFXBuildPaletteCandidate(*rgba, width.i, height.i, channels.i, codec.i, bits.i, predictor.i, orderMode.i, *OutSize.Integer)
    Protected pixels.i = width * height, i.i, p.i, count.i, found.i, maxColors.i
    Protected Dim palette.l(255)
    Protected Dim counts.i(255)
    Protected Dim hashUsed.b(1023)
    Protected Dim hashKey.l(1023)
    Protected Dim hashIndex.i(1023)
    Protected *indices = AllocateMemory(pixels), *packed, *coded, *payload, payloadSize.Integer
    Protected packedSize.i, b.i, shift.i, color.l, h.i, probe.i, slot.i, format.i, mw.MemWriter
    Protected *palRaw, *palPred, *palEncoded, palEncSize.Integer, useDeltaPal.i
    If *OutSize : *OutSize\i = 0 : EndIf
    If bits <> 1 And bits <> 2 And bits <> 4 And bits <> 8 : ProcedureReturn 0 : EndIf
    ; For bits=8 (256-color palette), predictor keeps its usual meaning:
    ; spatial predictor mode 0-4 applied to the index plane. For bits<8
    ; (<=16 colors), predictor is otherwise unused (spatial prediction on
    ; a narrow index alphabet isn't meaningful), so it's repurposed here:
    ; 0=raw indices, 1=MTF-transformed indices (see RFXMTFEncode above).
    If bits <> 8 And predictor > 1 : ProcedureReturn 0 : EndIf
    maxColors = 1 << bits
    If *indices = 0 : ProcedureReturn 0 : EndIf
    For i = 0 To pixels - 1
      color = PeekL(*rgba + i * 4)
      found = -1
      h = (color ! (color >> 8) ! (color >> 16) ! (color >> 24)) & 1023
      For probe = 0 To 1023
        slot = (h + probe) & 1023
        If hashUsed(slot) = 0
          If count >= maxColors
            FreeMemory(*indices) : ProcedureReturn 0
          EndIf
          hashUsed(slot) = 1
          hashKey(slot) = color
          hashIndex(slot) = count
          palette(count) = color
          found = count
          count + 1
          Break
        ElseIf hashKey(slot) = color
          found = hashIndex(slot)
          Break
        EndIf
      Next
      If found < 0
        FreeMemory(*indices) : ProcedureReturn 0
      EndIf
      counts(found) + 1
      PokeA(*indices + i, found)
    Next
    If count <= 0
      FreeMemory(*indices) : ProcedureReturn 0
    EndIf
    If orderMode > 0 And count > 1
      If orderMode = 1
        SortPaletteByKey(palette(), counts(), count)
      Else
        SortPaletteByCount(palette(), counts(), count)
      EndIf
      FillMemory(@hashUsed(0), 1024, 0)
      For p = 0 To count - 1
        color = palette(p)
        h = (color ! (color >> 8) ! (color >> 16) ! (color >> 24)) & 1023
        For probe = 0 To 1023
          slot = (h + probe) & 1023
          If hashUsed(slot) = 0
            hashUsed(slot) = 1
            hashKey(slot) = color
            hashIndex(slot) = p
            Break
          EndIf
        Next
      Next
      For i = 0 To pixels - 1
        color = PeekL(*rgba + i * 4)
        found = -1
        h = (color ! (color >> 8) ! (color >> 16) ! (color >> 24)) & 1023
        For probe = 0 To 1023
          slot = (h + probe) & 1023
          If hashUsed(slot) = 0
            Break
          ElseIf hashKey(slot) = color
            found = hashIndex(slot)
            Break
          EndIf
        Next
        If found < 0
          FreeMemory(*indices) : ProcedureReturn 0
        EndIf
        PokeA(*indices + i, found)
      Next
    EndIf
    If predictor = 1 And bits <> 8
      RFXMTFEncode(*indices, pixels, maxColors)
    EndIf
    Select bits
      Case 1 : packedSize = (pixels + 7) / 8
      Case 2 : packedSize = (pixels + 3) / 4
      Case 4 : packedSize = (pixels + 1) / 2
      Default : packedSize = pixels
    EndSelect
    *packed = AllocateMemory(packedSize)
    If *packed = 0
      FreeMemory(*indices) : ProcedureReturn 0
    EndIf
    If bits = 8
      CopyMemory(*indices, *packed, pixels)
    ElseIf bits = 4
      For i = 0 To pixels - 1
        b = PeekA(*indices + i) & $0F
        If i & 1
          PokeA(*packed + i / 2, (PeekA(*packed + i / 2) & $0F) | (b << 4))
        Else
          PokeA(*packed + i / 2, b)
        EndIf
      Next
    ElseIf bits = 2
      For i = 0 To pixels - 1
        b = PeekA(*indices + i) & 3
        shift = (i & 3) << 1
        PokeA(*packed + i / 4, (PeekA(*packed + i / 4) & $FF) | (b << shift))
      Next
    Else
      For i = 0 To pixels - 1
        b = PeekA(*indices + i) & 1
        shift = i & 7
        PokeA(*packed + i / 8, (PeekA(*packed + i / 8) & $FF) | (b << shift))
      Next
    EndIf
    ; Spatial prediction only applies to the bits=8 index plane, where
    ; packedSize==pixels==width*height so RFXPredict's shape is valid.
    ; For bits<8, predictor=1 already means "MTF" (applied above, before
    ; packing) - calling RFXPredict here too would treat width/height as
    ; if they matched the (much smaller) packed buffer, overrunning it.
    If predictor <> 0 And bits = 8
      *coded = AllocateMemory(packedSize)
      If *coded = 0
        FreeMemory(*packed) : FreeMemory(*indices) : ProcedureReturn 0
      EndIf
      RFXPredict(*packed, *coded, width, height, 1, predictor)
    Else
      *coded = *packed
    EndIf
    Select codec
      Case 0
        *payload = *coded
        payloadSize\i = packedSize
      Case 1
        *payload = RFXRLEEncode(*coded, packedSize, @payloadSize)
      Case 2
        *payload = RFXBytePackEncode(*coded, packedSize, @payloadSize)
      Case 3
        *payload = RFXRicePackEncode(*coded, packedSize, @payloadSize)
      Case 4
        *payload = RFXLZSSEncode(*coded, packedSize, @payloadSize)
      Default
        If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
        FreeMemory(*packed) : FreeMemory(*indices) : ProcedureReturn 0
    EndSelect
    If *payload = 0
      If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
      FreeMemory(*packed) : FreeMemory(*indices) : ProcedureReturn 0
    EndIf
    If bits = 1
      format = 6
    ElseIf bits = 2
      format = 1
    ElseIf bits = 4
      format = 5
    Else
      format = 4
    EndIf

    ; Try delta+zigzag+Rice encoding of the palette color table: after
    ; orderMode sorting, adjacent palette entries are often close in
    ; value, so per-channel delta (RFXPredict mode 1 "Left", treating the
    ; table as a count x1 image with bpp=4) plus RFXRicePackEncode on the
    ; residuals can beat the raw 4-bytes-per-color baseline. Always keep
    ; whichever is smaller (an explicit useDeltaPal flag byte says which)
    ; - this never regresses past the raw table, it only sometimes shrinks
    ; it. Small/unsorted palettes often have little to gain and the extra
    ; framing (flag byte + size prefix) can lose outright, hence the
    ; explicit size comparison rather than always using the smaller-in-
    ; theory transform.
    *palRaw = AllocateMemory(count * 4)
    If *palRaw
      For p = 0 To count - 1
        PokeL(*palRaw + p * 4, palette(p))
      Next
      *palPred = AllocateMemory(count * 4)
      If *palPred
        RFXPredict(*palRaw, *palPred, count, 1, 4, 1)
        *palEncoded = RFXRicePackEncode(*palPred, count * 4, @palEncSize)
        If *palEncoded And (2 + palEncSize\i) < (count * 4)
          useDeltaPal = #True
        EndIf
        FreeMemory(*palPred)
      EndIf
    EndIf

    If MW_Init(@mw, 22 + count * 4 + payloadSize\i) = #False
      If *palEncoded : FreeMemory(*palEncoded) : EndIf
      If *palRaw : FreeMemory(*palRaw) : EndIf
      If codec <> 0 : FreeMemory(*payload) : EndIf
      If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
      FreeMemory(*packed) : FreeMemory(*indices) : ProcedureReturn 0
    EndIf
    If RFXEmitHeaderMem(@mw, width, height, 0, channels) = #False Or MW_Byte(@mw, format) = #False Or MW_Byte(@mw, predictor) = #False Or MW_Byte(@mw, codec) = #False Or MW_U32(@mw, pixels) = #False Or MW_U16(@mw, count) = #False Or MW_Byte(@mw, useDeltaPal) = #False
      If *palEncoded : FreeMemory(*palEncoded) : EndIf
      If *palRaw : FreeMemory(*palRaw) : EndIf
      If codec <> 0 : FreeMemory(*payload) : EndIf
      If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
      FreeMemory(*packed) : FreeMemory(*indices) : FreeMemory(mw\memory) : ProcedureReturn 0
    EndIf
    If useDeltaPal
      If MW_U16(@mw, palEncSize\i) = #False Or MW_Data(@mw, *palEncoded, palEncSize\i) = #False
        FreeMemory(*palEncoded) : FreeMemory(*palRaw)
        If codec <> 0 : FreeMemory(*payload) : EndIf
        If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
        FreeMemory(*packed) : FreeMemory(*indices) : FreeMemory(mw\memory) : ProcedureReturn 0
      EndIf
      FreeMemory(*palEncoded)
      FreeMemory(*palRaw)
    Else
      If *palEncoded : FreeMemory(*palEncoded) : EndIf
      For p = 0 To count - 1
        If MW_U32(@mw, palette(p)) = #False
          FreeMemory(*palRaw)
          If codec <> 0 : FreeMemory(*payload) : EndIf
          If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
          FreeMemory(*packed) : FreeMemory(*indices) : FreeMemory(mw\memory) : ProcedureReturn 0
        EndIf
      Next
      FreeMemory(*palRaw)
    EndIf
    If MW_Data(@mw, *payload, payloadSize\i) = #False
      If codec <> 0 : FreeMemory(*payload) : EndIf
      If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
      FreeMemory(*packed) : FreeMemory(*indices) : FreeMemory(mw\memory) : ProcedureReturn 0
    EndIf
    If codec <> 0 : FreeMemory(*payload) : EndIf
    If predictor <> 0 And bits = 8 : FreeMemory(*coded) : EndIf
    FreeMemory(*packed)
    FreeMemory(*indices)
    If *OutSize : *OutSize\i = mw\size : EndIf
    ProcedureReturn mw\memory
  EndProcedure

  Procedure.i RFXBuildScreenshotCandidate(*rgba, width.i, height.i, codec.i, *OutSize.Integer)
    Protected pixels.i = width * height, i.i, key.i, bgKey.i, bgCount.i, changed.i, paletteCount.i, found.i, p.i
    Protected *hist = AllocateMemory(262144 * 4), *mask, *indices, *maskPayload, *indexPayload
    Protected maskSize.i = (pixels + 7) / 8, indexSize.i, maskPayloadSize.Integer, indexPayloadSize.Integer
    Protected Dim palette.l(255)
    Protected r.i, g.i, b.i, a.i, bit.i, mw.MemWriter
    If *OutSize : *OutSize\i = 0 : EndIf
    If codec <> 2 And codec <> 4 : ProcedureReturn 0 : EndIf
    If *hist = 0 : ProcedureReturn 0 : EndIf
    For i = 0 To pixels - 1
      r = PeekA(*rgba + i * 4 + 0) >> 2
      g = PeekA(*rgba + i * 4 + 1) >> 2
      b = PeekA(*rgba + i * 4 + 2) >> 2
      key = (r << 12) | (g << 6) | b
      PokeL(*hist + key * 4, PeekL(*hist + key * 4) + 1)
    Next
    For key = 0 To 262143
      If PeekL(*hist + key * 4) > bgCount
        bgCount = PeekL(*hist + key * 4) : bgKey = key
      EndIf
    Next
    If bgCount * 100 / pixels < 45
      FreeMemory(*hist) : ProcedureReturn 0
    EndIf
    changed = pixels - bgCount
    If changed <= 0 Or changed * 100 / pixels > 65
      FreeMemory(*hist) : ProcedureReturn 0
    EndIf
    For key = 0 To 262143
      If key <> bgKey And PeekL(*hist + key * 4) > 0
        If paletteCount >= 256
          FreeMemory(*hist) : ProcedureReturn 0
        EndIf
        r = ((key >> 12) & $3F) << 2
        g = ((key >> 6) & $3F) << 2
        b = (key & $3F) << 2
        palette(paletteCount) = RFXColorRGBA(r, g, b, 255)
        paletteCount + 1
      EndIf
    Next
    *mask = AllocateMemory(maskSize)
    *indices = AllocateMemory(changed)
    If *mask = 0 Or *indices = 0
      If *mask : FreeMemory(*mask) : EndIf
      If *indices : FreeMemory(*indices) : EndIf
      FreeMemory(*hist) : ProcedureReturn 0
    EndIf
    changed = 0
    For i = 0 To pixels - 1
      r = PeekA(*rgba + i * 4 + 0) >> 2
      g = PeekA(*rgba + i * 4 + 1) >> 2
      b = PeekA(*rgba + i * 4 + 2) >> 2
      key = (r << 12) | (g << 6) | b
      If key <> bgKey
        bit = i & 7
        PokeA(*mask + i / 8, (PeekA(*mask + i / 8) & $FF) | (1 << bit))
        found = -1
        r = ((key >> 12) & $3F) << 2
        g = ((key >> 6) & $3F) << 2
        b = (key & $3F) << 2
        a = RFXColorRGBA(r, g, b, 255)
        For p = 0 To paletteCount - 1
          If palette(p) = a : found = p : Break : EndIf
        Next
        If found < 0
          FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist) : ProcedureReturn 0
        EndIf
        PokeA(*indices + changed, found)
        changed + 1
      EndIf
    Next
    Select codec
      Case 2
        *maskPayload = RFXBytePackEncode(*mask, maskSize, @maskPayloadSize)
        *indexPayload = RFXBytePackEncode(*indices, changed, @indexPayloadSize)
      Case 4
        *maskPayload = RFXLZSSEncode(*mask, maskSize, @maskPayloadSize)
        *indexPayload = RFXLZSSEncode(*indices, changed, @indexPayloadSize)
    EndSelect
    If *maskPayload = 0 Or *indexPayload = 0
      If *maskPayload : FreeMemory(*maskPayload) : EndIf
      If *indexPayload : FreeMemory(*indexPayload) : EndIf
      FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist) : ProcedureReturn 0
    EndIf
    If MW_Init(@mw, 34 + paletteCount * 4 + maskPayloadSize\i + indexPayloadSize\i) = #False
      FreeMemory(*indexPayload) : FreeMemory(*maskPayload) : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist) : ProcedureReturn 0
    EndIf
    If RFXEmitHeaderMem(@mw, width, height, 4, 3) = #False
      FreeMemory(*indexPayload) : FreeMemory(*maskPayload) : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist) : FreeMemory(mw\memory) : ProcedureReturn 0
    EndIf
    r = ((bgKey >> 12) & $3F) << 2
    g = ((bgKey >> 6) & $3F) << 2
    b = (bgKey & $3F) << 2
    If MW_U32(@mw, RFXColorRGBA(r, g, b, 255)) = #False Or MW_Byte(@mw, codec) = #False Or MW_U32(@mw, pixels) = #False Or MW_U32(@mw, changed) = #False Or MW_U16(@mw, paletteCount) = #False Or MW_U32(@mw, maskPayloadSize\i) = #False Or MW_U32(@mw, indexPayloadSize\i) = #False
      FreeMemory(*indexPayload) : FreeMemory(*maskPayload) : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist) : FreeMemory(mw\memory) : ProcedureReturn 0
    EndIf
    For p = 0 To paletteCount - 1
      If MW_U32(@mw, palette(p)) = #False
        FreeMemory(*indexPayload) : FreeMemory(*maskPayload) : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist) : FreeMemory(mw\memory) : ProcedureReturn 0
      EndIf
    Next
    If MW_Data(@mw, *maskPayload, maskPayloadSize\i) = #False Or MW_Data(@mw, *indexPayload, indexPayloadSize\i) = #False
      FreeMemory(*indexPayload) : FreeMemory(*maskPayload) : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist) : FreeMemory(mw\memory) : ProcedureReturn 0
    EndIf
    FreeMemory(*indexPayload) : FreeMemory(*maskPayload) : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*hist)
    If *OutSize : *OutSize\i = mw\size : EndIf
    ProcedureReturn mw\memory
  EndProcedure

  Procedure.i TryRFXCandidate(*candidate, candidateSize.i, *bestPtr.Integer, *bestSize.Integer)
    Protected decodedSize.Integer, *decoded
    If *candidate = 0 Or candidateSize <= 0 : ProcedureReturn #False : EndIf
    If *bestSize\i >= 0 And candidateSize >= *bestSize\i
      FreeMemory(*candidate) : ProcedureReturn #False
    EndIf
    *decoded = CatchRFXLToRGBA(*candidate, candidateSize, @decodedSize)
    If *decoded = 0
      FreeMemory(*candidate) : ProcedureReturn #False
    EndIf
    FreeMemory(*decoded)
    If *bestPtr\i : FreeMemory(*bestPtr\i) : EndIf
    *bestPtr\i = *candidate
    *bestSize\i = candidateSize
    ProcedureReturn #True
  EndProcedure

  Structure RFXEncodeTask
    kind.i
    width.i
    height.i
    quality.i
    smallImage.i
    hasAlpha.i
    channels.i
    format.i
    paletteBits.i
    streamSize.i
    *rgba
    *stream
    *best
    bestSize.i
  EndStructure

  ; Raster/planar candidate search (5 predictors x 5 codecs = 25 calls,
  ; each recomputing RFXPredict from scratch) is the actual bottleneck of
  ; the whole encoder - measured raster and planar as the two dominant
  ; tasks in the existing 4-way (kind 0-3) thread split, consistently
  ; balanced against each other and together bounding total encode time.
  ; One thread per predictor here mirrors that same top-level pattern
  ; one level deeper: each job owns its own *best/bestSize (no shared
  ; mutable state, no locking needed), merged sequentially via
  ; TryRFXCandidate after WaitThread, exactly like EncodeRFXLMemory
  ; already merges the 4 top-level tasks.
  Structure RFXPredictorJob
    width.i
    height.i
    channels.i
    format.i
    streamSize.i
    *stream
    pred.i
    *best
    bestSize.i
  EndStructure

  Procedure RFXPredictorJobRun(*job.RFXPredictorJob)
    Protected codec.i, candSize.Integer, *cand
    If *job = 0 : ProcedureReturn : EndIf
    *job\best = 0
    *job\bestSize = -1
    For codec = 0 To 4
      *cand = RFXBuildRasterCandidate(*job\stream, *job\streamSize, *job\width, *job\height, *job\channels, *job\format, *job\pred, codec, @candSize)
      If *cand : TryRFXCandidate(*cand, candSize\i, @*job\best, @*job\bestSize) : EndIf
    Next
  EndProcedure

  Procedure RFXRunPredictorJobs(*task.RFXEncodeTask)
    Protected Dim pjob.RFXPredictorJob(4)
    Protected Dim pthread.i(4)
    Protected pred.i
    For pred = 0 To 4
      pjob(pred)\width = *task\width : pjob(pred)\height = *task\height : pjob(pred)\channels = *task\channels
      pjob(pred)\format = *task\format : pjob(pred)\streamSize = *task\streamSize : pjob(pred)\stream = *task\stream
      pjob(pred)\pred = pred
      ; CreateThread() is only safe when the HOST program was compiled
      ; with -t (thread-safe runtime/allocator) - #PB_Compiler_Thread is
      ; PureBasic's compile-time flag for that. Spawning a thread here
      ; regardless (the old behavior) corrupted memory in any caller not
      ; built with -t; the WaitThread loop below already falls back to
      ; calling *Run() directly whenever pthread(pred)=0, so skipping
      ; CreateThread entirely under a non-threadsafe build degrades
      ; cleanly to serial execution instead of crashing.
      CompilerIf #PB_Compiler_Thread
        pthread(pred) = CreateThread(@RFXPredictorJobRun(), @pjob(pred))
      CompilerElse
        pthread(pred) = 0
      CompilerEndIf
    Next
    For pred = 0 To 4
      If pthread(pred)
        WaitThread(pthread(pred))
      Else
        RFXPredictorJobRun(@pjob(pred))
      EndIf
      If pjob(pred)\best
        TryRFXCandidate(pjob(pred)\best, pjob(pred)\bestSize, @*task\best, @*task\bestSize)
        pjob(pred)\best = 0
      EndIf
    Next
  EndProcedure

  Procedure RFXEncodeTaskRun(*task.RFXEncodeTask)
    Protected pred.i, codec.i, bits.i, orderMode.i, candSize.Integer, *cand
    If *task = 0 : ProcedureReturn : EndIf
    *task\best = 0
    *task\bestSize = -1
    Select *task\kind
      Case 0
        If *task\hasAlpha = #False Or (*task\format <> 3 And *task\format <> 7)
          RFXRunPredictorJobs(*task)
        EndIf
      Case 1
        RFXRunPredictorJobs(*task)
      Case 2
        For bits = 1 To 8
          If bits = 1 Or bits = 2 Or bits = 4 Or bits = 8
            If *task\paletteBits = 0 Or bits = *task\paletteBits
              For pred = 0 To 4
                If bits = 8 Or pred = 0 Or (pred = 1 And bits <> 8)
                  For orderMode = 0 To 2
                    For codec = 0 To 4
                      *cand = RFXBuildPaletteCandidate(*task\rgba, *task\width, *task\height, *task\channels, codec, bits, pred, orderMode, @candSize)
                      If *cand : TryRFXCandidate(*cand, candSize\i, @*task\best, @*task\bestSize) : EndIf
                    Next
                  Next
                EndIf
              Next
            EndIf
          EndIf
        Next
      Case 3
        If *task\hasAlpha = #False And *task\quality <> #Quality_Lossless And *task\smallImage = #False
          For codec = 2 To 4 Step 2
            *cand = RFXBuildScreenshotCandidate(*task\rgba, *task\width, *task\height, codec, @candSize)
            If *cand : TryRFXCandidate(*cand, candSize\i, @*task\best, @*task\bestSize) : EndIf
          Next
        EndIf
    EndSelect
  EndProcedure

  Procedure.i EncodeRFXLMemory(*SourceRGBA, Width.i, Height.i, *OutSize.Integer, Quality.i, SourceStride.i = 0, SourcePixelBytes.i = 4)
    Protected outWidth.Integer, outHeight.Integer, rgbaSize.Integer
    Protected *rgba
    Protected *stream, streamSize.Integer, format.Integer
    Protected *exactStream, exactStreamSize.Integer, exactFormat.Integer
    Protected *planar, planarSize.Integer, planarFormat.Integer
    Protected *best
    Protected bestSize.Integer
    Protected i.i, effectiveQuality.i, smallImage.i, hasAlpha.i, channels.i, paletteColors.i, paletteBits.i = -1
    Protected Dim task.RFXEncodeTask(4)
    Protected Dim thread.i(4)
    bestSize\i = -1
    If *OutSize : *OutSize\i = 0 : EndIf
    If *SourceRGBA = 0 Or SourcePixelBytes < 3 Or RFXLDimensionsOK(Width, Height) = #False : ProcedureReturn #False : EndIf
    If SourceStride <= 0 : SourceStride = Width * SourcePixelBytes : EndIf
    If SourceStride < Width * SourcePixelBytes : ProcedureReturn #False : EndIf
    outWidth\i = Width
    outHeight\i = Height
    rgbaSize\i = Width * Height * 4
    *rgba = RFXCopySourceToRGBA(*SourceRGBA, Width, Height, SourceStride, SourcePixelBytes)
    If *rgba = 0 : ProcedureReturn #False : EndIf
    effectiveQuality = Quality
    smallImage = Bool(outWidth\i < 64 Or outHeight\i < 64)
    If smallImage And Quality <> #Quality_Lossless
      effectiveQuality = #Quality_Lossless
    EndIf
    hasAlpha = RFXHasAlpha(*rgba, outWidth\i * outHeight\i)
    channels = 3 + Bool(hasAlpha)
    QuantizeRGBA(*rgba, rgbaSize\i, effectiveQuality)
    paletteColors = RFXPaletteColorCount(*rgba, outWidth\i * outHeight\i, 256)
    If paletteColors > 0 And paletteColors <= 2
      paletteBits = 1
    ElseIf paletteColors > 0 And paletteColors <= 4
      paletteBits = 2
    ElseIf paletteColors > 0 And paletteColors <= 16
      paletteBits = 4
    ElseIf paletteColors > 0 And paletteColors <= 256
      paletteBits = 8
    EndIf
    *stream = RFXBuildPixelStream(*rgba, outWidth\i * outHeight\i, effectiveQuality, hasAlpha, @streamSize, @format)
    If *stream = 0
      FreeMemory(*rgba) : ProcedureReturn #False
    EndIf
    If effectiveQuality <> #Quality_Lossless
      ; The normal lossy raster path stores the already-quantized pixels in
      ; the tier's compact pixel layout. Also try storing those same
      ; quantized pixels exactly as RGB/RGBA; the existing candidate race
      ; keeps it only when compression beats the compact layout.
      *exactStream = RFXBuildPixelStream(*rgba, outWidth\i * outHeight\i, #Quality_Lossless, hasAlpha, @exactStreamSize, @exactFormat)
    EndIf
    *planar = RFXBuildPlanarStream(*rgba, outWidth\i * outHeight\i, hasAlpha, @planarSize, @planarFormat)
    task(0)\kind = 0 : task(0)\width = outWidth\i : task(0)\height = outHeight\i : task(0)\quality = Quality : task(0)\smallImage = smallImage : task(0)\hasAlpha = hasAlpha : task(0)\channels = channels : task(0)\format = format\i : task(0)\streamSize = streamSize\i : task(0)\rgba = *rgba : task(0)\stream = *stream
    task(1)\kind = 1 : task(1)\width = outWidth\i : task(1)\height = outHeight\i : task(1)\quality = Quality : task(1)\smallImage = smallImage : task(1)\hasAlpha = hasAlpha : task(1)\channels = channels : task(1)\format = planarFormat\i : task(1)\streamSize = planarSize\i : task(1)\rgba = *rgba : task(1)\stream = *planar
    If paletteBits > 0
      task(2)\kind = 2 : task(2)\width = outWidth\i : task(2)\height = outHeight\i : task(2)\quality = Quality : task(2)\smallImage = smallImage : task(2)\hasAlpha = hasAlpha : task(2)\channels = channels : task(2)\paletteBits = paletteBits : task(2)\rgba = *rgba
    EndIf
    task(3)\kind = 3 : task(3)\width = outWidth\i : task(3)\height = outHeight\i : task(3)\quality = Quality : task(3)\smallImage = smallImage : task(3)\hasAlpha = hasAlpha : task(3)\channels = channels : task(3)\rgba = *rgba
    If *exactStream
      task(4)\kind = 0 : task(4)\width = outWidth\i : task(4)\height = outHeight\i : task(4)\quality = Quality : task(4)\smallImage = smallImage : task(4)\hasAlpha = hasAlpha : task(4)\channels = channels : task(4)\format = exactFormat\i : task(4)\streamSize = exactStreamSize\i : task(4)\rgba = *rgba : task(4)\stream = *exactStream
    EndIf

    For i = 0 To 4
      If (i <> 1 Or *planar) And (i <> 2 Or paletteBits > 0) And (i <> 4 Or *exactStream)
        ; Same -t guard as RFXRunPredictorJobs above: only thread when
        ; the host was built thread-safe, otherwise leave thread(i)=0 so
        ; the WaitThread loop below runs RFXEncodeTaskRun() serially.
        CompilerIf #PB_Compiler_Thread
          thread(i) = CreateThread(@RFXEncodeTaskRun(), @task(i))
        CompilerElse
          thread(i) = 0
        CompilerEndIf
      EndIf
    Next
    For i = 0 To 4
      If (i <> 1 Or *planar) And (i <> 2 Or paletteBits > 0) And (i <> 4 Or *exactStream)
        If thread(i)
          WaitThread(thread(i))
        Else
          RFXEncodeTaskRun(@task(i))
        EndIf
      EndIf
      If task(i)\best
        TryRFXCandidate(task(i)\best, task(i)\bestSize, @*best, @bestSize)
        task(i)\best = 0
      EndIf
    Next
    If *best = 0
      If *exactStream : FreeMemory(*exactStream) : EndIf
      If *planar : FreeMemory(*planar) : EndIf
      FreeMemory(*stream) : FreeMemory(*rgba) : ProcedureReturn 0
    EndIf
    If *exactStream : FreeMemory(*exactStream) : EndIf
    If *planar : FreeMemory(*planar) : EndIf
    FreeMemory(*stream)
    FreeMemory(*rgba)
    If *OutSize : *OutSize\i = bestSize\i : EndIf
    ProcedureReturn *best
  EndProcedure

  Procedure.i CatchRFXLToRGBA(*Memory, MemorySize.i, *OutSize.Integer)
    Protected size.q, width.i, height.i, channels.i, top.i, payloadOffset.i
    Protected format.i, predictor.i, codec.i, streamSize.i, packedSize.i, decodedSize.Integer
    Protected *payload, *decoded, *stream, *rgba, *unpred
    Protected bpp.i, pixels.i
    Protected paletteCount.i, paletteOff.i, indexOff.i, palBits.i, palPackedSize.i, palByte.i
    Protected idx.i, tileIdx.i, bg.l, rgbaPos.i, packedPos.i, bit.i
    Protected changed.i, maskPayloadSize.i, indexPayloadSize.i, maskSize.i, fgPos.i, *mask, *indices
    Protected decodedOwned.i
    Protected useDeltaPal.i, palEncSize.i, *palTable, palTableOwned.i, *palDecodedPred, *palIndices
    If *OutSize : *OutSize\i = 0 : EndIf
    If *Memory = 0 Or MemorySize <= #RFXL_HeaderSize Or MemorySize > #RFXL_MaxMemorySize : ProcedureReturn 0 : EndIf
    size = MemorySize
    If PeekS(*Memory, 4, #PB_Ascii) <> "RFXL" Or (PeekA(*Memory + 4) & $FF) <> 26 : ProcedureReturn 0 : EndIf
    width = ReadU16Mem(*Memory, 5)
    height = ReadU16Mem(*Memory, 7)
    channels = PeekA(*Memory + 9) & $FF
    top = PeekA(*Memory + 10) & $FF
        If (channels <> 3 And channels <> 4) Or RFXLDimensionsOK(width, height) = #False Or (top <> 0 And top <> 4) : ProcedureReturn 0 : EndIf
    payloadOffset = #RFXL_HeaderSize
    pixels = width * height
    *payload = *Memory + payloadOffset
    *rgba = AllocateMemory(pixels * 4)
    If *rgba = 0
      ProcedureReturn 0
    EndIf
    Select top
      Case 0
        If size - payloadOffset < 7 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
        format = PeekA(*payload + 0) & $FF
        predictor = PeekA(*payload + 1) & $FF
        codec = PeekA(*payload + 2) & $FF
        streamSize = (PeekA(*payload + 3) & $FF) | ((PeekA(*payload + 4) & $FF) << 8) | ((PeekA(*payload + 5) & $FF) << 16) | ((PeekA(*payload + 6) & $FF) << 24)
        If streamSize <= 0 Or streamSize > #RFXL_MaxPixels * 4 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
        If predictor < 0 Or predictor > 4 Or codec < 0 Or codec > 4
          FreeMemory(*rgba) : ProcedureReturn #False
        EndIf
        If format <> 0 And format <> 1 And format <> 2 And format <> 3 And format <> 4 And format <> 5 And format <> 6 And format <> 7 And format <> 8 And format <> 9 And format <> 10
          FreeMemory(*rgba) : ProcedureReturn #False
        EndIf
        If format = 1 Or format = 4 Or format = 5 Or format = 6
          If size - payloadOffset < 10 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
          Select format
            Case 6 : palBits = 1 : palPackedSize = (pixels + 7) / 8
            Case 1 : palBits = 2 : palPackedSize = (pixels + 3) / 4
            Case 5 : palBits = 4 : palPackedSize = (pixels + 1) / 2
            Default : palBits = 8 : palPackedSize = pixels
          EndSelect
          paletteCount = (PeekA(*payload + 7) & $FF) | ((PeekA(*payload + 8) & $FF) << 8)
          If paletteCount <= 0 Or paletteCount > (1 << palBits) : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
          ; useDeltaPal=0: palette table is the usual raw 4-bytes/color
          ; array. useDeltaPal=1: table was delta+zigzag(RFXPredict mode
          ; 1 "Left") + Rice-coded (see RFXBuildPaletteCandidate) - decode
          ; it into a fresh *palTable buffer instead of aliasing *payload
          ; directly. Either way, *palTable/palTableOwned below is what
          ; the tileIdx lookup loop reads from.
          useDeltaPal = PeekA(*payload + 9) & $FF
          If useDeltaPal > 1 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
          If useDeltaPal
            If size - payloadOffset < 12 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            palEncSize = (PeekA(*payload + 10) & $FF) | ((PeekA(*payload + 11) & $FF) << 8)
            paletteOff = 12
            If paletteOff + palEncSize > size - payloadOffset : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            indexOff = paletteOff + palEncSize
            *palDecodedPred = RFXRicePackDecode(*payload + paletteOff, palEncSize, paletteCount * 4, @decodedSize)
            If *palDecodedPred = 0 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            *palTable = AllocateMemory(paletteCount * 4)
            If *palTable = 0 : FreeMemory(*palDecodedPred) : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            RFXUnpredict(*palDecodedPred, *palTable, paletteCount, 1, 4, 1)
            FreeMemory(*palDecodedPred)
            palTableOwned = #True
          Else
            paletteOff = 10
            indexOff = paletteOff + paletteCount * 4
            If indexOff > size - payloadOffset : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            *palTable = *payload + paletteOff
            palTableOwned = #False
          EndIf
          If streamSize <> pixels Or indexOff > size - payloadOffset
            If palTableOwned : FreeMemory(*palTable) : EndIf
            FreeMemory(*rgba) : ProcedureReturn #False
          EndIf
          packedSize = size - payloadOffset - indexOff
          ; codec=0 ("store") payload is already exactly the bytes we need -
          ; alias it directly instead of an Allocate+CopyMemory of the full
          ; index-plane size. *decoded then points inside the caller's
          ; input *Memory, so it must NOT be freed - decodedOwned tracks
          ; that. codec=0 is not rare here: it wins whenever the content is
          ; incompressible (noise, high-quality photos, tiny images).
          Select codec
            Case 0
              If packedSize <> palPackedSize
                If palTableOwned : FreeMemory(*palTable) : EndIf
                FreeMemory(*rgba) : ProcedureReturn #False
              EndIf
              *decoded = *payload + indexOff
              decodedOwned = #False
            Case 1
              *decoded = RFXRLEDecode(*payload + indexOff, packedSize, palPackedSize, @decodedSize)
              decodedOwned = #True
            Case 2
              *decoded = RFXBytePackDecode(*payload + indexOff, packedSize, palPackedSize, @decodedSize)
              decodedOwned = #True
            Case 3
              *decoded = RFXRicePackDecode(*payload + indexOff, packedSize, palPackedSize, @decodedSize)
              decodedOwned = #True
            Case 4
              *decoded = RFXLZSSDecode(*payload + indexOff, packedSize, palPackedSize, @decodedSize)
              decodedOwned = #True
            Default
              If palTableOwned : FreeMemory(*palTable) : EndIf
              FreeMemory(*rgba) : ProcedureReturn #False
          EndSelect
          If *decoded = 0
            If palTableOwned : FreeMemory(*palTable) : EndIf
            FreeMemory(*rgba) : ProcedureReturn #False
          EndIf
          ; predictor keeps its spatial-predict meaning only for palBits=8;
          ; for narrower palettes it's repurposed as the MTF flag (see
          ; RFXBuildPaletteCandidate/RFXMTFEncode) - values 2-4 are invalid
          ; there since spatial prediction on a <=16-color index alphabet
          ; isn't meaningful.
          If palBits <> 8 And predictor > 1
            If decodedOwned : FreeMemory(*decoded) : EndIf
            If palTableOwned : FreeMemory(*palTable) : EndIf
            FreeMemory(*rgba) : ProcedureReturn #False
          EndIf
          If predictor <> 0 And palBits = 8
            *unpred = AllocateMemory(palPackedSize)
            If *unpred = 0
              If decodedOwned : FreeMemory(*decoded) : EndIf
              If palTableOwned : FreeMemory(*palTable) : EndIf
              FreeMemory(*rgba) : ProcedureReturn #False
            EndIf
            RFXUnpredict(*decoded, *unpred, width, height, 1, predictor)
            If decodedOwned : FreeMemory(*decoded) : EndIf
            *decoded = *unpred
            decodedOwned = #True
          EndIf
          If predictor = 1 And palBits <> 8
            ; MTF was applied encoder-side: the bit-packed stream must be
            ; unpacked into a per-pixel rank array first and MTF-inverted
            ; as a whole sequence (its recency table advances in pixel
            ; order) before any RGBA lookup can happen - can't interleave
            ; unpack+lookup in one pass like the non-MTF branch below.
            *palIndices = AllocateMemory(pixels)
            If *palIndices = 0
              If decodedOwned : FreeMemory(*decoded) : EndIf
              If palTableOwned : FreeMemory(*palTable) : EndIf
              FreeMemory(*rgba) : ProcedureReturn #False
            EndIf
            For idx = 0 To pixels - 1
              If palBits = 4
                palByte = PeekA(*decoded + idx / 2) & $FF
                If idx & 1 : tileIdx = (palByte >> 4) & $0F : Else : tileIdx = palByte & $0F : EndIf
              ElseIf palBits = 2
                palByte = PeekA(*decoded + idx / 4) & $FF
                tileIdx = (palByte >> ((idx & 3) << 1)) & 3
              Else
                palByte = PeekA(*decoded + idx / 8) & $FF
                tileIdx = (palByte >> (idx & 7)) & 1
              EndIf
              PokeA(*palIndices + idx, tileIdx)
            Next
            RFXMTFDecode(*palIndices, pixels, 1 << palBits)
            For idx = 0 To pixels - 1
              tileIdx = PeekA(*palIndices + idx) & $FF
              If tileIdx >= paletteCount
                FreeMemory(*palIndices)
                If decodedOwned : FreeMemory(*decoded) : EndIf
                If palTableOwned : FreeMemory(*palTable) : EndIf
                FreeMemory(*rgba) : ProcedureReturn #False
              EndIf
              PokeL(*rgba + idx * 4, PeekL(*palTable + tileIdx * 4))
            Next
            FreeMemory(*palIndices)
          Else
            idx = 0
            rgbaPos = 0
            Select palBits
              Case 8
                For idx = 0 To pixels - 1
                  tileIdx = PeekA(*decoded + idx) & $FF
                  If tileIdx >= paletteCount
                    If decodedOwned : FreeMemory(*decoded) : EndIf
                    If palTableOwned : FreeMemory(*palTable) : EndIf
                    FreeMemory(*rgba) : ProcedureReturn #False
                  EndIf
                  PokeL(*rgba + rgbaPos, PeekL(*palTable + tileIdx * 4))
                  rgbaPos + 4
                Next
              Case 4
                For packedPos = 0 To palPackedSize - 1
                  palByte = PeekA(*decoded + packedPos) & $FF
                  tileIdx = palByte & $0F
                  If tileIdx >= paletteCount
                    If decodedOwned : FreeMemory(*decoded) : EndIf
                    If palTableOwned : FreeMemory(*palTable) : EndIf
                    FreeMemory(*rgba) : ProcedureReturn #False
                  EndIf
                  PokeL(*rgba + rgbaPos, PeekL(*palTable + tileIdx * 4))
                  rgbaPos + 4 : idx + 1
                  If idx < pixels
                    tileIdx = (palByte >> 4) & $0F
                    If tileIdx >= paletteCount
                      If decodedOwned : FreeMemory(*decoded) : EndIf
                      If palTableOwned : FreeMemory(*palTable) : EndIf
                      FreeMemory(*rgba) : ProcedureReturn #False
                    EndIf
                    PokeL(*rgba + rgbaPos, PeekL(*palTable + tileIdx * 4))
                    rgbaPos + 4 : idx + 1
                  EndIf
                Next
              Case 2
                For packedPos = 0 To palPackedSize - 1
                  palByte = PeekA(*decoded + packedPos) & $FF
                  For bit = 0 To 6 Step 2
                    If idx >= pixels : Break : EndIf
                    tileIdx = (palByte >> bit) & 3
                    If tileIdx >= paletteCount
                      If decodedOwned : FreeMemory(*decoded) : EndIf
                      If palTableOwned : FreeMemory(*palTable) : EndIf
                      FreeMemory(*rgba) : ProcedureReturn #False
                    EndIf
                    PokeL(*rgba + rgbaPos, PeekL(*palTable + tileIdx * 4))
                    rgbaPos + 4 : idx + 1
                  Next
                Next
              Default
                For packedPos = 0 To palPackedSize - 1
                  palByte = PeekA(*decoded + packedPos) & $FF
                  For bit = 0 To 7
                    If idx >= pixels : Break : EndIf
                    tileIdx = (palByte >> bit) & 1
                    If tileIdx >= paletteCount
                      If decodedOwned : FreeMemory(*decoded) : EndIf
                      If palTableOwned : FreeMemory(*palTable) : EndIf
                      FreeMemory(*rgba) : ProcedureReturn #False
                    EndIf
                    PokeL(*rgba + rgbaPos, PeekL(*palTable + tileIdx * 4))
                    rgbaPos + 4 : idx + 1
                  Next
                Next
            EndSelect
          EndIf
          If decodedOwned : FreeMemory(*decoded) : EndIf
          If palTableOwned : FreeMemory(*palTable) : EndIf
        Else
          bpp = RFXBytesPerPixel(format)
          If streamSize <> pixels * bpp : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
          packedSize = size - payloadOffset - 7
          Select codec
            Case 0
              If packedSize <> streamSize : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
              *decoded = *payload + 7
              decodedOwned = #False
            Case 1
              *decoded = RFXRLEDecode(*payload + 7, packedSize, streamSize, @decodedSize)
              decodedOwned = #True
            Case 2
              *decoded = RFXBytePackDecode(*payload + 7, packedSize, streamSize, @decodedSize)
              decodedOwned = #True
            Case 3
              *decoded = RFXRicePackDecode(*payload + 7, packedSize, streamSize, @decodedSize)
              decodedOwned = #True
            Case 4
              *decoded = RFXLZSSDecode(*payload + 7, packedSize, streamSize, @decodedSize)
              decodedOwned = #True
            Default
              FreeMemory(*rgba) : ProcedureReturn #False
          EndSelect
          If *decoded = 0 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
          If predictor = 0
            *stream = *decoded
          Else
            *stream = AllocateMemory(streamSize)
            If *stream = 0 : If decodedOwned : FreeMemory(*decoded) : EndIf : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            If format = 8
              RFXUnpredict(*decoded, *stream, width, height * 4, 1, predictor)
            ElseIf format = 10
              RFXUnpredict(*decoded, *stream, width, height * 3, 1, predictor)
            Else
              RFXUnpredict(*decoded, *stream, width, height, bpp, predictor)
            EndIf
          EndIf
          RFXDecodePixelStream(*stream, pixels, format, *rgba)
          If predictor <> 0 : FreeMemory(*stream) : EndIf
          If decodedOwned : FreeMemory(*decoded) : EndIf
        EndIf
      Case 4
        If size - payloadOffset < 23 : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
        bg = PeekL(*payload + 0)
        codec = PeekA(*payload + 4) & $FF
        streamSize = (PeekA(*payload + 5) & $FF) | ((PeekA(*payload + 6) & $FF) << 8) | ((PeekA(*payload + 7) & $FF) << 16) | ((PeekA(*payload + 8) & $FF) << 24)
        changed = (PeekA(*payload + 9) & $FF) | ((PeekA(*payload + 10) & $FF) << 8) | ((PeekA(*payload + 11) & $FF) << 16) | ((PeekA(*payload + 12) & $FF) << 24)
        paletteCount = (PeekA(*payload + 13) & $FF) | ((PeekA(*payload + 14) & $FF) << 8)
        maskPayloadSize = (PeekA(*payload + 15) & $FF) | ((PeekA(*payload + 16) & $FF) << 8) | ((PeekA(*payload + 17) & $FF) << 16) | ((PeekA(*payload + 18) & $FF) << 24)
        indexPayloadSize = (PeekA(*payload + 19) & $FF) | ((PeekA(*payload + 20) & $FF) << 8) | ((PeekA(*payload + 21) & $FF) << 16) | ((PeekA(*payload + 22) & $FF) << 24)
        paletteOff = 23
        indexOff = paletteOff + paletteCount * 4
        maskSize = (pixels + 7) / 8
        If streamSize <> pixels Or changed <= 0 Or changed > pixels Or paletteCount <= 0 Or paletteCount > 256 Or maskPayloadSize <= 0 Or indexPayloadSize <= 0 Or indexOff > size - payloadOffset
          FreeMemory(*rgba) : ProcedureReturn #False
        EndIf
        If maskPayloadSize > size - payloadOffset - indexOff Or indexPayloadSize <> size - payloadOffset - indexOff - maskPayloadSize
          FreeMemory(*rgba) : ProcedureReturn #False
        EndIf
        Select codec
          Case 2
            *mask = RFXBytePackDecode(*payload + indexOff, maskPayloadSize, maskSize, @decodedSize)
            *indices = RFXBytePackDecode(*payload + indexOff + maskPayloadSize, indexPayloadSize, changed, @decodedSize)
          Case 4
            *mask = RFXLZSSDecode(*payload + indexOff, maskPayloadSize, maskSize, @decodedSize)
            *indices = RFXLZSSDecode(*payload + indexOff + maskPayloadSize, indexPayloadSize, changed, @decodedSize)
          Default
            FreeMemory(*rgba) : ProcedureReturn #False
        EndSelect
        If *mask = 0 Or *indices = 0
          If *mask : FreeMemory(*mask) : EndIf
          If *indices : FreeMemory(*indices) : EndIf
          FreeMemory(*rgba) : ProcedureReturn #False
        EndIf
        For idx = 0 To pixels - 1
          PokeL(*rgba + idx * 4, bg)
        Next
        fgPos = 0
        For idx = 0 To pixels - 1
          palByte = PeekA(*mask + idx / 8) & $FF
          If (palByte >> (idx & 7)) & 1
            If fgPos >= changed : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            tileIdx = PeekA(*indices + fgPos) & $FF
            If tileIdx >= paletteCount : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
            PokeL(*rgba + idx * 4, PeekL(*payload + paletteOff + tileIdx * 4))
            fgPos + 1
          EndIf
        Next
        If fgPos <> changed : FreeMemory(*indices) : FreeMemory(*mask) : FreeMemory(*rgba) : ProcedureReturn #False : EndIf
        FreeMemory(*indices)
        FreeMemory(*mask)
      Default
        FreeMemory(*rgba) : ProcedureReturn #False
    EndSelect
    If *OutSize : *OutSize\i = pixels * 4 : EndIf
    ProcedureReturn *rgba
  EndProcedure

EndModule

CompilerEndIf

CompilerIf #PB_Compiler_IsMainFile
  XIncludeFile "test_rau_rfxl_hardening.pb"
CompilerEndIf
