Files
yaze/docs/internal/archive/investigations/duplicate-rendering-investigation-complete.md

457 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`