CI: every game-free test on every change, workflow hygiene, and the Windows builder attached to releases #695

Merged
hckrman101 merged 4 commits from ci-overhaul-tests-windows into main 2026-11-08 18:01:03 +00:00
hckrman101 commented 2026-11-08 17:26:42 +00:00 (Migrated from github.com)

What this does

Three commits, one per topic:

  1. Every test on every change, in one runner (tests/run_all.py).
  2. Workflow hygiene: actions pinned by commit, least-privilege permissions, realistic timeouts, Dependabot, actionlint.
  3. The Windows builder in CI: the setup and the Hookshot Builder package are built without game data, uploaded as an artifact on every run, and attached to the tag's release beside the AppImage.

No game data is fetched, built or uploaded anywhere. CI still never compiles the game or its lifted code.

1. Tests

Before: 70 of the 83 test files were listed one step each, each with its own entry in two hand-kept path lists (about 150 paths each).

  • The first failure stopped the job and hid the rest.
  • A change outside the lists started no check.
  • 13 of the 83 test files are in no step.
  • Windows runs 27 of them.

Now: the Python source checks run on every pull request to main and every push to main, with no path filters. They call one stdlib-only runner:

python -I -B tests/run_all.py [--strict] [--no-skip] [name ...]
  • It finds every tests/test_*.py and nx2/test_*.py, so a new test needs no workflow edit. Each one runs in its own process with a timeout, several at once.
  • All of them run, even after a failure. Each failure's output is printed in a fold, with an error annotation.
  • It prints a table to the log and to the run's summary: pass, fail or skip, time, slowest first.
  • The skip list at the top of the file says what can't run in CI, why, and on which OS. Skipped checks show in the summary. --no-skip runs them anyway.
  • --strict (used in CI) makes a check fail when its compiler, headers or package is missing, instead of skipping it quietly.
  • Arguments are the ones the workflow passes today (--cxx g++, --cc gcc, and so on).
  • The job names stay the same: Python source checks (ubuntu-24.04) and (windows-2022). So does the SPIR-V global16 job. The steps that aren't plain test files are kept, and they now run even when an earlier step failed:
    • the Windows native step (cl.exe present_rect, test_invalidate_vcore.py --cxx cl);
    • the Builder worker test;
    • the Builder compile.

Also checked now:

  • A byte-compile of every tracked .py file, in memory, so a syntax error anywhere fails.
  • The C++ unit tests under gpu/vk/tests:
    • present_rect_test.cpp now also runs on Linux with -Wall -Wextra -Werror. MSVC still builds it on Windows.
    • lower_test.cpp is built and run over an empty folder. The programs it really reads are dumped from the game, so in CI this only shows that the lowering still compiles.
  • The workflows, linted by actionlint 1.7.12 (its release binary, SHA-256 checked) together with the runner's shellcheck.

The 13 test files no step runs today:

