feat(vk): read texel formats MoltenVK cannot view through R8/R16/R32 carrier codecs #649
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!649
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/texel-codec"
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: 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.
gpu/vk/eden/shader_recompiler/texel_codec.h(new):TexelCodec(Native, NativeUint, NativeSint, and the tencarrier codecs), the carrier element sizes,
UnormFloatBits,SnormFloatBits.runtime_info.handprofile.h:TexelEntry {kind, desc, codec},RuntimeInfo::texel_codecs,Profile::support_texel_codecs.backend/spirv/spirv_emit_context.{h,cpp}:DefineTexelCodecschecks the list (refused withLogicErrorwithout 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_imgbuffunctions; whole-texel bounds (a read past thelast 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.
emit_spirv_image.cpp,emit_spirv_image_atomic.cpp: texel fetches, image buffer reads and writes and sizequeries through the codec; typed accesses and atomics keep the view of their declared format.
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.
gpu/vk/vk_shader.{h,cpp}:texel_buffer_features()(cachedvkGetPhysicalDeviceFormatProperties),texel_codecs_on()(macOS: on when one of the ten formats lacks uniform and storage texel features; elsewhere off;RX_VK_TEXEL_CODECSforces it; it logsvk: texel codecs: on|off (…)),texel_plan(),texel_plan_native();texel_entries()before every stage'sEmitSPIRV; a texel answer isdesc | Program::kTexelAnswerwith the codec intype, 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_maskandprogram_find_plainhandle bothkinds; "txcd" in
translator_stamp()when on, andRX_VK_TEXEL_CODECSin its switch list (and innx2/bake_ds/bake_ds.cpp's copy).gpu/vk/vk_progcache.cpp:kProgramCacheFormat1 → 2 (a record's answers may hold texel codecs).RX_VK_TEXEL_CODECSrow indocs/development.md.tests/spirv_run.h(new), a small interpreter of the back end's integer subset, andtests/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 checksadded),
test_state_list_plain; andtest_program_stamp_knobsholds 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.pyat this head with--base-treeagit archiveof #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). Everyoutput 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 maincannot.
6c9edffe9d48901ac16eac09spirv-valtexel-probe.spv7eb2c1a5…, as TexelCodec pinned; its vectors identicalHistory, 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) boundtexel-probe.spv(RGB32F read through an R32_UINT view, RGB8 read and writtenthrough an R8_UINT view) through the renderer side's real carrier views (TD1), and
codecseg-rgb32_float.spvover 3-, 2- and4-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.
RX_VK_TEXEL_CODECS0or1: 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 integers1on,0off, whatever the GPU=1on Windows or Linux=0only to compare on a MacTesting
Tested: on macOS 26.7.1, Apple M4 Max, at
c16eac09, this PR's head before the rebases ontoe090cc48andaa95d35f(main
6c9edffeplus #648 plus this PR), game-free:tests/test_seg_shader.pywith--base-tree#648 and with--base-treemain, arm64 and x86_64 (Rosetta): exit 0 allfour, 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 storagemodules equals the topic's original commit's (
783eb189ona4802cb5).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 main6c9edffewith main'srenderer flags (
nx2/build.py'svflagsplus-arch, and main's non-Windows-fPIC -I host/posix -D_GNU_SOURCE). Everytree got #492's two Darwin fixes (
environ,RUSAGE_THREAD) and a compile-only prelude declaringposix_fallocateandposix_fadvise, which main's renderer calls and macOS lacks (#476 gives them Darwin paths): 12 of 12 objects on each,exit 0.
(
tools/ls-suite.pynamed run): 12 pass, includingtest_program_stamp_knobs,test_program_cache_formatandtest_program_record_head.test_program_cache_cleanupandtest_program_cache_one_variantcannot compile their fixtureon this Mac, exactly as on main
6c9edffe(main'svk_progcache.cppusesPOSIX_FADV_WILLNEED; main's own macOS suitefails both the same way). With #476's two Darwin hunks (
vk_progcache.cpp,pipeline_cache_file.h) applied to scratchcopies of main, #648 and this head, both pass on all three (9 tests OK; the same variant for 9 picks).
reports/PubWave-files/PubShader/proof/p21-*.log,proof/fadv-*.log,compare-p21.txt,compile-*.out,named/,merge-tree.txt,stamps.txt.aa95d35f, atc5cbd39d(c16eac09before 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 B's mergedb0e2e16, which carriesthis 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=1on Windows or Linux. Writes are three element stores per RGBtexel, 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__); itstest request covers the build, run_all on Linux and a start.
Must be tested:
c5cbd39d, it carries #648) and mainaa95d35fthe same way (this PR's every-platform files: the back end'semit_spirv_image.cpp,emit_spirv_image_atomic.cpp,spirv_emit_context.{h,cpp},profile.h,runtime_info.h, the newtexel_codec.h;the renderer's new
gpu/vk/texel_codec.h,vk_shader.{h,cpp},vk_progcache.cpp;nx2/bake_ds/bake_ds.cpp); onLinux run
python -I -B tests/run_all.py, expectingtests/test_seg_shader.pyto pass (Windows skips it); start eachbuild to the title, expecting
vk: texel codecs: off (formats with no texel buffer view on this GPU: …), the shadercaches 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:
kProgramCacheFormatgoes from 1 to 2, so theold program cache is not read, and
eden/andvk_shader.cppchange the translator's stamp (40b414b30dad80e1at #648,c3bf927f4a1bb7f4here). On macOS the stamp also takes "txcd" while codecs are on, so a cache made with codecs is not usedwithout them. Nothing changes in saves,
longshot_options.inior the vk ABI (MKWVK_API_VERSION).Reviewer notes
c16eac09(this PR's own first commit and head: one commit,-xfrommacos/seg-shader783eb189):texel_codec.h(both),then
DefineTexelCodecsinspirv_emit_context.cpp, thentexel_codecs_on,texel_entriesandrecord_answersinvk_shader.cpp.SegEntry::Kind, the piece helpers, the test fixture); until #648 merges this PR's diff showsits two commits too. #663 binds the carrier views; #662, #674 and #676 build on this back end.
docs/development.md, one block: main's #444 (7494ce4e, Arc B-series native float adds) rewrote theRX_VK_NATIVE_FADDrow beside which this PR addsRX_VK_TEXEL_CODECS; kept main's row and added this one after it. Nosemantic rework: main's changes to
vk_shader.cppandvk_progcache.cpp(#252, #321) are beside this PR's lines, andkProgramCacheFormatwas still 1 on main.tests/test_seg_shader.py(two blocks): #648's array pointer check andthis PR's codec checks are both kept, the codec module check first.
6c9edffe:none (
git merge-tree).reports/2026-11-14-texel-codec.md): the answer encodingdesc | kTexelAnswerinstead of a
kindbyte keepsvk_draw.cppuntouched;gpu/vk/texel_codec.his not in the stamp's file list, since itonly chooses answers, which the variants record and
still_validchecks;bake_ds.cpp's olderdisk_stampcopy getsthe switch name only.
reports/2026-11-14-*.md(the port's design notes), which are not in this repository.Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), headc5cbd39d, on #6489b73e829, without aconflict; this PR's own commits, old → new:
c16eac09→c5cbd39d; the commit names elsewhere in this descriptionare from before the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis 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 excepttest_split_ub_flush, which #658's tree at the time broke and its head fixes;test_collect_widenow passes; the new tests pass,test_seg_shaderamong them, andtest_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/waveA/. After the rebase onto mainaa95d35f(headc5cbd39d): wave B's mergedb0e2e16, 569/32/18 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.tests/that reads changed source updated in this PR, not weakened.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)WIP: feat(vk): read texel formats MoltenVK cannot view through R8/R16/R32 carrier codecsto feat(vk): read texel formats MoltenVK cannot view through R8/R16/R32 carrier codecsc16eac09d9b78e421a97b78e421a97c5cbd39d26Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed