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
34 changes: 21 additions & 13 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2358,22 +2358,30 @@ impl Compositor {
let y = cache.make_texture_from_pixel_buffer(out_tex, 0, metal::MTLPixelFormat::R8Unorm)?;
let uv = cache.make_texture_from_pixel_buffer(out_tex, 1, metal::MTLPixelFormat::RG8Unorm)?;

let cmd_buf = self.gpu.context.new_command_buffer();
for (target, pipeline) in [(&y, &self.pipeline_fs_y), (&uv, &self.pipeline_fs_uv)] {
let enc = self.begin_pass(
cmd_buf,
target,
Some(metal::MTLClearColor::new(0.0, 0.0, 0.0, 1.0)),
pipeline,
)?;
enc.set_fragment_texture(0, Some(&self.rt));
enc.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 3);
enc.end_encoding();
{
let _p = crate::export_probe::scope(crate::export_probe::Stage::Nv12Passes);
let cmd_buf = self.gpu.context.new_command_buffer();
for (target, pipeline) in [(&y, &self.pipeline_fs_y), (&uv, &self.pipeline_fs_uv)] {
let enc = self.begin_pass(
cmd_buf,
target,
Some(metal::MTLClearColor::new(0.0, 0.0, 0.0, 1.0)),
pipeline,
)?;
enc.set_fragment_texture(0, Some(&self.rt));
enc.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 3);
enc.end_encoding();
}
self.submit(cmd_buf);
}
// Pas de miroir `Shared`, pas de `getBytes` : c'est tout l'intérêt. On attend
// quand même, parce que `avcodec_send_frame` va lire ce buffer juste après.
self.submit(cmd_buf);
self.sync();
// L'attente porte sur TOUT le travail GPU de la frame, composition comprise :
// `compose_frame` n'a fait que soumettre.
{
let _p = crate::export_probe::scope(crate::export_probe::Stage::GpuWait);
self.sync();
}
Ok(())
}

Expand Down
144 changes: 144 additions & 0 deletions crates/compositor/src/export_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//! Sondes de temps par étage pour l'export, activées par `OPENSCREEN_EXPORT_PROFILE=1`.
//!
//! Le but est de répondre à UNE question — où part le temps d'un export — sans avoir à
//! croire une intuition. Chaque étage accumule des nanosecondes et un compte d'appels ;
//! `report` imprime le tableau sur stderr à la fin de la marche.
//!
//! # Coût quand c'est éteint
//!
//! `scope()` lit un `OnceLock<bool>` et, si la sonde est éteinte, ne prend AUCUNE horloge :
//! le `Scope` rendu porte `None` et son `Drop` ne fait rien. Allumée, elle coûte deux
//! `Instant::now()` (un `mach_absolute_time` chacun, ~20 ns sur Apple Silicon) et un
//! `fetch_add` relaxé par étage et par frame.
//!
//! # Ce que les nombres veulent dire, et ne veulent pas dire
//!
//! Les étages sont mesurés là où le CPU les appelle, pas là où le GPU les exécute. Metal
//! est asynchrone : `compose_frame` ne fait que soumettre, et l'attente de TOUT le travail
//! GPU de la frame tombe dans `gpu_wait`. Lire `compose` comme « le coût de la composition »
//! est donc faux — c'est le coût de la CONSTRUIRE, pas de la rendre.

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use std::time::Instant;

#[derive(Clone, Copy)]
pub enum Stage {
DecodeScreen = 0,
DecodeWebcam = 1,
Compose = 2,
VtGetBuffer = 3,
Nv12Passes = 4,
GpuWait = 5,
SendFrame = 6,
DrainMux = 7,
Progress = 8,
Finalize = 9,
}

const N: usize = 10;

const NAMES: [&str; N] = [
"decode.screen",
"decode.webcam",
"compose.submit",
"vt.get_buffer",
"nv12.passes",
"gpu.wait",
"enc.send_frame",
"mux.drain",
"progress.cb",
"finalize",
];

static NANOS: [AtomicU64; N] = [
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
];
static COUNT: [AtomicU64; N] = [
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0),
];

static ENABLED: OnceLock<bool> = OnceLock::new();

pub fn enabled() -> bool {
*ENABLED.get_or_init(|| {
matches!(
std::env::var("OPENSCREEN_EXPORT_PROFILE").ok().as_deref(),
Some("1") | Some("true")
)
})
}

