Skip to content

chore(openspec): complete and archive openspec proposal change#98

Merged
K1ngst0m merged 2 commits intomainfrom
dev/complete-openspec
Feb 27, 2026
Merged

chore(openspec): complete and archive openspec proposal change#98
K1ngst0m merged 2 commits intomainfrom
dev/complete-openspec

Conversation

@K1ngst0m
Copy link
Copy Markdown
Collaborator

@K1ngst0m K1ngst0m commented Feb 27, 2026

User description

  • Mark remaining verification tasks as done in add-headless-mode tasks.md
  • Archive add-headless-mode, filter-chain-state-management, drop-wsi-proxy, and wayland-native-frame-delivery under changes/archive/2026-02-27-*
  • Promote headless-mode/spec.md to canonical spec; sync deltas into app-window and render-pipeline specs

PR Type

Bug fix, Enhancement


Description

  • Fix frame number tracking in headless render loop by moving assignment earlier

  • Refactor signal handling to block SIGTERM/SIGINT before thread spawning

  • Extract windowed mode logic into separate function for clarity

  • Modernize Vulkan semaphore import code to use C++ bindings

  • Mark all headless mode verification tasks as complete


Diagram Walkthrough

flowchart LR
  A["Headless Render Loop"] -->|frame tracking fix| B["Update frame_number earlier"]
  C["Signal Handling"] -->|block before threads| D["SIGTERM/SIGINT via signalfd"]
  E["Main Function"] -->|extract logic| F["run_windowed_mode function"]
  G["Vulkan Backend"] -->|modernize API| H["C++ semaphore bindings"]
  I["Verification Tasks"] -->|mark complete| J["All 8 sections done"]
Loading

File Walkthrough

Relevant files
Bug fix
1 files
application.cpp
Move frame number update before validation checks               
+1/-2     
Enhancement
2 files
main.cpp
Signal handling and windowed mode refactoring                       
+76/-64 
vulkan_backend.cpp
Modernize Vulkan semaphore import to C++ bindings               
+4/-6     
Documentation
4 files
tasks.md
Mark remaining verification tasks as complete                       
+4/-4     
spec.md
Add headless mode requirements and filter chain control scope
+77/-3   
spec.md
Create canonical headless mode specification                         
+104/-0 
spec.md
Expand filter chain routing and add headless backend requirements
+112/-12
Tests
1 files
CMakeLists.txt
Add headless mode integration test with PNG verification 
+30/-0   
Additional files
16 files
.openspec.yaml [link]   
design.md [link]   
proposal.md [link]   
spec.md [link]   
spec.md [link]   
spec.md [link]   
proposal.md [link]   
tasks.md [link]   
.openspec.yaml [link]   
design.md [link]   
proposal.md [link]   
spec.md [link]   
spec.md [link]   
tasks.md [link]   
proposal.md [link]   
tasks.md [link]   

Summary by CodeRabbit

  • New Features

    • Headless mode CLI added (--headless, --frames, --output) to run rendering without a window, produce PNG output, and exit after a configured frame count
    • Clear error when required flags (e.g., --output with --frames) are missing
  • Documentation

    • Expanded headless-mode and render-pipeline specs and startup scenarios, including offscreen rendering and filter-chain controls
  • Tests

    • New integration tests validating headless run and output PNG existence
  • Refactor

    • Separate headless and windowed launch flows and improved process/signal handling

- Mark remaining verification tasks as done in add-headless-mode tasks.md
- Archive add-headless-mode, filter-chain-state-management, drop-wsi-proxy,
and wayland-native-frame-delivery under changes/archive/2026-02-27-*
- Promote headless-mode/spec.md to canonical spec; sync deltas into
app-window and render-pipeline specs
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Adds comprehensive headless-mode support: new CLI flags and validation, surfaceless Vulkan backend and create_headless factory, headless render/readback/export loop with signal-based shutdown, posix_spawn signal-mask attributes, test additions for PNG output, and minor Vulkan-HPP API migrations.

Changes

