@driftengine/core 4.11.0 → 4.12.0
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/CHANGELOG.json +72 -0
- package/README.md +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/render/ambientOcclusionPass.d.ts +3 -2
- package/dist/render/ambientOcclusionPass.js +4 -2
- package/dist/render/backend/api.d.ts +21 -1
- package/dist/render/backend/webgl2/renderer.d.ts +61 -5
- package/dist/render/backend/webgl2/renderer.js +153 -21
- package/dist/render/backend/webgpu/renderer.d.ts +54 -1
- package/dist/render/backend/webgpu/renderer.js +246 -89
- package/dist/render/instances.d.ts +13 -1
- package/dist/render/instances.js +10 -0
- package/dist/render/occlusionFade.d.ts +21 -0
- package/dist/render/occlusionFade.js +27 -0
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -1
- package/dist/render/shaders/ambientOcclusion.js +12 -1
- package/dist/render/shaders/flat/areaLight.d.ts +1 -1
- package/dist/render/shaders/flat/areaLight.js +1 -1
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +1 -0
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +10 -3
- package/dist/render/shaders/flat/layerLooks.d.ts +16 -0
- package/dist/render/shaders/flat/layerLooks.js +116 -0
- package/dist/render/shaders/flat/layered.d.ts +19 -9
- package/dist/render/shaders/flat/layered.js +169 -37
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +17 -5
- package/dist/render/shaders/flat/models.js +1 -1
- package/dist/render/shaders/flat/normalMap.d.ts +1 -1
- package/dist/render/shaders/flat/normalMap.js +11 -2
- package/dist/render/shaders/flat/preamble.js +6 -0
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
- package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -48
- package/dist/render/shaders/generated/flat.wgsl.js +215 -87
- package/dist/render/surfaceLayerLooks.d.ts +80 -0
- package/dist/render/surfaceLayerLooks.js +83 -0
- package/dist/render/surfaceLayers.d.ts +128 -19
- package/dist/render/surfaceLayers.js +73 -13
- package/dist/render/surfaceTexture.d.ts +13 -4
- package/dist/render/transmission.d.ts +21 -0
- package/dist/render/transmission.js +21 -0
- package/dist/ui/splash.d.ts +42 -2
- package/dist/ui/splash.js +78 -13
- package/package.json +6 -6
- package/scripts/browser.d.mts +5 -0
- package/scripts/browser.mjs +68 -14
- package/scripts/look.d.mts +81 -0
- package/scripts/look.mjs +380 -0
- package/src/index.ts +4 -0
- package/src/render/ambientOcclusionPass.ts +4 -1
- package/src/render/backend/api.ts +31 -0
- package/src/render/backend/webgl2/renderer.ts +170 -22
- package/src/render/backend/webgpu/renderer.ts +379 -139
- package/src/render/instances.ts +18 -1
- package/src/render/occlusionFade.ts +29 -0
- package/src/render/sceneTarget.ts +3 -0
- package/src/render/shaders/ambientOcclusion.ts +12 -1
- package/src/render/shaders/flat/areaLight.ts +1 -1
- package/src/render/shaders/flat/glassTint.ts +3 -1
- package/src/render/shaders/flat/index.ts +15 -3
- package/src/render/shaders/flat/layerLooks.ts +116 -0
- package/src/render/shaders/flat/layered.ts +170 -37
- package/src/render/shaders/flat/main.ts +17 -5
- package/src/render/shaders/flat/models.ts +1 -1
- package/src/render/shaders/flat/normalMap.ts +11 -2
- package/src/render/shaders/flat/preamble.ts +6 -0
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
- package/src/render/shaders/generated/flat.wgsl.ts +215 -87
- package/src/render/surfaceLayerLooks.ts +155 -0
- package/src/render/surfaceLayers.ts +187 -23
- package/src/render/surfaceTexture.ts +13 -4
- package/src/render/transmission.ts +27 -0
- package/src/ui/splash.ts +83 -14
package/dist/ui/splash.js
CHANGED
|
@@ -80,8 +80,9 @@ export const SPLASH_FADE_MS = 420;
|
|
|
80
80
|
/**
|
|
81
81
|
* When the badge gives the screen to the game. Two conditions and a cap, each for a named failure.
|
|
82
82
|
*
|
|
83
|
-
* - **The game has painted
|
|
84
|
-
*
|
|
83
|
+
* - **The game has painted**, and every promise it asked the badge to wait for has settled
|
|
84
|
+
* (`holdSplash`). Swapping before that shows the empty canvas, or the half-loaded first screen,
|
|
85
|
+
* the badge exists to cover.
|
|
85
86
|
* - **The minimum has passed.** A badge that vanishes the instant a fast machine paints is a
|
|
86
87
|
* flicker, and the machines that boot fastest are the ones a developer tests on.
|
|
87
88
|
* - **The cap overrides both**, and is checked first so that a minimum longer than the cap
|
|
@@ -99,6 +100,58 @@ export function splashDecision(elapsedMs, minMs, contentReady, capMs = SPLASH_HA
|
|
|
99
100
|
return 'hold';
|
|
100
101
|
return elapsedMs >= minMs ? 'swap' : 'hold';
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* What a badge waits for besides the clock: the game's first frame, and every promise the game asked
|
|
105
|
+
* it to wait for (`holdSplash`), under the cap the game named, the longest of several, or the engine's.
|
|
106
|
+
*
|
|
107
|
+
* **A hold settles whichever way its promise does.** A load that failed must not pin the logo: the
|
|
108
|
+
* game is shown, broken and visible, which is the cap's own argument. Pure apart from the promises,
|
|
109
|
+
* so the rule is testable in Node.
|
|
110
|
+
*/
|
|
111
|
+
export class SplashState {
|
|
112
|
+
/** Whether a frame of the game has reached the screen. */
|
|
113
|
+
painted = false;
|
|
114
|
+
pending = 0;
|
|
115
|
+
/** The longest cap a hold has named, or 0 for none. */
|
|
116
|
+
asked = 0;
|
|
117
|
+
/** The longest the badge may stay: the cap a hold named, the longest of several, or the engine's. */
|
|
118
|
+
get capMs() {
|
|
119
|
+
return this.asked > 0 ? this.asked : SPLASH_HARD_CAP_MS;
|
|
120
|
+
}
|
|
121
|
+
/** Wait for `until` too, under `capMs` where it names one; `settled` once it has. */
|
|
122
|
+
hold(until, capMs, settled) {
|
|
123
|
+
this.pending += 1;
|
|
124
|
+
if (capMs !== undefined && Number.isFinite(capMs) && capMs > this.asked)
|
|
125
|
+
this.asked = capMs;
|
|
126
|
+
const done = () => {
|
|
127
|
+
this.pending -= 1;
|
|
128
|
+
settled();
|
|
129
|
+
};
|
|
130
|
+
Promise.resolve(until).then(done, done);
|
|
131
|
+
}
|
|
132
|
+
/** Painted, with every hold settled: what `splashDecision` calls the content being ready. */
|
|
133
|
+
get ready() {
|
|
134
|
+
return this.painted && this.pending === 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** The badge on the page, which `holdSplash` holds. One screen, so one badge: see `bootGate.ts`. */
|
|
138
|
+
let mounted = null;
|
|
139
|
+
/**
|
|
140
|
+
* Keep the engine's badge up until `until` settles, as well as until the game's first frame: a game
|
|
141
|
+
* whose first screen loads for longer than that frame — a stage, figures and their pictures on a
|
|
142
|
+
* first visit — is seen whole rather than loading. **`capMs` is the game's own ceiling** on the
|
|
143
|
+
* badge in place of the engine's twenty seconds, longer for a slow first visit or shorter, so a load
|
|
144
|
+
* that never settles is still shown in the end; the longest of several holds' wins, and the minimum
|
|
145
|
+
* hold is unchanged.
|
|
146
|
+
*
|
|
147
|
+
* **Frames run while the game loads behind the plate**, since a load may need them, and are held
|
|
148
|
+
* once it has settled, for whatever is left of the minimum, as they are after a first frame with no
|
|
149
|
+
* hold. Returns whether there was a badge to hold: none in a packaged build, whose shell shows its
|
|
150
|
+
* own until the first frame, nor where `splash: false` or `?splash=0` turned it off.
|
|
151
|
+
*/
|
|
152
|
+
export function holdSplash(until, options = {}) {
|
|
153
|
+
return mounted?.holdUntil(until, options.capMs) ?? false;
|
|
154
|
+
}
|
|
102
155
|
/**
|
|
103
156
|
* The badge markup. One constant string, interpolated into nothing.
|
|
104
157
|
*
|
|
@@ -228,17 +281,18 @@ export function mountSplash(options = {}) {
|
|
|
228
281
|
the document and already styled; nothing here needs to be undone. */
|
|
229
282
|
}
|
|
230
283
|
}
|
|
231
|
-
|
|
284
|
+
const state = new SplashState();
|
|
232
285
|
let leaving = false;
|
|
233
286
|
let timer = null;
|
|
234
287
|
/*
|
|
235
|
-
* **Freeze the game the moment it has proved it can draw.**
|
|
288
|
+
* **Freeze the game the moment it has proved it can draw and has loaded.**
|
|
236
289
|
*
|
|
237
|
-
*
|
|
238
|
-
* game would otherwise spend running behind an opaque screen.
|
|
290
|
+
* Ready is the first `endFrame` with every hold settled (`SplashState`), and everything after it
|
|
291
|
+
* until the plate leaves is time the game would otherwise spend running behind an opaque screen.
|
|
292
|
+
* Before it, frames run: a load the game is holding the badge for may need them. `bootGate` is read by `startLoop`
|
|
239
293
|
* and named by neither of them; see its own comment for why the state is shared that way.
|
|
240
294
|
*/
|
|
241
|
-
const openGate = holdFrames(() =>
|
|
295
|
+
const openGate = holdFrames(() => state.ready && !leaving);
|
|
242
296
|
const remove = () => {
|
|
243
297
|
/* Leaving the top layer is what `hidePopover` is for. `remove()` alone takes the element out
|
|
244
298
|
of the document, and an element removed while shown is not a state worth relying on. */
|
|
@@ -256,6 +310,8 @@ export function mountSplash(options = {}) {
|
|
|
256
310
|
if (leaving)
|
|
257
311
|
return;
|
|
258
312
|
leaving = true;
|
|
313
|
+
if (mounted === badge)
|
|
314
|
+
mounted = null;
|
|
259
315
|
/* Before the fade, not after it: the game resumes as the plate becomes transparent, so what
|
|
260
316
|
shows through is a running frame rather than the still one it was frozen on. */
|
|
261
317
|
openGate();
|
|
@@ -278,23 +334,32 @@ export function mountSplash(options = {}) {
|
|
|
278
334
|
if (leaving)
|
|
279
335
|
return;
|
|
280
336
|
const elapsed = performance.now() - started;
|
|
281
|
-
if (splashDecision(elapsed, minMs,
|
|
337
|
+
if (splashDecision(elapsed, minMs, state.ready, state.capMs) === 'swap') {
|
|
282
338
|
release();
|
|
283
339
|
return;
|
|
284
340
|
}
|
|
285
341
|
if (timer !== null)
|
|
286
342
|
clearTimeout(timer);
|
|
287
|
-
const next =
|
|
343
|
+
const next = state.ready ? minMs - elapsed : state.capMs - elapsed;
|
|
288
344
|
timer = setTimeout(settle, Math.max(0, next));
|
|
289
345
|
};
|
|
290
|
-
|
|
291
|
-
return {
|
|
346
|
+
const badge = {
|
|
292
347
|
present() {
|
|
293
|
-
if (painted || leaving)
|
|
348
|
+
if (state.painted || leaving)
|
|
294
349
|
return;
|
|
295
|
-
painted = true;
|
|
350
|
+
state.painted = true;
|
|
296
351
|
settle();
|
|
297
352
|
},
|
|
298
353
|
release,
|
|
354
|
+
holdUntil(until, capMs) {
|
|
355
|
+
if (leaving)
|
|
356
|
+
return false;
|
|
357
|
+
state.hold(until, capMs, settle);
|
|
358
|
+
settle();
|
|
359
|
+
return true;
|
|
360
|
+
},
|
|
299
361
|
};
|
|
362
|
+
mounted = badge;
|
|
363
|
+
settle();
|
|
364
|
+
return badge;
|
|
300
365
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.0",
|
|
4
4
|
"description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.
|
|
33
|
-
"@driftengine/physics": "4.
|
|
32
|
+
"@driftengine/drft": "4.12.0",
|
|
33
|
+
"@driftengine/physics": "4.12.0",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@driftengine/nav": "4.
|
|
71
|
-
"@driftengine/network": "4.
|
|
72
|
-
"@driftengine/texture": "4.
|
|
70
|
+
"@driftengine/nav": "4.12.0",
|
|
71
|
+
"@driftengine/network": "4.12.0",
|
|
72
|
+
"@driftengine/texture": "4.12.0"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/scripts/browser.d.mts
CHANGED
|
@@ -7,6 +7,11 @@ export interface LaunchedBrowser {
|
|
|
7
7
|
close(): Promise<void>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** Where a Chromium-based browser lives on a platform, in the order `chromePath` tries them. */
|
|
11
|
+
export function browserCandidates(
|
|
12
|
+
platform?: string,
|
|
13
|
+
env?: Readonly<Record<string, string | undefined>>,
|
|
14
|
+
): string[];
|
|
10
15
|
export function chromePath(): string;
|
|
11
16
|
export function launch(options?: {
|
|
12
17
|
flags?: readonly string[];
|
package/scripts/browser.mjs
CHANGED
|
@@ -27,22 +27,64 @@
|
|
|
27
27
|
* it, and it is what turns "leaks accumulate forever" into "at most one leak per still-running
|
|
28
28
|
* process."
|
|
29
29
|
*/
|
|
30
|
-
import { spawn } from 'node:child_process';
|
|
30
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
31
31
|
import { once } from 'node:events';
|
|
32
32
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
33
33
|
import { tmpdir } from 'node:os';
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
import { sleep } from './cdp.mjs';
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Where a Chromium-based browser lives on each platform, in the order worth trying.
|
|
39
|
+
*
|
|
40
|
+
* **Edge is last on every list and the only thing on Windows that is always there.** It is
|
|
41
|
+
* Chromium, it speaks the same DevTools protocol, and it ships with Windows 10 and 11, so a person
|
|
42
|
+
* who never installed Chrome still has a browser this module can drive. Chrome comes first where it
|
|
43
|
+
* exists because it is what the engine's captures are measured on.
|
|
44
|
+
*
|
|
45
|
+
* Windows paths are built from the environment rather than written out, because `Program Files`
|
|
46
|
+
* is wherever that machine put it and a per-user install sits under `LOCALAPPDATA`; a Mac's
|
|
47
|
+
* per-user install sits under `~/Applications`. Nothing here has been run on Windows or macOS by
|
|
48
|
+
* this repository yet; the lists are the browsers' documented install locations.
|
|
49
|
+
*/
|
|
50
|
+
export function browserCandidates(platform = process.platform, env = process.env) {
|
|
51
|
+
if (platform === 'win32') {
|
|
52
|
+
const roots = [env['PROGRAMFILES'], env['PROGRAMFILES(X86)'], env['LOCALAPPDATA']].filter(
|
|
53
|
+
(root) => typeof root === 'string' && root !== '',
|
|
54
|
+
);
|
|
55
|
+
return [
|
|
56
|
+
...roots.map((root) =>
|
|
57
|
+
path.win32.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
58
|
+
),
|
|
59
|
+
...roots.map((root) =>
|
|
60
|
+
path.win32.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
61
|
+
),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
if (platform === 'darwin') {
|
|
65
|
+
/* `/Applications` for an install by an administrator, `~/Applications` for one without. */
|
|
66
|
+
const roots = [
|
|
67
|
+
'/Applications',
|
|
68
|
+
typeof env['HOME'] === 'string' && env['HOME'] !== ''
|
|
69
|
+
? path.posix.join(env['HOME'], 'Applications')
|
|
70
|
+
: null,
|
|
71
|
+
].filter((root) => root !== null);
|
|
72
|
+
const apps = [
|
|
73
|
+
'Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
74
|
+
'Chromium.app/Contents/MacOS/Chromium',
|
|
75
|
+
'Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
76
|
+
];
|
|
77
|
+
return apps.flatMap((app) => roots.map((root) => path.posix.join(root, app)));
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
'/opt/google/chrome/chrome',
|
|
81
|
+
'/usr/bin/google-chrome',
|
|
82
|
+
'/usr/bin/google-chrome-stable',
|
|
83
|
+
'/usr/bin/chromium',
|
|
84
|
+
'/usr/bin/chromium-browser',
|
|
85
|
+
'/usr/bin/microsoft-edge',
|
|
86
|
+
];
|
|
87
|
+
}
|
|
46
88
|
|
|
47
89
|
/**
|
|
48
90
|
* What it takes to reach the real card from a headless process.
|
|
@@ -80,7 +122,7 @@ const PROFILE_PREFIX = 'driftengine-shots-';
|
|
|
80
122
|
const OWNER_FILE = 'driftengine-owner.json';
|
|
81
123
|
|
|
82
124
|
/**
|
|
83
|
-
*
|
|
125
|
+
* Stop a browser and every process it started, escalating if it does not go.
|
|
84
126
|
*
|
|
85
127
|
* **Chrome is a tree, not a process.** It forks a zygote, a GPU process and one renderer per
|
|
86
128
|
* page, and signalling the parent alone leaves the rest running with the port closed and the
|
|
@@ -89,9 +131,20 @@ const OWNER_FILE = 'driftengine-owner.json';
|
|
|
89
131
|
*
|
|
90
132
|
* SIGTERM first because a browser asked politely flushes and exits; SIGKILL after a grace period
|
|
91
133
|
* because one that is wedged never will, and a wedged browser is exactly the case this exists for.
|
|
134
|
+
*
|
|
135
|
+
* **Windows has no process groups to signal**, so there `taskkill /T` walks the tree from the
|
|
136
|
+
* browser's own PID and `/F` ends it at once. Synchronous, because this also runs from an `exit`
|
|
137
|
+
* handler, where nothing asynchronous gets to finish.
|
|
92
138
|
*/
|
|
93
139
|
function killTree(pid, graceMs = 2000) {
|
|
94
140
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
141
|
+
if (process.platform === 'win32') {
|
|
142
|
+
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], {
|
|
143
|
+
stdio: 'ignore',
|
|
144
|
+
windowsHide: true,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
95
148
|
try {
|
|
96
149
|
process.kill(-pid, 'SIGTERM');
|
|
97
150
|
} catch {
|
|
@@ -194,10 +247,11 @@ function defaultIsAlive(pid) {
|
|
|
194
247
|
export function chromePath() {
|
|
195
248
|
const asked = process.env['CHROME_PATH'];
|
|
196
249
|
if (asked !== undefined && asked !== '') return asked;
|
|
197
|
-
const
|
|
250
|
+
const candidates = browserCandidates();
|
|
251
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
198
252
|
if (found === undefined) {
|
|
199
253
|
throw new Error(
|
|
200
|
-
`no Chrome or
|
|
254
|
+
`no Chrome, Chromium or Edge found. Tried:\n ${candidates.join('\n ')}\nSet CHROME_PATH to one.`,
|
|
201
255
|
);
|
|
202
256
|
}
|
|
203
257
|
return found;
|
|
@@ -227,7 +281,7 @@ export async function launch({ flags = GPU_FLAGS, headless = true, timeoutMs = 2
|
|
|
227
281
|
process and every renderer with one signal to the negative PID. `unref()` is deliberately
|
|
228
282
|
not called: the parent must stay attached, or its own exit and signal handlers below never
|
|
229
283
|
fire, and those are half of what closes the other three holes. */
|
|
230
|
-
const child = spawn(binary, args, { stdio: 'ignore', detached: true });
|
|
284
|
+
const child = spawn(binary, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
|
231
285
|
writeFileSync(
|
|
232
286
|
path.join(profile, OWNER_FILE),
|
|
233
287
|
JSON.stringify({ ownerPid: process.pid, chromePid: child.pid }),
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { RgbaImage } from './png.d.mts';
|
|
2
|
+
|
|
3
|
+
/** The backends `look` photographs by default, in the order a consumer runs them. */
|
|
4
|
+
export const LOOK_BACKENDS: readonly ['webgpu', 'webgl2'];
|
|
5
|
+
|
|
6
|
+
/** At or above this share of one colour, a canvas is called empty. */
|
|
7
|
+
export const EMPTY_SHARE: number;
|
|
8
|
+
|
|
9
|
+
/** What a frame is made of. */
|
|
10
|
+
export interface FrameStats {
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
/** The most common colour, as `#rrggbb`. */
|
|
14
|
+
readonly dominant: string;
|
|
15
|
+
/** The share of the frame that colour covers, 0 to 1. */
|
|
16
|
+
readonly dominantShare: number;
|
|
17
|
+
/** Rec. 709 luminance averaged over the frame, 0 to 255. */
|
|
18
|
+
readonly meanLuminance: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One backend's photograph and everything the page said while it was taken. */
|
|
22
|
+
export interface LookCapture {
|
|
23
|
+
readonly backend: string;
|
|
24
|
+
readonly url: string;
|
|
25
|
+
/** What a player sees, interface included. */
|
|
26
|
+
readonly file: string;
|
|
27
|
+
/**
|
|
28
|
+
* The backend that actually drew, read from the context the page's canvas asked for: `'webgpu'`,
|
|
29
|
+
* `'webgl2'`, or `null` when no canvas in the page asked for either.
|
|
30
|
+
*/
|
|
31
|
+
readonly drawnWith: 'webgpu' | 'webgl2' | null;
|
|
32
|
+
/** Measured over the canvas alone; `null` when the page never booted. */
|
|
33
|
+
readonly stats: FrameStats | null;
|
|
34
|
+
/** The share of the canvas that changed between two frames; `null` when the page never booted. */
|
|
35
|
+
readonly moved: number | null;
|
|
36
|
+
readonly errors: readonly string[];
|
|
37
|
+
readonly warnings: readonly string[];
|
|
38
|
+
/** Every console line, `level: text`. */
|
|
39
|
+
readonly logs: readonly string[];
|
|
40
|
+
readonly problems: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LookReport {
|
|
44
|
+
readonly url: string;
|
|
45
|
+
/** The renderer string of the GPU that drew, read from a WebGL2 context. */
|
|
46
|
+
readonly gpu: string;
|
|
47
|
+
readonly captures: readonly LookCapture[];
|
|
48
|
+
readonly problems: readonly string[];
|
|
49
|
+
/** True when no capture found a problem. */
|
|
50
|
+
readonly ok: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LookOptions {
|
|
54
|
+
/** The address of the running game. */
|
|
55
|
+
readonly url: string;
|
|
56
|
+
readonly backends?: readonly string[];
|
|
57
|
+
/** Where the PNGs and `look.json` go. Defaults to `.driftengine/look`. */
|
|
58
|
+
readonly out?: string;
|
|
59
|
+
readonly width?: number;
|
|
60
|
+
readonly height?: number;
|
|
61
|
+
/** How long to wait for a canvas in the page to ask for a WebGPU or WebGL2 context. */
|
|
62
|
+
readonly bootMs?: number;
|
|
63
|
+
/** A pause after that for the first frames. */
|
|
64
|
+
readonly settleMs?: number;
|
|
65
|
+
/** How long to wait after that for the canvas to show more than one colour. */
|
|
66
|
+
readonly drawMs?: number;
|
|
67
|
+
/** The gap between the two frames compared to tell whether the game moves. */
|
|
68
|
+
readonly moveMs?: number;
|
|
69
|
+
/** Browser flags; defaults to `gpuFlagsFor()`. */
|
|
70
|
+
readonly flags?: readonly string[] | null;
|
|
71
|
+
/** `false` opens a real window, for a machine whose headless browser cannot reach its GPU. */
|
|
72
|
+
readonly headless?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function gpuFlagsFor(platform?: string): string[] | null;
|
|
76
|
+
export function lookUrl(url: string, backend: string): string;
|
|
77
|
+
export function frameStats(image: RgbaImage): FrameStats;
|
|
78
|
+
export function changedShare(a: RgbaImage, b: RgbaImage, tolerance?: number): number;
|
|
79
|
+
export function sortConsole(lines: readonly string[]): { errors: string[]; warnings: string[] };
|
|
80
|
+
export function look(options: LookOptions): Promise<LookReport>;
|
|
81
|
+
export function formatLook(report: LookReport): string;
|