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
1 change: 1 addition & 0 deletions crates/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ features = [
"Win32_Graphics_Direct2D_Common", # D2D1_COLOR_F / PIXEL_FORMAT
"Win32_Graphics_DirectWrite", # mise en page + rastérisation du texte
"Win32_Graphics_Gdi", # brosses/police GUI de la preview (poc-d3d/src/app.rs)
"Win32_Security", # IDXGIResource1::CreateSharedHandle (shared_frames.rs)
"Win32_System_Performance", # QueryPerformanceCounter / Frequency (§10)
"Win32_System_LibraryLoader", # GetModuleHandleW (hInstance)
"Win32_UI_WindowsAndMessaging", # fenêtres (harnais live + GUI du POC)
Expand Down
73 changes: 72 additions & 1 deletion crates/compositor-view-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use napi::{Env, JsFunction, Task};
use napi_derive::napi;
use openscreen_compositor::compositor::{live_params_from_scene, Compositor};
use openscreen_compositor::d3d::{Backend, Gpu};
use openscreen_compositor::frame_geometry::FootageQuad;
use openscreen_compositor::gif_export::{GifExportParams, GifStats};
use openscreen_compositor::gif_export_control::{GifExportCancelled, GifExportControl};
use openscreen_compositor::live::{LiveView, PausedPreviews};
Expand Down Expand Up @@ -249,12 +250,82 @@ pub fn read_frame(id: i32, since_gen: f64) -> Result<Option<FramePacket>> {
width: w,
height: h,
data: Buffer::from(pixels),
footage: footage.map(|q| q.corners.iter().flatten().map(|&v| v as f64).collect()),
footage: footage_corners(footage),
footage_projective: footage.is_some_and(|q| q.projective),
}
}))
}

/// Les coins du métrage (TL, TR, BR, BL) aplatis en huit nombres, tels que JS les lit.
fn footage_corners(footage: Option<FootageQuad>) -> Option<Vec<f64>> {
footage.map(|q| q.corners.iter().flatten().map(|&v| v as f64).collect())
}

/// Une frame de preview posée dans une texture partagée : de quoi l'importer côté GPU
/// (`sharedTexture.importSharedTexture`) au lieu d'en recevoir les pixels.
#[napi(object)]
pub struct SharedFramePacket {
pub gen: f64,
/// Case de l'anneau qui porte la frame, à rendre avec `gen` par `release_shared_frame`
/// quand Chromium l'a relâchée.
pub slot: u32,
/// Handle NT de la texture, 8 octets little-endian : la forme qu'attend Electron
/// (`SharedTextureHandle.ntHandle`). Valable dans ce processus seulement.
pub handle: Buffer,
pub width: u32,
pub height: u32,
pub footage: Option<Vec<f64>>,
pub footage_projective: bool,
}

/// Livrer les frames de la vue par textures partagées plutôt que par `read_frame`. Rend
/// `false` quand la machine ne le peut pas (hors Windows, backend logiciel) : la vue reste
/// alors au readback.
#[napi]
pub fn set_shared_frames(id: i32, enabled: bool) -> bool {
registry()
.lock()
.unwrap()
.get(&id)
.is_some_and(|v| v.set_shared_frames(enabled))
}

/// Le pendant de `read_frame` pour une vue en textures partagées : la dernière frame posée
/// dans l'anneau, si elle est plus récente que `since_gen`. Sa case reste tenue jusqu'à
/// `release_shared_frame`. `Ok(None)` aussi quand la vue relit en RAM.
#[napi]
pub fn read_shared_frame(id: i32, since_gen: f64) -> Result<Option<SharedFramePacket>> {
let frame = match registry().lock().unwrap().get(&id) {
None => return Ok(None),
Some(v) => {
// Même relais que `read_frame` : sans lui, un thread de rendu mort ne se verrait
// que par un canvas noir.
if let Some(fatal) = v.fatal_error() {
return Err(Error::from_reason(fatal));
}
v.take_shared_frame(since_gen.max(0.0) as u64)
}
};
Ok(frame.map(|f| SharedFramePacket {
gen: f.gen as f64,
slot: f.slot,
handle: f.handle.to_le_bytes().to_vec().into(),
width: f.width,
height: f.height,
footage: footage_corners(f.footage),
footage_projective: f.footage.is_some_and(|q| q.projective),
}))
}

/// Chromium a relâché la frame `gen` de la case `slot` (`allReferencesReleased` côté
/// Electron) : le thread de rendu peut y réécrire. Sans effet sur une vue détruite.
#[napi]
pub fn release_shared_frame(id: i32, slot: u32, gen: f64) {
if let Some(v) = registry().lock().unwrap().get(&id) {
v.release_shared_frame(slot, gen.max(0.0) as u64);
}
}

