8.18.1.30. Recording tutorial videos

Every tutorial recording is produced the same way: a driver script uses with_recording_app to spawn a tiny subject app as daslang-live, toggles visual aids, calls record_start, narrates each interaction, performs the action, and lets the helper shut down. The recorder writes .apng; a single ffmpeg pass converts each to .mp4 (H.264) before the file ships in-tree. This is the meta tutorial — the driver script is the artifact, and the embedded video demonstrates itself.

The helper passes --imgui-content-scale=1.0 + --no-hdpi-framebuffer to the spawned daslang-live so APNGs stay at logical 1x — small files, fast encode, even on retina. Tutorials run by users directly (no driver) keep their native HDPI look.

Source files:

  • modules/dasImgui/examples/tutorial/recording.das — the subject: a one-slider one-button window. Tiny on purpose so every frame in the APNG corresponds to one specific driver call.

  • modules/dasImgui/tests/record_recording.das — the driver. Walked through below.

8.18.1.30.1. The subject

  1options gen2
  2options _comment_hygiene = true
  3
  4require imgui
  5require imgui_app
  6require opengl/opengl_boost
  7require live/glfw_live
  8require live/live_api
  9require live/live_commands
 10require live/live_vars
 11require live_host
 12require imgui/imgui_live
 13require imgui/imgui_boost_runtime
 14require imgui/imgui_boost_v2
 15require imgui/imgui_widgets_builtin
 16require imgui/imgui_containers_builtin
 17require imgui/imgui_visual_aids
 18
 19// =============================================================================
 20// TUTORIAL: recording — the meta tutorial.
 21//
 22// Every previous tutorial's APNG was produced by the same workflow:
 23//
 24//   daslang  runs a DRIVER script that:
 25//     - uses with_recording_app to SPAWN this SUBJECT app as daslang-live
 26//     - the helper auto-toggles imgui_mouse_trail + imgui_cursor_sprite
 27//     - the helper auto-bracketts record_start / record_stop
 28//     - the body posts imgui_narrate + cursor moves + clicks / drags
 29//
 30// The DRIVER is the artifact that teaches recording — the subject is
 31// just whatever app you want to record. This file is intentionally tiny
 32// (one slider, one button) so the recording recipe is obvious: every
 33// frame of the APNG corresponds to one specific driver-body call.
 34//
 35// The driver script for this tutorial is:
 36//   tests/integration/record_recording.das
 37//
 38// The helper passes --imgui-content-scale=1.0 + --no-hdpi-framebuffer
 39// to the spawned daslang-live so APNG capture stays at logical 1x.
 40// When you run this tutorial DIRECTLY (no driver), no flags are passed,
 41// so retina users get native HDPI scaling.
 42//
 43// Read the driver alongside the walkthrough in the RST companion — the
 44// .das + driver + APNG triple is the tutorial.
 45//
 46// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/recording.das
 47// LIVE:       daslang-live modules/dasImgui/examples/tutorial/recording.das
 48//
 49// DRIVE: see tests/integration/record_recording.das
 50// =============================================================================
 51
 52[export]
 53def init() {
 54    live_create_window("dasImgui recording tutorial", 940, 560)
 55    live_imgui_init(live_window)
 56    DisableIniPersistence()
 57    let io & = unsafe(GetIO())
 58    GetStyle().FontScaleMain = 1.5
 59}
 60
 61[export]
 62def update() {
 63    if (!live_begin_frame()) return
 64    begin_frame()
 65
 66    ImGui_ImplGlfw_NewFrame()
 67    apply_synth_io_override()
 68    NewFrame()
 69
 70    SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.FirstUseEver)
 71    SetNextWindowSize(ImVec2(520.0f, 280.0f), ImGuiCond.FirstUseEver)
 72    window(REC_WIN, (text = "subject", closable = false,
 73                     flags = ImGuiWindowFlags.None)) {
 74        text("Tiny subject - driver narrates what's happening.")
 75        slider_float(VOLUME, (text = "Volume"))
 76        if (button(SAVE_BTN, (text = "Save"))) {}
 77        text("SAVE_BTN.click_count = {SAVE_BTN.click_count}")
 78    }
 79
 80    end_of_frame()
 81    Render()
 82    var w, h : int
 83    live_get_framebuffer_size(w, h)
 84    glViewport(0, 0, w, h)
 85    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
 86    glClear(GL_COLOR_BUFFER_BIT)
 87    live_imgui_render()
 88
 89    live_end_frame()
 90}
 91
 92[export]
 93def shutdown() {
 94    live_imgui_shutdown()
 95    live_destroy_window()
 96}
 97
 98[export]
 99def main() {
100    init()
101    while (!exit_requested()) {
102        update()
103    }
104    shutdown()
105}

