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).
198 lines
9.3 KiB
Markdown
198 lines
9.3 KiB
Markdown
# TODO — Wings of the 90s
|
||
|
||
Working notes from the 6 Oct 2026 audit. Everything below was measured by running the
|
||
code, not inferred from the README. Upstream: `mac-ai-playground/retro-flight-sim`
|
||
(read-only for `kevinhermes`); work happens in this fork.
|
||
|
||
## Status check (verified)
|
||
|
||
- `npm install` + `npm run build` — clean, 13 modules, 512 kB bundle, 581 ms.
|
||
- `npm test` — passes, but see T4: it only counts braces.
|
||
- Terrain is deterministic across instances (`getHeightAt(317,-812)` identical in two
|
||
separate `Terrain` objects) — the "persistent world" claim holds.
|
||
- Rendering **verified in software** (SwiftShader, see "How to run it headless" below):
|
||
WebGL 2 context, 60 fps, 143 distinct colours, 63,792/64,000 non-black pixels, HUD
|
||
canvas drawing 6,905 lit pixels. No GPU is involved — this LXC has no `/dev/dri` and no
|
||
`/dev/nvidia*`, so all rendering is CPU-rasterised.
|
||
- **T8 found during that verification**: the CRT post-processing shader does not compile,
|
||
so the 3D canvas renders pure black on any machine. See T8.
|
||
|
||
---
|
||
|
||
## T1 — Takeoff does not work (highest priority)
|
||
|
||
Simulated 40 s at full throttle against the real `Terrain` heightmap:
|
||
|
||
| t (s) | knots | alt (ft) | onGround |
|
||
|------:|------:|---------:|:--------:|
|
||
| 0 | 0.3 | 19.7 | false |
|
||
| 1 | 23.2 | 2.8 | false |
|
||
| 2 | 49.3 | 0 | true |
|
||
| 6 | 340.2 | 12.2 | false |
|
||
| 9 | 542.5 | 105.2 | false |
|
||
| 10 | 564.6 | 0 | true |
|
||
| 39 | 2,543.3 | 0 | true |
|
||
|
||
The aircraft becomes briefly airborne, then never leaves the ground and accelerates to
|
||
Mach 4.1. Causes:
|
||
|
||
- `js/flight-model.js:77` — ground friction is `-hSpeed * 20`, i.e. 20 N per m/s,
|
||
against `maxThrust = 50000` N on a 1200 kg airframe. Friction loses every argument.
|
||
- `js/flight-model.js:60-64` — lift is applied while `onGround`, scaled only by
|
||
`GROUND_LIFT_REDUCTION`, so the plane floats off the runway at arbitrary speed and
|
||
then the ground-collision clamp at `:95` slams it back down.
|
||
- No weight-on-wheels state: `onGround` is a position test, not a contact state, so
|
||
there is no normal force and no way to model a proper rotation/tail-up.
|
||
- `PITCH_LIMIT = 85°` and the auto-levelling at `:134-135` (`x *= 0.995`, `z *= 0.985`)
|
||
fight the player continuously.
|
||
|
||
**Acceptance criteria:** full throttle from rest → airborne within a plausible
|
||
takeoff roll (order 200–400 m), climb sustained, stall if nose raised too high, and
|
||
speed bounded near a sane VMO instead of 2,500 kt.
|
||
|
||
## T2 — No WebGL fallback
|
||
|
||
`js/main.js:36-43` constructs `THREE.WebGLRenderer` with no try/catch. On a machine
|
||
without WebGL the page throws an uncaught `Error creating WebGL context` and the loading
|
||
screen never advances — a blank screen with no message.
|
||
|
||
**Fix:** wrap renderer init, show a period-appropriate failure card ("GRAPHICS CARD NOT
|
||
DETECTED"), and keep the loading/menu screens usable without 3D.
|
||
|
||
## T3 — Stall speed is defined but unused
|
||
|
||
`STALL_SPEED = 25` (`js/flight-model.js:4`) is used only to scale lift. Nothing in
|
||
`hud.js` or `sound.js` references stalling. A period-accurate sim of this era should
|
||
shove a warning in your face.
|
||
|
||
**Fix:** stall flag on the flight model → HUD warning + stick shaker / audio cue.
|
||
|
||
## T4 — The test suite is theatre — FIXED on branch `fix/crt-shader-and-real-tests`
|
||
|
||
`scripts/check-syntax.js` checks that each file is >10 chars and that `{` and `}` counts
|
||
match. It cannot catch T1 or T8. CI (`.gitea/workflows/ci.yml`) runs it on every push, so
|
||
the green tick meant nothing.
|
||
|
||
Now: `npm test` runs `node --test "tests/*.test.js"` (no new dependency) with
|
||
`tests/shaders.test.js` and `tests/terrain.test.js`; the old brace-counter survives as
|
||
`npm run lint:syntax`. `npm run test:render` runs `scripts/headless-check.mjs`, a
|
||
dependency-free CDP check that renders the built game in software WebGL and fails if the
|
||
canvas is black or the shaders fail to compile.
|
||
|
||
Both new tests were proven **non-vacuous** by re-breaking the shader and watching them
|
||
fail: the shader tests fail 2/4 with glslang's own `'constructor' : too many arguments`,
|
||
and the render check fails four ways (1 distinct colour, 0/64,000 lit pixels, shader
|
||
error, scene frozen).
|
||
|
||
Still outstanding: no `FlightModel` tests yet — those belong with T1, where the expected
|
||
behaviour has to be decided first.
|
||
|
||
Note on CI: this instance currently has **no registered Actions runners**
|
||
(`GET /repos/kevinhermes/retro-flight-sim/actions/runners` → `total_count: 0`), and
|
||
upstream's only run (id 36) ended `cancelled`, not `success`. So CI has never actually
|
||
validated this repo. The workflow is updated to install `glslang-tools` and run the real
|
||
tests, but treat CI as unproven until a runner exists.
|
||
|
||
## T5 — No LICENSE
|
||
|
||
README and repo carry no licence. Decide one (MIT matches the sibling `wings88`) and add
|
||
the file.
|
||
|
||
## T6 — No save/load
|
||
|
||
The sibling project's brief promised persistent worlds. There is no persistence here —
|
||
no `localStorage`, no world seed stored. Terrain is deterministic per process, which is
|
||
not the same thing.
|
||
|
||
**Fix:** persist the world seed + aircraft state; add a save/load menu entry.
|
||
|
||
## T8 — The CRT shader does not compile, so the 3D canvas is black — FIXED on branch `fix/crt-shader-and-real-tests`
|
||
|
||
Found by running the game under software WebGL, not by reading the code. `js/retro.js:29`:
|
||
|
||
```glsl
|
||
gl_Position = vec4(position, 0.0, 1.0);
|
||
```
|
||
|
||
In three.js `position` is a **vec3**, so this asks for `vec4(vec3, float, float)` — five
|
||
components into a four-component vector. Invalid GLSL ES on both WebGL 1 and 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 post-processing pass therefore fails and the game canvas reads back as **one
|
||
colour, 0/64,000 non-black pixels**. Verified in isolation: `vec4(vec3, float)` compiles,
|
||
`vec4(vec3, float, float)` does not, on both WebGL 1 and WebGL 2.
|
||
|
||
**Fix (verified):** `gl_Position = vec4(position.xy, 0.0, 1.0);` — compiles clean, and the
|
||
canvas then reports 143 distinct colours, 63,792/64,000 non-black pixels, scanline banding
|
||
visible every 25 rows, and the scene hash changing frame to frame once the aircraft moves.
|
||
Committed on this branch.
|
||
|
||
Note this is exactly the class of bug the old test suite could not see: braces balanced
|
||
perfectly. `tests/shaders.test.js` now compiles these shaders with glslang on every run.
|
||
|
||
## T7 — Minor / housekeeping
|
||
|
||
- `index.html` requests `/favicon.ico` → 404 on every load. Add one, or drop the request.
|
||
- Bundle is 512 kB (132 kB gzipped); Vite warns about the 500 kB chunk. Fine for now,
|
||
worth a `manualChunks` split if it grows.
|
||
- `package-lock.json` is 31 kB for a two-dependency project — expected, just noting it.
|
||
- No FPS cap. The README brags about a retro feel; `wings88`'s design notes suggest a
|
||
configurable 20 FPS cap, which would be cheap to add and very on-theme.
|
||
|
||
---
|
||
|
||
## How to run it headless (no GPU, verified working)
|
||
|
||
This LXC has no GPU access — `lspci`/`lsmod` inside an LXC report the **Proxmox host's**
|
||
hardware (an RTX 3090 shows up), but the container has no `/dev/dri` and no `/dev/nvidia*`,
|
||
so nothing is usable. Software rendering is the only option and it does work.
|
||
|
||
The combination that works:
|
||
|
||
```bash
|
||
CHROME=/root/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome
|
||
$CHROME --headless=new --no-sandbox --disable-gpu-sandbox \
|
||
--use-angle=swiftshader --enable-unsafe-swiftshader \
|
||
--remote-debugging-port=9223 --user-data-dir=/tmp/probe --no-first-run
|
||
# then attach with playwright-core: chromium.connectOverCDP('http://127.0.0.1:9223')
|
||
```
|
||
|
||
Gotchas, all learned the hard way:
|
||
|
||
- **Playwright's own launcher defeats this.** `chromium.launch()` injects defaults
|
||
(`--disable-gpu` among them) that silently kill the GPU process; every attempt via
|
||
`launch()` returned `webgl: NULL` or crashed the page. Launch Chrome directly and attach
|
||
over CDP instead.
|
||
- `--use-gl=swiftshader` (legacy path) crashes the page. Use `--use-angle=swiftshader`.
|
||
- `--use-angle=vulkan` + Mesa lavapipe (`VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json`)
|
||
brings the GPU process up (`GPU.EstablishGpuChannel` records samples) but still yields no
|
||
WebGL context. SwiftShader is the path that works.
|
||
- Headful under `xvfb-run` crashes the page. `--headless=new` is required.
|
||
- `--dump-dom` races the page script — it captures before the probe writes its result.
|
||
- WebGL clears the drawing buffer after each frame (`preserveDrawingBuffer: false`), so a
|
||
synchronous `readPixels` returns all-black. Sample inside a `requestAnimationFrame`
|
||
callback, or the measurement is meaningless.
|
||
- `readPixels` origin is bottom-left; flip rows before reporting them as screen rows.
|
||
- Screenshots via CDP fail here (`Unable to capture screenshot`), so verification is by
|
||
pixel readback and colour histograms rather than images.
|
||
|
||
---
|
||
|
||
## Suggested order
|
||
|
||
T1 → T4 (tests first would be better, but T1 is the reason the project is worth keeping)
|
||
→ T2 → T3 → T5 → T6 → T7.
|
||
|
||
## Notes on provenance
|
||
|
||
Two commits, both authored by `mac-container-dev` on 5 May 2026. Not a fork of `wings88`
|
||
(`fork: false`, no cross-references in code) — an independent take on the same brief.
|
||
`wings88` has 214 source lines and no Three.js usage at all; its useful parts are the
|
||
Jest/Playwright setup and the design guidelines (16–32 colour palette, no anti-aliasing,
|
||
20 FPS cap), which would slot in here.
|