PCC-Decoder-Bugfix: RLE-Runs duerfen Zeilengrenzen ueberschreiten (Row-Reset war falsch)

Stefan hat zurecht bemaengelt dass KARTE.PCC/KELLOGGS.PCC verzerrt/verrutscht
aussehen (Smacks-Frosch an falscher Position, Notch-Fehlstelle im Logo-Rahmen).

Root Cause: Der Decoder hat RLE-Runs am Ende jeder Bildzeile hart abgeschnitten
und den Rest verworfen (klassisches PCX-Verhalten angenommen). Dieses Format
haelt sich aber NICHT an die PCX-Konvention 'ein Run ueberschreitet nie eine
Zeile' -- Runs laufen frei ueber Zeilengrenzen. Verifiziert per Pixel-fuer-Pixel-
Vergleich gegen echten DOSBox-Screenshot (world map): mit durchgehender
Dekodierung (kein Row-Reset) ist das Ergebnis jetzt pixel-identisch zur Referenz.

- tools/pcc_to_png.py: Row-Reset entfernt, decodiert jetzt width*height Pixel
  am Stueck aus dem RLE-Strom
- png_out/: alle 77 PNGs mit dem Fix neu generiert
- tools/wip_bob_sprite_research/: Backup der laufenden (noch ungeloesten)
  BOB-Sprite-Format-Exploration von aria-wohnung, damit nichts bei einem
  VM-Neustart verloren geht
