Skip to content

feat(render): surface unavailable GPU timestamp diagnostics#128

Merged
K1ngst0m merged 4 commits intomainfrom
dev/gpu-timestamp-test-cov
Mar 12, 2026
Merged

feat(render): surface unavailable GPU timestamp diagnostics#128
K1ngst0m merged 4 commits intomainfrom
dev/gpu-timestamp-test-cov

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Mar 12, 2026

  • Emit deterministic Tier 1 diagnostics when GPU timestamps are unavailable
  • Preserve GPU timing and debug-label evidence when timestamps are supported
  • Keep frame recording and profiling flows safe when timestamps are disabled
  • Add profiling tests for unavailable, available, and non-blocking readback paths
  • Archive the profiling OpenSpec proposal, design, tasks, and verification

Summary by CodeRabbit

  • New Features

    • Added diagnostics policy to force or auto-detect GPU timestamp availability and a deterministic no-op unavailable timestamp pool for testing; propagated graphics queue family index to runtime.
  • Tests

    • Added Vulkan-backed GPU timestamp tests and expanded runtime diagnostic tests for available/forced-unavailable paths, non-blocking readback, and debug-label capture.
  • Refactor

    • Replaced manual debug-label begin/end with RAII scoped labels and standardized initializer usage.
  • Documentation

    • Added design, proposal, tasks, verification report, and archive state for the work.

- Emit deterministic Tier 1 diagnostics when GPU timestamps are unavailable
- Preserve GPU timing and debug-label evidence when timestamps are supported
- Keep frame recording and profiling flows safe when timestamps are disabled
- Add profiling tests for unavailable, available, and non-blocking readback paths
- Archive the profiling OpenSpec proposal, design, tasks, and verification
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Surface unavailable GPU timestamp diagnostics with deterministic testability seam

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Add deterministic GPU timestamp unavailability testability seam via DiagnosticPolicy override
• Introduce explicit GpuTimestampPool::create_unavailable() factory for forced-unavailable testing
• Extract debug-label dispatch handling into ScopedDebugLabel RAII helper for safer profiling
• Expand profiling test coverage with deterministic unavailable-path and GPU-timing evidence
  assertions
• Archive profiling OpenSpec proposal, design, tasks, and verification artifacts
Diagram
flowchart LR
  DP["DiagnosticPolicy<br/>gpu_timestamp_availability"]
  CRT["ChainRuntime<br/>sync_gpu_timestamp_pool"]
  GTP["GpuTimestampPool<br/>create_unavailable"]
  DLS["ScopedDebugLabel<br/>RAII wrapper"]
  CE["ChainExecutor<br/>record"]
  Tests["Profiling Tests<br/>deterministic coverage"]
  
  DP -- "force_unavailable" --> CRT
  CRT -- "selects path" --> GTP
  GTP -- "no-op pool" --> CE
  DLS -- "wraps labels" --> CE
  CRT -- "emits event" --> Tests
  GTP -- "unit tests" --> Tests
  DLS -- "dispatch tests" --> Tests
Loading

Grey Divider

File Changes

1. src/util/diagnostics/diagnostic_policy.hpp ✨ Enhancement +4/-0

Add GPU timestamp availability mode override enum

src/util/diagnostics/diagnostic_policy.hpp


2. src/util/diagnostics/gpu_timestamp_pool.hpp ✨ Enhancement +3/-0

Declare explicit unavailable pool construction factory

src/util/diagnostics/gpu_timestamp_pool.hpp


3. src/util/diagnostics/gpu_timestamp_pool.cpp ✨ Enhancement +7/-3

Implement unavailable pool factory and delegation

src/util/diagnostics/gpu_timestamp_pool.cpp


View more (18)
4. src/render/chain/debug_label_scope.hpp ✨ Enhancement +53/-0

New RAII helper for Vulkan debug label dispatch

src/render/chain/debug_label_scope.hpp


5. src/render/chain/chain_executor.cpp ✨ Enhancement +17/-30

Replace manual debug labels with ScopedDebugLabel RAII

src/render/chain/chain_executor.cpp


6. src/render/chain/chain_runtime.cpp ✨ Enhancement +16/-10

Route timestamp availability through policy override

src/render/chain/chain_runtime.cpp


7. tests/render/test_gpu_timestamp_pool.cpp 🧪 Tests +490/-0

Add comprehensive GPU timestamp pool unit tests

tests/render/test_gpu_timestamp_pool.cpp


8. tests/render/test_runtime_diagnostics.cpp 🧪 Tests +430/-0

Add deterministic profiling and debug-label runtime tests

tests/render/test_runtime_diagnostics.cpp


9. tests/render/test_diagnostic_event_model.cpp 🧪 Tests +1/-0

Update policy defaults test for new enum field

tests/render/test_diagnostic_event_model.cpp


10. tests/render/test_diagnostic_reporting.cpp 🧪 Tests +15/-4

Refactor aggregate initialization to explicit assignment

tests/render/test_diagnostic_reporting.cpp


11. tests/visual/runtime_capture_main.cpp 🧪 Tests +7/-6

Refactor aggregate initialization to explicit assignment

tests/visual/runtime_capture_main.cpp


12. tests/visual/test_image_compare.cpp 🧪 Tests +8/-5

Refactor aggregate initialization to explicit assignment

tests/visual/test_image_compare.cpp


13. tests/visual/test_intermediate_golden.cpp 🧪 Tests +7/-6

Refactor aggregate initialization to explicit assignment

tests/visual/test_intermediate_golden.cpp


14. tests/visual/test_semantic_probes.cpp 🧪 Tests +20/-20

Refactor aggregate initialization to explicit assignment

tests/visual/test_semantic_probes.cpp


15. tests/visual/test_temporal_golden.cpp 🧪 Tests +7/-6

Refactor aggregate initialization to explicit assignment

tests/visual/test_temporal_golden.cpp


16. tests/CMakeLists.txt ⚙️ Configuration changes +1/-0

Register new GPU timestamp pool test source

tests/CMakeLists.txt


17. openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md 📝 Documentation +84/-0

Archive profiling GPU timestamp proposal document

openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md


18. openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/design.md 📝 Documentation +153/-0

Archive profiling GPU timestamp design document

openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/design.md


19. openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/tasks.md 📝 Documentation +52/-0

Archive profiling GPU timestamp tasks document

openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/tasks.md


20. openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/state.yaml 📝 Documentation +13/-0

Archive profiling GPU timestamp change state metadata

openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/state.yaml


21. openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/verify-report.md 📝 Documentation +90/-0

Archive profiling GPU timestamp verification report

openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/verify-report.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 12, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Remediation recommended

1. Readback test skips submit🐞 Bug ⛯ Reliability
Description
The new "non-blocking readback" test calls GpuTimestampPool::read_results(0) after recording
timestamps into a command buffer that is never submitted, so the test does not exercise the intended
eNotReady asynchronous readback path. This can make the assertion meaningless and potentially flaky
across Vulkan implementations/drivers.
Code

tests/render/test_gpu_timestamp_pool.cpp[R437-459]

+    REQUIRE(vkBeginCommandBuffer(first_frame, &begin_info) == VK_SUCCESS);
+    pool->reset_frame(vk::CommandBuffer{first_frame}, 0u);
+    pool->write_prechain_timestamp(vk::CommandBuffer{first_frame}, 0u, true);
+    transition_image(first_frame, first_source->image,
+                     {.old_layout = VK_IMAGE_LAYOUT_UNDEFINED,
+                      .new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL});
+    pool->write_prechain_timestamp(vk::CommandBuffer{first_frame}, 0u, false);
+    pool->write_pass_timestamp(vk::CommandBuffer{first_frame}, 0u, 0u, true);
+    transition_image(first_frame, first_target->image,
+                     {.old_layout = VK_IMAGE_LAYOUT_UNDEFINED,
+                      .new_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL});
+    pool->write_pass_timestamp(vk::CommandBuffer{first_frame}, 0u, 0u, false);
+    pool->write_final_composition_timestamp(vk::CommandBuffer{first_frame}, 0u, true);
+    transition_image(first_frame, first_target->image,
+                     {.old_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
+                      .new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL});
+    pool->write_final_composition_timestamp(vk::CommandBuffer{first_frame}, 0u, false);
+    REQUIRE(vkEndCommandBuffer(first_frame) == VK_SUCCESS);
+
+    auto pending_results = pool->read_results(0u);
+    REQUIRE(pending_results);
+    CHECK(pending_results->empty());
+
Evidence
The test records commands into first_frame and immediately calls read_results(0) without any queue
submission of first_frame, so emptiness may simply be because no GPU work ever executed. Meanwhile,
the production implementation returns empty specifically on vk::Result::eNotReady, which the test is
trying (but failing) to validate.

