Files
yaze/docs/dungeon_graphics_pipeline_analysis.md
scawful 5c863b1445 Refactor graphics optimizations documentation and add ImGui widget testing guide
- Updated the gfx_optimizations_complete.md to streamline the overview and implementation details of graphics optimizations, removing completed status indicators and enhancing clarity on future recommendations.
- Introduced imgui_widget_testing_guide.md, detailing the usage of YAZE's ImGui testing infrastructure for automated GUI testing, including architecture, integration steps, and best practices.
- Created ollama_integration_status.md to document the current status of Ollama integration, highlighting completed tasks, ongoing issues, and next steps for improvement.
- Revised developer_guide.md to reflect the latest updates in AI provider configuration and input methods for the z3ed agent, ensuring clarity on command-line flags and supported providers.
2025-10-04 03:24:42 -04:00

18 KiB

Dungeon Graphics Rendering Pipeline Analysis

Overview

This document provides a comprehensive analysis of how the YAZE dungeon editor renders room graphics, including the interaction between bitmaps, arena buffers, palettes, and palette groups.

Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│                         ROM Data                                 │
│ ┌────────────────────────┐ ┌──────────────────────────────────┐│
│ │ Room Headers           │ │ Dungeon Palettes                 ││
│ │  - Palette ID          │ │  - dungeon_main[id][180 colors] ││
│ │  - Blockset ID         │ │  - sprites_aux1[id][colors]      ││
│ │  - Spriteset ID        │ │  - Palette Groups                ││
│ │  - Background ID       │ │                                  ││
│ └────────────────────────┘ └──────────────────────────────────┘│
└───────────────────────────┬─────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                  Room Loading (room.cc)                          │
│                                                                  │
│  LoadRoomFromRom() →  LoadRoomGraphics() → Copy RoomGraphics   │
│    ├─ Load 16 blocks (graphics sheets)                          │
│    ├─ blocks[0-7]: Main blockset                                │
│    ├─ blocks[8-11]: Static sprites (fairies, pots, etc.)        │
│    └─ blocks[12-15]: Spriteset sprites                          │
└───────────────────────────┬─────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                Graphics Arena (arena.h/.cc)                      │
│                                                                  │
│  ┌──────────────────┐  ┌──────────────────┐                    │
│  │ gfx_sheets_[223] │  │ Background       │                    │
│  │ (Bitmap objects) │  │ Buffers          │                    │
│  │                  │  │  - bg1_          │                    │
│  │  Each holds:     │  │  - bg2_          │                    │
│  │  - Pixel data    │  │                  │                    │
│  │  - SDL Surface   │  │  layer1_buffer_  │                    │
│  │  - SDL Texture   │  │  layer2_buffer_  │                    │
│  │  - Palette       │  │  [64x64 = 4096   │                    │
│  └──────────────────┘  │   tile words]    │                    │
│                        └──────────────────┘                    │
└───────────────────────────┬─────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│          Room::RenderRoomGraphics() Pipeline                     │
│                                                                  │
│  Step 1: CopyRoomGraphicsToBuffer()                             │
│    └─ Copy 16 blocks to current_gfx16_[32768] buffer           │
│                                                                  │
│  Step 2: DrawFloor() on both BG1 and BG2                        │
│    ├─ Read floor tile IDs from ROM                              │
│    ├─ Create TileInfo objects (id, palette, mirror flags)       │
│    └─ SetTileAt() in background buffers (repeating pattern)     │
│                                                                  │
│  Step 3: RenderObjectsToBackground() ⚠️ NEW                     │
│    ├─ Iterate through tile_objects_                             │
│    ├─ For each object, get its Tile16 array                     │
│    ├─ Each Tile16 contains 4 TileInfo (8x8 tiles)               │
│    ├─ Convert TileInfo → 16-bit word:                           │
│    │    (vflip<<15) | (hflip<<14) | (palette<<10) | tile_id    │
│    └─ SetTileAt() in correct layer (BG1 or BG2)                 │
│                                                                  │
│  Step 4: DrawBackground() on both BG1 and BG2                   │
│    ├─ BackgroundBuffer::DrawBackground(current_gfx16_)          │
│    ├─ For each tile in buffer_[4096]:                           │
│    │    ├─ Extract 16-bit word                                  │
│    │    ├─ WordToTileInfo() → TileInfo                          │
│    │    └─ DrawTile() → Write 8x8 pixels to bitmap_.data_      │
│    └─ bitmap_.Create(512, 512, 8, pixel_data)                   │
│                                                                  │
│  Step 5: Apply Palette & Create/Update Texture                  │
│    ├─ Get dungeon_main palette for this room                    │
│    ├─ bitmap_.SetPaletteWithTransparent(palette, 0)             │
│    ├─ If first time:                                             │
│    │    └─ CreateAndRenderBitmap() → Create SDL_Texture         │
│    └─ Else:                                                      │
│         └─ UpdateBitmap() → Update existing SDL_Texture         │
└─────────────────────────────────────────────────────────────────┘

