Files
Joe MattielloandClaude Opus 5.5 a314dd3aa4 ci: GL qualification: windowed boot + every shader preset on Linux, Windows x64/arm64 and macOS (#1155) (#1160)
* feat(app): --shader PRESET|off for one run, and a [shader] result line per chain

`--shader PRESET.slangp` runs this one run through that preset and `--shader off`
through none; neither is written back to config.ini (like --theme).  A missing or
malformed preset is a usage error (exit 2) instead of a plain picture with a toast.

shaders.cpp now logs one stderr line when a chain is built or cannot be:
  [shader] <preset> built: GLSL 410 on OpenGL 4.1
  [shader] <preset> refused: ... (the GL_ARB_arrays_of_arrays limit, macOS GL 4.1)
  [shader] <preset> failed: ... (librashader's message)
so a CI run has a record of what the chain did (#1155 GL qualification).

Refs #1155

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci: GL qualification workflow: windowed boot + every shader preset on Linux, Windows, macOS

One reusable workflow, gl-qualification.yml, boots the desktop app windowed with a
real OpenGL context on hosted runners and renders every bundled .slangp through
librashader, keeping the screenshots.  Legs: Linux (ubuntu-24.04, Xvfb + llvmpipe),
Windows x64 (hosted, no vendor GL: the automatic bundled-Mesa relaunch of #1138/#1143
must fire), Windows arm64 (folds in windows-gl-test.yml, which is retired), macOS
arm64 (Apple OpenGL 4.1; the 15 arrays-of-arrays presets are allowlisted as refused).

Per leg: one build, cmake --install into stage/, vj_shader_tests gl run directly
(exit 77 = skipped = failure here), then app/test/gl-qualification/gl_qualify.py
launches --shader off and every preset with --screenshot and asserts exit 0, the
last [gl] renderer line, a non-black PNG, a [shader] built line and a picture that
differs from the plain one.  Artifacts upload pass or fail; the job summary lists
failures with the [gl]/[shader] log tail.  Goldens are designed
(app/test/golden/gl/<os>/<preset>.png, refresh_goldens input) but not gated.

Triggers: pull_request on the GL/shader/packaging paths, workflow_call (release.yml
requires it on tags and release/**), nightly schedule, workflow_dispatch.

Closes #1155

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): judge shaders_gl after the preset loop, fail fast, time the x64 fallback

First run (41565368188) findings: shaders_gl fails on llvmpipe and on Apple's software
renderer (crt-royale renders black in the 128x64 test picture) and the step stopped the
leg before any screenshot existed; the x64 automatic-fallback boot hit `timeout 120`
where arm64 took 3 s; the arm64 shaders_gl could not load the upstream prebuilt
librashader.dll (eager D3DX9_43.dll import, #1153/#1161).

- shaders_gl runs with continue-on-error and a "shaders_gl verdict" step fails the job
  after the preset loop, so the screenshots and logs upload either way.
- gl_qualify.py --fail-fast N (default 8): the loop stops after N consecutive failed
  presets and the summary says how many were not run.
- Windows fallback step: timeout 300 s, elapsed seconds logged (automatic-seconds.txt).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): empty-array expansion safe for macOS bash 3.2 (set -u)

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): Windows leg records the boot, runs timing diagnostics, gates the loop

Second run (41567744247): the x64 automatic-fallback boot did not return within 300 s
although the screenshot was written (frame 20 reached); arm64 does the same in 2 s.

- the loader probe (context create/draw/destroy through the bundled Mesa) runs before
  the app boot, timed;
- the boot step records exit code, seconds and each side condition (automatic-seconds.txt)
  instead of aborting at the first failed check; a verdict step fails the leg later;
- when the boot did not come back, three bounded experiments (the Mesa-side child run
  directly, --emu-thread off, video.vsync=0 seeded) go to timing.txt;
- the preset loop runs only once the plain boot works (500 launches of a boot that does
  not return are hours of nothing); per-launch timeout 150 s.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): known-broken allowlist for presets librashader rejects everywhere

The second Linux run (41567744247) built and rendered 494 of the 500 bundled presets
in ~13 minutes of wall time; the six nedi bilateral-variant presets (all but variant2)
fail reflection with UnknownSemantics("play"): they chain vhs/shaders/vhs.slang, whose
`play` sampler is an LUT the presets never declare.  A preset bug, not a GL one, the
same on every leg, so it gets the same treatment as the macOS refused set: an allowlist
with the reason that may only shrink (a listed preset that builds is an error).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): verdict steps run after an earlier failed step too (always())

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* test(app): shaders_gl renders the bundled presets at 640x480, not the 128x64 pixel-check picture

On llvmpipe (Linux) and Apple's software renderer (hosted macOS) crt-royale came out
black in shaders_gl: its mask passes scale by 0.0625 of the viewport and the preset says
"Safe for >= 341.333 horizontal triads at viewport size", so at 128x64 they are 8x4 px.
The app at 960x760 renders it (gl-qualification run 41567744247, 76% of pixels differ
from the plain picture).  The pixel checks keep their 128x64 (an exact 4x of the 32x16
source); the bundled loop and VJ_TEST_PRESETS render at a real-world 640x480.

Refs #1155

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): every Windows Mesa launch bounded; llvmpipe single-thread and softpipe probes

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): own librashader artifacts, macOS from source and sized down, release staging excludes qualification artifacts

Review (Codex P1/P2 on #1160) and run 41571727219:
- librashader.yml gains an `artifact_suffix` input; this workflow calls it with `-glq`, so a
  release run (app-package.yml + gl-qualification.yml) uploads no colliding artifact names.
- release.yml's two staging sweeps exclude artifacts/gl-qualification-* (screenshots, logs and
  the arm64 test zip are not release assets).
- macOS leg: the from-source aarch64 runtime the package ships, not upstream's prebuilt 0.12.0
  (crt-black_crush-koko.slangp fails to parse on the prebuilt, parses on the pinned source);
  3 launches at once in a 480x360 window (small-window.ini via gl_qualify.py --seed-config):
  Apple's software renderer managed 205 of 500 presets in 150 min at 960x760 and 2 jobs.
- refused-<os>.txt also covers presets the leg's GL cannot COMPILE (macOS: fsr and smaa+fsr need
  packHalf2x16, GLSL 4.20); a listed preset that builds is an error, the set only shrinks.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* test(app): gl_context_integration prints unbuffered stage markers (where does the x64 Mesa probe stop?)

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): tracked known failures; the x64 exit hang counted, not masked; loop before shaders_gl

app/test/gl-qualification/known-failures.txt lists one failure per line (<leg> <kind> <reference>);
the workflow reads it and turns exactly that failure into a recorded pass with a note. Nothing else
on the leg is forgiven, and removing the line makes the leg fail again. First entry: windows-x64
exit-hang (#1167), the app never exits on the bundled MSYS2 MINGW64 Mesa 26.2.3 llvmpipe
(SDL_DestroyWindow/SDL_Quit hang after "context destroyed"; LP_NUM_THREADS=0 and softpipe exit
at once, arm64 Mesa exits in 2 s).

gl_qualify.py: --known-exit-hang <ref> polls the launch, kills its process tree 5 s after the
screenshot is complete (IEND present) and counts the preset, noting the known failure in
summary.md and results.json; a launch without a complete screenshot still times out and fails.
The Windows boot step accepts exit 124 when the screenshot and the [gl] lines are there, the
loaders step accepts it on PASS, and shaders_gl passes without its final OK line only when
nothing failed or skipped and its last check (the broken shader) ran.

Every leg runs the preset loop before shaders_gl: on the macOS software renderer a SIGALRM-killed
GL process left the next window creation hanging. shaders_gl's bundled loop and VJ_TEST_PRESETS
render at 384x288 (crt-royale's mask passes need >= 341 px; 640x480 exceeded a 600 s alarm on
Apple's software renderer), the macOS alarm is 1200 s. gl_context_integration prints unbuffered
stage markers so a log shows where a probe stops. docs/agent/testing.md describes the lists.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): capture waits for core frames; unbuffered shaders_gl; macOS 640x480 + capture diagnostics

Run 41727418224 (3eef3305) read per leg:
- Windows x64: the preset loop passed under the tracked exit hang (494 built, 501 launches killed 5 s after
  their screenshot, #1167) but the shaders_gl allowance failed: the test's stdout verdict lines were lost when
  the hung process was killed (a pipe is fully buffered; only the stderr [shader] lines survived).
  vj_shader_tests writes stdout unbuffered from main() on.
- Windows arm64: 493 built, CuNNy-8x32-nvl-2x-rgb timed out at 150 s on the 128-bit llvmpipe. The Windows
  per-launch timeout is 240 s like the other legs.
- macOS: shaders_gl passes at 384x288 (10 min). In the seeded 480x360 window every capture, --shader off
  included, came back a uniform white picture at 1.9 s a launch; not the game (yarc is black at core frame
  1-2 and shows its picture from frame 10 on), not the theme. The leg seeds 640x480 (fits the hosted display,
  keeps crt-royale above 341 px) and a "Capture diagnostics" step launches `off` once per window
  configuration (default, 480x360, +frame 120, +emu frames, no viewports, no vsync, 480x362, 640x480,
  960x760) and prints each picture's statistics to the step summary, so the artifact names the variable.

App: --screenshot-emu-frames M (dev): the capture also waits until the core has shown M frames, with a line
and a capture anyway 600 UI frames later if it never does; gl_qualify.py --emu-frames passes it, every leg
uses 60. VideoOut::emuFrames() exposes the counter.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* ci(gl-qualification): even indentation in the x64 shaders_gl allowance (editorconfig-checker)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): macOS captures after 4 UI frames, 240 min budget; 480x360 white = ImGui viewports

Run 41760278887 (6afaae1a): Linux, Windows x64 (loop and shaders_gl under #1167) and Windows arm64
green. macOS: the capture diagnostics named the white picture's variable. With ImGui's extra OS
windows on (ui.viewports, the default) a 480x360 or 480x362 window captures uniform white; with them
off, or at 640x480 and up, it captures the game. The 640x480 leg then built every chain it reached but
ran out of time: 172 built, 57 timeouts at 240 s (49 of them had built the chain; Apple's software
renderer spends ~12 s a frame on the heavy ones), fail-fast after 250 presets, and the job hit its
150 min limit right after the upload.

- The macOS loop captures after 4 UI frames instead of 20; --emu-frames 60 already makes the capture
  the game's picture. The other legs keep 20.
- macOS timeout-minutes 240 (build ~4 min, shaders_gl 10-18 min), shaders_gl alarm 1800 s.
- The diagnostics step keeps four launches (default, 480x360 with and without viewports, 640x480).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): shard the macOS leg 3 ways, track presets over 120 s as macos-arm64 slow

Apple's software OpenGL 4.1 renderer on hosted Macs cannot render ~500 presets in one job
(runs 41760278887 and 41776934638 each ran out of time).  The macOS job becomes a 3-shard
matrix (gl_qualify.py --shard I/N: sorted preset index modulo N, every preset once, each
shard with its own `off` baseline, 2 launches at once), the GL probe, capture diagnostics and
shaders_gl in shard 0 only, and a macos-summary job joins the shards with --merge, failing
when a shard is missing or the shards did not run every preset exactly once.  Only the
joined table goes to the run summary.

known-failures.txt gains a `macos-arm64 slow <measured>@<run> <preset>` kind: 68 presets slow
in every run that measured them.  A listed preset is stopped at --slow-threshold (120 s) and
counted as a known failure, not a checked picture; an unlisted one over the limit fails and
the summary prints its line to paste; a listed one that renders in time is flagged for
removal.  A launch with no [gl] line is retried once and never counts as slow; POSIX kills
send SIGTERM first (--graceful-kill) and report a surviving process group.  Every run writes
times.tsv and a "Slowest launches" table.  macOS captures after SHOT_FRAME UI frames again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): no exit allowance for app launches (#1172); test programs under #1179

#1172 ends the app's bundled-Mesa child without unloading Mesa, so the `windows-x64 exit-hang
#1167` entry and every app-launch allowance go: the automatic-fallback boot step and the preset
loop (--known-exit-hang) fail again if an app launch does not exit on its own.

The two test programs, vj_gl_context_integration and `vj_shader_tests gl`, load the bundled Mesa
in-process and still hang in its DllMain after SDL_Quit; their allowances stay, unchanged in what
they require (the probe's `PASS GL loader` line; shaders_gl with no FAIL/SKIP and its last check
run), now keyed to `windows-x64 test-exit-hang #1179`.

The macOS summary states plainly how many presets are listed `macos-arm64 slow` and therefore
not rendered, with their measured times (finished median and range, timeouts counted apart) and
how many this run stopped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): macOS slow list from the first sharded run; ntsc-blastem refused on GL 4.1

Run 41891214865 (3 shards, all 500 presets, every one run once): Linux, Windows x64 and
Windows arm64 green; macOS 384 built, 57 listed presets stopped at 120 s, 36 failures.

- 31 presets took over 120 s with the chain built (12 timed out at 240 s): listed as
  `macos-arm64 slow` with their measured times.  7 listed presets that rendered in under 100 s
  are dropped; 4 that took 102-114 s stay listed (a listed preset that renders in time is still
  checked).  92 presets are listed.
- ntsc/ntsc-blastem fails to link on Apple's GL 4.1 ("Input of fragment shader
  'LIBRA_VARYING_2' differs in type/qualifiers"); it builds on GL 4.6: refused-macos.txt.
- 4 consecutive shard-0 launches (and their retries) never got a window for ~15 min, then the
  runner recovered, as in run 41760278887: on macOS a launch that times out with no [gl] line
  now leaves a `sample` stack of where the app was stuck.

The summary sentence now says a listed preset stopped at the limit is not rendered or checked,
and counts how many this run stopped and how many rendered in time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): fail a slow macOS preset only if it never renders; reset Apple's GL compile service; no Windows exit allowances

Gate change (option b): an unlisted preset now FAILS only if it does not render within the
240 s timeout.  One that renders but takes over SLOW_LIMIT (120 s) is a ::notice:: with the
line to paste into known-failures.txt and a count in the summary ("Unlisted, rendered but over
120 s").  Hosted-Mac times swing ~2x between runs (sabr-hybrid-deposterize: 131 s, 91 s,
229 s), so the hard 120 s line failed a few new presets every run.  Listed presets keep the
120 s cap; the summary sentence states both rules.

Run 41957577821 (fd76515e): Linux, Windows x64, Windows arm64 green; macOS 18 failures:
- 10 rendered in 121-205 s: notices under the new gate, not listed.
- 2 built and timed out at 240 s: listed (vectorscale-exact-energy-single-aa, xbr-lv2-sharp).
  94 presets are listed; fd76515e's three lines (all rendered within 240 s) are not.
- 6 never got a window, again in the vectorscale/xbr stretch (third run).  The new stack
  samples all wait in CGLChoosePixelFormat -> cvms_connection_create on an XPC reply from
  CVMServer (com.apple.cvmsServ, Apple's GL compile service) that never came.  --reset-cvms
  (macOS leg) kills CVMCompiler/CVMServer with sudo -n before the no-[gl] retry; launchd starts
  a fresh one; the summary counts the resets and how many retries then got a window.

#1181 fixed #1179 (the GL test programs now exit without unloading the bundled Mesa): the
`windows-x64 test-exit-hang #1179` entry, the Known failures step and both test-program
allowances are gone, so any Windows process that does not exit on its own fails the leg.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* ci(gl-qualification): list the 2 macOS presets that timed out with a chain on run 42110580090

crt-yah/presets/pure-lcd (204.9 s on 41957577821, over 240 s now) and vectorscale (no window on
41957577821, the CVMServer stall; chain built but no picture in 240 s now) are added as
macos-arm64 slow, 96 listed.  The 15 unlisted presets that rendered in 121-205 s on that run are
notices under the current rule and are not listed.

Also corrects the SMOKE_PRESETS comment (Kimi review): PR runs render all presets; only
workflow_dispatch's preset_set=smoke selects the subset.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-11-05 20:04:02 -05:00
..