/// Param live (inspector). Le type de valeur route vers le bon setter :
/// bool = switch (webcamMirror…), number = slider (shadow/roundness/motionBlur/backgroundBlur),
/// string = sélection (backgroundColor "#rrggbb").
Expand Down
6 changes: 6 additions & 0 deletions crates/compositor/src/compositor_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3008,6 +3008,12 @@ impl Compositor {
Ok((rw, rh, out))
}

/// Le RT de la dernière composition, à `render_size()`, pour qui le copie ailleurs sans
/// passer par la RAM : l'anneau de textures partagées de la preview (`shared_frames`).
pub fn render_target(&self) -> &ID3D11Texture2D {
&self.rt
}

/// Comme `rgb_to_nv12`, mais vers `target_w`×`target_h` : si la cible diffère de la taille
/// de rendu, le RT composé est d'abord redimensionné (bilinéaire, `ps_tex`/`sampler` déjà
/// utilisés partout ailleurs dans le fichier, cf. `blit_resized`). Le RT suivant la sortie,
Expand Down
2 changes: 2 additions & 0 deletions crates/compositor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub mod sculpt;
// `segmentation` ses deux entrées échouent proprement, ce qui garde le reste du crate
// indépendant du choix de packaging d'ONNX Runtime.
pub mod segmentation;
// Livraison de la preview par textures partagées (Windows) ; la tenue des cases est portable.
pub mod shared_frames;
pub mod text_anim;
pub mod text_fonts;
pub mod text_plate;
Expand Down
215 changes: 185 additions & 30 deletions crates/compositor/src/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ use crate::regions::{speed_at, ProgrammeClock};
use crate::scene::Scene;
use crate::config::{self, Cfg};
use crate::cursor::CursorTrack;
use crate::d3d::Gpu;
use crate::d3d::{Backend, Gpu};
use crate::frame_geometry::webcam_is_real;
use crate::pipeline::Decoder;
#[cfg(windows)]
use crate::shared_frames::SharedRing;
use crate::shared_frames::{SharedFrame, SlotBook};
use crate::timeline_walk::{frame_step, FrameStep, NextFrameTime};
use anyhow::Result;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
Expand Down Expand Up @@ -800,6 +803,15 @@ struct Shared {
/// vide la ferait repartir à 1 — donc rejouer des générations déjà peintes. Monotone,
/// jamais remise à zéro.
frame_gen: AtomicU64,
/// La vue livre ses frames dans l'anneau de textures partagées (`take_shared_frame`) au
/// lieu de les relire en RAM (`latest_frame_since`). Posé par JS ; le thread de rendu le
/// rabat à `false` si l'anneau ne peut pas servir, et les frames repassent par la RAM.
shared_frames: AtomicBool,
/// Tenue des cases de l'anneau, entre le thread de rendu et le thread Node.
slot_book: Mutex<SlotBook>,
/// Republier la frame courante au prochain tour, même en pause : posé quand le transport
/// change, pour que le consommateur ait une image sans attendre que quelque chose bouge.
republish: AtomicBool,
/// Erreur fatale du thread de rendu (device D3D11 introuvable, décodeur qui refuse
/// le fichier…). Le thread meurt sur la première erreur ; sans ce champ, elle
/// finissait dans un `eprintln!` que personne ne lit et l'utilisateur n'avait
Expand Down Expand Up @@ -853,6 +865,9 @@ impl LiveView {
stop: AtomicBool::new(false),
latest_frame: Mutex::new(None),
frame_gen: AtomicU64::new(0),
shared_frames: AtomicBool::new(false),
slot_book: Mutex::new(SlotBook::default()),
republish: AtomicBool::new(false),
fatal: Mutex::new(None),
});
let sh = shared.clone();
Expand Down Expand Up @@ -929,6 +944,35 @@ impl LiveView {
}
}

/// Livrer les frames par textures partagées plutôt que par readback. Seul le backend
/// matériel de Windows sait le faire : ailleurs la demande est refusée et la vue continue
/// de relire en RAM. Rend l'état obtenu.
pub fn set_shared_frames(&self, enabled: bool) -> bool {
let on = enabled && cfg!(windows) && Gpu::probe() == Some(Backend::Hardware);
if self.shared.shared_frames.swap(on, Ordering::Relaxed) != on {
if !on {
if let Ok(mut book) = self.shared.slot_book.lock() {
book.clear_ready();
}
}
self.shared.republish.store(true, Ordering::Relaxed);
}
on
}

/// La dernière frame posée dans l'anneau, si elle est plus récente que `since_gen`. Sa
/// case reste tenue jusqu'à `release_shared_frame`. `None` aussi quand la vue relit en RAM.
pub fn take_shared_frame(&self, since_gen: u64) -> Option<SharedFrame> {
self.shared.slot_book.lock().ok()?.take(since_gen, Instant::now())
}

