255 lines
8.7 KiB
Markdown
255 lines
8.7 KiB
Markdown
|
|
# 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<Vertex>& vertices,
|
||
|
|
VkBuffer& buffer, VkDeviceMemory& memory);
|
||
|
|
bool createDynamicIndexBuffer(const std::vector<uint16_t>& 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
|