create-three-blocks-starter 0.5.6 → 0.6.0-alpha.402.g6a35bb19fa05
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.md +35 -0
- package/dist/artifacts.d.ts +1 -61
- package/dist/artifacts.js +3 -535
- package/dist/catalog.js +5 -5
- package/dist/compose.js +7 -38
- package/dist/constants.js +2 -2
- package/dist/generate.js +1 -1
- package/dist/layers.js +3 -5
- package/package.json +5 -1
- package/templates/core/gitignore +1 -0
- package/templates/core/src/assets.ts +1 -1
- package/templates/core/src/render.worker.ts +17 -8
- package/templates/core/src/style.css +1 -1
- package/templates/scene/src/scene.ts +0 -3
- package/templates/text/src/scene.ts +0 -3
- package/dist/shader-inputs.d.ts +0 -6
- package/dist/shader-inputs.js +0 -184
- package/template-assets/shaders/scene/meta.json +0 -74
- package/template-assets/shaders/scene/scene.webgl.ts +0 -2138
- package/template-assets/shaders/scene/scene.webgpu.ts +0 -1236
- package/template-assets/shaders/scene/timing.json +0 -28
- package/template-assets/shaders/text/meta.json +0 -74
- package/template-assets/shaders/text/text.webgl.ts +0 -3650
- package/template-assets/shaders/text/text.webgpu.ts +0 -2898
- package/template-assets/shaders/text/timing.json +0 -28
- package/templates/core/.gitattributes +0 -1
- package/templates/core/src/shaders.d.ts +0 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# create-three-blocks-starter
|
|
2
2
|
|
|
3
|
+
## 0.6.0-alpha.402.g6a35bb19fa05
|
|
4
|
+
|
|
5
|
+
- Library: Three.js r186 is supported, and development always compiles shaders live until previewed.
|
|
6
|
+
|
|
7
|
+
- Library: Vite builds capture precompiled shaders from the built bundle; nothing to commit.
|
|
8
|
+
|
|
9
|
+
- Devtools: The shader watch command, the 186dev Three.js lane and Playwright driver names are gone.
|
|
10
|
+
|
|
11
|
+
- Starter: New projects ship no generated shader files; the build captures them.
|
|
12
|
+
|
|
13
|
+
- CLI: One check command verifies types, the production build and every scene, with a PNG per scene.
|
|
14
|
+
|
|
15
|
+
- Devtools: Development names the shaders that compile after the first frame, once per burst.
|
|
16
|
+
|
|
17
|
+
- Library: Ambient occlusion compiles again with antialiasing on Three.js r186.
|
|
18
|
+
|
|
19
|
+
- Devtools: Interrupted captures and checks no longer leave renderer processes running.
|
|
20
|
+
|
|
21
|
+
- Devtools: Captures and checks deliver pointer and wheel input to listeners on the window.
|
|
22
|
+
|
|
23
|
+
- Starter: Add a scene by adding a folder; the build captures it and the check command boots it.
|
|
24
|
+
|
|
25
|
+
- CLI: JSON output of devtools commands no longer starts with a banner line.
|
|
26
|
+
|
|
27
|
+
- Devtools: Precompiled temporal antialiasing and upscaling jitter the camera again.
|
|
28
|
+
|
|
29
|
+
- Library: Ambient occlusion loads precompiled on WebGPU instead of compiling at startup.
|
|
30
|
+
|
|
31
|
+
- Website: The shader precompilation benchmark now measures the official Three.js r186 examples.
|
|
32
|
+
|
|
33
|
+
- Library: Precompiled ambient occlusion no longer crashes on its first frame on Three.js r186.
|
|
34
|
+
|
|
35
|
+
- Packages: Built from `6a35bb19fa05`.
|
|
36
|
+
|
|
37
|
+
|
|
3
38
|
## 0.5.6
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/dist/artifacts.d.ts
CHANGED
|
@@ -1,65 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
declare const SHADER_BACKENDS: readonly ["webgpu", "webgl"];
|
|
3
|
-
type ShaderBackend = (typeof SHADER_BACKENDS)[number];
|
|
4
|
-
declare const SHADER_COMPATIBILITIES: {
|
|
5
|
-
readonly webgpu: "three-webgpu-r185-v1";
|
|
6
|
-
readonly webgl: "three-webgl-r185-v1";
|
|
7
|
-
};
|
|
8
|
-
declare const SHADER_ADDRESS_VERSION: 2;
|
|
9
|
-
declare const SHADER_RECIPE_VERSION: 1;
|
|
10
|
-
declare const SHADER_HYDRATION_VERSION: 2;
|
|
11
|
-
declare const SHADER_TRANSFORM_VERSION: 64;
|
|
12
|
-
interface ShaderCoverage {
|
|
13
|
-
readonly render: number;
|
|
14
|
-
readonly vertex: number;
|
|
15
|
-
readonly fragment: number;
|
|
16
|
-
readonly compute: number;
|
|
17
|
-
}
|
|
18
|
-
interface ShaderSceneReceipt {
|
|
19
|
-
readonly backend: ShaderBackend;
|
|
20
|
-
readonly artifact: string;
|
|
21
|
-
readonly artifactHash: string;
|
|
22
|
-
readonly asset: string;
|
|
23
|
-
readonly assetHash: string;
|
|
24
|
-
readonly bytes: number;
|
|
25
|
-
readonly inputs: Readonly<Record<string, string>>;
|
|
26
|
-
readonly coverage: ShaderCoverage;
|
|
27
|
-
readonly skipped: {
|
|
28
|
-
readonly render: number;
|
|
29
|
-
readonly compute: number;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
interface ShaderReceipt {
|
|
33
|
-
readonly schemaVersion: 2;
|
|
34
|
-
readonly runtime: Pick<CompatibilityVersions, "three" | "threeBlocks"> & {
|
|
35
|
-
readonly threeBlocksShaderClosure: string;
|
|
36
|
-
};
|
|
37
|
-
readonly tool: {
|
|
38
|
-
readonly name: "@three-blocks/devtools";
|
|
39
|
-
readonly version: string;
|
|
40
|
-
readonly compatibility: (typeof SHADER_COMPATIBILITIES)[ShaderBackend];
|
|
41
|
-
readonly compatibilities: Readonly<Partial<Record<ShaderBackend, (typeof SHADER_COMPATIBILITIES)[ShaderBackend]>>>;
|
|
42
|
-
readonly transform: typeof SHADER_TRANSFORM_VERSION;
|
|
43
|
-
readonly address: typeof SHADER_ADDRESS_VERSION;
|
|
44
|
-
readonly recipe: typeof SHADER_RECIPE_VERSION;
|
|
45
|
-
readonly hydration: typeof SHADER_HYDRATION_VERSION;
|
|
46
|
-
};
|
|
47
|
-
readonly scenes: Readonly<Record<string, ShaderSceneReceipt>>;
|
|
48
|
-
}
|
|
49
|
-
interface LoadedShaderArtifact {
|
|
50
|
-
readonly status: ArtifactStatus;
|
|
51
|
-
readonly files: readonly GeneratedFile[];
|
|
52
|
-
readonly note: string;
|
|
53
|
-
}
|
|
54
|
-
/** Validate the exact pair emitted by public shader capture before it can become a fresh catalog payload. */
|
|
55
|
-
export declare function validateShaderArtifactPayload(template: TemplateName, manifest: string | Uint8Array, metadata: string | Uint8Array, versions?: Pick<CompatibilityVersions, "three" | "threeBlocks">, backend?: ShaderBackend): ShaderReceipt;
|
|
56
|
-
export declare function shaderArtifactPaths(template: TemplateName): readonly string[];
|
|
57
|
-
export declare function shaderArtifactFiles(template: TemplateName): readonly GeneratedFile[];
|
|
58
|
-
export declare function shaderArtifactState(template: TemplateName): Pick<LoadedShaderArtifact, "status" | "note">;
|
|
59
|
-
/** Prove that a fresh payload was captured from the exact fully composed generated template. */
|
|
60
|
-
export declare function validateShaderArtifactProject(template: TemplateName, files: ReadonlyMap<string, GeneratedFile>, versions: Pick<CompatibilityVersions, "three" | "threeBlocks">): void;
|
|
1
|
+
import type { GeneratedFile } from "./types.js";
|
|
61
2
|
export declare function textArtifactFiles(): readonly GeneratedFile[];
|
|
62
3
|
export declare const textArtifactPaths: readonly string[];
|
|
63
4
|
export declare function environmentArtifactFiles(): readonly GeneratedFile[];
|
|
64
5
|
export declare const environmentArtifactPaths: readonly string[];
|
|
65
|
-
export {};
|
package/dist/artifacts.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { COMPATIBILITY_VERSIONS } from "./constants.js";
|
|
5
|
-
import { TEMPLATE_NAMES, } from "./types.js";
|
|
6
|
-
import { snapshotGeneratedShaderInputs } from "./shader-inputs.js";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
// Shaders are not here on purpose: `vite build` captures the built bundle and writes its
|
|
3
|
+
// manifests next to it, so a scaffold carries no generated shader file to keep fresh.
|
|
7
4
|
const TEXT_FILES = [
|
|
8
5
|
"public/fonts/OFL-1.1.txt",
|
|
9
6
|
"public/fonts/geist.woff2",
|
|
@@ -17,535 +14,6 @@ const TEXT_FILES = [
|
|
|
17
14
|
"public/fonts/noto-jp.msdf.ktx2",
|
|
18
15
|
"public/fonts/noto-jp.msdf.meta.json",
|
|
19
16
|
];
|
|
20
|
-
const SHADER_BACKENDS = ["webgpu", "webgl"];
|
|
21
|
-
const SHADER_COMPATIBILITIES = {
|
|
22
|
-
webgpu: "three-webgpu-r185-v1",
|
|
23
|
-
webgl: "three-webgl-r185-v1",
|
|
24
|
-
};
|
|
25
|
-
const SHADER_ADDRESS_VERSION = 2;
|
|
26
|
-
const SHADER_RECIPE_VERSION = 1;
|
|
27
|
-
const SHADER_HYDRATION_VERSION = 2;
|
|
28
|
-
const SHADER_TRANSFORM_VERSION = 64;
|
|
29
|
-
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
30
|
-
const SAFE_VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/u;
|
|
31
|
-
const MAX_MANIFEST_BYTES = 32 * 1_024 * 1_024;
|
|
32
|
-
const MAX_RECEIPT_BYTES = 2 * 1_024 * 1_024;
|
|
33
|
-
const MAX_TIMING_BYTES = 1_024 * 1_024;
|
|
34
|
-
const MAX_INPUTS = 4_096;
|
|
35
|
-
const MAX_ENTRIES = 4_096;
|
|
36
|
-
function isRecord(value) {
|
|
37
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
-
}
|
|
39
|
-
function bytes(content) {
|
|
40
|
-
return typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
41
|
-
}
|
|
42
|
-
function text(content, label) {
|
|
43
|
-
try {
|
|
44
|
-
return typeof content === "string"
|
|
45
|
-
? content
|
|
46
|
-
: new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
throw new Error(`${label} must be valid UTF-8.`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function sha256(content) {
|
|
53
|
-
return createHash("sha256").update(bytes(content)).digest("hex");
|
|
54
|
-
}
|
|
55
|
-
function shaderReceiptKey(template, backend) {
|
|
56
|
-
return backend === "webgpu" ? template : `${template}.webgl`;
|
|
57
|
-
}
|
|
58
|
-
function nonNegativeInteger(value, label) {
|
|
59
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
60
|
-
throw new Error(`${label} must be a non-negative integer.`);
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
function safeRelativePath(value, label) {
|
|
65
|
-
if (typeof value !== "string" ||
|
|
66
|
-
value.length === 0 ||
|
|
67
|
-
value.includes("\\") ||
|
|
68
|
-
path.posix.isAbsolute(value) ||
|
|
69
|
-
path.posix.normalize(value) !== value ||
|
|
70
|
-
value === ".." ||
|
|
71
|
-
value.startsWith("../") ||
|
|
72
|
-
/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
73
|
-
throw new Error(`${label} must be a safe project-relative path.`);
|
|
74
|
-
}
|
|
75
|
-
return value;
|
|
76
|
-
}
|
|
77
|
-
function coverage(value, label) {
|
|
78
|
-
if (!isRecord(value))
|
|
79
|
-
throw new Error(`${label} is missing.`);
|
|
80
|
-
return {
|
|
81
|
-
render: nonNegativeInteger(value.render, `${label}.render`),
|
|
82
|
-
vertex: nonNegativeInteger(value.vertex, `${label}.vertex`),
|
|
83
|
-
fragment: nonNegativeInteger(value.fragment, `${label}.fragment`),
|
|
84
|
-
compute: nonNegativeInteger(value.compute, `${label}.compute`),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
function parseInputs(value, template) {
|
|
88
|
-
if (!isRecord(value))
|
|
89
|
-
throw new Error(`Shader receipt inputs for "${template}" are invalid.`);
|
|
90
|
-
const entries = Object.entries(value);
|
|
91
|
-
if (entries.length === 0 || entries.length > MAX_INPUTS) {
|
|
92
|
-
throw new Error(`Shader receipt inputs for "${template}" must contain 1...${MAX_INPUTS} files.`);
|
|
93
|
-
}
|
|
94
|
-
const result = {};
|
|
95
|
-
for (const [inputPath, hash] of entries) {
|
|
96
|
-
safeRelativePath(inputPath, `Shader receipt input for "${template}"`);
|
|
97
|
-
if (typeof hash !== "string" || !SHA256.test(hash))
|
|
98
|
-
throw new Error(`Shader receipt input hash for "${template}" is invalid.`);
|
|
99
|
-
result[inputPath] = hash;
|
|
100
|
-
}
|
|
101
|
-
return result;
|
|
102
|
-
}
|
|
103
|
-
function parseReceipt(template, content, versions) {
|
|
104
|
-
if (bytes(content).byteLength > MAX_RECEIPT_BYTES)
|
|
105
|
-
throw new Error(`Shader metadata for "${template}" exceeds ${MAX_RECEIPT_BYTES} bytes.`);
|
|
106
|
-
let value;
|
|
107
|
-
try {
|
|
108
|
-
value = JSON.parse(text(content, `Shader metadata for "${template}"`));
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
if (error instanceof Error &&
|
|
112
|
-
error.message.endsWith("must be valid UTF-8."))
|
|
113
|
-
throw error;
|
|
114
|
-
throw new Error(`Shader metadata for "${template}" is not valid JSON.`);
|
|
115
|
-
}
|
|
116
|
-
if (!isRecord(value) ||
|
|
117
|
-
value.schemaVersion !== 2 ||
|
|
118
|
-
!isRecord(value.runtime) ||
|
|
119
|
-
value.runtime.three !== versions.three ||
|
|
120
|
-
typeof value.runtime.threeBlocks !== "string" ||
|
|
121
|
-
!SAFE_VERSION.test(value.runtime.threeBlocks)) {
|
|
122
|
-
throw new Error(`Shader metadata for "${template}" has an incompatible schema or runtime version.`);
|
|
123
|
-
}
|
|
124
|
-
if (typeof value.runtime.threeBlocksShaderClosure !== "string" ||
|
|
125
|
-
!SHA256.test(value.runtime.threeBlocksShaderClosure)) {
|
|
126
|
-
throw new Error(`Shader metadata for "${template}" has an invalid three-blocks shader runtime closure.`);
|
|
127
|
-
}
|
|
128
|
-
if (!isRecord(value.tool) ||
|
|
129
|
-
value.tool.name !== "@three-blocks/devtools" ||
|
|
130
|
-
typeof value.tool.version !== "string" ||
|
|
131
|
-
!SAFE_VERSION.test(value.tool.version) ||
|
|
132
|
-
(value.tool.compatibility !== SHADER_COMPATIBILITIES.webgpu &&
|
|
133
|
-
value.tool.compatibility !== SHADER_COMPATIBILITIES.webgl) ||
|
|
134
|
-
value.tool.transform !== SHADER_TRANSFORM_VERSION ||
|
|
135
|
-
value.tool.address !== SHADER_ADDRESS_VERSION ||
|
|
136
|
-
value.tool.recipe !== SHADER_RECIPE_VERSION ||
|
|
137
|
-
value.tool.hydration !== SHADER_HYDRATION_VERSION) {
|
|
138
|
-
throw new Error(`Shader metadata for "${template}" has incompatible public tool metadata.`);
|
|
139
|
-
}
|
|
140
|
-
if (value.tool.compatibilities !== undefined &&
|
|
141
|
-
(!isRecord(value.tool.compatibilities) ||
|
|
142
|
-
Object.entries(value.tool.compatibilities).some(([backend, compatibility]) => (backend !== "webgpu" && backend !== "webgl") ||
|
|
143
|
-
compatibility !==
|
|
144
|
-
SHADER_COMPATIBILITIES[backend]))) {
|
|
145
|
-
throw new Error(`Shader metadata for "${template}" has incompatible backend metadata.`);
|
|
146
|
-
}
|
|
147
|
-
const rawScenes = isRecord(value.scenes) ? value.scenes : undefined;
|
|
148
|
-
const sceneKeys = rawScenes === undefined ? [] : Object.keys(rawScenes);
|
|
149
|
-
const allowedSceneKeys = new Set(SHADER_BACKENDS.map((backend) => shaderReceiptKey(template, backend)));
|
|
150
|
-
if (rawScenes === undefined ||
|
|
151
|
-
sceneKeys.length === 0 ||
|
|
152
|
-
sceneKeys.length > SHADER_BACKENDS.length ||
|
|
153
|
-
sceneKeys.some((key) => !allowedSceneKeys.has(key) || !isRecord(rawScenes[key]))) {
|
|
154
|
-
throw new Error(`Shader metadata for "${template}" must contain only that template's backend scenes.`);
|
|
155
|
-
}
|
|
156
|
-
const scenes = {};
|
|
157
|
-
for (const key of sceneKeys) {
|
|
158
|
-
const backend = key.endsWith(".webgl")
|
|
159
|
-
? "webgl"
|
|
160
|
-
: "webgpu";
|
|
161
|
-
const raw = rawScenes[key];
|
|
162
|
-
const expectedArtifact = `${template}.${backend}.ts`;
|
|
163
|
-
const expectedAsset = `three-blocks/shaders/${template}.${backend}.json`;
|
|
164
|
-
if (raw.backend !== undefined &&
|
|
165
|
-
raw.backend !== backend) {
|
|
166
|
-
throw new Error(`Shader receipt for "${key}" names the wrong backend.`);
|
|
167
|
-
}
|
|
168
|
-
if (raw.artifact !== expectedArtifact ||
|
|
169
|
-
typeof raw.artifactHash !== "string" ||
|
|
170
|
-
!SHA256.test(raw.artifactHash)) {
|
|
171
|
-
throw new Error(`Shader receipt for "${key}" references an invalid artifact.`);
|
|
172
|
-
}
|
|
173
|
-
if (raw.asset !== expectedAsset ||
|
|
174
|
-
typeof raw.assetHash !== "string" ||
|
|
175
|
-
!SHA256.test(raw.assetHash)) {
|
|
176
|
-
throw new Error(`Shader receipt for "${key}" references an invalid public asset.`);
|
|
177
|
-
}
|
|
178
|
-
const selectedCoverage = coverage(raw.coverage, `Shader coverage for "${key}"`);
|
|
179
|
-
if (selectedCoverage.render === 0 ||
|
|
180
|
-
selectedCoverage.vertex === 0 ||
|
|
181
|
-
selectedCoverage.fragment === 0) {
|
|
182
|
-
throw new Error(`Shader capture for "${key}" has no complete render pipeline coverage.`);
|
|
183
|
-
}
|
|
184
|
-
if (!isRecord(raw.skipped))
|
|
185
|
-
throw new Error(`Shader skipped-build counts for "${key}" are missing.`);
|
|
186
|
-
scenes[key] = {
|
|
187
|
-
backend,
|
|
188
|
-
artifact: expectedArtifact,
|
|
189
|
-
artifactHash: raw.artifactHash,
|
|
190
|
-
asset: expectedAsset,
|
|
191
|
-
assetHash: raw.assetHash,
|
|
192
|
-
bytes: nonNegativeInteger(raw.bytes, `Shader artifact bytes for "${key}"`),
|
|
193
|
-
inputs: parseInputs(raw.inputs, template),
|
|
194
|
-
coverage: selectedCoverage,
|
|
195
|
-
skipped: {
|
|
196
|
-
render: nonNegativeInteger(raw.skipped.render, `Shader skipped render builds for "${key}"`),
|
|
197
|
-
compute: nonNegativeInteger(raw.skipped.compute, `Shader skipped compute builds for "${key}"`),
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
schemaVersion: 2,
|
|
203
|
-
runtime: {
|
|
204
|
-
three: versions.three,
|
|
205
|
-
threeBlocks: value.runtime.threeBlocks,
|
|
206
|
-
threeBlocksShaderClosure: value.runtime.threeBlocksShaderClosure,
|
|
207
|
-
},
|
|
208
|
-
tool: {
|
|
209
|
-
name: "@three-blocks/devtools",
|
|
210
|
-
version: value.tool.version,
|
|
211
|
-
compatibility: value.tool.compatibility,
|
|
212
|
-
compatibilities: isRecord(value.tool.compatibilities)
|
|
213
|
-
? Object.fromEntries(Object.entries(value.tool.compatibilities).filter(([backend]) => backend === "webgpu" || backend === "webgl"))
|
|
214
|
-
: {},
|
|
215
|
-
transform: SHADER_TRANSFORM_VERSION,
|
|
216
|
-
address: SHADER_ADDRESS_VERSION,
|
|
217
|
-
recipe: SHADER_RECIPE_VERSION,
|
|
218
|
-
hydration: SHADER_HYDRATION_VERSION,
|
|
219
|
-
},
|
|
220
|
-
scenes,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
function parseManifestObject(template, content) {
|
|
224
|
-
const manifestBytes = bytes(content);
|
|
225
|
-
if (manifestBytes.byteLength === 0 ||
|
|
226
|
-
manifestBytes.byteLength > MAX_MANIFEST_BYTES) {
|
|
227
|
-
throw new Error(`Shader manifest for "${template}" must contain 1...${MAX_MANIFEST_BYTES} bytes.`);
|
|
228
|
-
}
|
|
229
|
-
const source = text(content, `Shader manifest for "${template}"`);
|
|
230
|
-
const exportDefault = source.indexOf("export default");
|
|
231
|
-
const firstBrace = source.indexOf("{", exportDefault);
|
|
232
|
-
const lastBrace = source.lastIndexOf("}");
|
|
233
|
-
if (exportDefault < 0 || firstBrace < 0 || lastBrace <= firstBrace) {
|
|
234
|
-
throw new Error(`Shader manifest for "${template}" is not a generated TypeScript manifest module.`);
|
|
235
|
-
}
|
|
236
|
-
for (const match of source.matchAll(/^\s*import(?:\s+type)?[\s\S]*?\sfrom\s+['"]([^'"]+)['"];?\s*$/gmu)) {
|
|
237
|
-
if (match[1] !== "three-blocks/shaders")
|
|
238
|
-
throw new Error(`Shader manifest for "${template}" imports a non-runtime module.`);
|
|
239
|
-
}
|
|
240
|
-
let parsed;
|
|
241
|
-
try {
|
|
242
|
-
parsed = JSON.parse(source.slice(firstBrace, lastBrace + 1));
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
throw new Error(`Shader manifest for "${template}" does not contain the deterministic JSON payload emitted by public devtools.`);
|
|
246
|
-
}
|
|
247
|
-
if (!isRecord(parsed))
|
|
248
|
-
throw new Error(`Shader manifest for "${template}" has no object payload.`);
|
|
249
|
-
return parsed;
|
|
250
|
-
}
|
|
251
|
-
function manifestCoverage(template, content, versions, backend, capturedThreeBlocks) {
|
|
252
|
-
const manifest = parseManifestObject(template, content);
|
|
253
|
-
if (manifest.version !== 3 ||
|
|
254
|
-
manifest.scene !== template ||
|
|
255
|
-
manifest.three !== versions.three ||
|
|
256
|
-
manifest.threeBlocks !== capturedThreeBlocks ||
|
|
257
|
-
!isRecord(manifest.runtime) ||
|
|
258
|
-
manifest.runtime.id !== SHADER_COMPATIBILITIES[backend] ||
|
|
259
|
-
manifest.runtime.address !== SHADER_ADDRESS_VERSION ||
|
|
260
|
-
manifest.runtime.recipe !== SHADER_RECIPE_VERSION ||
|
|
261
|
-
manifest.runtime.hydration !== SHADER_HYDRATION_VERSION ||
|
|
262
|
-
!isRecord(manifest.entries)) {
|
|
263
|
-
throw new Error(`Shader manifest for "${template}" has an incompatible scene, runtime, or schema.`);
|
|
264
|
-
}
|
|
265
|
-
const entries = Object.entries(manifest.entries);
|
|
266
|
-
if (entries.length === 0 || entries.length > MAX_ENTRIES)
|
|
267
|
-
throw new Error(`Shader manifest for "${template}" must contain 1...${MAX_ENTRIES} entries.`);
|
|
268
|
-
const states = Array.isArray(manifest.states) ? manifest.states : [];
|
|
269
|
-
let render = 0;
|
|
270
|
-
let vertex = 0;
|
|
271
|
-
let fragment = 0;
|
|
272
|
-
let compute = 0;
|
|
273
|
-
for (const [key, stateIndex] of entries) {
|
|
274
|
-
const state = typeof stateIndex === "number" ? states[stateIndex] : undefined;
|
|
275
|
-
if (!isRecord(state))
|
|
276
|
-
throw new Error(`Shader manifest entry "${key}" for "${template}" references an invalid state.`);
|
|
277
|
-
for (const stage of ["vertex", "fragment", "compute"]) {
|
|
278
|
-
if (state[stage] !== null && typeof state[stage] !== "number")
|
|
279
|
-
throw new Error(`Shader manifest entry "${key}" has an invalid ${stage} module reference.`);
|
|
280
|
-
}
|
|
281
|
-
const hasVertex = typeof state.vertex === "number";
|
|
282
|
-
const hasFragment = typeof state.fragment === "number";
|
|
283
|
-
const hasCompute = typeof state.compute === "number";
|
|
284
|
-
if (!hasVertex && !hasFragment && !hasCompute)
|
|
285
|
-
throw new Error(`Shader manifest entry "${key}" has no shader stage.`);
|
|
286
|
-
if (hasVertex || hasFragment)
|
|
287
|
-
render++;
|
|
288
|
-
if (hasVertex)
|
|
289
|
-
vertex++;
|
|
290
|
-
if (hasFragment)
|
|
291
|
-
fragment++;
|
|
292
|
-
if (hasCompute)
|
|
293
|
-
compute++;
|
|
294
|
-
}
|
|
295
|
-
return { render, vertex, fragment, compute };
|
|
296
|
-
}
|
|
297
|
-
function sameCoverage(left, right) {
|
|
298
|
-
return (left.render === right.render &&
|
|
299
|
-
left.vertex === right.vertex &&
|
|
300
|
-
left.fragment === right.fragment &&
|
|
301
|
-
left.compute === right.compute);
|
|
302
|
-
}
|
|
303
|
-
function manifestAsset(template, content) {
|
|
304
|
-
const source = text(content, `Shader manifest for "${template}"`);
|
|
305
|
-
return `${source.slice(source.indexOf("{", source.indexOf("export default")), source.lastIndexOf("}") + 1)}\n`;
|
|
306
|
-
}
|
|
307
|
-
/** Validate the exact pair emitted by public shader capture before it can become a fresh catalog payload. */
|
|
308
|
-
export function validateShaderArtifactPayload(template, manifest, metadata, versions = COMPATIBILITY_VERSIONS, backend = "webgpu") {
|
|
309
|
-
const receipt = parseReceipt(template, metadata, versions);
|
|
310
|
-
const key = shaderReceiptKey(template, backend);
|
|
311
|
-
const selected = receipt.scenes[key];
|
|
312
|
-
if (selected === undefined)
|
|
313
|
-
throw new Error(`Shader receipt for "${key}" is missing.`);
|
|
314
|
-
if (selected.artifactHash !== sha256(manifest))
|
|
315
|
-
throw new Error(`Shader manifest checksum for "${key}" does not match metadata.`);
|
|
316
|
-
if (selected.bytes !== bytes(manifest).byteLength)
|
|
317
|
-
throw new Error(`Shader manifest byte count for "${key}" does not match metadata.`);
|
|
318
|
-
if (!sameCoverage(manifestCoverage(template, manifest, versions, backend, receipt.runtime.threeBlocks), selected.coverage)) {
|
|
319
|
-
throw new Error(`Shader manifest coverage for "${key}" does not match metadata.`);
|
|
320
|
-
}
|
|
321
|
-
if (selected.assetHash !== sha256(manifestAsset(template, manifest))) {
|
|
322
|
-
throw new Error(`Shader public asset checksum for "${key}" does not match metadata.`);
|
|
323
|
-
}
|
|
324
|
-
return receipt;
|
|
325
|
-
}
|
|
326
|
-
function nonNegativeDuration(value, label) {
|
|
327
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
328
|
-
throw new Error(`${label} must be a non-negative duration.`);
|
|
329
|
-
}
|
|
330
|
-
return value;
|
|
331
|
-
}
|
|
332
|
-
function validateShaderTimingPayload(template, content, metadata, receipt) {
|
|
333
|
-
if (bytes(content).byteLength > MAX_TIMING_BYTES)
|
|
334
|
-
throw new Error(`Shader timing for "${template}" exceeds ${MAX_TIMING_BYTES} bytes.`);
|
|
335
|
-
let value;
|
|
336
|
-
try {
|
|
337
|
-
value = JSON.parse(text(content, `Shader timing for "${template}"`));
|
|
338
|
-
}
|
|
339
|
-
catch (error) {
|
|
340
|
-
if (error instanceof Error &&
|
|
341
|
-
error.message.endsWith("must be valid UTF-8."))
|
|
342
|
-
throw error;
|
|
343
|
-
throw new Error(`Shader timing for "${template}" is not valid JSON.`);
|
|
344
|
-
}
|
|
345
|
-
if (!isRecord(value) ||
|
|
346
|
-
value.schemaVersion !== 3 ||
|
|
347
|
-
value.source !== "committed-capture" ||
|
|
348
|
-
value.receiptHash !== sha256(metadata) ||
|
|
349
|
-
typeof value.measuredAt !== "string" ||
|
|
350
|
-
!Number.isFinite(Date.parse(value.measuredAt)) ||
|
|
351
|
-
typeof value.adapter !== "string" ||
|
|
352
|
-
value.adapter.length === 0 ||
|
|
353
|
-
value.adapter.length > 256 ||
|
|
354
|
-
typeof value.platform !== "string" ||
|
|
355
|
-
value.platform.length === 0 ||
|
|
356
|
-
value.platform.length > 256 ||
|
|
357
|
-
/[\u0000-\u001f\u007f]/u.test(value.adapter) ||
|
|
358
|
-
/[\u0000-\u001f\u007f]/u.test(value.platform) ||
|
|
359
|
-
!isRecord(value.scenes)) {
|
|
360
|
-
throw new Error(`Shader timing for "${template}" is invalid or does not match its receipt.`);
|
|
361
|
-
}
|
|
362
|
-
const timingScenes = value.scenes;
|
|
363
|
-
const keys = Object.keys(receipt.scenes).sort();
|
|
364
|
-
if (Object.keys(timingScenes).sort().join("\0") !== keys.join("\0") ||
|
|
365
|
-
keys.some((key) => !isRecord(timingScenes[key]))) {
|
|
366
|
-
throw new Error(`Shader timing for "${template}" does not cover its receipt matrix.`);
|
|
367
|
-
}
|
|
368
|
-
for (const key of keys) {
|
|
369
|
-
const selected = timingScenes[key];
|
|
370
|
-
const scene = receipt.scenes[key];
|
|
371
|
-
const expectedBuilds = scene.coverage.render + scene.coverage.compute;
|
|
372
|
-
const builds = nonNegativeInteger(selected.builds, `Shader timing builds for "${key}"`);
|
|
373
|
-
const runs = nonNegativeInteger(selected.runs, `Shader timing runs for "${key}"`);
|
|
374
|
-
const liveBuildMs = nonNegativeDuration(selected.liveBuildMs, `Shader live timing for "${key}"`);
|
|
375
|
-
const precompiledBuildMs = nonNegativeDuration(selected.precompiledBuildMs, `Shader precompiled timing for "${key}"`);
|
|
376
|
-
const avoidedBuildMs = nonNegativeDuration(selected.avoidedBuildMs, `Shader avoided timing for "${key}"`);
|
|
377
|
-
nonNegativeDuration(selected.spreadMs, `Shader timing spread for "${key}"`);
|
|
378
|
-
if (builds !== expectedBuilds ||
|
|
379
|
-
runs < 1 ||
|
|
380
|
-
Math.abs(avoidedBuildMs - Math.max(0, liveBuildMs - precompiledBuildMs)) > 0.001) {
|
|
381
|
-
throw new Error(`Shader timing for "${key}" does not match its captured pipelines.`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
export function shaderArtifactPaths(template) {
|
|
386
|
-
return [
|
|
387
|
-
`.three-blocks/shaders/${template}.webgpu.ts`,
|
|
388
|
-
".three-blocks/shaders/meta.json",
|
|
389
|
-
".three-blocks/shaders/timing.json",
|
|
390
|
-
`public/three-blocks/shaders/${template}.webgpu.json`,
|
|
391
|
-
`.three-blocks/shaders/${template}.webgl.ts`,
|
|
392
|
-
`public/three-blocks/shaders/${template}.webgl.json`,
|
|
393
|
-
];
|
|
394
|
-
}
|
|
395
|
-
function loadShaderArtifact(template) {
|
|
396
|
-
const directory = new URL(`../template-assets/shaders/${template}/`, import.meta.url);
|
|
397
|
-
if (!existsSync(directory))
|
|
398
|
-
return {
|
|
399
|
-
status: "pending",
|
|
400
|
-
files: [],
|
|
401
|
-
note: "Pending verified shader capture; runtime behavior must not treat this descriptor as fresh.",
|
|
402
|
-
};
|
|
403
|
-
const directoryMetadata = lstatSync(directory);
|
|
404
|
-
if (directoryMetadata.isSymbolicLink() || !directoryMetadata.isDirectory())
|
|
405
|
-
throw new Error(`Shader template asset root for "${template}" must be a real directory.`);
|
|
406
|
-
const expected = [
|
|
407
|
-
`${template}.webgpu.ts`,
|
|
408
|
-
`${template}.webgl.ts`,
|
|
409
|
-
"meta.json",
|
|
410
|
-
"timing.json",
|
|
411
|
-
];
|
|
412
|
-
const entries = readdirSync(directory, { withFileTypes: true });
|
|
413
|
-
if (entries.length === 0)
|
|
414
|
-
return {
|
|
415
|
-
status: "pending",
|
|
416
|
-
files: [],
|
|
417
|
-
note: "Pending verified shader capture; runtime behavior must not treat this descriptor as fresh.",
|
|
418
|
-
};
|
|
419
|
-
const unexpected = entries
|
|
420
|
-
.filter((entry) => !expected.includes(entry.name))
|
|
421
|
-
.map((entry) => entry.name);
|
|
422
|
-
if (unexpected.length > 0)
|
|
423
|
-
throw new Error(`Shader payload for "${template}" contains non-allowlisted files: ${unexpected.join(", ")}.`);
|
|
424
|
-
for (const expectedFile of expected) {
|
|
425
|
-
const entry = entries.find((candidate) => candidate.name === expectedFile);
|
|
426
|
-
if (entry === undefined || entry.isSymbolicLink() || !entry.isFile())
|
|
427
|
-
throw new Error(`Shader payload for "${template}" is incomplete: expected ${expected.join(" and ")}.`);
|
|
428
|
-
}
|
|
429
|
-
const webgpuManifest = readFileSync(new URL(`${template}.webgpu.ts`, directory));
|
|
430
|
-
const webglManifest = readFileSync(new URL(`${template}.webgl.ts`, directory));
|
|
431
|
-
const metadata = readFileSync(new URL("meta.json", directory));
|
|
432
|
-
const timing = readFileSync(new URL("timing.json", directory));
|
|
433
|
-
const metadataValue = JSON.parse(text(metadata, `Shader metadata for "${template}"`));
|
|
434
|
-
if (isRecord(metadataValue) &&
|
|
435
|
-
isRecord(metadataValue.runtime) &&
|
|
436
|
-
metadataValue.runtime.threeBlocksShaderClosure === undefined) {
|
|
437
|
-
return {
|
|
438
|
-
status: "pending",
|
|
439
|
-
files: [],
|
|
440
|
-
note: "Existing capture predates shader-relevant closure receipts and must be recaptured.",
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
const receipt = validateShaderArtifactPayload(template, webgpuManifest, metadata);
|
|
444
|
-
validateShaderArtifactPayload(template, webglManifest, metadata, COMPATIBILITY_VERSIONS, "webgl");
|
|
445
|
-
validateShaderTimingPayload(template, timing, metadata, receipt);
|
|
446
|
-
const selected = receipt.scenes[template];
|
|
447
|
-
const selectedWebGL = receipt.scenes[`${template}.webgl`];
|
|
448
|
-
if (selected === undefined)
|
|
449
|
-
throw new Error(`Shader receipt for "${template}" is missing.`);
|
|
450
|
-
if (selectedWebGL === undefined)
|
|
451
|
-
throw new Error(`Shader receipt for "${template}.webgl" is missing.`);
|
|
452
|
-
const targets = shaderArtifactPaths(template);
|
|
453
|
-
return {
|
|
454
|
-
status: "fresh",
|
|
455
|
-
files: [
|
|
456
|
-
{
|
|
457
|
-
path: targets[0],
|
|
458
|
-
content: webgpuManifest,
|
|
459
|
-
owner: "generator",
|
|
460
|
-
},
|
|
461
|
-
{ path: targets[1], content: metadata, owner: "generator" },
|
|
462
|
-
{ path: targets[2], content: timing, owner: "generator" },
|
|
463
|
-
{
|
|
464
|
-
path: targets[3],
|
|
465
|
-
content: manifestAsset(template, webgpuManifest),
|
|
466
|
-
owner: "generator",
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
path: targets[4],
|
|
470
|
-
content: webglManifest,
|
|
471
|
-
owner: "generator",
|
|
472
|
-
},
|
|
473
|
-
{
|
|
474
|
-
path: targets[5],
|
|
475
|
-
content: manifestAsset(template, webglManifest),
|
|
476
|
-
owner: "generator",
|
|
477
|
-
},
|
|
478
|
-
],
|
|
479
|
-
note: `Captured complete ${template} template for WebGPU and WebGL.`,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
const SHADER_ARTIFACTS = Object.freeze(Object.fromEntries(TEMPLATE_NAMES.map((template) => [
|
|
483
|
-
template,
|
|
484
|
-
loadShaderArtifact(template),
|
|
485
|
-
])));
|
|
486
|
-
export function shaderArtifactFiles(template) {
|
|
487
|
-
return SHADER_ARTIFACTS[template].files;
|
|
488
|
-
}
|
|
489
|
-
export function shaderArtifactState(template) {
|
|
490
|
-
const artifact = SHADER_ARTIFACTS[template];
|
|
491
|
-
return { status: artifact.status, note: artifact.note };
|
|
492
|
-
}
|
|
493
|
-
/** Prove that a fresh payload was captured from the exact fully composed generated template. */
|
|
494
|
-
export function validateShaderArtifactProject(template, files, versions) {
|
|
495
|
-
const targets = shaderArtifactPaths(template);
|
|
496
|
-
const manifest = files.get(targets[0]);
|
|
497
|
-
const metadata = files.get(targets[1]);
|
|
498
|
-
const timing = files.get(targets[2]);
|
|
499
|
-
const asset = files.get(targets[3]);
|
|
500
|
-
const webglManifest = files.get(targets[4]);
|
|
501
|
-
const webglAsset = files.get(targets[5]);
|
|
502
|
-
if (manifest === undefined ||
|
|
503
|
-
metadata === undefined ||
|
|
504
|
-
timing === undefined ||
|
|
505
|
-
asset === undefined ||
|
|
506
|
-
webglManifest === undefined ||
|
|
507
|
-
webglAsset === undefined) {
|
|
508
|
-
throw new Error(`Fresh shader artifact for "${template}" is missing its allowlisted manifest, metadata, timing, or public asset.`);
|
|
509
|
-
}
|
|
510
|
-
const receipt = validateShaderArtifactPayload(template, manifest.content, metadata.content, versions);
|
|
511
|
-
validateShaderArtifactPayload(template, webglManifest.content, metadata.content, versions, "webgl");
|
|
512
|
-
validateShaderTimingPayload(template, timing.content, metadata.content, receipt);
|
|
513
|
-
const selected = receipt.scenes[template];
|
|
514
|
-
const selectedWebGL = receipt.scenes[`${template}.webgl`];
|
|
515
|
-
if (selected === undefined)
|
|
516
|
-
throw new Error(`Shader receipt for "${template}" is missing.`);
|
|
517
|
-
if (selectedWebGL === undefined)
|
|
518
|
-
throw new Error(`Shader receipt for "${template}.webgl" is missing.`);
|
|
519
|
-
if (selected.assetHash !== sha256(asset.content)) {
|
|
520
|
-
throw new Error(`Fresh shader public asset for "${template}" does not match its receipt.`);
|
|
521
|
-
}
|
|
522
|
-
if (selectedWebGL.assetHash !== sha256(webglAsset.content)) {
|
|
523
|
-
throw new Error(`Fresh shader public asset for "${template}.webgl" does not match its receipt.`);
|
|
524
|
-
}
|
|
525
|
-
const current = snapshotGeneratedShaderInputs(files);
|
|
526
|
-
// Receipts hash the exact composed source bytes. Keep hash-relevant inputs
|
|
527
|
-
// ASCII so character representation stays explicit and stable across editors,
|
|
528
|
-
// archives, and capture runtimes; write other characters as source escapes.
|
|
529
|
-
for (const input of Object.keys(current)) {
|
|
530
|
-
const file = files.get(input);
|
|
531
|
-
if (file === undefined)
|
|
532
|
-
continue;
|
|
533
|
-
const content = typeof file.content === "string"
|
|
534
|
-
? Buffer.from(file.content)
|
|
535
|
-
: file.content;
|
|
536
|
-
if (content.some((byte) => byte > 0x7f)) {
|
|
537
|
-
throw new Error(`Shader semantic input "${input}" for "${template}" contains non-ASCII bytes; embed them as escape sequences so composition is runtime-independent.`);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
const recorded = selected.inputs;
|
|
541
|
-
const changed = Object.keys(current).filter((input) => recorded[input] !== current[input]);
|
|
542
|
-
const removed = Object.keys(recorded).filter((input) => !Object.hasOwn(current, input));
|
|
543
|
-
if (changed.length > 0 || removed.length > 0) {
|
|
544
|
-
throw new Error(`Fresh shader artifact for "${template}" does not match its composed semantic inputs` +
|
|
545
|
-
` (${changed.length} added/changed: ${changed.join(", ") || "none"};` +
|
|
546
|
-
` ${removed.length} removed: ${removed.join(", ") || "none"}).`);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
17
|
function textPayload(relativePath) {
|
|
550
18
|
return {
|
|
551
19
|
path: relativePath,
|