The subject is a normal dasImgui live-reload app — same shape as every other tutorial’s subject. The recorder doesn’t touch the subject at all; it operates entirely through the live-command HTTP surface.

8.18.1.30.2. The driver

The video below is produced by the driver listed under it — the meta loop closes on itself. It drags Volume (asserting the value moved) and clicks Save (asserting the click landed), narrating the recipe as it goes; a silently-broken beat would abort the recording instead of shipping.

 1options gen2
 2options _comment_hygiene = true
 3options indenting = 4
 4options no_unused_block_arguments = false
 5options no_unused_function_arguments = false
 6
 7require imgui public
 8require imgui/imgui_playwright public
 9require daslib/json public
10require daslib/json_boost public
11
12//! Driver: record ``recording.apng`` against ``examples/tutorial/recording.das``.
13//! This is the META tutorial - the driver IS the artifact that teaches recording.
14//! It demonstrates the recipe every other recording uses (narrate caption + cursor
15//! trail + scripted gesture + self-verify) on a tiny subject, and verifies each:
16//!   1. scripted drag  - VOLUME.value must CHANGE (drag_through_voice).
17//!   2. scripted click - SAVE_BTN must register (hold_through_voice).
18
19def widget_bbox(var snap : JsonValue?; ident : string) : float4 {
20    var b = snap?["globals"]?[ident]?["bbox"]
21    return float4(b?["x"] ?? 0.0f, b?["y"] ?? 0.0f, b?["z"] ?? 0.0f, b?["w"] ?? 0.0f)
22}
23
24def widget_center(var snap : JsonValue?; ident : string) : tuple<float; float> {
25    let b = widget_bbox(snap, ident)
26    return ((b.x + b.z) * 0.5f, (b.y + b.w) * 0.5f)
27}
28
29[export]
30def main {
31    with_recording_app("examples/tutorial/recording.das",
32                       "recording.apng", 35) $(app) {
33        let T_VOLUME = "REC_WIN/VOLUME"
34        let T_SAVE   = "REC_WIN/SAVE_BTN"
35
36        var snap = wait_for_render(app, T_SAVE, 10.0f)
37        if (snap == null) {
38            panic("{T_SAVE} never rendered - wrong app running?")
39        }
40        let p_save = widget_center(snap, T_SAVE)
41        let vol_bb = widget_bbox(snap, T_VOLUME)
42        let vol_y  = (vol_bb.y + vol_bb.w) * 0.5f
43        let vol_x0 = vol_bb.x + (vol_bb.z - vol_bb.x) * 0.15f
44        let vol_x1 = vol_bb.x + (vol_bb.z - vol_bb.x) * 0.80f
45
46        // Park the cursor at the slider's left and warm up the capture pipeline.
47        move_to(app, (vol_x0, vol_y), 800)
48        hold_content(app, 1800u)
49
50        // ---- Beat 1: the recipe - narrate, trail, scripted drag ----
51        drag_through_voice(app, say_begin(app, "narrate + scripted drag", T_VOLUME,
52            [voice = "This is the recording recipe itself. A driver shows a caption box, moves the cursor along a trail, and scripts a real drag - here the Volume slider, and the snapshot confirms the value moved."]),
53            T_VOLUME, (vol_x1, vol_y))
54
55        // ---- Beat 2: scripted click + self-verify ----
56        move_to(app, p_save, 600)
57        hold_content(app, SETTLE_MS)
58        hold_through_voice(app, say_begin(app, "scripted click + verify", T_SAVE,
59            [voice = "Then it scripts a click and asserts the effect landed. Every recording you have seen is this recipe - narrate, move, act, verify - so a demo that quietly broke aborts instead of shipping."]),
60            [T_SAVE])
61    }
62}

