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

420 lines
18 KiB
Markdown

# 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)`
```cpp
// 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)`
```cpp
// 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()`
```cpp
// 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)`
```cpp
// 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)`
```cpp
// 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()`
```cpp
// 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)`
```cpp
// 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()`
```cpp
// 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**
```cpp
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**
```cpp
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
```cpp
// 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.
```cpp
// 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.
```cpp
// 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`.
```cpp
// ✅ 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.