Skip to content

feat(sdl): conform window to the canonical contract (#386) - #437

Merged
apotema merged 1 commit into
mainfrom
feat/386-sdl-window-conformance
Jun 30, 2026
Merged

feat(sdl): conform window to the canonical contract (#386)#437
apotema merged 1 commit into
mainfrom
feat/386-sdl-window-conformance

Conversation

@apotema

@apotema apotema commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

First step of extracting SDL out-of-tree (#386): backends/sdl/src/window.zig satisfies labelle-core's assertWindow. Additive + byte-identical (templates still call legacy names). Adds width/height (gfx size), frameDuration (SDL_GetPerformanceCounter delta), requestQuit (latches should_close), shouldQuit (alias → loop-style); initWindow params renamed *_px; state reset on re-init. backends/sdl + repo-root tests green.

Summary by CodeRabbit

  • New Features

    • Added standard window controls for querying the current window size, checking frame timing, and requesting shutdown.
    • Improved window lifecycle handling so reopening a window starts from a clean state.
  • Bug Fixes

    • Reset timing and close-state tracking during window setup for more consistent frame timing and quit behavior.

First step of extracting the SDL backend out-of-tree (epic #386): make
backends/sdl/src/window.zig satisfy labelle-core's canonical window
contract (core.assertWindow), mirroring the raylib backend's prior
conformance.

This is purely ADDITIVE and byte-identical for generated output: the
generated SDL run-loop templates still call the legacy names
(windowShouldClose, getScreenWidth via gfx, beginDrawing, ...). The new
decls are thin aliases/wrappers over SDL's existing internals:

  - width()/height()  -> gfx.getScreenWidth()/getScreenHeight()
  - frameDuration()   -> seconds since last call from a dedicated
                         SDL_GetPerformanceCounter baseline (SDL stores no
                         frame-time; the FPS limiter discards its delta)
  - requestQuit()     -> latches the existing should_close flag
  - shouldQuit()      -> alias of windowShouldClose() (its presence marks
                         SDL as a loop-model backend)

Also resets should_close and the frameDuration baseline at the top of
initWindow so a close->reopen starts clean, and renames initWindow's
width/height params to width_px/height_px to avoid shadowing the new
module-level width()/height() decls.

Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 95281a00-0085-4cdc-88dd-09482b215dc6

📥 Commits

Reviewing files that changed from the base of the PR and between f0d5639 and bb5e213.

📒 Files selected for processing (1)
  • backends/sdl/src/window.zig

📝 Walkthrough

Walkthrough

The SDL backend window module gains a canonical window contract: new exported functions width(), height(), frameDuration(), requestQuit(), and shouldQuit(). A frame_dur_last timing baseline is added and initialized alongside the existing FPS limiter, and initWindow() now clears should_close on reinitialization.

Changes

SDL Window Contract

Layer / File(s) Summary
State and init setup
backends/sdl/src/window.zig
Adds frame_dur_last baseline and window_hidden state, clears should_close in initWindow(), and initializes timing baselines from the SDL performance counter using renamed pixel-dimension parameters.
Canonical contract functions
backends/sdl/src/window.zig
Adds width()/height() wrappers over gfx, frameDuration() for elapsed-time tracking, and requestQuit()/shouldQuit() for loop control.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant WindowModule
  participant gfx
  App->>WindowModule: initWindow(width_px, height_px, title)
  WindowModule->>WindowModule: reset should_close, last_frame_time, frame_dur_last
  loop Frame loop
    App->>WindowModule: frameDuration()
    WindowModule-->>App: elapsed seconds
    App->>WindowModule: width() / height()
    WindowModule->>gfx: query size
    gfx-->>WindowModule: dimensions
    App->>WindowModule: shouldQuit()
    WindowModule-->>App: bool
  end
  App->>WindowModule: requestQuit()
  WindowModule->>WindowModule: set should_close = true
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

A rabbit hops by the SDL pane,
Counting frames like drops of rain. 🐇⏱️
width(), height(), now clear and bright,
shouldQuit() whispers "good night, good night."
Clean reboots, no stale close-state stew—
Hop hop hooray, the contract's true!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: updating the SDL window backend to conform to the canonical contract.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/386-sdl-window-conformance

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements the canonical window contract for the SDL backend in backends/sdl/src/window.zig, adding functions such as width(), height(), frameDuration(), requestQuit(), and shouldQuit(). The review feedback identifies a critical issue with the current implementation of frameDuration(): because it mutates the baseline state (frame_dur_last) on every call, it is not idempotent and will return incorrect values if queried multiple times within the same frame. The reviewer suggests caching the calculated frame duration once per frame (e.g., in beginDrawing()) and returning this cached value to ensure consistent timing across all subsystems.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

var should_close: bool = false;
var target_fps_val: i32 = 60;
var last_frame_time: u64 = 0;
var frame_dur_last: u64 = 0; // baseline for the canonical frameDuration() dt source

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To make frameDuration() idempotent and safe to call multiple times per frame, we should cache the calculated frame duration in a state variable. Declare frame_dur_seconds here, which will be updated once per frame (e.g., in beginDrawing()).

var frame_dur_last: u64 = 0; // baseline for the canonical frameDuration() dt source
var frame_dur_seconds: f64 = 0.0; // cached frame duration for idempotent queries

Comment on lines +43 to +45
const now = c.SDL_GetPerformanceCounter();
last_frame_time = now;
frame_dur_last = now; // reset the frameDuration() baseline too

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Reset the cached frame_dur_seconds to 0.0 during window initialization to ensure a clean state on re-initialization.

    const now = c.SDL_GetPerformanceCounter();
    last_frame_time = now;
    frame_dur_last = now; // reset the frameDuration() baseline too
    frame_dur_seconds = 0.0;

Comment on lines +85 to +92
pub fn frameDuration() f64 {
const freq = c.SDL_GetPerformanceFrequency();
const now = c.SDL_GetPerformanceCounter();
const elapsed = now - frame_dur_last;
frame_dur_last = now;
if (freq == 0) return 0;
return @as(f64, @floatFromInt(elapsed)) / @as(f64, @floatFromInt(freq));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Non-idempotent frameDuration()

The current implementation of frameDuration() mutates frame_dur_last on every call. If multiple subsystems or scripts query frameDuration() in the same frame, only the first call returns the correct elapsed time, while subsequent calls return approximately 0.0 seconds. This can cause severe timing and physics bugs.

Solution

  1. Simplify frameDuration() to return a cached frame_dur_seconds variable.
  2. Update frame_dur_seconds once per frame at the start of beginDrawing() (which is outside the modified diff hunks, so you will need to add this manually):
pub fn beginDrawing() void {
    const freq = c.SDL_GetPerformanceFrequency();
    const now = c.SDL_GetPerformanceCounter();
    const elapsed = now - frame_dur_last;
    frame_dur_last = now;
    frame_dur_seconds = if (freq == 0) 0 else @as(f64, @floatFromInt(elapsed)) / @as(f64, @floatFromInt(freq));

    input.newFrame();
    // ... rest of beginDrawing()
}
pub fn frameDuration() f64 {
    return frame_dur_seconds;
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb5e21342c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const freq = c.SDL_GetPerformanceFrequency();
const now = c.SDL_GetPerformanceCounter();
const elapsed = now - frame_dur_last;
frame_dur_last = now;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return a cached frame duration

When any SDL consumer queries frameDuration() more than once during the same frame, this assignment advances the baseline on the first read, so the second read reports only the time between the two calls instead of the last frame's duration. Other backends expose this as a query of the last-frame dt source, so using SDL through the canonical window contract can make one subsystem receive the real dt while another gets near-zero dt; update/cache the value once per frame instead of consuming it inside the getter.

Useful? React with 👍 / 👎.


/// Current framebuffer width (physical px).
pub fn width() i32 {
return gfx.getScreenWidth();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Query the live framebuffer size

When the SDL drawable size changes, for example after setFullscreen(true) switches the window to desktop fullscreen, this still returns the init-time gfx cache because gfx.setScreenSize is only called during initWindow and no resize/fullscreen path refreshes it. The canonical window API is meant to report the current physical framebuffer dimensions, so external/shared loops using window.width()/height() will keep layout or render targets at the old size; query SDL for the current output size or update the cache on size changes.

Useful? React with 👍 / 👎.

@apotema
apotema merged commit 6da06fd into main Jun 30, 2026
4 checks passed
@apotema
apotema deleted the feat/386-sdl-window-conformance branch June 30, 2026 20:55
apotema added a commit that referenced this pull request Jun 30, 2026
…nDrawing) (#444)

The conformance's frameDuration() advanced its baseline on every call, so a
second query within a frame returned ~0 (flagged by gemini/codex on #437).
Compute the dt once per frame in beginDrawing, cache it, and return the cache —
idempotent. Not exercised by current templates (fixed dt), but correct for when
the extracted backend's manifest template wires window.frameDuration() as dt.
apotema added a commit that referenced this pull request Jun 30, 2026
…445)

sdl extraction step (window conformed in #437). Opts the sdl DESKTOP build into
the manifest-splice path. Loop-style, desktop-only, NO params; EMPTY link beyond
the single blank the enum `.link_sdl` emitted (SDL2 is linked by the backend's
own build.zig). The backend_dep fragment carries the input core-diamond override
(underscore `labelle_core` key, #258). Byte-identical to the enum path
(diffed a generated baseline → 0 diff). zig build test green.
apotema added a commit that referenced this pull request Jul 1, 2026
* feat(sdl): add backend.manifest.zon — manifest-splice codegen (#386)

sdl extraction step (window conformed in #437). Opts the sdl DESKTOP build into
the manifest-splice path. Loop-style, desktop-only, NO params; EMPTY link beyond
the single blank the enum `.link_sdl` emitted (SDL2 is linked by the backend's
own build.zig). The backend_dep fragment carries the input core-diamond override
(underscore `labelle_core` key, #258). Byte-identical to the enum path
(diffed a generated baseline → 0 diff). zig build test green.

* feat(manifest): emit pack/feature manifest sidecar (#442)

Extend the generated catalog into the RFC §7 pack/feature manifest,
written to <game>/.labelle/manifest.json alongside flow_catalog.json.

Two-tier, realm-structured + sliceable:
- index (always loaded): contracts (events/enums) + a realm map
  (game root + each plugin) with owns / depends_on / exposes
  (commands vs queries, derived from FlowNode void-ness) / recipes.
- per-realm detail: game realm gets full component field schemas +
  save policy + event payloads (light AST pass over components/ and
  events/); plugin realms surface event + flow-node names (payloads
  already live in flow_catalog.json).

Everything except recipes is derived from already-scanned data, so it
can't drift. Additive: a failure is logged, never fatal (same contract
as the flow catalog).

Deferred and called out in the schema: recipes (empty, share the
scaffold source — cli #271), event emitted_by/subscribed_by cross-refs
(empty arrays), and component visibility (needs pack.labelle).

Part of #651, implements #442.

* fix(manifest): address review findings on pack/feature manifest sidecar (#442)

- parseStructFile: deinit the parsed Ast (per-file leak); propagate OOM
  from extractSavePolicy instead of swallowing it via `catch null`.
- parseStructDir: propagate OutOfMemory; degrade non-fatal read failures
  and no-match files to a name-only StructDecl (file-stem Pascal name,
  empty fields) so the manifest still lists what the registries import.
- parseStructDir: emit only the file-stem Pascal decl per file, so helper
  containers (`const Options`/`pub const Clip`) no longer leak as phantom
  components/events.
- contracts.events: realm-qualify entries (`engine.tick`,
  `box2d.collision_begin`, `game.<Event>`) to match the toolchain's
  dotted qualified form and avoid cross-realm name collisions.
- Emit a dedicated `engine` realm (index + detail) when engine lifecycle
  events are discovered, so `engine.<event>` contract entries resolve to a
  realm that actually appears.
- Game-realm exposes: script-qualify FlowNode commands/queries
  (`flows.hit_counter.spawn`) via the scriptModuleLabel dotted form so two
  scripts exposing the same bare name stay distinct.
- root.zig: use std.log.warn (not std.debug.print) for the manifest
  sidecar emission-failed warning.
- Tests: name-only fallback + helper-container exclusion (parseStructDir);
  engine realm emission + realm-qualified contracts + script-qualified
  exposes (writeManifestJson).

Claude-Session: https://claude.ai/code/session_017pW3ifKf9wgxNg4viy6okw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant