@matjash/pixi-native-linux-x64 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,14 @@ Pixi Native uses these projects through direct packages or native integration:
22
22
  The installed packages and their upstream repositories contain their copyright
23
23
  and license terms. Those terms continue to apply to each dependency.
24
24
 
25
+ ## Source Sans 3 demo font
26
+
27
+ The PixiJS 7 WebGL text demo includes Source Sans 3 Regular from
28
+ [Adobe Fonts](https://github.com/adobe-fonts/source-sans). Copyright 2010-2024
29
+ Adobe, with Reserved Font Name "Source". The font is distributed under the SIL
30
+ Open Font License 1.1; the complete license is stored beside the font at
31
+ `src/demo/assets/fonts/SourceSans3-LICENSE.md`.
32
+
25
33
  ## Dawn and Tint
26
34
 
27
35
  The packaged `pixi_native_gpu.node` binaries contain Dawn and Tint code.
@@ -25,6 +25,13 @@ export interface VideoFrameInfo {
25
25
  timestampUs: number;
26
26
  }
27
27
 
28
+ export interface VideoShutdownDiagnostics {
29
+ activeDecoderWorkers: number;
30
+ pendingDecoderShutdowns: number;
31
+ completedDecoderShutdowns: number;
32
+ maxDecoderShutdownMs: number;
33
+ }
34
+
28
35
  export class NativeVideoDecoder {
29
36
  public constructor(options: DecoderOptions);
30
37
  public open(source: string): void;
@@ -46,3 +53,5 @@ export class NativeVideoDecoder {
46
53
  public isFinished(): boolean;
47
54
  public close(): void;
48
55
  }
56
+
57
+ export function videoShutdownDiagnostics(): VideoShutdownDiagnostics;
@@ -101,4 +101,13 @@ class NativeVideoDecoder {
101
101
  }
102
102
  }
103
103
 
104
- module.exports = { NativeVideoDecoder };
104
+ function videoShutdownDiagnostics() {
105
+ return native.videoShutdownDiagnostics?.() ?? {
106
+ activeDecoderWorkers: 0,
107
+ pendingDecoderShutdowns: 0,
108
+ completedDecoderShutdowns: 0,
109
+ maxDecoderShutdownMs: 0,
110
+ };
111
+ }
112
+
113
+ module.exports = { NativeVideoDecoder, videoShutdownDiagnostics };
@@ -6,7 +6,7 @@ use std::process::{Child, ChildStdout, Command, Stdio};
6
6
  use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
7
7
  use std::sync::{Arc, Condvar, Mutex};
8
8
  use std::thread;
9
- use std::time::Duration;
9
+ use std::time::{Duration, Instant};
10
10
 
11
11
  use napi::bindgen_prelude::*;
12
12
  use napi_derive::napi;
@@ -15,6 +15,11 @@ const FRAME_QUEUE_CAPACITY: usize = 4;
15
15
  const HARDWARE_DECODE_ATTEMPTS: usize = 5;
16
16
  const HARDWARE_RETRY_DELAY: Duration = Duration::from_millis(500);
17
17
 
18
+ static ACTIVE_DECODER_WORKERS: AtomicU64 = AtomicU64::new(0);
19
+ static PENDING_DECODER_SHUTDOWNS: AtomicU64 = AtomicU64::new(0);
20
+ static COMPLETED_DECODER_SHUTDOWNS: AtomicU64 = AtomicU64::new(0);
21
+ static MAX_DECODER_SHUTDOWN_MS: AtomicU64 = AtomicU64::new(0);
22
+
18
23
  #[napi(object)]
19
24
  pub struct DecoderOptions {
20
25
  pub width: i64,
@@ -45,6 +50,24 @@ pub struct VideoFrameInfo {
45
50
  pub timestamp_us: i64,
46
51
  }
47
52
 
53
+ #[napi(object)]
54
+ pub struct VideoShutdownDiagnostics {
55
+ pub active_decoder_workers: i64,
56
+ pub pending_decoder_shutdowns: i64,
57
+ pub completed_decoder_shutdowns: i64,
58
+ pub max_decoder_shutdown_ms: i64,
59
+ }
60
+
61
+ #[napi]
62
+ pub fn video_shutdown_diagnostics() -> VideoShutdownDiagnostics {
63
+ VideoShutdownDiagnostics {
64
+ active_decoder_workers: atomic_i64(&ACTIVE_DECODER_WORKERS),
65
+ pending_decoder_shutdowns: atomic_i64(&PENDING_DECODER_SHUTDOWNS),
66
+ completed_decoder_shutdowns: atomic_i64(&COMPLETED_DECODER_SHUTDOWNS),
67
+ max_decoder_shutdown_ms: atomic_i64(&MAX_DECODER_SHUTDOWN_MS),
68
+ }
69
+ }
70
+
48
71
  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
49
72
  enum DecoderBackend {
50
73
  D3d11va,
@@ -190,6 +213,7 @@ pub struct NativeVideoDecoder {
190
213
  options: DecoderOptions,
191
214
  state: DecoderState,
192
215
  worker: Option<thread::JoinHandle<()>>,
216
+ retired: bool,
193
217
  }
194
218
 
195
219
  #[napi]
@@ -207,6 +231,7 @@ impl NativeVideoDecoder {
207
231
  Ok(Self {
208
232
  options,
209
233
  worker: None,
234
+ retired: false,
210
235
  state: DecoderState {
211
236
  closed: Arc::new(AtomicBool::new(true)),
212
237
  finished: Arc::new(AtomicBool::new(false)),
@@ -228,6 +253,11 @@ impl NativeVideoDecoder {
228
253
 
229
254
  #[napi]
230
255
  pub fn open(&mut self, source: String) -> Result<()> {
256
+ if self.retired {
257
+ return Err(Error::from_reason(
258
+ "Video decoder cannot reopen after shutdown",
259
+ ));
260
+ }
231
261
  if self.state.closed.load(Ordering::SeqCst) {
232
262
  self.join_worker();
233
263
  }
@@ -314,7 +344,9 @@ impl NativeVideoDecoder {
314
344
  let initial_stdout = install_child(&state, spawned)
315
345
  .map_err(|error| Error::from_reason(error.to_string()))?;
316
346
 
347
+ ACTIVE_DECODER_WORKERS.fetch_add(1, Ordering::SeqCst);
317
348
  self.worker = Some(thread::spawn(move || {
349
+ let _active_worker = ActiveDecoderWorker;
318
350
  let mut result =
319
351
  consume_decoder_attempt(initial_stdout, width, height, fps, start_time, &state);
320
352
 
@@ -538,8 +570,16 @@ impl NativeVideoDecoder {
538
570
 
539
571
  #[napi]
540
572
  pub fn close(&mut self) {
541
- close_state(&self.state);
542
- self.join_worker();
573
+ self.begin_shutdown();
574
+ }
575
+
576
+ fn begin_shutdown(&mut self) {
577
+ if self.retired {
578
+ return;
579
+ }
580
+ self.retired = true;
581
+ let child = request_close_state(&self.state);
582
+ retire_decoder_cleanup(self.worker.take(), child);
543
583
  }
544
584
 
545
585
  fn join_worker(&mut self) {
@@ -577,8 +617,7 @@ impl NativeVideoDecoder {
577
617
 
578
618
  impl Drop for NativeVideoDecoder {
579
619
  fn drop(&mut self) {
580
- close_state(&self.state);
581
- self.join_worker();
620
+ self.begin_shutdown();
582
621
  }
583
622
  }
584
623
 
@@ -751,7 +790,25 @@ fn redact_url_credentials(value: &str) -> String {
751
790
  result
752
791
  }
753
792
 
754
- fn install_child(state: &DecoderState, spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
793
+ fn install_child(state: &DecoderState, mut spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
794
+ let mut child = state
795
+ .child
796
+ .lock()
797
+ .map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?;
798
+ if state.closed.load(Ordering::SeqCst) {
799
+ drop(child);
800
+ let _ = spawned.child.kill();
801
+ let _ = spawned.child.wait();
802
+ if let Some(worker) = spawned.stderr_worker {
803
+ let _ = worker.join();
804
+ }
805
+ return Err(io::Error::new(
806
+ io::ErrorKind::Interrupted,
807
+ "Video decoder closed before FFmpeg startup completed",
808
+ ));
809
+ }
810
+ *child = Some(spawned.child);
811
+ drop(child);
755
812
  if let Some(worker) = spawned.stderr_worker {
756
813
  state
757
814
  .stderr_workers
@@ -759,10 +816,6 @@ fn install_child(state: &DecoderState, spawned: SpawnedFfmpeg) -> io::Result<Chi
759
816
  .map_err(|_| io::Error::other("FFmpeg stderr worker lock poisoned"))?
760
817
  .push(worker);
761
818
  }
762
- *state
763
- .child
764
- .lock()
765
- .map_err(|_| io::Error::other("FFmpeg process lock poisoned"))? = Some(spawned.child);
766
819
  Ok(spawned.stdout)
767
820
  }
768
821
 
@@ -798,13 +851,7 @@ fn consume_ffmpeg_output(
798
851
  }
799
852
  };
800
853
 
801
- let status = state
802
- .child
803
- .lock()
804
- .map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?
805
- .take()
806
- .map(|mut process| process.wait())
807
- .transpose()?;
854
+ let status = wait_for_child_exit(state)?;
808
855
  join_stderr_workers(state);
809
856
 
810
857
  read_result?;
@@ -950,20 +997,78 @@ fn store_error(state: &DecoderState, message: String) {
950
997
  }
951
998
  }
952
999
 
953
- fn close_state(state: &DecoderState) {
1000
+ /** Polls without holding the child mutex while the FFmpeg process exits. */
1001
+ fn wait_for_child_exit(state: &DecoderState) -> io::Result<Option<std::process::ExitStatus>> {
1002
+ loop {
1003
+ let status = {
1004
+ let mut child = state
1005
+ .child
1006
+ .lock()
1007
+ .map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?;
1008
+ match child.as_mut() {
1009
+ Some(process) => process.try_wait()?,
1010
+ None => return Ok(None),
1011
+ }
1012
+ };
1013
+ if let Some(status) = status {
1014
+ if let Ok(mut child) = state.child.lock() {
1015
+ child.take();
1016
+ }
1017
+ return Ok(Some(status));
1018
+ }
1019
+ if state.closed.load(Ordering::SeqCst) {
1020
+ return Ok(None);
1021
+ }
1022
+ thread::sleep(Duration::from_millis(10));
1023
+ }
1024
+ }
1025
+
1026
+ /** Signals decoder shutdown without waiting on FFmpeg or worker threads. */
1027
+ fn request_close_state(state: &DecoderState) -> Option<Child> {
954
1028
  state.closed.store(true, Ordering::SeqCst);
955
1029
  let (frames, available) = &*state.frames;
956
1030
  available.notify_all();
957
- if let Ok(mut child) = state.child.lock() {
958
- if let Some(mut process) = child.take() {
959
- let _ = process.kill();
960
- let _ = process.wait();
961
- }
1031
+ let mut child = state.child.lock().ok().and_then(|mut child| child.take());
1032
+ if let Some(process) = child.as_mut() {
1033
+ let _ = process.kill();
962
1034
  }
963
1035
  if let Ok(mut frames) = frames.lock() {
964
1036
  frames.clear();
965
1037
  }
966
- join_stderr_workers(state);
1038
+ child
1039
+ }
1040
+
1041
+ /** Reaps the killed process and joins workers away from the N-API thread. */
1042
+ fn retire_decoder_cleanup(worker: Option<thread::JoinHandle<()>>, child: Option<Child>) {
1043
+ if worker.is_none() && child.is_none() {
1044
+ return;
1045
+ }
1046
+ PENDING_DECODER_SHUTDOWNS.fetch_add(1, Ordering::SeqCst);
1047
+ thread::spawn(move || {
1048
+ let started = Instant::now();
1049
+ if let Some(mut process) = child {
1050
+ let _ = process.wait();
1051
+ }
1052
+ if let Some(worker) = worker {
1053
+ let _ = worker.join();
1054
+ }
1055
+ let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
1056
+ MAX_DECODER_SHUTDOWN_MS.fetch_max(elapsed_ms, Ordering::SeqCst);
1057
+ COMPLETED_DECODER_SHUTDOWNS.fetch_add(1, Ordering::SeqCst);
1058
+ PENDING_DECODER_SHUTDOWNS.fetch_sub(1, Ordering::SeqCst);
1059
+ });
1060
+ }
1061
+
1062
+ fn atomic_i64(value: &AtomicU64) -> i64 {
1063
+ i64::try_from(value.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
1064
+ }
1065
+
1066
+ struct ActiveDecoderWorker;
1067
+
1068
+ impl Drop for ActiveDecoderWorker {
1069
+ fn drop(&mut self) {
1070
+ ACTIVE_DECODER_WORKERS.fetch_sub(1, Ordering::SeqCst);
1071
+ }
967
1072
  }
968
1073
 
969
1074
  #[cfg(test)]
@@ -1180,6 +1285,40 @@ mod tests {
1180
1285
  assert_eq!(decoder.state.frames.0.lock().unwrap().recycled.len(), 1);
1181
1286
  }
1182
1287
 
1288
+ #[test]
1289
+ fn close_retires_a_blocked_worker_without_waiting_for_it() {
1290
+ let mut decoder = test_decoder();
1291
+ let (started_tx, started_rx) = mpsc::channel();
1292
+ let (release_tx, release_rx) = mpsc::channel();
1293
+ decoder.worker = Some(thread::spawn(move || {
1294
+ started_tx.send(()).unwrap();
1295
+ release_rx.recv().unwrap();
1296
+ }));
1297
+ started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
1298
+ let completed_before = COMPLETED_DECODER_SHUTDOWNS.load(Ordering::SeqCst);
1299
+
1300
+ let started = Instant::now();
1301
+ decoder.close();
1302
+ assert!(started.elapsed() < Duration::from_millis(100));
1303
+ assert!(PENDING_DECODER_SHUTDOWNS.load(Ordering::SeqCst) >= 1);
1304
+
1305
+ release_tx.send(()).unwrap();
1306
+ let deadline = Instant::now() + Duration::from_secs(1);
1307
+ while COMPLETED_DECODER_SHUTDOWNS.load(Ordering::SeqCst) == completed_before {
1308
+ assert!(Instant::now() < deadline);
1309
+ thread::sleep(Duration::from_millis(5));
1310
+ }
1311
+ }
1312
+
1313
+ #[test]
1314
+ fn explicit_close_makes_the_decoder_terminal_without_blocking_reopen() {
1315
+ let mut decoder = test_decoder();
1316
+ decoder.close();
1317
+
1318
+ let error = decoder.open("unused.mp4".to_string()).unwrap_err();
1319
+ assert!(error.to_string().contains("cannot reopen after shutdown"));
1320
+ }
1321
+
1183
1322
  #[test]
1184
1323
  fn hardware_retry_stops_after_five_attempts_or_first_frame() {
1185
1324
  let state = decoder_state(false);
@@ -1203,6 +1342,23 @@ mod tests {
1203
1342
  }
1204
1343
  }
1205
1344
 
1345
+ fn test_decoder() -> NativeVideoDecoder {
1346
+ NativeVideoDecoder::new(DecoderOptions {
1347
+ width: 2,
1348
+ height: 2,
1349
+ fps: Some(30.0),
1350
+ start_time: None,
1351
+ ffmpeg_path: None,
1352
+ vaapi_device: None,
1353
+ playback_rate: None,
1354
+ end_time: None,
1355
+ source_paced: None,
1356
+ input_args: None,
1357
+ output_args: None,
1358
+ })
1359
+ .unwrap()
1360
+ }
1361
+
1206
1362
  fn decoder_state(source_paced: bool) -> DecoderState {
1207
1363
  DecoderState {
1208
1364
  closed: Arc::new(AtomicBool::new(false)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matjash/pixi-native-linux-x64",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "commonjs",
5
5
  "description": "Linux x64 native binaries for @matjash/pixi-native",
6
6
  "license": "MIT",