Cohort / File(s) Summary
Specs & Tasks
openspec/specs/headless-mode/spec.md, openspec/specs/app-window/spec.md, openspec/specs/render-pipeline/spec.md, openspec/changes/archive/2026-02-27-add-headless-mode/tasks.md
Add full headless-mode spec and CLI rules; conditional SDL/init behavior; surfaceless Vulkan backend and VulkanBackend::create_headless(RenderSettings); expanded filter-chain/control rules; test task verifications marked completed.
CLI / Process Orchestration
src/app/main.cpp
posix_spawn attributes set with POSIX_SPAWN_SETSIGMASK; headless signalfd created externally and passed into run_headless_mode; added run_windowed_mode; refactored run flow and error handling.
Application Logic
src/app/application.cpp
Consolidated frame-number update in run_headless to a single assignment right after moving presented frame.
Vulkan Backend
src/render/backend/vulkan_backend.cpp
Migrated semaphore import code from C Vulkan API to Vulkan-HPP (vk::ImportSemaphoreFdInfoKHR, device.importSemaphoreFdKHR) and corresponding enum values; preserved headless import/sync behavior and cleanup paths.
Tests & CI
tests/CMakeLists.txt
Add goggles_headless_integration and dependent PNG-existence test; working dir/timeout settings; ASAN leak detection disabled; tests skipped in CI/GitHub Actions environments.
Project Metadata
pixi.toml, .gitignore
Add dependency vulkan-tools = "1.4.328.*" to manifest; change .gitignore pattern to **/AGENTS.md (recursive ignore).

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant SigFD as SignalFD
    participant Comp as CompositorServer
    participant Backend as VulkanBackend (Surfaceless)
    participant PNG as PNG Export

    App->>SigFD: create signalfd (SIGTERM/SIGINT)
    App->>Backend: create_headless(RenderSettings)
    Backend->>Backend: Init (no SDL, no swapchain, offscreen image)
    loop frame 0..N-1
        App->>Comp: get_presented_frame()
        Comp-->>App: Presented frame (DMA-BUF fd, meta)
        App->>Backend: import DMA-BUF / import semaphore fd
        Backend->>Backend: render to offscreen image, wait fence
        Backend-->>App: frame rendered
    end
    App->>PNG: readback offscreen -> staging buffer
    PNG->>PNG: write PNG (stb_image_write_png)
    par Monitor signals
        SigFD->>App: poll/signalfd event
        alt signal received
            App->>Backend: request clean shutdown
            App->>Comp: release surfaces/resources
        end
    end
    App-->>App: exit (status)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • zhangzhousuper

Poem

🐰 Offscreen we hop, no window in sight,
Frames tumble in silence and pixels take flight,
Signals we listen, clean exits we plot,
PNGs leave the burrow—exactly on dot! 🎨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately describes the main objective: completing and archiving the openspec proposal change, which is the primary focus of this PR with multiple spec files updated, verification tasks marked complete, and headless mode promoted to canonical spec.

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

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

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.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Feb 27, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Insecure temp file path

Description: The new integration tests write and then check a predictable file path in /tmp
(/tmp/goggles_headless_test.png), which can enable a local symlink/hardlink attack
(clobbering or reading an unintended file) if the tests are ever executed with elevated
privileges or under a more-privileged user than an attacker on the same machine.
CMakeLists.txt [227-255]

Referred Code
# Integration test: headless mode end-to-end (disabled in CI — requires display server + GPU)
add_test(NAME goggles_headless_integration
         COMMAND $<TARGET_FILE:goggles>
             --headless --frames 3 --output /tmp/goggles_headless_test.png
             -- /home/kingstom/workspaces/vksdk/1.4.328.1/x86_64/bin/vkcube)

set_property(TEST goggles_headless_integration
    PROPERTY WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})

set_property(TEST goggles_headless_integration
    PROPERTY TIMEOUT 60)

set_property(TEST goggles_headless_integration
    PROPERTY ENVIRONMENT "ASAN_OPTIONS=detect_leaks=0")

