fix(vk): NVN's RGB5A1 and RGB5 sample with red in bits 0-4 #452

Open
mooki wants to merge 1 commit from mooki/Longshot:fix/nvn-rgb5-bit-order into main
Member

Type: fix · Risk: medium · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks, no game run) · Needs: Windows or Linux, any GPU (a visual check against main)

Why

The Vulkan renderer samples NVN's RGB5A1 textures with red and blue swapped, and RGB5 textures with every
component a bit off and alpha from the wrong bit. NVN names packed formats from the lowest bit: RGB5A1 is red in bits
0-4, green 5-9, blue 10-14, alpha 15. Main keeps it as Vulkan's A1R5G5B5, which has red in bits 10-14. This changes
colours on every platform.

How to reproduce

Seen on: macOS 26.7.1, Apple M4 Max, in the game-free test (tests/test_nvn_rgb5_order.py against main's sources).
vk_formats.cpp and texture_view are the same code on every OS. Not seen in the game: whether it uses RGB5A1 or
RGB5 textures is not known.
Settings and switches: defaults.
Steps: in a checkout of main with this PR's tests/test_nvn_rgb5_order.py copied in, run
python -I -B tests/test_nvn_rgb5_order.py --cxx clang++ --headers <Vulkan headers>: every 16-bit texel is decoded by
the Vulkan format's layout and the sampled view's mapping, and by the WebGPU renderer's decode of the same NVN format.
Expected: the two decodes agree for every texel of RGB5A1, RGB5, RGB565, BGR565 and RGBA4: red in bits 0-4.
Actual: RGB5A1 texel 0x0001, swizzle 2 3 4 5: component 0 reads 0, NVN 8 (FAIL:254 want == got (RGB5A1)): blue
read as red. With RGB5 first (on main 5e072fc8, before the rebase): RGB5 texel 000000, swizzle 2 3 4 5: component 3 reads 0, NVN 255: alpha 0 for 0.

What changed

RGB5 now uses the same Vulkan format as RGB5A1 (A1R5G5B5), and a sampled view of either swaps red and blue in its
swizzle, so both read red from bits 0-4 as NVN defines it; RGB5's alpha reads one. Storage views and copies are
unchanged.

  • Read first: the two vk_formats.cpp cases with the swap in texture_view.
  • vk: gpu/vk/vk_formats.cpp.
    • RGB5A1 stays A1R5G5B5_UNORM_PACK16. Vulkan's exact match, A1B5G5R5_UNORM_PACK16, needs
      VK_KHR_maintenance5, which the renderer does not enable.
    • RGB5 is now A1R5G5B5_UNORM_PACK16 too. It was B5G5R5A1_UNORM_PACK16, which has red in bits 1-5 and alpha in
      bit 0.
  • vk: gpu/vk/vk_texture.cpp texture_view.
    • A sampled view of RGB5A1 or RGB5 swaps red and blue in its swizzle. The swap is on the swizzle's sources, so the
      game's own swizzle (a descriptor's or a view's) still applies.
    • RGB5 joins the X formats: its alpha reads one.
  • nx2: nx2/pipeline_states.py V1_NVN drops B5G5R5A1 (7), which no NVN format makes now, as
    tests/test_pipeline_states.py requires. The shipped lists' check (tests/test_pipeline_state_sources.py) passes.
  • tests: tests/test_nvn_rgb5_order.py (new). It fails on main.
    • It cuts vk_format, component and texture_view's sampled swizzle out of gpu/vk, and the WebGPU renderer's
      expand and convert_packed16 out of gpu/render/rx_texture.cpp. It compiles them with stubs.
    • Every 16-bit texel of RGB5A1, RGB5, RGB565, BGR565 and RGBA4 is decoded twice. One decode follows the
      Vulkan format's bit layout from the spec, then the sampled view's component mapping. The other is the WebGPU
      renderer's decode of the same NVN format (rx_nvn.cpp nvn_pixel_format), with alpha one for RGB5, which the
      WebGPU renderer does not map. That is 983,040 reads under three descriptor swizzles. The two must agree.
    • RGB565, BGR565 and RGBA4 already pass on main. They show that the same reading of NVN's order holds for the
      formats main already gets right.
  • Design: the references for NVN's order are the WebGPU renderer's decode (gpu/render/rx_nvn.cpp maps RGB5A1 to
    A1B5G5R5_UNORM; gpu/render/rx_texture.cpp convert_packed16 reads its red from bits 0-4), the same order as
    main's own RGB565 mapping (B5G6R5), and deko3d's format table (A1B5G5R5 with sources R,G,B,A; zlib, read for
    reference only: nothing copied).
  • Left out: on a driver with VK_KHR_maintenance5, A1B5G5R5_UNORM_PACK16 would hold these formats as they are,
    without the swap. Enabling that extension is a larger change than this fix.

Measurements

Verdict: fixed in the game-free test: it fails on main and passes here (983,040 reads agree), and the suite adds no
failure. The colour change in the game still needs a visual check (Must be tested).

Commits: main 6c9edffe5d (2026-11-14); this PR fix/nvn-rgb5-bit-order @
295016f707.
Hardware: Apple M4 Max (16 CPUs), macOS 26.7.1, Apple clang 21. Game-free: no GPU, no game
Scene: none. The test is the proof of the bit order (CONTRIBUTING's fix row). The colour change in the game still
needs a visual check (see Testing).
Method: tests/test_nvn_rgb5_order.py against main's sources (6c9edffe) and against this PR; the full
game-free suite on both
How to reproduce: python -I -B tests/test_nvn_rgb5_order.py --cxx clang++ --headers <Vulkan headers>

Before: main @ 6c9edffe After: this PR @ 295016f7
tests/test_nvn_rgb5_order.py FAIL: RGB5A1 reads blue as red; RGB5 reads alpha 0 for 0 PASS: 983,040 reads agree
full game-free suite on macOS (tests/run_all.py --jobs 6) 531 passed, 32 failed, 17 skipped 532 passed, 32 failed, 17 skipped
main @ 6c9edffe:
RGB5A1 texel 0x0001, swizzle 2 3 4 5: component 0 reads 0, NVN 8
FAIL:254 want == got (RGB5A1)
(the same test with RGB5 first, on main 5e072fc8 before the rebase:)
RGB5 texel 000000, swizzle 2 3 4 5: component 3 reads 0, NVN 255
FAIL:254 want == got (RGB5)

this PR @ 295016f7:
PASS: every texel of RGB5A1, RGB5, RGB565, BGR565 and RGBA4 (983040 reads, 3 swizzles) samples as NVN defines it

Screenshots: none yet: the visual check under Must be tested is still to do.

Flags and settings

None.

Testing

Tested:

  • On an Apple M4 Max with macOS 26.7.1, game-free only.
  • tests/test_nvn_rgb5_order.py fails on main and passes here.
  • tests/test_pipeline_states.py, tests/test_pipeline_state_portable.py, tests/test_pipeline_state_sources.py and
    tests/test_image_view_retry.py pass.
  • The full suite on both sides, compared by test name.
    • By test name, against main 6c9edffe's run on the same Mac: only one status changed: test_nvn_rgb5_order, absent on
      main, passes here.
    • These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull
      requests, some are checks main added or changed since 5e072fc8): test_appimage_package_retry,
      test_appimage_player_build, test_collect_wide, test_control, test_cpm_discovery, test_crash_dialog,
      test_crash_record, test_env_on, test_game_layout, test_lift_cleanup_error, test_linux_preflight,
      test_longshot_ctl, test_low_memory_notice, test_make_game_cache, test_mem_gate, test_mem_gate_children,
      test_mirror_trim, test_oot_state_tap, test_pcache_header_read, test_pipeline_cache_file,
      test_pipeline_cache_save, test_pipeline_play_cache, test_pipeline_tour, test_prof_symbols,
      test_program_cache_cleanup, test_program_cache_one_variant, test_readback_late, test_report_run,
      test_split_giants, test_vk_pgo, test_vram_budget_row, test_walk_key.
    • The same 17 checks skipped as on main.
    • Crash reports during the run: none (main's run: fault-arm64-2026-11-14-180704.ips,
      recorder_sync_test.exe-2026-11-14-180744.ips).
  • Object compiles of gpu/vk/vk_formats.cpp and vk_texture.cpp with the renderer's flags (nx2/build.py), Apple
    clang 21, arm64 and x86_64: all 8 build, 4 on main and 4 here.
    • Main's renderer does not build on macOS as it is (environ in vk_shader.cpp, posix_fallocate in
      pipeline_cache_file.h). These compiles include a compile-only prelude that declares those two names; it is part of
      no tree.
  • After the rebase onto main aa95d35f, at 1439e874 (295016f7 before the rebases onto e090cc48 and aa95d35f):
    Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
    apply) gave the same results as before; python -I -B tests/run_all.py on wave S2's merge 67e25333, which carries
    this head: 545/33/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
    (reports/2026-11-15-rebase-sweep-3.md).

