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:

  • 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.

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

The subject

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

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.

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

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.

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.

The driver workflow in shell

One shell, one command:

bin/Release/daslang.exe -project_root <dasimgui> \
    <dasimgui>/tests/integration/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 and what ships in source.

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/recording.md). The driver script is for the canonical artifact pass.

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.

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.

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.

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.

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 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: examples/tutorial/recording.das

Driver source: tests/integration/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