feat(render): Vulkan campaign V11 step 3 — drop the GL packages and shaders

Commit 2 deleted the GL rendering backend's implementations; this step
removes the package references and shader vocabulary they leave behind,
so nothing in the App project still spells Silk.NET.OpenGL.

Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from
AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its
Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are
used directly and extensively across the Wb texture/mesh pipeline,
independent of the deleted GL IUniformBuffer implementers the package
comment used to cite. The stale comment is corrected in place.

IMeshPipelineDevice.Gl is removed along with the GL? gl parameter
threaded through WbMeshAdapter's four constructors, WorldRenderComposition's
CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null
implementation — nothing read any of them once the legacy per-mesh
upload bodies were gone (confirmed by grep: the sole non-doc-comment hit
was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed
a real bug along the way: its teardown still pattern-matched the deleted
GL GpuFrameFlightController to decide whether to wait for submitted work,
which VulkanFrameFlightController replaced at slice V6a without this site
being updated — so the wait had been silently dead on every Vulkan run
since then. Retargeted to VulkanFrameFlightController, which carries the
same WaitForSubmittedWork().

The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that
WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for
upload validation is replaced by AcDream.Content's existing Silk.NET-free
UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake
tool GL-free); two new members (Rgb, Red, Float) extend that enum with
their GL ABI constants to cover the full vocabulary WorldTextureArray
needs, since MP1a's original set only covered what the extractor itself
emits. ObjectMeshManager's App-boundary cast
`(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct
pass-through now that both sides share the type.

GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of
the Vulkan texture table) is deleted and StorageBindingCount drops from
10 to 9; the descriptor-set-layout code that builds from that count
(VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just
allocates one fewer always-dummy-seeded, always-unused binding.

Several fully dead GL-only classes came along for the ride, confirmed by
zero construction sites: SilkFramebufferViewportTarget
(NullFramebufferViewportTarget is the sole production
IFramebufferViewportTarget), SilkRenderGlStateReader
(NullRenderGlStateReader.Instance is the sole IRenderGlStateReader),
RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole
IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a
pass load-op instead), and GpuFrameTimer plus FrameProfiler's
GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame
bracket (RecordGpuSample is the only GPU-timing path any backend uses
now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no
longer applies, since WbDrawDispatcher's own diagnostic GPU sampling
already moved to the device's Vulkan timer pool). GpuFrameFlightController
itself stays (never constructed with a real fence API in production, but
its retirement-ledger/serial-ring logic is backend-neutral and still
covered by its own unit tests) — only its GL-specific parts (the public
GL constructor overload, SilkGpuFenceApi) are deleted, since removing the
whole class would mean restructuring the frozen Slice-8 composition
shape's GpuFrameFlightController? threading, which is out of this
commit's scope. TextureParameters.cs and BufferUsageExtensions.cs
(zero callers each) are deleted outright.

common.glsl is deleted: nothing in the actual Vulkan .spv build reads
it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair
directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own
complete self-contained preamble per file; common.glsl's textual
concatenation was exclusively Shader.cs's GL-only mechanism, deleted at
Commit 2. The five shader files that named it in comments
(mesh_modern.vert, particle.vert, particle.frag, sky.frag,
terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs
instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the
mandatory modern path already made unreachable, with zero C# consumers
and no compiled .spv — are deleted too. Regenerated via
tools/compile-shaders.ps1: 9/9 remaining shader pairs compile
(previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests
doc comment's "nine of ten are not Vulkan-expressible" was already
stale before this commit).

Test fallout: dead-subject test methods/files are deleted rather than
patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs,
GpuResourceRetirementTransactionTests.cs's GL queue tests, one
WorldRenderDiagnosticsTests source-order test, one
RenderFrameResourceControllerTests clear-phase-order test); tests whose
subject moved or was renamed are updated in place rather than deleted
(GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests'
pinned seven-member surface now reads six, ParticleBindlessInstanceTests'
cross-dialect check now covers the one surviving dialect,
WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was
always the parameter that actually threw).

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors,
with the Silk.NET.OpenGL/.Extensions.ARB package references physically
removed from the csproj (not just unreferenced in code).
Tests: full-solution `dotnet test` green across every project.
Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-29 02:58:15 +02:00
parent 8a7a0837e1
commit 7a0227c12e
44 changed files with 300 additions and 977 deletions

View file

@ -1,84 +0,0 @@
// Campaign V slice V2 shared preamble (docs/plans/2026-07-27-vulkan-campaign.md
// §3.4, §5.2). GL has no #include, so AcDream.App.Rendering.Shader
// concatenates this file's text into every shader source that opts in
// (Shader(gl, vertPath, fragPath, includeCommonPreamble: true)), inserted
// right after the leading #version / #extension block so it can declare new
// layout bindings and macros before the rest of the shader body runs.
//
// --- set 1 (uniform buffers) ------------------------------------------------
// GL keeps the SSBO and UBO binding-number namespaces separate, so today's
// SceneLighting UBO (binding=1) never collides with BatchBuffer's SSBO
// (also binding=1). Vulkan has ONE binding namespace per set, so the Vulkan
// backend (from V6 on) moves every uniform buffer to its own set (1) to keep
// both binding numbers. ACDREAM_UBO_SET is a no-op under GL today and is
// redefined to `set = 1,` when the same source is compiled for Vulkan, so
// applying it to every UBO layout now costs nothing and needs no source edit
// at the call sites later.
#define ACDREAM_UBO_SET
// --- set 0 binding 9 / set 2 (the global texture table) --------------------
// GL emulates Vulkan's set 2 variable-count sampled-texture descriptor array
// (GpuBindingModel.TextureTableSet) with a plain storage buffer of packed
// GL_ARB_bindless_texture handles at set 0 binding 9
// (GpuBindingModel.StorageTextureTable). A batch/pass no longer carries a
// 64-bit bindless handle directly into its GPU-visible struct; it carries a
// small integer slot index into this table instead, which is what makes the
// CPU-side data model backend-neutral (Campaign V slice V2). The table itself
// — and every per-renderer GL-side handle-slot allocator that fills it
// (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer)
// — is deleted once each renderer moves onto IGpuDevice's own retirement-gated
// table at V4c/V4d/V4e; see the campaign doc's §5.2 for why V2 cannot reach
// that table yet.
layout(std430, binding = 9) readonly buffer TextureTableBuf {
uvec2 gTextureTable[];
};
// Looks up the packed bindless handle for table slot `idx`. Callers still
// wrap the result in `sampler2DArray(...)` themselves at the use site (kept
// explicit rather than folded into one sampler-returning macro) because every
// existing call site already follows that exact pattern and a function cannot
// return an opaque sampler type built from a runtime value in GLSL.
//
// Campaign V slice V6e: this macro is GL-shaped — under Vulkan it degenerates
// to the index itself, because there is no handle to look up. Every NEW call
// site should use ACDREAM_SAMPLE_ARRAY / ACDREAM_SAMPLE_2D below, which ask the
// dialect-neutral question ("sample table slot N") instead of the GL-only one
// ("what handle does slot N hold"). The remaining direct users are the shaders
// whose port slice has not run yet.
#define ACDREAM_TEXTURE_HANDLE(idx) gTextureTable[idx]
// Campaign V slice V6e: samples a table slot that holds a 2-D ARRAY texture —
// the world/particle/terrain case, as opposed to the retained UI's plain 2-D
// entries that ACDREAM_SAMPLE_2D below covers.
//
// Expressed as a SAMPLING macro rather than a sampler-returning one on purpose.
// Under Vulkan the expansion carries a `nonuniformEXT` qualifier, and that
// qualifier belongs on the indexing expression at the point of use; binding the
// result to a local `sampler2DArray` variable first is where a driver is free to
// lose it. Both dialects therefore read the texture in one expression.
#define ACDREAM_SAMPLE_ARRAY(idx, uvw) texture(sampler2DArray(gTextureTable[idx]), uvw)
// Campaign V slice V6e: the reserved "this draw has no texture" slot index.
//
// GL could ask the question directly — an unregistered slot holds the handle 0,
// so `gTextureTable[idx] == uvec2(0)` answered it. Vulkan cannot: set 2 is an
// opaque descriptor array with nothing to compare, and reading an unwritten
// element of a partially-bound array is undefined rather than zero. So the
// answer moves to the index itself, which both dialects can test identically,
// and the CPU writes this value instead of registering a null handle.
#define ACDREAM_TEXTURE_NONE 0xFFFFFFFFu
// Campaign V slice V6d: samples a table slot that holds a plain 2-D texture.
//
// The two backends disagree about what a 2-D table entry IS, and this macro is
// the one place that difference lives. Under GL a bindless handle carries its
// own texture type, so a GL_TEXTURE_2D entry is reconstructed as a sampler2D
// and read with a 2-component UV. Under Vulkan the table is one descriptor
// array whose element type is fixed at sampler2DArray, so the same entry is a
// one-layer array read at layer 0 (see tools/ShaderCompiler/VulkanGlslPreamble.cs).
//
// That asymmetry is deliberate and is what keeps the retained UI's textures
// exactly as they are on GL — including the paperdoll/appraisal FBO colour
// texture, which is an externally-owned GL_TEXTURE_2D registered by the §7.1
// transitional seam and cannot be made an array before V4g moves its renderer.
#define ACDREAM_SAMPLE_2D(idx, uv) texture(sampler2D(gTextureTable[idx]), uv)

View file

@ -1,132 +0,0 @@
#version 430 core
in vec2 vTex;
in vec3 vWorldNormal;
in vec3 vWorldPos;
out vec4 fragColor;
uniform sampler2D uDiffuse;
// Phase 9.1: translucency kind — matches TranslucencyKind C# enum.
// 0 = Opaque — depth write+test, no blend; shader never discards
// 1 = ClipMap — alpha-key discard (doors, windows, vegetation)
// 2 = AlphaBlend — GL blending handles compositing; do NOT discard
// 3 = Additive — GL additive blending; do NOT discard
// 4 = InvAlpha — GL inverted-alpha blending; do NOT discard
uniform int uTranslucencyKind;
// ─────────────────────────────────────────────────────────────
// Phase G.1+G.2: shared scene-lighting UBO (binding = 1).
//
// Layout mirrors SceneLightingUbo in C#:
// struct Light {
// vec4 posAndKind; xyz = world pos, w = kind (0=dir,1=point,2=spot)
// vec4 dirAndRange; xyz = forward, w = range (metres, hard cutoff)
// vec4 colorAndIntensity; xyz = RGB linear, w = intensity
// vec4 coneAngleEtc; x = cone (rad), yzw = reserved
// };
// layout(std140, binding = 1) uniform SceneLighting {
// Light uLights[8];
// vec4 uCellAmbient; xyz = ambient RGB, w = active count
// vec4 uFogParams; x = start, y = end, z = flash, w = mode
// vec4 uFogColor; xyz = color
// vec4 uCameraAndTime; xyz = camera pos, w = day fraction
// };
// ─────────────────────────────────────────────────────────────
struct Light {
vec4 posAndKind;
vec4 dirAndRange;
vec4 colorAndIntensity;
vec4 coneAngleEtc;
};
layout(std140, binding = 1) uniform SceneLighting {
Light uLights[8];
vec4 uCellAmbient;
vec4 uFogParams;
vec4 uFogColor;
vec4 uCameraAndTime;
};
// Retail per-vertex point-light ramp (calc_point_light 0x0059c8b0): the
// contribution scales by (1 - dist/falloff_eff) — a LINEAR fade to exactly
// 0 at the edge, NOT a hard-cutoff bubble. (The prior "no attenuation inside
// Range / crisp boundaries" note was a misread; it is the literal cause of
// the #133 "spotlight" look. falloff_eff = Falloff * static_light_factor 1.3
// is folded into Range by LightInfoLoader.) Spots add a binary cos-cone test.
vec3 accumulateLights(vec3 N, vec3 worldPos) {
vec3 lit = uCellAmbient.xyz;
int activeLights = int(uCellAmbient.w);
for (int i = 0; i < 8; ++i) {
if (i >= activeLights) break;
int kind = int(uLights[i].posAndKind.w);
vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w;
if (kind == 0) {
// Directional: "forward" is the light's direction vector
// pointing INTO the scene. N·(-forward) = light-facing.
vec3 Ldir = -uLights[i].dirAndRange.xyz;
float ndl = max(0.0, dot(N, Ldir));
lit += Lcol * ndl;
} else {
// Point / spot: falloff is a HARD bubble at Range.
vec3 toL = uLights[i].posAndKind.xyz - worldPos;
float d = length(toL);
float range = uLights[i].dirAndRange.w;
if (d < range && range > 1e-3) {
vec3 Ldir = toL / max(d, 1e-4);
float ndl = max(0.0, dot(N, Ldir));
// calc_point_light (1 - dist/falloff_eff) linear ramp; Range already
// carries falloff_eff (Falloff * 1.3), so it fades to 0 at the cutoff.
float atten = clamp(1.0 - d / max(range, 1e-3), 0.0, 1.0);
if (kind == 2) {
// Spotlight: hard-edged cos-cone test.
float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5);
float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz);
atten *= (cos_l > cos_edge) ? 1.0 : 0.0;
}
// Retail per-channel "no-blowout" cap (calc_point_light 0x0059c8b0): a single
// point/spot light can't push a channel past its own colour, regardless of
// intensity (~100) — kills the close-torch overblow (#93). See mesh_modern.frag.
lit += min(Lcol * ndl * atten, uLights[i].colorAndIntensity.xyz);
}
}
}
return lit;
}
// Linear fog (r12 §5.1): mode 1 = LINEAR, 0 = off, others reserved.
vec3 applyFog(vec3 lit, vec3 worldPos) {
int mode = int(uFogParams.w);
if (mode == 0) return lit;
float d = length(worldPos - uCameraAndTime.xyz);
float fogStart = uFogParams.x;
float fogEnd = uFogParams.y;
float span = max(1e-3, fogEnd - fogStart);
float fog = clamp((d - fogStart) / span, 0.0, 1.0);
return mix(lit, uFogColor.xyz, fog);
}
void main() {
vec4 sampled = texture(uDiffuse, vTex);
// Alpha cutout only for clip-map surfaces (doors, windows, vegetation).
if (uTranslucencyKind == 1 && sampled.a < 0.5) discard;
vec3 N = normalize(vWorldNormal);
vec3 lit = accumulateLights(N, vWorldPos);
// Lightning flash (r12 §9) — additive cold-white pulse layered on top
// of diffuse lighting.
float flash = uFogParams.z;
lit += flash * vec3(0.6, 0.6, 0.75);
// Clamp per-channel to 1.0 — matches retail (r13 §13.1).
lit = min(lit, vec3(1.0));
vec3 rgb = sampled.rgb * lit;
// Atmospheric fog — applied after lighting.
rgb = applyFog(rgb, vWorldPos);
fragColor = vec4(rgb, sampled.a);
}

View file

@ -1,24 +0,0 @@
#version 430 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTex;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec2 vTex;
out vec3 vWorldNormal;
out vec3 vWorldPos;
void main() {
vTex = aTex;
// Transform the mesh normal into world space. For uniform-scale transforms
// (the common case), the upper-left 3x3 of uModel is correct. Non-uniform
// scale would require the inverse transpose; we accept that as a Phase 3+
// concern.
vWorldNormal = normalize(mat3(uModel) * aNormal);
vec4 world = uModel * vec4(aPos, 1.0);
vWorldPos = world.xyz;
gl_Position = uProjection * uView * world;
}

View file

@ -11,14 +11,14 @@ struct InstanceData {
// Campaign V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
// pad word. textureIndex is a slot into the binding=9 handle table
// (common.glsl) which main() below forwards to the fragment stage, where slice
// V6e moved the lookup so the same source compiles for Vulkan. The pad word keeps
// textureLayer/flags at their original std430 offsets (8/12), so the struct
// is still 16 bytes and every existing CPU writer's layout is unchanged
// (GpuBindingModel.GpuBatchDataStrideBytes).
// pad word. textureIndex is a slot into the global texture table (set 2,
// injected by tools/ShaderCompiler/VulkanGlslPreamble.cs — see
// ACDREAM_TEXTURE_HANDLE/ACDREAM_SAMPLE_ARRAY) which main() below forwards to
// the fragment stage. The pad word keeps textureLayer/flags at their original
// std430 offsets (8/12), so the struct is still 16 bytes and every existing
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
struct BatchData {
uint textureIndex; // slot into the binding=9 handle table
uint textureIndex; // slot into the global texture table
uint _pad; // keeps textureLayer/flags at offsets 8/12
uint textureLayer; // layer in the shared WB or pooled composite array
uint flags; // reserved — N.5 dispatcher owns all blend state

View file

@ -6,7 +6,8 @@ in vec4 vColor;
// Campaign V slice V6e: the texture-table SLOT, not the bindless handle — see
// particle.vert. ACDREAM_TEXTURE_NONE is the untextured particle, which used to
// be spelled "the slot whose handle is zero"; a Vulkan descriptor array cannot
// be asked that question, so the answer lives in the index (common.glsl).
// be asked that question, so the answer lives in the index (see
// tools/ShaderCompiler/VulkanGlslPreamble.cs).
flat in uint vTextureIndex;
out vec4 fragColor;

View file

@ -10,8 +10,9 @@ layout(location = 3) in vec4 aAxisX;
layout(location = 4) in vec4 aAxisY;
layout(location = 5) in vec4 aColor;
// Campaign V slice V2c (2026-07-27): was uvec2 aTextureHandle (a raw
// ARB_bindless_texture handle); now a slot into the binding=9 handle table
// (ACDREAM_TEXTURE_HANDLE, common.glsl).
// ARB_bindless_texture handle); now a slot into the global texture table
// (ACDREAM_TEXTURE_HANDLE, injected by
// tools/ShaderCompiler/VulkanGlslPreamble.cs).
layout(location = 6) in uint aTextureIndex;
uniform mat4 uViewProjection;

View file

@ -7,9 +7,10 @@ in float vFogFactor; // 1 = no fog, 0 = full fog color
out vec4 fragColor;
// Campaign V slice V6e: the sky's texture is now read through the shared table
// (ACDREAM_SAMPLE_2D, common.glsl) rather than a `uniform sampler2D` bound to
// texture unit 0. Vulkan has no default uniform block to declare a loose
// sampler in, and set 2 is where every sampled texture lives.
// (ACDREAM_SAMPLE_2D, injected by tools/ShaderCompiler/VulkanGlslPreamble.cs)
// rather than a `uniform sampler2D` bound to texture unit 0. Vulkan has no
// default uniform block to declare a loose sampler in, and set 2 is where
// every sampled texture lives.
//
// The wrap mode travels WITH the slot: SkyRenderer registers a distinct table
// entry per (texture, sampler) pair, so the per-submesh Repeat-vs-ClampToEdge

View file

@ -17,31 +17,13 @@
}
]
},
{
"name": "mesh",
"vulkanReady": false,
"stages": [
{
"stage": "vert",
"sourceSha256": "c35f767ab07fa9df805f9e77f4851f517c153dd2ef2efa6d49d0c24b688e4f56",
"compiled": false,
"message": "mesh.vert:73: error: \u0027uModel\u0027 : undeclared identifier"
},
{
"stage": "frag",
"sourceSha256": "4d6478543a9a903a3453581fa847e096aaecf01f38ebb2921572663bad8e24ea",
"compiled": false,
"message": "mesh.frag:160: error: \u0027uDiffuse\u0027 : undeclared identifier"
}
]
},
{
"name": "mesh_modern",
"vulkanReady": true,
"stages": [
{
"stage": "vert",
"sourceSha256": "770e6300e023bd2600e8fed52768c624d8fff2ff8d6f997f076980502d3f675b",
"sourceSha256": "2c37aa0fd1ee4af5ee1f6541b00e6a7226d4173988e748b1fb1c271b0a9b91b2",
"compiled": true
},
{
@ -57,12 +39,12 @@
"stages": [
{
"stage": "vert",
"sourceSha256": "9629271f8997853a3c78a3cb1ec7af02a13518d68bdaa5ae1378997da7e5ab62",
"sourceSha256": "921c32617708b3931a6304b4697ee96d728077d96f489d6c09eea4c5fe225b63",
"compiled": true
},
{
"stage": "frag",
"sourceSha256": "bc08e4fb6f57da94d6c41d52aa81d73ab287c968214303a446c40a04d0495b40",
"sourceSha256": "6f2a5769670087bf58b0c1a7a007c1853952354dfd9106973656311bcbb855cd",
"compiled": true
}
]
@ -110,7 +92,7 @@
},
{
"stage": "frag",
"sourceSha256": "bcb21fd47fc6f74a75edd349bc3dff6120b8995115b2a08269a22f98c0bedd6c",
"sourceSha256": "8de9a5d8f819d1abf893f134cf8ed9fa7edf937a353a74d2befab7b05a3ff700",
"compiled": true
}
]
@ -126,7 +108,7 @@
},
{
"stage": "frag",
"sourceSha256": "21f41dcca4d4973ad9ee148c5433e0dae015489414c164c51bde958963f03598",
"sourceSha256": "a9f0ad2e679b68ee2de6c4877202f6f5e5089d0f7368fcedd6048891c6848915",
"compiled": true
}
]

View file

@ -5,15 +5,16 @@
// Math identical to terrain.frag (Phase 3c per-cell maskBlend3 +
// Phase G fog + lightning flash).
//
// Texture reads go through ACDREAM_SAMPLE_ARRAY (common.glsl) since slice
// V6f-3, so this source compiles for both backends. Under GL that still expands
// to the uvec2-handle + sampler2DArray-constructor pattern this shader has
// always used — the documented "always works" form per the ARB_bindless_texture
// spec, and the one that avoids the GL_INVALID_OPERATION the alternative
// (`uniform sampler2DArray` set via glProgramUniformHandleARB) produces on at
// least one driver in practice. Under Vulkan it indexes the set-2 descriptor
// array instead. The extension requirement above is dropped for Vulkan by the
// compiler's preamble, where it would be an error rather than a no-op.
// Texture reads go through ACDREAM_SAMPLE_ARRAY, which
// tools/ShaderCompiler/VulkanGlslPreamble.cs injects to index the set-2
// descriptor array. GL is deleted (Campaign V slice V11); before that this
// macro expanded (via the now-deleted common.glsl) to the uvec2-handle +
// sampler2DArray-constructor pattern this shader used on that arm — the
// documented "always works" form per the ARB_bindless_texture spec, and the
// one that avoided the GL_INVALID_OPERATION the alternative (`uniform
// sampler2DArray` set via glProgramUniformHandleARB) produced on at least one
// driver in practice. The extension requirement above is dropped for Vulkan
// by the compiler's preamble, where it would be an error rather than a no-op.
in vec2 vBaseUV;
in vec3 vWorldNormal;
@ -30,8 +31,9 @@ out vec4 fragColor;
// Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2, raw
// ARB_bindless_texture handles) became uTextureIndexA/uTextureIndexB (slots
// into the binding=9 handle table, ACDREAM_TEXTURE_HANDLE in common.glsl).
// Named to match the pinned GpuPushConstants.TextureIndexA/B fields so V4d's
// into the global texture table, ACDREAM_TEXTURE_HANDLE injected by
// tools/ShaderCompiler/VulkanGlslPreamble.cs). Named to match the pinned
// GpuPushConstants.TextureIndexA/B fields so V4d's
// move to push constants is a rename, not a redesign — there is no
// push-constant plumbing yet, so these stay plain uniforms for now.
uniform uint uTextureIndexA;
@ -45,8 +47,8 @@ uniform uint uTextureIndexB;
// question both dialects can answer ("sample table slot N at these
// coordinates") and expands to the right thing on each.
//
// A SAMPLING macro, not a sampler-returning one, for the reason common.glsl
// records: under Vulkan the expansion carries `nonuniformEXT` on the indexing
// A SAMPLING macro, not a sampler-returning one, for the reason
// VulkanGlslPreamble.cs records: under Vulkan the expansion carries `nonuniformEXT` on the indexing
// expression, and binding the result to a local sampler2DArray first is exactly
// where an implementation may drop that qualifier. The old `#define uTerrain
// sampler2DArray(...)` was that shape textually, so keeping it would have