Skip to content

Commit c2a6042

Browse files
authored
fix(playground): report GLSL compiler errors (#72)
* fix(playground): report GLSL compiler errors * test(playground): cover GLSL link fallback
1 parent ad9f98b commit c2a6042

File tree

4 files changed

+178
-26
lines changed

4 files changed

+178
-26
lines changed

apps/web/src/components/playground/PlaygroundCanvas.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
22
import { buildTslPreviewModule } from '../../../../../packages/schema/src/tsl-preview-module.ts'
3+
import { collectShaderDiagnostics, diagnosticsToMessages } from '../../lib/webgl-shader-errors'
34
import TslPreviewCanvas from '../TslPreviewCanvas'
45

56
type THREE = typeof import('three')
@@ -186,32 +187,40 @@ export default function PlaygroundCanvas(props: PlaygroundCanvasProps) {
186187
mesh = new THREE.Mesh(geometry, material)
187188
scene.add(mesh)
188189

189-
// Test compile
190-
renderer.render(scene, camera)
191-
const gl = renderer.getContext()
192-
const glProgram = gl.getParameter(gl.CURRENT_PROGRAM)
193-
194-
if (glProgram) {
195-
// Check vertex shader
196-
const vertShader = gl.getAttachedShaders(glProgram)
197-
const errors: string[] = []
198-
if (vertShader) {
199-
for (const s of vertShader) {
200-
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
201-
const log = gl.getShaderInfoLog(s)
202-
if (log) errors.push(log)
203-
}
204-
}
205-
}
206-
if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) {
207-
const log = gl.getProgramInfoLog(glProgram)
208-
if (log) errors.push(log)
209-
}
190+
const shaderDiagnostics: ReturnType<typeof collectShaderDiagnostics> = []
191+
const previousShaderError = renderer.debug.onShaderError
192+
193+
renderer.debug.onShaderError = (gl, program, glVertexShader, glFragmentShader) => {
194+
shaderDiagnostics.splice(
195+
0,
196+
shaderDiagnostics.length,
197+
...collectShaderDiagnostics({
198+
gl,
199+
program,
200+
vertexShader: glVertexShader,
201+
fragmentShader: glFragmentShader,
202+
}),
203+
)
204+
}
210205

211-
if (errors.length > 0) {
212-
props.onError(errors)
213-
return
214-
}
206+
try {
207+
renderer.render(scene, camera)
208+
} catch (renderError) {
209+
const fallbackMessage = renderError instanceof Error
210+
? renderError.message
211+
: 'Shader compilation failed'
212+
213+
props.onError(
214+
shaderDiagnostics.length > 0 ? diagnosticsToMessages(shaderDiagnostics) : [fallbackMessage],
215+
)
216+
return
217+
} finally {
218+
renderer.debug.onShaderError = previousShaderError
219+
}
220+
221+
if (shaderDiagnostics.length > 0) {
222+
props.onError(diagnosticsToMessages(shaderDiagnostics))
223+
return
215224
}
216225

217226
// No errors — clear any previous errors and capture screenshot
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import assert from 'node:assert/strict'
2+
import { collectShaderDiagnostics, diagnosticsToMessages } from './webgl-shader-errors.ts'
3+
4+
function runTest(name: string, callback: () => void) {
5+
callback()
6+
console.log(`ok ${name}`)
7+
}
8+
9+
type MockShader = {
10+
ok: boolean
11+
log: string | null
12+
}
13+
14+
type MockProgram = {
15+
log: string | null
16+
}
17+
18+
const mockGl = {
19+
COMPILE_STATUS: 1,
20+
getProgramInfoLog(program: unknown) {
21+
return (program as MockProgram).log
22+
},
23+
getShaderInfoLog(shader: unknown) {
24+
return (shader as MockShader).log
25+
},
26+
getShaderParameter(shader: unknown) {
27+
return (shader as MockShader).ok
28+
},
29+
}
30+
31+
runTest('collectShaderDiagnostics returns shader and link logs', () => {
32+
const diagnostics = collectShaderDiagnostics({
33+
gl: mockGl,
34+
program: { log: 'Program Info Log: link failed' },
35+
vertexShader: { ok: false, log: 'VERTEX ERROR: undeclared identifier' },
36+
fragmentShader: { ok: false, log: 'FRAGMENT ERROR: syntax error' },
37+
})
38+
39+
assert.deepEqual(diagnostics, [
40+
{ kind: 'glsl-compile', message: 'VERTEX ERROR: undeclared identifier' },
41+
{ kind: 'glsl-compile', message: 'FRAGMENT ERROR: syntax error' },
42+
{ kind: 'glsl-link', message: 'Program Info Log: link failed' },
43+
])
44+
assert.deepEqual(diagnosticsToMessages(diagnostics), [
45+
'VERTEX ERROR: undeclared identifier',
46+
'FRAGMENT ERROR: syntax error',
47+
'Program Info Log: link failed',
48+
])
49+
})
50+
51+
runTest('collectShaderDiagnostics falls back to a generic compile message', () => {
52+
const diagnostics = collectShaderDiagnostics({
53+
gl: mockGl,
54+
program: { log: null },
55+
vertexShader: { ok: false, log: null },
56+
fragmentShader: { ok: true, log: null },
57+
})
58+
59+
assert.deepEqual(diagnostics, [
60+
{ kind: 'glsl-compile', message: 'GLSL shader compilation failed.' },
61+
])
62+
})
63+
64+
runTest('collectShaderDiagnostics falls back to a generic link message', () => {
65+
const diagnostics = collectShaderDiagnostics({
66+
gl: mockGl,
67+
program: { log: null },
68+
vertexShader: { ok: true, log: null },
69+
fragmentShader: { ok: true, log: null },
70+
})
71+
72+
assert.deepEqual(diagnostics, [
73+
{ kind: 'glsl-link', message: 'GLSL program linking failed.' },
74+
])
75+
})
76+
77+
console.log('webgl-shader-errors tests passed')
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
export type ShaderDiagnostic = {
2+
kind: 'glsl-compile' | 'glsl-link'
3+
message: string
4+
}
5+
6+
type ShaderLogContext = {
7+
gl: {
8+
COMPILE_STATUS: number
9+
getProgramInfoLog: (program: unknown) => string | null
10+
getShaderInfoLog: (shader: unknown) => string | null
11+
getShaderParameter: (shader: unknown, pname: number) => boolean
12+
}
13+
program: unknown
14+
vertexShader?: unknown
15+
fragmentShader?: unknown
16+
}
17+
18+
function normalizeLog(log: string | null | undefined): string {
19+
return log?.trim() ?? ''
20+
}
21+
22+
function pushDiagnostic(
23+
diagnostics: ShaderDiagnostic[],
24+
kind: ShaderDiagnostic['kind'],
25+
message: string | null | undefined,
26+
) {
27+
const normalized = normalizeLog(message)
28+
if (!normalized) return
29+
if (diagnostics.some((entry) => entry.kind === kind && entry.message === normalized)) return
30+
diagnostics.push({ kind, message: normalized })
31+
}
32+
33+
export function collectShaderDiagnostics({
34+
gl,
35+
program,
36+
vertexShader,
37+
fragmentShader,
38+
}: ShaderLogContext): ShaderDiagnostic[] {
39+
const diagnostics: ShaderDiagnostic[] = []
40+
let compileFailure = false
41+
42+
for (const shader of [vertexShader, fragmentShader]) {
43+
if (!shader) continue
44+
45+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
46+
compileFailure = true
47+
pushDiagnostic(diagnostics, 'glsl-compile', gl.getShaderInfoLog(shader))
48+
}
49+
}
50+
51+
pushDiagnostic(diagnostics, 'glsl-link', gl.getProgramInfoLog(program))
52+
53+
if (diagnostics.length > 0) {
54+
return diagnostics
55+
}
56+
57+
if (compileFailure) {
58+
return [{ kind: 'glsl-compile', message: 'GLSL shader compilation failed.' }]
59+
}
60+
61+
return [{ kind: 'glsl-link', message: 'GLSL program linking failed.' }]
62+
}
63+
64+
export function diagnosticsToMessages(diagnostics: ShaderDiagnostic[]): string[] {
65+
return diagnostics.map(({ message }) => message)
66+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"check": "bun run test && bun run typecheck && bun run validate:shaders && bun run build:web",
1414
"dev:web": "cd apps/web && bun run dev",
1515
"test": "node --experimental-strip-types packages/schema/src/index.test.ts && bun run test:cli && bun run test:mcp && bun run test:registry && bun run test:web",
16-
"test:web": "node --experimental-strip-types apps/web/src/lib/server/shaders.test.ts && node --experimental-strip-types apps/web/src/lib/server/reviews-db.test.node.ts && node --experimental-strip-types apps/web/src/lib/server/playground-db.test.node.ts",
16+
"test:web": "node --experimental-strip-types apps/web/src/lib/server/shaders.test.ts && node --experimental-strip-types apps/web/src/lib/server/reviews-db.test.node.ts && node --experimental-strip-types apps/web/src/lib/server/playground-db.test.node.ts && node --experimental-strip-types apps/web/src/lib/webgl-shader-errors.test.ts",
1717
"test:cli": "node --experimental-strip-types packages/cli/src/registry-types.test.ts && node --experimental-strip-types packages/cli/src/commands/search.test.ts && node --experimental-strip-types packages/cli/src/commands/add.test.ts && node --experimental-strip-types packages/cli/src/lib/resolve-source.test.ts && node --experimental-strip-types packages/cli/src/lib/build-manifest.test.ts && node --experimental-strip-types packages/cli/src/lib/github-pr.test.ts && node --experimental-strip-types packages/cli/src/commands/submit.test.ts",
1818
"test:mcp": "node --experimental-strip-types packages/mcp/src/handlers.test.ts && node --experimental-strip-types packages/mcp/src/index.test.ts",
1919
"test:registry": "node --experimental-strip-types scripts/build-registry.test.ts",

0 commit comments

Comments
 (0)