Skip to content

Use VK_KHR_present_wait for Frame Pacing#47

Merged
K1ngst0m merged 6 commits intomainfrom
dev/improve-frame-pacing
Jan 10, 2026
Merged

Use VK_KHR_present_wait for Frame Pacing#47
K1ngst0m merged 6 commits intomainfrom
dev/improve-frame-pacing

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Jan 10, 2026

User description

Why

Goggles currently defaults to mailbox present mode and allows uncapped frame submission. On high-end GPUs this drives extremely high FPS, wasting power and producing inconsistent frame pacing. Vulkan's VK_KHR_present_wait lets us explicitly pace presentation and reduce unnecessary GPU/CPU work.

What Changes

  • Prefer FIFO present mode with VK_KHR_present_wait when supported.
  • Use VK_KHR_present_wait to pace presentation to render.target_fps (0 = uncapped).
  • Fallback behavior when VK_KHR_present_wait is unavailable:
    • Prefer MAILBOX present mode.
    • Apply CPU-side frame cap using render.target_fps (0 = uncapped).
    • If MAILBOX is unavailable, use FIFO without present wait.
  • Add CLI override for render.target_fps (mirrors config; 0 = uncapped).

Impact

  • Affected specs: render-pipeline, app-window.
  • Affected code:
    • src/render/backend/vulkan_backend.cpp - present mode selection and pacing integration.
    • src/render/backend/vulkan_backend.hpp - present wait capability state.
    • src/util/config.hpp + src/util/config.cpp - allow target_fps = 0 for uncapped.
    • src/app/cli.hpp - CLI override for target fps.
    • src/app/main.cpp - apply CLI override and log.

PR Type

Enhancement


Description

  • Implement VK_KHR_present_wait for GPU-assisted frame pacing

  • Support uncapped FPS (0) and configurable target frame rates

  • Add CLI override for --target-fps parameter

  • Fallback to mailbox/FIFO with CPU-side throttling when unavailable


Diagram Walkthrough

flowchart LR
  A["Config/CLI"] -->|target_fps| B["RenderSettings"]
  B -->|settings| C["VulkanBackend::create"]
  C -->|detect support| D["select_physical_device"]
  D -->|present_wait_supported| E["create_device"]
  E -->|enable extensions| F["create_swapchain"]
  F -->|choose mode| G{present_wait ready?}
  G -->|yes| H["FIFO + present_wait"]
  G -->|no| I["MAILBOX or FIFO"]
  H -->|apply_present_wait| J["Frame Pacing"]
  I -->|throttle_present| J
Loading

File Walkthrough

Relevant files
Enhancement
6 files
vulkan_backend.hpp
Add present wait state and frame pacing fields                     
+22/-6   
vulkan_backend.cpp
Implement present wait detection and frame pacing logic   
+128/-10
application.cpp
Pass RenderSettings struct to VulkanBackend                           
+6/-2     
cli.hpp
Add target-fps CLI option with optional value                       
+5/-0     
main.cpp
Apply CLI target-fps override to config                                   
+8/-0     
config.cpp
Allow target_fps value of zero in validation                         
+2/-2     
Documentation
5 files
config.hpp
Document target_fps zero means uncapped                                   
+1/-1     
proposal.md
Document frame pacing feature proposal                                     
+25/-0   
spec.md
Define render pipeline present wait requirements                 
+31/-0   
spec.md
Define CLI target FPS override requirements                           
+17/-0   
tasks.md
List implementation and validation tasks                                 
+14/-0   
Tests
1 files
test_config.cpp
Update tests to include zero FPS value                                     
+2/-2     

Summary by CodeRabbit

  • New Features

    • Added --target-fps CLI to override render target FPS (0 = uncapped).
    • Improved frame pacing: use present-wait when available (prefers FIFO), with fallbacks and CPU-side capping for smoother presentation.
  • Bug Fixes / Validation

    • Config validation now accepts 0 for target_fps (0 = uncapped); error message updated.
    • Tests updated to accept 0 as valid.
  • Refactor

    • Consolidated render settings into a single settings object and updated backend API surface.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 10, 2026

Warning

Rate limit exceeded

@K1ngst0m has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b385b8d and 88a826b.

📒 Files selected for processing (1)
  • src/render/backend/vulkan_backend.cpp
📝 Walkthrough

Walkthrough

Consolidates render settings into a new RenderSettings struct, updates VulkanBackend::create to accept it, detects and uses VK_KHR_present_wait for present-wait frame pacing (fallback CPU cap when unavailable, honoring render.target_fps, 0 = uncapped), adds --target-fps CLI override, and allows target_fps = 0 in config validation.

Changes

Cohort / File(s) Summary
CLI & App
src/app/cli.hpp, src/app/main.cpp, src/app/application.cpp
Adds std::optional<uint32_t> target_fps and --target-fps CLI option; applies CLI override to config.render.target_fps with log; adapts calls to VulkanBackend::create to pass RenderSettings.
Config Validation & Tests
src/util/config.hpp, src/util/config.cpp, tests/util/test_config.cpp
Allows target_fps = 0 (uncapped) by changing validation bounds and error text; updates tests to accept 0 and new error message.
Vulkan Backend API & Impl
src/render/backend/vulkan_backend.hpp, src/render/backend/vulkan_backend.cpp
Introduces render::RenderSettings (scale_mode, integer_scale, target_fps); changes VulkanBackend::create signature to accept RenderSettings; detects VK_KHR_present_wait, stores m_present_wait_supported, adds present-id path and CPU-side throttle fallback, new helpers (apply_present_wait, throttle_present, update_target_fps, is_present_wait_ready), and removes consume_format_change() from public interface.
Docs & Specs
openspec/changes/archive/..., openspec/specs/app-window/spec.md, openspec/specs/render-pipeline/spec.md, openspec/changes/.../tasks.md
Adds proposal, specs, and tasks documenting present-wait frame pacing behavior, present-mode selection, and CLI-driven target FPS semantics.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI Parser
    participant App as Application
    participant Config as Config System
    participant Backend as Vulkan Backend
    participant Device as GPU/Device
    participant Swapchain as Swapchain/Queue

    CLI->>Config: Parse/override target_fps (--target-fps)
    App->>Config: Load config (render.target_fps)
    App->>Backend: create(RenderSettings)
    Backend->>Device: Query VK_KHR_present_wait support
    Device-->>Backend: present_wait_supported flag
    Backend->>Swapchain: Create swapchain (FIFO if present_wait else MAILBOX/FIFO)

    loop Frame loop
        App->>Backend: submit_and_present()
        alt present_wait_supported
            Backend->>Swapchain: Submit with present ID
            Backend->>Backend: apply_present_wait(present_id)
            Swapchain-->>Backend: Present completion
        else
            Backend->>Swapchain: Submit frame
            Backend->>Backend: throttle_present() per target_fps
            Swapchain-->>Backend: Present completion
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Poem

🐰 I hop through frames with a patient beat,

Present IDs march in cadence, soft and neat,
A CLI carrot sets the pace to chase,
Zero frees the dance — uncapped, boundless space,
GPU hums, CPU nudges, together they meet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% 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 clearly and concisely summarizes the main change: implementing VK_KHR_present_wait for frame pacing, which is the central feature across all modified files.

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@K1ngst0m K1ngst0m force-pushed the dev/improve-frame-pacing branch from 5bd2b8f to 25f927b Compare January 10, 2026 11:43
@K1ngst0m K1ngst0m marked this pull request as ready for review January 10, 2026 11:48
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 10, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing feature validation: The code enables VK_KHR_present_id/VK_KHR_present_wait features based only on extension
presence (m_present_wait_supported) without validating the queried feature flags from
getFeatures2, risking device creation failure on edge-case drivers.

Referred Code
vk::PhysicalDeviceVulkan11Features vk11_features{};
vk::PhysicalDeviceVulkan12Features vk12_features{};
vk::PhysicalDeviceVulkan13Features vk13_features{};
vk::PhysicalDevicePresentIdFeaturesKHR present_id_features{};
vk::PhysicalDevicePresentWaitFeaturesKHR present_wait_features{};
vk11_features.pNext = &vk12_features;
vk12_features.pNext = &vk13_features;
bool present_wait_ready = is_present_wait_ready();
if (present_wait_ready) {
    vk13_features.pNext = &present_id_features;
    present_id_features.pNext = &present_wait_features;
}
vk::PhysicalDeviceFeatures2 features2{};
features2.pNext = &vk11_features;
m_physical_device.getFeatures2(&features2);

