13 KiB
Object Rendering Fixes - Action Plan
Date: 2025-11-26 Based on: ZScream comparison analysis Status: Ready for implementation
Problem Summary
After fixing the layout loading issue (walls now render correctly), some dungeon objects still render incorrectly. Analysis of ZScream's implementation reveals yaze loads incorrect tile counts per object.
Root Cause: yaze hardcodes 8 tiles per object, while ZScream loads 1-242 tiles based on object type.
Fix 1: Object Tile Count Lookup Table (CRITICAL)
Files to Modify
src/zelda3/dungeon/object_parser.hsrc/zelda3/dungeon/object_parser.cc
Implementation
Step 1: Add tile count lookup table in object_parser.h:
// Object-specific tile counts (from ZScream's RoomObjectTileLister)
// These specify how many 16-bit tile words to read from ROM per object
static const std::unordered_map<int16_t, int> kObjectTileCounts = {
// Subtype 1 objects (0x000-0x0FF)
{0x000, 4}, {0x001, 8}, {0x002, 8}, {0x003, 8},
{0x004, 8}, {0x005, 8}, {0x006, 8}, {0x007, 8},
{0x008, 4}, {0x009, 5}, {0x00A, 5}, {0x00B, 5},
{0x00C, 5}, {0x00D, 5}, {0x00E, 5}, {0x00F, 5},
{0x010, 5}, {0x011, 5}, {0x012, 5}, {0x013, 5},
{0x014, 5}, {0x015, 5}, {0x016, 5}, {0x017, 5},
{0x018, 5}, {0x019, 5}, {0x01A, 5}, {0x01B, 5},
{0x01C, 5}, {0x01D, 5}, {0x01E, 5}, {0x01F, 5},
{0x020, 5}, {0x021, 9}, {0x022, 3}, {0x023, 3},
{0x024, 3}, {0x025, 3}, {0x026, 3}, {0x027, 3},
{0x028, 3}, {0x029, 3}, {0x02A, 3}, {0x02B, 3},
{0x02C, 3}, {0x02D, 3}, {0x02E, 3}, {0x02F, 6},
{0x030, 6}, {0x031, 0}, {0x032, 0}, {0x033, 16},
{0x034, 1}, {0x035, 1}, {0x036, 16}, {0x037, 16},
{0x038, 6}, {0x039, 8}, {0x03A, 12}, {0x03B, 12},
{0x03C, 4}, {0x03D, 8}, {0x03E, 4}, {0x03F, 3},
{0x040, 3}, {0x041, 3}, {0x042, 3}, {0x043, 3},
{0x044, 3}, {0x045, 3}, {0x046, 3}, {0x047, 0},
{0x048, 0}, {0x049, 8}, {0x04A, 8}, {0x04B, 4},
{0x04C, 9}, {0x04D, 16}, {0x04E, 16}, {0x04F, 16},
{0x050, 1}, {0x051, 18}, {0x052, 18}, {0x053, 4},
{0x054, 0}, {0x055, 8}, {0x056, 8}, {0x057, 0},
{0x058, 0}, {0x059, 0}, {0x05A, 0}, {0x05B, 18},
{0x05C, 18}, {0x05D, 15}, {0x05E, 4}, {0x05F, 3},
{0x060, 4}, {0x061, 8}, {0x062, 8}, {0x063, 8},
{0x064, 8}, {0x065, 8}, {0x066, 8}, {0x067, 4},
{0x068, 4}, {0x069, 3}, {0x06A, 1}, {0x06B, 1},
{0x06C, 6}, {0x06D, 6}, {0x06E, 0}, {0x06F, 0},
{0x070, 16}, {0x071, 1}, {0x072, 0}, {0x073, 16},
{0x074, 16}, {0x075, 8}, {0x076, 16}, {0x077, 16},
{0x078, 4}, {0x079, 1}, {0x07A, 1}, {0x07B, 4},
{0x07C, 1}, {0x07D, 4}, {0x07E, 0}, {0x07F, 8},
{0x080, 8}, {0x081, 12}, {0x082, 12}, {0x083, 12},
{0x084, 12}, {0x085, 18}, {0x086, 18}, {0x087, 8},
{0x088, 12}, {0x089, 4}, {0x08A, 3}, {0x08B, 3},
{0x08C, 3}, {0x08D, 1}, {0x08E, 1}, {0x08F, 6},
{0x090, 8}, {0x091, 8}, {0x092, 4}, {0x093, 4},
{0x094, 16}, {0x095, 4}, {0x096, 4}, {0x097, 0},
{0x098, 0}, {0x099, 0}, {0x09A, 0}, {0x09B, 0},
{0x09C, 0}, {0x09D, 0}, {0x09E, 0}, {0x09F, 0},
{0x0A0, 1}, {0x0A1, 1}, {0x0A2, 1}, {0x0A3, 1},
{0x0A4, 24}, {0x0A5, 1}, {0x0A6, 1}, {0x0A7, 1},
{0x0A8, 1}, {0x0A9, 1}, {0x0AA, 1}, {0x0AB, 1},
{0x0AC, 1}, {0x0AD, 0}, {0x0AE, 0}, {0x0AF, 0},
{0x0B0, 1}, {0x0B1, 1}, {0x0B2, 16}, {0x0B3, 3},
{0x0B4, 3}, {0x0B5, 8}, {0x0B6, 8}, {0x0B7, 8},
{0x0B8, 4}, {0x0B9, 4}, {0x0BA, 16}, {0x0BB, 4},
{0x0BC, 4}, {0x0BD, 4}, {0x0BE, 0}, {0x0BF, 0},
{0x0C0, 1}, {0x0C1, 68}, {0x0C2, 1}, {0x0C3, 1},
{0x0C4, 8}, {0x0C5, 8}, {0x0C6, 8}, {0x0C7, 8},
{0x0C8, 8}, {0x0C9, 8}, {0x0CA, 8}, {0x0CB, 0},
{0x0CC, 0}, {0x0CD, 28}, {0x0CE, 28}, {0x0CF, 0},
{0x0D0, 0}, {0x0D1, 8}, {0x0D2, 8}, {0x0D3, 0},
{0x0D4, 0}, {0x0D5, 0}, {0x0D6, 0}, {0x0D7, 1},
{0x0D8, 8}, {0x0D9, 8}, {0x0DA, 8}, {0x0DB, 8},
{0x0DC, 21}, {0x0DD, 16}, {0x0DE, 4}, {0x0DF, 8},
{0x0E0, 8}, {0x0E1, 8}, {0x0E2, 8}, {0x0E3, 8},
{0x0E4, 8}, {0x0E5, 8}, {0x0E6, 8}, {0x0E7, 8},
{0x0E8, 8}, {0x0E9, 0}, {0x0EA, 0}, {0x0EB, 0},
{0x0EC, 0}, {0x0ED, 0}, {0x0EE, 0}, {0x0EF, 0},
{0x0F0, 0}, {0x0F1, 0}, {0x0F2, 0}, {0x0F3, 0},
{0x0F4, 0}, {0x0F5, 0}, {0x0F6, 0}, {0x0F7, 0},
// Subtype 2 objects (0x100-0x13F) - all 16 tiles for corners
{0x100, 16}, {0x101, 16}, {0x102, 16}, {0x103, 16},
{0x104, 16}, {0x105, 16}, {0x106, 16}, {0x107, 16},
{0x108, 16}, {0x109, 16}, {0x10A, 16}, {0x10B, 16},
{0x10C, 16}, {0x10D, 16}, {0x10E, 16}, {0x10F, 16},
{0x110, 12}, {0x111, 12}, {0x112, 12}, {0x113, 12},
{0x114, 12}, {0x115, 12}, {0x116, 12}, {0x117, 12},
{0x118, 4}, {0x119, 4}, {0x11A, 4}, {0x11B, 4},
{0x11C, 16}, {0x11D, 6}, {0x11E, 4}, {0x11F, 4},
{0x120, 4}, {0x121, 6}, {0x122, 20}, {0x123, 12},
{0x124, 16}, {0x125, 16}, {0x126, 6}, {0x127, 4},
{0x128, 20}, {0x129, 16}, {0x12A, 8}, {0x12B, 4},
{0x12C, 18}, {0x12D, 16}, {0x12E, 16}, {0x12F, 16},
{0x130, 16}, {0x131, 16}, {0x132, 16}, {0x133, 16},
{0x134, 4}, {0x135, 8}, {0x136, 8}, {0x137, 40},
{0x138, 12}, {0x139, 12}, {0x13A, 12}, {0x13B, 12},
{0x13C, 24}, {0x13D, 12}, {0x13E, 18}, {0x13F, 56},
// Subtype 3 objects (0x200-0x27F)
{0x200, 12}, {0x201, 20}, {0x202, 28}, {0x203, 1},
{0x204, 1}, {0x205, 1}, {0x206, 1}, {0x207, 1},
{0x208, 1}, {0x209, 1}, {0x20A, 1}, {0x20B, 1},
{0x20C, 1}, {0x20D, 6}, {0x20E, 1}, {0x20F, 1},
{0x210, 4}, {0x211, 4}, {0x212, 4}, {0x213, 4},
{0x214, 12}, {0x215, 80}, {0x216, 4}, {0x217, 6},
{0x218, 4}, {0x219, 4}, {0x21A, 4}, {0x21B, 16},
{0x21C, 16}, {0x21D, 16}, {0x21E, 16}, {0x21F, 16},
{0x220, 16}, {0x221, 16}, {0x222, 4}, {0x223, 4},
{0x224, 4}, {0x225, 4}, {0x226, 16}, {0x227, 16},
{0x228, 16}, {0x229, 16}, {0x22A, 16}, {0x22B, 4},
{0x22C, 16}, {0x22D, 84}, {0x22E, 127}, {0x22F, 4},
{0x230, 4}, {0x231, 12}, {0x232, 12}, {0x233, 16},
{0x234, 6}, {0x235, 6}, {0x236, 18}, {0x237, 18},
{0x238, 18}, {0x239, 18}, {0x23A, 24}, {0x23B, 24},
{0x23C, 24}, {0x23D, 24}, {0x23E, 4}, {0x23F, 4},
{0x240, 4}, {0x241, 4}, {0x242, 4}, {0x243, 4},
{0x244, 4}, {0x245, 4}, {0x246, 4}, {0x247, 16},
{0x248, 16}, {0x249, 4}, {0x24A, 4}, {0x24B, 24},
{0x24C, 48}, {0x24D, 18}, {0x24E, 12}, {0x24F, 4},
{0x250, 4}, {0x251, 4}, {0x252, 4}, {0x253, 4},
{0x254, 26}, {0x255, 16}, {0x256, 4}, {0x257, 4},
{0x258, 6}, {0x259, 4}, {0x25A, 8}, {0x25B, 32},
{0x25C, 24}, {0x25D, 18}, {0x25E, 4}, {0x25F, 4},
{0x260, 18}, {0x261, 18}, {0x262, 242}, {0x263, 4},
{0x264, 4}, {0x265, 4}, {0x266, 16}, {0x267, 12},
{0x268, 12}, {0x269, 12}, {0x26A, 12}, {0x26B, 16},
{0x26C, 12}, {0x26D, 12}, {0x26E, 12}, {0x26F, 12},
{0x270, 32}, {0x271, 64}, {0x272, 80}, {0x273, 1},
{0x274, 64}, {0x275, 4}, {0x276, 64}, {0x277, 24},
{0x278, 32}, {0x279, 12}, {0x27A, 16}, {0x27B, 8},
{0x27C, 4}, {0x27D, 4}, {0x27E, 4},
};
// Helper function to get tile count for an object
inline int GetObjectTileCount(int16_t object_id) {
auto it = kObjectTileCounts.find(object_id);
return (it != kObjectTileCounts.end()) ? it->second : 8; // Default 8 if not found
}
Step 2: Update object_parser.cc to use the lookup table:
// Replace lines 141, 160, 178 with:
int tile_count = GetObjectTileCount(object_id);
return ReadTileData(tile_data_ptr, tile_count);
Before:
absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseSubtype1(
int16_t object_id) {
// ...
return ReadTileData(tile_data_ptr, 8); // ❌ WRONG
}
After:
absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseSubtype1(
int16_t object_id) {
// ...
int tile_count = GetObjectTileCount(object_id);
return ReadTileData(tile_data_ptr, tile_count); // ✅ CORRECT
}
Testing
Test with these specific objects after fix:
| Object | Tile Count | What It Is | Expected Result |
|---|---|---|---|
| 0x033 | 16 | Carpet | Full 4×4 pattern visible |
| 0x0C1 | 68 | Chest platform (tall) | Complete platform structure |
| 0x215 | 80 | Kholdstare prison cell | Full prison bars |
| 0x22D | 84 | Agahnim's altar | Symmetrical 14-tile-wide altar |
| 0x22E | 127 | Agahnim's boss room | Complete room structure |
| 0x262 | 242 | Fortune teller room | Full room layout |
Fix 2: Tile Transformation Support (MEDIUM PRIORITY)
Problem
Objects with horizontal/vertical mirroring (like Agahnim's altar) render incorrectly because tile transformations aren't applied.
Files to Modify
src/zelda3/dungeon/object_drawer.hsrc/zelda3/dungeon/object_drawer.cc
Implementation
Update WriteTile8() signature:
// In object_drawer.h
void WriteTile8(gfx::BackgroundBuffer& bg, uint8_t x_grid, uint8_t y_grid,
const gfx::TileInfo& tile_info,
bool h_flip = false, bool v_flip = false);
// In object_drawer.cc
void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, uint8_t x_grid,
uint8_t y_grid, const gfx::TileInfo& tile_info,
bool h_flip, bool v_flip) {
// ... existing code ...
for (int py = 0; py < 8; py++) {
for (int px = 0; px < 8; px++) {
// Apply transformations
int src_x = h_flip ? (7 - px) : px;
int src_y = v_flip ? (7 - py) : py;
int src_index = (src_y * 128) + src_x + tile_base_x + tile_base_y;
// ... rest of pixel drawing code ...
}
}
}
Then update draw routines to use transformations from TileInfo:
WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0],
tiles[0].horizontal_mirror_, tiles[0].vertical_mirror_);
Fix 3: Update Draw Routines (OPTIONAL - For Complex Objects)
Some objects may need custom draw routines beyond pattern-based drawing. Consider implementing for:
- 0x22D (Agahnim's Altar): Symmetrical mirrored structure
- 0x22E (Agahnim's Boss Room): Complex multi-tile layout
- 0x262 (Fortune Teller Room): Extremely large 242-tile object
These could use a DrawInfo-based approach like ZScream:
struct DrawInfo {
int tile_index;
int x_offset; // In pixels
int y_offset; // In pixels
bool h_flip;
bool v_flip;
};
void DrawFromInstructions(const RoomObject& obj, gfx::BackgroundBuffer& bg,
std::span<const gfx::TileInfo> tiles,
const std::vector<DrawInfo>& instructions);
Expected Outcomes
After implementing Fix 1:
- ✅ Carpets (0x33) render with full 4×4 tile pattern
- ✅ Chest platforms (0xC1) render with complete structure
- ✅ Large objects (0x215, 0x22D, 0x22E) appear (though possibly with wrong orientation)
After implementing Fix 2:
- ✅ Symmetrical objects render correctly with mirroring
- ✅ Agahnim's altar/room display properly
Verification Steps
-
Build and test:
cmake --build build --target yaze -j4 ./build/bin/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon -
Test rooms with specific objects:
- Room 0x0C (Eastern Palace): Basic walls and carpets
- Room 0x20 (Agahnim's Tower): Agahnim's altar (0x22D)
- Room 0x00 (Sanctuary): Basic objects
-
Compare with ZScream:
- Open same room in ZScream
- Verify tile-by-tile rendering matches
-
Log verification:
printf("[ObjectParser] Object %04X: Loading %d tiles\n", object_id, tile_count);
Notes for Implementation
- The tile count lookup table comes directly from ZScream's
RoomObjectTileLister.cs:23-534 - Each entry represents the number of 16-bit tile words (2 bytes each) to read from ROM
- Objects with
0tile count are empty placeholders or special objects (moving walls, etc.) - Tile transformations (h_flip, v_flip) are stored in TileInfo from ROM data (bits in tile word)
- Some objects (0x0CD, 0x0CE) load tiles from multiple ROM addresses (not yet supported)
References
- ZScream Source:
/Users/scawful/Code/ZScreamDungeon/ZeldaFullEditor/Data/Underworld/RoomObjectTileLister.cs - Analysis Document:
/Users/scawful/Code/yaze/docs/internal/zscream-comparison-object-rendering.md - Rendering Analysis:
/Users/scawful/Code/yaze/docs/internal/dungeon-rendering-analysis.md