Skip to content

Commit 332440a

Browse files
authored
Merge pull request #22 from mgth/fix/mpv-overlay-requires-orender-session
fix(overlay): only draw the spatial overlay while an orender session is live
2 parents 5cc5656 + 4f338c0 commit 332440a

2 files changed

Lines changed: 100 additions & 4 deletions

File tree

omniphony-renderer/orender_engine/src/overlay.rs

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
2222
use std::collections::HashMap;
2323
use std::path::{Path, PathBuf};
24-
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
24+
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
2525
use std::sync::{Mutex, OnceLock};
2626
use std::time::Instant;
2727

@@ -193,6 +193,12 @@ impl Default for OverlayState {
193193

194194
struct Overlay {
195195
enabled: AtomicBool,
196+
/// Number of live orender render sessions (one per `orender_create` that has
197+
/// not yet been destroyed). The overlay only draws while at least one session
198+
/// is decoding: without `--ad=orender` no session is ever created, so the Lua
199+
/// shim — which loads unconditionally — pulls an empty payload and draws
200+
/// nothing instead of a permanent, scene-less wireframe box.
201+
sessions: AtomicUsize,
196202
/// `start.elapsed()` ms at the last FFI pull; self-gates per-frame work.
197203
last_pull_ms: AtomicU64,
198204
start: Instant,
@@ -206,13 +212,51 @@ fn overlay() -> &'static Overlay {
206212
static OVERLAY: OnceLock<Overlay> = OnceLock::new();
207213
OVERLAY.get_or_init(|| Overlay {
208214
enabled: AtomicBool::new(true),
215+
sessions: AtomicUsize::new(0),
209216
last_pull_ms: AtomicU64::new(0),
210217
start: Instant::now(),
211218
state: Mutex::new(OverlayState::default()),
212219
prefs_path: Mutex::new(None),
213220
})
214221
}
215222

223+
/// True while at least one orender render session is live (see [`Overlay::sessions`]).
224+
fn session_active() -> bool {
225+
overlay().sessions.load(Ordering::Relaxed) > 0
226+
}
227+
228+
/// Register the start of an orender render session. Called by the host when a
229+
/// renderer is created (`orender_create`); the overlay stays blank until the
230+
/// first session arms it.
231+
pub fn session_started() {
232+
overlay().sessions.fetch_add(1, Ordering::Relaxed);
233+
}
234+
235+
/// Register the end of an orender render session (`orender_destroy`). Saturates
236+
/// at zero, and clears the scene + trails when the last session goes away so the
237+
/// overlay can't linger past the stream it belonged to.
238+
pub fn session_ended() {
239+
let o = overlay();
240+
let mut cur = o.sessions.load(Ordering::Relaxed);
241+
loop {
242+
if cur == 0 {
243+
return; // unbalanced end; nothing to do
244+
}
245+
match o
246+
.sessions
247+
.compare_exchange_weak(cur, cur - 1, Ordering::Relaxed, Ordering::Relaxed)
248+
{
249+
Ok(_) => {
250+
if cur == 1 {
251+
clear();
252+
}
253+
return;
254+
}
255+
Err(observed) => cur = observed,
256+
}
257+
}
258+
}
259+
216260
fn now_secs() -> f64 {
217261
overlay().start.elapsed().as_secs_f64()
218262
}
@@ -529,7 +573,7 @@ pub fn build_ass(res_x: u32, res_y: u32) -> String {
529573
let o = overlay();
530574
o.last_pull_ms
531575
.store(o.start.elapsed().as_millis() as u64, Ordering::Relaxed);
532-
if !o.enabled.load(Ordering::Relaxed) || res_x == 0 || res_y == 0 {
576+
if !o.enabled.load(Ordering::Relaxed) || !session_active() || res_x == 0 || res_y == 0 {
533577
return String::new();
534578
}
535579
let now = now_secs();
@@ -1004,7 +1048,7 @@ fn build_heatmap_bitmap(s: &OverlayState, res_x: f64, res_y: f64) -> Option<Heat
10041048
/// not touch the trails or the pull clock (the ASS pull already advances those).
10051049
pub fn build_heatmap(res_x: u32, res_y: u32) -> Option<HeatmapBitmap> {
10061050
let o = overlay();
1007-
if !o.enabled.load(Ordering::Relaxed) || res_x == 0 || res_y == 0 {
1051+
if !o.enabled.load(Ordering::Relaxed) || !session_active() || res_x == 0 || res_y == 0 {
10081052
return None;
10091053
}
10101054
let s = o.state.lock().ok()?;
@@ -1323,6 +1367,9 @@ mod tests {
13231367
set_labels_enabled(true);
13241368
set_objects_visible(true);
13251369
set_heatmap_enabled(true);
1370+
// Arm exactly one render session so the draw paths are reachable; the
1371+
// no-session gate is exercised on its own in `no_session_returns_empty`.
1372+
overlay().sessions.store(1, Ordering::Relaxed);
13261373
g
13271374
}
13281375

@@ -1349,6 +1396,46 @@ mod tests {
13491396
assert!(build_ass(0, 0).is_empty());
13501397
}
13511398

1399+
// Without an orender session (e.g. mpv started without `--ad=orender`) the
1400+
// overlay must draw nothing — not a permanent, scene-less wireframe box —
1401+
// even when enabled and fed positions. `session_ended` clears the scene as
1402+
// the last session goes away.
1403+
#[test]
1404+
fn no_session_returns_empty() {
1405+
let _g = guard();
1406+
update_positions(vec![(0, 0.0, 0.0, 0.5, String::new())]);
1407+
update_levels(&[(0, -6.0)]);
1408+
// guard() armed one session; end it → back to the no-session state.
1409+
session_ended();
1410+
assert!(!session_active(), "session counter should be back to zero");
1411+
assert!(
1412+
build_ass(1920, 1080).is_empty(),
1413+
"ASS must be empty with no session"
1414+
);
1415+
assert!(
1416+
build_heatmap(1920, 1080).is_none(),
1417+
"heatmap must be absent with no session"
1418+
);
1419+
// A fresh session re-arms the overlay (positions were cleared on the last
1420+
// session end, so feed them again to get a non-empty draw).
1421+
session_started();
1422+
update_positions(vec![(0, 0.0, 0.0, 0.5, String::new())]);
1423+
assert!(
1424+
build_ass(1920, 1080).contains("\\p1"),
1425+
"ASS draws once a session is live"
1426+
);
1427+
}
1428+
1429+
// `session_ended` without a matching `session_started` must not underflow.
1430+
#[test]
1431+
fn session_counter_saturates_at_zero() {
1432+
let _g = guard();
1433+
overlay().sessions.store(0, Ordering::Relaxed);
1434+
session_ended();
1435+
session_ended();
1436+
assert_eq!(overlay().sessions.load(Ordering::Relaxed), 0);
1437+
}
1438+
13521439
#[test]
13531440
fn toggles_flip_and_return_new_state() {
13541441
let _g = guard();

omniphony-renderer/orender_ffi/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,13 @@ pub unsafe extern "C" fn orender_create(cfg: *const OrenderConfig) -> *mut Orend
271271
return ptr::null_mut();
272272
}
273273
match build_engine(&*cfg) {
274-
Ok(engine) => Box::into_raw(Box::new(engine)) as *mut OrenderRenderer,
274+
Ok(engine) => {
275+
// Arm the spatial overlay: it stays blank until a session exists,
276+
// so a host that loads the overlay shim without selecting
277+
// `--ad=orender` (no session created) draws nothing.
278+
orender_engine::overlay::session_started();
279+
Box::into_raw(Box::new(engine)) as *mut OrenderRenderer
280+
}
275281
Err(e) => {
276282
eprintln!("orender_create failed: {e:#}");
277283
ptr::null_mut()
@@ -289,6 +295,9 @@ pub unsafe extern "C" fn orender_destroy(r: *mut OrenderRenderer) {
289295
}
290296
let _ = catch_unwind(AssertUnwindSafe(|| {
291297
drop(Box::from_raw(r as *mut Engine));
298+
// Disarm the overlay as this session goes away (clears the scene when it
299+
// was the last one), so the box can't linger past the stream.
300+
orender_engine::overlay::session_ended();
292301
}));
293302
}
294303

0 commit comments

Comments
 (0)