backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

View File

@@ -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`