recording: consumer side of lockstep capture (content-time gating)#189
Merged
Conversation
There was a problem hiding this comment.
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_appnow sets a module-scope recording FPS used to interpret “content time” holds. - Harness: windowed
harness_begin_framenow assignsio.DeltaTimefromget_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.
39491d8 to
6452dc5
Compare
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>
6452dc5 to
fc361f6
Compare
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)) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/fpsper frame, decoupled from wall-clock). This PR wires the two consumer-side clocks to follow it.harness —
io.DeltaTimefrom the master clockIn the windowed begin-frame path, drive ImGui's
io.DeltaTimefromget_dt()instead of glfw's own clock, so under lockstep recording ImGui's own animations step on the fixed clock too. In normal modeget_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/fpsper captured frame (decoupled from wall-clock), so a beat must be held by captured-frame count, notsleep(ms):say()now holds viahold_contentwait_content_frames/hold_content/hold_remainder_content/record_frame_count(pollrecord_status.frames)g_record_fpsset around the recording body in both attach and spawn pathssleepThe cursor lerp (
move_to) needs no change —advance_mouse_timelinealready readsget_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
harness(already in the diff):require opengl/opengldirectly instead ofopengl/opengl_boost(STYLE029);// nolint:STYLE030onrequire live/opengl_live(a side-effect require that loads the APNG recorder).say_begin+ manual-hold drivers (record_*.dashere + in node-editor and implot) still pace those beats by wall-clock — converting them tohold_remainder_content, plus a batch re-record across all three repos, is the follow-up.🤖 Generated with Claude Code