8.18.1.30.2.1. Anatomy of a driver

The driver’s main is a single with_recording_app(...) $(app) { ... } call. The helper owns the boilerplate; the body owns the timeline.

  1. Helper-owned: spawn + ready-wait. with_recording_app spawns daslang-live <feature_path> with --imgui-content-scale=1.0 + --no-hdpi-framebuffer (and forwards -project_root from the driver’s own argv), waits for the HTTP server to answer /status 200, and yields an ImguiApp to the body block.

  2. Body-owned: resolve widget centers. Inside the body, wait_for_render polls imgui_snapshot until the named widget shows up in the registry (covers the first-frame gap where the subject’s window exists but hasn’t rendered yet). The helper widget_bbox / widget_center extract the geometry from the snapshot JSON.

  3. Helper-owned: enable visual aids + ``record_start``. The helper posts imgui_mouse_trail + imgui_cursor_sprite (enabled=true) and then record_start with the (file, fps, max_seconds) you passed in. The writer pulls from a PBO ring on the GL side (4 buffers by default) — glReadPixels returns immediately, the actual readback happens 3 frames later, the encoder runs on a worker thread. Frames drop only if the worker can’t keep up with the GL output rate, and even then dropping is graceful (the APNG just gets slightly choppier — never breaks).

    The absolute APNG path is <dasimgui>/doc/source/_static/tutorials/<basename>, resolved via get_this_module_dir() so caller cwd is irrelevant.

  4. Body-owned: narrate, act, verify — repeat. Each beat pairs an ear-first caption with a real gesture and a self-check:

    • say_begin(app, caption, target, [voice = "..."]) posts the caption box and returns the voiceover’s length, so the beat is paced by the spoken line — no hand-tuned frame counts. The terse on-screen caption is decoupled from the natural spoken voice (which keys the TTS manifest). Both must be ASCII — the bundled ImGui font has no em-dash / arrow / smart-quote glyph, so non-ASCII renders as tofu.

    • The gesture runs UNDER the voice and is VERIFIED: drag_through_voice scrubs a slider and asserts the value changed; hold_through_voice clicks and asserts the effect registered; force_set_verified drives a value from outside and asserts it took. A no-op accumulates a miss and the recording aborts at teardown (g_record_failures panic) — a silently broken demo never ships. The driver below is exactly this shape.

  5. Helper-owned: ``record_stop`` + shutdown. When the body returns, the helper posts record_stop (flushes the writer, joins the encoder thread, prints (saved, frames, duration_s, dropped, ok)), disables the visual aids, posts /shutdown, drains stdout. The driver process exits when daslang-live does.

8.18.1.30.2.2. The visual-aids stack

Four [live_command] toggles in imgui_visual_aids.das:

  • imgui_mouse_trail — fading line behind io.MousePos. Args: enabled, fade (seconds), color (RGBA uint).

  • imgui_cursor_sprite — visible pointer drawn at io.MousePos. Without it the cursor only exists in the OS layer, which screen recordings don’t capture.

  • imgui_narrate — a sticky-note callout with optional connector line to a target widget. Auto-fits to avoid sibling overlap; frames controls visibility duration.

  • imgui_highlight — a colored rectangle around a widget’s bbox for N frames. Useful for “look here” without text. imgui_auto_highlight flips a global flag that fires highlight on every accepted live command — a one-shot debug aid.

Two more for keyboard work:

  • imgui_key_hud — bottom-center keycap overlay + modifier strip. Pops a keycap for every synth key event; lights modifier pills while held. Useful for input-heavy demos.

  • imgui_focus_rect — a rectangle around io.active_widget so the viewer can tell which widget keystrokes will land in.

