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