# Verify the output PNG was written (depends on the run test passing first)
add_test(NAME goggles_headless_integration_png_exists
         COMMAND ${CMAKE_COMMAND} -E md5sum /tmp/goggles_headless_test.png)

set_property(TEST goggles_headless_integration_png_exists
    PROPERTY DEPENDS goggles_headless_integration)


 ... (clipped 8 lines)
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: 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: 🏷️
Ignored return codes: The new posix_spawnattr_* calls and waitpid usage do not check for failures (e.g.,
init/setsigmask/setflags/destroy return values and waitpid returning -1), which can lead
to silent misbehavior and hard-to-debug runtime issues.

Referred Code
    // Reset the signal mask in the child so blocked signals in the parent
    // (e.g. SIGTERM blocked for headless signalfd) don't affect the reaper.
    posix_spawnattr_t attr{};
    posix_spawnattr_init(&attr);
    sigset_t empty_mask{};
    sigemptyset(&empty_mask);
    posix_spawnattr_setsigmask(&attr, &empty_mask);
    posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK);
    const int rc = posix_spawn(&pid, reaper_path.c_str(), nullptr, &attr, argv.data(), envp.data());
    posix_spawnattr_destroy(&attr);
    if (rc != 0) {
        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
                                          std::string("posix_spawn() failed: ") +
                                              std::strerror(rc));
    }

    return pid;
}

static auto terminate_child(pid_t pid) -> void {
    GOGGLES_PROFILE_FUNCTION();


 ... (clipped 310 lines)

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: 🏷️
Structured logging unclear: New log lines are added via GOGGLES_LOG_*, but the diff does not show whether the logging
backend is structured (e.g., JSON) as required, so compliance cannot be verified from the
PR hunks alone.

Referred Code
if (!spawn_result) {
    GOGGLES_LOG_CRITICAL("Failed to launch target app: {} ({})", spawn_result.error().message,
                         goggles::error_code_name(spawn_result.error().code));
    return EXIT_FAILURE;
}
pid_t child_pid = spawn_result.value();
GOGGLES_LOG_INFO("Launched target app in headless mode (pid={})", child_pid);

auto headless_result = app.run_headless({
    .frames = cli_opts.frames,
    .output = cli_opts.output_path,
    .signal_fd = signal_fd.get(),
    .child_pid = child_pid,
});

terminate_child(child_pid);

if (!headless_result) {
    GOGGLES_LOG_ERROR("Headless run failed: {} ({})", headless_result.error().message,
                      goggles::error_code_name(headless_result.error().code));
    return EXIT_FAILURE;


 ... (clipped 43 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 Feb 27, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Avoid hardcoded path in integration test
Suggestion Impact:The commit removed the user-specific hardcoded vkcube path and added a find_program-based lookup (VKCUBE_EXEC). The integration tests are now only added when vkcube is found, otherwise a status message indicates the tests are skipped; it also made the output PNG path use the build directory instead of /tmp.

code diff:

+if(NOT VKCUBE_EXEC)
+    unset(VKCUBE_EXEC CACHE)
+    find_program(VKCUBE_EXEC NAMES vkcube)
+endif()
+
+set(HEADLESS_TEST_PNG "${CMAKE_CURRENT_BINARY_DIR}/goggles_headless_test.png")
+
+if(VKCUBE_EXEC)
+    add_test(NAME goggles_headless_integration
+             COMMAND $<TARGET_FILE:goggles>
+                 --headless --frames 3 --output ${HEADLESS_TEST_PNG}
+                 -- ${VKCUBE_EXEC})
+
+    set_property(TEST goggles_headless_integration
+        PROPERTY WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+
+    set_property(TEST goggles_headless_integration
+        PROPERTY TIMEOUT 60)
+
+    set_property(TEST goggles_headless_integration
+        PROPERTY ENVIRONMENT "ASAN_OPTIONS=detect_leaks=0")
+
+    # Verify the output PNG was written (depends on the run test passing first)
+    add_test(NAME goggles_headless_integration_png_exists
+             COMMAND ${CMAKE_COMMAND} -E md5sum ${HEADLESS_TEST_PNG})
+
+    set_property(TEST goggles_headless_integration_png_exists
+        PROPERTY DEPENDS goggles_headless_integration)
+
+    set_property(TEST goggles_headless_integration_png_exists
+        PROPERTY TIMEOUT 10)
+
+    if(DEFINED ENV{CI} OR DEFINED ENV{GITHUB_ACTIONS})
+        set_property(TEST goggles_headless_integration PROPERTY DISABLED TRUE)
+        set_property(TEST goggles_headless_integration_png_exists PROPERTY DISABLED TRUE)
+    endif()
+else()
+    message(STATUS "vkcube not found — skipping headless integration tests")
+endif()

Replace the hardcoded path to vkcube in the goggles_headless_integration test
with a portable solution using find_program to locate the executable.

tests/CMakeLists.txt [227-231]

 # Integration test: headless mode end-to-end (disabled in CI — requires display server + GPU)
-add_test(NAME goggles_headless_integration
-         COMMAND $<TARGET_FILE:goggles>
-             --headless --frames 3 --output /tmp/goggles_headless_test.png
-             -- /home/kingstom/workspaces/vksdk/1.4.328.1/x86_64/bin/vkcube)
+find_program(VKCUBE_EXECUTABLE vkcube)
+if(VKCUBE_EXECUTABLE)
+    add_test(NAME goggles_headless_integration
+             COMMAND $<TARGET_FILE:goggles>
+                 --headless --frames 3 --output /tmp/goggles_headless_test.png
+                 -- ${VKCUBE_EXECUTABLE})
+else()
+    message(WARNING "vkcube not found, skipping goggles_headless_integration test.")
+    add_test(NAME goggles_headless_integration COMMAND ${CMAKE_COMMAND} -E echo "Skipping: vkcube not found")
+endif()

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a hardcoded, user-specific path in a test, which would break it for any other user. The proposed fix using find_program makes the test portable and robust.

Medium
Initialize import struct via constructor
Suggestion Impact:The struct declaration was changed from default-initialization to value-initialization (`vk::ImportSemaphoreFdInfoKHR import_info{}`), aligning with the suggestion's goal of ensuring fields are initialized, though it did not switch to constructor-parameter initialization.

code diff:

-                vk::ImportSemaphoreFdInfoKHR import_info;
+                vk::ImportSemaphoreFdInfoKHR import_info{};
                 import_info.semaphore = sem;
                 import_info.handleType = vk::ExternalSemaphoreHandleTypeFlagBits::eSyncFd;
                 import_info.fd = sync_dup.get();

Initialize the vk::ImportSemaphoreFdInfoKHR struct using its constructor for
better readability and to ensure all fields are correctly set.

src/render/backend/vulkan_backend.cpp [1946-1950]

-vk::ImportSemaphoreFdInfoKHR import_info;
-import_info.semaphore = sem;
-import_info.handleType = vk::ExternalSemaphoreHandleTypeFlagBits::eSyncFd;
-import_info.fd = sync_dup.get();
-import_info.flags = vk::SemaphoreImportFlagBits::eTemporary;
+vk::ImportSemaphoreFdInfoKHR import_info{
+    sem,
+    vk::ExternalSemaphoreHandleTypeFlagBits::eSyncFd,
+    sync_dup.get(),
+    vk::SemaphoreImportFlagBits::eTemporary
+};

[Suggestion processed]

Suggestion importance[1-10]: 3

__

Why: This is a valid code style suggestion that improves readability and conciseness by using the designated vulkan.hpp constructor for initialization, which is good practice.

Low
Possible issue
Add error handling for spawn attributes
Suggestion Impact:The patch adds return-value checks for posix_spawnattr_init and posix_spawnattr_setsigmask, returning an error and destroying the spawn attributes on failure; it also checks posix_spawnattr_setflags as part of the same configuration failure path.

code diff:

     posix_spawnattr_t attr{};
-    posix_spawnattr_init(&attr);
+    if (posix_spawnattr_init(&attr) != 0) {
+        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
+                                          std::string("posix_spawnattr_init() failed: ") +
+                                              std::strerror(errno));
+    }
     sigset_t empty_mask{};
     sigemptyset(&empty_mask);
-    posix_spawnattr_setsigmask(&attr, &empty_mask);
-    posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK);
+    if (posix_spawnattr_setsigmask(&attr, &empty_mask) != 0 ||
+        posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK) != 0) {
+        posix_spawnattr_destroy(&attr);
+        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
+                                          "posix_spawnattr configuration failed");
+    }

