From 30e94da607796f6bdd7756b6e24dc696806cda23 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 28 Jul 2026 10:14:37 +0200 Subject: [PATCH] feat(render): Campaign V slice V6f-3 - terrain's atlas reads cross the dialect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last thing keeping terrain_modern out of SPIR-V was how it named its two atlases: #define uTerrain sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) #define uAlpha sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexB)) `sampler2DArray(handle)` is a GL_ARB_bindless_texture construction with no Vulkan equivalent. Vulkan's table is an opaque descriptor array in set 2; there is no handle, so there is nothing to construct a sampler from. The ten sample sites now go through ACDREAM_SAMPLE_ARRAY, the dialect-neutral read V6e introduced for mesh_modern, wrapped in two shader-local macros that keep the call sites reading as "sample the terrain atlas" rather than "index the table". They are SAMPLING macros, not sampler-returning ones, and that is not cosmetic. 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 it. The old `#define uTerrain sampler2DArray(...)` was textually that shape, so preserving it would have reintroduced the hazard at every use site. On GL nothing about the sampled result changes: the same slot resolves through the same binding=9 table to the same handle to the same texel, and the macro expands to the identical expression the shader wrote by hand. With this, terrain_modern compiles for Vulkan and the manifest reads 8/9. The remaining pair is `mesh`, which the campaign doc records as having no consumer at all - so every production shader acdream actually draws with is now Vulkan-expressible. That closes the obligation §5 recorded against V6e ("the one production pair still not Vulkan-expressible after V6e is terrain_modern") and it closes the shader half of V4d's parked content. What this does NOT do is give the Vulkan backend a world to draw. That is reported separately with the rest of slice V6f; the shaders were the part that could be finished, gated and landed on GL today. Gates. Release build clean. App tests 4,073 passed / 3 skipped over four consecutive runs. A fifth run failed only CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing, an allocation-counting test over a CPU dictionary path that touches nothing in this diff; it passes in isolation and passed in every other full run. That is the known #250 flake class on an otherwise unchanged tree. Offline pixel gate against fac09407: 12 differing pixels of 563,200 compared (fraction 2.13e-05) - below even the floor of the documented 15-23 pixel band. Cumulatively, across all three V6f commits against 7faaaa34: 22 pixels (3.91e-05, maximumChannelDelta 52), which is the same number the first commit measured on its own. Three changes to the shader terrain draws with, and the drift has not accumulated. No divergence-register row: the sampled result is unchanged on GL and no retail-facing behaviour moves. Co-Authored-By: Claude Fable 5 --- .../Shaders/spv/shaders.manifest.json | 7 +-- .../Shaders/spv/terrain_modern.frag.spv | Bin 0 -> 8872 bytes .../Shaders/spv/terrain_modern.vert.spv | Bin 0 -> 9824 bytes .../Rendering/Shaders/terrain_modern.frag | 56 +++++++++++------- 4 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 src/AcDream.App/Rendering/Shaders/spv/terrain_modern.frag.spv create mode 100644 src/AcDream.App/Rendering/Shaders/spv/terrain_modern.vert.spv diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json index 15d29592..f6a65f17 100644 --- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json +++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json @@ -101,7 +101,7 @@ }, { "name": "terrain_modern", - "vulkanReady": false, + "vulkanReady": true, "stages": [ { "stage": "vert", @@ -110,9 +110,8 @@ }, { "stage": "frag", - "sourceSha256": "dc4d38b5eb356629c83681fbc6a1d5d867792e99a8c4e659031f390333edb7b8", - "compiled": false, - "message": "terrain_modern.frag:150: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled" + "sourceSha256": "21f41dcca4d4973ad9ee148c5433e0dae015489414c164c51bde958963f03598", + "compiled": true } ] }, diff --git a/src/AcDream.App/Rendering/Shaders/spv/terrain_modern.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/terrain_modern.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..4eee3319997ca5210ea31d6255dff68b4bb20056 GIT binary patch literal 8872 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 F`9HUU7jFOn literal 0 HcmV?d00001 diff --git a/src/AcDream.App/Rendering/Shaders/terrain_modern.frag b/src/AcDream.App/Rendering/Shaders/terrain_modern.frag index 2e527308..2d09c9f7 100644 --- a/src/AcDream.App/Rendering/Shaders/terrain_modern.frag +++ b/src/AcDream.App/Rendering/Shaders/terrain_modern.frag @@ -5,14 +5,15 @@ // Math identical to terrain.frag (Phase 3c per-cell maskBlend3 + // Phase G fog + lightning flash). // -// Bindless texture handles are passed as uvec2 (low/high 32 bits) and -// reconstructed into sampler2DArray at use sites via the GLSL -// sampler-from-handle constructor. The alternative pattern — -// `uniform sampler2DArray` set via glProgramUniformHandleARB — produces -// GL_INVALID_OPERATION on at least one driver in practice (NVIDIA on -// Windows). The uvec2 + constructor pattern is what N.5's mesh_modern -// shader uses and is the documented "always works" form per the -// ARB_bindless_texture spec. +// 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. in vec2 vBaseUV; in vec3 vWorldNormal; @@ -35,8 +36,23 @@ out vec4 fragColor; // push-constant plumbing yet, so these stay plain uniforms for now. uniform uint uTextureIndexA; uniform uint uTextureIndexB; -#define uTerrain sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) -#define uAlpha sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexB)) + +// Campaign V slice V6f-3: the two atlases are sampled through the +// dialect-neutral table read instead of a GL sampler-from-handle constructor. +// `sampler2DArray(handle)` is a GL_ARB_bindless_texture form with no Vulkan +// equivalent — Vulkan's table is an opaque descriptor array in set 2, and there +// is no handle to construct a sampler from. ACDREAM_SAMPLE_ARRAY asks the +// 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 +// 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 +// reintroduced the hazard at every use site. +#define sampleTerrain(uvw) ACDREAM_SAMPLE_ARRAY(uTextureIndexA, uvw) +#define sampleAlpha(uvw) ACDREAM_SAMPLE_ARRAY(uTextureIndexB, uvw) // Campaign V slice V6f-2: the 36 per-layer tiling factors moved out of a loose // `uniform float uTexTiling[36]` and into the uniform buffer GpuBindingModel @@ -97,23 +113,23 @@ vec4 combineOverlays(vec2 baseUV, vec4 pOverlay0, vec4 pOverlay1, vec4 pOverlay2 vec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0); if (h0 > 0.0) { - t0 = texture(uTerrain, vec3(baseUV * terrainTiling(pOverlay0.z), pOverlay0.z)); + t0 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay0.z), pOverlay0.z)); if (pOverlay0.w >= 0.0) { - vec4 a = texture(uAlpha, vec3(pOverlay0.xy, pOverlay0.w)); + vec4 a = sampleAlpha(vec3(pOverlay0.xy, pOverlay0.w)); t0.a = a.a; } } if (h1 > 0.0) { - t1 = texture(uTerrain, vec3(baseUV * terrainTiling(pOverlay1.z), pOverlay1.z)); + t1 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay1.z), pOverlay1.z)); if (pOverlay1.w >= 0.0) { - vec4 a = texture(uAlpha, vec3(pOverlay1.xy, pOverlay1.w)); + vec4 a = sampleAlpha(vec3(pOverlay1.xy, pOverlay1.w)); t1.a = a.a; } } if (h2 > 0.0) { - t2 = texture(uTerrain, vec3(baseUV * terrainTiling(pOverlay2.z), pOverlay2.z)); + t2 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay2.z), pOverlay2.z)); if (pOverlay2.w >= 0.0) { - vec4 a = texture(uAlpha, vec3(pOverlay2.xy, pOverlay2.w)); + vec4 a = sampleAlpha(vec3(pOverlay2.xy, pOverlay2.w)); t2.a = a.a; } } @@ -125,12 +141,12 @@ vec4 combineRoad(vec2 baseUV, vec4 pRoad0, vec4 pRoad1) { float h1 = pRoad1.z < 0.0 ? 0.0 : 1.0; vec4 result = vec4(0.0); if (h0 > 0.0) { - result = texture(uTerrain, vec3(baseUV * terrainTiling(pRoad0.z), pRoad0.z)); + result = sampleTerrain(vec3(baseUV * terrainTiling(pRoad0.z), pRoad0.z)); if (pRoad0.w >= 0.0) { - vec4 a0 = texture(uAlpha, vec3(pRoad0.xy, pRoad0.w)); + vec4 a0 = sampleAlpha(vec3(pRoad0.xy, pRoad0.w)); result.a = 1.0 - a0.a; if (h1 > 0.0 && pRoad1.w >= 0.0) { - vec4 a1 = texture(uAlpha, vec3(pRoad1.xy, pRoad1.w)); + vec4 a1 = sampleAlpha(vec3(pRoad1.xy, pRoad1.w)); result.a = 1.0 - (a0.a * a1.a); } } @@ -152,7 +168,7 @@ vec3 applyFog(vec3 lit, vec3 worldPos) { void main() { vec4 baseColor = vec4(0.0); if (vBaseTexIdx >= 0.0) { - baseColor = texture(uTerrain, vec3(vBaseUV * terrainTiling(vBaseTexIdx), vBaseTexIdx)); + baseColor = sampleTerrain(vec3(vBaseUV * terrainTiling(vBaseTexIdx), vBaseTexIdx)); } vec4 overlays = vec4(0.0);