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
- ✅ Fix Bug #1 (ROM addresses) - 5 minutes
- ✅ Fix Bug #2 (palette application) - 15 minutes
- ✅ Fix Bug #3 (palette offset) - 5 minutes
- ⚠️ 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
- ALTTP Disassembly: https://github.com/
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