Skip to content

Commit caab6fc

Browse files
committed
feat(shaders): detect stale WGSL twins by hashing the authored GLSL
A generated WGSL twin was only ever (re)generated when MISSING — edit the GLSL and the committed/generated twin silently went stale, so WebGPU kept rendering the old shader and CI --check (presence-only) waved it through. That undermined the contract that GLSL is the single source and the WGSL twin is a derivative. Make the derivation verifiable: each generated `wgsl full` twin now stores `// source-hash: <hash>` of the authored GLSL (everything but the generated sections, trimmed so generate→check round-trips exactly). needsTwin()/ sourceHash() are exported, pure and engine-free, so one definition of "stale" is shared by: - the CLI + CI --check (a stale or hash-less twin now reports would-generate), - the editor's on-open generation (regenerates stale twins, not just missing). Hand-authored `wgsl` twins (builtins) and `#pragma switch` shaders are left alone. All committed generated twins gain their source-hash (WGSL bytes unchanged — deterministic; +2 lines each). Extends the CI twin guard from presence to freshness. Verified: unit tests for the missing/fresh/stale/legacy/hand-authored/switch/ twin-edit-vs-glsl-edit cases; editing a shader's GLSL then opening it under ESTELLA_SHOT_BACKEND=webgpu logs "generated 1 WGSL twin(s) on open" (the editor now regenerates the stale twin). 3612 SDK tests green.
1 parent 9631e20 commit caab6fc

14 files changed

Lines changed: 113 additions & 18 deletions

File tree

desktop/electron/shaderTwins.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ async function findEsshaders(dir: string, out: string[] = []): Promise<string[]>
4242
}
4343