pub struct Scope {
stage: usize,
t0: Option<Instant>,
}

impl Drop for Scope {
fn drop(&mut self) {
if let Some(t0) = self.t0 {
NANOS[self.stage].fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
COUNT[self.stage].fetch_add(1, Ordering::Relaxed);
}
}
}

/// Ouvre une sonde sur `stage`. Le temps est compté jusqu'au `Drop` du `Scope` rendu.
pub fn scope(stage: Stage) -> Scope {
Scope {
stage: stage as usize,
t0: if enabled() { Some(Instant::now()) } else { None },
}
}

/// Imprime le tableau sur stderr. `wall_s` est le mur total de la fonction d'export, ce qui
/// permet de voir ce que les étages NE couvrent pas.
pub fn report(wall_s: f64, frames: u64) {
if !enabled() {
return;
}
let total_ns: u64 = (0..N).map(|i| NANOS[i].load(Ordering::Relaxed)).sum();
eprintln!("[profile] {frames} frames en {wall_s:.3} s ({:.1} fps)", frames as f64 / wall_s.max(1e-9));
eprintln!("[profile] {:<16} {:>10} {:>9} {:>8} {:>7}", "étage", "total (s)", "µs/frame", "% mur", "appels");
let mut rows: Vec<usize> = (0..N).collect();
rows.sort_by_key(|&i| std::cmp::Reverse(NANOS[i].load(Ordering::Relaxed)));
for i in rows {
let ns = NANOS[i].load(Ordering::Relaxed);
let c = COUNT[i].load(Ordering::Relaxed);
if c == 0 {
continue;
}
eprintln!(
"[profile] {:<16} {:>10.3} {:>9.1} {:>7.1}% {:>7}",
NAMES[i],
ns as f64 / 1e9,
ns as f64 / 1e3 / c as f64,
100.0 * (ns as f64 / 1e9) / wall_s.max(1e-9),
c
);
}
eprintln!(
"[profile] {:<16} {:>10.3} {:>9} {:>7.1}%",
"SOMME sondes",
total_ns as f64 / 1e9,
"",
100.0 * (total_ns as f64 / 1e9) / wall_s.max(1e-9)
);
eprintln!(
"[profile] {:<16} {:>10.3} {:>9} {:>7.1}% <- ce que les sondes ne couvrent pas",
"non sondé",
wall_s - total_ns as f64 / 1e9,
"",
100.0 * (wall_s - total_ns as f64 / 1e9) / wall_s.max(1e-9)
);
}

/// Remet tous les compteurs à zéro. Un même process peut enchaîner deux exports.
pub fn reset() {
for i in 0..N {
NANOS[i].store(0, Ordering::Relaxed);
COUNT[i].store(0, Ordering::Relaxed);
}
}
2 changes: 1 addition & 1 deletion crates/compositor/src/gif_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ fn export_gif_inner(
let mut screen_decs: HashMap<String, Decoder> = HashMap::new();
let mut webcam_decs: HashMap<String, Decoder> = HashMap::new();
screen_decs.insert(clips[0].screen.clone(), unsafe {
Decoder::open(&clips[0].screen, gpu)?
Decoder::open_for_export(&clips[0].screen, gpu)?
});

let frames = unsafe {
Expand Down
1 change: 1 addition & 0 deletions crates/compositor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub mod audio;
pub mod audio_jobs;
pub mod config;
pub mod cursor;
pub mod export_probe;
pub mod ffi;
pub mod frame_geometry;
pub mod gif_export;
Expand Down
6 changes: 6 additions & 0 deletions crates/compositor/src/pipeline_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ pub struct Decoder {
unsafe impl Send for Decoder {}

impl Decoder {
/// Même point d'entrée que sur macOS, pour que `timeline_walk` reste portable. Le backend
/// Linux décode déjà en logiciel (`SwDecoder`) : l'intention n'a rien à trancher.
pub fn open_for_export(path: &str, gpu: &Gpu) -> Result<Decoder> {
Self::open(path, gpu)
}

pub fn open(path: &str, gpu: &Gpu) -> Result<Decoder> {
let sw = SwDecoder::open(path)?;
let fps = sw.fps();
Expand Down
Loading
Loading