Skip to content

Commit 4394baf

Browse files
committed
chore(desktop-gpui): benchmark bubble-sized camera preview conversion
1 parent cadef4a commit 4394baf

1 file changed

Lines changed: 292 additions & 2 deletions

File tree

‎apps/desktop-gpui/examples/camera-preview-convert-benchmark.rs‎

Lines changed: 292 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111
//! Metal-compatible IOSurface-backed BGRA buffer from a fixed ring, ready for
1212
//! `paint_surface` with zero further CPU touches.
1313
//!
14-
//! Run: cargo run -p cap-desktop-gpui --example camera-preview-convert-benchmark --release
14+
//! `--bubble` instead measures the preview's conversion at full camera size
15+
//! (what `main` converts every frame to) against the bubble-sized cover
16+
//! target, for small/medium/large bubbles on a 2x display, mirrored and not:
17+
//! per-frame time, bytes written per frame, the ring's resident memory, and
18+
//! how close each looks once gpui's linear sampler draws it at bubble size.
19+
//!
20+
//! Run: cargo run -p cap-desktop-gpui --example camera-preview-convert-benchmark --release [-- --bubble]
1521
1622
#[cfg(not(target_os = "macos"))]
1723
fn main() {
@@ -20,7 +26,11 @@ fn main() {
2026

2127
#[cfg(target_os = "macos")]
2228
fn main() {
23-
macos::run();
29+
if std::env::args().any(|arg| arg == "--bubble") {
30+
macos::bubble();
31+
} else {
32+
macos::run();
33+
}
2434
}
2535

2636
#[cfg(target_os = "macos")]
@@ -249,4 +259,284 @@ mod macos {
249259
);
250260
}
251261
}
262+
263+
type CreateRotationSession = unsafe extern "C-unwind" fn(
264+
Option<&cf::Allocator>,
265+
*mut Option<arc::R<vt::PixelRotationSession>>,
266+
) -> cidre::os::Status;
267+
type RotateImage = unsafe extern "C-unwind" fn(
268+
&vt::PixelRotationSession,
269+
&cv::PixelBuf,
270+
&mut cv::PixelBuf,
271+
) -> cidre::os::Status;
272+
273+
struct FlipSession {
274+
session: arc::R<vt::PixelRotationSession>,
275+
rotate_image: RotateImage,
276+
}
277+
278+
impl FlipSession {
279+
fn new() -> Self {
280+
let create = unsafe {
281+
libc::dlsym(libc::RTLD_DEFAULT, c"VTPixelRotationSessionCreate".as_ptr())
282+
};
283+
let rotate = unsafe {
284+
libc::dlsym(
285+
libc::RTLD_DEFAULT,
286+
c"VTPixelRotationSessionRotateImage".as_ptr(),
287+
)
288+
};
289+
let key = unsafe {
290+
libc::dlsym(
291+
libc::RTLD_DEFAULT,
292+
c"kVTPixelRotationPropertyKey_FlipHorizontalOrientation".as_ptr(),
293+
)
294+
};
295+
assert!(!create.is_null() && !rotate.is_null() && !key.is_null());
296+
let create =
297+
unsafe { std::mem::transmute::<*mut libc::c_void, CreateRotationSession>(create) };
298+
let rotate_image =
299+
unsafe { std::mem::transmute::<*mut libc::c_void, RotateImage>(rotate) };
300+
let key = unsafe { key.cast::<*const cf::String>().read().as_ref() }
301+
.expect("flip property key");
302+
let mut session = None;
303+
unsafe { create(None, &mut session).result() }.expect("rotation session");
304+
let mut session = session.expect("rotation session");
305+
session
306+
.set_prop(key, Some(cf::Boolean::value_true().as_ref()))
307+
.expect("flip property");
308+
Self {
309+
session,
310+
rotate_image,
311+
}
312+
}
313+
314+
fn rotate(&self, source: &cv::PixelBuf, destination: &mut cv::PixelBuf) {
315+
unsafe { (self.rotate_image)(&self.session, source, destination).result() }
316+
.expect("flip");
317+
}
318+
}
319+
320+
const MAIN_RING: usize = 4;
321+
const PREVIEW_RING: usize = 6;
322+
const QUALITY_SEED: u8 = 200;
323+
324+
struct Case {
325+
dims: (usize, usize),
326+
stats: (f64, f64, f64),
327+
buffer_bytes: usize,
328+
output: Vec<u8>,
329+
}
330+
331+
fn bgra_ring(count: usize, (width, height): (usize, usize)) -> Vec<arc::R<cv::PixelBuf>> {
332+
let attrs = iosurface_bgra_attrs();
333+
(0..count)
334+
.map(|_| {
335+
let buf = cv::PixelBuf::new(width, height, cv::PixelFormat::_32_BGRA, Some(&attrs))
336+
.expect("IOSurface BGRA destination");
337+
assert!(buf.io_surf().is_some());
338+
buf
339+
})
340+
.collect()
341+
}
342+
343+
fn run_case(
344+
sources: &[arc::R<cv::PixelBuf>],
345+
quality_source: &cv::PixelBuf,
346+
dims: (usize, usize),
347+
mirrored: bool,
348+
) -> Case {
349+
let mut session = vt::PixelTransferSession::new().expect("VT session");
350+
session.set_realtime(true).expect("VT realtime");
351+
let flip = mirrored.then(FlipSession::new);
352+
let mut ring = bgra_ring(PREVIEW_RING, dims);
353+
let scratch = flip.as_ref().map(|_| bgra_ring(1, dims).remove(0));
354+
let mut stats = Stats::new();
355+
let mut next = 0;
356+
for iteration in 0..(WARMUP + ITERATIONS) {
357+
let source = &sources[iteration % sources.len()];
358+
let out = &mut ring[next];
359+
let started = Instant::now();
360+
match (&flip, &scratch) {
361+
(Some(flip), Some(scratch)) => {
362+
session.transfer(source, scratch).expect("transfer");
363+
flip.rotate(scratch, out);
364+
}
365+
_ => session.transfer(source, out).expect("transfer"),
366+
}
367+
let elapsed = started.elapsed().as_secs_f64() * 1e6;
368+
next = (next + 1) % PREVIEW_RING;
369+
if iteration >= WARMUP {
370+
stats.push(elapsed);
371+
}
372+
}
373+
session
374+
.transfer(quality_source, &ring[0])
375+
.expect("quality transfer");
376+
let buffer_bytes = {
377+
let ptr = ring[0].as_ref() as *const cv::PixelBuf;
378+
unsafe { CVPixelBufferGetBytesPerRow(ptr) * dims.1 }
379+
};
380+
let output = read_bgra(&mut ring[0], dims.0, dims.1);
381+
Case {
382+
dims,
383+
stats: stats.report(),
384+
buffer_bytes,
385+
output,
386+
}
387+
}
388+
389+
fn make_quality_source(width: usize, height: usize) -> arc::R<cv::PixelBuf> {
390+
let mut buf = make_420v_source(width, height, QUALITY_SEED);
391+
unsafe {
392+
buf.lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT)
393+
.result()
394+
.expect("lock 420v source");
395+
let y_stride = buf.plane_bytes_per_row(0);
396+
let y_base = buf.plane_base_address(0).cast_mut();
397+
for row in 0..height {
398+
for col in 0..width {
399+
let x = col as f64 / width as f64;
400+
let y = row as f64 / height as f64;
401+
let smooth = 0.5 + 0.25 * (x * 23.0).sin() * (y * 17.0).cos();
402+
let detail = 0.15 * ((x * 640.0).sin() * (y * 360.0).sin());
403+
let edge = if ((col * 8 / width) + (row * 6 / height)) % 2 == 0 {
404+
0.1
405+
} else {
406+
-0.1
407+
};
408+
let value = (smooth + detail + edge).clamp(0.0, 1.0);
409+
*y_base.add(row * y_stride + col) = (16.0 + value * 219.0) as u8;
410+
}
411+
}
412+
buf.unlock_lock_base_addr(cv::pixel_buffer::LockFlags::DEFAULT);
413+
}
414+
buf
415+
}
416+
417+
fn area_average(pixels: &[u8], src: (usize, usize), dst: (usize, usize)) -> Vec<f64> {
418+
let mut out = vec![0.0; dst.0 * dst.1 * 4];
419+
for y in 0..dst.1 {
420+
let y0 = y * src.1 / dst.1;
421+
let y1 = ((y + 1) * src.1 / dst.1).max(y0 + 1);
422+
for x in 0..dst.0 {
423+
let x0 = x * src.0 / dst.0;
424+
let x1 = ((x + 1) * src.0 / dst.0).max(x0 + 1);
425+
let mut sum = [0.0; 4];
426+
for sy in y0..y1 {
427+
for sx in x0..x1 {
428+
let offset = (sy * src.0 + sx) * 4;
429+
for (channel, total) in sum.iter_mut().enumerate() {
430+
*total += pixels[offset + channel] as f64;
431+
}
432+
}
433+
}
434+
let count = ((y1 - y0) * (x1 - x0)) as f64;
435+
for (channel, total) in sum.iter().enumerate() {
436+
out[(y * dst.0 + x) * 4 + channel] = total / count;
437+
}
438+
}
439+
}
440+
out
441+
}
442+
443+
fn linear_sample(pixels: &[u8], src: (usize, usize), dst: (usize, usize)) -> Vec<f64> {
444+
let mut out = vec![0.0; dst.0 * dst.1 * 4];
445+
let texel =
446+
|x: usize, y: usize, channel: usize| pixels[(y * src.0 + x) * 4 + channel] as f64;
447+
for y in 0..dst.1 {
448+
let v = ((y as f64 + 0.5) * src.1 as f64 / dst.1 as f64 - 0.5)
449+
.clamp(0.0, (src.1 - 1) as f64);
450+
let (y0, fy) = (v.floor() as usize, v.fract());
451+
let y1 = (y0 + 1).min(src.1 - 1);
452+
for x in 0..dst.0 {
453+
let u = ((x as f64 + 0.5) * src.0 as f64 / dst.0 as f64 - 0.5)
454+
.clamp(0.0, (src.0 - 1) as f64);
455+
let (x0, fx) = (u.floor() as usize, u.fract());
456+
let x1 = (x0 + 1).min(src.0 - 1);
457+
for channel in 0..4 {
458+
let top = texel(x0, y0, channel) * (1.0 - fx) + texel(x1, y0, channel) * fx;
459+
let bottom = texel(x0, y1, channel) * (1.0 - fx) + texel(x1, y1, channel) * fx;
460+
out[(y * dst.0 + x) * 4 + channel] = top * (1.0 - fy) + bottom * fy;
461+
}
462+
}
463+
}
464+
out
465+
}
466+
467+
fn psnr(reference: &[f64], candidate: &[f64]) -> f64 {
468+
let mut squared = 0.0;
469+
let mut count = 0usize;
470+
for (pixel, other) in reference.chunks_exact(4).zip(candidate.chunks_exact(4)) {
471+
for (a, b) in pixel[..3].iter().zip(&other[..3]) {
472+
squared += (a - b).powi(2);
473+
count += 1;
474+
}
475+
}
476+
let mse = squared / count as f64;
477+
if mse == 0.0 {
478+
return f64::INFINITY;
479+
}
480+
10.0 * (255.0f64.powi(2) / mse).log10()
481+
}
482+
483+
fn mb(bytes: usize) -> f64 {
484+
bytes as f64 / 1_048_576.0
485+
}
486+
487+
pub fn bubble() {
488+
let bubbles = [
489+
("small 230pt round", (460.0f32, 460.0f32)),
490+
("medium 400pt round", (800.0, 800.0)),
491+
("large 600pt full", (600.0 * 16.0 / 9.0 * 2.0, 1200.0)),
492+
];
493+
println!(
494+
"| source | bubble | mirrored | path | output | mean us | p50 us | p95 us | MB written/frame | ring MB | PSNR at bubble size dB |"
495+
);
496+
println!("|---|---|---|---|---|---|---|---|---|---|---|");
497+
for (width, height) in [(1280usize, 720usize), (1920, 1080), (3840, 2160)] {
498+
let sources: Vec<_> = (0..SOURCE_RING)
499+
.map(|seed| make_420v_source(width, height, seed as u8))
500+
.collect();
501+
let quality_source = make_quality_source(width, height);
502+
for mirrored in [false, true] {
503+
let passes = if mirrored { 2 } else { 1 };
504+
let scratch = usize::from(mirrored);
505+
let full = run_case(&sources, &quality_source, (width, height), mirrored);
506+
for (label, physical) in bubbles {
507+
let displayed = cap_gpui_kernels::camera_preview::cover_dimensions(
508+
(width, height),
509+
(physical.0.ceil() as usize, physical.1.ceil() as usize),
510+
);
511+
let target = cap_gpui_kernels::camera_preview::quantized_target(physical)
512+
.map_or((width, height), |target| {
513+
cap_gpui_kernels::camera_preview::cover_dimensions(
514+
(width, height),
515+
target,
516+
)
517+
});
518+
let bubble = run_case(&sources, &quality_source, target, mirrored);
519+
let reference = area_average(&full.output, full.dims, displayed);
520+
for (path, case, slots) in [
521+
("main (full size)", &full, MAIN_RING),
522+
("branch (bubble)", &bubble, PREVIEW_RING),
523+
] {
524+
let (mean, p50, p95) = case.stats;
525+
let shown = linear_sample(&case.output, case.dims, displayed);
526+
println!(
527+
"| {width}x{height} | {label} | {mirrored} | {path} | {}x{} | {mean:.1} | {p50:.1} | {p95:.1} | {:.2} | {:.1} | {:.2} |",
528+
case.dims.0,
529+
case.dims.1,
530+
mb(case.buffer_bytes * passes),
531+
mb(case.buffer_bytes * (slots + scratch)),
532+
psnr(&reference, &shown),
533+
);
534+
}
535+
}
536+
}
537+
}
538+
println!(
539+
"ring MB: main keeps {MAIN_RING} slots (#2761 and this branch {PREVIEW_RING}), plus one scratch buffer when mirrored; PSNR compares gpui's linear sampling of each output at the displayed cover size against an area average of the full-size conversion."
540+
);
541+
}
252542
}

0 commit comments

Comments
 (0)