feat(render): V6e — the sky's uniforms become a buffer and its texture a table slot

Campaign V slice V6e, last of three. Sky was the hardest of the four pairs
because it was the only one that still worked the way a 2004 shader works: a
dozen loose uniforms pushed one glUniform call at a time, and a texture bound to
unit 0 with a sampler object chosen per submesh. Vulkan GLSL has neither a
default uniform block nor a way to declare a bare sampler, so both had to move —
and the second one had a sting in it.

The uniforms go into a `SkyParams` std140 block at uniform binding 4, the new
pre-authorized constant in GpuBindingModel (1, 2 and 3 are SceneLighting, the
terrain clip block and terrain tiling; the contract test now proves the three
constants and that literal 2 do not collide). Three matrices are 192 bytes on
their own, so the 96-byte push-constant block was never in the running. The
block's member order IS its layout: std140 aligns a vec3 to 16 bytes while using
12, so each of the three lighting vectors is followed by the float that rides in
its pad word, which is why colours and per-surface scalars interleave rather
than grouping by meaning. SkyParamsLayoutTests asserts all twelve offsets and
the 256-byte size, because getting one member wrong would read the sun direction
as a colour with no compile error, no link error and no GL error to say so.

The texture is the interesting half. sky.frag now reads through the shared table
(ACDREAM_SAMPLE_2D), and a bindless handle BAKES its sampler — so the
per-submesh Repeat-versus-ClampToEdge choice, which used to be a glBindSampler
on unit 0, becomes which slot the submesh asks for. SkyRenderer interns one
handle per (texture, wrap) pair, exactly as ManagedGLTextureArray has done since
the world path went bindless, and exactly the shape Vulkan's table has, where an
entry is a combined image sampler. Same two SamplerCache objects, same wrap
behaviour, consulted once at interning instead of once per draw. A pleasant
consequence: the sky no longer touches texture unit 0, so the load-bearing
`BindSampler(0, 0)` restore at the end of the pass — there because the binding
was global state that would otherwise force ClampToEdge on the next renderer —
has nothing left to undo and is gone.

Gates. Release build clean; App tests 4,072 passed / 3 skipped (4,057 baseline,
plus the sentinel guard from the previous commit and fourteen sky-layout
assertions). Offline pixel gate against 95f8c25f: 18 px of 563,200 compared
(3.20e-05), inside the documented 15–23 px band.

That gate masks the sky for determinism, so it proves nothing about this commit
and the sky renderer has no automated pixel coverage at all. What was done
instead: a base-versus-head offline capture at ALL SEVEN day groups, built by
stashing the change and rebuilding so the two runs differ only in this commit.
Every pair matches in gradient, cloud sheet, horizon band and fog — including
day group 2's salmon cloud band and day group 6's green one, which between them
exercise texture sampling, per-vertex tint, blend mode and fog. Then 3/3
RENDERED on the desktop-witness repeat-connected gate.

That bounds the risk; it does not close it. The offline camera is fixed and
looks down, so a thin band of dome is all it ever sees: the sun and moon
(additive, high) and the rain cylinder (the one sky mesh that surrounds the
camera, and the one whose REPEAT wrap is most visible) remain unproven. Recorded
as user-gate debt in §5.1 alongside V2c's and V4e's particles — check it by
standing outside at dawn or dusk, and by standing in rain.

Manifest: 8/9 pairs compile. `terrain_modern` is the last production pair, and
it is blocked on V4d's content rather than on dialect — details in §5.5's slice
table. `mesh` has no consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-28 09:54:13 +02:00
parent 602bc9dddb
commit 7faaaa347b
11 changed files with 480 additions and 60 deletions

View file

