feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b70b9832ff
commit
8a7a0837e1
121 changed files with 1243 additions and 19840 deletions
|
|
@ -1,132 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.OpenGL.Extensions.ARB;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Thin wrapper around <see cref="ArbBindlessTexture"/> + capability detection
|
||||
/// for the modern rendering path. Constructed once at startup via
|
||||
/// <see cref="TryCreate"/>, which returns false if the extension isn't present.
|
||||
/// </summary>
|
||||
public sealed class BindlessSupport
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly ArbBindlessTexture _ext;
|
||||
|
||||
private BindlessSupport(GL gl, ArbBindlessTexture extension)
|
||||
{
|
||||
_gl = gl;
|
||||
_ext = extension;
|
||||
}
|
||||
|
||||
public static bool TryCreate(GL gl, out BindlessSupport? support)
|
||||
{
|
||||
if (gl.TryGetExtension<ArbBindlessTexture>(out var ext))
|
||||
{
|
||||
support = new BindlessSupport(gl, ext);
|
||||
return true;
|
||||
}
|
||||
support = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Get a 64-bit bindless handle for the texture and make it resident.
|
||||
/// Idempotent: handle is the same for a given texture name.</summary>
|
||||
public ulong GetResidentHandle(uint textureName)
|
||||
{
|
||||
ulong h = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"get bindless handle for texture {textureName}",
|
||||
() => _ext.GetTextureHandle(textureName));
|
||||
if (h == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"OpenGL returned no bindless handle for texture {textureName}.");
|
||||
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {h} residency",
|
||||
() => _ext.IsTextureHandleResident(h));
|
||||
if (!resident)
|
||||
{
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {h} resident",
|
||||
() => _ext.MakeTextureHandleResident(h));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a 64-bit bindless handle combining a texture with an EXPLICIT
|
||||
/// sampler object (rather than the texture's own baked sampler state) and
|
||||
/// make it resident. Idempotent per (texture, sampler) pair.
|
||||
///
|
||||
/// Added for Campaign V slice V1's <c>GlGpuDevice.RegisterTexture</c>,
|
||||
/// which registers a (texture, sampler) pair per the RHI contract — "the
|
||||
/// same texture registered with two samplers occupies two slots." The
|
||||
/// texture-only <see cref="GetResidentHandle(uint)"/> above cannot express
|
||||
/// that; <c>ManagedGLTextureArray</c> already calls the equivalent
|
||||
/// <c>ArbBindlessTexture.GetTextureSamplerHandle</c> directly through
|
||||
/// <c>OpenGLGraphicsDevice.BindlessExtension</c>, so this simply exposes
|
||||
/// the same GL entry point through this class for the RHI's use.
|
||||
/// </summary>
|
||||
public ulong GetResidentHandle(uint textureName, uint samplerName)
|
||||
{
|
||||
ulong h = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"get bindless handle for texture {textureName} + sampler {samplerName}",
|
||||
() => _ext.GetTextureSamplerHandle(textureName, samplerName));
|
||||
if (h == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"OpenGL returned no bindless handle for texture {textureName} + sampler {samplerName}.");
|
||||
}
|
||||
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {h} residency",
|
||||
() => _ext.IsTextureHandleResident(h));
|
||||
if (!resident)
|
||||
{
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {h} resident",
|
||||
() => _ext.MakeTextureHandleResident(h));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>Release residency for a handle. Call before deleting the underlying texture.</summary>
|
||||
public void MakeNonResident(ulong handle)
|
||||
{
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {handle} residency before release",
|
||||
() => _ext.IsTextureHandleResident(handle));
|
||||
if (!resident)
|
||||
return;
|
||||
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {handle} non-resident",
|
||||
() => _ext.MakeTextureHandleNonResident(handle));
|
||||
}
|
||||
|
||||
// Phase N.5b note: a `SetSamplerHandleUniform` wrapper was added in T6
|
||||
// and removed when terrain rendering surfaced GL_INVALID_OPERATION on
|
||||
// NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB
|
||||
// combination. The replacement pattern (uvec2 handle uniform + GLSL
|
||||
// sampler-from-handle constructor — see terrain_modern.frag) lives at the
|
||||
// call site via plain `_gl.ProgramUniform2(program, loc, low, high)`. If
|
||||
// you re-introduce a sampler-handle helper, restrict it to drivers known
|
||||
// to accept the direct sampler-uniform path.
|
||||
|
||||
/// <summary>Detect <c>GL_ARB_shader_draw_parameters</c> in addition to bindless.
|
||||
/// N.5's vertex shader uses <c>gl_BaseInstanceARB</c> and <c>gl_DrawIDARB</c>
|
||||
/// from this extension.</summary>
|
||||
public bool HasShaderDrawParameters(GL gl)
|
||||
{
|
||||
return gl.IsExtensionPresent("GL_ARB_shader_draw_parameters");
|
||||
}
|
||||
}
|
||||
|
|
@ -25,20 +25,15 @@ using System.Runtime.CompilerServices;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DatReaderWriter.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
public sealed unsafe partial class EnvCellRenderer :
|
||||
public sealed partial class EnvCellRenderer :
|
||||
IDisposable,
|
||||
IEnvCellLandblockPublisher
|
||||
{
|
||||
private readonly object _publicationOwner = new();
|
||||
|
||||
// Campaign V slice V6j: null on the RHI arm. Every GL statement below is
|
||||
// reached only when this is non-null; the encoder arm lives in
|
||||
// EnvCellRenderer.Rhi.cs and the GL arm is unchanged.
|
||||
private readonly GL? _gl;
|
||||
private readonly ObjectMeshManager _meshManager;
|
||||
private readonly WbFrustum _frustum;
|
||||
|
||||
|
|
@ -52,14 +47,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
private readonly object _renderLock = new();
|
||||
private EnvCellVisibilitySnapshot _activeSnapshot = new();
|
||||
|
||||
// Shader (set by caller via Initialize).
|
||||
// Uses acdream's legacy Shader type (not WB's GLSLShader) to match the
|
||||
// existing wire-in pattern in GameWindow.cs where _meshShader is loaded
|
||||
// for mesh_modern.{vert,frag} and shared across multiple consumers.
|
||||
// API mapping: Bind() -> Use(), SetUniform(s, int) -> SetInt(s, int),
|
||||
// SetUniform(s, Vector4) -> SetVec4(s, Vector4).
|
||||
private AcDream.App.Rendering.Shader? _shader;
|
||||
|
||||
// Phase U.4 root-cause fix: the view-projection captured in PrepareRenderBatches,
|
||||
// re-uploaded by Render() so the cell-shell pass is self-contained and does NOT
|
||||
// inherit WbDrawDispatcher's uViewProjection (which the opaque pass would read one
|
||||
|
|
@ -84,35 +71,22 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
|
||||
// Modern-MDI scratch buffers (single slot — we re-upload every frame).
|
||||
// WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers
|
||||
// We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws.
|
||||
private uint _mdiCommandBuffer;
|
||||
private int _mdiCommandCapacity;
|
||||
private uint _modernInstanceBuffer;
|
||||
private int _modernInstanceCapacity;
|
||||
private uint _modernBatchBuffer;
|
||||
private int _modernBatchCapacity;
|
||||
// mesh_modern.vert's SSBO InstanceData is only mat4 transform. The CPU
|
||||
// InstanceData below also carries CellId/Flags for filtering, so upload a
|
||||
// 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 int _clipSlotCapacity;
|
||||
// Phase U.3: per-instance clip-slot data, parallel to _gpuInstanceTransforms.
|
||||
// One uint per instance selecting its CellClip slot, indexed by the same
|
||||
// BaseInstance + gl_InstanceID the shader uses for binding=0. ALL ZEROS ⇒
|
||||
// slot 0 ⇒ no-clip.
|
||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||
|
||||
// A7 Fix D (D-2): this renderer owns its lighting (self-contained GL state,
|
||||
// like uViewProjection) instead of reading the SSBO 4/5 WbDrawDispatcher last
|
||||
// left bound. binding=4 = global point-light snapshot (same data/indices as the
|
||||
// dispatcher, via GlobalLightPacker); binding=5 = 8 int indices per instance.
|
||||
private uint _globalLightsSsbo; // binding=4
|
||||
private int _globalLightsCapacity;
|
||||
// A7 Fix D (D-2): this renderer owns its lighting (self-contained state,
|
||||
// like uViewProjection) instead of reading whatever WbDrawDispatcher last
|
||||
// bound. Global point-light snapshot (same data/indices as the dispatcher,
|
||||
// via GlobalLightPacker) plus 8 int indices per instance.
|
||||
private float[] _globalLightData = new float[AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * 16];
|
||||
private uint _instLightSetSsbo; // binding=5
|
||||
private int _instLightSetCapacity;
|
||||
private int[] _lightSetData = new int[1024 * AcDream.Core.Lighting.LightManager.MaxLightsPerObject];
|
||||
private System.Collections.Generic.IReadOnlyList<AcDream.Core.Lighting.LightSource>? _pointSnapshot;
|
||||
private sealed class CachedCellLightSet
|
||||
|
|
@ -125,51 +99,17 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
private readonly List<uint> _cellLightRemovalScratch = new();
|
||||
private int _lightFrameGeneration;
|
||||
|
||||
private sealed class DynamicBufferSet
|
||||
{
|
||||
public uint MdiCommandBuffer;
|
||||
public uint ModernInstanceBuffer;
|
||||
public uint ModernBatchBuffer;
|
||||
public uint ClipSlotBuffer;
|
||||
public uint GlobalLightsSsbo;
|
||||
public uint InstanceLightSetSsbo;
|
||||
public int MdiCommandCapacity;
|
||||
public int ModernInstanceCapacity;
|
||||
public int ModernBatchCapacity;
|
||||
public int ClipSlotCapacity;
|
||||
public int GlobalLightsCapacity;
|
||||
public int InstanceLightSetCapacity;
|
||||
}
|
||||
|
||||
private readonly List<DynamicBufferSet>[] _dynamicBufferSetsByFrame =
|
||||
[[], [], []];
|
||||
// Per-GPU-fenced-frame-slot draw bookkeeping.
|
||||
private int _dynamicFrameSlot;
|
||||
private int _dynamicBufferSetCursor;
|
||||
private bool _dynamicFrameStarted;
|
||||
private DynamicBufferSet? _activeDynamicBufferSet;
|
||||
|
||||
internal int DynamicBufferSetCount =>
|
||||
_dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count);
|
||||
|
||||
// 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;
|
||||
|
||||
// Campaign V slice V4t (2026-07-28): the interim per-renderer
|
||||
// GlBindlessHandleTable is retired. ObjectRenderBatch already carries the
|
||||
// device's own GpuTextureSlot, so this renderer shares WbDrawDispatcher's
|
||||
// table — the device's — and only has to flush and bind it before its own
|
||||
// raw-GL draws. That also removes the V2 caveat that two renderers could
|
||||
// legitimately number the same texture differently: there is now one
|
||||
// numbering, and it is the one the mesh manager assigned at upload.
|
||||
private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
(_meshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"EnvCellRenderer was constructed without a mesh manager: its texture " +
|
||||
"slots come from that manager's GL device table (Campaign V slice V4t)."))
|
||||
.WorldTextureTable;
|
||||
/// <summary>
|
||||
/// The dynamic per-frame-slot SSBO pool this used to report was raw-GL-only
|
||||
/// bookkeeping, deleted with that arm at Campaign V slice V11. The RHI arm
|
||||
/// allocates its storage from the GPU frame's own upload ring instead, so
|
||||
/// there is no separate pool to count.
|
||||
/// </summary>
|
||||
internal int DynamicBufferSetCount => 0;
|
||||
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
|
|
@ -197,11 +137,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
private readonly Dictionary<ulong, List<InstanceData>> _activeSnapshotGlobalGroups = new();
|
||||
private readonly List<ulong> _activeSnapshotGlobalGfxObjIds = new();
|
||||
|
||||
// Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
|
||||
// Shared across all manager instances on the same GL context.
|
||||
private static uint _currentVao;
|
||||
private static CullMode? _currentCullMode;
|
||||
|
||||
public bool NeedsPrepare { get; private set; } = true;
|
||||
|
||||
// --- Prepare gate (2026-07-24) -------------------------------------------
|
||||
|
|
@ -302,31 +237,19 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor + Initialize
|
||||
// Constructor
|
||||
// Campaign V slice V11: the raw-GL constructor + Initialize(Shader) two-step
|
||||
// are deleted. EnvCellRenderer.Rhi.cs's constructor is now the class's sole
|
||||
// constructor — it builds the three shell pipelines itself, so there is no
|
||||
// second initialization step.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public EnvCellRenderer(GL gl, ObjectMeshManager meshManager, WbFrustum frustum)
|
||||
{
|
||||
_gl = gl;
|
||||
_meshManager = meshManager;
|
||||
_frustum = frustum;
|
||||
}
|
||||
|
||||
public void Initialize(AcDream.App.Rendering.Shader shader)
|
||||
{
|
||||
_shader = shader;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <summary>Resets the per-frame submission cursor for the GPU-fenced slot.</summary>
|
||||
public void BeginFrame(int frameSlot)
|
||||
{
|
||||
if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(frameSlot));
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
|
||||
_dynamicFrameSlot = frameSlot;
|
||||
_dynamicBufferSetCursor = 0;
|
||||
_dynamicFrameStarted = true;
|
||||
_activeDynamicBufferSet = null;
|
||||
if (++_lightFrameGeneration == 0)
|
||||
{
|
||||
_cellLightSetCache.Clear();
|
||||
|
|
@ -334,15 +257,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
// Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
|
||||
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
|
||||
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
|
||||
|
|
@ -975,18 +889,14 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
HashSet<uint>? filter,
|
||||
IReadOnlyList<uint>? orderedCellIds)
|
||||
{
|
||||
// WB EnvCellRenderManager.cs:400:
|
||||
// WB EnvCellRenderManager.cs:400: the RHI arm's three pipelines are built
|
||||
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
||||
// answers whether this renderer is ready to draw.
|
||||
if (!_initialized) return;
|
||||
// Campaign V slice V6j: the RHI arm has no linked program to check — the
|
||||
// pipeline it draws with was built at construction and the same readiness
|
||||
// question is answered by _initialized alone.
|
||||
if (_gl is not null && (_shader is null || _shader.Program == 0)) return;
|
||||
|
||||
lock (_renderLock)
|
||||
{
|
||||
var snapshot = _activeSnapshot;
|
||||
// WB EnvCellRenderManager.cs:403-404:
|
||||
_shader?.Use();
|
||||
// FIX 2026-05-28 (pool aliasing root cause): mirror WB
|
||||
// EnvCellRenderManager.cs:405 — restore the pool cursor to the
|
||||
// high-water mark Prepare's merge phase reached, so any
|
||||
|
|
@ -997,43 +907,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
// mid-Render. See docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
|
||||
_poolIndex = snapshot.PostPreparePoolIndex;
|
||||
|
||||
// FIX 2026-05-28: invalidate static GL-state caches at start of Render.
|
||||
// Mirrors WB EnvCellRenderManager.cs:404-410:
|
||||
// CurrentVAO = 0; CurrentIBO = 0; CurrentAtlas = 0;
|
||||
// CurrentInstanceBuffer = 0; CurrentCullMode = null;
|
||||
//
|
||||
// These caches let SetCullMode / BindVertexArray skip redundant GL
|
||||
// calls when the state is already correct. BUT: between two Render()
|
||||
// invocations, OTHER consumers (WbDrawDispatcher, terrain, the
|
||||
// RenderInsideOutAcdream stencil pipeline) change the actual GL
|
||||
// state without updating these caches. The cache then lies, and
|
||||
// the per-batch SetCullMode in RenderModernMDIInternal skips its
|
||||
// glCullFace call — leaving stale cull state from the prior
|
||||
// consumer. For a cottage with mixed CullMode batches, half the
|
||||
// walls end up culled and the user sees "missing walls".
|
||||
//
|
||||
// Forcing the cache to null/0 at entry guarantees each Render call
|
||||
// re-establishes the GL state it expects.
|
||||
_currentVao = 0;
|
||||
_currentCullMode = null;
|
||||
|
||||
// WB EnvCellRenderManager.cs:406-409: uniform state setup.
|
||||
_shader?.SetInt("uRenderPass", (int)renderPass);
|
||||
_shader?.SetInt("uFilterByCell", 0);
|
||||
_shader?.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun)
|
||||
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
|
||||
_shader?.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
|
||||
|
||||
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
|
||||
// moving"): upload uViewProjection HERE rather than inheriting it from
|
||||
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
|
||||
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
|
||||
// this the opaque shells used the PREVIOUS frame's matrix — a stale
|
||||
// gl_Position against this frame's clip planes → pose-dependent clipping,
|
||||
// worst while moving. Same self-contained-GL-state precedent as the
|
||||
// 2026-05-28 cull-state cache fix above.
|
||||
_shader?.SetMatrix4("uViewProjection", _lastViewProjection);
|
||||
|
||||
List<InstanceData> allInstances = _renderInstances;
|
||||
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls =
|
||||
_renderDrawCalls;
|
||||
|
|
@ -1146,7 +1019,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
if (_drawCallRanges.Count == 0 && drawCalls.Count > 0)
|
||||
_drawCallRanges.Add(new DrawCallRange(0, drawCalls.Count));
|
||||
RenderModernMDIInternal(
|
||||
_shader,
|
||||
drawCalls,
|
||||
allInstances,
|
||||
_drawCallRanges,
|
||||
|
|
@ -1155,20 +1027,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
|
||||
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
|
||||
|
||||
// WB EnvCellRenderManager.cs:506-509: cleanup.
|
||||
_shader?.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0));
|
||||
_shader?.SetInt("uRenderPass", (int)renderPass);
|
||||
_gl?.BindVertexArray(0);
|
||||
_currentVao = 0;
|
||||
|
||||
// No cull restore at exit, matching WB's manager pattern: the
|
||||
// last SetCullMode call reflects actual GL state, and the next
|
||||
// Render call invalidates `_currentCullMode` before issuing its
|
||||
// own per-batch state. The Landblock->None override below can
|
||||
// intentionally leave cull disabled for the following IndoorPass,
|
||||
// preserving the shipped Gate #5 baseline while deeper evidence is
|
||||
// gathered.
|
||||
|
||||
// Update frame stats for probe emission at the call site.
|
||||
_lastFrameStats.CellsRendered = orderedCellIds?.Count
|
||||
?? filter?.Count
|
||||
|
|
@ -1285,58 +1143,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
// issues glMultiDrawElementsIndirect.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private void ActivateNextDynamicBufferSet()
|
||||
{
|
||||
if (!_dynamicFrameStarted)
|
||||
throw new InvalidOperationException("BeginFrame must be called before drawing EnvCells.");
|
||||
|
||||
List<DynamicBufferSet> slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot];
|
||||
if (_dynamicBufferSetCursor == slotSets.Count)
|
||||
slotSets.Add(CreateDynamicBufferSet());
|
||||
|
||||
DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++];
|
||||
_activeDynamicBufferSet = set;
|
||||
_mdiCommandBuffer = set.MdiCommandBuffer;
|
||||
_modernInstanceBuffer = set.ModernInstanceBuffer;
|
||||
_modernBatchBuffer = set.ModernBatchBuffer;
|
||||
_clipSlotBuffer = set.ClipSlotBuffer;
|
||||
_globalLightsSsbo = set.GlobalLightsSsbo;
|
||||
_instLightSetSsbo = set.InstanceLightSetSsbo;
|
||||
_mdiCommandCapacity = set.MdiCommandCapacity;
|
||||
_modernInstanceCapacity = set.ModernInstanceCapacity;
|
||||
_modernBatchCapacity = set.ModernBatchCapacity;
|
||||
_clipSlotCapacity = set.ClipSlotCapacity;
|
||||
_globalLightsCapacity = set.GlobalLightsCapacity;
|
||||
_instLightSetCapacity = set.InstanceLightSetCapacity;
|
||||
}
|
||||
|
||||
private DynamicBufferSet CreateDynamicBufferSet()
|
||||
{
|
||||
var set = new DynamicBufferSet();
|
||||
try
|
||||
{
|
||||
set.MdiCommandBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell MDI buffer");
|
||||
set.ModernInstanceBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell instance SSBO");
|
||||
set.ModernBatchBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell batch SSBO");
|
||||
set.ClipSlotBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell clip-slot SSBO");
|
||||
set.GlobalLightsSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell global-light SSBO");
|
||||
set.InstanceLightSetSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell light-set SSBO");
|
||||
return set;
|
||||
}
|
||||
catch (Exception creationFailure)
|
||||
{
|
||||
try { DeleteDynamicBufferSet(set); }
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"EnvCell dynamic-buffer creation and rollback failed.",
|
||||
creationFailure,
|
||||
cleanupFailure);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildUnfilteredGroups(EnvCellVisibilitySnapshot snapshot)
|
||||
{
|
||||
foreach (List<InstanceData> instances in _activeSnapshotGlobalGroups.Values)
|
||||
|
|
@ -1359,59 +1165,7 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
private void DeleteDynamicBufferSet(DynamicBufferSet set)
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
void Attempt(uint buffer, long bytes, string name)
|
||||
{
|
||||
try { TrackedGlResource.DeleteBuffer(_gl!, buffer, bytes, $"deleting {name}"); }
|
||||
catch (Exception ex) { (failures ??= []).Add(ex); }
|
||||
}
|
||||
|
||||
Attempt(
|
||||
set.MdiCommandBuffer,
|
||||
(long)set.MdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
"EnvCell MDI buffer");
|
||||
Attempt(
|
||||
set.ModernInstanceBuffer,
|
||||
(long)set.ModernInstanceCapacity * sizeof(Matrix4x4),
|
||||
"EnvCell instance SSBO");
|
||||
Attempt(
|
||||
set.ModernBatchBuffer,
|
||||
(long)set.ModernBatchCapacity * sizeof(ModernBatchData),
|
||||
"EnvCell batch SSBO");
|
||||
Attempt(set.ClipSlotBuffer, (long)set.ClipSlotCapacity * sizeof(uint), "EnvCell clip-slot SSBO");
|
||||
Attempt(
|
||||
set.GlobalLightsSsbo,
|
||||
(long)set.GlobalLightsCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
"EnvCell global-light SSBO");
|
||||
Attempt(
|
||||
set.InstanceLightSetSsbo,
|
||||
(long)set.InstanceLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
"EnvCell light-set SSBO");
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException("One or more EnvCell dynamic buffers failed to delete.", failures);
|
||||
}
|
||||
|
||||
private void PersistActiveDynamicBufferCapacities()
|
||||
{
|
||||
DynamicBufferSet set = _activeDynamicBufferSet
|
||||
?? throw new InvalidOperationException("No dynamic EnvCell buffer set is active.");
|
||||
set.MdiCommandCapacity = _mdiCommandCapacity;
|
||||
set.ModernInstanceCapacity = _modernInstanceCapacity;
|
||||
set.ModernBatchCapacity = _modernBatchCapacity;
|
||||
set.ClipSlotCapacity = _clipSlotCapacity;
|
||||
set.GlobalLightsCapacity = _globalLightsCapacity;
|
||||
set.InstanceLightSetCapacity = _instLightSetCapacity;
|
||||
}
|
||||
|
||||
private void RenderModernMDIInternal(
|
||||
AcDream.App.Rendering.Shader? shader,
|
||||
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
|
||||
List<InstanceData> allInstances,
|
||||
IReadOnlyList<DrawCallRange> drawCallRanges,
|
||||
|
|
@ -1423,26 +1177,11 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
int passIdx = (int)renderPass;
|
||||
if (passIdx < 0 || passIdx > 2) return;
|
||||
|
||||
// §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
|
||||
// Without the global VAO nothing can draw, and returning AFTER the pass state
|
||||
// was established leaked it (same early-out shape as the totalDraws==0 leak —
|
||||
// see the comment on the state-establish block below).
|
||||
// Campaign V slice V6j: the RHI arm has no vertex array — the pipeline
|
||||
// owns one shaped by GpuVertexLayout.WorldMesh — so its readiness test is
|
||||
// the backend-neutral HasStores the arena publishes (V6i-3).
|
||||
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
|
||||
if (_gl is not null)
|
||||
{
|
||||
if (globalVao == 0) return;
|
||||
}
|
||||
else if (_meshManager.GlobalBuffer is not { HasStores: true })
|
||||
{
|
||||
// the backend-neutral HasStores flag the mesh arena publishes (V6i-3).
|
||||
if (_meshManager.GlobalBuffer is not { HasStores: true })
|
||||
return;
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:715-716:
|
||||
shader?.Use();
|
||||
shader?.SetInt("uFilterByCell", 0);
|
||||
|
||||
// WB BaseObjectRenderManager.cs:718-740: count the pass-filtered batches.
|
||||
// A normal render has one range. The ordered transparent-shell path has
|
||||
|
|
@ -1479,120 +1218,16 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
// WB BaseObjectRenderManager.cs:743:
|
||||
if (totalDraws == 0) return;
|
||||
int uniqueInstanceCount = allInstances.Count;
|
||||
|
||||
// Campaign V slice V6j: the encoder arm owns no buffer pool and no
|
||||
// imperative state bracket. Every per-frame section is a ring slice, so
|
||||
// there is nothing to activate or grow, and blend plus depth-write are
|
||||
// baked into the three shell pipelines rather than set here.
|
||||
if (_gl is not null)
|
||||
{
|
||||
ActivateNextDynamicBufferSet();
|
||||
|
||||
// Phase U.4 ROOT-CAUSE FIX (cell-shell "transparent walls / only bluish
|
||||
// background, flickering when moving"): establish this pass's BLEND + DepthMask
|
||||
// state OURSELVES rather than inheriting it. Mirror the working WbDrawDispatcher
|
||||
// passes (Disable(Blend)+DepthMask(true) opaque; Enable(Blend)+DepthMask(false)
|
||||
// transparent). Restored to opaque defaults at the end of the draw loop so a
|
||||
// Transparent pass can't leak into later draws.
|
||||
//
|
||||
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
|
||||
// totalDraws==0 early-out above. It used to run before the batch grouping, so a
|
||||
// Transparent pass over a cell whose batches are ALL opaque (a plain cottage
|
||||
// interior) set Blend-on/DepthMask-off and then returned at the count check
|
||||
// WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's
|
||||
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
|
||||
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
|
||||
// whole screen dropped to the fog-tinted clear color — onset-locked to the
|
||||
// building-flood merge (the first frame a flooded building shell draws), holding
|
||||
// until camera rotation dropped the cell from the flood. From here down every
|
||||
// path reaches the end-of-pass restore.
|
||||
if (renderPass == WbRenderPass.Transparent)
|
||||
{
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.DepthMask(true);
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:745-759: resize buffers if needed.
|
||||
if (totalDraws > _mdiCommandCapacity)
|
||||
{
|
||||
int grownMdiCapacity = Math.Max(_mdiCommandCapacity * 2, totalDraws);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.DrawIndirectBuffer,
|
||||
_mdiCommandBuffer,
|
||||
(long)_mdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
(long)grownMdiCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell MDI buffer to {grownMdiCapacity} commands");
|
||||
_mdiCommandCapacity = grownMdiCapacity;
|
||||
|
||||
int grownBatchCapacity = grownMdiCapacity;
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_modernBatchBuffer,
|
||||
(long)_modernBatchCapacity * sizeof(ModernBatchData),
|
||||
(long)grownBatchCapacity * sizeof(ModernBatchData),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell batch SSBO to {grownBatchCapacity} batches");
|
||||
_modernBatchCapacity = grownBatchCapacity;
|
||||
}
|
||||
|
||||
if (uniqueInstanceCount > _modernInstanceCapacity)
|
||||
{
|
||||
int grownInstanceCapacity = Math.Max(_modernInstanceCapacity * 2, uniqueInstanceCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_modernInstanceBuffer,
|
||||
(long)_modernInstanceCapacity * sizeof(Matrix4x4),
|
||||
(long)grownInstanceCapacity * sizeof(Matrix4x4),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell instance SSBO to {grownInstanceCapacity} instances");
|
||||
_modernInstanceCapacity = grownInstanceCapacity;
|
||||
}
|
||||
|
||||
// Phase U.3: keep the clip-slot buffer (binding=3) sized to the
|
||||
// instance prefix so instanceClipSlot[BaseInstance + gl_InstanceID]
|
||||
// is always in range. It owns an independent committed capacity so a
|
||||
// failed allocation can never publish the instance buffer's growth as
|
||||
// if both resources had succeeded.
|
||||
if (uniqueInstanceCount > _clipSlotCapacity)
|
||||
{
|
||||
int grownClipCapacity = Math.Max(_clipSlotCapacity * 2, uniqueInstanceCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_clipSlotBuffer,
|
||||
(long)_clipSlotCapacity * sizeof(uint),
|
||||
(long)grownClipCapacity * sizeof(uint),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell clip-slot SSBO to {grownClipCapacity} instances");
|
||||
_clipSlotCapacity = grownClipCapacity;
|
||||
}
|
||||
|
||||
if (uniqueInstanceCount > _instLightSetCapacity)
|
||||
{
|
||||
int grownLightSetCapacity = Math.Max(_instLightSetCapacity * 2, uniqueInstanceCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_instLightSetSsbo,
|
||||
(long)_instLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
(long)grownLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell light-set SSBO to {grownLightSetCapacity} instances");
|
||||
_instLightSetCapacity = grownLightSetCapacity;
|
||||
}
|
||||
}
|
||||
// imperative state bracket. Every per-frame section is a ring slice
|
||||
// allocated fresh in SubmitRhi, and blend plus depth-write are baked
|
||||
// into the three shell pipelines rather than set here. The frame-
|
||||
// started invariant this used to enforce via ActivateNextDynamicBufferSet
|
||||
// still matters (it gates the light-frame-generation cache), so it is
|
||||
// checked directly.
|
||||
if (!_dynamicFrameStarted)
|
||||
throw new InvalidOperationException("BeginFrame must be called before drawing EnvCells.");
|
||||
|
||||
// WB BaseObjectRenderManager.cs:761-762: grow scratch arrays.
|
||||
if (_commands.Length < totalDraws)
|
||||
|
|
@ -1684,189 +1319,7 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
if (_gl is null)
|
||||
{
|
||||
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:784-805 upload. Retain capacity and
|
||||
// update the active prefix so portal frames cannot enqueue an unbounded
|
||||
// chain of retired driver allocations.
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||
fixed (DrawElementsIndirectCommand* ptr = _commands)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.DrawIndirectBuffer, 0,
|
||||
(nuint)(totalDraws * sizeof(DrawElementsIndirectCommand)), ptr);
|
||||
}
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
||||
if (_gpuInstanceTransforms.Length < uniqueInstanceCount)
|
||||
Array.Resize(ref _gpuInstanceTransforms, Math.Max(_gpuInstanceTransforms.Length * 2, uniqueInstanceCount));
|
||||
for (int i = 0; i < uniqueInstanceCount; i++)
|
||||
_gpuInstanceTransforms[i] = allInstances[i].Transform;
|
||||
fixed (Matrix4x4* ptr = _gpuInstanceTransforms)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * sizeof(Matrix4x4)), ptr);
|
||||
}
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernBatchBuffer);
|
||||
fixed (ModernBatchData* ptr = _modernBatches)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(totalDraws * sizeof(ModernBatchData)), ptr);
|
||||
}
|
||||
|
||||
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
|
||||
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
|
||||
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
|
||||
// the map (shouldn't happen — the Render filter is the map's keys) and the
|
||||
// U.3 path both map to slot 0 (no-clip). allInstances is laid out in the
|
||||
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
|
||||
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
|
||||
if (_clipSlotData.Length < uniqueInstanceCount)
|
||||
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
|
||||
// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell
|
||||
// instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
|
||||
if (_cellIdToSlot is null
|
||||
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
|
||||
{
|
||||
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < uniqueInstanceCount; i++)
|
||||
_clipSlotData[i] = _cellIdToSlot.TryGetValue(allInstances[i].CellId, out int slot)
|
||||
? (uint)slot : 0u;
|
||||
}
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
||||
fixed (uint* ptr = _clipSlotData)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * sizeof(uint)), ptr);
|
||||
}
|
||||
|
||||
// A7 Fix D (D-2): per-instance 8-int light set, parallel to the transforms,
|
||||
// keyed on the cell each shell instance belongs to (mirrors _clipSlotData).
|
||||
int lightStride = AcDream.Core.Lighting.LightManager.MaxLightsPerObject;
|
||||
if (_lightSetData.Length < uniqueInstanceCount * lightStride)
|
||||
_lightSetData = new int[System.Math.Max(_lightSetData.Length * 2, uniqueInstanceCount * lightStride)];
|
||||
for (int i = 0; i < uniqueInstanceCount; i++)
|
||||
{
|
||||
int[] cellSet = GetCellLightSet(allInstances[i].CellId);
|
||||
System.Array.Copy(cellSet, 0, _lightSetData, i * lightStride, lightStride);
|
||||
}
|
||||
|
||||
// #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light
|
||||
// sets read through the just-cleared cache against THIS frame's
|
||||
// _pointSnapshot — the exact data the SSBO upload below carries.
|
||||
if (renderPass == WbRenderPass.Opaque
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
|
||||
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
|
||||
|
||||
// A7 Fix D (D-2): upload binding=4 (global lights) + binding=5 (per-instance set).
|
||||
int lightCount = AcDream.Core.Lighting.GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
|
||||
int glUploadCount = lightCount > 0 ? lightCount : 1;
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _globalLightsSsbo);
|
||||
if (glUploadCount > _globalLightsCapacity)
|
||||
{
|
||||
int grownGlobalLightCapacity = Math.Max(_globalLightsCapacity * 2, glUploadCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_globalLightsSsbo,
|
||||
(long)_globalLightsCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
(long)grownGlobalLightCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell global-light SSBO to {grownGlobalLightCapacity} lights");
|
||||
_globalLightsCapacity = grownGlobalLightCapacity;
|
||||
}
|
||||
fixed (float* gp = _globalLightData)
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(glUploadCount * AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * sizeof(float)), gp);
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _instLightSetSsbo);
|
||||
fixed (int* lp = _lightSetData)
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * lightStride * sizeof(int)), lp);
|
||||
|
||||
PersistActiveDynamicBufferCapacities();
|
||||
|
||||
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
||||
// (globalVao validated at the top of the method — a return here would leak the
|
||||
// pass state established above.)
|
||||
if (_currentVao != globalVao)
|
||||
{
|
||||
_gl.BindVertexArray(globalVao);
|
||||
_currentVao = globalVao;
|
||||
}
|
||||
|
||||
_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.BindBufferBase(GLEnum.ShaderStorageBuffer, 4, _globalLightsSsbo); // A7 Fix D (D-2)
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 5, _instLightSetSsbo); // A7 Fix D (D-2)
|
||||
FlushAndBindTextureTable(); // Campaign V slice V2 (binding=9)
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||
|
||||
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
|
||||
|
||||
// WB BaseObjectRenderManager.cs:821-847: issue per-group multi-draw calls.
|
||||
// The ranges retain ordered-cell boundaries, so transparent geometry
|
||||
// stays far-to-near even though all command data was uploaded once.
|
||||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
int groupIndex = drawRange.GroupIndex;
|
||||
var cullMode = (CullMode)(groupIndex % 4);
|
||||
// Phase A8 visual-gate evidence: cell meshes use CullMode.Landblock
|
||||
// uniformly, but the room surfaces need to be visible from inside
|
||||
// under acdream's current global winding state. Render cell polys
|
||||
// double-sided while the architectural cause is isolated.
|
||||
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
|
||||
if (_currentCullMode != cullMode)
|
||||
{
|
||||
SetCullMode(cullMode);
|
||||
}
|
||||
|
||||
bool isAdditive = groupIndex >= 4;
|
||||
if (isAdditive)
|
||||
{
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
shader!.SetInt("uRenderPass", (int)renderPass | 0x100);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
shader!.SetInt("uRenderPass", (int)renderPass);
|
||||
}
|
||||
|
||||
shader!.SetInt("uDrawIDOffset", drawRange.FirstCommand);
|
||||
_gl.MultiDrawElementsIndirect(
|
||||
PrimitiveType.Triangles,
|
||||
DrawElementsType.UnsignedShort,
|
||||
(void*)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
||||
(uint)drawRange.CommandCount,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
|
||||
// Phase U.4: leave a clean opaque-default render state (mirrors WbDrawDispatcher's
|
||||
// post-transparent restore) so a Transparent pass's Blend-on / DepthMask-off does
|
||||
// not leak into particles or the next frame's draws.
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.DepthMask(true);
|
||||
|
||||
// WB BaseObjectRenderManager.cs:845-847:
|
||||
shader!.SetInt("uDrawIDOffset", 0);
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
||||
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
||||
}
|
||||
|
||||
internal static void AppendMdiDrawRange(
|
||||
|
|
@ -1994,116 +1447,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
System.Console.WriteLine($"[seam-blk] t={now} changed={(changed ? 1 : 0)}{sig}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SetCullMode
|
||||
// Verbatim copy of WB BaseObjectRenderManager.cs:850-866.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private void SetCullMode(CullMode mode)
|
||||
{
|
||||
_currentCullMode = mode;
|
||||
switch (mode)
|
||||
{
|
||||
case CullMode.None:
|
||||
_gl!.Disable(EnableCap.CullFace);
|
||||
break;
|
||||
case CullMode.Clockwise:
|
||||
_gl!.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Front);
|
||||
break;
|
||||
case CullMode.CounterClockwise:
|
||||
case CullMode.Landblock:
|
||||
_gl!.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Back);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FlushAndBindTextureTable (Campaign V slice V2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: drains the device texture table's dirty runs and
|
||||
/// (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// A genuinely new slot is rare — new dat surfaces/atlases, not every frame
|
||||
/// — but the bind is unconditional, because GL's storage-buffer binding
|
||||
/// points are global and another raw-GL renderer's binding 9 sits there
|
||||
/// between two of these draws.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
|
||||
device.FlushTextureTable();
|
||||
_gl!.BindBufferBase(
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
device.TextureTableGlName);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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)
|
||||
{
|
||||
uint fallback = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell fallback clip SSBO");
|
||||
bool allocated = false;
|
||||
try
|
||||
{
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl!,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
fallback,
|
||||
0,
|
||||
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
|
||||
GLEnum.DynamicDraw,
|
||||
"allocating EnvCell fallback clip SSBO");
|
||||
allocated = true;
|
||||
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
|
||||
Span<byte> zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
|
||||
zero.Clear();
|
||||
fixed (byte* p = zero)
|
||||
{
|
||||
_gl!.BufferSubData(
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
0,
|
||||
(nuint)zero.Length,
|
||||
p);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, "initializing EnvCell fallback clip SSBO");
|
||||
_fallbackClipRegionSsbo = fallback;
|
||||
}
|
||||
catch
|
||||
{
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl!,
|
||||
fallback,
|
||||
allocated ? AcDream.App.Rendering.ClipFrame.CellClipStrideBytes : 0,
|
||||
"rolling back EnvCell fallback clip SSBO");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
_gl!.BindBufferBase(GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// List pool (GetPooledList)
|
||||
// Copied from WB ObjectRenderManagerBase (pattern).
|
||||
|
|
@ -2154,67 +1497,12 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
{
|
||||
("prepare-scratch", _prepareScratch.Dispose),
|
||||
};
|
||||
// Campaign V slice V6j: the encoder arm owns no GL names — its
|
||||
// pipelines route their physical free through the device's
|
||||
// retirement queue — so the ledger below holds only the scratch.
|
||||
if (_gl is null)
|
||||
DisposeRhiResources();
|
||||
// Campaign V slice V11: the raw-GL arm's dynamic buffer-set pool is
|
||||
// deleted along with it. The RHI arm's pipelines route their
|
||||
// physical free through the device's own retirement queue, so the
|
||||
// ledger above holds only the scratch.
|
||||
DisposeRhiResources();
|
||||
|
||||
for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++)
|
||||
{
|
||||
List<DynamicBufferSet> frameSets = _dynamicBufferSetsByFrame[frame];
|
||||
for (int index = 0; index < frameSets.Count; index++)
|
||||
{
|
||||
DynamicBufferSet set = frameSets[index];
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.MdiCommandBuffer,
|
||||
(long)set.MdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
$"dynamic-{frame}-{index}-mdi",
|
||||
"deleting EnvCell MDI buffer");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.ModernInstanceBuffer,
|
||||
(long)set.ModernInstanceCapacity * sizeof(Matrix4x4),
|
||||
$"dynamic-{frame}-{index}-instances",
|
||||
"deleting EnvCell instance SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.ModernBatchBuffer,
|
||||
(long)set.ModernBatchCapacity * sizeof(ModernBatchData),
|
||||
$"dynamic-{frame}-{index}-batches",
|
||||
"deleting EnvCell batch SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.ClipSlotBuffer,
|
||||
(long)set.ClipSlotCapacity * sizeof(uint),
|
||||
$"dynamic-{frame}-{index}-clip-slots",
|
||||
"deleting EnvCell clip-slot SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.GlobalLightsSsbo,
|
||||
(long)set.GlobalLightsCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
$"dynamic-{frame}-{index}-global-lights",
|
||||
"deleting EnvCell global-light SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.InstanceLightSetSsbo,
|
||||
(long)set.InstanceLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
$"dynamic-{frame}-{index}-light-sets",
|
||||
"deleting EnvCell light-set SSBO");
|
||||
}
|
||||
}
|
||||
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
_fallbackClipRegionSsbo,
|
||||
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
|
||||
"fallback-clip-region",
|
||||
"deleting EnvCell fallback clip SSBO");
|
||||
_disposeResources = new RetryableResourceReleaseLedger(releases);
|
||||
}
|
||||
|
||||
|
|
@ -2225,17 +1513,7 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
"One or more EnvCell renderer resources could not be released.");
|
||||
}
|
||||
|
||||
foreach (List<DynamicBufferSet> frameSets in _dynamicBufferSetsByFrame)
|
||||
frameSets.Clear();
|
||||
_activeDynamicBufferSet = null;
|
||||
_dynamicFrameStarted = false;
|
||||
_mdiCommandBuffer = 0;
|
||||
_modernInstanceBuffer = 0;
|
||||
_modernBatchBuffer = 0;
|
||||
_clipSlotBuffer = 0;
|
||||
_globalLightsSsbo = 0;
|
||||
_instLightSetSsbo = 0;
|
||||
_fallbackClipRegionSsbo = 0;
|
||||
_disposeResources = null;
|
||||
IsDisposed = true;
|
||||
|
||||
|
|
@ -2250,22 +1528,4 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
_disposing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTrackedBufferRelease(
|
||||
List<(string Name, Action Release)> releases,
|
||||
uint buffer,
|
||||
long capacityBytes,
|
||||
string name,
|
||||
string context)
|
||||
{
|
||||
if (buffer == 0)
|
||||
return;
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl!,
|
||||
buffer,
|
||||
capacityBytes,
|
||||
context);
|
||||
releases.Add((name, release.Run));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.Core.Native;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public static class GLHelpers {
|
||||
public static OpenGLGraphicsDevice? Device { get; set; }
|
||||
public static ILogger? Logger { get; set; }
|
||||
|
||||
public static void Init(OpenGLGraphicsDevice device, ILogger logger) {
|
||||
Logger = logger;
|
||||
Device = device;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always-on error boundary for resource transactions. Most render-path
|
||||
/// checks remain Debug-only because <c>glGetError</c> is a synchronous
|
||||
/// driver call; allocation/upload code must not publish CPU state after
|
||||
/// OpenGL reported OOM, context loss, or a rejected transfer in Release.
|
||||
/// Call exactly once before committing each transaction.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ThrowOnResourceError(GL gl, string context) {
|
||||
GLEnum error = gl.GetError();
|
||||
if (error == GLEnum.NoError)
|
||||
return;
|
||||
|
||||
var errors = new System.Text.StringBuilder();
|
||||
do {
|
||||
if (errors.Length != 0)
|
||||
errors.Append(", ");
|
||||
errors.Append(error).Append(" (").Append(GetErrorDetails(error)).Append(')');
|
||||
error = gl.GetError();
|
||||
} while (error != GLEnum.NoError);
|
||||
|
||||
string message = $"OpenGL resource transaction failed: {errors}. Context: {context}";
|
||||
Logger?.LogError(message);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static bool _loggedVersion = false;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrors(GL gl, bool logErrors = false, [CallerMemberName] string callerName = "",
|
||||
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
|
||||
var error = gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
if (!_loggedVersion) {
|
||||
_loggedVersion = true;
|
||||
var version = gl.GetStringS(GLEnum.Version);
|
||||
var vendor = gl.GetStringS(GLEnum.Vendor);
|
||||
var renderer = gl.GetStringS(GLEnum.Renderer);
|
||||
Logger?.LogInformation($"GL Version: {version}, Vendor: {vendor}, Renderer: {renderer}");
|
||||
}
|
||||
string errorDetails = GetErrorDetails(error);
|
||||
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
|
||||
|
||||
var program = (uint)gl.GetInteger(GLEnum.CurrentProgram);
|
||||
var vao = gl.GetInteger(GLEnum.VertexArrayBinding);
|
||||
var activeTex = gl.GetInteger(GLEnum.ActiveTexture);
|
||||
var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
string extraInfo = "";
|
||||
if (program != 0) {
|
||||
bool isProgram = gl.IsProgram(program);
|
||||
gl.GetProgram(program, GLEnum.LinkStatus, out int linkStatus);
|
||||
gl.GetProgram(program, GLEnum.DeleteStatus, out int deleteStatus);
|
||||
gl.GetProgram(program, GLEnum.ValidateStatus, out int validateStatus);
|
||||
extraInfo = $", IsProg: {isProgram}, Link: {linkStatus}, Del: {deleteStatus}, Valid: {validateStatus}";
|
||||
}
|
||||
|
||||
string message = $"OpenGL Error: {error} ({errorDetails}) at {location}. Thread: {threadId}, Program: {program}{extraInfo}, VAO: {vao}, ActiveTex: {activeTex}";
|
||||
|
||||
Logger?.LogError(message);
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
#else
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrors(GL gl, bool logErrors = false, string callerName = "",
|
||||
string callerFile = "", int callerLine = 0) {
|
||||
}
|
||||
#endif
|
||||
|
||||
public static string GetErrorDetails(GLEnum error) {
|
||||
return error switch {
|
||||
GLEnum.InvalidEnum => "Invalid enum - An unacceptable value is specified for an enumerated argument",
|
||||
GLEnum.InvalidValue => "Invalid value - A numeric argument is out of range",
|
||||
GLEnum.InvalidOperation =>
|
||||
"Invalid operation - The specified operation is not allowed in the current state",
|
||||
GLEnum.StackOverflow => "Stack overflow - An operation would cause an internal stack to overflow",
|
||||
GLEnum.StackUnderflow => "Stack underflow - An operation would cause an internal stack to underflow",
|
||||
GLEnum.OutOfMemory => "Out of memory - There is not enough memory left to execute the command",
|
||||
GLEnum.InvalidFramebufferOperation =>
|
||||
"Invalid framebuffer operation - The framebuffer object is not complete",
|
||||
GLEnum.ContextLost => "Context lost - The OpenGL context has been lost due to a graphics card reset",
|
||||
_ => "Unknown error"
|
||||
};
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// Checks for OpenGL errors and provides context-specific information
|
||||
/// </summary>
|
||||
public static void CheckErrorsWithContext(GL gl, string context, [CallerMemberName] string callerName = "",
|
||||
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
|
||||
var error = gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
string errorDetails = GetErrorDetails(error);
|
||||
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
|
||||
string message = $"OpenGL Error: {error} ({errorDetails})\nContext: {context}\nLocation: {location}";
|
||||
|
||||
Logger?.LogError(message);
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
#else
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrorsWithContext(GL gl, string context, string callerName = "",
|
||||
string callerFile = "", int callerLine = 0) {
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed information about the current texture state for debugging
|
||||
/// </summary>
|
||||
public static string GetTextureDebugInfo(GL gl, GLEnum target) {
|
||||
var info = new System.Text.StringBuilder();
|
||||
info.AppendLine($"Texture Debug Info for {target}:");
|
||||
|
||||
try {
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureWidth, out int width);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureHeight, out int height);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureDepthExt, out int depth);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureInternalFormat, out int format);
|
||||
|
||||
info.AppendLine($" Dimensions: {width}x{height}x{depth}");
|
||||
info.AppendLine($" Internal Format: {(InternalFormat)format}");
|
||||
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMinFilter, out int minFilter);
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMagFilter, out int magFilter);
|
||||
info.AppendLine($" Min Filter: {(TextureMinFilter)minFilter}");
|
||||
info.AppendLine($" Mag Filter: {(TextureMagFilter)magFilter}");
|
||||
|
||||
// Get max mipmap level
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMaxLevelSgis, out int maxLevel);
|
||||
info.AppendLine($" Max Level: {maxLevel}");
|
||||
|
||||
// Check completeness
|
||||
int maxMipLevel = (int)Math.Floor(Math.Log2(Math.Max(width, height)));
|
||||
info.AppendLine($" Calculated Max Mip Level: {maxMipLevel}");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
info.AppendLine($" Error getting texture info: {ex.Message}");
|
||||
}
|
||||
|
||||
return info.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs current OpenGL state for debugging
|
||||
/// </summary>
|
||||
public static void LogGLState(GL gl, string context = "") {
|
||||
var state = new System.Text.StringBuilder();
|
||||
state.AppendLine($"=== OpenGL State ({context}) ===");
|
||||
|
||||
try {
|
||||
state.AppendLine(
|
||||
$"Active Texture Unit: GL_TEXTURE{gl.GetInteger(GetPName.ActiveTexture) - (int)GLEnum.Texture0}");
|
||||
state.AppendLine($"Bound 2D Array Texture: {gl.GetInteger(GetPName.TextureBinding2DArray)}");
|
||||
state.AppendLine($"Current Program: {gl.GetInteger(GetPName.CurrentProgram)}");
|
||||
|
||||
gl.GetInteger(GetPName.MaxTextureSize, out int maxTexSize);
|
||||
state.AppendLine($"Max Texture Size: {maxTexSize}");
|
||||
|
||||
gl.GetInteger(GetPName.Max3DTextureSize, out int max3DSize);
|
||||
state.AppendLine($"Max 3D Texture Size: {max3DSize}");
|
||||
|
||||
gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maxLayers);
|
||||
state.AppendLine($"Max Array Texture Layers: {maxLayers}");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
state.AppendLine($"Error getting GL state: {ex.Message}");
|
||||
}
|
||||
|
||||
state.AppendLine("======================");
|
||||
Logger?.LogInformation(state.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicit defaults to prevent Avalonia state leakage into our custom rendering pipeline.
|
||||
/// Call this at the start of complex render cycles immediately inside a GLStateScope.
|
||||
/// </summary>
|
||||
public static void SetupDefaultRenderState(GL gl) {
|
||||
gl.BindSampler(0, 0);
|
||||
gl.BindSampler(1, 0);
|
||||
gl.BindSampler(2, 0);
|
||||
|
||||
gl.ActiveTexture(TextureUnit.Texture1);
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
gl.ActiveTexture(TextureUnit.Texture2);
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
gl.ActiveTexture(TextureUnit.Texture0); // End on Texture0
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
|
||||
gl.BindVertexArray(0);
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
|
||||
gl.UseProgram(0);
|
||||
|
||||
gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
|
||||
|
||||
gl.Disable(EnableCap.StencilTest);
|
||||
gl.BlendColor(0, 0, 0, 0);
|
||||
gl.PolygonMode(GLEnum.FrontAndBack, PolygonMode.Fill);
|
||||
|
||||
// Disable Avalonia/Skia specific states
|
||||
gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
gl.Disable(EnableCap.SampleAlphaToOne);
|
||||
gl.Disable(EnableCap.Multisample);
|
||||
gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
|
||||
gl.LineWidth(1.0f);
|
||||
gl.PolygonOffset(0f, 0f);
|
||||
gl.Disable(EnableCap.PolygonOffsetFill);
|
||||
gl.Disable((EnableCap)GLEnum.ProgramPointSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using AcDream.App.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
|
||||
public unsafe class GLSLShader : BaseShader, IDisposable {
|
||||
private OpenGLGraphicsDevice _device;
|
||||
private Dictionary<string, int> _uniformLocations = [];
|
||||
private Dictionary<int, object> _uniformValues = [];
|
||||
private readonly object _lock = new();
|
||||
private GL GL => _device.GL;
|
||||
public uint Program { get; protected set; }
|
||||
|
||||
public bool HasUniform(string name) {
|
||||
lock (_lock) {
|
||||
return GetUniformLocation(Program, name) != -1;
|
||||
}
|
||||
}
|
||||
|
||||
public GLSLShader(OpenGLGraphicsDevice device, string name, string vertSource, string fragSource, ILogger log) : base(name, vertSource, fragSource, log) {
|
||||
_device = device;
|
||||
|
||||
Load(vertSource, fragSource);
|
||||
}
|
||||
|
||||
public GLSLShader(OpenGLGraphicsDevice device, string name, string shaderDirectory, ILogger log) : base(name, shaderDirectory, log) {
|
||||
_device = device;
|
||||
|
||||
Load();
|
||||
}
|
||||
|
||||
public override void Dispose() {
|
||||
Unload();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
private int GetUniformLocation(uint program, string name) {
|
||||
lock (_lock) {
|
||||
if (!_uniformLocations.ContainsKey(name)) {
|
||||
_uniformLocations.Add(name, GL.GetUniformLocation(program, name));
|
||||
}
|
||||
return _uniformLocations[name];
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Matrix4x4 m) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation(Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Matrix4x4 mCached && mCached == m) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = m;
|
||||
|
||||
GL.UniformMatrix4(loc, 1, false, (float*)&m);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, int v) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is int vCached && vCached == v) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = v;
|
||||
|
||||
GL.Uniform1(loc, v);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector2 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector2 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform2(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector3 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector3 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform3(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void SetUniform(string location, Vector3[] vecs) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
fixed (float* v = &vecs[0].X) {
|
||||
GL.Uniform3(loc, (uint)vecs.Length, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector4 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector4 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform4(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, float v) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is float vCached && vCached == v) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = v;
|
||||
|
||||
GL.Uniform1(loc, v);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, float[] vs) {
|
||||
lock (_lock) {
|
||||
fixed (float* v = &vs[0]) {
|
||||
GL.Uniform1(GetUniformLocation((uint)Program, location), (uint)vs.Length, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load(string vertShaderSource, string fragShaderSource) {
|
||||
|
||||
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
|
||||
_log.LogError($"Shader {Name} has no source code!");
|
||||
throw new InvalidOperationException($"Shader {Name} has no source code.");
|
||||
}
|
||||
|
||||
if (_device.HasOpenGL43 && _device.HasBindless) {
|
||||
string replacement = "#version 430 core\n#extension GL_ARB_bindless_texture : require";
|
||||
vertShaderSource = vertShaderSource.Replace("#version 330 core", replacement);
|
||||
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
|
||||
}
|
||||
|
||||
var resources = new ResourceCleanupGroup();
|
||||
uint prog = 0;
|
||||
bool accountingPublished = false;
|
||||
try {
|
||||
prog = ShaderProgramConstruction.Build(
|
||||
new GlShaderProgramBuildApi(GL),
|
||||
vertShaderSource,
|
||||
fragShaderSource);
|
||||
uint ownedProgram = prog;
|
||||
var unpublishedProgramRelease = new RetryableGpuResourceRelease(
|
||||
() => GlResourceCommand.DeleteProgram(
|
||||
GL,
|
||||
ownedProgram,
|
||||
$"delete unpublished WB shader program {ownedProgram}"),
|
||||
() => {
|
||||
if (accountingPublished)
|
||||
{
|
||||
GpuMemoryTracker.TrackResourceDeallocation(
|
||||
GpuResourceType.Shader);
|
||||
}
|
||||
});
|
||||
resources.Add("WB shader program", unpublishedProgramRelease.Run);
|
||||
|
||||
// Bind SceneData uniform block to point 0 if it exists.
|
||||
GlResourceCommand.Execute(GL, $"configure shader {Name} SceneData binding", () => {
|
||||
uint sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
|
||||
if (sceneDataIndex != uint.MaxValue)
|
||||
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
|
||||
});
|
||||
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
|
||||
accountingPublished = true;
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
_log.LogError(constructionFailure, "Failed to construct shader {ShaderName}", Name);
|
||||
resources.RollbackConstructionAndThrow(
|
||||
$"Shader {Name} construction failed and its GL program did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
|
||||
|
||||
if (Program != 0) {
|
||||
Unload();
|
||||
}
|
||||
_uniformLocations.Clear();
|
||||
_uniformValues.Clear();
|
||||
|
||||
Program = prog;
|
||||
ProgramId = prog;
|
||||
NeedsLoad = false;
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public override void Bind() {
|
||||
lock (_lock) {
|
||||
SetActive();
|
||||
if (Program != 0) {
|
||||
GL.UseProgram((uint)Program);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unbind() {
|
||||
lock (_lock) {
|
||||
GL.UseProgram(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Unload() {
|
||||
lock (_lock) {
|
||||
if (Program != 0) {
|
||||
var prog = Program;
|
||||
Program = 0;
|
||||
ProgramId = 0;
|
||||
_device.QueueGLAction(gl => {
|
||||
gl.DeleteProgram(prog);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Shader);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// A RAII scope for saving and restoring OpenGL state.
|
||||
/// </summary>
|
||||
public unsafe struct GLStateScope : IDisposable {
|
||||
private readonly GL _gl;
|
||||
private fixed int _viewport[4];
|
||||
private bool _scissorTest;
|
||||
private fixed int _scissorBox[4];
|
||||
private bool _depthTest;
|
||||
private int _depthFunc;
|
||||
private bool _depthMask;
|
||||
private bool _cullFace;
|
||||
private int _cullFaceMode;
|
||||
private int _frontFace;
|
||||
private bool _blend;
|
||||
private int _blendSrc;
|
||||
private int _blendDst;
|
||||
private int _blendEquation;
|
||||
|
||||
// Extended state
|
||||
private int _blendSrcAlpha;
|
||||
private int _blendDstAlpha;
|
||||
private int _blendEquationAlpha;
|
||||
private fixed byte _colorMask[4];
|
||||
private fixed float _clearColor[4];
|
||||
private float _clearDepth;
|
||||
private int _currentProgram;
|
||||
private int _vertexArrayBinding;
|
||||
private int _arrayBufferBinding;
|
||||
private int _elementArrayBufferBinding;
|
||||
private int _activeTexture;
|
||||
private int _textureBinding2D;
|
||||
private bool _stencilTest;
|
||||
private int _stencilFunc;
|
||||
private int _stencilRef;
|
||||
private int _stencilValueMask;
|
||||
private int _stencilFail;
|
||||
private int _stencilPassDepthFail;
|
||||
private int _stencilPassDepthPass;
|
||||
private int _stencilWritemask;
|
||||
private int _unpackAlignment;
|
||||
private int _packAlignment;
|
||||
|
||||
private int _drawFramebufferBinding;
|
||||
|
||||
// Skia / Avalonia extra state protections
|
||||
private fixed float _blendColor[4];
|
||||
private int _polygonMode;
|
||||
private bool _sampleAlphaToCoverage;
|
||||
private bool _multisample;
|
||||
private bool _primitiveRestart;
|
||||
private int _readFramebufferBinding;
|
||||
private int _uniformBufferBinding0;
|
||||
private float _lineWidth;
|
||||
private bool _programPointSize;
|
||||
private int _samplerBinding0;
|
||||
private int _samplerBinding1;
|
||||
private int _samplerBinding2;
|
||||
private int _unpackRowLength;
|
||||
private int _unpackSkipRows;
|
||||
private int _unpackSkipPixels;
|
||||
private bool _sampleAlphaToOne;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current OpenGL state.
|
||||
/// </summary>
|
||||
/// <param name="gl"></param>
|
||||
public GLStateScope(GL gl) {
|
||||
_gl = gl;
|
||||
_isDisposed = false;
|
||||
|
||||
fixed (int* v = _viewport) _gl.GetInteger(GetPName.Viewport, v);
|
||||
_scissorTest = _gl.IsEnabled(EnableCap.ScissorTest);
|
||||
fixed (int* s = _scissorBox) _gl.GetInteger(GetPName.ScissorBox, s);
|
||||
|
||||
_depthTest = _gl.IsEnabled(EnableCap.DepthTest);
|
||||
_gl.GetInteger(GetPName.DepthFunc, out _depthFunc);
|
||||
byte depthMask = 0;
|
||||
_gl.GetBoolean((GetPName)GLEnum.DepthWritemask, (bool*)&depthMask);
|
||||
_depthMask = depthMask != 0;
|
||||
|
||||
_cullFace = _gl.IsEnabled(EnableCap.CullFace);
|
||||
_gl.GetInteger(GetPName.CullFaceMode, out _cullFaceMode);
|
||||
_gl.GetInteger(GetPName.FrontFace, out _frontFace);
|
||||
|
||||
_blend = _gl.IsEnabled(EnableCap.Blend);
|
||||
_gl.GetInteger(GetPName.BlendSrcRgb, out _blendSrc);
|
||||
_gl.GetInteger(GetPName.BlendDstRgb, out _blendDst);
|
||||
_gl.GetInteger(GetPName.BlendSrcAlpha, out _blendSrcAlpha);
|
||||
_gl.GetInteger(GetPName.BlendDstAlpha, out _blendDstAlpha);
|
||||
_gl.GetInteger(GetPName.BlendEquationRgb, out _blendEquation);
|
||||
_gl.GetInteger(GetPName.BlendEquationAlpha, out _blendEquationAlpha);
|
||||
|
||||
fixed (byte* c = _colorMask) _gl.GetBoolean((GetPName)GLEnum.ColorWritemask, (bool*)c);
|
||||
fixed (float* cc = _clearColor) _gl.GetFloat(GetPName.ColorClearValue, cc);
|
||||
_gl.GetFloat(GetPName.DepthClearValue, out _clearDepth);
|
||||
|
||||
_gl.GetInteger(GetPName.CurrentProgram, out _currentProgram);
|
||||
_gl.GetInteger(GetPName.VertexArrayBinding, out _vertexArrayBinding);
|
||||
_gl.GetInteger(GetPName.ArrayBufferBinding, out _arrayBufferBinding);
|
||||
_gl.GetInteger(GetPName.ElementArrayBufferBinding, out _elementArrayBufferBinding);
|
||||
|
||||
_gl.GetInteger(GetPName.ActiveTexture, out _activeTexture);
|
||||
_gl.GetInteger(GetPName.TextureBinding2D, out _textureBinding2D);
|
||||
|
||||
_stencilTest = _gl.IsEnabled(EnableCap.StencilTest);
|
||||
_gl.GetInteger(GetPName.StencilFunc, out _stencilFunc);
|
||||
_gl.GetInteger(GetPName.StencilRef, out _stencilRef);
|
||||
_gl.GetInteger(GetPName.StencilValueMask, out _stencilValueMask);
|
||||
_gl.GetInteger(GetPName.StencilFail, out _stencilFail);
|
||||
_gl.GetInteger(GetPName.StencilPassDepthFail, out _stencilPassDepthFail);
|
||||
_gl.GetInteger(GetPName.StencilPassDepthPass, out _stencilPassDepthPass);
|
||||
_gl.GetInteger(GetPName.StencilWritemask, out _stencilWritemask);
|
||||
|
||||
_gl.GetInteger(GetPName.UnpackAlignment, out _unpackAlignment);
|
||||
_gl.GetInteger(GetPName.PackAlignment, out _packAlignment);
|
||||
|
||||
_gl.GetInteger(GetPName.DrawFramebufferBinding, out _drawFramebufferBinding);
|
||||
|
||||
fixed (float* bc = _blendColor) _gl.GetFloat(GetPName.BlendColor, bc);
|
||||
_gl.GetInteger(GetPName.PolygonMode, out _polygonMode);
|
||||
_sampleAlphaToCoverage = _gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
|
||||
_multisample = _gl.IsEnabled(EnableCap.Multisample);
|
||||
_primitiveRestart = _gl.IsEnabled((EnableCap)GLEnum.PrimitiveRestart);
|
||||
_gl.GetInteger(GetPName.ReadFramebufferBinding, out _readFramebufferBinding);
|
||||
|
||||
_gl.GetInteger(GetPName.UniformBufferBinding, out _uniformBufferBinding0);
|
||||
|
||||
_gl.GetFloat(GetPName.LineWidth, out _lineWidth);
|
||||
_programPointSize = _gl.IsEnabled((EnableCap)GLEnum.ProgramPointSize);
|
||||
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding0);
|
||||
_gl.ActiveTexture(TextureUnit.Texture1);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding1);
|
||||
_gl.ActiveTexture(TextureUnit.Texture2);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding2);
|
||||
_gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackRowLength, out _unpackRowLength);
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackSkipRows, out _unpackSkipRows);
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackSkipPixels, out _unpackSkipPixels);
|
||||
_sampleAlphaToOne = _gl.IsEnabled(EnableCap.SampleAlphaToOne);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores only the scissor state from the scope.
|
||||
/// </summary>
|
||||
public void RestoreScissor() {
|
||||
if (_scissorTest) _gl.Enable(EnableCap.ScissorTest);
|
||||
else _gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.Scissor(_scissorBox[0], _scissorBox[1], (uint)_scissorBox[2], (uint)_scissorBox[3]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the captured OpenGL state.
|
||||
/// </summary>
|
||||
public void Dispose() {
|
||||
if (_isDisposed) return;
|
||||
|
||||
// Restoring state
|
||||
if (_currentProgram != 0) _gl.UseProgram((uint)_currentProgram); else _gl.UseProgram(0);
|
||||
|
||||
_gl.BindVertexArray((uint)_vertexArrayBinding);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBufferBinding);
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, (uint)_elementArrayBufferBinding);
|
||||
_gl.BindBuffer(GLEnum.UniformBuffer, (uint)_uniformBufferBinding0);
|
||||
|
||||
_gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, (uint)_textureBinding2D);
|
||||
|
||||
if (_stencilTest) _gl.Enable(EnableCap.StencilTest); else _gl.Disable(EnableCap.StencilTest);
|
||||
_gl.StencilFunc((StencilFunction)_stencilFunc, _stencilRef, (uint)_stencilValueMask);
|
||||
_gl.StencilOp((StencilOp)_stencilFail, (StencilOp)_stencilPassDepthFail, (StencilOp)_stencilPassDepthPass);
|
||||
_gl.StencilMask((uint)_stencilWritemask);
|
||||
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, _unpackAlignment);
|
||||
_gl.PixelStore(PixelStoreParameter.PackAlignment, _packAlignment);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackRowLength, _unpackRowLength);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackSkipRows, _unpackSkipRows);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, _unpackSkipPixels);
|
||||
|
||||
_gl.ClearColor(_clearColor[0], _clearColor[1], _clearColor[2], _clearColor[3]);
|
||||
_gl.ClearDepth(_clearDepth);
|
||||
|
||||
_gl.Viewport(_viewport[0], _viewport[1], (uint)_viewport[2], (uint)_viewport[3]);
|
||||
RestoreScissor();
|
||||
|
||||
if (_depthTest) _gl.Enable(EnableCap.DepthTest); else _gl.Disable(EnableCap.DepthTest);
|
||||
_gl.DepthFunc((DepthFunction)_depthFunc);
|
||||
_gl.DepthMask(_depthMask);
|
||||
|
||||
if (_cullFace) _gl.Enable(EnableCap.CullFace); else _gl.Disable(EnableCap.CullFace);
|
||||
_gl.CullFace((TriangleFace)_cullFaceMode);
|
||||
_gl.FrontFace((FrontFaceDirection)_frontFace);
|
||||
|
||||
if (_blend) _gl.Enable(EnableCap.Blend); else _gl.Disable(EnableCap.Blend);
|
||||
_gl.BlendFuncSeparate((BlendingFactor)_blendSrc, (BlendingFactor)_blendDst, (BlendingFactor)_blendSrcAlpha, (BlendingFactor)_blendDstAlpha);
|
||||
_gl.BlendEquationSeparate((BlendEquationModeEXT)_blendEquation, (BlendEquationModeEXT)_blendEquationAlpha);
|
||||
_gl.BlendColor(_blendColor[0], _blendColor[1], _blendColor[2], _blendColor[3]);
|
||||
|
||||
_gl.ColorMask(_colorMask[0] != 0, _colorMask[1] != 0, _colorMask[2] != 0, _colorMask[3] != 0);
|
||||
|
||||
_gl.PolygonMode(GLEnum.FrontAndBack, (PolygonMode)_polygonMode);
|
||||
|
||||
if (_sampleAlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); else _gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
if (_sampleAlphaToOne) _gl.Enable(EnableCap.SampleAlphaToOne); else _gl.Disable(EnableCap.SampleAlphaToOne);
|
||||
if (_multisample) _gl.Enable(EnableCap.Multisample); else _gl.Disable(EnableCap.Multisample);
|
||||
if (_primitiveRestart) _gl.Enable((EnableCap)GLEnum.PrimitiveRestart); else _gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
|
||||
if (_programPointSize) _gl.Enable((EnableCap)GLEnum.ProgramPointSize); else _gl.Disable((EnableCap)GLEnum.ProgramPointSize);
|
||||
|
||||
_gl.LineWidth(_lineWidth);
|
||||
|
||||
_gl.BindSampler(0, (uint)_samplerBinding0);
|
||||
_gl.BindSampler(1, (uint)_samplerBinding1);
|
||||
_gl.BindSampler(2, (uint)_samplerBinding2);
|
||||
|
||||
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, (uint)_drawFramebufferBinding);
|
||||
_gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, (uint)_readFramebufferBinding);
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using AcDream.Content;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -53,15 +51,6 @@ internal sealed class GlobalMeshMigrationAbortTicket
|
|||
public void Advance() => _release.Run();
|
||||
}
|
||||
|
||||
internal static class GlobalMeshVaoAccounting
|
||||
{
|
||||
public static void TrackAllocation() =>
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
|
||||
|
||||
public static void TrackDeallocation() =>
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
|
||||
}
|
||||
|
||||
internal enum GlobalMeshCapacityResult
|
||||
{
|
||||
Ready,
|
||||
|
|
@ -82,19 +71,15 @@ internal enum GlobalMeshCapacityResult
|
|||
/// backend implements with <c>vkCmdCopyBuffer</c>. The reclaimable-range
|
||||
/// allocator, growth quanta, budgeted incremental migration, retirement-ledger
|
||||
/// gating and the dual-generation physical ceiling are unchanged; only the
|
||||
/// resource handle type moved. The vertex array object stays raw GL because a
|
||||
/// VAO has no RHI equivalent (Vulkan bakes vertex input into the pipeline) and
|
||||
/// <c>WbDrawDispatcher</c>, <c>EnvCellRenderer</c> and <c>ParticleRenderer</c>
|
||||
/// still bind <see cref="VAO"/>/<see cref="VBO"/>/<see cref="IBO"/> directly
|
||||
/// on the GL arm.
|
||||
/// resource handle type moved.
|
||||
///
|
||||
/// <para>Campaign V slice V6i-3 made the GL context optional. A backend that has
|
||||
/// none builds no vertex array and publishes no raw names — <see cref="VAO"/>,
|
||||
/// <see cref="VBO"/> and <see cref="IBO"/> are 0 there — and its consumers bind
|
||||
/// <para>Campaign V slice V6i-3 made the GL context (and its vertex array
|
||||
/// object, which has no RHI equivalent — Vulkan bakes vertex input into the
|
||||
/// pipeline) optional; Campaign V slice V11 deleted the GL arm entirely, so
|
||||
/// this arena now only ever builds the two backing stores. Its consumers bind
|
||||
/// <see cref="VertexStore"/> and <see cref="IndexStore"/> through the pass
|
||||
/// encoder instead, which is the same 32-byte position/normal/texcoord layout
|
||||
/// expressed as pipeline vertex input. Everything above the handle — the
|
||||
/// allocator, the migration, the ledger — is one body on both arms.</para>
|
||||
/// encoder, which is the same 32-byte position/normal/texcoord layout
|
||||
/// expressed as pipeline vertex input.</para>
|
||||
/// </summary>
|
||||
public sealed class GlobalMeshBuffer : IDisposable
|
||||
{
|
||||
|
|
@ -113,10 +98,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
internal const int MaximumIndexCapacity =
|
||||
(int)(MaximumIndexBufferBytes / sizeof(ushort));
|
||||
|
||||
// Retained only for the vertex array object and its attribute layout, which
|
||||
// the RHI has no verb for, and null on a backend with no such object. It is
|
||||
// retired with the raw-GL dispatcher.
|
||||
private readonly GL? _gl;
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly GpuRetirementLedger _retirementLedger;
|
||||
private readonly GpuRetiredRangeAllocator _vertices;
|
||||
|
|
@ -134,20 +115,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
store ?? throw new InvalidOperationException(
|
||||
"The global mesh arena has no live backing store.");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4b transitional bridge. The arena owns its stores as
|
||||
/// <see cref="IGpuBuffer"/>, but its consumers — the vertex array object here,
|
||||
/// and <c>WbDrawDispatcher</c>/<c>EnvCellRenderer</c>/<c>ParticleRenderer</c>
|
||||
/// through <see cref="VBO"/>/<see cref="IBO"/> — are still raw GL until slice
|
||||
/// V4c. This is the only place that reaches through the interface, and it
|
||||
/// disappears with those consumers.
|
||||
/// </summary>
|
||||
private static GlGpuBuffer RequireGlBuffer(IGpuBuffer buffer) =>
|
||||
buffer as GlGpuBuffer
|
||||
?? throw new NotSupportedException(
|
||||
"The global mesh arena requires a GL-backed buffer while its draw paths "
|
||||
+ "still bind raw GL names (Campaign V slice V4c retires that requirement).");
|
||||
|
||||
private enum BufferKind
|
||||
{
|
||||
Vertices,
|
||||
|
|
@ -167,35 +134,16 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
public long CopiedBytes { get; set; }
|
||||
}
|
||||
|
||||
public uint VAO { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The vertex store's raw GL name, or 0 on a backend with no GL context.
|
||||
/// Transitional: the GL draw paths still bind the arena themselves, so the
|
||||
/// arena keeps publishing the backend name of the buffer it now owns as an
|
||||
/// <see cref="IGpuBuffer"/>.
|
||||
/// </summary>
|
||||
public uint VBO =>
|
||||
_gl is null || _vertexBuffer is null ? 0u : RequireGlBuffer(_vertexBuffer).GlName;
|
||||
|
||||
/// <summary>The index store's raw GL name. See <see cref="VBO"/>.</summary>
|
||||
public uint IBO =>
|
||||
_gl is null || _indexBuffer is null ? 0u : RequireGlBuffer(_indexBuffer).GlName;
|
||||
|
||||
/// <summary>
|
||||
/// The vertex store as the contract's own handle. This is what a pass
|
||||
/// encoder binds, and it is live on both arms — <see cref="VAO"/> is the
|
||||
/// GL-only expression of the same thing.
|
||||
/// encoder binds.
|
||||
/// </summary>
|
||||
internal IGpuBuffer? VertexStore => _vertexBuffer;
|
||||
|
||||
/// <summary>The index store as the contract's own handle. See <see cref="VertexStore"/>.</summary>
|
||||
internal IGpuBuffer? IndexStore => _indexBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// True once both backing stores exist. The backend-neutral form of the
|
||||
/// <c>VAO != 0</c> readiness test the raw-GL draw paths make.
|
||||
/// </summary>
|
||||
/// <summary>True once both backing stores exist.</summary>
|
||||
internal bool HasStores => _vertexBuffer is not null && _indexBuffer is not null;
|
||||
internal long UploadCount { get; private set; }
|
||||
internal long UploadedBytes { get; private set; }
|
||||
|
|
@ -268,9 +216,8 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
newBuffers);
|
||||
}
|
||||
|
||||
internal GlobalMeshBuffer(GL? gl, IGpuDevice device, IGpuResourceRetirementQueue retirement)
|
||||
internal GlobalMeshBuffer(IGpuDevice device, IGpuResourceRetirementQueue retirement)
|
||||
{
|
||||
_gl = gl;
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
ArgumentNullException.ThrowIfNull(retirement);
|
||||
_retirementLedger = new GpuRetirementLedger(retirement);
|
||||
|
|
@ -295,47 +242,20 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
| GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.DeviceLocal);
|
||||
|
||||
private unsafe void InitBuffers()
|
||||
private void InitBuffers()
|
||||
{
|
||||
uint vao = 0;
|
||||
IGpuBuffer? vbo = null;
|
||||
IGpuBuffer? ibo = null;
|
||||
long vertexBytes = (long)_vertices.Capacity * VertexPositionNormalTexture.Size;
|
||||
long indexBytes = (long)_indices.Capacity * sizeof(ushort);
|
||||
bool vaoTracked = false;
|
||||
bool vertexTracked = false;
|
||||
bool indexTracked = false;
|
||||
|
||||
try
|
||||
{
|
||||
// The vertex array is the one object here with no RHI equivalent —
|
||||
// Vulkan bakes vertex input into the pipeline — so a backend with no
|
||||
// GL context builds the two stores and nothing else.
|
||||
if (_gl is { } gl)
|
||||
{
|
||||
gl.GenVertexArrays(1, out vao);
|
||||
if (vao == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create the global mesh-buffer objects.");
|
||||
}
|
||||
|
||||
vbo = _device.CreateBuffer(DescribeStore(BufferKind.Vertices, vertexBytes, _storeGeneration));
|
||||
ibo = _device.CreateBuffer(DescribeStore(BufferKind.Indices, indexBytes, _storeGeneration));
|
||||
|
||||
if (_gl is { } glBind)
|
||||
{
|
||||
glBind.BindVertexArray(vao);
|
||||
glBind.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(vbo).GlName);
|
||||
ConfigureVertexAttributes(glBind);
|
||||
|
||||
glBind.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(ibo).GlName);
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
glBind,
|
||||
$"creating global mesh buffers ({vertexBytes} vertex bytes, {indexBytes} index bytes)");
|
||||
|
||||
GlobalMeshVaoAccounting.TrackAllocation();
|
||||
vaoTracked = true;
|
||||
}
|
||||
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GpuMemoryTracker.TrackAllocation(vertexBytes, GpuResourceType.Buffer);
|
||||
vertexTracked = true;
|
||||
|
|
@ -343,25 +263,15 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
GpuMemoryTracker.TrackAllocation(indexBytes, GpuResourceType.Buffer);
|
||||
indexTracked = true;
|
||||
|
||||
VAO = vao;
|
||||
_vertexBuffer = vbo;
|
||||
_indexBuffer = ibo;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Construction rollback: nothing was ever submitted, so the physical
|
||||
// stores are released on the spot rather than deferred. Pattern-matched
|
||||
// rather than RequireGlBuffer'd so a non-GL store could never raise a
|
||||
// cast failure that masks the original construction exception.
|
||||
if (ibo is GlGpuBuffer stagedIndexStore)
|
||||
stagedIndexStore.DeleteRetired("rolling back the global index arena buffer");
|
||||
else
|
||||
ibo?.Dispose();
|
||||
if (vbo is GlGpuBuffer stagedVertexStore)
|
||||
stagedVertexStore.DeleteRetired("rolling back the global vertex arena buffer");
|
||||
else
|
||||
vbo?.Dispose();
|
||||
if (vao != 0) _gl!.DeleteVertexArray(vao);
|
||||
// stores are released on the spot rather than deferred.
|
||||
ibo?.Dispose();
|
||||
vbo?.Dispose();
|
||||
if (indexTracked)
|
||||
{
|
||||
GpuMemoryTracker.TrackDeallocation(indexBytes, GpuResourceType.Buffer);
|
||||
|
|
@ -372,25 +282,8 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
GpuMemoryTracker.TrackDeallocation(vertexBytes, GpuResourceType.Buffer);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
}
|
||||
if (vaoTracked)
|
||||
GlobalMeshVaoAccounting.TrackDeallocation();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gl?.BindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void ConfigureVertexAttributes(GL gl)
|
||||
{
|
||||
int stride = VertexPositionNormalTexture.Size;
|
||||
gl.EnableVertexAttribArray(0);
|
||||
gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
|
||||
gl.EnableVertexAttribArray(1);
|
||||
gl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
|
||||
gl.EnableVertexAttribArray(2);
|
||||
gl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
}
|
||||
|
||||
internal GlobalMeshAllocation UploadMesh(
|
||||
|
|
@ -673,9 +566,9 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Copies at most <paramref name="maximumCopyBytes"/> of the immutable
|
||||
/// live prefix into the staged backing store. The active VAO continues to
|
||||
/// reference the old store until the final chunk succeeds, then one atomic
|
||||
/// VAO rebind publishes the destination.
|
||||
/// live prefix into the staged backing store. Draws continue to reference
|
||||
/// the old store until the final chunk succeeds, then one atomic field swap
|
||||
/// (<see cref="CommitMigration"/>) publishes the destination.
|
||||
/// </summary>
|
||||
internal GlobalMeshMaintenanceStep AdvanceMigration(long maximumCopyBytes)
|
||||
{
|
||||
|
|
@ -819,48 +712,9 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
private void CommitMigration(BufferMigration migration)
|
||||
{
|
||||
// The atomic publication step is a VAO rebind on GL and nothing at all
|
||||
// on a backend whose vertex source is a per-draw encoder bind: the field
|
||||
// swap below IS the publication there, and the next pass reads the new
|
||||
// store. The rollback arm exists for the same reason it did — a failed
|
||||
// rebind must leave the vertex array pointing at the live store.
|
||||
if (_gl is { } gl)
|
||||
{
|
||||
try
|
||||
{
|
||||
gl.BindVertexArray(VAO);
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
|
||||
ConfigureVertexAttributes(gl);
|
||||
}
|
||||
else
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(gl, $"publishing staged {migration.Kind} arena buffer");
|
||||
}
|
||||
catch
|
||||
{
|
||||
gl.BindVertexArray(VAO);
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
|
||||
ConfigureVertexAttributes(gl);
|
||||
}
|
||||
else
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
|
||||
}
|
||||
gl.BindVertexArray(0);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
gl.BindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
// The atomic publication step is nothing at all here: the vertex source
|
||||
// is a per-draw pass-encoder bind, so the field swap below IS the
|
||||
// publication, and the next pass reads the new store.
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
_vertexBuffer = migration.NewBuffer;
|
||||
|
|
@ -908,11 +762,10 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
/// <summary>
|
||||
/// The arena's own flight gate — <see cref="_retirementLedger"/> and the abort
|
||||
/// ticket — already proves no submitted frame can reference the store, so the
|
||||
/// physical delete runs here rather than being deferred a second time by
|
||||
/// <see cref="IGpuBuffer.Dispose"/>. Stages match
|
||||
/// <c>TrackedGlResource.CreateRetryableBufferDeletion</c> exactly: precondition,
|
||||
/// mutation-with-validation, byte accounting, then resource-count accounting,
|
||||
/// so a driver failure re-issues only the delete and never double-counts.
|
||||
/// physical delete runs here rather than being deferred a second time.
|
||||
/// Stages: precondition (no-op), mutation (dispose), byte accounting, then
|
||||
/// resource-count accounting, so a failure re-issues only the delete and
|
||||
/// never double-counts.
|
||||
/// </summary>
|
||||
private RetryableGpuResourceRelease CreateRetryableStoreDeletion(
|
||||
IGpuBuffer buffer,
|
||||
|
|
@ -921,24 +774,9 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
|
||||
GL? gl = _gl;
|
||||
return new RetryableGpuResourceRelease(
|
||||
() =>
|
||||
{
|
||||
if (gl is not null)
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
},
|
||||
// On GL the delete runs here rather than through IGpuBuffer.Dispose
|
||||
// because the arena's own flight gate has already proven no submitted
|
||||
// frame can reference the store. A backend with no GL context has no
|
||||
// second deferral to skip: Dispose IS its retirement-queued release.
|
||||
() =>
|
||||
{
|
||||
if (gl is not null)
|
||||
RequireGlBuffer(buffer).DeleteRetired(context);
|
||||
else
|
||||
buffer.Dispose();
|
||||
},
|
||||
() => { },
|
||||
() => buffer.Dispose(),
|
||||
() =>
|
||||
{
|
||||
if (capacityBytes != 0)
|
||||
|
|
@ -1020,16 +858,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
releases.Add(("staged-migration-buffer", release.Run));
|
||||
}
|
||||
|
||||
if (VAO != 0 && _gl is { } vaoGl)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
||||
vaoGl,
|
||||
VAO,
|
||||
$"deleting global mesh vertex array {VAO}",
|
||||
GlobalMeshVaoAccounting.TrackDeallocation);
|
||||
releases.Add(("global-vao", release.Run));
|
||||
}
|
||||
if (_vertexBuffer is { } vertexStore)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
|
|
@ -1058,7 +886,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
_migration = null;
|
||||
_migrationAbort = null;
|
||||
VAO = 0;
|
||||
_vertexBuffer = null;
|
||||
_indexBuffer = null;
|
||||
_disposeResources = null;
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// retirement queue, the shared instance VBO, and two capability flags. Seven
|
||||
/// members out of a 760-line class.</para>
|
||||
///
|
||||
/// <para>So the coupling is expressed as an interface at exactly that surface
|
||||
/// and <see cref="OpenGLGraphicsDevice"/> declares it — every member already
|
||||
/// existed, so the GL arm executes not one changed statement. What this buys is
|
||||
/// that <c>ObjectMeshManager</c> and <c>WbMeshAdapter</c> no longer NAME a
|
||||
/// backend, which is the prerequisite for the slice that gives them a second
|
||||
/// <para>So the coupling is expressed as an interface at exactly that surface,
|
||||
/// and <c>OpenGLGraphicsDevice</c> declared it — every member already existed,
|
||||
/// so the GL arm executed not one changed statement. What this buys is that
|
||||
/// <c>ObjectMeshManager</c> and <c>WbMeshAdapter</c> no longer NAME a backend,
|
||||
/// which is the prerequisite for the slice that gives them a second
|
||||
/// implementation.</para>
|
||||
///
|
||||
/// <para><b>What slice V6i-3 then moved.</b> V6i-2 left the upload bodies raw —
|
||||
|
|
@ -27,10 +27,16 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// GL but not RUN. The arena now builds its stores through
|
||||
/// <c>IGpuDevice.CreateBuffer</c> and publishes them as
|
||||
/// <c>GlobalMeshBuffer.VertexStore</c>/<c>IndexStore</c>, which a pass encoder
|
||||
/// binds; the vertex array is built only where one exists. What still reads
|
||||
/// <see cref="Gl"/> is the LEGACY per-mesh upload the N.5 ship amendment made
|
||||
/// unreachable, and <c>AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice</c>
|
||||
/// is the second implementation this interface was cut for.</para>
|
||||
/// binds; the vertex array is built only where one exists.
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice"/> is the
|
||||
/// second implementation this interface was cut for.</para>
|
||||
///
|
||||
/// <para><b>Campaign V slice V11</b> deleted <c>OpenGLGraphicsDevice</c> along
|
||||
/// with the rest of the raw-GL arm it fronted, so
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice"/> is now
|
||||
/// the interface's only implementation. <see cref="Gl"/> always answers null
|
||||
/// there; removing it (and the legacy per-mesh upload bodies it alone still
|
||||
/// gated) is Campaign V's package/shader cleanup slice, not this one.</para>
|
||||
/// </summary>
|
||||
internal interface IMeshPipelineDevice : IDisposable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
|
||||
/// </summary>
|
||||
public class ManagedGLFramebuffer : IFramebuffer {
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL _gl => _device.GL;
|
||||
private readonly uint _fboId;
|
||||
private readonly uint _depthStencilRenderbuffer; // 0 if not used
|
||||
private readonly ITexture _texture;
|
||||
private readonly int _width;
|
||||
private readonly int _height;
|
||||
|
||||
public ITexture Texture => _texture;
|
||||
public IntPtr NativeHandle => new IntPtr(_fboId);
|
||||
|
||||
public ManagedGLFramebuffer(OpenGLGraphicsDevice device, ITexture texture, int width, int height, bool hasDepthStencil) {
|
||||
_device = device;
|
||||
_texture = texture;
|
||||
_width = width;
|
||||
_height = height;
|
||||
|
||||
// Generate and bind the framebuffer
|
||||
_fboId = _gl.GenFramebuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.FBO);
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fboId);
|
||||
|
||||
// Attach the texture as the color attachment
|
||||
_gl.FramebufferTexture2D(
|
||||
FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.ColorAttachment0,
|
||||
TextureTarget.Texture2D,
|
||||
(uint)texture.NativePtr.ToInt32(),
|
||||
0
|
||||
);
|
||||
|
||||
// Create and attach a depth-stencil renderbuffer if requested
|
||||
if (true || hasDepthStencil) {
|
||||
_depthStencilRenderbuffer = _gl.GenRenderbuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.RBO);
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthStencilRenderbuffer);
|
||||
_gl.RenderbufferStorage(
|
||||
RenderbufferTarget.Renderbuffer,
|
||||
InternalFormat.Depth24Stencil8,
|
||||
(uint)width,
|
||||
(uint)height
|
||||
);
|
||||
_gl.FramebufferRenderbuffer(
|
||||
FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.DepthStencilAttachment,
|
||||
RenderbufferTarget.Renderbuffer,
|
||||
_depthStencilRenderbuffer
|
||||
);
|
||||
GpuMemoryTracker.TrackAllocation(_width * _height * 4, GpuResourceType.RBO); // Depth24Stencil8 is 4 bytes per pixel
|
||||
}
|
||||
|
||||
// Check framebuffer completeness
|
||||
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
|
||||
if (status != GLEnum.FramebufferComplete) {
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
_gl.DeleteFramebuffer(_fboId);
|
||||
if (_depthStencilRenderbuffer != 0) {
|
||||
_gl.DeleteRenderbuffer(_depthStencilRenderbuffer);
|
||||
}
|
||||
throw new InvalidOperationException($"Framebuffer creation failed: {status}");
|
||||
}
|
||||
|
||||
var error = _gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
throw new InvalidOperationException($"OpenGL error during framebuffer setup: {error}");
|
||||
}
|
||||
|
||||
// Unbind the framebuffer
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
var fboId = _fboId;
|
||||
var depthStencilRenderbuffer = _depthStencilRenderbuffer;
|
||||
var width = _width;
|
||||
var height = _height;
|
||||
|
||||
_device.QueueGLAction(gl => {
|
||||
if (fboId != 0) {
|
||||
gl.DeleteFramebuffer(fboId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.FBO);
|
||||
}
|
||||
if (depthStencilRenderbuffer != 0) {
|
||||
gl.DeleteRenderbuffer(depthStencilRenderbuffer);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.RBO);
|
||||
GpuMemoryTracker.TrackDeallocation(width * height * 4, GpuResourceType.RBO);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Silk.NET.OpenGL;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL index buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private void* _mappedPtr;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLIndexBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLIndexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device;
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Allocate the buffer with the specified size but no initial data
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_device.HasBufferStorage) {
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ElementArrayBuffer, (uint)Size, (void*)0, flags);
|
||||
_mappedPtr = GL.MapBufferRange(GLEnum.ElementArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)Size, (void*)0, Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetData(uint[] data) {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData(Span<uint> data) {
|
||||
uint dataSize = (uint)data.Length * sizeof(uint);
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<uint> mappedSpan = new Span<uint>(_mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[0]) {
|
||||
GL.BufferData(GLEnum.ElementArrayBuffer, dataSize, (void*)dataPtr, Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData(Span<uint> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
|
||||
|
||||
if (dataSizeBytes == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're not trying to write past the end of the buffer
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<uint> mappedSpan = new Span<uint>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
|
||||
GL.BufferSubData(
|
||||
GLEnum.ElementArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
(void*)dataPtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData(uint[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
|
||||
|
||||
if (dataSizeBytes == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're not trying to write past the end of the buffer
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
|
||||
GL.BufferSubData(
|
||||
GLEnum.ElementArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
(void*)dataPtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Bind() {
|
||||
RenderStateCache.CurrentIBO = 0;
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
_mappedPtr = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public unsafe class ManagedGLTexture : ITexture {
|
||||
private uint _texture;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
|
||||
private GL GL => (_device as OpenGLGraphicsDevice).GL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr NativePtr => (IntPtr)_texture;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Width { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Height { get; private set; }
|
||||
|
||||
public TextureFormat Format => TextureFormat.RGBA8;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ManagedGLTexture(OpenGLGraphicsDevice device, byte[]? source, int width, int height, TextureParameters? texParams = null) {
|
||||
var p = texParams ?? TextureParameters.Default;
|
||||
_device = device;
|
||||
_texture = GL.GenTexture();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
Width = width;
|
||||
Height = height;
|
||||
GL.BindTexture(GLEnum.Texture2D, _texture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
|
||||
if (_device.HasTextureStorage) {
|
||||
GL.TexStorage2D(GLEnum.Texture2D, (uint)mipLevels, GLEnum.Rgba8, (uint)width, (uint)height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
else {
|
||||
GL.TexImage2D(GLEnum.Texture2D, 0, (int)InternalFormat.Rgba8, (uint)width, (uint)height, 0, PixelFormat.Rgba, (PixelType)0x1401, (void*)0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMinFilter, (int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (p.EnableAnisotropicFiltering && _device.RenderSettings.EnableAnisotropicFiltering)
|
||||
{
|
||||
if (_device.MaxSupportedAnisotropy > 0)
|
||||
{
|
||||
GL.TexParameter(GLEnum.Texture2D, GLEnum.TextureMaxAnisotropy,
|
||||
_device.MaxSupportedAnisotropy);
|
||||
}
|
||||
}
|
||||
|
||||
if (p.EnableMipmaps) {
|
||||
GL.GenerateMipmap(GLEnum.Texture2D);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindTexture(GLEnum.Texture2D, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(CalculateSize(), GpuResourceType.Texture);
|
||||
|
||||
}
|
||||
|
||||
private long CalculateSize() {
|
||||
int maxDimension = Math.Max(Width, Height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
long totalSize = 0;
|
||||
|
||||
for (int i = 0; i < mipLevels; i++) {
|
||||
int w = Math.Max(1, Width >> i);
|
||||
int h = Math.Max(1, Height >> i);
|
||||
totalSize += (long)w * h * 4;
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ManagedGLTexture(OpenGLGraphicsDevice device, string file) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetData(Rectangle rectangle, byte[] data) {
|
||||
if (_texture == 0) return;
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
|
||||
GL.GetInteger(GLEnum.TextureBinding2D, out int oldBinding);
|
||||
GL.BindTexture(GLEnum.Texture2D, _texture);
|
||||
|
||||
fixed (byte* ptr = data) {
|
||||
GL.TexSubImage2D(
|
||||
GLEnum.Texture2D,
|
||||
0, // level
|
||||
rectangle.X,
|
||||
rectangle.Y,
|
||||
(uint)rectangle.Width,
|
||||
(uint)rectangle.Height,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
ptr
|
||||
);
|
||||
}
|
||||
|
||||
// Generate mipmaps if needed
|
||||
GL.GenerateMipmap(GLEnum.Texture2D);
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2D, (uint)oldBinding);
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Bind(int slot = 0) {
|
||||
if (slot == 0) {
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
}
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
||||
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture(targetTextureUnit);
|
||||
}
|
||||
|
||||
GL.BindSampler((uint)slot, 0);
|
||||
GL.BindTexture(GLEnum.Texture2D, (uint)NativePtr);
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2D, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected void ReleaseTexture() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_texture != 0) {
|
||||
GL.DeleteTexture(_texture);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
GpuMemoryTracker.TrackDeallocation(CalculateSize(), GpuResourceType.Texture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
_texture = 0;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ReleaseTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,705 +0,0 @@
|
|||
using AcDream.Core.Rendering.Wb;
|
||||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
|
||||
using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public class ManagedGLTextureArray : ITextureArray, IWorldTextureArray {
|
||||
private readonly bool[] _usedLayers;
|
||||
private readonly GL GL;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the device whose one texture table this
|
||||
/// array's two resident handles are interned into. Before this slice
|
||||
/// <c>ObjectMeshManager</c> read the handles off this object and did the
|
||||
/// interning itself; a 64-bit bindless handle cannot cross to Vulkan, so
|
||||
/// the array now answers <see cref="ResolveSlot"/> instead. Null only
|
||||
/// for the legacy <c>OpenGLGraphicsDevice.CreateTextureArrayInternal</c>
|
||||
/// entry points, which no shared atlas uses.
|
||||
/// </summary>
|
||||
private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? _worldTextureTable;
|
||||
private static int _nextId = 0;
|
||||
private bool _needsMipmapRegeneration = false;
|
||||
private readonly bool _isCompressed;
|
||||
private int _mipmapDirtyCount = 0;
|
||||
private readonly object _mipmapLock = new object();
|
||||
private readonly List<TextureLayerUpdate> _pendingUpdates = new();
|
||||
private int _disposeQueued;
|
||||
private int _disposePublicationQueued;
|
||||
private int _disposeRetirementAccepted;
|
||||
private RetryableGpuResourceRelease? _disposeRelease;
|
||||
|
||||
private struct TextureLayerUpdate {
|
||||
public int Layer;
|
||||
public required byte[] Data;
|
||||
public PixelFormat? UploadPixelFormat;
|
||||
public PixelType? UploadPixelType;
|
||||
}
|
||||
|
||||
public int Slot { get; } = _nextId++;
|
||||
public int Width { get; private set; }
|
||||
public int Height { get; private set; }
|
||||
public int Size { get; private set; }
|
||||
public TextureFormat Format { get; private set; }
|
||||
public nint NativePtr { get; private set; }
|
||||
public ulong BindlessWrapHandle { get; private set; }
|
||||
public ulong BindlessClampHandle { get; private set; }
|
||||
public long TotalSizeInBytes => CalculateTotalSize();
|
||||
|
||||
/// <summary>
|
||||
/// #105 diagnostic: staged layer updates (retained decoded payloads) not yet
|
||||
/// applied to the GL texture by <see cref="ProcessDirtyUpdates"/>. Layers with
|
||||
/// a pending update sample UNDEFINED content (TexStorage3D contents) until the
|
||||
/// flush runs — a stuck non-zero count at standstill is the white-walls mechanism.
|
||||
/// </summary>
|
||||
public int PendingUpdateCount {
|
||||
get { lock (_mipmapLock) { return _pendingUpdates.Count; } }
|
||||
}
|
||||
|
||||
public ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
|
||||
int size, ILogger logger, TextureParameters? texParams = null)
|
||||
: this(graphicsDevice, format, width, height, size, logger, worldTextureTable: null, texParams) {
|
||||
}
|
||||
|
||||
internal ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
|
||||
int size, ILogger logger,
|
||||
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? worldTextureTable,
|
||||
TextureParameters? texParams = null) {
|
||||
_worldTextureTable = worldTextureTable;
|
||||
var p = texParams ?? TextureParameters.Default;
|
||||
if (width <= 0 || height <= 0 || size <= 0) {
|
||||
throw new ArgumentException($"Invalid texture array dimensions: {width}x{height}x{size}");
|
||||
}
|
||||
|
||||
Format = format;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Size = size;
|
||||
_usedLayers = new bool[size];
|
||||
_device = graphicsDevice;
|
||||
GL = graphicsDevice.GL;
|
||||
_logger = logger;
|
||||
_isCompressed = IsCompressedFormat(format);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
uint textureName = 0;
|
||||
ulong wrapHandle = 0;
|
||||
ulong clampHandle = 0;
|
||||
bool textureTracked = false;
|
||||
bool textureBytesTracked = false;
|
||||
bool wrapResident = false;
|
||||
bool clampResident = false;
|
||||
long textureBytes = CalculateTotalSize();
|
||||
|
||||
try {
|
||||
textureName = GL.GenTexture();
|
||||
if (textureName == 0)
|
||||
throw new InvalidOperationException("Failed to generate texture array.");
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
textureTracked = true;
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2DArray, textureName);
|
||||
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
|
||||
GL.TexStorage3D(GLEnum.Texture2DArray, (uint)mipLevels, format.ToGL(), (uint)width, (uint)height,
|
||||
(uint)size);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMinFilter,
|
||||
(int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMaxLevel, mipLevels - 1);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
|
||||
if (p.EnableAnisotropicFiltering
|
||||
&& graphicsDevice.RenderSettings.EnableAnisotropicFiltering
|
||||
&& graphicsDevice.MaxSupportedAnisotropy > 0) {
|
||||
GL.TexParameter(
|
||||
GLEnum.Texture2DArray,
|
||||
GLEnum.TextureMaxAnisotropy,
|
||||
graphicsDevice.MaxSupportedAnisotropy);
|
||||
}
|
||||
|
||||
if (format == TextureFormat.A8) {
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleR, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleG, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleB, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleA, (int)GLEnum.Red);
|
||||
}
|
||||
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
GL,
|
||||
$"creating texture array {format} {width}x{height}x{size} ({mipLevels} mip levels)");
|
||||
GpuMemoryTracker.TrackAllocation(textureBytes, GpuResourceType.Texture);
|
||||
textureBytesTracked = true;
|
||||
|
||||
if (_device.HasBindless && _device.BindlessExtension != null) {
|
||||
wrapHandle = _device.BindlessExtension.GetTextureSamplerHandle(textureName, _device.WrapSampler);
|
||||
clampHandle = _device.BindlessExtension.GetTextureSamplerHandle(textureName, _device.ClampSampler);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(wrapHandle);
|
||||
wrapResident = true;
|
||||
_device.BindlessExtension.MakeTextureHandleResident(clampHandle);
|
||||
clampResident = true;
|
||||
GLHelpers.ThrowOnResourceError(GL, "making texture-array sampler handles resident");
|
||||
}
|
||||
|
||||
NativePtr = (nint)textureName;
|
||||
BindlessWrapHandle = wrapHandle;
|
||||
BindlessClampHandle = clampHandle;
|
||||
}
|
||||
catch (Exception constructionFailure) {
|
||||
// Constructor failure cannot use Dispose: the object was never
|
||||
// published and queued teardown would make retries accumulate
|
||||
// invalid resident handles. Attempt every independent cleanup.
|
||||
List<Exception>? cleanupFailures = null;
|
||||
void Attempt(Action cleanup) {
|
||||
try { cleanup(); }
|
||||
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
|
||||
}
|
||||
if (_device.BindlessExtension != null) {
|
||||
if (clampResident)
|
||||
Attempt(() => {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(clampHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back clamp texture-array handle");
|
||||
clampResident = false;
|
||||
});
|
||||
if (wrapResident)
|
||||
Attempt(() => {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(wrapHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back wrap texture-array handle");
|
||||
wrapResident = false;
|
||||
});
|
||||
}
|
||||
// Deleting a texture while either bindless sampler handle is
|
||||
// still resident is undefined. A pre-commit residency failure
|
||||
// therefore retains the texture instead of risking a driver
|
||||
// reset during constructor rollback.
|
||||
if (textureName != 0 && !clampResident && !wrapResident)
|
||||
Attempt(() => {
|
||||
GL.DeleteTexture(textureName);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back texture array");
|
||||
if (textureBytesTracked)
|
||||
GpuMemoryTracker.TrackDeallocation(textureBytes, GpuResourceType.Texture);
|
||||
if (textureTracked)
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
});
|
||||
if (cleanupFailures is not null) {
|
||||
cleanupFailures.Insert(0, constructionFailure);
|
||||
throw new AggregateException(
|
||||
"Texture-array construction and rollback both failed.",
|
||||
cleanupFailures);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
finally {
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public long CalculateTotalSize() {
|
||||
int maxDimension = Math.Max(Width, Height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
long layerSize = GetExpectedDataSize();
|
||||
long totalSize = 0;
|
||||
|
||||
for (int i = 0; i < mipLevels; i++) {
|
||||
int w = Math.Max(1, Width >> i);
|
||||
int h = Math.Max(1, Height >> i);
|
||||
if (_isCompressed) {
|
||||
totalSize += TextureHelpers.GetCompressedLayerSize(w, h, Format) * Size;
|
||||
}
|
||||
else {
|
||||
totalSize += (long)w * h * (layerSize / (Width * Height)) * Size;
|
||||
}
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
private static bool IsCompressedFormat(TextureFormat format) {
|
||||
return format == TextureFormat.DXT1 ||
|
||||
format == TextureFormat.DXT3 ||
|
||||
format == TextureFormat.DXT5;
|
||||
}
|
||||
|
||||
public void Bind(int slot = 0) {
|
||||
if (NativePtr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
||||
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture(targetTextureUnit);
|
||||
}
|
||||
|
||||
GL.BindSampler((uint)slot, 0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(byte[] data) {
|
||||
return AddLayer(data, null, null);
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
||||
for (int i = 0; i < _usedLayers.Length; i++) {
|
||||
if (!_usedLayers[i]) {
|
||||
UpdateLayerInternal(i, data, uploadPixelFormat, uploadPixelType);
|
||||
_usedLayers[i] = true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No free layers available in texture array (Slot={Slot}, Size={Width}x{Height}x{Size}).");
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(Span<byte> data) {
|
||||
return AddLayer(data.ToArray());
|
||||
}
|
||||
|
||||
public void UpdateLayer(int layer, byte[] data) {
|
||||
UpdateLayer(layer, data, null, null);
|
||||
}
|
||||
|
||||
public void UpdateLayer(int layer, byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
||||
UpdateLayerInternal(layer, data, uploadPixelFormat, uploadPixelType);
|
||||
_usedLayers[layer] = true;
|
||||
}
|
||||
|
||||
private unsafe void UpdateLayerInternal(int layer, byte[] data, PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType) {
|
||||
if (NativePtr == 0) {
|
||||
throw new InvalidOperationException("Texture array not created.");
|
||||
}
|
||||
|
||||
if (layer < 0 || layer >= Size) {
|
||||
throw new ArgumentOutOfRangeException(nameof(layer),
|
||||
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
||||
}
|
||||
|
||||
ValidateUploadPayload(
|
||||
Format,
|
||||
Width,
|
||||
Height,
|
||||
data.Length,
|
||||
uploadPixelFormat,
|
||||
uploadPixelType);
|
||||
|
||||
lock (_mipmapLock) {
|
||||
// Retain the immutable decoded payload until the once-per-frame
|
||||
// atlas flush. The former per-atlas PBO permanently reserved
|
||||
// several MiB for every array and duplicated each upload
|
||||
// through BufferSubData before TexSubImage3D.
|
||||
var update = new TextureLayerUpdate {
|
||||
Layer = layer,
|
||||
Data = data,
|
||||
UploadPixelFormat = uploadPixelFormat,
|
||||
UploadPixelType = uploadPixelType
|
||||
};
|
||||
int existingIndex = _pendingUpdates.FindLastIndex(pending => pending.Layer == layer);
|
||||
if (existingIndex >= 0)
|
||||
_pendingUpdates[existingIndex] = update;
|
||||
else
|
||||
_pendingUpdates.Add(update);
|
||||
|
||||
_needsMipmapRegeneration = true;
|
||||
if (existingIndex < 0)
|
||||
_mipmapDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public long ProcessDirtyUpdates() {
|
||||
lock (_mipmapLock) {
|
||||
return ProcessDirtyUpdatesInternal(generateMipmaps: true);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe long ProcessDirtyUpdatesInternal(bool generateMipmaps) {
|
||||
if (_pendingUpdates.Count == 0
|
||||
&& (!generateMipmaps || !_needsMipmapRegeneration)) return 0;
|
||||
|
||||
long generatedBytes = 0;
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// This runs in WbMeshAdapter.Tick before any draw pass. Establish
|
||||
// the upload phase's canonical texture state directly instead of
|
||||
// synchronously querying driver state for every dirty array.
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
|
||||
bool mipmapWorkCompleted = false;
|
||||
try {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
// A non-zero pixel-unpack binding changes pointer arguments
|
||||
// into byte offsets. Direct client-memory uploads therefore
|
||||
// establish the canonical zero binding once for the batch.
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
|
||||
|
||||
foreach (var update in _pendingUpdates) {
|
||||
fixed (byte* data = update.Data) {
|
||||
if (_isCompressed) {
|
||||
var internalFormat = Format.ToCompressedGL();
|
||||
GL.CompressedTexSubImage3D(
|
||||
GLEnum.Texture2DArray,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
update.Layer,
|
||||
(uint)Width,
|
||||
(uint)Height,
|
||||
1,
|
||||
internalFormat,
|
||||
(uint)update.Data.Length,
|
||||
data);
|
||||
}
|
||||
else {
|
||||
var pixelFormat = update.UploadPixelFormat ?? Format.ToPixelFormat();
|
||||
var pixelType = update.UploadPixelType ?? Format.ToPixelType();
|
||||
GL.TexSubImage3D(
|
||||
GLEnum.Texture2DArray,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
update.Layer,
|
||||
(uint)Width,
|
||||
(uint)Height,
|
||||
1,
|
||||
pixelFormat,
|
||||
pixelType,
|
||||
data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (generateMipmaps && _needsMipmapRegeneration && _mipmapDirtyCount > 0) {
|
||||
if (_isCompressed) {
|
||||
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// Width, height and format were validated when the
|
||||
// immutable storage was allocated. Re-reading them
|
||||
// here forced three CPU/GPU synchronization points
|
||||
// for every dirty atlas without adding safety.
|
||||
GL.GenerateMipmap(GLEnum.Texture2DArray);
|
||||
generatedBytes = TotalSizeInBytes;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogWarning(ex, "Failed to generate mipmaps for texture array (Slot={Slot}); retaining upload state for retry.", Slot);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release builds must observe transfer/OOM/context errors
|
||||
// before the pending offsets and dirty mip state are cleared.
|
||||
// One check covers every layer in this array plus its single
|
||||
// mip generation, keeping the synchronization cost bounded by
|
||||
// dirty arrays rather than uploaded textures.
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
GL,
|
||||
$"committing texture-array updates (Slot={Slot}, Layers={_pendingUpdates.Count})");
|
||||
mipmapWorkCompleted = generateMipmaps
|
||||
&& _needsMipmapRegeneration
|
||||
&& _mipmapDirtyCount > 0;
|
||||
}
|
||||
finally {
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
}
|
||||
|
||||
// Commit CPU-side completion only after glGetError confirms the
|
||||
// uploads/mipmap work succeeded. If the driver rejects an
|
||||
// operation, the retained payloads and dirty flags remain intact and the
|
||||
// atlas stays in ObjectMeshManager's dirty set for a later retry.
|
||||
_pendingUpdates.Clear();
|
||||
if (mipmapWorkCompleted) {
|
||||
_mipmapDirtyCount = 0;
|
||||
_needsMipmapRegeneration = false;
|
||||
}
|
||||
return generatedBytes;
|
||||
}
|
||||
|
||||
private void ClearLayerForMipmap(int layer) {
|
||||
// Upload a single black/transparent pixel to make layer defined
|
||||
byte[] clearData = new byte[GetExpectedDataSize()];
|
||||
Array.Clear(clearData, 0, clearData.Length); // Zero-fill (black/transparent)
|
||||
UpdateLayerInternal(layer, clearData, null, null);
|
||||
}
|
||||
|
||||
private int GetExpectedDataSize() {
|
||||
return CalculateExpectedDataSize(Format, Width, Height);
|
||||
}
|
||||
|
||||
internal static int CalculateExpectedDataSize(TextureFormat format, int width, int height) {
|
||||
if (IsCompressedFormat(format))
|
||||
return TextureHelpers.GetCompressedLayerSize(width, height, format);
|
||||
|
||||
return format switch {
|
||||
TextureFormat.RGBA8 => checked(width * height * 4),
|
||||
TextureFormat.RGB8 => checked(width * height * 3),
|
||||
TextureFormat.A8 => checked(width * height),
|
||||
TextureFormat.Rgba32f => checked(width * height * 16),
|
||||
_ => throw new NotSupportedException($"Unsupported format {format}")
|
||||
};
|
||||
}
|
||||
|
||||
internal static void ValidateUploadPayload(
|
||||
TextureFormat format,
|
||||
int width,
|
||||
int height,
|
||||
int dataLength,
|
||||
PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType) {
|
||||
int expectedBytes = CalculateExpectedDataSize(format, width, height);
|
||||
if (dataLength != expectedBytes) {
|
||||
throw new ArgumentException(
|
||||
$"Texture-array layer payload has {dataLength} bytes; expected exactly {expectedBytes} "
|
||||
+ $"for {format} {width}x{height}.",
|
||||
nameof(dataLength));
|
||||
}
|
||||
|
||||
if (IsCompressedFormat(format)) {
|
||||
if (uploadPixelFormat.HasValue || uploadPixelType.HasValue)
|
||||
throw new ArgumentException("Compressed texture uploads cannot specify pixel format/type overrides.");
|
||||
return;
|
||||
}
|
||||
|
||||
PixelFormat expectedFormat = format.ToPixelFormat();
|
||||
PixelType expectedType = format.ToPixelType();
|
||||
if ((uploadPixelFormat ?? expectedFormat) != expectedFormat
|
||||
|| (uploadPixelType ?? expectedType) != expectedType) {
|
||||
throw new ArgumentException(
|
||||
$"Upload descriptor {uploadPixelFormat}/{uploadPixelType} does not match "
|
||||
+ $"the {expectedFormat}/{expectedType} transfer required by {format}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveLayer(int layer) {
|
||||
if (layer < 0 || layer >= Size) {
|
||||
throw new ArgumentOutOfRangeException(nameof(layer),
|
||||
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
||||
}
|
||||
|
||||
if (!_usedLayers[layer]) {
|
||||
throw new InvalidOperationException($"Layer {layer} is already free (Slot={Slot}).");
|
||||
}
|
||||
|
||||
_usedLayers[layer] = false;
|
||||
|
||||
// An unreferenced layer needs no clear or whole-array mip
|
||||
// regeneration before AddTexture overwrites it on reuse.
|
||||
}
|
||||
|
||||
public bool IsLayerUsed(int layer) {
|
||||
if (layer < 0 || layer >= Size) return false;
|
||||
return _usedLayers[layer];
|
||||
}
|
||||
|
||||
public int GetUsedLayerCount() {
|
||||
return _usedLayers.Count(x => x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True once disposal is durably owned by a queued GL publication,
|
||||
/// the frame-retirement queue, or a completed retained release. A
|
||||
/// caller may only commit its own logical disposal after this becomes
|
||||
/// true; otherwise a synchronous enqueue failure still needs retry.
|
||||
/// </summary>
|
||||
internal bool HasDurableDisposeOwnership {
|
||||
get {
|
||||
if (Volatile.Read(ref _disposeQueued) == 0)
|
||||
return false;
|
||||
return Volatile.Read(ref _disposePublicationQueued) != 0
|
||||
|| Volatile.Read(ref _disposeRetirementAccepted) != 0
|
||||
|| Volatile.Read(ref _disposeRelease) is null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True only after every retained bindless-handle, GL-name, and memory
|
||||
/// accounting release stage has completed. Logical disposal can become
|
||||
/// durable earlier while the frame fence still owns the physical array.
|
||||
/// </summary>
|
||||
internal bool IsPhysicalRetirementComplete =>
|
||||
Volatile.Read(ref _disposeQueued) != 0
|
||||
&& Volatile.Read(ref _disposeRelease) is null;
|
||||
|
||||
bool IWorldTextureArray.HasDurableDisposeOwnership => HasDurableDisposeOwnership;
|
||||
|
||||
bool IWorldTextureArray.IsPhysicalRetirementComplete => IsPhysicalRetirementComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: this array's device-table slot for the
|
||||
/// requested address mode.
|
||||
///
|
||||
/// <para>The interning call is the one <c>ObjectMeshManager</c> made
|
||||
/// itself before this slice, moved one level down so the caller can be
|
||||
/// written against <see cref="IWorldTextureArray"/> instead of against a
|
||||
/// 64-bit <c>ARB_bindless_texture</c> handle that has no Vulkan
|
||||
/// spelling. It is idempotent by handle, which is why it stays a per-batch
|
||||
/// call rather than becoming cached state — exactly as before.</para>
|
||||
/// </summary>
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot IWorldTextureArray.ResolveSlot(bool wrapping) {
|
||||
if (_worldTextureTable is null)
|
||||
return AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
||||
ulong handle = wrapping ? _retiredWrapHandle : _retiredClampHandle;
|
||||
if (handle == 0)
|
||||
handle = wrapping ? BindlessWrapHandle : BindlessClampHandle;
|
||||
return _worldTextureTable.RegisterWorldTextureHandle(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires both table entries. <see cref="Dispose"/> zeroes the public
|
||||
/// handle properties, so the values are captured there and read from the
|
||||
/// captures here — this is called after physical retirement completes,
|
||||
/// which is necessarily after Dispose.
|
||||
/// </summary>
|
||||
public void ReleaseTextureSlots() {
|
||||
if (_worldTextureTable is null)
|
||||
return;
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(_retiredWrapHandle);
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(_retiredClampHandle);
|
||||
_retiredWrapHandle = 0;
|
||||
_retiredClampHandle = 0;
|
||||
}
|
||||
|
||||
private ulong _retiredWrapHandle;
|
||||
private ulong _retiredClampHandle;
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void GenerateMipmaps() {
|
||||
_needsMipmapRegeneration = true;
|
||||
lock (_mipmapLock) {
|
||||
_mipmapDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (Interlocked.CompareExchange(ref _disposeQueued, 1, 0) != 0) {
|
||||
ScheduleDisposeRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
uint textureName = (uint)NativePtr;
|
||||
ulong bindlessWrapHandle = BindlessWrapHandle;
|
||||
ulong bindlessClampHandle = BindlessClampHandle;
|
||||
long textureBytes = CalculateTotalSize();
|
||||
|
||||
// Slice V6i-2: the handles the two table entries are keyed by. The
|
||||
// properties are zeroed below, so ReleaseTextureSlots — which runs
|
||||
// only once physical retirement completes — reads these captures.
|
||||
_retiredWrapHandle = bindlessWrapHandle;
|
||||
_retiredClampHandle = bindlessClampHandle;
|
||||
|
||||
NativePtr = 0;
|
||||
BindlessWrapHandle = 0;
|
||||
BindlessClampHandle = 0;
|
||||
|
||||
_disposeRelease = new RetryableGpuResourceRelease(
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessWrapHandle != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing wrap texture-array handle (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessWrapHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(bindlessWrapHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing wrap texture-array handle");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessClampHandle != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing clamp texture-array handle (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessClampHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(bindlessClampHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing clamp texture-array handle");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName} (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0) {
|
||||
GL.DeleteTexture(textureName);
|
||||
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName}");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(textureBytes, GpuResourceType.Texture);
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
},
|
||||
() => _disposeRelease = null);
|
||||
|
||||
ScheduleDisposeRelease();
|
||||
}
|
||||
|
||||
private void ScheduleDisposeRelease(bool forNextPass = false) {
|
||||
RetryableGpuResourceRelease? release = _disposeRelease;
|
||||
if (release is null || release.IsComplete || Volatile.Read(ref _disposeRetirementAccepted) != 0)
|
||||
return;
|
||||
if (Interlocked.CompareExchange(ref _disposePublicationQueued, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
try {
|
||||
Action<GL> publish = GL => {
|
||||
Volatile.Write(ref _disposePublicationQueued, 0);
|
||||
try {
|
||||
_device.RetireGpuResource(release.Run);
|
||||
Volatile.Write(ref _disposeRetirementAccepted, 1);
|
||||
}
|
||||
catch {
|
||||
// Retire may fail before accepting the callback, or an
|
||||
// immediate queue may surface a partial release. The
|
||||
// release cursor makes this next-pass retry exact.
|
||||
ScheduleDisposeRelease(forNextPass: true);
|
||||
throw;
|
||||
}
|
||||
};
|
||||
if (forNextPass)
|
||||
_device.QueueGLActionForNextPass(publish);
|
||||
else
|
||||
_device.QueueGLAction(publish);
|
||||
}
|
||||
catch {
|
||||
Volatile.Write(ref _disposePublicationQueued, 0);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Runtime.InteropServices;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
|
||||
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL uniform buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLUniformBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="device">Graphics device</param>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
||||
uint buffer = 0;
|
||||
bool allocated = false;
|
||||
try {
|
||||
buffer = TrackedGlResource.CreateBuffer(
|
||||
GL,
|
||||
"creating managed uniform buffer");
|
||||
uint ownedBuffer = buffer;
|
||||
RetryableGpuResourceRelease unpublishedBufferRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
GL,
|
||||
ownedBuffer,
|
||||
() => allocated ? Size : 0,
|
||||
"rolling back managed uniform buffer");
|
||||
resources.Add(
|
||||
"managed uniform buffer",
|
||||
unpublishedBufferRelease.Run);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
GL,
|
||||
GLEnum.UniformBuffer,
|
||||
buffer,
|
||||
0,
|
||||
Size,
|
||||
GLEnum.DynamicDraw,
|
||||
"allocating managed uniform buffer");
|
||||
allocated = true;
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"ManagedGLUniformBuffer construction failed and its GL buffer did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
bufferId = buffer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(T[] data) where T : unmanaged {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(Span<T> data) where T : unmanaged {
|
||||
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
fixed (T* ptr = data) {
|
||||
GL.BufferSubData(GLEnum.UniformBuffer, 0, (nuint)dataSize, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
|
||||
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Validate buffer bounds
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
fixed (T* ptr = data.Slice(sourceOffsetElements, lengthElements)) {
|
||||
GL.BufferSubData(GLEnum.UniformBuffer, (nint)destinationOffsetBytes, (nuint)dataSizeBytes, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a single piece of data in the buffer.
|
||||
/// </summary>
|
||||
public unsafe void SetData<T>(ref T data) where T : unmanaged {
|
||||
fixed (T* pData = &data) {
|
||||
SetData(new Span<T>(pData, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the buffer to the specified binding point.
|
||||
/// </summary>
|
||||
/// <param name="bindingPoint">The binding point to bind to</param>
|
||||
public void Bind(uint bindingPoint) {
|
||||
GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Bind() {
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases an unpublished constructor-owned buffer synchronously on
|
||||
/// the GL thread. This is deliberately separate from ordinary queued
|
||||
/// disposal so an enclosing constructor can prove rollback before it
|
||||
/// propagates its failure.
|
||||
/// </summary>
|
||||
internal void DisposeImmediately() {
|
||||
if (bufferId == 0)
|
||||
return;
|
||||
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
GL,
|
||||
bufferId,
|
||||
Size,
|
||||
"rolling back unpublished managed uniform buffer");
|
||||
release.Run();
|
||||
bufferId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VertexAttribType = Silk.NET.OpenGL.VertexAttribType;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public unsafe class ManagedGLVertexArray : IVertexArray {
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL GL => _device.GL;
|
||||
private uint _vaoId = 0;
|
||||
|
||||
public ManagedGLVertexArray(OpenGLGraphicsDevice device, IVertexBuffer buffer, VertexFormat format) {
|
||||
_device = device;
|
||||
|
||||
// Generate the vertex array
|
||||
_vaoId = GL.GenVertexArray();
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_vaoId == 0) {
|
||||
throw new Exception("Failed to generate vertex array.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
|
||||
|
||||
SetVertexBuffer(buffer, format);
|
||||
}
|
||||
|
||||
public void SetVertexBuffer(IVertexBuffer buffer, VertexFormat format) {
|
||||
GL.BindVertexArray(_vaoId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
buffer.Bind();
|
||||
for (int i = 0; i < format.Attributes.Length; i++) {
|
||||
var attr = format.Attributes[i];
|
||||
GL.EnableVertexAttribArray((uint)i);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.VertexAttribPointer((uint)i, attr.Size, Convert(attr.Type), attr.Normalized, (uint)format.Stride, attr.Offset);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
GL.BindVertexArray(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
private GLEnum Convert(Chorizite.Core.Render.Enums.VertexAttribType type) => type switch {
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Float => GLEnum.Float,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Int => GLEnum.Int,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedInt => GLEnum.UnsignedInt,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedByte => GLEnum.UnsignedByte,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Byte => GLEnum.Byte,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
public void Bind() {
|
||||
GL.BindVertexArray(_vaoId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindVertexArray(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_vaoId != 0) {
|
||||
GL.DeleteVertexArray(_vaoId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
|
||||
_vaoId = 0;
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Buffers;
|
||||
using System.Runtime.InteropServices;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL vertex buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private void* _mappedPtr;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLVertexBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLVertexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device;
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
if (bufferId == 0) {
|
||||
throw new Exception("Failed to generate vertex buffer.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Allocate the buffer with the specified size but no initial data
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_device.HasBufferStorage) {
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ArrayBuffer, (uint)Size, (void*)0, flags);
|
||||
_mappedPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(uint)Size,
|
||||
(void*)0, // No initial data
|
||||
Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(T[] data) where T : IVertex {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(Span<T> data) where T : IVertex {
|
||||
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<T> mappedSpan = new Span<T>(_mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Map the buffer for writing
|
||||
void* mappedPtr = GL.MapBufferRange(
|
||||
GLEnum.ArrayBuffer,
|
||||
0, // offset
|
||||
dataSize,
|
||||
MapBufferAccessMask.WriteBit | MapBufferAccessMask.InvalidateBufferBit // Overwrite entire buffer
|
||||
);
|
||||
|
||||
if (mappedPtr == null) {
|
||||
throw new Exception("Failed to map buffer for writing.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Copy data directly to mapped memory
|
||||
Span<T> mappedSpan = new Span<T>(mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
}
|
||||
finally {
|
||||
// Unmap the buffer
|
||||
GL.UnmapBuffer(GLEnum.ArrayBuffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
|
||||
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Validate buffer bounds
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<T> mappedSpan = new Span<T>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Map the specific range of the buffer
|
||||
void* mappedPtr = GL.MapBufferRange(
|
||||
GLEnum.ArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
MapBufferAccessMask.WriteBit // Write access for partial update
|
||||
);
|
||||
|
||||
if (mappedPtr == null) {
|
||||
throw new Exception("Failed to map buffer for writing.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Copy the specified range of data to the mapped memory
|
||||
Span<T> mappedSpan = new Span<T>(mappedPtr, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
}
|
||||
finally {
|
||||
// Unmap the buffer
|
||||
GL.UnmapBuffer(GLEnum.ArrayBuffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind() {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
_mappedPtr = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using DatReaderWriter.Enums;
|
||||
using Chorizite.Core.Render;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
|
|
@ -19,17 +17,4 @@ namespace AcDream.App.Rendering.Wb {
|
|||
public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
|
||||
}
|
||||
|
||||
public struct LandblockMdiCommand {
|
||||
public ulong SortKey;
|
||||
public ulong ObjectId;
|
||||
public DrawElementsIndirectCommand Command;
|
||||
public ModernBatchData BatchData;
|
||||
public uint TextureIndex;
|
||||
public ManagedGLTextureArray Atlas;
|
||||
public uint VAO;
|
||||
public uint IBO;
|
||||
public bool IsTransparent;
|
||||
public bool IsAdditive;
|
||||
public bool HasWrappingUVs;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,17 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
public class ObjectRenderData
|
||||
{
|
||||
/// <summary>
|
||||
/// Campaign V slice V11 deleted the per-mesh raw-GL vertex array/buffer
|
||||
/// this used to carry — Vulkan bakes vertex input into the pipeline and
|
||||
/// the shared <see cref="GlobalMeshBuffer"/> arena has no VAO/VBO
|
||||
/// concept at all (see <see cref="GlobalMeshBuffer.VertexStore"/>). This
|
||||
/// is always 0 now; it survives only because
|
||||
/// <c>WbDrawDispatcher.cs</c>'s legacy (non-RHI) dispatcher still reads
|
||||
/// it into its own dead <c>anyVao</c> bookkeeping.
|
||||
/// </summary>
|
||||
public uint VAO { get; set; }
|
||||
/// <summary>See <see cref="VAO"/> — always 0 for the same reason.</summary>
|
||||
public uint VBO { get; set; }
|
||||
public int VertexCount { get; set; }
|
||||
public List<ObjectRenderBatch> Batches { get; set; } = new();
|
||||
|
|
@ -76,6 +86,11 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
public class ObjectRenderBatch
|
||||
{
|
||||
/// <summary>See <see cref="ObjectRenderData.VAO"/> — Campaign V slice
|
||||
/// V11 deleted the legacy per-batch raw-GL index buffer this used to
|
||||
/// carry. Always 0 now; every batch's actual index range lives in the
|
||||
/// shared arena via <see cref="FirstIndex"/>/<see cref="BaseVertex"/>.
|
||||
/// </summary>
|
||||
public uint IBO { get; set; }
|
||||
public int IndexCount { get; set; }
|
||||
public TextureAtlasManager Atlas { get; set; } = null!;
|
||||
|
|
@ -124,26 +139,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
private readonly IMeshPipelineDevice _graphicsDevice;
|
||||
|
||||
/// <summary>
|
||||
/// The GL context the LEGACY (pre-modern-path) upload bodies write
|
||||
/// through.
|
||||
///
|
||||
/// <para>Campaign V slice V6i-3 narrowed what still needs it. The modern
|
||||
/// path's arena upload is <see cref="GlobalMeshBuffer"/>'s, and that is
|
||||
/// now <see cref="AcDream.App.Rendering.Gpu.IGpuBuffer"/> work on both
|
||||
/// arms; what remains raw is the per-mesh VAO/VBO/IBO construction the
|
||||
/// N.5 ship amendment made unreachable — missing bindless or
|
||||
/// draw-parameters throws at startup, so <c>_useModernRendering</c> is
|
||||
/// true in every shipping configuration. The accessor therefore survives
|
||||
/// as the guard on genuinely dead code rather than as a blocker, and it
|
||||
/// is deleted with that code.</para>
|
||||
/// </summary>
|
||||
private GL RequireGl() =>
|
||||
_graphicsDevice.Gl
|
||||
?? throw new InvalidOperationException(
|
||||
"The mesh pipeline's legacy per-mesh vertex-array upload is raw GL and this "
|
||||
+ "device has no context. The modern path is mandatory (N.5 ship amendment), "
|
||||
+ "so reaching this is a composition error rather than a backend gap.");
|
||||
private readonly IPreparedAssetSource _preparedAssets;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
|
@ -158,20 +153,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
private readonly AcDream.App.Rendering.Gpu.IGpuDevice _gpuDevice;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the downcast moved here from the constructor.
|
||||
/// Only the raw-GL world renderers reach this — the Vulkan backend binds
|
||||
/// set 2 and never touches the handle table — so a Vulkan-composed mesh
|
||||
/// pipeline can now be CONSTRUCTED, and only a caller that genuinely
|
||||
/// needs a GL handle table fails, naming why.
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
_gpuDevice as AcDream.App.Rendering.Gpu.Gl.GlGpuDevice
|
||||
?? throw new InvalidOperationException(
|
||||
"The GL bindless handle table was requested from a mesh pipeline composed against "
|
||||
+ $"the {_gpuDevice.Backend} backend. It is GL-only emulation of the Vulkan texture "
|
||||
+ "table and is deleted with the raw-GL world path.");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: how a shared atlas's physical array is made.
|
||||
/// Composed once; see <see cref="IWorldTextureArrayFactory"/>.
|
||||
|
|
@ -339,7 +320,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
public GlobalMeshBuffer? GlobalBuffer { get; }
|
||||
private readonly bool _useModernRendering;
|
||||
internal (int RenderData, int AtlasArrays, int UnusedLru, long EstimatedBytes) Diagnostics
|
||||
{
|
||||
get
|
||||
|
|
@ -527,11 +507,16 @@ namespace AcDream.App.Rendering.Wb
|
|||
_stagedMeshData = new MeshUploadStagingQueue(
|
||||
budgets.MeshStagingEntries,
|
||||
budgets.MeshStagingBytes);
|
||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||
if (_useModernRendering)
|
||||
// The modern path is mandatory (N.5 ship amendment) and Campaign V
|
||||
// slice V11 deleted the only other backend, so a production
|
||||
// IMeshPipelineDevice always reports both flags true. The gate
|
||||
// survives because AcDream.App.Tests.Rendering.Wb.
|
||||
// MeshPipelineDeviceSeamTests exercises a device that reports
|
||||
// neither, to prove the arena is genuinely optional rather than
|
||||
// dereferenced unconditionally.
|
||||
if (_graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless)
|
||||
{
|
||||
GlobalBuffer = new GlobalMeshBuffer(
|
||||
_graphicsDevice.Gl,
|
||||
gpuDevice,
|
||||
_graphicsDevice.ResourceRetirement);
|
||||
}
|
||||
|
|
@ -680,7 +665,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
/// <summary>
|
||||
/// #105 diagnostic: counts staged-but-unflushed texture layer updates across all
|
||||
/// shared atlases (see <see cref="ManagedGLTextureArray.PendingUpdateCount"/>).
|
||||
/// shared atlases (see <see cref="IWorldTextureArray.PendingUpdateCount"/>).
|
||||
/// Render thread only — <c>_globalAtlases</c> is render-thread-owned.
|
||||
/// </summary>
|
||||
public (int PendingUpdates, int ArraysWithPending, int TotalArrays) GetPendingTextureUpdateStats()
|
||||
|
|
@ -901,7 +886,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
private long GetReclaimableBytes(ObjectRenderData data)
|
||||
{
|
||||
if (_useModernRendering && data.GlobalAllocation is { } allocation)
|
||||
if (data.GlobalAllocation is { } allocation)
|
||||
{
|
||||
return checked(
|
||||
(long)allocation.Vertices.Length * VertexPositionNormalTexture.Size
|
||||
|
|
@ -1989,16 +1974,10 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
#region Private: GPU Upload
|
||||
|
||||
private unsafe ObjectRenderData? UploadGfxObjMeshData(ObjectMeshData meshData)
|
||||
private ObjectRenderData? UploadGfxObjMeshData(ObjectMeshData meshData)
|
||||
{
|
||||
if (meshData.Vertices.Length == 0) return null;
|
||||
|
||||
// Resolved lazily since Campaign V slice V6i-3: every reader below
|
||||
// is inside a !_useModernRendering branch, and the modern path is
|
||||
// mandatory, so a backend with no GL context uploads meshes here
|
||||
// without ever asking for one.
|
||||
GL? gl = _graphicsDevice.Gl;
|
||||
uint vao = 0, vbo = 0;
|
||||
var modernIndexBatches = meshData.TextureBatches.Values
|
||||
.SelectMany(batches => batches)
|
||||
.Where(batch => batch.Indices.Count != 0)
|
||||
|
|
@ -2007,62 +1986,23 @@ namespace AcDream.App.Rendering.Wb
|
|||
GlobalMeshAllocation? globalAllocation = null;
|
||||
var renderBatches = new List<ObjectRenderBatch>();
|
||||
var acquiredTextures = new List<(TextureAtlasManager Atlas, TextureKey Key)>();
|
||||
var legacyIndexBuffers = new List<(uint Name, int Bytes)>();
|
||||
|
||||
try
|
||||
{
|
||||
if (_useModernRendering)
|
||||
{
|
||||
// One mesh owns one vertex range and one contiguous index
|
||||
// range. The former append path duplicated the full vertex
|
||||
// array per material and never reclaimed evicted ranges.
|
||||
vao = GlobalBuffer!.VAO;
|
||||
vbo = GlobalBuffer!.VBO;
|
||||
}
|
||||
else
|
||||
{
|
||||
GL legacyGl = RequireGl();
|
||||
legacyGl.GenVertexArrays(1, out vao);
|
||||
legacyGl.BindVertexArray(vao);
|
||||
|
||||
legacyGl.GenBuffers(1, out vbo);
|
||||
legacyGl.BindBuffer(GLEnum.ArrayBuffer, vbo);
|
||||
fixed (VertexPositionNormalTexture* ptr = meshData.Vertices)
|
||||
{
|
||||
legacyGl.BufferData(GLEnum.ArrayBuffer, (nuint)(meshData.Vertices.Length * VertexPositionNormalTexture.Size), ptr, GLEnum.StaticDraw);
|
||||
}
|
||||
GpuMemoryTracker.TrackAllocation(meshData.Vertices.Length * VertexPositionNormalTexture.Size, GpuResourceType.Buffer);
|
||||
|
||||
int stride = VertexPositionNormalTexture.Size;
|
||||
// Position (location 0)
|
||||
legacyGl.EnableVertexAttribArray(0);
|
||||
legacyGl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
|
||||
// Normal (location 1)
|
||||
legacyGl.EnableVertexAttribArray(1);
|
||||
legacyGl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
|
||||
// TexCoord (location 2)
|
||||
legacyGl.EnableVertexAttribArray(2);
|
||||
legacyGl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
|
||||
// Instance data (shared VBO)
|
||||
legacyGl.BindBuffer(GLEnum.ArrayBuffer, _graphicsDevice.InstanceVBO);
|
||||
for (uint i = 0; i < 4; i++)
|
||||
{
|
||||
var loc = 3 + i;
|
||||
legacyGl.EnableVertexAttribArray(loc);
|
||||
legacyGl.VertexAttribPointer(loc, 4, GLEnum.Float, false, (uint)sizeof(InstanceData), (void*)(i * 16));
|
||||
legacyGl.VertexAttribDivisor(loc, 1);
|
||||
}
|
||||
legacyGl.EnableVertexAttribArray(8);
|
||||
legacyGl.VertexAttribIPointer(8, 1, GLEnum.UnsignedInt, (uint)sizeof(InstanceData), (void*)64);
|
||||
legacyGl.VertexAttribDivisor(8, 1);
|
||||
}
|
||||
|
||||
// Allocate the shared vertex/index range before acquiring texture
|
||||
// references. A buffer-growth failure therefore leaves every atlas
|
||||
// untouched; later failures still roll this allocation back below.
|
||||
if (_useModernRendering && modernIndexBatches.Length != 0)
|
||||
globalAllocation = GlobalBuffer!.UploadMesh(meshData.Vertices, modernIndexBatches);
|
||||
//
|
||||
// GlobalBuffer is null only for a test double that reports no
|
||||
// modern-path capability (MeshPipelineDeviceSeamTests); every
|
||||
// production IMeshPipelineDevice is Vulkan-backed and reports both
|
||||
// flags true (N.5 ship amendment; Campaign V slice V11 deleted the
|
||||
// only other backend). There is no longer a per-mesh vertex array
|
||||
// or vertex/index buffer to build here — Vulkan bakes vertex input
|
||||
// into the pipeline, and the shared arena's stores are bound once
|
||||
// per pass (see WbDrawDispatcher.Rhi.cs's BindPipelineWithMesh).
|
||||
if (GlobalBuffer is not null && modernIndexBatches.Length != 0)
|
||||
globalAllocation = GlobalBuffer.UploadMesh(meshData.Vertices, modernIndexBatches);
|
||||
|
||||
foreach (var (format, batches) in meshData.TextureBatches)
|
||||
{
|
||||
|
|
@ -2070,7 +2010,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
if (batch.Indices.Count == 0) continue;
|
||||
|
||||
uint ibo = 0;
|
||||
TextureAtlasManager? atlasManager = null;
|
||||
int textureIndex = 0;
|
||||
uint firstIndex = 0;
|
||||
|
|
@ -2128,24 +2067,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
if (uploadsNewLayer)
|
||||
_dirtyAtlases.Add(atlasManager);
|
||||
|
||||
if (_useModernRendering)
|
||||
{
|
||||
ibo = GlobalBuffer!.IBO;
|
||||
}
|
||||
else
|
||||
{
|
||||
GL legacyGl = RequireGl();
|
||||
legacyGl.GenBuffers(1, out ibo);
|
||||
legacyGl.BindBuffer(GLEnum.ElementArrayBuffer, ibo);
|
||||
var indexArray = batch.Indices.ToArray();
|
||||
fixed (ushort* iptr = indexArray)
|
||||
{
|
||||
legacyGl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(indexArray.Length * sizeof(ushort)), iptr, GLEnum.StaticDraw);
|
||||
}
|
||||
GpuMemoryTracker.TrackAllocation(indexArray.Length * sizeof(ushort), GpuResourceType.Buffer);
|
||||
legacyIndexBuffers.Add((ibo, indexArray.Length * sizeof(ushort)));
|
||||
}
|
||||
|
||||
// Campaign V slice V4t interned the atlas's resident
|
||||
// handle into the device's one texture table here and
|
||||
// carried the slot. Slice V6i-2 asks the array for the
|
||||
|
|
@ -2159,7 +2080,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
renderBatches.Add(new ObjectRenderBatch
|
||||
{
|
||||
IBO = ibo,
|
||||
IndexCount = batch.Indices.Count,
|
||||
Atlas = atlasManager!,
|
||||
TextureIndex = textureIndex,
|
||||
|
|
@ -2178,7 +2098,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
}
|
||||
|
||||
if (_useModernRendering && globalAllocation is not null)
|
||||
if (globalAllocation is not null)
|
||||
{
|
||||
if (renderBatches.Count != globalAllocation.BatchFirstIndices.Count)
|
||||
{
|
||||
|
|
@ -2197,8 +2117,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
+ renderBatches.Sum(b => (long)b.IndexCount * sizeof(ushort)));
|
||||
var renderData = new ObjectRenderData
|
||||
{
|
||||
VAO = vao,
|
||||
VBO = vbo,
|
||||
VertexCount = meshData.Vertices.Length,
|
||||
Batches = renderBatches,
|
||||
GlobalAllocation = globalAllocation,
|
||||
|
|
@ -2209,26 +2127,17 @@ namespace AcDream.App.Rendering.Wb
|
|||
CPUEdgeLines = meshData.EdgeLines,
|
||||
MemorySize = geometryBytes,
|
||||
NonArenaGpuBytes = CalculateNonArenaGeometryBytes(
|
||||
_useModernRendering,
|
||||
GlobalBuffer is not null,
|
||||
geometryBytes),
|
||||
};
|
||||
|
||||
if (!_useModernRendering)
|
||||
{
|
||||
RequireGl().BindVertexArray(0);
|
||||
}
|
||||
return renderData;
|
||||
}
|
||||
catch (Exception uploadFailure)
|
||||
{
|
||||
RetryableResourceReleaseLedger rollback = CreateUploadRollback(
|
||||
meshData,
|
||||
gl,
|
||||
vao,
|
||||
vbo,
|
||||
globalAllocation,
|
||||
acquiredTextures,
|
||||
legacyIndexBuffers);
|
||||
acquiredTextures);
|
||||
ResourceReleaseAttempt attempt = rollback.Advance();
|
||||
if (!rollback.IsComplete)
|
||||
{
|
||||
|
|
@ -2248,13 +2157,8 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
private RetryableResourceReleaseLedger CreateUploadRollback(
|
||||
ObjectMeshData meshData,
|
||||
GL? gl,
|
||||
uint vao,
|
||||
uint vbo,
|
||||
GlobalMeshAllocation? globalAllocation,
|
||||
IReadOnlyList<(TextureAtlasManager Atlas, TextureKey Key)> acquiredTextures,
|
||||
IReadOnlyList<(uint Name, int Bytes)> legacyIndexBuffers)
|
||||
IReadOnlyList<(TextureAtlasManager Atlas, TextureKey Key)> acquiredTextures)
|
||||
{
|
||||
var releases = new List<(string Name, Action Release)>();
|
||||
|
||||
|
|
@ -2282,35 +2186,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
acquiredTextures[releaseIndex].Key)));
|
||||
}
|
||||
|
||||
if (!_useModernRendering)
|
||||
{
|
||||
GL legacyGl = gl ?? RequireGl();
|
||||
for (int i = 0; i < legacyIndexBuffers.Count; i++)
|
||||
{
|
||||
int bufferIndex = i;
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{bufferIndex}-delete",
|
||||
() => legacyGl.DeleteBuffer(legacyIndexBuffers[bufferIndex].Name)));
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{bufferIndex}-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
legacyIndexBuffers[bufferIndex].Bytes,
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
|
||||
if (vbo != 0)
|
||||
{
|
||||
releases.Add(("legacy-vertex-buffer-delete", () => legacyGl.DeleteBuffer(vbo)));
|
||||
releases.Add((
|
||||
"legacy-vertex-buffer-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
meshData.Vertices.Length * VertexPositionNormalTexture.Size,
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
if (vao != 0)
|
||||
releases.Add(("legacy-vertex-array-delete", () => legacyGl.DeleteVertexArray(vao)));
|
||||
}
|
||||
|
||||
return new RetryableResourceReleaseLedger(releases);
|
||||
}
|
||||
|
||||
|
|
@ -2447,48 +2322,14 @@ namespace AcDream.App.Rendering.Wb
|
|||
return null;
|
||||
|
||||
var releases = new List<(string Name, Action Release)>();
|
||||
if (_useModernRendering)
|
||||
if (data.GlobalAllocation is { } allocation)
|
||||
{
|
||||
if (data.GlobalAllocation is { } allocation)
|
||||
{
|
||||
releases.Add((
|
||||
"global-index-range",
|
||||
() => GlobalBuffer!.ReleaseIndexRange(allocation)));
|
||||
releases.Add((
|
||||
"global-vertex-range",
|
||||
() => GlobalBuffer!.ReleaseVertexRange(allocation)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GL gl = RequireGl();
|
||||
if (data.VAO != 0)
|
||||
releases.Add(("legacy-vertex-array-delete", () => gl.DeleteVertexArray(data.VAO)));
|
||||
if (data.VBO != 0)
|
||||
{
|
||||
releases.Add(("legacy-vertex-buffer-delete", () => gl.DeleteBuffer(data.VBO)));
|
||||
releases.Add((
|
||||
"legacy-vertex-buffer-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
data.VertexCount * VertexPositionNormalTexture.Size,
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.Batches.Count; i++)
|
||||
{
|
||||
int batchIndex = i;
|
||||
ObjectRenderBatch batch = data.Batches[batchIndex];
|
||||
if (batch.IBO == 0)
|
||||
continue;
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{batchIndex}-delete",
|
||||
() => gl.DeleteBuffer(data.Batches[batchIndex].IBO)));
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{batchIndex}-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
data.Batches[batchIndex].IndexCount * sizeof(ushort),
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
releases.Add((
|
||||
"global-index-range",
|
||||
() => GlobalBuffer!.ReleaseIndexRange(allocation)));
|
||||
releases.Add((
|
||||
"global-vertex-range",
|
||||
() => GlobalBuffer!.ReleaseVertexRange(allocation)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.Batches.Count; i++)
|
||||
|
|
@ -2841,7 +2682,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
"One or more texture atlases could not be disposed.",
|
||||
failures!);
|
||||
|
||||
if (_useModernRendering && GlobalBuffer is not null)
|
||||
if (GlobalBuffer is not null)
|
||||
Capture(ref failures, GlobalBuffer.Dispose);
|
||||
|
||||
if (failures is not null)
|
||||
|
|
|
|||
|
|
@ -1,776 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using AcDream.App.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
|
||||
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
|
||||
using Silk.NET.OpenGL.Extensions.ARB;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using PolygonMode = Silk.NET.OpenGL.PolygonMode;
|
||||
using PrimitiveType = Silk.NET.OpenGL.PrimitiveType;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL graphics device
|
||||
/// </summary>
|
||||
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice, IMeshPipelineDevice {
|
||||
private readonly ILogger _log;
|
||||
private readonly DebugRenderSettings _renderSettings;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement;
|
||||
|
||||
public GL GL { get; }
|
||||
public DebugRenderSettings RenderSettings => _renderSettings;
|
||||
|
||||
private readonly ConcurrentQueue<Action<GL>> _glThreadQueue = new();
|
||||
private readonly ConcurrentQueue<Action<GL>> _nextGlThreadQueue = new();
|
||||
|
||||
internal bool HasPendingGLWork =>
|
||||
!_glThreadQueue.IsEmpty || !_nextGlThreadQueue.IsEmpty;
|
||||
|
||||
// Campaign V slice V6i-2: IMeshPipelineDevice. Every member below already
|
||||
// existed under a GL-specific name; these are aliases, not behaviour, so
|
||||
// the shipping backend executes exactly the statements it executed
|
||||
// before. See the interface for what the mesh pipeline actually needs
|
||||
// and what still has to move before it has a second implementation.
|
||||
GL? IMeshPipelineDevice.Gl => GL;
|
||||
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue IMeshPipelineDevice.ResourceRetirement =>
|
||||
_resourceRetirement;
|
||||
|
||||
bool IMeshPipelineDevice.HasPendingWork => HasPendingGLWork;
|
||||
|
||||
void IMeshPipelineDevice.ProcessQueue() => ProcessGLQueue();
|
||||
|
||||
public void QueueGLAction(Action<GL> action) {
|
||||
_glThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
internal void QueueGLActionForNextPass(Action<GL> action) {
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_nextGlThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
public void ProcessGLQueue() {
|
||||
// Retry the prior pass before ordinary work (notably sampler
|
||||
// deletion), but process only the captured generation so a
|
||||
// persistent driver failure cannot spin this frame forever.
|
||||
int retryCount = _nextGlThreadQueue.Count;
|
||||
for (int i = 0; i < retryCount && _nextGlThreadQueue.TryDequeue(out Action<GL>? retry); i++) {
|
||||
try {
|
||||
retry(GL);
|
||||
} catch (Exception ex) {
|
||||
_log.LogError(ex, "Error processing retryable GL queue action");
|
||||
}
|
||||
}
|
||||
// Normal actions retain drain-to-empty semantics because teardown
|
||||
// actions intentionally enqueue dependent atlas releases here.
|
||||
// A persistent retryable error must not starve unrelated uploads
|
||||
// and releases forever: the retry generation remains bounded to
|
||||
// one attempt per pass, while ordinary work still makes progress.
|
||||
while (_glThreadQueue.TryDequeue(out var action)) {
|
||||
try {
|
||||
action(GL);
|
||||
} catch (Exception ex) {
|
||||
_log.LogError(ex, "Error processing GL queue action");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasBindless { get; private set; }
|
||||
public bool HasOpenGL43 { get; private set; }
|
||||
public bool HasBufferStorage { get; private set; }
|
||||
public bool HasTextureStorage { get; private set; }
|
||||
public ArbBindlessTexture? BindlessExtension { get; private set; }
|
||||
|
||||
public uint InstanceVBO { get; private set; }
|
||||
public void* InstanceVBOPtr { get; private set; }
|
||||
|
||||
public uint SharedQuadVBO { get; private set; }
|
||||
public uint SharedDebugVAO { get; private set; }
|
||||
public uint SharedDebugInstanceVBO { get; private set; }
|
||||
|
||||
/// <summary>OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).</summary>
|
||||
public uint WrapSampler { get; private set; }
|
||||
/// <summary>OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).</summary>
|
||||
public uint ClampSampler { get; private set; }
|
||||
internal float MaxSupportedAnisotropy { get; private set; }
|
||||
|
||||
private ManagedGLUniformBuffer? _sceneDataBuffer;
|
||||
/// <summary>Shared SceneData UBO.</summary>
|
||||
public ManagedGLUniformBuffer SceneDataBuffer => _sceneDataBuffer!;
|
||||
|
||||
private SceneData _currentSceneData;
|
||||
public SceneData CurrentSceneData => _currentSceneData;
|
||||
|
||||
public void SetSceneData(ref SceneData data) {
|
||||
_currentSceneData = data;
|
||||
SceneDataBuffer.SetData(ref data);
|
||||
}
|
||||
|
||||
private int _instanceBufferCapacity = 0;
|
||||
private int _instanceBufferStride = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IntPtr NativeDevice { get; }
|
||||
|
||||
protected OpenGLGraphicsDevice() : base() {
|
||||
_log = null!;
|
||||
_renderSettings = null!;
|
||||
_resourceRetirement = null!;
|
||||
GL = null!;
|
||||
}
|
||||
|
||||
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true)
|
||||
: this(gl, log, renderSettings, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance, allowBindless) {
|
||||
}
|
||||
|
||||
internal OpenGLGraphicsDevice(
|
||||
GL gl,
|
||||
ILogger log,
|
||||
DebugRenderSettings renderSettings,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||
bool allowBindless = true) : base() {
|
||||
_log = log;
|
||||
_renderSettings = renderSettings;
|
||||
_resourceRetirement = resourceRetirement ?? throw new ArgumentNullException(nameof(resourceRetirement));
|
||||
|
||||
GL = gl;
|
||||
GLHelpers.Init(this, log);
|
||||
|
||||
try {
|
||||
GL.GetInteger(GLEnum.MajorVersion, out int major);
|
||||
GL.GetInteger(GLEnum.MinorVersion, out int minor);
|
||||
HasOpenGL43 = major > 4 || (major == 4 && minor >= 3);
|
||||
HasTextureStorage = major > 4 || (major == 4 && minor >= 2) || GL.IsExtensionPresent("GL_ARB_texture_storage");
|
||||
HasBufferStorage = major > 4 || (major == 4 && minor >= 4) || GL.IsExtensionPresent("GL_ARB_buffer_storage");
|
||||
|
||||
if (allowBindless && GL.TryGetExtension(out ArbBindlessTexture ext)) {
|
||||
BindlessExtension = ext;
|
||||
HasBindless = true;
|
||||
} else {
|
||||
HasBindless = false;
|
||||
}
|
||||
} catch {
|
||||
HasOpenGL43 = false;
|
||||
HasBindless = false;
|
||||
}
|
||||
|
||||
var resources = new ResourceCleanupGroup();
|
||||
try {
|
||||
InstanceVBO = CreateConstructionBuffer(resources, "WB instance buffer");
|
||||
|
||||
// Query this immutable device limit once. Atlas construction can
|
||||
// happen hundreds of times during portal streaming; repeating a
|
||||
// driver GetFloat for every texture serialized the upload burst.
|
||||
if (renderSettings.EnableAnisotropicFiltering) {
|
||||
MaxSupportedAnisotropy = GlResourceCommand.Execute(
|
||||
GL,
|
||||
"query maximum texture anisotropy",
|
||||
() => {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
return Math.Max(0f, maxAniso);
|
||||
});
|
||||
}
|
||||
|
||||
WrapSampler = CreateConstructionSampler(
|
||||
resources,
|
||||
TextureWrapMode.Repeat,
|
||||
"WB repeat sampler");
|
||||
ClampSampler = CreateConstructionSampler(
|
||||
resources,
|
||||
TextureWrapMode.ClampToEdge,
|
||||
"WB clamp sampler");
|
||||
|
||||
_sceneDataBuffer = new ManagedGLUniformBuffer(
|
||||
this,
|
||||
BufferUsage.Dynamic,
|
||||
Marshal.SizeOf<SceneData>());
|
||||
ManagedGLUniformBuffer ownedSceneDataBuffer = _sceneDataBuffer;
|
||||
resources.Add(
|
||||
"WB scene-data uniform buffer",
|
||||
ownedSceneDataBuffer.DisposeImmediately);
|
||||
|
||||
InitializeSharedDebugResources(resources);
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"OpenGLGraphicsDevice construction failed and its GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires a GL resource only after every submitted draw that could
|
||||
/// reference it has completed on the GPU.
|
||||
/// </summary>
|
||||
internal void RetireGpuResource(Action release) => _resourceRetirement.Retire(release);
|
||||
|
||||
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
|
||||
_resourceRetirement;
|
||||
|
||||
private uint CreateConstructionBuffer(ResourceCleanupGroup resources, string name) {
|
||||
uint buffer = GlResourceCommand.CreateName(GL, name, GL.GenBuffer, GL.DeleteBuffer);
|
||||
resources.Add(
|
||||
name,
|
||||
() => GlResourceCommand.DeleteBuffer(
|
||||
GL,
|
||||
buffer,
|
||||
$"delete {name} {buffer}"));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private uint CreateConstructionSampler(
|
||||
ResourceCleanupGroup resources,
|
||||
TextureWrapMode wrapMode,
|
||||
string name) {
|
||||
uint sampler = GlResourceCommand.CreateName(GL, name, GL.GenSampler, GL.DeleteSampler);
|
||||
resources.Add(
|
||||
name,
|
||||
() => GlResourceCommand.Execute(
|
||||
GL,
|
||||
$"delete {name} {sampler}",
|
||||
() => GL.DeleteSampler(sampler)));
|
||||
GlResourceCommand.Execute(GL, $"configure {name}", () => {
|
||||
GL.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrapMode);
|
||||
GL.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrapMode);
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
SamplerParameterI.MinFilter,
|
||||
(int)TextureMinFilter.LinearMipmapLinear);
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
SamplerParameterI.MagFilter,
|
||||
(int)TextureMagFilter.Linear);
|
||||
if (MaxSupportedAnisotropy > 0)
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
GLEnum.TextureMaxAnisotropy,
|
||||
MaxSupportedAnisotropy);
|
||||
});
|
||||
return sampler;
|
||||
}
|
||||
|
||||
private void InitializeSharedDebugResources(ResourceCleanupGroup resources) {
|
||||
// Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
|
||||
float[] quadVertices = {
|
||||
0.0f, -0.5f,
|
||||
1.0f, -0.5f,
|
||||
1.0f, 0.5f,
|
||||
0.0f, -0.5f,
|
||||
1.0f, 0.5f,
|
||||
0.0f, 0.5f
|
||||
};
|
||||
|
||||
SharedQuadVBO = CreateConstructionBuffer(resources, "WB shared debug quad buffer");
|
||||
SharedDebugInstanceVBO = CreateConstructionBuffer(
|
||||
resources,
|
||||
"WB shared debug instance buffer");
|
||||
SharedDebugVAO = GlResourceCommand.CreateName(
|
||||
GL,
|
||||
"WB shared debug vertex array",
|
||||
GL.GenVertexArray,
|
||||
GL.DeleteVertexArray);
|
||||
uint ownedDebugVao = SharedDebugVAO;
|
||||
resources.Add(
|
||||
"WB shared debug vertex array",
|
||||
() => GlResourceCommand.DeleteVertexArray(
|
||||
GL,
|
||||
ownedDebugVao,
|
||||
$"delete WB shared debug vertex array {ownedDebugVao}"));
|
||||
|
||||
GlResourceCommand.Execute(GL, "configure WB shared debug resources", () => {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
fixed (float* pQuad = quadVertices) {
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(nuint)(quadVertices.Length * sizeof(float)),
|
||||
pQuad,
|
||||
GLEnum.StaticDraw);
|
||||
}
|
||||
|
||||
// Initial capacity for debug instances.
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(nuint)(1024 * 44),
|
||||
(void*)0,
|
||||
GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
|
||||
|
||||
GL.BindVertexArray(SharedDebugVAO);
|
||||
|
||||
// Quad Pos attribute (location 0)
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
|
||||
|
||||
// Instance attributes
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
uint lineInstanceSize = 44;
|
||||
|
||||
// aStart (location 1)
|
||||
GL.EnableVertexAttribArray(1);
|
||||
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
|
||||
GL.VertexAttribDivisor(1, 1);
|
||||
|
||||
// aEnd (location 2)
|
||||
GL.EnableVertexAttribArray(2);
|
||||
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12);
|
||||
GL.VertexAttribDivisor(2, 1);
|
||||
|
||||
// aColor (location 3)
|
||||
GL.EnableVertexAttribArray(3);
|
||||
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24);
|
||||
GL.VertexAttribDivisor(3, 1);
|
||||
|
||||
// aThickness (location 4)
|
||||
GL.EnableVertexAttribArray(4);
|
||||
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40);
|
||||
GL.VertexAttribDivisor(4, 1);
|
||||
|
||||
GL.BindVertexArray(0);
|
||||
});
|
||||
}
|
||||
|
||||
public void EnsureInstanceBufferCapacity(int count, int stride, bool forceOrphan = false) {
|
||||
if (count <= _instanceBufferCapacity && !forceOrphan) return;
|
||||
|
||||
if (_instanceBufferCapacity > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(_instanceBufferCapacity * _instanceBufferStride);
|
||||
}
|
||||
|
||||
_instanceBufferCapacity = Math.Max(count, 256);
|
||||
_instanceBufferStride = stride;
|
||||
|
||||
if (HasBufferStorage) {
|
||||
if (InstanceVBO != 0) {
|
||||
GL.DeleteBuffer(InstanceVBO);
|
||||
}
|
||||
GL.GenBuffers(1, out uint instanceVbo);
|
||||
InstanceVBO = instanceVbo;
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride), (void*)0, flags);
|
||||
InstanceVBOPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)(_instanceBufferCapacity * _instanceBufferStride), MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride),
|
||||
(void*)null, GLEnum.DynamicDraw);
|
||||
InstanceVBOPtr = null;
|
||||
}
|
||||
GpuMemoryTracker.TrackAllocation(_instanceBufferCapacity * _instanceBufferStride);
|
||||
}
|
||||
|
||||
public void UpdateInstanceBuffer<T>(List<T> data) where T : unmanaged {
|
||||
EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf<T>(), true);
|
||||
var span = CollectionsMarshal.AsSpan(data);
|
||||
if (InstanceVBOPtr != null) {
|
||||
var destSpan = new Span<T>(InstanceVBOPtr, data.Count);
|
||||
span.CopyTo(destSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
fixed (T* ptr = span) {
|
||||
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Count * Marshal.SizeOf<T>()), ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateInstanceBuffer<T>(Span<T> data) where T : unmanaged {
|
||||
EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf<T>(), true);
|
||||
if (InstanceVBOPtr != null) {
|
||||
var destSpan = new Span<T>(InstanceVBOPtr, data.Length);
|
||||
data.CopyTo(destSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
fixed (T* ptr = data) {
|
||||
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Length * Marshal.SizeOf<T>()), ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear(ColorVec color, ClearFlags flags, float depth, int stencil) {
|
||||
GL.ClearColor(color.R, color.G, color.B, color.A);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.Clear((uint)Convert(flags));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IIndexBuffer CreateIndexBuffer(int size,
|
||||
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
|
||||
return new ManagedGLIndexBuffer(this, usage, size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IVertexBuffer CreateVertexBuffer(int size,
|
||||
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
|
||||
return new ManagedGLVertexBuffer(this, usage, size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) {
|
||||
return new ManagedGLVertexArray(this, vertexBuffer, format);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawElements(Chorizite.Core.Render.Enums.PrimitiveType type, int numElements, int indiceOffset = 0) {
|
||||
GL.DrawElements(Convert(type), (uint)numElements, GLEnum.UnsignedInt, (void*)(indiceOffset * sizeof(uint)));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public override IShader CreateShader(string name, string vertexCode, string fragmentCode) {
|
||||
var key = $"{GL.GetHashCode()}_{name}_{vertexCode.GetHashCode()}_{fragmentCode.GetHashCode()}";
|
||||
|
||||
while (true) {
|
||||
if (_shaderCache.TryGetValue(key, out var existing)) {
|
||||
if (existing is SharedShader shared && shared.TryIncrement()) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
var inner = new GLSLShader(this, name, vertexCode, fragmentCode, _log);
|
||||
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
|
||||
|
||||
if (_shaderCache.TryAdd(key, newShader)) {
|
||||
return newShader;
|
||||
}
|
||||
|
||||
// Someone else added it first, dispose ours and try again
|
||||
newShader.DisposeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IShader CreateShader(string name, string shaderDirectory) {
|
||||
var key = $"{GL.GetHashCode()}_{name}";
|
||||
|
||||
while (true) {
|
||||
if (_shaderCache.TryGetValue(key, out var existing)) {
|
||||
if (existing is SharedShader shared && shared.TryIncrement()) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
var inner = new GLSLShader(this, name, shaderDirectory, _log);
|
||||
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
|
||||
|
||||
if (_shaderCache.TryAdd(key, newShader)) {
|
||||
return newShader;
|
||||
}
|
||||
|
||||
// Someone else added it first, dispose ours and try again
|
||||
newShader.DisposeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, IShader> _shaderCache = new();
|
||||
|
||||
private class SharedShader : IShader, IDisposable {
|
||||
private readonly IShader _shader;
|
||||
private readonly Action _onDispose;
|
||||
private int _refCount = 1;
|
||||
|
||||
public string Name => _shader.Name;
|
||||
public uint ProgramId => _shader.ProgramId;
|
||||
|
||||
public SharedShader(IShader shader, Action onDispose) {
|
||||
_shader = shader;
|
||||
_onDispose = onDispose;
|
||||
}
|
||||
|
||||
public bool TryIncrement() {
|
||||
while (true) {
|
||||
int current = _refCount;
|
||||
if (current <= 0) return false;
|
||||
if (Interlocked.CompareExchange(ref _refCount, current + 1, current) == current) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind() => _shader.Bind();
|
||||
public void Unbind() => _shader.Unbind();
|
||||
public void Load(string vertexSource, string fragmentSource) => _shader.Load(vertexSource, fragmentSource);
|
||||
|
||||
public void SetUniform(string name, int value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, float value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Vector2 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Vector3 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Vector4 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Matrix4x4 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, float[] values) => _shader.SetUniform(name, values);
|
||||
|
||||
public void DisposeInternal() {
|
||||
_refCount = 0;
|
||||
(_shader as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (Interlocked.Decrement(ref _refCount) == 0) {
|
||||
(_shader as IDisposable)?.Dispose();
|
||||
_onDispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITexture
|
||||
CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data = null) {
|
||||
if (format != TextureFormat.RGBA8) {
|
||||
throw new NotImplementedException($"Texture format {format} is not supported.");
|
||||
}
|
||||
|
||||
return new ManagedGLTexture(this, data, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture with custom texture parameters.
|
||||
/// </summary>
|
||||
public ITexture CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data, TextureParameters texParams) {
|
||||
if (format != TextureFormat.RGBA8) {
|
||||
throw new NotImplementedException($"Texture format {format} is not supported.");
|
||||
}
|
||||
return new ManagedGLTexture(this, data, width, height, texParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITexture? CreateTextureInternal(TextureFormat format, string filename) {
|
||||
if (format != TextureFormat.RGBA8) {
|
||||
throw new NotImplementedException($"Texture format {format} is not supported.");
|
||||
}
|
||||
|
||||
return new ManagedGLTexture(this, filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITextureArray
|
||||
CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) {
|
||||
return new ManagedGLTextureArray(this, format, width, height, size, _log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture array with custom texture parameters.
|
||||
/// </summary>
|
||||
public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) {
|
||||
return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void BeginFrame() {
|
||||
GL.Viewport(Viewport.X, Viewport.Y, (uint)Viewport.Width, (uint)Viewport.Height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void EndFrame() {
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetRenderStateInternal(RenderState state, bool enabled) {
|
||||
switch (state) {
|
||||
case RenderState.AlphaBlend:
|
||||
if (enabled) GL.Enable(EnableCap.Blend);
|
||||
else GL.Disable(EnableCap.Blend);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.DepthTest:
|
||||
if (enabled) GL.Enable(EnableCap.DepthTest);
|
||||
else GL.Disable(EnableCap.DepthTest);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.ScissorTest:
|
||||
if (enabled) GL.Enable(EnableCap.ScissorTest);
|
||||
else GL.Disable(EnableCap.ScissorTest);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.DepthWrite:
|
||||
if (enabled) GL.DepthMask(true);
|
||||
else GL.DepthMask(false);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.Fog:
|
||||
break;
|
||||
case RenderState.Lighting:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetBlendFactorInternal(BlendFactor srcBlendFactor, BlendFactor dstBlendFactor) {
|
||||
GL.BlendFunc(Convert(srcBlendFactor), Convert(dstBlendFactor));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetScissorRectInternal(Rectangle scissor) {
|
||||
var gtop = (int)Viewport.Height - scissor.Y - scissor.Height;
|
||||
GL.Scissor(scissor.X, gtop, (uint)scissor.Width, (uint)scissor.Height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetViewportInternal(Rectangle viewport) {
|
||||
GL.Viewport(viewport.X, viewport.Y, (uint)viewport.Width, (uint)viewport.Height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetPolygonModeInternal(Chorizite.Core.Render.Enums.PolygonMode polygonMode) {
|
||||
GL.PolygonMode(GLEnum.FrontAndBack, Convert(polygonMode));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetCullModeInternal(CullMode cullMode) {
|
||||
switch (cullMode) {
|
||||
case CullMode.None:
|
||||
GL.Disable(EnableCap.CullFace);
|
||||
break;
|
||||
case CullMode.Front:
|
||||
GL.Enable(EnableCap.CullFace);
|
||||
GL.CullFace(GLEnum.Front);
|
||||
break;
|
||||
case CullMode.Back:
|
||||
GL.Enable(EnableCap.CullFace);
|
||||
GL.CullFace(GLEnum.Back);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private GLEnum Convert(Chorizite.Core.Render.Enums.PolygonMode mode) {
|
||||
switch (mode) {
|
||||
case Chorizite.Core.Render.Enums.PolygonMode.Fill:
|
||||
return GLEnum.Fill;
|
||||
case Chorizite.Core.Render.Enums.PolygonMode.Line:
|
||||
return GLEnum.Line;
|
||||
case Chorizite.Core.Render.Enums.PolygonMode.Point:
|
||||
return GLEnum.Point;
|
||||
default:
|
||||
return GLEnum.Fill;
|
||||
}
|
||||
}
|
||||
|
||||
private GLEnum Convert(ClearFlags flags) {
|
||||
GLEnum mask = 0;
|
||||
|
||||
if ((flags & ClearFlags.Color) == ClearFlags.Color) mask |= GLEnum.ColorBufferBit;
|
||||
if ((flags & ClearFlags.Depth) == ClearFlags.Depth) mask |= GLEnum.DepthBufferBit;
|
||||
if ((flags & ClearFlags.Stencil) == ClearFlags.Stencil) mask |= GLEnum.StencilBufferBit;
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
private GLEnum Convert(BlendFactor factor) {
|
||||
switch (factor) {
|
||||
case BlendFactor.One:
|
||||
return GLEnum.One;
|
||||
case BlendFactor.SrcAlpha:
|
||||
return GLEnum.SrcAlpha;
|
||||
case BlendFactor.OneMinusSrcAlpha:
|
||||
return GLEnum.OneMinusSrcAlpha;
|
||||
case BlendFactor.DstAlpha:
|
||||
return GLEnum.DstAlpha;
|
||||
case BlendFactor.OneMinusDstAlpha:
|
||||
return GLEnum.OneMinusDstAlpha;
|
||||
default:
|
||||
return GLEnum.One;
|
||||
}
|
||||
}
|
||||
|
||||
private PrimitiveType Convert(Chorizite.Core.Render.Enums.PrimitiveType type) {
|
||||
switch (type) {
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.PointList:
|
||||
return PrimitiveType.Points;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.LineList:
|
||||
return PrimitiveType.Lines;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.LineStrip:
|
||||
return PrimitiveType.LineStrip;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleList:
|
||||
return PrimitiveType.Triangles;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleStrip:
|
||||
return PrimitiveType.TriangleStrip;
|
||||
default:
|
||||
throw new NotImplementedException($"Primitive type {type} is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IFramebuffer CreateFramebuffer(ITexture texture, int width, int height,
|
||||
bool hasDepthStencil = true) {
|
||||
if (texture == null) {
|
||||
throw new ArgumentNullException(nameof(texture));
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new ArgumentException("Width and height must be positive.");
|
||||
}
|
||||
|
||||
return new ManagedGLFramebuffer(this, texture, width, height, hasDepthStencil);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void BindFramebuffer(IFramebuffer? framebuffer) {
|
||||
uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0;
|
||||
GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose() {
|
||||
var instanceVBO = InstanceVBO;
|
||||
var instanceBufferCapacity = _instanceBufferCapacity;
|
||||
var instanceBufferStride = _instanceBufferStride;
|
||||
var wrapSampler = WrapSampler;
|
||||
var clampSampler = ClampSampler;
|
||||
|
||||
var sharedQuadVbo = SharedQuadVBO;
|
||||
var sharedDebugInstanceVbo = SharedDebugInstanceVBO;
|
||||
var sharedDebugVao = SharedDebugVAO;
|
||||
|
||||
QueueGLAction(gl => {
|
||||
if (sharedQuadVbo != 0) gl.DeleteBuffer(sharedQuadVbo);
|
||||
if (sharedDebugInstanceVbo != 0) gl.DeleteBuffer(sharedDebugInstanceVbo);
|
||||
if (sharedDebugVao != 0) gl.DeleteVertexArray(sharedDebugVao);
|
||||
if (instanceVBO != 0) {
|
||||
gl.DeleteBuffer(instanceVBO);
|
||||
if (instanceBufferCapacity > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(instanceBufferCapacity * instanceBufferStride);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Bindless texture-array retirements embed these samplers in their
|
||||
// resident handles. Ordinary GL work must keep flowing when one
|
||||
// retry is sick, but sampler deletion itself is dependency-ordered
|
||||
// behind the retry queue. Requeue into the next generation (never
|
||||
// the drain-to-empty ordinary queue) to remain one attempt/frame.
|
||||
Action<GL>? deleteSamplersWhenSafe = null;
|
||||
deleteSamplersWhenSafe = gl => {
|
||||
if (!_nextGlThreadQueue.IsEmpty) {
|
||||
QueueGLActionForNextPass(deleteSamplersWhenSafe!);
|
||||
return;
|
||||
}
|
||||
if (wrapSampler != 0)
|
||||
gl.DeleteSampler(wrapSampler);
|
||||
if (clampSampler != 0)
|
||||
gl.DeleteSampler(clampSampler);
|
||||
};
|
||||
QueueGLActionForNextPass(deleteSamplersWhenSafe);
|
||||
|
||||
InstanceVBO = 0;
|
||||
InstanceVBOPtr = null;
|
||||
WrapSampler = 0;
|
||||
ClampSampler = 0;
|
||||
_sceneDataBuffer?.Dispose();
|
||||
_sceneDataBuffer = null;
|
||||
}
|
||||
|
||||
public override IUniformBuffer CreateUniformBuffer(BufferUsage usage, int size) {
|
||||
return (IUniformBuffer)new ManagedGLUniformBuffer(this, usage, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks currently-bound GL state to skip redundant rebinds across the
|
||||
/// WB-derived render path. Previously these were static fields on
|
||||
/// <c>BaseObjectRenderManager</c> in the WorldBuilder.Shared project; inlined
|
||||
/// here in Phase O-T7 to eliminate the WorldBuilder project reference.
|
||||
///
|
||||
/// Semantics are identical to the WB originals:
|
||||
/// <c>CurrentAtlas</c> — slot index of the currently bound texture atlas.
|
||||
/// <c>CurrentVAO</c> — OpenGL name of the currently bound vertex array object.
|
||||
/// <c>CurrentIBO</c> — OpenGL name of the currently bound index buffer object.
|
||||
/// Sentinel value 0 means "no valid binding cached."
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All accesses must occur on the render thread. GL state binding is
|
||||
/// not thread-safe; these sentinels are written immediately after the
|
||||
/// corresponding glBind* call and read by the next dispatch on the
|
||||
/// same thread.
|
||||
/// </remarks>
|
||||
public static class RenderStateCache
|
||||
{
|
||||
public static uint CurrentAtlas = 0;
|
||||
public static uint CurrentVAO = 0;
|
||||
public static uint CurrentIBO = 0;
|
||||
}
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using AcDream.App.Rendering;
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Always-on transaction boundary and accounting for raw dynamic GL objects.
|
||||
/// Growth keeps the published CPU capacity unchanged until BufferData succeeds;
|
||||
/// OpenGL leaves the previous data store intact when allocation reports an
|
||||
/// error, so the caller can continue using the old capacity or unwind.
|
||||
/// </summary>
|
||||
internal static unsafe class TrackedGlResource
|
||||
{
|
||||
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
|
||||
GL gl,
|
||||
uint buffer,
|
||||
long capacityBytes,
|
||||
string context) =>
|
||||
CreateRetryableBufferDeletion(
|
||||
gl,
|
||||
buffer,
|
||||
() => capacityBytes,
|
||||
context);
|
||||
|
||||
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
|
||||
GL gl,
|
||||
uint buffer,
|
||||
Func<long> capacityBytes,
|
||||
string context)
|
||||
{
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
ArgumentNullException.ThrowIfNull(capacityBytes);
|
||||
return new RetryableGpuResourceRelease(
|
||||
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
|
||||
() =>
|
||||
{
|
||||
gl.DeleteBuffer(buffer);
|
||||
// Per the GL error contract, a command which generates an
|
||||
// error does not change object state. Keep mutation and its
|
||||
// validation in one retryable stage so that failed deletion
|
||||
// is issued again, while later accounting remains untouched.
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
long bytes = capacityBytes();
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(bytes);
|
||||
if (bytes != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(bytes, GpuResourceType.Buffer);
|
||||
},
|
||||
() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer));
|
||||
}
|
||||
|
||||
public static RetryableGpuResourceRelease CreateRetryableVertexArrayDeletion(
|
||||
GL gl,
|
||||
uint vertexArray,
|
||||
string context,
|
||||
Action? trackDeallocation = null)
|
||||
{
|
||||
if (vertexArray == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(vertexArray));
|
||||
return new RetryableGpuResourceRelease(
|
||||
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
|
||||
() =>
|
||||
{
|
||||
gl.DeleteVertexArray(vertexArray);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
},
|
||||
trackDeallocation
|
||||
?? (() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO)));
|
||||
}
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
BufferTargetARB target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
BufferUsageARB usage,
|
||||
string context)
|
||||
=> AllocateBufferStorage(
|
||||
gl,
|
||||
(GLEnum)target,
|
||||
buffer,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
(GLEnum)usage,
|
||||
context);
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
BufferTargetARB target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
BufferUsageARB usage,
|
||||
void* data,
|
||||
string context)
|
||||
=> AllocateBufferStorage(
|
||||
gl,
|
||||
(GLEnum)target,
|
||||
buffer,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
(GLEnum)usage,
|
||||
data,
|
||||
context);
|
||||
|
||||
public static uint CreateBuffer(GL gl, string context)
|
||||
{
|
||||
return CreateTrackedName(
|
||||
gl,
|
||||
"buffer",
|
||||
context,
|
||||
gl.GenBuffer,
|
||||
name => GlResourceCommand.DeleteBuffer(
|
||||
gl,
|
||||
name,
|
||||
$"rollback buffer {name} after failed {context}"),
|
||||
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer));
|
||||
}
|
||||
|
||||
public static uint CreateVertexArray(GL gl, string context)
|
||||
{
|
||||
return CreateTrackedName(
|
||||
gl,
|
||||
"vertex-array",
|
||||
context,
|
||||
gl.GenVertexArray,
|
||||
name => GlResourceCommand.DeleteVertexArray(
|
||||
gl,
|
||||
name,
|
||||
$"rollback vertex array {name} after failed {context}"),
|
||||
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO));
|
||||
}
|
||||
|
||||
private static uint CreateTrackedName(
|
||||
GL gl,
|
||||
string resourceName,
|
||||
string context,
|
||||
Func<uint> create,
|
||||
Action<uint> rollback,
|
||||
Action publishAccounting)
|
||||
{
|
||||
uint name = GlResourceCommand.CreateName(
|
||||
gl,
|
||||
$"{resourceName} for {context}",
|
||||
create,
|
||||
rollback);
|
||||
var cleanup = new ResourceCleanupGroup();
|
||||
cleanup.Add($"{resourceName} name {name}", () => rollback(name));
|
||||
try
|
||||
{
|
||||
publishAccounting();
|
||||
return name;
|
||||
}
|
||||
catch (Exception publicationFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
cleanup.RetryCleanup();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new GlResourceConstructionException(
|
||||
$"Publishing {resourceName} accounting failed and GL name {name} could not be released.",
|
||||
cleanup,
|
||||
[publicationFailure, cleanupFailure]);
|
||||
}
|
||||
|
||||
ExceptionDispatchInfo.Capture(publicationFailure).Throw();
|
||||
throw new InvalidOperationException("Unreachable resource-publication path.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
GLEnum target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
GLEnum usage,
|
||||
string context)
|
||||
=> AllocateBufferStorage(
|
||||
gl,
|
||||
target,
|
||||
buffer,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
usage,
|
||||
null,
|
||||
context);
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
GLEnum target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
GLEnum usage,
|
||||
void* data,
|
||||
string context)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(previousBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(newBytes, 1);
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.BindBuffer(target, buffer);
|
||||
gl.BufferData(target, checked((nuint)newBytes), data, usage);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
|
||||
long delta = checked(newBytes - previousBytes);
|
||||
if (delta > 0)
|
||||
GpuMemoryTracker.TrackAllocation(delta, GpuResourceType.Buffer);
|
||||
else if (delta < 0)
|
||||
GpuMemoryTracker.TrackDeallocation(-delta, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
public static void UpdateBufferSubData(
|
||||
GL gl,
|
||||
BufferTargetARB target,
|
||||
uint buffer,
|
||||
nint byteOffset,
|
||||
long byteCount,
|
||||
void* data,
|
||||
string context)
|
||||
=> UpdateBufferSubData(
|
||||
gl,
|
||||
(GLEnum)target,
|
||||
buffer,
|
||||
byteOffset,
|
||||
byteCount,
|
||||
data,
|
||||
context);
|
||||
|
||||
public static void UpdateBufferSubData(
|
||||
GL gl,
|
||||
GLEnum target,
|
||||
uint buffer,
|
||||
nint byteOffset,
|
||||
long byteCount,
|
||||
void* data,
|
||||
string context)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(byteOffset);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(byteCount, 1);
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.BindBuffer(target, buffer);
|
||||
gl.BufferSubData(target, byteOffset, checked((nuint)byteCount), data);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
}
|
||||
|
||||
public static void DeleteBuffer(GL gl, uint buffer, long capacityBytes, string context)
|
||||
{
|
||||
if (buffer == 0)
|
||||
return;
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.DeleteBuffer(buffer);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
if (capacityBytes != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(capacityBytes, GpuResourceType.Buffer);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
public static void DeleteVertexArray(GL gl, uint vertexArray, string context)
|
||||
{
|
||||
if (vertexArray == 0)
|
||||
return;
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.DeleteVertexArray(vertexArray);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -200,39 +200,15 @@ public sealed class WbMeshAdapter
|
|||
{
|
||||
// Campaign V slice V6i-3: WHICH mesh-pipeline device is decided
|
||||
// here, once, and it is the only place in the mesh pipeline that
|
||||
// names a backend. The GL arm is unchanged — same construction,
|
||||
// same queue-drain guarantee on rollback. A backend with no context
|
||||
// gets the RHI arm, whose queue is empty by construction because
|
||||
// Vulkan resource work is recorded or retirement-queued rather than
|
||||
// deferred onto a context-owning thread.
|
||||
if (gl is { } context)
|
||||
{
|
||||
var openGl = new OpenGLGraphicsDevice(
|
||||
context,
|
||||
logger,
|
||||
new DebugRenderSettings(),
|
||||
resourceRetirement);
|
||||
graphicsDevice = openGl;
|
||||
var graphicsDeviceRelease = new RetryableGpuResourceRelease(
|
||||
openGl.Dispose,
|
||||
() =>
|
||||
{
|
||||
openGl.ProcessGLQueue();
|
||||
if (openGl.HasPendingGLWork)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"WB graphics-device construction cleanup still has queued GL work.");
|
||||
}
|
||||
});
|
||||
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rhiDevice = new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(
|
||||
resourceRetirement);
|
||||
graphicsDevice = rhiDevice;
|
||||
resources.Add("WB graphics device", rhiDevice.Dispose);
|
||||
}
|
||||
// names a backend. The raw-GL arm (OpenGLGraphicsDevice) was
|
||||
// deleted at Campaign V slice V11; the RHI arm's queue is empty by
|
||||
// construction because Vulkan resource work is recorded or
|
||||
// retirement-queued rather than deferred onto a context-owning
|
||||
// thread.
|
||||
var rhiDevice = new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(
|
||||
resourceRetirement);
|
||||
graphicsDevice = rhiDevice;
|
||||
resources.Add("WB graphics device", rhiDevice.Dispose);
|
||||
if (resolvedPreparedAssets is null)
|
||||
{
|
||||
resolvedPreparedAssets = new DatPreparedAssetSource(
|
||||
|
|
@ -267,19 +243,6 @@ public sealed class WbMeshAdapter
|
|||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: the GL device whose texture table every mesh
|
||||
/// batch's <c>GpuTextureSlot</c> indexes. The world renderers this adapter
|
||||
/// feeds flush and bind that table before their raw-GL draws, and taking it
|
||||
/// from here rather than from a second composition wire is what makes "the
|
||||
/// slot and the table came from the same device" true by construction.
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
(_meshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"An initialized mesh adapter is required for the world texture table."))
|
||||
.WorldTextureTable;
|
||||
|
||||
internal void RegisterResidencySources(ResidencyManager manager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manager);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Gl;
|
||||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
using AcDream.Core.Rendering.Wb;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
|
|
@ -15,21 +15,21 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// deliberately left CREATION with the caches — plan §5.5.11 records why, and
|
||||
/// §5.5.12 item 1 hands the remainder forward: "the missing piece is an
|
||||
/// <c>ITextureArray</c> implementation over <see cref="IGpuTexture"/>, not a
|
||||
/// codec." This is that interface. <see cref="ManagedGLTextureArray"/> and
|
||||
/// <see cref="RhiWorldTextureArray"/> implement it, and which one exists is
|
||||
/// decided once at composition by <see cref="IWorldTextureArrayFactory"/> —
|
||||
/// never per call, so the GL path executes exactly the statements it executed
|
||||
/// before.</para>
|
||||
/// codec." This is that interface. <c>ManagedGLTextureArray</c> used to be its
|
||||
/// GL implementation, alongside <see cref="RhiWorldTextureArray"/>; which one
|
||||
/// existed was decided once at composition by
|
||||
/// <see cref="IWorldTextureArrayFactory"/>, never per call. Campaign V slice
|
||||
/// V11 deleted <c>ManagedGLTextureArray</c> along with the rest of the raw-GL
|
||||
/// arm, so <see cref="RhiWorldTextureArray"/> is now the sole implementation.</para>
|
||||
///
|
||||
/// <para><b>The slot, not the handle, is the seam.</b> Before this slice
|
||||
/// <para><b>The slot, not the handle, is the seam.</b> Before V6i-2
|
||||
/// <c>ObjectMeshManager</c> read <c>BindlessWrapHandle</c>/
|
||||
/// <c>BindlessClampHandle</c> off the concrete GL array and interned them into
|
||||
/// the device table itself. A 64-bit <c>ARB_bindless_texture</c> handle is
|
||||
/// unspellable on Vulkan, so the array now answers the question the caller was
|
||||
/// really asking — <see cref="ResolveSlot"/> — and each implementation gets
|
||||
/// there its own way: the GL array interns its resident handle (the same
|
||||
/// idempotent call, one level down), while the RHI array registered its two
|
||||
/// (texture, sampler) pairs at construction and returns a field.</para>
|
||||
/// unspellable on Vulkan, so the array answers the question the caller was
|
||||
/// really asking — <see cref="ResolveSlot"/> — instead: the RHI array
|
||||
/// registered its two (texture, sampler) pairs at construction and returns a
|
||||
/// field.</para>
|
||||
/// </summary>
|
||||
internal interface IWorldTextureArray : IDisposable
|
||||
{
|
||||
|
|
@ -116,9 +116,9 @@ internal interface IWorldTextureArrayFactory
|
|||
ArgumentNullException.ThrowIfNull(graphicsDevice);
|
||||
ArgumentNullException.ThrowIfNull(gpuDevice);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
return graphicsDevice is OpenGLGraphicsDevice gl && gpuDevice is GlGpuDevice table
|
||||
? new GlWorldTextureArrayFactory(gl, table, logger)
|
||||
: new RhiWorldTextureArrayFactory(gpuDevice);
|
||||
// The GL arm this used to select between was deleted at Campaign V
|
||||
// slice V11; the RHI arm is the only one left.
|
||||
return new RhiWorldTextureArrayFactory(gpuDevice);
|
||||
}
|
||||
|
||||
/// <summary>The retirement queue array layers and images are released through.</summary>
|
||||
|
|
@ -132,36 +132,6 @@ internal interface IWorldTextureArrayFactory
|
|||
IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL arm. Delegates to the same <c>OpenGLGraphicsDevice</c> entry point
|
||||
/// <see cref="TextureAtlasManager"/> called directly before this slice, so the
|
||||
/// shipping backend's construction is textually unchanged.
|
||||
/// </summary>
|
||||
internal sealed class GlWorldTextureArrayFactory(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
GlGpuDevice worldTextureTable,
|
||||
ILogger logger) : IWorldTextureArrayFactory
|
||||
{
|
||||
private readonly OpenGLGraphicsDevice _graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
private readonly GlGpuDevice _worldTextureTable = worldTextureTable
|
||||
?? throw new ArgumentNullException(nameof(worldTextureTable));
|
||||
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
public IGpuResourceRetirementQueue Retirement => _graphicsDevice.ResourceRetirement;
|
||||
|
||||
public IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers) =>
|
||||
new ManagedGLTextureArray(
|
||||
_graphicsDevice,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
layers,
|
||||
_logger,
|
||||
_worldTextureTable,
|
||||
TextureParameters.ClampToEdge);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The backend-neutral arm. Creates through <see cref="IGpuDevice.CreateTexture"/>
|
||||
/// and registers both address modes into the device's one texture table, so an
|
||||
|
|
@ -353,11 +323,7 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
|
|||
ArgumentNullException.ThrowIfNull(data);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(layer);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, Size);
|
||||
// The GL array validates the payload against the format's expected byte
|
||||
// count and rejects transfer overrides that contradict it. Reusing that
|
||||
// validator rather than writing a second one keeps the two arms agreeing
|
||||
// on what a well-formed layer is.
|
||||
ManagedGLTextureArray.ValidateUploadPayload(
|
||||
ValidateUploadPayload(
|
||||
SourceFormat,
|
||||
_width,
|
||||
_height,
|
||||
|
|
@ -504,4 +470,65 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
|
|||
+ "and is not part of GpuTextureDescription. Campaign V's world-draw slice owns "
|
||||
+ "extending the contract or proving no such atlas exists."),
|
||||
};
|
||||
|
||||
private static bool IsCompressedFormat(TextureFormat format) =>
|
||||
format is TextureFormat.DXT1 or TextureFormat.DXT3 or TextureFormat.DXT5;
|
||||
|
||||
/// <summary>
|
||||
/// The expected byte count for one uploaded layer of <paramref name="format"/>
|
||||
/// at <paramref name="width"/>x<paramref name="height"/>.
|
||||
/// </summary>
|
||||
internal static int CalculateExpectedDataSize(TextureFormat format, int width, int height)
|
||||
{
|
||||
if (IsCompressedFormat(format))
|
||||
return TextureHelpers.GetCompressedLayerSize(width, height, format);
|
||||
|
||||
return format switch
|
||||
{
|
||||
TextureFormat.RGBA8 => checked(width * height * 4),
|
||||
TextureFormat.RGB8 => checked(width * height * 3),
|
||||
TextureFormat.A8 => checked(width * height),
|
||||
TextureFormat.Rgba32f => checked(width * height * 16),
|
||||
_ => throw new NotSupportedException($"Unsupported format {format}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an upload payload against the format's expected byte count and
|
||||
/// rejects transfer overrides that contradict it.
|
||||
/// </summary>
|
||||
internal static void ValidateUploadPayload(
|
||||
TextureFormat format,
|
||||
int width,
|
||||
int height,
|
||||
int dataLength,
|
||||
PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType)
|
||||
{
|
||||
int expectedBytes = CalculateExpectedDataSize(format, width, height);
|
||||
if (dataLength != expectedBytes)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Texture-array layer payload has {dataLength} bytes; expected exactly {expectedBytes} "
|
||||
+ $"for {format} {width}x{height}.",
|
||||
nameof(dataLength));
|
||||
}
|
||||
|
||||
if (IsCompressedFormat(format))
|
||||
{
|
||||
if (uploadPixelFormat.HasValue || uploadPixelType.HasValue)
|
||||
throw new ArgumentException("Compressed texture uploads cannot specify pixel format/type overrides.");
|
||||
return;
|
||||
}
|
||||
|
||||
PixelFormat expectedFormat = format.ToPixelFormat();
|
||||
PixelType expectedType = format.ToPixelType();
|
||||
if ((uploadPixelFormat ?? expectedFormat) != expectedFormat
|
||||
|| (uploadPixelType ?? expectedType) != expectedType)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Upload descriptor {uploadPixelFormat}/{uploadPixelType} does not match "
|
||||
+ $"the {expectedFormat}/{expectedType} transfer required by {format}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue