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:
parent
602bc9dddb
commit
7faaaa347b
11 changed files with 480 additions and 60 deletions
|
|
@ -968,7 +968,10 @@ internal sealed class LivePresentationCompositionPhase
|
|||
() => new AcDream.App.Rendering.Shader(
|
||||
d.Gl,
|
||||
Path.Combine(foundation.ShadersDirectory, "sky.vert"),
|
||||
Path.Combine(foundation.ShadersDirectory, "sky.frag")));
|
||||
Path.Combine(foundation.ShadersDirectory, "sky.frag"),
|
||||
// Campaign V slice V6e: sky reads the shared texture table and
|
||||
// ACDREAM_UBO_SET now, both of which common.glsl declares.
|
||||
includeCommonPreamble: true));
|
||||
var skyShaderLease = scope.Own(
|
||||
"sky shader lifetime",
|
||||
skyShader,
|
||||
|
|
@ -980,7 +983,10 @@ internal sealed class LivePresentationCompositionPhase
|
|||
content.Dats,
|
||||
skyShader,
|
||||
foundation.TextureCache,
|
||||
foundation.Samplers),
|
||||
foundation.Samplers,
|
||||
// Slice V6e: the sky samples through the binding=9 handle table,
|
||||
// so it needs the same bindless entry point the world path uses.
|
||||
foundation.Bindless),
|
||||
static value => value.Dispose());
|
||||
var particleLease = scope.Acquire(
|
||||
"particle renderer",
|
||||
|
|
|
|||
|
|
@ -89,6 +89,21 @@ internal static class GpuBindingModel
|
|||
/// </summary>
|
||||
public const uint UniformTerrainTiling = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Sky per-draw parameters — the sky/model transforms, the keyframe's
|
||||
/// ambient/sun colours and sun direction, the UV scroll, and the
|
||||
/// per-surface emissive/diffuse/opacity/fog scalars.
|
||||
///
|
||||
/// Added at slice V6e for the same reason as
|
||||
/// <see cref="UniformTerrainTiling"/>: Vulkan GLSL has no default uniform
|
||||
/// block, so a loose <c>uniform mat4 uSkyView;</c> is unspellable, and this
|
||||
/// set is 256 bytes in std140 — three matrices alone are twice the entire
|
||||
/// 96-byte push-constant block. A uniform buffer is the only legal home.
|
||||
/// Bindings 1, 2 and 3 are taken by SceneLighting, the terrain clip block
|
||||
/// and terrain tiling, so this is 4.
|
||||
/// </summary>
|
||||
public const uint UniformSkyParams = 4;
|
||||
|
||||
/// <summary>Set index carrying every uniform buffer.</summary>
|
||||
public const uint UniformSet = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
BIN
src/AcDream.App/Rendering/Shaders/spv/sky.frag.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/sky.frag.spv
Normal file
Binary file not shown.
BIN
src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv
Normal file
Binary file not shown.
|
|
@ -51,6 +51,30 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
private readonly TextureCache _textures;
|
||||
private readonly SamplerCache _samplers;
|
||||
|
||||
// Campaign V slice V6e. Three GL objects replace what used to be a run of
|
||||
// glUniform* calls and a texture/sampler binding on unit 0:
|
||||
//
|
||||
// _paramsUbo — the std140 SkyParams block both stages declare.
|
||||
// _textureTableSsbo — the binding=9 handle table (the GL-only emulation
|
||||
// of Vulkan's set 2), holding one entry per
|
||||
// (texture, wrap-mode) pair the sky samples.
|
||||
// _uTextureIndexALoc — the push-constant-named uniform carrying the slot.
|
||||
//
|
||||
// The wrap mode is what makes the pairing necessary. A bindless handle
|
||||
// BAKES its sampler state, so the per-submesh Repeat-vs-ClampToEdge choice
|
||||
// that used to be a glBindSampler call has to be a different handle — which
|
||||
// is also exactly how the Vulkan table works, where an entry is a combined
|
||||
// image sampler. ManagedGLTextureArray has interned wrap/clamp handle pairs
|
||||
// the same way since the world path went bindless.
|
||||
private readonly Wb.BindlessSupport _bindless;
|
||||
private readonly GlBindlessHandleTable _textureTable = new();
|
||||
private readonly Dictionary<(uint Texture, bool Repeat), ulong> _handleByTextureAndWrap = new();
|
||||
private uint _paramsUbo;
|
||||
private uint _textureTableSsbo;
|
||||
private int _textureTableSsboCapacityBytes;
|
||||
private int _uTextureIndexALoc = -1;
|
||||
private SkyParams _params;
|
||||
|
||||
// Lazily-built GPU resources per sky-GfxObj.
|
||||
private readonly Dictionary<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
|
||||
|
||||
|
|
@ -63,13 +87,39 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
public float Near { get; set; } = 0.1f;
|
||||
public float Far { get; set; } = 1_000_000f;
|
||||
|
||||
public SkyRenderer(GL gl, IDatReaderWriter dats, Shader shader, TextureCache textures, SamplerCache samplers)
|
||||
public SkyRenderer(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
Shader shader,
|
||||
TextureCache textures,
|
||||
SamplerCache samplers,
|
||||
Wb.BindlessSupport bindless)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_shader = shader ?? throw new ArgumentNullException(nameof(shader));
|
||||
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
|
||||
_samplers = samplers ?? throw new ArgumentNullException(nameof(samplers));
|
||||
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
|
||||
|
||||
_uTextureIndexALoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexA");
|
||||
|
||||
_paramsUbo = Wb.TrackedGlResource.CreateBuffer(_gl, "sky params UBO creation");
|
||||
// Fixed size: SkyParams is a 256-byte std140 struct, so the buffer never
|
||||
// grows and every write is one whole-struct BufferSubData.
|
||||
_gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo);
|
||||
Wb.TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
BufferTargetARB.UniformBuffer,
|
||||
_paramsUbo,
|
||||
0,
|
||||
SkyParams.SizeInBytes,
|
||||
BufferUsageARB.DynamicDraw,
|
||||
"allocating sky params UBO");
|
||||
_gl.BindBuffer(BufferTargetARB.UniformBuffer, 0);
|
||||
|
||||
_textureTableSsbo = Wb.TrackedGlResource.CreateBuffer(
|
||||
_gl, "sky texture-table SSBO creation");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -179,17 +229,27 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
skyView.M43 = 0f;
|
||||
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uSkyView", skyView);
|
||||
_shader.SetMatrix4("uSkyProjection", skyProj);
|
||||
// Campaign V slice V6e: the values below used to be individual
|
||||
// glUniform* calls. They now populate the std140 SkyParams block, which
|
||||
// is uploaded once per submesh right before its draw — the same cadence
|
||||
// the per-submesh uniforms already had, in one BufferSubData instead of
|
||||
// five calls.
|
||||
_params.SkyView = skyView;
|
||||
_params.SkyProjection = skyProj;
|
||||
|
||||
// Retail per-vertex lighting inputs (AdjustPlanes formula).
|
||||
// AmbColor/SunColor are already × AmbBright/DirBright from
|
||||
// SkyDescLoader. SunDir is the unit vector FROM surface TO sun
|
||||
// derived from the keyframe's DirHeading/DirPitch.
|
||||
_shader.SetVec3("uAmbientColor", keyframe.AmbientColor);
|
||||
_shader.SetVec3("uSunColor", keyframe.SunColor);
|
||||
_shader.SetVec3("uSunDir",
|
||||
AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(keyframe));
|
||||
_params.AmbientColor = keyframe.AmbientColor;
|
||||
_params.SunColor = keyframe.SunColor;
|
||||
_params.SunDir =
|
||||
AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(keyframe);
|
||||
|
||||
_gl.BindBufferBase(
|
||||
BufferTargetARB.UniformBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.UniformSkyParams,
|
||||
_paramsUbo);
|
||||
|
||||
// Save + override GL state.
|
||||
_gl.DepthMask(false);
|
||||
|
|
@ -308,15 +368,15 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
if (postScenePass && obj.IsWeather && (obj.Properties & 0x08u) == 0u)
|
||||
model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f);
|
||||
|
||||
_shader.SetMatrix4("uModel", model);
|
||||
_params.Model = model;
|
||||
|
||||
// UV scroll accumulates real-time × velocity. Wrap to [0, 1]
|
||||
// so long-running sessions don't accumulate float precision
|
||||
// loss in the fragment UV.
|
||||
float uOffset = (obj.TexVelocityX * secondsSinceStart) % 1f;
|
||||
float vOffset = (obj.TexVelocityY * secondsSinceStart) % 1f;
|
||||
_shader.SetVec2("uUvScroll", new Vector2(uOffset, vOffset));
|
||||
_shader.SetFloat("uTransparency", transparent);
|
||||
_params.UvScroll = new Vector2(uOffset, vOffset);
|
||||
_params.Transparency = transparent;
|
||||
|
||||
EnsureMeshUploaded(gfxObjId);
|
||||
if (!_gpuByGfxObj.TryGetValue(gfxObjId, out var subMeshes)) continue;
|
||||
|
|
@ -363,14 +423,14 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
float effDiffuse = float.IsNaN(replaceDiffuse)
|
||||
? sub.SurfDiffuse
|
||||
: replaceDiffuse;
|
||||
_shader.SetFloat("uEmissive", effEmissive);
|
||||
_shader.SetFloat("uDiffuseFactor", effDiffuse);
|
||||
_params.Emissive = effEmissive;
|
||||
_params.DiffuseFactor = effDiffuse;
|
||||
|
||||
// Material alpha is final opacity: 1 - Surface.Translucency
|
||||
// for Translucent surfaces, 1 for non-Translucent surfaces.
|
||||
// The CPU computes it once so the shader just multiplies it
|
||||
// with texture alpha and keyframe transparency.
|
||||
_shader.SetFloat("uSurfOpacity", sub.SurfOpacity);
|
||||
_params.SurfOpacity = sub.SurfOpacity;
|
||||
|
||||
// Retail D3DPolyRender::SetSurface at 0x59c882 calls
|
||||
// SetFFFogAlphaDisabled(1) when the Additive flag (0x10000)
|
||||
|
|
@ -383,11 +443,9 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
// in sky.frag. That restores the broad green/purple Rainy
|
||||
// DayGroup tint behind the cloud sheet while raw-additive
|
||||
// 0x08000023 remains unfogged and keeps the pink detail.
|
||||
_shader.SetFloat("uApplyFog", sub.DisableFog ? 0f : 1f);
|
||||
_params.ApplyFog = sub.DisableFog ? 0f : 1f;
|
||||
|
||||
uint tex = _textures.GetOrUpload(sub.SurfaceId);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
// Sky meshes need per-object wrap mode driven by the
|
||||
// mesh's authored UV range, not by TexVelocity:
|
||||
|
|
@ -411,16 +469,30 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
// UV offset can drift outside [0,1] regardless of authored
|
||||
// range, and they'd show their own seam bleed otherwise).
|
||||
//
|
||||
// Implementation: bind a persistent sampler object to
|
||||
// texture unit 0. Sampler state overrides the texture's
|
||||
// own wrap state, so two renderers can share the same
|
||||
// texture handle but sample it with different wrap modes
|
||||
// safely. Ported from WorldBuilder
|
||||
// Implementation, before Campaign V slice V6e: bind one of two
|
||||
// persistent sampler objects to texture unit 0, because sampler
|
||||
// state overrides the texture's own wrap parameters and two
|
||||
// renderers can then share a texture but sample it differently.
|
||||
// Ported from WorldBuilder
|
||||
// (Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs:312).
|
||||
//
|
||||
// After V6e the same two sampler objects are still what decides
|
||||
// the wrap mode — they are just consulted once, when the
|
||||
// (texture, wrap) pair is interned as a bindless handle, rather
|
||||
// than per draw. A bindless handle carries its sampler with it,
|
||||
// so the choice is now WHICH TABLE SLOT this submesh asks for.
|
||||
// Same two GL sampler objects, same wrap behaviour, and it is
|
||||
// the shape Vulkan's table already has.
|
||||
bool needsRepeat = sub.NeedsUvRepeat
|
||||
|| obj.TexVelocityX != 0f
|
||||
|| obj.TexVelocityY != 0f;
|
||||
_gl.BindSampler(0, needsRepeat ? _samplers.Wrap : _samplers.Clamp);
|
||||
_gl.ProgramUniform1(
|
||||
_shader.Program,
|
||||
_uTextureIndexALoc,
|
||||
TextureTableSlot(tex, needsRepeat));
|
||||
|
||||
UploadParams();
|
||||
FlushAndBindTextureTable();
|
||||
|
||||
_gl.BindVertexArray(sub.Vao);
|
||||
_gl.DrawElements(PrimitiveType.Triangles,
|
||||
|
|
@ -431,12 +503,13 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
}
|
||||
|
||||
// Restore GL state expected by the rest of the pipeline.
|
||||
// Critical: unbind the sampler from unit 0. While bound, sampler
|
||||
// state overrides the texture's own wrap parameters, so leaving
|
||||
// (e.g.) Clamp bound would silently force ClampToEdge on every
|
||||
// subsequent draw on unit 0 regardless of how that texture was
|
||||
// configured at upload time.
|
||||
_gl.BindSampler(0, 0);
|
||||
//
|
||||
// Slice V6e removed the glBindSampler(0, …) that used to happen here
|
||||
// and its matching unbind. The sky no longer touches texture unit 0 at
|
||||
// all — its wrap mode rides inside the bindless handle — so there is
|
||||
// nothing left to leak onto the next renderer's unit 0. That unbind was
|
||||
// load-bearing precisely because the binding was global state; a table
|
||||
// slot is not.
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
|
|
@ -444,6 +517,88 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6e: the table slot for one (texture, wrap-mode) pair,
|
||||
/// interning a resident bindless handle on first use.
|
||||
///
|
||||
/// <para>Two slots per texture is not waste — it is the wrap mode. A
|
||||
/// bindless handle bakes its sampler, so the dome sampled CLAMP_TO_EDGE and
|
||||
/// a scrolling cloud sheet sampled REPEAT are two different handles even
|
||||
/// when they name the same GL texture. Vulkan's table has the same property
|
||||
/// for the same reason: an entry there is a combined image sampler.</para>
|
||||
///
|
||||
/// <para>Entries accumulate for the renderer's lifetime, like every other
|
||||
/// handle table in the codebase — the sky's texture set is a fixed handful
|
||||
/// per day group and does not churn.</para>
|
||||
/// </summary>
|
||||
private uint TextureTableSlot(uint textureName, bool repeat)
|
||||
{
|
||||
var key = (textureName, repeat);
|
||||
if (!_handleByTextureAndWrap.TryGetValue(key, out ulong handle))
|
||||
{
|
||||
handle = _bindless.GetResidentHandle(
|
||||
textureName,
|
||||
repeat ? _samplers.Wrap : _samplers.Clamp);
|
||||
_handleByTextureAndWrap.Add(key, handle);
|
||||
}
|
||||
|
||||
return _textureTable.GetOrAdd(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the current <see cref="SkyParams"/> to its uniform buffer. Called
|
||||
/// immediately before each draw, which is the cadence the per-submesh
|
||||
/// glUniform* calls it replaces already had.
|
||||
/// </summary>
|
||||
private void UploadParams()
|
||||
{
|
||||
fixed (void* p = &_params)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo);
|
||||
_gl.BufferSubData(
|
||||
BufferTargetARB.UniformBuffer, 0, (nuint)SkyParams.SizeInBytes, p);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads newly interned handles to the binding=9 table and (re)binds it.
|
||||
/// Mirrors <c>ParticleRenderer.FlushAndBindTextureTable</c>, including its
|
||||
/// reason for running per draw rather than per pass: a submesh partway
|
||||
/// through the sky stack can be the first to ask for a wrap mode.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
if (_textureTable.Dirty)
|
||||
{
|
||||
ReadOnlySpan<ulong> handles = _textureTable.Handles;
|
||||
int byteCount = handles.Length * sizeof(ulong);
|
||||
fixed (ulong* p = handles)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _textureTableSsbo);
|
||||
if (_textureTableSsboCapacityBytes < byteCount)
|
||||
{
|
||||
int grown = DynamicBufferCapacity.Grow(_textureTableSsboCapacityBytes, byteCount);
|
||||
Wb.TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
grown,
|
||||
BufferUsageARB.DynamicDraw,
|
||||
"growing sky texture-table SSBO");
|
||||
_textureTableSsboCapacityBytes = grown;
|
||||
}
|
||||
_gl.BufferSubData(BufferTargetARB.ShaderStorageBuffer, 0, (nuint)byteCount, p);
|
||||
}
|
||||
_textureTable.MarkFlushed();
|
||||
}
|
||||
|
||||
_gl.BindBufferBase(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the <see cref="SkyObjectReplaceData"/> entries for the
|
||||
/// keyframe currently "active" at <paramref name="dayFraction"/>.
|
||||
|
|
@ -737,6 +892,62 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
_gpuByGfxObj.Clear();
|
||||
|
||||
// Campaign V slice V6e. Residency first: a handle made resident pins
|
||||
// its texture's GPU address, and the textures themselves belong to
|
||||
// TextureCache, which outlives this renderer and disposes them itself.
|
||||
foreach (ulong handle in _handleByTextureAndWrap.Values)
|
||||
_bindless.MakeNonResident(handle);
|
||||
_handleByTextureAndWrap.Clear();
|
||||
|
||||
if (_paramsUbo != 0)
|
||||
{
|
||||
Wb.TrackedGlResource.DeleteBuffer(
|
||||
_gl, _paramsUbo, SkyParams.SizeInBytes, "sky params UBO disposal");
|
||||
_paramsUbo = 0;
|
||||
}
|
||||
|
||||
if (_textureTableSsbo != 0)
|
||||
{
|
||||
Wb.TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
"sky texture-table SSBO disposal");
|
||||
_textureTableSsbo = 0;
|
||||
_textureTableSsboCapacityBytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6e: the CPU mirror of sky.{vert,frag}'s <c>SkyParams</c>
|
||||
/// std140 block. Sequential layout with 4-byte packing reproduces std140
|
||||
/// exactly here because every member is placed so that the float following
|
||||
/// each <c>vec3</c> occupies the pad word std140 would insert anyway — which
|
||||
/// is why the lighting colours and per-surface scalars interleave rather
|
||||
/// than being grouped by meaning. <c>SkyParamsLayoutTests</c> asserts every
|
||||
/// offset and the total size, because a silent one-word slip here would
|
||||
/// misread the sun direction as a colour and nothing would say so.
|
||||
/// </summary>
|
||||
[System.Runtime.InteropServices.StructLayout(
|
||||
System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4)]
|
||||
internal struct SkyParams
|
||||
{
|
||||
public Matrix4x4 Model; // 0
|
||||
public Matrix4x4 SkyView; // 64
|
||||
public Matrix4x4 SkyProjection; // 128
|
||||
public Vector3 AmbientColor; // 192
|
||||
public float Emissive; // 204
|
||||
public Vector3 SunColor; // 208
|
||||
public float DiffuseFactor; // 220
|
||||
public Vector3 SunDir; // 224
|
||||
public float Transparency; // 236
|
||||
public Vector2 UvScroll; // 240
|
||||
public float ApplyFog; // 248
|
||||
public float SurfOpacity; // 252
|
||||
|
||||
/// <summary>256 — the std140 size of the block, a whole number of vec4s.</summary>
|
||||
public const int SizeInBytes = 256;
|
||||
}
|
||||
|
||||
private sealed class SubMeshGpu
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue