backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# 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
|
||||
```cpp
|
||||
// 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
|
||||
```bash
|
||||
# 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)
|
||||
```cpp
|
||||
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
|
||||
```cpp
|
||||
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 |
|
||||
@@ -0,0 +1,456 @@
|
||||
# Complete Duplicate Rendering Investigation
|
||||
|
||||
**Date:** 2025-11-25
|
||||
**Status:** Investigation Complete - Root Cause Analysis
|
||||
**Issue:** Elements inside editor cards appear twice (visually stacked)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Traced the complete call chain from main loop to editor content rendering. **No duplicate Update() or Draw() calls found**. The issue is NOT caused by multiple rendering paths in the editor system.
|
||||
|
||||
**Key Finding:** The diagnostic code added to `EditorCard::Begin()` will definitively identify if cards are being rendered twice. If no duplicates are detected by the diagnostic, the issue lies outside the EditorCard system (likely ImGui draw list submission or Z-ordering).
|
||||
|
||||
---
|
||||
|
||||
## Complete Call Chain (Main Loop → Editor Content)
|
||||
|
||||
### 1. Main Loop (`controller.cc`)
|
||||
|
||||
```
|
||||
Controller::OnLoad() [Line 56]
|
||||
├─ ImGui::NewFrame() [Line 63-65] ← SINGLE CALL
|
||||
├─ DockSpace Setup [Lines 67-116]
|
||||
│ ├─ Calculate sidebar offsets [Lines 70-78]
|
||||
│ ├─ Create main dockspace window [Lines 103-116]
|
||||
│ └─ EditorManager::DrawMenuBar() [Line 112]
|
||||
│
|
||||
└─ EditorManager::Update() [Line 124] ← SINGLE CALL
|
||||
└─ DoRender() [Line 134]
|
||||
└─ ImGui::Render() ← SINGLE CALL
|
||||
```
|
||||
|
||||
**Verdict:** ✅ Clean single-path rendering - no duplicates at main loop level
|
||||
|
||||
---
|
||||
|
||||
### 2. EditorManager Update Flow (`editor_manager.cc:616-843`)
|
||||
|
||||
```
|
||||
EditorManager::Update()
|
||||
├─ [Lines 617-626] Process deferred actions
|
||||
├─ [Lines 632-662] Draw UI systems (popups, toasts, dialogs)
|
||||
├─ [Lines 664-693] Draw UICoordinator (welcome screen, command palette)
|
||||
│
|
||||
├─ [Lines 698-772] Draw Sidebar (BEFORE ROM check)
|
||||
│ ├─ Check: IsCardSidebarVisible() && !IsSidebarCollapsed()
|
||||
│ ├─ Mutual exclusion: IsTreeViewMode() ?
|
||||
│ │ ├─ TRUE → DrawTreeSidebar() [Line 758]
|
||||
│ │ └─ FALSE → DrawSidebar() [Line 761]
|
||||
│ └─ Note: Different window names prevent overlap
|
||||
│ - DrawSidebar() → "##EditorCardSidebar"
|
||||
│ - DrawTreeSidebar() → "##TreeSidebar"
|
||||
│
|
||||
├─ [Lines 774-778] Draw RightPanelManager (BEFORE ROM check)
|
||||
│ └─ RightPanelManager::Draw() → "##RightPanel"
|
||||
│
|
||||
├─ [Lines 802-812] Early return if no ROM loaded
|
||||
│
|
||||
└─ [Lines 1043-1056] Update active editors (ONLY PATH TO EDITOR UPDATE)
|
||||
└─ for (editor : active_editors_)
|
||||
└─ if (*editor->active())
|
||||
└─ editor->Update() ← SINGLE CALL PER EDITOR PER FRAME
|
||||
```
|
||||
|
||||
**Verdict:** ✅ Only one `editor->Update()` call per active editor per frame
|
||||
|
||||
---
|
||||
|
||||
### 3. Editor Update Implementation (e.g., OverworldEditor)
|
||||
|
||||
**File:** `src/app/editor/overworld/overworld_editor.cc:228`
|
||||
|
||||
```
|
||||
OverworldEditor::Update()
|
||||
├─ [Lines 240-258] Create local EditorCard instances
|
||||
│ └─ EditorCard overworld_canvas_card(...)
|
||||
│ EditorCard tile16_card(...)
|
||||
│ ... (8 cards total)
|
||||
│
|
||||
├─ [Lines 294-300] Overworld Canvas Card
|
||||
│ └─ if (show_overworld_canvas_)
|
||||
│ if (overworld_canvas_card.Begin(&show_overworld_canvas_))
|
||||
│ DrawToolset()
|
||||
│ DrawOverworldCanvas()
|
||||
│ overworld_canvas_card.End() ← ALWAYS CALLED
|
||||
│
|
||||
├─ [Lines 303-308] Tile16 Selector Card
|
||||
│ └─ if (show_tile16_selector_)
|
||||
│ if (tile16_card.Begin(&show_tile16_selector_))
|
||||
│ DrawTile16Selector()
|
||||
│ tile16_card.End()
|
||||
│
|
||||
└─ ... (6 more cards, same pattern)
|
||||
```
|
||||
|
||||
**Pattern:** Each card follows strict Begin/End pairing:
|
||||
```cpp
|
||||
if (visibility_flag) {
|
||||
if (card.Begin(&visibility_flag)) {
|
||||
// Draw content ONCE
|
||||
}
|
||||
card.End(); // ALWAYS called after Begin()
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** ✅ No duplicate Begin() calls - each card rendered exactly once per Update()
|
||||
|
||||
---
|
||||
|
||||
### 4. EditorCard Rendering (`editor_layout.cc`)
|
||||
|
||||
```
|
||||
EditorCard::Begin(bool* p_open) [Lines 256-366]
|
||||
├─ [Lines 257-261] Check visibility flag
|
||||
│ └─ if (p_open && !*p_open) return false
|
||||
│
|
||||
├─ [Lines 263-285] 🔍 DUPLICATE DETECTION (NEW)
|
||||
│ └─ Track which cards have called Begin() this frame
|
||||
│ if (duplicate detected)
|
||||
│ fprintf(stderr, "DUPLICATE DETECTED: '%s' frame %d")
|
||||
│ duplicate_detected_ = true
|
||||
│
|
||||
├─ [Lines 288-292] Handle collapsed state
|
||||
├─ [Lines 294-336] Setup ImGui window
|
||||
└─ [Lines 352-356] Call ImGui::Begin()
|
||||
└─ imgui_begun_ = true ← Tracks that End() must be called
|
||||
|
||||
EditorCard::End() [Lines 369-380]
|
||||
└─ if (imgui_begun_)
|
||||
ImGui::End()
|
||||
imgui_begun_ = false
|
||||
```
|
||||
|
||||
**Diagnostic Behavior:**
|
||||
- Frame tracking resets on `ImGui::GetFrameCount()` change
|
||||
- Each `Begin()` call checks if card name already in `cards_begun_this_frame_`
|
||||
- Duplicate detected → logs to stderr and sets flag
|
||||
- **This will definitively identify double Begin() calls**
|
||||
|
||||
**Verdict:** ✅ Diagnostic will catch any duplicate Begin() calls
|
||||
|
||||
---
|
||||
|
||||
### 5. RightPanelManager (ProposalDrawer, AgentChat, Settings)
|
||||
|
||||
**File:** `src/app/editor/ui/right_panel_manager.cc`
|
||||
|
||||
```
|
||||
RightPanelManager::Draw() [Lines 117-181]
|
||||
└─ if (active_panel_ != PanelType::kNone)
|
||||
ImGui::Begin("##RightPanel", ...)
|
||||
DrawPanelHeader(...)
|
||||
switch (active_panel_)
|
||||
case kProposals: DrawProposalsPanel() [Line 162]
|
||||
└─ proposal_drawer_->DrawContent() [Line 238]
|
||||
NOT Draw()! Only DrawContent()!
|
||||
case kAgentChat: DrawAgentChatPanel()
|
||||
case kSettings: DrawSettingsPanel()
|
||||
ImGui::End()
|
||||
```
|
||||
|
||||
**Key Discovery:** ProposalDrawer has TWO methods:
|
||||
- `Draw()` - Creates own window (lines 75-107 in proposal_drawer.cc) ← **NEVER CALLED**
|
||||
- `DrawContent()` - Renders inside existing window (line 238) ← **ONLY THIS IS USED**
|
||||
|
||||
**Verification in EditorManager:**
|
||||
```cpp
|
||||
// Line 827 in editor_manager.cc
|
||||
// Proposal drawer is now drawn through RightPanelManager
|
||||
// Removed duplicate direct call - DrawProposalsPanel() in RightPanelManager handles it
|
||||
```
|
||||
|
||||
**Verdict:** ✅ ProposalDrawer::Draw() is dead code - only DrawContent() used
|
||||
|
||||
---
|
||||
|
||||
## What Was Ruled Out
|
||||
|
||||
### ❌ Multiple Update() Calls
|
||||
- **EditorManager::Update()** calls `editor->Update()` exactly once per active editor (line 1047)
|
||||
- **Controller::OnLoad()** calls `EditorManager::Update()` exactly once per frame (line 124)
|
||||
- **No loops, no recursion, no duplicate paths**
|
||||
|
||||
### ❌ ImGui Begin/End Mismatches
|
||||
- Every `EditorCard::Begin()` has matching `End()` call
|
||||
- `imgui_begun_` flag prevents double End() calls
|
||||
- Verified in OverworldEditor: 8 cards × 1 Begin + 1 End each = balanced
|
||||
|
||||
### ❌ Sidebar Double Rendering
|
||||
- `DrawSidebar()` and `DrawTreeSidebar()` are **mutually exclusive**
|
||||
- Different window names: `##EditorCardSidebar` vs `##TreeSidebar`
|
||||
- Only one is called based on `IsTreeViewMode()` check (lines 757-763)
|
||||
|
||||
### ❌ RightPanel vs Direct Drawer Calls
|
||||
- ProposalDrawer::Draw() is **never called** (confirmed with grep)
|
||||
- Only `DrawContent()` used via RightPanelManager::DrawProposalsPanel()
|
||||
- Comment at line 827 confirms duplicate call was removed
|
||||
|
||||
### ❌ EditorCard Registry Drawing Cards
|
||||
- `card_registry_.ShowCard()` only sets **visibility flags**
|
||||
- Cards are **not drawn by registry** - only drawn in editor Update() methods
|
||||
- Registry only manages: visibility state, sidebar UI, card browser
|
||||
|
||||
### ❌ Multi-Viewport Issues
|
||||
- `ImGuiConfigFlags_ViewportsEnable` is **NOT enabled**
|
||||
- Only `ImGuiConfigFlags_DockingEnable` is active
|
||||
- Single viewport architecture - no platform windows
|
||||
|
||||
---
|
||||
|
||||
## Possible Root Causes (Outside Editor System)
|
||||
|
||||
If the diagnostic does NOT detect duplicate Begin() calls, the issue must be:
|
||||
|
||||
### 1. ImGui Draw List Submission
|
||||
**Hypothesis:** Draw data is being submitted to GPU twice
|
||||
```cpp
|
||||
// In Controller::DoRender()
|
||||
ImGui::Render(); // Generate draw lists
|
||||
renderer_->Clear(); // Clear framebuffer
|
||||
ImGui_ImplSDLRenderer2_RenderDrawData(...); // Submit to GPU
|
||||
renderer_->Present(); // Swap buffers
|
||||
```
|
||||
|
||||
**Check:**
|
||||
- Are draw lists being submitted twice?
|
||||
- Is `ImGui_ImplSDLRenderer2_RenderDrawData()` called more than once?
|
||||
- Add: `printf("RenderDrawData called: frame %d\n", ImGui::GetFrameCount());`
|
||||
|
||||
### 2. Z-Ordering / Layering Bug
|
||||
**Hypothesis:** Two overlapping windows with same content at same position
|
||||
```cpp
|
||||
// ImGui windows at same coordinates with same content
|
||||
ImGui::SetNextWindowPos(ImVec2(100, 100));
|
||||
ImGui::Begin("Window1");
|
||||
DrawContent(); // Content rendered
|
||||
ImGui::End();
|
||||
|
||||
// Another window at SAME position
|
||||
ImGui::SetNextWindowPos(ImVec2(100, 100));
|
||||
ImGui::Begin("Window2");
|
||||
DrawContent(); // SAME content rendered again
|
||||
ImGui::End();
|
||||
```
|
||||
|
||||
**Check:**
|
||||
- ImGui Metrics window → Show "Windows" section
|
||||
- Look for duplicate windows with same position
|
||||
- Check window Z-order and docking state
|
||||
|
||||
### 3. Texture Double-Binding
|
||||
**Hypothesis:** Textures are bound/drawn twice in rendering backend
|
||||
```cpp
|
||||
// In SDL2 renderer backend
|
||||
SDL_RenderCopy(renderer, texture, ...); // First draw
|
||||
// ... some code ...
|
||||
SDL_RenderCopy(renderer, texture, ...); // Accidental second draw
|
||||
```
|
||||
|
||||
**Check:**
|
||||
- SDL2 render target state
|
||||
- Multiple texture binding in same frame
|
||||
- Backend drawing primitives twice
|
||||
|
||||
### 4. Stale ImGui State
|
||||
**Hypothesis:** Old draw commands not cleared between frames
|
||||
```cpp
|
||||
// Missing clear in backend
|
||||
void NewFrame() {
|
||||
// Should clear old draw data here!
|
||||
ImGui_ImplSDLRenderer2_NewFrame();
|
||||
}
|
||||
```
|
||||
|
||||
**Check:**
|
||||
- Is `ImGui::NewFrame()` clearing old state?
|
||||
- Backend implementation of `NewFrame()` correct?
|
||||
- Add: `ImGui::GetDrawData()->CmdListsCount` logging
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Steps
|
||||
|
||||
### Step 1: Run with Diagnostic
|
||||
```bash
|
||||
cmake --build build --target yaze -j4
|
||||
./build/bin/yaze --rom_file=zelda3.sfc --editor=Overworld 2>&1 | grep "DUPLICATE"
|
||||
```
|
||||
|
||||
**Expected Output:**
|
||||
- If duplicates exist: `[EditorCard] DUPLICATE DETECTED: 'Overworld Canvas' Begin() called twice in frame 1234`
|
||||
- If no duplicates: (no output)
|
||||
|
||||
### Step 2: Check Programmatically
|
||||
```cpp
|
||||
// In EditorManager::Update() after line 1056, add:
|
||||
if (gui::EditorCard::HasDuplicateRendering()) {
|
||||
LOG_ERROR("Duplicate card rendering detected: %s",
|
||||
gui::EditorCard::GetDuplicateCardName().c_str());
|
||||
// Breakpoint here to inspect call stack
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3A: If Duplicates Detected
|
||||
**Trace the duplicate Begin() call:**
|
||||
1. Set breakpoint in `EditorCard::Begin()` at line 279 (duplicate detection)
|
||||
2. Condition: `duplicate_detected_ == true`
|
||||
3. Inspect call stack to find second caller
|
||||
4. Fix the duplicate code path
|
||||
|
||||
### Step 3B: If No Duplicates Detected
|
||||
**Issue is outside EditorCard system:**
|
||||
1. Enable ImGui Metrics: `ImGui::ShowMetricsWindow()`
|
||||
2. Check "Windows" section for duplicate windows
|
||||
3. Add logging to `Controller::DoRender()`:
|
||||
```cpp
|
||||
static int render_count = 0;
|
||||
printf("DoRender #%d: DrawData CmdLists=%d\n",
|
||||
++render_count, ImGui::GetDrawData()->CmdListsCount);
|
||||
```
|
||||
4. Inspect SDL2 backend for double submission
|
||||
5. Check for stale GPU state between frames
|
||||
|
||||
### Step 4: Alternative Debugging
|
||||
If issue persists, try:
|
||||
```cpp
|
||||
// In OverworldEditor::Update(), add frame tracking
|
||||
static int last_frame = -1;
|
||||
int current_frame = ImGui::GetFrameCount();
|
||||
if (current_frame == last_frame) {
|
||||
LOG_ERROR("OverworldEditor::Update() called TWICE in frame %d!", current_frame);
|
||||
}
|
||||
last_frame = current_frame;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Insights
|
||||
|
||||
### Editor Rendering Pattern
|
||||
**Decentralized Card Creation:**
|
||||
- Each editor creates `EditorCard` instances **locally** in its `Update()` method
|
||||
- Cards are **not global** - they're stack-allocated temporaries
|
||||
- Visibility is managed by **pointers to bool flags** that persist across frames
|
||||
|
||||
**Example:**
|
||||
```cpp
|
||||
// In OverworldEditor::Update() - called ONCE per frame
|
||||
gui::EditorCard tile16_card("Tile16 Selector", ICON_MD_GRID_3X3);
|
||||
if (show_tile16_selector_) { // Persistent flag
|
||||
if (tile16_card.Begin(&show_tile16_selector_)) {
|
||||
DrawTile16Selector(); // Content rendered ONCE
|
||||
}
|
||||
tile16_card.End();
|
||||
}
|
||||
// Card destroyed at end of Update() - stack unwinding
|
||||
```
|
||||
|
||||
### Registry vs Direct Rendering
|
||||
**EditorCardRegistry:**
|
||||
- **Purpose:** Manage visibility flags, sidebar UI, card browser
|
||||
- **Does NOT render cards** - only manages state
|
||||
- **Does render:** Sidebar buttons, card browser UI, tree view
|
||||
|
||||
**Direct Rendering (in editors):**
|
||||
- Each editor creates and renders its own cards
|
||||
- Registry provides visibility flag pointers
|
||||
- Editor checks flag, renders if true
|
||||
|
||||
### Separation of Concerns
|
||||
**Clear boundaries:**
|
||||
1. **Controller** - Main loop, window management, single Update() call
|
||||
2. **EditorManager** - Editor lifecycle, session management, single editor->Update() per editor
|
||||
3. **Editor (e.g., OverworldEditor)** - Card creation, content rendering, one Begin/End pair per card
|
||||
4. **EditorCard** - ImGui window wrapper, duplicate detection, Begin/End state tracking
|
||||
5. **EditorCardRegistry** - Visibility management, sidebar UI, no direct card rendering
|
||||
|
||||
**This architecture prevents duplicate rendering by design** - there is only ONE path from main loop to card content.
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic Code Summary
|
||||
|
||||
**Location:** `src/app/gui/app/editor_layout.h` (lines 121-135) and `editor_layout.cc` (lines 17-285)
|
||||
|
||||
**Static Tracking Variables:**
|
||||
```cpp
|
||||
static int last_frame_count_ = 0;
|
||||
static std::vector<std::string> cards_begun_this_frame_;
|
||||
static bool duplicate_detected_ = false;
|
||||
static std::string duplicate_card_name_;
|
||||
```
|
||||
|
||||
**Detection Logic:**
|
||||
```cpp
|
||||
// In EditorCard::Begin()
|
||||
int current_frame = ImGui::GetFrameCount();
|
||||
if (current_frame != last_frame_count_) {
|
||||
// New frame - reset tracking
|
||||
cards_begun_this_frame_.clear();
|
||||
duplicate_detected_ = false;
|
||||
}
|
||||
|
||||
// Check for duplicate
|
||||
for (const auto& card_name : cards_begun_this_frame_) {
|
||||
if (card_name == window_name_) {
|
||||
duplicate_detected_ = true;
|
||||
fprintf(stderr, "[EditorCard] DUPLICATE: '%s' frame %d\n",
|
||||
window_name_.c_str(), current_frame);
|
||||
}
|
||||
}
|
||||
cards_begun_this_frame_.push_back(window_name_);
|
||||
```
|
||||
|
||||
**Public API:**
|
||||
```cpp
|
||||
static void ResetFrameTracking(); // Manual reset (optional)
|
||||
static bool HasDuplicateRendering(); // Check if duplicate detected
|
||||
static const std::string& GetDuplicateCardName(); // Get duplicate card name
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The editor system has a **clean, single-path rendering architecture**. No code paths exist that could cause duplicate card rendering through the normal Update() flow.
|
||||
|
||||
**If duplicate rendering occurs:**
|
||||
1. The diagnostic WILL detect it if it's in EditorCard::Begin()
|
||||
2. If diagnostic doesn't fire, issue is outside EditorCard (ImGui backend, GPU state, Z-order)
|
||||
|
||||
**Next Agent Action:**
|
||||
- Build and run with diagnostic
|
||||
- Report findings based on stderr output
|
||||
- Follow appropriate Step 3A or 3B from "Recommended Next Steps"
|
||||
|
||||
---
|
||||
|
||||
## Files Referenced
|
||||
|
||||
**Core Investigation Files:**
|
||||
- `/Users/scawful/Code/yaze/src/app/controller.cc` - Main loop (lines 56-165)
|
||||
- `/Users/scawful/Code/yaze/src/app/editor/editor_manager.cc` - Update flow (lines 616-1079)
|
||||
- `/Users/scawful/Code/yaze/src/app/editor/overworld/overworld_editor.cc` - Editor Update (lines 228-377)
|
||||
- `/Users/scawful/Code/yaze/src/app/gui/app/editor_layout.cc` - EditorCard implementation (lines 256-380)
|
||||
- `/Users/scawful/Code/yaze/src/app/editor/ui/right_panel_manager.cc` - Panel system (lines 117-242)
|
||||
- `/Users/scawful/Code/yaze/src/app/editor/system/editor_card_registry.cc` - Card registry (lines 456-787)
|
||||
- `/Users/scawful/Code/yaze/src/app/editor/system/proposal_drawer.h` - Draw vs DrawContent (lines 39-43)
|
||||
|
||||
**Diagnostic Code:**
|
||||
- `/Users/scawful/Code/yaze/src/app/gui/app/editor_layout.h` (lines 121-135)
|
||||
- `/Users/scawful/Code/yaze/src/app/gui/app/editor_layout.cc` (lines 17-285)
|
||||
|
||||
**Previous Investigation:**
|
||||
- `/Users/scawful/Code/yaze/docs/internal/handoff-duplicate-rendering-investigation.md`
|
||||
@@ -0,0 +1,265 @@
|
||||
# Emulator Regression Trace
|
||||
|
||||
Tracking git history to find root cause of title screen BG being black.
|
||||
|
||||
## Issue Description
|
||||
- **Symptom**: Title screen background is black (after sword comes down)
|
||||
- **Working**: Triforce animation, Nintendo logo, cutscene after title screen, file select
|
||||
- **Broken**: Title screen BG layer specifically
|
||||
|
||||
---
|
||||
|
||||
## Commit Analysis
|
||||
|
||||
### Commit: e37497e9ef - "feat(emu): add PPU JIT catch-up for mid-scanline raster effects"
|
||||
**Date**: Sun Nov 23 00:40:58 2025
|
||||
**Author**: scawful + Claude
|
||||
|
||||
This is the commit that introduced the JIT progressive rendering system.
|
||||
|
||||
#### Changes Made
|
||||
|
||||
**ppu.h additions:**
|
||||
```cpp
|
||||
void StartLine(int line);
|
||||
void CatchUp(int h_pos);
|
||||
// New members:
|
||||
int last_rendered_x_ = 0;
|
||||
int current_scanline_; // (implicit, used in CatchUp)
|
||||
```
|
||||
|
||||
**ppu.cc changes:**
|
||||
```cpp
|
||||
// OLD RunLine - rendered entire line at once:
|
||||
void Ppu::RunLine(int line) {
|
||||
obj_pixel_buffer_.fill(0);
|
||||
if (!forced_blank_) EvaluateSprites(line - 1);
|
||||
if (mode == 7) CalculateMode7Starts(line);
|
||||
for (int x = 0; x < 256; x++) {
|
||||
HandlePixel(x, line);
|
||||
}
|
||||
}
|
||||
|
||||
// NEW - Split into StartLine + CatchUp:
|
||||
void Ppu::StartLine(int line) {
|
||||
current_scanline_ = line;
|
||||
last_rendered_x_ = 0;
|
||||
obj_pixel_buffer_.fill(0);
|
||||
if (!forced_blank_) EvaluateSprites(line - 1);
|
||||
if (mode == 7) CalculateMode7Starts(line);
|
||||
}
|
||||
|
||||
void Ppu::CatchUp(int h_pos) {
|
||||
int target_x = h_pos / 4; // 1 pixel = 4 master cycles
|
||||
if (target_x > 256) target_x = 256;
|
||||
if (target_x <= last_rendered_x_) return;
|
||||
|
||||
for (int x = last_rendered_x_; x < target_x; x++) {
|
||||
HandlePixel(x, current_scanline_);
|
||||
}
|
||||
last_rendered_x_ = target_x;
|
||||
}
|
||||
|
||||
void Ppu::RunLine(int line) {
|
||||
// Legacy wrapper
|
||||
StartLine(line);
|
||||
CatchUp(2000); // Force full line render
|
||||
}
|
||||
```
|
||||
|
||||
**snes.cc changes:**
|
||||
```cpp
|
||||
// Timing calls in RunCycle():
|
||||
case 16: { // Was: case 0 in some versions
|
||||
// ... init_hdma_request ...
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.StartLine(memory_.v_pos()); // NEW: Initialize scanline
|
||||
}
|
||||
case 512: {
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.CatchUp(512); // CHANGED: Was ppu_.RunLine(memory_.v_pos())
|
||||
}
|
||||
case 1104: {
|
||||
if (!in_vblank_ && memory_.v_pos() > 0)
|
||||
ppu_.CatchUp(1104); // NEW: Finish line
|
||||
// Then run HDMA...
|
||||
}
|
||||
|
||||
// WriteBBus addition:
|
||||
void Snes::WriteBBus(uint8_t adr, uint8_t val) {
|
||||
if (adr < 0x40) {
|
||||
// NEW: Catch up before PPU register write for mid-scanline effects
|
||||
if (!in_vblank_ && memory_.v_pos() > 0 && memory_.h_pos() < 1100) {
|
||||
ppu_.CatchUp(memory_.h_pos());
|
||||
}
|
||||
ppu_.Write(adr, val);
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Potential Issues Identified
|
||||
|
||||
1. **`current_scanline_` not declared in ppu.h** - The variable is used but may not be properly declared as a member. Need to verify.
|
||||
|
||||
2. **h_pos timing at case 16 vs case 0** - The code shows `case 16:` but original may have been different. Need to verify StartLine is called at correct time.
|
||||
|
||||
3. **CatchUp(512) only renders pixels 0-127** - With `target_x = 512/4 = 128`, this only renders half the line. The second `CatchUp(1104)` renders `1104/4 = 276` → clamped to 256, so pixels 128-255.
|
||||
|
||||
4. **WriteBBus CatchUp may cause double-rendering** - If HDMA writes to PPU registers, CatchUp is called, but then CatchUp(1104) is also called. This should be fine since `last_rendered_x_` prevents re-rendering.
|
||||
|
||||
5. **HDMA runs AFTER CatchUp(1104)** - HDMA modifications to PPU registers happen after the scanline is fully rendered. This is correct for HDMA (affects next line), but need to verify.
|
||||
|
||||
---
|
||||
|
||||
### Commit: 9d788fe6b0 - "perf: Implement lazy SNES emulator initialization..."
|
||||
**Date**: Tue Nov 25 14:58:52 2025
|
||||
|
||||
**Changes to emulator:**
|
||||
- Only changed `Snes::Init` signature from `std::vector<uint8_t>&` to `const std::vector<uint8_t>&`
|
||||
- No changes to PPU or rendering logic
|
||||
|
||||
**Verdict**: NOT RELATED to rendering bug.
|
||||
|
||||
---
|
||||
|
||||
### Commit: a0ab5a5eee - "perf(wasm): optimize emulator performance and audio system"
|
||||
**Date**: Tue Nov 25 19:02:21 2025
|
||||
|
||||
**Changes to emulator:**
|
||||
- emulator.cc: Frame timing and progressive frame skip (no PPU changes)
|
||||
- wasm_audio.cc: AudioWorklet implementation (no PPU changes)
|
||||
|
||||
**Verdict**: NOT RELATED to rendering bug.
|
||||
|
||||
---
|
||||
|
||||
## Commits Between Pre-JIT and Now
|
||||
|
||||
Only 2 commits modified PPU/snes.cc:
|
||||
1. `e37497e9ef` - JIT introduction (SUSPECT)
|
||||
2. `9d788fe6b0` - Init signature change (NOT RELATED)
|
||||
|
||||
---
|
||||
|
||||
## Hypothesis
|
||||
|
||||
The JIT commit `e37497e9ef` is the root cause. Possible bugs:
|
||||
|
||||
### Theory 1: `current_scanline_` initialization issue
|
||||
If `current_scanline_` is not properly initialized or declared, `HandlePixel(x, current_scanline_)` could be passing garbage values.
|
||||
|
||||
**To verify**: Check if `current_scanline_` is declared in ppu.h
|
||||
|
||||
### Theory 2: h_pos timing mismatch
|
||||
The title screen may rely on specific timing that the JIT system breaks. If PPU registers are read/written at different h_pos values than expected, rendering could be affected.
|
||||
|
||||
### Theory 3: WriteBBus CatchUp interference with HDMA
|
||||
The title screen uses HDMA for wavy cloud scroll. If the WriteBBus CatchUp is being called for HDMA writes and causing state issues, BG rendering could be affected.
|
||||
|
||||
**HDMA flow:**
|
||||
1. h_pos=1104: CatchUp(1104) renders pixels 128-255
|
||||
2. h_pos=1104: run_hdma_request() executes HDMA
|
||||
3. HDMA writes to scroll registers via WriteBBus
|
||||
4. WriteBBus calls CatchUp(h_pos) - but line is already fully rendered!
|
||||
|
||||
This should be harmless since `last_rendered_x_ = 256` means CatchUp returns early. But need to verify.
|
||||
|
||||
### Theory 4: Title screen uses unusual PPU setup
|
||||
The title screen may have a specific PPU configuration that triggers a bug in the JIT system that other screens don't trigger.
|
||||
|
||||
---
|
||||
|
||||
## Verification Results
|
||||
|
||||
### Theory 1: VERIFIED - `current_scanline_` is declared
|
||||
Found at ppu.h:335: `int current_scanline_ = 0;` - properly declared and initialized.
|
||||
|
||||
### Theory 2: Timing Analysis
|
||||
- h_pos=16: StartLine(v_pos) called, resets `last_rendered_x_=0`
|
||||
- h_pos=512: CatchUp(512) renders pixels 0-127
|
||||
- h_pos=1104: CatchUp(1104) renders pixels 128-255, then HDMA runs
|
||||
|
||||
Math check:
|
||||
- 512/4 = 128 pixels (0-127)
|
||||
- 1104/4 = 276 → clamped to 256 (128-255)
|
||||
- Total: 256 pixels ✓
|
||||
|
||||
### Theory 3: WriteBBus CatchUp Analysis
|
||||
```cpp
|
||||
if (!in_vblank_ && memory_.v_pos() > 0 && memory_.h_pos() < 1100) {
|
||||
ppu_.CatchUp(memory_.h_pos());
|
||||
}
|
||||
```
|
||||
- HDMA runs at h_pos=1104, which is > 1100, so NO catchup for HDMA writes ✓
|
||||
- This is correct - HDMA changes affect next scanline
|
||||
|
||||
### New Theory 5: HandleFrameStart doesn't reset JIT state
|
||||
`HandleFrameStart()` doesn't reset `last_rendered_x_` or `current_scanline_`. These are only reset in `StartLine()` which is called for scanlines 1-224.
|
||||
|
||||
**Potential issue**: If WriteBBus CatchUp is called during vblank or on scanline 0, `current_scanline_` might have stale value from previous frame.
|
||||
|
||||
Check: WriteBBus condition is `!in_vblank_ && memory_.v_pos() > 0`, so this should be safe.
|
||||
|
||||
### New Theory 6: Title screen specific PPU configuration
|
||||
The title screen might use a specific PPU configuration that triggers a rendering bug. Need to compare $212C (layer enables), $2105 (mode), $210B-$210C (tile addresses) between title screen and working screens.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Verify `current_scanline_` is properly declared - DONE, it's fine
|
||||
2. **Test pre-JIT commit** to confirm title screen BG works without JIT
|
||||
3. **Add targeted debug logging** for title screen (module 0x01) PPU state
|
||||
4. **Compare PPU register state** between title screen and cutscene
|
||||
5. **Check if forced_blank is stuck** during title screen BG rendering
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Root Cause Commit**: `e37497e9ef` - "feat(emu): add PPU JIT catch-up for mid-scanline raster effects"
|
||||
|
||||
This is the ONLY commit that changed PPU rendering logic. The bug must be in this commit.
|
||||
|
||||
**What the JIT system changed:**
|
||||
1. Split `RunLine()` into `StartLine()` + `CatchUp()`
|
||||
2. Added progressive pixel rendering based on h_pos
|
||||
3. Added WriteBBus CatchUp to handle mid-scanline PPU register writes
|
||||
|
||||
**Why title screen specifically might be affected:**
|
||||
The title screen uses HDMA for the wavy cloud scroll effect on BG1. While basic HDMA timing appears correct (runs after CatchUp(1104)), there may be a subtle timing or state issue that affects only certain screen configurations.
|
||||
|
||||
**Recommended debugging approach:**
|
||||
1. Test pre-JIT to confirm title screen works: `git checkout e37497e9ef~1 -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h src/app/emu/snes.cc`
|
||||
2. If confirmed, binary search within the JIT changes to isolate the specific bug
|
||||
3. Add logging to compare PPU state ($212C, $2105, etc.) between title screen and working screens
|
||||
|
||||
---
|
||||
|
||||
## Testing Commands
|
||||
|
||||
```bash
|
||||
# Checkout pre-JIT to test (CONFIRMS if JIT is the culprit)
|
||||
git checkout e37497e9ef~1 -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h src/app/emu/snes.cc
|
||||
|
||||
# Restore JIT version
|
||||
git checkout HEAD -- src/app/emu/video/ppu.cc src/app/emu/video/ppu.h src/app/emu/snes.cc
|
||||
|
||||
# Test with just WriteBBus CatchUp removed (isolate that change)
|
||||
# Edit snes.cc WriteBBus to comment out the CatchUp call
|
||||
|
||||
# Test with RunLine instead of StartLine/CatchUp (but keep WriteBBus CatchUp)
|
||||
# Would require manual code changes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Identified root cause commit: `e37497e9ef`
|
||||
- [x] Verified no other commits changed PPU rendering
|
||||
- [x] Documented JIT system changes
|
||||
- [ ] **PENDING**: Test pre-JIT to confirm
|
||||
- [ ] **PENDING**: Isolate specific bug in JIT implementation
|
||||
@@ -0,0 +1,141 @@
|
||||
# Graphics Loading Regression Analysis (2024)
|
||||
|
||||
## Overview
|
||||
|
||||
This document records the root cause analysis and fix for a critical graphics loading regression where all overworld maps appeared green and graphics sheets appeared "brownish purple" (solid 0xFF fill).
|
||||
|
||||
## Symptoms
|
||||
|
||||
- Overworld maps rendered as solid green tiles
|
||||
- Graphics sheets in the Graphics Editor appeared as solid purple/brown color
|
||||
- All 223 graphics sheets were filled with 0xFF bytes
|
||||
- Issue appeared after WASM-related changes to `src/app/rom.cc`
|
||||
|
||||
## Root Cause
|
||||
|
||||
**Two bugs combined to cause complete graphics loading failure:**
|
||||
|
||||
### Bug 1: DecompressV2 Size Parameter = 0
|
||||
|
||||
The most critical bug was in the `DecompressV2()` calls in `LoadAllGraphicsData()` and `Load2BppGraphics()`:
|
||||
|
||||
```cpp
|
||||
// BROKEN - size parameter is 0, causes immediate empty return
|
||||
gfx::lc_lz2::DecompressV2(rom.data(), offset, 0, 1, rom.size())
|
||||
|
||||
// CORRECT - size must be 0x800 (2048 bytes)
|
||||
gfx::lc_lz2::DecompressV2(rom.data(), offset, 0x800, 1, rom.size())
|
||||
```
|
||||
|
||||
In `compression.cc`, the `DecompressV2` function has this early-exit check:
|
||||
|
||||
```cpp
|
||||
if (size == 0) {
|
||||
return std::vector<uint8_t>(); // Returns empty immediately!
|
||||
}
|
||||
```
|
||||
|
||||
When `size=0` was passed, every single graphics sheet decompression returned an empty vector, triggering the fallback path that fills the graphics buffer with 0xFF bytes.
|
||||
|
||||
### Bug 2: Header Stripping Logic Change (Secondary)
|
||||
|
||||
The SMC header detection was also modified from:
|
||||
|
||||
```cpp
|
||||
// ORIGINAL (working) - modulo 1MB
|
||||
size % kBaseRomSize == kHeaderSize // kBaseRomSize = 1,048,576
|
||||
|
||||
// CHANGED TO (problematic) - modulo 32KB
|
||||
size % 0x8000 == kHeaderSize
|
||||
```
|
||||
|
||||
The 32KB modulo check could cause false positives on ROMs that happened to have sizes matching the pattern, potentially stripping data that wasn't actually an SMC header.
|
||||
|
||||
## Investigation Process
|
||||
|
||||
### Initial Hypothesis
|
||||
|
||||
1. **Header/Footer Mismatch** - Suspected incorrect ROM alignment causing 512-byte offset in all pointer lookups
|
||||
2. **Pointer Table Corruption** - Suspected `GetGraphicsAddress` reading garbage due to misalignment
|
||||
3. **Decompression Failure** - Suspected `DecompressV2` failing silently
|
||||
|
||||
### Discovery Method
|
||||
|
||||
1. **Agent-based parallel investigation** - Spawned three agents to analyze:
|
||||
- ROM alignment (header stripping logic)
|
||||
- Graphics pipeline (pointer tables and decompression)
|
||||
- WASM integration (data transfer integrity)
|
||||
|
||||
2. **Git archaeology** - Compared working commit (`43dfd65b2c`) with broken code:
|
||||
```bash
|
||||
git show 43dfd65b2c:src/app/rom.cc | grep "DecompressV2"
|
||||
# Output: gfx::lc_lz2::DecompressV2(rom.data(), offset) # 2 args!
|
||||
```
|
||||
|
||||
3. **Function signature analysis** - Found `DecompressV2` signature:
|
||||
```cpp
|
||||
DecompressV2(data, offset, size=0x800, mode=1, rom_size=-1)
|
||||
```
|
||||
|
||||
4. **Root cause identified** - The broken code passed explicit `0` for the size parameter, overriding the default of `0x800`.
|
||||
|
||||
## Fix Applied
|
||||
|
||||
### File: `src/app/rom.cc`
|
||||
|
||||
1. **Restored header stripping logic** to use 1MB modulo:
|
||||
```cpp
|
||||
if (size % kBaseRomSize == kHeaderSize && size >= kHeaderSize &&
|
||||
rom_data.size() >= kHeaderSize)
|
||||
```
|
||||
|
||||
2. **Fixed DecompressV2 calls** (2 locations):
|
||||
- Line ~126 (Load2BppGraphics)
|
||||
- Line ~332 (LoadAllGraphicsData)
|
||||
|
||||
Changed from `DecompressV2(..., 0, 1, rom.size())` to `DecompressV2(..., 0x800, 1, rom.size())`
|
||||
|
||||
3. **Added diagnostic logging** to help future debugging:
|
||||
- ROM alignment verification after header stripping
|
||||
- SNES checksum validation logging
|
||||
- Graphics pointer table probe (first 5 sheets)
|
||||
|
||||
### File: `src/app/gfx/util/compression.h`
|
||||
|
||||
Added comprehensive documentation to `DecompressV2()` with explicit warning about size=0.
|
||||
|
||||
## Prevention Measures
|
||||
|
||||
### Code Comments Added
|
||||
|
||||
1. **MaybeStripSmcHeader** - Warning not to change modulo base from 1MB to 32KB
|
||||
2. **DecompressV2 calls** - Comments explaining the 0x800 size requirement
|
||||
3. **LoadAllGraphicsData** - Function header documenting the regression
|
||||
|
||||
### Documentation Added
|
||||
|
||||
1. Updated `compression.h` with full parameter documentation
|
||||
2. Added `@warning` tags about size=0 behavior
|
||||
3. Documented sheet categories and compression formats in `rom.cc`
|
||||
|
||||
## Key Learnings
|
||||
|
||||
1. **Default parameters can be overridden accidentally** - When adding new parameters to a function call, be careful not to override defaults with wrong values.
|
||||
|
||||
2. **Early-exit conditions can cause silent failures** - The `if (size == 0) return empty` was valid behavior, but calling code must respect it.
|
||||
|
||||
3. **Diagnostic logging is valuable** - The added probe logging for the first 5 graphics sheets helps quickly identify alignment issues.
|
||||
|
||||
4. **Git archaeology is essential** - Comparing with known-working commits reveals exactly what changed.
|
||||
|
||||
## Related Files
|
||||
|
||||
- `src/app/rom.cc` - Main ROM handling and graphics loading
|
||||
- `src/app/gfx/util/compression.cc` - LC-LZ2 decompression implementation
|
||||
- `src/app/gfx/util/compression.h` - Decompression function declarations
|
||||
- `incl/zelda.h` - Version-specific pointer table offsets
|
||||
|
||||
## Commits
|
||||
|
||||
- **Breaking commit**: Changes to WASM memory safety in `rom.cc`
|
||||
- **Fix commit**: Restored header stripping, fixed DecompressV2 size parameter
|
||||
323
docs/internal/archive/investigations/object-rendering-fixes.md
Normal file
323
docs/internal/archive/investigations/object-rendering-fixes.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 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`:
|
||||
|
||||
```cpp
|
||||
// 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:
|
||||
|
||||
```cpp
|
||||
// Replace lines 141, 160, 178 with:
|
||||
int tile_count = GetObjectTileCount(object_id);
|
||||
return ReadTileData(tile_data_ptr, tile_count);
|
||||
```
|
||||
|
||||
**Before:**
|
||||
```cpp
|
||||
absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseSubtype1(
|
||||
int16_t object_id) {
|
||||
// ...
|
||||
return ReadTileData(tile_data_ptr, 8); // ❌ WRONG
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```cpp
|
||||
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:**
|
||||
|
||||
```cpp
|
||||
// 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:**
|
||||
|
||||
```cpp
|
||||
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:
|
||||
|
||||
```cpp
|
||||
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:**
|
||||
```bash
|
||||
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:**
|
||||
```cpp
|
||||
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`
|
||||
@@ -0,0 +1,157 @@
|
||||
# WASM Bounds Checking Audit
|
||||
|
||||
This document tracks potentially unsafe array accesses that can cause "index out of bounds" RuntimeErrors in WASM builds with assertions enabled.
|
||||
|
||||
## Background
|
||||
|
||||
WASM builds with `-sASSERTIONS=1` perform runtime bounds checking on all memory accesses. Invalid accesses trigger a `RuntimeError: index out of bounds` that halts the module.
|
||||
|
||||
## Analysis Tools
|
||||
|
||||
Run the static analysis script to find potentially dangerous patterns:
|
||||
```bash
|
||||
./scripts/find-unsafe-array-access.sh
|
||||
```
|
||||
|
||||
## Known Fixed Issues
|
||||
|
||||
### 1. object_drawer.cc - Tile Rendering (Fixed 2024-11)
|
||||
**File:** `src/zelda3/dungeon/object_drawer.cc`
|
||||
**Issue:** `tiledata[src_index]` access without bounds validation
|
||||
**Fix:** Added `kMaxTileRow = 63` validation before access
|
||||
|
||||
### 2. background_buffer.cc - Background Tile Rendering (Fixed 2024-11)
|
||||
**File:** `src/app/gfx/render/background_buffer.cc`
|
||||
**Issue:** Same pattern as object_drawer.cc
|
||||
**Fix:** Added same bounds checking
|
||||
|
||||
### 3. arena.h - Graphics Sheet Accessors (Fixed 2024-11)
|
||||
**File:** `src/app/gfx/resource/arena.h`
|
||||
**Issue:** `gfx_sheets_[i]` accessed without bounds check
|
||||
**Fix:** Added bounds validation returning empty/null for invalid indices
|
||||
|
||||
### 4. bitmap.cc - Palette Application (Fixed 2024-11)
|
||||
**File:** `src/app/gfx/core/bitmap.cc`
|
||||
**Issue:** `palette[i]` accessed without checking palette size
|
||||
**Fix:** Added bounds check against `sdl_palette->ncolors`
|
||||
|
||||
### 5. tilemap.cc - FetchTileDataFromGraphicsBuffer (Fixed 2024-11)
|
||||
**File:** `src/app/gfx/render/tilemap.cc`
|
||||
**Issue:** `data[src_index]` accessed without checking data vector size
|
||||
**Fix:** Added `src_index >= 0 && src_index < data_size` validation
|
||||
|
||||
### 6. overworld.h - Map Accessors (Fixed 2025-11-26)
|
||||
**File:** `src/zelda3/overworld/overworld.h`
|
||||
**Issue:** `overworld_map(int i)` and `mutable_overworld_map(int i)` accessed vector without bounds check
|
||||
**Fix:** Added bounds validation returning nullptr for invalid indices
|
||||
|
||||
### 7. overworld.h - Sprite Accessors (Fixed 2025-11-26)
|
||||
**File:** `src/zelda3/overworld/overworld.h`
|
||||
**Issue:** `sprites(int state)` accessed array without validating state (0-2)
|
||||
**Fix:** Added bounds check returning empty vector for invalid state
|
||||
|
||||
### 8. overworld.h - Current Map Accessors (Fixed 2025-11-26)
|
||||
**File:** `src/zelda3/overworld/overworld.h`
|
||||
**Issue:** `current_graphics()`, `current_area_palette()`, etc. accessed `overworld_maps_[current_map_]` without validating `current_map_`
|
||||
**Fix:** Added `is_current_map_valid()` helper and validation in all accessors
|
||||
|
||||
### 9. snes_palette.h - PaletteGroup Negative Index (Fixed 2025-11-26)
|
||||
**File:** `src/app/gfx/types/snes_palette.h`
|
||||
**Issue:** `operator[]` only checked upper bound, not negative indices
|
||||
**Fix:** Added `i < 0` check to bounds validation
|
||||
|
||||
### 10. room.cc - LoadAnimatedGraphics sizeof vs size() (Fixed 2025-11-26)
|
||||
**File:** `src/zelda3/dungeon/room.cc`
|
||||
**Issue:** Used `sizeof(current_gfx16_)` instead of `.size()` for bounds checking
|
||||
**Fix:** Changed to use `.size()` for clarity and maintainability
|
||||
|
||||
## Patterns Requiring Caution
|
||||
|
||||
### Critical Risk Patterns
|
||||
|
||||
These patterns directly access memory buffers and are most likely to crash:
|
||||
|
||||
1. **`tiledata[index]`** - Graphics buffer access
|
||||
- Buffer size: 0x10000 (65536 bytes)
|
||||
- Max tile row: 63 (rows 0-63)
|
||||
- Stride: 128 bytes per row
|
||||
- **Validation:** `tile_row <= 63` before computing `src_index`
|
||||
|
||||
2. **`buffer_[index]`** - Tile buffer access
|
||||
- Check: `index < buffer_.size()`
|
||||
|
||||
3. **`canvas[index]`** - Pixel canvas access
|
||||
- Check: `index < width * height`
|
||||
|
||||
4. **`.data()[index]`** - Vector data access
|
||||
- Check: `index < vector.size()`
|
||||
|
||||
### High Risk Patterns
|
||||
|
||||
These access ROM data or game structures that may contain corrupt values:
|
||||
|
||||
1. **`rom.data()[offset]`** - ROM data access
|
||||
- Check: `offset < rom.size()`
|
||||
|
||||
2. **`palette[index]`** - Palette color access
|
||||
- Check: `index < palette.size()`
|
||||
|
||||
3. **`overworld_maps_[i]`** - Map access
|
||||
- Check: `i < 160` (or appropriate constant)
|
||||
|
||||
4. **`rooms_[i]`** - Room access
|
||||
- Check: `i < 296`
|
||||
|
||||
### Medium Risk Patterns
|
||||
|
||||
Usually safe but worth verifying:
|
||||
|
||||
1. **`gfx_sheet(i)`** - Already has bounds check returning empty Bitmap
|
||||
2. **`vram[index]`, `cgram[index]`, `oam[index]`** - Usually masked with `& 0x7fff`, `& 0xff`
|
||||
|
||||
## Bounds Checking Template
|
||||
|
||||
```cpp
|
||||
// For tile data access
|
||||
constexpr int kGfxBufferSize = 0x10000;
|
||||
constexpr int kMaxTileRow = 63;
|
||||
|
||||
int tile_row = tile_id / 16;
|
||||
if (tile_row > kMaxTileRow) {
|
||||
return; // Skip invalid tile
|
||||
}
|
||||
|
||||
int src_index = (src_row * 128) + src_col + tile_base_x + tile_base_y;
|
||||
if (src_index < 0 || src_index >= kGfxBufferSize) {
|
||||
continue; // Skip invalid access
|
||||
}
|
||||
|
||||
// For destination canvas
|
||||
int dest_index = dest_y * width + dest_x;
|
||||
if (dest_index < 0 || dest_index >= static_cast<int>(canvas.size())) {
|
||||
continue; // Skip invalid access
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
1. Run WASM build with assertions: `-sASSERTIONS=1`
|
||||
2. Load ROMs with varying data quality
|
||||
3. Open each editor and interact with all features
|
||||
4. Monitor browser console for `RuntimeError: index out of bounds`
|
||||
|
||||
## Error Reporting
|
||||
|
||||
The crash reporter (`src/web/core/crash_reporter.js`) provides:
|
||||
- Stack trace parsing to identify WASM function indices
|
||||
- Auto-diagnosis of known error patterns
|
||||
- Problems panel for non-fatal errors
|
||||
- Console log history capture
|
||||
|
||||
## Recovery
|
||||
|
||||
The recovery system (`src/web/core/wasm_recovery.js`) provides:
|
||||
- Automatic crash detection
|
||||
- Non-blocking recovery overlay
|
||||
- Module reinitialization (up to 3 attempts)
|
||||
- ROM data preservation via IndexedDB
|
||||
44
docs/internal/archive/investigations/wasm_release_crash.md
Normal file
44
docs/internal/archive/investigations/wasm_release_crash.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# WASM Release Crash Plan
|
||||
|
||||
Status: **ACTIVE**
|
||||
Owner: **backend-infra-engineer**
|
||||
Created: 2025-11-26
|
||||
Last Reviewed: 2025-11-26
|
||||
Next Review: 2025-12-03
|
||||
Coordination: [coordination-board entry](./coordination-board.md#2025-11-25-backend-infra-engineer--wasm-release-crash-triage)
|
||||
|
||||
## Context
|
||||
Release WASM build crashes with `RuntimeError: memory access out of bounds` during ROM load; debug build succeeds. Stack maps to `std::unordered_map<int, gfx::Bitmap>` construction and sprite name static init, implying UB/heap corruption in bitmap/tile caching paths surfaced by `-O3`/no `SAFE_HEAP`/LTO.
|
||||
|
||||
## Goals / Exit Criteria
|
||||
- Reproduce crash on a “sanitized release” build with symbolized stack (SAFE_HEAP/ASSERTIONS) and isolate the exact C++ site.
|
||||
- Implement a fix eliminating the heap corruption in release (likely bitmap/tile cache ownership/eviction) and verify both release/debug load ROM successfully.
|
||||
- Ship a safe release configuration (temporary SAFE_HEAP or LTO toggle acceptable) for GitHub Pages until root cause fix lands.
|
||||
- Remove/mitigate boot-time `Module` setter warning in `core/namespace.js` to reduce noise.
|
||||
- Document findings and updated build guidance in wasm playbook.
|
||||
|
||||
## Plan
|
||||
1) **Repro + Instrumentation (today)**
|
||||
- Build “sanitized release” preset (O3 + `-s SAFE_HEAP=1 -s ASSERTIONS=2`, LTO off) and re-run ROM load via Playwright harness to capture precise stack.
|
||||
- Add temporary addr2line helper script for wasm (if needed) to map addresses quickly.
|
||||
|
||||
2) **Root Cause Fix (next)**
|
||||
- Inspect bitmap/tile cache ownership and eviction (`TileCache::CacheTile`, `BitmapTable`, `Arena::ProcessTextureQueue`, `Canvas::DrawBitmapTable`) for dangling surface/texture pointers or moved-from Bitmaps stored in unordered_map under optimization.
|
||||
- Patch to avoid storing moved-from Bitmap references (prefer emplace/move with clear ownership), ensure surface/texture pointers nulled before eviction, and guard palette/renderer access in release.
|
||||
- Rebuild release (standard flags) and verify ROM load succeeds without SAFE_HEAP.
|
||||
|
||||
3) **Mitigations & Cleanup (parallel/after fix)**
|
||||
- If fix needs longer: ship interim release with SAFE_HEAP/LTO-off to unblock Pages users.
|
||||
- Fix `window.yaze.core.Module` setter clash (define writable property) to remove boot warning.
|
||||
- Triage double `FS.syncfs` warning (low priority) while in code.
|
||||
- Update `wasm-antigravity-playbook` with debug steps + interim release flag guidance.
|
||||
|
||||
## Validation
|
||||
- Automated ROM load (Playwright script) passes on release and debug builds, no runtime errors or aborts.
|
||||
- Manual spot-check in browser confirms ROM loads and renders; no console OOB errors; yazeDebug ROM status shows loaded.
|
||||
- GitHub Pages deployment built with chosen flags loads ROM without crash.
|
||||
- No regressions in debug build (SAFE_HEAP path still works).
|
||||
|
||||
## Notes / Risks
|
||||
- SAFE_HEAP in release increases bundle size/perf cost; acceptable as interim but not final.
|
||||
- If root cause lives in SDL surface/texture lifetimes, need to validate on native as well (possible hidden UB masked by sanitizer).
|
||||
@@ -0,0 +1,48 @@
|
||||
# Web DOM Interaction & Color Picker Report
|
||||
|
||||
## Overview
|
||||
This document details the investigation into the Web DOM structure of the YAZE WASM application, the interaction with the ImGui-based Color Picker, and the recommended workflow for future agents.
|
||||
|
||||
## Web DOM Structure
|
||||
The YAZE WASM application is primarily an ImGui application rendered onto an HTML5 Canvas.
|
||||
- **Canvas Element**: The main interaction point is the `<canvas>` element (ID `canvas`).
|
||||
- **DOM Elements**: There are very few standard DOM elements for UI controls. Most UI is rendered by ImGui within the canvas.
|
||||
- **Input**: Interaction relies on mouse events (clicks, drags) and keyboard input sent to the canvas.
|
||||
|
||||
## Color Picker Investigation
|
||||
- **Initial Request**: The user mentioned a "web color picker".
|
||||
- **Findings**:
|
||||
- No standard HTML `<input type="color">` or JavaScript-based color picker was found in `src/web`.
|
||||
- The color picker is part of the ImGui interface (`PaletteEditorWidget`).
|
||||
- It appears as a popup window ("Edit Color") when a color swatch is clicked.
|
||||
- **Fix Implemented**:
|
||||
- Standardized `PaletteEditorWidget` to use `gui::SnesColorEdit4` instead of manual `ImGui::ColorEdit3`.
|
||||
- Used `gui::MakePopupIdWithInstance` to generate unique IDs for the "Edit Color" popup, preventing conflicts when multiple editors are open.
|
||||
- Verified the fix by rebuilding the WASM app and interacting with it via the browser subagent.
|
||||
|
||||
## Recommended Editing Flow for Agents
|
||||
Since the application is heavily ImGui-based, standard DOM manipulation tools (`click_element`, `fill_input`) are of limited use for the core application features.
|
||||
|
||||
### 1. Navigation & Setup
|
||||
- **Navigate**: Use `open_browser_url` to go to `http://localhost:8080`.
|
||||
- **Wait**: Always wait for the WASM module to load (look for "Ready" in console or wait 5-10 seconds).
|
||||
- **ROM Loading**:
|
||||
- Drag and drop is the most reliable way to load a ROM if `window.yaze.control.loadRom` is not available or robust.
|
||||
- Use `browser_drag_file_to_pixel` (or similar) to drop `zelda3.sfc` onto the canvas center.
|
||||
|
||||
### 2. Interacting with ImGui
|
||||
- **JavaScript Bridge**: Use `window.yaze.control.*` APIs to switch editors and query state.
|
||||
- Example: `window.yaze.control.switchEditor('Palette')`
|
||||
- **Pixel-Based Interaction**:
|
||||
- Use `click_browser_pixel` and `browser_drag_pixel_to_pixel` to interact with ImGui elements.
|
||||
- **Coordinates**: You may need to infer coordinates or use a "visual search" approach (taking screenshots and analyzing them) to find buttons.
|
||||
- **Feedback**: Take screenshots after actions to verify the UI updated as expected.
|
||||
|
||||
### 3. Debugging & Inspection
|
||||
- **Console Logs**: Use `capture_browser_console_logs` to check for WASM errors or status messages.
|
||||
- **Screenshots**: Essential for verifying rendering and UI state.
|
||||
|
||||
## Key Files
|
||||
- `src/app/gui/widgets/palette_editor_widget.cc`: Implements the Palette Editor UI.
|
||||
- `src/web/app.js`: Main JavaScript entry point, exposes `window.yaze` API.
|
||||
- `docs/internal/wasm-yazeDebug-api-reference.md`: Reference for the JavaScript API.
|
||||
@@ -0,0 +1,559 @@
|
||||
# ZScream vs yaze: Dungeon Object Rendering Comparison
|
||||
|
||||
**Date:** 2025-11-26
|
||||
**Author:** Claude (Sonnet 4.5)
|
||||
**Purpose:** Identify discrepancies between ZScream's proven object rendering and yaze's implementation
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This analysis compares yaze's dungeon object rendering system with ZScream's reference implementation. The goal is to identify bugs causing incorrect object rendering, particularly issues observed where "some objects don't look right" despite walls rendering correctly after the LoadLayout fix.
|
||||
|
||||
### Key Findings
|
||||
|
||||
1. **Tile Count Mismatch**: yaze loads only 4-8 tiles per object, while ZScream loads variable counts (1-242 tiles) based on object type
|
||||
2. **Drawing Method Difference**: ZScream uses tile-by-tile DrawInfo instructions, yaze uses pattern-based draw routines
|
||||
3. **Graphics Sheet Access**: Different approaches to accessing tile graphics data
|
||||
4. **Palette Handling**: Both use similar palette offset calculations (correct in yaze)
|
||||
|
||||
---
|
||||
|
||||
## 1. Tile Loading Architecture
|
||||
|
||||
### ZScream's Approach (Reference Implementation)
|
||||
|
||||
**File:** `/ZScreamDungeon/ZeldaFullEditor/Data/Underworld/RoomObjectTileLister.cs`
|
||||
|
||||
```csharp
|
||||
// Initialization specifies exact tile counts per object
|
||||
AutoFindTiles(0x000, 4); // Object 0x00: 4 tiles
|
||||
AutoFindTiles(0x001, 8); // Object 0x01: 8 tiles
|
||||
AutoFindTiles(0x033, 16); // Object 0x33: 16 tiles
|
||||
AutoFindTiles(0x0C1, 68); // Object 0xC1: 68 tiles (Chest platform)
|
||||
AutoFindTiles(0x215, 80); // Object 0x215: 80 tiles (Kholdstare prison)
|
||||
AutoFindTiles(0x262, 242); // Object 0x262: 242 tiles (Fortune teller room!)
|
||||
SetTilesFromKnownOffset(0x22D, 0x1B4A, 84); // Agahnim's altar: 84 tiles
|
||||
SetTilesFromKnownOffset(0x22E, 0x1BF2, 127); // Agahnim's boss room: 127 tiles
|
||||
```
|
||||
|
||||
**Key Method:**
|
||||
```csharp
|
||||
public static TilesList CreateNewDefinition(ZScreamer ZS, int position, int count)
|
||||
{
|
||||
Tile[] list = new Tile[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
list[i] = new Tile(ZS.ROM.Read16(position + i * 2)); // 2 bytes per tile
|
||||
}
|
||||
return new TilesList(list);
|
||||
}
|
||||
```
|
||||
|
||||
**Critical Insight:** ZScream reads **exactly 2 bytes per tile** (one 16-bit word per tile) and loads **object-specific counts** (not fixed 8 tiles for all).
|
||||
|
||||
### yaze's Approach (Current Implementation)
|
||||
|
||||
**File:** `/yaze/src/zelda3/dungeon/object_parser.cc`
|
||||
|
||||
```cpp
|
||||
absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ParseSubtype1(
|
||||
int16_t object_id) {
|
||||
int index = object_id & 0xFF;
|
||||
int tile_ptr = kRoomObjectSubtype1 + (index * 2);
|
||||
|
||||
uint8_t low = rom_->data()[tile_ptr];
|
||||
uint8_t high = rom_->data()[tile_ptr + 1];
|
||||
int tile_data_ptr = kRoomObjectTileAddress + ((high << 8) | low);
|
||||
|
||||
// Read 8 tiles (most subtype 1 objects use 8 tiles) ❌
|
||||
return ReadTileData(tile_data_ptr, 8); // HARDCODED to 8!
|
||||
}
|
||||
|
||||
absl::StatusOr<std::vector<gfx::TileInfo>> ObjectParser::ReadTileData(
|
||||
int address, int tile_count) {
|
||||
for (int i = 0; i < tile_count; i++) {
|
||||
int tile_offset = address + (i * 2); // ✅ Correct: 2 bytes per tile
|
||||
uint16_t tile_word =
|
||||
rom_->data()[tile_offset] | (rom_->data()[tile_offset + 1] << 8);
|
||||
tiles.push_back(gfx::WordToTileInfo(tile_word));
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
```
|
||||
|
||||
**Problems Identified:**
|
||||
1. ❌ **Hardcoded tile count**: Always reads 8 tiles, regardless of object type
|
||||
2. ❌ **Missing object-specific counts**: No lookup table for actual tile requirements
|
||||
3. ✅ **Correct byte stride**: 2 bytes per tile (matches ZScream)
|
||||
4. ✅ **Correct pointer resolution**: Matches ZScream's tile address calculation
|
||||
|
||||
---
|
||||
|
||||
## 2. Object Drawing Methods
|
||||
|
||||
### ZScream's Drawing Architecture
|
||||
|
||||
**File:** `/ZScreamDungeon/ZeldaFullEditor/Data/Types/DungeonObjectDraw.cs`
|
||||
|
||||
ZScream uses explicit `DrawInfo` instructions that specify:
|
||||
- Which tile index to draw
|
||||
- X/Y pixel offset from object origin
|
||||
- Whether to flip horizontally/vertically
|
||||
|
||||
**Example: Agahnim's Altar (Object 0x22D)**
|
||||
```csharp
|
||||
public static void RoomDraw_AgahnimsAltar(ZScreamer ZS, RoomObject obj)
|
||||
{
|
||||
int tid = 0;
|
||||
for (int y = 0; y < 14 * 8; y += 8)
|
||||
{
|
||||
DrawTiles(ZS, obj, false,
|
||||
new DrawInfo(tid, 0, y, hflip: false),
|
||||
new DrawInfo(tid + 14, 8, y, hflip: false),
|
||||
new DrawInfo(tid + 14, 16, y, hflip: false),
|
||||
new DrawInfo(tid + 28, 24, y, hflip: false),
|
||||
new DrawInfo(tid + 42, 32, y, hflip: false),
|
||||
new DrawInfo(tid + 56, 40, y, hflip: false),
|
||||
new DrawInfo(tid + 70, 48, y, hflip: false),
|
||||
|
||||
new DrawInfo(tid + 70, 56, y, hflip: true),
|
||||
new DrawInfo(tid + 56, 64, y, hflip: true),
|
||||
new DrawInfo(tid + 42, 72, y, hflip: true),
|
||||
new DrawInfo(tid + 28, 80, y, hflip: true),
|
||||
new DrawInfo(tid + 14, 88, y, hflip: true),
|
||||
new DrawInfo(tid + 14, 96, y, hflip: true),
|
||||
new DrawInfo(tid, 104, y, hflip: true)
|
||||
);
|
||||
tid++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates a **14-tile-high, 14-tile-wide symmetrical structure** using 84 total tile placements with mirroring.
|
||||
|
||||
**Core Drawing Method:**
|
||||
```csharp
|
||||
public static unsafe void DrawTiles(ZScreamer ZS, RoomObject obj, bool allbg,
|
||||
params DrawInfo[] instructions)
|
||||
{
|
||||
foreach (DrawInfo d in instructions)
|
||||
{
|
||||
if (obj.Width < d.XOff + 8) obj.Width = d.XOff + 8;
|
||||
if (obj.Height < d.YOff + 8) obj.Height = d.YOff + 8;
|
||||
|
||||
int tm = (d.XOff / 8) + obj.GridX + ((obj.GridY + (d.YOff / 8)) * 64);
|
||||
|
||||
if (tm < Constants.TilesPerUnderworldRoom && tm >= 0)
|
||||
{
|
||||
ushort td = obj.Tiles[d.TileIndex].GetModifiedUnsignedShort(
|
||||
hflip: d.HFlip, vflip: d.VFlip);
|
||||
|
||||
ZS.GFXManager.tilesBg1Buffer[tm] = td; // Direct tile buffer write
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- ✅ Uses tile index (`d.TileIndex`) to access specific tiles from the object's tile array
|
||||
- ✅ Calculates linear buffer index: `(x_tile) + (y_tile * 64)`
|
||||
- ✅ Applies tile transformations (hflip, vflip) before writing
|
||||
- ✅ Dynamically updates object bounds based on drawn tiles
|
||||
|
||||
### yaze's Drawing Architecture
|
||||
|
||||
**File:** `/yaze/src/zelda3/dungeon/object_drawer.cc`
|
||||
|
||||
yaze uses **pattern-based draw routines** that assume tile arrangements:
|
||||
|
||||
```cpp
|
||||
void ObjectDrawer::DrawRightwards2x2_1to15or32(
|
||||
const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
std::span<const gfx::TileInfo> tiles) {
|
||||
int size = obj.size_;
|
||||
if (size == 0) size = 32; // Special case for object 0x00
|
||||
|
||||
for (int s = 0; s < size; s++) {
|
||||
if (tiles.size() >= 4) {
|
||||
WriteTile8(bg, obj.x_ + (s * 2), obj.y_, tiles[0]); // Top-left
|
||||
WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_, tiles[1]); // Top-right
|
||||
WriteTile8(bg, obj.x_ + (s * 2), obj.y_ + 1, tiles[2]); // Bottom-left
|
||||
WriteTile8(bg, obj.x_ + (s * 2) + 1, obj.y_ + 1, tiles[3]); // Bottom-right
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- ❌ **Assumes 2x2 pattern works for all size values** (but ZScream uses explicit tile indices)
|
||||
- ❌ **Only uses first 4 tiles** (`tiles[0-3]`) even if more tiles are loaded
|
||||
- ❌ **No tile transformation support** (hflip, vflip not implemented in draw routines)
|
||||
- ⚠️ **Pattern might not match actual ROM data** for complex objects
|
||||
|
||||
**Comparison:**
|
||||
|
||||
| Feature | ZScream | yaze |
|
||||
|---------|---------|------|
|
||||
| Drawing Style | Explicit tile indices + offsets | Pattern-based (2x2, 2x4, etc.) |
|
||||
| Tile Selection | `obj.Tiles[d.TileIndex]` | `tiles[0..3]` |
|
||||
| Tile Transforms | ✅ hflip, vflip per tile | ❌ Not implemented |
|
||||
| Object Bounds | ✅ Dynamic, updated per tile | ❌ Fixed by pattern |
|
||||
| Large Objects | ✅ 84+ tile instructions | ❌ Limited by pattern size |
|
||||
|
||||
---
|
||||
|
||||
## 3. Graphics Sheet Access
|
||||
|
||||
### ZScream's Graphics Manager
|
||||
|
||||
```csharp
|
||||
// ZScream accesses graphics via GFXManager
|
||||
byte* ptr = (byte*) ZS.GFXManager.currentgfx16Ptr.ToPointer();
|
||||
byte* alltilesData = (byte*) ZS.GFXManager.currentgfx16Ptr.ToPointer();
|
||||
|
||||
// For preview rendering:
|
||||
byte* previewPtr = (byte*) ZS.GFXManager.previewObjectsPtr[pre.ObjectType.FullID].ToPointer();
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
- `currentgfx16`: Room-specific graphics buffer (16 blocks × 4096 bytes = 64KB)
|
||||
- Tiles accessed as **4BPP packed data** (2 bytes per pixel row for 8 pixels)
|
||||
|
||||
### yaze's Graphics Buffer
|
||||
|
||||
```cpp
|
||||
// File: src/zelda3/dungeon/object_drawer.cc
|
||||
void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, uint8_t x_grid,
|
||||
uint8_t y_grid, const gfx::TileInfo& tile_info) {
|
||||
int tile_index = tile_info.id_;
|
||||
int blockset_index = tile_index / 0x200;
|
||||
int sheet_tile_id = tile_index % 0x200;
|
||||
|
||||
// Access from room_gfx_buffer_ (set during Room initialization)
|
||||
uint8_t* gfx_sheet = const_cast<uint8_t*>(room_gfx_buffer_) + (blockset_index * 0x1000);
|
||||
|
||||
for (int py = 0; py < 8; py++) {
|
||||
for (int px = 0; px < 8; px++) {
|
||||
int tile_col = sheet_tile_id % 16;
|
||||
int tile_row = sheet_tile_id / 16;
|
||||
int tile_base_x = tile_col * 8;
|
||||
int tile_base_y = tile_row * 1024; // 8 rows × 128 bytes
|
||||
int src_index = (py * 128) + px + tile_base_x + tile_base_y;
|
||||
|
||||
if (src_index < 0 || src_index >= 0x1000) continue; // Bounds check
|
||||
|
||||
uint8_t pixel_value = gfx_sheet[src_index];
|
||||
if (pixel_value == 0) continue; // Skip transparent
|
||||
|
||||
uint8_t palette_offset = (tile_info.palette_ & 0x07) * 15;
|
||||
uint8_t color_index = (pixel_value - 1) + palette_offset;
|
||||
|
||||
bg.SetPixel(x_pixel, y_pixel, color_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
- `room_gfx_buffer_`: 8BPP linear pixel data (1 byte per pixel, values 0-7)
|
||||
- Sheet size: 128×32 pixels = 4096 bytes
|
||||
- Tile layout: 16 columns × 32 rows (512 tiles per sheet)
|
||||
|
||||
**Differences:**
|
||||
|
||||
| Aspect | ZScream | yaze |
|
||||
|--------|---------|------|
|
||||
| Format | 4BPP packed (planar) | 8BPP linear (indexed) |
|
||||
| Access | Pointer arithmetic on packed data | Array indexing on 8BPP buffer |
|
||||
| Tile Stride | 16 bytes per tile | 64 bytes per tile (8×8 pixels) |
|
||||
| Palette Offset | `* 16` (SNES standard) | `* 15` (packed 90-color format) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Specific Object Rendering Comparison
|
||||
|
||||
### Example: Object 0x33 (Carpet)
|
||||
|
||||
**ZScream:**
|
||||
```csharp
|
||||
AutoFindTiles(0x033, 16); // Loads 16 tiles from ROM
|
||||
|
||||
public static readonly RoomObjectType Object033 = new RoomObjectType(0x033,
|
||||
RoomDraw_4x4FloorIn4x4SuperSquare, Horizontal, ...);
|
||||
|
||||
public static void RoomDraw_4x4FloorIn4x4SuperSquare(ZScreamer ZS, RoomObject obj)
|
||||
{
|
||||
RoomDraw_Arbtrary4x4in4x4SuperSquares(ZS, obj);
|
||||
}
|
||||
|
||||
private static void RoomDraw_Arbtrary4x4in4x4SuperSquares(ZScreamer ZS, RoomObject obj,
|
||||
bool bothbg = false, int sizebonus = 1)
|
||||
{
|
||||
int sizex = 32 * (sizebonus + ((obj.Size >> 2) & 0x03));
|
||||
int sizey = 32 * (sizebonus + ((obj.Size) & 0x03));
|
||||
|
||||
for (int x = 0; x < sizex; x += 32)
|
||||
{
|
||||
for (int y = 0; y < sizey; y += 32)
|
||||
{
|
||||
DrawTiles(ZS, obj, bothbg,
|
||||
new DrawInfo(0, x, y),
|
||||
new DrawInfo(1, x + 8, y),
|
||||
new DrawInfo(2, x + 16, y),
|
||||
new DrawInfo(3, x + 24, y),
|
||||
|
||||
new DrawInfo(4, x, y + 8),
|
||||
new DrawInfo(5, x + 8, y + 8),
|
||||
new DrawInfo(6, x + 16, y + 8),
|
||||
new DrawInfo(7, x + 24, y + 8),
|
||||
|
||||
// ... continues with tiles 0-15 in 4x4 pattern
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**yaze:**
|
||||
```cpp
|
||||
// Object 0x33 maps to routine 16 in InitializeDrawRoutines()
|
||||
object_to_routine_map_[0x33] = 16;
|
||||
|
||||
// Routine 16 calls:
|
||||
void ObjectDrawer::DrawRightwards4x4_1to16(
|
||||
const RoomObject& obj, gfx::BackgroundBuffer& bg,
|
||||
std::span<const gfx::TileInfo> tiles) {
|
||||
int size = obj.size_ & 0x0F;
|
||||
|
||||
for (int s = 0; s < size; s++) {
|
||||
if (tiles.size() >= 16) { // ⚠️ Requires 16 tiles
|
||||
for (int ty = 0; ty < 4; ty++) {
|
||||
for (int tx = 0; tx < 4; tx++) {
|
||||
int tile_idx = ty * 4 + tx; // 0-15
|
||||
WriteTile8(bg, obj.x_ + tx + (s * 4), obj.y_ + ty, tiles[tile_idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Analysis:**
|
||||
- ✅ Both use 16 tiles
|
||||
- ✅ Both use 4×4 grid pattern
|
||||
- ⚠️ yaze's pattern assumes linear tile ordering (0-15), but actual ROM data might be different
|
||||
- ⚠️ ZScream explicitly places each tile with DrawInfo, yaze assumes pattern
|
||||
|
||||
---
|
||||
|
||||
## 5. Critical Bugs in yaze
|
||||
|
||||
### Bug 1: Hardcoded Tile Count (HIGH PRIORITY)
|
||||
|
||||
**Location:** `src/zelda3/dungeon/object_parser.cc:141,160,178`
|
||||
|
||||
```cpp
|
||||
// Current code (WRONG):
|
||||
return ReadTileData(tile_data_ptr, 8); // Always 8 tiles!
|
||||
|
||||
// Should be:
|
||||
return ReadTileData(tile_data_ptr, GetObjectTileCount(object_id));
|
||||
```
|
||||
|
||||
**Impact:**
|
||||
- Objects requiring 16+ tiles (carpets, chests, altars) only get first 8 tiles
|
||||
- Complex objects (Agahnim's room: 127 tiles) rendered with only 8 tiles
|
||||
- Results in incomplete/incorrect object rendering
|
||||
|
||||
**Fix Required:**
|
||||
Create lookup table based on ZScream's `RoomObjectTileLister.InitializeTilesFromROM()`:
|
||||
|
||||
```cpp
|
||||
static const std::unordered_map<int16_t, int> kObjectTileCounts = {
|
||||
{0x000, 4},
|
||||
{0x001, 8},
|
||||
{0x002, 8},
|
||||
{0x033, 16}, // Carpet
|
||||
{0x0C1, 68}, // Chest platform
|
||||
{0x215, 80}, // Kholdstare prison
|
||||
{0x22D, 84}, // Agahnim's altar
|
||||
{0x22E, 127}, // Agahnim's boss room
|
||||
{0x262, 242}, // Fortune teller room
|
||||
// ... complete table from ZScream
|
||||
};
|
||||
|
||||
int ObjectParser::GetObjectTileCount(int16_t object_id) {
|
||||
auto it = kObjectTileCounts.find(object_id);
|
||||
return (it != kObjectTileCounts.end()) ? it->second : 8; // Default 8
|
||||
}
|
||||
```
|
||||
|
||||
### Bug 2: Pattern-Based Drawing Limitations (MEDIUM PRIORITY)
|
||||
|
||||
**Location:** `src/zelda3/dungeon/object_drawer.cc`
|
||||
|
||||
**Problem:** Pattern-based routines don't match ZScream's explicit tile placement for complex objects.
|
||||
|
||||
**Example:** Agahnim's altar uses symmetrical mirroring:
|
||||
```csharp
|
||||
// ZScream places tiles explicitly with transformations
|
||||
new DrawInfo(tid + 70, 56, y, hflip: true), // Mirror tile 70
|
||||
new DrawInfo(tid + 56, 64, y, hflip: true), // Mirror tile 56
|
||||
```
|
||||
|
||||
yaze's `DrawRightwards4x4_1to16()` can't replicate this behavior.
|
||||
|
||||
**Fix Options:**
|
||||
1. **Option A:** Implement ZScream-style `DrawInfo` instructions per object
|
||||
2. **Option B:** Pre-bake tile transformations into tile arrays during loading
|
||||
3. **Option C:** Add tile transformation support to draw routines
|
||||
|
||||
**Recommended:** Option A (most accurate, matches ZScream)
|
||||
|
||||
### Bug 3: Missing Tile Transformation (MEDIUM PRIORITY)
|
||||
|
||||
**Location:** `src/zelda3/dungeon/object_drawer.cc:WriteTile8()`
|
||||
|
||||
**Current code:**
|
||||
```cpp
|
||||
void ObjectDrawer::WriteTile8(gfx::BackgroundBuffer& bg, uint8_t x_grid,
|
||||
uint8_t y_grid, const gfx::TileInfo& tile_info) {
|
||||
// No handling of tile_info.horizontal_mirror_ or vertical_mirror_
|
||||
// Pixels always drawn in normal orientation
|
||||
}
|
||||
```
|
||||
|
||||
**ZScream code:**
|
||||
```csharp
|
||||
ushort td = obj.Tiles[d.TileIndex].GetModifiedUnsignedShort(
|
||||
hflip: d.HFlip, vflip: d.VFlip);
|
||||
```
|
||||
|
||||
**Impact:**
|
||||
- Symmetrical objects (altars, rooms with mirrors) render incorrectly
|
||||
- Diagonal walls may have wrong orientation
|
||||
|
||||
**Fix Required:**
|
||||
```cpp
|
||||
void ObjectDrawer::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) {
|
||||
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;
|
||||
|
||||
// Use src_x, src_y for pixel access
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bug 4: Graphics Buffer Format Mismatch (RESOLVED)
|
||||
|
||||
**Status:** ✅ Fixed in previous session (2025-11-26)
|
||||
|
||||
The 8BPP linear format is correct. Palette stride of `* 15` is correct for 90-color packed palettes.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommended Fixes (Priority Order)
|
||||
|
||||
### Priority 1: Fix Tile Count Loading
|
||||
|
||||
**File:** `src/zelda3/dungeon/object_parser.cc`
|
||||
|
||||
1. Add complete tile count lookup table from ZScream
|
||||
2. Replace hardcoded `8` with `GetObjectTileCount(object_id)`
|
||||
3. Test with objects requiring 16+ tiles (0x33, 0xC1, 0x22D)
|
||||
|
||||
**Expected Result:** Complex objects render with all tiles present
|
||||
|
||||
### Priority 2: Add Tile Transformation Support
|
||||
|
||||
**File:** `src/zelda3/dungeon/object_drawer.cc`
|
||||
|
||||
1. Add `h_flip` and `v_flip` parameters to `WriteTile8()`
|
||||
2. Implement pixel coordinate transformation
|
||||
3. Pass transformation flags from draw routines
|
||||
|
||||
**Expected Result:** Symmetrical objects render correctly
|
||||
|
||||
### Priority 3: Implement Object-Specific Draw Instructions
|
||||
|
||||
**File:** `src/zelda3/dungeon/object_drawer.cc`
|
||||
|
||||
Consider refactoring to support ZScream-style DrawInfo:
|
||||
|
||||
```cpp
|
||||
struct DrawInstruction {
|
||||
int tile_index;
|
||||
int x_offset;
|
||||
int y_offset;
|
||||
bool h_flip;
|
||||
bool v_flip;
|
||||
};
|
||||
|
||||
void ObjectDrawer::DrawFromInstructions(
|
||||
const RoomObject& obj,
|
||||
gfx::BackgroundBuffer& bg,
|
||||
std::span<const gfx::TileInfo> tiles,
|
||||
const std::vector<DrawInstruction>& instructions) {
|
||||
|
||||
for (const auto& inst : instructions) {
|
||||
if (inst.tile_index >= tiles.size()) continue;
|
||||
WriteTile8(bg, obj.x_ + (inst.x_offset / 8), obj.y_ + (inst.y_offset / 8),
|
||||
tiles[inst.tile_index], inst.h_flip, inst.v_flip);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Expected Result:** Ability to replicate ZScream's complex object rendering exactly
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### Test Cases
|
||||
|
||||
1. **Simple Objects (0x00-0x08)**: Walls, ceilings - should work with current code
|
||||
2. **Medium Objects (0x33)**: 16-tile carpet - currently broken due to tile count
|
||||
3. **Complex Objects (0x22D, 0x22E)**: Agahnim's altar/room - broken, needs transformations
|
||||
4. **Special Objects (0xC1, 0x215)**: Large platforms - broken due to tile count
|
||||
|
||||
### Verification Method
|
||||
|
||||
Compare rendered output with:
|
||||
1. ZScream's dungeon editor rendering
|
||||
2. In-game ALTTP screenshots
|
||||
3. ZSNES/bsnes emulator tile viewers
|
||||
|
||||
---
|
||||
|
||||
## 8. Reference: ZScream Object Tile Counts (Partial List)
|
||||
|
||||
```
|
||||
0x000: 4 | 0x001: 8 | 0x002: 8 | 0x003: 8
|
||||
0x033: 16 | 0x036: 16 | 0x037: 16 | 0x03A: 12
|
||||
0x0C1: 68 | 0x0CD: 28 | 0x0CE: 28 | 0x0DC: 21
|
||||
0x100-0x13F: 16 (all subtype2 corners use 16 tiles)
|
||||
0x200: 12 | 0x201: 20 | 0x202: 28 | 0x214: 12
|
||||
0x215: 80 | 0x22D: 84 | 0x22E: 127 | 0x262: 242
|
||||
```
|
||||
|
||||
Full list available in ZScream's `RoomObjectTileLister.cs:23-534`.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The primary issue causing "some objects don't look right" is yaze's **hardcoded 8-tile limit** per object. ZScream loads object-specific tile counts ranging from 1 to 242 tiles, while yaze loads a fixed 8 tiles regardless of object type. This causes complex objects (carpets, chests, altars) to render with incomplete graphics.
|
||||
|
||||
Secondary issues include:
|
||||
- Missing tile transformation support (h_flip, v_flip)
|
||||
- Pattern-based drawing doesn't match ROM data for complex objects
|
||||
|
||||
**Immediate Action:** Implement the tile count lookup table from ZScream (Priority 1 fix above) to restore correct rendering for most objects.
|
||||
Reference in New Issue
Block a user