Result Tests
Now run, passing nx2/test_mesh_first_types, nx2/test_texture_init_barrier, nx2/test_default_point_size, nx2/test_coop_spirv, nx2/test_spirv_struct_identity, nx2/test_nup_codec, tests/test_present_semaphores, tests/test_report_words
Fixed, now passing nx2/test_draw_parameters: the DrawParameters block it compiles now reads EmitContext::wants_mesh_first, and the test's stand-in context lacked that member (test-only fix)
Skipped nx2/test_insn, nx2/test_leaf (need the game's modules and Unicorn), nx2/test_dll (needs a full game build), nx2/test_bc_storage_views (needs a Windows Vulkan device)

The headers come from pinned checkouts at the versions scripts/linux/deps.sh uses: Vulkan-Headers v1.4.345 and SPIRV-Headers vulkan-sdk-1.4.341.0. zstandard==0.25.0 is installed for the two NUP tests.

Windows coverage: windows-2022 runs 27 test files today. Nearly all the tests marked Linux-only pass there too, so it now runs 74 of the 83. It skips the 4 above, plus:

  • test_hookshot_launch: the Linux launcher is a shell script.
  • The 4 SPIR-V tests that need spirv-val, which only the Linux runner installs.

Linux runs 79 of the 83.

Fails on main today:

  • tests/test_cas_loop_ceiling.py (both OSes).
  • The SPIR-V global16 job: spirv-val reports "All OpVariable instructions in a function must be the first instructions in the first block".

Both are now fixed here, in a separate commit (SPIR-V: compare-exchange loops declare their attempt count first, ...):

  • The compare-exchange loops' attempt counter is declared in the entry block.
  • The f32 add fallback gets the #548 ceiling.
  • The test no longer counts ATOMG.CAS's single exchange as a loop.

CONTRIBUTING.md now documents the runner.

2. Hygiene

  • Pins: every action is pinned to the commit of the version it used before, with the version in a comment. That covers all workflows; linux-appimage.yml and release.yml used @v4 / @v2.
  • Dependabot: .github/dependabot.yml proposes action updates weekly, grouped into one PR.
  • Permissions: each workflow defaults to contents: read. contents: write is granted only to the jobs that push the release commit or attach release assets.
  • Timeouts:
    • The AppImage job goes from 150 to 45 minutes. It takes about 4 with its dependencies cached, and 9 with a cold cache on a fork.
    • The release job gets 15 minutes, the source checks 20, the SPIR-V job 15, the Windows jobs 30 and 45.
  • Concurrency: a pull request's older run is cancelled. A push run, a tag run or a release run never is. The AppImage used to cancel pushes too.

3. Windows builder (windows-builder.yml, windows-2022)

A player's download is HookshotBuilder.exe (the setup) and hookshot-setup.txt, plus the parts that list names in the builder-parts release. This workflow makes them the way the current package is laid out:

  • The setup: builder/setup/setup.c built with the llvm-mingw that make_setup_parts.py names. The zip is SHA-256 checked and cached. The result is the same size, byte for byte, as v2.0.1's HookshotBuilder.exe. The build line in setup.c's comment gains -municode, because the entry point is wWinMain.
  • Hookshot Builder: dotnet publish -c Release -p:Version=<tag without v>, as docs/versioning.md says. Any other commit gets git describe's version.
  • The package folder (scripts/release_parts.py package):
    • the published builder, the setup and LICENSE;
    • the tracked gpu, host, nx2 and scripts under tools/;
    • the play profile in tools/work;
    • tools/third_party: the tracked files, plus the prebuilt files the repository doesn't carry (a64emit.dll, lld-link.exe, SDL2.dll, SDL3.dll, zbic.dll), taken from the latest release's third_party part. a64emit.dll and zbic.dll are rebuilt on the player's PC when their sources' stamps change, as today.
  • The parts: make_setup_parts.py splits the package. Its new --reuse option takes the Python, emsdk, CPM and wgpu-native parts from the latest release's list, unchanged, since CI can't make those.
    • I checked this against v2.0.1: rebuilt from that tag, the app, .NET and third_party parts hold the same files as the maintainer's, with the same contents.
    • The only difference is CRLF in a few files of the maintainer's checkout (the repo's .gitattributes says LF).
    • The zip bytes differ, because zlib versions differ. So the first CI release uploads new .NET and third_party parts (about 90 MB, once). After that, a part whose files don't change keeps its SHA and isn't uploaded again.
  • The Windows Vulkan back end: compiled with the same llvm-mingw, alone, the way appimage.sh builds Linux's (RX_VK_ONLY=1, no lift, no game). Players build it on their PC, so it isn't a release asset. This job only checks that it still builds, and uploads hookshot_vk.dll as an artifact.
  • Artifacts on every PR and push: HookshotBuilder-windows (the setup, the list, the new parts) and hookshot_vk-windows.

Releases

On a tag, the Windows release job runs after both Windows jobs pass:

  1. It uploads the parts that builder-parts doesn't have yet. They are named by SHA, so existing ones are skipped. If the release is missing, it is created as a prerelease that is never latest.
  2. It then attaches HookshotBuilder.exe and hookshot-setup.txt to the tag's release.

The AppImage's release step moves into its own job (needs: appimage) and uses the same script, scripts/release_upload.sh:

  • Whichever job gets there first creates the release from the tag, with generated notes. The other one adds its files.
  • A create that loses the race falls back to the existing release.
  • The -rc rule is unchanged: a tag containing -rc is a prerelease and never latest. Any other tag is made latest.

The AppImage's play-profile step now uses the same helper. It never reads the release being made: before, the Windows job could create it first, and releases/latest would then have pointed at a release with no setup list. A fork with no release falls back to upstream's.

release.yml keeps semantic-release. Its comments, and docs/versioning.md, now say that both workflows publish the release. No new secrets are needed: RELEASE_TOKEN already makes the tag start both workflows.

