@driftengine/native-host 4.3.0 → 4.4.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.
- package/dist/frameLoop.d.ts +50 -0
- package/dist/frameLoop.js +46 -0
- package/dist/images.js +3 -18
- package/dist/index.d.ts +1 -0
- package/dist/runtime.d.ts +9 -9
- package/dist/runtime.js +16 -21
- package/dist/window.d.ts +14 -0
- package/dist/window.js +3 -0
- package/package.json +6 -6
- package/src/frameLoop.ts +84 -0
- package/src/images.ts +3 -19
- package/src/index.ts +1 -0
- package/src/runtime.ts +24 -30
- package/src/window.ts +17 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's frames, in the order a browser runs one: the page's animation frames, the frame
|
|
3
|
+
* itself, the task's microtask checkpoint, then the present.
|
|
4
|
+
*
|
|
5
|
+
* **The checkpoint before the present is the part that is easy to lose.** A browser puts the canvas
|
|
6
|
+
* on the screen when the task ends, after every microtask queued in it has run, and the WebGPU
|
|
7
|
+
* renderer relies on that: whatever is drawn after `endFrame` is submitted on a microtask, since
|
|
8
|
+
* there is no second `endFrame` to hang it on. A host that presents as soon as the frame returns
|
|
9
|
+
* blits the canvas before that submission, so the window never shows an overlay while the canvas
|
|
10
|
+
* texture, which a capture reads back, holds it — a loading screen seen by every capture and by
|
|
11
|
+
* nobody at the machine.
|
|
12
|
+
*
|
|
13
|
+
* Taken as steps rather than as a window, so the order can be held by a test without a display.
|
|
14
|
+
*/
|
|
15
|
+
/** One frame's call: the page's clock in milliseconds, and how many frames were drawn before it. */
|
|
16
|
+
export interface FrameCall {
|
|
17
|
+
readonly now: number;
|
|
18
|
+
readonly drawn: number;
|
|
19
|
+
}
|
|
20
|
+
/** The device's error scope around a frame, where there is a device to scope. */
|
|
21
|
+
export interface FrameErrors {
|
|
22
|
+
open(): void;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
export interface FrameSteps {
|
|
26
|
+
/** Stop after this many; unbounded when absent. */
|
|
27
|
+
readonly frames?: number;
|
|
28
|
+
/** Whether to draw another: false once the window has closed or the game has asked to quit. */
|
|
29
|
+
readonly open: () => boolean;
|
|
30
|
+
/** The page's clock, read once a frame. */
|
|
31
|
+
readonly clock: () => number;
|
|
32
|
+
/** Where a held clock moves, before anything reads it. */
|
|
33
|
+
readonly before?: (drawn: number) => void;
|
|
34
|
+
/** The page's animation-frame callbacks, which a browser runs before it paints. */
|
|
35
|
+
readonly animationFrames: (now: number) => void;
|
|
36
|
+
/** The device's error scope for this frame, or null before the canvas has a device. */
|
|
37
|
+
readonly errors: () => FrameErrors | null;
|
|
38
|
+
/**
|
|
39
|
+
* Draw the frame, or answer with a promise when there is nothing to draw until it settles — a
|
|
40
|
+
* scene still mounting. The loop waits for it, presents nothing for that frame and does not count
|
|
41
|
+
* it, rather than spinning through empty frames: that burned a core between two scenes, and spent
|
|
42
|
+
* a run's frame budget before the next scene had drawn once.
|
|
43
|
+
*/
|
|
44
|
+
readonly frame?: (call: FrameCall) => void | Promise<void>;
|
|
45
|
+
/** Put the canvas on the screen. */
|
|
46
|
+
readonly present: () => void;
|
|
47
|
+
readonly after?: (call: FrameCall) => void;
|
|
48
|
+
}
|
|
49
|
+
/** Draw until `open` says stop or `frames` are drawn, and answer how many were. */
|
|
50
|
+
export declare function runFrames(steps: FrameSteps): Promise<number>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's frames, in the order a browser runs one: the page's animation frames, the frame
|
|
3
|
+
* itself, the task's microtask checkpoint, then the present.
|
|
4
|
+
*
|
|
5
|
+
* **The checkpoint before the present is the part that is easy to lose.** A browser puts the canvas
|
|
6
|
+
* on the screen when the task ends, after every microtask queued in it has run, and the WebGPU
|
|
7
|
+
* renderer relies on that: whatever is drawn after `endFrame` is submitted on a microtask, since
|
|
8
|
+
* there is no second `endFrame` to hang it on. A host that presents as soon as the frame returns
|
|
9
|
+
* blits the canvas before that submission, so the window never shows an overlay while the canvas
|
|
10
|
+
* texture, which a capture reads back, holds it — a loading screen seen by every capture and by
|
|
11
|
+
* nobody at the machine.
|
|
12
|
+
*
|
|
13
|
+
* Taken as steps rather than as a window, so the order can be held by a test without a display.
|
|
14
|
+
*/
|
|
15
|
+
/** Draw until `open` says stop or `frames` are drawn, and answer how many were. */
|
|
16
|
+
export async function runFrames(steps) {
|
|
17
|
+
const frames = steps.frames ?? Number.POSITIVE_INFINITY;
|
|
18
|
+
let drawn = 0;
|
|
19
|
+
while (steps.open() && drawn < frames) {
|
|
20
|
+
steps.before?.(drawn);
|
|
21
|
+
const now = steps.clock();
|
|
22
|
+
steps.animationFrames(now);
|
|
23
|
+
const errors = steps.errors();
|
|
24
|
+
errors?.open();
|
|
25
|
+
const waiting = steps.frame?.({ now, drawn });
|
|
26
|
+
if (waiting !== undefined) {
|
|
27
|
+
errors?.close();
|
|
28
|
+
await waiting;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
/*
|
|
32
|
+
* The microtask checkpoint, then the present. A macrotask is the one boundary Node guarantees
|
|
33
|
+
* every microtask has run by, chained ones included. What it gives up: the device's and SDL's
|
|
34
|
+
* callbacks may take a turn between the frame and the present, where a browser runs nothing
|
|
35
|
+
* else. Both only queue work; neither draws.
|
|
36
|
+
*/
|
|
37
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
38
|
+
steps.present();
|
|
39
|
+
errors?.close();
|
|
40
|
+
steps.after?.({ now, drawn });
|
|
41
|
+
drawn += 1;
|
|
42
|
+
/* A task between frames, so SDL's events and the device's callbacks get their turn. */
|
|
43
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
44
|
+
}
|
|
45
|
+
return drawn;
|
|
46
|
+
}
|
package/dist/images.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { readFileSync } from 'node:fs';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
22
|
import decodeJpeg, { init as initJpeg } from '@jsquash/jpeg/decode.js';
|
|
23
|
-
import { decodePng } from '@driftengine/core/scripts/png.mjs';
|
|
23
|
+
import { decodePng, rgbaOf } from '@driftengine/core/scripts/png.mjs';
|
|
24
24
|
const HOST_BITMAP = Symbol('driftengine host bitmap');
|
|
25
25
|
export function isHostBitmap(value) {
|
|
26
26
|
return typeof value === 'object' && value !== null && HOST_BITMAP in value;
|
|
@@ -35,21 +35,6 @@ function bitmap(width, height, data, premultiplied) {
|
|
|
35
35
|
[HOST_BITMAP]: true,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
/** Four bytes a pixel out of three or four. */
|
|
39
|
-
function rgba(pixels, channels, count) {
|
|
40
|
-
const out = new Uint8ClampedArray(count * 4);
|
|
41
|
-
if (channels === 4) {
|
|
42
|
-
out.set(pixels.subarray(0, count * 4));
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
for (let at = 0; at < count; at += 1) {
|
|
46
|
-
out[at * 4] = pixels[at * channels];
|
|
47
|
-
out[at * 4 + 1] = pixels[at * channels + 1];
|
|
48
|
-
out[at * 4 + 2] = pixels[at * channels + 2];
|
|
49
|
-
out[at * 4 + 3] = 255;
|
|
50
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
38
|
let jpegReady = null;
|
|
54
39
|
/** MozJPEG's module, compiled once from the file its package ships. */
|
|
55
40
|
function readyJpeg() {
|
|
@@ -78,8 +63,8 @@ export async function hostCreateImageBitmap(source, options = {}) {
|
|
|
78
63
|
}
|
|
79
64
|
const bytes = new Uint8Array(await source.arrayBuffer());
|
|
80
65
|
if (source.type === 'image/png') {
|
|
81
|
-
const png = decodePng(Buffer.from(bytes));
|
|
82
|
-
const pixels =
|
|
66
|
+
const png = rgbaOf(decodePng(Buffer.from(bytes)));
|
|
67
|
+
const pixels = new Uint8ClampedArray(png.rgba.buffer, png.rgba.byteOffset, png.rgba.length);
|
|
83
68
|
return bitmap(png.width, png.height, pixels, premultiplied);
|
|
84
69
|
}
|
|
85
70
|
if (source.type === 'image/jpeg') {
|
package/dist/index.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export type { GameEntry, GameOptions } from './game.ts';
|
|
|
21
21
|
export { configDirectory, startHost } from './runtime.ts';
|
|
22
22
|
export type { FrameCall, HostOptions, NativeHost, RunOptions } from './runtime.ts';
|
|
23
23
|
export { tileFiles } from './tileFiles.ts';
|
|
24
|
+
export type { PresentMode } from './window.ts';
|
package/dist/runtime.d.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
import { type AudioDefaults } from './audio.ts';
|
|
17
17
|
import type { NativeBridge } from './bridge.ts';
|
|
18
18
|
import type { NativeCanvas } from './canvas.ts';
|
|
19
|
+
import { type FrameCall } from './frameLoop.ts';
|
|
19
20
|
import { HostPage } from './page.ts';
|
|
20
|
-
import { HostWindow } from './window.ts';
|
|
21
|
+
import { HostWindow, type PresentMode } from './window.ts';
|
|
21
22
|
import type { WindowMode } from './windowMode.ts';
|
|
22
23
|
export interface HostOptions {
|
|
23
24
|
readonly title: string;
|
|
@@ -37,17 +38,15 @@ export interface HostOptions {
|
|
|
37
38
|
/** `location.search`, for a game that reads its address. */
|
|
38
39
|
readonly query?: string;
|
|
39
40
|
readonly audio?: AudioDefaults;
|
|
41
|
+
/** How frames reach the screen; `fifo` by default. See `HostWindowOptions.presentMode`. */
|
|
42
|
+
readonly presentMode?: PresentMode;
|
|
40
43
|
}
|
|
41
|
-
export
|
|
42
|
-
/** The page's clock for this frame, in milliseconds. */
|
|
43
|
-
readonly now: number;
|
|
44
|
-
/** Frames drawn before this one. */
|
|
45
|
-
readonly drawn: number;
|
|
46
|
-
}
|
|
44
|
+
export type { FrameCall } from './frameLoop.ts';
|
|
47
45
|
export interface RunOptions {
|
|
48
46
|
readonly frames?: number;
|
|
49
47
|
readonly before?: (drawn: number) => void;
|
|
50
|
-
|
|
48
|
+
/** Draw, or answer with a promise to be waited for when there is nothing to draw: see `runFrames`. */
|
|
49
|
+
readonly frame?: (call: FrameCall) => void | Promise<void>;
|
|
51
50
|
readonly after?: (call: FrameCall) => void;
|
|
52
51
|
}
|
|
53
52
|
export interface NativeHost {
|
|
@@ -58,7 +57,8 @@ export interface NativeHost {
|
|
|
58
57
|
/**
|
|
59
58
|
* Draw until the window closes, the game asks to quit, or `frames` have been drawn. Each frame
|
|
60
59
|
* calls `before` (where a held clock moves), runs the page's animation frames, calls `frame`,
|
|
61
|
-
* puts the canvas on the screen and calls `after`. Answers how
|
|
60
|
+
* lets the microtasks it queued run, puts the canvas on the screen and calls `after`. Answers how
|
|
61
|
+
* many were drawn.
|
|
62
62
|
*/
|
|
63
63
|
run(options?: RunOptions): Promise<number>;
|
|
64
64
|
/** Flush the store and close the window. */
|
package/dist/runtime.js
CHANGED
|
@@ -20,6 +20,7 @@ import sdl from '@kmamal/sdl';
|
|
|
20
20
|
import { BRIDGE_KEY } from '@driftengine/package/bridge';
|
|
21
21
|
import { installAudio } from './audio.js';
|
|
22
22
|
import { errorsOf } from './device.js';
|
|
23
|
+
import { runFrames } from './frameLoop.js';
|
|
23
24
|
import { hostFetch } from './files.js';
|
|
24
25
|
import { HostGamepads } from './gamepads.js';
|
|
25
26
|
import { installGpu, installLocation } from './globals.js';
|
|
@@ -46,6 +47,7 @@ export function startHost(options) {
|
|
|
46
47
|
hidden: options.hidden === true,
|
|
47
48
|
mode: options.mode,
|
|
48
49
|
resizable: options.resizable,
|
|
50
|
+
presentMode: options.presentMode,
|
|
49
51
|
});
|
|
50
52
|
installGpu(window.gpu);
|
|
51
53
|
installLocation(options.query ?? '');
|
|
@@ -73,27 +75,20 @@ export function startHost(options) {
|
|
|
73
75
|
page,
|
|
74
76
|
canvas: window.canvas,
|
|
75
77
|
bridge,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
errors?.close();
|
|
91
|
-
run.after?.({ now, drawn });
|
|
92
|
-
drawn += 1;
|
|
93
|
-
/* A task between frames, so SDL's events and the device's callbacks get their turn. */
|
|
94
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
95
|
-
}
|
|
96
|
-
return drawn;
|
|
78
|
+
/* In a browser's order, the microtask checkpoint before the present included: see `frameLoop.ts`. */
|
|
79
|
+
run(run = {}) {
|
|
80
|
+
return runFrames({
|
|
81
|
+
...run,
|
|
82
|
+
open: () => !window.isClosed && !quitting,
|
|
83
|
+
clock: () => performance.now(),
|
|
84
|
+
animationFrames: (now) => page.runFrame(now),
|
|
85
|
+
errors: () => {
|
|
86
|
+
const context = window.canvas.getContext('webgpu');
|
|
87
|
+
const configuration = context.getConfiguration();
|
|
88
|
+
return configuration === null ? null : errorsOf(configuration.device);
|
|
89
|
+
},
|
|
90
|
+
present: () => window.present(),
|
|
91
|
+
});
|
|
97
92
|
},
|
|
98
93
|
async close() {
|
|
99
94
|
bridge.flushSync();
|
package/dist/window.d.ts
CHANGED
|
@@ -28,7 +28,20 @@ export interface HostWindowOptions {
|
|
|
28
28
|
/** As a packaged game's manifest says; windowed and resizable when it says nothing. */
|
|
29
29
|
readonly mode?: WindowMode;
|
|
30
30
|
readonly resizable?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* How a frame reaches the screen: `fifo`, the default, waits for the display's next refresh.
|
|
33
|
+
*
|
|
34
|
+
* **Named because the pacing is the compositor's, not the engine's.** The binding renders into
|
|
35
|
+
* X11 windows only, so a Wayland session runs this through XWayland, and what `fifo` waits for
|
|
36
|
+
* there differs by window: measured on one session, a hidden window held 58 fps where a visible
|
|
37
|
+
* one drew a light scene at 250. A caller measuring the engine wants the wait out of the way.
|
|
38
|
+
* `mailbox` never waits and the display takes the newest frame at its own rate, at the cost of
|
|
39
|
+
* drawing frames nobody sees; `immediate` also tears.
|
|
40
|
+
*/
|
|
41
|
+
readonly presentMode?: PresentMode;
|
|
31
42
|
}
|
|
43
|
+
/** The binding's swap-chain modes, as `renderGPUDeviceToWindow` takes them. */
|
|
44
|
+
export type PresentMode = 'fifo' | 'fifoRelaxed' | 'immediate' | 'mailbox';
|
|
32
45
|
export declare class HostWindow {
|
|
33
46
|
readonly canvas: NativeCanvas;
|
|
34
47
|
readonly gpu: GPU;
|
|
@@ -37,6 +50,7 @@ export declare class HostWindow {
|
|
|
37
50
|
private device;
|
|
38
51
|
private renderer;
|
|
39
52
|
private presenter;
|
|
53
|
+
private readonly presentMode;
|
|
40
54
|
private closed;
|
|
41
55
|
constructor(options: HostWindowOptions);
|
|
42
56
|
/**
|
package/dist/window.js
CHANGED
|
@@ -33,6 +33,7 @@ export class HostWindow {
|
|
|
33
33
|
device = null;
|
|
34
34
|
renderer = null;
|
|
35
35
|
presenter = null;
|
|
36
|
+
presentMode;
|
|
36
37
|
closed = false;
|
|
37
38
|
constructor(options) {
|
|
38
39
|
this.window = sdl.video.createWindow({
|
|
@@ -43,6 +44,7 @@ export class HostWindow {
|
|
|
43
44
|
visible: options.hidden !== true,
|
|
44
45
|
webgpu: true,
|
|
45
46
|
});
|
|
47
|
+
this.presentMode = options.presentMode ?? 'fifo';
|
|
46
48
|
this.canvas = new NativeCanvas(this.window.pixelWidth, this.window.pixelHeight);
|
|
47
49
|
this.instance = gpuModule.create([]);
|
|
48
50
|
this.gpu = nativeGpu(this.instance);
|
|
@@ -158,6 +160,7 @@ export class HostWindow {
|
|
|
158
160
|
this.renderer = gpuModule.renderGPUDeviceToWindow({
|
|
159
161
|
device: device,
|
|
160
162
|
window: this.window,
|
|
163
|
+
presentMode: this.presentMode,
|
|
161
164
|
});
|
|
162
165
|
this.presenter = new Presenter(device, this.renderer.getCurrentTexture().format);
|
|
163
166
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/native-host",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "The engine on a native window and a native WebGPU device, with no browser between them",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"node-web-audio-api": "2.2.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@driftengine/audio": "4.
|
|
40
|
+
"@driftengine/audio": "4.4.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@driftengine/core": "4.
|
|
44
|
-
"@driftengine/package": "4.
|
|
45
|
-
"@driftengine/texture": "4.
|
|
46
|
-
"@driftengine/ui2d": "4.
|
|
43
|
+
"@driftengine/core": "4.4.1",
|
|
44
|
+
"@driftengine/package": "4.4.1",
|
|
45
|
+
"@driftengine/texture": "4.4.1",
|
|
46
|
+
"@driftengine/ui2d": "4.4.1"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"@driftengine/texture": {
|
package/src/frameLoop.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's frames, in the order a browser runs one: the page's animation frames, the frame
|
|
3
|
+
* itself, the task's microtask checkpoint, then the present.
|
|
4
|
+
*
|
|
5
|
+
* **The checkpoint before the present is the part that is easy to lose.** A browser puts the canvas
|
|
6
|
+
* on the screen when the task ends, after every microtask queued in it has run, and the WebGPU
|
|
7
|
+
* renderer relies on that: whatever is drawn after `endFrame` is submitted on a microtask, since
|
|
8
|
+
* there is no second `endFrame` to hang it on. A host that presents as soon as the frame returns
|
|
9
|
+
* blits the canvas before that submission, so the window never shows an overlay while the canvas
|
|
10
|
+
* texture, which a capture reads back, holds it — a loading screen seen by every capture and by
|
|
11
|
+
* nobody at the machine.
|
|
12
|
+
*
|
|
13
|
+
* Taken as steps rather than as a window, so the order can be held by a test without a display.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One frame's call: the page's clock in milliseconds, and how many frames were drawn before it. */
|
|
17
|
+
export interface FrameCall {
|
|
18
|
+
readonly now: number;
|
|
19
|
+
readonly drawn: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The device's error scope around a frame, where there is a device to scope. */
|
|
23
|
+
export interface FrameErrors {
|
|
24
|
+
open(): void;
|
|
25
|
+
close(): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FrameSteps {
|
|
29
|
+
/** Stop after this many; unbounded when absent. */
|
|
30
|
+
readonly frames?: number;
|
|
31
|
+
/** Whether to draw another: false once the window has closed or the game has asked to quit. */
|
|
32
|
+
readonly open: () => boolean;
|
|
33
|
+
/** The page's clock, read once a frame. */
|
|
34
|
+
readonly clock: () => number;
|
|
35
|
+
/** Where a held clock moves, before anything reads it. */
|
|
36
|
+
readonly before?: (drawn: number) => void;
|
|
37
|
+
/** The page's animation-frame callbacks, which a browser runs before it paints. */
|
|
38
|
+
readonly animationFrames: (now: number) => void;
|
|
39
|
+
/** The device's error scope for this frame, or null before the canvas has a device. */
|
|
40
|
+
readonly errors: () => FrameErrors | null;
|
|
41
|
+
/**
|
|
42
|
+
* Draw the frame, or answer with a promise when there is nothing to draw until it settles — a
|
|
43
|
+
* scene still mounting. The loop waits for it, presents nothing for that frame and does not count
|
|
44
|
+
* it, rather than spinning through empty frames: that burned a core between two scenes, and spent
|
|
45
|
+
* a run's frame budget before the next scene had drawn once.
|
|
46
|
+
*/
|
|
47
|
+
readonly frame?: (call: FrameCall) => void | Promise<void>;
|
|
48
|
+
/** Put the canvas on the screen. */
|
|
49
|
+
readonly present: () => void;
|
|
50
|
+
readonly after?: (call: FrameCall) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Draw until `open` says stop or `frames` are drawn, and answer how many were. */
|
|
54
|
+
export async function runFrames(steps: FrameSteps): Promise<number> {
|
|
55
|
+
const frames = steps.frames ?? Number.POSITIVE_INFINITY;
|
|
56
|
+
let drawn = 0;
|
|
57
|
+
while (steps.open() && drawn < frames) {
|
|
58
|
+
steps.before?.(drawn);
|
|
59
|
+
const now = steps.clock();
|
|
60
|
+
steps.animationFrames(now);
|
|
61
|
+
const errors = steps.errors();
|
|
62
|
+
errors?.open();
|
|
63
|
+
const waiting = steps.frame?.({ now, drawn });
|
|
64
|
+
if (waiting !== undefined) {
|
|
65
|
+
errors?.close();
|
|
66
|
+
await waiting;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
/*
|
|
70
|
+
* The microtask checkpoint, then the present. A macrotask is the one boundary Node guarantees
|
|
71
|
+
* every microtask has run by, chained ones included. What it gives up: the device's and SDL's
|
|
72
|
+
* callbacks may take a turn between the frame and the present, where a browser runs nothing
|
|
73
|
+
* else. Both only queue work; neither draws.
|
|
74
|
+
*/
|
|
75
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
76
|
+
steps.present();
|
|
77
|
+
errors?.close();
|
|
78
|
+
steps.after?.({ now, drawn });
|
|
79
|
+
drawn += 1;
|
|
80
|
+
/* A task between frames, so SDL's events and the device's callbacks get their turn. */
|
|
81
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
82
|
+
}
|
|
83
|
+
return drawn;
|
|
84
|
+
}
|
package/src/images.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { createRequire } from 'node:module';
|
|
|
23
23
|
|
|
24
24
|
import decodeJpeg, { init as initJpeg } from '@jsquash/jpeg/decode.js';
|
|
25
25
|
|
|
26
|
-
import { decodePng } from '@driftengine/core/scripts/png.mjs';
|
|
26
|
+
import { decodePng, rgbaOf } from '@driftengine/core/scripts/png.mjs';
|
|
27
27
|
|
|
28
28
|
/** What a host hands back for an image: its size and four bytes a pixel, top row first. */
|
|
29
29
|
export interface HostBitmap {
|
|
@@ -63,22 +63,6 @@ function bitmap(
|
|
|
63
63
|
} as HostBitmap;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** Four bytes a pixel out of three or four. */
|
|
67
|
-
function rgba(pixels: Uint8Array, channels: number, count: number): Uint8ClampedArray {
|
|
68
|
-
const out = new Uint8ClampedArray(count * 4);
|
|
69
|
-
if (channels === 4) {
|
|
70
|
-
out.set(pixels.subarray(0, count * 4));
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
for (let at = 0; at < count; at += 1) {
|
|
74
|
-
out[at * 4] = pixels[at * channels] as number;
|
|
75
|
-
out[at * 4 + 1] = pixels[at * channels + 1] as number;
|
|
76
|
-
out[at * 4 + 2] = pixels[at * channels + 2] as number;
|
|
77
|
-
out[at * 4 + 3] = 255;
|
|
78
|
-
}
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
66
|
let jpegReady: Promise<void> | null = null;
|
|
83
67
|
|
|
84
68
|
/** MozJPEG's module, compiled once from the file its package ships. */
|
|
@@ -112,8 +96,8 @@ export async function hostCreateImageBitmap(
|
|
|
112
96
|
}
|
|
113
97
|
const bytes = new Uint8Array(await source.arrayBuffer());
|
|
114
98
|
if (source.type === 'image/png') {
|
|
115
|
-
const png = decodePng(Buffer.from(bytes));
|
|
116
|
-
const pixels =
|
|
99
|
+
const png = rgbaOf(decodePng(Buffer.from(bytes)));
|
|
100
|
+
const pixels = new Uint8ClampedArray(png.rgba.buffer, png.rgba.byteOffset, png.rgba.length);
|
|
117
101
|
return bitmap(png.width, png.height, pixels, premultiplied);
|
|
118
102
|
}
|
|
119
103
|
if (source.type === 'image/jpeg') {
|
package/src/index.ts
CHANGED
|
@@ -22,3 +22,4 @@ export type { GameEntry, GameOptions } from './game.ts';
|
|
|
22
22
|
export { configDirectory, startHost } from './runtime.ts';
|
|
23
23
|
export type { FrameCall, HostOptions, NativeHost, RunOptions } from './runtime.ts';
|
|
24
24
|
export { tileFiles } from './tileFiles.ts';
|
|
25
|
+
export type { PresentMode } from './window.ts';
|
package/src/runtime.ts
CHANGED
|
@@ -25,13 +25,14 @@ import { type AudioDefaults, installAudio } from './audio.ts';
|
|
|
25
25
|
import type { NativeBridge } from './bridge.ts';
|
|
26
26
|
import type { NativeCanvas } from './canvas.ts';
|
|
27
27
|
import { errorsOf } from './device.ts';
|
|
28
|
+
import { type FrameCall, runFrames } from './frameLoop.ts';
|
|
28
29
|
import { hostFetch } from './files.ts';
|
|
29
30
|
import { HostGamepads, type JoystickModule, type PadModule } from './gamepads.ts';
|
|
30
31
|
import { installGpu, installLocation } from './globals.ts';
|
|
31
32
|
import { installImages } from './images.ts';
|
|
32
33
|
import { HostPage } from './page.ts';
|
|
33
34
|
import { installThreads } from './threads.ts';
|
|
34
|
-
import { HostWindow } from './window.ts';
|
|
35
|
+
import { HostWindow, type PresentMode } from './window.ts';
|
|
35
36
|
import type { WindowMode } from './windowMode.ts';
|
|
36
37
|
|
|
37
38
|
export interface HostOptions {
|
|
@@ -52,19 +53,17 @@ export interface HostOptions {
|
|
|
52
53
|
/** `location.search`, for a game that reads its address. */
|
|
53
54
|
readonly query?: string;
|
|
54
55
|
readonly audio?: AudioDefaults;
|
|
56
|
+
/** How frames reach the screen; `fifo` by default. See `HostWindowOptions.presentMode`. */
|
|
57
|
+
readonly presentMode?: PresentMode;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
export
|
|
58
|
-
/** The page's clock for this frame, in milliseconds. */
|
|
59
|
-
readonly now: number;
|
|
60
|
-
/** Frames drawn before this one. */
|
|
61
|
-
readonly drawn: number;
|
|
62
|
-
}
|
|
60
|
+
export type { FrameCall } from './frameLoop.ts';
|
|
63
61
|
|
|
64
62
|
export interface RunOptions {
|
|
65
63
|
readonly frames?: number;
|
|
66
64
|
readonly before?: (drawn: number) => void;
|
|
67
|
-
|
|
65
|
+
/** Draw, or answer with a promise to be waited for when there is nothing to draw: see `runFrames`. */
|
|
66
|
+
readonly frame?: (call: FrameCall) => void | Promise<void>;
|
|
68
67
|
readonly after?: (call: FrameCall) => void;
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -76,7 +75,8 @@ export interface NativeHost {
|
|
|
76
75
|
/**
|
|
77
76
|
* Draw until the window closes, the game asks to quit, or `frames` have been drawn. Each frame
|
|
78
77
|
* calls `before` (where a held clock moves), runs the page's animation frames, calls `frame`,
|
|
79
|
-
* puts the canvas on the screen and calls `after`. Answers how
|
|
78
|
+
* lets the microtasks it queued run, puts the canvas on the screen and calls `after`. Answers how
|
|
79
|
+
* many were drawn.
|
|
80
80
|
*/
|
|
81
81
|
run(options?: RunOptions): Promise<number>;
|
|
82
82
|
/** Flush the store and close the window. */
|
|
@@ -102,6 +102,7 @@ export function startHost(options: HostOptions): NativeHost {
|
|
|
102
102
|
hidden: options.hidden === true,
|
|
103
103
|
mode: options.mode,
|
|
104
104
|
resizable: options.resizable,
|
|
105
|
+
presentMode: options.presentMode,
|
|
105
106
|
});
|
|
106
107
|
installGpu(window.gpu);
|
|
107
108
|
installLocation(options.query ?? '');
|
|
@@ -132,27 +133,20 @@ export function startHost(options: HostOptions): NativeHost {
|
|
|
132
133
|
page,
|
|
133
134
|
canvas: window.canvas,
|
|
134
135
|
bridge,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
errors?.close();
|
|
150
|
-
run.after?.({ now, drawn });
|
|
151
|
-
drawn += 1;
|
|
152
|
-
/* A task between frames, so SDL's events and the device's callbacks get their turn. */
|
|
153
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
154
|
-
}
|
|
155
|
-
return drawn;
|
|
136
|
+
/* In a browser's order, the microtask checkpoint before the present included: see `frameLoop.ts`. */
|
|
137
|
+
run(run = {}) {
|
|
138
|
+
return runFrames({
|
|
139
|
+
...run,
|
|
140
|
+
open: () => !window.isClosed && !quitting,
|
|
141
|
+
clock: () => performance.now(),
|
|
142
|
+
animationFrames: (now) => page.runFrame(now),
|
|
143
|
+
errors: () => {
|
|
144
|
+
const context = window.canvas.getContext('webgpu') as GPUCanvasContext;
|
|
145
|
+
const configuration = context.getConfiguration();
|
|
146
|
+
return configuration === null ? null : errorsOf(configuration.device);
|
|
147
|
+
},
|
|
148
|
+
present: () => window.present(),
|
|
149
|
+
});
|
|
156
150
|
},
|
|
157
151
|
async close() {
|
|
158
152
|
bridge.flushSync();
|
package/src/window.ts
CHANGED
|
@@ -47,8 +47,22 @@ export interface HostWindowOptions {
|
|
|
47
47
|
/** As a packaged game's manifest says; windowed and resizable when it says nothing. */
|
|
48
48
|
readonly mode?: WindowMode;
|
|
49
49
|
readonly resizable?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* How a frame reaches the screen: `fifo`, the default, waits for the display's next refresh.
|
|
52
|
+
*
|
|
53
|
+
* **Named because the pacing is the compositor's, not the engine's.** The binding renders into
|
|
54
|
+
* X11 windows only, so a Wayland session runs this through XWayland, and what `fifo` waits for
|
|
55
|
+
* there differs by window: measured on one session, a hidden window held 58 fps where a visible
|
|
56
|
+
* one drew a light scene at 250. A caller measuring the engine wants the wait out of the way.
|
|
57
|
+
* `mailbox` never waits and the display takes the newest frame at its own rate, at the cost of
|
|
58
|
+
* drawing frames nobody sees; `immediate` also tears.
|
|
59
|
+
*/
|
|
60
|
+
readonly presentMode?: PresentMode;
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
/** The binding's swap-chain modes, as `renderGPUDeviceToWindow` takes them. */
|
|
64
|
+
export type PresentMode = 'fifo' | 'fifoRelaxed' | 'immediate' | 'mailbox';
|
|
65
|
+
|
|
52
66
|
export class HostWindow {
|
|
53
67
|
readonly canvas: NativeCanvas;
|
|
54
68
|
readonly gpu: GPU;
|
|
@@ -57,6 +71,7 @@ export class HostWindow {
|
|
|
57
71
|
private device: GPUDevice | null = null;
|
|
58
72
|
private renderer: WindowRenderer | null = null;
|
|
59
73
|
private presenter: Presenter | null = null;
|
|
74
|
+
private readonly presentMode: PresentMode;
|
|
60
75
|
private closed = false;
|
|
61
76
|
|
|
62
77
|
constructor(options: HostWindowOptions) {
|
|
@@ -68,6 +83,7 @@ export class HostWindow {
|
|
|
68
83
|
visible: options.hidden !== true,
|
|
69
84
|
webgpu: true,
|
|
70
85
|
});
|
|
86
|
+
this.presentMode = options.presentMode ?? 'fifo';
|
|
71
87
|
this.canvas = new NativeCanvas(this.window.pixelWidth, this.window.pixelHeight);
|
|
72
88
|
this.instance = gpuModule.create([]);
|
|
73
89
|
this.gpu = nativeGpu(this.instance as unknown as Parameters<typeof nativeGpu>[0]);
|
|
@@ -186,6 +202,7 @@ export class HostWindow {
|
|
|
186
202
|
this.renderer = gpuModule.renderGPUDeviceToWindow({
|
|
187
203
|
device: device as unknown as DawnDevice,
|
|
188
204
|
window: this.window,
|
|
205
|
+
presentMode: this.presentMode,
|
|
189
206
|
});
|
|
190
207
|
this.presenter = new Presenter(device, this.renderer.getCurrentTexture().format);
|
|
191
208
|
}
|