@matjash/pixi-native-linux-x64 0.1.2 → 0.2.1

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.
@@ -1,47 +1,48 @@
1
- const Fs = require('fs')
2
- const { getBindingPath } = require('./binding-path.js')
1
+ const Fs = require("fs");
2
+ const { getBindingPath } = require("./binding-path.js");
3
3
 
4
- const bindingPath = getBindingPath()
4
+ const bindingPath = getBindingPath();
5
5
  if (!Fs.existsSync(bindingPath)) {
6
- throw new Error(
7
- `Native WebGPU is not supported by this pixi-native distribution for ${process.platform}-${process.arch}: ` +
8
- 'pixi_native_gpu.node is not included.',
9
- )
6
+ throw new Error(
7
+ `Native WebGPU is not supported by this pixi-native distribution for ${process.platform}-${process.arch}: ` +
8
+ "pixi_native_gpu.node is not included.",
9
+ );
10
10
  }
11
- const binding = require(bindingPath)
11
+ const binding = require(bindingPath);
12
12
 
13
- const contexts = new Set()
13
+ const contexts = new Set();
14
14
 
15
15
  // Pixi probes navigator.gpu by requesting a device before it initializes with
16
16
  // the explicitly supplied adapter/device pair. Return the context-owned device
17
17
  // so capability detection cannot create an unrelated second Dawn device.
18
- const createNavigatorAdapter = (context) => new Proxy(context.adapter, {
19
- get: (target, property) => {
20
- if (property === 'requestDevice') return async () => context.device
21
- const value = Reflect.get(target, property, target)
22
- return typeof value === 'function' ? value.bind(target) : value
23
- },
24
- })
18
+ const createNavigatorAdapter = (context) =>
19
+ new Proxy(context.adapter, {
20
+ get: (target, property) => {
21
+ if (property === "requestDevice") return async () => context.device;
22
+ const value = Reflect.get(target, property, target);
23
+ return typeof value === "function" ? value.bind(target) : value;
24
+ },
25
+ });
25
26
 
26
27
  const createWindowContext = (options) => {
27
- const context = binding.createWindowContext(options)
28
- const navigatorAdapter = createNavigatorAdapter(context)
29
- const gpu = {
30
- requestAdapter: async () => navigatorAdapter,
31
- getPreferredCanvasFormat: () => context.renderer.getPreferredFormat(),
32
- wgslLanguageFeatures: new Set(),
33
- }
34
- const result = { ...context, gpu }
35
- contexts.add(result)
36
- return result
37
- }
28
+ const context = binding.createWindowContext(options);
29
+ const navigatorAdapter = createNavigatorAdapter(context);
30
+ const gpu = {
31
+ requestAdapter: async () => navigatorAdapter,
32
+ getPreferredCanvasFormat: () => context.renderer.getPreferredFormat(),
33
+ wgslLanguageFeatures: new Set(),
34
+ };
35
+ const result = { ...context, gpu };
36
+ contexts.add(result);
37
+ return result;
38
+ };
38
39
 
39
40
  const destroy = (context) => {
40
- contexts.delete(context)
41
- }
41
+ contexts.delete(context);
42
+ };
42
43
 
43
44
  module.exports = {
44
- createWindowContext,
45
- destroy,
46
- ...binding.globals,
47
- }
45
+ createWindowContext,
46
+ destroy,
47
+ ...binding.globals,
48
+ };
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "@pixi-native/native-video",
3
- "version": "0.1.0",
4
- "private": true,
5
- "main": "./src/index.js",
6
- "types": "./src/index.d.ts"
2
+ "name": "@pixi-native/native-video",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "./src/index.js",
6
+ "types": "./src/index.d.ts"
7
7
  }
@@ -2,16 +2,25 @@ const path = require("node:path");
2
2
 
3
3
  const SUPPORTED_TARGETS = new Set(["linux-x64", "win32-x64"]);
4
4
 
