Skip to content

recording: consumer side of lockstep capture (content-time gating)#189

Merged
borisbat merged 1 commit into
masterfrom
bbatkin/lockstep-recording
Jun 7, 2026
Merged

recording: consumer side of lockstep capture (content-time gating)#189
borisbat merged 1 commit into
masterfrom
bbatkin/lockstep-recording

Conversation

@borisbat

@borisbat borisbat commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Companion to daslang's lockstep-recording change

daslang (GaijinEntertainment/daScript#3028) puts the host clock into a fixed-dt lockstep mode during recording (capture every frame, content time advances exactly 1/fps per frame, decoupled from wall-clock). This PR wires the two consumer-side clocks to follow it.

harness — io.DeltaTime from the master clock

In the windowed begin-frame path, drive ImGui's io.DeltaTime from get_dt() instead of glfw's own clock, so under lockstep recording ImGui's own animations step on the fixed clock too. In normal mode get_dt() equals the wall dt, so it's a no-op there.

playwright — content-time gating

During a recording session the app advances a fixed 1/fps per captured frame (decoupled from wall-clock), so a beat must be held by captured-frame count, not sleep(ms):

  • blocking say() now holds via hold_content
  • new helpers: wait_content_frames / hold_content / hold_remainder_content / record_frame_count (poll record_status.frames)
  • g_record_fps set around the recording body in both attach and spawn paths
  • outside a recording session, all fall back to sleep

The cursor lerp (move_to) needs no changeadvance_mouse_timeline already reads get_uptime(), which is the fixed clock under lockstep.

Validation

Re-recorded display_widgets: uniform 30 fps (1123 frames / 37.43 s = 1123/30 exactly), the three voices (frames 78 / 445 / 864, durations 9.95 / 8.53 / 7.98 s) never overlap, and the recording self-verification passed.

Notes

  • Backward-compatible: these changes work against an un-patched daslang (gating then keys off the old throttled frame count); full lockstep needs both PRs.
  • Incidental lint cleanup in harness (already in the diff): require opengl/opengl directly instead of opengl/opengl_boost (STYLE029); // nolint:STYLE030 on require live/opengl_live (a side-effect require that loads the APNG recorder).
  • The say_begin + manual-hold drivers (record_*.das here + in node-editor and implot) still pace those beats by wall-clock — converting them to hold_remainder_content, plus a batch re-record across all three repos, is the follow-up.

🤖 Generated with Claude Code

Copilot AI 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.

Pull request overview

This PR updates dasImgui’s recording consumer-side behavior to align with daslang’s lockstep recording mode by (1) driving ImGui’s io.DeltaTime from the master clock and (2) changing Playwright driver pacing from wall-clock sleeps to content-time (captured-frame-count) gating during recording sessions.

Changes:

  • Playwright: say() now holds via content-time gating, and new helpers were added to wait/hold by captured-frame count during recording.
  • Playwright: with_recording_app now sets a module-scope recording FPS used to interpret “content time” holds.
  • Harness: windowed harness_begin_frame now assigns io.DeltaTime from get_dt() to keep ImGui animation timing consistent under lockstep.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
widgets/imgui_playwright.das Adds content-time gating helpers and routes narration holds through captured-frame count during recording.
widgets/imgui_harness.das Drives ImGui DeltaTime from the master clock to match lockstep recording timing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread widgets/imgui_playwright.das
@borisbat borisbat force-pushed the bbatkin/lockstep-recording branch from 39491d8 to 6452dc5 Compare June 7, 2026 06:54
@borisbat borisbat requested a review from Copilot June 7, 2026 06:54

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread widgets/imgui_playwright.das
Companion to daslang's lockstep-recording change. With the host clock in fixed-dt
mode during recording, the two consumer-side clocks must follow it:

- harness: drive ImGui io.DeltaTime from the master clock (get_dt) in the windowed
  path instead of glfw's own clock, so under lockstep recording io.DeltaTime is the
  fixed step too. In normal mode get_dt() equals the wall dt, so this is a no-op there.
- playwright: content-time gating. During a recording session the host clock advances
  a fixed 1/fps per captured frame (decoupled from wall-clock), so a beat must be held
  by captured-frame count, not sleep(ms). The blocking say() now holds via hold_content;
  new helpers wait_content_frames / hold_content / hold_remainder_content / record_frame_count
  poll record_status.frames; g_record_fps is set around the recording body in both
  attach and spawn paths. Outside a recording session they fall back to sleep.

The cursor lerp (move_to) needs no change: advance_mouse_timeline already reads
get_uptime(), which is the fixed clock under lockstep.

Validated by re-recording display_widgets: uniform 30fps, voices (frames 78/445/864)
never overlap, recording self-verification passed.

Incidental lint cleanup in harness (already in the diff): require opengl/opengl
directly instead of opengl/opengl_boost (STYLE029); suppress STYLE030 on
require live/opengl_live (a side-effect require that loads the APNG recorder).

The say_begin + manual hold drivers (record_*.das across this repo, node-editor and
implot) still pace those beats by wall-clock; converting them to hold_remainder_content
is a mechanical follow-up sweep.

Review/CI fixes:
- wait_content_frames hardening. The target is now anchored on the first VALID
  status read (a transient null read can no longer lower it and shorten the hold to
  ~0 frames), and a null read keeps polling instead of ending the hold early.
- macOS CI hang fix + graceful degradation. Content-time gating couples the driver's
  progress to the app's frame production; on headless macOS the unfocused window is
  OS-throttled, so frames don't advance during an input-less hold and the wait spun
  to its 120s cap (× N says = a hang). wait_content_frames now detects a capture
  stall (frames idle RECORD_STALL_US = 2s) and degrades to a wall-clock sleep for the
  remaining beat, with a one-shot warning. Uniform video stays where capture works
  (Windows/ubuntu); macOS reverts to wall-clock pacing (throwaway CI recordings). The
  real fix — render continuously during recording on macOS — is dasImgui issue #190.
- imgui2rst: register the four new public helpers under a "Content-time gating" doc
  group so the Uncategorized gate passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@borisbat borisbat force-pushed the bbatkin/lockstep-recording branch from 6452dc5 to fc361f6 Compare June 7, 2026 07:23
@borisbat borisbat requested a review from Copilot June 7, 2026 07:23

Copilot AI 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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment on lines +899 to +909
def private record_frames_now(app : ImguiApp) : int {
// Captured-frame counter from a VALID active status. Tolerates a transient null read
// (a 0 here would corrupt a hold anchor) by briefly retrying; 0 only if never valid.
let start = ref_time_ticks()
while (get_time_usec(start) < RECORD_STALL_US) {
let st = post_command(app, "record_status", null)
if (st != null && (st?["active"] ?? false)) return st?["frames"] ?? 0
sleep(RECORD_POLL_MS)
}
return 0
}
Comment on lines +957 to +962
def public record_frame_count(app : ImguiApp) : int {
//! Current captured-frame count (content-time index). Capture right after say_begin to
//! anchor a hold_remainder_content. Returns 0 outside a recording session.
if (g_record_fps <= 0) return 0
return record_frames_now(app)
}
Comment on lines +964 to 972
def public hold_remainder_content(app : ImguiApp; dwell_ms : uint; start_frames : int) {
//! Pad a beat to `dwell_ms` of CONTENT time: wait until the recorder reaches
//! start_frames + dwell_ms/1000*fps (start_frames from record_frame_count right after
//! say_begin). Work done mid-dwell consumes content frames; this holds the rest. The
//! content-clock replacement for a driver's wall-clock hold_remainder. No-op when idle.
if (g_record_fps <= 0) return
let target = start_frames + int((float(dwell_ms) / 1000.0f) * float(g_record_fps) + 0.5f)
wait_content_frames(app, target - record_frames_now(app))
}
@borisbat borisbat merged commit d1a8301 into master Jun 7, 2026
7 of 10 checks passed
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.

2 participants