@ -1,14 +1,41 @@
#version 430 core
#extension GL_ARB_bindless_texture : require
in vec2 vTex;
in vec3 vTint;
in float vFogFactor; // 1 = no fog, 0 = full fog color
out vec4 fragColor;
uniform sampler2D uDiffuse;
uniform float uTransparency; // keyframe transparency: 0 visible, 1 transparent
uniform float uApplyFog; // 1 for foggable sky layers; raw-additive surfaces keep retail fog disabled
uniform float uSurfOpacity; // final surface opacity multiplier from the CPU
// 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.
//
// The wrap mode travels WITH the slot: SkyRenderer registers a distinct table
// entry per (texture, sampler) pair, so the per-submesh Repeat-vs-ClampToEdge
// choice that used to be a glBindSampler call is now which slot it asks for.
// That is the same shape the Vulkan table has, where a table entry is a
// combined image sampler.
uniform uint uTextureIndexA;
// The per-draw block sky.vert declares. A uniform block must be declared
// identically in every stage of a program that names it — the same rule the
// SceneLighting block below has always followed — so the whole thing appears
// here even though the fragment stage reads only the last three scalars.
layout(std140, ACDREAM_UBO_SET binding = 4) uniform SkyParams {
mat4 uModel;
mat4 uSkyView;
mat4 uSkyProjection;
vec3 uAmbientColor;
float uEmissive;
vec3 uSunColor;
float uDiffuseFactor;
vec3 uSunDir;
float uTransparency;
vec2 uUvScroll;
float uApplyFog;
float uSurfOpacity;
};
struct Light {
vec4 posAndKind;
@ -16,7 +43,7 @@ struct Light {
vec4 colorAndIntensity;
vec4 coneAngleEtc;
};
layout(std140, binding = 1) uniform SceneLighting {
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
Light uLights[8];
vec4 uCellAmbient;
vec4 uFogParams;
@ -25,7 +52,7 @@ layout(std140, binding = 1) uniform SceneLighting {
};
void main() {
vec4 sampled = texture(uDiffuse, vTex);
vec4 sampled = ACDREAM_SAMPLE_2D(uTextureIndexA, vTex);
vec3 rgb = sampled.rgb * vTint;

View file

@ -35,19 +35,39 @@ layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTex;
uniform mat4 uModel;
uniform mat4 uSkyView;
uniform mat4 uSkyProjection;
uniform vec2 uUvScroll;
// Campaign V slice V6e: everything below used to be a dozen loose uniforms set
// with glUniform* between draws. Vulkan GLSL has no default uniform block, so
// those declarations are not merely unsupported — they are unspellable — and
// three matrices are already twice the whole 96-byte push-constant block. A
// uniform buffer is the only legal home, and the same declaration is legal in
// both dialects (GpuBindingModel.UniformSkyParams).
//
// The member ORDER is the std140 layout and must stay in step with
// SkyRenderer's SkyParams struct: each vec3 is followed by the float that rides
// in its 4-byte pad word, which is why the lighting colours and the per-surface
// scalars interleave. Offsets are asserted by SkyParamsLayoutTests.
//
// uModel 0 uSkyView 64 uSkyProjection 128
// uAmbientColor 192 uEmissive 204
// uSunColor 208 uDiffuseFactor 220
// uSunDir 224 uTransparency 236
// uUvScroll 240 uApplyFog 248 uSurfOpacity 252 (size 256)
layout(std140, ACDREAM_UBO_SET binding = 4) uniform SkyParams {
mat4 uModel;
mat4 uSkyView;
mat4 uSkyProjection;
// Per-frame lighting (from SkyKeyframe):
uniform vec3 uAmbientColor; // AmbColor × AmbBright (retail light.Ambient)
uniform vec3 uSunColor; // DirColor × DirBright (retail light.Diffuse)
uniform vec3 uSunDir; // unit vector FROM surface TO sun
// Per-submesh (from Surface.Luminosity float):
uniform float uEmissive;
uniform float uDiffuseFactor;
// Per-frame lighting (from SkyKeyframe):
vec3 uAmbientColor; // AmbColor × AmbBright (retail light.Ambient)
float uEmissive; // per-submesh Surface.Luminosity
vec3 uSunColor; // DirColor × DirBright (retail light.Diffuse)
float uDiffuseFactor;
vec3 uSunDir; // unit vector FROM surface TO sun
float uTransparency; // keyframe transparency: 0 visible, 1 transparent
vec2 uUvScroll;
float uApplyFog; // 1 for foggable layers; raw-additive keeps fog off
float uSurfOpacity; // final surface opacity multiplier from the CPU
};
// Shared SceneLighting UBO — we need uFogParams.xy (fog start/end) to
// compute the vertex fog factor. Must match sky.frag's declaration.
@ -57,7 +77,7 @@ struct Light {
vec4 colorAndIntensity;
vec4 coneAngleEtc;
};
layout(std140, binding = 1) uniform SceneLighting {
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
Light uLights[8];
vec4 uCellAmbient;
vec4 uFogParams; // x=fogStart, y=fogEnd, z=flash, w=fogMode
@ -80,7 +100,7 @@ layout(std140, binding = 1) uniform SceneLighting {
// ungates the sky entirely (the second loop sets all 8 distances to +1.0 ⇒
// full-screen sky, bit-identical to pre-Stage-4). Host enables GL_CLIP_DISTANCE0..7
// only around the sky/weather draws.
layout(std140, binding = 2) uniform TerrainClip {
layout(std140, ACDREAM_UBO_SET binding = 2) uniform TerrainClip {
int uTerrainClipCount;
vec4 uTerrainClipPlanes[8];
};

View file

@ -85,19 +85,17 @@
},
{
"name": "sky",
"vulkanReady": false,
"vulkanReady": true,
"stages": [
{
"stage": "vert",
"sourceSha256": "d338e9b03686b7baf79d5121c5c8d0f24037979cc58f203957d7bd97b02b1cc2",
"compiled": false,
"message": "sky.vert:153: error: \u0027uUvScroll\u0027 : undeclared identifier"
"sourceSha256": "a89580f54c8d36b33d50b84b9fb61024861c737bb0b5bd7a4df0704bf010c3c3",
"compiled": true
},
{
"stage": "frag",
"sourceSha256": "8084af39f65ae399c73e3ca864376ef20ba8a1c495ee4774be6a82af3872c51c",
"compiled": false,
"message": "sky.frag:78: error: \u0027uDiffuse\u0027 : undeclared identifier"
"sourceSha256": "bcb21fd47fc6f74a75edd349bc3dff6120b8995115b2a08269a22f98c0bedd6c",
"compiled": true
}
]
},

Binary file not shown.

Binary file not shown.