Component Breakdown

1. Arena (gfx/arena.h/.cc)

Purpose: Global graphics resource manager using singleton pattern.

Key Members:

  • gfx_sheets_[223]: Array of Bitmap objects (one for each graphics sheet in ROM)
  • bg1_, bg2_: BackgroundBuffer objects for SNES layer 1 and layer 2
  • layer1_buffer_[4096], layer2_buffer_[4096]: Raw tile word arrays

Responsibilities:

  • Resource pooling for SDL textures and surfaces
  • Batch texture updates for performance
  • Centralized access to graphics sheets

2. BackgroundBuffer (gfx/background_buffer.h/.cc)

Purpose: Manages a single SNES background layer (512x512 pixels = 64x64 tiles).

Key Members:

  • buffer_[4096]: Array of 16-bit tile words (vflip|hflip|palette|tile_id)
  • bitmap_: The Bitmap object that holds the rendered pixel data
  • width_, height_: Dimensions in pixels (typically 512x512)

Key Methods:

SetTileAt(int x, int y, uint16_t value)

// Sets a tile word at tile coordinates (x, y)
// x, y are in tile units (0-63)
buffer_[y * tiles_w + x] = value;

DrawBackground(std::span<uint8_t> gfx16_data)

// Renders all tiles in buffer_ to bitmap_
1. Create bitmap (512x512, 8bpp)
2. For each tile (64x64 grid):
   - Get tile word from buffer_[xx + yy * 64]
   - WordToTileInfo() to extract: id, palette, hflip, vflip
   - DrawTile() writes 64 pixels to bitmap at correct position

DrawFloor()

// Special case: Draws floor pattern from ROM data
1. Read 8 floor tile IDs from ROM (2 rows of 4)
2. Repeat pattern across entire 64x64 grid
3. SetTileAt() for each position

3. Bitmap (gfx/bitmap.h/.cc)

Purpose: Represents a 2D image with SNES-specific features.

Key Members:

  • data_[width * height]: Raw indexed pixel data (palette indices)
  • palette_: SnesPalette object (15-bit RGB colors)
  • surface_: SDL_Surface for pixel manipulation
  • texture_: SDL_Texture for rendering to screen
  • active_, modified_: State flags

Key Methods:

Create(int w, int h, int depth, vector<uint8_t> data)

// Initialize bitmap with pixel data
width_ = w;
height_ = h;
depth_ = depth;  // Usually 8 (bits per pixel)
data_ = data;
active_ = true;

SetPaletteWithTransparent(SnesPalette palette, size_t index)

// Apply palette and make color[index] transparent
palette_ = palette;
// Update surface_->format->palette with SDL_Colors
// Set color[index] alpha to 0 for transparency

CreateTexture(SDL_Renderer* renderer) / UpdateTexture()

// Convert surface_ to hardware-accelerated texture_
texture_ = SDL_CreateTextureFromSurface(renderer, surface_);
// or
SDL_UpdateTexture(texture_, nullptr, surface_->pixels, surface_->pitch);

4. Room (zelda3/dungeon/room.h/.cc)

Purpose: Represents a single dungeon room with all its data.

Key Members:

  • room_id_: Room index (0-295)
  • palette, blockset, spriteset: IDs from ROM header
  • blocks_[16]: Graphics sheet indices for this room
  • current_gfx16_[32768]: Raw graphics data for this room
  • tile_objects_: Vector of RoomObject instances
  • rom_: Pointer to ROM data

Key Methods:

LoadRoomGraphics(uint8_t entrance_blockset)

// Load 16 graphics sheets for this room
blocks_[0-7]:   Main blockset sheets
blocks_[8-11]:  Static sprites (fairies, pots, etc.)
blocks_[12-15]: Spriteset sprites

