@odori/cli 0.0.6 → 0.0.7
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/{chunk-6CE7U5KB.js → chunk-STVORYOF.js} +39 -9
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/commands/test.ts +40 -9
- package/src/render.ts +18 -1
|
@@ -1757,9 +1757,10 @@ var renderUrl = (origin, target, frame) => {
|
|
|
1757
1757
|
if (target.prepared !== void 0) params.set("prepared", encodeParam(target.prepared));
|
|
1758
1758
|
return `${origin}/?${params.toString()}`;
|
|
1759
1759
|
};
|
|
1760
|
+
var BROWSER_ARGS = ["--enable-unsafe-swiftshader"];
|
|
1760
1761
|
var openRenderPage = async (origin, target, config) => {
|
|
1761
1762
|
const executablePath = await browserExecutable(config);
|
|
1762
|
-
const browser = await chromium.launch({ executablePath, headless: true });
|
|
1763
|
+
const browser = await chromium.launch({ executablePath, headless: true, args: BROWSER_ARGS });
|
|
1763
1764
|
const page = await browser.newPage({
|
|
1764
1765
|
viewport: { width: target.width, height: target.height },
|
|
1765
1766
|
deviceScaleFactor: 1
|
|
@@ -3427,15 +3428,36 @@ var CANVAS_SCRIPT = `(() => {
|
|
|
3427
3428
|
});
|
|
3428
3429
|
|
|
3429
3430
|
return canvases.map(function (canvas, index) {
|
|
3430
|
-
|
|
3431
|
-
if (!context || canvas.width === 0 || canvas.height === 0) {
|
|
3431
|
+
if (canvas.width === 0 || canvas.height === 0) {
|
|
3432
3432
|
return {index: index, hash: "no-context", blank: true};
|
|
3433
3433
|
}
|
|
3434
3434
|
var data;
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3435
|
+
var isGl = false;
|
|
3436
|
+
var context = canvas.getContext("2d");
|
|
3437
|
+
if (context) {
|
|
3438
|
+
try {
|
|
3439
|
+
data = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
return {index: index, hash: "tainted", blank: false};
|
|
3442
|
+
}
|
|
3443
|
+
} else {
|
|
3444
|
+
/*
|
|
3445
|
+
* A canvas that already holds a GL context returns null for "2d", and
|
|
3446
|
+
* reading that as an empty canvas reported every shader video as having
|
|
3447
|
+
* drawn nothing while it was drawing correctly. Pixels come back through
|
|
3448
|
+
* readPixels instead, which is the only way to see a GL surface.
|
|
3449
|
+
*
|
|
3450
|
+
* Worth knowing when this comes back empty: a drawing buffer is cleared
|
|
3451
|
+
* once it has been composited unless the context was asked for with
|
|
3452
|
+
* preserveDrawingBuffer, so an author who has not set that gets a real
|
|
3453
|
+
* frame on screen and nothing here.
|
|
3454
|
+
*/
|
|
3455
|
+
var gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
3456
|
+
if (!gl) return {index: index, hash: "no-context", blank: true};
|
|
3457
|
+
var pixels4 = new Uint8Array(canvas.width * canvas.height * 4);
|
|
3458
|
+
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels4);
|
|
3459
|
+
data = pixels4;
|
|
3460
|
+
isGl = true;
|
|
3439
3461
|
}
|
|
3440
3462
|
|
|
3441
3463
|
// A prime stride over the whole buffer rather than a coarse grid: a grid
|
|
@@ -3451,7 +3473,7 @@ var CANVAS_SCRIPT = `(() => {
|
|
|
3451
3473
|
hash =
|
|
3452
3474
|
((hash << 5) + hash + data[offset] + data[offset + 1] * 3 + data[offset + 2] * 7 + data[offset + 3] * 11) | 0;
|
|
3453
3475
|
}
|
|
3454
|
-
return {index: index, hash: String(hash), blank: opaque === 0};
|
|
3476
|
+
return {index: index, hash: String(hash), blank: opaque === 0, gl: isGl};
|
|
3455
3477
|
});
|
|
3456
3478
|
})()`;
|
|
3457
3479
|
var FRAME_SCRIPT = `(() => {
|
|
@@ -3466,6 +3488,11 @@ var FRAME_SCRIPT = `(() => {
|
|
|
3466
3488
|
|
|
3467
3489
|
for (var index = 0; index < nodes.length; index += 1) {
|
|
3468
3490
|
var node = nodes[index];
|
|
3491
|
+
/* The subtree an effect surface is photographing is deliberately parked
|
|
3492
|
+
outside the frame. It is the input to a canvas, not something a viewer
|
|
3493
|
+
ever sees, so judging its position or its type size is judging the
|
|
3494
|
+
wrong thing. */
|
|
3495
|
+
if (node.closest("[data-odori-capture]")) continue;
|
|
3469
3496
|
var box = node.getBoundingClientRect();
|
|
3470
3497
|
if (box.width === 0 || box.height === 0) continue;
|
|
3471
3498
|
var style = getComputedStyle(node);
|
|
@@ -3572,7 +3599,10 @@ var testVideo = async (origin, video, config, failures, quiet = false) => {
|
|
|
3572
3599
|
for (const canvas of canvases) {
|
|
3573
3600
|
const second = again.find((item) => item.index === canvas.index);
|
|
3574
3601
|
if (canvas.blank) {
|
|
3575
|
-
failures.push({
|
|
3602
|
+
failures.push({
|
|
3603
|
+
video: id,
|
|
3604
|
+
message: canvas.gl ? `Frame ${frame}: canvas ${canvas.index} read back empty. A WebGL drawing buffer is cleared once it has been composited, so ask for the context with {preserveDrawingBuffer: true}.` : `Frame ${frame}: canvas ${canvas.index} drew nothing.`
|
|
3605
|
+
});
|
|
3576
3606
|
continue;
|
|
3577
3607
|
}
|
|
3578
3608
|
if (canvas.hash === "tainted") {
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odori/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The odori command line: discovery, Studio, component installation, stills, tests, and export jobs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"playwright-core": "1.55.0",
|
|
29
29
|
"tsx": "4.20.5",
|
|
30
30
|
"vite": "7.3.0",
|
|
31
|
-
"odori": "0.0.
|
|
31
|
+
"odori": "0.0.7"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "22.19.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@types/react-dom": "19.2.3",
|
|
37
37
|
"tsup": "^8.5.1",
|
|
38
38
|
"typescript": "5.9.3",
|
|
39
|
-
"@odori/registry": "0.0.
|
|
39
|
+
"@odori/registry": "0.0.7"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
package/src/commands/test.ts
CHANGED
|
@@ -35,15 +35,36 @@ const CANVAS_SCRIPT = `(() => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
return canvases.map(function (canvas, index) {
|
|
38
|
-
|
|
39
|
-
if (!context || canvas.width === 0 || canvas.height === 0) {
|
|
38
|
+
if (canvas.width === 0 || canvas.height === 0) {
|
|
40
39
|
return {index: index, hash: "no-context", blank: true};
|
|
41
40
|
}
|
|
42
41
|
var data;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
var isGl = false;
|
|
43
|
+
var context = canvas.getContext("2d");
|
|
44
|
+
if (context) {
|
|
45
|
+
try {
|
|
46
|
+
data = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return {index: index, hash: "tainted", blank: false};
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
/*
|
|
52
|
+
* A canvas that already holds a GL context returns null for "2d", and
|
|
53
|
+
* reading that as an empty canvas reported every shader video as having
|
|
54
|
+
* drawn nothing while it was drawing correctly. Pixels come back through
|
|
55
|
+
* readPixels instead, which is the only way to see a GL surface.
|
|
56
|
+
*
|
|
57
|
+
* Worth knowing when this comes back empty: a drawing buffer is cleared
|
|
58
|
+
* once it has been composited unless the context was asked for with
|
|
59
|
+
* preserveDrawingBuffer, so an author who has not set that gets a real
|
|
60
|
+
* frame on screen and nothing here.
|
|
61
|
+
*/
|
|
62
|
+
var gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
63
|
+
if (!gl) return {index: index, hash: "no-context", blank: true};
|
|
64
|
+
var pixels4 = new Uint8Array(canvas.width * canvas.height * 4);
|
|
65
|
+
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels4);
|
|
66
|
+
data = pixels4;
|
|
67
|
+
isGl = true;
|
|
47
68
|
}
|
|
48
69
|
|
|
49
70
|
// A prime stride over the whole buffer rather than a coarse grid: a grid
|
|
@@ -59,11 +80,11 @@ const CANVAS_SCRIPT = `(() => {
|
|
|
59
80
|
hash =
|
|
60
81
|
((hash << 5) + hash + data[offset] + data[offset + 1] * 3 + data[offset + 2] * 7 + data[offset + 3] * 11) | 0;
|
|
61
82
|
}
|
|
62
|
-
return {index: index, hash: String(hash), blank: opaque === 0};
|
|
83
|
+
return {index: index, hash: String(hash), blank: opaque === 0, gl: isGl};
|
|
63
84
|
});
|
|
64
85
|
})()`;
|
|
65
86
|
|
|
66
|
-
type CanvasReport = Array<{index: number; hash: string; blank: boolean}>;
|
|
87
|
+
type CanvasReport = Array<{index: number; hash: string; blank: boolean; gl?: boolean}>;
|
|
67
88
|
|
|
68
89
|
/**
|
|
69
90
|
* Evaluated as source in the page so no bundler helpers leak into the browser.
|
|
@@ -81,6 +102,11 @@ const FRAME_SCRIPT = `(() => {
|
|
|
81
102
|
|
|
82
103
|
for (var index = 0; index < nodes.length; index += 1) {
|
|
83
104
|
var node = nodes[index];
|
|
105
|
+
/* The subtree an effect surface is photographing is deliberately parked
|
|
106
|
+
outside the frame. It is the input to a canvas, not something a viewer
|
|
107
|
+
ever sees, so judging its position or its type size is judging the
|
|
108
|
+
wrong thing. */
|
|
109
|
+
if (node.closest("[data-odori-capture]")) continue;
|
|
84
110
|
var box = node.getBoundingClientRect();
|
|
85
111
|
if (box.width === 0 || box.height === 0) continue;
|
|
86
112
|
var style = getComputedStyle(node);
|
|
@@ -211,7 +237,12 @@ const testVideo = async (
|
|
|
211
237
|
for (const canvas of canvases) {
|
|
212
238
|
const second = again.find((item) => item.index === canvas.index);
|
|
213
239
|
if (canvas.blank) {
|
|
214
|
-
failures.push({
|
|
240
|
+
failures.push({
|
|
241
|
+
video: id,
|
|
242
|
+
message: canvas.gl
|
|
243
|
+
? `Frame ${frame}: canvas ${canvas.index} read back empty. A WebGL drawing buffer is cleared once it has been composited, so ask for the context with {preserveDrawingBuffer: true}.`
|
|
244
|
+
: `Frame ${frame}: canvas ${canvas.index} drew nothing.`,
|
|
245
|
+
});
|
|
215
246
|
continue;
|
|
216
247
|
}
|
|
217
248
|
if (canvas.hash === "tainted") {
|
package/src/render.ts
CHANGED
|
@@ -35,6 +35,23 @@ const renderUrl = (origin: string, target: RenderTarget, frame: number) => {
|
|
|
35
35
|
return `${origin}/?${params.toString()}`;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Flags the render browser is launched with.
|
|
40
|
+
*
|
|
41
|
+
* Headless Chrome has no GPU, so a WebGL context is simply refused and
|
|
42
|
+
* `getContext("webgl2")` returns null: a video that post-processes through a
|
|
43
|
+
* shader renders black in the export and nowhere else, which is the worst
|
|
44
|
+
* shape a bug can take. This turns on ANGLE's software backend, which draws
|
|
45
|
+
* the same picture on any machine and is therefore the right one for a
|
|
46
|
+
* renderer that has to agree with itself across parallel workers.
|
|
47
|
+
*
|
|
48
|
+
* Measured against the pinned build rather than guessed. The combination that
|
|
49
|
+
* reads as obvious, `--use-gl=angle --use-angle=swiftshader`, leaves WebGL2
|
|
50
|
+
* off; this flag alone turns it on. Nothing changes for a video that never
|
|
51
|
+
* asks for a GL context.
|
|
52
|
+
*/
|
|
53
|
+
export const BROWSER_ARGS = ["--enable-unsafe-swiftshader"];
|
|
54
|
+
|
|
38
55
|
export type RenderPage = {browser: Browser; page: Page; errors: string[]};
|
|
39
56
|
|
|
40
57
|
/** Open one video in render mode and wait for its first frame to mount. */
|
|
@@ -44,7 +61,7 @@ export const openRenderPage = async (
|
|
|
44
61
|
config: ResolvedConfig,
|
|
45
62
|
): Promise<RenderPage> => {
|
|
46
63
|
const executablePath = await browserExecutable(config);
|
|
47
|
-
const browser = await chromium.launch({executablePath, headless: true});
|
|
64
|
+
const browser = await chromium.launch({executablePath, headless: true, args: BROWSER_ARGS});
|
|
48
65
|
const page = await browser.newPage({
|
|
49
66
|
viewport: {width: target.width, height: target.height},
|
|
50
67
|
deviceScaleFactor: 1,
|