Files
yaze/docs/internal/archive/investigations/dungeon-rendering-analysis.md

12 KiB
Raw Blame History

Dungeon Rendering System Analysis

This document analyzes the dungeon object and background rendering pipeline, identifying potential issues with palette indexing, graphics buffer access, and memory safety.

Graphics Pipeline Overview

ROM Data (3BPP compressed)
    ↓
DecompressV2() → SnesTo8bppSheet()
    ↓
graphics_buffer_ (8BPP linear, values 0-7)
    ↓
Room::CopyRoomGraphicsToBuffer()
    ↓
current_gfx16_ (room-specific graphics buffer)
    ↓
ObjectDrawer::DrawTileToBitmap() / BackgroundBuffer::DrawTile()
    ↓
Bitmap pixel data (indexed 8BPP)
    ↓
SetPalette() → SDL Surface → Texture

Palette Structure

ROM Storage (kDungeonMainPalettes = 0xDD734)

  • 20 dungeon palette sets
  • 90 colors per set (180 bytes)
  • Colors packed without transparent entries

SNES Hardware Layout

The SNES expects 16-color rows with transparent at indices 0, 16, 32...

Current yaze Implementation

  • 90 colors loaded as linear array (indices 0-89)
  • 6 groups of 15 colors each
  • Palette stride: * 15

Palette Offset Analysis

Current Implementation

// object_drawer.cc:916
uint8_t palette_offset = (tile_info.palette_ & 0x07) * 15;

// background_buffer.cc:64
uint8_t palette_offset = palette_idx * 15;

The Math

For 3BPP graphics (pixel values 0-7):

  • Pixel 0 = transparent (skipped)
  • Pixels 1-7 → (pixel - 1) + palette_offset

With * 15 stride:

Palette Pixel 1 Pixel 7 Colors Used
0 0 6 0-6
1 15 21 15-21
2 30 36 30-36
3 45 51 45-51
4 60 66 60-66
5 75 81 75-81

Unused colors: 7-14, 22-29, 37-44, 52-59, 67-74, 82-89 (8 colors per group)

Verdict

The * 15 stride is correct for the 90-color packed format. The "wasted" colors are an artifact of:

  • ROM storing 15 colors per group (4BPP capacity)
  • Graphics using only 8 values (3BPP)

Current Status & Findings (2025-11-26)

1. 8BPP Conversion Mismatch (Fixed)

  • Issue: LoadAllGraphicsData converted 3BPP to 8BPP linear (1 byte/pixel), but Room::CopyRoomGraphicsToBuffer was treating it as 3BPP planar and trying to convert it to 4BPP packed. This caused double conversion and data corruption.
  • Fix: Updated CopyRoomGraphicsToBuffer to copy 8BPP data directly (4096 bytes per sheet). Updated draw routines to read 1 byte per pixel.

2. Palette Stride (Fixed)

  • Issue: Previous code used * 16 stride, which skipped colors in the packed 90-color palette.
  • Fix: Updated to * 15 stride and pixel - 1 indexing.

3. Buffer vs. Arena (Investigation)

  • Issue: We attempted to switch to gfx::Arena::Get().gfx_sheets() for safer access, but this resulted in an empty frame (likely due to initialization order or empty Arena).
  • Status: Reverted to rom()->graphics_buffer() but added strict 8BPP offset calculations (4096 bytes/sheet).
  • Artifacts: We observed "number-like" tiles (5, 7, 8) instead of dungeon walls. This suggests we might be reading from a font sheet or an incorrect offset in the buffer.
  • Next Step: Debug logging added to CopyRoomGraphicsToBuffer to print block IDs and raw bytes. This will confirm if we are reading valid graphics data or garbage.

4. LoadAnimatedGraphics sizeof vs size() (Fixed 2025-11-26)

  • Issue: room.cc:821,836 used sizeof(current_gfx16_) instead of .size() for bounds checking.
  • Context: For std::array<uint8_t, N>, sizeof equals N (works), but this pattern is confusing and fragile.
  • Fix: Updated to use current_gfx16_.size() for clarity and maintainability.

5. LoadRoomGraphics Entrance Blockset Condition (Not a Bug - 2025-11-26)

  • File: src/zelda3/dungeon/room.cc:352
  • Observation: Condition if (i == 6) applies entrance graphics only to block 6
  • Status: This is intentional behavior. The misleading "3-6" comment was removed.
  • Note: Changing to i >= 3 && i <= 6 caused tiling artifacts - reverted.