- NOTES.md: Root-Cause-Analyse dokumentiert, alte Fehldiagnose ('byte-identisch
  zwischen Row-Reset und kontinuierlich') korrigiert, Rauser-Intro-Frage
  beantwortet (3 Frames vorhanden, Text-Logo, keine Video-Datei)
This commit is contained in:
ARIA
2026-07-22 07:33:01 +00:00
parent 3c650e6aef
commit 5efba4b165
91 changed files with 561 additions and 43 deletions
+48 -43
View File
@@ -1,49 +1,54 @@
#!/usr/bin/env python3
"""Finaler PCC-Decoder fuer Kellogg's Tony and Friends (2026-07-22, ARIA).
Format (verifiziert gegen MENU.PCC/RAUSER1-3/FACTOR5, visuell + byte-exakt):
Format (verifiziert gegen MENU.PCC/RAUSER1-3/FACTOR5, visuell + byte-exakt,
UND jetzt zusaetzlich Pixel-fuer-Pixel gegen echte DOSBox-Screenshots von
KARTE.PCC und KELLOGGS.PCC verglichen -- siehe Bugfix 2 unten):
- Byte 0-15: echter PCX-Header-Anfang (Manufacturer=0x0A, Version=5,
Encoding=1/RLE, BPP=8, dann Xmin/Ymin/Xmax/Ymax als LE16 bei Offset 4-11).
WICHTIG (Bugfix 2026-07-22): Width/Height MUESSEN aus Xmax-Xmin+1 /
WICHTIG (Bugfix 1, 2026-07-22): Width/Height MUESSEN aus Xmax-Xmin+1 /
Ymax-Ymin+1 berechnet werden. Die 2 LE16-Werte bei Offset 12-15 sehen fuer
Vollbild-Screens (320x200) zufaellig identisch aus und wurden erst dafuer
gehalten -- sind aber tatsaechlich NICHT die Bilddimensionen (vermutlich
ein DPI/Reserved-Feld wie im echten 128-Byte-PCX-Header), sondern ein
Konstantwert der bei kleinen Sprites/Logos (z.B. RAUSER1.PCC: 182x46,
FAC0.PCC: 16x16) komplett falsch war und zu kaputten/leeren Bildern fuehrte.
Mit dem Xmax/Ymax-Fix konsumiert z.B. FACTOR5.PCC exakt 100% der
komprimierten Bytes (51609/51609) statt vorher mit falscher Hoehe (200
statt echten 199) zu ueberlaufen.
- Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten, SCANLINE-weise
decodiert (pro Zeile wird bis width Pixel gefuellt, ueberschuessige Pixel
eines Runs am Zeilenende werden verworfen -- klassisches PCX-Verhalten).
- Byte 16 .. (len-769): RLE-komprimierte Pixel-Indexdaten.
WICHTIG (Bugfix 2, 2026-07-22): Die RLE-Runs werden NICHT pro Scanline
zurueckgesetzt/abgeschnitten. Frueher wurde nach `width` Pixeln pro Zeile
hart getrimmt und der Rest eines laufenden RLE-Runs verworfen ("row reset").
Das war FALSCH: dieses Format haelt sich nicht an die klassische PCX-Regel
"ein Run ueberschreitet nie eine Scanline" -- Runs koennen frei ueber
Zeilengrenzen hinweglaufen. Der Beweis: mit Row-Reset waren KARTE.PCC und
KELLOGGS.PCC (die einzigen zwei echten 320x200-Vollbilder) sichtbar verwuerfelt
(Charaktere an falscher Position, "Kerbe" im Logo-Rahmen), obwohl der
Byte-Konsum fast vollstaendig war -- der Fehler kostet nur ~0.3-0.5% der
Pixel, aber genau die falschen, wodurch ganze Bildbereiche sichtbar
verrutschen. Mit kontinuierlicher Dekodierung (einfach `width*height` Pixel
am Stueck aus dem RLE-Strom lesen, OHNE pro-Zeile zu trimmen) sind beide
Bilder jetzt Pixel-fuer-Pixel identisch zu echten DOSBox-Screenshots
(verifiziert per ImageMagick-Vergleich, siehe NOTES.md). Kleine Sprites
waren von diesem Bug kaum betroffen, weil sie selten/nie einen Run ueber
eine Zeilengrenze hinweg haben -- deshalb fiel es dort nicht auf.
RLE-Tupel: Byte mit oberen 2 Bits gesetzt (0xC0-0xFF) = Lauflaenge (&0x3F),
gefolgt von einem Wert-Byte. Sonst literaler Pixel.
- Letzte 769 Bytes: 0x0C-Marker + 768 Byte (256 x RGB) eingebettete Palette
(klassische PCX-v5-256-Farben-Erweiterung). PRO DATEI eigene Palette,
keine globale Palette noetig.
BEKANNTER OFFENER BUG (Stand 2026-07-22, noch nicht geloest):
KELLOGGS.PCC und KARTE.PCC (volle 320x200-Screens) zeigen weiterhin einen
lokalen Deko-Fehler (ein "Kerbe"/Notch-Artefakt rechts neben dem Kellogg's-
Schriftzug, roter/gelber Fleck der nicht ins Referenz-Screenshot passt).
Ausgeschlossen als Ursache: (a) horizontales Rollen/Verschieben des ganzen
Bilds -- getestet mit 18 Shift-Kandidaten, Artefakt bleibt IMMER an
derselben Position relativ zum Bildinhalt, nicht zur Leinwand -> kein
Rotations-/Scroll-Bug. (b) Zeilenweises Verwerfen von RLE-Overshoot --
zeilenbasierte und "kontinuierliche" (ohne Row-Reset) Decodierung liefern
BYTE-IDENTISCHE Ergebnisse fuer diese Dateien. Vermutung: entweder eine
Palette-Fehlzuordnung fuer einzelne Indizes, oder eine RLE-Ambiguitaet bei
literalen Pixelwerten >= 0xC0 (die durch (b&0xC0)==0xC0 faelschlich als
Lauflaengen-Token statt als literaler Indexwert interpretiert werden
koennten) -- noch nicht verifiziert. FACTOR5.PCC's "Geister"-Doppellogo
ist dagegen vermutlich KEIN Bug, sondern ein Reflexions-/Schatten-
Designelement (Byte-Konsum ist bei diesem File exakt 100%).
GELOEST (frueher "bekannter offener Bug", Stand vor 2026-07-22 Nachmittag):
Das "Kerbe"/Notch-Artefakt neben dem Kellogg's-Schriftzug und die verrutschten
Charaktere auf KARTE.PCC waren beide der gleiche Bug (Row-Reset, s.o.), NICHT
ein horizontales Rollen und NICHT eine Palette-Fehlzuordnung. Mit der
kontinuierlichen Dekodierung ist das Artefakt komplett weg.
Nutzung: python3 pcc_to_png.py <input.PCC> <output.png>
Schreibt ein PPM und konvertiert via ImageMagick `convert` zu PNG.
Schreibt ein PPM und konvertiert via ImageMagick `convert`/`magick` zu PNG.
"""
import sys, struct, subprocess, os
import sys, struct, subprocess, os, shutil
def decode_pcc(data):
manuf, version, encoding, bpp = data[0], data[1], data[2], data[3]
@@ -55,25 +60,23 @@ def decode_pcc(data):
pal_bytes = data[pal_start+1:pal_start+1+768]
palette = [(pal_bytes[i], pal_bytes[i+1], pal_bytes[i+2]) for i in range(0, 768, 3)]
total = width * height
out = bytearray()
i = 0
n = len(pixel_region)
for row in range(height):
row_out = bytearray()
while len(row_out) < width and i < n:
b = pixel_region[i]; i += 1
if (b & 0xC0) == 0xC0:
count = b & 0x3F
if i >= n:
break
val = pixel_region[i]; i += 1
row_out.extend([val] * count)
else:
row_out.append(b)
row_out = row_out[:width]
if len(row_out) < width:
row_out.extend([0] * (width - len(row_out)))
out.extend(row_out)
while len(out) < total and i < n:
b = pixel_region[i]; i += 1
if (b & 0xC0) == 0xC0:
count = b & 0x3F
if i >= n:
break
val = pixel_region[i]; i += 1
remaining = total - len(out)
out.extend([val] * min(count, remaining))
else:
out.append(b)
if len(out) < total:
out.extend([0] * (total - len(out)))
return dict(width=width, height=height, marker_ok=(marker == 0x0C),
pixels=bytes(out), palette=palette,
@@ -88,7 +91,9 @@ def write_png(pixels, palette, width, height, out_png):
r, g, b = palette[px]
buf.extend([r, g, b])
f.write(bytes(buf))
subprocess.run(['convert', ppm, out_png], check=True)
convert_bin = shutil.which('magick') or shutil.which('convert')
args = [convert_bin, ppm, out_png] if 'magick' not in (convert_bin or '') or convert_bin.endswith('convert') else [convert_bin, 'convert', ppm, out_png]
subprocess.run(args, check=True)
os.remove(ppm)
def main():