diff --git a/crates/compositor/examples/decode_bench_macos.rs b/crates/compositor/examples/decode_bench_macos.rs new file mode 100644 index 000000000..d03866ec9 --- /dev/null +++ b/crates/compositor/examples/decode_bench_macos.rs @@ -0,0 +1,65 @@ +//! Bench décodage seul (macOS) : déroule `pipeline_macos::Decoder` sur N frames et +//! imprime le débit. C'est le même décodeur que la marche d'export (`next()` inclut +//! la conversion NV12/CVPixelBuffer du chemin logiciel), piloté par la même variable +//! `OPENSCREEN_MAC_DECODE=software|videotoolbox` que l'arbitrage de production. +//! +//! Utilisation : `cargo run --release --example decode_bench_macos -- [frames]` + +#[cfg(target_os = "macos")] +fn main() -> anyhow::Result<()> { + let path = std::env::args().nth(1).unwrap_or_else(|| { + eprintln!("usage: decode_bench_macos [frames=1200]"); + std::process::exit(2); + }); + let frames: u64 = std::env::args() + .nth(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(1200); + // Un mode mal orthographié serait ignoré par l'arbitrage de production (qui garde + // son défaut) : le bench mesurerait l'autre chemin en croyant mesurer celui demandé. + let forced = std::env::var("OPENSCREEN_MAC_DECODE").ok(); + match forced.as_deref() { + None | Some("software") | Some("videotoolbox") => {} + Some(other) => anyhow::bail!("OPENSCREEN_MAC_DECODE={other} invalide (software|videotoolbox)"), + } + + let gpu = openscreen_compositor::d3d_macos::Gpu::create(false)?; + unsafe { + let mut dec = openscreen_compositor::pipeline_macos::Decoder::open_for_export(&path, &gpu)?; + // `open_with` retombe silencieusement sur logiciel si `av_hwdevice_ctx_create` + // échoue : refuser plutôt que chronométrer un autre chemin que celui demandé. + if forced.as_deref() == Some("videotoolbox") && !dec.uses_videotoolbox() { + anyhow::bail!( + "OPENSCREEN_MAC_DECODE=videotoolbox demandé, mais VideoToolbox n'a pas pris (retombe logiciel)" + ); + } + let t0 = std::time::Instant::now(); + let mut n: u64 = 0; + // Boucle sur le fichier jusqu'à N frames décodées : une source plus courte que la + // cible ne doit pas raccourcir la mesure. Le rewind (seek 0 + flush) reste dans le + // chronomètre, c'est négligeable devant le décodage. + let mut pass_frames: u64 = 0; + while n < frames { + let f = dec.next()?; + if f.is_null() { + // Une passe complète sans aucune frame décodable rewinderait indéfiniment. + if pass_frames == 0 { + anyhow::bail!("source sans aucune frame décodable"); + } + pass_frames = 0; + dec.rewind()?; + continue; + } + n += 1; + pass_frames += 1; + } + let dt = t0.elapsed().as_secs_f64(); + println!("{n} frames en {dt:.3} s — {:.0} fps", n as f64 / dt); + } + Ok(()) +} + +#[cfg(not(target_os = "macos"))] +fn main() { + eprintln!("macOS only"); +} diff --git a/crates/compositor/examples/decode_pure_macos.rs b/crates/compositor/examples/decode_pure_macos.rs new file mode 100644 index 000000000..2ae9e5ca0 --- /dev/null +++ b/crates/compositor/examples/decode_pure_macos.rs @@ -0,0 +1,155 @@ +//! Diagnostic : décodage H.264/HEVC PUR (avcodec_receive_frame, thread_count=0), +//! sans la conversion NV12/CVPixelBuffer de `mac_frames::CpuFrames` — pour séparer +//! le coût du décodeur logiciel de celui de la présentation dans decode_bench_macos. +//! Le chemin VT y est câblé explicitement (`get_format` + vérification du format de +//! la première frame) : un comptage de frames logiciel étiqueté VideoToolbox serait +//! pire qu'une erreur. +//! +//! Utilisation : `cargo run --release --example decode_pure_macos -- [frames] [software|videotoolbox]` + +#[cfg(target_os = "macos")] +fn main() -> anyhow::Result<()> { + use std::ffi::CString; + use std::ptr; + + use openscreen_compositor::ffi; + + let path = std::env::args().nth(1).unwrap_or_else(|| { + eprintln!("usage: decode_pure_macos [frames=1200] [software|videotoolbox]"); + std::process::exit(2); + }); + let frames: u64 = std::env::args() + .nth(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(1200); + let mode = std::env::args().nth(3).unwrap_or_else(|| "software".into()); + if !matches!(mode.as_str(), "software" | "videotoolbox") { + anyhow::bail!("mode {mode} invalide (software|videotoolbox)"); + } + + unsafe extern "C" fn get_videotoolbox_format( + _ctx: *mut ffi::AVCodecContext, + pix_fmts: *const ffi::AVPixelFormat::Type, + ) -> ffi::AVPixelFormat::Type { + if pix_fmts.is_null() { + return ffi::AVPixelFormat::AV_PIX_FMT_NONE; + } + let mut p = pix_fmts; + while *p != ffi::AVPixelFormat::AV_PIX_FMT_NONE { + if *p == ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX { + return *p; + } + p = p.add(1); + } + ffi::AVPixelFormat::AV_PIX_FMT_NONE + } + + unsafe { + let mut fmt: *mut ffi::AVFormatContext = ptr::null_mut(); + let cpath = CString::new(path)?; + ffi::averr( + ffi::avformat_open_input(&mut fmt, cpath.as_ptr(), ptr::null(), ptr::null_mut()), + "open", + )?; + ffi::averr(ffi::avformat_find_stream_info(fmt, ptr::null_mut()), "find_stream_info")?; + let vidx = ffi::av_find_best_stream( + fmt, + ffi::AVMediaType::AVMEDIA_TYPE_VIDEO, + -1, + -1, + ptr::null_mut(), + 0, + ); + if vidx < 0 { + anyhow::bail!("pas de flux vidéo"); + } + let codecpar = (*ffi::sn_fmt_stream(fmt, vidx)).codecpar; + + let dec = ffi::avcodec_find_decoder((*codecpar).codec_id); + let dctx = ffi::avcodec_alloc_context3(dec); + ffi::averr(ffi::avcodec_parameters_to_context(dctx, codecpar), "params_to_ctx")?; + + let mut hwdev: *mut ffi::AVBufferRef = ptr::null_mut(); + if mode == "videotoolbox" { + ffi::averr( + ffi::av_hwdevice_ctx_create( + &mut hwdev, + ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX, + ptr::null(), + ptr::null_mut(), + 0, + ), + "hwdevice", + )?; + (*dctx).hw_device_ctx = ffi::av_buffer_ref(hwdev); + // Sans `get_format`, libavcodec choisit son format natif : `hw_device_ctx` + // seul ne garantit pas AV_PIX_FMT_VIDEOTOOLBOX. La boucle ci-dessous refuse + // en plus toute première frame qui n'aurait pas ce format. + (*dctx).get_format = Some(get_videotoolbox_format); + } else { + (*dctx).thread_count = 0; + } + ffi::averr(ffi::avcodec_open2(dctx, dec, ptr::null_mut()), "open2")?; + + let pkt = ffi::av_packet_alloc(); + let frame = ffi::av_frame_alloc(); + let mut sent_eof = false; + let mut n: u64 = 0; + let t0 = std::time::Instant::now(); + while n < frames { + let r = ffi::avcodec_receive_frame(dctx, frame); + if r == 0 { + // En VT, compter une frame système serait étiqueter « VideoToolbox » un + // décodage logiciel (retombée silencieuse du hwaccel). + if mode == "videotoolbox" + && (*frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32 + { + anyhow::bail!( + "frame reçue en format {}, pas VIDEOTOOLBOX — le chemin demandé n'a pas pris", + (*frame).format + ); + } + n += 1; + continue; + } + if r == ffi::AVERROR_EOF { + break; + } + if r != ffi::AVERROR_EAGAIN { + ffi::averr(r, "receive_frame")?; + } + if sent_eof { + ffi::avcodec_send_packet(dctx, ptr::null_mut()); + continue; + } + let rr = ffi::av_read_frame(fmt, pkt); + if rr == ffi::AVERROR_EOF { + ffi::avcodec_send_packet(dctx, ptr::null_mut()); + sent_eof = true; + } else { + ffi::averr(rr, "read_frame")?; + if (*pkt).stream_index == vidx { + ffi::averr(ffi::avcodec_send_packet(dctx, pkt), "send_packet")?; + } + ffi::av_packet_unref(pkt); + } + } + if n < frames { + anyhow::bail!("source finie après {n} frames ; {frames} demandées — mesure incomplète"); + } + let dt = t0.elapsed().as_secs_f64(); + eprintln!( + "codec={} profil={} format={} mode={mode}", + (*codecpar).codec_id, + (*dctx).profile, + (*codecpar).format + ); + println!("{n} frames en {dt:.3} s — {:.0} fps (pur, sans present)", n as f64 / dt); + } + Ok(()) +} + +#[cfg(not(target_os = "macos"))] +fn main() { + eprintln!("macOS only"); +} diff --git a/crates/compositor/src/pipeline_macos.rs b/crates/compositor/src/pipeline_macos.rs index 5721c2011..e1f206fec 100644 --- a/crates/compositor/src/pipeline_macos.rs +++ b/crates/compositor/src/pipeline_macos.rs @@ -235,10 +235,30 @@ impl Decoder { // rapide du cas qui en profite le plus — 71 fps, c'est en dessous du temps // réel pour une timeline 4K60. // - // RESTE NON MESURÉ : 10 bits et HEVC. Ils gardent VideoToolbox, et la - // condition les écarte par construction (`format == YUV420P` et - // `codec_id == H264`). La preview aussi n'a pas été mesurée, et la changer - // sans la mesurer serait exactement l'erreur que ce commit corrige. + // HEVC 8 bits : MESURÉ DEPUIS, et le logiciel ne gagne plus nettement. Même + // protocole via le Decoder de production (present NV12/CVPixelBuffer compris), + // 1200 frames, meilleur de trois passes, Mac mini M1 8 Go / macOS 26.5, Parsec + // déconnecté. Sources hevc_videotoolbox Main yuv420p encodées depuis la capture + // réelle la plus dense (11,5 Mbps) ; le 4K est un upscale lanczos à 24 Mbps : + // + // HEVC 1080p60 logiciel 452 fps VideoToolbox 303 fps x1,5 + // HEVC 4K60 logiciel 179 fps VideoToolbox 102 fps x1,8 + // + // Le décodeur logiciel HEVC coûte ~3x le H.264 (452 contre 1862 fps en 1080p, + // même session), et VideoToolbox tient le temps réel dans les deux cas (303 + // fps en 1080p60, 102 fps en 4K60) : la marche d'export, bornée par l'encodeur + // (cf. technical-documentation/engineering/rendering-performance.md), n'y + // gagnerait presque rien. HEVC garde VideoToolbox. Témoin de méthode : le + // côté logiciel du cas 1080p ci-dessus reproduit les 2586 fps d'alors (2721 + // fps en décodage pur) ; le 212 fps VideoToolbox ne se reproduit sous aucune + // variante du harnais (294-330), le x12,2 est donc une borne haute. Outil : + // `examples/decode_bench_macos.rs` et `examples/decode_pure_macos.rs`. + // + // RESTE NON MESURÉ : 10 bits. Il garde VideoToolbox, la condition l'écarte + // par construction (`format == YUV420P`), et le chemin logiciel le tronquerait + // en 8 bits (`CpuFrames` ne rend que du NV12). La preview aussi n'a pas été + // mesurée, et la changer sans la mesurer serait exactement l'erreur que ce + // commit corrige. _ if intent == DecodeIntent::Export && is_h264_8bit => false, _ => true, }; @@ -518,6 +538,16 @@ impl Decoder { } } + /// Le décodeur rend-il ses frames par VideoToolbox (`false` = chemin logiciel + /// `CpuFrames`). Exposé pour que le harnais de mesure + /// (`examples/decode_bench_macos.rs`) puisse vérifier que le mode demandé via + /// `OPENSCREEN_MAC_DECODE` a bien été pris : `open_with` retombe silencieusement sur + /// logiciel si `av_hwdevice_ctx_create` échoue, et un bench qui mesure l'autre chemin + /// que celui qu'il croit mesurer est pire qu'un bench qui échoue. + pub fn uses_videotoolbox(&self) -> bool { + self.cpu.is_none() + } + /// Temps (s) de la frame courante, via son pts. 0 si pas de pts fiable. /// Symétrique de `pipeline_windows::Decoder::cur_time_sec`. pub unsafe fn cur_time_sec(&self) -> f64 {