# Graphics Loading Regression Analysis (2024) ## Overview This document records the root cause analysis and fix for a critical graphics loading regression where all overworld maps appeared green and graphics sheets appeared "brownish purple" (solid 0xFF fill). ## Symptoms - Overworld maps rendered as solid green tiles - Graphics sheets in the Graphics Editor appeared as solid purple/brown color - All 223 graphics sheets were filled with 0xFF bytes - Issue appeared after WASM-related changes to `src/app/rom.cc` ## Root Cause **Two bugs combined to cause complete graphics loading failure:** ### Bug 1: DecompressV2 Size Parameter = 0 The most critical bug was in the `DecompressV2()` calls in `LoadAllGraphicsData()` and `Load2BppGraphics()`: ```cpp // BROKEN - size parameter is 0, causes immediate empty return gfx::lc_lz2::DecompressV2(rom.data(), offset, 0, 1, rom.size()) // CORRECT - size must be 0x800 (2048 bytes) gfx::lc_lz2::DecompressV2(rom.data(), offset, 0x800, 1, rom.size()) ``` In `compression.cc`, the `DecompressV2` function has this early-exit check: ```cpp if (size == 0) { return std::vector(); // Returns empty immediately! } ``` When `size=0` was passed, every single graphics sheet decompression returned an empty vector, triggering the fallback path that fills the graphics buffer with 0xFF bytes. ### Bug 2: Header Stripping Logic Change (Secondary) The SMC header detection was also modified from: ```cpp // ORIGINAL (working) - modulo 1MB size % kBaseRomSize == kHeaderSize // kBaseRomSize = 1,048,576 // CHANGED TO (problematic) - modulo 32KB size % 0x8000 == kHeaderSize ``` The 32KB modulo check could cause false positives on ROMs that happened to have sizes matching the pattern, potentially stripping data that wasn't actually an SMC header. ## Investigation Process ### Initial Hypothesis 1. **Header/Footer Mismatch** - Suspected incorrect ROM alignment causing 512-byte offset in all pointer lookups 2. **Pointer Table Corruption** - Suspected `GetGraphicsAddress` reading garbage due to misalignment 3. **Decompression Failure** - Suspected `DecompressV2` failing silently ### Discovery Method 1. **Agent-based parallel investigation** - Spawned three agents to analyze: - ROM alignment (header stripping logic) - Graphics pipeline (pointer tables and decompression) - WASM integration (data transfer integrity) 2. **Git archaeology** - Compared working commit (`43dfd65b2c`) with broken code: ```bash git show 43dfd65b2c:src/app/rom.cc | grep "DecompressV2" # Output: gfx::lc_lz2::DecompressV2(rom.data(), offset) # 2 args! ``` 3. **Function signature analysis** - Found `DecompressV2` signature: ```cpp DecompressV2(data, offset, size=0x800, mode=1, rom_size=-1) ``` 4. **Root cause identified** - The broken code passed explicit `0` for the size parameter, overriding the default of `0x800`. ## Fix Applied ### File: `src/app/rom.cc` 1. **Restored header stripping logic** to use 1MB modulo: ```cpp if (size % kBaseRomSize == kHeaderSize && size >= kHeaderSize && rom_data.size() >= kHeaderSize) ``` 2. **Fixed DecompressV2 calls** (2 locations): - Line ~126 (Load2BppGraphics) - Line ~332 (LoadAllGraphicsData) Changed from `DecompressV2(..., 0, 1, rom.size())` to `DecompressV2(..., 0x800, 1, rom.size())` 3. **Added diagnostic logging** to help future debugging: - ROM alignment verification after header stripping - SNES checksum validation logging - Graphics pointer table probe (first 5 sheets) ### File: `src/app/gfx/util/compression.h` Added comprehensive documentation to `DecompressV2()` with explicit warning about size=0. ## Prevention Measures ### Code Comments Added 1. **MaybeStripSmcHeader** - Warning not to change modulo base from 1MB to 32KB 2. **DecompressV2 calls** - Comments explaining the 0x800 size requirement 3. **LoadAllGraphicsData** - Function header documenting the regression ### Documentation Added 1. Updated `compression.h` with full parameter documentation 2. Added `@warning` tags about size=0 behavior 3. Documented sheet categories and compression formats in `rom.cc` ## Key Learnings 1. **Default parameters can be overridden accidentally** - When adding new parameters to a function call, be careful not to override defaults with wrong values. 2. **Early-exit conditions can cause silent failures** - The `if (size == 0) return empty` was valid behavior, but calling code must respect it. 3. **Diagnostic logging is valuable** - The added probe logging for the first 5 graphics sheets helps quickly identify alignment issues. 4. **Git archaeology is essential** - Comparing with known-working commits reveals exactly what changed. ## Related Files - `src/app/rom.cc` - Main ROM handling and graphics loading - `src/app/gfx/util/compression.cc` - LC-LZ2 decompression implementation - `src/app/gfx/util/compression.h` - Decompression function declarations - `incl/zelda.h` - Version-specific pointer table offsets ## Commits - **Breaking commit**: Changes to WASM memory safety in `rom.cc` - **Fix commit**: Restored header stripping, fixed DecompressV2 size parameter