Add return value checks for posix_spawnattr_init and posix_spawnattr_setsigmask
to handle potential failures during child process attribute setup.

src/app/main.cpp [117-124]

 posix_spawnattr_t attr{};
-posix_spawnattr_init(&attr);
+if (posix_spawnattr_init(&attr) != 0) {
+    return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
+                                      "posix_spawnattr_init() failed");
+}
+
 sigset_t empty_mask{};
 sigemptyset(&empty_mask);
-posix_spawnattr_setsigmask(&attr, &empty_mask);
+if (posix_spawnattr_setsigmask(&attr, &empty_mask) != 0) {
+    posix_spawnattr_destroy(&attr);
+    return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
+                                      "posix_spawnattr_setsigmask() failed");
+}
+
 posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK);
 const int rc = posix_spawn(&pid, reaper_path.c_str(), nullptr, &attr, argv.data(), envp.data());
 posix_spawnattr_destroy(&attr);

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies missing error handling for posix_spawnattr_* functions, which could lead to silent failures. Adding these checks improves the robustness of the new process spawning logic.

Medium
  • 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: 5

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

Inline comments:
In `@src/app/main.cpp`:
- Around line 118-124: Check the return values of posix_spawnattr_init,
posix_spawnattr_setsigmask, and posix_spawnattr_setflags before calling
posix_spawn; if any of these (called on attr) fail, log or propagate the error,
call posix_spawnattr_destroy(&attr) where appropriate, and avoid calling
posix_spawn so you don't spawn the child with an
uninitialized/partially-configured attribute object. Specifically, update the
block that uses posix_spawnattr_init(&attr), posix_spawnattr_setsigmask(&attr,
&empty_mask), and posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK) to
check each rc, handle failures (cleanup attr and return/exit), and only call
posix_spawn(&pid, reaper_path.c_str(), nullptr, &attr, argv.data(), envp.data())
when all prior calls succeeded.

In `@src/render/backend/vulkan_backend.cpp`:
- Around line 1946-1952: vk::ImportSemaphoreFdInfoKHR import_info is
default-constructed without brace/value-initialization which can leave members
uninitialized when VULKAN_HPP_NO_EXCEPTIONS is set; replace the default
construction with brace/value-initialization (e.g., vk::ImportSemaphoreFdInfoKHR
import_info{} ) so all fields are zero-initialized before you set semaphore,
handleType, fd and flags used by m_device.importSemaphoreFdKHR; update the same
pattern wherever vk::ImportSemaphoreFdInfoKHR is constructed.

In `@tests/CMakeLists.txt`:
- Around line 228-231: The test hardcodes a machine-local vkcube path and a
fixed /tmp output file; update the test so it discovers the vkcube executable
and writes the artifact into the build/test output area. Use CMake's
find_program or an overridable cache variable (e.g. set(VKCUBE_EXEC CACHE
FILEPATH ...) / find_program(VKCUBE_EXEC NAMES vkcube)) and replace the literal
"/home/..." with ${VKCUBE_EXEC} in the goggles_headless_integration add_test
invocation; similarly replace "/tmp/goggles_headless_test.png" with a path under
the build tree (e.g. ${CMAKE_CURRENT_BINARY_DIR} or
${CMAKE_BINARY_DIR}/tests/goggles_headless_test.png) or a per-test variable, and
apply the same changes to the other test at lines 243-244 so tests are portable
and avoid artifact collisions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 697e06d and 874426b.