4444
interface TwinGenerator {
45+
/** True when the shader has no twin, or a generated twin whose stored
46+
* source-hash no longer matches its GLSL (stale). Pure + cheap (no engine). */
47+
needsTwin(source: string): boolean;
4548
loadTwinModule(): Promise<{ esshader_cookInfo(source: string, features: string): unknown }>;
4649
processFile(
4750
module: unknown,
@@ -67,15 +70,11 @@ export async function ensureProjectShaderTwins(
6770
const result = { generated: [] as string[], failed: [] as string[], skipped: [] as string[] };
6871

6972
const files = await findEsshaders(root);
70-
const needy: string[] = [];
71-
for (const f of files) {
72-
const src = await readFile(f, 'utf8').catch(() => '');
73-
if (src.includes('#pragma fragment wgsl')) continue; // already has a twin
74-
if (src.includes('#pragma switch')) { result.skipped.push(f); continue; }
75-
needy.push(f);
76-
}
77-
if (needy.length === 0) return result;
73+
if (files.length === 0) return result;
7874

75+
// Import the generator up front — cheap (pure JS; the engine + converter wasm
76+
// load only in loadTwinModule below) — so its needsTwin decides which shaders
77+
// are missing OR stale (a twin whose source-hash no longer matches its GLSL).
7978
let gen: TwinGenerator;
8079
try {
8180
const url = pathToFileURL(path.join(repoRoot, 'tools', 'gen-shader-twins.mjs')).href;
@@ -85,6 +84,13 @@ export async function ensureProjectShaderTwins(
8584
return result;
8685
}
8786

87+
const needy: string[] = [];
88+
for (const f of files) {
89+
const src = await readFile(f, 'utf8').catch(() => '');
90+
if (gen.needsTwin(src)) needy.push(f);
91+
}
92+
if (needy.length === 0) return result;
93+
8894
const module = await gen.loadTwinModule();
8995
for (const f of needy) {
9096
try {

desktop/public/scenes/mat-builtin-lit/lit.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ void main() {
2626
#pragma vertex wgsl full
2727
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
2828
// Regenerate after changing the GLSL stages above.
29+
// source-hash: 9f608a54b0f5c530
2930
struct gl_PerVertex {
3031
@builtin(position) gl_Position: vec4<f32>,
3132
gl_PointSize: f32,
@@ -101,6 +102,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
101102
#pragma fragment wgsl full
102103
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
103104
// Regenerate after changing the GLSL stages above.
105+
// source-hash: 9f608a54b0f5c530
104106
struct Light2D {
105107
posDir: vec4<f32>,
106108
color: vec4<f32>,

desktop/public/scenes/mat-dissolve/dissolve.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ void main() {
3939
#pragma vertex wgsl full
4040
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
4141
// Regenerate after changing the GLSL stages above.
42+
// source-hash: 99e3d67aae0be3c6
4243
struct gl_PerVertex {
4344
@builtin(position) gl_Position: vec4<f32>,
4445
gl_PointSize: f32,
@@ -108,6 +109,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
108109
#pragma fragment wgsl full
109110
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
110111
// Regenerate after changing the GLSL stages above.
112+
// source-hash: 99e3d67aae0be3c6
111113
struct MaterialConstants {
112114
u_progress: f32,
113115
u_edgeColor: vec4<f32>,

desktop/public/scenes/mat-graph/graph-tint.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ void main() {
4242
#pragma vertex wgsl full
4343
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
4444
// Regenerate after changing the GLSL stages above.
45+
// source-hash: 17d9cf90b8470378
4546
struct gl_PerVertex {
4647
@builtin(position) gl_Position: vec4<f32>,
4748
gl_PointSize: f32,
@@ -114,6 +115,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
114115
#pragma fragment wgsl full
115116
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
116117
// Regenerate after changing the GLSL stages above.
118+
// source-hash: 17d9cf90b8470378
117119
struct MaterialConstants {
118120
u_tint: vec4<f32>,
119121
}

desktop/public/scenes/mat-lit-flatnormal/lit-fn.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ void main() {
5050
#pragma vertex wgsl full
5151
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
5252
// Regenerate after changing the GLSL stages above.
53+
// source-hash: 2109a83ca59fdb20
5354
struct gl_PerVertex {
5455
@builtin(position) gl_Position: vec4<f32>,
5556
gl_PointSize: f32,
@@ -121,6 +122,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
121122
#pragma fragment wgsl full
122123
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
123124
// Regenerate after changing the GLSL stages above.
125+
// source-hash: 2109a83ca59fdb20
124126
struct Light2D {
125127
posDir: vec4<f32>,
126128
color: vec4<f32>,

desktop/public/scenes/mat-lit-normal/lit-normal.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ void main() {
5050
#pragma vertex wgsl full
5151
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
5252
// Regenerate after changing the GLSL stages above.
53+
// source-hash: 60ae4382caf672f0
5354
struct gl_PerVertex {
5455
@builtin(position) gl_Position: vec4<f32>,
5556
gl_PointSize: f32,
@@ -121,6 +122,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
121122
#pragma fragment wgsl full
122123
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
123124
// Regenerate after changing the GLSL stages above.
125+
// source-hash: 60ae4382caf672f0
124126
struct Light2D {
125127
posDir: vec4<f32>,
126128
color: vec4<f32>,

desktop/public/scenes/mat-lit/lit.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ void main() {
4949
#pragma vertex wgsl full
5050
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
5151
// Regenerate after changing the GLSL stages above.
52+
// source-hash: 95a1f35f723b831c
5253
struct gl_PerVertex {
5354
@builtin(position) gl_Position: vec4<f32>,
5455
gl_PointSize: f32,
@@ -116,6 +117,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
116117
#pragma fragment wgsl full
117118
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
118119
// Regenerate after changing the GLSL stages above.
120+
// source-hash: 95a1f35f723b831c
119121
struct Light2D {
120122
posDir: vec4<f32>,
121123
color: vec4<f32>,

desktop/public/scenes/mat-tex/masked.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ void main() {
4141
#pragma vertex wgsl full
4242
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
4343
// Regenerate after changing the GLSL stages above.
44+
// source-hash: 70fc4540fdcdb04a
4445
struct gl_PerVertex {
4546
@builtin(position) gl_Position: vec4<f32>,
4647
gl_PointSize: f32,
@@ -107,6 +108,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
107108
#pragma fragment wgsl full
108109
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
109110
// Regenerate after changing the GLSL stages above.
111+
// source-hash: 70fc4540fdcdb04a
110112
struct TimeConstants {
111113
u_time: vec4<f32>,
112114
u_viewport: vec4<f32>,

examples/effects-gallery/assets/materials/hit-flash.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ void main() {
2424
#pragma vertex wgsl full
2525
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
2626
// Regenerate after changing the GLSL stages above.
27+
// source-hash: 1c3115bd7b73f674
2728
struct gl_PerVertex {
2829
@builtin(position) gl_Position: vec4<f32>,
2930
gl_PointSize: f32,
@@ -91,6 +92,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
9192
#pragma fragment wgsl full
9293
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
9394
// Regenerate after changing the GLSL stages above.
95+
// source-hash: 1c3115bd7b73f674
9496
struct MaterialConstants {
9597
u_flash: f32,
9698
u_flashColor: vec4<f32>,

examples/effects-gallery/assets/materials/outline.esshader

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ void main() {
3333
#pragma vertex wgsl full
3434
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
3535
// Regenerate after changing the GLSL stages above.
36+
// source-hash: 0d65463ea0c29816
3637
struct gl_PerVertex {
3738
@builtin(position) gl_Position: vec4<f32>,
3839
gl_PointSize: f32,
@@ -100,6 +101,7 @@ fn vs_main(@location(0) a_position: vec2<f32>, @location(1) a_color: vec4<f32>,
100101
#pragma fragment wgsl full
101102
// GENERATED by tools/gen-shader-twins.mjs (glslang + naga) - do not edit.
102103
// Regenerate after changing the GLSL stages above.
104+
// source-hash: 0d65463ea0c29816
103105
struct MaterialConstants {
104106
u_outlineColor: vec4<f32>,
105107
u_outlineWidth: f32,

0 commit comments

Comments
 (0)