Files
yaze/docs/internal/archive/investigations/graphics-loading-regression-2024.md

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

  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:

    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:

    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:

    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.

  • 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