📒 Files selected for processing (25)
  • .gitignore
  • openspec/changes/archive/2026-02-27-add-headless-mode/.openspec.yaml
  • openspec/changes/archive/2026-02-27-add-headless-mode/design.md
  • openspec/changes/archive/2026-02-27-add-headless-mode/proposal.md
  • openspec/changes/archive/2026-02-27-add-headless-mode/specs/app-window/spec.md
  • openspec/changes/archive/2026-02-27-add-headless-mode/specs/headless-mode/spec.md
  • openspec/changes/archive/2026-02-27-add-headless-mode/specs/render-pipeline/spec.md
  • openspec/changes/archive/2026-02-27-add-headless-mode/tasks.md
  • openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/proposal.md
  • openspec/changes/archive/2026-02-27-drop-wsi-proxy-simplify-capture/tasks.md
  • openspec/changes/archive/2026-02-27-update-filter-chain-state-management/.openspec.yaml
  • openspec/changes/archive/2026-02-27-update-filter-chain-state-management/design.md
  • openspec/changes/archive/2026-02-27-update-filter-chain-state-management/proposal.md
  • openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/app-window/spec.md
  • openspec/changes/archive/2026-02-27-update-filter-chain-state-management/specs/render-pipeline/spec.md
  • openspec/changes/archive/2026-02-27-update-filter-chain-state-management/tasks.md
  • openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/proposal.md
  • openspec/changes/archive/2026-02-27-wayland-native-frame-delivery/tasks.md
  • openspec/specs/app-window/spec.md
  • openspec/specs/headless-mode/spec.md
  • openspec/specs/render-pipeline/spec.md
  • src/app/application.cpp
  • src/app/main.cpp
  • src/render/backend/vulkan_backend.cpp
  • tests/CMakeLists.txt

Comment on lines +208 to +212
#### Scenario: First discovery defaults ON for direct Vulkan sessions
- **GIVEN** a direct Vulkan capture session and a newly discovered active surface
- **WHEN** the surface appears in Surface List without user override
- **THEN** its per-surface filter toggle SHALL default to enabled

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface defaulting rule conflicts with current runtime behavior.

This scenario states newly discovered direct-Vulkan surfaces default to enabled, but runtime logic currently initializes new surface filter state to disabled (src/app/application.cpp, Line 515). Please align spec or implementation to a single rule.

Comment on lines +1487 to +1489
### Requirement: Surfaceless VulkanBackend Factory
`VulkanBackend` SHALL provide a `create_headless(RenderSettings) -> ResultPtr<VulkanBackend>` static factory that creates a Vulkan instance, selects a physical device, and creates a logical device and queue without requiring a `vk::SurfaceKHR`. This factory SHALL NOT create a swapchain, present semaphores, or frame-pacing resources.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Headless factory signature in spec is out of sync with the implemented API.