tests/render/test_gpu_timestamp_pool.cpp[437-487]
src/util/diagnostics/gpu_timestamp_pool.cpp[101-109]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GpuTimestampPool readback stays non-blocking while the next frame records` currently calls `read_results(0)` after recording `first_frame` but never submits `first_frame`. This means the test does not validate the intended async `vk::Result::eNotReady` path (it may be empty simply because the GPU never executed the queries), and it never verifies that frame 0 results become available after completion.
### Issue Context
`GpuTimestampPool::read_results()` returns an empty vector for `vk::Result::eNotReady`, which is the behavior the test should be proving for an in-flight frame.
### Fix Focus Areas
- tests/render/test_gpu_timestamp_pool.cpp[437-487]
- src/util/diagnostics/gpu_timestamp_pool.cpp[101-109]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

Adds a test-only seam to deterministically force GPU timestamp unavailability: new GpuTimestampAvailabilityMode policy and DiagnosticPolicy::gpu_timestamp_availability, GpuTimestampPool::create_unavailable(), runtime wiring in ChainRuntime::sync_gpu_timestamp_pool(), RAII ScopedDebugLabel, VulkanContext queue-family propagation, and tests/docs updates.

Changes

Cohort / File(s) Summary
Diagnostics Policy & Timestamp Pool
src/util/diagnostics/diagnostic_policy.hpp, src/util/diagnostics/gpu_timestamp_pool.hpp, src/util/diagnostics/gpu_timestamp_pool.cpp
Add GpuTimestampAvailabilityMode and DiagnosticPolicy::gpu_timestamp_availability; introduce GpuTimestampPoolCreateInfo, supports_timestamps(), change create() to accept CreateInfo, and add create_unavailable() (explicit no-op pool).
Runtime Integration
src/render/chain/chain_runtime.cpp
sync_gpu_timestamp_pool() consults diagnostic policy and uses create_unavailable() when force_unavailable; otherwise builds create(create_info). Pools are managed via a unique_ptr and previous event/error behavior is preserved.
Debug Label Utilities & Executor
src/render/chain/debug_label_scope.hpp, src/render/chain/chain_executor.cpp
Add DebugLabelDispatch and RAII ScopedDebugLabel; refactor chain_executor to use scoped labels instead of manual begin/end helpers.
Vulkan Context Propagation
src/render/backend/vulkan_context.cpp, src/render/chain/vulkan_context.hpp, src/render/chain/api/c/goggles_filter_chain.cpp, tests/visual/runtime_capture.cpp
Add graphics_queue_family_index to VulkanContext and propagate it through runtime creation to support timestamp pool creation and detection.
Tests — GPU Timestamp & Diagnostics
tests/render/test_gpu_timestamp_pool.cpp, tests/render/test_runtime_diagnostics.cpp, tests/render/test_diagnostic_event_model.cpp, tests/render/test_diagnostic_reporting.cpp
Add Vulkan-backed timestamp pool tests (including forced-unavailable deterministic tests), extend runtime diagnostic and debug-label capture tests, and update diagnostics default expectations and some test initializer styles.
Test Registration & CMake
tests/CMakeLists.txt
Register new GPU timestamp pool test (tests/render/test_gpu_timestamp_pool.cpp).
Visual Test Refactors
tests/visual/...
tests/visual/test_image_compare.cpp, tests/visual/runtime_capture_main.cpp, tests/visual/test_intermediate_golden.cpp, tests/visual/test_semantic_probes.cpp, tests/visual/test_temporal_golden.cpp
Replace inline aggregate initializers with explicit RuntimeCapturePlan / temporary object construction; semantics unchanged.
Spec & Archive Docs
openspec/changes/archive/.../{design.md,proposal.md,tasks.md,verify-report.md,state.yaml}
Add design, proposal, tasks, verification report, and archive state files documenting the seam, implementation plan, and test coverage.

Sequence Diagram

sequenceDiagram
    participant Test as Test
    participant ChainRuntime as ChainRuntime
    participant Policy as DiagnosticPolicy
    participant Pool as GpuTimestampPool

    Test->>ChainRuntime: sync_gpu_timestamp_pool()
    ChainRuntime->>Policy: query gpu_timestamp_availability
    alt force_unavailable
        Policy-->>ChainRuntime: force_unavailable
        ChainRuntime->>Pool: create_unavailable()
        Pool-->>ChainRuntime: unavailable pool
        ChainRuntime->>ChainRuntime: mark timestamps_inactive, emit unavailable event
    else auto_detect
        Policy-->>ChainRuntime: auto_detect
        ChainRuntime->>Pool: create(create_info)
        Pool-->>ChainRuntime: available / unavailable
        alt available
            ChainRuntime->>ChainRuntime: proceed with timestamp recording
        else unavailable
            ChainRuntime->>ChainRuntime: emit unavailable event, use no-op behavior
        end
    end
    ChainRuntime-->>Test: pool ready (available or unavailable)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #127: Directly overlaps diagnostics/GpuTimestampPool and ChainRuntime policy wiring (gpu timestamp availability mode and create_unavailable()).
  • #2: Modifies VulkanContext shape and runtime initialization; relates to added graphics_queue_family_index propagation.
  • #126: Overlaps ChainRuntime/ChainExecutor area; relevant to sync_gpu_timestamp_pool() and executor refactor.

Suggested labels

Review effort 4/5

Poem

🐰 I nudge the pool to hide or play,
One path records, one slips away.
Scoped labels curl, then softly end,
Tests hop through both—my eager friend.
A carrot for seams that help us stay! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: introducing a seam to deterministically emit diagnostics when GPU timestamps are unavailable, which aligns with the PR's core objective and the changes across diagnostic policy, pool construction, and runtime wiring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev/gpu-timestamp-test-cov

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/render/chain/chain_runtime.cpp (1)

417-436: ⚠️ Potential issue | 🟡 Minor

Differentiate policy-disabled timestamps from device unavailability.

When gpu_timestamp_availability == force_unavailable, Lines 434-436 still emit "GPU timestamps are unavailable on this device". That makes the deterministic test/policy path report a hardware limitation, which muddies the diagnostic evidence.

Suggested fix
     std::unique_ptr<diagnostics::GpuTimestampPool> pool;
+    const bool forced_unavailable =
+        m_diagnostic_session->policy().gpu_timestamp_availability ==
+        diagnostics::GpuTimestampAvailabilityMode::force_unavailable;
-    if (m_diagnostic_session->policy().gpu_timestamp_availability ==
-        diagnostics::GpuTimestampAvailabilityMode::force_unavailable) {
+    if (forced_unavailable) {
         pool = diagnostics::GpuTimestampPool::create_unavailable();
     } else {
         auto pool_result = diagnostics::GpuTimestampPool::create(
             m_resources->m_vk_ctx.device, m_resources->m_vk_ctx.physical_device,
             static_cast<uint32_t>(std::max<size_t>(m_resources->pass_count(), 1U)),
             m_resources->m_num_sync_indices);
         if (!pool_result) {
             emit_timestamp_event(m_diagnostic_session.get(), diagnostics::Severity::warning,
                                  "Failed to enable GPU timestamps: " + pool_result.error().message);
             return;
         }

         pool = std::move(*pool_result);
     }

     if (!pool->is_available()) {
-        emit_timestamp_event(m_diagnostic_session.get(), diagnostics::Severity::info,
-                             "GPU timestamps are unavailable on this device");
+        emit_timestamp_event(
+            m_diagnostic_session.get(), diagnostics::Severity::info,
+            forced_unavailable
+                ? "GPU timestamps are disabled by diagnostic policy"
+                : "GPU timestamps are unavailable on this device");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/render/chain/chain_runtime.cpp` around lines 417 - 436, The code
currently treats policy-forced unavailability the same as hardware
unavailability and emits "GPU timestamps are unavailable on this device"; change
the logic so that when m_diagnostic_session->policy().gpu_timestamp_availability
== diagnostics::GpuTimestampAvailabilityMode::force_unavailable (after creating
diagnostics::GpuTimestampPool::create_unavailable()), you either emit a distinct
diagnostic (e.g., "GPU timestamps disabled by policy") or suppress the
hardware-unavailable message; update the check around pool->is_available() to
inspect the policy (or a local flag set when calling
GpuTimestampPool::create_unavailable()) and call emit_timestamp_event
accordingly (reference m_diagnostic_session, policy(),
gpu_timestamp_availability, GpuTimestampAvailabilityMode::force_unavailable,
diagnostics::GpuTimestampPool::create_unavailable, emit_timestamp_event, and
pool->is_available()).
🧹 Nitpick comments (2)
openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md (1)

36-42: Update the affected-areas table to include the extracted debug-label helper.

Lines 36-42 no longer match the implemented file set: this change also ships src/render/chain/debug_label_scope.hpp. Leaving it out makes the archived proposal under-report the production delta and drift from the rest of the archive set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md`
around lines 36 - 42, The affected-areas table omission: add an entry for the
newly shipped header by updating the table to include
`src/render/chain/debug_label_scope.hpp` with a brief note like "Modified | Add
extracted debug-label helper used by profiling/debug paths" so the archived
proposal accurately reflects the production delta; reference the table rows that
list other files (`gpu_timestamp_pool.hpp`, `gpu_timestamp_pool.cpp`,
`chain_executor.cpp`, tests/*`) and insert the new row for
`debug_label_scope.hpp` adjacent to the other `src/render/chain` entries to keep
ordering consistent.
tests/render/test_gpu_timestamp_pool.cpp (1)

12-124: Consider extracting the shared Vulkan test harness.

This fixture plus the image/command helper block is now largely duplicated from tests/render/test_runtime_diagnostics.cpp. Pulling it into a small render-test utility would keep future Vulkan setup fixes from drifting between the two profiling suites.

