fix(vk): NVN's RGB5A1 and RGB5 sample with red in bits 0-4 #452
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!452
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/nvn-rgb5-bit-order"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
RGB5A1textures with red and blue swapped, andRGB5textures with everycomponent a bit off and alpha from the wrong bit. NVN names packed formats from the lowest bit:
RGB5A1is red in bits0-4, green 5-9, blue 10-14, alpha 15. Main keeps it as Vulkan's
A1R5G5B5, which has red in bits 10-14. This changescolours 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.pyagainst main's sources).vk_formats.cppandtexture_vieware the same code on every OS. Not seen in the game: whether it usesRGB5A1orRGB5textures is not known.Settings and switches: defaults.
Steps: in a checkout of main with this PR's
tests/test_nvn_rgb5_order.pycopied in, runpython -I -B tests/test_nvn_rgb5_order.py --cxx clang++ --headers <Vulkan headers>: every 16-bit texel is decoded bythe 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,BGR565andRGBA4: 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)): blueread as red. With
RGB5first (on main5e072fc8, before the rebase):RGB5 texel 000000, swizzle 2 3 4 5: component 3 reads 0, NVN 255: alpha 0 for 0.What changed
RGB5now uses the same Vulkan format asRGB5A1(A1R5G5B5), and a sampled view of either swaps red and blue in itsswizzle, so both read red from bits 0-4 as NVN defines it;
RGB5's alpha reads one. Storage views and copies areunchanged.
vk_formats.cppcases with the swap intexture_view.gpu/vk/vk_formats.cpp.RGB5A1staysA1R5G5B5_UNORM_PACK16. Vulkan's exact match,A1B5G5R5_UNORM_PACK16, needsVK_KHR_maintenance5, which the renderer does not enable.RGB5is nowA1R5G5B5_UNORM_PACK16too. It wasB5G5R5A1_UNORM_PACK16, which has red in bits 1-5 and alpha inbit 0.
gpu/vk/vk_texture.cpptexture_view.RGB5A1orRGB5swaps red and blue in its swizzle. The swap is on the swizzle's sources, so thegame's own swizzle (a descriptor's or a view's) still applies.
RGB5joins the X formats: its alpha reads one.nx2/pipeline_states.pyV1_NVNdropsB5G5R5A1(7), which no NVN format makes now, astests/test_pipeline_states.pyrequires. The shipped lists' check (tests/test_pipeline_state_sources.py) passes.tests/test_nvn_rgb5_order.py(new). It fails on main.vk_format,componentandtexture_view's sampled swizzle out ofgpu/vk, and the WebGPU renderer'sexpandandconvert_packed16out ofgpu/render/rx_texture.cpp. It compiles them with stubs.RGB5A1,RGB5,RGB565,BGR565andRGBA4is decoded twice. One decode follows theVulkan 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.cppnvn_pixel_format), with alpha one forRGB5, which theWebGPU renderer does not map. That is 983,040 reads under three descriptor swizzles. The two must agree.
RGB565,BGR565andRGBA4already pass on main. They show that the same reading of NVN's order holds for theformats main already gets right.
gpu/render/rx_nvn.cppmapsRGB5A1toA1B5G5R5_UNORM;gpu/render/rx_texture.cppconvert_packed16reads its red from bits 0-4), the same order asmain's own
RGB565mapping (B5G6R5), and deko3d's format table (A1B5G5R5with sources R,G,B,A; zlib, read forreference only: nothing copied).
VK_KHR_maintenance5,A1B5G5R5_UNORM_PACK16would 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
fixrow). The colour change in the game stillneeds a visual check (see Testing).
Method:
tests/test_nvn_rgb5_order.pyagainst main's sources (6c9edffe) and against this PR; the fullgame-free suite on both
How to reproduce:
python -I -B tests/test_nvn_rgb5_order.py --cxx clang++ --headers <Vulkan headers>6c9edffe295016f7tests/test_nvn_rgb5_order.pyRGB5A1reads blue as red;RGB5reads alpha 0 for 0tests/run_all.py --jobs 6)Screenshots: none yet: the visual check under Must be tested is still to do.
Flags and settings
None.
Testing
Tested:
tests/test_nvn_rgb5_order.pyfails on main and passes here.tests/test_pipeline_states.py,tests/test_pipeline_state_portable.py,tests/test_pipeline_state_sources.pyandtests/test_image_view_retry.pypass.test_nvn_rgb5_order, absent onmain, passes here.
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.recorder_sync_test.exe-2026-11-14-180744.ips).
gpu/vk/vk_formats.cppandvk_texture.cppwith the renderer's flags (nx2/build.py), Appleclang 21, arm64 and x86_64: all 8 build, 4 on main and 4 here.
environinvk_shader.cpp,posix_fallocateinpipeline_cache_file.h). These compiles include a compile-only prelude that declares those two names; it is part ofno tree.
aa95d35f, at1439e874(295016f7before the rebases ontoe090cc48andaa95d35f):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.pyon wave S2's merge67e25333, which carriesthis 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:
RGB5A1orRGB5textures is not known. If it does, their red and blue change on everyplatform with this PR.
checklist.
Must be tested:
1439e874) and mainaa95d35fthe same way, and take ascreenshot 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
RGB5A1orRGB5texture is onscreen; 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
RGB5A1andRGB5textures only, and no gamerun has looked at it yet.
source/maxwell/format_traits.inc) has no render-target format forRGB5A1orRGB5, so a game cannot render into them. A view of a renderedBGR5A1target asRGB5A1reads itsbits in NVN's order: the swap gives exactly that.
6c9edffe(2026-11-14) without a conflict; main had changedvk_texture.cpp(#404) andnx2/pipeline_states.py(#563) away from this PR's lines. It conflicts with the format-mapping PR (#451,feat/nvn-format-map) invk_texture.cpp(texture_view's alpha list) andnx2/pipeline_states.py(V1_NVN),which both change;
vk_formats.cppmerges. Checked withgit merge-treeon both rebased heads. Whichever landssecond 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
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head1439e874, without a conflict; this PR's owncommits, old → new:
295016f7→1439e874; the commit names elsewhere in this description are from before therebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). On macOS: 532 passed, 17skipped, and 32 failures that also fail on main
6c9edffeon this Mac, named under Testing. After the rebase onto mainaa95d35f(head1439e874): wave S2's merge67e25333, 545/33/17 against main aa95d35f's 543/33/17, no new failure.third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.43b2cf1f45295016f707WIP: fix(vk): NVN's RGB5A1 and RGB5 sample with red in bits 0-4to fix(vk): NVN's RGB5A1 and RGB5 sample with red in bits 0-4295016f70774656c09b874656c09b81439e87448The 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
RGB5A1orRGB5texture 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)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.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.