Skip to content

perf(editor): avoid playback hitches during unrelated live edits - #2507

Merged
richiemcilroy merged 3 commits into
CapSoftware:mainfrom
AksharP5:perf/playback-cursor-timeline-reuse
Oct 18, 2026
Merged

richiemcilroy merged 3 commits into
CapSoftware:mainfrom
AksharP5:perf/playback-cursor-timeline-reuse

Conversation

@AksharP5

@AksharP5 AksharP5 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Changing background settings during GPUI playback can interrupt frame delivery on long recordings. Every project update currently reruns the cursor spring simulation across every recording source, even when the edit cannot change that simulation.

Reuse the existing timelines unless raw mode or the effective movement/click spring parameters change. Preview and playback share the existing bitwise settings key. Rendering, audio updates and zoom updates still receive the new project.

The existing playback benchmark now supports timed background or spring edits and reports gaps between renderer callbacks. It uses the real project watch channel and playback/rendering path used by GPUI.

Benchmark

Three serial, interleaved before/after runs; medians below. Each completed 300 submitted/rendered/callback frames, with no renderer drops or send failures.

Metric Main This change
p95 callback delivery gap 44.77 ms 17.83 ms
Playback skips per run 55 0
Rendered frames/s including startup 50.3 59.4

The p95 gap is 60.2% lower for this workload. Per-run p95: main 44.77 / 44.99 / 44.66 ms; changed 17.76 / 17.83 / 17.84 ms.

Linux x86_64, i5-14400F, Rust 1.88, opt-level 3, FFmpeg 7.1, llvmpipe software Vulkan. Synthetic one-hour 320×180 H.264 recording, 216,001 sinusoidal mouse moves at 60 Hz and a click pair every five seconds; smoothed circle cursor. Preview base 480×270 at 60 fps; background padding alternates between 10 and 11 every 100 ms, starting 500 ms after start() returns. Both binaries use the identical updated benchmark; baseline production code is main 3d19c5cfa5.

No-edit controls: p95 17.78 ms before / 17.81 ms after, zero skips. Changing spring tension still rebuilds: 44.95 ms / 55 skips after the fix. At 20 Hz, an initial baseline stress run completed with 243 skips; the next timed out at 136/300 frames. That incomplete stress run is excluded from the table.

These are callback delivery measurements from production playback, not GPUI window presentation or display scanout. Software Vulkan was used because this machine's installed NVIDIA userspace and loaded kernel driver versions differ.

CARGO_PROFILE_RELEASE_OPT_LEVEL=3 cargo build -p cap-editor --release --example editor-playback-benchmark
CAP_RENDER_FORCE_SOFTWARE_ADAPTER=1 VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json \
  target/release/examples/editor-playback-benchmark \
  --recording-path /path/to/long-recording.cap --headless \
  --fps 60 --frames 300 --resolution 480x270 \
  --live-edit background --edit-interval-ms 100

Repeat with --live-edit cursor-spring for the rebuild control; omit both edit flags for unchanged playback. Check the printed submitted frame count reaches 300 before comparing results, since the example also returns on timeout.

Create the synthetic fixture

Save as create-live-fixture.py, then run python create-live-fixture.py /tmp/live-1h.cap. The output directory must not already exist; FFmpeg needs the libx264 encoder.

import json
import math
import subprocess
import sys
from pathlib import Path

output = Path(sys.argv[1]).resolve()
output.mkdir(parents=True, exist_ok=False)
content = output / 'content'
content.mkdir()
(content / 'cursor.json').write_text(json.dumps({
    'moves': [dict(active_modifiers=[], cursor_id='primary', time_ms=i / 60 * 1000, x=0.5 + 0.35 * math.sin(i / 60 * 0.7), y=0.5 + 0.3 * math.cos(i / 60 * 0.5)) for i in range(216001)],
    'clicks': [dict(active_modifiers=[], cursor_num=0, cursor_id='primary', time_ms=i * 5000 + (0 if down else 80), down=down) for i in range(1, 720) for down in (True, False)],
}))
(output / 'recording-meta.json').write_text(json.dumps(dict(pretty_name='Performance verification', display=dict(path='content/display.mp4', fps=60), cursor='content/cursor.json')))
(output / 'project-config.json').write_text(json.dumps(dict(cursor=dict(type='circle', raw=False, motionBlur=0.6, rotationAmount=0.4), background=dict(padding=10), timeline=dict(segments=[dict(recordingSegment=0, timescale=1, start=0, end=3600, name=None)], zoomSegments=[]))))
seed = content / 'seed.mp4'
subprocess.run(['ffmpeg', '-hide_banner', '-loglevel', 'error', '-f', 'lavfi', '-i', 'color=c=0x506070:s=320x180:r=60', '-t', '10', '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '30', '-g', '120', '-pix_fmt', 'yuv420p', str(seed)], check=True)
subprocess.run(['ffmpeg', '-hide_banner', '-loglevel', 'error', '-stream_loop', '359', '-i', str(seed), '-t', '3600', '-c', 'copy', str(content / 'display.mp4')], check=True)
seed.unlink()
print(output)

Validation

  • cargo fmt --all, cargo check -p cap-editor, strict Clippy on all editor targets.
  • Editor library tests: 183 passed, 3 existing ignored, using isolated software Vulkan and app cache directories. Existing spring/raw/source invalidation tests pass; unrelated-setting reuse coverage is extended.
  • Full Linux GPUI binary build and linking passed with the patched SDK and standalone Cargo/Rust 1.99.

Checked current public PRs/issues for duplicates. #2499 optimizes per-frame cursor event searches in rendering and complements this change; it does not address watch-update reconstruction.

Closes #2506.

RetriggerView in GreptileConfidence Score: 5/5

The current head appears safe to merge; the repeated-press issue is fixed.

Summary

Playback now reuses cursor timelines when edits leave raw mode and spring settings unchanged.

  • Playback keeps cursor timelines when raw mode and spring settings stay the same.
  • The playback benchmark can edit settings and report renderer callback gaps.

Reviews (2) · Last reviewed commit: "fix(editor): preserve live-edit toggles ..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

@AksharP5 is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread crates/editor/examples/editor-playback-benchmark.rs
@richiemcilroy

Copy link
Copy Markdown
Member

Thanks for the PR. Greptile currently has this at 4/5, and we only merge PRs once Greptile scores the current head 5/5. Please address the remaining Greptile feedback, then comment @greptileai to request a re-review, and repeat until it reaches 5/5.

@AksharP5

Copy link
Copy Markdown
Contributor Author

@greptileai Please re-review current head 03ebccf. The repeated-press finding is fixed: the high/low toggle now persists across presses while edit counts remain per press. Native playback verified that the next press changes the setting rather than writing the same value again. The current 4/5 summary still refers to the earlier 9df8106 commit.

@richiemcilroy
richiemcilroy merged commit 7a4bac9 into CapSoftware:main Oct 18, 2026
43 of 45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GPUI playback hitches during background edits on long recordings

2 participants