feat(render): Phase U.3 — GPU clip-plane gate (gl_ClipDistance), no-clip default
Adds the GPU mechanism to clip drawing to a per-cell screen-space convex
region via gl_ClipDistance, consumed by the mesh + terrain vertex shaders.
This is the MECHANISM only — every instance defaults to slot 0 (no-clip /
pass-all) and terrain to count 0, so the running game renders IDENTICALLY to
pre-U.3 (verified: offline launch compiles both shaders and reaches steady
state; no GL errors). U.4 populates real clip data from portal visibility.
Binding contract (define once, both sides obey):
- mesh_modern.vert: SSBO binding=2 CellClip[] (shared per-frame regions, slot 0
reserved no-clip) + SSBO binding=3 uint[] per-instance slot, indexed by the
IDENTICAL gl_BaseInstanceARB+gl_InstanceID used for binding=0. binding=0/1
untouched.
- terrain_modern.vert: UBO binding=2 TerrainClip { int count; vec4 planes[8]; }
for the single OutsideView region (UBO namespace; SceneLighting is UBO
binding=1, so binding=2 is free and does not collide with the mesh SSBO
binding=2). count 0 = ungated.
- Both redeclare out gl_PerVertex { vec4 gl_Position; float gl_ClipDistance[8]; }
and set unused planes (i >= count) to +1.0 so they pass everything.
CellClip std430 layout (144 bytes/slot): count@0, 3 pad uints@4/8/12,
planes[8]@16 (vec4 stride 16). Terrain UBO std140: count@0 (padded to 16),
planes[8]@16 → 144 bytes. Verified by ClipFrameLayoutTests (8 new tests).
Pieces:
- ClipFrame: per-frame container + uploader for the SHARED clip data (binding=2
SSBO + terrain UBO). NoClip() = slot 0 + terrain count 0. AppendSlot /
SetTerrainClip pack std430/std140 bytes for U.4. UploadShared binds both.
- WbDrawDispatcher + EnvCellRenderer: each owns its binding=3 zero buffer
(all-zeros sized to its instance count → slot 0), re-binds binding=2 from the
shared ClipFrame id (or an internal no-clip fallback if unwired) before MDI.
gl_ClipDistance is per-vertex, so the single glMultiDrawElementsIndirect per
group is preserved — no draw splitting.
- TerrainModernRenderer: binds the terrain clip UBO (shared or no-clip fallback)
before its draw.
- GameWindow: glEnable(GL_CLIP_DISTANCE0..7) once at init (unused planes pass-all
so always-on avoids per-draw thrash); per frame builds ClipFrame.NoClip(),
UploadShared, and hands the buffer ids to the three renderers (tiny diff; U.4
swaps NoClip() for the real portal-visibility frame).
Gate: dotnet build green; App suite 134/134; offline launch confirms both
shaders compile + link with no GL errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b125830fe
commit
bf2e559369
8 changed files with 797 additions and 1 deletions
276
src/AcDream.App/Rendering/ClipFrame.cs
Normal file
276
src/AcDream.App/Rendering/ClipFrame.cs
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
// ClipFrame.cs
|
||||
//
|
||||
// Phase U.3: the GPU-side container + uploader for the SHARED per-frame clip
|
||||
// data consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert
|
||||
// (UBO binding=2). This is the "shared" half of the U.3 clip mechanism; the
|
||||
// per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by
|
||||
// each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance
|
||||
// buffer — it is NOT here.
|
||||
//
|
||||
// === The contract (both shader sides obey) ===================================
|
||||
// binding=2 mesh SSBO holds an array of CellClip, one per "slot":
|
||||
// struct CellClip { uint count; uint _p0; uint _p1; uint _p2; vec4 planes[8]; };
|
||||
// std430 layout: count at byte 0, three pad uints at 4/8/12, planes[8] at 16
|
||||
// (vec4 stride 16) → 144 bytes per slot. Slot 0 is RESERVED = no-clip (count 0).
|
||||
// binding=2 terrain UBO holds the single OutsideView region:
|
||||
// layout(std140) { int uTerrainClipCount; vec4 uTerrainClipPlanes[8]; };
|
||||
// std140 layout: count at byte 0 (padded to 16), planes[8] at 16 → 144 bytes.
|
||||
//
|
||||
// In U.3 a ClipFrame is built via NoClip(): one slot (slot 0, count 0) and a
|
||||
// terrain count of 0. Everything renders exactly as before. U.4 populates real
|
||||
// slots from a PortalVisibilityFrame (one CellClip per visible cell) and sets the
|
||||
// terrain OutsideView planes, then points each renderer's per-instance slot
|
||||
// buffer at the right slots.
|
||||
//
|
||||
// Pure CPU byte-packing + a thin GL upload. NO GL types appear except in
|
||||
// UploadShared. The byte layout is asserted by ClipFrameLayoutTests so a silent
|
||||
// std430/std140 drift can't reach the GPU.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame container + uploader for the SHARED clip data: the binding=2 mesh
|
||||
/// SSBO (one <c>CellClip</c> per slot, slot 0 reserved no-clip) and the binding=2
|
||||
/// terrain UBO (the single OutsideView region). See the file header for the exact
|
||||
/// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by
|
||||
/// each renderer, not here.
|
||||
/// </summary>
|
||||
public sealed class ClipFrame : IDisposable
|
||||
{
|
||||
// ---- Layout constants (mirror mesh_modern.vert + terrain_modern.vert) ----
|
||||
|
||||
/// <summary>Max planes per clip region — matches the shader's <c>planes[8]</c>
|
||||
/// and GL's guaranteed <c>GL_MAX_CLIP_DISTANCES >= 8</c>.</summary>
|
||||
public const int MaxPlanes = 8;
|
||||
|
||||
/// <summary>std430 stride of one <c>CellClip</c>: 16 (count + 3 pad uints) +
|
||||
/// 8 × 16 (vec4 planes) = 144 bytes.</summary>
|
||||
public const int CellClipStrideBytes = 16 + MaxPlanes * 16; // 144
|
||||
|
||||
/// <summary>Byte offset of <c>planes[0]</c> within a <c>CellClip</c> (after the
|
||||
/// count + 3 pad uints).</summary>
|
||||
public const int CellClipPlanesOffset = 16;
|
||||
|
||||
/// <summary>std140 size of the terrain UBO block: int count padded to 16, then
|
||||
/// 8 × 16 (vec4 planes) = 144 bytes. Same number as the SSBO stride by
|
||||
/// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.</summary>
|
||||
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
|
||||
|
||||
/// <summary>SSBO binding index for the shared per-cell clip regions
|
||||
/// (mesh_modern.vert binding=2).</summary>
|
||||
public const uint MeshClipSsboBinding = 2;
|
||||
|
||||
/// <summary>UBO binding index for the terrain OutsideView clip region
|
||||
/// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO
|
||||
/// binding=2 above.</summary>
|
||||
public const uint TerrainClipUboBinding = 2;
|
||||
|
||||
// ---- CPU-side state ------------------------------------------------------
|
||||
|
||||
// Packed std430 bytes for clipRegions[]. Always holds at least slot 0.
|
||||
private byte[] _regionBytes;
|
||||
private int _slotCount;
|
||||
|
||||
// Packed std140 bytes for the terrain UBO (always TerrainUboBytes long).
|
||||
private readonly byte[] _terrainBytes = new byte[TerrainUboBytes];
|
||||
|
||||
// ---- GL-side state (lazily created on first UploadShared) ----------------
|
||||
|
||||
private uint _regionSsbo;
|
||||
private uint _terrainUbo;
|
||||
private bool _glInitialized;
|
||||
private bool _disposed;
|
||||
|
||||
private ClipFrame(byte[] regionBytes, int slotCount)
|
||||
{
|
||||
_regionBytes = regionBytes;
|
||||
_slotCount = slotCount;
|
||||
// Terrain defaults to count 0 (ungated). _terrainBytes is already all
|
||||
// zeros, which encodes count=0 + zeroed (unused) planes.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The U.3 default frame: exactly slot 0 (no-clip, count 0) and a terrain
|
||||
/// count of 0. The whole scene renders ungated — identical to pre-U.3. U.4
|
||||
/// replaces this with a frame built from real portal visibility.
|
||||
/// </summary>
|
||||
public static ClipFrame NoClip()
|
||||
{
|
||||
// One slot, all zeros: count=0 ⇒ shader passes every plane.
|
||||
var bytes = new byte[CellClipStrideBytes];
|
||||
return new ClipFrame(bytes, slotCount: 1);
|
||||
}
|
||||
|
||||
/// <summary>Number of clip slots currently packed (always >= 1 — slot 0 is
|
||||
/// the reserved no-clip slot).</summary>
|
||||
public int SlotCount => _slotCount;
|
||||
|
||||
/// <summary>The shared mesh-clip SSBO id, or 0 before the first
|
||||
/// <see cref="UploadShared"/>. Renderers may bind this directly if they don't
|
||||
/// receive it via a parameter; <see cref="UploadShared"/> already binds it to
|
||||
/// <see cref="MeshClipSsboBinding"/>.</summary>
|
||||
public uint RegionSsbo => _regionSsbo;
|
||||
|
||||
/// <summary>The terrain-clip UBO id, or 0 before the first
|
||||
/// <see cref="UploadShared"/>. Handed to <see cref="TerrainModernRenderer"/>
|
||||
/// so it can re-bind binding=2 (UBO namespace) before its draw.</summary>
|
||||
public uint TerrainUbo => _terrainUbo;
|
||||
|
||||
/// <summary>
|
||||
/// Append one clip region (becomes the next slot index) from a
|
||||
/// <see cref="ClipPlaneSet"/>. Only the convex-plane case is supported in
|
||||
/// U.3 — <c>Count > 0</c> packs that many planes; <c>Count == 0</c> packs a
|
||||
/// no-clip region (pass-all). The scissor / nothing-visible fallbacks that
|
||||
/// <see cref="ClipPlaneSet"/> can carry are deferred to U.4 (which will draw
|
||||
/// the AABB box or skip the cell on the CPU side, not via this slot). Returns
|
||||
/// the new slot's index.
|
||||
/// </summary>
|
||||
public int AppendSlot(ClipPlaneSet set)
|
||||
{
|
||||
int count = Math.Min(set.Count, MaxPlanes);
|
||||
if (count == 0)
|
||||
return AppendSlot(ReadOnlySpan<Vector4>.Empty);
|
||||
|
||||
Span<Vector4> planes = stackalloc Vector4[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
planes[i] = set.Planes[i];
|
||||
return AppendSlot(planes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append one clip region from a raw plane list. <paramref name="planes"/>
|
||||
/// length 0 packs a no-clip (pass-all) region; otherwise up to
|
||||
/// <see cref="MaxPlanes"/> planes are packed (extras ignored). Each plane is
|
||||
/// <c>(nx, ny, 0, dw)</c> in clip space; a clip-space vertex is inside iff
|
||||
/// <c>dot(plane, gl_Position) >= 0</c> for every plane. Returns the new
|
||||
/// slot index.
|
||||
/// </summary>
|
||||
public int AppendSlot(ReadOnlySpan<Vector4> planes)
|
||||
{
|
||||
int count = Math.Min(planes.Length, MaxPlanes);
|
||||
|
||||
int slot = _slotCount;
|
||||
int byteOffset = slot * CellClipStrideBytes;
|
||||
EnsureRegionCapacity(byteOffset + CellClipStrideBytes);
|
||||
|
||||
// count (uint) at byteOffset; the 3 pad uints stay zero.
|
||||
WriteUInt(_regionBytes, byteOffset, (uint)count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int po = byteOffset + CellClipPlanesOffset + i * 16;
|
||||
WriteVec4(_regionBytes, po, planes[i]);
|
||||
}
|
||||
|
||||
_slotCount++;
|
||||
return slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the terrain OutsideView clip region (the single region the terrain
|
||||
/// shader gates against). <paramref name="planes"/> length 0 ungates terrain
|
||||
/// (count 0). U.3 callers never touch this — <see cref="NoClip"/> leaves it
|
||||
/// at count 0. U.4 calls it with the OutsideView planes.
|
||||
/// </summary>
|
||||
public void SetTerrainClip(ReadOnlySpan<Vector4> planes)
|
||||
{
|
||||
int count = Math.Min(planes.Length, MaxPlanes);
|
||||
Array.Clear(_terrainBytes);
|
||||
WriteInt(_terrainBytes, 0, count);
|
||||
for (int i = 0; i < count; i++)
|
||||
WriteVec4(_terrainBytes, CellClipPlanesOffset + i * 16, planes[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload the shared mesh-clip SSBO (binding=2) and the terrain-clip UBO
|
||||
/// (binding=2, UBO namespace) and bind both to their binding points. Idempotent
|
||||
/// to call once per frame. Creates the GL buffers lazily on first call.
|
||||
/// </summary>
|
||||
public unsafe void UploadShared(GL gl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
if (!_glInitialized)
|
||||
{
|
||||
_regionSsbo = gl.GenBuffer();
|
||||
_terrainUbo = gl.GenBuffer();
|
||||
_glInitialized = true;
|
||||
}
|
||||
|
||||
int regionByteCount = _slotCount * CellClipStrideBytes;
|
||||
gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _regionSsbo);
|
||||
fixed (byte* p = _regionBytes)
|
||||
{
|
||||
gl.BufferData(BufferTargetARB.ShaderStorageBuffer,
|
||||
(nuint)regionByteCount, p, BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, MeshClipSsboBinding, _regionSsbo);
|
||||
|
||||
gl.BindBuffer(BufferTargetARB.UniformBuffer, _terrainUbo);
|
||||
fixed (byte* p = _terrainBytes)
|
||||
{
|
||||
gl.BufferData(BufferTargetARB.UniformBuffer,
|
||||
(nuint)TerrainUboBytes, p, BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
gl.BindBufferBase(BufferTargetARB.UniformBuffer, TerrainClipUboBinding, _terrainUbo);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
// GL buffers are deleted by the owner's GL context teardown; ClipFrame
|
||||
// is a per-frame transient in U.3 (NoClip() each frame). We do not hold a
|
||||
// GL handle to delete here because UploadShared may not have run. If a
|
||||
// future phase makes ClipFrame long-lived, add buffer deletion guarded by
|
||||
// _glInitialized + a captured GL reference.
|
||||
}
|
||||
|
||||
// ---- byte helpers (little-endian; matches x86/x64 GPU upload) ------------
|
||||
|
||||
private void EnsureRegionCapacity(int requiredBytes)
|
||||
{
|
||||
if (_regionBytes.Length >= requiredBytes) return;
|
||||
int newLen = Math.Max(requiredBytes, _regionBytes.Length * 2);
|
||||
Array.Resize(ref _regionBytes, newLen);
|
||||
}
|
||||
|
||||
private static void WriteUInt(byte[] dst, int offset, uint value)
|
||||
{
|
||||
dst[offset + 0] = (byte)(value & 0xFF);
|
||||
dst[offset + 1] = (byte)((value >> 8) & 0xFF);
|
||||
dst[offset + 2] = (byte)((value >> 16) & 0xFF);
|
||||
dst[offset + 3] = (byte)((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
private static void WriteInt(byte[] dst, int offset, int value)
|
||||
=> WriteUInt(dst, offset, unchecked((uint)value));
|
||||
|
||||
private static void WriteVec4(byte[] dst, int offset, Vector4 v)
|
||||
{
|
||||
WriteFloat(dst, offset + 0, v.X);
|
||||
WriteFloat(dst, offset + 4, v.Y);
|
||||
WriteFloat(dst, offset + 8, v.Z);
|
||||
WriteFloat(dst, offset + 12, v.W);
|
||||
}
|
||||
|
||||
private static void WriteFloat(byte[] dst, int offset, float value)
|
||||
{
|
||||
uint bits = BitConverter.SingleToUInt32Bits(value);
|
||||
WriteUInt(dst, offset, bits);
|
||||
}
|
||||
|
||||
// ---- Test seams ----------------------------------------------------------
|
||||
|
||||
/// <summary>Test seam: the packed std430 region bytes (slot 0..SlotCount-1).
|
||||
/// Read-only snapshot used by ClipFrameLayoutTests to assert the byte layout.</summary>
|
||||
internal ReadOnlySpan<byte> RegionBytesForTest => _regionBytes.AsSpan(0, _slotCount * CellClipStrideBytes);
|
||||
|
||||
/// <summary>Test seam: the packed std140 terrain UBO bytes.</summary>
|
||||
internal ReadOnlySpan<byte> TerrainBytesForTest => _terrainBytes;
|
||||
}
|
||||
|
|
@ -166,6 +166,14 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer;
|
||||
private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
// UBO). In U.3 this is rebuilt as ClipFrame.NoClip() every frame and uploaded
|
||||
// once before terrain/entities draw, so the whole scene renders ungated
|
||||
// (identical to pre-U.3). The buffer ids are handed to the three renderers so
|
||||
// each re-binds binding=2 immediately before its own draw. U.4 replaces the
|
||||
// NoClip() frame with one built from the portal-visibility result.
|
||||
private ClipFrame? _clipFrame;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.4: per-entity animation playback state for entities whose
|
||||
/// MotionTable resolved to a real cycle. The render loop ticks each
|
||||
|
|
@ -1079,6 +1087,17 @@ public sealed class GameWindow : IDisposable
|
|||
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
|
||||
// Phase U.3: enable all 8 hardware clip planes once at startup. The mesh
|
||||
// and terrain vertex shaders write gl_ClipDistance[0..7] every frame;
|
||||
// unused planes are set to +1.0 (pass-all) when a region's plane count is
|
||||
// below 8, so leaving all 8 enabled permanently is correct and avoids
|
||||
// per-draw glEnable/glDisable thrash. In U.3 every region is no-clip, so
|
||||
// nothing is actually clipped — the running game renders identically. U.4
|
||||
// populates real clip regions; the enables stay as-is.
|
||||
// (EnableCap.ClipDistance0 == GL_CLIP_DISTANCE0 0x3000; +i selects plane i.)
|
||||
for (int _cp = 0; _cp < ClipFrame.MaxPlanes; _cp++)
|
||||
_gl.Enable(EnableCap.ClipDistance0 + _cp);
|
||||
|
||||
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
|
||||
|
||||
// Phase N.5b: terrain_modern shader pair — bindless texture handles +
|
||||
|
|
@ -7261,6 +7280,21 @@ public sealed class GameWindow : IDisposable
|
|||
goto SkipWorldGeometry;
|
||||
}
|
||||
|
||||
// Phase U.3: build + upload the SHARED per-frame clip data once,
|
||||
// ahead of both terrain and entity draws. In U.3 this is the no-clip
|
||||
// frame (slot 0 only, terrain count 0) so the whole scene renders
|
||||
// ungated — bit-identical to pre-U.3. UploadShared binds binding=2
|
||||
// (mesh SSBO) + binding=2 (terrain UBO); each renderer below re-binds
|
||||
// its binding=2 defensively from the ids we hand it. The single
|
||||
// _clipFrame instance reuses its GL buffers across frames (NoClip is
|
||||
// cheap CPU-only state we copy into it). U.4 swaps NoClip() for the
|
||||
// real portal-visibility frame here.
|
||||
_clipFrame ??= ClipFrame.NoClip();
|
||||
_clipFrame.UploadShared(_gl);
|
||||
_wbDrawDispatcher?.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
_envCellRenderer?.SetClipRegionSsbo(_clipFrame.RegionSsbo);
|
||||
_terrain?.SetClipUbo(_clipFrame.TerrainUbo);
|
||||
|
||||
// Phase N.5b: wrap Draw in CPU stopwatch for [TERRAIN-DIAG] rollup
|
||||
// (gated on ACDREAM_WB_DIAG=1, same env var as [WB-DIAG]). Stopwatch
|
||||
// is cheap; only the periodic Console.WriteLine is gated.
|
||||
|
|
@ -10659,6 +10693,7 @@ public sealed class GameWindow : IDisposable
|
|||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_clipFrame?.Dispose(); // Phase U.3
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
_textureCache?.Dispose();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,48 @@ layout(std430, binding = 1) readonly buffer BatchBuffer {
|
|||
BatchData Batches[];
|
||||
};
|
||||
|
||||
// === Phase U.3: per-cell screen-space clip gate (gl_ClipDistance) =============
|
||||
// Two SSBOs add the clip mechanism without disturbing binding=0/1 above.
|
||||
//
|
||||
// binding=2 — SHARED per-frame clip regions, one CellClip per "slot". Uploaded
|
||||
// ONCE per frame by ClipFrame.UploadShared (shared across WbDrawDispatcher +
|
||||
// EnvCellRenderer). Slot 0 is RESERVED = no-clip (count 0 ⇒ every plane passes).
|
||||
//
|
||||
// binding=3 — PER-RENDERER per-instance slot index, parallel to the binding=0
|
||||
// instance buffer and indexed by the IDENTICAL per-instance index
|
||||
// (gl_BaseInstanceARB + gl_InstanceID). instanceClipSlot[i] selects which
|
||||
// CellClip region instance i is clipped against. Default all-zeros in U.3 ⇒
|
||||
// every instance maps to slot 0 ⇒ no clipping ⇒ identical render to pre-U.3.
|
||||
//
|
||||
// CellClip std430 layout (144 bytes/slot): a uint count + 3 pad uints (16 bytes)
|
||||
// then vec4 planes[8] (8 × 16 = 128 bytes). vec4 array stride is 16 under std430.
|
||||
// ClipFrame on the CPU side lays out the bytes to match exactly (verified by
|
||||
// ClipFrameLayoutTests). A clip-space vertex is INSIDE iff dot(plane, gl_Position)
|
||||
// >= 0 for every active plane (see ClipPlaneSet for the plane convention).
|
||||
struct CellClip {
|
||||
uint count;
|
||||
uint _p0;
|
||||
uint _p1;
|
||||
uint _p2;
|
||||
vec4 planes[8];
|
||||
};
|
||||
layout(std430, binding = 2) readonly buffer ClipRegionBuf {
|
||||
CellClip clipRegions[];
|
||||
};
|
||||
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
||||
uint instanceClipSlot[];
|
||||
};
|
||||
|
||||
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal
|
||||
// alongside gl_Position. The array is sized 8 to match the CellClip plane budget
|
||||
// and the GL guarantee (GL_MAX_CLIP_DISTANCES >= 8). The host enables
|
||||
// GL_CLIP_DISTANCE0..7 once at startup; unused planes are set to +1.0 below so
|
||||
// they pass everything (no clipping) when the slot's count < 8.
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
float gl_ClipDistance[8];
|
||||
};
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
|
||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
|
||||
|
|
@ -67,6 +109,18 @@ void main() {
|
|||
vec4 worldPos = model * vec4(aPosition, 1.0);
|
||||
gl_Position = uViewProjection * worldPos;
|
||||
|
||||
// Phase U.3: per-instance clip gate. instanceClipSlot is indexed by the
|
||||
// SAME instanceIndex used for the binding=0 transform above, so the slot
|
||||
// travels with the instance through the MDI BaseInstance offsets. Slot 0
|
||||
// (the U.3 default) has count 0 ⇒ the second loop sets all 8 distances to
|
||||
// +1.0 ⇒ nothing is clipped.
|
||||
uint _slot = instanceClipSlot[instanceIndex];
|
||||
CellClip _c = clipRegions[_slot];
|
||||
for (uint i = 0u; i < _c.count; ++i)
|
||||
gl_ClipDistance[i] = dot(_c.planes[i], gl_Position);
|
||||
for (uint i = _c.count; i < 8u; ++i)
|
||||
gl_ClipDistance[i] = 1.0;
|
||||
|
||||
vWorldPos = worldPos.xyz;
|
||||
vNormal = normalize(mat3(model) * aNormal);
|
||||
vTexCoord = aTexCoord;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,28 @@ layout(std140, binding = 1) uniform SceneLighting {
|
|||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
// === Phase U.3: terrain screen-space clip gate (OutsideView region) ===========
|
||||
// Terrain is a single global region (the OutsideView), so it needs one set of
|
||||
// clip planes, not a per-instance slot table like the mesh shader. A std140 UBO
|
||||
// at binding=2 carries it. The UBO binding namespace is distinct from the SSBO
|
||||
// binding namespace, so this does NOT collide with the mesh shader's SSBO
|
||||
// binding=2 — and within THIS shader binding=1 (SceneLighting) is the only other
|
||||
// UBO, leaving binding=2 free. uTerrainClipCount == 0 (the U.3 default) ungates
|
||||
// terrain entirely (the second loop sets all 8 distances to +1.0). Uploaded by
|
||||
// ClipFrame.UploadShared each frame; TerrainModernRenderer binds it before draw.
|
||||
layout(std140, binding = 2) uniform TerrainClip {
|
||||
int uTerrainClipCount;
|
||||
vec4 uTerrainClipPlanes[8];
|
||||
};
|
||||
|
||||
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal.
|
||||
// Sized 8 to match GL_MAX_CLIP_DISTANCES >= 8. Host enables GL_CLIP_DISTANCE0..7
|
||||
// once at startup; unused planes are set to +1.0 below so they pass everything.
|
||||
out gl_PerVertex {
|
||||
vec4 gl_Position;
|
||||
float gl_ClipDistance[8];
|
||||
};
|
||||
|
||||
out vec2 vBaseUV;
|
||||
out vec3 vWorldNormal;
|
||||
out vec3 vWorldPos;
|
||||
|
|
@ -144,4 +166,12 @@ void main() {
|
|||
// Closes issue #100; supersedes the hiddenTerrainCells cell-collapse hack.
|
||||
vec3 terrainPos = vec3(aPos.xy, aPos.z - 0.01);
|
||||
gl_Position = uProjection * uView * vec4(terrainPos, 1.0);
|
||||
|
||||
// Phase U.3: terrain clip gate against the single OutsideView region. With
|
||||
// uTerrainClipCount == 0 (U.3 default) the first loop is skipped and the
|
||||
// second sets all 8 distances to +1.0 ⇒ no clipping ⇒ identical terrain.
|
||||
for (int i = 0; i < uTerrainClipCount; ++i)
|
||||
gl_ClipDistance[i] = dot(uTerrainClipPlanes[i], gl_Position);
|
||||
for (int i = uTerrainClipCount; i < 8; ++i)
|
||||
gl_ClipDistance[i] = 1.0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
|||
private uint _indirectBuffer;
|
||||
private int _indirectCapacity;
|
||||
|
||||
// Phase U.3: terrain clip UBO (binding=2, terrain_modern.vert TerrainClip).
|
||||
// The shared one is created + uploaded by the GameWindow-level ClipFrame and
|
||||
// handed in via SetClipUbo. When 0, we bind a lazily-created no-clip fallback
|
||||
// (count 0 = ungated) so the shader never reads an unbound UBO at binding=2.
|
||||
private uint _sharedClipUbo;
|
||||
private uint _fallbackClipUbo;
|
||||
|
||||
// Cached uvec2-handle uniform locations (matrix uniforms are set by name via Shader.SetMatrix4).
|
||||
private int _uTerrainHandleLoc;
|
||||
private int _uAlphaHandleLoc;
|
||||
|
|
@ -93,6 +100,14 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
|||
_indirectBuffer = _gl.GenBuffer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: hand the renderer the SHARED terrain-clip UBO (binding=2)
|
||||
/// created by <see cref="ClipFrame.UploadShared"/>. The renderer binds it to
|
||||
/// binding=2 before its draw. Pass 0 to fall back to the internal no-clip UBO
|
||||
/// (count 0 = ungated terrain).
|
||||
/// </summary>
|
||||
public void SetClipUbo(uint sharedClipUbo) => _sharedClipUbo = sharedClipUbo;
|
||||
|
||||
/// <summary>
|
||||
/// Two-tier streaming entry point. Accepts a prebuilt mesh from
|
||||
/// <see cref="LandblockStreamResult.Loaded.MeshData"/> built on the worker
|
||||
|
|
@ -258,6 +273,10 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
|||
_gl.ProgramUniform2(_shader.Program, _uAlphaHandleLoc,
|
||||
(uint)(alphaHandle & 0xFFFFFFFFu), (uint)(alphaHandle >> 32));
|
||||
|
||||
// Phase U.3: bind the terrain clip UBO (binding=2). Shared ClipFrame UBO
|
||||
// when wired, else the no-clip fallback (count 0 = ungated terrain).
|
||||
BindClipUboBinding2();
|
||||
|
||||
_gl.BindVertexArray(_globalVao);
|
||||
_gl.MemoryBarrier(MemoryBarrierMask.CommandBarrierBit);
|
||||
_gl.MultiDrawElementsIndirect(
|
||||
|
|
@ -275,12 +294,42 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
|||
_gl.DeleteBuffer(_globalVbo);
|
||||
_gl.DeleteBuffer(_globalEbo);
|
||||
_gl.DeleteBuffer(_indirectBuffer);
|
||||
if (_fallbackClipUbo != 0) { _gl.DeleteBuffer(_fallbackClipUbo); _fallbackClipUbo = 0; } // Phase U.3
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: bind the terrain clip UBO to binding=2. Prefers the shared
|
||||
/// <see cref="ClipFrame"/> UBO (<see cref="SetClipUbo"/>); otherwise lazily
|
||||
/// creates + binds a no-clip fallback (count 0 = ungated) so the shader never
|
||||
/// reads an unbound UBO. The fallback is std140-sized to
|
||||
/// <see cref="ClipFrame.TerrainUboBytes"/> and zero-filled (count 0).
|
||||
/// </summary>
|
||||
private void BindClipUboBinding2()
|
||||
{
|
||||
if (_sharedClipUbo != 0)
|
||||
{
|
||||
_gl.BindBufferBase(BufferTargetARB.UniformBuffer,
|
||||
ClipFrame.TerrainClipUboBinding, _sharedClipUbo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fallbackClipUbo == 0)
|
||||
{
|
||||
_fallbackClipUbo = _gl.GenBuffer();
|
||||
var zero = stackalloc byte[ClipFrame.TerrainUboBytes];
|
||||
for (int i = 0; i < ClipFrame.TerrainUboBytes; i++) zero[i] = 0;
|
||||
_gl.BindBuffer(BufferTargetARB.UniformBuffer, _fallbackClipUbo);
|
||||
_gl.BufferData(BufferTargetARB.UniformBuffer,
|
||||
(nuint)ClipFrame.TerrainUboBytes, zero, BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
_gl.BindBufferBase(BufferTargetARB.UniformBuffer,
|
||||
ClipFrame.TerrainClipUboBinding, _fallbackClipUbo);
|
||||
}
|
||||
|
||||
private void AllocateGpuBuffers(int capacitySlots)
|
||||
{
|
||||
nuint vboBytes = (nuint)(capacitySlots * VertsPerLandblock * VertexSize);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,19 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
// packed transform array instead of the 80-byte CPU struct.
|
||||
private Matrix4x4[] _gpuInstanceTransforms = Array.Empty<Matrix4x4>();
|
||||
|
||||
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
|
||||
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
|
||||
// indexed by the same BaseInstance + gl_InstanceID the shader uses for
|
||||
// binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
|
||||
private uint _clipSlotBuffer;
|
||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||
|
||||
// Phase U.3: SHARED per-cell clip-region SSBO (binding=2) handed in via
|
||||
// SetClipRegionSsbo (the GameWindow-level ClipFrame buffer). When 0, we bind
|
||||
// our own one-slot no-clip fallback so the shader never reads an unbound SSBO.
|
||||
private uint _sharedClipRegionSsbo;
|
||||
private uint _fallbackClipRegionSsbo;
|
||||
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
||||
|
|
@ -204,10 +217,26 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernBatchCapacity * sizeof(ModernBatchData)), null, GLEnum.DynamicDraw);
|
||||
|
||||
// Phase U.3: per-instance clip-slot SSBO (binding=3), sized to the
|
||||
// instance capacity. Uploaded all-zeros each frame in RenderModernMDIInternal.
|
||||
_gl.GenBuffers(1, out _clipSlotBuffer);
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernInstanceCapacity * sizeof(uint)), null, GLEnum.DynamicDraw);
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, 0);
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: hand the renderer the SHARED per-cell clip-region SSBO
|
||||
/// (binding=2) created by <see cref="ClipFrame.UploadShared"/>. The renderer
|
||||
/// re-binds it to binding=2 immediately before its MDI. Pass 0 to fall back to
|
||||
/// the internal one-slot no-clip region buffer.
|
||||
/// </summary>
|
||||
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
||||
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetEnvCellGeomId
|
||||
// Verbatim copy of WB EnvCellRenderManager.cs:94-103.
|
||||
|
|
@ -947,6 +976,13 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernInstanceCapacity * sizeof(Matrix4x4)), null, GLEnum.DynamicDraw);
|
||||
|
||||
// Phase U.3: keep the clip-slot buffer (binding=3) sized to the
|
||||
// instance buffer so instanceClipSlot[BaseInstance + gl_InstanceID]
|
||||
// is always in range.
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(_modernInstanceCapacity * sizeof(uint)), null, GLEnum.DynamicDraw);
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:761-762: grow scratch arrays.
|
||||
|
|
@ -1011,6 +1047,23 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
(nuint)(totalDraws * sizeof(ModernBatchData)), ptr);
|
||||
}
|
||||
|
||||
// Phase U.3: upload the per-instance clip-slot buffer (binding=3), all
|
||||
// zeros ⇒ every instance maps to slot 0 ⇒ no-clip. Re-zero the reused head
|
||||
// each frame so stale U.4 slot indices can't leak. Sized to
|
||||
// uniqueInstanceCount; the buffer was already grown above with the
|
||||
// instance buffer when capacity increased.
|
||||
if (_clipSlotData.Length < uniqueInstanceCount)
|
||||
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
|
||||
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)(uniqueInstanceCount * sizeof(uint)), null, GLEnum.DynamicDraw);
|
||||
fixed (uint* ptr = _clipSlotData)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * sizeof(uint)), ptr);
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
||||
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
|
||||
if (globalVao == 0) return;
|
||||
|
|
@ -1022,6 +1075,10 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 0, _modernInstanceBuffer);
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 1, _modernBatchBuffer);
|
||||
// Phase U.3: per-instance clip slots (binding=3) + shared clip regions
|
||||
// (binding=2, via the GameWindow ClipFrame or our no-clip fallback).
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 3, _clipSlotBuffer);
|
||||
BindClipRegionBinding2();
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||
|
||||
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
|
||||
|
|
@ -1095,6 +1152,41 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BindClipRegionBinding2 (Phase U.3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Bind the per-cell clip-region SSBO to binding=2. Prefers the shared
|
||||
/// <see cref="ClipFrame"/> buffer (<see cref="SetClipRegionSsbo"/>); otherwise
|
||||
/// lazily creates + binds a one-slot no-clip fallback (count 0 = pass-all) so
|
||||
/// the shader never reads an unbound SSBO.
|
||||
/// </summary>
|
||||
private void BindClipRegionBinding2()
|
||||
{
|
||||
if (_sharedClipRegionSsbo != 0)
|
||||
{
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _sharedClipRegionSsbo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fallbackClipRegionSsbo == 0)
|
||||
{
|
||||
_gl.GenBuffers(1, out _fallbackClipRegionSsbo);
|
||||
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
|
||||
var zero = new byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _fallbackClipRegionSsbo);
|
||||
fixed (byte* p = zero)
|
||||
{
|
||||
_gl.BufferData(GLEnum.ShaderStorageBuffer,
|
||||
(nuint)AcDream.App.Rendering.ClipFrame.CellClipStrideBytes, p, GLEnum.DynamicDraw);
|
||||
}
|
||||
}
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// List pool (GetPooledList)
|
||||
// Copied from WB ObjectRenderManagerBase (pattern).
|
||||
|
|
@ -1194,5 +1286,7 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
|||
if (_mdiCommandBuffer != 0) { _gl.DeleteBuffer(_mdiCommandBuffer); _mdiCommandBuffer = 0; }
|
||||
if (_modernInstanceBuffer != 0){ _gl.DeleteBuffer(_modernInstanceBuffer); _modernInstanceBuffer = 0; }
|
||||
if (_modernBatchBuffer != 0) { _gl.DeleteBuffer(_modernBatchBuffer); _modernBatchBuffer = 0; }
|
||||
if (_clipSlotBuffer != 0) { _gl.DeleteBuffer(_clipSlotBuffer); _clipSlotBuffer = 0; } // Phase U.3
|
||||
if (_fallbackClipRegionSsbo != 0) { _gl.DeleteBuffer(_fallbackClipRegionSsbo); _fallbackClipRegionSsbo = 0; } // Phase U.3
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,21 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private uint _batchSsbo;
|
||||
private uint _indirectBuffer;
|
||||
|
||||
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
|
||||
// _instanceSsbo. One uint per instance selecting its CellClip slot. In U.3
|
||||
// this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is
|
||||
// identical to pre-U.3. U.4 populates real slot indices.
|
||||
private uint _clipSlotSsbo;
|
||||
private uint[] _clipSlotData = new uint[256];
|
||||
|
||||
// Phase U.3: the SHARED per-cell clip-region SSBO (binding=2), owned by the
|
||||
// GameWindow-level ClipFrame and handed to us via SetClipRegionSsbo. When 0
|
||||
// (not yet wired), we bind our OWN fallback no-clip region buffer below so the
|
||||
// shader never reads an unbound SSBO. The fallback holds exactly slot 0
|
||||
// (count 0 = pass-all), matching ClipFrame.NoClip's slot 0.
|
||||
private uint _sharedClipRegionSsbo;
|
||||
private uint _fallbackClipRegionSsbo;
|
||||
|
||||
// Per-frame scratch arrays — Tasks 9-10 fully wire these.
|
||||
private float[] _instanceData = new float[256 * 16]; // mat4 floats per instance
|
||||
private BatchData[] _batchData = new BatchData[256];
|
||||
|
|
@ -255,8 +270,19 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_instanceSsbo = _gl.GenBuffer();
|
||||
_batchSsbo = _gl.GenBuffer();
|
||||
_indirectBuffer = _gl.GenBuffer();
|
||||
_clipSlotSsbo = _gl.GenBuffer(); // Phase U.3 binding=3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: hand the dispatcher the SHARED per-cell clip-region SSBO
|
||||
/// (binding=2) that <see cref="ClipFrame.UploadShared"/> created. The
|
||||
/// dispatcher re-binds it to binding=2 immediately before each MDI so a
|
||||
/// consumer that touched binding=2 in between can't leave it pointing
|
||||
/// elsewhere. Pass 0 to fall back to the internal no-clip region buffer.
|
||||
/// </summary>
|
||||
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
||||
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
|
||||
|
||||
public static Matrix4x4 ComposePartWorldMatrix(
|
||||
Matrix4x4 entityWorld,
|
||||
Matrix4x4 animOverride,
|
||||
|
|
@ -975,13 +1001,25 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_transparentDrawCount,
|
||||
totalTriangles);
|
||||
|
||||
// ── Phase 5: upload three buffers ───────────────────────────────────
|
||||
// ── Phase 5: upload four buffers ────────────────────────────────────
|
||||
fixed (float* ip = _instanceData)
|
||||
UploadSsbo(_instanceSsbo, 0, ip, totalInstances * 16 * sizeof(float));
|
||||
|
||||
fixed (BatchData* bp = _batchData)
|
||||
UploadSsbo(_batchSsbo, 1, bp, totalDraws * sizeof(BatchData));
|
||||
|
||||
// Phase U.3: per-instance clip-slot buffer (binding=3), one uint per
|
||||
// instance, laid out parallel to _instanceData so the shader's
|
||||
// instanceClipSlot[instanceIndex] tracks the same instance as
|
||||
// Instances[instanceIndex]. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. Grow +
|
||||
// zero the scratch as needed (Array.Resize zero-fills the new tail; the
|
||||
// reused head is re-zeroed below so stale U.4 slot indices can't leak).
|
||||
if (_clipSlotData.Length < totalInstances)
|
||||
_clipSlotData = new uint[totalInstances + 256];
|
||||
Array.Clear(_clipSlotData, 0, totalInstances);
|
||||
fixed (uint* sp = _clipSlotData)
|
||||
UploadSsbo(_clipSlotSsbo, 3, sp, totalInstances * sizeof(uint));
|
||||
|
||||
fixed (DrawElementsIndirectCommand* cp = _indirectCommands)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
|
||||
|
|
@ -989,6 +1027,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
(nuint)(totalDraws * sizeof(DrawElementsIndirectCommand)), cp, BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
|
||||
// Phase U.3: bind the SHARED per-cell clip-region SSBO (binding=2). The
|
||||
// GameWindow-level ClipFrame already uploaded + bound it this frame; we
|
||||
// re-bind defensively in case another consumer touched binding=2 since.
|
||||
// When no shared id is set (0), bind our own no-clip fallback so the
|
||||
// shader never reads an unbound SSBO at binding=2.
|
||||
BindClipRegionBinding2();
|
||||
|
||||
// ── Phase 6: bind global VAO once ───────────────────────────────────
|
||||
_gl.BindVertexArray(anyVao);
|
||||
|
||||
|
|
@ -1228,6 +1273,36 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, binding, ssbo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: bind the per-cell clip-region SSBO to binding=2. Prefers the
|
||||
/// shared <see cref="ClipFrame"/> buffer (set via <see cref="SetClipRegionSsbo"/>);
|
||||
/// otherwise lazily creates + binds a one-slot no-clip fallback so the shader
|
||||
/// never reads an unbound SSBO. The fallback's single slot has count 0
|
||||
/// (pass-all), matching <see cref="ClipFrame.NoClip"/>'s slot 0.
|
||||
/// </summary>
|
||||
private unsafe void BindClipRegionBinding2()
|
||||
{
|
||||
if (_sharedClipRegionSsbo != 0)
|
||||
{
|
||||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer,
|
||||
ClipFrame.MeshClipSsboBinding, _sharedClipRegionSsbo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fallbackClipRegionSsbo == 0)
|
||||
{
|
||||
_fallbackClipRegionSsbo = _gl.GenBuffer();
|
||||
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
|
||||
var zero = stackalloc byte[ClipFrame.CellClipStrideBytes];
|
||||
for (int i = 0; i < ClipFrame.CellClipStrideBytes; i++) zero[i] = 0;
|
||||
_gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _fallbackClipRegionSsbo);
|
||||
_gl.BufferData(BufferTargetARB.ShaderStorageBuffer,
|
||||
(nuint)ClipFrame.CellClipStrideBytes, zero, BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer,
|
||||
ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo);
|
||||
}
|
||||
|
||||
private void MaybeFlushDiag()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
|
|
@ -1517,6 +1592,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_gl.DeleteBuffer(_instanceSsbo);
|
||||
_gl.DeleteBuffer(_batchSsbo);
|
||||
_gl.DeleteBuffer(_indirectBuffer);
|
||||
if (_clipSlotSsbo != 0) _gl.DeleteBuffer(_clipSlotSsbo); // Phase U.3
|
||||
if (_fallbackClipRegionSsbo != 0) _gl.DeleteBuffer(_fallbackClipRegionSsbo); // Phase U.3
|
||||
if (_gpuQueriesInitialized)
|
||||
{
|
||||
for (int i = 0; i < GpuQueryRingDepth; i++)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue