5.1 KiB
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():
// 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:
if (size == 0) {
return std::vector<uint8_t>(); // 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:
// 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
- Header/Footer Mismatch - Suspected incorrect ROM alignment causing 512-byte offset in all pointer lookups
- Pointer Table Corruption - Suspected
GetGraphicsAddressreading garbage due to misalignment - Decompression Failure - Suspected
DecompressV2failing silently
Discovery Method
-
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)
-
Git archaeology - Compared working commit (
43dfd65b2c) with broken code:git show 43dfd65b2c:src/app/rom.cc | grep "DecompressV2" # Output: gfx::lc_lz2::DecompressV2(rom.data(), offset) # 2 args! -
Function signature analysis - Found
DecompressV2signature:DecompressV2(data, offset, size=0x800, mode=1, rom_size=-1) -
Root cause identified - The broken code passed explicit
0for the size parameter, overriding the default of0x800.
Fix Applied
File: src/app/rom.cc
-
Restored header stripping logic to use 1MB modulo:
if (size % kBaseRomSize == kHeaderSize && size >= kHeaderSize && rom_data.size() >= kHeaderSize) -
Fixed DecompressV2 calls (2 locations):
- Line ~126 (Load2BppGraphics)
- Line ~332 (LoadAllGraphicsData)
Changed from
DecompressV2(..., 0, 1, rom.size())toDecompressV2(..., 0x800, 1, rom.size()) -
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
- MaybeStripSmcHeader - Warning not to change modulo base from 1MB to 32KB
- DecompressV2 calls - Comments explaining the 0x800 size requirement
- LoadAllGraphicsData - Function header documenting the regression
Documentation Added
- Updated
compression.hwith full parameter documentation - Added
@warningtags about size=0 behavior - Documented sheet categories and compression formats in
rom.cc
Key Learnings
-
Default parameters can be overridden accidentally - When adding new parameters to a function call, be careful not to override defaults with wrong values.
-
Early-exit conditions can cause silent failures - The
if (size == 0) return emptywas valid behavior, but calling code must respect it. -
Diagnostic logging is valuable - The added probe logging for the first 5 graphics sheets helps quickly identify alignment issues.
-
Git archaeology is essential - Comparing with known-working commits reveals exactly what changed.
Related Files
src/app/rom.cc- Main ROM handling and graphics loadingsrc/app/gfx/util/compression.cc- LC-LZ2 decompression implementationsrc/app/gfx/util/compression.h- Decompression function declarationsincl/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