The 3D canvas rendered pure black — one colour, 0 of 64,000 pixels lit. Cause is js/retro.js:29:
gl_Position=vec4(position,0.0,1.0);
In three.js position is a vec3, so this is vec4(vec3, float, float) — five components into a four-component vector. Invalid GLSL ES on both WebGL 1 and WebGL 2:
THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
Program Info Log: Vertex shader is not compiled.
ERROR: 0:71: 'constructor' : too many arguments
The whole CRT post-processing pass failed, so nothing drew. This is not a driver quirk: glslangValidator (Khronos' reference compiler) rejects the same line with the same message, and accepts the fix.
The fix
One line: vec4(position.xy, 0.0, 1.0). position.xy is the vec2 a fullscreen quad actually needs.
Why the old tests missed it
npm test was scripts/check-syntax.js, which checks each file is >10 characters and that { and } counts match. Braces balanced perfectly. It passed on a build that could not draw anything.
What this branch adds
tests/shaders.test.js — compiles the game's real shaders with glslangValidator under a three.js-style attribute prefix. Includes a canary asserting a known-bad vec4 construction is rejected, so the suite cannot pass vacuously.
tests/terrain.test.js — heightfield determinism (the "persistent world" claim), airport flattening inside the 200 m radius with no leakage outside it, out-of-bounds behaviour, plausible height range, vertex colour counts.
scripts/headless-check.mjs (npm run test:render) — serves dist/, drives the game over raw CDP with SwiftShader software WebGL, and fails if the canvas is black, the shaders fail to compile, or the scene does not change after input. No new dependencies (Node 22 built-in fetch/WebSocket), 120 s watchdog so it cannot hang a runner.
npm test → node --test "tests/*.test.js". The old brace counter survives as npm run lint:syntax.
CI installs glslang-tools and runs the real tests. The render job is continue-on-error because not every runner has a Chrome binary.
Evidence
With the fix (fresh clone of this branch, npm ci → build → tests):
Gate
Result
npm test
10/10 pass, 0 skipped
npm run build
clean, 565 ms
npm run lint:syntax
pass
npm run test:render
PASSED — 55–143 distinct colours, 63,750/64,000 lit pixels, no shader errors, scene hash changes after throttle + pitch
With the shader deliberately re-broken (proving the tests are not decorative):
npm test → fails 2/4 with glslang's own 'constructor' : too many arguments
npm run test:render → fails four ways: 1 distinct colour, 0/64,000 lit pixels, shader compile error, scene did not change after input
Honest caveats
Rendering was verified in software only. This box has no GPU (no /dev/dri, no /dev/nvidia*), so all the pixel evidence above is SwiftShader CPU rasterisation. It proves the shaders compile and geometry draws. It does not prove the CRT effect looks right — that needs your eyes.
CI has never validated this repo. This instance has no registered Actions runners (/actions/runners → total_count: 0), and upstream's only run (id 36) ended cancelled, not success. Do not read a missing CI tick as a pass.
The flight model is still broken (TODO T1): full throttle gives a brief hop to ~105 ft, then the aircraft stays grounded and accelerates to 2,543 knots. Deliberately left out of this PR so a verified one-line fix isn't tangled with judgement-heavy physics. That's the next branch.
How to test it
git clone -b fix/crt-shader-and-real-tests https://repos.retroweb.dev/kevinhermes/retro-flight-sim.git
cd retro-flight-sim
npm ci
npm test
npm run build && npm run preview # http://localhost:4173
You should see the terrain, runway and aircraft instead of a black rectangle. Takeoff will still not work — that's T1, not this PR.
## What was wrong
The 3D canvas rendered **pure black** — one colour, 0 of 64,000 pixels lit. Cause is `js/retro.js:29`:
```glsl
gl_Position = vec4(position, 0.0, 1.0);
```
In three.js `position` is a **vec3**, so this is `vec4(vec3, float, float)` — five components into a four-component vector. Invalid GLSL ES on both WebGL 1 and WebGL 2:
```
THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
Program Info Log: Vertex shader is not compiled.
ERROR: 0:71: 'constructor' : too many arguments
```
The whole CRT post-processing pass failed, so nothing drew. This is not a driver quirk: `glslangValidator` (Khronos' reference compiler) rejects the same line with the same message, and accepts the fix.
## The fix
One line: `vec4(position.xy, 0.0, 1.0)`. `position.xy` is the vec2 a fullscreen quad actually needs.
## Why the old tests missed it
`npm test` was `scripts/check-syntax.js`, which checks each file is >10 characters and that `{` and `}` counts match. Braces balanced perfectly. It passed on a build that could not draw anything.
## What this branch adds
- **`tests/shaders.test.js`** — compiles the game's *real* shaders with `glslangValidator` under a three.js-style attribute prefix. Includes a canary asserting a known-bad `vec4` construction is **rejected**, so the suite cannot pass vacuously.
- **`tests/terrain.test.js`** — heightfield determinism (the "persistent world" claim), airport flattening inside the 200 m radius with no leakage outside it, out-of-bounds behaviour, plausible height range, vertex colour counts.
- **`scripts/headless-check.mjs`** (`npm run test:render`) — serves `dist/`, drives the game over raw CDP with SwiftShader software WebGL, and fails if the canvas is black, the shaders fail to compile, or the scene does not change after input. No new dependencies (Node 22 built-in `fetch`/`WebSocket`), 120 s watchdog so it cannot hang a runner.
- `npm test` → `node --test "tests/*.test.js"`. The old brace counter survives as `npm run lint:syntax`.
- CI installs `glslang-tools` and runs the real tests. The render job is `continue-on-error` because not every runner has a Chrome binary.
## Evidence
**With the fix** (fresh clone of this branch, `npm ci` → build → tests):
| Gate | Result |
|---|---|
| `npm test` | 10/10 pass, 0 skipped |
| `npm run build` | clean, 565 ms |
| `npm run lint:syntax` | pass |
| `npm run test:render` | **PASSED** — 55–143 distinct colours, 63,750/64,000 lit pixels, no shader errors, scene hash changes after throttle + pitch |
**With the shader deliberately re-broken** (proving the tests are not decorative):
- `npm test` → fails 2/4 with glslang's own `'constructor' : too many arguments`
- `npm run test:render` → fails four ways: *1 distinct colour*, *0/64,000 lit pixels*, *shader compile error*, *scene did not change after input*
## Honest caveats
1. **Rendering was verified in software only.** This box has no GPU (no `/dev/dri`, no `/dev/nvidia*`), so all the pixel evidence above is SwiftShader CPU rasterisation. It proves the shaders compile and geometry draws. It does not prove the CRT effect looks right — that needs your eyes.
2. **CI has never validated this repo.** This instance has no registered Actions runners (`/actions/runners` → `total_count: 0`), and upstream's only run (id 36) ended `cancelled`, not `success`. Do not read a missing CI tick as a pass.
3. **The flight model is still broken** (TODO T1): full throttle gives a brief hop to ~105 ft, then the aircraft stays grounded and accelerates to 2,543 knots. Deliberately left out of this PR so a verified one-line fix isn't tangled with judgement-heavy physics. That's the next branch.
## How to test it
```bash
git clone -b fix/crt-shader-and-real-tests https://repos.retroweb.dev/kevinhermes/retro-flight-sim.git
cd retro-flight-sim
npm ci
npm test
npm run build && npm run preview # http://localhost:4173
```
You should see the terrain, runway and aircraft instead of a black rectangle. Takeoff will still not work — that's T1, not this PR.
vec4(position, 0.0, 1.0) passes a vec3 plus two floats into a vec4 - five
components into four. Invalid GLSL ES on both WebGL 1 and 2:
THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
ERROR: 0:71: 'constructor' : too many arguments
The whole post-processing pass failed and the game canvas read back as one
colour, 0/64000 non-black pixels. position.xy is the vec2 the fullscreen quad
needs.
Verified headlessly with SwiftShader: 55-143 distinct colours, 63750/64000
lit pixels, no shader errors, scene hash changes after throttle + pitch.
npm test was a brace counter: it checked each file was >10 chars and that {
and } counts matched. It passed on a build whose shader would not compile.
- tests/shaders.test.js compiles the game's real GLSL with glslangValidator
(Khronos' reference compiler) under a three.js-style attribute prefix, and
includes a canary that asserts a known-bad vec4 construction is REJECTED, so
the suite cannot pass vacuously.
- tests/terrain.test.js covers heightfield determinism (the "persistent world"
claim), airport flattening inside the 200 m radius with no leakage outside it,
out-of-bounds behaviour, plausible height range, and vertex colour counts.
- scripts/headless-check.mjs (npm run test:render) serves dist/, drives the
game over raw CDP with SwiftShader software WebGL, and fails if the canvas is
black, the shaders fail to compile, or the scene does not change after input.
No new dependencies: Node 22 built-in fetch/WebSocket. 120 s watchdog so it
cannot hang a runner.
- npm test -> node --test "tests/*.test.js"; old checker kept as npm run lint:syntax.
- CI installs glslang-tools and runs the real tests; render job is
continue-on-error because not every runner has a Chrome binary.
Verified non-vacuous: re-breaking the shader makes the unit tests fail 2/4 with
glslang's own "constructor: too many arguments" and makes the render check fail
four ways (1 distinct colour, 0/64000 lit pixels, shader error, frozen scene).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What was wrong
The 3D canvas rendered pure black — one colour, 0 of 64,000 pixels lit. Cause is
js/retro.js:29:In three.js
positionis a vec3, so this isvec4(vec3, float, float)— five components into a four-component vector. Invalid GLSL ES on both WebGL 1 and WebGL 2:The whole CRT post-processing pass failed, so nothing drew. This is not a driver quirk:
glslangValidator(Khronos' reference compiler) rejects the same line with the same message, and accepts the fix.The fix
One line:
vec4(position.xy, 0.0, 1.0).position.xyis the vec2 a fullscreen quad actually needs.Why the old tests missed it
npm testwasscripts/check-syntax.js, which checks each file is >10 characters and that{and}counts match. Braces balanced perfectly. It passed on a build that could not draw anything.What this branch adds
tests/shaders.test.js— compiles the game's real shaders withglslangValidatorunder a three.js-style attribute prefix. Includes a canary asserting a known-badvec4construction is rejected, so the suite cannot pass vacuously.tests/terrain.test.js— heightfield determinism (the "persistent world" claim), airport flattening inside the 200 m radius with no leakage outside it, out-of-bounds behaviour, plausible height range, vertex colour counts.scripts/headless-check.mjs(npm run test:render) — servesdist/, drives the game over raw CDP with SwiftShader software WebGL, and fails if the canvas is black, the shaders fail to compile, or the scene does not change after input. No new dependencies (Node 22 built-infetch/WebSocket), 120 s watchdog so it cannot hang a runner.npm test→node --test "tests/*.test.js". The old brace counter survives asnpm run lint:syntax.glslang-toolsand runs the real tests. The render job iscontinue-on-errorbecause not every runner has a Chrome binary.Evidence
With the fix (fresh clone of this branch,
npm ci→ build → tests):npm testnpm run buildnpm run lint:syntaxnpm run test:renderWith the shader deliberately re-broken (proving the tests are not decorative):
npm test→ fails 2/4 with glslang's own'constructor' : too many argumentsnpm run test:render→ fails four ways: 1 distinct colour, 0/64,000 lit pixels, shader compile error, scene did not change after inputHonest caveats
/dev/dri, no/dev/nvidia*), so all the pixel evidence above is SwiftShader CPU rasterisation. It proves the shaders compile and geometry draws. It does not prove the CRT effect looks right — that needs your eyes./actions/runners→total_count: 0), and upstream's only run (id 36) endedcancelled, notsuccess. Do not read a missing CI tick as a pass.How to test it
You should see the terrain, runway and aircraft instead of a black rectangle. Takeoff will still not work — that's T1, not this PR.
npm test was a brace counter: it checked each file was >10 chars and that { and } counts matched. It passed on a build whose shader would not compile. - tests/shaders.test.js compiles the game's real GLSL with glslangValidator (Khronos' reference compiler) under a three.js-style attribute prefix, and includes a canary that asserts a known-bad vec4 construction is REJECTED, so the suite cannot pass vacuously. - tests/terrain.test.js covers heightfield determinism (the "persistent world" claim), airport flattening inside the 200 m radius with no leakage outside it, out-of-bounds behaviour, plausible height range, and vertex colour counts. - scripts/headless-check.mjs (npm run test:render) serves dist/, drives the game over raw CDP with SwiftShader software WebGL, and fails if the canvas is black, the shaders fail to compile, or the scene does not change after input. No new dependencies: Node 22 built-in fetch/WebSocket. 120 s watchdog so it cannot hang a runner. - npm test -> node --test "tests/*.test.js"; old checker kept as npm run lint:syntax. - CI installs glslang-tools and runs the real tests; render job is continue-on-error because not every runner has a Chrome binary. Verified non-vacuous: re-breaking the shader makes the unit tests fail 2/4 with glslang's own "constructor: too many arguments" and makes the render check fail four ways (1 distinct colour, 0/64000 lit pixels, shader error, frozen scene).View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.