# Bug Fixes Documentation ## Overview This document describes the bugs found in the ScreenLockDetector application and the fixes applied to resolve them. --- ## Bug #1: Synchronous Buffer Copy During Active Rendering ### Symptoms ``` VulkanRenderer Error: Failed to wait for queue idle after copy VulkanRenderer Error: Failed to copy staging buffer to vertex buffer Segmentation fault (core dumped) ``` ### Root Cause The application was creating vertex and index buffers during the `recordCommandBuffer()` function, which is called every frame during active rendering. The buffer creation process involved: 1. Creating a staging buffer (host-visible memory) 2. Copying data to the staging buffer 3. Creating a device-local buffer 4. **Synchronously copying from staging to device-local buffer** 5. Waiting for the copy operation to complete The synchronous copy operation in step 4 was attempting to: - Submit a separate command buffer to the graphics queue - Wait for the graphics queue to become idle using `vkQueueWaitIdle()` or wait for a fence However, this created a **deadlock/contention issue** because: - The graphics queue was already busy recording commands for the current frame - Waiting for the queue to be idle while in the middle of recording commands is contradictory - This caused either a timeout or a failure to synchronize ### Failed Attempted Fixes #### Attempt 1: Return false on vkQueueWaitIdle failure ```cpp // Added proper error handling if (result != VK_SUCCESS) { logError("Failed to wait for queue idle after copy"); vkFreeCommandBuffers(m_device, m_transferCommandPool, 1, &commandBuffer); return false; // Previously returned true! } ``` **Result**: Exposed the error but didn't fix the root cause. #### Attempt 2: Use fence instead of vkQueueWaitIdle ```cpp VkFence fence; result = vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, fence); result = vkWaitForFences(m_device, 1, &fence, VK_TRUE, UINT64_MAX); ``` **Result**: Still failed because waiting for the fence had the same synchronization issue. #### Attempt 3: Add vkDeviceWaitIdle before copy ```cpp VkResult idleResult = vkDeviceWaitIdle(m_device); ``` **Result**: Created immediate deadlock - can't wait for device idle while recording commands. ### Final Solution: Use Host-Visible Buffers for Dynamic Data The proper fix was to recognize that **dynamic geometry** (created every frame) should use a different buffer strategy than **static geometry** (created once during initialization). #### Implementation **Created new methods for dynamic buffers:** ```cpp bool createDynamicVertexBuffer(const std::vector& vertices, VkBuffer& buffer, VkDeviceMemory& memory); bool createDynamicIndexBuffer(const std::vector& indices, VkBuffer& buffer, VkDeviceMemory& memory); ``` **Key differences from original methods:** 1. **No staging buffer** - data is written directly to the buffer 2. **Host-visible memory** - uses `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` 3. **No synchronous copy** - no need to submit commands and wait 4. **Immediate availability** - data is available as soon as `vkUnmapMemory()` is called **Modified code in recordCommandBuffer():** ```cpp // OLD (caused deadlock): if (!createVertexBuffer(circleVertices, m_circleVertexBuffer, m_circleVertexMemory)) { return; } // NEW (no synchronization issues): if (!createDynamicVertexBuffer(circleVertices, m_circleVertexBuffer, m_circleVertexMemory)) { return; } ``` **Applied to:** - Circle vertex/index buffers (animated rotating circles) - Wave vertex/index buffers (animated wave effect) - Text vertex/index buffers (rendered every frame) **Kept original methods for:** - Background vertex/index buffers (static, created during initialization) - Any other static geometry created before rendering starts ### Performance Considerations **Trade-off:** - **Host-visible memory** is slower to access by the GPU than device-local memory - However, for geometry that's **recreated every frame anyway**, this overhead is negligible - The benefit of avoiding synchronization issues far outweighs the minimal performance cost **Best practices:** - Use device-local buffers for static data (created once, used many times) - Use host-visible buffers for dynamic data (created every frame) - For data updated occasionally, consider double-buffering with device-local memory --- ## Bug #2: Incorrect Return Value on Buffer Copy Failure ### Symptoms The original code continued execution even when `vkQueueWaitIdle()` failed, leading to undefined behavior. ### Original Code ```cpp result = vkQueueWaitIdle(m_graphicsQueue); if (result != VK_SUCCESS) { logError("Failed to wait for queue idle after copy"); } // Missing: return false; vkFreeCommandBuffers(m_device, m_transferCommandPool, 1, &commandBuffer); return true; // Always returned true! ``` ### Fix ```cpp result = vkQueueWaitIdle(m_graphicsQueue); if (result != VK_SUCCESS) { logError("Failed to wait for queue idle after copy"); vkFreeCommandBuffers(m_device, m_transferCommandPool, 1, &commandBuffer); return false; // Properly propagate error } ``` ### Impact This fix ensured that buffer copy failures were properly detected and handled, preventing the use of invalid/incomplete buffers. --- ## Bug #3: Frame Count Confusion ### Symptoms ``` CustomWidget destroyed, total frames painted: 0 ``` ### Explanation This is **not actually a bug**, but rather expected behavior: 1. The application has two rendering modes: - **VulkanWidget**: Uses Vulkan for rendering (primary mode) - **CustomWidget**: Uses QPainter for rendering (alternative mode) 2. Each widget maintains its own frame counter: - `VulkanWidget::m_frameCount` - tracks Vulkan frames - `CustomWidget::m_frameCount` - tracks QPainter frames 3. When running in Vulkan mode (default), the VulkanWidget tab is active: - VulkanWidget renders frames and increments its counter - CustomWidget is not active and doesn't render, so its counter stays at 0 4. On application exit, the destructor shows: - `CustomWidget destroyed, total frames painted: 0` ✓ Correct (wasn't used) - VulkanWidget frames are shown in the status display during runtime ### Verification The logs show: ``` Drawing text with 572 vertices, 858 indices <- Vulkan is rendering VulkanWidget::m_frameCount++ <- Vulkan counter incrementing CustomWidget destroyed, total frames painted: 0 <- CustomWidget wasn't used ``` This is **working as designed**. --- ## Summary of Changes ### Files Modified 1. **src/vulkanrenderer.h** - Added `createDynamicVertexBuffer()` method declaration - Added `createDynamicIndexBuffer()` method declaration - Updated documentation to clarify usage 2. **src/vulkanrenderer.cpp** - Implemented `createDynamicVertexBuffer()` using host-visible memory - Implemented `createDynamicIndexBuffer()` using host-visible memory - Modified `recordCommandBuffer()` to use dynamic buffer methods for animated geometry - Modified `drawText()` to use dynamic buffer methods for text rendering - Fixed `copyBuffer()` to properly return false on synchronization failures - Added `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` flag to command pool ### Testing Results **Before fixes:** - Application crashed with segmentation fault - "Failed to wait for queue idle" errors repeated 30+ times - Rendering failed to display properly **After fixes:** - Application runs without errors - No segmentation faults - Smooth rendering with animated circles and waves - Text rendering works correctly - No synchronization warnings or errors --- ## Lessons Learned 1. **Never perform synchronous operations during command buffer recording** - Command buffers should be recorded without blocking operations - Queue submissions should happen after recording is complete 2. **Choose appropriate memory types for buffer usage patterns** - Static data: Device-local memory with one-time staging copy - Dynamic data (per-frame): Host-visible memory with direct writes - Occasionally updated: Double-buffering or triple-buffering 3. **Proper error propagation is critical** - Always check Vulkan function return values - Propagate errors up the call stack with appropriate returns - Clean up resources before returning on error 4. **Understand the rendering lifecycle** - Initialization phase: Can use synchronous operations - Rendering phase: Must be asynchronous and non-blocking - Shutdown phase: Can use synchronous cleanup --- ## References - Vulkan Specification: Memory Types and Properties - Vulkan Best Practices: Buffer Usage Patterns - Vulkan Tutorial: Buffer Management - Real-Time Rendering Best Practices --- **Date**: 2025-11-10 **Version**: 1.0 **Status**: Resolved