Tested on a fork

  • Pull-request runs of all three workflows. The only red checks are the two that also fail on main today (see above).
  • A throwaway stable tag on the fork. Its release got the AppImage, HookshotBuilder.exe and hookshot-setup.txt, and was made latest. builder-parts was created with the new parts. Every part the list names was fetched and its size and SHA-256 checked.
  • Then a throwaway -rc tag on the fork. Its release was a prerelease, "latest" stayed on the stable one, and the parts already in builder-parts were skipped.
  • The fork's main was never pushed, so semantic-release didn't run.
  • The test tags and releases were deleted afterwards.
Before (upstream main, last runs) After (fork)
Python source checks, Linux about 40 s, 70 test files about 90 s, 79 test files + C++ + byte-compile
Python source checks, Windows about 75 s, 27 test files about 100 s, 74 test files + C++ + byte-compile
SPIR-V global16 about 85 s about 75 s (unchanged)
actionlint none 6 s
Linux AppImage about 4 min (warm cache) 8.5-10 min on the fork, all cold-cache runs; the job is unchanged apart from the profile step
Windows package and setup none (made by hand) about 80 s
Windows Vulkan back end none about 3 min

The jobs run in parallel, so a pull request's checks finish in about the time of the slowest job.

Notes for the maintainer

  • Required checks: the job names are unchanged, so any branch protection on them still works. Consider also requiring Workflow lint (actionlint), and the Windows builder's two jobs.
  • Your PC is no longer needed for the Windows assets. If you still upload them by hand for a release, upload after CI, or the two will overwrite each other's HookshotBuilder.exe / hookshot-setup.txt.
  • The python, emsdk, CPM and wgpu-native parts still come from your PC. When one of them changes, upload the new part to builder-parts and its line goes into the next release's hookshot-setup.txt. Until then, CI keeps reusing the previous one.
  • Later PRs that add a test step to source-regressions.yml will conflict with this one. With the runner, they only need to add the test file (and an ARGS entry in tests/run_all.py if it takes a compiler argument).

A suggestion for later: release-please instead of semantic-release

Not done here. I can do it in a follow-up if you want it.

release-please reads the same Conventional Commits. The difference is that it doesn't release on every push to main. Instead, it keeps one open release PR that carries:

  • the version bump;
  • the changelog, which can be reviewed and edited before it ships.

Merging that PR creates the tag and the GitHub release, which start the AppImage and Windows workflows the same way.

  • One version across the project: a manifest config (release-please-config.json + .release-please-manifest.json) keeps one version for the game, the builder and the AppImage, as docs/versioning.md wants.
  • Changelog: changelog.txt can stay in Factorio's format through a custom changelog step, or scripts/changelog.py can be run from the release PR.
  • Prereleases: -rc versions work through its prerelease settings on next.
  • Less tooling: no npm install of release tooling on every push.
  • Fewer surprises: a fix: merged by mistake doesn't publish a release by itself.
