Class SpirvCombinedSamplerPairs
- Namespace
- ShadowDusk.Core.Reflection
- Assembly
- ShadowDusk.Core.dll
Derives, in exactly the order SPIRV-Cross declares them, the combined-sampler uniforms that the SPIR-V → GLSL transpile will emit — purely managed, straight from the SPIR-V words.
Why this exists. The MonoGame OpenGL runtime binds a texture unit to a sampler by
GLSL uniform NAME (glGetUniformLocation("ps_s{k}")), and MonoGameGlslRewriter
names those ps_s{k} in emitted-declaration order. SPIRV-Cross emits one combined
sampler per (texture, sampler) pair, so neither the reflected texture list nor the
reflected sampler list is the list the .mgfx sampler table has to mirror: N textures
read through one shared SamplerState is N uniforms but one reflected sampler, and the
mirror shape (one texture, N samplers) is N uniforms but one reflected texture. Keying the
table on either list silently binds the wrong texture (Phase 51 A7).
Why not just call SPIRV-Cross. The pass already computes this list, but
spvc_compiler_get_combined_image_samplers is not one of the 11 functions
src/ShadowDusk.Wasm/wwwroot/spirv-cross/spirv-cross.wasm exports, and that module is an
out-of-band emscripten build. Reading it natively would fix the desktop path only and break
the CLI-vs-WASM byte-identity promise for this shape. Deriving it from the SPIR-V is
host-independent by construction, the same reason RdefReader and
SpirvReflector exist.
The ordering rule is transcribed, not guessed (read from the pinned SPIRV-Cross
tree the WASM module is built from, tag vulkan-sdk-1.4.335.0):
Compiler::build_combined_image_samplers(spirv_cross.cpp) runstraverse_all_reachable_opcodesfrom the single entry-point function: blocks in binary order, ops in binary order, recursing into eachOpFunctionCalltarget with a parameter-to-argument remapping pushed for the callee's scope.- The only trigger is
OpSampledImage. Its image and sampler operands are resolved back to global variables throughremap_parameter→maybe_get_backing_variable(back throughOpLoad/OpAccessChain), and the pair is appended if not already present. So the order is first-use order, deduplicated — unrelated to declaration order, bind slots, or binding numbers. - The synthesized variable ids come from
ir.increase_bound_by(2), so they are monotonic in first-use order and sort after every original module id;CompilerGLSL::emit_resourceswalks variables in id order and skips every separate image/sampler whenvulkan_semanticsis off. Hence emitted declaration order IS first-use pair order.
The emitted GLSL cannot be used instead. SPIRV-Cross's readable
SPIRV_Cross_Combined<Image><Sampler> name is applied by its command-line
tool, not by the pass, so through the C API the uniforms arrive as bare _<id>
(uniform sampler2D _40;) carrying no pair identity at all.
Every shape this model does not cover fails loudly with SD0217 rather than
producing a plausible-but-wrong order, because a subtly wrong order is exactly the silent
mis-bind this class exists to eliminate.
public static class SpirvCombinedSamplerPairs
- Inheritance
-
SpirvCombinedSamplerPairs
- Inherited Members
Methods
Extract(ReadOnlyMemory<byte>)
Extracts the combined-sampler pairs in SPIRV-Cross declaration order. An empty list is a valid result (a shader that samples nothing).
public static Result<IReadOnlyList<CombinedSamplerPair>, ShaderError> Extract(ReadOnlyMemory<byte> spirvBlob)
Parameters
spirvBlobReadOnlyMemory<byte>
Returns
ResolveSlots(IReadOnlyList<CombinedSamplerPair>, IReadOnlyDictionary<string, int>?, IReadOnlySet<int>?)
The GL texture unit each pair must bind to, indexed by SPIRV-Cross declaration order —
the SINGLE definition of that rule, shared by the GLSL rewriter (which names the
uniforms ps_s{slot}) and the .mgfx sampler table (whose records must name
those same uniforms). Two callers deriving it independently is exactly how a table and
the GLSL it describes drift apart, so both go through here.
The rule, in one sentence: in texture-declaration order, a pair whose sampler
declared an explicit register takes it, and every other pair takes the lowest register not
already taken and not reserved by a modern SamplerState : register(sN).
That single rule reproduces every shape measured against the pinned mgfxc,
and it is why the legacy form is not a special case: compiling for OpenGL means
compiling at ps_3_0, where a texture and a sampler are ONE object in ONE register
namespace. A legacy sampler X : register(sN) IS that combined object, so it lands on
N. A modern SamplerState is a sampler-only object that still occupies its
register, so the combined samplers fxc synthesizes are allocated around it — which is why
one texture plus SamplerState S : register(s0) yields ps_s1, not
ps_s0.
Two pairs sharing one texture (the linear+point idiom) need no special handling
either: they share a declaration index, neither carries its own explicit register, and
pass 2 hands them consecutive free registers — the same 0/1 that shipped before issue #189.
That fallback is deliberate rather than derived: no mgfxc golden exists for the
shape, so the previous positional numbering is kept instead of inventing an answer.
It is pinned at record level, NOT by a render, and the distinction matters:
MonoGame 3.8.2's GL backend has no sampler objects and applies filtering with
glTexParameteri on the bound texture, so one texture object on two units
collapses to whichever filter was applied last — the shape is un-renderable as a visible
distinction in that runtime. The guarantees are held by
OpenGl_OneTextureTwoSamplers_BakesEachPairsOwnSamplerState and
OpenGl_OneTextureTwoSamplers_NamesEverySamplerUniformTheGlslDeclares.
validation/SamplerPairsGl arm B does not cover this shape (its fixture
SamplerPairMirror.fx uses two DISTINCT textures, and its identical pixels make it
blind to slot assignment anyway — the retracted claim behind issue #189).
public static IReadOnlyList<int> ResolveSlots(IReadOnlyList<CombinedSamplerPair> pairs, IReadOnlyDictionary<string, int>? explicitSlots = null, IReadOnlySet<int>? reservedSlots = null)
Parameters
pairsIReadOnlyList<CombinedSamplerPair>explicitSlotsIReadOnlyDictionary<string, int>reservedSlotsIReadOnlySet<int>