Campaign V slice V6e, second of three. Billboard particles and mesh particles
are the last two pairs blocked on the texture-table shape; sky follows.
particle takes the same treatment mesh_modern took: the `flat uvec2` handle
varying becomes a `flat uint` slot and the fragment stage samples through
ACDREAM_SAMPLE_ARRAY. What is different here is the untextured particle. The
shader used to ask "is the handle I was given zero", which GL can answer because
its emulated table stores handles; Vulkan cannot, because set 2 is an opaque
descriptor array and reading an element nobody wrote is undefined rather than
zero. So the question moves to the index: the CPU writes ACDREAM_TEXTURE_NONE
for a particle with no texture instead of interning the null handle as a table
slot, and both dialects test the same value. GL renders identically — the same
particles take the same branch to the same procedural blob — and the handle
table simply stops carrying an entry that never named a texture. A test pins the
sentinel across all three declarations of it, because a silent disagreement here
would sample slot 0xFFFFFFFF instead of drawing the blob.
particle_mesh needed no restructuring, only names. Vulkan GLSL has no default
uniform block, so `uniform uint uTextureIndex;` is not unsupported but
unspellable, and the two values are per-pass — one texture and one layer for a
whole sub-batch — which is exactly what the shared push-constant block is for.
uTextureIndex becomes uTextureIndexA; uTextureLayer becomes uParamA, which was
the spare scalar and is a natural fit because the shader converted the layer to
float anyway. The widening moved from the shader to the CPU; layers are small
integers, so the sampled value is bit-identical.
Gates: Release build clean; App tests 4,058 passed / 3 skipped (baseline 4,057
plus the sentinel drift guard). Offline pixel gate against 95f8c25f: two
captures, 29 px and 21 px of 563,200 compared (3.73e-05 and 5.15e-05), with a
same-commit control between them of 13 px and this commit measuring 14 px
against its own parent. The scene draws no particles, so this gate is a tripwire
that the world path is undisturbed, not evidence about particles.
Particles remain user-gate debt — the same debt V2c and V4e already carry, to be
paid by casting a spell in a connected session.
Manifest: 6/9 pairs compile. Remaining: mesh (legacy, no consumer), sky (next
commit), terrain_modern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
26 lines
1 KiB
GLSL
26 lines
1 KiB
GLSL
#version 430 core
|
|
#extension GL_ARB_bindless_texture : require
|
|
|
|
in vec2 vTexCoord;
|
|
in vec4 vColor;
|
|
out vec4 fragColor;
|
|
|
|
// Campaign V slice V2c (2026-07-27): was uvec2 uTextureHandle (a raw
|
|
// ARB_bindless_texture handle); now a slot into the binding=9 handle table.
|
|
//
|
|
// Slice V6e renamed both onto members of the shared 96-byte push-constant block
|
|
// (GpuPushConstants), which is the only home Vulkan has for a loose uniform:
|
|
// there is no default uniform block, so `uniform uint uTextureIndex;` is not
|
|
// merely unsupported, it is unspellable. Both values are per-pass — one texture,
|
|
// one layer, for a whole mesh-particle sub-batch — which is exactly what the
|
|
// block is for. uParamA carries the layer because it is consumed as a float
|
|
// anyway; the CPU writes the same integral value it always did.
|
|
uniform uint uTextureIndexA;
|
|
uniform float uParamA;
|
|
|
|
void main() {
|
|
vec4 color = ACDREAM_SAMPLE_ARRAY(uTextureIndexA, vec3(vTexCoord, uParamA)) * vColor;
|
|
if (color.a < 0.02)
|
|
discard;
|
|
fragColor = color;
|
|
}
|