Files
yaze/docs/internal/agents/archive/reports-2025/dungeon-graphics-bug-report.md

9.0 KiB

Dungeon Graphics Rendering Bug Report

Status: CRITICAL - Objects not rendering Affected System: Dungeon Object Editor Root Causes: 4 critical bugs identified Research By: zelda3-hacking-expert + backend-infra-engineer agents


Executive Summary

Dungeon objects are not rendering correctly due to incorrect ROM addresses and missing palette application. Four critical bugs have been identified in the rendering pipeline.


CRITICAL BUG #1: Wrong ROM Addresses in ObjectParser ⚠️

Priority: P0 - BLOCKER File: src/zelda3/dungeon/object_parser.cc (Lines 10-14) Impact: Objects read garbage data from ROM

Current Code (WRONG)

static constexpr int kRoomObjectSubtype1 = 0x0A8000;  // ❌ PLACEHOLDER
static constexpr int kRoomObjectSubtype2 = 0x0A9000;  // ❌ PLACEHOLDER
static constexpr int kRoomObjectSubtype3 = 0x0AA000;  // ❌ PLACEHOLDER
static constexpr int kRoomObjectTileAddress = 0x0AB000;  // ❌ PLACEHOLDER

These addresses don't exist in ALTTP's ROM! They are placeholders from early development.

Fix (CORRECT)

// ALTTP US 1.0 ROM addresses (PC format)
static constexpr int kRoomObjectSubtype1 = 0x0F8000;      // SNES: $08:8000
static constexpr int kRoomObjectSubtype2 = 0x0F83F0;      // SNES: $08:83F0
static constexpr int kRoomObjectSubtype3 = 0x0F84F0;      // SNES: $08:84F0
static constexpr int kRoomObjectTileAddress = 0x091B52;   // SNES: $09:1B52

Explanation

How ALTTP Object Graphics Work:

1. Object ID (e.g., $10 = wall) → Subtype Table Lookup
   ├─ Read pointer from: kRoomObjectSubtype1 + (ID * 2)
   └─ Pointer is 16-bit offset from kRoomObjectTileAddress

2. Calculate Tile Data Address
   ├─ tile_data_addr = kRoomObjectTileAddress + offset
   └─ Each tile = 2 bytes (TileInfo word)

3. TileInfo Word Format (16-bit: vhopppcccccccccc)
   ├─ v (bit 15): Vertical flip
   ├─ h (bit 14): Horizontal flip
   ├─ o (bit 13): Priority/Over flag
   ├─ ppp (bits 10-12): Palette index (0-7)
   └─ cccccccccc (bits 0-9): CHR tile ID (0-1023)

Example for Object $10 (Wall):

1. Subtype 1 table: 0x0F8000 + ($10 * 2) = 0x0F8020
2. Read offset: [Low, High] = $0234
3. Tile data: 0x091B52 + $0234 = 0x091D86
4. Read TileInfo words (8 tiles = 16 bytes)

CRITICAL BUG #2: Missing Palette Application ⚠️

Priority: P0 - BLOCKER File: src/zelda3/dungeon/object_drawer.cc (Lines 76-104) Impact: Black screen or wrong colors

The Problem

ObjectDrawer writes palette index values (0-255) to the bitmap, but never applies the dungeon palette to the SDL surface. The bitmap has no color information!

Current Flow:

ObjectDrawer writes index values → memcpy to SDL surface → Display ❌
                                     ↑
                            No palette applied!

Should Be:

ObjectDrawer writes index values → Apply palette → memcpy to SDL → Display ✅

Fix

Add to ObjectDrawer::DrawObjectList() after line 77:

absl::Status ObjectDrawer::DrawObjectList(
    const std::vector<RoomObject>& objects,
    gfx::BackgroundBuffer& bg1,
    gfx::BackgroundBuffer& bg2,
    const gfx::PaletteGroup& palette_group) {

  // Draw all objects
  for (const auto& object : objects) {
    RETURN_IF_ERROR(DrawObject(object, bg1, bg2, palette_group));
  }

  // ✅ FIX: Apply dungeon palette to background buffers
  auto& bg1_bmp = bg1.bitmap();
  auto& bg2_bmp = bg2.bitmap();

  if (!palette_group.empty()) {
    const auto& dungeon_palette = palette_group[0];  // Main dungeon palette (90 colors)
    bg1_bmp.SetPalette(dungeon_palette);
    bg2_bmp.SetPalette(dungeon_palette);
  }

  // Sync bitmap data to SDL surfaces AFTER palette is applied
  if (bg1_bmp.modified() && bg1_bmp.surface() && !bg1_bmp.data().empty()) {
    SDL_LockSurface(bg1_bmp.surface());
    memcpy(bg1_bmp.surface()->pixels, bg1_bmp.data().data(), bg1_bmp.data().size());
    SDL_UnlockSurface(bg1_bmp.surface());
  }

  if (bg2_bmp.modified() && bg2_bmp.surface() && !bg2_bmp.data().empty()) {
    SDL_LockSurface(bg2_bmp.surface());
    memcpy(bg2_bmp.surface()->pixels, bg2_bmp.data().data(), bg2_bmp.data().size());
    SDL_UnlockSurface(bg2_bmp.surface());
  }

  return absl::OkStatus();
}

BUG #3: Incorrect Palette Offset Calculation