/// Chromium a relâché la frame `gen` de la case `slot` : le thread de rendu peut y réécrire.
pub fn release_shared_frame(&self, slot: u32, gen: u64) {
if let Ok(mut book) = self.shared.slot_book.lock() {
book.release(slot, gen);
}
}

/// Switch inspector (booléen).
pub fn set_param_bool(&self, key: &str, value: bool) {
if let Ok(mut p) = self.shared.inspector.lock() {
Expand Down Expand Up @@ -1426,8 +1470,19 @@ unsafe fn render_thread(
// appliquée, on refuse de jouer le layout fixture (POC) : un fallback fixture ne ferait que
// MASQUER un scene-push cassé. On attend la scène avant de produire le 1er frame.
let mut scene_applied = false;
// Textures partagées de la preview, créées au premier besoin (voir `publish_shared`).
let mut ring = Ring::default();
// Une frame composée qui n'a trouvé aucune case libre dans l'anneau : le RT la garde, elle
// repart dès qu'une case se libère. Sans ça, une frame composée en pause (seek, réglage)
// restait invisible jusqu'au changement suivant — en lecture, la suivante la remplace.
let mut publish_pending = false;

while !shared.stop.load(Ordering::SeqCst) {
// Le transport vient de changer : le consommateur doit recevoir la frame courante
// sans attendre qu'une autre soit composée (en pause, il n'y en aurait pas).
if shared.republish.swap(false, Ordering::Relaxed) {
first = true;
}
// params inspector : booléens/taps → cfg ; valeurs continues → live_params
let ip = *shared.inspector.lock().unwrap();
let mut clip_changed = false;
Expand Down Expand Up @@ -1783,39 +1838,52 @@ unsafe fn render_thread(
stepped = true;
}

if stepped || first {
if stepped || first || publish_pending {
if pw > 0 && ph > 0 {
// Step complet : `compose_frame` (déjà appelé par `step`/`present_frame`/
// `recompose`) a rastérisé le RT à la géométrie de sortie ramenée au panneau.
// On lit ce RT DIRECTEMENT à sa résolution de rendu (`readback_direct` : copy
// rt → staging → Map/Unmap), sans le resize `blit_resized` qui, depuis la
// refonte ratio, n'était plus qu'une copie identité + une alloc NV12 inutile.
match comp.readback_direct() {
Ok((rw, rh, rgba)) => {
// Publie dans `latest_frame` : on remplace le buffer précédent
// (le canvas ne montre que la dernière frame, peu importe combien
// le renderer en a raté entre deux lectures napi). On incrémente
// la génération sous le MÊME lock que l'écriture du buffer, pour
// qu'un lecteur ne puisse jamais voir un `gen` neuf appairé à un
// buffer périmé (ou l'inverse). `+ 1` depuis la précédente, `1` au
// premier publish. Les dims publiées sont celles du RENDU (`rw`×`rh`) :
// le canvas JS s'y dimensionne (packet auto-descriptif) puis CSS met à
// l'échelle vers la boîte du panneau — plus de resize GPU intermédiaire.
// La génération vient d'un compteur atomique et non du slot : la
// livraison sans copie VIDE le slot en le lisant, et un
// `unwrap_or(1)` repartirait alors de 1 — le consommateur recevrait
// des générations déjà peintes et boucherait. Séquence identique à
// l'ancienne dérivation tant que le slot n'est pas vidé.
let next_gen = shared.frame_gen.fetch_add(1, Ordering::Relaxed) + 1;
if let Ok(mut slot) = shared.latest_frame.lock() {
*slot = Some((next_gen, rw, rh, rgba, comp.footage_quad()));
}
match publish_shared(&shared, &gpu, &comp, &mut ring) {
SharedPublish::Published => {
first = false;
publish_pending = false;
}
Err(e) => {
eprintln!("[live] readback_direct: {e:#}");
std::thread::sleep(Duration::from_millis(8));
// Chromium tient toutes les cases : la frame attend dans le RT, et la boucle
// ne tourne pas à vide le temps qu'il en relâche une.
SharedPublish::NoFreeSlot => {
publish_pending = true;
std::thread::sleep(Duration::from_millis(2));
}
// Step complet : `compose_frame` (déjà appelé par `step`/`present_frame`/
// `recompose`) a rastérisé le RT à la géométrie de sortie ramenée au panneau.
// On lit ce RT DIRECTEMENT à sa résolution de rendu (`readback_direct` : copy
// rt → staging → Map/Unmap), sans le resize `blit_resized` qui, depuis la
// refonte ratio, n'était plus qu'une copie identité + une alloc NV12 inutile.
SharedPublish::Off => match comp.readback_direct() {
Ok((rw, rh, rgba)) => {
// Publie dans `latest_frame` : on remplace le buffer précédent
// (le canvas ne montre que la dernière frame, peu importe combien
// le renderer en a raté entre deux lectures napi). On incrémente
// la génération sous le MÊME lock que l'écriture du buffer, pour
// qu'un lecteur ne puisse jamais voir un `gen` neuf appairé à un
// buffer périmé (ou l'inverse). `+ 1` depuis la précédente, `1` au
// premier publish. Les dims publiées sont celles du RENDU (`rw`×`rh`) :
// le canvas JS s'y dimensionne (packet auto-descriptif) puis CSS met à
// l'échelle vers la boîte du panneau — plus de resize GPU intermédiaire.
// La génération vient d'un compteur atomique et non du slot : la
// livraison sans copie VIDE le slot en le lisant, et un
// `unwrap_or(1)` repartirait alors de 1 — le consommateur recevrait
// des générations déjà peintes et boucherait. Séquence identique à
// l'ancienne dérivation tant que le slot n'est pas vidé.
let next_gen = shared.frame_gen.fetch_add(1, Ordering::Relaxed) + 1;
if let Ok(mut slot) = shared.latest_frame.lock() {
*slot = Some((next_gen, rw, rh, rgba, comp.footage_quad()));
}
first = false;
publish_pending = false;
}
Err(e) => {
eprintln!("[live] readback_direct: {e:#}");
std::thread::sleep(Duration::from_millis(8));
}
},
}
}
} else {
Expand All @@ -1825,6 +1893,93 @@ unsafe fn render_thread(
Ok(())
}

/// Ce que la publication dans l'anneau partagé a fait de la frame composée.
enum SharedPublish {
/// Posée dans une case de l'anneau.
Published,
/// Chromium tient toutes les cases : la frame attend dans le RT qu'il en relâche une.
NoFreeSlot,
/// Transport partagé coupé ou indisponible : la frame repasse par le readback.
Off,
}

#[cfg(windows)]
type Ring = Option<SharedRing>;
#[cfg(not(windows))]
type Ring = ();

/// Pose la frame composée dans l'anneau de textures partagées, si la vue livre ainsi.
///
/// Toute panne coupe le transport (`shared_frames` à `false`) et rend `Off` : la frame part
/// par le readback, et le service, qui lit les deux, n'a rien à décider.
#[cfg(windows)]
unsafe fn publish_shared(
shared: &Shared,
gpu: &Gpu,
comp: &Compositor,
ring: &mut Ring,
) -> SharedPublish {
if !shared.shared_frames.load(Ordering::Relaxed) {
return SharedPublish::Off;
}
let turn_off = |why: String| {
eprintln!("[live] textures partagées coupées ({why}) — retour au readback");
shared.shared_frames.store(false, Ordering::Relaxed);
SharedPublish::Off
};
// WARP ne partage rien avec le device de Chromium.
if gpu.backend != Backend::Hardware {
return turn_off("backend logiciel".into());
}
if ring.is_none() {
match SharedRing::new(gpu) {
Ok(created) => *ring = Some(created),
Err(e) => return turn_off(format!("{e:#}")),
}
}
let Some(ring) = ring.as_mut() else {
return SharedPublish::Off;
};
let (claimed, lost) = match shared.slot_book.lock() {
Ok(mut book) => (book.claim(crate::shared_frames::RING_SLOTS), book.lost(Instant::now())),
Err(_) => (None, false),
};
let Some(slot) = claimed else {
// Chromium relâche d'ordinaire une case dans la milliseconde. Une case perdue ne
// reviendra plus : la réécrire déchirerait peut-être une image qu'il lit encore, et
// l'attendre figerait la preview.
if lost {
return turn_off("une case jamais relâchée par Chromium".into());
}
return SharedPublish::NoFreeSlot;
};
let (width, height) = comp.render_size();
match ring.write(slot, comp.render_target(), width, height) {
Ok(handle) => {
if let Ok(mut book) = shared.slot_book.lock() {
// Le compteur du readback : une seule suite de générations quel que soit le
// transport, incrémentée sous le lock de la publication comme là-bas.
let gen = shared.frame_gen.fetch_add(1, Ordering::Relaxed) + 1;
book.publish(SharedFrame {
gen,
slot,
handle,
width,
height,
footage: comp.footage_quad(),
});
}
SharedPublish::Published
}
Err(e) => turn_off(format!("{e:#}")),
}
}

#[cfg(not(windows))]
unsafe fn publish_shared(_: &Shared, _: &Gpu, _: &Compositor, _: &mut Ring) -> SharedPublish {
SharedPublish::Off
}

// ---------- harnais standalone (poc-d3d.exe --live) ----------
//
// Le harnais crée une fenêtre Win32 simple qui héberge la preview live ; c'est un
Expand Down
Loading
Loading