The spec requires create_headless(RenderSettings), but the current implementation takes additional parameters (enable_validation, shader_dir, cache_dir, settings). Please reconcile the canonical signature.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@src/app/main.cpp`:
- Around line 118-129: posix_spawnattr_* calls return error codes directly, so
change the error handling in the posix_spawnattr_init/setsigmask/setflags
sequence to capture each function's return value and use that value with
std::strerror() in the error message instead of reading errno; specifically, in
the posix_spawnattr_init() branch capture the int rc (from
posix_spawnattr_init), and in the two subsequent calls call
posix_spawnattr_setsigmask(&attr, &empty_mask) and
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK) into separate int
rc1/rc2 variables, check each independently, call posix_spawnattr_destroy(&attr)
on failure, and return
goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
"posix_spawnattr_setsigmask() failed: " + std::string(std::strerror(rc1))) or
similarly for setflags using rc2 so the actual return codes are reported; update
references to attr and the existing make_error calls accordingly.
- Around line 430-440: The loop's waitpid call can return -1 and currently gets
ignored; update the child-watching logic around waitpid(child_pid,
&child_status, WNOHANG) to handle negative returns: if result == -1 and errno ==
EINTR, simply retry (continue) the wait loop; if result == -1 for any other
errno (including ECHILD), treat the child as exited by setting child_exited =
true and calling push_quit_event(); keep the existing handling when result ==
child_pid. Reference symbols: waitpid, child_exited, child_pid, child_status,
push_quit_event, errno, EINTR, ECHILD.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 874426b and 05523b7.

⛔ Files ignored due to path filters (1)
  • pixi.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • pixi.toml
  • src/app/main.cpp
  • src/render/backend/vulkan_backend.cpp
  • tests/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/CMakeLists.txt

Comment on lines +118 to +129
if (posix_spawnattr_init(&attr) != 0) {
return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
std::string("posix_spawnattr_init() failed: ") +
std::strerror(errno));
}
sigset_t empty_mask{};
sigemptyset(&empty_mask);
if (posix_spawnattr_setsigmask(&attr, &empty_mask) != 0 ||
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK) != 0) {
posix_spawnattr_destroy(&attr);
return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
"posix_spawnattr configuration failed");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

POSIX posix_spawnattr_init posix_spawnattr_setsigmask posix_spawnattr_setflags return error code directly or errno

💡 Result:

For these POSIX spawn-attribute functions:

  • posix_spawnattr_init()
  • posix_spawnattr_setsigmask()
  • posix_spawnattr_setflags()
  • (and the other posix_spawnattr_set*() / get*() style routines)

they return the error number directly:

  • Return 0 on success
  • Return an error number (an errno-style value such as EINVAL, ENOMEM, etc.) on failure

So you generally do not check -1 or rely on errno; you check the function’s return value and treat it as the error code. This is explicitly stated in the man pages (“return an error number from <errno.h>” / “otherwise, an error number shall be returned”). [1], [2]

Practical rule:

int rc = posix_spawnattr_setflags(&attr, flags);
if (rc != 0) {
    // rc is the error code (errno value). You can do: strerror(rc)
}

errno may or may not be modified by a given implementation, but POSIX only guarantees the returned error number for these interfaces. [1], [2]

Sources: Apple man page for posix_spawnattr_init(3) (also covering related setters) [1], and the POSIX/Linux man page text (POSIX wording) [2].

[1] (developer.apple.com)
[2] (unix.com)

Citations:


Use return values directly instead of errno for spawn-attr error messages.

The POSIX spawn-attribute functions (posix_spawnattr_init(), posix_spawnattr_setsigmask(), posix_spawnattr_setflags()) return error codes directly, not via errno. Line 121 incorrectly uses std::strerror(errno) instead of the function's return value, which can emit misleading diagnostics.

Additionally, the combined conditional at lines 125–126 masks which operation failed and omits actual error codes from the error message. Separate the checks and report the actual return value for each operation.

Suggested fix
-    if (posix_spawnattr_init(&attr) != 0) {
+    const int init_rc = posix_spawnattr_init(&attr);
+    if (init_rc != 0) {
         return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
                                           std::string("posix_spawnattr_init() failed: ") +
-                                              std::strerror(errno));
+                                              std::strerror(init_rc));
     }
     sigset_t empty_mask{};
     sigemptyset(&empty_mask);
-    if (posix_spawnattr_setsigmask(&attr, &empty_mask) != 0 ||
-        posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK) != 0) {
+    const int mask_rc = posix_spawnattr_setsigmask(&attr, &empty_mask);
+    if (mask_rc != 0) {
         posix_spawnattr_destroy(&attr);
         return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
-                                          "posix_spawnattr configuration failed");
+                                          std::string("posix_spawnattr_setsigmask() failed: ") +
+                                              std::strerror(mask_rc));
+    }
+    const int flags_rc = posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK);
+    if (flags_rc != 0) {
+        posix_spawnattr_destroy(&attr);
+        return goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
+                                          std::string("posix_spawnattr_setflags() failed: ") +
+                                              std::strerror(flags_rc));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/main.cpp` around lines 118 - 129, posix_spawnattr_* calls return
error codes directly, so change the error handling in the
posix_spawnattr_init/setsigmask/setflags sequence to capture each function's
return value and use that value with std::strerror() in the error message
instead of reading errno; specifically, in the posix_spawnattr_init() branch
capture the int rc (from posix_spawnattr_init), and in the two subsequent calls
call posix_spawnattr_setsigmask(&attr, &empty_mask) and
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK) into separate int
rc1/rc2 variables, check each independently, call posix_spawnattr_destroy(&attr)
on failure, and return
goggles::make_error<pid_t>(goggles::ErrorCode::unknown_error,
"posix_spawnattr_setsigmask() failed: " + std::string(std::strerror(rc1))) or
similarly for setflags using rc2 so the actual return codes are reported; update
references to attr and the existing make_error calls accordingly.