Priority: P1 - HIGH File: src/zelda3/dungeon/object_drawer.cc (Line 900) Impact: Wrong colors for objects

Current Code (WRONG)

// Line 899-900
uint8_t palette_offset = (tile_info.palette_ & 0x0F) * 8;

Problem: Uses 4 bits (& 0x0F) but dungeon graphics are 3BPP with only 3-bit palette indices!

Fix

// Dungeon graphics are 3BPP (8 colors per palette)
// Only use 3 bits for palette index (0-7)
uint8_t palette_offset = (tile_info.palette_ & 0x07) * 8;

Dungeon Palette Structure

From snes_palette.cc line 198:

  • Total: 90 colors per dungeon palette
  • Colors 0-29: Main graphics (palettes 0-3)
  • Colors 30-59: Secondary graphics (palettes 4-7)
  • Colors 60-89: Sprite graphics (palettes 8-11)

Each sub-palette has 8 colors (3BPP), arranged:

  • Palette 0: Colors 0-7
  • Palette 1: Colors 8-15
  • Palette 2: Colors 16-23
  • Palette 3: Colors 24-29 (NOT 24-31!)

BUG #4: Palette Metadata Not Initialized

Priority: P2 - MEDIUM File: src/app/gfx/render/background_buffer.cc (constructor) Impact: ApplyPaletteByMetadata() may not work correctly

Fix

Ensure BackgroundBuffer initializes bitmap metadata:

BackgroundBuffer::BackgroundBuffer(int width, int height)
    : width_(width), height_(height) {
  buffer_.resize((width / 8) * (height / 8));
  std::vector<uint8_t> data(width * height, 0);

  // Create 8-bit indexed color bitmap
  bitmap_.Create(width, height, 8, data);

  // Set metadata for dungeon rendering
  auto& metadata = bitmap_.metadata();
  metadata.source_bpp = 3;               // 3BPP dungeon graphics
  metadata.palette_format = 0;           // Full palette (90 colors)
  metadata.source_type = "dungeon_background";
  metadata.palette_colors = 90;          // Dungeon main palette size
}

Complete Rendering Pipeline

Correct Flow

1. ROM Data (0x0F8000+) → ObjectParser
   ├─ Read subtype table
   ├─ Calculate tile data offset
   └─ Parse TileInfo words

2. TileInfo[] → ObjectDrawer
   ├─ For each tile:
   │   ├─ Calculate position in graphics sheet
   │   ├─ Read 8x8 indexed pixels (0-7)
   │   ├─ Apply palette offset: pixel + (palette * 8)
   │   └─ Write to BackgroundBuffer bitmap
   └─ Apply dungeon palette to bitmap (SetPalette)

3. BackgroundBuffer → SDL Surface
   ├─ memcpy indexed pixel data
   └─ SDL uses surface palette for display

4. SDL Surface → ImGui Texture → Screen

ROM Address Reference

Structure SNES Address PC Address Purpose
Subtype 1 Table $08:8000 0x0F8000 Objects $00-$FF pointers (512 bytes)
Subtype 2 Table $08:83F0 0x0F83F0 Objects $100-$1FF pointers (256 bytes)
Subtype 3 Table $08:84F0 0x0F84F0 Objects $F00-$FFF pointers (256 bytes)
Tile Data Base $09:1B52 0x091B52 TileInfo word arrays (~8KB)
Graphics Sheets $0C:8000+ 0x0C8000+ 4BPP compressed CHR data

Implementation Order

  1. Fix Bug #1 (ROM addresses) - 5 minutes
  2. Fix Bug #2 (palette application) - 15 minutes
  3. Fix Bug #3 (palette offset) - 5 minutes
  4. ⚠️ Fix Bug #4 (metadata) - 10 minutes (verify needed)

Total Time: ~35 minutes to fix all critical bugs


Testing Checklist

After fixes:

  • Load dungeon room 0x01 (Eastern Palace entrance)
  • Verify gray stone walls render correctly
  • Check that objects have distinct colors
  • Verify no black/transparent artifacts
  • Test multiple rooms with different palettes
  • Verify BG1 and BG2 layers are distinct

Debugging Commands

Add these logs to verify the fix:

// In ObjectParser::ReadTileData() after reading first tile:
if (i == 0) {
  printf("[ObjectParser] Object 0x%03X: tile_addr=0x%06X word=0x%04X → id=%03X pal=%d\n",
         object_id, tile_offset, tile_word, tile_info.id_, tile_info.palette_);
}

// In ObjectDrawer::DrawObjectList() after applying palette:
if (!palette_group.empty()) {
  const auto& pal = palette_group[0];
  printf("[ObjectDrawer] Applied palette: %zu colors, first=RGB(%d,%d,%d)\n",
         pal.size(), pal[0].rom_color().red, pal[0].rom_color().green, pal[0].rom_color().blue);
}

// In DrawTileToBitmap() after palette calculation:
printf("[Tile] ID=0x%03X pal_idx=%d offset=%d pixel[0]=%d\n",
       tile_info.id_, tile_info.palette_, palette_offset, tiledata[0]);

References

spannerisms/ALTTPR-estrela

  • ZScream Source: DungeonObjectData.cs (C# implementation)
  • yaze Graphics System: CLAUDE.md Pattern 4 (Bitmap sync requirements)

Last Updated: 2025-11-21 Research By: CLAUDE_CORE (zelda3-hacking-expert + backend-infra-engineer) Status: Ready for implementation