Files
yaze/docs/internal/archive/investigations/object-rendering-fixes.md

13 KiB
Raw Blame History

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.h
  • src/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.h
  • src/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

  1. Build and test:

    cmake --build build --target yaze -j4
    ./build/bin/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon
    
  2. 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
  3. Compare with ZScream:

    • Open same room in ZScream
    • Verify tile-by-tile rendering matches
  4. 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 0 tile 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