8.18.1.30.2.3. The driver workflow in shell

One shell, one command:

bin/Release/daslang.exe -project_root <dasimgui> \
    <dasimgui>/modules/dasImgui/tests/record_recording.das

The helper spawns daslang-live, runs the body, posts /shutdown, drains stdout. Wall time = max_seconds + ~3s headroom. The APNG lands at <dasimgui>/doc/source/_static/tutorials/recording.apng, which is gitignored — it’s the raw artifact, not the deliverable.

After the recorder finishes, one ffmpeg pass converts to the shipped .mp4:

ffmpeg -y -i recording.apng -c:v libx264 -crf 23 -pix_fmt yuv420p \
       -movflags +faststart recording.mp4

Typical UI recording: 50-300 KB MP4 (vs 50-100 MB APNG, ~300x smaller). The .mp4 is what RSTs reference; it ships on the rolling docs-assets GitHub release, not in git.

If you want to iterate without re-recording the host’s state, drive a manually-launched host via mcp__daslang__live_command instead (see “Verifying a recording” in skills/imgui_recording.md). The driver script is for the canonical artifact pass.

8.18.1.30.2.4. Stop conditions

record_stop is the clean exit. Three other ways the writer finalizes:

  • max_seconds expires — same APNG, frame count + duration match the cap.

  • stbi_apng_frame returns an error (rare; usually disk full or permission denied) — writer auto-stops, returns the partial APNG.

  • daslang-live shuts down — the [on_app_exit] hook on the recorder finalizes any in-flight ring before tearing down GL state.

In practice record_stop is the only exit you’ll see; the others are safety nets.

8.18.1.30.2.5. Replay stability

The same driver run against the same subject produces APNGs that look the same to a viewer — but they’re not byte-identical. ImGui’s frame times jitter, vsync alignment shifts, the PBO ring may stall once or twice on encoder backpressure. dropped in the response is the useful metric: anything under capture_pbo_count + a few is fine. Higher means the encoder couldn’t keep up — try lowering the subject’s render rate, simplifying the subject, or raising capture_pbo_count.

8.18.1.30.2.6. Standalone vs live

The whole recording surface — visual aids, record_start / record_stop, the playwright helpers — runs only under daslang-live. Standalone daslang.exe runs the subject, but the HTTP-bound live commands aren’t registered. The driver script itself is a normal daslang.exe script — it just talks HTTP to a host running on another process.

8.18.1.30.2.7. Driving from outside

The same JSON commands the driver issues are reachable from curl:

curl -X POST -d '{"name":"imgui_mouse_trail","args":{"enabled":true}}'      localhost:9090/command
curl -X POST -d '{"name":"imgui_cursor_sprite","args":{"enabled":true}}'    localhost:9090/command
curl -X POST -d '{"name":"record_start","args":{"file":"manual.apng","fps":30,"max_seconds":15}}' \
     localhost:9090/command

# do whatever interactions interactively in the live window ...

curl -X POST -d '{"name":"record_stop"}' localhost:9090/command

Use this for ad-hoc captures when the scripted driver would be overkill — bug repros, “show me what this looks like” pings.

8.18.1.30.2.8. The end of the curriculum

The 12 tutorials in this set walk the dasImgui surface end-to-end: basics, widgets, layout, docking, style, identity, state, containers, live reload, the JSON-driven view, the visual aids tour, and this recording recipe. Beyond the curriculum, every modules/dasImgui/examples/features/ demo is a richer reference for one specific surface — those are the go-to once the tutorials are familiar.

See also

Subject source: modules/dasImgui/examples/tutorial/recording.das

Driver source: modules/dasImgui/tests/record_recording.das

Recorder implementation: modules/dasOpenGL/opengl/opengl_live.das (record_start / record_stop plus the PBO ring).

Visual aids: modules/dasImgui/widgets/imgui_visual_aids.das.

Playwright helpers: modules/dasImgui/widgets/imgui_playwright.das.

Previous tutorial: Visual aids tour

Curriculum top: dasImgui tutorials