CopyRoomGraphicsToBuffer()

// Copy 16 blocks of 2KB each into current_gfx16_[32KB]
for (int i = 0; i < 16; i++) {
  int block = blocks_[i];
  memcpy(current_gfx16_ + i*2048, 
         graphics_buffer[block*2048], 
         2048);
}
LoadAnimatedGraphics();  // Overlay animated frames

RenderRoomGraphics() Main Rendering Method

void Room::RenderRoomGraphics() {
  // Step 1: Copy graphics data from ROM
  CopyRoomGraphicsToBuffer();  
  
  // Step 2: Draw floor pattern
  Arena::Get().bg1().DrawFloor(rom->vector(), tile_address, 
                                tile_address_floor, floor1_graphics_);
  Arena::Get().bg2().DrawFloor(rom->vector(), tile_address, 
                                tile_address_floor, floor2_graphics_);
  
  // Step 3: ⚠️ NEW - Render room objects to buffers
  RenderObjectsToBackground();
  
  // Step 4: Convert tile buffers to bitmaps
  Arena::Get().bg1().DrawBackground(span<uint8_t>(current_gfx16_));
  Arena::Get().bg2().DrawBackground(span<uint8_t>(current_gfx16_));
  
  // Step 5: Apply palette and create/update textures
  auto palette = rom->palette_group().dungeon_main[palette_id][0];
  if (!Arena::Get().bg1().bitmap().is_active()) {
    Renderer::Get().CreateAndRenderBitmap(..., Arena::Get().bg1().bitmap(), palette);
    Renderer::Get().CreateAndRenderBitmap(..., Arena::Get().bg2().bitmap(), palette);
  } else {
    Renderer::Get().UpdateBitmap(&Arena::Get().bg1().bitmap());
    Renderer::Get().UpdateBitmap(&Arena::Get().bg2().bitmap());
  }
}

RenderObjectsToBackground() ⚠️ Critical New Method Fixed

void Room::RenderObjectsToBackground() {
  auto& bg1 = Arena::Get().bg1();
  auto& bg2 = Arena::Get().bg2();
  
  for (const auto& obj : tile_objects_) {
    // Ensure object has tiles loaded
    obj.EnsureTilesLoaded();
    auto tiles_result = obj.GetTiles();  // Returns span<const Tile16>
    
    // Calculate the width of the object in Tile16 units
    // Most objects are arranged in a grid, typically 1-8 tiles wide
    int tiles_wide = 1;
    if (tiles.size() > 1) {
      // Try to determine optimal layout based on tile count
      // Common patterns: 1x1, 2x2, 4x1, 2x4, 4x4, 8x1, etc.
      int sq = static_cast<int>(std::sqrt(tiles.size()));
      if (sq * sq == tiles.size()) {
        tiles_wide = sq;  // Perfect square (4, 9, 16, etc.)
      } else if (tiles.size() <= 4) {
        tiles_wide = tiles.size();  // Small objects laid out horizontally
      } else {
        // For larger objects, try common widths (4 or 8)
        tiles_wide = (tiles.size() >= 8) ? 8 : 4;
      }
    }
    
    // Each Tile16 is 16x16 (4 TileInfo of 8x8)
    for (size_t i = 0; i < tiles.size(); i++) {
      const auto& tile16 = tiles[i];
      
      // Calculate base position using calculated width (in 8x8 units)
      int base_x = obj.x_ + ((i % tiles_wide) * 2);
      int base_y = obj.y_ + ((i / tiles_wide) * 2);
      
      // Tile16.tiles_info[4] contains the 4 sub-tiles:
      // [0][1]  (top-left, top-right)
      // [2][3]  (bottom-left, bottom-right)
      for (int sub = 0; sub < 4; sub++) {
        int tile_x = base_x + (sub % 2);
        int tile_y = base_y + (sub / 2);
        
        // Bounds check
        if (tile_x < 0 || tile_x >= 64 || tile_y < 0 || tile_y >= 64) {
          continue;
        }
        
        // Convert TileInfo to 16-bit word
        uint16_t word = TileInfoToWord(tile16.tiles_info[sub]);
        
        // Set in correct layer
        bool is_bg2 = (obj.layer_ == RoomObject::LayerType::BG2);
        auto& buffer = is_bg2 ? bg2 : bg1;
        buffer.SetTileAt(tile_x, tile_y, word);
      }
    }
  }
}