Comment on lines +430 to +440
while (app.is_running()) {
app.process_event();
app.tick_frame();

if (!child_exited) {
const pid_t result = waitpid(child_pid, &child_status, WNOHANG);
if (result == child_pid) {
child_exited = true;
push_quit_event();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/app/main.cpp | sed -n '420,450p'

Repository: K1ngst0m/Goggles

Length of output: 1263


🌐 Web query:

waitpid return -1 error handling EINTR WNOHANG recommended approach

💡 Result:

Recommended waitpid() error-handling pattern:

  • > 0: you reaped a child (PID returned).
  • == 0 with WNOHANG: no child status is available right now (not an error). (man7.org)
  • == -1:
    • errno == EINTR: interrupted by a signal → retry. (man7.org)
    • errno == ECHILD: no (matching) unwaited-for children → you’re done. (man7.org)
    • otherwise: treat as a real error (e.g., EINVAL). (docs.oracle.com)

Common nonblocking “reap all exited children” loop (typical in a SIGCHLD-driven design):

for (;;) {
    int status;
    pid_t pid = waitpid(-1, &status, WNOHANG);
    if (pid > 0) {
        // handle this child's status
        continue;               // try to reap more
    }
    if (pid == 0) {
        break;                  // none ready right now (WNOHANG)
    }
    // pid == -1
    if (errno == EINTR) continue; // retry if interrupted
    if (errno == ECHILD) break;   // no more children
    // other error
    perror("waitpid");
    break;
}

Blocking wait (no WNOHANG): same idea—loop on EINTR until you get a PID or a real error. (man7.org)

Citations:


Handle waitpid negative returns to avoid a stuck windowed loop.

If waitpid(child_pid, &child_status, WNOHANG) returns -1, the loop currently ignores it and can keep running with child_exited == false. According to standard error handling for waitpid, -1 with errno == EINTR should be retried, while -1 with other errors (including ECHILD when the child is already gone) should trigger shutdown to prevent an infinite loop.

Suggested fix
         if (!child_exited) {
             const pid_t result = waitpid(child_pid, &child_status, WNOHANG);
             if (result == child_pid) {
                 child_exited = true;
                 push_quit_event();
+            } else if (result < 0) {
+                if (errno == EINTR) {
+                    continue;
+                }
+                GOGGLES_LOG_ERROR("waitpid() failed for pid {}: {}", child_pid, std::strerror(errno));
+                child_exited = true;
+                push_quit_event();
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/main.cpp` around lines 430 - 440, The loop's waitpid call can return
-1 and currently gets ignored; update the child-watching logic around
waitpid(child_pid, &child_status, WNOHANG) to handle negative returns: if result
== -1 and errno == EINTR, simply retry (continue) the wait loop; if result == -1
for any other errno (including ECHILD), treat the child as exited by setting
child_exited = true and calling push_quit_event(); keep the existing handling
when result == child_pid. Reference symbols: waitpid, child_exited, child_pid,
child_status, push_quit_event, errno, EINTR, ECHILD.

@K1ngst0m K1ngst0m merged commit deb7a11 into main Feb 27, 2026
4 checks passed
@K1ngst0m K1ngst0m deleted the dev/complete-openspec branch February 27, 2026 06:20
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