2121
2222use std:: collections:: HashMap ;
2323use std:: path:: { Path , PathBuf } ;
24- use std:: sync:: atomic:: { AtomicBool , AtomicU64 , Ordering } ;
24+ use std:: sync:: atomic:: { AtomicBool , AtomicU64 , AtomicUsize , Ordering } ;
2525use std:: sync:: { Mutex , OnceLock } ;
2626use std:: time:: Instant ;
2727
@@ -193,6 +193,12 @@ impl Default for OverlayState {
193193
194194struct 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+
216260fn 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).
10051049pub 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 ( ) ;
0 commit comments