feat(vk): read texel formats MoltenVK cannot view through R8/R16/R32 carrier codecs #649

Closed
mooki wants to merge 3 commits from mooki/Longshot:mac/texel-codec into main
Member

Type: feat · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head; GPU probe on both CPU routes (history) · Needs: Windows, Linux

Why

On a Mac, MoltenVK 1.4.2 has no texel buffer view for R32G32B32_SFLOAT, the four R8G8B8 formats and five 16-bit packed
formats (R5G6B5, B5G6R5, A1R5G5B5, B5G5R5A1, A4B4G4R4). A shader that reads a texel buffer in one of those formats gets
nothing usable: MoltenVK binds no view, so it reads zeros. And a texel buffer of an integer format is read through a float
declaration, so its values come out wrong. This PR is the shader half of reading those formats anyway: the SPIR-V back end
reads and writes such a texel buffer through an R8, R16 or R32 UINT view of the same bytes (a carrier) and decodes and
encodes the format exactly in the shader, and it reads an integer format's own view as integers. It is on only where the
GPU lacks one of those formats, so on macOS today; other systems are unchanged unless asked. The views themselves come with
#663; until then the carrier formats still read what MoltenVK gives them today (zeros). It is part of the macOS port. See
#420.

What changed

Each texel buffer's codec becomes part of the program variant's answers, so a program is translated for the formats it is
bound to, as it already is for texture types. With a codec the back end declares the carrier image and calls a decode or
encode function made of integer operations only (tables of the exact floats for normalized formats, writes rounded to
nearest even from the float's bits), so MoltenVK's fast math cannot change a result. With no codec the SPIR-V is byte for byte
what it was. The gate is the device's own format query, per format, so a GPU that views a format keeps its native view.

  • back end, gpu/vk/eden/shader_recompiler/texel_codec.h (new): TexelCodec (Native, NativeUint, NativeSint, and the ten
    carrier codecs), the carrier element sizes, UnormFloatBits, SnormFloatBits.
  • back end, runtime_info.h and profile.h: TexelEntry {kind, desc, codec}, RuntimeInfo::texel_codecs,
    Profile::support_texel_codecs.
  • back end, backend/spirv/spirv_emit_context.{h,cpp}: DefineTexelCodecs checks the list (refused with LogicError
    without the profile bit, for Native, a typed image buffer or atomic, an array, a descriptor named twice or out of range)
    and makes the carrier images and the decode, encode and texel_write_imgbuf functions; whole-texel bounds (a read past the
    last texel is 0, a write past it dropped, the size the elements over k); the piece helpers of #648 take each descriptor's
    own image type, so codecs compose with pieces.
  • back end, emit_spirv_image.cpp, emit_spirv_image_atomic.cpp: texel fetches, image buffer reads and writes and size
    queries through the codec; typed accesses and atomics keep the view of their declared format.
  • renderer, gpu/vk/texel_codec.h (new): texel_codec::plan(), the rule the variants use now and the views (#663) next:
    gate off, today's view; typed storage, the declared format; viewable, its own view; D16/D32F/S8 aliases; a carrier; else
    a zero view.
  • renderer, gpu/vk/vk_shader.{h,cpp}: texel_buffer_features() (cached vkGetPhysicalDeviceFormatProperties),
    texel_codecs_on() (macOS: on when one of the ten formats lacks uniform and storage texel features; elsewhere off;
    RX_VK_TEXEL_CODECS forces it; it logs vk: texel codecs: on|off (…)), texel_plan(), texel_plan_native();
    texel_entries() before every stage's EmitSPIRV; a texel answer is desc | Program::kTexelAnswer with the codec in
    type, so the program cache and the pipeline state list carry it as they are; record_answers, still_valid,
    answers_fit, answer_handle, answer_of, canonical_answer, answer_stage_mask and program_find_plain handle both
    kinds; "txcd" in translator_stamp() when on, and RX_VK_TEXEL_CODECS in its switch list (and in
    nx2/bake_ds/bake_ds.cpp's copy).
  • renderer, gpu/vk/vk_progcache.cpp: kProgramCacheFormat 1 → 2 (a record's answers may hold texel codecs).
  • docs: the RX_VK_TEXEL_CODECS row in docs/development.md.
  • tests: tests/spirv_run.h (new), a small interpreter of the back end's integer subset, and tests/seg_shader_ir.cpp /
    tests/test_seg_shader.py: every codec plain and in pieces, validated, its decode and encode run on the CPU against C++
    references (every input of the 8- and 16-bit formats, all 2^24 texels of each RGB8 format, specials and 2^18 random texels
    for RGB32F, rounding boundaries and random floats for encode, round trips), whole entry points over texel buffers in
    memory bound whole and in 2 to 4 pieces (texels straddling cuts, an empty piece), and the GPU probe's module and vectors
    (--probe). Six source-reading checks follow the new code: test_texture_handle_banks, test_program_cache_one_variant,
    test_program_cache_cleanup, test_amd_subgroup_profiles, test_program_answer_domains (15 texel-answer checks
    added), test_state_list_plain; and test_program_stamp_knobs holds the new switch.

Measurements

Verdict: passes. With no codec the SPIR-V is byte for byte #648's and main's on both CPU routes; every codec is
bit-exact against its reference on the CPU, whole and in pieces; on the GPU (history, this head's modules byte for byte) the
carrier views and the codec SPIR-V gave the reference's bytes. No speed measured: no frame uses a codec until #663 binds the
views, and off macOS nothing is translated differently.

Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free: no GPU, no game)
Scene: none (an emitter change: its proof is the fixture's modules and CPU runs)
Method: tests/test_seg_shader.py at this head with --base-tree a git archive of #648's head (its back end: the plain,
segmented and arm E programs) and of main 6c9edffe (the plain programs), on arm64 and with --arch x86_64 (Rosetta). Every
output is a byte comparison, a hash or an exact CPU run, so one run per route and base; the two routes' 57 modules have
equal SHA-256.
How to reproduce: python -I -B tests/test_seg_shader.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --base-tree <git archive of the base> [--arch x86_64] [--probe <folder>]
Baseline: the middle column is this PR's base, #648 (mac/seg-shader), whose back end emits the segmented programs main
cannot.

Before: main @ 6c9edffe Before, its base #648: mac/seg-shader @ 9d48901a After: this PR @ c16eac09
plain modules, arm64 and x86_64 16 emitted 16 emitted 16 of 16 byte-identical to main's, both routes
plain, segmented and arm E modules — 32 emitted 32 of 32 byte-identical to #648's, both routes
modules passing spirv-val — 29 57 of 57 (12 codec, 12 codec in pieces, the probe, 32 others), both routes
codecs on the CPU against the references — — 10 of 10 carrier codecs bit-exact; 20 entry-point runs, 0 words differ, 0 writes outside a view
the profile bit alone, no codec — — 5 of 5 programs byte-identical
GPU probe module texel-probe.spv — — SHA-256 7eb2c1a5…, as TexelCodec pinned; its vectors identical
this PR @ c16eac09, arm64 (x86_64 the same lines):
profile bit: 5 of 5 programs the same, byte for byte, with support_texel_codecs and no codec
codec-rgb8_unorm functions: 16785664 decodes, 625648 encodes, 256 round trips, 292040096 instructions run on the CPU: all bit-exact against the reference
codecseg-rgb32_float entry point: 128 lanes over texel buffers in 3, 2 and 4 pieces (0: whole), 35605 instructions: 0 output words and 0 written elements differ, 0 writes outside a view, 0 reads past one
codec refusals: 8 lists that do not fit the program or the profile refused
array pointers: 57 of 57 modules pass no pointer into a buffer descriptor array to a function
base tree …/base-9d48901a (-DTEXEL_CODEC_BASE=1): 32 of 32 modules byte-identical (16 plain)
base tree …/base-6c9edffe (-DSEG_SHADER_BASE=1): 16 of 16 modules byte-identical (16 plain)
PASS: segmented descriptors and texel codecs through the real back end; every module validates

History, GPU (MoltenVK 1.4.2, M4 Max, arm64 and x86_64 under Rosetta), and why it holds for this head: TexelDescProbe
(reports/2026-11-14-texel-desc.md) bound texel-probe.spv (RGB32F read through an R32_UINT view, RGB8 read and written
through an R8_UINT view) through the renderer side's real carrier views (TD1), and codecseg-rgb32_float.spv over 3-, 2- and
4-piece R32_UINT arrays (TD2), pushed and in a set: the output and every written byte equalled the CPU reference, on both
routes (186 of 186 data checks; the run's one failure was another arm's push layout). This head emits both modules byte for
byte (7eb2c1a5…, d0956f62…).

Flags and settings

One new switch; by default it is on only on macOS, where the GPU lacks the formats.

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
RX_VK_TEXEL_CODECS 0 or 1: texel buffers of a format the GPU gives no texel buffer view read and written through an R8/R16/R32 UINT view with the decode and encode in the shader, and an integer format's own view read as integers (no switch: native views, float reads) unset: on on macOS when the format query lacks one of the ten formats (it does on MoltenVK 1.4.2), off on every other system; 1 on, 0 off, whatever the GPU on: those programs are translated with codecs (one more preparation of the shaders; "txcd" in the translator stamp) helps: macOS once #663 binds the carrier views; doesn't help: a GPU that views every format (it keeps native views per format); untested: =1 on Windows or Linux leave unset; =0 only to compare on a Mac

Testing

Tested: on macOS 26.7.1, Apple M4 Max, at c16eac09, this PR's head before the rebases onto e090cc48 and aa95d35f
(main 6c9edffe plus #648 plus this PR), game-free:

  • tests/test_seg_shader.py with --base-tree #648 and with --base-tree main, arm64 and x86_64 (Rosetta): exit 0 all
    four, as in Measurements; the 57 modules are byte-identical across the routes and to the macOS port's integration tree
    after NullShader and MslArrayPtr merged (7cd720da, bit off), and every module but #648's three fixed segmented storage
    modules equals the topic's original commit's (783eb189 on a4802cb5).
  • Renderer files that include vk_shader.h (vk_shader.cpp, vk_progcache.cpp, vk_draw.cpp, vk_predict.cpp,
    vk_replay.cpp, vk_programs.cpp) compiled for arm64 and x86_64 at this head, at #648 and on main 6c9edffe with main's
    renderer flags (nx2/build.py's vflags plus -arch, and main's non-Windows -fPIC -I host/posix -D_GNU_SOURCE). Every
    tree got #492's two Darwin fixes (environ, RUSAGE_THREAD) and a compile-only prelude declaring posix_fallocate and
    posix_fadvise, which main's renderer calls and macOS lacks (#476 gives them Darwin paths): 12 of 12 objects on each,
    exit 0.
  • The fourteen source-reading checks #648 and this PR update or that read the cache format, the record head and the stamp
    (tools/ls-suite.py named run): 12 pass, including test_program_stamp_knobs, test_program_cache_format and
    test_program_record_head. test_program_cache_cleanup and test_program_cache_one_variant cannot compile their fixture
    on this Mac, exactly as on main 6c9edffe (main's vk_progcache.cpp uses POSIX_FADV_WILLNEED; main's own macOS suite
    fails both the same way). With #476's two Darwin hunks (vk_progcache.cpp, pipeline_cache_file.h) applied to scratch
    copies of main, #648 and this head, both pass on all three (9 tests OK; the same variant for 9 picks).
  • Logs: reports/PubWave-files/PubShader/proof/p21-*.log, proof/fadv-*.log, compare-p21.txt, compile-*.out,
    named/, merge-tree.txt, stamps.txt.
  • After the rebase onto main aa95d35f, at c5cbd39d (c16eac09 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 B's merge db0e2e16, which carries
    this head: 569/32/18 (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 at this head: per-PR game runs are not planned, and the carrier views come with #663. A Mac whose
GPU views these formats (not this one). RX_VK_TEXEL_CODECS=1 on Windows or Linux. Writes are three element stores per RGB
texel, so another writer of the same texel may interleave (as the design says); no fixture races them. Windows and Linux:
not built or run there (12 changed renderer, back-end and bake files differ from #648's off Apple, unifdef -U__APPLE__); its
test request covers the build, run_all on Linux and a start.

Must be tested:

  • Linux and Windows, any GPU (Linux runs the SPIR-V test): build this PR (head c5cbd39d, it carries #648) and main
    aa95d35f the same way (this PR's every-platform files: the back end's emit_spirv_image.cpp,
    emit_spirv_image_atomic.cpp, spirv_emit_context.{h,cpp}, profile.h, runtime_info.h, the new texel_codec.h;
    the renderer's new gpu/vk/texel_codec.h, vk_shader.{h,cpp}, vk_progcache.cpp; nx2/bake_ds/bake_ds.cpp); on
    Linux run python -I -B tests/run_all.py, expecting tests/test_seg_shader.py to pass (Windows skips it); start each
    build to the title, expecting vk: texel codecs: off (formats with no texel buffer view on this GPU: …), the shader
    caches prepared again once (format 2, a new stamp) and nothing else to differ from main (by hand: test request T-98EX)

Risks

What could break: on macOS, with the gate on: a program with a texel buffer now records each one's codec as an answer,
and variant matching handles both kinds of answer; an integer-format texel buffer is now read as U32/S32 texels there. A
pipeline state list recorded on another system has Native answers, so on a Mac a gap format's variant is translated again
once at first use. Off macOS: one start-up log line and one cached format query of ten formats; translation, answers and
views are unchanged.

Turning it off: RX_VK_TEXEL_CODECS=0, or revert the commit.

Compatibility: every platform's shader caches are prepared again once: kProgramCacheFormat goes from 1 to 2, so the
old program cache is not read, and eden/ and vk_shader.cpp change the translator's stamp (40b414b30dad80e1 at #648,
c3bf927f4a1bb7f4 here). On macOS the stamp also takes "txcd" while codecs are on, so a cache made with codecs is not used
without them. Nothing changes in saves, longshot_options.ini or the vk ABI (MKWVK_API_VERSION).

Reviewer notes

  • Review c16eac09 (this PR's own first commit and head: one commit, -x from macos/seg-shader 783eb189): texel_codec.h (both),
    then DefineTexelCodecs in spirv_emit_context.cpp, then texel_codecs_on, texel_entries and record_answers in
    vk_shader.cpp.
  • Stacking: depends on #648 (SegEntry::Kind, the piece helpers, the test fixture); until #648 merges this PR's diff shows
    its two commits too. #663 binds the carrier views; #662, #674 and #676 build on this back end.
  • Conflicts with main: docs/development.md, one block: main's #444 (7494ce4e, Arc B-series native float adds) rewrote the
    RX_VK_NATIVE_FADD row beside which this PR adds RX_VK_TEXEL_CODECS; kept main's row and added this one after it. No
    semantic rework: main's changes to vk_shader.cpp and vk_progcache.cpp (#252, #321) are beside this PR's lines, and
    kProgramCacheFormat was still 1 on main.
  • Conflict with #648, resolved in the cherry-pick: tests/test_seg_shader.py (two blocks): #648's array pointer check and
    this PR's codec checks are both kept, the codec module check first.
  • With the open macOS PRs (#431, #437, #438, #479, #480, #490, #491, #492) and this wave's branches on main 6c9edffe:
    none (git merge-tree).
  • Design choices kept from the topic (reports/2026-11-14-texel-codec.md): the answer encoding desc | kTexelAnswer
    instead of a kind byte keeps vk_draw.cpp untouched; gpu/vk/texel_codec.h is not in the stamp's file list, since it
    only chooses answers, which the variants record and still_valid checks; bake_ds.cpp's older disk_stamp copy gets
    the switch name only.
  • Code comments cite reports/2026-11-14-*.md (the port's design notes), which are not in this repository.

Issues

See #420

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. (Test request T-98EX.)
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head c5cbd39d, on #648 9b73e829, without a
    conflict; this PR's own commits, old → new: c16eac09 → c5cbd39d; 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 the wave's merge of every macOS PR built with this one, 453eb678: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own except test_split_ub_flush, which #658's tree at the time broke and its head fixes; test_collect_wide now passes; the new tests pass, test_seg_shader among them, and test_darwin_audio_service, Darwin-only, is skipped by the runner; reports/PubWave-files/waveA/. After the rebase onto main aa95d35f (head c5cbd39d): wave B's merge db0e2e16, 569/32/18 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.
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.
**Type:** feat · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head; GPU probe on both CPU routes (history) · **Needs:** Windows, Linux ## Why On a Mac, MoltenVK 1.4.2 has no texel buffer view for R32G32B32_SFLOAT, the four R8G8B8 formats and five 16-bit packed formats (R5G6B5, B5G6R5, A1R5G5B5, B5G5R5A1, A4B4G4R4). A shader that reads a texel buffer in one of those formats gets nothing usable: MoltenVK binds no view, so it reads zeros. And a texel buffer of an integer format is read through a float declaration, so its values come out wrong. This PR is the shader half of reading those formats anyway: the SPIR-V back end reads and writes such a texel buffer through an R8, R16 or R32 UINT view of the same bytes (a carrier) and decodes and encodes the format exactly in the shader, and it reads an integer format's own view as integers. It is on only where the GPU lacks one of those formats, so on macOS today; other systems are unchanged unless asked. The views themselves come with #663; until then the carrier formats still read what MoltenVK gives them today (zeros). It is part of the macOS port. See #420. ## What changed Each texel buffer's codec becomes part of the program variant's answers, so a program is translated for the formats it is bound to, as it already is for texture types. With a codec the back end declares the carrier image and calls a decode or encode function made of integer operations only (tables of the exact floats for normalized formats, writes rounded to nearest even from the float's bits), so MoltenVK's fast math cannot change a result. With no codec the SPIR-V is byte for byte what it was. The gate is the device's own format query, per format, so a GPU that views a format keeps its native view. - back end, `gpu/vk/eden/shader_recompiler/texel_codec.h` (new): `TexelCodec` (Native, NativeUint, NativeSint, and the ten carrier codecs), the carrier element sizes, `UnormFloatBits`, `SnormFloatBits`. - back end, `runtime_info.h` and `profile.h`: `TexelEntry {kind, desc, codec}`, `RuntimeInfo::texel_codecs`, `Profile::support_texel_codecs`. - back end, `backend/spirv/spirv_emit_context.{h,cpp}`: `DefineTexelCodecs` checks the list (refused with `LogicError` without the profile bit, for `Native`, a typed image buffer or atomic, an array, a descriptor named twice or out of range) and makes the carrier images and the decode, encode and `texel_write_imgbuf` functions; whole-texel bounds (a read past the last texel is 0, a write past it dropped, the size the elements over k); the piece helpers of #648 take each descriptor's own image type, so codecs compose with pieces. - back end, `emit_spirv_image.cpp`, `emit_spirv_image_atomic.cpp`: texel fetches, image buffer reads and writes and size queries through the codec; typed accesses and atomics keep the view of their declared format. - renderer, `gpu/vk/texel_codec.h` (new): `texel_codec::plan()`, the rule the variants use now and the views (#663) next: gate off, today's view; typed storage, the declared format; viewable, its own view; D16/D32F/S8 aliases; a carrier; else a zero view. - renderer, `gpu/vk/vk_shader.{h,cpp}`: `texel_buffer_features()` (cached `vkGetPhysicalDeviceFormatProperties`), `texel_codecs_on()` (macOS: on when one of the ten formats lacks uniform and storage texel features; elsewhere off; `RX_VK_TEXEL_CODECS` forces it; it logs `vk: texel codecs: on|off (…)`), `texel_plan()`, `texel_plan_native()`; `texel_entries()` before every stage's `EmitSPIRV`; a texel answer is `desc | Program::kTexelAnswer` with the codec in `type`, so the program cache and the pipeline state list carry it as they are; `record_answers`, `still_valid`, `answers_fit`, `answer_handle`, `answer_of`, `canonical_answer`, `answer_stage_mask` and `program_find_plain` handle both kinds; "txcd" in `translator_stamp()` when on, and `RX_VK_TEXEL_CODECS` in its switch list (and in `nx2/bake_ds/bake_ds.cpp`'s copy). - renderer, `gpu/vk/vk_progcache.cpp`: `kProgramCacheFormat` 1 → 2 (a record's answers may hold texel codecs). - docs: the `RX_VK_TEXEL_CODECS` row in `docs/development.md`. - tests: `tests/spirv_run.h` (new), a small interpreter of the back end's integer subset, and `tests/seg_shader_ir.cpp` / `tests/test_seg_shader.py`: every codec plain and in pieces, validated, its decode and encode run on the CPU against C++ references (every input of the 8- and 16-bit formats, all 2^24 texels of each RGB8 format, specials and 2^18 random texels for RGB32F, rounding boundaries and random floats for encode, round trips), whole entry points over texel buffers in memory bound whole and in 2 to 4 pieces (texels straddling cuts, an empty piece), and the GPU probe's module and vectors (`--probe`). Six source-reading checks follow the new code: `test_texture_handle_banks`, `test_program_cache_one_variant`, `test_program_cache_cleanup`, `test_amd_subgroup_profiles`, `test_program_answer_domains` (15 texel-answer checks added), `test_state_list_plain`; and `test_program_stamp_knobs` holds the new switch. ## Measurements **Verdict:** passes. With no codec the SPIR-V is byte for byte #648's and main's on both CPU routes; every codec is bit-exact against its reference on the CPU, whole and in pieces; on the GPU (history, this head's modules byte for byte) the carrier views and the codec SPIR-V gave the reference's bytes. No speed measured: no frame uses a codec until #663 binds the views, and off macOS nothing is translated differently. Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free: no GPU, no game) Scene: none (an emitter change: its proof is the fixture's modules and CPU runs) Method: `tests/test_seg_shader.py` at this head with `--base-tree` a `git archive` of #648's head (its back end: the plain, segmented and arm E programs) and of main 6c9edffe (the plain programs), on arm64 and with `--arch x86_64` (Rosetta). Every output is a byte comparison, a hash or an exact CPU run, so one run per route and base; the two routes' 57 modules have equal SHA-256. How to reproduce: `python -I -B tests/test_seg_shader.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --base-tree <git archive of the base> [--arch x86_64] [--probe <folder>]` Baseline: the middle column is this PR's base, #648 (`mac/seg-shader`), whose back end emits the segmented programs main cannot. | | Before: main @ 6c9edffe | Before, its base #648: mac/seg-shader @ 9d48901a | After: this PR @ c16eac09 | |---|---|---|---| | plain modules, arm64 and x86_64 | 16 emitted | 16 emitted | 16 of 16 byte-identical to main's, both routes | | plain, segmented and arm E modules | — | 32 emitted | 32 of 32 byte-identical to #648's, both routes | | modules passing `spirv-val` | — | 29 | 57 of 57 (12 codec, 12 codec in pieces, the probe, 32 others), both routes | | codecs on the CPU against the references | — | — | 10 of 10 carrier codecs bit-exact; 20 entry-point runs, 0 words differ, 0 writes outside a view | | the profile bit alone, no codec | — | — | 5 of 5 programs byte-identical | | GPU probe module `texel-probe.spv` | — | — | SHA-256 `7eb2c1a5…`, as TexelCodec pinned; its vectors identical | ``` this PR @ c16eac09, arm64 (x86_64 the same lines): profile bit: 5 of 5 programs the same, byte for byte, with support_texel_codecs and no codec codec-rgb8_unorm functions: 16785664 decodes, 625648 encodes, 256 round trips, 292040096 instructions run on the CPU: all bit-exact against the reference codecseg-rgb32_float entry point: 128 lanes over texel buffers in 3, 2 and 4 pieces (0: whole), 35605 instructions: 0 output words and 0 written elements differ, 0 writes outside a view, 0 reads past one codec refusals: 8 lists that do not fit the program or the profile refused array pointers: 57 of 57 modules pass no pointer into a buffer descriptor array to a function base tree …/base-9d48901a (-DTEXEL_CODEC_BASE=1): 32 of 32 modules byte-identical (16 plain) base tree …/base-6c9edffe (-DSEG_SHADER_BASE=1): 16 of 16 modules byte-identical (16 plain) PASS: segmented descriptors and texel codecs through the real back end; every module validates ``` History, GPU (MoltenVK 1.4.2, M4 Max, arm64 and x86_64 under Rosetta), and why it holds for this head: TexelDescProbe (`reports/2026-11-14-texel-desc.md`) bound `texel-probe.spv` (RGB32F read through an R32_UINT view, RGB8 read and written through an R8_UINT view) through the renderer side's real carrier views (TD1), and `codecseg-rgb32_float.spv` over 3-, 2- and 4-piece R32_UINT arrays (TD2), pushed and in a set: the output and every written byte equalled the CPU reference, on both routes (186 of 186 data checks; the run's one failure was another arm's push layout). This head emits both modules byte for byte (`7eb2c1a5…`, `d0956f62…`). ### Flags and settings One new switch; by default it is on only on macOS, where the GPU lacks the formats. | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | `RX_VK_TEXEL_CODECS` | `0` or `1`: texel buffers of a format the GPU gives no texel buffer view read and written through an R8/R16/R32 UINT view with the decode and encode in the shader, and an integer format's own view read as integers | (no switch: native views, float reads) | unset: on on macOS when the format query lacks one of the ten formats (it does on MoltenVK 1.4.2), off on every other system; `1` on, `0` off, whatever the GPU | on: those programs are translated with codecs (one more preparation of the shaders; "txcd" in the translator stamp) | helps: macOS once #663 binds the carrier views; doesn't help: a GPU that views every format (it keeps native views per format); untested: `=1` on Windows or Linux | leave unset; `=0` only to compare on a Mac | ## Testing **Tested:** on macOS 26.7.1, Apple M4 Max, at `c16eac09`, this PR's head before the rebases onto e090cc48 and aa95d35f (main 6c9edffe plus #648 plus this PR), game-free: - `tests/test_seg_shader.py` with `--base-tree` #648 and with `--base-tree` main, arm64 and x86_64 (Rosetta): exit 0 all four, as in Measurements; the 57 modules are byte-identical across the routes and to the macOS port's integration tree after NullShader and MslArrayPtr merged (`7cd720da`, bit off), and every module but #648's three fixed segmented storage modules equals the topic's original commit's (`783eb189` on `a4802cb5`). - Renderer files that include `vk_shader.h` (`vk_shader.cpp`, `vk_progcache.cpp`, `vk_draw.cpp`, `vk_predict.cpp`, `vk_replay.cpp`, `vk_programs.cpp`) compiled for arm64 and x86_64 at this head, at #648 and on main 6c9edffe with main's renderer flags (`nx2/build.py`'s `vflags` plus `-arch`, and main's non-Windows `-fPIC -I host/posix -D_GNU_SOURCE`). Every tree got #492's two Darwin fixes (`environ`, `RUSAGE_THREAD`) and a compile-only prelude declaring `posix_fallocate` and `posix_fadvise`, which main's renderer calls and macOS lacks (#476 gives them Darwin paths): 12 of 12 objects on each, exit 0. - The fourteen source-reading checks #648 and this PR update or that read the cache format, the record head and the stamp (`tools/ls-suite.py` named run): 12 pass, including `test_program_stamp_knobs`, `test_program_cache_format` and `test_program_record_head`. `test_program_cache_cleanup` and `test_program_cache_one_variant` cannot compile their fixture on this Mac, exactly as on main 6c9edffe (main's `vk_progcache.cpp` uses `POSIX_FADV_WILLNEED`; main's own macOS suite fails both the same way). With #476's two Darwin hunks (`vk_progcache.cpp`, `pipeline_cache_file.h`) applied to scratch copies of main, #648 and this head, both pass on all three (9 tests OK; the same variant for 9 picks). - Logs: `reports/PubWave-files/PubShader/proof/p21-*.log`, `proof/fadv-*.log`, `compare-p21.txt`, `compile-*.out`, `named/`, `merge-tree.txt`, `stamps.txt`. - **After the rebase onto main aa95d35f**, at `c5cbd39d` (`c16eac09` 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 B's merge `db0e2e16`, which carries this head: 569/32/18 (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 at this head: per-PR game runs are not planned, and the carrier views come with #663. A Mac whose GPU views these formats (not this one). `RX_VK_TEXEL_CODECS=1` on Windows or Linux. Writes are three element stores per RGB texel, so another writer of the same texel may interleave (as the design says); no fixture races them. Windows and Linux: not built or run there (12 changed renderer, back-end and bake files differ from #648's off Apple, `unifdef -U__APPLE__`); its test request covers the build, run_all on Linux and a start. **Must be tested:** - Linux and Windows, any GPU (Linux runs the SPIR-V test): build this PR (head c5cbd39d, it carries #648) and main aa95d35f the same way (this PR's every-platform files: the back end's `emit_spirv_image.cpp`, `emit_spirv_image_atomic.cpp`, `spirv_emit_context.{h,cpp}`, `profile.h`, `runtime_info.h`, the new `texel_codec.h`; the renderer's new `gpu/vk/texel_codec.h`, `vk_shader.{h,cpp}`, `vk_progcache.cpp`; `nx2/bake_ds/bake_ds.cpp`); on Linux run `python -I -B tests/run_all.py`, expecting `tests/test_seg_shader.py` to pass (Windows skips it); start each build to the title, expecting `vk: texel codecs: off (formats with no texel buffer view on this GPU: …)`, the shader caches prepared again once (format 2, a new stamp) and nothing else to differ from main (by hand: test request T-98EX) ## Risks **What could break:** on macOS, with the gate on: a program with a texel buffer now records each one's codec as an answer, and variant matching handles both kinds of answer; an integer-format texel buffer is now read as U32/S32 texels there. A pipeline state list recorded on another system has Native answers, so on a Mac a gap format's variant is translated again once at first use. Off macOS: one start-up log line and one cached format query of ten formats; translation, answers and views are unchanged. **Turning it off:** `RX_VK_TEXEL_CODECS=0`, or revert the commit. **Compatibility:** every platform's shader caches are prepared again once: `kProgramCacheFormat` goes from 1 to 2, so the old program cache is not read, and `eden/` and `vk_shader.cpp` change the translator's stamp (`40b414b30dad80e1` at #648, `c3bf927f4a1bb7f4` here). On macOS the stamp also takes "txcd" while codecs are on, so a cache made with codecs is not used without them. Nothing changes in saves, `longshot_options.ini` or the vk ABI (`MKWVK_API_VERSION`). ## Reviewer notes - Review `c16eac09` (this PR's own first commit and head: one commit, `-x` from `macos/seg-shader` `783eb189`): `texel_codec.h` (both), then `DefineTexelCodecs` in `spirv_emit_context.cpp`, then `texel_codecs_on`, `texel_entries` and `record_answers` in `vk_shader.cpp`. - Stacking: depends on #648 (`SegEntry::Kind`, the piece helpers, the test fixture); until #648 merges this PR's diff shows its two commits too. #663 binds the carrier views; #662, #674 and #676 build on this back end. - Conflicts with main: `docs/development.md`, one block: main's #444 (`7494ce4e`, Arc B-series native float adds) rewrote the `RX_VK_NATIVE_FADD` row beside which this PR adds `RX_VK_TEXEL_CODECS`; kept main's row and added this one after it. No semantic rework: main's changes to `vk_shader.cpp` and `vk_progcache.cpp` (#252, #321) are beside this PR's lines, and `kProgramCacheFormat` was still 1 on main. - Conflict with #648, resolved in the cherry-pick: `tests/test_seg_shader.py` (two blocks): #648's array pointer check and this PR's codec checks are both kept, the codec module check first. - With the open macOS PRs (#431, #437, #438, #479, #480, #490, #491, #492) and this wave's branches on main 6c9edffe: none (`git merge-tree`). - Design choices kept from the topic (`reports/2026-11-14-texel-codec.md`): the answer encoding `desc | kTexelAnswer` instead of a `kind` byte keeps `vk_draw.cpp` untouched; `gpu/vk/texel_codec.h` is not in the stamp's file list, since it only chooses answers, which the variants record and `still_valid` checks; `bake_ds.cpp`'s older `disk_stamp` copy gets the switch name only. - Code comments cite `reports/2026-11-14-*.md` (the port's design notes), which are not in this repository. ## Issues See #420 ## 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. (Test request T-98EX.) - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `c5cbd39d`, on #648 `9b73e829`, without a conflict; this PR's own commits, old → new: `c16eac09` → `c5cbd39d`; 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 the wave's merge of every macOS PR built with this one, `453eb678`: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own except `test_split_ub_flush`, which #658's tree at the time broke and its head fixes; `test_collect_wide` now passes; the new tests pass, `test_seg_shader` among them, and `test_darwin_audio_service`, Darwin-only, is skipped by the runner; `reports/PubWave-files/waveA/`. After the rebase onto main aa95d35f (head `c5cbd39d`): wave B's merge `db0e2e16`, 569/32/18 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] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated.
A guest span larger than one device allocation (the cap on MoltenVK/M4 Max is 41,747,087,360 bytes) is bound as
pieces: one descriptor per allocation segment, at the descriptor's own binding (reports/2026-11-14-full-range-design.md
sections 4.1-4.4). This is the shader half; the draw side (descriptors, layout, gathers) comes later.

- runtime_info.h: SegEntry {Storage|Table|TexBuf|ImgBuf, desc, pieces} and RuntimeInfo::segmented.
- Back end: a segmented storage buffer is an array of pieces at its binding (a table count * pieces, entry k's piece j
  at k * pieces + j); its loads, stores and atomics call DontInline helpers made after main, which find the piece by
  the pieces' OpArrayLength in order, with constant indices. The global memory fallbacks sum the pieces' lengths for a
  segmented descriptor's bound end and branch to its piece (single, native candidate, table written out and in the
  loop). Texel and image buffers are arrays too, their fetches, reads, writes and atomics through helpers that pick
  the piece by OpImageQuerySize, and a texel buffer's size query sums the pieces. ImageQuery was declared for every
  module already. With RuntimeInfo::segmented empty nothing new is made: the SPIR-V is byte-identical.
- vk_shader: SegSig (entries, hash) on Program, seg_sig and seg_variant; program_segmented finds or translates the
  variant of a plain program with plain's texture questions and answers and checks its descriptors are plain's;
  program_pick, mesh_program_locked, program_find_plain, loaded_variants and portable_variant keep the plain and
  segmented variants apart; every stage's RuntimeInfo::segmented comes from Program::seg.
- vk_progcache: a segmented variant is never written to the program cache nor listed among the loaded programs.
- tests/test_seg_shader.py + tests/seg_shader_ir.cpp: synthetic IR programs through the real back end, plain and
  segmented; structure checks, spirv-val (Vulkan 1.3, uniform buffer standard layout), and with --base-tree a byte
  comparison of the plain modules against an older tree. Five source-reading checks follow the new Program field and
  the atomics' new branch.

The translator stamp changes with this (eden/, vk_shader.cpp, sizeof(Program)): every program cache is rebuilt once.
Land it in the same release as the vk_api.h change that already does so.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit b7928876dab846485f667c1c934d34d25ecef035)
fix(vk): MslArrayPtr: no pointer into a storage buffer array is passed to a function
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
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 4s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
9d48901abf
On MoltenVK a segmented storage program with a compare-exchange loop (an
increment, decrement or float add the device does not have) failed pipeline
creation: NullProbe revision 02's NS2, "no matching function for call",
&ssbo0_4[0u] given to a device _66 * parameter. SPIRV-Cross's MSL (6c09849f,
MoltenVK 1.4.2) passes a storage buffer pointer to a function by value, and
takes the address of the argument's expression: for an element of a buffer
array, which MSL holds as an array of pointers, that is the address of the
pointer (reports/2026-11-14-msl-arrayptr.md).

A descriptor bound as pieces now has its loops written out in the piece,
inside the segmented helper (CasLoopHere, CasStoreHere: CasLoop's loop,
attempt for attempt, its variables made first in the helper), instead of
calling the CasLoop function or write_storage_cas_loop_func with a pointer
into the pieces' array. Every other program is as before byte for byte.

Tests: test_seg_shader.py checks that no module passes a pointer into a
buffer descriptor array to a function. The two checks that cut the back
end's source follow the new loop and parameter.

Ported from macos/msl-arrayptr, where it was committed on NullShader, to the
pieces alone: here every storage access that reaches a segmented helper is
in pieces, so the loops are always written out. Left for the NullShader
change: the bound-whole branch that still calls the function with its
block, the guarded helper's cas_loop, and the tests that need NullShader or
tests/spirv_run.h (the CPU runs of the loops and of a storage buffer table,
--changed-segmented and the *.cpurun comparison).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bd396010887afab9520181009db1b56a4cc425e3)
feat(vk): texel codecs in the SPIR-V back end and program variants
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
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 4s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
c16eac09d9
A texel buffer whose format the GPU gives no texel buffer view (MoltenVK 1.4.2 on Apple GPUs: R32G32B32_SFLOAT, the
R8G8B8 formats and the 16-bit packed ones) is read and written through an R8, R16 or R32 UINT view of the same bytes,
with the format's decode and encode in the shader (reports/2026-11-14-texel-formats-design.md section 4).

- Back end: RuntimeInfo::texel_codecs (TexelEntry: a texture or image buffer descriptor and its Shader::TexelCodec),
  honoured with Profile::support_texel_codecs only. Carrier images, per-codec decode and encode functions of integer
  operations alone (normalized components through tables of the exact floats, writes rounded to nearest even from the
  float's bits), whole-texel bounds (reads past the last texel 0, writes dropped, the size the elements over k), an
  integer format's own view read as U32 or S32 texels, and composition with the segmented piece arrays (the helpers
  take each descriptor's own image type). Typed storage accesses and atomics keep the view of their declared format:
  a codec there is refused. With no codec the SPIR-V is byte for byte as before.
- Renderer: gpu/vk/texel_codec.h, the plan shared by the variants and (next) the views; texel_codecs_on (macOS, from
  the format query, RX_VK_TEXEL_CODECS); each texel buffer's codec a variant answer (desc | Program::kTexelAnswer, so
  the program cache and the pipeline state list carry it as they are), checked by still_valid; "txcd" in the
  translator stamp when on; program cache format 2.
- Tests: tests/seg_shader_ir.cpp and tests/spirv_run.h emit every codec plain and in pieces, validate it, run its
  decode and encode on the CPU against reference conversions (every input of the 8- and 16-bit formats, random and
  boundary sets for 32-bit) and run its entry point over texel buffers in memory; the GPU probe's codec module.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 783eb1898b91d505ff22a31f337256c5047d0177)
mooki changed title from WIP: feat(vk): read texel formats MoltenVK cannot view through R8/R16/R32 carrier codecs to feat(vk): read texel formats MoltenVK cannot view through R8/R16/R32 carrier codecs 2026-11-15 05:25:35 +00:00
mooki force-pushed mac/texel-codec from c16eac09d9
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
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 4s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
to b78e421a97
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 15s
CI / Workflow lint (actionlint) (pull_request) Successful in 23s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 20s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 5s
2026-11-15 07:16:38 +00:00
Compare
mooki force-pushed mac/texel-codec from b78e421a97
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 15s
CI / Workflow lint (actionlint) (pull_request) Successful in 23s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 20s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 5s
to c5cbd39d26
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 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
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 11s
PR description / Description follows the template (pull_request_target) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-15 13:10:04 +00:00
Compare
Author
Member

Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)

Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
mooki closed this pull request 2026-11-15 18:07:53 +00:00
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 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
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 11s
PR description / Description follows the template (pull_request_target) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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!649
No description provided.