This native wgpu + Tauri 2 example replaces Example 27’s simulated CPU worker with a real GPU-to-CPU readback pipeline and FFmpeg file writer.
It records the same authoritative GPU frame used by the native preview, but recording resolution and frame rate are independent from both the renderer and the window.
1280 × 720 @ 60 fps wgpu texture1280 × 720, 960 × 540, and 640 × 36060, 30, and 24 fpslibx264prores_ksffprobe inspectionWGSL source renderer
↓
Authoritative GPU texture
1280 × 720 @ 60 fps
↓ sampled and scaled
Independent recording texture
↓ selected capture cadence
Three mapped readback buffers
↓ padded rows removed
Packed RGBA CPU frame
↓ bounded queue
FFmpeg worker thread
↓
H.264 MP4 or ProRes MOV
The native renderer window shows:
The recording texture remains a GPU texture. CPU readback occurs only when a recording is active and the selected recording cadence requests a frame.
Use the same Rust and Tauri 2 setup as the existing native wgpu examples.
ffmpeg must be available on PATH.
ffmpeg -version
You can also point directly to a custom executable:
export FFMPEG_PATH=/absolute/path/to/ffmpeg
ffprobe is optional. When available, the example reports codec, dimensions, frame rate, duration, and final size after recording.
export FFPROBE_PATH=/absolute/path/to/ffprobe
npm install
npm run dev
960 × 54030 fps0 ms writer delay960 × 540;Recordings are written to:
./recordings/
relative to the process working directory.
.mov file was created and that the verification panel reports prores.ProRes files are significantly larger than H.264 files. This is expected.
1280 × 720 @ 60 fps.35 ms or 80 ms.The artificial delay exists only to make the live-safe backpressure policy visible. Normal recording should use 0 ms.
| Metric | Meaning |
|---|---|
| Capture requests | Frames requested by the independent recording cadence |
| GPU readbacks | Mapped GPU buffers successfully converted into packed CPU frames |
| Encoded frames | Frames written completely into FFmpeg stdin |
| GPU drops | Capture requests skipped because all readback slots were busy |
| Worker drops | Packed frames rejected because the FFmpeg worker queue was full |
| Worker pending | Frames currently queued for FFmpeg |
| Readback slots busy | GPU buffers currently waiting for mapping completion |
| Raw bytes piped | Uncompressed RGBA bytes written to FFmpeg |
| Final file size | Encoded file size after FFmpeg exits |
src-tauri/src/
├── frame.rs Shared frame descriptor and borrowed VideoFrame
├── output.rs FrameSink contract and native preview sink
├── recording.rs Recording texture, readback ring, FFmpeg worker, verification
├── renderer.rs Authoritative renderer and recording routing
├── source.wgsl Animated authoritative frame
├── scale.wgsl Source-to-recording texture scaling pass
├── preview.wgsl Source and recording-stage window composite
└── main.rs Tauri lifecycle and commands
Each readback slot moves through:
Idle
→ texture-to-buffer copy encoded
→ map scheduled on command submission
→ GPU completion callback
→ mapped bytes copied row by row
→ buffer unmapped
→ Idle
The mapping callback only sends a small completion message. Copying and padding removal happen during the render loop’s nonblocking poll stage.
When Stop and finalize is pressed:
ffprobe details are displayed when available.Do not force-quit the application while a recording is finalizing if the output file matters.
This example intentionally has no audio track. It establishes the video-frame recording contract first.
A later example can add:
Milestone 3 passes when: