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.
Helper-owned: spawn + ready-wait.
with_recording_appspawnsdaslang-live <feature_path>with--imgui-content-scale=1.0+--no-hdpi-framebuffer(and forwards-project_rootfrom the driver’s own argv), waits for the HTTP server to answer/status200, and yields anImguiAppto the body block.Body-owned: resolve widget centers. Inside the body,
wait_for_renderpollsimgui_snapshotuntil 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 helperwidget_bbox/widget_centerextract the geometry from the snapshot JSON.Helper-owned: enable visual aids + ``record_start``. The helper posts
imgui_mouse_trail+imgui_cursor_sprite(enabled=true)and thenrecord_startwith the(file, fps, max_seconds)you passed in. The writer pulls from a PBO ring on the GL side (4 buffers by default) —glReadPixelsreturns 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 viaget_this_module_dir()so caller cwd is irrelevant.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-screencaptionis decoupled from the natural spokenvoice(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_voicescrubs a slider and asserts the value changed;hold_through_voiceclicks and asserts the effect registered;force_set_verifieddrives a value from outside and asserts it took. A no-op accumulates a miss and the recording aborts at teardown (g_record_failurespanic) — a silently broken demo never ships. The driver below is exactly this shape.
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 behindio.MousePos. Args:enabled,fade(seconds),color(RGBA uint).imgui_cursor_sprite— visible pointer drawn atio.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;framescontrols visibility duration.imgui_highlight— a colored rectangle around a widget’s bbox for N frames. Useful for “look here” without text.imgui_auto_highlightflips 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 aroundio.active_widgetso 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_secondsexpires — same APNG, frame count + duration match the cap.stbi_apng_framereturns 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