Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f95be9f
fix(rendering): find the bundled DXC from example binaries
cesarfrancots Oct 14, 2026
b227881
chore: add capbench, a repeatable A/B runner for media pipeline bench…
cesarfrancots Oct 14, 2026
6794aab
fix(wgpu-hal): give each DX12 fence wait its own event
cesarfrancots Oct 14, 2026
520b598
perf(rendering): wait on the GPU fence for readbacks instead of sleep…
cesarfrancots Oct 14, 2026
e0f0ec2
perf(rendering): serve Media Foundation frame requests during readahead
cesarfrancots Oct 14, 2026
6535881
chore(export): log NV12 export loop stage timings under CAP_RENDER_LO…
cesarfrancots Oct 14, 2026
a4dae84
perf(recording): pool the CPU frames Windows screen capture copies into
cesarfrancots Oct 14, 2026
75da548
perf(recording): reuse the Windows cursor image while its handle is u…
cesarfrancots Oct 14, 2026
b3a08c5
perf(desktop-gpui): drop the export timer-resolution hold
cesarfrancots Oct 14, 2026
bad975f
fix(capbench): fail runs whose recording does not validate and keep f…
cesarfrancots Oct 14, 2026
6d80438
fix(video-decode): read past Media Foundation samples that carry no f…
cesarfrancots Oct 14, 2026
ba1f2c2
refactor(rendering): drive the Media Foundation decoder from one requ…
cesarfrancots Oct 14, 2026
610ba39
docs(export): note that the loop stats are process-wide
cesarfrancots Oct 14, 2026
9d0ef5a
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 14, 2026
67cdf7b
Merge remote-tracking branch 'origin/main' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
5fd3a90
Merge remote-tracking branch 'origin/main' into perf/windows-media-pi…
cesarfrancots Oct 16, 2026
470f90d
fix(rendering): tag readahead Media Foundation frames with the source…
cesarfrancots Oct 16, 2026
246c17d
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
9611675
fix(scripts): reject benchmark runs that report no measurements
cesarfrancots Oct 16, 2026
ec11a9b
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
f7aa44c
Merge remote-tracking branch 'origin/main' into perf/windows-media-pi…
cesarfrancots Oct 16, 2026
a7ac091
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
0505abc
fix(scripts): check benchmark helpers up front and reject decode runs…
cesarfrancots Oct 16, 2026
2f22c03
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
e908f42
Merge remote-tracking branch 'origin/main' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
b95bfa6
Merge remote-tracking branch 'origin/main' into perf/windows-media-pi…
cesarfrancots Oct 16, 2026
2858f64
fix(scripts): refuse a benchmark fixture that overlaps the work direc…
cesarfrancots Oct 16, 2026
0d5f379
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
03c72d9
fix(scripts): keep successful-run logs when --keep-logs is set
cesarfrancots Oct 16, 2026
65a4b7b
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 16, 2026
9afd214
Merge remote-tracking branch 'origin/main' into perf/windows-media-pi…
cesarfrancots Oct 18, 2026
ad4041d
Merge remote-tracking branch 'origin/main' into perf/mf-decoder-state…
cesarfrancots Oct 18, 2026
3074903
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 18, 2026
e26f981
fix(rendering): pass the submission index in the macOS surface comple…
cesarfrancots Oct 18, 2026
e00c11e
fix(scripts): refuse binaries inside the work directory and playback …
cesarfrancots Oct 18, 2026
1e10ac5
Merge branch 'perf/windows-media-pipeline' into perf/mf-decoder-state…
cesarfrancots Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/sync-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ on:
- "scripts/recording-reliability.py"
- "scripts/recording-reliability-owned-process.cs"
- "scripts/test-recording-reliability.py"
- "scripts/capbench.py"
- "scripts/test-capbench.py"
- ".github/workflows/sync-tests.yml"

concurrency:
Expand Down Expand Up @@ -99,6 +101,7 @@ jobs:
run: |
cargo test --locked -p cap-project --lib
python -B scripts/test-recording-reliability.py
python -B scripts/test-capbench.py

- name: Compile recording process supervisor (Windows)
if: runner.os == 'Windows'
Expand Down
1 change: 0 additions & 1 deletion apps/desktop-gpui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ raw-window-handle = "0.6"
tray-icon = { version = "0.21.1", default-features = false }
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_Media",
"Win32_Security",
"Win32_System_Registry",
"Win32_System_SystemInformation",
Expand Down
23 changes: 0 additions & 23 deletions apps/desktop-gpui/src/editor_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2863,26 +2863,6 @@ fn matches_compression(current: ExportCompression, expected: ExportCompression)
std::mem::discriminant(&current) == std::mem::discriminant(&expected)
}

// The NV12 readback wait sleeps in 1 ms steps (`frame_pipeline.rs`), which the
// default 15.6 ms Windows timer resolution stretches to most of a frame each.
#[cfg(windows)]
struct TimerResolution;

#[cfg(windows)]
impl TimerResolution {
fn hold_one_millisecond() -> Self {
unsafe { windows_sys::Win32::Media::timeBeginPeriod(1) };
Self
}
}

#[cfg(windows)]
impl Drop for TimerResolution {
fn drop(&mut self) {
unsafe { windows_sys::Win32::Media::timeEndPeriod(1) };
}
}

