Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 65 additions & 0 deletions crates/compositor/examples/decode_bench_macos.rs
Original file line number Diff line number Diff line change
@@ -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 -- <fichier> [frames]`

#[cfg(target_os = "macos")]
fn main() -> anyhow::Result<()> {
let path = std::env::args().nth(1).unwrap_or_else(|| {
eprintln!("usage: decode_bench_macos <fichier> [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)?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// `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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
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");
}
155 changes: 155 additions & 0 deletions crates/compositor/examples/decode_pure_macos.rs
Original file line number Diff line number Diff line change
@@ -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 -- <fichier> [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 <fichier> [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);
Comment thread
EtienneLescot marked this conversation as resolved.
let mode = std::env::args().nth(3).unwrap_or_else(|| "software".into());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// 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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
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");
}
38 changes: 34 additions & 4 deletions crates/compositor/src/pipeline_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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 {
Expand Down
Loading