if (!vk11_features.shaderDrawParameters) {
    return make_error<void>(ErrorCode::vulkan_init_failed,
                            "shaderDrawParameters not supported (required for vertex shaders)");
}
if (!vk12_features.timelineSemaphore) {


 ... (clipped 44 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 10, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Improve frame pacing stability

Improve the CPU-side frame throttling in throttle_present by advancing
m_last_present_time by a fixed duration instead of resetting it to now,
preventing frame time drift.

src/render/backend/vulkan_backend.cpp [1361-1382]

 void VulkanBackend::throttle_present() {
     if (m_target_fps == 0) {
         return;
     }
 
     using clock = std::chrono::steady_clock;
-    auto frame_duration = std::chrono::nanoseconds(1'000'000'000ULL / m_target_fps);
+    const auto frame_duration = std::chrono::nanoseconds(1'000'000'000ULL / m_target_fps);
 
     if (m_last_present_time.time_since_epoch().count() == 0) {
         m_last_present_time = clock::now();
         return;
     }
 
-    auto next_frame = m_last_present_time + frame_duration;
+    m_last_present_time += frame_duration;
     auto now = clock::now();
-    if (now < next_frame) {
-        std::this_thread::sleep_until(next_frame);
-        m_last_present_time = next_frame;
+    if (now < m_last_present_time) {
+        std::this_thread::sleep_until(m_last_present_time);
     } else {
-        m_last_present_time = now;
+        // The frame took too long, we are behind.
+        // To prevent a spiral of death, we can either reset the timer or let it try to catch up.
+        // For now, let's reset to prevent large 'jumps' if the app stalls for a long time.
+        if (now > m_last_present_time + frame_duration) {
+            m_last_present_time = now;
+        }
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential for frame time drift in the CPU-side throttling logic and proposes a more stable pacing algorithm that avoids resetting the timer on missed frames.

Low
Remove duplicated code block

Remove the duplicated code block in src/app/main.cpp that applies the
--target-fps CLI override.

src/app/main.cpp [380-387]

-if (cli_opts.target_fps.has_value()) {
-    config.render.target_fps = *cli_opts.target_fps;
-    GOGGLES_LOG_INFO("Target FPS overridden by CLI: {}", config.render.target_fps);
-}
 if (cli_opts.target_fps.has_value()) {
     config.render.target_fps = *cli_opts.target_fps;
     GOGGLES_LOG_INFO("Target FPS overridden by CLI: {}", config.render.target_fps);
 }
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly identifies a duplicated block of code introduced in the PR and suggests removing it, which improves code cleanliness.

Low
  • Update

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: 1

🤖 Fix all issues with AI agents
In @src/app/main.cpp:
- Around line 380-387: There is a duplicated CLI override block for target_fps
in main.cpp causing double assignment and duplicate logs; remove the redundant
if (cli_opts.target_fps.has_value()) { ... } block so only one check sets
config.render.target_fps = *cli_opts.target_fps and calls
GOGGLES_LOG_INFO("Target FPS overridden by CLI: {}", config.render.target_fps);
keep the first (or preferred) occurrence and delete the second to avoid repeated
assignment/logging.
🧹 Nitpick comments (3)
openspec/changes/update-present-wait-frame-pacing/tasks.md (1)

1-14: Clarify that this is a task checklist (not a requirements/spec doc) to avoid openspec scenario-format expectations.

This lives under openspec/**/*.md, where we have scenario-format guidelines for requirements/specs; a one-liner note (or moving requirement-like statements into the spec doc) can prevent future “format drift” confusion. Based on coding guidelines / learnings.

tests/util/test_config.cpp (1)

198-214: Good coverage of target_fps = 0 (uncapped). Consider unique temp paths to avoid parallel-test collisions.
Using fixed names under util/test_data/*.toml can be flaky if tests ever run concurrently; generating a unique filename in std::filesystem::temp_directory_path() would make this more robust.

src/render/backend/vulkan_backend.cpp (1)

1361-1382: CPU-side frame pacing fallback looks correct.

The implementation properly handles:

  • Uncapped mode (m_target_fps == 0) with early return
  • First-frame initialization
  • Catching up when behind schedule

One minor note: the check m_last_present_time.time_since_epoch().count() == 0 works for detecting default-constructed time_point, but could be fragile if the epoch happens to be crossed. Consider using std::optional<time_point> for clearer semantics, though this is unlikely to be an issue in practice.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99380b0 and 25f927b.

📒 Files selected for processing (12)
  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • openspec/changes/update-present-wait-frame-pacing/specs/app-window/spec.md
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • openspec/changes/update-present-wait-frame-pacing/tasks.md
  • src/app/application.cpp
  • src/app/cli.hpp
  • src/app/main.cpp
  • src/render/backend/vulkan_backend.cpp
  • src/render/backend/vulkan_backend.hpp
  • src/util/config.cpp
  • src/util/config.hpp
  • tests/util/test_config.cpp
🧰 Additional context used
📓 Path-based instructions (3)
openspec/**/*.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

openspec/**/*.md: Use #### Scenario: format (4 hashtags) for scenario headers in requirements, not bullets or bold text, with at least one scenario per requirement
Use format - **WHEN** [condition] - **THEN** [expected result] for scenario steps in requirements

Files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • openspec/changes/update-present-wait-frame-pacing/specs/app-window/spec.md
  • openspec/changes/update-present-wait-frame-pacing/tasks.md
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
openspec/changes/**/proposal.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

List spec deltas in proposal.md under 'What Changes' section, marking breaking changes with BREAKING

Files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
openspec/changes/**/specs/**/*.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

openspec/changes/**/specs/**/*.md: Write spec deltas using ## ADDED|MODIFIED|REMOVED|RENAMED Requirements format with at least one #### Scenario: per requirement in spec files
When modifying existing requirements in a MODIFIED delta, paste the full requirement block including the header and all scenarios, as the archiver will replace the entire requirement
Use ADDED for new capabilities that can stand alone; use MODIFIED for changes to existing requirement behavior, scope, or acceptance criteria; use RENAMED for name-only changes

Files:

  • openspec/changes/update-present-wait-frame-pacing/specs/app-window/spec.md
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Vulkan capture layer is the primary frame capture method, intercepting rendered frames before presentation for zero-copy access
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/tests/**/*.{cpp,hpp} : Focus unit tests on non-GPU logic: utility functions, configuration parsing, and pipeline graph logic

Applied to files:

  • tests/util/test_config.cpp
  • src/app/main.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/tests/**/*.cpp : Use Catch2 v3 as the project-wide testing framework. Header-only option, provided by Pixi environment, loaded via `find_package`.

Applied to files:

  • tests/util/test_config.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Per-frame code paths on main thread MUST NOT: perform dynamic memory allocations (`new`, `malloc`, `std::make_shared`); use blocking synchronization primitives (`std::mutex`, `std::condition_variable`); exceed 8ms CPU time budget (excluding GPU sync).

Applied to files:

  • src/util/config.cpp
  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
  • src/util/config.hpp
  • openspec/changes/update-present-wait-frame-pacing/tasks.md
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • src/app/main.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
  • openspec/changes/update-present-wait-frame-pacing/tasks.md
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
  • openspec/changes/update-present-wait-frame-pacing/tasks.md
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • src/app/application.cpp
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
  • src/app/application.cpp
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • src/app/application.cpp
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • src/app/application.cpp
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2025-12-19T08:54:18.412Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-12-19T08:54:18.412Z
Learning: Applies to openspec/changes/**/specs/**/*.md : Use ADDED for new capabilities that can stand alone; use MODIFIED for changes to existing requirement behavior, scope, or acceptance criteria; use RENAMED for name-only changes

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/specs/app-window/spec.md
  • openspec/changes/update-present-wait-frame-pacing/tasks.md
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp (C++ bindings), NOT raw Vulkan C API. Use `vk::` types (e.g., `vk::Instance`, `vk::Device`). Do not use raw C handles like `VkInstance`, `VkDevice`. Required configuration: `#define VULKAN_HPP_NO_EXCEPTIONS`, `#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1`, `#include <vulkan/vulkan.hpp>`.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
  • src/app/application.cpp
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : All vulkan-hpp calls returning `vk::Result` must be explicitly checked. Prohibited pattern: `static_cast<void>(device.waitIdle())`. Use macro `VK_TRY(call, ErrorCode, "message")` for vk::Result early return or manual pattern with error handling.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
  • openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp with `VULKAN_HPP_NO_EXCEPTIONS` and dynamic dispatch

Applied to files:

  • src/render/backend/vulkan_backend.cpp
  • src/app/application.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Default to single-threaded render loop on main thread; use central job system (goggles::util::JobSystem) for any task parallelism

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.cpp : All concurrent processing MUST use the project's central job system (`goggles::util::JobSystem`). Phase 1: BS::thread_pool. Phase 2: Taskflow. Direct use of `std::thread` or `std::jthread` for pipeline work is PROHIBITED. Exception: External integration code (networking, IPC) outside real-time path may use `std::jthread` with RAII.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/app/**/*.{cpp,hpp} : Do not perform dynamic memory allocations or use blocking synchronization primitives in per-frame main render loop code paths

Applied to files:

  • src/render/backend/vulkan_backend.cpp
  • src/util/config.hpp
  • src/app/main.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Vulkan capture layer is the primary frame capture method, intercepting rendered frames before presentation for zero-copy access

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-19T08:54:18.412Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-12-19T08:54:18.412Z
Learning: Create a change proposal with `proposal.md`, `tasks.md`, and optional `design.md` under `openspec/changes/<change-id>/` before implementing changes to add features, make breaking changes, or update architecture

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/tasks.md
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Testing scope: Only non-GPU logic tested initially. In scope: utility functions, configuration parsing, error handling, pipeline graph logic. Out of scope: Vulkan initialization, GPU resource management, rendering, capture layer.

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/tasks.md
  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2025-12-19T08:54:18.412Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-12-19T08:54:18.412Z
Learning: Read `proposal.md`, `design.md` (if exists), and `tasks.md` in order, then implement tasks sequentially before updating checklist status to completed

Applied to files:

  • openspec/changes/update-present-wait-frame-pacing/tasks.md
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : RAII handle guidelines for Vulkan resources: Use `vk::Unique*` only for appropriate resource types: Instance, Device, Surface (long-lived singletons); Swapchain, Pipelines, Layouts (created once). Use plain `vk::` handles for command buffers (pooled lifetime), per-frame sync primitives (reused), and imported external images (requires explicit sync).

Applied to files:

  • src/render/backend/vulkan_backend.hpp
🧬 Code graph analysis (2)
src/app/application.cpp (2)
src/app/application.hpp (1)
  • config (30-31)
src/render/backend/vulkan_backend.cpp (2)
  • create (170-213)
  • create (170-172)
src/render/backend/vulkan_backend.hpp (2)
src/util/config.hpp (1)
  • nodiscard (19-31)
src/render/chain/pass.hpp (1)
  • nodiscard (54-130)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and Test (test preset)
  • GitHub Check: Static Analysis (clang-tidy)
🔇 Additional comments (23)
src/util/config.hpp (1)

46-52: Comment update matches new semantics (0 = uncapped).

tests/util/test_config.cpp (1)

94-111: Updated assertion matches new valid range (0-1000).

src/app/cli.hpp (1)

7-65: CLI11 correctly supports this pattern—no action required.

Verification confirms that CLI11 supports std::optional<T> option types and validators (including Range) are applied only when the option is provided. The Range(0u, 1000u) validator is inclusive on both bounds and correctly accepts --target-fps 0. The codebase validates this behavior at the config file level (tests include 0 as a valid FPS value), and the code properly handles the optional with .has_value() checks. No functional issues exist.

openspec/changes/update-present-wait-frame-pacing/specs/app-window/spec.md (1)

1-17: LGTM! Spec format is correct.

The spec follows the required format with ## ADDED Requirements, #### Scenario: headers, and proper GIVEN-WHEN-THEN structure. The requirements clearly document the CLI override behavior for target FPS with appropriate scenarios for both setting a specific value and disabling the frame cap.

As per coding guidelines, the spec delta format is correctly applied.

src/util/config.cpp (1)

64-67: LGTM! Validation correctly allows 0 for uncapped mode.

The change properly updates the validation bounds to allow target_fps = 0 for uncapped presentation pacing while maintaining the upper bound of 1000. The error message clearly documents the valid range and the meaning of 0.

openspec/changes/update-present-wait-frame-pacing/specs/render-pipeline/spec.md (1)

1-31: LGTM! Spec format and content are correct.

The spec follows the required format with ## ADDED Requirements, #### Scenario: headers, and proper GIVEN-WHEN-THEN structure. The requirements comprehensively document the Present Wait Frame Pacing feature with scenarios covering:

  • Present wait with FIFO mode when supported
  • Uncapped behavior (target_fps = 0)
  • Fallback to MAILBOX with CPU-side capping
  • Config-driven target fps changes

All scenarios have clear acceptance criteria.

As per coding guidelines, the spec delta format is correctly applied.

openspec/changes/update-present-wait-frame-pacing/proposal.md (1)

1-25: LGTM! Proposal is well-structured and comprehensive.

The proposal clearly documents the motivation, changes, and impact for the VK_KHR_present_wait frame pacing feature. The "What Changes" section appropriately lists the spec deltas, and the "Impact" section identifies all affected specs and code files. The proposal provides sufficient context for reviewers to understand the scope and rationale.

src/app/application.cpp (1)

57-63: LGTM! Clean consolidation of render settings.

The refactor appropriately replaces separate scale_mode and integer_scale parameters with a unified RenderSettings struct that also includes target_fps. This improves the API design by grouping related configuration and simplifies future extensions. All values are correctly transferred from the config.

src/render/backend/vulkan_backend.hpp (5)

23-27: LGTM!

The RenderSettings struct cleanly consolidates render configuration. The default target_fps = 60 provides sensible frame pacing out of the box, with 0 available for uncapped mode as documented in the PR objectives.


31-33: LGTM!

The consolidated RenderSettings parameter simplifies the API and makes it easier to add future render settings without further signature changes.


91-94: LGTM!

Resetting m_last_present_time when target_fps changes ensures correct frame pacing behavior after configuration updates.


101-102: LGTM!

The helper declarations are well-designed: is_present_wait_ready() for capability queries, apply_present_wait() with error handling for Vulkan calls, and throttle_present() as a simple void function for CPU-side timing.

Also applies to: 127-128


183-186: LGTM!

Member declarations are appropriate. Using std::chrono::steady_clock::time_point for timing is correct as it provides monotonic time suitable for frame pacing.

src/render/backend/vulkan_backend.cpp (10)

11-11: Verify intentional blocking on main thread for frame pacing.

The <thread> include enables std::this_thread::sleep_until() for CPU-side throttling. Based on learnings, the main thread should avoid blocking >1ms. However, frame pacing is an intentional exception to reduce CPU/GPU usage when present-wait is unavailable. Ensure this fallback path is acceptable for the project's performance requirements.


37-40: LGTM!

Optional extensions are correctly declared separately from required extensions, allowing graceful fallback when present-wait is unavailable.


170-190: LGTM!

The creation flow properly extracts settings and initializes the target FPS. Calling update_target_fps() ensures both m_target_fps and m_last_present_time are initialized correctly.


403-416: LGTM!

Correctly checking for both VK_KHR_PRESENT_ID and VK_KHR_PRESENT_WAIT extensions, as both are required for the present-wait functionality to work.


485-538: LGTM!

The feature chain is correctly constructed, and optional extensions are only added when present-wait is supported. The feature enable pattern properly matches the query pattern.


613-630: LGTM!

Present mode selection correctly implements the PR objectives: FIFO with present-wait when available (for explicit pacing), MAILBOX fallback (for lower latency without present-wait), and FIFO as the final fallback.


669-671: LGTM!

Resetting present-wait state during swapchain creation ensures correct behavior after swapchain recreation.


1316-1323: LGTM!

The present ID is correctly incremented and attached to the present info. This enables tracking which presents have completed via vkWaitForPresentKHR.


1336-1343: LGTM!

The pacing logic correctly uses apply_present_wait when available, falling back to throttle_present otherwise. Error handling properly propagates failures.


1349-1359: LGTM!

The implementation correctly handles the timeout calculation and treats eTimeout as success (expected behavior when pacing). Using the raw dispatcher call is appropriate for this extension function.

@goggles-dev goggles-dev deleted a comment from coderabbitai bot Jan 10, 2026
@K1ngst0m K1ngst0m force-pushed the dev/improve-frame-pacing branch from 25f927b to c260c74 Compare January 10, 2026 11:58
- enable present_id/present_wait extensions when available
- use present wait timeout from target_fps
- throttle via sleep when present wait isn’t supported
- refactor backend init with RenderSettings
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: 1

🧹 Nitpick comments (1)
src/render/backend/vulkan_backend.cpp (1)

1336-1336: Remove redundant check present_value > 0.

Since present_value = ++m_present_id at line 1319 guarantees present_value > 0 (starting from 1), the condition present_value > 0 at line 1336 is always true and can be simplified to just if (is_present_wait_ready()).

♻️ Simplify condition
-    if (is_present_wait_ready() && present_value > 0) {
+    if (is_present_wait_ready()) {
         auto wait_result = apply_present_wait(present_value);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25f927b and c260c74.

📒 Files selected for processing (14)
  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/app-window/spec.md
  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/tasks.md
  • openspec/specs/app-window/spec.md
  • openspec/specs/render-pipeline/spec.md
  • src/app/application.cpp
  • src/app/cli.hpp
  • src/app/main.cpp
  • src/render/backend/vulkan_backend.cpp
  • src/render/backend/vulkan_backend.hpp
  • src/util/config.cpp
  • src/util/config.hpp
  • tests/util/test_config.cpp
✅ Files skipped from review due to trivial changes (3)
  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/render-pipeline/spec.md
  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/tasks.md
  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/specs/app-window/spec.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/util/config.hpp
  • tests/util/test_config.cpp
  • src/app/main.cpp
🧰 Additional context used
📓 Path-based instructions (3)
openspec/**/*.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

openspec/**/*.md: Use #### Scenario: format (4 hashtags) for scenario headers in requirements, not bullets or bold text, with at least one scenario per requirement
Use format - **WHEN** [condition] - **THEN** [expected result] for scenario steps in requirements

Files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • openspec/specs/render-pipeline/spec.md
  • openspec/specs/app-window/spec.md
openspec/changes/**/proposal.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

List spec deltas in proposal.md under 'What Changes' section, marking breaking changes with BREAKING

Files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
openspec/specs/**/*.md

📄 CodeRabbit inference engine (openspec/AGENTS.md)

Use SHALL/MUST for normative requirements in specifications, avoiding 'should' or 'may' unless intentionally non-normative

Files:

  • openspec/specs/render-pipeline/spec.md
  • openspec/specs/app-window/spec.md
🧠 Learnings (20)
📓 Common learnings
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Testing scope: Only non-GPU logic tested initially. In scope: utility functions, configuration parsing, error handling, pipeline graph logic. Out of scope: Vulkan initialization, GPU resource management, rendering, capture layer.
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • src/app/application.cpp
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • src/app/application.cpp
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Per-frame code paths on main thread MUST NOT: perform dynamic memory allocations (`new`, `malloc`, `std::make_shared`); use blocking synchronization primitives (`std::mutex`, `std::condition_variable`); exceed 8ms CPU time budget (excluding GPU sync).

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.cpp
  • src/util/config.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • src/app/application.cpp
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • src/app/application.cpp
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Vulkan capture layer is the primary frame capture method, intercepting rendered frames before presentation for zero-copy access

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Testing scope: Only non-GPU logic tested initially. In scope: utility functions, configuration parsing, error handling, pipeline graph logic. Out of scope: Vulkan initialization, GPU resource management, rendering, capture layer.

Applied to files:

  • openspec/changes/archive/2026-01-10-update-present-wait-frame-pacing/proposal.md
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp (C++ bindings), NOT raw Vulkan C API. Use `vk::` types (e.g., `vk::Instance`, `vk::Device`). Do not use raw C handles like `VkInstance`, `VkDevice`. Required configuration: `#define VULKAN_HPP_NO_EXCEPTIONS`, `#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1`, `#include <vulkan/vulkan.hpp>`.

Applied to files:

  • src/app/application.cpp
  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp with `VULKAN_HPP_NO_EXCEPTIONS` and dynamic dispatch

Applied to files:

  • src/app/application.cpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-19T08:54:18.412Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-12-19T08:54:18.412Z
Learning: Applies to openspec/changes/**/specs/**/*.md : When modifying existing requirements in a MODIFIED delta, paste the full requirement block including the header and all scenarios, as the archiver will replace the entire requirement

Applied to files:

  • openspec/specs/render-pipeline/spec.md
📚 Learning: 2025-12-19T08:54:18.412Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/AGENTS.md:0-0
Timestamp: 2025-12-19T08:54:18.412Z
Learning: Applies to openspec/changes/**/specs/**/*.md : Use ADDED for new capabilities that can stand alone; use MODIFIED for changes to existing requirement behavior, scope, or acceptance criteria; use RENAMED for name-only changes

Applied to files:

  • openspec/specs/render-pipeline/spec.md
  • openspec/specs/app-window/spec.md
📚 Learning: 2026-01-07T07:20:57.053Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.053Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer logging constraints: Use only `error` and `critical` levels. No file I/O or blocking operations in hot paths. Prefix all logs with `[goggles_vklayer]`. Never log in `vkQueuePresentKHR` hot path.

Applied to files:

  • openspec/specs/render-pipeline/spec.md
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : All vulkan-hpp calls returning `vk::Result` must be explicitly checked. Prohibited pattern: `static_cast<void>(device.waitIdle())`. Use macro `VK_TRY(call, ErrorCode, "message")` for vk::Result early return or manual pattern with error handling.

Applied to files:

  • openspec/specs/render-pipeline/spec.md
  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : RAII handle guidelines for Vulkan resources: Use `vk::Unique*` only for appropriate resource types: Instance, Device, Surface (long-lived singletons); Swapchain, Pipelines, Layouts (created once). Use plain `vk::` handles for command buffers (pooled lifetime), per-frame sync primitives (reused), and imported external images (requires explicit sync).

Applied to files:

  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Default to single-threaded render loop on main thread; use central job system (goggles::util::JobSystem) for any task parallelism

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.cpp : All concurrent processing MUST use the project's central job system (`goggles::util::JobSystem`). Phase 1: BS::thread_pool. Phase 2: Taskflow. Direct use of `std::thread` or `std::jthread` for pipeline work is PROHIBITED. Exception: External integration code (networking, IPC) outside real-time path may use `std::jthread` with RAII.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/app/**/*.{cpp,hpp} : Do not perform dynamic memory allocations or use blocking synchronization primitives in per-frame main render loop code paths

Applied to files:

  • src/render/backend/vulkan_backend.cpp
🧬 Code graph analysis (3)
src/app/application.cpp (1)
src/render/backend/vulkan_backend.cpp (2)
  • create (170-213)
  • create (170-172)
src/render/backend/vulkan_backend.cpp (2)
src/render/backend/vulkan_backend.hpp (1)
  • present_id (127-127)
src/util/error.hpp (1)
  • make_error (50-52)
src/util/config.cpp (1)
src/util/error.hpp (1)
  • make_error (50-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Analysis (clang-tidy)
  • GitHub Check: Build and Test (test preset)
🔇 Additional comments (9)
src/util/config.cpp (1)

64-69: LGTM! Validation correctly allows uncapped FPS.

The change from fps <= 0 to fps < 0 correctly permits 0 as a valid value for uncapped frame pacing. The error message clearly communicates the intent, and the cast from int64_t to uint32_t is safe since the validation ensures fps >= 0.

src/app/cli.hpp (1)

7-7: LGTM! CLI option properly structured.

The std::optional<uint32_t> type correctly represents an optional override, and the range validation CLI::Range(0u, 1000u) matches the config validation. The description clearly indicates that 0 means uncapped.

Also applies to: 21-21, 63-64

openspec/specs/app-window/spec.md (1)

92-106: LGTM! Specification follows coding guidelines.

The new requirement uses the correct format with #### Scenario: (4 hashtags), SHALL for normative requirements, and the - **WHEN** / - **THEN** structure. Both scenarios clearly specify the expected behavior for CLI override and uncapped mode.

As per coding guidelines, the specification format is correct.

openspec/specs/render-pipeline/spec.md (1)

823-851: LGTM! Comprehensive frame pacing specification.

The new requirement properly documents the present wait frame pacing behavior with clear fallback logic. The scenarios cover:

  • Present wait with FIFO when extension is supported
  • Uncapped behavior when target_fps = 0
  • MAILBOX + CPU throttling fallback when present_wait is unavailable
  • FIFO as ultimate fallback if MAILBOX is also unavailable

The specification follows coding guidelines with proper use of SHALL and scenario formatting.

As per coding guidelines, the specification structure is correct.

src/render/backend/vulkan_backend.hpp (2)

23-27: LGTM! Well-structured render settings.

The RenderSettings struct effectively consolidates render configuration parameters. The default target_fps = 60 provides a reasonable fallback, and the struct aligns with the broader API simplification of passing a single settings object instead of multiple parameters.


91-94: LGTM! Present wait integration follows project patterns.

The present wait capability tracking and frame pacing methods are appropriately designed:

  • is_present_wait_ready() checks extension support capability
  • update_target_fps() correctly resets timing state when FPS changes
  • apply_present_wait() returns Result<void> for proper error propagation with manual error checking pattern
  • New members (m_present_wait_supported, m_target_fps, m_present_id, m_last_present_time) are POD types at the end, which doesn't affect destruction order

The blocking nature of apply_present_wait (calling vkWaitForPresentKHR) and throttle_present (using sleep_for) is acceptable on the main thread for frame pacing, as these are intentional synchronization/throttling operations, not I/O or computation. Error handling is properly implemented at call sites, target_fps = 0 correctly yields uncapped behavior with UINT64_MAX timeout, and RenderSettings is properly constructed from Config during initialization.

src/app/application.cpp (1)

57-63: LGTM - Clean API refactoring.

The consolidation of render configuration into a RenderSettings struct improves the VulkanBackend::create API by reducing parameter count and making future extensions easier.

src/render/backend/vulkan_backend.cpp (2)

625-629: Verify FIFO + CPU throttle interaction when target_fps differs from display refresh rate.

When is_present_wait_ready() is false and MAILBOX is unavailable, FIFO mode is used with CPU-side throttling. FIFO inherently paces to display vsync, so ensure the CPU throttle in throttle_present() doesn't conflict with FIFO's natural pacing when target_fps doesn't match the display refresh rate (e.g., 60 FPS target on 144Hz display).

Based on learnings, this PR introduces frame pacing that affects the main thread's render loop timing.


1361-1382: Consider the main thread blocking implications of CPU-side throttling.

The throttle_present() function calls std::this_thread::sleep_until() (line 1377), which blocks the main thread for up to ~16.67ms at 60 FPS. While this is intentional frame pacing (CPU-side fallback when VK_KHR_present_wait is unavailable), ensure this aligns with the application's threading model and doesn't interfere with other main thread responsibilities like event processing.

The implementation correctly handles the case where frame processing exceeds the target duration (lines 1376-1381), preventing a catch-up spiral.

Based on learnings, this is a per-frame code path on the main thread that introduces blocking sleep. While intentional for frame pacing, verify this is acceptable for the application's event loop and input responsiveness.

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: 0

🧹 Nitpick comments (2)
src/render/backend/vulkan_backend.cpp (2)

1349-1362: Reconsider treating VK_TIMEOUT as success.

The implementation treats vk::Result::eTimeout as success (line 1357-1358), allowing the frame loop to continue. While this prevents indefinite hangs (as noted in the commit message), silently ignoring timeouts may mask presentation issues or compositor problems.

Consider logging a warning when timeout occurs, as it indicates the present may not have completed within the expected frame time, which could signal a problem that warrants investigation.

💡 Suggested improvement
     auto wait_result = static_cast<vk::Result>(VULKAN_HPP_DEFAULT_DISPATCHER.vkWaitForPresentKHR(
         *m_device, *m_swapchain, present_id, timeout_ns));
-    if (wait_result == vk::Result::eSuccess || wait_result == vk::Result::eTimeout) {
+    if (wait_result == vk::Result::eSuccess) {
         return {};
     }
+    if (wait_result == vk::Result::eTimeout) {
+        GOGGLES_LOG_WARN("vkWaitForPresentKHR timed out after {}ms", timeout_ns / 1'000'000);
+        return {};
+    }
     return make_error<void>(ErrorCode::vulkan_device_lost,
                             "vkWaitForPresentKHR failed: " + vk::to_string(wait_result));

1364-1385: Document the trade-off of CPU-based throttling fallback.

This CPU-based frame pacing is the fallback when VK_KHR_present_wait is unavailable. The std::this_thread::sleep_until blocks the main render thread, which conflicts with the 8ms per-frame budget from learnings. However, this is an intentional trade-off: without this throttling, MAILBOX mode would run uncapped when target_fps is set.

Consider adding a comment documenting this trade-off and noting that GPU-based pacing via VK_KHR_present_wait is preferred when available.

Based on learnings, main thread threading constraints apply.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c260c74 and 02c60c2.

📒 Files selected for processing (1)
  • src/render/backend/vulkan_backend.cpp
🧰 Additional context used
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp (C++ bindings), NOT raw Vulkan C API. Use `vk::` types (e.g., `vk::Instance`, `vk::Device`). Do not use raw C handles like `VkInstance`, `VkDevice`. Required configuration: `#define VULKAN_HPP_NO_EXCEPTIONS`, `#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1`, `#include <vulkan/vulkan.hpp>`.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : All vulkan-hpp calls returning `vk::Result` must be explicitly checked. Prohibited pattern: `static_cast<void>(device.waitIdle())`. Use macro `VK_TRY(call, ErrorCode, "message")` for vk::Result early return or manual pattern with error handling.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp with `VULKAN_HPP_NO_EXCEPTIONS` and dynamic dispatch

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Testing scope: Only non-GPU logic tested initially. In scope: utility functions, configuration parsing, error handling, pipeline graph logic. Out of scope: Vulkan initialization, GPU resource management, rendering, capture layer.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Per-frame code paths on main thread MUST NOT: perform dynamic memory allocations (`new`, `malloc`, `std::make_shared`); use blocking synchronization primitives (`std::mutex`, `std::condition_variable`); exceed 8ms CPU time budget (excluding GPU sync).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/app/**/*.{cpp,hpp} : Do not perform dynamic memory allocations or use blocking synchronization primitives in per-frame main render loop code paths

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : RAII handle guidelines for Vulkan resources: Use `vk::Unique*` only for appropriate resource types: Instance, Device, Surface (long-lived singletons); Swapchain, Pipelines, Layouts (created once). Use plain `vk::` handles for command buffers (pooled lifetime), per-frame sync primitives (reused), and imported external images (requires explicit sync).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.053Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.053Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer logging constraints: Use only `error` and `critical` levels. No file I/O or blocking operations in hot paths. Prefix all logs with `[goggles_vklayer]`. Never log in `vkQueuePresentKHR` hot path.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Default to single-threaded render loop on main thread; use central job system (goggles::util::JobSystem) for any task parallelism

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.cpp : All concurrent processing MUST use the project's central job system (`goggles::util::JobSystem`). Phase 1: BS::thread_pool. Phase 2: Taskflow. Direct use of `std::thread` or `std::jthread` for pipeline work is PROHIBITED. Exception: External integration code (networking, IPC) outside real-time path may use `std::jthread` with RAII.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Vulkan capture layer is the primary frame capture method, intercepting rendered frames before presentation for zero-copy access

Applied to files:

  • src/render/backend/vulkan_backend.cpp
🧬 Code graph analysis (1)
src/render/backend/vulkan_backend.cpp (3)
src/render/backend/vulkan_backend.hpp (1)
  • present_id (127-127)
src/util/error.hpp (1)
  • make_error (50-52)
src/util/config.hpp (1)
  • to_string (19-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Analysis (clang-tidy)
  • GitHub Check: Build and Test (test preset)
🔇 Additional comments (10)
src/render/backend/vulkan_backend.cpp (10)

11-11: Verify blocking sleep on main thread aligns with threading policy.

The include suggests std::this_thread::sleep_until is used for frame throttling (line 1380). Based on learnings, the main thread MUST NOT use blocking synchronization primitives or exceed 8ms CPU time budget in per-frame paths. When throttle_present() executes with a low target FPS (e.g., 30 FPS = ~33ms per frame), the sleep can significantly exceed the 8ms budget.

Consider whether this blocking approach is acceptable for the fallback path when VK_KHR_present_wait is unavailable, or if an alternative non-blocking pacing mechanism should be used.

Based on learnings, main thread threading constraints apply.


37-40: LGTM!

The optional extensions are correctly paired (VK_KHR_present_id is a prerequisite for VK_KHR_present_wait) and properly checked together during device selection.


170-190: LGTM!

The consolidation of render settings into a RenderSettings struct improves API clarity and reduces parameter count. The extraction and application of settings is straightforward.


355-452: LGTM!

The present-wait capability detection correctly requires both VK_KHR_present_id and VK_KHR_present_wait extensions, and the capability is properly propagated to the selected device.


475-559: LGTM!

The device creation correctly chains present-wait feature structs, enables the required features, and conditionally includes the optional extensions only when both are supported. The pattern follows proper Vulkan initialization order.


613-632: LGTM!

The present mode selection logic is sound: prefer FIFO with present-wait for precise frame pacing when available, otherwise prefer MAILBOX for lower latency, with FIFO as the guaranteed fallback. This aligns with the PR objectives.


669-670: LGTM!

Initialization of present-pacing state during swapchain creation is correct and uses appropriate zero values.


1316-1343: Verify the present ID increment and conditional pacing.

The present path correctly chains PresentIdKHR and increments the present ID when VK_KHR_present_wait is supported. The conditional branching between apply_present_wait() and throttle_present() implements the fallback strategy described in the PR objectives.

However, verify the implementations of apply_present_wait() and throttle_present() to ensure they handle error conditions and timeouts correctly (reviewed separately below).


1351-1354: LGTM!

The timeout calculation correctly handles both capped and uncapped modes, with the 1-second maximum providing a safety net against indefinite hangs. The integer arithmetic is safe: 1'000'000'000ULL / m_target_fps cannot overflow for reasonable FPS values (target_fps ≥ 1), and the std::min ensures the result is bounded.


489-489: Helper method implementations are present and correct. is_present_wait_ready() is a straightforward const getter in the header returning m_present_wait_supported, and update_target_fps() correctly updates the target FPS and resets the present time tracking. Both are properly defined and accessible for all usage sites.

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: 1

Caution

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

⚠️ Outside diff range comments (1)
src/render/backend/vulkan_backend.cpp (1)

1328-1356: Present-wait path doesn’t actually honor target_fps (and “uncapped” still blocks).

  • vkWaitForPresentKHR(present_id, timeout) returns as soon as the present happens; the timeout is a maximum, so this won’t enforce a lower FPS cap (e.g., 30 FPS on a 60Hz FIFO swapchain).
  • When m_target_fps == 0, apply_present_wait() still waits up to 1s every frame, which contradicts “0 = uncapped”.
  • Treating eTimeout as success can silently disable the “GPU-assisted” pacing under load.

A minimal fix is to: (1) skip all pacing when target_fps == 0, and (2) apply a real interval-based throttle even in the present-wait path (you can still use wait-for-present as a “present completed” anchor). For example:

Proposed fix (make target_fps effective; keep present-wait as an anchor)
@@
-    if (m_present_wait_supported && present_value > 0) {
-        auto wait_result = apply_present_wait(present_value);
-        if (!wait_result) {
-            return make_error<bool>(wait_result.error().code, wait_result.error().message);
-        }
-    } else {
-        throttle_present();
-    }
+    if (m_target_fps != 0) {
+        if (m_present_wait_supported && present_value > 0) {
+            auto wait_result = apply_present_wait(present_value);
+            if (!wait_result) {
+                return make_error<bool>(wait_result.error().code, wait_result.error().message);
+            }
+        }
+        // Enforce the actual FPS interval regardless of present-wait availability.
+        throttle_present();
+    }
@@
 auto VulkanBackend::apply_present_wait(uint64_t present_id) -> Result<void> {
-    constexpr uint64_t MAX_TIMEOUT_NS = 1'000'000'000ULL; // 1 second max
-    const uint64_t timeout_ns =
-        (m_target_fps == 0)
-            ? MAX_TIMEOUT_NS
-            : std::min(MAX_TIMEOUT_NS, static_cast<uint64_t>(1'000'000'000ULL / m_target_fps));
+    // Uncapped: do not introduce extra waits.
+    if (m_target_fps == 0) {
+        return {};
+    }
+
+    constexpr uint64_t MAX_TIMEOUT_NS = 1'000'000'000ULL; // avoid unbounded stalls
+    const uint64_t timeout_ns = MAX_TIMEOUT_NS;
     auto wait_result = static_cast<vk::Result>(VULKAN_HPP_DEFAULT_DISPATCHER.vkWaitForPresentKHR(
         *m_device, *m_swapchain, present_id, timeout_ns));
     if (wait_result == vk::Result::eSuccess || wait_result == vk::Result::eTimeout) {
         return {};
     }
     return make_error<void>(ErrorCode::vulkan_device_lost,
                             "vkWaitForPresentKHR failed: " + vk::to_string(wait_result));
 }

Also: std::this_thread::sleep_until() is now in the per-frame hot path. That’s probably intended for the MAILBOX/no-present-wait fallback, but please sanity-check this against the “main thread must not block” guidance (frame pacing may be the exception, but it should be explicit).
Based on learnings, per-frame main-thread blocking should be minimized/justified.

Also applies to: 1361-1397

🤖 Fix all issues with AI agents
In @src/render/backend/vulkan_backend.hpp:
- Around line 91-95: The pacing currently treats 0 as “uncapped” but still
allows apply_present_wait() and throttle_present() to introduce waits; change
both functions (apply_present_wait and throttle_present) to immediately no-op
when m_target_fps == 0 and update every call site (including the places
referenced around lines 125-127) to skip calling them when m_target_fps == 0 so
uncapped mode truly performs no extra waits; keep the existing reset of
m_last_present_time in update_target_fps but ensure it does not re-enable waits
for target_fps == 0.
🧹 Nitpick comments (2)
src/render/backend/vulkan_backend.cpp (1)

355-416: GPU selection ignores present-wait capability when choosing between “suitable” devices.
Right now a discrete GPU without present-wait will always beat an integrated GPU that supports it, even if the user explicitly cares about pacing. Consider adding a small score bump for present_wait_supported (or a config/CLI preference) so the feature is actually preferred when available.
Based on learnings, render-thread behavior should stay predictable and policy-driven.

Also applies to: 449-453

src/render/backend/vulkan_backend.hpp (1)

9-34: RenderSettings API change is clean; consider the default for target_fps.
Header defaults RenderSettings::target_fps = 60 while m_target_fps defaults to 0. That’s fine as long as all creation paths call update_target_fps() (they do in this PR), but it’s easy to regress later—consider initializing m_target_fps to 60 as well, or documenting the invariant (“always set via update_target_fps on create”).

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02c60c2 and aa8079f.

📒 Files selected for processing (2)
  • src/render/backend/vulkan_backend.cpp
  • src/render/backend/vulkan_backend.hpp
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp (C++ bindings), NOT raw Vulkan C API. Use `vk::` types (e.g., `vk::Instance`, `vk::Device`). Do not use raw C handles like `VkInstance`, `VkDevice`. Required configuration: `#define VULKAN_HPP_NO_EXCEPTIONS`, `#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1`, `#include <vulkan/vulkan.hpp>`.

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Testing scope: Only non-GPU logic tested initially. In scope: utility functions, configuration parsing, error handling, pipeline graph logic. Out of scope: Vulkan initialization, GPU resource management, rendering, capture layer.

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : All vulkan-hpp calls returning `vk::Result` must be explicitly checked. Prohibited pattern: `static_cast<void>(device.waitIdle())`. Use macro `VK_TRY(call, ErrorCode, "message")` for vk::Result early return or manual pattern with error handling.

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.

Applied to files:

  • src/render/backend/vulkan_backend.hpp
  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : RAII handle guidelines for Vulkan resources: Use `vk::Unique*` only for appropriate resource types: Instance, Device, Surface (long-lived singletons); Swapchain, Pipelines, Layouts (created once). Use plain `vk::` handles for command buffers (pooled lifetime), per-frame sync primitives (reused), and imported external images (requires explicit sync).

Applied to files:

  • src/render/backend/vulkan_backend.hpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp with `VULKAN_HPP_NO_EXCEPTIONS` and dynamic dispatch

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Per-frame code paths on main thread MUST NOT: perform dynamic memory allocations (`new`, `malloc`, `std::make_shared`); use blocking synchronization primitives (`std::mutex`, `std::condition_variable`); exceed 8ms CPU time budget (excluding GPU sync).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/app/**/*.{cpp,hpp} : Do not perform dynamic memory allocations or use blocking synchronization primitives in per-frame main render loop code paths

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Default to single-threaded render loop on main thread; use central job system (goggles::util::JobSystem) for any task parallelism

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.cpp : All concurrent processing MUST use the project's central job system (`goggles::util::JobSystem`). Phase 1: BS::thread_pool. Phase 2: Taskflow. Direct use of `std::thread` or `std::jthread` for pipeline work is PROHIBITED. Exception: External integration code (networking, IPC) outside real-time path may use `std::jthread` with RAII.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Vulkan capture layer is the primary frame capture method, intercepting rendered frames before presentation for zero-copy access

Applied to files:

  • src/render/backend/vulkan_backend.cpp
🧬 Code graph analysis (2)
src/render/backend/vulkan_backend.hpp (2)
src/util/config.hpp (1)
  • nodiscard (19-31)
src/render/chain/pass.hpp (1)
  • nodiscard (54-130)
src/render/backend/vulkan_backend.cpp (2)
src/render/backend/vulkan_backend.hpp (1)
  • present_id (125-125)
src/util/error.hpp (1)
  • make_error (50-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Analysis (clang-tidy)
  • GitHub Check: Build and Test (test preset)
🔇 Additional comments (5)
src/render/backend/vulkan_backend.cpp (4)

37-41: Optional extension list looks correct (present_id + present_wait as a pair).
Good to keep these as an explicit bundle since present-wait depends on present-id.


170-191: RenderSettings plumb-through is straightforward.
update_target_fps() resetting m_last_present_time on create is a good touch for deterministic pacing state.


625-642: Present-mode fallback matches the PR description.
FIFO when present-wait is ready, otherwise MAILBOX if supported, else FIFO. Resetting m_present_id / m_last_present_time on swapchain creation is correct.

Also applies to: 681-683


482-571: No raw dispatcher calls to refactor in this section — code is already vulkan-hpp compliant.

The code at lines 482–571 correctly uses vulkan-hpp throughout (getFeatures2(), createDeviceUnique(), getQueue(), waitIdle()). The VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_device) call is the proper pattern for initializing the dynamic dispatcher with vulkan-hpp when VULKAN_HPP_DISPATCH_LOADER_DYNAMIC is enabled. No changes needed here.

src/render/backend/vulkan_backend.hpp (1)

181-185: Present-wait state additions look reasonable.
m_present_id and m_last_present_time are the right minimal state for pacing; just ensure they’re reset on swapchain recreation and on target-fps changes (both appear covered in this PR).

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: 0

🧹 Nitpick comments (1)
src/render/backend/vulkan_backend.cpp (1)

1371-1384: Verify timeout behavior with mismatched refresh rates.

When m_target_fps differs significantly from the display refresh rate, the timeout (line 1376: 1'000'000'000ULL / m_target_fps) may frequently trigger. For example, with target_fps=60 on a 30Hz display, the ~16.67ms timeout will expire before the ~33.33ms present completes.

Since eTimeout returns success, this won't break functionality, but it may reduce the effectiveness of present-wait pacing. Consider using a longer timeout (e.g., 2-3x frame duration or a fixed value like 100ms) to accommodate display variance while still protecting against hangs.

💡 Suggested adjustment
-    const uint64_t timeout_ns =
-        (m_target_fps == 0)
-            ? MAX_TIMEOUT_NS
-            : std::min(MAX_TIMEOUT_NS, static_cast<uint64_t>(1'000'000'000ULL / m_target_fps));
+    // Use 3x frame duration to accommodate display refresh variance
+    constexpr uint64_t MIN_TIMEOUT_NS = 100'000'000ULL; // 100ms floor
+    const uint64_t timeout_ns =
+        (m_target_fps == 0)
+            ? MAX_TIMEOUT_NS
+            : std::min(MAX_TIMEOUT_NS,
+                       std::max(MIN_TIMEOUT_NS, static_cast<uint64_t>(3'000'000'000ULL / m_target_fps)));
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa8079f and f30728a.

📒 Files selected for processing (1)
  • src/render/backend/vulkan_backend.cpp
🧰 Additional context used
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : All vulkan-hpp calls returning `vk::Result` must be explicitly checked. Prohibited pattern: `static_cast<void>(device.waitIdle())`. Use macro `VK_TRY(call, ErrorCode, "message")` for vk::Result early return or manual pattern with error handling.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp (C++ bindings), NOT raw Vulkan C API. Use `vk::` types (e.g., `vk::Instance`, `vk::Device`). Do not use raw C handles like `VkInstance`, `VkDevice`. Required configuration: `#define VULKAN_HPP_NO_EXCEPTIONS`, `#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1`, `#include <vulkan/vulkan.hpp>`.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp with `VULKAN_HPP_NO_EXCEPTIONS` and dynamic dispatch

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Testing scope: Only non-GPU logic tested initially. In scope: utility functions, configuration parsing, error handling, pipeline graph logic. Out of scope: Vulkan initialization, GPU resource management, rendering, capture layer.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Per-frame code paths on main thread MUST NOT: perform dynamic memory allocations (`new`, `malloc`, `std::make_shared`); use blocking synchronization primitives (`std::mutex`, `std::condition_variable`); exceed 8ms CPU time budget (excluding GPU sync).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/app/**/*.{cpp,hpp} : Do not perform dynamic memory allocations or use blocking synchronization primitives in per-frame main render loop code paths

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Default to single-threaded render loop on main thread; use central job system (goggles::util::JobSystem) for any task parallelism

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.cpp : All concurrent processing MUST use the project's central job system (`goggles::util::JobSystem`). Phase 1: BS::thread_pool. Phase 2: Taskflow. Direct use of `std::thread` or `std::jthread` for pipeline work is PROHIBITED. Exception: External integration code (networking, IPC) outside real-time path may use `std::jthread` with RAII.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Vulkan capture layer is the primary frame capture method, intercepting rendered frames before presentation for zero-copy access

Applied to files:

  • src/render/backend/vulkan_backend.cpp
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and Test (test preset)
  • GitHub Check: Static Analysis (clang-tidy)
🔇 Additional comments (8)
src/render/backend/vulkan_backend.cpp (8)

37-41: LGTM!

The optional extension list correctly pairs VK_KHR_PRESENT_ID and VK_KHR_PRESENT_WAIT together, as present-wait requires present-id functionality.


170-190: LGTM!

The consolidated RenderSettings parameter simplifies the API and ensures all render settings are initialized atomically during backend creation.


403-416: LGTM!

The extension detection correctly requires both VK_KHR_PRESENT_ID and VK_KHR_PRESENT_WAIT to be present, as the latter depends on the former.


499-508: LGTM!

Good defensive validation—the code correctly verifies that both presentId and presentWait features are actually enabled by the driver, not just that the extensions are advertised. The warning at line 503 provides helpful diagnostics when the fallback is triggered.


625-652: LGTM!

The present mode selection strategy is well-designed:

  • FIFO + present-wait for GPU-assisted frame pacing
  • Mailbox + CPU throttle as fallback for reduced input latency
  • FIFO without pacing as last resort

The strategy logging aids runtime diagnostics.


691-692: LGTM!

Correctly resets present-wait state on swapchain recreation to prevent stale present IDs and ensure the first-frame detection in throttle_present() works properly.


1338-1366: LGTM!

The present-id submission path is correctly implemented. The present_value > 0 check at line 1358 is technically redundant (since ++m_present_id guarantees a positive value), but it's harmless and provides clarity.


1386-1407: Verify intentional main-thread sleep aligns with project threading policy.

std::this_thread::sleep_until (line 1402) blocks the main thread, which the project learnings indicate should be avoided for per-frame code paths. However, this is the deliberate fallback frame-pacing mechanism when VK_KHR_present_wait is unavailable.

Please confirm this intentional blocking is acceptable given that:

  1. It only activates when GPU-assisted pacing is unavailable
  2. Without it, MAILBOX mode would spin at uncapped rates
  3. The sleep duration is bounded by the target frame interval

If the policy strictly prohibits main-thread blocking, an alternative would be to yield to SDL event processing in a polling loop, though this may be less precise. Based on learnings, main thread blocking should be avoided.

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: 1

🤖 Fix all issues with AI agents
In @src/render/backend/vulkan_backend.cpp:
- Around line 1373-1388: VulkanBackend::apply_present_wait currently treats
vk::Result::eSuboptimalKHR, eErrorOutOfDateKHR and eErrorSurfaceLostKHR as
fatal; update logic so eSuccess, eTimeout and eSuboptimalKHR are treated as
success (return {}), while eErrorOutOfDateKHR and eErrorSurfaceLostKHR set
m_needs_resize = true and return {} to trigger swapchain recreation, and any
other non-success result still returns
make_error<void>(ErrorCode::vulkan_device_lost, ...) with the
vk::to_string(wait_result); locate the vkWaitForPresentKHR call and adjust the
if/else checks to explicitly handle vk::Result::eSuboptimalKHR,
vk::Result::eErrorOutOfDateKHR and vk::Result::eErrorSurfaceLostKHR as
described.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f30728a and b385b8d.

📒 Files selected for processing (1)
  • src/render/backend/vulkan_backend.cpp
🧰 Additional context used
🧠 Learnings (16)
📓 Common learnings
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Vulkan resource management: Follow RAII guidelines. Use `vk::Unique*` only for appropriate resource types. Call `device.waitIdle()` or wait on fences before destroying GPU-async resources. Store creation info with resources for debugging/recreation. Never leak Vulkan objects. Member ordering: declare in reverse destruction order (device before instance).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Main thread owns: Vulkan instance, device, swapchain lifecycle; queue submission; window events and user input; job coordination. Main thread MUST NOT: block on I/O, perform heavy computation (>1ms), allocate memory in per-frame code paths.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : All vulkan-hpp calls returning `vk::Result` must be explicitly checked. Prohibited pattern: `static_cast<void>(device.waitIdle())`. Use macro `VK_TRY(call, ErrorCode, "message")` for vk::Result early return or manual pattern with error handling.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Use `vk::Unique*` for long-lived Vulkan resources; use plain handles for per-frame/GPU-async resources

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/capture/vk_layer/**/*.{cpp,hpp} : Do not perform blocking operations or file I/O in capture layer performance-critical paths (e.g., vkQueuePresentKHR)

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp (C++ bindings), NOT raw Vulkan C API. Use `vk::` types (e.g., `vk::Instance`, `vk::Device`). Do not use raw C handles like `VkInstance`, `VkDevice`. Required configuration: `#define VULKAN_HPP_NO_EXCEPTIONS`, `#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1`, `#include <vulkan/vulkan.hpp>`.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Testing scope: Only non-GPU logic tested initially. In scope: utility functions, configuration parsing, error handling, pipeline graph logic. Out of scope: Vulkan initialization, GPU resource management, rendering, capture layer.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/render/**/*.{cpp,hpp} : Application code MUST use vulkan-hpp with `VULKAN_HPP_NO_EXCEPTIONS` and dynamic dispatch

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Per-frame code paths on main thread MUST NOT: perform dynamic memory allocations (`new`, `malloc`, `std::make_shared`); use blocking synchronization primitives (`std::mutex`, `std::condition_variable`); exceed 8ms CPU time budget (excluding GPU sync).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/capture/vk_layer/**/*.{cpp,hpp} : Vulkan capture layer threading: Hooks execute in calling thread (usually render thread). No blocking in hooks, especially `vkQueuePresentKHR` hot path. Use atomics or locks for thread-safe layer state.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Applies to openspec/src/app/**/*.{cpp,hpp} : Do not perform dynamic memory allocations or use blocking synchronization primitives in per-frame main render loop code paths

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/src/render/**/*.cpp : Single-threaded render loop on main thread by default. Render backend (`goggles::render`) runs on main thread. Pipeline execution runs on main thread. Job system for non-render work on worker threads. Threading introduced only when profiling justifies it (main thread CPU consistently >8ms).

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Default to single-threaded render loop on main thread; use central job system (goggles::util::JobSystem) for any task parallelism

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2026-01-07T07:20:57.054Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: docs/project_policies.md:0-0
Timestamp: 2026-01-07T07:20:57.054Z
Learning: Applies to docs/**/*.cpp : All concurrent processing MUST use the project's central job system (`goggles::util::JobSystem`). Phase 1: BS::thread_pool. Phase 2: Taskflow. Direct use of `std::thread` or `std::jthread` for pipeline work is PROHIBITED. Exception: External integration code (networking, IPC) outside real-time path may use `std::jthread` with RAII.

Applied to files:

  • src/render/backend/vulkan_backend.cpp
📚 Learning: 2025-12-29T15:44:30.520Z
Learnt from: CR
Repo: K1ngst0m/Goggles PR: 0
File: openspec/project.md:0-0
Timestamp: 2025-12-29T15:44:30.520Z
Learning: Vulkan capture layer is the primary frame capture method, intercepting rendered frames before presentation for zero-copy access

Applied to files:

  • src/render/backend/vulkan_backend.cpp
🧬 Code graph analysis (1)
src/render/backend/vulkan_backend.cpp (4)
src/app/sdl_platform.hpp (1)
  • create_info (26-26)
src/render/backend/vulkan_backend.hpp (1)
  • present_id (125-125)
src/util/error.hpp (1)
  • make_error (50-52)
src/util/config.hpp (1)
  • to_string (19-19)
🔇 Additional comments (9)
src/render/backend/vulkan_backend.cpp (9)

11-11: LGTM!

Using <thread> for std::this_thread::sleep_until in CPU-based frame throttling is appropriate and doesn't violate the project policy against direct thread creation for pipeline work.


37-41: LGTM!

Clean separation of optional present-wait extensions from required device extensions.


170-191: LGTM!

The API consolidation to RenderSettings is clean and allows for easier future extension. Initialization of settings members looks correct.


403-416: LGTM!

Present-wait detection correctly requires both VK_KHR_PRESENT_ID and VK_KHR_PRESENT_WAIT extensions to be available.


499-508: LGTM!

Excellent defensive validation - checking that present-wait features are actually enabled even when extensions are present, with informative fallback logging.


627-651: LGTM!

Present mode selection logic correctly implements the priority: FIFO with present-wait when supported, MAILBOX with CPU throttle as fallback, and plain FIFO as last resort. The logging is helpful for debugging pacing issues.


691-692: LGTM!

Correctly resets present-wait state when swapchain is recreated, preventing stale present IDs from being used with the new swapchain.


1338-1367: LGTM!

Present ID handling is correctly implemented. The VkPresentIdKHR struct is properly chained only when present-wait is supported, and the pacing logic correctly branches between uncapped, present-wait, and CPU throttle modes.


1390-1411: LGTM!

The CPU-based frame throttle is a reasonable fallback when present-wait isn't available. The sleep-based pacing is acceptable per project policies, which specifically prohibit blocking on I/O and synchronization primitives—intentional frame timing via sleep_until is a valid use case.

One minor note: the first-frame check at line 1398 using time_since_epoch().count() == 0 works correctly for default-constructed time_point, though an explicit flag or std::optional could be slightly more readable. This is fine as-is.

@K1ngst0m K1ngst0m force-pushed the dev/improve-frame-pacing branch from b385b8d to 88a826b Compare January 10, 2026 13:39
@K1ngst0m K1ngst0m merged commit c19edaa into main Jan 10, 2026
4 checks passed
@K1ngst0m K1ngst0m deleted the dev/improve-frame-pacing branch January 10, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant