95 lines
4.2 KiB
JavaScript
95 lines
4.2 KiB
JavaScript
import { test, describe } from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||
|
|
import { execFileSync } from 'node:child_process';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { join, dirname } from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
|
||
|
|
// Compiles the game's real GLSL with Khronos' reference compiler (glslangValidator).
|
||
|
|
// This is the test that would have caught the vec4(vec3, float, float) bug: the shader
|
||
|
|
// failed to compile in the browser and the 3D canvas rendered pure black.
|
||
|
|
|
||
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||
|
|
const retroSrc = readFileSync(join(root, 'js', 'retro.js'), 'utf8');
|
||
|
|
|
||
|
|
function glslangAvailable() {
|
||
|
|
try {
|
||
|
|
execFileSync('glslangValidator', ['--version'], { stdio: ['ignore', 'ignore', 'ignore'] });
|
||
|
|
return true;
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// three.js prepends these declarations for a GLSL ES 1.00 ShaderMaterial.
|
||
|
|
const VERTEX_PREFIX = `precision highp float;
|
||
|
|
attribute vec3 position;
|
||
|
|
attribute vec3 normal;
|
||
|
|
attribute vec2 uv;
|
||
|
|
uniform mat4 modelViewMatrix;
|
||
|
|
uniform mat4 projectionMatrix;
|
||
|
|
uniform mat3 normalMatrix;
|
||
|
|
`;
|
||
|
|
const FRAGMENT_PREFIX = 'precision highp float;\n';
|
||
|
|
|
||
|
|
function extractShader(source, name) {
|
||
|
|
const re = new RegExp(`${name}:\\s*\`([\\s\\S]*?)\``, 'm');
|
||
|
|
const m = source.match(re);
|
||
|
|
if (!m) throw new Error(`could not extract ${name} from js/retro.js`);
|
||
|
|
return m[1];
|
||
|
|
}
|
||
|
|
|
||
|
|
function compile(source, stage, label) {
|
||
|
|
// glslang infers the shader stage from the file extension, so the extension matters.
|
||
|
|
const ext = stage === 'vert' ? 'vert' : 'frag';
|
||
|
|
const file = join(tmpdir(), `shader-test-${label}.${ext}`);
|
||
|
|
writeFileSync(file, `#version 100\n${source}`);
|
||
|
|
try {
|
||
|
|
execFileSync('glslangValidator', [file], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
||
|
|
return { ok: true, log: '' };
|
||
|
|
} catch (e) {
|
||
|
|
const log = `${e.stdout || ''}${e.stderr || ''}`;
|
||
|
|
// A usage banner means glslang never read the file: that is a harness fault, not a
|
||
|
|
// shader fault, and must not be mistaken for a compile error.
|
||
|
|
if (/^Usage: glslang/m.test(log.trim())) {
|
||
|
|
throw new Error(`glslang invoked incorrectly (no stage recognised) for ${label}.${ext}`);
|
||
|
|
}
|
||
|
|
return { ok: false, log };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('CRT post-processing shaders', () => {
|
||
|
|
const vertex = extractShader(retroSrc, 'vertexShader');
|
||
|
|
const fragment = extractShader(retroSrc, 'fragmentShader');
|
||
|
|
|
||
|
|
test('both shaders are present in js/retro.js', () => {
|
||
|
|
assert.ok(vertex.includes('gl_Position'), 'vertex shader has no gl_Position assignment');
|
||
|
|
assert.ok(fragment.includes('gl_FragColor'), 'fragment shader has no gl_FragColor assignment');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('shaders compile as GLSL ES 1.00', { skip: glslangAvailable() ? false : 'glslangValidator not installed (apt: glslang-tools)' }, () => {
|
||
|
|
const v = compile(VERTEX_PREFIX + vertex, 'vert', 'vertex');
|
||
|
|
assert.ok(v.ok, `vertex shader failed to compile:\n${v.log}`);
|
||
|
|
const f = compile(FRAGMENT_PREFIX + fragment, 'frag', 'fragment');
|
||
|
|
assert.ok(f.ok, `fragment shader failed to compile:\n${f.log}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Canary: proves the harness can actually reject a bad shader, so the test above
|
||
|
|
// cannot pass vacuously.
|
||
|
|
test('harness rejects a known-bad vec4 construction', { skip: glslangAvailable() ? false : 'glslangValidator not installed (apt: glslang-tools)' }, () => {
|
||
|
|
const bad = `${VERTEX_PREFIX}varying vec2 vUv;\nvoid main(){ vUv = uv; gl_Position = vec4(position, 0.0, 1.0); }`;
|
||
|
|
const r = compile(bad, 'vert', 'canary');
|
||
|
|
assert.equal(r.ok, false, 'canary shader unexpectedly compiled — the test harness is not validating anything');
|
||
|
|
assert.match(r.log, /constructor/, 'canary rejected for an unexpected reason');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('vertex shader does not over-fill gl_Position', () => {
|
||
|
|
// position is a vec3 in three.js, so vec4(position, x, y) is 5 components into a vec4.
|
||
|
|
const offenders = vertex
|
||
|
|
.split('\n')
|
||
|
|
.map((line, i) => ({ line: line.trim(), i: i + 1 }))
|
||
|
|
.filter(l => /vec4\(\s*position\s*,\s*[\d.]+\s*,\s*[\d.]+\s*\)/.test(l.line));
|
||
|
|
assert.deepEqual(offenders, [], `vec4(vec3, float, float) is invalid GLSL ES: ${JSON.stringify(offenders)}`);
|
||
|
|
});
|
||
|
|
});
|