Palette System

Palette Hierarchy

ROM Palette Data
│
├─ dungeon_main[palette_group_id]
│   └─ Large palette (180 colors)
│       └─ Split into PaletteGroup:
│           ├─ palette(0): Main dungeon palette
│           ├─ palette(1): Alternate palette 1
│           └─ palette(2-n): More palettes
│
└─ sprites_aux1[palette_id]
    └─ Sprite auxiliary palettes

Palette Loading Flow

// In DungeonEditor::Load()
auto dungeon_pal_group = rom->palette_group().dungeon_main;
full_palette_ = dungeon_pal_group[current_palette_group_id_];
ASSIGN_OR_RETURN(current_palette_group_,
                 CreatePaletteGroupFromLargePalette(full_palette_));

// In DungeonCanvasViewer::LoadAndRenderRoomGraphics()
auto dungeon_palette_ptr = rom->paletteset_ids[room.palette][0];
auto palette_id = rom->ReadWord(0xDEC4B + dungeon_palette_ptr);
current_palette_group_id_ = palette_id.value() / 180;
full_palette = rom->palette_group().dungeon_main[current_palette_group_id_];

// Apply to graphics sheets
for (int i = 0; i < 8; i++) {  // BG1 layers
  int block = room.blocks()[i];
  Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent(
      current_palette_group_[current_palette_id_], 0);
}

for (int i = 8; i < 16; i++) {  // BG2 layers (sprites)
  int block = room.blocks()[i];
  Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent(
      sprites_aux1_pal_group[current_palette_id_], 0);
}

Appendix: Blank Canvas Bug - Detailed Root Cause Analysis

This section, merged from dungeon_canvas_blank_fix.md, details the investigation and resolution of the blank canvas bug.

Problem

The DungeonEditor canvas displayed as blank white despite the rendering pipeline appearing to execute correctly.

Diagnostic Output Analysis

Using a comprehensive diagnostic system, the data flow was traced through 8 steps. Steps 1-6 (ROM loading, buffer population, bitmap creation, texture creation) were all passing. The failure was in Step 7.

Step 7: PALETTE MISSING

=== Step 7: Palette ===
  Palette size: 0 colors  ❌❌❌ ROOT CAUSE!

Root Cause Analysis

Three distinct issues were identified and fixed in sequence:

Cause 1: Missing Palette Application

The palette was never applied to the bitmap objects! The bitmaps contained indexed pixel data (e.g., color indices 8, 9, 12), but without a color palette, SDL couldn't map these indices to actual colors, resulting in a blank texture.

The Fix (Location: src/app/zelda3/dungeon/room.cc:319-322): Added SetPaletteWithTransparent() calls to the bitmaps before creating the SDL textures. This ensures the renderer has the color information it needs.

// CRITICAL: Apply palette to bitmaps BEFORE creating/updating textures
bg1_bmp.SetPaletteWithTransparent(bg1_palette, 0);
bg2_bmp.SetPaletteWithTransparent(bg1_palette, 0);

// Now create/update textures (palette is already set in bitmap)
Renderer::Get().CreateAndRenderBitmap(..., bg1_bmp, bg1_palette);

Cause 2: All Black Canvas (Incorrect Tile Word)

After the first fix, the canvas was all black. This was because DrawFloor() was only passing the tile ID to the background buffer, losing the crucial palette information.

The Fix: Converted the TileInfo struct to a full 16-bit word (which includes palette bits) before writing it to the buffer.

// CORRECT: Convert TileInfo to word with all metadata
uint16_t word1 = gfx::TileInfoToWord(floorTile1);
SetTileAt((xx * 4), (yy * 2), word1);  // ✅ Now includes palette!

Cause 3: Wrong Palette (All Rooms Look "Gargoyle-y")

After the second fix, all rooms rendered, but with the same incorrect palette (from the first dungeon).

The Fix: Used the room's specific palette ID loaded from the ROM header instead of hardcoding palette index 0.

// ✅ CORRECT: Use the room's palette ID
auto bg1_palette =
    rom()->mutable_palette_group()->get_group("dungeon_main")[0].palette(palette);

Key Takeaway

Always apply a palette to indexed-color bitmaps before creating SDL textures. The rendering pipeline requires this step to translate color indices into visible pixels. Each subsequent fix ensured the correct palette information was being passed at each stage.