7. Layout Not Being Loaded (Fixed 2025-11-26) - MAJOR BREAKTHROUGH

  • File: src/zelda3/dungeon/room.cc - LoadLayoutTilesToBuffer()
  • Issue: layout_.LoadLayout(layout) was never called, so layout_.GetObjects() always returned empty
  • Impact: Only floor tiles were drawn, no layout tiles appeared
  • Fix: Added layout_.set_rom(rom_) and layout_.LoadLayout(layout) call before accessing layout objects
  • Result: WALLS NOW RENDER CORRECTLY! Left/right walls display properly.
  • Remaining: Some objects still don't look right - needs further investigation

Breakthrough Status (2025-11-26)

What's Working Now

  • Floor tiles render correctly
  • Layout tiles load from ROM
  • Left/right walls display correctly
  • Basic room structure visible

What Still Needs Work

  • ⚠️ Some objects don't render correctly
  • ⚠️ Need to verify object tile IDs and graphics lookup
  • ⚠️ May be palette or graphics sheet issues for specific object types
  • ⚠️ Floor rendering (screenshot shows grid, need to confirm if floor tiles are actually rendering or if the grid is obscuring them)

Next Investigation Steps

  1. Verify Floor Rendering: Check if the floor tiles are actually rendering underneath the grid or if they are missing.
  2. Check Object Types: Identify which specific objects are rendering incorrectly (e.g., chests, pots, enemies).
  3. Verify Tile IDs: Check RoomObject::DecodeObjectFromBytes() and GetTile() to ensure correct tile IDs are being calculated.
  4. Debug Logging: Use the added logging to verify that the correct graphics sheets are being loaded for the objects.

Detailed Context for Next Session

Architecture Overview

The dungeon rendering has TWO separate tile systems:

  1. Layout Tiles (RoomLayout class) - Pre-defined room templates (8 layouts total)

    • Loaded from ROM via kRoomLayoutPointers[] in dungeon_rom_addresses.h
    • Rendered by LoadLayoutTilesToBuffer()bg1_buffer_.SetTileAt() / bg2_buffer_.SetTileAt()
    • NOW WORKING after the LoadLayout fix
  2. Object Tiles (RoomObject class) - Placed objects (walls, doors, decorations, etc.)

    • Loaded from ROM via LoadObjects()ParseObjectsFromLocation()
    • Rendered by RenderObjectsToBackground()ObjectDrawer::DrawObject()
    • PARTIALLY WORKING - walls visible but some objects look wrong

Key Files for Object Rendering

File Purpose
room.cc:LoadObjects() Parses object data from ROM
room.cc:RenderObjectsToBackground() Iterates objects, calls ObjectDrawer
object_drawer.cc Main object rendering logic
object_drawer.cc:DrawTileToBitmap() Draws individual 8x8 tiles
room_object.cc:DecodeObjectFromBytes() Decodes 3-byte object format
room_object.cc:GetTile() Returns TileInfo for object tiles

Object Encoding Format (3 bytes)

Byte 1: YYYYY XXX  (Y = tile Y position bits 4-0, X = tile X position bits 2-0)
Byte 2: S XXX YYYY (S = size bit, X = tile X position bits 5-3, Y = tile Y position bits 8-5)
Byte 3: OOOOOOOO  (Object ID)

Potential Object Rendering Issues to Investigate

  1. Tile ID Calculation

    • Objects use GetTile(index) to get TileInfo for each sub-tile
    • The tile ID might be calculated incorrectly for some object types
    • Check RoomObject::EnsureTilesLoaded() and tile lookup tables
  2. Graphics Sheet Selection

    • Objects should use tiles from current_gfx16_ (room-specific buffer)
    • Different object types may need tiles from different sheet ranges:
      • Blocks 0-7: Main dungeon graphics
      • Blocks 8-11: Static sprites (pots, fairies, etc.)
      • Blocks 12-15: Enemy sprites
  3. Palette Assignment

    • Objects have a palette_ field in TileInfo
    • Dungeon palette has 6 groups × 15 colors = 90 colors
    • Palette offset = (palette_ & 0x07) * 15
    • Some objects might have wrong palette index
  4. Object Type Handlers

    • ObjectDrawer has different draw methods for different object sizes
    • DrawSingle(), Draw2x2(), DrawVertical(), DrawHorizontal(), etc.
    • Some handlers might have bugs in tile placement

