Summary
Pixely's texture/sampler register convention cannot be carried into WGSL by any Slang flag. SDL GPU wants a sampled texture and its sampler on one Vulkan binding and on two consecutive WebGPU bindings, and no register-to-binding mapping produces both from one set of annotations. Recording the constraint before a WGSL target is attempted.
There is no WGSL target today, and nothing is broken.
Where the two layouts diverge
Pixely pairs a texture and its sampler on the same register index and space, matching SDL GPU's descriptor layout — e.g. src/Pixely.Ui/Content/shaders/ui_quad.slang:
Texture2D<float4> texture : register(t0, space2);
SamplerState textureSampler : register(s0, space2);
On Vulkan that pair is one descriptor: SDL declares binding k of the read-only set as VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER (SDL_gpu_vulkan.c, VULKAN_INTERNAL_CreateDescriptorSetLayout), and Vulkan lets a shader read it as a separate sampled image and sampler at the same set/binding. D3D12 and Metal keep t and s in separate namespaces, so nothing collides there either.
WebGPU has one binding namespace per group, so the same pair needs two bindings.
What an SDL WebGPU backend would require
SDL has no WebGPU backend and no SDL_GPU_SHADERFORMAT_WGSL in main today. The only implementation is an open, experimental pull request against SDL (libsdl-org/SDL#16020). Per its SDL_CreateGPUShader documentation and src/gpu/webgpu/SDL_gpu_webgpu.c:
- Groups map straight onto Pixely's spaces — vertex 0/1, fragment 2/3, compute 0/1/2. No change needed.
- Texture k at
@binding(2k), its sampler at @binding(2k+1), contiguous from 0, then storage textures, then storage buffers. WEBGPU_INTERNAL_CreateBindGroup builds bind groups with exactly that interleave.
- The backend infers the whole bind group layout by string-scanning the WGSL source (
WEBGPU_INTERNAL_ParseBindGroupLayoutEntriesFromShader), so the WGSL text is the layout metadata.
- Sampling a depth texture needs its comment macro
//SDLGPU_ForceAllowSamplingForTexture(group, binding) emitted into the WGSL.
Treat all of this as a moving target until that work is merged.
Why no Slang flag gets there
Shifts are additive: binding = shift + index. The layout above needs stride 2 with the sampler offset by one — t_k -> 2k, s_k -> 2k+1. No pair of constants satisfies that for more than one texture.
A per-space shift set therefore produces valid WGSL that SDL still rejects, because the bindings have gaps and the wrong order:
-fvk-b-shift 0 <space> -fvk-t-shift 10 <space> -fvk-s-shift 20 <space> -fvk-u-shift 30 <space>
@binding(10) @group(2) texture @binding(20) @group(2) textureSampler
Explicit interleaved bindings do produce the required layout exactly:
[[vk::binding(0, 2)]] Texture2D<float4> albedo; // @group(2) @binding(0)
[[vk::binding(1, 2)]] SamplerState albedoSampler; // @group(2) @binding(1)
[[vk::binding(2, 2)]] Texture2D<float4> mask; // @group(2) @binding(2)
[[vk::binding(3, 2)]] SamplerState maskSampler; // @group(2) @binding(3)
…and simultaneously break SPIR-V: albedoSampler lands on set 2 binding 1, where SDL's Vulkan backend has only one combined image sampler at binding 0. vk::binding applies to every Vulkan-flavoured target, so one source cannot carry both layouts.
Declaring the pair as a combined Sampler2D doesn't rescue it either. It gives a real combined image sampler on SPIR-V and clean WGSL for one texture, but with two the split halves collide again (albedo's sampler and mask's texture both at @binding(1)), the DXIL sampler drops to space0 unless a second register semantic is written, and the reflection parser would need to handle "combined": true for num_samplers.
What adding a WGSL target would need
- Per-target binding assignment. Options, in the order I'd try them:
- Rewrite the
@binding numbers in the emitted WGSL, computing the interleave from the resource list the compiler already builds. NormalizeMetalBufferBindings is precedent for the same shape of fix.
- Per-target
-D plus macro-based resource declarations in shader sources — works, but leaks the target into user code.
- Upstream Slang support for a WGSL binding mode matching SDL GPU.
ShaderFormatDto.Wgsl through ShaderFormatDto, the JSON metadata, runtime shader-format conversion, cache validation/cleanup, binding validation and the shader tests. Binding numbers do not need to reach the metadata — SDL reads them out of the WGSL.
Not the shader build's warnings
An earlier reading of this blamed -warnings-disable 39001,... in SdlangCompiler, on the theory that Slang's "explicit binding overlap" warning was the signal a WGSL target would need. It isn't. #482 removed that suppression by stating the register-to-binding mapping instead (a zero shift per register class), and stating the mapping silences the warning for the texture/sampler pairing on every target, WGSL included. Pixely's own ValidateSamplerTexturePairings and index-ordering checks already reject a genuine duplicate with a better message.
Environment
- Slang 2026.17 via
SlangDxcBundle.Toolchain 2026.17.0
- SDL upstream
main as of 2026-09-09; the WebGPU work read at e56d6ca
Summary
Pixely's texture/sampler register convention cannot be carried into WGSL by any Slang flag. SDL GPU wants a sampled texture and its sampler on one Vulkan binding and on two consecutive WebGPU bindings, and no register-to-binding mapping produces both from one set of annotations. Recording the constraint before a WGSL target is attempted.
There is no WGSL target today, and nothing is broken.
Where the two layouts diverge
Pixely pairs a texture and its sampler on the same register index and space, matching SDL GPU's descriptor layout — e.g.
src/Pixely.Ui/Content/shaders/ui_quad.slang:On Vulkan that pair is one descriptor: SDL declares binding k of the read-only set as
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER(SDL_gpu_vulkan.c,VULKAN_INTERNAL_CreateDescriptorSetLayout), and Vulkan lets a shader read it as a separate sampled image and sampler at the same set/binding. D3D12 and Metal keeptandsin separate namespaces, so nothing collides there either.WebGPU has one binding namespace per group, so the same pair needs two bindings.
What an SDL WebGPU backend would require
SDL has no WebGPU backend and no
SDL_GPU_SHADERFORMAT_WGSLinmaintoday. The only implementation is an open, experimental pull request against SDL (libsdl-org/SDL#16020). Per itsSDL_CreateGPUShaderdocumentation andsrc/gpu/webgpu/SDL_gpu_webgpu.c:@binding(2k), its sampler at@binding(2k+1), contiguous from 0, then storage textures, then storage buffers.WEBGPU_INTERNAL_CreateBindGroupbuilds bind groups with exactly that interleave.WEBGPU_INTERNAL_ParseBindGroupLayoutEntriesFromShader), so the WGSL text is the layout metadata.//SDLGPU_ForceAllowSamplingForTexture(group, binding)emitted into the WGSL.Treat all of this as a moving target until that work is merged.
Why no Slang flag gets there
Shifts are additive:
binding = shift + index. The layout above needs stride 2 with the sampler offset by one —t_k -> 2k,s_k -> 2k+1. No pair of constants satisfies that for more than one texture.A per-space shift set therefore produces valid WGSL that SDL still rejects, because the bindings have gaps and the wrong order:
Explicit interleaved bindings do produce the required layout exactly:
…and simultaneously break SPIR-V:
albedoSamplerlands on set 2 binding 1, where SDL's Vulkan backend has only one combined image sampler at binding 0.vk::bindingapplies to every Vulkan-flavoured target, so one source cannot carry both layouts.Declaring the pair as a combined
Sampler2Ddoesn't rescue it either. It gives a real combined image sampler on SPIR-V and clean WGSL for one texture, but with two the split halves collide again (albedo's sampler and mask's texture both at@binding(1)), the DXIL sampler drops tospace0unless a second register semantic is written, and the reflection parser would need to handle"combined": truefornum_samplers.What adding a WGSL target would need
@bindingnumbers in the emitted WGSL, computing the interleave from the resource list the compiler already builds.NormalizeMetalBufferBindingsis precedent for the same shape of fix.-Dplus macro-based resource declarations in shader sources — works, but leaks the target into user code.ShaderFormatDto.WgslthroughShaderFormatDto, the JSON metadata, runtime shader-format conversion, cache validation/cleanup, binding validation and the shader tests. Binding numbers do not need to reach the metadata — SDL reads them out of the WGSL.Not the shader build's warnings
An earlier reading of this blamed
-warnings-disable 39001,...inSdlangCompiler, on the theory that Slang's "explicit binding overlap" warning was the signal a WGSL target would need. It isn't. #482 removed that suppression by stating the register-to-binding mapping instead (a zero shift per register class), and stating the mapping silences the warning for the texture/sampler pairing on every target, WGSL included. Pixely's ownValidateSamplerTexturePairingsand index-ordering checks already reject a genuine duplicate with a better message.Environment
SlangDxcBundle.Toolchain 2026.17.0mainas of 2026-09-09; the WebGPU work read ate56d6ca