frame-pacing-scope2 (#108) #116

Merged
OragonEfreet merged 8 commits from frame-pacing-scope2 into main 2026-05-29 12:10:54 +02:00
Owner
No description provided.
Locks down four behavioural invariants of the fixed-step accumulator
that the existing tests didn't reach:

* accumulator_catches_up_during_stall — a 50ms sleep in step at 100Hz
  must produce ≥4 fixed_step fires on the post-stall iteration, proving
  the drain loop actually catches up rather than firing once per step.

* dt_cap_bounds_fixed_step_drain_after_long_stall — a 300ms sleep at
  1000Hz must stay under ~290 fires, proving the 0.25s spiral-of-death
  cap actually bounds the per-iteration accumulator gain.

* fixed_step_delta_is_constant_inverse_rate — every fixed_step tick.delta
  in a session is bit-identical to the first one, in (0, 1/50), proving
  the deterministic-delta contract.

* step_delta_is_non_negative_across_iterations — five step iterations
  see only delta ≥ 0, with max delta below the cap, proving bj_run_time
  monotonicity propagates without arithmetic surprises.

Total suite time goes from 0.45s to 0.83s, dominated by the 300ms cap
test (necessary — the cap itself is 0.25s, so the stall must exceed it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both examples were the original motivation for the fixed-step slot in
#108: their integrators want a deterministic dt to stay stable. Migrate
them to the intended pattern so they double as documentation for how
to use bj_app_set_fixed_step_callback.

* physics_kinematics: update(dt) moves into fixed_step. The closed-form
  bj_compute_kinematics_2d doesn't care about dt size, but routing it
  through fixed_step removes the bj_stopwatch global and makes the
  example a clean reference for the API.

* physics_particle: physics(dt) moves into fixed_step at 120 Hz, which
  matches the integrator's stability bound the old code was hand-clamping
  to via DT_CLAMP. The clamp, the no-op update(t) trampoline, and the
  bj_stopwatch global all drop out.

pong.c deliberately stays on the single-step pattern: it's the capstone
tutorial and benefits from showing the simplest possible app shape, not
from demonstrating dual-callback dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape the per-window framebuffer state from four parallel scalars
(fb_shm_fd, fb_shm_size, fb_shm_mapped, fb_buffer) into a two-element
array of the existing wayland_shm_buffer helper struct. Both slots are
allocated up front; only fb[0] is presented for now, matching today's
behavior exactly. The follow-up commit wires a wl_buffer.release
listener and swaps to the free slot each frame so the compositor can
hold one buffer for scan-out while the user draws into the other.

Also drops the manual fb_shm_fd = -1 sentinel — wayland_destroy_shm_buffer
is idempotent on a zero-initialised input from bj_memset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire wl_buffer.release on both fb slots; each release clears the
slot's busy flag. In frame_handle_done, before firing the user's
draw callback, pick whichever slot the compositor is not currently
reading and re-point fb_bitmap onto its mmap so the user's pixels
land in the free buffer. wayland_commit_drawn attaches the current
slot and flags it busy until the next release.

Under steady frame-callback pacing the just-presented buffer is
typically released before the next frame.done, so the picker keeps
returning slot 0 (degenerate single-buffering, but correct under
the protocol). When the compositor holds the buffer longer, the
picker uses the other slot, eliminating the previous attach-while-
reading race.

If both slots happen to be busy at draw time, dispatch any pending
events once to pull in release notifications, then fall back to the
previous slot (rare; only under severe stutter or resize).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DwmFlush blocks until the next DWM composition tick. Calling it at the
tail of win32_window_poll makes each bj_dispatch_events round-trip
land on a refresh boundary, so the surrounding step loop's natural
cadence tracks the display rate. Matches the Wayland behavior where
the frame_listener drives draw cadence.

dwmapi.dll is loaded with LoadLibraryA at init and freed in
win32_end_video, mirroring the X11/ALSA pattern: libbanjo carries no
link-time dependency on dwmapi (verified — objdump on a cross-compiled
example shows only GDI32/KERNEL32/msvcrt/USER32/WS2_32 in the import
table).

On a Win7-Embedded or similar stripped SKU where DWM is absent, both
LoadLibrary and GetProcAddress can fail. The backend falls through
gracefully — poll returns the way it did before this commit, and the
user's bj_sleep in step remains the only throttle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Catch-up for the previous Win32 commit (DwmFlush plumbing). Notes the
runtime dlopen alongside the existing winmm.dll / libX11.so.6 /
libwayland-client.so.0 / libasound.so.2 entries, plus the graceful
degradation on systems where dwmapi is absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-window placeholder wl_buffer stays around just to keep the
surface visible until the user wires up a draw callback. Today it is
allocated at create_window and not destroyed until delete_window,
even though the moment the user's first paint lands the compositor
no longer reads it. That's wasted memory + an SHM fd + an mmap
mapping for the lifetime of every Banjo window with a renderer.

Wire a wl_buffer.release listener on the placeholder so its lifetime
can be tracked. Mark it doomed in wayland_create_framebuffer; the
next commit_drawn attaches a real buffer, which makes the compositor
release the placeholder, and the release handler then tears it down.
delete_window still calls wayland_destroy_shm_buffer as a fallback
for windows that are destroyed before the placeholder ever released
(idempotent on the now-zero struct).

Moves buffer_handle_release + buffer_listener up next to
wayland_destroy_shm_buffer so they're available to create_window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pace Cocoa poll on CVDisplayLink for vsync-matched step cadence
All checks were successful
Documentation / build-docs (pull_request) Successful in 9s
QA / cert (pull_request) Successful in 31s
Build and Test / linux-linux-gcc (pull_request) Successful in 17s
Build and Test / linux-windows-mingw (pull_request) Successful in 26s
7f52c39505
Mirrors the Win32 DwmFlush and Wayland frame-listener work for the
last graphical backend. A single global CVDisplayLink is created at
backend init; its callback signals a dispatch semaphore on every
display refresh. cocoa_poll_events waits on that semaphore at the
tail of the event drain (with a 100ms timeout so a sleeping display
can't hang the loop).

CVDisplayLink failure at init (no active CG display, etc.) is
non-fatal: display_link stays NULL, the wait is skipped, and the
backend keeps working at the previous uncapped cadence. Matches the
graceful degradation already in the Win32 / Wayland paths.

Adds `-framework CoreVideo` to the cocoa link line and documents it
in BUILDING.md alongside the existing `-framework Cocoa`.

VERIFICATION GAP: no Mac toolchain in the local presets, so this
change is compile-verified only against the linux + mingw paths
(which don't touch the cocoa code under its #ifdef guard). The
syntax follows Apple's documented CVDisplayLink lifecycle; ctest and
doxygen both pass. CI on macOS, or a local macOS build, is needed
to confirm the .m file actually compiles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OragonEfreet changed title from frame-pacing-scope2 to frame-pacing-scope2 (#108) 2026-05-29 12:09:36 +02:00
OragonEfreet deleted branch frame-pacing-scope2 2026-05-29 12:10:54 +02:00
OragonEfreet added this to the 1.0 milestone 2026-06-04 19:46:07 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
OragonEfreet/banjo!116
No description provided.