Debug Logging Currently Active

  • [CopyRoomGraphicsToBuffer] - Logs block/sheet IDs and first bytes
  • [RenderRoomGraphics] - Logs dirty flags and floor graphics
  • [LoadLayoutTilesToBuffer] - Logs layout object count
  • [ObjectDrawer] - Logs first 5 tile draws with position/palette info

Files Modified in This Session

  1. src/zelda3/dungeon/room.cc:LoadLayoutTilesToBuffer() - Added layout loading call
  2. src/zelda3/dungeon/room.cc:LoadRoomGraphics() - Fixed comment, kept i==6 condition
  3. src/app/app.cmake - Added z3ed WASM exports
  4. src/app/editor/ui/ui_coordinator.cc - Fixed menu bar right panel positioning
  5. src/app/rom.cc - Added debug logging for graphics loading

Quick Test Commands

# Build
cmake --build build --target yaze -j4

# Run with dungeon editor
./build/bin/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon

6. 2BPP Placeholder Sheets (Verified 2025-11-26)

  • Sheets 113-114: These are 2BPP font/title sheets loaded separately via Load2BppGraphics()
  • In graphics_buffer: They contain 0xFF placeholder data (4096 bytes each)
  • Impact: If blockset IDs accidentally point to 113-114, tiles render as solid color
  • Status: This is expected behavior, not a bug

Debugging the "Number-Like" Artifacts

The observed "5, 7, 8" number patterns could indicate:

  1. Font Sheet Access: Blockset IDs pointing to font sheets (but sheets 113-114 have 0xFF, not font data)
  2. Debug Rendering: Tile IDs or coordinates rendered as text (check for printf to canvas)
  3. Corrupted Offset: Wrong src_index calculation causing read from arbitrary memory
  4. Uninitialized blocks_: If LoadRoomGraphics() not called before CopyRoomGraphicsToBuffer()

Debug Logging Added (room.cc)

printf("[CopyRoomGraphicsToBuffer] Block %d (Sheet %d): Offset %d\n", block, sheet_id, src_sheet_offset);
printf("  Bytes: %02X %02X %02X %02X %02X %02X %02X %02X\n", ...);

Next Steps

  1. Run the application and check console output for:
    • Sheet IDs for each block (should be 0-112 or 115-126 for valid dungeon graphics)
    • First bytes of each sheet (should NOT be 0xFF for valid graphics)
  2. If sheet IDs are valid but graphics are wrong, check:
    • LoadGfxGroups() output for blockset 0 (verify main_blockset_ids)
    • GetGraphicsAddress() returning correct ROM offsets

Graphics Buffer Layout

Per-Sheet (4096 bytes each)

  • Width: 128 pixels (16 tiles × 8 pixels)
  • Height: 32 pixels (4 tiles × 8 pixels)
  • Format: 8BPP linear (1 byte per pixel)
  • Values: 0-7 for 3BPP graphics

Room Graphics Buffer (current_gfx16_)

  • Size: 64KB (0x10000 bytes)
  • Layout: 16 blocks × 4096 bytes
  • Contains: Room-specific graphics from blocks_[0..15]

Index Calculation

int tile_col = tile_id % 16;
int tile_row = tile_id / 16;
int tile_base_x = tile_col * 8;
int tile_base_y = tile_row * 1024;  // 8 rows * 128 bytes stride
int src_index = (py * 128) + px + tile_base_x + tile_base_y;

Files Involved

File Function Purpose
rom.cc LoadAllGraphicsData() Decompresses and converts 3BPP→8BPP
room.cc CopyRoomGraphicsToBuffer() Copies sheet data to room buffer
room.cc LoadAnimatedGraphics() Loads animated tile data
room.cc RenderRoomGraphics() Renders room with palette
object_drawer.cc DrawTileToBitmap() Draws object tiles
background_buffer.cc DrawTile() Draws background tiles
snes_palette.cc LoadDungeonMainPalettes() Loads 90-color palettes
snes_tile.cc SnesTo8bppSheet() Converts 3BPP→8BPP