Not tested:

  • The game: whether it uses RGB5A1 or RGB5 textures is not known. If it does, their red and blue change on every
    platform with this PR.
  • A GPU: the bit layouts are checked against the Vulkan spec, not sampled on a device.
  • Screenshots of the same spot on main and here: they wait for the visual check, so the screenshots box is not in the
    checklist.
  • Not built on Windows or Linux. The change is a format constant and a swizzle loop, like the code around it.

Must be tested:

  • Windows or Linux, any GPU, any driver: build this PR (head 1439e874) and main aa95d35f the same way, and take a
    screenshot of the same spots with the same settings on each (the title screen, the pause menu and its item screens,
    and a minute into a save, standing still), expecting each pair to match unless an RGB5A1 or RGB5 texture is on
    screen; where a pair differs, this PR's colours should look natural and main's show red and blue traded; post the
    pairs side by side, captioned with the build (by hand: test request T-FP8A)

Risks

What could break: medium: it changes colour on every platform, for RGB5A1 and RGB5 textures only, and no game
run has looked at it yet.

  • Rendering is not affected. deko3d's table (source/maxwell/format_traits.inc) has no render-target format for
    RGB5A1 or RGB5, so a game cannot render into them. A view of a rendered BGR5A1 target as RGB5A1 reads its
    bits in NVN's order: the swap gives exactly that.
  • Storage views and copies are unchanged: the image's bytes are still the storage's.
  • Landing order: rebased on main 6c9edffe (2026-11-14) without a conflict; main had changed vk_texture.cpp (#404) and
    nx2/pipeline_states.py (#563) away from this PR's lines. It conflicts with the format-mapping PR (#451,
    feat/nvn-format-map) in vk_texture.cpp (texture_view's alpha list) and nx2/pipeline_states.py (V1_NVN),
    which both change; vk_formats.cpp merges. Checked with git merge-tree on both rebased heads. Whichever lands
    second takes the other's lines.

Turning it off: revert the commit.

Compatibility: nothing changes but the colours above. None of the changed files is in the translator stamp
(nx2/translator_files.py), so no program is translated again; saves, the options file and the vk ABI are unchanged.

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head 1439e874, without a conflict; this PR's own
    commits, old → new: 295016f7 → 1439e874; the commit names elsewhere in this description are from before the
    rebase.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). On macOS: 532 passed, 17
    skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing. After the rebase onto main
    aa95d35f (head 1439e874): wave S2's merge 67e25333, 545/33/17 against main aa95d35f's 543/33/17, no new failure.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
**Type:** fix · **Risk:** medium · **Tested on:** macOS 26.7.1, Apple M4 Max (game-free checks, no game run) · **Needs:** Windows or Linux, any GPU (a visual check against main) ## Why The Vulkan renderer samples NVN's `RGB5A1` textures with red and blue swapped, and `RGB5` textures with every component a bit off and alpha from the wrong bit. NVN names packed formats from the lowest bit: `RGB5A1` is red in bits 0-4, green 5-9, blue 10-14, alpha 15. Main keeps it as Vulkan's `A1R5G5B5`, which has red in bits 10-14. This changes colours on every platform. ## How to reproduce **Seen on:** macOS 26.7.1, Apple M4 Max, in the game-free test (`tests/test_nvn_rgb5_order.py` against main's sources). `vk_formats.cpp` and `texture_view` are the same code on every OS. Not seen in the game: whether it uses `RGB5A1` or `RGB5` textures is not known. **Settings and switches:** defaults. **Steps:** in a checkout of main with this PR's `tests/test_nvn_rgb5_order.py` copied in, run `python -I -B tests/test_nvn_rgb5_order.py --cxx clang++ --headers <Vulkan headers>`: every 16-bit texel is decoded by the Vulkan format's layout and the sampled view's mapping, and by the WebGPU renderer's decode of the same NVN format. **Expected:** the two decodes agree for every texel of `RGB5A1`, `RGB5`, `RGB565`, `BGR565` and `RGBA4`: red in bits 0-4. **Actual:** `RGB5A1 texel 0x0001, swizzle 2 3 4 5: component 0 reads 0, NVN 8` (`FAIL:254 want == got (RGB5A1)`): blue read as red. With `RGB5` first (on main 5e072fc8, before the rebase): `RGB5 texel 000000, swizzle 2 3 4 5: component 3 reads 0, NVN 255`: alpha 0 for 0. ## What changed `RGB5` now uses the same Vulkan format as `RGB5A1` (`A1R5G5B5`), and a sampled view of either swaps red and blue in its swizzle, so both read red from bits 0-4 as NVN defines it; `RGB5`'s alpha reads one. Storage views and copies are unchanged. - Read first: the two `vk_formats.cpp` cases with the swap in `texture_view`. - vk: `gpu/vk/vk_formats.cpp`. - `RGB5A1` stays `A1R5G5B5_UNORM_PACK16`. Vulkan's exact match, `A1B5G5R5_UNORM_PACK16`, needs `VK_KHR_maintenance5`, which the renderer does not enable. - `RGB5` is now `A1R5G5B5_UNORM_PACK16` too. It was `B5G5R5A1_UNORM_PACK16`, which has red in bits 1-5 and alpha in bit 0. - vk: `gpu/vk/vk_texture.cpp` `texture_view`. - A sampled view of `RGB5A1` or `RGB5` swaps red and blue in its swizzle. The swap is on the swizzle's sources, so the game's own swizzle (a descriptor's or a view's) still applies. - `RGB5` joins the X formats: its alpha reads one. - nx2: `nx2/pipeline_states.py` `V1_NVN` drops `B5G5R5A1` (7), which no NVN format makes now, as `tests/test_pipeline_states.py` requires. The shipped lists' check (`tests/test_pipeline_state_sources.py`) passes. - tests: `tests/test_nvn_rgb5_order.py` (new). It fails on main. - It cuts `vk_format`, `component` and `texture_view`'s sampled swizzle out of `gpu/vk`, and the WebGPU renderer's `expand` and `convert_packed16` out of `gpu/render/rx_texture.cpp`. It compiles them with stubs. - Every 16-bit texel of `RGB5A1`, `RGB5`, `RGB565`, `BGR565` and `RGBA4` is decoded twice. One decode follows the Vulkan format's bit layout from the spec, then the sampled view's component mapping. The other is the WebGPU renderer's decode of the same NVN format (`rx_nvn.cpp` `nvn_pixel_format`), with alpha one for `RGB5`, which the WebGPU renderer does not map. That is 983,040 reads under three descriptor swizzles. The two must agree. - `RGB565`, `BGR565` and `RGBA4` already pass on main. They show that the same reading of NVN's order holds for the formats main already gets right. - Design: the references for NVN's order are the WebGPU renderer's decode (`gpu/render/rx_nvn.cpp` maps `RGB5A1` to `A1B5G5R5_UNORM`; `gpu/render/rx_texture.cpp` `convert_packed16` reads its red from bits 0-4), the same order as main's own `RGB565` mapping (`B5G6R5`), and deko3d's format table (`A1B5G5R5` with sources R,G,B,A; zlib, read for reference only: nothing copied). - Left out: on a driver with `VK_KHR_maintenance5`, `A1B5G5R5_UNORM_PACK16` would hold these formats as they are, without the swap. Enabling that extension is a larger change than this fix. ## Measurements **Verdict:** fixed in the game-free test: it fails on main and passes here (983,040 reads agree), and the suite adds no failure. The colour change in the game still needs a visual check (Must be tested). Commits: main 6c9edffe5da1f5b5383977aeab3f544c0de25d4c (2026-11-14); this PR fix/nvn-rgb5-bit-order @ 295016f7077bd35a4afd778240bce6f633826f50. Hardware: Apple M4 Max (16 CPUs), macOS 26.7.1, Apple clang 21. Game-free: no GPU, no game Scene: none. The test is the proof of the bit order (CONTRIBUTING's `fix` row). The colour change in the game still needs a visual check (see Testing). Method: `tests/test_nvn_rgb5_order.py` against main's sources (6c9edffe) and against this PR; the full game-free suite on both How to reproduce: `python -I -B tests/test_nvn_rgb5_order.py --cxx clang++ --headers <Vulkan headers>` | | Before: main @ 6c9edffe | After: this PR @ 295016f7 | |---|---|---| | `tests/test_nvn_rgb5_order.py` | FAIL: `RGB5A1` reads blue as red; `RGB5` reads alpha 0 for 0 | PASS: 983,040 reads agree | | full game-free suite on macOS (`tests/run_all.py --jobs 6`) | 531 passed, 32 failed, 17 skipped | 532 passed, 32 failed, 17 skipped | ``` main @ 6c9edffe: RGB5A1 texel 0x0001, swizzle 2 3 4 5: component 0 reads 0, NVN 8 FAIL:254 want == got (RGB5A1) (the same test with RGB5 first, on main 5e072fc8 before the rebase:) RGB5 texel 000000, swizzle 2 3 4 5: component 3 reads 0, NVN 255 FAIL:254 want == got (RGB5) this PR @ 295016f7: PASS: every texel of RGB5A1, RGB5, RGB565, BGR565 and RGBA4 (983040 reads, 3 swizzles) samples as NVN defines it ``` Screenshots: none yet: the visual check under Must be tested is still to do. ### Flags and settings None. ## Testing **Tested:** - On an Apple M4 Max with macOS 26.7.1, game-free only. - `tests/test_nvn_rgb5_order.py` fails on main and passes here. - `tests/test_pipeline_states.py`, `tests/test_pipeline_state_portable.py`, `tests/test_pipeline_state_sources.py` and `tests/test_image_view_retry.py` pass. - The full suite on both sides, compared by test name. - By test name, against main 6c9edffe's run on the same Mac: only one status changed: `test_nvn_rgb5_order`, absent on main, passes here. - These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull requests, some are checks main added or changed since 5e072fc8): `test_appimage_package_retry`, `test_appimage_player_build`, `test_collect_wide`, `test_control`, `test_cpm_discovery`, `test_crash_dialog`, `test_crash_record`, `test_env_on`, `test_game_layout`, `test_lift_cleanup_error`, `test_linux_preflight`, `test_longshot_ctl`, `test_low_memory_notice`, `test_make_game_cache`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_oot_state_tap`, `test_pcache_header_read`, `test_pipeline_cache_file`, `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_pipeline_tour`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`, `test_readback_late`, `test_report_run`, `test_split_giants`, `test_vk_pgo`, `test_vram_budget_row`, `test_walk_key`. - The same 17 checks skipped as on main. - Crash reports during the run: none (main's run: fault-arm64-2026-11-14-180704.ips, recorder_sync_test.exe-2026-11-14-180744.ips). - Object compiles of `gpu/vk/vk_formats.cpp` and `vk_texture.cpp` with the renderer's flags (`nx2/build.py`), Apple clang 21, arm64 and x86_64: all 8 build, 4 on main and 4 here. - Main's renderer does not build on macOS as it is (`environ` in `vk_shader.cpp`, `posix_fallocate` in `pipeline_cache_file.h`). These compiles include a compile-only prelude that declares those two names; it is part of no tree. - **After the rebase onto main aa95d35f**, at `1439e874` (`295016f7` before the rebases onto e090cc48 and aa95d35f): Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they apply) gave the same results as before; `python -I -B tests/run_all.py` on wave S2's merge `67e25333`, which carries this head: 545/33/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). **Not tested:** - The game: whether it uses `RGB5A1` or `RGB5` textures is not known. If it does, their red and blue change on every platform with this PR. - A GPU: the bit layouts are checked against the Vulkan spec, not sampled on a device. - Screenshots of the same spot on main and here: they wait for the visual check, so the screenshots box is not in the checklist. - Not built on Windows or Linux. The change is a format constant and a swizzle loop, like the code around it. **Must be tested:** - Windows or Linux, any GPU, any driver: build this PR (head 1439e874) and main aa95d35f the same way, and take a screenshot of the same spots with the same settings on each (the title screen, the pause menu and its item screens, and a minute into a save, standing still), expecting each pair to match unless an `RGB5A1` or `RGB5` texture is on screen; where a pair differs, this PR's colours should look natural and main's show red and blue traded; post the pairs side by side, captioned with the build (by hand: test request T-FP8A) ## Risks **What could break:** medium: it changes colour on every platform, for `RGB5A1` and `RGB5` textures only, and no game run has looked at it yet. - Rendering is not affected. deko3d's table (`source/maxwell/format_traits.inc`) has no render-target format for `RGB5A1` or `RGB5`, so a game cannot render into them. A view of a rendered `BGR5A1` target as `RGB5A1` reads its bits in NVN's order: the swap gives exactly that. - Storage views and copies are unchanged: the image's bytes are still the storage's. - Landing order: rebased on main 6c9edffe (2026-11-14) without a conflict; main had changed `vk_texture.cpp` (#404) and `nx2/pipeline_states.py` (#563) away from this PR's lines. It conflicts with the format-mapping PR (#451, `feat/nvn-format-map`) in `vk_texture.cpp` (`texture_view`'s alpha list) and `nx2/pipeline_states.py` (`V1_NVN`), which both change; `vk_formats.cpp` merges. Checked with `git merge-tree` on both rebased heads. Whichever lands second takes the other's lines. **Turning it off:** revert the commit. **Compatibility:** nothing changes but the colours above. None of the changed files is in the translator stamp (`nx2/translator_files.py`), so no program is translated again; saves, the options file and the vk ABI are unchanged. ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `1439e874`, without a conflict; this PR's own commits, old → new: `295016f7` → `1439e874`; the commit names elsewhere in this description are from before the rebase. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). On macOS: 532 passed, 17 skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing. After the rebase onto main aa95d35f (head `1439e874`): wave S2's merge `67e25333`, 545/33/17 against main aa95d35f's 543/33/17, no new failure. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
fix(vk): NVN's RGB5A1 and RGB5 sample with red in bits 0-4
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
43b2cf1f45
NVN names packed formats from the lowest bit: RGB5A1 is red in bits 0-4, green 5-9, blue 10-14, alpha 15, as RGB565
(B5G6R5 in the same table) is red in bits 0-4 and as the WebGPU renderer decodes it (A1B5G5R5_UNORM). vk_format kept
RGB5A1 as A1R5G5B5 (red in bits 10-14), so red and blue sampled swapped, and RGB5 as B5G5R5A1 (red in bits 1-5), so
every component read a bit off. Vulkan's own A1B5G5R5 needs VK_KHR_maintenance5, which the renderer does not enable.

Both are now A1R5G5B5 (BGR5A1's layout), and texture_view's sampled view swaps red and blue back for them; RGB5's
alpha reads one, as the X formats' does. nx2/pipeline_states.py's V1_NVN drops B5G5R5A1, which no NVN format makes now.

tests/test_nvn_rgb5_order.py (new): every 16-bit texel of RGB5A1, RGB5, RGB565, BGR565 and RGBA4, decoded by the
Vulkan format's bit layout through texture_view's swizzle, reads what the WebGPU renderer's decode of the NVN format
reads, under three descriptor swizzles. Fails on main.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki force-pushed fix/nvn-rgb5-bit-order from 43b2cf1f45
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to 295016f707
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 14s
2026-11-15 01:52:04 +00:00
Compare
mooki changed title from WIP: fix(vk): NVN's RGB5A1 and RGB5 sample with red in bits 0-4 to fix(vk): NVN's RGB5A1 and RGB5 sample with red in bits 0-4 2026-11-15 03:33:57 +00:00
mooki force-pushed fix/nvn-rgb5-bit-order from 295016f707
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 14s
to 74656c09b8
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 7s
2026-11-15 07:01:43 +00:00
Compare
mooki force-pushed fix/nvn-rgb5-bit-order from 74656c09b8
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 7s
to 1439e87448
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
PR description / Description follows the template (pull_request_target) Successful in 5s
2026-11-15 13:02:59 +00:00
Compare
HumanBean left a comment

The change reads right for sampling: the image stays A1R5G5B5 (red in bits 10-14), and swapping the sources R and B in the sampled view's swizzle gives NVN's red-in-bits-0-4, under the game's own swizzle too. The test checks that well. One thing to settle before merge, below.

Question: an RGB5A1 or RGB5 texture that is also a render target. On main the colour attachment and the sampled view of such an image were consistent: write R, sample R. Now the attachment view still writes Vulkan R into bits 10-14, and the sampled view swaps, so red rendered into it is sampled as blue. NVN renders red into bits 0-4, so the memory contents also differ from the real GPU. A1R5G5B5 is a mandatory colour attachment format, so this is reachable. Does the game render to either format (the PR says it is not known whether it uses them at all)? If it might, either say in Risks that render-then-sample is swapped (and why that is acceptable), or take the swap out of the sampled view for images with the colour attachment usage. If neither format is ever a target, a line in Risks saying so is enough.

The change reads right for sampling: the image stays A1R5G5B5 (red in bits 10-14), and swapping the sources R and B in the sampled view's swizzle gives NVN's red-in-bits-0-4, under the game's own swizzle too. The test checks that well. One thing to settle before merge, below. Question: an `RGB5A1` or `RGB5` texture that is also a render target. On main the colour attachment and the sampled view of such an image were consistent: write R, sample R. Now the attachment view still writes Vulkan R into bits 10-14, and the sampled view swaps, so red rendered into it is sampled as blue. NVN renders red into bits 0-4, so the memory contents also differ from the real GPU. A1R5G5B5 is a mandatory colour attachment format, so this is reachable. Does the game render to either format (the PR says it is not known whether it uses them at all)? If it might, either say in Risks that render-then-sample is swapped (and why that is acceptable), or take the swap out of the sampled view for images with the colour attachment usage. If neither format is ever a target, a line in Risks saying so is enough.
@ -2849,1 +2849,4 @@
}
// (RGB5A1 and RGB5 hold red in bits 0-4 and blue in 10-14, and their image is A1R5G5B5, the other way round
// (vk_formats.cpp): what reads red reads the image's blue, and the other way)
if (nf == NVN_FORMAT_RGB5A1 || nf == NVN_FORMAT_RGB5)
Owner

Only sampled views swap. A texture used as a render target (attachment view, unswapped) and then sampled through this swizzle reads red as blue; main was self-consistent here. See the summary.

Only sampled views swap. A texture used as a render target (attachment view, unswapped) and then sampled through this swizzle reads red as blue; main was self-consistent here. See the summary.
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
PR description / Description follows the template (pull_request_target) Successful in 5s
This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/nvn-rgb5-bit-order:mooki-fix/nvn-rgb5-bit-order'
git switch 'mooki-fix/nvn-rgb5-bit-order'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!452
No description provided.