## What this does Three commits, one per topic: 1. **Every test on every change, in one runner** (`tests/run_all.py`). 2. **Workflow hygiene**: actions pinned by commit, least-privilege permissions, realistic timeouts, Dependabot, actionlint. 3. **The Windows builder in CI**: the setup and the Hookshot Builder package are built without game data, uploaded as an artifact on every run, and attached to the tag's release beside the AppImage. No game data is fetched, built or uploaded anywhere. CI still never compiles the game or its lifted code. ## 1. Tests **Before:** 70 of the 83 test files were listed one step each, each with its own entry in two hand-kept path lists (about 150 paths each). - The first failure stopped the job and hid the rest. - A change outside the lists started no check. - 13 of the 83 test files are in no step. - Windows runs 27 of them. **Now:** the Python source checks run on every pull request to `main` and every push to `main`, with no path filters. They call one stdlib-only runner: ``` python -I -B tests/run_all.py [--strict] [--no-skip] [name ...] ``` - It finds every `tests/test_*.py` and `nx2/test_*.py`, so a new test needs no workflow edit. Each one runs in its own process with a timeout, several at once. - All of them run, even after a failure. Each failure's output is printed in a fold, with an error annotation. - It prints a table to the log and to the run's summary: pass, fail or skip, time, slowest first. - The skip list at the top of the file says what can't run in CI, why, and on which OS. Skipped checks show in the summary. `--no-skip` runs them anyway. - `--strict` (used in CI) makes a check fail when its compiler, headers or package is missing, instead of skipping it quietly. - Arguments are the ones the workflow passes today (`--cxx g++`, `--cc gcc`, and so on). - The job names stay the same: `Python source checks (ubuntu-24.04)` and `(windows-2022)`. So does the SPIR-V global16 job. The steps that aren't plain test files are kept, and they now run even when an earlier step failed: - the Windows native step (`cl.exe` present_rect, `test_invalidate_vcore.py --cxx cl`); - the Builder worker test; - the Builder compile. **Also checked now:** - A byte-compile of every tracked `.py` file, in memory, so a syntax error anywhere fails. - The C++ unit tests under `gpu/vk/tests`: - `present_rect_test.cpp` now also runs on Linux with `-Wall -Wextra -Werror`. MSVC still builds it on Windows. - `lower_test.cpp` is built and run over an empty folder. The programs it really reads are dumped from the game, so in CI this only shows that the lowering still compiles. - The workflows, linted by actionlint 1.7.12 (its release binary, SHA-256 checked) together with the runner's shellcheck. **The 13 test files no step runs today:** | Result | Tests | |---|---| | Now run, passing | `nx2/test_mesh_first_types`, `nx2/test_texture_init_barrier`, `nx2/test_default_point_size`, `nx2/test_coop_spirv`, `nx2/test_spirv_struct_identity`, `nx2/test_nup_codec`, `tests/test_present_semaphores`, `tests/test_report_words` | | Fixed, now passing | `nx2/test_draw_parameters`: the DrawParameters block it compiles now reads `EmitContext::wants_mesh_first`, and the test's stand-in context lacked that member (test-only fix) | | Skipped | `nx2/test_insn`, `nx2/test_leaf` (need the game's modules and Unicorn), `nx2/test_dll` (needs a full game build), `nx2/test_bc_storage_views` (needs a Windows Vulkan device) | The headers come from pinned checkouts at the versions `scripts/linux/deps.sh` uses: Vulkan-Headers v1.4.345 and SPIRV-Headers vulkan-sdk-1.4.341.0. `zstandard==0.25.0` is installed for the two NUP tests. **Windows coverage:** windows-2022 runs 27 test files today. Nearly all the tests marked Linux-only pass there too, so it now runs 74 of the 83. It skips the 4 above, plus: - `test_hookshot_launch`: the Linux launcher is a shell script. - The 4 SPIR-V tests that need `spirv-val`, which only the Linux runner installs. Linux runs 79 of the 83. **Fails on main today:** - `tests/test_cas_loop_ceiling.py` (both OSes). - The SPIR-V global16 job: `spirv-val` reports "All OpVariable instructions in a function must be the first instructions in the first block". Both are now fixed here, in a separate commit (`SPIR-V: compare-exchange loops declare their attempt count first, ...`): - The compare-exchange loops' attempt counter is declared in the entry block. - The f32 add fallback gets the #548 ceiling. - The test no longer counts ATOMG.CAS's single exchange as a loop. `CONTRIBUTING.md` now documents the runner. ## 2. Hygiene - **Pins:** every action is pinned to the commit of the version it used before, with the version in a comment. That covers all workflows; `linux-appimage.yml` and `release.yml` used `@v4` / `@v2`. - **Dependabot:** `.github/dependabot.yml` proposes action updates weekly, grouped into one PR. - **Permissions:** each workflow defaults to `contents: read`. `contents: write` is granted only to the jobs that push the release commit or attach release assets. - **Timeouts:** - The AppImage job goes from 150 to 45 minutes. It takes about 4 with its dependencies cached, and 9 with a cold cache on a fork. - The release job gets 15 minutes, the source checks 20, the SPIR-V job 15, the Windows jobs 30 and 45. - **Concurrency:** a pull request's older run is cancelled. A push run, a tag run or a release run never is. The AppImage used to cancel pushes too. ## 3. Windows builder (`windows-builder.yml`, windows-2022) A player's download is `HookshotBuilder.exe` (the setup) and `hookshot-setup.txt`, plus the parts that list names in the `builder-parts` release. This workflow makes them the way the current package is laid out: - **The setup:** `builder/setup/setup.c` built with the llvm-mingw that `make_setup_parts.py` names. The zip is SHA-256 checked and cached. The result is the same size, byte for byte, as v2.0.1's `HookshotBuilder.exe`. The build line in `setup.c`'s comment gains `-municode`, because the entry point is `wWinMain`. - **Hookshot Builder:** `dotnet publish -c Release -p:Version=<tag without v>`, as `docs/versioning.md` says. Any other commit gets `git describe`'s version. - **The package folder** (`scripts/release_parts.py package`): - the published builder, the setup and `LICENSE`; - the tracked `gpu`, `host`, `nx2` and `scripts` under `tools/`; - the play profile in `tools/work`; - `tools/third_party`: the tracked files, plus the prebuilt files the repository doesn't carry (`a64emit.dll`, `lld-link.exe`, `SDL2.dll`, `SDL3.dll`, `zbic.dll`), taken from the latest release's third_party part. `a64emit.dll` and `zbic.dll` are rebuilt on the player's PC when their sources' stamps change, as today. - **The parts:** `make_setup_parts.py` splits the package. Its new `--reuse` option takes the Python, emsdk, CPM and wgpu-native parts from the latest release's list, unchanged, since CI can't make those. - I checked this against v2.0.1: rebuilt from that tag, the app, .NET and third_party parts hold the same files as the maintainer's, with the same contents. - The only difference is CRLF in a few files of the maintainer's checkout (the repo's `.gitattributes` says LF). - The zip bytes differ, because zlib versions differ. So the first CI release uploads new .NET and third_party parts (about 90 MB, once). After that, a part whose files don't change keeps its SHA and isn't uploaded again. - **The Windows Vulkan back end:** compiled with the same llvm-mingw, alone, the way `appimage.sh` builds Linux's (`RX_VK_ONLY=1`, no lift, no game). Players build it on their PC, so it isn't a release asset. This job only checks that it still builds, and uploads `hookshot_vk.dll` as an artifact. - **Artifacts** on every PR and push: `HookshotBuilder-windows` (the setup, the list, the new parts) and `hookshot_vk-windows`. ### Releases On a tag, the Windows `release` job runs after both Windows jobs pass: 1. It uploads the parts that `builder-parts` doesn't have yet. They are named by SHA, so existing ones are skipped. If the release is missing, it is created as a prerelease that is never latest. 2. It then attaches `HookshotBuilder.exe` and `hookshot-setup.txt` to the tag's release. The AppImage's release step moves into its own job (`needs: appimage`) and uses the same script, `scripts/release_upload.sh`: - Whichever job gets there first creates the release from the tag, with generated notes. The other one adds its files. - A create that loses the race falls back to the existing release. - The `-rc` rule is unchanged: a tag containing `-rc` is a prerelease and never latest. Any other tag is made latest. The AppImage's play-profile step now uses the same helper. It never reads the release being made: before, the Windows job could create it first, and `releases/latest` would then have pointed at a release with no setup list. A fork with no release falls back to upstream's. `release.yml` keeps semantic-release. Its comments, and `docs/versioning.md`, now say that both workflows publish the release. No new secrets are needed: `RELEASE_TOKEN` already makes the tag start both workflows. ## Tested on a fork - Pull-request runs of all three workflows. The only red checks are the two that also fail on main today (see above). - A throwaway stable tag on the fork. Its release got the AppImage, `HookshotBuilder.exe` and `hookshot-setup.txt`, and was made latest. `builder-parts` was created with the new parts. Every part the list names was fetched and its size and SHA-256 checked. - Then a throwaway `-rc` tag on the fork. Its release was a prerelease, "latest" stayed on the stable one, and the parts already in `builder-parts` were skipped. - The fork's main was never pushed, so semantic-release didn't run. - The test tags and releases were deleted afterwards. | | Before (upstream main, last runs) | After (fork) | |---|---|---| | Python source checks, Linux | about 40 s, 70 test files | about 90 s, 79 test files + C++ + byte-compile | | Python source checks, Windows | about 75 s, 27 test files | about 100 s, 74 test files + C++ + byte-compile | | SPIR-V global16 | about 85 s | about 75 s (unchanged) | | actionlint | none | 6 s | | Linux AppImage | about 4 min (warm cache) | 8.5-10 min on the fork, all cold-cache runs; the job is unchanged apart from the profile step | | Windows package and setup | none (made by hand) | about 80 s | | Windows Vulkan back end | none | about 3 min | The jobs run in parallel, so a pull request's checks finish in about the time of the slowest job. ## Notes for the maintainer - **Required checks:** the job names are unchanged, so any branch protection on them still works. Consider also requiring `Workflow lint (actionlint)`, and the Windows builder's two jobs. - **Your PC is no longer needed for the Windows assets.** If you still upload them by hand for a release, upload after CI, or the two will overwrite each other's `HookshotBuilder.exe` / `hookshot-setup.txt`. - **The python, emsdk, CPM and wgpu-native parts** still come from your PC. When one of them changes, upload the new part to `builder-parts` and its line goes into the next release's `hookshot-setup.txt`. Until then, CI keeps reusing the previous one. - **Later PRs that add a test step** to `source-regressions.yml` will conflict with this one. With the runner, they only need to add the test file (and an `ARGS` entry in `tests/run_all.py` if it takes a compiler argument). ## A suggestion for later: release-please instead of semantic-release Not done here. I can do it in a follow-up if you want it. [release-please](https://github.com/googleapis/release-please-action) reads the same Conventional Commits. The difference is that it doesn't release on every push to `main`. Instead, it keeps one open **release PR** that carries: - the version bump; - the changelog, which can be reviewed and edited before it ships. Merging that PR creates the tag and the GitHub release, which start the AppImage and Windows workflows the same way. - **One version across the project:** a manifest config (`release-please-config.json` + `.release-please-manifest.json`) keeps one version for the game, the builder and the AppImage, as `docs/versioning.md` wants. - **Changelog:** `changelog.txt` can stay in Factorio's format through a custom changelog step, or `scripts/changelog.py` can be run from the release PR. - **Prereleases:** `-rc` versions work through its prerelease settings on `next`. - **Less tooling:** no `npm install` of release tooling on every push. - **Fewer surprises:** a `fix:` merged by mistake doesn't publish a release by itself.
klimovich008 commented 2026-11-08 19:08:05 +00:00 (Migrated from github.com)