5
- function getPlatformDirectory(platform = process.platform, arch = process.arch) {
6
- const target = `${platform}-${arch}`;
7
- if (!SUPPORTED_TARGETS.has(target)) {
8
- throw new Error(`Native video is unsupported on ${target}`);
9
- }
10
- return target;
5
+ function getPlatformDirectory(
6
+ platform = process.platform,
7
+ arch = process.arch,
8
+ ) {
9
+ const target = `${platform}-${arch}`;
10
+ if (!SUPPORTED_TARGETS.has(target)) {
11
+ throw new Error(`Native video is unsupported on ${target}`);
12
+ }
13
+ return target;
11
14
  }
12
15
 
13
16
  function getBindingPath(platform = process.platform, arch = process.arch) {
14
- return path.resolve(__dirname, "..", "dist", getPlatformDirectory(platform, arch), "native_video.node");
17
+ return path.resolve(
18
+ __dirname,
19
+ "..",
20
+ "dist",
21
+ getPlatformDirectory(platform, arch),
22
+ "native_video.node",
23
+ );
15
24
  }
16
25
 
17
26
  module.exports = { getBindingPath, getPlatformDirectory };
@@ -1,36 +1,36 @@
1
1
  export interface DecoderOptions {
2
- width: number;
3
- height: number;
4
- fps?: number;
5
- startTime?: number;
6
- ffmpegPath?: string;
7
- vaapiDevice?: string;
8
- playbackRate?: number;
9
- endTime?: number;
10
- sourcePaced?: boolean;
11
- inputArgs?: string[];
12
- outputArgs?: string[];
2
+ width: number;
3
+ height: number;
4
+ fps?: number;
5
+ startTime?: number;
6
+ ffmpegPath?: string;
7
+ vaapiDevice?: string;
8
+ playbackRate?: number;
9
+ endTime?: number;
10
+ sourcePaced?: boolean;
11
+ inputArgs?: string[];
12
+ outputArgs?: string[];
13
13
  }
14
14
 
15
15
  export interface VideoFrame {
16
- width: number;
17
- height: number;
18
- timestampUs: number;
19
- data: Uint8Array;
16
+ width: number;
17
+ height: number;
18
+ timestampUs: number;
19
+ data: Uint8Array;
20
20
  }
21
21
 
22
22
  export class NativeVideoDecoder {
23
- public constructor(options: DecoderOptions);
24
- public open(source: string): void;
25
- public pollLatest(): VideoFrame | null;
26
- public pollNext(): VideoFrame | null;
27
- public queuedFrames(): number;
28
- public catchUpTo(timestampUs: number): void;
29
- public pollError(): string | null;
30
- public backend(): string;
31
- public decodedFrames(): number;
32
- public droppedFrames(): number;
33
- public skippedFrames(): number;
34
- public isFinished(): boolean;
35
- public close(): void;
23
+ public constructor(options: DecoderOptions);
24
+ public open(source: string): void;
25
+ public pollLatest(): VideoFrame | null;
26
+ public pollNext(): VideoFrame | null;
27
+ public queuedFrames(): number;
28
+ public catchUpTo(timestampUs: number): void;
29
+ public pollError(): string | null;
30
+ public backend(): string;
31
+ public decodedFrames(): number;
32
+ public droppedFrames(): number;
33
+ public skippedFrames(): number;
34
+ public isFinished(): boolean;
35
+ public close(): void;
36
36
  }
@@ -5,57 +5,57 @@ const requireNative = createRequire(__filename);
5
5
  const native = requireNative(getBindingPath());
6
6
 
7
7
  class NativeVideoDecoder {
8
- constructor(options) {
9
- this.decoder = new native.NativeVideoDecoder(options);
10
- }
8
+ constructor(options) {
9
+ this.decoder = new native.NativeVideoDecoder(options);
10
+ }
11
11
 
12
- open(source) {
13
- this.decoder.open(source);
14
- }
12
+ open(source) {
13
+ this.decoder.open(source);
14
+ }
15
15
 
16
- pollLatest() {
17
- return this.decoder.pollLatest();
18
- }
16
+ pollLatest() {
17
+ return this.decoder.pollLatest();
18
+ }
19
19
 
20
- pollNext() {
21
- return this.decoder.pollNext();
22
- }
20
+ pollNext() {
21
+ return this.decoder.pollNext();
22
+ }
23
23
 
24
- queuedFrames() {
25
- return this.decoder.queuedFrames();
26
- }
24
+ queuedFrames() {
25
+ return this.decoder.queuedFrames();
26
+ }
27
27
 
28
- catchUpTo(timestampUs) {
29
- this.decoder.catchUpTo(timestampUs);
30
- }
28
+ catchUpTo(timestampUs) {
29
+ this.decoder.catchUpTo(timestampUs);
30
+ }
31
31
 
32
- pollError() {
33
- return this.decoder.pollError();
34
- }
32
+ pollError() {
33
+ return this.decoder.pollError();
34
+ }
35
35
 
36
- backend() {
37
- return this.decoder.backend();
38
- }
36
+ backend() {
37
+ return this.decoder.backend();
38
+ }
39
39
 
40
- decodedFrames() {
41
- return this.decoder.decodedFrames();
42
- }
40
+ decodedFrames() {
41
+ return this.decoder.decodedFrames();
42
+ }
43
43
 
44
- droppedFrames() {
45
- return this.decoder.droppedFrames();
46
- }
44
+ droppedFrames() {
45
+ return this.decoder.droppedFrames();
46
+ }
47
47
 
48
- skippedFrames() {
49
- return this.decoder.skippedFrames();
50
- }
48
+ skippedFrames() {
49
+ return this.decoder.skippedFrames();
50
+ }
51
51
 
52
- isFinished() {
53
- return this.decoder.isFinished();
54
- }
52
+ isFinished() {
53
+ return this.decoder.isFinished();
54
+ }
55
55
 
56
- close() {
57
- this.decoder.close();
58
- }
56
+ close() {
57
+ this.decoder.close();
58
+ }
59
59
  }
60
60
 
61
61
  module.exports = { NativeVideoDecoder };
@@ -40,7 +40,6 @@ pub struct VideoFrame {
40
40
 
41
41
  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
42
42
  enum DecoderBackend {
43
- #[cfg(target_os = "windows")]
44
43
  D3d11va,
45
44
  Vaapi,
46
45
  Cpu,
@@ -49,7 +48,6 @@ enum DecoderBackend {
49
48
  impl DecoderBackend {
50
49
  fn name(self) -> &'static str {
51
50
  match self {
52
- #[cfg(target_os = "windows")]
53
51
  Self::D3d11va => "D3D11VA",
54
52
  Self::Vaapi => "VA-API",
55
53
  Self::Cpu => "CPU",
@@ -504,7 +502,6 @@ fn ffmpeg_args(request: &FfmpegRequest, backend: DecoderBackend) -> Vec<String>
504
502
  ];
505
503
 
506
504
  match backend {
507
- #[cfg(target_os = "windows")]
508
505
  DecoderBackend::D3d11va => {
509
506
  args.extend(["-hwaccel".to_string(), "d3d11va".to_string()]);
510
507
  }
@@ -538,12 +535,29 @@ fn ffmpeg_args(request: &FfmpegRequest, backend: DecoderBackend) -> Vec<String>
538
535
  "fps={},scale={}:{}:flags=fast_bilinear:in_range=auto:out_range=tv:in_color_matrix=auto:out_color_matrix=bt709,format=nv12",
539
536
  request.fps, request.width, request.height
540
537
  );
541
- let filter = if backend == DecoderBackend::Vaapi {
542
- format!("hwdownload,format=nv12,{scale}")
543
- } else {
544
- scale
538
+ let filter = match backend {
539
+ DecoderBackend::Vaapi => format!("hwdownload,format=nv12,{scale}"),
540
+ DecoderBackend::D3d11va => scale,
541
+ // Keep the CPU fallback independent from the color-negotiation path
542
+ // used after VA-API download. Some FFmpeg builds fail to initialize
543
+ // auto_scale when that path is reused for software yuv420p frames.
544
+ DecoderBackend::Cpu => format!(
545
+ "fps={},scale={}:{}:flags=fast_bilinear,format=nv12",
546
+ request.fps, request.width, request.height
547
+ ),
545
548
  };
546
549
 
550
+ if backend == DecoderBackend::Cpu {
551
+ // The bundled/minimal FFmpeg builds can fail filter negotiation when
552
+ // multiple filter workers initialize the software graph concurrently.
553
+ args.extend([
554
+ "-filter_threads".to_string(),
555
+ "1".to_string(),
556
+ "-filter_complex_threads".to_string(),
557
+ "1".to_string(),
558
+ ]);
559
+ }
560
+
547
561
  args.extend([
548
562
  "-vf".to_string(),
549
563
  filter,
@@ -820,6 +834,19 @@ mod tests {
820
834
  assert!(threads_index > input_index && threads_index < pipe_index);
821
835
  }
822
836
 
837
+ #[test]
838
+ fn builds_serial_software_fallback_filter() {
839
+ let args = ffmpeg_args(&request(30.0, 0.0), DecoderBackend::Cpu);
840
+ assert!(args.windows(2).any(|pair| pair == ["-filter_threads", "1"]));
841
+ assert!(args
842
+ .windows(2)
843
+ .any(|pair| pair == ["-filter_complex_threads", "1"]));
844
+ assert!(args
845
+ .iter()
846
+ .any(|arg| arg == "fps=30,scale=1280:720:flags=fast_bilinear,format=nv12"));
847
+ assert!(!args.iter().any(|arg| arg.contains("in_color_matrix=auto")));
848
+ }
849
+
823
850
  #[test]
824
851
  fn redacts_url_credentials_from_ffmpeg_errors() {
825
852
  assert_eq!(
@@ -1,5 +1,11 @@
1
- const Path = require('path')
1
+ const Path = require("path");
2
2
 
3
- const getBindingPath = () => Path.join(__dirname, 'dist', `${process.platform}-${process.arch}`, 'native_window.node')
3
+ const getBindingPath = () =>
4
+ Path.join(
5
+ __dirname,
6
+ "dist",
7
+ `${process.platform}-${process.arch}`,
8
+ "native_window.node",
9
+ );
4
10
 
5
- module.exports = { getBindingPath }
11
+ module.exports = { getBindingPath };
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "@pixi-native/native-window",
3
- "version": "0.1.0",
4
- "private": true,
5
- "main": "./src/index.js",
6
- "types": "./src/index.d.ts"
2
+ "name": "@pixi-native/native-window",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "./src/index.js",
6
+ "types": "./src/index.d.ts"
7
7
  }
@@ -1,11 +1,46 @@
1
- export interface NativeWindowApi {
2
- setTransparent(nativeData: Uint8Array, transparent: boolean): void;
3
- waitForCompositorFrame(timeoutMs?: number): Promise<boolean>;
4
- create(
5
- nativeData: Uint8Array,
6
- onFrame: () => void,
7
- onState: (active: boolean) => void,
8
- ): ModalFrameController;
1
+ export interface NativeSurfaceDescriptor {
2
+ readonly version: 1;
3
+ readonly api: "win32" | "x11" | "wayland";
4
+ readonly window: Buffer;
5
+ readonly display?: Buffer;
6
+ readonly instance?: Buffer;
7
+ readonly eglWindow?: Buffer;
8
+ }
9
+
10
+ export interface NativeWindowOptions {
11
+ readonly title: string;
12
+ readonly width: number;
13
+ readonly height: number;
14
+ readonly resizable: boolean;
15
+ readonly borderless: boolean;
16
+ readonly transparent: boolean;
17
+ readonly x?: number;
18
+ readonly y?: number;
19
+ readonly graphics: "webgpu" | "webgl";
20
+ }
21
+
22
+ export interface NativeWindow {
23
+ readonly id: number;
24
+ readonly x: number;
25
+ readonly y: number;
26
+ readonly pixelWidth: number;
27
+ readonly pixelHeight: number;
28
+ readonly display: { readonly frequency: number };
29
+ readonly destroyed: boolean;
30
+ readonly surface: NativeSurfaceDescriptor;
31
+ readonly videoDriver: string;
32
+ readonly transparent: boolean;
33
+ pollEvents(): void;
34
+ setPosition(x: number, y: number): void;
35
+ minimize(): void;
36
+ maximize(): void;
37
+ restore(): void;
38
+ makeGlCurrent(): boolean;
39
+ setGlSwapInterval(interval: number): boolean;
40
+ swapGl(): boolean;
41
+ glVersion(): string;
42
+ on(event: string, listener: (event: Record<string, unknown>) => void): this;
43
+ destroy(): void;
9
44
  }
10
45
 
11
46
  export interface ModalFrameController {
@@ -13,5 +48,16 @@ export interface ModalFrameController {
13
48
  detach(): void;
14
49
  }
15
50
 
51
+ export interface NativeWindowApi {
52
+ createWindow(options: NativeWindowOptions): NativeWindow;
53
+ pollEvents(): void;
54
+ waitForCompositorFrame(timeoutMs?: number): Promise<boolean>;
55
+ createModalFrameController(
56
+ nativeData: Buffer,
57
+ onFrame: () => void,
58
+ onState: (active: boolean) => void,
59
+ ): ModalFrameController;
60
+ }
61
+
16
62
  declare const nativeWindow: NativeWindowApi;
17
63
  export = nativeWindow;
@@ -1,6 +1,8 @@
1
- const Fs = require("fs");
1
+ const { EventEmitter } = require("node:events");
2
+ const Fs = require("node:fs");
2
3
  const { createRequire } = require("node:module");
3
4
  const { getBindingPath } = require("../binding-path.js");
5
+ const { normalizeSdlKeyName } = require("./key-mapping.js");
4
6
 
5
7
  const requireNative = createRequire(__filename);
6
8
  const bindingPath = getBindingPath();
@@ -11,22 +13,89 @@ if (!Fs.existsSync(bindingPath)) {
11
13
  }
12
14
 
13
15
  const binding = requireNative(bindingPath);
16
+ const windows = new Map();
17
+ let waylandPositionWarned = false;
14
18
 
15
- const setTransparent = (nativeData, transparent) => {
16
- binding.setTransparent(nativeData, transparent);
19
+ class NativeWindow extends EventEmitter {
20
+ constructor(options) {
21
+ super();
22
+ this._nativeWindow = new binding.NativeSdlWindow(options);
23
+ windows.set(this._nativeWindow.id, this);
24
+ }
25
+
26
+ get id() { return this._nativeWindow.id; }
27
+ get x() { return this._nativeWindow.x; }
28
+ get y() { return this._nativeWindow.y; }
29
+ get pixelWidth() { return this._nativeWindow.pixelWidth; }
30
+ get pixelHeight() { return this._nativeWindow.pixelHeight; }
31
+ get display() { return { frequency: this._nativeWindow.refreshRate }; }
32
+ get destroyed() { return this._nativeWindow.destroyed; }
33
+ get surface() { return this._nativeWindow.surface; }
34
+ get videoDriver() { return this._nativeWindow.videoDriver; }
35
+ get transparent() { return this._nativeWindow.transparent; }
36
+
37
+ pollEvents() { pollEvents(); }
38
+
39
+ setPosition(x, y) {
40
+ if (this._nativeWindow.setPosition(x, y)) return;
41
+ if (!waylandPositionWarned) {
42
+ waylandPositionWarned = true;
43
+ console.warn(
44
+ "[pixi-native] SDL3 Wayland ignores absolute toplevel window positioning.",
45
+ );
46
+ }
47
+ }
48
+
49
+ minimize() { this._nativeWindow.minimize(); }
50
+ maximize() { this._nativeWindow.maximize(); }
51
+ restore() { this._nativeWindow.restore(); }
52
+ makeGlCurrent() { return this._nativeWindow.makeGlCurrent(); }
53
+ setGlSwapInterval(interval) { return this._nativeWindow.setGlSwapInterval(interval); }
54
+ swapGl() { return this._nativeWindow.swapGl(); }
55
+ glVersion() { return this._nativeWindow.glVersion(); }
56
+
57
+ destroy() {
58
+ if (this.destroyed) return;
59
+ windows.delete(this.id);
60
+ this._nativeWindow.destroy();
61
+ this.removeAllListeners();
62
+ }
63
+ }
64
+
65
+ const emitNativeEvent = (window, event) => {
66
+ const { kind, windowId, superKey, ...payload } = event;
67
+ if (kind === "keyDown" || kind === "keyUp") {
68
+ payload.key = normalizeSdlKeyName(payload.key);
69
+ }
70
+ window.emit(kind, { ...payload, type: kind, super: superKey });
71
+ };
72
+
73
+ const pollEvents = () => {
74
+ for (const event of binding.pollEvents()) {
75
+ if (event.kind === "quit") {
76
+ for (const window of [...windows.values()]) {
77
+ emitNativeEvent(window, { ...event, kind: "close" });
78
+ }
79
+ continue;
80
+ }
81
+ const window = windows.get(event.windowId);
82
+ if (window) emitNativeEvent(window, event);
83
+ }
17
84
  };
18
85
 
86
+ const createWindow = (options) => new NativeWindow(options);
19
87
  const waitForCompositorFrame = (timeoutMs) =>
20
88
  binding.waitForCompositorFrame(timeoutMs);
21
-
22
- const create = (nativeData, onFrame, onState) => {
23
- const controller = new binding.ModalFrameController(
24
- nativeData,
25
- onFrame,
26
- onState,
27
- );
89
+ const createModalFrameController = (nativeData, onFrame, onState) => {
90
+ const controller = new binding.ModalFrameController(nativeData, onFrame, onState);
28
91
  controller.attach();
29
92
  return controller;
30
93
  };
31
94
 
32
- module.exports = { create, setTransparent, waitForCompositorFrame };
95
+ module.exports = {
96
+ NativeWindow,
97
+ createWindow,
98
+ createModalFrameController,
99
+ pollEvents,
100
+ waitForCompositorFrame,
101
+ };
@@ -0,0 +1,32 @@
1
+ const SDL_KEY_NAMES = Object.freeze({
2
+ Up: "up",
3
+ Down: "down",
4
+ Left: "left",
5
+ Right: "right",
6
+ Space: "space",
7
+ Return: "return",
8
+ Escape: "escape",
9
+ Backspace: "backspace",
10
+ Tab: "tab",
11
+ "Left Ctrl": "ctrl",
12
+ "Right Ctrl": "ctrl",
13
+ "Left Shift": "shift",
14
+ "Right Shift": "shift",
15
+ "Left Alt": "alt",
16
+ "Right Alt": "alt",
17
+ "Left GUI": "gui",
18
+ "Right GUI": "gui",
19
+ });
20
+
21
+ /** Preserves the key names exposed by the previous @kmamal/sdl adapter. */
22
+ const normalizeSdlKeyName = (name) => {
23
+ if (typeof name !== "string") return name;
24
+ const mapped = SDL_KEY_NAMES[name];
25
+ if (mapped) return mapped;
26
+ if (/^[A-Z]$/.test(name) || /^F\d{1,2}$/.test(name)) {
27
+ return name.toLowerCase();
28
+ }
29
+ return name;
30
+ };
31
+
32
+ module.exports = { normalizeSdlKeyName };