Files

95 lines
4.2 KiB
JavaScript
Raw Permalink Normal View History

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)}`);
});
});