Independent AMD confirmation for the declaration-order issue covered here: a real compute module fails VUID-VkShaderModuleCreateInfo-pCode-08737 because a function-local variable follows executable instructions.

Moving only that declaration to the entry block's declaration list makes both SPIR-V validation and Vulkan pipeline construction pass on an RX 9070 XT. IDs, variable instructions, all other instructions and control flow were retained. Fails on the old code, passes now in this isolated module test.

Test command:

spirv-val shader.spv --relax-block-layout --uniform-buffer-standard-layout --scalar-block-layout --allow-localsizeid --target-env vulkan1.3

I also compiled the real API4 emitter with only the three declaration moves, reusing the matching backend/IR/sirit objects. Generic shared increment, storage U8, shared U8 and shared U16 each fail spirv-val --target-env vulkan1.3 --uniform-buffer-standard-layout before the change and each pass afterward (4/4 red, 4/4 green). An independent instruction check confirms two locals, one late declaration before the fix and none afterward in each selected helper. These constructor-path tests generate complete modules; they do not dispatch GPU work.

Scope: the AMD result covers pipeline construction only, using an isolated SPIR-V ordering repair. I have not tested the complete PR or its retry-ceiling changes, and this does not verify shader output.

No game data is included.

Independent AMD confirmation for the declaration-order issue covered here: a real compute module fails `VUID-VkShaderModuleCreateInfo-pCode-08737` because a function-local variable follows executable instructions. Moving only that declaration to the entry block's declaration list makes both SPIR-V validation and Vulkan pipeline construction pass on an RX 9070 XT. IDs, variable instructions, all other instructions and control flow were retained. Fails on the old code, passes now in this isolated module test. Test command: ```text spirv-val shader.spv --relax-block-layout --uniform-buffer-standard-layout --scalar-block-layout --allow-localsizeid --target-env vulkan1.3 ``` I also compiled the real API4 emitter with only the three declaration moves, reusing the matching backend/IR/sirit objects. Generic shared increment, storage U8, shared U8 and shared U16 each fail `spirv-val --target-env vulkan1.3 --uniform-buffer-standard-layout` before the change and each pass afterward (4/4 red, 4/4 green). An independent instruction check confirms two locals, one late declaration before the fix and none afterward in each selected helper. These constructor-path tests generate complete modules; they do not dispatch GPU work. Scope: the AMD result covers pipeline construction only, using an isolated SPIR-V ordering repair. I have not tested the complete PR or its retry-ceiling changes, and this does not verify shader output. No game data is included.
Commenting is not possible because the repository is archived.
No description provided.