#[allow(clippy::too_many_arguments)]
async fn run_export(
project_path: PathBuf,
Expand All @@ -2903,9 +2883,6 @@ async fn run_export(
if cancel.load(Ordering::Relaxed) {
return Err("Export cancelled".into());
}
#[cfg(windows)]
let _timer_resolution = TimerResolution::hold_one_millisecond();

let mut builder = ExporterBase::builder(project_path).with_force_ffmpeg_decoder(force);
if cursor_only {
builder = builder.with_config(make_cursor_only_project(project));
Expand Down
7 changes: 7 additions & 0 deletions crates/editor/examples/playback-pipeline-benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,12 @@ async fn main() {
.and_then(|s| s.parse().ok())
.unwrap_or(300);
let force_ffmpeg = args.iter().any(|arg| arg == "--force-ffmpeg");
// The editor holds a 1ms timer resolution while playing; this measures under the same condition.
let timer_1ms = args.iter().any(|arg| arg == "--timer-1ms");
#[cfg(target_os = "windows")]
if timer_1ms {
unsafe { windows::Win32::Media::timeBeginPeriod(1) };
}

println!("{}", "=".repeat(60));
println!(" CAP PLAYBACK PIPELINE BENCHMARK");
Expand All @@ -858,6 +864,7 @@ async fn main() {
println!("Target FPS: {fps}");
println!("Max frames: {frame_count}");
println!("Force FFmpeg: {force_ffmpeg}");
println!("1ms timer: {timer_1ms}");
println!();

let (recording_meta, meta, project, recordings) = match load_recording(&recording_path).await {
Expand Down
29 changes: 26 additions & 3 deletions crates/export/src/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,7 @@ async fn export_render_to_channel(
let mut first_frame_data: Option<FirstFrameNv12> = None;
let mut frame_count = 0u32;
let mut consecutive_timeouts = 0u32;
let mut encoder_blocked = Duration::ZERO;

loop {
let timeout_secs = if frame_count == 0 {
Expand Down Expand Up @@ -1530,7 +1531,10 @@ async fn export_render_to_channel(
});
}

if sender.send(export_frame).is_err() {
let send_started = std::time::Instant::now();
let sent = sender.send(export_frame);
encoder_blocked += send_started.elapsed();
if sent.is_err() {
warn!("Encoder dropped, stopping render forwarding");
if stop_on_encoder_drop {
return Err(cap_rendering::RenderingError::ImageLoadError(
Expand Down Expand Up @@ -1559,13 +1563,32 @@ async fn export_render_to_channel(
});
}

Ok::<_, cap_rendering::RenderingError>(())
Ok::<_, cap_rendering::RenderingError>(encoder_blocked)
};

cap_rendering::loop_stats::take();
tokio::try_join!(render_future, forward_future)
};

render_result?;
let (_, encoder_blocked) = render_result?;

// The loop_stats counters are process-wide, so an export that overlaps another one reports
// a mix of both. That is fine for profiling one export at a time; per-export numbers need
// the counters passed through the render loop instead of kept in statics.
if cap_rendering::loop_stats::enabled() {
let [decode, render, prefetch, join, render_send, frames] =
cap_rendering::loop_stats::take();
Comment thread
cesarfrancots marked this conversation as resolved.
info!(
frames,
decode_ms = decode / 1000,
render_ms = render / 1000,
prefetch_ms = prefetch / 1000,
join_ms = join / 1000,
render_send_ms = render_send / 1000,
encoder_send_ms = encoder_blocked.as_millis() as u64,
"NV12 export loop stats"
);
}

Ok(())
}
Expand Down
25 changes: 22 additions & 3 deletions crates/recording/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ pub fn spawn_cursor_recorder(
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
struct CursorData {
image: Vec<u8>,
hotspot: XY<f64>,
Expand Down Expand Up @@ -956,6 +956,23 @@ fn get_cursor_data() -> Option<CursorData> {
return None;
}

// Rendering, trimming and PNG-encoding the cursor every tick only to hash it is wasted
// while the shape is unchanged. Keyed on the handle, so a cursor scheme change that keeps
// the system cursor handles shows the old image until the shape changes; key on the
// bitmap contents instead if that ever matters.
thread_local! {
static LAST_CURSOR: std::cell::RefCell<Option<(usize, CursorData)>> =
const { std::cell::RefCell::new(None) };
}
let handle = cursor_info.hCursor.0 as usize;
if let Some(cached) = LAST_CURSOR.with_borrow(|last| {
last.as_ref()
.filter(|(last_handle, _)| *last_handle == handle)
.map(|(_, data)| data.clone())
}) {
return Some(cached);
}

// Get icon info
let mut icon_info = ICONINFO::default();
if GetIconInfo(cursor_info.hCursor.into(), &mut icon_info).is_err() {
Expand Down Expand Up @@ -1229,11 +1246,13 @@ fn get_cursor_data() -> Option<CursorData> {
)
.ok()?;

Some(CursorData {
let data = CursorData {
image: png_data,
hotspot: XY::new(hotspot_x, hotspot_y),
shape: CursorShape::try_from(&cursor_info.hCursor).ok(),
})
};
LAST_CURSOR.set(Some((handle, data.clone())));
Some(data)
}
}

Expand Down
Loading