Also applies to: 126-302

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/render/test_gpu_timestamp_pool.cpp` around lines 12 - 124, The
VulkanRuntimeFixture and the duplicated image/command helper should be extracted
into a shared test utility: create a new header (e.g., render_test_utils.hpp)
that defines VulkanRuntimeFixture (including its constructor, destructor,
available(), timestamp_period()) and the image/command helper functions used in
both tests, move the duplicated setup/teardown and helper code there, and update
tests test_gpu_timestamp_pool.cpp and test_runtime_diagnostics.cpp to include
that header and use the shared VulkanRuntimeFixture and helpers instead of their
local copies; ensure symbol names (VulkanRuntimeFixture, available(),
timestamp_period(), and the image/command helper functions) remain unchanged so
callers need only switch the include.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/render/chain/debug_label_scope.hpp`:
- Around line 20-31: The constructor of ScopedDebugLabel passes name.data() to
VkDebugUtilsLabelEXT::pLabelName but std::string_view may not be
null-terminated; fix by making a null-terminated std::string copy of the view
(e.g. local or a member like m_label_name) and pass m_label_name.c_str() to
pLabelName so the string remains valid for the duration of
dispatch.begin/dispatch.end calls; update the ScopedDebugLabel constructor and
any places using pLabelName to use this c_str() instead of name.data().

In `@tests/render/test_gpu_timestamp_pool.cpp`:
- Around line 108-115: The timestamp_period() implementation is using
properties.limits.timestampPeriod as a support check which is incorrect; update
VulkanRuntimeFixture::timestamp_period() to detect timestamp support by querying
VkQueueFamilyProperties for the selected queue family and checking
timestampValidBits (>0) and/or falling back to
VkPhysicalDeviceProperties.limits.timestampComputeAndGraphics (true) instead of
using timestampPeriod; then change the test skip gates (the checks at lines
referenced in tests/render/test_gpu_timestamp_pool.cpp and
tests/render/test_runtime_diagnostics.cpp) and GpuTimestampPool::create() to use
the same supported-flag logic (timestampValidBits on the chosen queue family or
timestampComputeAndGraphics) to decide availability, while still using
timestampPeriod only for the nanoseconds-per-tick conversion when timestamps are
supported.

---

Outside diff comments:
In `@src/render/chain/chain_runtime.cpp`:
- Around line 417-436: The code currently treats policy-forced unavailability
the same as hardware unavailability and emits "GPU timestamps are unavailable on
this device"; change the logic so that when
m_diagnostic_session->policy().gpu_timestamp_availability ==
diagnostics::GpuTimestampAvailabilityMode::force_unavailable (after creating
diagnostics::GpuTimestampPool::create_unavailable()), you either emit a distinct
diagnostic (e.g., "GPU timestamps disabled by policy") or suppress the
hardware-unavailable message; update the check around pool->is_available() to
inspect the policy (or a local flag set when calling
GpuTimestampPool::create_unavailable()) and call emit_timestamp_event
accordingly (reference m_diagnostic_session, policy(),
gpu_timestamp_availability, GpuTimestampAvailabilityMode::force_unavailable,
diagnostics::GpuTimestampPool::create_unavailable, emit_timestamp_event, and
pool->is_available()).

---

Nitpick comments:
In
`@openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md`:
- Around line 36-42: The affected-areas table omission: add an entry for the
newly shipped header by updating the table to include
`src/render/chain/debug_label_scope.hpp` with a brief note like "Modified | Add
extracted debug-label helper used by profiling/debug paths" so the archived
proposal accurately reflects the production delta; reference the table rows that
list other files (`gpu_timestamp_pool.hpp`, `gpu_timestamp_pool.cpp`,
`chain_executor.cpp`, tests/*`) and insert the new row for
`debug_label_scope.hpp` adjacent to the other `src/render/chain` entries to keep
ordering consistent.

In `@tests/render/test_gpu_timestamp_pool.cpp`:
- Around line 12-124: The VulkanRuntimeFixture and the duplicated image/command
helper should be extracted into a shared test utility: create a new header
(e.g., render_test_utils.hpp) that defines VulkanRuntimeFixture (including its
constructor, destructor, available(), timestamp_period()) and the image/command
helper functions used in both tests, move the duplicated setup/teardown and
helper code there, and update tests test_gpu_timestamp_pool.cpp and
test_runtime_diagnostics.cpp to include that header and use the shared
VulkanRuntimeFixture and helpers instead of their local copies; ensure symbol
names (VulkanRuntimeFixture, available(), timestamp_period(), and the
image/command helper functions) remain unchanged so callers need only switch the
include.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d9156350-a817-417a-afb9-ef3810d9d880

📥 Commits

Reviewing files that changed from the base of the PR and between 408512c and 12931ef.

📒 Files selected for processing (21)
  • openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/design.md
  • openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/proposal.md
  • openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/state.yaml
  • openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/tasks.md
  • openspec/changes/archive/2026-03-11-profiling-gpu-timestamp-test-coverage/verify-report.md
  • src/render/chain/chain_executor.cpp
  • src/render/chain/chain_runtime.cpp
  • src/render/chain/debug_label_scope.hpp
  • src/util/diagnostics/diagnostic_policy.hpp
  • src/util/diagnostics/gpu_timestamp_pool.cpp
  • src/util/diagnostics/gpu_timestamp_pool.hpp
  • tests/CMakeLists.txt
  • tests/render/test_diagnostic_event_model.cpp
  • tests/render/test_diagnostic_reporting.cpp
  • tests/render/test_gpu_timestamp_pool.cpp
  • tests/render/test_runtime_diagnostics.cpp
  • tests/visual/runtime_capture_main.cpp
  • tests/visual/test_image_compare.cpp
  • tests/visual/test_intermediate_golden.cpp
  • tests/visual/test_semantic_probes.cpp
  • tests/visual/test_temporal_golden.cpp

Comment on lines +20 to +31
ScopedDebugLabel(vk::CommandBuffer cmd, std::string_view name,
const std::array<float, 4>& color, DebugLabelDispatch dispatch = {})
: m_command_buffer(cmd) {
if (!dispatch.is_available()) {
return;
}

VkDebugUtilsLabelEXT label{};
label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
label.pLabelName = name.data();
std::copy(color.begin(), color.end(), label.color);
dispatch.begin(static_cast<VkCommandBuffer>(cmd), &label);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What does the Vulkan specification require for VkDebugUtilsLabelEXT::pLabelNameinvkCmdBeginDebugUtilsLabelEXT? Does it need to point to a null-terminated UTF-8 string, and is std::string_view::data() sufficient for that contract?

💡 Result:

The Vulkan spec (via the reference pages / registry metadata) requires VkDebugUtilsLabelEXT::pLabelName to point to a null-terminated UTF-8 string. In particular, the VkDebugUtilsLabelEXT description states that pLabelName is “a pointer to a null-terminated UTF-8 string containing the name of the label” [1], and the Vulkan registry XML marks pLabelName as len="null-terminated" [2]. vkCmdBeginDebugUtilsLabelEXT in turn requires pLabelInfo to point to a valid VkDebugUtilsLabelEXT [3], so that null-termination requirement applies when recording the command.

std::string_view::data() is not sufficient by itself for that contract, because string_view does not guarantee a trailing '\0' at data()[size()]. It’s only safe if the view is over a buffer that you know is null-terminated at exactly the end of the view (e.g., a view of a whole std::string, and you ensure view.size() == strlen(view.data())). Otherwise, use something that guarantees null-termination such as std::string::c_str() (or materialize a std::string/null-terminated buffer before the call).


🏁 Script executed:

cat -n src/render/chain/debug_label_scope.hpp

Repository: goggles-dev/Goggles

Length of output: 2118


Don't pass std::string_view::data() directly to Vulkan here.

Line 29 hands name.data() to VkDebugUtilsLabelEXT::pLabelName, but std::string_view does not guarantee a trailing '\0'. The Vulkan specification requires pLabelName to point to a null-terminated UTF-8 string. For sliced or non-literal views, passing a non-null-terminated pointer can cause invalid label recording or out-of-bounds reads in the driver.

Suggested fix
+#include <string>
+
 ScopedDebugLabel(vk::CommandBuffer cmd, std::string_view name,
                  const std::array<float, 4>& color, DebugLabelDispatch dispatch = {})
     : m_command_buffer(cmd) {
     if (!dispatch.is_available()) {
         return;
     }
 
+    const std::string label_name{name};
     VkDebugUtilsLabelEXT label{};
     label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
-    label.pLabelName = name.data();
+    label.pLabelName = label_name.c_str();
     std::copy(color.begin(), color.end(), label.color);
     dispatch.begin(static_cast<VkCommandBuffer>(cmd), &label);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ScopedDebugLabel(vk::CommandBuffer cmd, std::string_view name,
const std::array<float, 4>& color, DebugLabelDispatch dispatch = {})
: m_command_buffer(cmd) {
if (!dispatch.is_available()) {
return;
}
VkDebugUtilsLabelEXT label{};
label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
label.pLabelName = name.data();
std::copy(color.begin(), color.end(), label.color);
dispatch.begin(static_cast<VkCommandBuffer>(cmd), &label);
`#include` <string>
ScopedDebugLabel(vk::CommandBuffer cmd, std::string_view name,
const std::array<float, 4>& color, DebugLabelDispatch dispatch = {})
: m_command_buffer(cmd) {
if (!dispatch.is_available()) {
return;
}
const std::string label_name{name};
VkDebugUtilsLabelEXT label{};
label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
label.pLabelName = label_name.c_str();
std::copy(color.begin(), color.end(), label.color);
dispatch.begin(static_cast<VkCommandBuffer>(cmd), &label);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/render/chain/debug_label_scope.hpp` around lines 20 - 31, The constructor
of ScopedDebugLabel passes name.data() to VkDebugUtilsLabelEXT::pLabelName but
std::string_view may not be null-terminated; fix by making a null-terminated
std::string copy of the view (e.g. local or a member like m_label_name) and pass
m_label_name.c_str() to pLabelName so the string remains valid for the duration
of dispatch.begin/dispatch.end calls; update the ScopedDebugLabel constructor
and any places using pLabelName to use this c_str() instead of name.data().

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/render/test_runtime_diagnostics.cpp (1)

1050-1053: Assert the disabled-dispatch path, not just the available one.

This block only proves record_frame() doesn't crash with the dispatcher nulled. It doesn't fail if profiling labels are still emitted through some other path, so the fallback behavior remains unverified.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/render/test_runtime_diagnostics.cpp` around lines 1050 - 1053, The test
currently only calls record_frame() with DebugLabelDispatcherDisable but doesn't
assert the disabled-dispatch behavior; modify the block that uses
DebugLabelDispatcherDisable to also verify that no profiling labels were emitted
after record_frame() (e.g., assert the recorded-labels container/count is zero
or that the label-dispatch callback was not invoked). Locate the block around
DebugLabelDispatcherDisable and record_frame() and add an assertion (using the
test's existing label inspection helpers or profiler API) that confirms the
disabled path produced no labels.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/render/test_gpu_timestamp_pool.cpp`:
- Around line 67-76: The test fixture currently forces
VkPhysicalDeviceVulkan13Features.dynamicRendering = VK_TRUE and attaches
features13 to device_info.pNext before calling vkCreateDevice (symbols:
VkPhysicalDeviceVulkan13Features, features13, device_info, vkCreateDevice,
physical_device, device), which can fail on hardware without that feature;
remove the unconditional enable by deleting the features13.dynamicRendering =
VK_TRUE assignment (or only set it after querying the physical device for
support) and avoid unconditionally chaining features13 into device_info.pNext so
device creation succeeds on timestamp-only capable devices.
- Around line 441-490: read_results(0u) is being checked before first_frame is
submitted, so the test never exercises the non-blocking/pending-read path;
submit first_frame to the queue (e.g. call submit_and_wait or an equivalent
submit that ensures it reaches the queue) after vkEndCommandBuffer(first_frame)
and before calling pool->read_results(0u), then proceed with the rest of the
test (keep using pool->reset_frame/read/write_* and the second_frame flow
unchanged) so that read_results(0u) observes a pending/in-progress result rather
than the unsubmitted empty case.

---

Nitpick comments:
In `@tests/render/test_runtime_diagnostics.cpp`:
- Around line 1050-1053: The test currently only calls record_frame() with
DebugLabelDispatcherDisable but doesn't assert the disabled-dispatch behavior;
modify the block that uses DebugLabelDispatcherDisable to also verify that no
profiling labels were emitted after record_frame() (e.g., assert the
recorded-labels container/count is zero or that the label-dispatch callback was
not invoked). Locate the block around DebugLabelDispatcherDisable and
record_frame() and add an assertion (using the test's existing label inspection
helpers or profiler API) that confirms the disabled path produced no labels.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b9802688-d2f7-416a-aa90-aa6e6ae6b590

📥 Commits

Reviewing files that changed from the base of the PR and between 12931ef and 92d082d.

📒 Files selected for processing (10)
  • src/render/backend/vulkan_context.cpp
  • src/render/chain/api/c/goggles_filter_chain.cpp
  • src/render/chain/chain_runtime.cpp
  • src/render/chain/debug_label_scope.hpp
  • src/render/chain/vulkan_context.hpp
  • src/util/diagnostics/gpu_timestamp_pool.cpp
  • src/util/diagnostics/gpu_timestamp_pool.hpp
  • tests/render/test_gpu_timestamp_pool.cpp
  • tests/render/test_runtime_diagnostics.cpp
  • tests/visual/runtime_capture.cpp

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/render/test_gpu_timestamp_pool.cpp`:
- Around line 40-55: The current selection picks the first graphics queue family
found and may miss families/devices that support timestamps; change the loop
that iterates `devices` and queue families so that for each candidate graphics
family you call
`goggles::diagnostics::GpuTimestampPool::supports_timestamps(...)` and prefer
the first family that returns true, setting `physical_device` and
`queue_family_index` to that family; only if no graphics family across all
devices supports timestamps, fall back to the original behavior of choosing the
first graphics-capable family found (using the existing
`vkGetPhysicalDeviceQueueFamilyProperties` logic and variables `devices`,
`physical_device`, `queue_family_index`).
- Around line 455-469: The test reads pending results immediately after
vkQueueSubmit but on fast hardware the GPU may already have finished; modify the
test around vkQueueSubmit/first_frame_fence and pool->read_results(0u) to guard
the empty assertion by checking vkGetFenceStatus(first_frame_fence) ==
VK_NOT_READY before calling CHECK(pending_results->empty()), or alternatively
block until the fence is signaled (vkWaitForFences) and then assert non-empty as
appropriate; locate the vkQueueSubmit call, the VkFence first_frame_fence, and
the pool->read_results(0u) usage to implement the chosen fence-status check or
explicit wait to avoid the race.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e380677-0126-45fa-9e1e-829e364a1fd8

📥 Commits

Reviewing files that changed from the base of the PR and between 92d082d and a6cba87.

📒 Files selected for processing (1)
  • tests/render/test_gpu_timestamp_pool.cpp

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/render/test_gpu_timestamp_pool.cpp`:
- Around line 252-267: The helper submit_and_wait currently uses an infinite
wait via vkWaitForFences(..., UINT64_MAX) which can hang CI; change it to use a
bounded timeout (e.g. a constant like kFenceTimeoutNs = 5'000'000'000ULL for 5s)
and pass that instead of UINT64_MAX to vkWaitForFences, treat a timeout or
non-success return as a test failure (return false), and keep the existing
vkDestroyFence cleanup; update any test expectations accordingly to rely on the
limited wait.
- Around line 201-245: The test currently hard-codes VK_FORMAT_B8G8R8A8_UNORM in
image_info and view_info which may not support the requested usage/tiling on all
GPUs; before creating the image (before using image_info and view_info and
calling vkCreateImage/vkCreateImageView), query format support using
vkGetPhysicalDeviceImageFormatProperties (or
vkGetPhysicalDeviceFormatProperties) for the desired VkImageTiling
(VK_IMAGE_TILING_OPTIMAL) and usage bits (VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT), choose a format that returns supported
properties, and assign that format to image_info.format and view_info.format
(with a sensible fallback or early skip/return if no format is supported). Also
ensure any memory/type selection (find_memory_type) and subsequent operations
use the selected format consistently.
- Around line 14-24: The test requests Vulkan 1.3 which can cause
vkCreateInstance to fail on older drivers; update
VkApplicationInfo.app_info.apiVersion to VK_API_VERSION_1_0 and change the
vkCreateInstance failure handling in the test (the block around
VkApplicationInfo app_info and vkCreateInstance) to report/propagate the error
instead of silently returning so test failures aren’t masked as skips.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a151f03-99a1-4e00-af75-38df585ce172

📥 Commits

Reviewing files that changed from the base of the PR and between a6cba87 and e6786df.

📒 Files selected for processing (1)
  • tests/render/test_gpu_timestamp_pool.cpp

Comment on lines +14 to +24
VkApplicationInfo app_info{};
app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app_info.pApplicationName = "goggles_gpu_timestamp_pool_tests";
app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
app_info.apiVersion = VK_API_VERSION_1_3;

VkInstanceCreateInfo instance_info{};
instance_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
instance_info.pApplicationInfo = &app_info;
if (vkCreateInstance(&instance_info, nullptr, &instance) != VK_SUCCESS) {
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the test file to understand all APIs being used
fd -t f "test_gpu_timestamp_pool.cpp" && wc -l tests/render/test_gpu_timestamp_pool.cpp

Repository: goggles-dev/Goggles

Length of output: 148


🏁 Script executed:

# Read the full test file to identify all Vulkan API calls
cat -n tests/render/test_gpu_timestamp_pool.cpp

Repository: goggles-dev/Goggles

Length of output: 27184


🏁 Script executed:

# Search for all vk* function calls in the file to understand API usage
rg "vk[A-Z]" tests/render/test_gpu_timestamp_pool.cpp -o

Repository: goggles-dev/Goggles

Length of output: 238


🌐 Web query:

Vulkan VkApplicationInfo apiVersion behavior failure vkCreateInstance older driver compatibility

💡 Result:

  • VkApplicationInfo::apiVersion is the highest Vulkan core API version your app is designed to use. If you set it to 0 (or omit pApplicationInfo), that is treated as Vulkan 1.0.0. [1]
  • On older Vulkan 1.0 implementations/drivers, vkCreateInstance can fail with VK_ERROR_INCOMPATIBLE_DRIVER if the driver does not support the apiVersion you requested. The Vulkan docs explicitly warn about this and recommend checking the available version before calling vkCreateInstance. [1][4]
  • The intended compatibility pattern is:
    1. Call vkEnumerateInstanceVersion to get the maximum instance-level Vulkan version supported by the loader/runtime. [2][3]
    2. Set VkApplicationInfo::apiVersion = min(appDesiredVersion, reportedVersion) (and if vkEnumerateInstanceVersion is unavailable, assume only 1.0). [3][1]
  • Loader/ICD nuance (multi-driver / very old ICDs):
    • If multiple drivers are present and some are Vulkan-1.0-only, a conforming loader should downgrade apiVersion to 1.0 when talking to those 1.0-only drivers. [5]
    • With newer loader/driver interface negotiation, drivers using newer interfaces are instructed not to return VK_ERROR_INCOMPATIBLE_DRIVER merely because the app passed a higher apiVersion than the driver supports (the loader/negotiation is expected to handle this more gracefully). [6]

Practical takeaway: if you want “old driver compatibility”, don’t hardcode VK_API_VERSION_1_3/1_4 in VkApplicationInfo unconditionally—query with vkEnumerateInstanceVersion and clamp, otherwise you can trigger VK_ERROR_INCOMPATIBLE_DRIVER on older stacks. [1][2][4]

Sources:
[1] VkApplicationInfo (Khronos) (registry.khronos.org)
[2] vkEnumerateInstanceVersion (Khronos) (registry.khronos.org)
[3] LunarG tutorial: Vulkan 1.1 changes / version query (vulkan.lunarg.com)
[4] vkCreateInstance error (VK_ERROR_INCOMPATIBLE_DRIVER) (registry.khronos.org)
[5] LunarG Loader/Driver Interface: loader adjusts apiVersion for 1.0-only drivers (vulkan.lunarg.com)
[6] LunarG Loader/Driver Interface: driver should not return incompatible-driver just due to apiVersion mismatch (vulkan.lunarg.com)

Citations:


Set apiVersion to VK_API_VERSION_1_0.

This test uses only Vulkan 1.0 core APIs (instance/device creation, queues, memory, command buffers, fences, and barriers). Requesting VK_API_VERSION_1_3 can cause vkCreateInstance to fail with VK_ERROR_INCOMPATIBLE_DRIVER on older Vulkan 1.0/1.1/1.2–only drivers. The current code silently returns on failure, creating false test skips on hardware that actually supports all the APIs being used.

Fix
-        app_info.apiVersion = VK_API_VERSION_1_3;
+        app_info.apiVersion = VK_API_VERSION_1_0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/render/test_gpu_timestamp_pool.cpp` around lines 14 - 24, The test
requests Vulkan 1.3 which can cause vkCreateInstance to fail on older drivers;
update VkApplicationInfo.app_info.apiVersion to VK_API_VERSION_1_0 and change
the vkCreateInstance failure handling in the test (the block around
VkApplicationInfo app_info and vkCreateInstance) to report/propagate the error
instead of silently returning so test failures aren’t masked as skips.

Comment on lines +201 to +245
VkImageCreateInfo image_info{};
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.format = VK_FORMAT_B8G8R8A8_UNORM;
image_info.extent = {.width = extent.width, .height = extent.height, .depth = 1u};
image_info.mipLevels = 1u;
image_info.arrayLayers = 1u;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(device, &image_info, nullptr, &guard.image) != VK_SUCCESS) {
return std::nullopt;
}

VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device, guard.image, &requirements);
const auto memory_type = find_memory_type(physical_device, requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (!memory_type.has_value()) {
return std::nullopt;
}

VkMemoryAllocateInfo alloc_info{};
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc_info.allocationSize = requirements.size;
alloc_info.memoryTypeIndex = *memory_type;
if (vkAllocateMemory(device, &alloc_info, nullptr, &guard.memory) != VK_SUCCESS) {
return std::nullopt;
}
if (vkBindImageMemory(device, guard.image, guard.memory, 0u) != VK_SUCCESS) {
return std::nullopt;
}

VkImageViewCreateInfo view_info{};
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
view_info.image = guard.image;
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = VK_FORMAT_B8G8R8A8_UNORM;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view_info.subresourceRange.levelCount = 1u;
view_info.subresourceRange.layerCount = 1u;
if (vkCreateImageView(device, &view_info, nullptr, &guard.view) != VK_SUCCESS) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n tests/render/test_gpu_timestamp_pool.cpp | head -260 | tail -80

Repository: goggles-dev/Goggles

Length of output: 3955


🏁 Script executed:

rg "vkGetPhysicalDeviceFormatProperties" --type cpp -B 2 -A 5

Repository: goggles-dev/Goggles

Length of output: 45


🏁 Script executed:

rg "VK_FORMAT_" tests/render/ --type cpp | head -20

Repository: goggles-dev/Goggles

Length of output: 1242


🏁 Script executed:

rg "formatProperties\|FormatProperties" --type cpp -B 2 -A 3

Repository: goggles-dev/Goggles

Length of output: 45


🌐 Web query:

Vulkan VK_FORMAT_B8G8R8A8_UNORM support guarantee requirements

💡 Result:

  • As a regular image format (VkFormatProperties): VK_FORMAT_B8G8R8A8_UNORM is part of Vulkan’s mandatory format support set (“Mandatory Format Support: 4 Byte-Sized Components”). That table specifies the minimum VkFormatFeatureFlagBits an implementation must advertise for the format (notably in optimalTilingFeatures, i.e., VK_IMAGE_TILING_OPTIMAL). In other words, you can rely on it being supported with at least the required baseline feature bits listed in that table, and you query the exact bitmask via vkGetPhysicalDeviceFormatProperties. [1]

  • As a presentable / swapchain format (VkSurfaceFormatKHR): Vulkan does not guarantee that a surface will support VK_FORMAT_B8G8R8A8_UNORM. The only hard guarantee is that vkGetPhysicalDeviceSurfaceFormatsKHR returns at least 1 (format, colorSpace) pair, and none of them may have format = VK_FORMAT_UNDEFINED. You must enumerate and pick from what the surface reports. [2]

Sources: [1] Vulkan Spec – Formats chapter (Required/Mandatory format support tables) https://github.khronos.org/Vulkan-Site/spec/latest/chapters/formats.html, [2] Vulkan Refpage – vkGetPhysicalDeviceSurfaceFormatsKHR https://docs.vulkan.org/refpages/latest/refpages/source/vkGetPhysicalDeviceSurfaceFormatsKHR.html


🏁 Script executed:

rg "create_image" tests/render/test_gpu_timestamp_pool.cpp -B 2 -A 5

Repository: goggles-dev/Goggles

Length of output: 1269


Probe format support before hard-coding VK_FORMAT_B8G8R8A8_UNORM.

While VK_FORMAT_B8G8R8A8_UNORM is a mandatory Vulkan format, the Vulkan spec only guarantees minimum feature support—not that all usage combinations (sampled, transfer source/destination, color attachment with optimal tiling) are supported on every device. Tests will fail on otherwise valid devices that don't support this exact format/usage combination, masking actual test failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/render/test_gpu_timestamp_pool.cpp` around lines 201 - 245, The test
currently hard-codes VK_FORMAT_B8G8R8A8_UNORM in image_info and view_info which
may not support the requested usage/tiling on all GPUs; before creating the
image (before using image_info and view_info and calling
vkCreateImage/vkCreateImageView), query format support using
vkGetPhysicalDeviceImageFormatProperties (or
vkGetPhysicalDeviceFormatProperties) for the desired VkImageTiling
(VK_IMAGE_TILING_OPTIMAL) and usage bits (VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT), choose a format that returns supported
properties, and assign that format to image_info.format and view_info.format
(with a sensible fallback or early skip/return if no format is supported). Also
ensure any memory/type selection (find_memory_type) and subsequent operations
use the selected format consistently.

Comment on lines +252 to +267
auto submit_and_wait(VkDevice device, VkQueue queue, VkCommandBuffer command_buffer) -> bool {
VkFenceCreateInfo fence_info{};
fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
VkFence fence = VK_NULL_HANDLE;
if (vkCreateFence(device, &fence_info, nullptr, &fence) != VK_SUCCESS) {
return false;
}

VkSubmitInfo submit_info{};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.commandBufferCount = 1u;
submit_info.pCommandBuffers = &command_buffer;
const bool ok = vkQueueSubmit(queue, 1u, &submit_info, fence) == VK_SUCCESS &&
vkWaitForFences(device, 1u, &fence, VK_TRUE, UINT64_MAX) == VK_SUCCESS;
vkDestroyFence(device, fence, nullptr);
return ok;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the test file and examine the specific lines
find . -name "test_gpu_timestamp_pool.cpp" -type f

Repository: goggles-dev/Goggles

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Read the specific lines mentioned in the review (252-267)
sed -n '252,267p' ./tests/render/test_gpu_timestamp_pool.cpp

Repository: goggles-dev/Goggles

Length of output: 808


🏁 Script executed:

#!/bin/bash
# Check for other vkWaitForFences usage in the codebase to see patterns
rg "vkWaitForFences" --type cpp -B 2 -A 2

Repository: goggles-dev/Goggles

Length of output: 1707


🏁 Script executed:

#!/bin/bash
# Check if there are any existing timeout constants or patterns in the test files
rg "timeout|Timeout|TIMEOUT|30\|seconds|Duration" tests/render/test_gpu_timestamp_pool.cpp -i

Repository: goggles-dev/Goggles

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Look for timeout constants or patterns in test files generally
rg "const.*=.*[0-9].*000" tests/ --type cpp | head -20

Repository: goggles-dev/Goggles

Length of output: 497


🏁 Script executed:

#!/bin/bash
# Get more context around the submit_and_wait function to understand its usage
sed -n '240,275p' ./tests/render/test_gpu_timestamp_pool.cpp

Repository: goggles-dev/Goggles

Length of output: 1405


🏁 Script executed:

#!/bin/bash
# Find where submit_and_wait is called
rg "submit_and_wait" tests/render/test_gpu_timestamp_pool.cpp -B 2 -A 2

Repository: goggles-dev/Goggles

Length of output: 736


Use a bounded fence wait in the test helper.

vkWaitForFences(..., UINT64_MAX) can hang the entire test job indefinitely if the GPU or driver wedges. A finite timeout makes this fail as a test error instead of stalling CI.

Suggested fix
+constexpr uint64_t kFenceWaitTimeoutNs = 30'000'000'000ULL;
+
 auto submit_and_wait(VkDevice device, VkQueue queue, VkCommandBuffer command_buffer) -> bool {
     VkFenceCreateInfo fence_info{};
     fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
     VkFence fence = VK_NULL_HANDLE;
     if (vkCreateFence(device, &fence_info, nullptr, &fence) != VK_SUCCESS) {
         return false;
     }
 
     VkSubmitInfo submit_info{};
     submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
     submit_info.commandBufferCount = 1u;
     submit_info.pCommandBuffers = &command_buffer;
-    const bool ok = vkQueueSubmit(queue, 1u, &submit_info, fence) == VK_SUCCESS &&
-                    vkWaitForFences(device, 1u, &fence, VK_TRUE, UINT64_MAX) == VK_SUCCESS;
+    if (vkQueueSubmit(queue, 1u, &submit_info, fence) != VK_SUCCESS) {
+        vkDestroyFence(device, fence, nullptr);
+        return false;
+    }
+    const bool ok =
+        vkWaitForFences(device, 1u, &fence, VK_TRUE, kFenceWaitTimeoutNs) == VK_SUCCESS;
     vkDestroyFence(device, fence, nullptr);
     return ok;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/render/test_gpu_timestamp_pool.cpp` around lines 252 - 267, The helper
submit_and_wait currently uses an infinite wait via vkWaitForFences(...,
UINT64_MAX) which can hang CI; change it to use a bounded timeout (e.g. a
constant like kFenceTimeoutNs = 5'000'000'000ULL for 5s) and pass that instead
of UINT64_MAX to vkWaitForFences, treat a timeout or non-success return as a
test failure (return false), and keep the existing vkDestroyFence cleanup;
update any test expectations accordingly to rely on the limited wait.

@K1ngst0m K1ngst0m merged commit 9703bd2 into main Mar 12, 2026
5 checks passed
@K1ngst0m K1ngst0m deleted the dev/gpu-timestamp-test-cov branch March 12, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant