From 8a7a0837e121d3f7172d2b710c2305f920a22791 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 02:19:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Vulkan=20campaign=20V11=20step?= =?UTF-8?q?=202=20=E2=80=94=20delete=20the=20OpenGL=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ContentEffectsAudioComposition.cs | 1 - .../Composition/FrameRootComposition.cs | 102 +- .../Composition/GameWindowGraphics.cs | 71 +- .../Composition/HostInputCameraComposition.cs | 98 +- .../InteractionRetainedUiComposition.cs | 1 - .../LivePresentationComposition.cs | 236 ++-- ...VulkanHostInputCameraCompositionFactory.cs | 39 +- .../Composition/WorldRenderComposition.cs | 322 +---- .../Diagnostics/FrameScreenshotController.cs | 183 +-- .../Platform/GraphicalCapabilityRecord.cs | 525 -------- .../Platform/GraphicalGlFunctionProbe.cs | 513 -------- .../GraphicalWindowBackendSelection.cs | 55 + src/AcDream.App/RenderBackendKind.cs | 28 - .../Rendering/BindlessTextureMutationGuard.cs | 68 -- .../Rendering/BindlessTexturePair.cs | 94 -- src/AcDream.App/Rendering/ClipFrame.cs | 633 +--------- .../Rendering/CompositeTextureArrayCache.cs | 207 ---- .../CreatureAppraisalPresentation.cs | 6 +- .../Rendering/FrameProfilerGpuMeasurement.cs | 32 - .../Rendering/GameRenderResourceLifetime.cs | 6 - src/AcDream.App/Rendering/GameWindow.cs | 155 +-- .../Rendering/GameWindowLifetime.cs | 14 +- .../Rendering/GlConstructionCleanupLedger.cs | 102 -- .../Rendering/GlResourceCommand.cs | 152 --- .../GlTextureConstructionTransaction.cs | 85 -- .../Gpu/Gl/GlAmbientCapabilityState.cs | 313 ----- .../Rendering/Gpu/Gl/GlDirtySlotRuns.cs | 108 -- .../Rendering/Gpu/Gl/GlEnumMapping.cs | 145 --- .../Rendering/Gpu/Gl/GlGpuBuffer.cs | 287 ----- .../Rendering/Gpu/Gl/GlGpuDevice.cs | 667 ---------- .../Rendering/Gpu/Gl/GlGpuFrame.cs | 58 - .../Rendering/Gpu/Gl/GlGpuPassEncoder.cs | 316 ----- .../Rendering/Gpu/Gl/GlGpuPipeline.cs | 97 -- .../Gpu/Gl/GlGpuPushConstantBinder.cs | 81 -- .../Rendering/Gpu/Gl/GlGpuRenderTarget.cs | 121 -- .../Rendering/Gpu/Gl/GlGpuSampler.cs | 61 - .../Rendering/Gpu/Gl/GlGpuTexture.cs | 202 ---- .../Gpu/Gl/GlGpuTextureFormatMapping.cs | 56 - .../Rendering/Gpu/Gl/GlGpuTimerPool.cs | 186 --- .../Gpu/Gl/GlPushConstantUniformNames.cs | 72 -- .../Rendering/Gpu/Gl/GlRenderStateCache.cs | 84 -- .../Rendering/Gpu/Gl/GlRingBufferState.cs | 140 --- .../Gpu/Gl/GlTextureSlotAllocator.cs | 62 - src/AcDream.App/Rendering/Gpu/GpuEnums.cs | 3 - .../Rendering/PaperdollFramePresenter.cs | 7 +- .../Rendering/PaperdollViewportRenderer.cs | 5 +- .../Rendering/ParticleRenderer.Rhi.cs | 9 +- src/AcDream.App/Rendering/ParticleRenderer.cs | 930 +------------- .../Rendering/PortalDepthMaskRenderer.cs | 336 +----- .../Rendering/PortalTunnelPresentation.cs | 115 +- .../PrivateEntityViewportRenderer.cs | 94 +- src/AcDream.App/Rendering/RenderBootstrap.cs | 310 ----- .../Rendering/RenderFrameGlStateController.cs | 113 -- .../Rendering/ResourceCleanupGroup.cs | 119 +- .../Rendering/RetailPViewPassExecutor.cs | 1 - src/AcDream.App/Rendering/SamplerCache.cs | 103 -- .../Rendering/SceneLightingUboBinding.cs | 153 +-- src/AcDream.App/Rendering/Shader.cs | 143 --- .../Rendering/ShaderProgramConstruction.cs | 325 ----- src/AcDream.App/Rendering/Sky/SkyRenderer.cs | 307 +---- src/AcDream.App/Rendering/TerrainAtlas.cs | 518 +------- .../Rendering/TerrainModernRenderer.Rhi.cs | 1 - .../Rendering/TerrainModernRenderer.cs | 856 +------------ src/AcDream.App/Rendering/TextRenderer.cs | 3 +- src/AcDream.App/Rendering/TextureCache.cs | 499 +------- .../Rendering/TrackedTextureConstruction.cs | 31 - .../Rendering/Wb/BindlessSupport.cs | 132 -- .../Rendering/Wb/EnvCellRenderer.cs | 826 +------------ src/AcDream.App/Rendering/Wb/GLHelpers.cs | 240 ---- src/AcDream.App/Rendering/Wb/GLSLShader.cs | 258 ---- src/AcDream.App/Rendering/Wb/GLStateScope.cs | 230 ---- .../Rendering/Wb/GlobalMeshBuffer.cs | 225 +--- .../Rendering/Wb/IMeshPipelineDevice.cs | 24 +- .../Rendering/Wb/ManagedGLFrameBuffer.cs | 104 -- .../Rendering/Wb/ManagedGLIndexBuffer.cs | 184 --- .../Rendering/Wb/ManagedGLTexture.cs | 165 --- .../Rendering/Wb/ManagedGLTextureArray.cs | 705 ----------- .../Rendering/Wb/ManagedGLUniformBuffer.cs | 177 --- .../Rendering/Wb/ManagedGLVertexArray.cs | 77 -- .../Rendering/Wb/ManagedGLVertexBuffer.cs | 185 --- .../Rendering/Wb/ModernRenderData.cs | 15 - .../Rendering/Wb/ObjectMeshManager.cs | 257 +--- .../Rendering/Wb/OpenGLGraphicsDevice.cs | 776 ------------ .../Rendering/Wb/RenderStateCache.cs | 26 - .../Rendering/Wb/TrackedGlResource.cs | 283 ----- .../Rendering/Wb/WbDrawDispatcher.cs | 1075 ++--------------- src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs | 55 +- .../Rendering/Wb/WorldTextureArray.cs | 127 +- src/AcDream.App/Rendering/WorldPassSurface.cs | 110 +- src/AcDream.App/RuntimeOptions.cs | 28 - src/AcDream.App/UI/UiHost.cs | 1 - .../ContentEffectsAudioCompositionTests.cs | 5 +- .../HostInputCameraCompositionTests.cs | 5 +- .../Composition/TestGameWindowGraphics.cs | 55 +- .../WorldRenderCompositionTests.cs | 53 +- .../GraphicalCapabilityRequirementsTests.cs | 261 ---- .../Rendering/ClipFrameLayoutTests.cs | 7 +- .../Rendering/ClipFrameUploadTests.cs | 130 -- .../GameWindowRenderLeafCompositionTests.cs | 4 +- .../GameWindowSlice8BoundaryTests.cs | 34 +- .../Rendering/GlTextureOwnershipTests.cs | 417 ------- .../Rendering/Gpu/Gl/GlDirtySlotRunsTests.cs | 198 --- .../Rendering/Gpu/Gl/GlEnumMappingTests.cs | 79 -- .../Gpu/Gl/GlGpuTextureFormatMappingTests.cs | 75 -- .../Rendering/Gpu/Gl/GlGpuTimerPoolTests.cs | 123 -- .../Gpu/Gl/GlPushConstantUniformNamesTests.cs | 35 - .../Gpu/Gl/GlRenderStateCacheTests.cs | 140 --- .../Gpu/Gl/GlRingBufferStateTests.cs | 182 --- .../Gpu/Gl/GlTextureSlotAllocatorTests.cs | 60 - .../GpuResourceRetirementTransactionTests.cs | 82 -- .../Rendering/PortalDepthShaderParityTests.cs | 79 -- .../Rendering/PortalTunnelAssetTests.cs | 21 +- .../RenderFrameGlStateControllerTests.cs | 106 -- .../Rendering/ResourceCleanupGroupTests.cs | 103 +- .../ShaderProgramConstructionTests.cs | 197 --- .../TextRendererFailureSafetyTests.cs | 337 ------ .../Rendering/Wb/EnvCellRendererTests.cs | 108 +- .../Wb/MeshPipelineDeviceSeamTests.cs | 20 - .../Rendering/Wb/TextureAtlasCapacityTests.cs | 16 +- .../AcDream.App.Tests/RuntimeOptionsTests.cs | 69 -- .../Rendering/TextureCacheBindlessTests.cs | 32 - 121 files changed, 1243 insertions(+), 19840 deletions(-) delete mode 100644 src/AcDream.App/Platform/GraphicalCapabilityRecord.cs delete mode 100644 src/AcDream.App/Platform/GraphicalGlFunctionProbe.cs delete mode 100644 src/AcDream.App/RenderBackendKind.cs delete mode 100644 src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs delete mode 100644 src/AcDream.App/Rendering/BindlessTexturePair.cs delete mode 100644 src/AcDream.App/Rendering/FrameProfilerGpuMeasurement.cs delete mode 100644 src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs delete mode 100644 src/AcDream.App/Rendering/GlResourceCommand.cs delete mode 100644 src/AcDream.App/Rendering/GlTextureConstructionTransaction.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlDirtySlotRuns.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlEnumMapping.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuBuffer.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuFrame.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuPipeline.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuPushConstantBinder.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuRenderTarget.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuSampler.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuTexture.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuTextureFormatMapping.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlGpuTimerPool.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlPushConstantUniformNames.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlRingBufferState.cs delete mode 100644 src/AcDream.App/Rendering/Gpu/Gl/GlTextureSlotAllocator.cs delete mode 100644 src/AcDream.App/Rendering/RenderBootstrap.cs delete mode 100644 src/AcDream.App/Rendering/RenderFrameGlStateController.cs delete mode 100644 src/AcDream.App/Rendering/SamplerCache.cs delete mode 100644 src/AcDream.App/Rendering/Shader.cs delete mode 100644 src/AcDream.App/Rendering/ShaderProgramConstruction.cs delete mode 100644 src/AcDream.App/Rendering/TrackedTextureConstruction.cs delete mode 100644 src/AcDream.App/Rendering/Wb/BindlessSupport.cs delete mode 100644 src/AcDream.App/Rendering/Wb/GLHelpers.cs delete mode 100644 src/AcDream.App/Rendering/Wb/GLSLShader.cs delete mode 100644 src/AcDream.App/Rendering/Wb/GLStateScope.cs delete mode 100644 src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs delete mode 100644 src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs delete mode 100644 src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs delete mode 100644 src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs delete mode 100644 src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs delete mode 100644 src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs delete mode 100644 src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs delete mode 100644 src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs delete mode 100644 src/AcDream.App/Rendering/Wb/RenderStateCache.cs delete mode 100644 src/AcDream.App/Rendering/Wb/TrackedGlResource.cs delete mode 100644 tests/AcDream.App.Tests/Platform/GraphicalCapabilityRequirementsTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/ClipFrameUploadTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlDirtySlotRunsTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlEnumMappingTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTextureFormatMappingTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTimerPoolTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlPushConstantUniformNamesTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRingBufferStateTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlTextureSlotAllocatorTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/PortalDepthShaderParityTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/RenderFrameGlStateControllerTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/ShaderProgramConstructionTests.cs delete mode 100644 tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs delete mode 100644 tests/AcDream.Core.Tests/Rendering/TextureCacheBindlessTests.cs diff --git a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs index 9265c1f2..ab545721 100644 --- a/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs +++ b/src/AcDream.App/Composition/ContentEffectsAudioComposition.cs @@ -16,7 +16,6 @@ using AcDream.Runtime; using AcDream.Runtime.Gameplay; using DatReaderWriter; using Silk.NET.Input; -using Silk.NET.OpenGL; namespace AcDream.App.Composition; diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index 0e56982c..7248f712 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -15,7 +15,6 @@ using AcDream.Core.Physics; using AcDream.Core.Selection; using AcDream.Core.World; using Silk.NET.Input; -using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace AcDream.App.Composition; @@ -236,20 +235,15 @@ internal sealed class FrameRootCompositionPhase { FrameRootDependencies d = _dependencies; WorldRenderFoundation foundation = world.Foundation; - // Campaign V slice V6h: the frame root is the one seam that genuinely - // forks. On GL every world renderer exists and the render graph is - // unchanged. On Vulkan none of them does, so the graph is the clear pass, - // the private-presentation phase, and the retained UI inside it — the - // client's own frame, drawn entirely through the RHI. - GL? gl = d.Graphics.Gl; + // Campaign V slice V6h: the frame root's raw-GL render graph fork was + // deleted at slice V11. The graph is the clear pass, the private- + // presentation phase, and the retained UI inside it — the client's + // own frame, drawn entirely through the RHI. var teleportRenderState = new LocalPlayerTeleportRenderStateSource(session.LocalTeleport); var renderLoginState = new RenderLoginStateSource( d.Options.LiveMode, d.PlayerMode); - RenderFrameGlStateController? renderFrameGlState = gl is null - ? null - : new RenderFrameGlStateController(new SilkRenderFrameGlStateApi(gl)); // Campaign V slice V6i-3: on Vulkan the frame's clear is a load op of the // world pass rather than a pass of its own, so the two phases share this // one value. See VulkanWorldScenePhase for why the merge is required @@ -268,18 +262,8 @@ internal sealed class FrameRootCompositionPhase live.ParticleRenderer, d.FrameProfiler, d.FrameDiagnosticsEnabled); - IRenderFrameClearPhase clearPhase = gl is not null - ? new RuntimeRenderFrameClearPhase( - gl, - d.WorldTime, - d.Weather, - teleportRenderState, - d.ParticleVisibility, - host.WorldRenderDiagnostics - ?? throw new InvalidOperationException( - "The GL frame root requires the GL state tripwire."), - renderFrameGlState!) - : new AcDream.App.Rendering.Gpu.Vk.VulkanRenderFrameClearPhase( + IRenderFrameClearPhase clearPhase = + new AcDream.App.Rendering.Gpu.Vk.VulkanRenderFrameClearPhase( d.WorldTime, d.Weather, teleportRenderState, @@ -335,27 +319,17 @@ internal sealed class FrameRootCompositionPhase ?? new WorldRenderDiagnostics( NullRenderGlStateReader.Instance, d.RenderDiagnosticLog); - IRenderFrameGlState worldFrameGlState = - (IRenderFrameGlState?)renderFrameGlState - ?? NullRenderFrameGlState.Instance; + IRenderFrameGlState worldFrameGlState = NullRenderFrameGlState.Instance; IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope; var worldFramebufferSource = new SilkRetailPViewFramebufferSource(d.Window); - IWorldPassSurface worldPassSurface = gl is not null - ? new GlWorldPassSurface( - gl, - live.ClipFrame, - worldFramebufferSource, - live.DrawDispatcher!, - live.EnvCellRenderer!, - foundation.Terrain) - : new RhiWorldPassSurface( - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope."), - host.GpuFrameLifetime, - live.ClipFrame, - worldFramebufferSource); + IWorldPassSurface worldPassSurface = new RhiWorldPassSurface( + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), + host.GpuFrameLifetime, + live.ClipFrame, + worldFramebufferSource); var worldFrameEnvironment = new RuntimeWorldFrameEnvironmentPreparation( d.Options, @@ -438,7 +412,7 @@ internal sealed class FrameRootCompositionPhase // V11, so nothing is lost — composing it would throw on the // first wireframe frame rather than silently misdraw (plan // §5.5.14 item 7). - gl is not null ? foundation.DebugLines : null, + null, d.PhysicsEngine, d.PlayerMode, d.PlayerController, @@ -488,21 +462,17 @@ internal sealed class FrameRootCompositionPhase d.RenderRange, worldSceneDiagnostics, live.WorldAvailability); - if (gl is null) - { - // On Vulkan the world renderer runs INSIDE the frame's one - // backbuffer pass, which this phase opens, publishes on the - // scope, and closes. - worldSceneRenderer = - new AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase( - host.GpuFrameLifetime, - vulkanClear, - () => d.Graphics.Vulkan?.SampleCount ?? 1, - (d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore - ?? throw new InvalidOperationException( - "The Vulkan world phase requires the Vulkan graphics handle."), - worldSceneRenderer); - } + // The world renderer runs INSIDE the frame's one backbuffer pass, + // which this phase opens, publishes on the scope, and closes. + worldSceneRenderer = + new AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase( + host.GpuFrameLifetime, + vulkanClear, + () => d.Graphics.Vulkan?.SampleCount ?? 1, + (d.Graphics as VulkanGameWindowGraphics)?.WorldPassScopeCore + ?? throw new InvalidOperationException( + "The Vulkan world phase requires the Vulkan graphics handle."), + worldSceneRenderer); } Fault(FrameRootCompositionPoint.WorldRendererCreated); bindings = new FrameRootRuntimeBindings(); @@ -591,17 +561,15 @@ internal sealed class FrameRootCompositionPhase ?? NullRenderFramePostDiagnosticsPhase.Instance; var renderFrame = new RenderFrameOrchestrator( host.GpuFrameLifetime, - gl is not null - ? new FrameProfilerGpuMeasurement(d.FrameProfiler, gl) - // Campaign V slice V8: the Vulkan arm measures the same bracket - // through its own timestamp scope. It stayed on the null adapter - // from V6h until V8, which meant no [frame-prof] line existed on - // Vulkan at all — see VulkanFrameGpuMeasurement. - : d.Graphics.Vulkan is { } vulkanGraphics - ? new AcDream.App.Rendering.Gpu.Vk.VulkanFrameGpuMeasurement( - d.FrameProfiler, - vulkanGraphics.Device) - : AcDream.App.Rendering.Gpu.Vk.NullRenderFrameGpuMeasurement.Instance, + // Campaign V slice V8: the Vulkan arm measures the frame bracket + // through its own timestamp scope — see VulkanFrameGpuMeasurement. + // The raw-GL adapter (FrameProfilerGpuMeasurement) was deleted at + // slice V11. + d.Graphics.Vulkan is { } vulkanGraphics + ? new AcDream.App.Rendering.Gpu.Vk.VulkanFrameGpuMeasurement( + d.FrameProfiler, + vulkanGraphics.Device) + : AcDream.App.Rendering.Gpu.Vk.NullRenderFrameGpuMeasurement.Instance, framePreparation, worldSceneRenderer, privatePresentation, diff --git a/src/AcDream.App/Composition/GameWindowGraphics.cs b/src/AcDream.App/Composition/GameWindowGraphics.cs index 6070adb2..5c174416 100644 --- a/src/AcDream.App/Composition/GameWindowGraphics.cs +++ b/src/AcDream.App/Composition/GameWindowGraphics.cs @@ -1,79 +1,32 @@ using AcDream.App.Rendering.Gpu.Vk; -using Silk.NET.OpenGL; namespace AcDream.App.Composition; /// -/// Campaign V slice V6h: the graphics ownership that platform acquisition -/// publishes, once per backend. +/// The graphics ownership that platform acquisition publishes. /// -/// was already generic over its -/// graphics type; only the call sites pinned GL. This class is what they -/// pin instead, so one composition pipeline drives both backends and the fork -/// lives at the handful of construction sites that genuinely differ rather than -/// in a second copy of the startup topology. -/// -/// It is deliberately NOT a capability abstraction. Nothing dispatches on -/// it per frame; phases that still speak raw GL ask for the context by name and -/// take their Vulkan arm when it is absent. The GL arm is deleted at slice V11 -/// and this class with it. +/// Vulkan is the only backend as of Campaign V slice V11: the raw-GL +/// implementation (OpenGlGameWindowGraphics) and the members that only +/// existed to branch on it (Backend, Gl, RequireGl) were +/// deleted along with it. This class stays as the seam +/// and the composition phases are +/// already written against, so a future second backend would still add one +/// class of this shape rather than reopening every call site. /// internal abstract class GameWindowGraphics : IDisposable { - /// Which backend this handle owns. - public abstract RenderBackendKind Backend { get; } - - /// - /// The live GL context, or null on any other backend. Phases whose owners - /// are still raw GL branch on this; each such branch is a slice of Campaign - /// V that has not landed yet, and the null arm names which one. - /// - public virtual GL? Gl => null; - - /// The live Vulkan context, or null on any other backend. + /// The live Vulkan context. public virtual VulkanGraphicsContext? Vulkan => null; /// - /// Campaign V slice V6j: the backend's world-pass seam, or null where the - /// world renderers open their own passes. - /// - /// It lives here because the three composition phases that need it — - /// world render, live presentation and the frame root — already borrow this - /// handle, and because whether a backend HAS such a seam is exactly the kind - /// of thing this type exists to answer. GL returns null: its renderers - /// submit raw, and the frame spine still owns framebuffer management until - /// V4h. + /// The backend's world-pass seam. Every remaining renderer records into the + /// pass this publishes rather than opening its own. /// public virtual AcDream.App.Rendering.IWorldPassScope? WorldPassScope => null; - /// - /// The GL context, or a failure naming the caller. Used where the call site - /// has already established that the GL arm is running, so a null would be a - /// composition bug rather than a backend difference. - /// - public GL RequireGl(string owner) => - Gl ?? throw new InvalidOperationException( - $"'{owner}' requires the OpenGL context, but the {Backend} backend is active."); - public abstract void Dispose(); } -/// OpenGL ownership: the Silk.NET context itself. -internal sealed class OpenGlGameWindowGraphics : GameWindowGraphics -{ - public OpenGlGameWindowGraphics(GL gl) => - Context = gl ?? throw new ArgumentNullException(nameof(gl)); - - /// The owned context. is the borrowed view. - public GL Context { get; } - - public override RenderBackendKind Backend => RenderBackendKind.Gl; - - public override GL? Gl => Context; - - public override void Dispose() => Context.Dispose(); -} - /// /// Vulkan ownership: the instance, surface, device, swapchain and RHI device /// that acquired and gated. @@ -91,8 +44,6 @@ internal sealed class VulkanGameWindowGraphics : GameWindowGraphics public VulkanGraphicsContext Context { get; } - public override RenderBackendKind Backend => RenderBackendKind.Vulkan; - public override VulkanGraphicsContext? Vulkan => Context; /// The concrete scope, for the phase that publishes the encoder on it. diff --git a/src/AcDream.App/Composition/HostInputCameraComposition.cs b/src/AcDream.App/Composition/HostInputCameraComposition.cs index af96be07..7be703e7 100644 --- a/src/AcDream.App/Composition/HostInputCameraComposition.cs +++ b/src/AcDream.App/Composition/HostInputCameraComposition.cs @@ -4,7 +4,6 @@ using AcDream.App.Rendering.Gpu; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; using Silk.NET.Maths; -using Silk.NET.OpenGL; namespace AcDream.App.Composition; @@ -115,91 +114,6 @@ internal interface IHostInputCameraCompositionFactory PointerPositionState pointer); } -internal sealed class RetailHostInputCameraCompositionFactory - : IHostInputCameraCompositionFactory -{ - public IFramebufferViewportTarget CreateViewportTarget(GameWindowGraphics graphics) => - new SilkFramebufferViewportTarget(graphics.RequireGl("viewport target")); - - public GpuFrameFlightController? CreateGpuFrameFlights(GameWindowGraphics graphics) => - new(graphics.RequireGl("GPU frame flights")); - - public IGpuDevice CreateGpuDevice( - GameWindowGraphics graphics, - GpuFrameFlightController? frameFlights) => - new AcDream.App.Rendering.Gpu.Gl.GlGpuDevice( - graphics.RequireGl("GPU device (RHI)"), - frameFlights ?? throw new ArgumentNullException(nameof(frameFlights)), - Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders")); - - public IGpuResourceRetirementQueue CreateRetirement( - GameWindowGraphics graphics, - GpuFrameFlightController? frameFlights, - IGpuDevice device) => - frameFlights ?? throw new ArgumentNullException(nameof(frameFlights)); - - public IRenderFrameSlotSource CreateFrameSlots( - GameWindowGraphics graphics, - GpuFrameFlightController? frameFlights, - IGpuDevice device) => - frameFlights ?? throw new ArgumentNullException(nameof(frameFlights)); - - public WorldRenderDiagnostics? CreateWorldRenderDiagnostics( - GameWindowGraphics graphics, - IRenderFrameDiagnosticLog log) => - new( - new SilkRenderGlStateReader( - graphics.RequireGl("world render diagnostics")), - log); - - public SilkKeyboardSource CreateKeyboardSource( - IKeyboard keyboard, - HostQuiescenceGate quiescence) => - SilkKeyboardSource.CreateDetached(keyboard, quiescence); - - public SilkMouseSource CreateMouseSource( - IMouse mouse, - IInputCaptureSource capture, - IKeyboardSource? keyboard, - HostQuiescenceGate quiescence) => - SilkMouseSource.CreateDetached(mouse, capture, keyboard, quiescence); - - public IMouseLookCursor CreateMouseLookCursor(IMouse mouse) => - new SilkMouseLookCursor(mouse); - - public InputDispatcher CreateInputDispatcher( - IKeyboardSource keyboard, - IMouseSource mouse, - KeyBindings bindings) => - InputDispatcher.CreateDetached(keyboard, mouse, bindings); - - public CameraController CreateCameraController() => - new(new OrbitCamera(), new FlyCamera()); - - public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) => - new CameraFramebufferTarget(camera); - - public CameraPointerInputController CreateCameraPointerInput( - IReadOnlyList mice, - HostQuiescenceGate quiescence, - IInputCaptureSource capture, - LocalPlayerModeState playerMode, - CameraController camera, - ChaseCameraInputState chase, - IMouseSource mouse, - PointerPositionState pointer) => - CameraPointerInputController.Create( - mice, - quiescence, - capture, - playerMode, - camera, - chase, - mouse, - pointer, - new EnvironmentInputMonotonicClock()); -} - internal enum HostInputCameraCompositionPoint { ViewportBound, @@ -236,7 +150,7 @@ internal sealed class HostInputCameraCompositionPhase : private readonly IHostInputCameraCompositionFactory? _injectedFactory; private readonly Action? _faultInjection; private IHostInputCameraCompositionFactory _factory = - new RetailHostInputCameraCompositionFactory(); + new VulkanHostInputCameraCompositionFactory(); public HostInputCameraCompositionPhase( HostInputCameraDependencies dependencies, @@ -253,17 +167,13 @@ internal sealed class HostInputCameraCompositionPhase : } /// - /// Campaign V slice V6h: the default factory is chosen from the platform - /// result, not from the host. The backend is a property of the graphics - /// ownership that acquisition published, so the phase reads it rather than - /// making every call site branch — and an injected factory (composition + /// The Vulkan factory is the only one left since the raw-GL arm was + /// deleted at Campaign V slice V11; an injected factory (composition /// tests) still wins. /// private static IHostInputCameraCompositionFactory DefaultFactoryFor( GameWindowGraphics graphics) => - graphics.Backend == RenderBackendKind.Vulkan - ? new VulkanHostInputCameraCompositionFactory() - : new RetailHostInputCameraCompositionFactory(); + new VulkanHostInputCameraCompositionFactory(); public HostInputCameraResult Compose( GameWindowPlatformResult platform) diff --git a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs index 792ad2e0..535121ef 100644 --- a/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs +++ b/src/AcDream.App/Composition/InteractionRetainedUiComposition.cs @@ -25,7 +25,6 @@ using AcDream.UI.Abstractions.Panels.Chat; using AcDream.UI.Abstractions.Panels.Vitals; using DatReaderWriter; using Silk.NET.Input; -using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace AcDream.App.Composition; diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index c00325b0..b1cfe102 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -30,7 +30,6 @@ using AcDream.Runtime.Entities; using AcDream.Runtime.World; using DatReaderWriter; using DatReaderWriter.DBObjs; -using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace AcDream.App.Composition; @@ -678,46 +677,28 @@ internal sealed class LivePresentationCompositionPhase AlphaScratchBudgetProfile.Create( d.Options.ResidencyBudgets.AlphaScratchBytes); - // Campaign V slice V6h: every renderer below this line is still raw GL. - // On a backend without a GL context none of them is constructed, and the - // CPU owners this phase also produces — entity lifetime, motion, - // selection, effects, lights, the landblock pipeline — run unchanged. - GL? gl = d.Graphics.Gl; var selectionScene = new RetailSelectionScene( new RetailSelectionGeometryCache(content.Dats, d.DatLock)); - // Campaign V slice V6j: the world dispatcher exists on BOTH arms. The GL - // arm is unchanged; the RHI arm records into the pass the world scene - // phase publishes on this scope. + // Campaign V slice V6j: the world dispatcher records into the pass the + // world scene phase publishes on this scope. The raw-GL arm was + // deleted at slice V11. IWorldPassScope? worldPassScope = d.Graphics.WorldPassScope; var dispatcherLease = scope.Acquire( "WB draw dispatcher", - () => gl is not null - ? new WbDrawDispatcher( - gl, - foundation.MeshShader!, - foundation.TextureCache, - foundation.MeshAdapter!, - entitySpawnAdapter, - foundation.Bindless!, - d.ClassificationCache, - d.TranslucencyFades, - selectionScene, - d.RetailAlphaQueue, - alphaScratchBudgets.DispatcherBytes) - : new WbDrawDispatcher( - host.GpuDevice, - host.GpuFrameLifetime, - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope."), - foundation.TextureCache, - foundation.MeshAdapter!, - entitySpawnAdapter, - d.ClassificationCache, - d.TranslucencyFades, - selectionScene, - d.RetailAlphaQueue, - alphaScratchBudgets.DispatcherBytes), + () => new WbDrawDispatcher( + host.GpuDevice, + host.GpuFrameLifetime, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), + foundation.TextureCache, + foundation.MeshAdapter!, + entitySpawnAdapter, + d.ClassificationCache, + d.TranslucencyFades, + selectionScene, + d.RetailAlphaQueue, + alphaScratchBudgets.DispatcherBytes), static value => value.Dispose()); var selectionQuery = new WorldSelectionQuery( liveEntities, @@ -815,8 +796,9 @@ internal sealed class LivePresentationCompositionPhase paperdollLease = scope.Acquire( "paperdoll viewport", () => new PaperdollViewportRenderer( - gl, - worldPassScope, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), host.GpuDevice, host.GpuFrameLifetime, paperdollDispatcher, @@ -861,8 +843,9 @@ internal sealed class LivePresentationCompositionPhase creatureAppraisalLease = scope.Acquire( "creature appraisal viewport", () => new CreatureAppraisalViewportRenderer( - gl, - worldPassScope, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), host.GpuDevice, host.GpuFrameLifetime, appraisalDispatcher, @@ -897,24 +880,17 @@ internal sealed class LivePresentationCompositionPhase var envCellFrustum = new WbFrustum(); var envCellLease = scope.Acquire( "environment-cell renderer", - () => gl is not null - ? new EnvCellRenderer( - gl, - foundation.MeshAdapter!.MeshManager!, - envCellFrustum) - : new EnvCellRenderer( - host.GpuDevice, - host.GpuFrameLifetime, - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope."), - foundation.MeshAdapter!.MeshManager!, - envCellFrustum), + () => new EnvCellRenderer( + host.GpuDevice, + host.GpuFrameLifetime, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), + foundation.MeshAdapter!.MeshManager!, + envCellFrustum), static value => value.Dispose()); - // The RHI arm's three pipelines ARE its program, built at construction, - // so only the GL arm has a second initialisation step. - if (foundation.MeshShader is { } envCellShader) - envCellLease.Resource.Initialize(envCellShader); + // The three pipelines ARE its program, built at construction — the + // raw-GL arm's separate Initialize(Shader) step was deleted at V11. Fault(LivePresentationCompositionPoint.EnvironmentCellsCreated); // The streaming pipeline itself is backend-neutral and runs on both @@ -972,20 +948,18 @@ internal sealed class LivePresentationCompositionPhase "portal clip frame", ClipFrame.NoClip, static value => value.Dispose()); - // Campaign V slice V6l: the portal depth mask exists on BOTH arms. The - // GL arm keeps its inline program and raw draws; the RHI arm compiles - // portal_depth from SPIR-V into three pipelines and records into the - // pass the world scene phase publishes on this scope. + // Campaign V slice V6l: the portal depth mask compiles portal_depth + // from SPIR-V into three pipelines and records into the pass the + // world scene phase publishes on this scope. The raw-GL inline + // program and raw draws were deleted at slice V11. var portalDepthLease = scope.Acquire( "portal depth mask", - () => gl is not null - ? new PortalDepthMaskRenderer(gl) - : new PortalDepthMaskRenderer( - host.GpuDevice, - host.GpuFrameLifetime, - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope.")), + () => new PortalDepthMaskRenderer( + host.GpuDevice, + host.GpuFrameLifetime, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope.")), static value => value.Dispose()); CompositionAcquisitionScope.CompositionAcquisitionLease< PortalWaitNoticeController>? portalWaitNoticeLease = null; @@ -1003,11 +977,9 @@ internal sealed class LivePresentationCompositionPhase : null; CompositionAcquisitionScope.CompositionAcquisitionLease< PortalTunnelPresentation>? portalTunnelLease = null; - // Campaign V slice V6m: portal space exists on BOTH arms. The GL arm is - // unchanged; the RHI arm opens a backbuffer pass of its own and publishes - // it on the scope for the span of the draw, the way the two offscreen - // viewports do. It was the last raw-GL world-adjacent renderer, so the - // Vulkan arm no longer composes a portal-less teleport presentation. + // Campaign V slice V6m: portal space opens a backbuffer pass of its + // own and publishes it on the scope for the span of the draw, the way + // the two offscreen viewports do. The raw-GL arm was deleted at V11. if (dispatcherLease.Resource is { } portalDispatcher) { PortalTunnelPresentation portalTunnel; @@ -1015,8 +987,9 @@ internal sealed class LivePresentationCompositionPhase { portalTunnel = d.PortalTunnelFallback.AcquirePrepared( () => PortalTunnelPresentation.CreateRequired( - gl, - worldPassScope, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), host.GpuFrameLifetime, content.Dats, content.AnimationLoader, @@ -1051,87 +1024,45 @@ internal sealed class LivePresentationCompositionPhase } Fault(LivePresentationCompositionPoint.PortalResourcesCreated); - AcDream.App.Rendering.Shader? skyShader = gl is null - ? null - : d.RenderResourceLifetime.AcquireSkyShader( - () => new AcDream.App.Rendering.Shader( - gl, - Path.Combine(foundation.ShadersDirectory, "sky.vert"), - Path.Combine(foundation.ShadersDirectory, "sky.frag"), - // Campaign V slice V6e: sky reads the shared texture table and - // ACDREAM_UBO_SET now, both of which common.glsl declares. - includeCommonPreamble: true)); - var skyShaderLease = skyShader is null - ? null - : scope.Own( - "sky shader lifetime", - skyShader, - _ => d.RenderResourceLifetime.ReleaseSkyShader()); - // Campaign V slice V6k: the sky exists on BOTH arms. The GL arm is - // unchanged except for where its table slots come from; the RHI arm - // compiles the sky pair from SPIR-V and records into the pass the world - // scene phase publishes on this scope. + // Campaign V slice V6k: the sky compiles its pair from SPIR-V and + // records into the pass the world scene phase publishes on this + // scope. The raw-GL arm — its own shader pair (acquired through + // GameRenderResourceLifetime.AcquireSkyShader, deleted with it) and + // its own bindless/device-table wiring — was deleted at slice V11. var skyLease = scope.Acquire( "sky renderer", - () => gl is not null - ? new SkyRenderer( - gl, - content.Dats, - skyShader!, - foundation.TextureCache, - foundation.Samplers!, - // Slice V6e: the sky samples through the shared texture table, - // so it needs the same bindless entry point the world path uses. - foundation.Bindless!, - // Slice V6k: and V4t's world-handle seam on the device, which - // retired the last per-renderer GlBindlessHandleTable. - (AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)host.GpuDevice) - { - // Campaign V slice V7: null unless ACDREAM_SKY_PHASE_SECONDS - // is set, which is every run but a differential gate's. - AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds, - } - : new SkyRenderer( - host.GpuDevice, - host.GpuFrameLifetime, - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope."), - content.Dats, - foundation.TextureCache) - { - AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds, - }, + () => new SkyRenderer( + host.GpuDevice, + host.GpuFrameLifetime, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), + content.Dats, + foundation.TextureCache) + { + // Campaign V slice V7: null unless ACDREAM_SKY_PHASE_SECONDS + // is set, which is every run but a differential gate's. + AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds, + }, static value => value.Dispose()); - // Campaign V slice V6l: particles exist on BOTH arms. The GL arm is - // unchanged; the RHI arm compiles the two particle pairs from SPIR-V, - // draws instances through the vertex binding the V6l contract amendment - // added, and records into the pass the world scene phase publishes on - // this scope. + // Campaign V slice V6l: the RHI arm compiles the two particle pairs + // from SPIR-V, draws instances through the vertex binding the V6l + // contract amendment added, and records into the pass the world scene + // phase publishes on this scope. The raw-GL arm was deleted at V11. var particleLease = scope.AcquireOptional( "particle renderer", - () => gl is not null - ? new ParticleRenderer( - gl, - foundation.ShadersDirectory, - content.ParticleSystem, - foundation.TextureCache, - content.Dats, - foundation.MeshAdapter!, - d.RetailAlphaQueue, - alphaScratchBudgets.ParticleBytes) - : new ParticleRenderer( - host.GpuDevice, - host.GpuFrameLifetime, - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope."), - content.ParticleSystem, - foundation.TextureCache, - content.Dats, - foundation.MeshAdapter!, - d.RetailAlphaQueue, - alphaScratchBudgets.ParticleBytes), + () => new ParticleRenderer( + host.GpuDevice, + host.GpuFrameLifetime, + worldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."), + content.ParticleSystem, + foundation.TextureCache, + content.Dats, + foundation.MeshAdapter!, + d.RetailAlphaQueue, + alphaScratchBudgets.ParticleBytes), static value => value.Dispose()); Fault(LivePresentationCompositionPoint.SkyAndParticlesCreated); @@ -1252,7 +1183,6 @@ internal sealed class LivePresentationCompositionPhase portalTunnelLease?.Transfer(); portalWaitNoticeLease?.Transfer(); skyLease.Transfer(); - skyShaderLease?.Transfer(); particleLease.Transfer(); bindingsLease.Transfer(); Fault(LivePresentationCompositionPoint.ResultPublished); diff --git a/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs b/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs index 74cffdec..6a19956d 100644 --- a/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs +++ b/src/AcDream.App/Composition/VulkanHostInputCameraCompositionFactory.cs @@ -8,28 +8,22 @@ using Silk.NET.Input; namespace AcDream.App.Composition; /// -/// Campaign V slice V6h: the Vulkan arm of the host phase. -/// -/// This is the whole of the backend fork at composition Phase 1. Only the -/// four graphics members differ from -/// ; input, camera and -/// pointer construction are platform concerns, not graphics ones, so they -/// delegate rather than duplicate. Adding a fifth backend would add one more -/// class of this shape and touch nothing else in the composition pipeline — -/// which is the property §5.5.9 asked this slice to establish. +/// Campaign V slice V6h: the Vulkan arm of the host phase — now the only arm, +/// the raw-GL RetailHostInputCameraCompositionFactory it used to fork +/// from having been deleted at slice V11. Input, camera and pointer +/// construction are inlined directly rather than delegated, since there is no +/// longer a second implementation to share them with. /// /// What is absent, and why. There is no GL fence ring: the RHI /// device owns its own frames-in-flight through a timeline semaphore, so /// retirement and slot indexing come from the device instead. There is no -/// : it is a raw-GL state tripwire, and +/// : it was a raw-GL state tripwire, and /// Vulkan has no global state for it to watch. Both nulls are read by the /// phases that would otherwise consume them. /// internal sealed class VulkanHostInputCameraCompositionFactory : IHostInputCameraCompositionFactory { - private readonly RetailHostInputCameraCompositionFactory _platform = new(); - public IFramebufferViewportTarget CreateViewportTarget( GameWindowGraphics graphics) => // The viewport is a pipeline dynamic state on Vulkan, set per pass by @@ -64,29 +58,29 @@ internal sealed class VulkanHostInputCameraCompositionFactory public SilkKeyboardSource CreateKeyboardSource( IKeyboard keyboard, HostQuiescenceGate quiescence) => - _platform.CreateKeyboardSource(keyboard, quiescence); + SilkKeyboardSource.CreateDetached(keyboard, quiescence); public SilkMouseSource CreateMouseSource( IMouse mouse, IInputCaptureSource capture, IKeyboardSource? keyboard, HostQuiescenceGate quiescence) => - _platform.CreateMouseSource(mouse, capture, keyboard, quiescence); + SilkMouseSource.CreateDetached(mouse, capture, keyboard, quiescence); public IMouseLookCursor CreateMouseLookCursor(IMouse mouse) => - _platform.CreateMouseLookCursor(mouse); + new SilkMouseLookCursor(mouse); public InputDispatcher CreateInputDispatcher( IKeyboardSource keyboard, IMouseSource mouse, KeyBindings bindings) => - _platform.CreateInputDispatcher(keyboard, mouse, bindings); + InputDispatcher.CreateDetached(keyboard, mouse, bindings); public CameraController CreateCameraController() => - _platform.CreateCameraController(); + new(new OrbitCamera(), new FlyCamera()); public IFramebufferCameraTarget CreateCameraTarget(CameraController camera) => - _platform.CreateCameraTarget(camera); + new CameraFramebufferTarget(camera); public CameraPointerInputController CreateCameraPointerInput( IReadOnlyList mice, @@ -97,7 +91,7 @@ internal sealed class VulkanHostInputCameraCompositionFactory ChaseCameraInputState chase, IMouseSource mouse, PointerPositionState pointer) => - _platform.CreateCameraPointerInput( + CameraPointerInputController.Create( mice, quiescence, capture, @@ -105,14 +99,15 @@ internal sealed class VulkanHostInputCameraCompositionFactory camera, chase, mouse, - pointer); + pointer, + new EnvironmentInputMonotonicClock()); private static VulkanGraphicsContext RequireContext( GameWindowGraphics graphics) => graphics.Vulkan ?? throw new InvalidOperationException( - "The Vulkan host factory was composed against the " + - $"{graphics.Backend} backend."); + "The Vulkan host factory was composed against a backend with no " + + "Vulkan context."); /// /// Vulkan sets the viewport per pass from the pass extent, so there is no diff --git a/src/AcDream.App/Composition/WorldRenderComposition.cs b/src/AcDream.App/Composition/WorldRenderComposition.cs index 7b72ce07..47513e25 100644 --- a/src/AcDream.App/Composition/WorldRenderComposition.cs +++ b/src/AcDream.App/Composition/WorldRenderComposition.cs @@ -14,7 +14,6 @@ using DatReaderWriter.DBObjs; using Microsoft.Extensions.Logging.Abstractions; using Silk.NET.Input; using Silk.NET.OpenGL; -using Shader = AcDream.App.Rendering.Shader; namespace AcDream.App.Composition; @@ -31,27 +30,21 @@ internal sealed record WorldTerrainBuildContext( /// /// The render foundation the later phases build on. /// -/// Campaign V slice V6h made every raw-GL member nullable. They are all -/// present on GL and all absent on Vulkan, because the world renderers that own -/// them are still raw GL until slices V4t/V4c/V4d land the Vulkan world arm. -/// What survives on both backends is exactly the RHI-ported set — the texture -/// cache's UI path, the debug font, the text renderer and the debug lines — plus -/// the backend-neutral residency ledger and shader directory. +/// Every member here is backend-neutral. The raw-GL-only members this +/// record used to carry (Bindless, TerrainShader, MeshShader, +/// Samplers) were deleted at Campaign V slice V11 along with the GL arm +/// that was their only producer. /// internal sealed record WorldRenderFoundation( string ShadersDirectory, - BindlessSupport? Bindless, TerrainAtlas? TerrainAtlas, - Shader? TerrainShader, SceneLightingUboBinding? SceneLighting, DebugLineRenderer DebugLines, BitmapFont? DebugFont, TextRenderer? TextRenderer, TerrainModernRenderer? Terrain, - Shader? MeshShader, WbMeshAdapter? MeshAdapter, TextureCache TextureCache, - SamplerCache? Samplers, ResidencyManager Residency); internal sealed record WorldRenderResult( @@ -71,8 +64,6 @@ internal sealed record WorldRenderDependencies( internal interface IGameWindowWorldRenderPublication { - void PublishBindlessSupport(BindlessSupport value); - void PublishTerrainShader(Shader value); void PublishSceneLighting(SceneLightingUboBinding value); void PublishDebugLines(DebugLineRenderer value); void PublishHudResources(BitmapFont font, TextRenderer text); @@ -81,27 +72,20 @@ internal interface IGameWindowWorldRenderPublication float[] heightTable, TerrainBlendingContext blending, ConcurrentDictionary surfaceCache); - void PublishMeshShader(Shader value); void PublishWbMeshAdapter(WbMeshAdapter value); void PublishTextureCache(TextureCache value); - void PublishSamplerCache(SamplerCache value); } internal interface IWorldRenderCompositionFactory { - void InitializeGlState(GL gl); WorldRegionData LoadRegion(IDatReaderWriter dats); void InitializeEnvironment(WorldEnvironmentController environment, Region region); - BindlessSupport RequireBindless(GL gl, Action log); - TerrainAtlas AcquireTerrainAtlas( - IGameRenderResourceLifetime lifetime, - GL gl, - IDatReaderWriter dats, - BindlessSupport bindless); /// /// Campaign V slice V6i-2: the terrain atlas built through - /// rather than raw GL. - /// Same DATs, same decode, same layer ordering — only the upload differs. + /// . The raw-GL arm this + /// used to fork from (AcquireTerrainAtlas) was deleted at slice V11; + /// the name keeps its "BackendNeutral" suffix rather than being renamed, to + /// avoid touching every call site for a purely cosmetic change. /// TerrainAtlas AcquireBackendNeutralTerrainAtlas( IGameRenderResourceLifetime lifetime, @@ -120,12 +104,11 @@ internal interface IWorldRenderCompositionFactory Action log); void SetTerrainAnisotropic(TerrainAtlas atlas, int level); - Shader CreateTerrainShader(GL gl, string shadersDirectory); - SceneLightingUboBinding CreateSceneLighting(GL gl); /// /// Campaign V slice V6j: scene lighting on a backend with no global uniform /// binding point. It publishes a ring section on the world pass scope and - /// each renderer binds it inside the pass. + /// each renderer binds it inside the pass. The raw-GL global-uniform-binding + /// arm (CreateSceneLighting) was deleted at slice V11. /// SceneLightingUboBinding CreateBackendNeutralSceneLighting( ICurrentGpuFrameSource frameSource, @@ -140,17 +123,11 @@ internal interface IWorldRenderCompositionFactory AcDream.App.Rendering.Gpu.IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory); - TerrainModernRenderer CreateTerrain( - GL gl, - BindlessSupport bindless, - Shader shader, - TerrainAtlas atlas, - AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice, - IGpuResourceRetirementQueue retirement); /// /// Campaign V slice V6j: terrain's RHI arm. No GL context, no linked /// program — the pipeline compiles terrain_modern from the committed - /// SPIR-V and records into the world pass the scope publishes. + /// SPIR-V and records into the world pass the scope publishes. The raw-GL + /// arm (CreateTerrain) was deleted at slice V11. /// TerrainModernRenderer CreateBackendNeutralTerrain( AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice, @@ -170,7 +147,6 @@ internal interface IWorldRenderCompositionFactory uint initialCenterLandblockId, float[] heightTable, TerrainAtlas? atlas); - Shader CreateMeshShader(GL gl, string shadersDirectory); WbMeshAdapter CreateMeshAdapter( GL? gl, AcDream.App.Rendering.Gpu.IGpuDevice device, @@ -179,10 +155,8 @@ internal interface IWorldRenderCompositionFactory IGpuResourceRetirementQueue retirement, ResidencyBudgetOptions budgets); TextureCache CreateTextureCache( - GL? gl, AcDream.App.Rendering.Gpu.IGpuDevice device, IDatReaderWriter dats, - BindlessSupport? bindless, IGpuResourceRetirementQueue retirement, string diagnosticsDirectory, ResidencyBudgetOptions budgets); @@ -193,20 +167,12 @@ internal interface IWorldRenderCompositionFactory IPreparedAssetSource preparedAssets, IAnimationLoader animations, DatSoundCache? audio); - SamplerCache CreateSamplerCache(GL gl); void Release(IDisposable resource); } internal sealed class RetailWorldRenderCompositionFactory : IWorldRenderCompositionFactory { - public void InitializeGlState(GL gl) - { - ArgumentNullException.ThrowIfNull(gl); - gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f); - gl.Enable(EnableCap.DepthTest); - } - public WorldRegionData LoadRegion(IDatReaderWriter dats) { ArgumentNullException.ThrowIfNull(dats); @@ -231,46 +197,6 @@ internal sealed class RetailWorldRenderCompositionFactory environment.Initialize(region); } - public BindlessSupport RequireBindless(GL gl, Action log) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentNullException.ThrowIfNull(log); - if (BindlessSupport.TryCreate(gl, out BindlessSupport? bindless)) - { - if (bindless!.HasShaderDrawParameters(gl)) - { - log("[N.5] modern path capabilities present " + - "(bindless + ARB_shader_draw_parameters)"); - return bindless; - } - log("[N.5] GL_ARB_shader_draw_parameters not present — " + - "modern path not available"); - } - else - { - log("[N.5] GL_ARB_bindless_texture not present — " + - "modern path not available"); - } - - throw new NotSupportedException( - "acdream requires GL_ARB_bindless_texture + " + - "GL_ARB_shader_draw_parameters (GL 4.3+ with bindless support). " + - "Your GPU/driver does not expose these extensions. If this is " + - "unexpected, please file a bug report with your GPU vendor + " + - "driver version."); - } - - public TerrainAtlas AcquireTerrainAtlas( - IGameRenderResourceLifetime lifetime, - GL gl, - IDatReaderWriter dats, - BindlessSupport bindless) - { - ArgumentNullException.ThrowIfNull(lifetime); - return lifetime.AcquireTerrainAtlas( - () => TerrainAtlas.Build(gl, dats, bindless)); - } - public TerrainAtlas AcquireBackendNeutralTerrainAtlas( IGameRenderResourceLifetime lifetime, AcDream.App.Rendering.Gpu.IGpuDevice device, @@ -289,15 +215,6 @@ internal sealed class RetailWorldRenderCompositionFactory public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) => atlas.SetAnisotropic(level); - public Shader CreateTerrainShader(GL gl, string shadersDirectory) => - new( - gl, - Path.Combine(shadersDirectory, "terrain_modern.vert"), - Path.Combine(shadersDirectory, "terrain_modern.frag"), - includeCommonPreamble: true); - - public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl); - public SceneLightingUboBinding CreateBackendNeutralSceneLighting( ICurrentGpuFrameSource frameSource, IWorldPassScope scope) => @@ -321,25 +238,6 @@ internal sealed class RetailWorldRenderCompositionFactory string shadersDirectory) => new(device, frameSource, shadersDirectory); - public TerrainModernRenderer CreateTerrain( - GL gl, - BindlessSupport bindless, - Shader shader, - TerrainAtlas atlas, - AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice, - IGpuResourceRetirementQueue retirement) => - // Campaign V slice V4t: terrain's atlas slots live in the device's one - // texture table. This renderer only exists on GL — the `gl is not null` - // gate at its call site is the same one — so the backend cast is a - // statement of that fact rather than a narrowing. - new( - gl, - bindless, - shader, - atlas, - (AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)gpuDevice, - retirement); - public TerrainModernRenderer CreateBackendNeutralTerrain( AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice, ICurrentGpuFrameSource frameSource, @@ -400,13 +298,6 @@ internal sealed class RetailWorldRenderCompositionFactory new ConcurrentDictionary()); } - public Shader CreateMeshShader(GL gl, string shadersDirectory) => - new( - gl, - Path.Combine(shadersDirectory, "mesh_modern.vert"), - Path.Combine(shadersDirectory, "mesh_modern.frag"), - includeCommonPreamble: true); - public WbMeshAdapter CreateMeshAdapter( GL? gl, AcDream.App.Rendering.Gpu.IGpuDevice device, @@ -424,18 +315,14 @@ internal sealed class RetailWorldRenderCompositionFactory budgets); public TextureCache CreateTextureCache( - GL? gl, AcDream.App.Rendering.Gpu.IGpuDevice device, IDatReaderWriter dats, - BindlessSupport? bindless, IGpuResourceRetirementQueue retirement, string diagnosticsDirectory, ResidencyBudgetOptions budgets) => new( - gl, device, dats, - bindless, retirement, diagnosticsDirectory, budgets); @@ -501,19 +388,14 @@ internal sealed class RetailWorldRenderCompositionFactory } } - public SamplerCache CreateSamplerCache(GL gl) => new(gl); - public void Release(IDisposable resource) => resource.Dispose(); } internal enum WorldRenderCompositionPoint { - GlStateInitialized, RegionLoaded, EnvironmentInitialized, - BindlessPublished, TerrainAtlasAcquired, - TerrainShaderPublished, SceneLightingPublished, DebugLinesPublished, DebugFontCreated, @@ -522,10 +404,8 @@ internal enum WorldRenderCompositionPoint HudResourcesCompleted, TerrainPublished, TerrainBuildStatePublished, - MeshShaderPublished, MeshAdapterPublished, TextureCachePublished, - SamplerCachePublished, } /// @@ -571,91 +451,53 @@ internal sealed class WorldRenderCompositionPhase var scope = new CompositionAcquisitionScope(); try { - // Campaign V slice V6h: null on a backend with no GL context. Every - // world renderer below is still raw GL, so the Vulkan arm composes - // the RHI-ported subset only — the texture cache's UI path, the - // debug font, the text renderer, the debug lines — and leaves the - // world absent until slices V4t/V4c/V4d land it. - GL? gl = platform.Graphics.Gl; var residency = new ResidencyManager( _dependencies.ResidencyBudgets); - if (gl is not null) - _factory.InitializeGlState(gl); - Fault(WorldRenderCompositionPoint.GlStateInitialized); WorldRegionData region = _factory.LoadRegion(content.Dats); Fault(WorldRenderCompositionPoint.RegionLoaded); _factory.InitializeEnvironment(_dependencies.Environment, region.Region); Fault(WorldRenderCompositionPoint.EnvironmentInitialized); - BindlessSupport? bindless = gl is null - ? null - : _factory.RequireBindless(gl, _dependencies.Log); - if (bindless is not null) - _publication.PublishBindlessSupport(bindless); - Fault(WorldRenderCompositionPoint.BindlessPublished); - - // Campaign V slice V6i-2: the atlas exists on BOTH arms now. GL - // builds it from raw texture names as before; a backend with no GL - // context builds the same layers through IGpuDevice.CreateTexture. - // That is what makes the blending/T-code tables below real on - // Vulkan — and, more to the point, it is what puts the new creation - // path under the validation layer instead of leaving it a claim. - TerrainAtlas? terrainAtlas = gl is not null && bindless is not null - ? _factory.AcquireTerrainAtlas( - _dependencies.RenderResources, - gl, - content.Dats, - bindless) - : _factory.AcquireBackendNeutralTerrainAtlas( - _dependencies.RenderResources, - _dependencies.GpuDevice, - content.Dats); + // Campaign V slice V6i-2: the atlas builds through IGpuDevice on + // every remaining backend. The raw-GL arm that built it from raw + // texture names was deleted at slice V11. + TerrainAtlas terrainAtlas = _factory.AcquireBackendNeutralTerrainAtlas( + _dependencies.RenderResources, + _dependencies.GpuDevice, + content.Dats); _factory.SetTerrainAnisotropic( terrainAtlas, settings.ResolvedQuality.AnisotropicLevel); Fault(WorldRenderCompositionPoint.TerrainAtlasAcquired); - // The rest of the world texture stack's creation path, exercised on - // the arm that has no GL: one shared array of each format family and - // one composite array, created and released here. Nothing draws - // them; see the type's own documentation for why they are built - // anyway. It claims no composition point and enters no acquisition - // scope — the frozen publication order is a pinned assertion, and an - // exercise that owns nothing past its own statement is not a - // published owner. - if (gl is null) - { - _factory.ExerciseBackendNeutralWorldTextures( - _dependencies.GpuDevice, - _dependencies.Log); - } + // The rest of the world texture stack's creation path: one shared + // array of each format family and one composite array, created and + // released here. Nothing draws them; see the type's own + // documentation for why they are built anyway. It claims no + // composition point and enters no acquisition scope — the frozen + // publication order is a pinned assertion, and an exercise that + // owns nothing past its own statement is not a published owner. + _factory.ExerciseBackendNeutralWorldTextures( + _dependencies.GpuDevice, + _dependencies.Log); string shadersDirectory = Path.Combine( AppContext.BaseDirectory, "Rendering", "Shaders"); - Shader? terrainShader = AcquireAndPublishIf( - gl is not null, - scope, - "terrain shader", - () => _factory.CreateTerrainShader(gl!, shadersDirectory), - _publication.PublishTerrainShader, - WorldRenderCompositionPoint.TerrainShaderPublished); - // Campaign V slice V6j: scene lighting exists on BOTH arms — the - // world renderers read it on both, and on the RHI arm it publishes a - // ring section instead of holding a global binding point. - IWorldPassScope? worldPassScope = platform.Graphics.WorldPassScope; - SceneLightingUboBinding? sceneLighting = AcquireAndPublish( + // Campaign V slice V6j: scene lighting publishes a ring section on + // the world pass scope; the raw-GL global-uniform-binding arm was + // deleted at slice V11. + IWorldPassScope worldPassScope = platform.Graphics.WorldPassScope + ?? throw new InvalidOperationException( + "The graphics backend must publish a world pass scope."); + SceneLightingUboBinding sceneLighting = AcquireAndPublish( scope, "scene lighting", - () => gl is not null - ? _factory.CreateSceneLighting(gl) - : _factory.CreateBackendNeutralSceneLighting( - _dependencies.GpuFrameSource, - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope.")), + () => _factory.CreateBackendNeutralSceneLighting( + _dependencies.GpuFrameSource, + worldPassScope), _publication.PublishSceneLighting, WorldRenderCompositionPoint.SceneLightingPublished); DebugLineRenderer debugLines = AcquireAndPublish( @@ -671,28 +513,18 @@ internal sealed class WorldRenderCompositionPhase (BitmapFont? debugFont, TextRenderer? textRenderer) = ComposeOptionalHudResources(scope, shadersDirectory); - // Campaign V slice V6j: terrain exists on BOTH arms. The GL arm is - // unchanged; the RHI arm records into the world pass and reads the - // same backend-neutral atlas built above. - TerrainModernRenderer? terrain = AcquireAndPublish( + // Campaign V slice V6j: terrain records into the world pass and + // reads the same backend-neutral atlas built above. The raw-GL arm + // was deleted at slice V11. + TerrainModernRenderer terrain = AcquireAndPublish( scope, "terrain renderer", - () => gl is not null - ? _factory.CreateTerrain( - gl, - bindless!, - terrainShader!, - terrainAtlas!, - _dependencies.GpuDevice, - _dependencies.ResourceRetirement) - : _factory.CreateBackendNeutralTerrain( - _dependencies.GpuDevice, - _dependencies.GpuFrameSource, - worldPassScope - ?? throw new InvalidOperationException( - "A backend without a GL context must publish a world pass scope."), - terrainAtlas!, - _dependencies.ResourceRetirement), + () => _factory.CreateBackendNeutralTerrain( + _dependencies.GpuDevice, + _dependencies.GpuFrameSource, + worldPassScope, + terrainAtlas, + _dependencies.ResourceRetirement), _publication.PublishTerrain, WorldRenderCompositionPoint.TerrainPublished); @@ -707,25 +539,17 @@ internal sealed class WorldRenderCompositionPhase terrainBuild.SurfaceCache); Fault(WorldRenderCompositionPoint.TerrainBuildStatePublished); - Shader? meshShader = AcquireAndPublishIf( - gl is not null, - scope, - "mesh shader", - () => _factory.CreateMeshShader(gl!, shadersDirectory), - _publication.PublishMeshShader, - WorldRenderCompositionPoint.MeshShaderPublished); - if (meshShader is not null) - _dependencies.Log("[N.5] mesh_modern shader loaded"); - // Campaign V slice V6i-3: the mesh pipeline exists on BOTH arms. Its - // upload bodies reached IGpuBuffer, so a backend with no GL context - // builds the same arena, the same atlases and the same render data — - // which is what makes streaming's publication into GPU state real - // there rather than a no-op. + // Campaign V slice V6i-3: the mesh pipeline builds the same arena, + // atlases and render data on every remaining backend, which is what + // makes streaming's publication into GPU state real rather than a + // no-op. gl is always null here — the raw-GL arm was deleted at + // slice V11, and the constructor's GL? gl parameter is Commit 3's + // (IMeshPipelineDevice.Gl) to remove. WbMeshAdapter meshAdapter = AcquireAndPublish( scope, "WB mesh adapter", () => _factory.CreateMeshAdapter( - gl, + gl: null, _dependencies.GpuDevice, content.Dats, content.PreparedAssets, @@ -737,22 +561,13 @@ internal sealed class WorldRenderCompositionPhase scope, "texture cache", () => _factory.CreateTextureCache( - gl, _dependencies.GpuDevice, content.Dats, - bindless, _dependencies.ResourceRetirement, _dependencies.DiagnosticsDirectory, residency.Budgets), _publication.PublishTextureCache, WorldRenderCompositionPoint.TextureCachePublished); - SamplerCache? samplers = AcquireAndPublishIf( - gl is not null, - scope, - "sampler cache", - () => _factory.CreateSamplerCache(gl!), - _publication.PublishSamplerCache, - WorldRenderCompositionPoint.SamplerCachePublished); _factory.RegisterResidencySources( residency, meshAdapter, @@ -769,18 +584,14 @@ internal sealed class WorldRenderCompositionPhase terrainBuild, new WorldRenderFoundation( shadersDirectory, - bindless, terrainAtlas, - terrainShader, sceneLighting, debugLines, debugFont, textRenderer, terrain, - meshShader, meshAdapter, textureCache, - samplers, residency)); } catch (Exception failure) @@ -843,27 +654,6 @@ internal sealed class WorldRenderCompositionPhase return value; } - /// - /// Campaign V slice V6h: acquires an owner the active backend may not have. - /// The fault point still fires on both arms so a failure-injection test - /// covers the same ordered sequence whichever backend composed it. - /// - private T? AcquireAndPublishIf( - bool supported, - CompositionAcquisitionScope scope, - string name, - Func factory, - Action publish, - WorldRenderCompositionPoint point) - where T : class, IDisposable - { - T? value = supported - ? scope.Acquire(name, factory, _factory.Release).Publish(publish) - : null; - Fault(point); - return value; - } - private void Fault(WorldRenderCompositionPoint point) => _faultInjection?.Invoke(point); } diff --git a/src/AcDream.App/Diagnostics/FrameScreenshotController.cs b/src/AcDream.App/Diagnostics/FrameScreenshotController.cs index 70dd0e16..f6b9ada9 100644 --- a/src/AcDream.App/Diagnostics/FrameScreenshotController.cs +++ b/src/AcDream.App/Diagnostics/FrameScreenshotController.cs @@ -1,6 +1,4 @@ -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; -using SixLabors.ImageSharp; +using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace AcDream.App.Diagnostics; @@ -28,39 +26,6 @@ internal sealed class FrameScreenshotController private readonly Dictionary _status = new(StringComparer.OrdinalIgnoreCase); - public FrameScreenshotController( - GL gl, - string directory, - Action? log = null) - : this( - CreateReader(gl), - directory, - log) - { - } - - private static Func CreateReader(GL gl) - { - ArgumentNullException.ThrowIfNull(gl); - var surface = new GlDefaultFramebufferSurface(gl); - return (width, height) => ReadDefaultFramebuffer(surface, width, height); - } - - /// - /// Reads the default framebuffer through the same resolve-aware path the - /// screenshot gates use. Shared so there is exactly one implementation of - /// "read the backbuffer" in the process — see - /// . - /// - internal static byte[] ReadDefaultFramebuffer(GL gl, int width, int height) - { - ArgumentNullException.ThrowIfNull(gl); - return ReadDefaultFramebuffer( - new GlDefaultFramebufferSurface(gl), - width, - height); - } - internal FrameScreenshotController( Func readRgba, string directory, @@ -186,7 +151,7 @@ internal sealed class FrameScreenshotController /// /// Blits the whole colour buffer from the bound read framebuffer to the /// bound draw framebuffer with GL_NEAREST and identical rectangles - /// — the multisample resolve. + /// — the multisample resolve. /// void BlitColorNearest(int width, int height); @@ -194,7 +159,7 @@ internal sealed class FrameScreenshotController } /// - /// Reads the default framebuffer — framebuffer name 0, the backbuffer — + /// Reads the default framebuffer — framebuffer name 0, the backbuffer — /// and nothing else. /// /// @@ -204,13 +169,13 @@ internal sealed class FrameScreenshotController /// offscreen target the previous renderer left bound. The frame this runs in /// draws several: PrivateEntityViewportRenderer clears its paperdoll /// and appraisal FBOs to exactly RGBA(0,0,0,0), which is what a leaked - /// binding writes to disk — a fully transparent PNG that reads as a + /// binding writes to disk — a fully transparent PNG that reads as a /// blank-world failure while the backbuffer on screen was correct. /// /// /// This was latent for as long as something else rebound framebuffer 0 often /// enough to mask it (before Campaign V slice V4c, GL BeginPass did so - /// on every pass — see plan §5.4). The capture states its own source instead + /// on every pass — see plan §5.4). The capture states its own source instead /// of inheriting one, and restores the caller's binding so a diagnostic /// capture cannot perturb the frame it observes. /// @@ -218,15 +183,15 @@ internal sealed class FrameScreenshotController /// Multisampling. The window is created with the quality preset's /// MSAA sample count, so the default framebuffer is normally 4x multisampled, /// and glReadPixels against a multisampled read framebuffer is - /// undefined per the GL spec (GL 4.6 §18.2: an INVALID_OPERATION is + /// undefined per the GL spec (GL 4.6 §18.2: an INVALID_OPERATION is /// generated only for framebuffer objects; for the default framebuffer the /// result is simply unspecified, and AMD returns real pixels most of the time /// and something else the rest). Every automated pixel gate and every blank- /// world verdict in Campaign V reads through here, so an unspecified read is /// an unsound instrument, not a cosmetic issue. When the default framebuffer - /// is multisampled the capture resolves it first — blit the whole colour + /// is multisampled the capture resolves it first — blit the whole colour /// buffer into a single-sampled RGBA8 framebuffer with identical rectangles - /// and GL_NEAREST, which is the defined resolve — and reads that. + /// and GL_NEAREST, which is the defined resolve — and reads that. /// A single-sampled default framebuffer keeps the original direct read, so /// non-MSAA captures stay byte-for-byte what they were. /// @@ -271,7 +236,7 @@ internal sealed class FrameScreenshotController uint resolve = surface.CreateResolveTarget(width, height); try { - // Read is still framebuffer 0 — the multisampled source. + // Read is still framebuffer 0 — the multisampled source. surface.BindDrawFramebuffer(resolve); surface.BlitColorNearest(width, height); surface.BindReadFramebuffer(resolve); @@ -283,134 +248,4 @@ internal sealed class FrameScreenshotController } } - private sealed class GlDefaultFramebufferSurface : IDefaultFramebufferSurface - { - private readonly GL _gl; - private uint _resolveRenderbuffer; - - public GlDefaultFramebufferSurface(GL gl) => _gl = gl; - - public uint ReadFramebufferBinding - { - get - { - _gl.GetInteger(GetPName.ReadFramebufferBinding, out int binding); - return (uint)binding; - } - } - - public uint DrawFramebufferBinding - { - get - { - _gl.GetInteger(GetPName.DrawFramebufferBinding, out int binding); - return (uint)binding; - } - } - - public int DefaultFramebufferSamples - { - get - { - _gl.GetInteger(GetPName.Samples, out int samples); - return samples; - } - } - - public void BindReadFramebuffer(uint framebuffer) => - _gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, framebuffer); - - public void BindDrawFramebuffer(uint framebuffer) => - _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, framebuffer); - - public uint CreateResolveTarget(int width, int height) - { - // RGBA8 matches the default framebuffer's colour encoding — the app - // never enables GL_FRAMEBUFFER_SRGB — so the blit is a pure resolve - // with no encoding conversion. - uint renderbuffer = _gl.GenRenderbuffer(); - _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, renderbuffer); - _gl.RenderbufferStorage( - RenderbufferTarget.Renderbuffer, - InternalFormat.Rgba8, - (uint)width, - (uint)height); - _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0u); - - uint framebuffer = _gl.GenFramebuffer(); - _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, framebuffer); - _gl.FramebufferRenderbuffer( - FramebufferTarget.DrawFramebuffer, - FramebufferAttachment.ColorAttachment0, - RenderbufferTarget.Renderbuffer, - renderbuffer); - GLEnum status = - _gl.CheckFramebufferStatus(FramebufferTarget.DrawFramebuffer); - if (status != GLEnum.FramebufferComplete) - { - _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, 0u); - _gl.DeleteFramebuffer(framebuffer); - _gl.DeleteRenderbuffer(renderbuffer); - throw new InvalidOperationException( - $"multisample resolve framebuffer {width}x{height} is " - + $"incomplete: {status}"); - } - - GLHelpers.ThrowOnResourceError( - _gl, - $"create screenshot resolve target {width}x{height}"); - _resolveRenderbuffer = renderbuffer; - return framebuffer; - } - - public void DeleteResolveTarget(uint framebuffer) - { - _gl.DeleteFramebuffer(framebuffer); - if (_resolveRenderbuffer != 0u) - _gl.DeleteRenderbuffer(_resolveRenderbuffer); - _resolveRenderbuffer = 0u; - } - - public void BlitColorNearest(int width, int height) - { - // A blit is subject to the scissor test, so a frame that left a - // scissor rectangle armed would resolve only part of the image. - // This operation states the state it needs and restores it — the - // same self-contained-GL-state rule the render passes follow. - bool scissor = _gl.IsEnabled(EnableCap.ScissorTest); - if (scissor) - _gl.Disable(EnableCap.ScissorTest); - _gl.BlitFramebuffer( - 0, - 0, - width, - height, - 0, - 0, - width, - height, - ClearBufferMask.ColorBufferBit, - BlitFramebufferFilter.Nearest); - if (scissor) - _gl.Enable(EnableCap.ScissorTest); - GLHelpers.ThrowOnResourceError( - _gl, - $"resolve default framebuffer {width}x{height}"); - } - - public unsafe void ReadRgba(int width, int height, byte[] destination) - { - fixed (byte* pointer = destination) - { - _gl.ReadPixels( - 0, - 0, - (uint)width, - (uint)height, - PixelFormat.Rgba, - PixelType.UnsignedByte, - pointer); - } - } - } } diff --git a/src/AcDream.App/Platform/GraphicalCapabilityRecord.cs b/src/AcDream.App/Platform/GraphicalCapabilityRecord.cs deleted file mode 100644 index 7a5e3f2d..00000000 --- a/src/AcDream.App/Platform/GraphicalCapabilityRecord.cs +++ /dev/null @@ -1,525 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using AcDream.App.Rendering; -using Silk.NET.Input; -using Silk.NET.GLFW; -using Silk.NET.OpenGL; -using Silk.NET.Windowing; - -namespace AcDream.App.Platform; - -internal sealed record GraphicalFramebufferCapabilities( - int RedBits, - int GreenBits, - int BlueBits, - int AlphaBits, - int DepthBits, - int StencilBits, - int SampleBuffers, - int Samples, - bool FramebufferSrgbApi); - -internal sealed record GraphicalInputCapabilities( - int KeyboardCount, - int MouseCount, - int GamepadCount, - int JoystickCount); - -internal sealed record GraphicalWindowCapabilities( - int LogicalWidth, - int LogicalHeight, - int FramebufferWidth, - int FramebufferHeight, - string MonitorName, - double RefreshRateHz, - bool VSync); - -internal sealed record GraphicalAudioCapabilities( - bool Requested, - bool Available, - bool PlaybackSubmitted, - bool DisposalComplete, - string Backend); - -internal sealed record GraphicalSmokeLifecycleCapabilities( - int OwnedWindowCount, - int OwnedGlApiCount, - int OwnedInputContextCount, - int OwnedAudioEngineCount, - bool ShutdownComplete); - -internal sealed record GraphicalFunctionProbeResult( - bool BindlessTexture, - bool ShaderDrawParameters, - bool MultiDrawIndirect, - bool ShaderStorageBuffer, - bool TimerQuery, - bool SrgbFramebuffer, - bool? PersistentBufferStorage, - IReadOnlyList Failures) -{ - internal static GraphicalFunctionProbeResult NotRun { get; } = new( - false, - false, - false, - false, - false, - false, - null, - ["active OpenGL function probe did not run"]); -} - -internal sealed record GraphicalCapabilityRecord( - DateTimeOffset CapturedAtUtc, - string RuntimeIdentifier, - GraphicalHostOperatingSystem OperatingSystem, - GraphicalDisplayProtocol RequestedDisplayProtocol, - GraphicalDisplayProtocol ActiveDisplayProtocol, - string DisplaySelectionReason, - string WindowBackend, - string WindowBackendVersion, - string GlVendor, - string GlRenderer, - string GlVersion, - string GlslVersion, - int GlMajorVersion, - int GlMinorVersion, - int ContextProfileMask, - int ContextFlags, - bool HasBindlessTexture, - bool HasShaderDrawParameters, - bool HasMultiDrawIndirect, - bool HasShaderStorageBuffer, - bool HasBufferStorage, - bool HasTimerQuery, - int MaximumShaderStorageBufferBindings, - int MaximumUniformBufferBindings, - int MaximumTextureSize, - int MaximumArrayTextureLayers, - int MaximumCombinedTextureImageUnits, - GraphicalFramebufferCapabilities Framebuffer, - GraphicalInputCapabilities Input, - GraphicalWindowCapabilities Window, - GraphicalAudioCapabilities Audio, - GraphicalSmokeLifecycleCapabilities Lifecycle, - IReadOnlyList Extensions, - GraphicalFunctionProbeResult FunctionProbe, - IReadOnlyList SupportFailures) -{ - internal bool IsSupported => SupportFailures.Count == 0; -} - -internal static class GraphicalCapabilityRequirements -{ - internal static IReadOnlyList Evaluate( - GraphicalCapabilityRecord capabilities) - { - ArgumentNullException.ThrowIfNull(capabilities); - var failures = new List(); - - if (capabilities.GlMajorVersion < 4 - || capabilities.GlMajorVersion == 4 - && capabilities.GlMinorVersion < 3) - { - failures.Add( - "OpenGL 4.3 core is required; the active context reports " + - $"{capabilities.GlMajorVersion}.{capabilities.GlMinorVersion}."); - } - if (!capabilities.HasBindlessTexture) - failures.Add("GL_ARB_bindless_texture is required."); - if (!capabilities.HasShaderDrawParameters) - failures.Add("GL_ARB_shader_draw_parameters is required."); - if (!capabilities.HasMultiDrawIndirect) - failures.Add("multi-draw indirect support is required."); - if (!capabilities.HasShaderStorageBuffer) - failures.Add("shader-storage buffers are required."); - if (!capabilities.HasTimerQuery) - failures.Add("OpenGL timer queries are required."); - if (capabilities.Framebuffer.DepthBits < 24) - { - failures.Add( - "the default framebuffer must provide at least 24 depth bits."); - } - if (capabilities.Framebuffer.StencilBits < 8) - { - failures.Add( - "the default framebuffer must provide at least 8 stencil bits."); - } - if (!capabilities.Framebuffer.FramebufferSrgbApi) - failures.Add("framebuffer sRGB support is required."); - if (capabilities.Input.KeyboardCount < 1) - failures.Add("the graphical input backend exposed no keyboard."); - if (capabilities.Input.MouseCount < 1) - failures.Add("the graphical input backend exposed no mouse."); - - if (capabilities.FunctionProbe.Failures.Count != 0) - { - failures.AddRange( - capabilities.FunctionProbe.Failures.Select( - failure => $"OpenGL function probe: {failure}")); - } - else - { - if (!capabilities.FunctionProbe.BindlessTexture) - failures.Add("the bindless texture call probe did not pass."); - if (!capabilities.FunctionProbe.ShaderDrawParameters) - { - failures.Add( - "the shader draw-parameters compile probe did not pass."); - } - if (!capabilities.FunctionProbe.MultiDrawIndirect) - failures.Add("the multi-draw indirect call probe did not pass."); - if (!capabilities.FunctionProbe.ShaderStorageBuffer) - failures.Add("the shader-storage buffer call probe did not pass."); - if (!capabilities.FunctionProbe.TimerQuery) - failures.Add("the timer-query call probe did not pass."); - if (!capabilities.FunctionProbe.SrgbFramebuffer) - failures.Add("the sRGB framebuffer call probe did not pass."); - if (capabilities.HasBufferStorage - && capabilities.FunctionProbe.PersistentBufferStorage != true) - { - failures.Add( - "GL_ARB_buffer_storage was advertised but the persistent " + - "mapping call probe did not pass."); - } - } - - return failures; - } -} - -internal static class GraphicalCapabilityProbe -{ - private const int GlSampleBuffers = 0x80A8; - private const int GlSamples = 0x80A9; - private const int GlMaxShaderStorageBufferBindings = 0x90DD; - private const int GlMaxUniformBufferBindings = 0x8A2F; - private const int GlMaxCombinedTextureImageUnits = 0x8B4D; - - internal static GraphicalCapabilityRecord Capture( - GL gl, - IWindow window, - IInputContext input, - GraphicalHostPlatformServices platform) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentNullException.ThrowIfNull(window); - ArgumentNullException.ThrowIfNull(input); - ArgumentNullException.ThrowIfNull(platform); - - int major = gl.GetInteger(GetPName.MajorVersion); - int minor = gl.GetInteger(GetPName.MinorVersion); - bool openGl43 = major > 4 || major == 4 && minor >= 3; - bool timerQuery = - major > 3 - || major == 3 && minor >= 3 - || gl.IsExtensionPresent("GL_ARB_timer_query"); - bool framebufferSrgb = - major > 3 - || major == 3 && minor >= 0 - || gl.IsExtensionPresent("GL_ARB_framebuffer_sRGB") - || gl.IsExtensionPresent("GL_EXT_framebuffer_sRGB"); - - IReadOnlyList extensions = ReadExtensions(gl); - IMonitor? monitor = window.Monitor; - Silk.NET.Windowing.VideoMode mode = - monitor?.VideoMode ?? Silk.NET.Windowing.VideoMode.Default; - var framebuffer = window.FramebufferSize; - var logical = window.Size; - - var captured = new GraphicalCapabilityRecord( - DateTimeOffset.UtcNow, - platform.RuntimeIdentifier, - platform.OperatingSystem, - platform.WindowBackend.RequestedProtocol, - GlfwNativePlatformProbe.GetActiveProtocol( - platform.OperatingSystem), - platform.WindowBackend.Reason, - "Silk.NET.Windowing.Glfw", - GlfwNativePlatformProbe.GetVersion( - platform.OperatingSystem), - gl.GetStringS(GLEnum.Vendor), - gl.GetStringS(GLEnum.Renderer), - gl.GetStringS(GLEnum.Version), - gl.GetStringS(GLEnum.ShadingLanguageVersion), - major, - minor, - GetInteger(gl, (GetPName)GLEnum.ContextProfileMask), - GetInteger(gl, (GetPName)GLEnum.ContextFlags), - gl.IsExtensionPresent("GL_ARB_bindless_texture"), - gl.IsExtensionPresent("GL_ARB_shader_draw_parameters"), - openGl43 - || gl.IsExtensionPresent("GL_ARB_multi_draw_indirect"), - openGl43 - || gl.IsExtensionPresent("GL_ARB_shader_storage_buffer_object"), - major > 4 - || major == 4 && minor >= 4 - || gl.IsExtensionPresent("GL_ARB_buffer_storage"), - timerQuery, - GetInteger( - gl, - (GetPName)GlMaxShaderStorageBufferBindings), - GetInteger(gl, (GetPName)GlMaxUniformBufferBindings), - GetInteger(gl, GetPName.MaxTextureSize), - GetInteger(gl, GetPName.MaxArrayTextureLayers), - GetInteger( - gl, - (GetPName)GlMaxCombinedTextureImageUnits), - new GraphicalFramebufferCapabilities( - GetFramebufferAttachmentInteger( - gl, - (FramebufferAttachment)GLEnum.BackLeft, - FramebufferAttachmentParameterName.RedSize), - GetFramebufferAttachmentInteger( - gl, - (FramebufferAttachment)GLEnum.BackLeft, - FramebufferAttachmentParameterName.GreenSize), - GetFramebufferAttachmentInteger( - gl, - (FramebufferAttachment)GLEnum.BackLeft, - FramebufferAttachmentParameterName.BlueSize), - GetFramebufferAttachmentInteger( - gl, - (FramebufferAttachment)GLEnum.BackLeft, - FramebufferAttachmentParameterName.AlphaSize), - GetFramebufferAttachmentInteger( - gl, - (FramebufferAttachment)GLEnum.Depth, - FramebufferAttachmentParameterName.DepthSize), - GetFramebufferAttachmentInteger( - gl, - (FramebufferAttachment)GLEnum.Stencil, - FramebufferAttachmentParameterName.StencilSize), - GetInteger(gl, (GetPName)GlSampleBuffers), - GetInteger(gl, (GetPName)GlSamples), - framebufferSrgb), - new GraphicalInputCapabilities( - input.Keyboards.Count, - input.Mice.Count, - input.Gamepads.Count, - input.Joysticks.Count), - new GraphicalWindowCapabilities( - logical.X, - logical.Y, - framebuffer.X, - framebuffer.Y, - monitor?.Name ?? "unknown", - mode.RefreshRate ?? 0, - window.VSync), - new GraphicalAudioCapabilities( - Requested: false, - Available: false, - PlaybackSubmitted: false, - DisposalComplete: true, - Backend: "not requested"), - new GraphicalSmokeLifecycleCapabilities( - OwnedWindowCount: 1, - OwnedGlApiCount: 1, - OwnedInputContextCount: 1, - OwnedAudioEngineCount: 0, - ShutdownComplete: false), - extensions, - GraphicalFunctionProbeResult.NotRun, - []); - return captured with - { - SupportFailures = - GraphicalCapabilityRequirements.Evaluate(captured), - }; - } - - internal static GraphicalCapabilityRecord WithFunctionProbe( - GraphicalCapabilityRecord capabilities, - GraphicalFunctionProbeResult functionProbe) - { - ArgumentNullException.ThrowIfNull(capabilities); - ArgumentNullException.ThrowIfNull(functionProbe); - GraphicalCapabilityRecord updated = capabilities with - { - FunctionProbe = functionProbe, - SupportFailures = [], - }; - return updated with - { - SupportFailures = - GraphicalCapabilityRequirements.Evaluate(updated), - }; - } - - private static int GetInteger(GL gl, GetPName name) - { - return GlResourceCommand.Execute( - gl, - $"query OpenGL integer 0x{(uint)name:X}", - () => - { - gl.GetInteger(name, out int value); - return value; - }); - } - - private static int GetFramebufferAttachmentInteger( - GL gl, - FramebufferAttachment attachment, - FramebufferAttachmentParameterName name) - { - return GlResourceCommand.Execute( - gl, - $"query default framebuffer attachment {attachment}/{name}", - () => - { - gl.GetFramebufferAttachmentParameter( - FramebufferTarget.Framebuffer, - attachment, - name, - out int value); - return value; - }); - } - - private static IReadOnlyList ReadExtensions(GL gl) - { - int count = GetInteger(gl, GetPName.NumExtensions); - var extensions = new string[Math.Max(0, count)]; - for (uint index = 0; index < extensions.Length; index++) - extensions[index] = gl.GetStringS(GLEnum.Extensions, index); - Array.Sort(extensions, StringComparer.Ordinal); - return extensions; - } -} - -internal static class GraphicalCapabilityGuard -{ - internal static GraphicalCapabilityRecord CaptureVerifyAndWrite( - GL gl, - IWindow window, - IInputContext input, - GraphicalHostPlatformServices platform, - string reportPath) - { - GraphicalCapabilityRecord passive = - GraphicalCapabilityProbe.Capture( - gl, - window, - input, - platform); - GraphicalFunctionProbeResult functions = - GraphicalGlFunctionProbe.Run(gl, passive); - GraphicalCapabilityRecord verified = - GraphicalCapabilityProbe.WithFunctionProbe( - passive, - functions); - GraphicalCapabilityReportWriter.Write(reportPath, verified); - - return verified; - } - - internal static void ThrowIfUnsupported( - GraphicalCapabilityRecord capabilities, - string reportPath) - { - ArgumentNullException.ThrowIfNull(capabilities); - if (!capabilities.IsSupported) - { - throw new NotSupportedException( - FormatUnsupportedMessage(capabilities, reportPath)); - } - } - - internal static string FormatUnsupportedMessage( - GraphicalCapabilityRecord capabilities, - string reportPath) - { - ArgumentNullException.ThrowIfNull(capabilities); - ArgumentException.ThrowIfNullOrWhiteSpace(reportPath); - return - "acdream's mandatory modern renderer is unsupported by the " + - "active graphical backend.\n" + - $"Platform: {capabilities.RuntimeIdentifier}, " + - $"{capabilities.ActiveDisplayProtocol}, " + - $"{capabilities.GlVendor} / {capabilities.GlRenderer}, " + - $"{capabilities.GlVersion}\n" + - string.Join( - "\n", - capabilities.SupportFailures.Select( - failure => $" - {failure}")) + - $"\nFull capability report: {Path.GetFullPath(reportPath)}"; - } -} - -internal static class GraphicalCapabilityReportWriter -{ - private static readonly JsonSerializerOptions Options = new() - { - WriteIndented = true, - Converters = - { - new JsonStringEnumConverter(), - }, - }; - - internal static void Write( - string path, - GraphicalCapabilityRecord capabilities) - { - ArgumentException.ThrowIfNullOrWhiteSpace(path); - ArgumentNullException.ThrowIfNull(capabilities); - string fullPath = Path.GetFullPath(path); - string? directory = Path.GetDirectoryName(fullPath); - if (!string.IsNullOrEmpty(directory)) - Directory.CreateDirectory(directory); - - string temporaryPath = fullPath + ".tmp"; - File.WriteAllText( - temporaryPath, - JsonSerializer.Serialize(capabilities, Options)); - File.Move(temporaryPath, fullPath, overwrite: true); - } -} - -internal static unsafe class GlfwNativePlatformProbe -{ - private const int GlfwWin32Platform = 0x00060001; - private const int GlfwWaylandPlatform = 0x00060003; - private const int GlfwX11Platform = 0x00060004; - - internal static GraphicalDisplayProtocol GetActiveProtocol( - GraphicalHostOperatingSystem operatingSystem) - { - if (operatingSystem == GraphicalHostOperatingSystem.Windows) - return GraphicalDisplayProtocol.Windows; - - if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi( - out Glfw? glfw) - || glfw is null - || !glfw.Context.TryGetProcAddress( - "glfwGetPlatform", - out nint export)) - { - return GraphicalDisplayProtocol.Unknown; - } - - int platform = - ((delegate* unmanaged[Cdecl])export)(); - return platform switch - { - GlfwX11Platform => GraphicalDisplayProtocol.X11, - GlfwWaylandPlatform => GraphicalDisplayProtocol.Wayland, - GlfwWin32Platform => GraphicalDisplayProtocol.Windows, - _ => GraphicalDisplayProtocol.Unknown, - }; - } - - internal static string GetVersion( - GraphicalHostOperatingSystem operatingSystem) - { - if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi( - out Glfw? glfw) - || glfw is null) - { - return "unknown"; - } - - return glfw.GetVersionString() ?? "unknown"; - } -} diff --git a/src/AcDream.App/Platform/GraphicalGlFunctionProbe.cs b/src/AcDream.App/Platform/GraphicalGlFunctionProbe.cs deleted file mode 100644 index c9f5a95a..00000000 --- a/src/AcDream.App/Platform/GraphicalGlFunctionProbe.cs +++ /dev/null @@ -1,513 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Platform; - -/// -/// Executes one minimal, ownership-balanced call path for every OpenGL feature -/// the mandatory renderer relies upon. Advertisement alone is insufficient: -/// several Linux driver failures present an extension string while returning -/// a missing entry point, invalid bindless handle, or unusable framebuffer. -/// -internal static unsafe class GraphicalGlFunctionProbe -{ - private const string VertexSource = """ - #version 430 core - #extension GL_ARB_shader_draw_parameters : require - void main() - { - float identity = - float(gl_DrawIDARB + gl_BaseInstanceARB) * 0.0; - gl_Position = vec4(identity, 0.0, 0.0, 1.0); - } - """; - - private const string FragmentSource = """ - #version 430 core - layout(location = 0) out vec4 outColor; - void main() - { - outColor = vec4(1.0); - } - """; - - internal static GraphicalFunctionProbeResult Run( - GL gl, - GraphicalCapabilityRecord capabilities) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentNullException.ThrowIfNull(capabilities); - var failures = new List(); - - bool bindless = capabilities.HasBindlessTexture - && Attempt( - "bindless texture handle/residency", - () => ProbeBindless(gl), - failures); - - bool shaderDraw = capabilities.HasShaderDrawParameters - && Attempt( - "shader draw parameters and multi-draw indirect", - () => ProbeShaderDrawAndMultiDraw(gl), - failures); - bool multiDraw = shaderDraw - && capabilities.HasMultiDrawIndirect; - - bool shaderStorage = capabilities.HasShaderStorageBuffer - && Attempt( - "shader-storage buffer", - () => ProbeShaderStorageBuffer(gl), - failures); - - bool timer = capabilities.HasTimerQuery - && Attempt( - "timer query", - () => ProbeTimerQuery(gl), - failures); - - bool srgb = capabilities.Framebuffer.FramebufferSrgbApi - && Attempt( - "sRGB depth/stencil framebuffer", - () => ProbeSrgbFramebuffer(gl), - failures); - - bool? persistent = capabilities.HasBufferStorage - ? Attempt( - "persistent coherent buffer storage", - () => ProbePersistentBufferStorage(gl), - failures) - : null; - - if (!capabilities.HasBindlessTexture) - failures.Add("GL_ARB_bindless_texture was not advertised."); - if (!capabilities.HasShaderDrawParameters) - failures.Add("GL_ARB_shader_draw_parameters was not advertised."); - if (!capabilities.HasMultiDrawIndirect) - failures.Add("multi-draw indirect was not advertised."); - if (!capabilities.HasShaderStorageBuffer) - failures.Add("shader-storage buffers were not advertised."); - if (!capabilities.HasTimerQuery) - failures.Add("timer queries were not advertised."); - if (!capabilities.Framebuffer.FramebufferSrgbApi) - failures.Add("framebuffer sRGB was not advertised."); - - return new GraphicalFunctionProbeResult( - bindless, - shaderDraw, - multiDraw, - shaderStorage, - timer, - srgb, - persistent, - failures); - } - - private static bool Attempt( - string name, - Action action, - List failures) - { - try - { - action(); - return true; - } - catch (Exception error) - { - failures.Add($"{name}: {error.GetType().Name}: {error.Message}"); - return false; - } - } - - private static void ProbeBindless(GL gl) - { - if (!BindlessSupport.TryCreate(gl, out BindlessSupport? bindless) - || bindless is null) - { - throw new NotSupportedException( - "the ARB bindless extension object could not be loaded."); - } - - uint texture = GlResourceCommand.CreateTexture( - gl, - "graphical capability bindless texture"); - ulong handle = 0; - try - { - byte* pixel = stackalloc byte[4] { 255, 255, 255, 255 }; - GlResourceCommand.Execute( - gl, - "initialize graphical capability bindless texture", - () => - { - gl.BindTexture(TextureTarget.Texture2D, texture); - gl.TexImage2D( - TextureTarget.Texture2D, - 0, - InternalFormat.Rgba8, - 1, - 1, - 0, - PixelFormat.Rgba, - PixelType.UnsignedByte, - pixel); - gl.TexParameter( - TextureTarget.Texture2D, - TextureParameterName.TextureMinFilter, - (int)TextureMinFilter.Nearest); - gl.TexParameter( - TextureTarget.Texture2D, - TextureParameterName.TextureMagFilter, - (int)TextureMagFilter.Nearest); - }); - handle = bindless.GetResidentHandle(texture); - bindless.MakeNonResident(handle); - handle = 0; - } - finally - { - if (handle != 0) - bindless.MakeNonResident(handle); - gl.BindTexture(TextureTarget.Texture2D, 0); - GlResourceCommand.DeleteTexture( - gl, - texture, - "delete graphical capability bindless texture"); - } - } - - private static void ProbeShaderDrawAndMultiDraw(GL gl) - { - uint program = ShaderProgramConstruction.Build( - new GlShaderProgramBuildApi(gl), - VertexSource, - FragmentSource); - uint vertexArray = 0; - uint elementBuffer = 0; - uint indirectBuffer = 0; - try - { - vertexArray = GlResourceCommand.CreateName( - gl, - "graphical capability vertex array", - gl.GenVertexArray, - gl.DeleteVertexArray); - elementBuffer = GlResourceCommand.CreateName( - gl, - "graphical capability element buffer", - gl.GenBuffer, - gl.DeleteBuffer); - indirectBuffer = GlResourceCommand.CreateName( - gl, - "graphical capability indirect buffer", - gl.GenBuffer, - gl.DeleteBuffer); - - uint* index = stackalloc uint[1] { 0 }; - uint* command = stackalloc uint[5] - { - 0, - 1, - 0, - 0, - 0, - }; - GLHelpers.ThrowOnResourceError( - gl, - "execute graphical capability multi-draw indirect call " + - "(precondition)"); - gl.UseProgram(program); - gl.BindVertexArray(vertexArray); - gl.BindBuffer( - BufferTargetARB.ElementArrayBuffer, - elementBuffer); - gl.BufferData( - BufferTargetARB.ElementArrayBuffer, - (nuint)sizeof(uint), - index, - BufferUsageARB.StaticDraw); - gl.BindBuffer( - BufferTargetARB.DrawIndirectBuffer, - indirectBuffer); - gl.BufferData( - BufferTargetARB.DrawIndirectBuffer, - (nuint)(sizeof(uint) * 5), - command, - BufferUsageARB.StaticDraw); - gl.MultiDrawElementsIndirect( - PrimitiveType.Triangles, - DrawElementsType.UnsignedInt, - null, - 1, - 0); - GLHelpers.ThrowOnResourceError( - gl, - "execute graphical capability multi-draw indirect call"); - } - finally - { - gl.UseProgram(0); - gl.BindVertexArray(0); - gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, 0); - if (indirectBuffer != 0) - { - GlResourceCommand.DeleteBuffer( - gl, - indirectBuffer, - "delete graphical capability indirect buffer"); - } - if (elementBuffer != 0) - { - GlResourceCommand.DeleteBuffer( - gl, - elementBuffer, - "delete graphical capability element buffer"); - } - if (vertexArray != 0) - { - GlResourceCommand.DeleteVertexArray( - gl, - vertexArray, - "delete graphical capability vertex array"); - } - GlResourceCommand.DeleteProgram( - gl, - program, - "delete graphical capability shader program"); - } - } - - private static void ProbeShaderStorageBuffer(GL gl) - { - uint buffer = GlResourceCommand.CreateName( - gl, - "graphical capability shader-storage buffer", - gl.GenBuffer, - gl.DeleteBuffer); - try - { - uint* value = stackalloc uint[1] { 0xACD0_0001u }; - GLHelpers.ThrowOnResourceError( - gl, - "bind graphical capability shader-storage buffer " + - "(precondition)"); - gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, buffer); - gl.BufferData( - BufferTargetARB.ShaderStorageBuffer, - (nuint)sizeof(uint), - value, - BufferUsageARB.StaticDraw); - gl.BindBufferBase( - BufferTargetARB.ShaderStorageBuffer, - 0, - buffer); - gl.BindBufferBase( - BufferTargetARB.ShaderStorageBuffer, - 0, - 0); - GLHelpers.ThrowOnResourceError( - gl, - "bind graphical capability shader-storage buffer"); - } - finally - { - gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, 0); - GlResourceCommand.DeleteBuffer( - gl, - buffer, - "delete graphical capability shader-storage buffer"); - } - } - - private static void ProbeTimerQuery(GL gl) - { - uint query = GlResourceCommand.CreateName( - gl, - "graphical capability timer query", - gl.GenQuery, - gl.DeleteQuery); - try - { - GlResourceCommand.Execute( - gl, - "execute graphical capability timer query", - () => - { - gl.BeginQuery(QueryTarget.TimeElapsed, query); - gl.EndQuery(QueryTarget.TimeElapsed); - }); - } - finally - { - GlResourceCommand.Execute( - gl, - "delete graphical capability timer query", - () => gl.DeleteQuery(query)); - } - } - - private static void ProbeSrgbFramebuffer(GL gl) - { - uint texture = 0; - uint depthStencil = 0; - uint framebuffer = 0; - try - { - texture = GlResourceCommand.CreateTexture( - gl, - "graphical capability sRGB color texture"); - depthStencil = GlResourceCommand.CreateName( - gl, - "graphical capability depth/stencil renderbuffer", - gl.GenRenderbuffer, - gl.DeleteRenderbuffer); - framebuffer = GlResourceCommand.CreateName( - gl, - "graphical capability framebuffer", - gl.GenFramebuffer, - gl.DeleteFramebuffer); - - GlResourceCommand.Execute( - gl, - "configure graphical capability sRGB framebuffer", - () => - { - gl.BindTexture(TextureTarget.Texture2D, texture); - gl.TexImage2D( - TextureTarget.Texture2D, - 0, - InternalFormat.Srgb8Alpha8, - 2, - 2, - 0, - PixelFormat.Rgba, - PixelType.UnsignedByte, - null); - - gl.BindRenderbuffer( - RenderbufferTarget.Renderbuffer, - depthStencil); - gl.RenderbufferStorage( - RenderbufferTarget.Renderbuffer, - InternalFormat.Depth24Stencil8, - 2, - 2); - - gl.BindFramebuffer( - FramebufferTarget.Framebuffer, - framebuffer); - gl.FramebufferTexture2D( - FramebufferTarget.Framebuffer, - FramebufferAttachment.ColorAttachment0, - TextureTarget.Texture2D, - texture, - 0); - gl.FramebufferRenderbuffer( - FramebufferTarget.Framebuffer, - FramebufferAttachment.DepthStencilAttachment, - RenderbufferTarget.Renderbuffer, - depthStencil); - if (gl.CheckFramebufferStatus( - FramebufferTarget.Framebuffer) - != GLEnum.FramebufferComplete) - { - throw new InvalidOperationException( - "the sRGB depth/stencil framebuffer is incomplete."); - } - - gl.Enable(EnableCap.FramebufferSrgb); - gl.Clear( - ClearBufferMask.ColorBufferBit - | ClearBufferMask.DepthBufferBit - | ClearBufferMask.StencilBufferBit); - gl.Disable(EnableCap.FramebufferSrgb); - }); - } - finally - { - gl.Disable(EnableCap.FramebufferSrgb); - gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0); - gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0); - gl.BindTexture(TextureTarget.Texture2D, 0); - if (framebuffer != 0) - { - GlResourceCommand.Execute( - gl, - "delete graphical capability framebuffer", - () => gl.DeleteFramebuffer(framebuffer)); - } - if (depthStencil != 0) - { - GlResourceCommand.Execute( - gl, - "delete graphical capability depth/stencil renderbuffer", - () => gl.DeleteRenderbuffer(depthStencil)); - } - if (texture != 0) - { - GlResourceCommand.DeleteTexture( - gl, - texture, - "delete graphical capability sRGB texture"); - } - } - } - - private static void ProbePersistentBufferStorage(GL gl) - { - uint buffer = GlResourceCommand.CreateName( - gl, - "graphical capability persistent buffer", - gl.GenBuffer, - gl.DeleteBuffer); - void* mapped = null; - try - { - GlResourceCommand.Execute( - gl, - "allocate graphical capability persistent buffer", - () => - { - gl.BindBuffer(BufferTargetARB.ArrayBuffer, buffer); - gl.BufferStorage( - GLEnum.ArrayBuffer, - 64, - null, - (uint)( - BufferStorageMask.MapWriteBit - | BufferStorageMask.MapPersistentBit - | BufferStorageMask.MapCoherentBit - | BufferStorageMask.DynamicStorageBit)); - mapped = gl.MapBufferRange( - BufferTargetARB.ArrayBuffer, - 0, - 64, - MapBufferAccessMask.WriteBit - | MapBufferAccessMask.PersistentBit - | MapBufferAccessMask.CoherentBit); - if (mapped is null) - { - throw new InvalidOperationException( - "persistent mapping returned a null pointer."); - } - *((byte*)mapped) = 0x5A; - }); - } - finally - { - if (mapped is not null) - { - GlResourceCommand.Execute( - gl, - "unmap graphical capability persistent buffer", - () => gl.UnmapBuffer(BufferTargetARB.ArrayBuffer)); - } - gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); - GlResourceCommand.DeleteBuffer( - gl, - buffer, - "delete graphical capability persistent buffer"); - } - } -} diff --git a/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs b/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs index 55931dce..15810643 100644 --- a/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs +++ b/src/AcDream.App/Platform/GraphicalWindowBackendSelection.cs @@ -187,3 +187,58 @@ internal static class GraphicalWindowBackendConfigurator DefaultPathResolver.BaseDirectoryResolver); } } + +/// +/// Reads the GLFW platform actually selected at runtime (as opposed to the one +/// requested — GLFW's Automatic hint can resolve to either X11 or +/// Wayland). Moved here from the deleted (Campaign V slice V11) raw-GL +/// GraphicalCapabilityRecord.cs: +/// depends on this for its own capability report, so it survived the GL arm +/// that used to sit alongside it. +/// +internal static unsafe class GlfwNativePlatformProbe +{ + private const int GlfwWin32Platform = 0x00060001; + private const int GlfwWaylandPlatform = 0x00060003; + private const int GlfwX11Platform = 0x00060004; + + internal static GraphicalDisplayProtocol GetActiveProtocol( + GraphicalHostOperatingSystem operatingSystem) + { + if (operatingSystem == GraphicalHostOperatingSystem.Windows) + return GraphicalDisplayProtocol.Windows; + + if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi( + out Glfw? glfw) + || glfw is null + || !glfw.Context.TryGetProcAddress( + "glfwGetPlatform", + out nint export)) + { + return GraphicalDisplayProtocol.Unknown; + } + + int platform = + ((delegate* unmanaged[Cdecl])export)(); + return platform switch + { + GlfwX11Platform => GraphicalDisplayProtocol.X11, + GlfwWaylandPlatform => GraphicalDisplayProtocol.Wayland, + GlfwWin32Platform => GraphicalDisplayProtocol.Windows, + _ => GraphicalDisplayProtocol.Unknown, + }; + } + + internal static string GetVersion( + GraphicalHostOperatingSystem operatingSystem) + { + if (!GraphicalWindowBackendConfigurator.TryGetConfiguredApi( + out Glfw? glfw) + || glfw is null) + { + return "unknown"; + } + + return glfw.GetVersionString() ?? "unknown"; + } +} diff --git a/src/AcDream.App/RenderBackendKind.cs b/src/AcDream.App/RenderBackendKind.cs deleted file mode 100644 index 64e5ad31..00000000 --- a/src/AcDream.App/RenderBackendKind.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace AcDream.App; - -/// -/// Campaign V slice V5: which rendering backend the graphical host starts. -/// -/// Slice V10 flipped the default. is what an unset -/// ACDREAM_RENDER_BACKEND now selects; remains reachable -/// for one slice by setting that variable to gl, and slice V11 deletes it -/// along with the escape hatch. -/// -/// This enum is public only because is public and -/// exposes it as a property. The backend-neutral -/// AcDream.App.Rendering.Gpu.GpuBackendKind stays internal and describes a -/// live device rather than a startup request; the two are deliberately separate -/// because a Vulkan-requested process can still fail its capability gate and -/// never own a Vulkan device at all. -/// -public enum RenderBackendKind -{ - /// - /// OpenGL 4.3 core + bindless + MDI. The escape hatch after slice V10, - /// reachable only by ACDREAM_RENDER_BACKEND=gl, deleted at V11. - /// - Gl, - - /// Vulkan 1.3 core. The default backend as of Campaign V slice V10. - Vulkan, -} diff --git a/src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs b/src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs deleted file mode 100644 index f3b5d2f0..00000000 --- a/src/AcDream.App/Rendering/BindlessTextureMutationGuard.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Runtime.ExceptionServices; - -namespace AcDream.App.Rendering; - -/// -/// Temporarily removes bindless residency around texture-state mutation while -/// retaining the obligation to restore the pair across failed mutation or -/// failed reacquisition attempts. -/// -internal sealed class BindlessTextureMutationGuard(BindlessTexturePair pair) -{ - private bool _restoreRequired; - private bool _operationActive; - - internal bool RestoreRequired => _restoreRequired; - - public void Execute(Action mutation) - { - ArgumentNullException.ThrowIfNull(mutation); - if (_operationActive) - throw new InvalidOperationException("A bindless texture mutation is already active."); - - _operationActive = true; - Exception? mutationFailure = null; - Exception? restoreFailure = null; - try - { - _restoreRequired |= pair.HasAnyResident; - if (pair.HasAnyResident) - pair.Release(); - - mutation(); - } - catch (Exception failure) - { - mutationFailure = failure; - } - finally - { - if (_restoreRequired) - { - try - { - _ = pair.Acquire(); - _restoreRequired = false; - } - catch (Exception failure) - { - restoreFailure = failure; - } - } - - _operationActive = false; - } - - if (mutationFailure is not null && restoreFailure is not null) - { - throw new AggregateException( - "Texture-state mutation failed and bindless residency could not be restored.", - mutationFailure, - restoreFailure); - } - if (mutationFailure is not null) - ExceptionDispatchInfo.Capture(mutationFailure).Throw(); - if (restoreFailure is not null) - ExceptionDispatchInfo.Capture(restoreFailure).Throw(); - } -} diff --git a/src/AcDream.App/Rendering/BindlessTexturePair.cs b/src/AcDream.App/Rendering/BindlessTexturePair.cs deleted file mode 100644 index 2b0cd50b..00000000 --- a/src/AcDream.App/Rendering/BindlessTexturePair.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace AcDream.App.Rendering; - -/// -/// Owns the two independent residency edges used by TerrainAtlas. Acquisition -/// and release publish each successful edge immediately, so a partial failure -/// retries only work that is still pending. -/// -internal sealed class BindlessTexturePair( - uint firstTexture, - uint secondTexture, - Func acquire, - Action release) -{ - private ulong _firstHandle; - private ulong _secondHandle; - private bool _firstResident; - private bool _secondResident; - - public bool IsFullyResident => _firstResident && _secondResident; - public bool HasAnyResident => _firstResident || _secondResident; - - public (ulong First, ulong Second) Acquire() - { - if (!_firstResident) - { - _firstHandle = acquire(firstTexture); - _firstResident = true; - } - - if (!_secondResident) - { - try - { - _secondHandle = acquire(secondTexture); - _secondResident = true; - } - catch (Exception acquisitionFailure) - { - try - { - Release(); - } - catch (Exception cleanupFailure) - { - throw new AggregateException( - "Second bindless texture acquisition failed and the resident prefix did not cleanly roll back.", - acquisitionFailure, - cleanupFailure); - } - - throw; - } - } - - return (_firstHandle, _secondHandle); - } - - public void Release() - { - List? failures = null; - if (_firstResident) - { - try - { - release(_firstHandle); - _firstResident = false; - _firstHandle = 0; - } - catch (Exception failure) - { - (failures ??= []).Add(failure); - } - } - - if (_secondResident) - { - try - { - release(_secondHandle); - _secondResident = false; - _secondHandle = 0; - } - catch (Exception failure) - { - (failures ??= []).Add(failure); - } - } - - if (failures is not null) - throw new AggregateException( - "One or more bindless texture handles could not be made non-resident.", - failures); - } -} diff --git a/src/AcDream.App/Rendering/ClipFrame.cs b/src/AcDream.App/Rendering/ClipFrame.cs index cf65065a..7f8020f1 100644 --- a/src/AcDream.App/Rendering/ClipFrame.cs +++ b/src/AcDream.App/Rendering/ClipFrame.cs @@ -1,8 +1,8 @@ // ClipFrame.cs // -// Phase U.3: the GPU-side container + uploader for the SHARED per-frame clip -// data consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert -// (UBO binding=2). This is the "shared" half of the U.3 clip mechanism; the +// Phase U.3: the per-frame container for the SHARED per-frame clip data +// consumed by mesh_modern.vert (SSBO binding=2) and terrain_modern.vert (UBO +// binding=2). This is the "shared" half of the U.3 clip mechanism; the // per-instance slot index buffer (SSBO binding=3) is PER-RENDERER and owned by // each renderer (WbDrawDispatcher / EnvCellRenderer), parallel to its instance // buffer — it is NOT here. @@ -22,21 +22,23 @@ // terrain OutsideView planes, then points each renderer's per-instance slot // buffer at the right slots. // -// Pure CPU byte-packing + a thin GL upload. The byte layout is asserted by -// ClipFrameLayoutTests so a silent -// std430/std140 drift can't reach the GPU. +// Pure CPU byte-packing. The GL upload machinery this file used to carry +// alongside the packing (a per-flight-slot region SSBO + terrain UBO arena, +// reservation/upload-once bookkeeping, and their disposal) was deleted at +// Campaign V slice V11: the RHI arm (see RhiWorldPassSurface.PrepareClipFrame / +// SetTerrainClip in WorldPassSurface.cs) reads RegionBytes/TerrainBytes below and +// copies them into a frame ring allocation instead, so nothing here owns a GPU +// resource anymore. The byte layout is asserted by ClipFrameLayoutTests so a +// silent std430/std140 drift can't reach the GPU. using System; -using System.Collections.Generic; using System.Numerics; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// -/// Per-frame container + uploader for the SHARED clip data: the binding=2 mesh -/// SSBO (one CellClip per slot, slot 0 reserved no-clip) and the binding=2 -/// terrain UBO (the single OutsideView region). See the file header for the exact +/// Per-frame container for the SHARED clip data: the binding=2 mesh SSBO (one +/// CellClip per slot, slot 0 reserved no-clip) and the binding=2 terrain +/// UBO (the single OutsideView region). See the file header for the exact /// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by /// each renderer, not here. /// @@ -61,13 +63,12 @@ public sealed class ClipFrame : IDisposable /// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family. public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144 - /// SSBO binding index for the shared per-cell clip regions - /// (mesh_modern.vert binding=2). - public const uint MeshClipSsboBinding = 2; - /// UBO binding index for the terrain OutsideView clip region - /// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO - /// binding=2 above. + /// (terrain_modern.vert binding=2). Read directly by both the RHI world-pass + /// section binder (WorldFrameSectionBinding.BindTerrainClip) and + /// PortalDepthMaskRenderer, so unlike the mesh SSBO binding (which the + /// RHI arm addresses through its own GpuBindingModel.StorageClipRegions + /// instead) this one is still genuinely shared. public const uint TerrainClipUboBinding = 2; // ---- CPU-side state ------------------------------------------------------ @@ -79,55 +80,14 @@ public sealed class ClipFrame : IDisposable // Packed std140 bytes for the terrain UBO (always TerrainUboBytes long). private readonly byte[] _terrainBytes = new byte[TerrainUboBytes]; - // ---- GL-side state (lazily created on first upload) ---------------------- - - private uint _regionSsbo; - private uint _terrainUbo; - private RetryableResourceReleaseLedger? _disposeResources; - private bool _disposing; - private bool _disposed; - - internal const int FrameSlotCount = GpuFrameFlightController.DefaultMaximumFramesInFlight; - - private sealed class RegionBuffer - { - public uint Name; - public int CapacityBytes; - public ClipBufferCapacityPolicy CapacityPolicy; - } - - private sealed class TerrainBufferArena - { - public uint Name; - public int CapacityBytes; - public ClipBufferCapacityPolicy CapacityPolicy; - } - - // A full clip-region table is immutable once ClipFrameAssembler has packed - // the frame. Keep exactly one SSBO per GPU-fenced frame slot. Terrain clip - // state changes between outside-view slices, so each frame slot owns one - // UBO arena and each draw binds a distinct aligned range within that arena. - // This bounds native object retention at six buffers total instead of one - // region/terrain pair per outside-view slice. - private readonly ClipFrameResourceRing _regionBuffers = - new(FrameSlotCount); - private readonly ClipFrameResourceRing _terrainBuffers = - new(FrameSlotCount); - private readonly ClipFrameUploadState _uploadState = new(); - private int _dynamicFrameSlot; - private bool _dynamicFrameStarted; - private int _terrainRecordStrideBytes; - private TerrainClipBufferBinding _terrainBinding; - - internal int DynamicBufferSetCount => - Math.Max(_regionBuffers.Count, _terrainBuffers.Count); - internal int RegionBufferCount => _regionBuffers.Count; - internal int TerrainBufferCount => _terrainBuffers.Count; - internal int TerrainRecordStrideBytes => _terrainRecordStrideBytes; - - // GL reference captured on the first upload so Dispose can delete each - // frame-slot buffer. ClipFrame is long-lived and owns their teardown. - private GL? _gl; + /// + /// The GL arm's per-flight-slot region/terrain buffer rings this used to + /// report on were deleted at Campaign V slice V11: every publication now + /// comes from the current frame's own ring allocator (see + /// RhiWorldPassSurface.Publish), which lives until its frame retires, + /// so there is no persistent dynamic-buffer pool left to size. + /// + internal int DynamicBufferSetCount => 0; private ClipFrame(byte[] regionBytes, int slotCount) { @@ -156,9 +116,10 @@ public sealed class ClipFrame : IDisposable /// /// Phase U.4: reset this frame back to the NoClip state — exactly slot 0 /// (no-clip, count 0) and a terrain count of 0 — WITHOUT allocating a new - /// frame or new GL buffers. The single long-lived _clipFrame in - /// GameWindow is reset + re-packed every frame by , - /// then uploaded through one SSBO and one terrain arena per fenced frame slot. + /// frame. The single long-lived _clipFrame in GameWindow is reset + + /// re-packed every frame by , then published + /// through one frame-ring allocation per section (see + /// RhiWorldPassSurface.PrepareClipFrame). /// public void Reset() { @@ -175,35 +136,18 @@ public sealed class ClipFrame : IDisposable Array.Clear(_terrainBytes); } - /// The shared mesh-clip SSBO id, or 0 before the first - /// . Renderers may bind this directly if they don't - /// receive it via a parameter; already binds it to - /// . - public uint RegionSsbo => _regionSsbo; - - /// The terrain-clip UBO id, or 0 before the first - /// . The buffer alone does not identify the - /// active range; consumers should use . - public uint TerrainUbo => _terrainUbo; - - /// The currently installed terrain-clip range. The buffer name is - /// stable for the current GPU-fenced frame slot; the offset advances once - /// for every outside-view draw. - public TerrainClipBufferBinding TerrainBinding => _terrainBinding; - /// - /// Selects one GPU-fenced frame slot. Its region SSBO and terrain arena are - /// safe to reuse because the frame-flight controller already waited for the - /// slot's preceding submission. + /// Per-frame hook kept for the shared render-frame-begin ordering — it is + /// called once per GPU-fenced frame slot alongside every other renderer's + /// BeginFrame, regardless of backend. The GL arm's frame-slot-scoped + /// region SSBO + terrain UBO arena this used to select was deleted at + /// Campaign V slice V11: the RHI arm takes a fresh ring allocation from the + /// current GPU frame for every publication instead, so there is no + /// frame-slot state left here to reset — only the argument to validate. /// public void BeginFrame(int frameSlot) { - if ((uint)frameSlot >= FrameSlotCount) - throw new ArgumentOutOfRangeException(nameof(frameSlot)); - _dynamicFrameSlot = frameSlot; - _dynamicFrameStarted = true; - _terrainBinding = default; - _uploadState.BeginFrame(); + ArgumentOutOfRangeException.ThrowIfNegative(frameSlot); } /// @@ -272,266 +216,14 @@ public sealed class ClipFrame : IDisposable } /// - /// Compatibility entry point for one-region/one-terrain callers. Complex - /// PView frames should reserve their complete terrain sequence, upload the - /// regions once, then call per slice. + /// No-op: this container owns no GPU resource of its own on the RHI arm — + /// the deleted GL arm's per-frame-slot region SSBO + terrain UBO arena was + /// the only thing to release. Kept as a method (and + /// kept as ) so GameWindowLifetime's ordered + /// shutdown doesn't need a special case for this one resource. /// - public unsafe void UploadShared(GL gl) - { - ReserveTerrainUploads(gl, 1); - UploadRegions(gl); - UploadTerrainClip(gl); - } - - /// - /// Allocates the current frame slot's terrain arena before any draw uses it. - /// The arena has one alignment-safe range per subsequent - /// call, so later slices never overwrite - /// uniform data referenced by earlier GPU commands. - /// - public void ReserveTerrainUploads(GL gl, int uploadCount) - { - ValidateGlContext(gl); - ArgumentOutOfRangeException.ThrowIfLessThan(uploadCount, 1); - _uploadState.ValidateTerrainReservation(uploadCount); - - if (_terrainRecordStrideBytes == 0) - { - GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain alignment query (precondition)"); - gl.GetInteger(GetPName.UniformBufferOffsetAlignment, out int alignment); - GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain alignment query"); - _terrainRecordStrideBytes = ClipFrameArenaLayout.RecordStride(alignment); - } - - TerrainBufferArena arena = GetOrCreateTerrainArena(gl); - int requiredBytes = ClipFrameArenaLayout.RequiredBytes( - uploadCount, - _terrainRecordStrideBytes); - int targetCapacity = arena.CapacityPolicy.SelectCapacity( - arena.CapacityBytes, - requiredBytes); - if (targetCapacity != arena.CapacityBytes) - { - ClipBufferCapacityTransaction.Resize( - ref arena.CapacityBytes, - targetCapacity, - (previousBytes, newBytes) => - TrackedGlResource.AllocateBufferStorage( - gl, - GLEnum.UniformBuffer, - arena.Name, - previousBytes, - newBytes, - GLEnum.DynamicDraw, - "ClipFrame terrain UBO arena resize")); - } - - _terrainUbo = arena.Name; - _uploadState.CommitTerrainReservation(uploadCount); - } - - /// Uploads and binds the immutable clip-region table once for the - /// assembled frame. - public unsafe void UploadRegions(GL gl) - { - ValidateGlContext(gl); - _uploadState.ValidateRegionsNotUploaded(); - - RegionBuffer region = GetOrCreateRegionBuffer(gl); - int regionByteCount = checked(_slotCount * CellClipStrideBytes); - int targetCapacity = region.CapacityPolicy.SelectCapacity( - region.CapacityBytes, - regionByteCount); - if (targetCapacity != region.CapacityBytes) - { - ClipBufferCapacityTransaction.Resize( - ref region.CapacityBytes, - targetCapacity, - (previousBytes, newBytes) => - TrackedGlResource.AllocateBufferStorage( - gl, - GLEnum.ShaderStorageBuffer, - region.Name, - previousBytes, - newBytes, - GLEnum.DynamicDraw, - "ClipFrame region SSBO resize")); - } - - fixed (byte* p = _regionBytes) - { - TrackedGlResource.UpdateBufferSubData( - gl, - GLEnum.ShaderStorageBuffer, - region.Name, - 0, - regionByteCount, - p, - "ClipFrame region SSBO update"); - } - gl.BindBufferBase( - BufferTargetARB.ShaderStorageBuffer, - MeshClipSsboBinding, - region.Name); - GLHelpers.ThrowOnResourceError(gl, "ClipFrame region SSBO binding"); - - _regionSsbo = region.Name; - _uploadState.MarkRegionsUploaded(); - } - - /// Uploads the current terrain clip bytes into the next unique - /// aligned arena record and installs that range at UBO binding 2. - public unsafe TerrainClipBufferBinding UploadTerrainClip(GL gl) - { - ValidateGlContext(gl); - int recordIndex = _uploadState.NextTerrainRecord(); - TerrainBufferArena arena = _terrainBuffers.GetRequired(_dynamicFrameSlot); - int offsetBytes = ClipFrameArenaLayout.RecordOffset( - recordIndex, - _terrainRecordStrideBytes); - - fixed (byte* p = _terrainBytes) - { - TrackedGlResource.UpdateBufferSubData( - gl, - GLEnum.UniformBuffer, - arena.Name, - offsetBytes, - TerrainUboBytes, - p, - "ClipFrame terrain UBO range update"); - } - - _terrainBinding = new TerrainClipBufferBinding( - arena.Name, - offsetBytes, - TerrainUboBytes); - _terrainBinding.Bind(gl); - _terrainUbo = arena.Name; - return _terrainBinding; - } - - public void BindTerrainClip(GL gl) - { - ValidateGlContext(gl); - if (!_terrainBinding.IsValid) - throw new InvalidOperationException("No terrain clip range has been uploaded for this frame."); - _terrainBinding.Bind(gl); - } - - private RegionBuffer GetOrCreateRegionBuffer(GL gl) - { - if (_regionBuffers.TryGet(_dynamicFrameSlot, out RegionBuffer? existing)) - return existing!; - - uint name = TrackedGlResource.CreateBuffer(gl, "ClipFrame region SSBO creation"); - var created = new RegionBuffer { Name = name }; - try - { - _regionBuffers.Set(_dynamicFrameSlot, created); - return created; - } - catch - { - TrackedGlResource.DeleteBuffer(gl, name, 0, "ClipFrame region SSBO rollback"); - throw; - } - } - - private TerrainBufferArena GetOrCreateTerrainArena(GL gl) - { - if (_terrainBuffers.TryGet(_dynamicFrameSlot, out TerrainBufferArena? existing)) - return existing!; - - uint name = TrackedGlResource.CreateBuffer(gl, "ClipFrame terrain UBO creation"); - var created = new TerrainBufferArena { Name = name }; - try - { - _terrainBuffers.Set(_dynamicFrameSlot, created); - return created; - } - catch - { - TrackedGlResource.DeleteBuffer(gl, name, 0, "ClipFrame terrain UBO rollback"); - throw; - } - } - - private void ValidateGlContext(GL gl) - { - ArgumentNullException.ThrowIfNull(gl); - ObjectDisposedException.ThrowIf(_disposed, this); - if (!_dynamicFrameStarted) - throw new InvalidOperationException("BeginFrame must be called before uploading clip data."); - if (_gl is null) - _gl = gl; - else if (!ReferenceEquals(_gl, gl)) - throw new InvalidOperationException("ClipFrame cannot span OpenGL contexts."); - } - public void Dispose() { - if (_disposed || _disposing) return; - _disposing = true; - try - { - if (_disposeResources is null) - { - var releases = new List<(string Name, Action Release)>(); - if (_gl is not null) - { - int regionIndex = 0; - foreach (RegionBuffer set in _regionBuffers.Values) - { - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - set.Name, - set.CapacityBytes, - "ClipFrame region SSBO disposal"); - releases.Add(($"region-ssbo-{regionIndex++}", release.Run)); - } - - int terrainIndex = 0; - foreach (TerrainBufferArena arena in _terrainBuffers.Values) - { - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - arena.Name, - arena.CapacityBytes, - "ClipFrame terrain UBO disposal"); - releases.Add(($"terrain-ubo-{terrainIndex++}", release.Run)); - } - } - - _disposeResources = new RetryableResourceReleaseLedger(releases); - } - - ResourceReleaseAttempt attempt = _disposeResources.Advance(); - if (!_disposeResources.IsComplete) - { - throw attempt.ToException( - "One or more ClipFrame GPU buffers could not be released."); - } - - _regionSsbo = 0; - _terrainUbo = 0; - _terrainBinding = default; - _dynamicFrameStarted = false; - _disposeResources = null; - _disposed = true; - - if (attempt.HasFailures) - { - throw attempt.ToException( - "ClipFrame GPU buffers released with exceptional committed outcomes."); - } - } - finally - { - _disposing = false; - } } // ---- byte helpers (little-endian; matches x86/x64 GPU upload) ------------ @@ -573,11 +265,11 @@ public sealed class ClipFrame : IDisposable /// /// The packed std430 region table for slots 0..-1. /// - /// Campaign V slice V6j: the GL arm hands these to - /// glBufferSubData against a renderer-owned SSBO; the RHI arm copies - /// them straight into a frame ring slice and publishes the range. Same bytes, - /// two destinations — which is why the packing above is backend-neutral and - /// stays where it is. + /// The raw-GL arm this used to feed via glBufferSubData against + /// a renderer-owned SSBO was deleted at Campaign V slice V11; the RHI arm + /// (RhiWorldPassSurface.PrepareClipFrame) now copies these bytes + /// straight into a frame ring slice and publishes the range. The packing + /// above stays backend-neutral regardless. /// internal ReadOnlySpan RegionBytes => _regionBytes.AsSpan(0, _slotCount * CellClipStrideBytes); @@ -594,230 +286,3 @@ public sealed class ClipFrame : IDisposable /// Test seam: the packed std140 terrain UBO bytes. internal ReadOnlySpan TerrainBytesForTest => TerrainBytes; } - -/// A single std140 terrain-clip record within a frame-slot UBO arena. -public readonly record struct TerrainClipBufferBinding( - uint Buffer, - int OffsetBytes, - int SizeBytes) -{ - public bool IsValid => Buffer != 0 && OffsetBytes >= 0 && SizeBytes > 0; - - public void Bind(GL gl) - { - ArgumentNullException.ThrowIfNull(gl); - if (!IsValid) - throw new InvalidOperationException("Cannot bind an empty terrain clip range."); - - gl.BindBufferRange( - BufferTargetARB.UniformBuffer, - ClipFrame.TerrainClipUboBinding, - Buffer, - (nint)OffsetBytes, - (nuint)SizeBytes); - GLHelpers.ThrowOnResourceError(gl, "ClipFrame terrain UBO range binding"); - } -} - -internal static class ClipFrameArenaLayout -{ - public static int RecordStride(int uniformBufferOffsetAlignment) - { - ArgumentOutOfRangeException.ThrowIfLessThan(uniformBufferOffsetAlignment, 1); - long stride = ((long)ClipFrame.TerrainUboBytes + uniformBufferOffsetAlignment - 1L) - / uniformBufferOffsetAlignment - * uniformBufferOffsetAlignment; - if (stride > int.MaxValue) - throw new OverflowException("Terrain clip UBO record stride exceeds Int32.MaxValue."); - return (int)stride; - } - - public static int RecordOffset(int recordIndex, int recordStrideBytes) - { - ArgumentOutOfRangeException.ThrowIfNegative(recordIndex); - ArgumentOutOfRangeException.ThrowIfLessThan(recordStrideBytes, ClipFrame.TerrainUboBytes); - return checked(recordIndex * recordStrideBytes); - } - - public static int RequiredBytes(int recordCount, int recordStrideBytes) - { - ArgumentOutOfRangeException.ThrowIfLessThan(recordCount, 1); - ArgumentOutOfRangeException.ThrowIfLessThan(recordStrideBytes, ClipFrame.TerrainUboBytes); - return checked(recordCount * recordStrideBytes); - } -} - -internal static class ClipBufferCapacityTransaction -{ - /// Publishes capacity only after BufferData succeeds, and before - /// any later upload/bind operation can throw. - public static void Resize( - ref int publishedCapacityBytes, - int targetCapacityBytes, - Action allocate) - { - ArgumentOutOfRangeException.ThrowIfNegative(publishedCapacityBytes); - ArgumentOutOfRangeException.ThrowIfLessThan(targetCapacityBytes, 1); - ArgumentNullException.ThrowIfNull(allocate); - - int previousCapacityBytes = publishedCapacityBytes; - allocate(previousCapacityBytes, targetCapacityBytes); - publishedCapacityBytes = targetCapacityBytes; - } -} - -/// -/// Growth is immediate; shrinking requires repeated severe under-use. This -/// avoids reallocating during ordinary portal-count jitter while ensuring a -/// one-off pathological flood does not permanently pin its peak GPU storage. -/// -internal struct ClipBufferCapacityPolicy -{ - internal const int ShrinkAfterUnderusedFrames = 3; - internal const int ShrinkUtilizationDivisor = 4; - - private int _underusedFrames; - - public int SelectCapacity(int currentBytes, int requiredBytes) - { - ArgumentOutOfRangeException.ThrowIfNegative(currentBytes); - ArgumentOutOfRangeException.ThrowIfLessThan(requiredBytes, 1); - - if (requiredBytes > currentBytes) - { - _underusedFrames = 0; - return DynamicBufferCapacity.Grow(currentBytes, requiredBytes); - } - - if ((long)requiredBytes * ShrinkUtilizationDivisor <= currentBytes) - { - _underusedFrames++; - if (_underusedFrames >= ShrinkAfterUnderusedFrames) - { - _underusedFrames = 0; - return DynamicBufferCapacity.Grow(0, requiredBytes); - } - } - else - { - _underusedFrames = 0; - } - - return currentBytes; - } -} - -/// Fixed-cardinality resource ownership indexed by the same frame -/// slots protected by . -internal sealed class ClipFrameResourceRing(int slotCount) where T : class -{ - private readonly T?[] _slots = new T?[slotCount > 0 - ? slotCount - : throw new ArgumentOutOfRangeException(nameof(slotCount))]; - - public int Count { get; private set; } - - public bool TryGet(int slot, out T? resource) - { - ValidateSlot(slot); - resource = _slots[slot]; - return resource is not null; - } - - public T GetRequired(int slot) - { - ValidateSlot(slot); - return _slots[slot] - ?? throw new InvalidOperationException($"Frame slot {slot} has no resource."); - } - - public void Set(int slot, T resource) - { - ValidateSlot(slot); - ArgumentNullException.ThrowIfNull(resource); - if (_slots[slot] is not null) - throw new InvalidOperationException($"Frame slot {slot} already owns a resource."); - _slots[slot] = resource; - Count++; - } - - public IEnumerable Values - { - get - { - for (int i = 0; i < _slots.Length; i++) - if (_slots[i] is T value) - yield return value; - } - } - - private void ValidateSlot(int slot) - { - if ((uint)slot >= (uint)_slots.Length) - throw new ArgumentOutOfRangeException(nameof(slot)); - } -} - -/// Pure sequencing state for one ClipFrame render submission. -internal sealed class ClipFrameUploadState -{ - private bool _frameStarted; - private bool _regionsUploaded; - private int _terrainReserved; - private int _terrainCursor; - - public int TerrainReserved => _terrainReserved; - public int TerrainUploaded => _terrainCursor; - public bool RegionsUploaded => _regionsUploaded; - - public void BeginFrame() - { - _frameStarted = true; - _regionsUploaded = false; - _terrainReserved = 0; - _terrainCursor = 0; - } - - public void ValidateRegionsNotUploaded() - { - EnsureFrameStarted(); - if (_regionsUploaded) - throw new InvalidOperationException("Clip regions may be uploaded only once per frame."); - } - - public void MarkRegionsUploaded() - { - ValidateRegionsNotUploaded(); - _regionsUploaded = true; - } - - public void ValidateTerrainReservation(int uploadCount) - { - EnsureFrameStarted(); - ArgumentOutOfRangeException.ThrowIfLessThan(uploadCount, 1); - if (_terrainReserved != 0) - throw new InvalidOperationException("Terrain uploads have already been reserved for this frame."); - } - - public void CommitTerrainReservation(int uploadCount) - { - ValidateTerrainReservation(uploadCount); - _terrainReserved = uploadCount; - } - - public int NextTerrainRecord() - { - EnsureFrameStarted(); - if (_terrainReserved == 0) - throw new InvalidOperationException("ReserveTerrainUploads must run before terrain clip uploads."); - if (_terrainCursor >= _terrainReserved) - throw new InvalidOperationException("Terrain clip uploads exceeded the reserved arena record count."); - return _terrainCursor++; - } - - private void EnsureFrameStarted() - { - if (!_frameStarted) - throw new InvalidOperationException("BeginFrame must be called before uploading clip data."); - } -} diff --git a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs index 405c796a..aa279667 100644 --- a/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs +++ b/src/AcDream.App/Rendering/CompositeTextureArrayCache.cs @@ -1,8 +1,6 @@ using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; using AcDream.Core.Textures; using AcDream.Core.World; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -163,192 +161,6 @@ internal interface ICompositeTextureArrayBackend void Delete(CompositeTextureArrayResource resource); } -/// -/// Narrow GL backend for composite arrays. Unlike ManagedGLTextureArray it -/// deliberately has one mip level, no PBO, and one resident handle: those are -/// the semantics of the standalone composite textures this pool replaces. -/// -internal sealed unsafe class GlCompositeTextureArrayBackend : ICompositeTextureArrayBackend -{ - private readonly GL _gl; - private readonly Wb.BindlessSupport _bindless; - private readonly GlGpuDevice _device; - - public GlCompositeTextureArrayBackend(GL gl, Wb.BindlessSupport bindless, GlGpuDevice device) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _bindless = bindless ?? throw new ArgumentNullException(nameof(bindless)); - _device = device ?? throw new ArgumentNullException(nameof(device)); - _gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maximumLayers); - MaximumArrayLayers = Math.Max(1, maximumLayers); - } - - public int MaximumArrayLayers { get; } - - public CompositeTextureArrayResource Create(int width, int height, int capacity) - { - uint name = _gl.GenTexture(); - if (name == 0) - throw new InvalidOperationException("OpenGL did not create a composite texture array."); - - bool resident = false; - ulong handle = 0; - long bytes = 0; - bool tracked = false; - try - { - // Composite creation/upload runs in the render thread's pre-draw - // preparation phase. Normalize that phase to texture unit zero - // instead of synchronously reading driver binding state. - _gl.ActiveTexture(TextureUnit.Texture0); - Wb.RenderStateCache.CurrentAtlas = 0; - _gl.BindTexture(TextureTarget.Texture2DArray, name); - _gl.TexStorage3D( - TextureTarget.Texture2DArray, - levels: 1, - SizedInternalFormat.Rgba8, - checked((uint)width), - checked((uint)height), - checked((uint)capacity)); - _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureBaseLevel, 0); - _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMaxLevel, 0); - _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); - _gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); - handle = _bindless.GetResidentHandle(name); - resident = true; - Wb.GLHelpers.ThrowOnResourceError( - _gl, - $"creating composite texture array {width}x{height}x{capacity}"); - bytes = checked((long)width * height * 4L * capacity); - Wb.GpuMemoryTracker.TrackResourceAllocation(Wb.GpuResourceType.Texture); - Wb.GpuMemoryTracker.TrackAllocation(bytes, Wb.GpuResourceType.Texture); - tracked = true; - // Campaign V slice V4t: intern the resident handle into the device's - // one texture table. A table-exhaustion throw here is caught by the - // same rollback below, and nothing was added to the table if it did. - GpuTextureSlot slot = _device.RegisterWorldTextureHandle(handle); - return new CompositeTextureArrayResource - { - Name = name, - Handle = handle, - Slot = slot, - Width = width, - Height = height, - Capacity = capacity, - Bytes = bytes, - }; - } - catch (Exception creationFailure) - { - List? cleanupFailures = null; - void Attempt(Action cleanup) - { - try { cleanup(); } - catch (Exception ex) { (cleanupFailures ??= []).Add(ex); } - } - - bool residencyReleased = !resident; - if (resident) - { - Attempt(() => - { - _bindless.MakeNonResident(handle); - Wb.GLHelpers.ThrowOnResourceError(_gl, "rolling back composite texture residency"); - residencyReleased = true; - }); - } - if (residencyReleased) - { - Attempt(() => - { - _gl.DeleteTexture(name); - Wb.GLHelpers.ThrowOnResourceError(_gl, "rolling back composite texture array"); - if (tracked) - { - Wb.GpuMemoryTracker.TrackDeallocation(bytes, Wb.GpuResourceType.Texture); - Wb.GpuMemoryTracker.TrackResourceDeallocation(Wb.GpuResourceType.Texture); - } - }); - } - - if (cleanupFailures is not null) - { - cleanupFailures.Insert(0, creationFailure); - throw new AggregateException( - "Composite texture-array construction and rollback both failed.", - cleanupFailures); - } - throw; - } - finally - { - _gl.BindTexture(TextureTarget.Texture2DArray, 0); - _gl.ActiveTexture(TextureUnit.Texture0); - } - } - - public void Upload(CompositeTextureArrayResource resource, int layer, byte[] rgba) - { - _gl.ActiveTexture(TextureUnit.Texture0); - Wb.RenderStateCache.CurrentAtlas = 0; - try - { - _gl.BindBuffer(BufferTargetARB.PixelUnpackBuffer, 0); - _gl.BindTexture(TextureTarget.Texture2DArray, resource.Name); - fixed (byte* pixels = rgba) - { - _gl.TexSubImage3D( - TextureTarget.Texture2DArray, - level: 0, - xoffset: 0, - yoffset: 0, - zoffset: layer, - checked((uint)resource.Width), - checked((uint)resource.Height), - depth: 1, - PixelFormat.Rgba, - PixelType.UnsignedByte, - pixels); - } - Wb.GLHelpers.ThrowOnResourceError( - _gl, - $"uploading composite texture layer {layer} ({resource.Width}x{resource.Height})"); - } - finally - { - _gl.BindTexture(TextureTarget.Texture2DArray, 0); - _gl.BindBuffer(BufferTargetARB.PixelUnpackBuffer, 0); - _gl.ActiveTexture(TextureUnit.Texture0); - } - } - - public void MakeNonResident(CompositeTextureArrayResource resource) - { - Wb.GLHelpers.ThrowOnResourceError( - _gl, - $"releasing composite texture handle {resource.Handle} (precondition)"); - // Campaign V slice V4t: retire the table entry before the handle it - // names stops being resident. Idempotent, so this stays correct when - // the retryable release ledger re-runs the operation. - _device.ReleaseWorldTextureHandle(resource.Handle); - _bindless.MakeNonResident(resource.Handle); - Wb.GLHelpers.ThrowOnResourceError(_gl, $"releasing composite texture handle {resource.Handle}"); - } - - public void Delete(CompositeTextureArrayResource resource) - { - Wb.GLHelpers.ThrowOnResourceError( - _gl, - $"deleting composite texture array {resource.Name} (precondition)"); - _gl.DeleteTexture(resource.Name); - Wb.GLHelpers.ThrowOnResourceError(_gl, $"deleting composite texture array {resource.Name}"); - Wb.GpuMemoryTracker.TrackDeallocation(resource.Bytes, Wb.GpuResourceType.Texture); - Wb.GpuMemoryTracker.TrackResourceDeallocation(Wb.GpuResourceType.Texture); - } -} - /// /// Campaign V slice V6i-2: the backend-neutral composite array backend. /// @@ -525,25 +337,6 @@ internal sealed class CompositeTextureArrayCache : IDisposable Accounted, } - public CompositeTextureArrayCache( - GL gl, - Wb.BindlessSupport bindless, - GlGpuDevice device, - IGpuResourceRetirementQueue retirementQueue, - long unownedBudgetBytes = DefaultUnownedBudgetBytes, - long physicalBudgetBytes = DefaultPhysicalBudgetBytes, - int maximumUploadsPerFrame = DefaultMaximumUploadsPerFrame, - long maximumUploadBytesPerFrame = DefaultMaximumUploadBytesPerFrame) - : this( - new GlCompositeTextureArrayBackend(gl, bindless, device), - retirementQueue, - unownedBudgetBytes, - physicalBudgetBytes, - maximumUploadsPerFrame, - maximumUploadBytesPerFrame) - { - } - internal CompositeTextureArrayCache( ICompositeTextureArrayBackend backend, IGpuResourceRetirementQueue retirementQueue, diff --git a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs index 824e585b..f27b37d3 100644 --- a/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs +++ b/src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs @@ -1,13 +1,11 @@ using System.Numerics; using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.App.World; using AcDream.Core.Lighting; using AcDream.Core.World; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -377,8 +375,7 @@ internal sealed class CreatureAppraisalViewportRenderer : private readonly PrivateEntityViewportRenderer _renderer; public CreatureAppraisalViewportRenderer( - GL? gl, - IWorldPassScope? scope, + IWorldPassScope scope, AcDream.App.Rendering.Gpu.IGpuDevice device, ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, @@ -387,7 +384,6 @@ internal sealed class CreatureAppraisalViewportRenderer : IWbMeshAdapter meshAdapter) { _renderer = new PrivateEntityViewportRenderer( - gl, scope, device, frames, diff --git a/src/AcDream.App/Rendering/FrameProfilerGpuMeasurement.cs b/src/AcDream.App/Rendering/FrameProfilerGpuMeasurement.cs deleted file mode 100644 index fe7d927a..00000000 --- a/src/AcDream.App/Rendering/FrameProfilerGpuMeasurement.cs +++ /dev/null @@ -1,32 +0,0 @@ -using AcDream.App.Diagnostics; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -/// -/// Production render-transaction measurement adapter. CPU frame boundaries -/// remain owned by while this adapter places the -/// GPU query around only the GL-producing render phases. -/// -internal sealed class FrameProfilerGpuMeasurement : IRenderFrameGpuMeasurement -{ - private readonly FrameProfiler _profiler; - private readonly GL _gl; - - public FrameProfilerGpuMeasurement(FrameProfiler profiler, GL gl) - { - _profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - } - - public void BeginFrame() - { - _profiler.FrameBoundary(_gl); - _profiler.BeginGpuFrame(); - } - - public void EndFrame() - { - _profiler.EndGpuFrame(); - } -} diff --git a/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs b/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs index 71aeb182..2c9f4b47 100644 --- a/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs +++ b/src/AcDream.App/Rendering/GameRenderResourceLifetime.cs @@ -12,15 +12,9 @@ internal interface IGameRenderResourceLifetime internal sealed class GameRenderResourceLifetime : IGameRenderResourceLifetime { private readonly OwnedResourceSlot _terrainAtlas = new(); - private readonly OwnedResourceSlot _skyShader = new(); public TerrainAtlas AcquireTerrainAtlas(Func factory) => _terrainAtlas.Acquire(factory); - public Shader AcquireSkyShader(Func factory) => - _skyShader.Acquire(factory); - public void ReleaseTerrainAtlas() => _terrainAtlas.Release(); - - public void ReleaseSkyShader() => _skyShader.Release(); } diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index e5f4e510..f60e9e24 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -16,7 +16,6 @@ using AcDream.Runtime.Session; using DatReaderWriter; using Silk.NET.Input; using Silk.NET.Maths; -using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace AcDream.App.Rendering; @@ -56,15 +55,11 @@ public sealed class GameWindow : AcDream.UI.Abstractions.Settings.QualityPreset.High); private IInputContext? _input; private TerrainModernRenderer? _terrain; - /// Phase N.5b: terrain_modern.vert/.frag program. Owned by - /// at draw time but allocated + disposed here. - private Shader? _terrainModernShader; private CameraController? _cameraController; private IDatReaderWriter? _dats; private IPreparedAssetSource? _preparedAssets; private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new(); private AcDream.App.Input.CameraPointerInputController? _cameraPointerInput; - private Shader? _meshShader; private TextureCache? _textureCache; /// Phase N.4+: WB-backed rendering pipeline adapter. Always non-null /// after OnLoad completes (modern path is mandatory as of N.5). @@ -77,11 +72,6 @@ public sealed class GameWindow : private AcDream.App.Rendering.Selection.RetailSelectionScene? _retailSelectionScene; private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery; private AcDream.App.Interaction.SelectionInteractionController? _selectionInteractions; - /// Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters - /// support. Required at startup — missing bindless throws - /// in OnLoad. - private AcDream.App.Rendering.Wb.BindlessSupport? _bindlessSupport; - private SamplerCache? _samplerCache; private DebugLineRenderer? _debugLines; // K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder // wireframes are noisy outdoors and confuse first-time users into @@ -128,8 +118,8 @@ public sealed class GameWindow : private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new(); private readonly AcDream.App.Rendering.GameRenderResourceLifetime _renderResourceLifetime = new(); - private readonly AcDream.App.Rendering.GlConstructionCleanupLedger - _glConstructionCleanup = new(); + private readonly AcDream.App.Rendering.ResourceConstructionCleanupLedger + _constructionCleanup = new(); private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment; private readonly GameWindowLifetime _lifetime = new(); private readonly DisplayFramePacingController _displayFramePacing; @@ -518,7 +508,6 @@ public sealed class GameWindow : private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings; private readonly GraphicalHostPlatformServices _platformServices; private readonly ApplicationPathSet _applicationPaths; - private GraphicalCapabilityRecord? _graphicalCapabilities; private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings( string path) @@ -687,8 +676,7 @@ public sealed class GameWindow : // attribute, so it must come from this same snapshot rather than a // second settings load during OnLoad. RuntimeSettingsSnapshot startup = _runtimeSettings.Startup; - if (_options.RenderBackend == RenderBackendKind.Vulkan - && _options.VulkanCapabilityProbe) + if (_options.VulkanCapabilityProbe) { // Campaign V slice V6h reduced the V5 bring-up host to what its name // says: a capability probe. It opens its own window, runs the gate, @@ -706,39 +694,17 @@ public sealed class GameWindow : FramePacingPolicy startupPacing = _displayFramePacing.InitializeStartup(startup.Display.VSync); - // Campaign V slice V6h: the ONLY startup difference between the two - // backends. Vulkan needs a client-API-less window (the surface comes from - // VK_KHR_surface), and neither MSAA nor the stencil bit count is a window + // Vulkan needs a client-API-less window (the surface comes from + // VK_KHR_surface); neither MSAA nor the stencil bit count is a window // attribute there — both are attachment properties the RHI device - // configures, so they are passed to VulkanGraphicsContext instead. - var options = _options.RenderBackend == RenderBackendKind.Vulkan - ? WindowOptions.DefaultVulkan with - { - Size = new Vector2D(1280, 720), - Title = "acdream — Vulkan", - VSync = startupPacing.UseVSync, - } - : WindowOptions.Default with - { - Size = new Vector2D(1280, 720), - Title = "acdream — phase 1", - API = new GraphicsAPI( - ContextAPI.OpenGL, - ContextProfile.Core, - ContextFlags.ForwardCompatible, - new APIVersion(4, 3)), - VSync = startupPacing.UseVSync, - // A.5 T22.5: MSAA from quality preset (0 = disabled, 2/4/8 = multisample). - // Silk.NET passes this to SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES). - // Cannot be changed at runtime; Quality changes mid-session that would - // alter MsaaSamples are logged as a restart-required warning. - Samples = startup.Quality.MsaaSamples, - // #117 (2026-06-11): the aperture punch's depth gate needs a - // stencil buffer (PortalDepthMaskRenderer two-pass mark+punch). - // GLFW defaults to 8 stencil bits, but make the requirement - // explicit rather than platform-implicit. - PreferredStencilBufferBits = 8, - }; + // configures instead. The raw-GL window options this used to fork to + // were deleted at Campaign V slice V11. + var options = WindowOptions.DefaultVulkan with + { + Size = new Vector2D(1280, 720), + Title = "acdream — Vulkan", + VSync = startupPacing.UseVSync, + }; _startupPacing = startupPacing; _startupQuality = startup.Quality; @@ -766,7 +732,7 @@ public sealed class GameWindow : } catch (Exception failure) { - _glConstructionCleanup.RetainFrom(failure); + _constructionCleanup.RetainFrom(failure); throw; } } @@ -912,19 +878,6 @@ public sealed class GameWindow : _audioSink = value.HookSink; } - void IGameWindowWorldRenderPublication.PublishBindlessSupport( - BindlessSupport value) => - PublishCompositionOwner( - ref _bindlessSupport, - value, - "bindless support"); - - void IGameWindowWorldRenderPublication.PublishTerrainShader(Shader value) => - PublishCompositionOwner( - ref _terrainModernShader, - value, - "terrain shader"); - void IGameWindowWorldRenderPublication.PublishSceneLighting( SceneLightingUboBinding value) => PublishCompositionOwner( @@ -975,9 +928,6 @@ public sealed class GameWindow : _surfaceCache = surfaceCache; } - void IGameWindowWorldRenderPublication.PublishMeshShader(Shader value) => - PublishCompositionOwner(ref _meshShader, value, "mesh shader"); - void IGameWindowWorldRenderPublication.PublishWbMeshAdapter( WbMeshAdapter value) => PublishCompositionOwner(ref _wbMeshAdapter, value, "WB mesh adapter"); @@ -986,10 +936,6 @@ public sealed class GameWindow : TextureCache value) => PublishCompositionOwner(ref _textureCache, value, "texture cache"); - void IGameWindowWorldRenderPublication.PublishSamplerCache( - SamplerCache value) => - PublishCompositionOwner(ref _samplerCache, value, "sampler cache"); - void IGameWindowInteractionRetainedUiPublication.PublishInteractionRetainedUi( InteractionRetainedUiResult result) { @@ -1192,41 +1138,31 @@ public sealed class GameWindow : } /// - /// Campaign V slice V6h: how the UI probe reads a completed frame. - /// - /// applies the bottom-up flip - /// glReadPixels needs, so GL hands it the raw read and Vulkan — whose - /// is documented top-left-origin — - /// pre-flips so the two cancel. Routing GL through the same - /// CaptureBackbuffer seam would double-flip, which is exactly the kind - /// of "usually right" instrument §5.5 of the campaign plan spent three - /// slices removing. + /// How the UI probe reads a completed frame. + /// is documented top-left-origin, + /// so flips it to match the PNG's + /// row order. The raw-GL read this used to fork from — whose rows already + /// come out bottom-up, needing no flip — was deleted at Campaign V slice + /// V11. /// private static Func CreateBackbufferReader( GameWindowGraphics graphics, IGpuDevice device) => - graphics.Gl is { } gl - ? (width, height) => - AcDream.App.Diagnostics.FrameScreenshotController - .ReadDefaultFramebuffer(gl, width, height) - : (width, height) => - AcDream.App.Diagnostics.FrameScreenshotController.FlipRows( - device.CaptureBackbuffer(width, height), - width, - height); + (width, height) => + AcDream.App.Diagnostics.FrameScreenshotController.FlipRows( + device.CaptureBackbuffer(width, height), + width, + height); /// - /// Campaign V slice V6h: the backend fork, in one expression. Vulkan's - /// context acquisition runs its own capability gate inside + /// Vulkan's context acquisition runs its own capability gate inside /// , /// throwing into the same exit-code-4 - /// contract Program.cs publishes for GL. + /// contract Program.cs publishes. The raw-GL fork this used to make + /// was deleted at Campaign V slice V11. /// private GameWindowGraphics CreateGraphics() { - if (_options.RenderBackend != RenderBackendKind.Vulkan) - return new OpenGlGameWindowGraphics(GL.GetApi(_window!)); - AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext vulkan = AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext.Acquire( _window!, @@ -1245,32 +1181,10 @@ public sealed class GameWindow : // run narrow-phase BSP tests during FindObjCollisions. GameWindowPlatformResult platform = AcquirePlatform(); - // The GL capability gate reads GL extension strings, so it runs only on - // the GL arm. Vulkan's equivalent gate already ran inside - // VulkanGraphicsContext.Acquire and wrote its own report. - if (platform.Graphics.Gl is { } capabilityGl) - { - string capabilityReportPath = Path.Combine( - _applicationPaths.DiagnosticsDirectory, - "graphical-capabilities.json"); - _graphicalCapabilities = - GraphicalCapabilityGuard.CaptureVerifyAndWrite( - capabilityGl, - _window!, - platform.Input, - _platformServices, - capabilityReportPath); - GraphicalCapabilityGuard.ThrowIfUnsupported( - _graphicalCapabilities, - capabilityReportPath); - Console.WriteLine( - "graphics: capability gate passed " + - $"({_graphicalCapabilities.ActiveDisplayProtocol}, " + - $"{_graphicalCapabilities.GlVendor}, " + - $"{_graphicalCapabilities.GlRenderer}, " + - $"{_graphicalCapabilities.GlVersion}); " + - $"report={capabilityReportPath}"); - } + // The raw-GL capability gate that used to run here (reading GL + // extension strings) was deleted at Campaign V slice V11. Vulkan's + // equivalent gate already ran inside VulkanGraphicsContext.Acquire and + // wrote its own report. GameWindowCompositionPipeline.Run< GameWindowPlatformResult, @@ -1727,12 +1641,9 @@ public sealed class GameWindow : _clipFrame, _skyRenderer, _particleRenderer, - _samplerCache, _textureCache, _wbMeshAdapter, - _meshShader, _terrain, - _terrainModernShader, _sceneLightingUbo, _debugLines, _textRenderer, @@ -1740,7 +1651,7 @@ public sealed class GameWindow : _displayFramePacing, _frameProfiler, _renderResourceLifetime, - _glConstructionCleanup), + _constructionCleanup), new PlatformShutdownRoots( _dats, _preparedAssets, diff --git a/src/AcDream.App/Rendering/GameWindowLifetime.cs b/src/AcDream.App/Rendering/GameWindowLifetime.cs index 8e8f29f1..72f66e87 100644 --- a/src/AcDream.App/Rendering/GameWindowLifetime.cs +++ b/src/AcDream.App/Rendering/GameWindowLifetime.cs @@ -25,7 +25,6 @@ using AcDream.Runtime.Session; using AcDream.UI.Abstractions.Input; using DatReaderWriter; using Silk.NET.Input; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -111,12 +110,9 @@ internal sealed record RenderShutdownRoots( ClipFrame? ClipFrame, SkyRenderer? Sky, ParticleRenderer? Particles, - SamplerCache? Samplers, TextureCache? Textures, WbMeshAdapter? MeshAdapter, - Shader? MeshShader, TerrainModernRenderer? Terrain, - Shader? TerrainShader, SceneLightingUboBinding? SceneLighting, DebugLineRenderer? DebugLines, TextRenderer? TextRenderer, @@ -124,7 +120,7 @@ internal sealed record RenderShutdownRoots( DisplayFramePacingController FramePacing, FrameProfiler FrameProfiler, GameRenderResourceLifetime DedicatedResources, - GlConstructionCleanupLedger ConstructionCleanup); + ResourceConstructionCleanupLedger ConstructionCleanup); internal sealed record PlatformShutdownRoots( IDatReaderWriter? Dats, @@ -439,7 +435,6 @@ internal static class GameWindowShutdownManifest ]), new ResourceShutdownStage("shared texture owners", [ - Hard("sampler cache", () => render.Samplers?.Dispose()), Hard("texture cache", () => render.Textures?.Dispose()), ]), new ResourceShutdownStage("mesh adapter", @@ -448,9 +443,7 @@ internal static class GameWindowShutdownManifest ]), new ResourceShutdownStage("remaining render owners", [ - Hard("mesh shader", () => render.MeshShader?.Dispose()), Hard("terrain", () => render.Terrain?.Dispose()), - Hard("terrain shader", () => render.TerrainShader?.Dispose()), Hard("scene lighting", () => render.SceneLighting?.Dispose()), Hard("debug lines", () => render.DebugLines?.Dispose()), Hard("text renderer", () => render.TextRenderer?.Dispose()), @@ -461,12 +454,11 @@ internal static class GameWindowShutdownManifest ]), new ResourceShutdownStage("dedicated render resources", [ - Hard("sky shader", render.DedicatedResources.ReleaseSkyShader), Hard("terrain atlas", render.DedicatedResources.ReleaseTerrainAtlas), ]), new ResourceShutdownStage("failed render construction cleanup", [ - Hard("GL construction ledger", render.ConstructionCleanup.Dispose), + Hard("resource construction ledger", render.ConstructionCleanup.Dispose), ]), new ResourceShutdownStage("frame flight owner", [ @@ -481,7 +473,7 @@ internal static class GameWindowShutdownManifest [ Hard("input context", () => platform.Input?.Dispose()), ]), - new ResourceShutdownStage("OpenGL context", + new ResourceShutdownStage("graphics API context", [ Hard("graphics API", () => platform.Graphics?.Dispose()), ])); diff --git a/src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs b/src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs deleted file mode 100644 index e1bc73ef..00000000 --- a/src/AcDream.App/Rendering/GlConstructionCleanupLedger.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace AcDream.App.Rendering; - -internal interface IRetryableResourceCleanup -{ - bool IsCleanupComplete { get; } - void RetryCleanup(); -} - -internal sealed class GlResourceConstructionException : AggregateException, - IRetryableResourceCleanup -{ - private readonly IRetryableResourceCleanup _cleanup; - - public GlResourceConstructionException( - string message, - IRetryableResourceCleanup cleanup, - IEnumerable failures) - : base(message, failures) - { - _cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup)); - } - - public bool IsCleanupComplete => _cleanup.IsCleanupComplete; - - public void RetryCleanup() => _cleanup.RetryCleanup(); -} - -/// -/// Lifetime root for cleanup work that could not finish before a throwing GL -/// factory returned control. The original exception remains the retry owner; -/// this ledger prevents it and its exact pending names from becoming local-only. -/// -internal sealed class GlConstructionCleanupLedger : IDisposable -{ - private readonly List _pending = []; - private bool _disposing; - - public bool IsComplete => _pending.Count == 0; - - public bool RetainFrom(Exception failure) - { - ArgumentNullException.ThrowIfNull(failure); - bool retained = false; - Visit(failure); - return retained; - - void Visit(Exception current) - { - if (current is IRetryableResourceCleanup cleanup) - { - if (!cleanup.IsCleanupComplete && !_pending.Contains(cleanup)) - _pending.Add(cleanup); - retained = true; - } - - if (current is AggregateException aggregate) - { - foreach (Exception inner in aggregate.InnerExceptions) - Visit(inner); - } - else if (current.InnerException is { } inner) - { - Visit(inner); - } - } - } - - public void Dispose() - { - if (_disposing || _pending.Count == 0) - return; - - _disposing = true; - List? failures = null; - try - { - for (int i = _pending.Count - 1; i >= 0; i--) - { - IRetryableResourceCleanup cleanup = _pending[i]; - try - { - cleanup.RetryCleanup(); - if (cleanup.IsCleanupComplete) - _pending.RemoveAt(i); - } - catch (Exception failure) - { - (failures ??= []).Add(failure); - } - } - } - finally - { - _disposing = false; - } - - if (failures is not null) - throw new AggregateException( - "One or more failed GL construction transactions remain pending.", - failures); - } -} diff --git a/src/AcDream.App/Rendering/GlResourceCommand.cs b/src/AcDream.App/Rendering/GlResourceCommand.cs deleted file mode 100644 index 57478bb5..00000000 --- a/src/AcDream.App/Rendering/GlResourceCommand.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Runtime.ExceptionServices; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -/// -/// Always-on commit boundary for GL resource commands. OpenGL reports ordinary -/// command failures through its error flag, so an ownership state machine may -/// advance only after the post-command check succeeds. -/// -internal static class GlResourceCommand -{ - public static void Execute(GL gl, string context, Action command) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentException.ThrowIfNullOrWhiteSpace(context); - ArgumentNullException.ThrowIfNull(command); - - GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"); - command(); - GLHelpers.ThrowOnResourceError(gl, context); - } - - public static T Execute(GL gl, string context, Func command) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentException.ThrowIfNullOrWhiteSpace(context); - ArgumentNullException.ThrowIfNull(command); - - GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"); - T result = command(); - GLHelpers.ThrowOnResourceError(gl, context); - return result; - } - - public static uint CreateName( - GL gl, - string resourceName, - Func create, - Action delete) - { - ArgumentException.ThrowIfNullOrWhiteSpace(resourceName); - ArgumentNullException.ThrowIfNull(create); - ArgumentNullException.ThrowIfNull(delete); - - return CreateNameCore( - resourceName, - () => GLHelpers.ThrowOnResourceError( - gl, - $"create {resourceName} (precondition)"), - create, - () => GLHelpers.ThrowOnResourceError(gl, $"create {resourceName}"), - ownedName => Execute( - gl, - $"rollback {resourceName} name {ownedName}", - () => delete(ownedName))); - } - - internal static uint CreateNameCore( - string resourceName, - Action precondition, - Func create, - Action postcondition, - Action deleteChecked) - { - ArgumentException.ThrowIfNullOrWhiteSpace(resourceName); - ArgumentNullException.ThrowIfNull(precondition); - ArgumentNullException.ThrowIfNull(create); - ArgumentNullException.ThrowIfNull(postcondition); - ArgumentNullException.ThrowIfNull(deleteChecked); - - uint name = 0; - Exception? creationFailure = null; - try - { - precondition(); - name = create(); - if (name == 0) - throw new InvalidOperationException($"OpenGL returned no {resourceName} name."); - postcondition(); - return name; - } - catch (Exception failure) - { - creationFailure = failure; - } - - if (name != 0) - { - var cleanup = new SingleNameCleanup(name, deleteChecked); - try - { - cleanup.RetryCleanup(); - } - catch (Exception cleanupFailure) - { - throw new GlResourceConstructionException( - $"Creating {resourceName} failed and its returned GL name could not be released.", - cleanup, - [creationFailure, cleanupFailure]); - } - } - - ExceptionDispatchInfo.Capture(creationFailure).Throw(); - throw new InvalidOperationException("Unreachable GL resource-creation path."); - } - - public static uint CreateTexture(GL gl, string context) => - CreateName(gl, context, gl.GenTexture, gl.DeleteTexture); - - public static void DeleteTexture(GL gl, uint texture, string context) => - Execute(gl, context, () => gl.DeleteTexture(texture)); - - public static void DeleteShader(GL gl, uint shader, string context) => - Execute(gl, context, () => gl.DeleteShader(shader)); - - public static void DeleteProgram(GL gl, uint program, string context) => - Execute(gl, context, () => gl.DeleteProgram(program)); - - public static void DeleteBuffer(GL gl, uint buffer, string context) => - Execute(gl, context, () => gl.DeleteBuffer(buffer)); - - public static void DeleteVertexArray(GL gl, uint vertexArray, string context) => - Execute(gl, context, () => gl.DeleteVertexArray(vertexArray)); - - private sealed class SingleNameCleanup(uint name, Action delete) - : IRetryableResourceCleanup - { - private uint _name = name; - private bool _running; - - public bool IsCleanupComplete => _name == 0; - - public void RetryCleanup() - { - if (_running || _name == 0) - return; - - _running = true; - try - { - delete(_name); - _name = 0; - } - finally - { - _running = false; - } - } - } -} diff --git a/src/AcDream.App/Rendering/GlTextureConstructionTransaction.cs b/src/AcDream.App/Rendering/GlTextureConstructionTransaction.cs deleted file mode 100644 index a7a04e69..00000000 --- a/src/AcDream.App/Rendering/GlTextureConstructionTransaction.cs +++ /dev/null @@ -1,85 +0,0 @@ -namespace AcDream.App.Rendering; - -internal interface IGlTextureNameApi -{ - uint GenTexture(); - void DeleteTexture(uint texture); -} - -internal sealed class GlTextureNameApi(Silk.NET.OpenGL.GL gl) : IGlTextureNameApi -{ - public uint GenTexture() => - GlResourceCommand.CreateTexture(gl, "terrain construction texture"); - - public void DeleteTexture(uint texture) => - GlResourceCommand.DeleteTexture( - gl, - texture, - $"delete terrain construction texture {texture}"); -} - -/// -/// Retains every texture name immediately after allocation until the complete -/// aggregate owner has been constructed. A failed factory rolls names back in -/// reverse acquisition order and attempts every deletion. -/// -internal sealed class GlTextureConstructionTransaction(IGlTextureNameApi api) - : IRetryableResourceCleanup -{ - private readonly List _ownedNames = []; - private bool _finished; - - public bool IsCleanupComplete => _finished; - - public uint Allocate() - { - if (_finished) - throw new InvalidOperationException("The texture construction transaction is finished."); - - uint texture = api.GenTexture(); - if (texture == 0) - throw new InvalidOperationException("OpenGL returned no texture name."); - _ownedNames.Add(texture); - return texture; - } - - public void Commit() - { - if (_finished) - throw new InvalidOperationException("The texture construction transaction is finished."); - _ownedNames.Clear(); - _finished = true; - } - - public void Rollback() - { - if (_finished) - return; - - List? failures = null; - for (int i = _ownedNames.Count - 1; i >= 0; i--) - { - uint texture = _ownedNames[i]; - try - { - api.DeleteTexture(texture); - _ownedNames.RemoveAt(i); - } - catch (Exception failure) - { - (failures ??= []).Add(new InvalidOperationException( - $"Texture construction rollback could not delete OpenGL name {texture}.", - failure)); - } - } - - if (failures is not null) - throw new AggregateException( - "Texture construction rollback did not release every allocated OpenGL name.", - failures); - - _finished = true; - } - - public void RetryCleanup() => Rollback(); -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs deleted file mode 100644 index af113ff4..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs +++ /dev/null @@ -1,313 +0,0 @@ -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// The narrow slice of GL that reads and -/// writes. It exists so the save/restore transaction can be exercised without a -/// GL context: the property that matters is "every value this pass can change -/// is put back, including when a draw throws", and that is a property of the -/// bookkeeping, not of the driver. -/// -/// Slice V6d generalized this from ITextRenderGlStateApi, which -/// TextRenderer owned privately and which restored a strict subset of the -/// same values. That renderer no longer touches GL at all, and the guarantee now -/// lives in one place for every RHI pass. -/// -internal interface IGlAmbientStateApi -{ - bool IsEnabled(EnableCap capability); - - int GetInteger(GetPName parameter); - - bool GetBoolean(GetPName parameter); - - /// The four colour-mask channels, in RGBA order. - bool[] GetColorMask(); - - void SetCapability(EnableCap capability, bool enabled); - - void DepthMask(bool enabled); - - void DepthFunc(DepthFunction function); - - void BlendFuncSeparate( - BlendingFactor sourceRgb, - BlendingFactor destinationRgb, - BlendingFactor sourceAlpha, - BlendingFactor destinationAlpha); - - void CullFace(TriangleFace face); - - void FrontFace(FrontFaceDirection direction); - - void StencilFunc(StencilFunction function, int reference, uint mask); - - void StencilOp(StencilOp fail, StencilOp depthFail, StencilOp pass); - - void StencilMask(uint mask); - - void ColorMask(bool red, bool green, bool blue, bool alpha); - - void UseProgram(uint program); - - void BindVertexArray(uint vertexArray); - - void BindBuffer(BufferTargetARB target, uint buffer); - - void ActiveTexture(TextureUnit unit); - - void BindTexture(TextureTarget target, uint texture); -} - -internal sealed class SilkGlAmbientStateApi : IGlAmbientStateApi -{ - private readonly GL _gl; - - public SilkGlAmbientStateApi(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - - public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability); - - public int GetInteger(GetPName parameter) - { - _gl.GetInteger(parameter, out int value); - return value; - } - - public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter); - - public unsafe bool[] GetColorMask() - { - var values = new bool[4]; - fixed (bool* first = values) - _gl.GetBoolean(GetPName.ColorWritemask, first); - return values; - } - - public void SetCapability(EnableCap capability, bool enabled) - { - if (enabled) - _gl.Enable(capability); - else - _gl.Disable(capability); - } - - public void DepthMask(bool enabled) => _gl.DepthMask(enabled); - - public void DepthFunc(DepthFunction function) => _gl.DepthFunc(function); - - public void BlendFuncSeparate( - BlendingFactor sourceRgb, - BlendingFactor destinationRgb, - BlendingFactor sourceAlpha, - BlendingFactor destinationAlpha) => - _gl.BlendFuncSeparate(sourceRgb, destinationRgb, sourceAlpha, destinationAlpha); - - public void CullFace(TriangleFace face) => _gl.CullFace(face); - - public void FrontFace(FrontFaceDirection direction) => _gl.FrontFace(direction); - - public void StencilFunc(StencilFunction function, int reference, uint mask) => - _gl.StencilFunc(function, reference, mask); - - public void StencilOp(StencilOp fail, StencilOp depthFail, StencilOp pass) => - _gl.StencilOp(fail, depthFail, pass); - - public void StencilMask(uint mask) => _gl.StencilMask(mask); - - public void ColorMask(bool red, bool green, bool blue, bool alpha) => - _gl.ColorMask(red, green, blue, alpha); - - public void UseProgram(uint program) => _gl.UseProgram(program); - - public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray); - - public void BindBuffer(BufferTargetARB target, uint buffer) => _gl.BindBuffer(target, buffer); - - public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit); - - public void BindTexture(TextureTarget target, uint texture) => _gl.BindTexture(target, texture); -} - -/// -/// Every ambient GL capability/binding a bind (or a -/// dynamic setter, or the pass's own sample count) can change, captured by raw -/// query and restored by raw call. -/// -/// Transitional for as long as raw-GL renderers coexist with RHI-ported ones: -/// each raw-GL renderer assumes whatever state the previous one left behind is -/// still there, so a pass that changes state and does not put it back is -/// invisible until the world silhouette changes. That is precisely how the first -/// V4a attempt lost multisampling (plan §7.1 rule 1). Deleted at V4h. -/// -internal readonly struct GlAmbientCapabilityState -{ - private readonly int _program; - private readonly int _vertexArray; - private readonly int _arrayBuffer; - private readonly int _activeTexture; - private readonly int _texture0Binding2D; - private readonly bool _depthTest; - private readonly bool _depthWrite; - private readonly int _depthFunc; - private readonly bool _blend; - private readonly int _blendSourceRgb; - private readonly int _blendDestinationRgb; - private readonly int _blendSourceAlpha; - private readonly int _blendDestinationAlpha; - private readonly bool _cullFace; - private readonly int _cullFaceMode; - private readonly int _frontFace; - private readonly bool _alphaToCoverage; - private readonly bool _multisample; - // Slice V6l: the stencil dimension. #117's portal punch is the only pipeline - // that enables it, and it draws in the middle of a frame whose other - // renderers are still raw GL and assume the test is off. - private readonly bool _stencilTest; - private readonly int _stencilFunc; - private readonly int _stencilReference; - private readonly int _stencilValueMask; - private readonly int _stencilWriteMask; - private readonly int _stencilFail; - private readonly int _stencilDepthFail; - private readonly int _stencilPass; - // Slice V6l: GpuPipelineDescription.ColorWrite has had no consumer until the - // portal depth mask, which is colour-invisible by construction. A pass that - // left the mask off would black out every raw-GL renderer that followed it, - // which is §7.1 rule 1's exact failure mode wearing a different name. - private readonly bool _colorMaskRed; - private readonly bool _colorMaskGreen; - private readonly bool _colorMaskBlue; - private readonly bool _colorMaskAlpha; - - private GlAmbientCapabilityState( - int program, int vertexArray, int arrayBuffer, int activeTexture, int texture0Binding2D, - bool depthTest, bool depthWrite, int depthFunc, - bool blend, int blendSourceRgb, int blendDestinationRgb, int blendSourceAlpha, int blendDestinationAlpha, - bool cullFace, int cullFaceMode, int frontFace, - bool alphaToCoverage, bool multisample, - bool stencilTest, int stencilFunc, int stencilReference, int stencilValueMask, - int stencilWriteMask, int stencilFail, int stencilDepthFail, int stencilPass, - bool colorMaskRed, bool colorMaskGreen, bool colorMaskBlue, bool colorMaskAlpha) - { - _program = program; - _vertexArray = vertexArray; - _arrayBuffer = arrayBuffer; - _activeTexture = activeTexture; - _texture0Binding2D = texture0Binding2D; - _depthTest = depthTest; - _depthWrite = depthWrite; - _depthFunc = depthFunc; - _blend = blend; - _blendSourceRgb = blendSourceRgb; - _blendDestinationRgb = blendDestinationRgb; - _blendSourceAlpha = blendSourceAlpha; - _blendDestinationAlpha = blendDestinationAlpha; - _cullFace = cullFace; - _cullFaceMode = cullFaceMode; - _frontFace = frontFace; - _alphaToCoverage = alphaToCoverage; - _multisample = multisample; - _stencilTest = stencilTest; - _stencilFunc = stencilFunc; - _stencilReference = stencilReference; - _stencilValueMask = stencilValueMask; - _stencilWriteMask = stencilWriteMask; - _stencilFail = stencilFail; - _stencilDepthFail = stencilDepthFail; - _stencilPass = stencilPass; - _colorMaskRed = colorMaskRed; - _colorMaskGreen = colorMaskGreen; - _colorMaskBlue = colorMaskBlue; - _colorMaskAlpha = colorMaskAlpha; - } - - internal static GlAmbientCapabilityState Capture(IGlAmbientStateApi gl) - { - ArgumentNullException.ThrowIfNull(gl); - int program = gl.GetInteger(GetPName.CurrentProgram); - int vertexArray = gl.GetInteger(GetPName.VertexArrayBinding); - int arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding); - int activeTexture = gl.GetInteger(GetPName.ActiveTexture); - - int texture0Binding2D; - try - { - gl.ActiveTexture(TextureUnit.Texture0); - texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D); - } - finally - { - gl.ActiveTexture((TextureUnit)activeTexture); - } - - bool[] colorMask = gl.GetColorMask(); - return new GlAmbientCapabilityState( - program, vertexArray, arrayBuffer, activeTexture, texture0Binding2D, - gl.IsEnabled(EnableCap.DepthTest), - gl.GetBoolean(GetPName.DepthWritemask), - gl.GetInteger(GetPName.DepthFunc), - gl.IsEnabled(EnableCap.Blend), - gl.GetInteger(GetPName.BlendSrcRgb), - gl.GetInteger(GetPName.BlendDstRgb), - gl.GetInteger(GetPName.BlendSrcAlpha), - gl.GetInteger(GetPName.BlendDstAlpha), - gl.IsEnabled(EnableCap.CullFace), - gl.GetInteger(GetPName.CullFaceMode), - gl.GetInteger(GetPName.FrontFace), - gl.IsEnabled(EnableCap.SampleAlphaToCoverage), - gl.IsEnabled(EnableCap.Multisample), - gl.IsEnabled(EnableCap.StencilTest), - gl.GetInteger(GetPName.StencilFunc), - gl.GetInteger(GetPName.StencilRef), - gl.GetInteger(GetPName.StencilValueMask), - gl.GetInteger(GetPName.StencilWritemask), - gl.GetInteger(GetPName.StencilFail), - gl.GetInteger(GetPName.StencilPassDepthFail), - gl.GetInteger(GetPName.StencilPassDepthPass), - colorMask[0], colorMask[1], colorMask[2], colorMask[3]); - } - - internal void Restore(IGlAmbientStateApi gl) - { - ArgumentNullException.ThrowIfNull(gl); - gl.UseProgram((uint)_program); - gl.BindVertexArray((uint)_vertexArray); - gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer); - - gl.ActiveTexture(TextureUnit.Texture0); - gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D); - gl.ActiveTexture((TextureUnit)_activeTexture); - - gl.SetCapability(EnableCap.DepthTest, _depthTest); - gl.DepthMask(_depthWrite); - gl.DepthFunc((DepthFunction)_depthFunc); - - gl.SetCapability(EnableCap.Blend, _blend); - gl.BlendFuncSeparate( - (BlendingFactor)_blendSourceRgb, - (BlendingFactor)_blendDestinationRgb, - (BlendingFactor)_blendSourceAlpha, - (BlendingFactor)_blendDestinationAlpha); - - gl.SetCapability(EnableCap.CullFace, _cullFace); - gl.CullFace((TriangleFace)_cullFaceMode); - gl.FrontFace((FrontFaceDirection)_frontFace); - - gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage); - gl.SetCapability(EnableCap.Multisample, _multisample); - - gl.SetCapability(EnableCap.StencilTest, _stencilTest); - gl.StencilFunc( - (StencilFunction)_stencilFunc, - _stencilReference, - (uint)_stencilValueMask); - gl.StencilOp( - (StencilOp)_stencilFail, - (StencilOp)_stencilDepthFail, - (StencilOp)_stencilPass); - gl.StencilMask((uint)_stencilWriteMask); - - gl.ColorMask(_colorMaskRed, _colorMaskGreen, _colorMaskBlue, _colorMaskAlpha); - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlDirtySlotRuns.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlDirtySlotRuns.cs deleted file mode 100644 index bc0766a4..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlDirtySlotRuns.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Tracks which texture-table slots changed since the last flush and gives them -/// back as maximal runs of consecutive dirty slots. -/// -/// Why runs, and not one merged range. The ring can flush a single -/// span covering everything written since the last draw, because every byte in -/// that span was allocated this frame and nothing in flight reads it. The -/// texture table cannot: it is a long-lived array of bindless handles, and two -/// registrations in one frame can land on slots 5 and 50 with forty-four live -/// slots in between. Those live slots are read by draws already submitted this -/// frame, so a single mapped write over [5, 51) — mapped with -/// GL_MAP_INVALIDATE_RANGE_BIT, which lets the driver discard the whole -/// range's contents while it is mapped, and GL_MAP_UNSYNCHRONIZED_BIT, -/// which stops it from waiting first — would expose those draws to torn -/// handles. Splitting on the gaps keeps every mapped range made only of slots -/// that no submitted draw can be reading. -/// -/// Why a dirty slot is safe to write unsynchronized. Two producers -/// touch a slot: GlGpuDevice.RegisterTexture, which writes a slot fresh -/// from and therefore one no batch has ever -/// indexed; and ReleaseTextureSlot's zeroing write, which runs inside a -/// retirement callback, i.e. after the fence covering every frame that could -/// still have referenced it. Both are already past the point where the GPU can -/// read the old value — which is exactly the assertion -/// GL_MAP_UNSYNCHRONIZED_BIT makes. -/// -/// GL-free by design, like and -/// , so the bookkeeping is unit tested -/// without a context. Enumeration is allocation-free: the tracker keeps the -/// min/max window that has been dirtied so a clean table costs one comparison, -/// and a dirty one scans only that window. -/// -internal sealed class GlDirtySlotRuns -{ - private readonly bool[] _dirty; - private int _windowStart = -1; - private int _windowEnd = -1; - - public GlDirtySlotRuns(uint capacity) - { - ArgumentOutOfRangeException.ThrowIfZero(capacity); - _dirty = new bool[capacity]; - } - - public uint Capacity => (uint)_dirty.Length; - - public bool HasDirtySlots => _windowStart >= 0; - - public void Mark(uint slot) - { - if (slot >= (uint)_dirty.Length) - { - throw new ArgumentOutOfRangeException( - nameof(slot), - slot, - $"The tracked table holds {_dirty.Length} slots."); - } - - _dirty[slot] = true; - _windowStart = _windowStart < 0 ? (int)slot : Math.Min(_windowStart, (int)slot); - _windowEnd = Math.Max(_windowEnd, (int)slot + 1); - } - - /// - /// Takes the lowest remaining run of consecutive dirty slots, clearing it, - /// and reports its first slot and length. Returns false once none - /// remain, so a caller drains with a while loop. - /// - public bool TryTakeNextRun(out uint firstSlot, out uint slotCount) - { - firstSlot = 0; - slotCount = 0; - if (_windowStart < 0) - return false; - - int index = _windowStart; - while (index < _windowEnd && !_dirty[index]) - index++; - if (index >= _windowEnd) - { - CloseWindow(); - return false; - } - - int start = index; - while (index < _windowEnd && _dirty[index]) - { - _dirty[index] = false; - index++; - } - - firstSlot = (uint)start; - slotCount = (uint)(index - start); - if (index >= _windowEnd) - CloseWindow(); - else - _windowStart = index; - return true; - } - - private void CloseWindow() - { - _windowStart = -1; - _windowEnd = -1; - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlEnumMapping.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlEnumMapping.cs deleted file mode 100644 index f9ed094a..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlEnumMapping.cs +++ /dev/null @@ -1,145 +0,0 @@ -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// One vertex attribute's GL shape: component count, element type, and whether integer values normalize to [0,1]/[-1,1]. -/// -/// How one vertex attribute reaches GL. selects -/// glVertexAttribIPointer over glVertexAttribPointer: GL requires -/// the integer entry point for an integer shader input (uvec4 and friends) -/// and leaves the value undefined otherwise. -/// -internal readonly record struct GlVertexAttributeShape( - int ComponentCount, - VertexAttribPointerType Type, - bool Normalized, - bool Integer = false); - -/// -/// Pure, GL-context-free mappings from the RHI's backend-neutral enums to -/// Silk.NET's OpenGL enum values. Kept as small switch expressions so each -/// mapping is independently unit-testable and — per the campaign spec's -/// "when in doubt, set the state" rule — every backend-neutral value has an -/// explicit case rather than a fallthrough default. -/// -internal static class GlEnumMapping -{ - public static GlVertexAttributeShape VertexShapeOf(GpuVertexFormat format) => format switch - { - GpuVertexFormat.Float1 => new GlVertexAttributeShape(1, VertexAttribPointerType.Float, false), - GpuVertexFormat.Float2 => new GlVertexAttributeShape(2, VertexAttribPointerType.Float, false), - GpuVertexFormat.Float3 => new GlVertexAttributeShape(3, VertexAttribPointerType.Float, false), - GpuVertexFormat.Float4 => new GlVertexAttributeShape(4, VertexAttribPointerType.Float, false), - GpuVertexFormat.UByte4Normalized => new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, true), - // Integer attributes carry Integer = true; the encoder must route them - // through glVertexAttribIPointer, not the normalized float path. - GpuVertexFormat.UByte4UInt => new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, false, Integer: true), - // Slice V6l: particle.vert's per-instance `in uint aTextureIndex`. - GpuVertexFormat.UInt1 => new GlVertexAttributeShape(1, VertexAttribPointerType.UnsignedInt, false, Integer: true), - _ => throw new NotSupportedException($"No GL vertex attribute shape for {format}."), - }; - - /// Slice V6l: the stencil comparison, which shares GL's depth-function enum values. - public static StencilFunction StencilFunctionOf(GpuCompareOp compare) => compare switch - { - GpuCompareOp.Never => StencilFunction.Never, - GpuCompareOp.Less => StencilFunction.Less, - GpuCompareOp.LessOrEqual => StencilFunction.Lequal, - GpuCompareOp.Equal => StencilFunction.Equal, - GpuCompareOp.Greater => StencilFunction.Greater, - GpuCompareOp.GreaterOrEqual => StencilFunction.Gequal, - GpuCompareOp.Always => StencilFunction.Always, - _ => throw new NotSupportedException($"No GL stencil function for {compare}."), - }; - - /// Slice V6l: what a stencil outcome does to the stored value. - public static StencilOp StencilOpOf(GpuStencilOp op) => op switch - { - GpuStencilOp.Keep => StencilOp.Keep, - GpuStencilOp.Zero => StencilOp.Zero, - GpuStencilOp.Replace => StencilOp.Replace, - _ => throw new NotSupportedException($"No GL stencil operation for {op}."), - }; - - public static PrimitiveType PrimitiveTypeOf(GpuPrimitiveTopology topology) => topology switch - { - GpuPrimitiveTopology.TriangleList => PrimitiveType.Triangles, - GpuPrimitiveTopology.LineList => PrimitiveType.Lines, - _ => throw new NotSupportedException($"No GL primitive type for {topology}."), - }; - - public static DrawElementsType DrawElementsTypeOf(GpuIndexType indexType) => indexType switch - { - GpuIndexType.UInt16 => DrawElementsType.UnsignedShort, - GpuIndexType.UInt32 => DrawElementsType.UnsignedInt, - _ => throw new NotSupportedException($"No GL index type for {indexType}."), - }; - - public static int IndexSizeBytesOf(GpuIndexType indexType) => indexType switch - { - GpuIndexType.UInt16 => sizeof(ushort), - GpuIndexType.UInt32 => sizeof(uint), - _ => throw new NotSupportedException($"No index byte size for {indexType}."), - }; - - public static DepthFunction DepthFunctionOf(GpuCompareOp compareOp) => compareOp switch - { - GpuCompareOp.Never => DepthFunction.Never, - GpuCompareOp.Less => DepthFunction.Less, - GpuCompareOp.LessOrEqual => DepthFunction.Lequal, - GpuCompareOp.Equal => DepthFunction.Equal, - GpuCompareOp.Greater => DepthFunction.Greater, - GpuCompareOp.GreaterOrEqual => DepthFunction.Gequal, - GpuCompareOp.Always => DepthFunction.Always, - _ => throw new NotSupportedException($"No GL depth function for {compareOp}."), - }; - - public static TriangleFace CullFaceModeOf(GpuCullMode cullMode) => cullMode switch - { - GpuCullMode.Back => TriangleFace.Back, - GpuCullMode.Front => TriangleFace.Front, - // GpuCullMode.None never reaches glCullFace — culling is disabled instead. - _ => throw new NotSupportedException($"No GL cull face mode for {cullMode}."), - }; - - public static FrontFaceDirection FrontFaceDirectionOf(GpuFrontFace frontFace) => frontFace switch - { - GpuFrontFace.CounterClockwise => FrontFaceDirection.Ccw, - GpuFrontFace.Clockwise => FrontFaceDirection.CW, - _ => throw new NotSupportedException($"No GL front-face direction for {frontFace}."), - }; - - public static (BlendingFactor Source, BlendingFactor Destination) BlendFactorsOf(GpuBlendMode blend) => blend switch - { - GpuBlendMode.StraightAlpha => (BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha), - GpuBlendMode.Additive => (BlendingFactor.SrcAlpha, BlendingFactor.One), - GpuBlendMode.InverseAlpha => (BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha), - // GpuBlendMode.None never reaches glBlendFunc — blending is disabled instead. - _ => throw new NotSupportedException($"No GL blend factors for {blend}."), - }; - - public static TextureMinFilter MinFilterOf(GpuFilter filter, GpuMipFilter mipFilter) => (filter, mipFilter) switch - { - (GpuFilter.Nearest, GpuMipFilter.None) => TextureMinFilter.Nearest, - (GpuFilter.Linear, GpuMipFilter.None) => TextureMinFilter.Linear, - (GpuFilter.Nearest, GpuMipFilter.Nearest) => TextureMinFilter.NearestMipmapNearest, - (GpuFilter.Linear, GpuMipFilter.Nearest) => TextureMinFilter.LinearMipmapNearest, - (GpuFilter.Nearest, GpuMipFilter.Linear) => TextureMinFilter.NearestMipmapLinear, - (GpuFilter.Linear, GpuMipFilter.Linear) => TextureMinFilter.LinearMipmapLinear, - _ => throw new NotSupportedException($"No GL min filter for {filter}/{mipFilter}."), - }; - - public static TextureMagFilter MagFilterOf(GpuFilter filter) => filter switch - { - GpuFilter.Nearest => TextureMagFilter.Nearest, - GpuFilter.Linear => TextureMagFilter.Linear, - _ => throw new NotSupportedException($"No GL mag filter for {filter}."), - }; - - public static TextureWrapMode WrapModeOf(GpuAddressMode addressMode) => addressMode switch - { - GpuAddressMode.Repeat => TextureWrapMode.Repeat, - GpuAddressMode.ClampToEdge => TextureWrapMode.ClampToEdge, - _ => throw new NotSupportedException($"No GL wrap mode for {addressMode}."), - }; -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuBuffer.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuBuffer.cs deleted file mode 100644 index 63ce8249..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuBuffer.cs +++ /dev/null @@ -1,287 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// A plain GL buffer object. Deliberately not one of the existing -/// ManagedGL* wrappers: those implement Chorizite's IVertexBuffer/ -/// IIndexBuffer (generic-over-IVertex, single-usage) and are -/// constructed against , which the campaign -/// explicitly sheds — is a fresh root. One GL buffer -/// object already serves every combination (the -/// usage only matters at bind time), so a single small class covers the whole -/// surface without forking per usage. -/// -/// Allocated once at the description's size via glBufferData with null -/// data. There are two write paths after that, and which one applies is a -/// property of the buffer's role rather than of the buffer object: -/// is an ordinary synchronized glBufferSubData, used -/// for one-off and streaming writes the driver must order for us (the mesh -/// arena, texture staging); maps the -/// range and is used only by the per-frame upload ring, whose non-overlap -/// invariant the caller can state. Neither path holds a persistent mapping. -/// -/// The glBufferData usage hint follows -/// : -/// is written rarely and read by -/// many draws, so it takes StaticDraw; the host-writable rings and -/// tables are rewritten every frame and take DynamicDraw. The hint is -/// advisory to the driver, but keeping it per-residency is what let the mesh -/// arena (Campaign V slice V4b) move onto this class without changing the -/// allocation it has always requested. -/// -internal sealed class GlGpuBuffer : IGpuBuffer -{ - private readonly GL _gl; - private readonly IGpuResourceRetirementQueue _retirement; - private uint _name; - - public GlGpuBuffer(GL gl, IGpuResourceRetirementQueue retirement, GpuBufferDescription description) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement)); - Name = description.Name; - SizeBytes = description.SizeBytes; - Usage = description.Usage; - Residency = description.Residency; - - _name = GlResourceCommand.CreateName(_gl, $"buffer '{Name}'", _gl.GenBuffer, _gl.DeleteBuffer); - try - { - _gl.BindBuffer(GLEnum.CopyWriteBuffer, _name); - unsafe - { - _gl.BufferData(GLEnum.CopyWriteBuffer, (nuint)SizeBytes, null, UsageHintFor(Residency)); - } - GLHelpers.ThrowOnResourceError(_gl, $"allocate buffer '{Name}' ({SizeBytes} bytes)"); - _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0); - } - catch - { - // A rejected data store (GL_OUT_OF_MEMORY is a real outcome for the - // mesh arena's 384 MiB growth destination) must not strand the name - // that was already created for it. The caller sees the original - // failure and owns nothing. - GlResourceCommand.DeleteBuffer( - _gl, - _name, - $"rollback buffer '{Name}' after a failed allocation"); - _name = 0; - throw; - } - } - - public string Name { get; } - public long SizeBytes { get; } - public GpuBufferUsage Usage { get; } - public GpuMemoryResidency Residency { get; } - - /// The physical GL buffer name. For bind calls issued by . - internal uint GlName => _name; - - private static BufferUsageARB UsageHintFor(GpuMemoryResidency residency) => - residency == GpuMemoryResidency.DeviceLocal - ? BufferUsageARB.StaticDraw - : BufferUsageARB.DynamicDraw; - - /// - /// Deletes the physical buffer on the calling thread instead of deferring it - /// through the device's retirement queue the way does. - /// - /// Campaign V slice V4b: the mesh arena (GlobalMeshBuffer) already gates - /// every arena delete behind its own GpuRetirementLedger and decrements - /// its MaximumPhysicalArenaBytes accounting in the same retirement stage. - /// Routing the physical free through the queue a second time would delay it by - /// a further flight generation, so the arena's physical-capacity accounting - /// would run ahead of real GPU residency and could admit a migration that - /// breaches the 896 MiB dual-generation ceiling. A caller of this method must - /// therefore already have proved no submitted frame can still reference the - /// buffer. - /// - /// Retryable by construction: the managed name is cleared only after the driver - /// reports success, so a failed delete is re-issued by the next attempt, and any - /// later call (including ) is a no-op. - /// - internal void DeleteRetired(string context) - { - uint name = _name; - if (name == 0) - return; - - _gl.DeleteBuffer(name); - // Per the GL error contract a command which generates an error does not - // change object state, so validation stays in the same stage as the - // mutation and the name is only surrendered once deletion committed. - GLHelpers.ThrowOnResourceError(_gl, context); - _name = 0; - } - - public void Upload(long offsetBytes, ReadOnlySpan data) - { - ThrowIfDisposed(); - if (offsetBytes < 0 || offsetBytes + data.Length > SizeBytes) - { - throw new ArgumentOutOfRangeException( - nameof(offsetBytes), - $"Upload of {data.Length} bytes at offset {offsetBytes} exceeds buffer '{Name}' ({SizeBytes} bytes)."); - } - if (data.IsEmpty) - return; - - _gl.BindBuffer(GLEnum.CopyWriteBuffer, _name); - unsafe - { - fixed (byte* pointer = data) - _gl.BufferSubData(GLEnum.CopyWriteBuffer, (nint)offsetBytes, (nuint)data.Length, pointer); - } - GLHelpers.ThrowOnResourceError(_gl, $"upload {data.Length} bytes to buffer '{Name}' at offset {offsetBytes}"); - _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0); - } - - /// - /// Writes at through - /// glMapBufferRange with - /// GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT - /// — the canonical GL upload-ring idiom, and the only write path the - /// per-frame ring uses. - /// - /// Why not . A partial glBufferSubData - /// into a buffer object that already-submitted draws are still reading leaves - /// the driver to guess how to keep both truths alive: it can stall, it can - /// rename the whole data store and copy the untouched remainder forward, or - /// it can route the write through an internal staging copy. Which one it - /// picks is a heuristic, and the heuristic is fed by the update pattern — - /// Campaign V's V4c revert (plan §5.5) saw a world that rendered blank - /// roughly one launch in three, with no GL error at any point, on the - /// connected path that issues 10–40 such partial updates per frame, and zero - /// times on the offline path that issues 2–4. Mapping the range instead - /// removes the guess: the three bits together say "I am writing this range, - /// I am overwriting all of it, and nothing in flight reads it," which is - /// exactly the ring's actual invariant. - /// - /// The precondition is the caller's to prove. - /// GL_MAP_UNSYNCHRONIZED_BIT means the driver inserts no wait, so the - /// caller asserts that no GL command already submitted and not yet complete - /// reads any byte of [offsetBytes, offsetBytes + data.Length). The ring - /// has that invariant on both axes: within a frame the allocation cursor only - /// moves forward and refuses a write below the - /// flushed high-water mark, and across frames a slot is only rewritten after - /// GpuFrameFlightController.BeginFrame has waited on the fence for the - /// last frame that used it. GL_MAP_INVALIDATE_RANGE_BIT is likewise - /// only sound because every byte of the mapped range is then written. - /// - /// A false return from glUnmapBuffer means the data store - /// was lost while mapped (a GPU reset or display-mode change), so the write - /// did not land. That throws rather than being retried: the frame's uploads - /// are already partly gone, and silently continuing would draw from a - /// half-written ring. - /// - internal unsafe void WriteRangeUnsynchronized(long offsetBytes, ReadOnlySpan data, string context) - { - ThrowIfDisposed(); - if (offsetBytes < 0 || offsetBytes + data.Length > SizeBytes) - { - throw new ArgumentOutOfRangeException( - nameof(offsetBytes), - $"Mapped write of {data.Length} bytes at offset {offsetBytes} exceeds buffer '{Name}' ({SizeBytes} bytes)."); - } - if (data.IsEmpty) - return; - - _gl.BindBuffer(GLEnum.CopyWriteBuffer, _name); - void* mapped = _gl.MapBufferRange( - GLEnum.CopyWriteBuffer, - (nint)offsetBytes, - (nuint)data.Length, - MapBufferAccessMask.WriteBit - | MapBufferAccessMask.UnsynchronizedBit - | MapBufferAccessMask.InvalidateRangeBit); - if (mapped is null) - { - // A null return always sets a GL error, so surface that first — it - // names the actual rejection instead of the symptom. - GLHelpers.ThrowOnResourceError( - _gl, - $"map {data.Length} bytes of buffer '{Name}' at offset {offsetBytes} ({context})"); - throw new InvalidOperationException( - $"glMapBufferRange returned no pointer for {data.Length} bytes of buffer '{Name}' " + - $"at offset {offsetBytes} ({context})."); - } - - data.CopyTo(new Span(mapped, data.Length)); - - bool unmapped = _gl.UnmapBuffer(GLEnum.CopyWriteBuffer); - GLHelpers.ThrowOnResourceError(_gl, $"unmap buffer '{Name}' after writing {context}"); - _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0); - if (!unmapped) - { - throw new InvalidOperationException( - $"Buffer '{Name}' lost its data store while {data.Length} bytes at offset " + - $"{offsetBytes} ({context}) were mapped; the write did not land."); - } - } - - public void CopyTo(IGpuBuffer destination, long sourceOffsetBytes, long destinationOffsetBytes, long byteCount) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(destination); - if (destination is not GlGpuBuffer target) - throw new ArgumentException("A GL device can only copy into a GL buffer.", nameof(destination)); - if (byteCount == 0) - return; - - _gl.BindBuffer(GLEnum.CopyReadBuffer, _name); - _gl.BindBuffer(GLEnum.CopyWriteBuffer, target._name); - _gl.CopyBufferSubData( - GLEnum.CopyReadBuffer, - GLEnum.CopyWriteBuffer, - (nint)sourceOffsetBytes, - (nint)destinationOffsetBytes, - (nuint)byteCount); - GLHelpers.ThrowOnResourceError( - _gl, - $"copy {byteCount} bytes from buffer '{Name}' to '{target.Name}'"); - _gl.BindBuffer(GLEnum.CopyReadBuffer, 0); - _gl.BindBuffer(GLEnum.CopyWriteBuffer, 0); - } - - public void Read(long offsetBytes, Span destination) - { - ThrowIfDisposed(); - if (destination.IsEmpty) - return; - - _gl.BindBuffer(GLEnum.CopyReadBuffer, _name); - unsafe - { - fixed (byte* pointer = destination) - _gl.GetBufferSubData(GLEnum.CopyReadBuffer, (nint)offsetBytes, (nuint)destination.Length, pointer); - } - GLHelpers.ThrowOnResourceError(_gl, $"read {destination.Length} bytes from buffer '{Name}' at offset {offsetBytes}"); - _gl.BindBuffer(GLEnum.CopyReadBuffer, 0); - } - - public void Dispose() - { - uint name = _name; - if (name == 0) - return; - _name = 0; - - GL gl = _gl; - string label = Name; - _retirement.Retire(() => - { - gl.DeleteBuffer(name); - GLHelpers.ThrowOnResourceError(gl, $"delete buffer '{label}' ({name})"); - }); - } - - private void ThrowIfDisposed() - { - if (_name == 0) - throw new ObjectDisposedException(Name); - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs deleted file mode 100644 index 9671122d..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs +++ /dev/null @@ -1,667 +0,0 @@ -using System.Numerics; -using System.Runtime.InteropServices; -using AcDream.App.Diagnostics; -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// OpenGL 4.3 implementation of — Campaign V slice V1. -/// See docs/plans/2026-07-27-vulkan-campaign.md §3 for the pinned -/// contract this backend fills and §5 for what this slice covers. -/// -/// Deliberately NOT derived from Chorizite's BaseGraphicsDevice/ -/// — this is a fresh root, which is one of -/// the things Campaign V sheds. It owns its own -/// instance (created in the constructor) rather than sharing the legacy WB -/// render path's; both simply wrap the same stateless -/// GL_ARB_bindless_texture extension, so two instances coexist safely, -/// and it lets this device be constructed the moment a GL context and a -/// exist — no dependency on when the -/// legacy path happens to detect bindless support during composition. -/// -/// Ring capacity. Each flight slot gets one managed staging -/// byte[] and one same-sized GL buffer, both fixed at construction -/// (default 16 MiB — see ). V1 -/// chose "throw with a message naming the needed size" over "grow by create- -/// copy-retire" for an over-capacity request: nothing consumes this device -/// yet, so there is no real per-frame data volume to size against, and a -/// silent/implicit grow would hide a future renderer's actual working set -/// from the person porting it. is the pure -/// piece that enforces this. -/// -/// Flush discipline. Ring bytes and the texture-handle table are -/// both flushed immediately before every -/// Draw/DrawIndexed/MultiDrawIndexedIndirect — see -/// — never at bind time, so a renderer that -/// writes after binding still uploads correctly. Both flushes go through -/// , i.e. -/// glMapBufferRange with -/// GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT, -/// rather than glBufferSubData: these are the only two buffers this -/// backend rewrites while the frame's own draws are still in flight, and that -/// is precisely the case where a partial glBufferSubData leaves the -/// result to a driver heuristic. See that method's remarks and campaign plan -/// §5.5 for the failure it produced. The two flushes differ in shape because -/// their non-overlap proofs differ: the ring writes one span (its cursor only -/// moves forward within a frame), the table writes one span per run of -/// consecutive dirty slots (see ). -/// -internal sealed class GlGpuDevice : IGpuDevice -{ - internal const int DefaultRingCapacityBytesPerSlot = 16 * 1024 * 1024; - - // GL tokens not exposed as named Silk.NET GetPName members (mirrors the - // same raw-hex pattern GraphicalCapabilityRecord.cs already uses for - // GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS). - private const int GlMaxShaderStorageBufferBindings = 0x90DD; - private const int GlMaxClipDistances = 0x0D32; - private const int GlMaxSamples = 0x8D57; - private const int GlShaderStorageBufferOffsetAlignment = 0x90DF; - - private readonly GL _gl; - private readonly GpuFrameFlightController _frameFlights; - private readonly BindlessSupport _bindless; - private readonly string _shadersDirectory; - private readonly int _ringCapacityBytesPerSlot; - - private readonly GlRingBufferState[] _ringStates; - private readonly byte[][] _ringStaging; - private readonly GlGpuBuffer[] _ringBuffers; - - private readonly GlTextureSlotAllocator _textureSlotAllocator = - new(GpuBindingModel.TextureTableCapacity); - private readonly ulong[] _textureHandleTable = new ulong[GpuBindingModel.TextureTableCapacity]; - private readonly GlGpuBuffer _textureTableBuffer; - private readonly GlDirtySlotRuns _textureTableDirtySlots = - new(GpuBindingModel.TextureTableCapacity); - - private readonly GlRenderStateCache _renderState = new(); - private readonly GlGpuPushConstantBinder _pushConstants; - private readonly GlGpuTimerPool _timerPool; - private readonly Dictionary _samplers = []; - private readonly List _queuedActions = []; - private readonly GlGpuTexture _defaultTexture; - - private long _nextSerial; - private bool _disposed; - - public GlGpuDevice( - GL gl, - GpuFrameFlightController frameFlights, - string shadersDirectory, - int ringCapacityBytesPerSlot = DefaultRingCapacityBytesPerSlot) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _frameFlights = frameFlights ?? throw new ArgumentNullException(nameof(frameFlights)); - ArgumentException.ThrowIfNullOrWhiteSpace(shadersDirectory); - _shadersDirectory = shadersDirectory; - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(ringCapacityBytesPerSlot); - _ringCapacityBytesPerSlot = ringCapacityBytesPerSlot; - - if (!BindlessSupport.TryCreate(_gl, out BindlessSupport? bindless) || bindless is null) - { - throw new NotSupportedException( - "GlGpuDevice requires GL_ARB_bindless_texture. The startup capability gate " + - "(GraphicalCapabilityGuard) should already have rejected a driver without it, " + - "so reaching this means the device was constructed before that gate ran."); - } - _bindless = bindless; - - Capabilities = CaptureCapabilities(); - - int slotCount = _frameFlights.SlotCount; - _ringStates = new GlRingBufferState[slotCount]; - _ringStaging = new byte[slotCount][]; - _ringBuffers = new GlGpuBuffer[slotCount]; - for (int slot = 0; slot < slotCount; slot++) - { - _ringStates[slot] = new GlRingBufferState(_ringCapacityBytesPerSlot); - _ringStaging[slot] = new byte[_ringCapacityBytesPerSlot]; - _ringBuffers[slot] = new GlGpuBuffer( - _gl, - Retirement, - new GpuBufferDescription( - $"gpu-ring-slot-{slot}", - _ringCapacityBytesPerSlot, - GpuBufferUsage.Storage | GpuBufferUsage.Uniform | GpuBufferUsage.Indirect, - GpuMemoryResidency.HostWritable)); - } - - _textureTableBuffer = new GlGpuBuffer( - _gl, - Retirement, - new GpuBufferDescription( - "gpu-texture-table", - GpuBindingModel.TextureTableCapacity * sizeof(ulong), - GpuBufferUsage.Storage, - GpuMemoryResidency.HostWritable)); - - _pushConstants = new GlGpuPushConstantBinder(_gl); - _timerPool = new GlGpuTimerPool(new SilkGlTimerQueryApi(_gl), Capabilities.SupportsTimestampQueries); - - _defaultTexture = new GlGpuTexture( - _gl, - Retirement, - new GpuTextureDescription( - "default-white", - GpuTextureKind.Texture2D, - GpuTextureFormat.Rgba8Unorm, - Width: 1, - Height: 1, - LayerCount: 1, - MipLevelCount: 1)); - _defaultTexture.Upload(0, 0, [255, 255, 255, 255]); - IGpuSampler defaultSampler = CreateSampler(GpuSamplerDescription.UiNearest); - DefaultTextureSlot = RegisterTexture(_defaultTexture, defaultSampler); - } - - public GpuBackendKind Backend => GpuBackendKind.OpenGl; - public GpuCapabilityRecord Capabilities { get; } - public IGpuResourceRetirementQueue Retirement => _frameFlights; - public IGpuTimerPool Timers => _timerPool; - public GpuTextureSlot DefaultTextureSlot { get; } - - internal GL Gl => _gl; - internal GlGpuPushConstantBinder PushConstants => _pushConstants; - internal GlGpuTimerPool TimerPool => _timerPool; - - /// - /// GL name of the buffer emulating the global texture table - /// (). Slice V6d: - /// binds it on every pipeline - /// bind, which is the GL analogue of the Vulkan backend binding descriptor - /// set 2 on every draw. Without it an RHI shader that samples the table - /// would read whichever raw-GL renderer's private handle table was left at - /// binding 9 — a different slot numbering entirely, which is the loudest - /// possible way to sample the wrong texture. - /// - internal uint TextureTableGlName => _textureTableBuffer.GlName; - - internal GlRenderStateSnapshot CurrentRenderState { get; private set; } - - public IGpuBuffer CreateBuffer(in GpuBufferDescription description) - { - ThrowIfDisposed(); - return new GlGpuBuffer(_gl, Retirement, description); - } - - public IGpuTexture CreateTexture(in GpuTextureDescription description) - { - ThrowIfDisposed(); - return new GlGpuTexture(_gl, Retirement, description); - } - - public IGpuSampler CreateSampler(in GpuSamplerDescription description) - { - ThrowIfDisposed(); - if (_samplers.TryGetValue(description, out GlGpuSampler? existing)) - return existing; - - var created = new GlGpuSampler(_gl, Retirement, description); - _samplers.Add(description, created); - return created; - } - - public IGpuPipeline CreatePipeline(GpuPipelineDescription description) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(description); - string vertexPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.vert"); - string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag"); - // Campaign V slice V6d: every RHI pipeline gets the shared preamble, - // unconditionally. The Vulkan backend injects its own preamble into - // every shader it compiles, so making the GL side selective would mean - // one source file compiling against two different sets of definitions - // depending on which pipeline happened to ask for it. A shader that - // reads nothing from the preamble simply carries an unused declaration. - string common = File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl")); - string vertexSource = Shader.InjectPreamble(File.ReadAllText(vertexPath), common); - string fragmentSource = Shader.InjectPreamble(File.ReadAllText(fragmentPath), common); - return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource); - } - - public IGpuRenderTarget CreateRenderTarget(in GpuRenderTargetDescription description) - { - ThrowIfDisposed(); - return new GlGpuRenderTarget(_gl, Retirement, description); - } - - public GpuTextureSlot RegisterTexture(IGpuTexture texture, IGpuSampler sampler) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(texture); - ArgumentNullException.ThrowIfNull(sampler); - if (texture is not GlGpuTexture glTexture) - throw new ArgumentException("The GL backend can only register a GL texture.", nameof(texture)); - if (sampler is not GlGpuSampler glSampler) - throw new ArgumentException("The GL backend can only register a GL sampler.", nameof(sampler)); - - uint slot = _textureSlotAllocator.Allocate(); - ulong handle = _bindless.GetResidentHandle(glTexture.GlName, glSampler.GlName); - WriteHandle(slot, handle); - return new GpuTextureSlot(slot); - } - - public void ReleaseTextureSlot(GpuTextureSlot slot) - { - ThrowIfDisposed(); - if (!slot.IsAssigned) - throw new ArgumentException("Cannot release an unassigned texture slot.", nameof(slot)); - - uint index = slot.Index; - // Deferred through the retirement queue: a submitted-but-not-yet- - // retired frame may still read this slot's handle, so the free list - // (and thus reuse) must wait for that frame to retire. - Retirement.Retire(() => - { - WriteHandle(index, 0); - _textureSlotAllocator.Release(index); - }); - } - - public IGpuFrame BeginFrame() - { - ThrowIfDisposed(); - _frameFlights.BeginFrame(); - long serial = ++_nextSerial; - int slot = _frameFlights.CurrentSlot; - _ringStates[slot].Reset(); - return new GlGpuFrame(this, slot, serial); - } - - internal void EndFrame() => _frameFlights.EndFrame(); - - internal GpuRingAllocation AllocateRing(int slotIndex, int byteCount, GpuRingUsage usage) - { - uint alignment = usage switch - { - GpuRingUsage.Storage => Capabilities.MinStorageBufferOffsetAlignment, - GpuRingUsage.Uniform => Capabilities.MinUniformBufferOffsetAlignment, - _ => 4u, - }; - uint offset = _ringStates[slotIndex].Allocate(byteCount, alignment); - Span data = byteCount == 0 - ? Span.Empty - : _ringStaging[slotIndex].AsSpan((int)offset, byteCount); - return new GpuRingAllocation(_ringBuffers[slotIndex], offset, data); - } - - /// - /// Writes this slot's dirty ring bytes and every dirty run of the texture - /// table into their GL buffers, then clears both watermarks. Called - /// immediately before every draw — never at bind time, because a renderer - /// may still write into a ring allocation after binding it. - /// - /// Both writes are mapped and unsynchronized (see the class remarks and - /// ). The ring's dirty - /// span may include alignment padding between allocations; those bytes are - /// below the cursor, were allocated this frame, and are read by nothing, so - /// covering them in one map is sound and saves a call. The table has no - /// equivalent licence, which is why it drains run by run. - /// - internal void FlushBeforeDraw(int slotIndex) - { - (int start, int length) = _ringStates[slotIndex].TakeDirtyRange(); - if (length > 0) - { - _ringBuffers[slotIndex].WriteRangeUnsynchronized( - start, - _ringStaging[slotIndex].AsSpan(start, length), - $"ring slot {slotIndex}"); - } - - FlushTextureTable(); - } - - /// - /// Drains every dirty run of the texture table into its GL buffer. Called by - /// for RHI draws, and directly by the still-raw-GL - /// world renderers before their own draws — see the V4t remarks on - /// . - /// - internal void FlushTextureTable() - { - while (_textureTableDirtySlots.TryTakeNextRun(out uint firstSlot, out uint slotCount)) - { - ReadOnlySpan bytes = MemoryMarshal.AsBytes( - _textureHandleTable.AsSpan((int)firstSlot, (int)slotCount)); - _textureTableBuffer.WriteRangeUnsynchronized( - (long)firstSlot * sizeof(ulong), - bytes, - $"texture table slots {firstSlot}..{firstSlot + slotCount - 1}"); - } - } - - internal void BeginPass(GpuPassDescription description) - { - ThrowIfDisposed(); - ArgumentNullException.ThrowIfNull(description); - if (description.Color.Store == GpuStoreOp.Resolve - || description.Depth is { Store: GpuStoreOp.Resolve }) - { - throw new NotSupportedException( - "GpuStoreOp.Resolve is a Vulkan-only path; Campaign V slice V1's GL backend " + - "only ever renders single-sampled targets."); - } - - uint framebuffer = 0; - if (description.Color.Target is { } target) - { - if (target is not GlGpuRenderTarget glTarget) - throw new ArgumentException("The GL backend can only render into a GL render target."); - framebuffer = glTarget.GlFramebufferName; - } - _gl.BindFramebuffer(GLEnum.Framebuffer, framebuffer); - - bool clearsColor = description.Color.Load == GpuLoadOp.Clear; - bool clearsDepth = description.Depth is { Load: GpuLoadOp.Clear }; - if (clearsColor || clearsDepth) - { - // Force the write masks on before clearing, regardless of what - // the previous pass's last draw left them at (e.g. depth-write - // disabled mid-translucent-pass) — glClear silently no-ops for a - // buffer whose mask is off. - ClearBufferMask mask = 0; - if (clearsColor) - { - _gl.ColorMask(true, true, true, true); - Vector4 clearColor = description.Color.ClearColor; - _gl.ClearColor(clearColor.X, clearColor.Y, clearColor.Z, clearColor.W); - mask |= ClearBufferMask.ColorBufferBit; - } - if (description.Depth is { Load: GpuLoadOp.Clear } depth) - { - _gl.DepthMask(true); - _gl.ClearDepth(depth.ClearDepth); - _gl.ClearStencil((int)depth.ClearStencil); - mask |= ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit; - } - _gl.Clear(mask); - GLHelpers.ThrowOnResourceError(_gl, $"clear pass '{description.Name}'"); - } - - // Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 2): - // reset unconditionally on EVERY pass, not only a clearing one. While - // raw-GL renderers coexist with RHI-ported ones (through V4h), a - // raw-GL renderer can run between two RHI passes within the same frame - // and change GL program/blend/depth/cull state the cache never - // observes. A reset gated on "this pass cleared" left the cache - // trusting a stale belief in that case, so a later BindPipeline - // skipped re-issuing glUseProgram and the following push-constant - // upload threw GL_INVALID_OPERATION against whatever program was - // actually bound — exactly the failure the first V4a attempt hit. - // Resetting on every BeginPass costs one redundant state application - // on the pass's first bind and is removed at V4h once nothing raw-GL - // remains. - _renderState.Reset(); - } - - // Campaign V slice V6k deleted the V4a pre-approved transitional seam - // (RegisterExternalColorTexture / TryResolveExternalColorTexture, campaign - // doc §7.1's final paragraph). It existed so the retained UI could blit the - // paperdoll and creature-appraisal viewport textures while their renderer - // still owned a hand-rolled FBO the RHI knew nothing about. That renderer now - // creates an IGpuRenderTarget and registers its colour attachment through - // RegisterTexture like anything else, so the escape hatch has no caller — - // exactly the end §7.1 wrote for it. - - // ── V4t transitional seam: the world texture stack's entry to this table ── - // - // Campaign V slice V4t moves the world's CPU data model off the raw 64-bit - // ARB_bindless_texture handle and onto GpuTextureSlot, but §5.5.6 closed the - // GL re-land of V4c/V4d, so WbDrawDispatcher, EnvCellRenderer, - // TerrainModernRenderer and ParticleRenderer still submit through raw GL and - // cannot reach FlushBeforeDraw. They therefore call FlushTextureTable and - // bind TextureTableGlName at GpuBindingModel.StorageTextureTable themselves, - // immediately before their own draws — the same shape their retired private - // GlBindlessHandleTable had, against the one table that is now the device's. - // - // Residency ownership does NOT move. Each world texture is created, made - // resident and destroyed by its own cache (TerrainAtlas, - // CompositeTextureArrayCache, StandaloneBindlessTextureCache, - // ManagedGLTextureArray); this device only owns the table entry, keyed 1:1 by - // the caller's already-resident handle. That is what separates this from - // RegisterTexture, which owns the residency it creates. - // - // Deleted with the raw-GL world path when the Vulkan world arm lands, at - // which point every one of those caches registers through RegisterTexture. - private readonly Dictionary _worldTextureSlotsByHandle = new(); - - /// - /// Interns an already-resident world texture handle into the device's table - /// and returns its slot. Idempotent: the same handle always resolves to the - /// same slot until retires it, which - /// is what lets a cache call this per draw rather than tracking the slot. - /// - internal GpuTextureSlot RegisterWorldTextureHandle(ulong residentHandle) - { - ThrowIfDisposed(); - if (residentHandle == 0) - return GpuTextureSlot.Unassigned; - if (_worldTextureSlotsByHandle.TryGetValue(residentHandle, out GpuTextureSlot existing)) - return existing; - - uint slotIndex = _textureSlotAllocator.Allocate(); - WriteHandle(slotIndex, residentHandle); - var slot = new GpuTextureSlot(slotIndex); - _worldTextureSlotsByHandle.Add(residentHandle, slot); - return slot; - } - - /// - /// Retires the table entry for a world handle the caller is about to make - /// non-resident. The slot itself returns to the free list only once the - /// retirement queue confirms no submitted frame can still read it, exactly - /// as for . A handle that was never - /// registered is a no-op, so a cache may call this unconditionally on its - /// teardown path. - /// - internal void ReleaseWorldTextureHandle(ulong residentHandle) - { - if (residentHandle == 0) - return; - if (!_worldTextureSlotsByHandle.Remove(residentHandle, out GpuTextureSlot slot)) - return; - ReleaseTextureSlot(slot); - } - - /// Live world-handle registrations. Diagnostics and tests only. - internal int WorldTextureSlotCount => _worldTextureSlotsByHandle.Count; - - internal void ApplyRenderState(GlRenderStateSnapshot desired) - { - GlRenderStateChanges changes = _renderState.Apply(desired); - CurrentRenderState = desired; - if (!changes.AnyChange) - return; - - if (changes.Program) - _gl.UseProgram(desired.Program); - if (changes.Blend) - { - if (desired.Blend == GpuBlendMode.None) - { - _gl.Disable(EnableCap.Blend); - } - else - { - _gl.Enable(EnableCap.Blend); - (BlendingFactor source, BlendingFactor destination) = GlEnumMapping.BlendFactorsOf(desired.Blend); - _gl.BlendFunc(source, destination); - } - } - if (changes.DepthTest) - { - if (desired.DepthTest) - _gl.Enable(EnableCap.DepthTest); - else - _gl.Disable(EnableCap.DepthTest); - } - if (changes.DepthWrite) - _gl.DepthMask(desired.DepthWrite); - if (changes.DepthCompare) - _gl.DepthFunc(GlEnumMapping.DepthFunctionOf(desired.DepthCompare)); - if (changes.Cull) - { - if (desired.Cull == GpuCullMode.None) - { - _gl.Disable(EnableCap.CullFace); - } - else - { - _gl.Enable(EnableCap.CullFace); - _gl.CullFace(GlEnumMapping.CullFaceModeOf(desired.Cull)); - } - } - if (changes.FrontFace) - _gl.FrontFace(GlEnumMapping.FrontFaceDirectionOf(desired.FrontFace)); - if (changes.AlphaToCoverage) - { - if (desired.AlphaToCoverage) - _gl.Enable(EnableCap.SampleAlphaToCoverage); - else - _gl.Disable(EnableCap.SampleAlphaToCoverage); - } - if (changes.ColorWrite) - { - _gl.ColorMask(desired.ColorWrite, desired.ColorWrite, desired.ColorWrite, desired.ColorWrite); - } - // Slice V6l. The stencil VALUES are issued whenever the test is on, even - // if only the enable changed: GL keeps func/op/mask as global state that a - // raw-GL renderer may have moved since this cache last saw it, and the - // portal punch's correctness depends on the exact triple it asked for. - if (changes.StencilTest) - { - if (desired.StencilTest) - _gl.Enable(EnableCap.StencilTest); - else - _gl.Disable(EnableCap.StencilTest); - } - if (desired.StencilTest && (changes.StencilTest || changes.Stencil)) - { - GpuStencilState stencil = desired.Stencil; - _gl.StencilFunc( - GlEnumMapping.StencilFunctionOf(stencil.Compare), - (int)stencil.Reference, - stencil.CompareMask); - _gl.StencilOp( - GlEnumMapping.StencilOpOf(stencil.Fail), - GlEnumMapping.StencilOpOf(stencil.DepthFail), - GlEnumMapping.StencilOpOf(stencil.Pass)); - _gl.StencilMask(stencil.WriteMask); - } - GLHelpers.ThrowOnResourceError(_gl, "apply GL render state"); - } - - public void QueueDeviceAction(Action action) - { - ArgumentNullException.ThrowIfNull(action); - _queuedActions.Add(action); - } - - public void ProcessDeviceActions() - { - Action[] pending = [.. _queuedActions]; - _queuedActions.Clear(); - foreach (Action action in pending) - action(); - } - - public byte[] CaptureBackbuffer(int width, int height) - { - ThrowIfDisposed(); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); - - // FrameScreenshotController owns the one backbuffer read in the process: - // it names framebuffer 0 rather than inheriting a binding, and it - // resolves the multisampled default framebuffer before reading, because - // glReadPixels against a multisampled read framebuffer is undefined. - // A second implementation here would be a second instrument to keep - // sound. FlipRows is the same top-left-origin flip the screenshot gates - // rely on, so this seam stays byte-for-byte compatible with them. - byte[] pixels = FrameScreenshotController.ReadDefaultFramebuffer(_gl, width, height); - GLHelpers.ThrowOnResourceError(_gl, $"capture backbuffer {width}x{height}"); - return FrameScreenshotController.FlipRows(pixels, width, height); - } - - public void WaitIdle() - { - ThrowIfDisposed(); - _frameFlights.WaitForSubmittedWork(); - } - - public void Dispose() - { - if (_disposed) - return; - _disposed = true; - - // Only resources THIS device created are disposed here. - // GpuFrameFlightController is constructor-injected and owned by - // whoever composed this device — disposing it would be a double- - // dispose from that owner's perspective, and every disposal below - // routes through it as the retirement queue, so it must still be - // alive when this method returns. - foreach (GlGpuSampler sampler in _samplers.Values) - sampler.Dispose(); - _samplers.Clear(); - - _defaultTexture.Dispose(); - - foreach (GlGpuBuffer ringBuffer in _ringBuffers) - ringBuffer.Dispose(); - - _textureTableBuffer.Dispose(); - _timerPool.DisposeQueries(); - } - - private void WriteHandle(uint slot, ulong handle) - { - _textureHandleTable[slot] = handle; - _textureTableDirtySlots.Mark(slot); - } - - private GpuCapabilityRecord CaptureCapabilities() - { - int major = _gl.GetInteger(GetPName.MajorVersion); - int minor = _gl.GetInteger(GetPName.MinorVersion); - bool openGl43 = major > 4 || (major == 4 && minor >= 3); - - _gl.GetInteger((GetPName)GlMaxShaderStorageBufferBindings, out int maxStorageBindings); - _gl.GetInteger((GetPName)GlMaxClipDistances, out int maxClipDistances); - _gl.GetInteger((GetPName)GlMaxSamples, out int maxSamples); - _gl.GetInteger(GetPName.UniformBufferOffsetAlignment, out int uniformAlignment); - _gl.GetInteger((GetPName)GlShaderStorageBufferOffsetAlignment, out int storageAlignment); - - bool timerQuery = major > 3 || (major == 3 && minor >= 3) || _gl.IsExtensionPresent("GL_ARB_timer_query"); - - return new GpuCapabilityRecord - { - Backend = GpuBackendKind.OpenGl, - DeviceName = _gl.GetStringS(GLEnum.Renderer), - DriverInfo = _gl.GetStringS(GLEnum.Vendor), - ApiVersion = $"OpenGL {_gl.GetStringS(GLEnum.Version)}", - MaxTextureTableSlots = GpuBindingModel.TextureTableCapacity, - MaxStorageBufferBindings = (uint)maxStorageBindings, - MaxPushConstantBytes = (uint)GpuBindingModel.MaxPushConstantBytes, - MinStorageBufferOffsetAlignment = (uint)storageAlignment, - MinUniformBufferOffsetAlignment = (uint)uniformAlignment, - MaxClipDistances = (uint)maxClipDistances, - MaxSampleCount = (uint)maxSamples, - SupportsMultiDrawIndirect = openGl43 || _gl.IsExtensionPresent("GL_ARB_multi_draw_indirect"), - SupportsDrawParameters = _gl.IsExtensionPresent("GL_ARB_shader_draw_parameters"), - SupportsTextureCompressionBc = _gl.IsExtensionPresent("GL_EXT_texture_compression_s3tc"), - SupportsTimestampQueries = timerQuery, - // The ring maps and unmaps per flush; it never holds a persistent - // mapping a renderer could write into between frames, which is what - // this capability advertises. Only Vulkan reports true. - SupportsPersistentlyMappedRings = false, - }; - } - - private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuFrame.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuFrame.cs deleted file mode 100644 index 49a9f231..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuFrame.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// One frame's recording context on the GL backend. Ring allocations are -/// forwarded to the device's per-slot ; -/// passes are single-level (no nesting, matching every acdream renderer -/// today) and enforced here. -/// -internal sealed class GlGpuFrame : IGpuFrame -{ - private readonly GlGpuDevice _device; - private bool _ended; - private GlGpuPassEncoder? _openPass; - - internal GlGpuFrame(GlGpuDevice device, int slotIndex, long serial) - { - _device = device; - SlotIndex = slotIndex; - Serial = serial; - } - - public int SlotIndex { get; } - public long Serial { get; } - - public GpuRingAllocation AllocateRing(int byteCount, GpuRingUsage usage) => - _device.AllocateRing(SlotIndex, byteCount, usage); - - public IGpuPassEncoder BeginPass(GpuPassDescription description) - { - ArgumentNullException.ThrowIfNull(description); - if (_openPass is not null) - { - throw new InvalidOperationException( - "A pass is already open on this frame; dispose it before beginning another."); - } - - _device.BeginPass(description); - var encoder = new GlGpuPassEncoder(_device, this, description); - _openPass = encoder; - return encoder; - } - - internal void ClosePass(GlGpuPassEncoder encoder) - { - if (ReferenceEquals(_openPass, encoder)) - _openPass = null; - } - - public void End() - { - if (_ended) - return; - _ended = true; - _device.EndFrame(); - } - - public void Dispose() => End(); -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs deleted file mode 100644 index d2b895c6..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs +++ /dev/null @@ -1,316 +0,0 @@ -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Records one pass's draw work. Binding calls translate almost mechanically -/// to GL (a storage/uniform binding is glBindBufferRange, an indexed -/// draw is glDrawElementsInstancedBaseVertexBaseInstance, and so on); -/// the two pieces of real logic are the render-state diff applied on -/// / the dynamic setters, and the "flush dirty -/// ring + texture-table bytes immediately before every draw" discipline -/// described on . -/// -internal sealed class GlGpuPassEncoder : IGpuPassEncoder -{ - private readonly GlGpuDevice _device; - private readonly GlGpuFrame _frame; - private readonly GL _gl; - private readonly IGlAmbientStateApi _ambientApi; - private readonly GlAmbientCapabilityState _ambientOnEntry; - private bool _closed; - - private GlGpuPipeline? _currentPipeline; - private GpuIndexType _currentIndexType = GpuIndexType.UInt16; - private uint _currentIndexBufferBaseOffset; - private GpuPushConstants? _currentPushConstants; - - internal GlGpuPassEncoder(GlGpuDevice device, GlGpuFrame frame, GpuPassDescription pass) - { - _device = device; - _frame = frame; - _gl = device.Gl; - Pass = pass; - - // Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 1): - // capture every ambient capability a bound pipeline can change, so - // Dispose can put it back. Every acdream renderer is still raw GL - // until V4c/V4d, so each one assumes whatever capability state the - // PREVIOUS renderer left behind is still there — GL_MULTISAMPLE and - // GL_SAMPLE_ALPHA_TO_COVERAGE in particular are set once per frame by - // quality settings and never re-asserted per draw. The first V4a - // attempt bound a pipeline that changed this state and never restored - // it, so the world drew without multisampling from the first UI frame - // on. Capturing here and restoring on Dispose keeps the GL backend's - // behaviour-preserving property true at this seam. Deleted at V4h - // once nothing raw-GL remains. - _ambientApi = new SilkGlAmbientStateApi(_gl); - _ambientOnEntry = GlAmbientCapabilityState.Capture(_ambientApi); - - // Campaign V slice V6d. GL_MULTISAMPLE is the one piece of pass state - // with no representation in GpuPipelineDescription, and the pass's own - // SampleCount is the contract's answer for it: a single-sampled pass - // does not multisample. Until now the retained UI asserted that with a - // raw glDisable of its own — exactly the kind of state a - // backend-neutral renderer cannot own. Quality settings enable - // GL_MULTISAMPLE once per frame for the world, and if it leaks into the - // UI pass every glyph's soft alpha edge becomes dithered coverage - // instead of a clean alpha blend (the "fuzzy text" artifact). The - // ambient capture above puts it back on Dispose, so the raw-GL world - // renderers that follow are unaffected. - _ambientApi.SetCapability(EnableCap.Multisample, pass.SampleCount > 1); - } - - public GpuPassDescription Pass { get; } - - public void BindPipeline(IGpuPipeline pipeline) - { - ArgumentNullException.ThrowIfNull(pipeline); - ThrowIfClosed(); - var p = (GlGpuPipeline)pipeline; - _currentPipeline = p; - - GpuPipelineDescription description = p.Description; - var desired = new GlRenderStateSnapshot( - p.GlProgram, - description.Blend, - description.Depth.Test, - description.Depth.Write, - description.Depth.Compare, - description.Cull, - description.FrontFace, - description.AlphaToCoverage, - description.ColorWrite, - description.StencilTest, - description.Stencil); - _device.ApplyRenderState(desired); - - _gl.BindVertexArray(p.GlVertexArray); - GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' VAO"); - - // Campaign V slice V6d: the device's texture table is bound with the - // pipeline, the GL analogue of the Vulkan backend binding descriptor - // set 2 on every draw. It has to happen here rather than once per frame - // because every raw-GL world renderer binds its OWN private handle - // table at this same binding before its own draws, with its own slot - // numbering; an RHI shader that read that instead would sample a - // plausible but entirely unrelated texture. Removed at V4h with the - // per-renderer tables. - _gl.BindBufferBase( - GLEnum.ShaderStorageBuffer, - GpuBindingModel.StorageTextureTable, - _device.TextureTableGlName); - GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' texture table"); - - // Push constants "survive pipeline changes within a pass" per the - // IGpuPassEncoder contract. GL uniforms are per-program state, so the - // GL backend must explicitly re-apply the last value to the newly - // bound program to honour that — Vulkan gets this for free from a - // shared pipeline layout. - if (_currentPushConstants is { } constants) - _device.PushConstants.Apply(p.GlProgram, in constants); - } - - public void BindStorageBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes) - { - ThrowIfClosed(); - var b = RequireGlBuffer(buffer); - _gl.BindBufferRange(GLEnum.ShaderStorageBuffer, binding, b.GlName, (nint)offsetBytes, sizeBytes); - GLHelpers.ThrowOnResourceError(_gl, $"bind storage buffer '{buffer.Name}' at binding {binding}"); - } - - public void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes) - { - ThrowIfClosed(); - var b = RequireGlBuffer(buffer); - _gl.BindBufferRange(GLEnum.UniformBuffer, binding, b.GlName, (nint)offsetBytes, sizeBytes); - GLHelpers.ThrowOnResourceError(_gl, $"bind uniform buffer '{buffer.Name}' at binding {binding}"); - } - - public unsafe void BindVertexBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes) - { - ThrowIfClosed(); - if (_currentPipeline is not { } pipeline) - throw new InvalidOperationException("BindPipeline must be called before BindVertexBuffer."); - var b = RequireGlBuffer(buffer); - - _gl.BindBuffer(GLEnum.ArrayBuffer, b.GlName); - GpuVertexLayout layout = pipeline.Description.VertexLayout; - // Slice V6l: only the attributes this binding actually supplies. GL has - // no binding indirection of its own — glVertexAttribPointer records the - // currently bound ARRAY_BUFFER per attribute — so the binding index is - // resolved here, by filtering, rather than by the driver. - uint stride = layout.StrideOf(binding); - foreach (GpuVertexAttribute attribute in layout.Attributes) - { - if (attribute.Binding != binding) - continue; - - GlVertexAttributeShape shape = GlEnumMapping.VertexShapeOf(attribute.Format); - nint attributeOffset = (nint)(offsetBytes + attribute.OffsetBytes); - if (shape.Integer) - { - // An integer shader input (uvec4) must come through the I-form. - // Supplying it via glVertexAttribPointer leaves the value undefined. - _gl.VertexAttribIPointer( - attribute.Location, - shape.ComponentCount, - (VertexAttribIType)shape.Type, - stride, - (void*)attributeOffset); - } - else - { - _gl.VertexAttribPointer( - attribute.Location, - shape.ComponentCount, - shape.Type, - shape.Normalized, - stride, - (void*)attributeOffset); - } - } - GLHelpers.ThrowOnResourceError(_gl, $"bind vertex buffer '{buffer.Name}'"); - _gl.BindBuffer(GLEnum.ArrayBuffer, 0); - } - - public void BindIndexBuffer(IGpuBuffer buffer, uint offsetBytes, GpuIndexType indexType) - { - ThrowIfClosed(); - var b = RequireGlBuffer(buffer); - _currentIndexType = indexType; - _currentIndexBufferBaseOffset = offsetBytes; - _gl.BindBuffer(GLEnum.ElementArrayBuffer, b.GlName); - GLHelpers.ThrowOnResourceError(_gl, $"bind index buffer '{buffer.Name}'"); - } - - public void SetPushConstants(in GpuPushConstants constants) - { - ThrowIfClosed(); - _currentPushConstants = constants; - if (_currentPipeline is { } pipeline) - _device.PushConstants.Apply(pipeline.GlProgram, in constants); - } - - public void SetViewport(int x, int y, int width, int height) - { - ThrowIfClosed(); - _gl.Viewport(x, y, (uint)width, (uint)height); - } - - public void SetScissor(int x, int y, int width, int height) - { - ThrowIfClosed(); - _gl.Enable(EnableCap.ScissorTest); - _gl.Scissor(x, y, (uint)width, (uint)height); - } - - public void SetCullMode(GpuCullMode cullMode) - { - ThrowIfClosed(); - _device.ApplyRenderState(_device.CurrentRenderState with { Cull = cullMode }); - } - - public void SetFrontFace(GpuFrontFace frontFace) - { - ThrowIfClosed(); - _device.ApplyRenderState(_device.CurrentRenderState with { FrontFace = frontFace }); - } - - public void SetDepthWrite(bool enabled) - { - ThrowIfClosed(); - _device.ApplyRenderState(_device.CurrentRenderState with { DepthWrite = enabled }); - } - - public void SetStencil(in GpuStencilState stencil) - { - ThrowIfClosed(); - _device.ApplyRenderState(_device.CurrentRenderState with { Stencil = stencil }); - } - - public unsafe void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance) - { - ThrowIfClosed(); - _device.FlushBeforeDraw(_frame.SlotIndex); - int indexSize = GlEnumMapping.IndexSizeBytesOf(_currentIndexType); - nint indexOffset = (nint)(_currentIndexBufferBaseOffset + firstIndex * (uint)indexSize); - _gl.DrawElementsInstancedBaseVertexBaseInstance( - GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology), - indexCount, - GlEnumMapping.DrawElementsTypeOf(_currentIndexType), - (void*)indexOffset, - instanceCount, - vertexOffset, - firstInstance); - GLHelpers.ThrowOnResourceError(_gl, "DrawIndexed"); - } - - public void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance) - { - ThrowIfClosed(); - _device.FlushBeforeDraw(_frame.SlotIndex); - _gl.DrawArraysInstancedBaseInstance( - (GLEnum)GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology), - (int)firstVertex, - vertexCount, - instanceCount, - firstInstance); - GLHelpers.ThrowOnResourceError(_gl, "Draw"); - } - - public unsafe void MultiDrawIndexedIndirect(IGpuBuffer commands, uint offsetBytes, uint drawCount, uint strideBytes) - { - ThrowIfClosed(); - var indirect = RequireGlBuffer(commands); - _device.FlushBeforeDraw(_frame.SlotIndex); - _gl.BindBuffer(GLEnum.DrawIndirectBuffer, indirect.GlName); - _gl.MultiDrawElementsIndirect( - GlEnumMapping.PrimitiveTypeOf(RequirePipeline().Description.Topology), - GlEnumMapping.DrawElementsTypeOf(_currentIndexType), - (void*)(nint)offsetBytes, - drawCount, - strideBytes); - GLHelpers.ThrowOnResourceError(_gl, "MultiDrawIndexedIndirect"); - _gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0); - } - - public IDisposable BeginTimerScope(string scopeName) => _device.TimerPool.BeginScope(scopeName); - - public void Dispose() - { - if (_closed) - return; - _closed = true; - // GL has no store-op work to do here: GpuStoreOp.Resolve was already - // rejected at BeginPass (V1 targets are single-sampled), and - // Store/DontCare need no explicit action — the framebuffer's contents - // simply persist until the next pass rebinds a target. - // - // Restore whatever capability state was ambient before this pass - // opened (see the constructor's comment) so a still-raw-GL renderer - // running immediately after this pass sees exactly what it would have - // seen had this pass never bound a pipeline. - _ambientOnEntry.Restore(_ambientApi); - _frame.ClosePass(this); - } - - private GlGpuPipeline RequirePipeline() => - _currentPipeline ?? throw new InvalidOperationException("BindPipeline must be called before drawing."); - - private static GlGpuBuffer RequireGlBuffer(IGpuBuffer buffer) - { - ArgumentNullException.ThrowIfNull(buffer); - if (buffer is not GlGpuBuffer glBuffer) - throw new ArgumentException("The GL backend can only bind GL buffers.", nameof(buffer)); - return glBuffer; - } - - private void ThrowIfClosed() - { - if (_closed) - throw new ObjectDisposedException(nameof(GlGpuPassEncoder)); - } -} - diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPipeline.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPipeline.cs deleted file mode 100644 index 60106a0e..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPipeline.cs +++ /dev/null @@ -1,97 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Compiles through the existing -/// (the same compiler every other GL -/// shader in the codebase uses — this is not a second compiler) and owns one -/// VAO shaped by . -/// -/// The VAO only records which attribute locations are enabled and their -/// component shape; it does NOT bind a vertex buffer at creation time. Vertex -/// attribute pointers are re-issued by -/// every time a buffer/offset is bound, because a ring allocation's offset -/// changes every frame — GL has no notion of "rebase this VAO's buffer at a -/// new offset" independent of re-issuing glVertexAttribPointer. -/// -internal sealed class GlGpuPipeline : IGpuPipeline -{ - private readonly GL _gl; - private readonly IGpuResourceRetirementQueue _retirement; - private uint _program; - private uint _vertexArray; - - public GlGpuPipeline(GL gl, IGpuResourceRetirementQueue retirement, GpuPipelineDescription description, string vertexSource, string fragmentSource) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement)); - Description = description ?? throw new ArgumentNullException(nameof(description)); - - _program = ShaderProgramConstruction.Build(new GlShaderProgramBuildApi(_gl), vertexSource, fragmentSource); - try - { - _vertexArray = GlResourceCommand.CreateName( - _gl, - $"pipeline '{description.Name}' VAO", - _gl.GenVertexArray, - _gl.DeleteVertexArray); - _gl.BindVertexArray(_vertexArray); - GpuVertexLayout layout = description.VertexLayout; - foreach (GpuVertexAttribute attribute in layout.Attributes) - { - _gl.EnableVertexAttribArray(attribute.Location); - // Slice V6l: the divisor is VAO state and survives every later - // glVertexAttribPointer, so it belongs here with the enables - // rather than in the per-frame rebind. Zero is the GL default and - // is restated explicitly — a VAO name can be recycled by the - // driver, and inheriting a stale divisor draws one instance's - // data across every vertex. - _gl.VertexAttribDivisor( - attribute.Location, - layout.InputRateOf(attribute.Binding) == GpuVertexInputRate.Instance ? 1u : 0u); - } - GLHelpers.ThrowOnResourceError(_gl, $"configure pipeline '{description.Name}' VAO"); - _gl.BindVertexArray(0); - } - catch - { - _gl.DeleteProgram(_program); - _program = 0; - throw; - } - } - - public GpuPipelineDescription Description { get; } - - internal uint GlProgram => _program; - internal uint GlVertexArray => _vertexArray; - - public void Dispose() - { - uint program = _program; - uint vertexArray = _vertexArray; - if (program == 0 && vertexArray == 0) - return; - _program = 0; - _vertexArray = 0; - - GL gl = _gl; - string label = Description.Name; - _retirement.Retire(() => - { - if (vertexArray != 0) - { - gl.DeleteVertexArray(vertexArray); - GLHelpers.ThrowOnResourceError(gl, $"delete pipeline '{label}' VAO"); - } - if (program != 0) - { - gl.DeleteProgram(program); - GLHelpers.ThrowOnResourceError(gl, $"delete pipeline '{label}' program"); - } - }); - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPushConstantBinder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPushConstantBinder.cs deleted file mode 100644 index 74713463..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPushConstantBinder.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Numerics; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Applies a value to the currently bound GL -/// program by uniform name (), -/// caching each program's resolved locations so the name lookup only happens -/// once per program. A location of -1 (the program does not declare that -/// uniform) is silently skipped, exactly as the contract's XML docs specify. -/// -internal sealed class GlGpuPushConstantBinder -{ - private readonly GL _gl; - private readonly Dictionary _locationsByProgram = new(); - - public GlGpuPushConstantBinder(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - - public unsafe void Apply(uint program, in GpuPushConstants constants) - { - ProgramLocations locations = GetOrResolveLocations(program); - - if (locations.ViewProjection >= 0) - { - Matrix4x4 m = constants.ViewProjection; - _gl.UniformMatrix4(locations.ViewProjection, 1, false, (float*)&m); - } - if (locations.DrawIdOffset >= 0) - _gl.Uniform1(locations.DrawIdOffset, constants.DrawIdOffset); - if (locations.LightingMode >= 0) - _gl.Uniform1(locations.LightingMode, constants.LightingMode); - if (locations.RenderPass >= 0) - _gl.Uniform1(locations.RenderPass, constants.RenderPass); - if (locations.LightDebug >= 0) - _gl.Uniform1(locations.LightDebug, constants.LightDebug); - if (locations.TextureIndexA >= 0) - _gl.Uniform1(locations.TextureIndexA, constants.TextureIndexA); - if (locations.TextureIndexB >= 0) - _gl.Uniform1(locations.TextureIndexB, constants.TextureIndexB); - if (locations.ParamA >= 0) - _gl.Uniform1(locations.ParamA, constants.ParamA); - if (locations.ParamB >= 0) - _gl.Uniform1(locations.ParamB, constants.ParamB); - GLHelpers.ThrowOnResourceError(_gl, $"apply push constants to program {program}"); - } - - /// Drops cached locations for a deleted program. Call before its GL name is reused. - public void Forget(uint program) => _locationsByProgram.Remove(program); - - private ProgramLocations GetOrResolveLocations(uint program) - { - if (_locationsByProgram.TryGetValue(program, out ProgramLocations existing)) - return existing; - - ProgramLocations locations = new( - _gl.GetUniformLocation(program, GlPushConstantUniformNames.ViewProjection), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.DrawIdOffset), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.LightingMode), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.RenderPass), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.LightDebug), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.TextureIndexA), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.TextureIndexB), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.ParamA), - _gl.GetUniformLocation(program, GlPushConstantUniformNames.ParamB)); - _locationsByProgram[program] = locations; - return locations; - } - - private readonly record struct ProgramLocations( - int ViewProjection, - int DrawIdOffset, - int LightingMode, - int RenderPass, - int LightDebug, - int TextureIndexA, - int TextureIndexB, - int ParamA, - int ParamB); -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuRenderTarget.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuRenderTarget.cs deleted file mode 100644 index 329374d3..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuRenderTarget.cs +++ /dev/null @@ -1,121 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// An offscreen colour(+depth) FBO. The colour attachment is a -/// so it can be registered into the texture table -/// after the pass; the depth/stencil attachment (when requested) is a plain -/// renderbuffer, mirroring ManagedGLFramebuffer's approach — nothing -/// ever samples depth for the targets this slice's contract describes -/// (paperdoll, creature appraisal, portal masking). -/// -internal sealed class GlGpuRenderTarget : IGpuRenderTarget -{ - private readonly GL _gl; - private readonly IGpuResourceRetirementQueue _retirement; - private uint _framebuffer; - private uint _depthStencilRenderbuffer; - private GlGpuTexture? _color; - - public GlGpuRenderTarget(GL gl, IGpuResourceRetirementQueue retirement, GpuRenderTargetDescription description) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement)); - Description = description; - if (description.SampleCount != 1) - { - throw new NotSupportedException( - "GL render targets are single-sampled in Campaign V slice V1; " + - "MSAA offscreen targets are not part of this contract."); - } - - _color = new GlGpuTexture( - _gl, - _retirement, - new GpuTextureDescription( - $"{description.Name}-color", - GpuTextureKind.Texture2D, - description.ColorFormat, - description.Width, - description.Height, - LayerCount: 1, - MipLevelCount: 1)); - - _framebuffer = GlResourceCommand.CreateName(_gl, $"framebuffer '{description.Name}'", _gl.GenFramebuffer, _gl.DeleteFramebuffer); - _gl.BindFramebuffer(GLEnum.Framebuffer, _framebuffer); - _gl.FramebufferTexture2D( - GLEnum.Framebuffer, - GLEnum.ColorAttachment0, - GLEnum.Texture2D, - _color.GlName, - 0); - - if (description.DepthFormat is not null) - { - _depthStencilRenderbuffer = GlResourceCommand.CreateName( - _gl, - $"depth renderbuffer '{description.Name}'", - _gl.GenRenderbuffer, - _gl.DeleteRenderbuffer); - _gl.BindRenderbuffer(GLEnum.Renderbuffer, _depthStencilRenderbuffer); - _gl.RenderbufferStorage(GLEnum.Renderbuffer, GLEnum.Depth24Stencil8, (uint)description.Width, (uint)description.Height); - _gl.FramebufferRenderbuffer( - GLEnum.Framebuffer, - GLEnum.DepthStencilAttachment, - GLEnum.Renderbuffer, - _depthStencilRenderbuffer); - } - - GLEnum status = _gl.CheckFramebufferStatus(GLEnum.Framebuffer); - _gl.BindFramebuffer(GLEnum.Framebuffer, 0); - if (status != GLEnum.FramebufferComplete) - { - // Roll back the two names this constructor already owns before - // surfacing the failure — nothing has been published yet. - _gl.DeleteFramebuffer(_framebuffer); - if (_depthStencilRenderbuffer != 0) - _gl.DeleteRenderbuffer(_depthStencilRenderbuffer); - _color.Dispose(); - throw new InvalidOperationException( - $"Render target '{description.Name}' framebuffer is incomplete: {status}."); - } - } - - public GpuRenderTargetDescription Description { get; } - - public IGpuTexture ColorTexture => _color ?? throw new ObjectDisposedException(Description.Name); - - internal uint GlFramebufferName => _framebuffer; - - public void Dispose() - { - uint framebuffer = _framebuffer; - uint renderbuffer = _depthStencilRenderbuffer; - GlGpuTexture? color = _color; - if (framebuffer == 0 && color is null) - return; - _framebuffer = 0; - _depthStencilRenderbuffer = 0; - _color = null; - - GL gl = _gl; - string label = Description.Name; - _retirement.Retire(() => - { - if (framebuffer != 0) - { - gl.DeleteFramebuffer(framebuffer); - GLHelpers.ThrowOnResourceError(gl, $"delete framebuffer '{label}'"); - } - if (renderbuffer != 0) - { - gl.DeleteRenderbuffer(renderbuffer); - GLHelpers.ThrowOnResourceError(gl, $"delete depth renderbuffer '{label}'"); - } - }); - color?.Dispose(); - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuSampler.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuSampler.cs deleted file mode 100644 index e2d8421f..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuSampler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// One GL sampler object. -/// de-duplicates by value, so a given -/// wrap/filter combination is only ever backed by one physical sampler name — -/// the same pattern SamplerCache already uses for its two fixed -/// samplers, generalized to the full description space the RHI exposes. -/// -internal sealed class GlGpuSampler : IGpuSampler -{ - private const uint GlTextureMaxAnisotropy = 0x84FE; - - private readonly GL _gl; - private readonly IGpuResourceRetirementQueue _retirement; - private uint _name; - - public GlGpuSampler(GL gl, IGpuResourceRetirementQueue retirement, GpuSamplerDescription description) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement)); - Description = description; - - _name = GlResourceCommand.CreateName(_gl, "sampler", _gl.GenSampler, _gl.DeleteSampler); - TextureMinFilter minFilter = GlEnumMapping.MinFilterOf(description.MinFilter, description.MipFilter); - TextureMagFilter magFilter = GlEnumMapping.MagFilterOf(description.MagFilter); - TextureWrapMode wrapU = GlEnumMapping.WrapModeOf(description.AddressU); - TextureWrapMode wrapV = GlEnumMapping.WrapModeOf(description.AddressV); - - _gl.SamplerParameter(_name, SamplerParameterI.MinFilter, (int)minFilter); - _gl.SamplerParameter(_name, SamplerParameterI.MagFilter, (int)magFilter); - _gl.SamplerParameter(_name, SamplerParameterI.WrapS, (int)wrapU); - _gl.SamplerParameter(_name, SamplerParameterI.WrapT, (int)wrapV); - if (description.MaxAnisotropy > 1f) - _gl.SamplerParameter(_name, (SamplerParameterF)GlTextureMaxAnisotropy, description.MaxAnisotropy); - GLHelpers.ThrowOnResourceError(_gl, $"configure sampler {_name}"); - } - - public GpuSamplerDescription Description { get; } - - internal uint GlName => _name; - - public void Dispose() - { - uint name = _name; - if (name == 0) - return; - _name = 0; - - GL gl = _gl; - _retirement.Retire(() => - { - gl.DeleteSampler(name); - GLHelpers.ThrowOnResourceError(gl, $"delete sampler {name}"); - }); - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTexture.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTexture.cs deleted file mode 100644 index f4f96e3d..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTexture.cs +++ /dev/null @@ -1,202 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// A GL texture (2D or 2D array) allocated with immutable storage -/// (glTexStorage2D/3D) so every mip level is defined the moment the -/// object is created, regardless of upload order — mirroring -/// ManagedGLTextureArray's allocation strategy without inheriting its -/// Chorizite/ coupling. -/// -/// The texture's own min/mag filter is set at creation (not left at GL -/// defaults) purely so the texture is "complete" for sampling the instant it -/// exists — completeness is judged from the texture object's own filter -/// state, independent of whatever a draw later -/// binds. The bound sampler object overrides actual filtering at draw time -/// (same override rule SamplerCache already documents), so this -/// default never affects the rendered image. -/// -internal sealed class GlGpuTexture : IGpuTexture -{ - private readonly GL _gl; - private readonly IGpuResourceRetirementQueue _retirement; - private readonly GlTextureFormatInfo _formatInfo; - private readonly GLEnum _target; - private uint _name; - - public GlGpuTexture(GL gl, IGpuResourceRetirementQueue retirement, GpuTextureDescription description) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _retirement = retirement ?? throw new ArgumentNullException(nameof(retirement)); - Name = description.Name; - Kind = description.Kind; - Format = description.Format; - Width = description.Width; - Height = description.Height; - LayerCount = description.LayerCount; - MipLevelCount = description.MipLevelCount; - _formatInfo = GlGpuTextureFormatMapping.Resolve(Format); - _target = Kind == GpuTextureKind.Texture2D ? GLEnum.Texture2D : GLEnum.Texture2DArray; - - _name = GlResourceCommand.CreateName(_gl, $"texture '{Name}'", _gl.GenTexture, _gl.DeleteTexture); - _gl.BindTexture(_target, _name); - if (Kind == GpuTextureKind.Texture2D) - { - _gl.TexStorage2D(_target, (uint)MipLevelCount, _formatInfo.SizedInternalFormat, (uint)Width, (uint)Height); - } - else - { - _gl.TexStorage3D( - _target, - (uint)MipLevelCount, - _formatInfo.SizedInternalFormat, - (uint)Width, - (uint)Height, - (uint)LayerCount); - } - GLHelpers.ThrowOnResourceError( - _gl, - $"allocate texture '{Name}' {Format} {Width}x{Height}x{LayerCount} ({MipLevelCount} mips)"); - - TextureMinFilter minFilter = MipLevelCount > 1 ? TextureMinFilter.LinearMipmapLinear : TextureMinFilter.Linear; - _gl.TexParameter(_target, TextureParameterName.TextureMinFilter, (int)minFilter); - _gl.TexParameter(_target, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - GLHelpers.ThrowOnResourceError(_gl, $"set default filter for texture '{Name}'"); - _gl.BindTexture(_target, 0); - } - - public string Name { get; } - public GpuTextureKind Kind { get; } - public GpuTextureFormat Format { get; } - public int Width { get; } - public int Height { get; } - public int LayerCount { get; } - public int MipLevelCount { get; } - - /// The physical GL texture name. Used by to obtain a bindless handle. - internal uint GlName => _name; - - internal GLEnum Target => _target; - - public void Upload(int mipLevel, int layer, ReadOnlySpan data) - { - ThrowIfDisposed(); - if (Format == GpuTextureFormat.Depth24Stencil8) - { - throw new NotSupportedException( - "Depth24Stencil8 textures are attachment-only; they are never uploaded from the CPU."); - } - if ((uint)mipLevel >= (uint)MipLevelCount) - throw new ArgumentOutOfRangeException(nameof(mipLevel)); - if (Kind == GpuTextureKind.Texture2D && layer != 0) - throw new ArgumentOutOfRangeException(nameof(layer), "A 2D texture only has layer 0."); - if (Kind == GpuTextureKind.Texture2DArray && (uint)layer >= (uint)LayerCount) - throw new ArgumentOutOfRangeException(nameof(layer)); - - int mipWidth = Math.Max(1, Width >> mipLevel); - int mipHeight = Math.Max(1, Height >> mipLevel); - long expected = _formatInfo.LayerByteCount(mipWidth, mipHeight); - if (data.Length != expected) - { - throw new ArgumentException( - $"Upload for texture '{Name}' mip {mipLevel} has {data.Length} bytes; expected {expected} " + - $"for {Format} {mipWidth}x{mipHeight}.", - nameof(data)); - } - - _gl.BindTexture(_target, _name); - _gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1); - unsafe - { - fixed (byte* pointer = data) - { - if (Kind == GpuTextureKind.Texture2D) - UploadTexture2D(mipLevel, mipWidth, mipHeight, data.Length, pointer); - else - UploadTexture2DArray(mipLevel, layer, mipWidth, mipHeight, data.Length, pointer); - } - } - GLHelpers.ThrowOnResourceError(_gl, $"upload texture '{Name}' mip {mipLevel} layer {layer}"); - _gl.BindTexture(_target, 0); - } - - private unsafe void UploadTexture2D(int mipLevel, int mipWidth, int mipHeight, int byteCount, byte* pointer) - { - if (_formatInfo.IsCompressed) - { - _gl.CompressedTexSubImage2D( - _target, mipLevel, 0, 0, (uint)mipWidth, (uint)mipHeight, - (InternalFormat)_formatInfo.SizedInternalFormat, (uint)byteCount, pointer); - } - else - { - _gl.TexSubImage2D( - _target, mipLevel, 0, 0, (uint)mipWidth, (uint)mipHeight, - _formatInfo.UploadPixelFormat, _formatInfo.UploadPixelType, pointer); - } - } - - private unsafe void UploadTexture2DArray(int mipLevel, int layer, int mipWidth, int mipHeight, int byteCount, byte* pointer) - { - if (_formatInfo.IsCompressed) - { - _gl.CompressedTexSubImage3D( - _target, mipLevel, 0, 0, layer, (uint)mipWidth, (uint)mipHeight, 1, - (InternalFormat)_formatInfo.SizedInternalFormat, (uint)byteCount, pointer); - } - else - { - _gl.TexSubImage3D( - _target, mipLevel, 0, 0, layer, (uint)mipWidth, (uint)mipHeight, 1, - _formatInfo.UploadPixelFormat, _formatInfo.UploadPixelType, pointer); - } - } - - public void GenerateMipChain() - { - ThrowIfDisposed(); - if (MipLevelCount <= 1) - return; - - // Matches ManagedGLTextureArray.ProcessDirtyUpdatesInternal: GL's - // driver-defined compressed-mip regeneration is the one behaviour this - // migration deliberately does not carry into the RHI contract (the - // Vulkan backend cannot blit BC images at all and requires a CPU-built - // chain instead). Callers that need BC mips must supply every level - // through Upload directly; this is a documented no-op for them on GL, - // not a silent gap, since the GL renderer path today already skips - // glGenerateMipmap for compressed arrays. - if (_formatInfo.IsCompressed) - return; - - _gl.BindTexture(_target, _name); - _gl.GenerateMipmap(_target); - GLHelpers.ThrowOnResourceError(_gl, $"generate mip chain for texture '{Name}'"); - _gl.BindTexture(_target, 0); - } - - public void Dispose() - { - uint name = _name; - if (name == 0) - return; - _name = 0; - - GL gl = _gl; - string label = Name; - _retirement.Retire(() => - { - gl.DeleteTexture(name); - GLHelpers.ThrowOnResourceError(gl, $"delete texture '{label}' ({name})"); - }); - } - - private void ThrowIfDisposed() - { - if (_name == 0) - throw new ObjectDisposedException(Name); - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTextureFormatMapping.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTextureFormatMapping.cs deleted file mode 100644 index 8c4aeb47..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTextureFormatMapping.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// GL format triple for a : the sized internal -/// format used at allocation (glTexStorage2D/3D), the upload -/// format/type for uncompressed uploads, and whether uploads go through -/// glCompressedTexSubImage2D/3D instead. -/// -/// is acdream's own enum, not the Chorizite -/// TextureFormat the existing TextureFormatExtensions targets, -/// so this is the "otherwise add a private mapper" fallback the campaign spec -/// calls for rather than an extension of that existing type. Pure data — -/// safe to unit test without a live GL context, since Silk's GL enums are -/// plain value types. -/// -internal readonly record struct GlTextureFormatInfo( - SizedInternalFormat SizedInternalFormat, - PixelFormat UploadPixelFormat, - PixelType UploadPixelType, - bool IsCompressed, - int BlockOrTexelBytes, - int BlockDimension) -{ - /// Bytes required for one full, uncompressed mip layer at the given dimensions, or one compressed layer rounded up to whole blocks. - public long LayerByteCount(int width, int height) - { - if (!IsCompressed) - return (long)width * height * BlockOrTexelBytes; - - int blocksWide = (width + BlockDimension - 1) / BlockDimension; - int blocksHigh = (height + BlockDimension - 1) / BlockDimension; - return (long)blocksWide * blocksHigh * BlockOrTexelBytes; - } -} - -internal static class GlGpuTextureFormatMapping -{ - public static GlTextureFormatInfo Resolve(GpuTextureFormat format) => format switch - { - GpuTextureFormat.Rgba8Unorm or GpuTextureFormat.Rgba8UnormRenderTarget => - new GlTextureFormatInfo(SizedInternalFormat.Rgba8, PixelFormat.Rgba, PixelType.UnsignedByte, false, 4, 1), - GpuTextureFormat.R8Unorm => - new GlTextureFormatInfo(SizedInternalFormat.R8, PixelFormat.Red, PixelType.UnsignedByte, false, 1, 1), - GpuTextureFormat.Bc1Unorm => - new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt1Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 8, 4), - GpuTextureFormat.Bc2Unorm => - new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt3Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 16, 4), - GpuTextureFormat.Bc3Unorm => - new GlTextureFormatInfo(SizedInternalFormat.CompressedRgbaS3TCDxt5Ext, PixelFormat.Rgba, PixelType.UnsignedByte, true, 16, 4), - GpuTextureFormat.Depth24Stencil8 => - new GlTextureFormatInfo(SizedInternalFormat.Depth24Stencil8, PixelFormat.DepthStencil, PixelType.UnsignedInt248, false, 4, 1), - _ => throw new NotSupportedException($"No GL format mapping for {format}."), - }; -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTimerPool.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTimerPool.cs deleted file mode 100644 index a1c015c6..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuTimerPool.cs +++ /dev/null @@ -1,186 +0,0 @@ -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Seam over the four GL calls a TimeElapsed timer scope needs, mirroring -/// IGpuFenceApi's role for : it lets -/// 's double-buffering and result-promotion logic run -/// under a unit test with no live GL context. -/// -internal interface IGlTimerQueryApi -{ - uint CreateQuery(); - void DeleteQuery(uint query); - void Begin(uint query); - void End(); - - /// Non-blocking: false when the result for is not yet available (or the query was never begun). - bool TryGetResult(uint query, out double milliseconds); -} - -internal sealed class SilkGlTimerQueryApi(GL gl) : IGlTimerQueryApi -{ - private readonly GL _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - - public uint CreateQuery() => _gl.GenQuery(); - - public void DeleteQuery(uint query) => _gl.DeleteQuery(query); - - public void Begin(uint query) => _gl.BeginQuery(QueryTarget.TimeElapsed, query); - - public void End() => _gl.EndQuery(QueryTarget.TimeElapsed); - - public bool TryGetResult(uint query, out double milliseconds) - { - _gl.GetQueryObject(query, QueryObjectParameterName.ResultAvailable, out int available); - if (available == 0) - { - milliseconds = 0; - return false; - } - - _gl.GetQueryObject(query, QueryObjectParameterName.Result, out ulong elapsedNanoseconds); - milliseconds = elapsedNanoseconds / 1_000_000d; - return true; - } -} - -/// -/// backed by TimeElapsed queries. Core GL -/// only allows one GL_TIME_ELAPSED query active at a time (the -/// restriction is per target, not per query object), so scopes must not -/// nest — throws if a previous scope in the same -/// pass hasn't been disposed yet, exactly as WbDrawDispatcher already -/// requires for its own opaque/transparent query pair. -/// -/// Each distinct scope name gets its own double-buffered pair of query -/// objects (per the campaign spec: "double-buffered so results are read from -/// a retired frame and never block"). A scope's second call reads back the -/// FIRST call's result non-blockingly — by the time a scope name repeats, -/// at least one full frame has usually retired, so the result is normally -/// ready; if it is not, the previous value is kept until it is. -/// -internal sealed class GlGpuTimerPool : IGpuTimerPool -{ - private const int BufferDepth = 2; - - private sealed class ScopeState - { - public readonly uint[] Queries = new uint[BufferDepth]; - public readonly bool[] Began = new bool[BufferDepth]; - public int NextSlot; - public double LastResolvedMilliseconds; - public bool HasResolvedValue; - } - - private readonly IGlTimerQueryApi _api; - private readonly Dictionary _scopes = new(StringComparer.Ordinal); - private string? _activeScopeName; - - public GlGpuTimerPool(IGlTimerQueryApi api, bool isSupported) - { - _api = api ?? throw new ArgumentNullException(nameof(api)); - IsSupported = isSupported; - } - - public bool IsSupported { get; } - - /// - /// Begins (or continues) the named scope. Returns a disposable that ends - /// the query; dispose it before beginning another scope in the same pass. - /// - public IDisposable BeginScope(string scopeName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(scopeName); - if (!IsSupported) - return NullTimerScope.Instance; - - if (_activeScopeName is not null) - { - throw new InvalidOperationException( - $"GPU timer scope '{_activeScopeName}' is still active; TimeElapsed queries " + - "cannot nest. Dispose the previous scope before beginning another."); - } - - if (!_scopes.TryGetValue(scopeName, out ScopeState? state)) - { - state = new ScopeState(); - for (int i = 0; i < BufferDepth; i++) - state.Queries[i] = _api.CreateQuery(); - _scopes.Add(scopeName, state); - } - - int slot = state.NextSlot; - state.NextSlot = (slot + 1) % BufferDepth; - - // Poll the OTHER slot's query — the one begun on the previous call to - // this scope name — before beginning this one. That is what makes a - // scope's Nth call read back the (N-1)th call's result: with only two - // slots, "the previous slot" and "the other slot" are the same index, - // and by now it has usually had at least one frame to retire. - int previousSlot = (slot + BufferDepth - 1) % BufferDepth; - if (state.Began[previousSlot] && _api.TryGetResult(state.Queries[previousSlot], out double milliseconds)) - { - state.LastResolvedMilliseconds = milliseconds; - state.HasResolvedValue = true; - } - - _api.Begin(state.Queries[slot]); - state.Began[slot] = true; - _activeScopeName = scopeName; - return new ActiveTimerScope(this, scopeName); - } - - public bool TryResolve(string scopeName, out double milliseconds) - { - if (_scopes.TryGetValue(scopeName, out ScopeState? state) && state.HasResolvedValue) - { - milliseconds = state.LastResolvedMilliseconds; - return true; - } - - milliseconds = 0; - return false; - } - - private void EndScope(string scopeName) - { - if (_activeScopeName != scopeName) - return; - - _api.End(); - _activeScopeName = null; - } - - internal void DisposeQueries() - { - foreach (ScopeState state in _scopes.Values) - { - foreach (uint query in state.Queries) - _api.DeleteQuery(query); - } - _scopes.Clear(); - } - - private sealed class ActiveTimerScope(GlGpuTimerPool pool, string scopeName) : IDisposable - { - private bool _disposed; - - public void Dispose() - { - if (_disposed) - return; - _disposed = true; - pool.EndScope(scopeName); - } - } - - private sealed class NullTimerScope : IDisposable - { - public static NullTimerScope Instance { get; } = new(); - public void Dispose() - { - } - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlPushConstantUniformNames.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlPushConstantUniformNames.cs deleted file mode 100644 index c61637a0..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlPushConstantUniformNames.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Reflection; - -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// The single source of truth mapping each -/// field to the GLSL uniform name the GL backend binds it to — the names -/// documented on the fields themselves. -/// reads these constants (not string literals of its own) when it calls -/// GL.GetUniformLocation, and lets a -/// test prove the table cannot silently drop a field that a later slice adds -/// to the shared struct. -/// -internal static class GlPushConstantUniformNames -{ - public const string ViewProjection = "uViewProjection"; - public const string DrawIdOffset = "uDrawIDOffset"; - public const string LightingMode = "uLightingMode"; - public const string RenderPass = "uRenderPass"; - public const string LightDebug = "uLightDebug"; - public const string TextureIndexA = "uTextureIndexA"; - public const string TextureIndexB = "uTextureIndexB"; - public const string ParamA = "uParamA"; - public const string ParamB = "uParamB"; - - /// - /// Field name (as declared on ) to GLSL - /// uniform name, built from the same constants the binder applies with — - /// so the binder and this completeness table can never disagree. - /// - public static IReadOnlyDictionary ByFieldName { get; } = - new Dictionary - { - [nameof(GpuPushConstants.ViewProjection)] = ViewProjection, - [nameof(GpuPushConstants.DrawIdOffset)] = DrawIdOffset, - [nameof(GpuPushConstants.LightingMode)] = LightingMode, - [nameof(GpuPushConstants.RenderPass)] = RenderPass, - [nameof(GpuPushConstants.LightDebug)] = LightDebug, - [nameof(GpuPushConstants.TextureIndexA)] = TextureIndexA, - [nameof(GpuPushConstants.TextureIndexB)] = TextureIndexB, - [nameof(GpuPushConstants.ParamA)] = ParamA, - [nameof(GpuPushConstants.ParamB)] = ParamB, - }; - - /// - /// Throws if declares a public instance - /// field this table does not map — the drift guard the campaign spec asks - /// for. Called from a unit test, not from production code. - /// - internal static void AssertMapsEveryField() - { - FieldInfo[] fields = typeof(GpuPushConstants).GetFields( - BindingFlags.Public | BindingFlags.Instance); - foreach (FieldInfo field in fields) - { - if (!ByFieldName.ContainsKey(field.Name)) - { - throw new InvalidOperationException( - $"GpuPushConstants.{field.Name} has no mapped GLSL uniform name in " + - $"{nameof(GlPushConstantUniformNames)}."); - } - } - - if (ByFieldName.Count != fields.Length) - { - throw new InvalidOperationException( - $"{nameof(GlPushConstantUniformNames)} maps {ByFieldName.Count} names but " + - $"{nameof(GpuPushConstants)} declares {fields.Length} fields — a stale entry " + - "survives a field rename or removal."); - } - } -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs deleted file mode 100644 index ae458909..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlRenderStateCache.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Everything bakes plus the handful of fields -/// core Vulkan 1.3 (and this backend) makes dynamic per draw. -/// is the GL program name, included so a pipeline -/// switch is itself a tracked dimension. -/// -internal readonly record struct GlRenderStateSnapshot( - uint Program, - GpuBlendMode Blend, - bool DepthTest, - bool DepthWrite, - GpuCompareOp DepthCompare, - GpuCullMode Cull, - GpuFrontFace FrontFace, - bool AlphaToCoverage, - bool ColorWrite, - bool StencilTest, - GpuStencilState Stencil); - -/// Which GL state calls are needed to move from the previous snapshot to the new one. -internal readonly record struct GlRenderStateChanges( - bool Program, - bool Blend, - bool DepthTest, - bool DepthWrite, - bool DepthCompare, - bool Cull, - bool FrontFace, - bool AlphaToCoverage, - bool ColorWrite, - bool StencilTest, - bool Stencil) -{ - public bool AnyChange => - Program || Blend || DepthTest || DepthWrite || DepthCompare - || Cull || FrontFace || AlphaToCoverage || ColorWrite - || StencilTest || Stencil; - - /// Every dimension reported changed — used for the first apply after a reset. - internal static GlRenderStateChanges All { get; } = - new(true, true, true, true, true, true, true, true, true, true, true); -} - -/// -/// Pure GL-free state cache: given the previously applied -/// and a newly desired one, reports which -/// dimensions actually changed so only issues -/// the GL calls that matter. Starts with no baseline, so the very first -/// call always reports every dimension changed — "when in -/// doubt, set the state" rather than risk stale driver state from before this -/// cache existed (e.g. from a previous pass, or default GL state that may not -/// match a pipeline's baked defaults). -/// -internal sealed class GlRenderStateCache -{ - private GlRenderStateSnapshot? _last; - - public GlRenderStateChanges Apply(GlRenderStateSnapshot desired) - { - GlRenderStateSnapshot? previous = _last; - _last = desired; - - if (previous is not { } p) - return GlRenderStateChanges.All; - - return new GlRenderStateChanges( - p.Program != desired.Program, - p.Blend != desired.Blend, - p.DepthTest != desired.DepthTest, - p.DepthWrite != desired.DepthWrite, - p.DepthCompare != desired.DepthCompare, - p.Cull != desired.Cull, - p.FrontFace != desired.FrontFace, - p.AlphaToCoverage != desired.AlphaToCoverage, - p.ColorWrite != desired.ColorWrite, - p.StencilTest != desired.StencilTest, - p.Stencil != desired.Stencil); - } - - /// Discards the cached baseline — the next reports every dimension changed. - public void Reset() => _last = null; -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlRingBufferState.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlRingBufferState.cs deleted file mode 100644 index 1771013d..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlRingBufferState.cs +++ /dev/null @@ -1,140 +0,0 @@ -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Pure bookkeeping for one flight slot's upload ring: an allocation cursor -/// that only grows across a frame, a "dirty" watermark tracking the byte range -/// written since the last flush, and the flushed high-water mark that makes the -/// ring's write path safe to issue unsynchronized. -/// -/// This is deliberately GL-free so it can be unit tested without a live -/// context. 's device owns one instance per flight slot -/// and pairs it with a managed staging byte[] and a real GL buffer; the -/// staging array receives every write, and -/// immediately before each Draw/DrawIndexed/ -/// MultiDrawIndexedIndirect the device writes exactly the dirty range -/// into the GL buffer through -/// and calls -/// to reset the watermark. The allocation cursor -/// itself only resets at (once per BeginFrame) — -/// writes made after a flush simply extend the dirty range again, to be picked -/// up by the next flush. This is what makes "flush before every draw, not at -/// bind time" correct: a renderer that writes after binding still gets uploaded -/// before its draw call runs. -/// -/// The forward-only invariant is load-bearing, not incidental. The -/// device's per-flush write is mapped with GL_MAP_UNSYNCHRONIZED_BIT, -/// which means the driver inserts no wait and the caller is asserting that no -/// submitted-and-unfinished draw reads the range. Within a frame that holds -/// because only ever hands out bytes at or above the -/// cursor, so each flush covers a range strictly above every range already -/// flushed. Rather than leave that as an emergent property of the arithmetic, -/// refuses a write below : -/// a future change that reused ring bytes mid-frame would fail loudly here -/// instead of producing an undefined read on the GPU. Across frames the -/// invariant belongs to GpuFrameFlightController, which waits on a -/// slot's fence in BeginFrame before this state is . -/// -/// -internal sealed class GlRingBufferState -{ - private readonly int _capacityBytes; - private uint _cursor; - private int _dirtyStart = -1; - private int _dirtyEnd = -1; - private int _flushedEnd; - - public GlRingBufferState(int capacityBytes) - { - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(capacityBytes); - _capacityBytes = capacityBytes; - } - - public int CapacityBytes => _capacityBytes; - - /// Bytes handed out since the last . - public uint AllocatedBytes => _cursor; - - /// - /// The exclusive end of the bytes already handed to the GPU this frame. - /// Every subsequent write must start at or above it. - /// - public int FlushedEndBytes => _flushedEnd; - - /// - /// Reserves bytes aligned to - /// , returning the aligned offset. - /// Throws — rather than truncating — when the request would exceed the - /// slot's capacity, because a silently shortened allocation would corrupt - /// the frame invisibly. - /// - public uint Allocate(int byteCount, uint alignmentBytes) - { - ArgumentOutOfRangeException.ThrowIfNegative(byteCount); - uint aligned = AlignUp(_cursor, alignmentBytes); - long end = (long)aligned + byteCount; - if (end > _capacityBytes) - { - throw new InvalidOperationException( - $"Ring allocation of {byteCount} bytes at aligned offset {aligned} " + - $"needs {end} bytes; the ring slot is {_capacityBytes} bytes. " + - "Increase the per-slot ring capacity (GlGpuDevice's ringCapacityBytesPerSlot)."); - } - - _cursor = (uint)end; - if (byteCount > 0) - MarkDirty((int)aligned, (int)end); - return aligned; - } - - /// Starts a new frame: rewinds the allocation cursor. Dirty state is untouched — a flush always runs before this is called. - public void Reset() - { - _cursor = 0; - _dirtyStart = -1; - _dirtyEnd = -1; - _flushedEnd = 0; - } - - /// - /// Returns the byte range written since the last flush (start, length), - /// or (0, 0) when nothing is dirty, and clears the watermark while - /// advancing over it. - /// - public (int Start, int Length) TakeDirtyRange() - { - if (_dirtyStart < 0) - return (0, 0); - - (int start, int length) = (_dirtyStart, _dirtyEnd - _dirtyStart); - _flushedEnd = Math.Max(_flushedEnd, _dirtyEnd); - _dirtyStart = -1; - _dirtyEnd = -1; - return (start, length); - } - - public bool HasDirtyBytes => _dirtyStart >= 0; - - /// - /// Records that [start, end) was written. Internal rather than - /// private for the same reason is: the forward-only - /// guard below is unreachable through by - /// construction, so proving it fires at all needs a direct call. - /// - internal void MarkDirty(int start, int end) - { - if (start < _flushedEnd) - { - throw new InvalidOperationException( - $"Ring write [{start}, {end}) reaches below the {_flushedEnd} bytes already " + - "uploaded this frame. Ring uploads are issued with GL_MAP_UNSYNCHRONIZED_BIT, " + - "so rewriting bytes a submitted draw may still be reading is undefined — the " + - "allocation cursor must only move forward until Reset."); - } - - _dirtyStart = _dirtyStart < 0 ? start : Math.Min(_dirtyStart, start); - _dirtyEnd = Math.Max(_dirtyEnd, end); - } - - internal static uint AlignUp(uint value, uint alignmentBytes) => - alignmentBytes <= 1 ? value : (value + alignmentBytes - 1) / alignmentBytes * alignmentBytes; -} diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlTextureSlotAllocator.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlTextureSlotAllocator.cs deleted file mode 100644 index 4db8c218..00000000 --- a/src/AcDream.App/Rendering/Gpu/Gl/GlTextureSlotAllocator.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace AcDream.App.Rendering.Gpu.Gl; - -/// -/// Pure bump-plus-free-list allocator for the GL texture table (the storage -/// buffer of bindless handles at ). -/// -/// GL-free by design: it knows nothing about bindless handles, retirement -/// queues, or the SSBO itself. calls -/// to get a slot to write a handle into; -/// defers the call to -/// through the device's IGpuResourceRetirementQueue -/// so a freed slot is never handed back out while a submitted frame could -/// still be reading the old handle at that index. -/// -internal sealed class GlTextureSlotAllocator -{ - private readonly uint _capacity; - private readonly Stack _freeList = new(); - private uint _nextBumpSlot; - - public GlTextureSlotAllocator(uint capacity) - { - ArgumentOutOfRangeException.ThrowIfZero(capacity); - _capacity = capacity; - } - - /// Slots currently handed out (bumped or reused) and not yet released. - public int LiveCount => (int)_nextBumpSlot - _freeList.Count; - - public uint Allocate() - { - if (_freeList.Count > 0) - return _freeList.Pop(); - - if (_nextBumpSlot >= _capacity) - { - throw new InvalidOperationException( - $"The GL texture table is exhausted: {_capacity} slots are all live. " + - "Every texture cache/atlas must release slots it no longer needs before " + - "registering new ones."); - } - - return _nextBumpSlot++; - } - - /// - /// Returns a slot to the free list. Callers must only call this once the - /// retirement queue confirms no live frame could still reference the slot. - /// - public void Release(uint slot) - { - if (slot >= _nextBumpSlot) - { - throw new ArgumentOutOfRangeException( - nameof(slot), - slot, - "Cannot release a slot that was never allocated."); - } - - _freeList.Push(slot); - } -} diff --git a/src/AcDream.App/Rendering/Gpu/GpuEnums.cs b/src/AcDream.App/Rendering/Gpu/GpuEnums.cs index a645786b..607d9a11 100644 --- a/src/AcDream.App/Rendering/Gpu/GpuEnums.cs +++ b/src/AcDream.App/Rendering/Gpu/GpuEnums.cs @@ -6,9 +6,6 @@ internal enum GpuBackendKind /// Test double — records calls, owns no driver objects. Recording, - /// OpenGL 4.3 + bindless/MDI. Deleted at Campaign V slice V11. - OpenGl, - /// Vulkan 1.3 core. Vulkan, } diff --git a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs index b22e7424..a808e483 100644 --- a/src/AcDream.App/Rendering/PaperdollFramePresenter.cs +++ b/src/AcDream.App/Rendering/PaperdollFramePresenter.cs @@ -1,7 +1,6 @@ using System.Numerics; using AcDream.App.Input; using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; using AcDream.App.UI; using AcDream.App.World; using AcDream.Content; @@ -46,9 +45,9 @@ internal interface IPaperdollPoseApplicator } /// -/// Owns paperdoll dirty/rebuild state and the private FBO presentation edge. -/// The GL renderer remains a borrowed resource disposed by the existing window -/// shutdown transaction. +/// Owns paperdoll dirty/rebuild state and the private render-target +/// presentation edge. The renderer remains a borrowed resource disposed by +/// the existing window shutdown transaction. /// internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame { diff --git a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs index 5666bef4..03309a72 100644 --- a/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PaperdollViewportRenderer.cs @@ -2,7 +2,6 @@ using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Core.Lighting; using AcDream.Core.World; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -18,8 +17,7 @@ public sealed class PaperdollViewportRenderer : private readonly PrivateEntityViewportRenderer _renderer; internal PaperdollViewportRenderer( - GL? gl, - IWorldPassScope? scope, + IWorldPassScope scope, AcDream.App.Rendering.Gpu.IGpuDevice device, ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, @@ -28,7 +26,6 @@ public sealed class PaperdollViewportRenderer : IWbMeshAdapter meshAdapter) { _renderer = new PrivateEntityViewportRenderer( - gl, scope, device, frames, diff --git a/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs b/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs index f7a35ef6..a1ff118e 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs @@ -185,12 +185,11 @@ public sealed unsafe partial class ParticleRenderer } /// - /// True when mesh particles can be submitted at all. The GL arm answers with - /// its second Shader, which is only built when a shared mesh arena - /// exists; the RHI arm answers with the pipelines the same condition builds. + /// True when mesh particles can be submitted at all. The GL arm's second + /// Shader answer was deleted at Campaign V slice V11; only the RHI + /// pipelines remain, built exactly when a shared mesh arena exists. /// - private bool MeshParticlesAvailable => - _glContext is null ? _meshAlphaPipeline is not null : _meshShader is not null; + private bool MeshParticlesAvailable => _meshAlphaPipeline is not null; private void CreateRhiResources(IGpuDevice device, int sampleCount) { diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs index 28c9f38d..a8cdc074 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -97,22 +97,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable } } - /// - /// The GL arm's context, or null on a backend that has none. - /// - /// Campaign V slice V6l: particles draw on both arms, so the context - /// became optional. below is what every GL-arm statement - /// still reads, unchanged — reaching it without a context is a composition - /// error and says so rather than dereferencing null. - /// - private readonly GL? _glContext; - private readonly Shader? _shader; - private readonly Shader? _meshShader; - - private GL _gl => _glContext - ?? throw new InvalidOperationException( - "ParticleRenderer's GL arm was reached on a backend with no GL context " - + "(campaign plan §5.5.16; the RHI arm lives in ParticleRenderer.Rhi.cs)."); private readonly TextureCache? _textures; private readonly IDatReaderWriter? _dats; private readonly WbMeshAdapter? _meshAdapter; @@ -129,70 +113,15 @@ public sealed unsafe partial class ParticleRenderer : IDisposable private bool _disposing; private bool _disposed; private readonly HashSet _meshLoadRequestedThisFrame = new(); - // Campaign V slice V6e: particle_mesh.frag's two loose uniforms were renamed - // onto members of the shared push-constant block (uTextureIndex → - // uTextureIndexA, uTextureLayer → uParamA), because Vulkan GLSL has no - // default uniform block to declare them in. Under GL they are still plain - // program uniforms set exactly as before; only the names moved. - private readonly int _meshTextureIndexLoc = -1; - private readonly int _meshTextureLayerLoc = -1; - - // Campaign V slice V4t (2026-07-28): the interim per-renderer - // GlBindlessHandleTable is retired. Both particle texture sources now hand - // out the device's own GpuTextureSlot — TextureCache.AcquireParticleTexture - // for billboards, ObjectRenderBatch.TextureSlot for mesh particles — so all - // that is left here is flushing and binding that one table before each - // raw-GL draw. There is still no automated pixel-gate coverage for - // particles (the offline gate's fixed outdoor view has none in frame), so - // this change is kept strictly mechanical, exactly as V2c's was. - private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable => - (_meshAdapter - ?? throw new InvalidOperationException( - "ParticleRenderer was constructed without a mesh adapter: its texture " + - "slots come from that adapter's GL device table (Campaign V slice V4t).")) - .WorldTextureTable; - - private uint _quadVao; - private readonly uint _quadVbo; - private readonly uint _quadEbo; - private uint _instanceVbo; - private uint _meshVao; - private uint _meshInstanceVbo; - private int _instanceVboCapacityBytes; - private int _meshInstanceVboCapacityBytes; - - private sealed class DynamicBufferSet - { - public uint BillboardVao; - public uint BillboardInstanceVbo; - public int BillboardCapacityBytes; - public uint MeshVao; - public uint MeshInstanceVbo; - public int MeshCapacityBytes; - } - - private readonly List[] _dynamicBufferSetsByFrame = - [[], [], []]; - private int _dynamicFrameSlot; - private int _dynamicBufferSetCursor; private bool _dynamicFrameStarted; - private DynamicBufferSet? _activeDynamicBufferSet; - internal (int SetCount, long CapacityBytes) DynamicBufferDiagnostics - { - get - { - int count = 0; - long bytes = 0; - foreach (List frameSets in _dynamicBufferSetsByFrame) - { - count += frameSets.Count; - foreach (DynamicBufferSet set in frameSets) - bytes += set.BillboardCapacityBytes + set.MeshCapacityBytes; - } - return (count, bytes); - } - } + /// + /// The GL arm's per-flight VAO/VBO pool this used to report on was deleted + /// at Campaign V slice V11: the RHI arm draws every particle instance from + /// a ring allocation that lives until its frame retires, so there is no + /// persistent dynamic-buffer pool left to size. + /// + internal (int SetCount, long CapacityBytes) DynamicBufferDiagnostics => (0, 0); private BillboardGpuInstance[] _instanceScratch = new BillboardGpuInstance[256]; private float[] _meshInstanceScratch = new float[256 * 20]; @@ -237,155 +166,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable => owner.ResetDeferredAlpha(); } - internal ParticleRenderer( - GL gl, - string shadersDir, - ParticleSystem particles, - TextureCache? textures = null, - IDatReaderWriter? dats = null, - WbMeshAdapter? meshAdapter = null, - RetailAlphaQueue? alphaQueue = null, - long? alphaScratchBudgetBytes = null) - { - _glContext = gl ?? throw new ArgumentNullException(nameof(gl)); - _textures = textures; - _dats = dats; - _meshAdapter = meshAdapter; - _particles = particles ?? throw new ArgumentNullException(nameof(particles)); - _alphaQueue = alphaQueue; - _alphaSource = new AlphaDrawSource(this); - long scratchBudget = alphaScratchBudgetBytes - ?? AlphaScratchBudgetProfile.Create( - ResidencyBudgetOptions.Default.AlphaScratchBytes).ParticleBytes; - _alphaScratchPolicy = - new RetainedScratchCapacityPolicy(scratchBudget); - if (_meshAdapter is not null) - { - _meshReferences = new ParticleMeshReferenceTracker( - gfxObjId => _meshAdapter.IncrementRefCount(gfxObjId), - gfxObjId => _meshAdapter.DecrementRefCount(gfxObjId)); - } - _emitterRetirements = new ParticleEmitterRetirementTracker( - handle => _meshReferences?.Release(handle), - handle => _particleGfxInfoByEmitter.Remove(handle), - handle => _textures?.ReleaseParticleTextureOwner(handle), - error => Console.Error.WriteLine($"[particles] {error}")); - var constructionResources = new ResourceCleanupGroup(); - try - { - _shader = new Shader(_gl, - System.IO.Path.Combine(shadersDir, "particle.vert"), - System.IO.Path.Combine(shadersDir, "particle.frag"), - includeCommonPreamble: true); - constructionResources.Add("particle shader", _shader.Dispose); - if (_meshAdapter?.MeshManager?.GlobalBuffer is not null) - { - _meshShader = new Shader(_gl, - System.IO.Path.Combine(shadersDir, "particle_mesh.vert"), - System.IO.Path.Combine(shadersDir, "particle_mesh.frag"), - includeCommonPreamble: true); - constructionResources.Add( - "particle mesh shader", - _meshShader.Dispose); - _meshTextureIndexLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureIndexA"); - _meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uParamA"); - } - - float[] quadVerts = - { - -0.5f, -0.5f, 0f, 0f, - 0.5f, -0.5f, 1f, 0f, - 0.5f, 0.5f, 1f, 1f, - -0.5f, 0.5f, 0f, 1f, - }; - uint[] quadIdx = { 0, 1, 2, 0, 2, 3 }; - - bool vboAllocated = false; - bool eboAllocated = false; - uint quadVbo = TrackedGlResource.CreateBuffer( - _gl, - "creating particle quad VBO"); - RetryableGpuResourceRelease quadVboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - quadVbo, - () => vboAllocated ? quadVerts.Length * sizeof(float) : 0, - "rolling back particle quad VBO"); - constructionResources.Add("particle quad VBO", quadVboRelease.Run); - fixed (void* p = quadVerts) - { - TrackedGlResource.AllocateBufferStorage( - _gl, - GLEnum.ArrayBuffer, - quadVbo, - 0, - quadVerts.Length * sizeof(float), - GLEnum.StaticDraw, - p, - "uploading particle quad VBO"); - } - vboAllocated = true; - - uint quadEbo = TrackedGlResource.CreateBuffer( - _gl, - "creating particle quad EBO"); - RetryableGpuResourceRelease quadEboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - quadEbo, - () => eboAllocated ? quadIdx.Length * sizeof(uint) : 0, - "rolling back particle quad EBO"); - constructionResources.Add("particle quad EBO", quadEboRelease.Run); - - uint uploadVao = TrackedGlResource.CreateVertexArray( - _gl, - "creating particle upload VAO"); - RetryableGpuResourceRelease uploadVaoRelease = - TrackedGlResource.CreateRetryableVertexArrayDeletion( - _gl, - uploadVao, - "rolling back particle upload VAO"); - constructionResources.Add("particle upload VAO", uploadVaoRelease.Run); - - GlResourceCommand.Execute( - _gl, - "bind particle upload VAO", - () => _gl.BindVertexArray(uploadVao)); - fixed (void* p = quadIdx) - { - TrackedGlResource.AllocateBufferStorage( - _gl, - GLEnum.ElementArrayBuffer, - quadEbo, - 0, - quadIdx.Length * sizeof(uint), - GLEnum.StaticDraw, - p, - "uploading particle quad EBO"); - } - eboAllocated = true; - GlResourceCommand.Execute(_gl, "finish particle static-buffer upload", () => - { - _gl.BindVertexArray(0); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); - }); - uploadVaoRelease.Run(); - - _quadVbo = quadVbo; - _quadEbo = quadEbo; - - _particles.EmitterDied += OnEmitterDied; - constructionResources.TransferAll(); - } - catch (Exception constructionFailure) - { - constructionResources.RollbackConstructionAndThrow( - "ParticleRenderer construction failed and its shader prefix did not cleanly roll back.", - constructionFailure); - throw new System.Diagnostics.UnreachableException(); - } - } - /// /// Starts one render frame. Wb point-of-use recovery is limited to one /// request per missing GfxObj even though portal slicing may invoke Draw @@ -393,13 +173,13 @@ public sealed unsafe partial class ParticleRenderer : IDisposable /// public void BeginFrame(int frameSlot) { - if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length) - throw new ArgumentOutOfRangeException(nameof(frameSlot)); + // The GL arm's per-flight VAO/VBO pool this used to index into was + // deleted at Campaign V slice V11; the RHI arm takes every instance + // from a ring allocation, so frameSlot is validated but otherwise + // unused here. + ArgumentOutOfRangeException.ThrowIfNegative(frameSlot); - _dynamicFrameSlot = frameSlot; - _dynamicBufferSetCursor = 0; _dynamicFrameStarted = true; - _activeDynamicBufferSet = null; _meshLoadRequestedThisFrame.Clear(); _emitterRetirements.RetryPending(); _textures?.TickParticleTextureCache(); @@ -498,176 +278,14 @@ public sealed unsafe partial class ParticleRenderer : IDisposable private void DrawOrdered(ICamera camera) { - if (_glContext is null) - { - DrawOrderedRhi(camera); - return; - } - - ParticleSubmissionOrdering.Sort(_submissionScratch); - GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer; - Matrix4x4 viewProjection = camera.View * camera.Projection; - - _gl.Enable(EnableCap.DepthTest); - _gl.Enable(EnableCap.Blend); - _gl.DepthMask(false); - - for (int i = 0; i < _submissionScratch.Count;) - { - ParticleSubmission submission = _submissionScratch[i]; - if (submission.Kind == ParticleSubmissionKind.Billboard) - { - ParticleDraw draw = _drawListScratch[submission.DrawIndex]; - BatchKey key = draw.Key; - _runScratch.Clear(); - do - { - _runScratch.Add(_drawListScratch[submission.DrawIndex].Instance); - i++; - if (i >= _submissionScratch.Count) - break; - submission = _submissionScratch[i]; - } - while (submission.Kind == ParticleSubmissionKind.Billboard - && _drawListScratch[submission.DrawIndex].Key == key); - - _gl.BlendFunc( - BlendingFactor.SrcAlpha, - key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha); - DrawInstances(_runScratch, viewProjection); - continue; - } - - if (_meshShader is null || global is null) - { - i++; - continue; - } - - MeshParticleDraw meshDraw = _meshDrawListScratch[submission.DrawIndex]; - MeshBatchKey meshKey = meshDraw.Key; - ObjectRenderBatch batch = meshDraw.Batch; - _meshRunScratch.Clear(); - do - { - _meshRunScratch.Add(_meshDrawListScratch[submission.DrawIndex].Instance); - i++; - if (i >= _submissionScratch.Count) - break; - submission = _submissionScratch[i]; - } - while (submission.Kind == ParticleSubmissionKind.Mesh - && _meshDrawListScratch[submission.DrawIndex].Key == meshKey); - - ApplyMeshCullMode(batch.CullMode); - TranslucencyKind blend = ResolveMeshBlend(batch); - _gl.BlendFunc( - blend == TranslucencyKind.InvAlpha - ? BlendingFactor.OneMinusSrcAlpha - : BlendingFactor.SrcAlpha, - blend switch - { - TranslucencyKind.Additive => BlendingFactor.One, - TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha, - _ => BlendingFactor.OneMinusSrcAlpha, - }); - - _gl.ProgramUniform1( - _meshShader.Program, - _meshTextureIndexLoc, - batch.TextureSlot.Index); - // Slice V6e: uParamA is a float, so the layer is widened here rather - // than in the shader's float(uTextureLayer). Layers are small - // integers; the sampled value is bit-identical. - _gl.ProgramUniform1(_meshShader.Program, _meshTextureLayerLoc, (float)batch.TextureIndex); - - UploadMeshInstances(_meshRunScratch); - PrepareMeshPipeline(viewProjection, global); - FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9) - _gl.DrawElementsInstancedBaseVertex( - PrimitiveType.Triangles, - (uint)batch.IndexCount, - DrawElementsType.UnsignedShort, - (void*)(batch.FirstIndex * sizeof(ushort)), - (uint)_meshRunScratch.Count, - (int)batch.BaseVertex); - } - - _gl.BindVertexArray(0); - _gl.DepthMask(true); - _gl.Disable(EnableCap.Blend); - _gl.Disable(EnableCap.CullFace); + DrawOrderedRhi(camera); } private void PrepareDeferredAlphaDraws(ReadOnlySpan tokens) { if (tokens.Length == 0) return; - - if (_glContext is null) - { - PrepareDeferredAlphaDrawsRhi(tokens); - return; - } - - ActivateNextDynamicBufferSet(); - - int count = tokens.Length; - if (_preparedAlpha.Length < count) - Array.Resize(ref _preparedAlpha, count + 256); - if (_preparedInstanceOffsets.Length < count) - Array.Resize(ref _preparedInstanceOffsets, count + 256); - if (_instanceScratch.Length < count) - Array.Resize(ref _instanceScratch, count + 256); - int neededMeshFloats = count * 20; - if (_meshInstanceScratch.Length < neededMeshFloats) - Array.Resize(ref _meshInstanceScratch, neededMeshFloats + 256 * 20); - - int billboardCount = 0; - int meshCount = 0; - for (int i = 0; i < count; i++) - { - DeferredParticleDraw deferred = _deferredAlpha[tokens[i]]; - _preparedAlpha[i] = deferred; - if (deferred.Kind == ParticleSubmissionKind.Billboard) - { - _preparedInstanceOffsets[i] = (uint)billboardCount; - WriteBillboardGpuInstance( - ref _instanceScratch[billboardCount++], - deferred.Billboard.Instance); - } - else - { - _preparedInstanceOffsets[i] = (uint)meshCount; - WriteMeshGpuInstance( - _meshInstanceScratch, - meshCount++ * 20, - deferred.Mesh.Instance); - } - } - - if (billboardCount > 0) - { - fixed (void* bp = _instanceScratch) - UploadDynamicArrayBuffer( - _instanceVbo, - ref _instanceVboCapacityBytes, - bp, - billboardCount * sizeof(BillboardGpuInstance)); - } - - if (meshCount > 0) - { - fixed (void* mp = _meshInstanceScratch) - UploadDynamicArrayBuffer( - _meshInstanceVbo, - ref _meshInstanceVboCapacityBytes, - mp, - meshCount * 20 * sizeof(float)); - } - - PersistActiveDynamicBufferCapacities(); - _preparedAlphaCount = count; + PrepareDeferredAlphaDrawsRhi(tokens); } private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) @@ -677,133 +295,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable if (firstPreparedDraw < 0 || firstPreparedDraw > _preparedAlphaCount - drawCount) throw new ArgumentOutOfRangeException(nameof(firstPreparedDraw)); - - if (_glContext is null) - { - DrawPreparedAlphaBatchRhi(firstPreparedDraw, drawCount); - return; - } - - GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer; - _gl.Enable(EnableCap.DepthTest); - _gl.Enable(EnableCap.Blend); - _gl.DepthMask(false); - - ParticleSubmissionKind? activeKind = null; - Matrix4x4 activeViewProjection = default; - int i = firstPreparedDraw; - int preparedEnd = firstPreparedDraw + drawCount; - while (i < preparedEnd) - { - DeferredParticleDraw deferred = _preparedAlpha[i]; - if (deferred.Kind == ParticleSubmissionKind.Billboard) - { - ParticleDraw draw = deferred.Billboard; - BatchKey key = draw.Key; - if (activeKind != ParticleSubmissionKind.Billboard - || activeViewProjection != deferred.ViewProjection) - { - PrepareBillboardPipeline(deferred.ViewProjection); - activeKind = ParticleSubmissionKind.Billboard; - activeViewProjection = deferred.ViewProjection; - } - - uint baseInstance = _preparedInstanceOffsets[i]; - int runStart = i; - do - { - i++; - if (i >= preparedEnd) - break; - deferred = _preparedAlpha[i]; - } - while (deferred.Kind == ParticleSubmissionKind.Billboard - && deferred.Billboard.Key == key - && deferred.ViewProjection == activeViewProjection); - - _gl.BlendFunc( - BlendingFactor.SrcAlpha, - key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha); - _gl.BindVertexArray(_quadVao); - FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9) - _gl.DrawElementsInstancedBaseInstance( - PrimitiveType.Triangles, - 6, - DrawElementsType.UnsignedInt, - (void*)0, - (uint)(i - runStart), - baseInstance); - continue; - } - - if (_meshShader is null || global is null) - { - i++; - continue; - } - - MeshParticleDraw meshDraw = deferred.Mesh; - MeshBatchKey meshKey = meshDraw.Key; - ObjectRenderBatch batch = meshDraw.Batch; - if (activeKind != ParticleSubmissionKind.Mesh - || activeViewProjection != deferred.ViewProjection) - { - PrepareMeshPipeline(deferred.ViewProjection, global); - activeKind = ParticleSubmissionKind.Mesh; - activeViewProjection = deferred.ViewProjection; - } - - uint meshBaseInstance = _preparedInstanceOffsets[i]; - int meshRunStart = i; - do - { - i++; - if (i >= preparedEnd) - break; - deferred = _preparedAlpha[i]; - } - while (deferred.Kind == ParticleSubmissionKind.Mesh - && deferred.Mesh.Key == meshKey - && deferred.ViewProjection == activeViewProjection); - - ApplyMeshCullMode(batch.CullMode); - TranslucencyKind blend = ResolveMeshBlend(batch); - _gl.BlendFunc( - blend == TranslucencyKind.InvAlpha - ? BlendingFactor.OneMinusSrcAlpha - : BlendingFactor.SrcAlpha, - blend switch - { - TranslucencyKind.Additive => BlendingFactor.One, - TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha, - _ => BlendingFactor.OneMinusSrcAlpha, - }); - - _gl.ProgramUniform1( - _meshShader.Program, - _meshTextureIndexLoc, - batch.TextureSlot.Index); - // Slice V6e: uParamA is a float, so the layer is widened here rather - // than in the shader's float(uTextureLayer). Layers are small - // integers; the sampled value is bit-identical. - _gl.ProgramUniform1(_meshShader.Program, _meshTextureLayerLoc, (float)batch.TextureIndex); - - _gl.BindVertexArray(_meshVao); - FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9) - _gl.DrawElementsInstancedBaseVertexBaseInstance( - PrimitiveType.Triangles, - (uint)batch.IndexCount, - DrawElementsType.UnsignedShort, - (void*)(batch.FirstIndex * sizeof(ushort)), - (uint)(i - meshRunStart), - (int)batch.BaseVertex, - meshBaseInstance); - } - - _gl.BindVertexArray(0); - _gl.DepthMask(true); - _gl.Disable(EnableCap.Blend); - _gl.Disable(EnableCap.CullFace); + DrawPreparedAlphaBatchRhi(firstPreparedDraw, drawCount); } private void ResetDeferredAlpha() @@ -834,54 +326,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable Array.Resize(ref _preparedInstanceOffsets, targetCapacity); } - private void PrepareBillboardPipeline(Matrix4x4 viewProjection) - { - _shader!.Use(); - _shader.SetMatrix4("uViewProjection", viewProjection); - _gl.Disable(EnableCap.CullFace); - _gl.BindVertexArray(_quadVao); - } - - private void PrepareMeshPipeline(Matrix4x4 viewProjection, GlobalMeshBuffer global) - { - _meshShader!.Use(); - _meshShader.SetMatrix4("uViewProjection", viewProjection); - _gl.FrontFace(FrontFaceDirection.CW); - _gl.BindVertexArray(_meshVao); - - // GlobalMeshBuffer may grow and replace either backing buffer. Bind - // today's buffer names on every pipeline switch instead of caching - // them in this VAO at construction time. - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, global.VBO); - int vertexStride = VertexPositionNormalTexture.Size; - _gl.EnableVertexAttribArray(0); - _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)0); - _gl.EnableVertexAttribArray(1); - _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(3 * sizeof(float))); - _gl.EnableVertexAttribArray(2); - _gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(6 * sizeof(float))); - _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, global.IBO); - - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _meshInstanceVbo); - const int instanceStride = 20 * sizeof(float); - for (uint column = 0; column < 4; column++) - { - uint location = 3 + column; - _gl.EnableVertexAttribArray(location); - _gl.VertexAttribPointer( - location, - 4, - VertexAttribPointerType.Float, - false, - instanceStride, - (void*)(column * 4 * sizeof(float))); - _gl.VertexAttribDivisor(location, 1); - } - _gl.EnableVertexAttribArray(7); - _gl.VertexAttribPointer(7, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(16 * sizeof(float))); - _gl.VertexAttribDivisor(7, 1); - } - private void BuildDrawLists( Vector3 cameraWorldPos, ParticleRenderPass renderPass, @@ -1042,51 +486,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable return true; } - private void DrawInstances(List instances, Matrix4x4 viewProjection) - { - if (instances.Count == 0) - return; - - ActivateNextDynamicBufferSet(); - - if (_instanceScratch.Length < instances.Count) - _instanceScratch = new BillboardGpuInstance[instances.Count + 256]; - - for (int i = 0; i < instances.Count; i++) - WriteBillboardGpuInstance(ref _instanceScratch[i], instances[i]); - - fixed (void* bp = _instanceScratch) - UploadDynamicArrayBuffer( - _instanceVbo, - ref _instanceVboCapacityBytes, - bp, - instances.Count * sizeof(BillboardGpuInstance)); - - PersistActiveDynamicBufferCapacities(); - PrepareBillboardPipeline(viewProjection); - FlushAndBindTextureTable(); // Campaign V slice V2c (binding=9) - _gl.DrawElementsInstanced(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedInt, (void*)0, (uint)instances.Count); - } - - private void UploadMeshInstances(List instances) - { - ActivateNextDynamicBufferSet(); - int needed = instances.Count * 20; - if (_meshInstanceScratch.Length < needed) - _meshInstanceScratch = new float[needed + 256 * 20]; - - for (int i = 0; i < instances.Count; i++) - WriteMeshGpuInstance(_meshInstanceScratch, i * 20, instances[i]); - - fixed (void* bp = _meshInstanceScratch) - UploadDynamicArrayBuffer( - _meshInstanceVbo, - ref _meshInstanceVboCapacityBytes, - bp, - instances.Count * 20 * sizeof(float)); - PersistActiveDynamicBufferCapacities(); - } - /// /// Campaign V slice V6e: the shader-side spelling of "this particle has no /// texture, draw the procedural blob". It must agree with @@ -1156,192 +555,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable destination[offset + 19] = ((instance.ColorArgb >> 24) & 0xFF) / 255f; } - private void ActivateNextDynamicBufferSet() - { - if (!_dynamicFrameStarted) - throw new InvalidOperationException("BeginFrame must be called before drawing particles."); - - List slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot]; - if (_dynamicBufferSetCursor == slotSets.Count) - slotSets.Add(CreateDynamicBufferSet()); - - DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++]; - _activeDynamicBufferSet = set; - _quadVao = set.BillboardVao; - _instanceVbo = set.BillboardInstanceVbo; - _instanceVboCapacityBytes = set.BillboardCapacityBytes; - _meshVao = set.MeshVao; - _meshInstanceVbo = set.MeshInstanceVbo; - _meshInstanceVboCapacityBytes = set.MeshCapacityBytes; - } - - private DynamicBufferSet CreateDynamicBufferSet() - { - var set = new DynamicBufferSet(); - try - { - set.BillboardVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle billboard VAO"); - set.BillboardInstanceVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle billboard instance VBO"); - set.MeshVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle mesh VAO"); - set.MeshInstanceVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle mesh instance VBO"); - - _gl.BindVertexArray(set.BillboardVao); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _quadVbo); - _gl.EnableVertexAttribArray(0); - _gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)0); - _gl.EnableVertexAttribArray(1); - _gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)(2 * sizeof(float))); - _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _quadEbo); - - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.BillboardInstanceVbo); - uint instanceStride = (uint)sizeof(BillboardGpuInstance); - _gl.EnableVertexAttribArray(2); - _gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)0); - _gl.VertexAttribDivisor(2, 1); - _gl.EnableVertexAttribArray(3); - _gl.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(4 * sizeof(float))); - _gl.VertexAttribDivisor(3, 1); - _gl.EnableVertexAttribArray(4); - _gl.VertexAttribPointer(4, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(8 * sizeof(float))); - _gl.VertexAttribDivisor(4, 1); - _gl.EnableVertexAttribArray(5); - _gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(12 * sizeof(float))); - _gl.VertexAttribDivisor(5, 1); - // Campaign V slice V2c: one uint table slot (was uvec2 low/high - // handle halves) — BillboardGpuInstance shrank by 4 bytes. - _gl.EnableVertexAttribArray(6); - _gl.VertexAttribIPointer( - 6, - 1, - VertexAttribIType.UnsignedInt, - instanceStride, - (void*)(16 * sizeof(float))); - _gl.VertexAttribDivisor(6, 1); - - _gl.BindVertexArray(0); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); - GLHelpers.ThrowOnResourceError(_gl, "configuring particle dynamic VAOs"); - return set; - } - catch (Exception creationFailure) - { - try { DeleteDynamicBufferSet(set); } - catch (Exception cleanupFailure) - { - throw new AggregateException( - "Particle dynamic-buffer creation and rollback failed.", - creationFailure, - cleanupFailure); - } - throw; - } - } - - private void DeleteDynamicBufferSet(DynamicBufferSet set) - { - List? failures = null; - void Attempt(Action action) - { - try { action(); } - catch (Exception ex) { (failures ??= []).Add(ex); } - } - - Attempt(() => TrackedGlResource.DeleteBuffer( - _gl, - set.BillboardInstanceVbo, - set.BillboardCapacityBytes, - "deleting particle billboard instance VBO")); - Attempt(() => TrackedGlResource.DeleteBuffer( - _gl, - set.MeshInstanceVbo, - set.MeshCapacityBytes, - "deleting particle mesh instance VBO")); - Attempt(() => TrackedGlResource.DeleteVertexArray( - _gl, - set.BillboardVao, - "deleting particle billboard VAO")); - Attempt(() => TrackedGlResource.DeleteVertexArray( - _gl, - set.MeshVao, - "deleting particle mesh VAO")); - if (failures is not null) - throw new AggregateException("One or more particle dynamic resources failed to delete.", failures); - } - - private void PersistActiveDynamicBufferCapacities() - { - DynamicBufferSet set = _activeDynamicBufferSet - ?? throw new InvalidOperationException("No dynamic particle buffer set is active."); - set.BillboardCapacityBytes = _instanceVboCapacityBytes; - set.MeshCapacityBytes = _meshInstanceVboCapacityBytes; - } - - private void UploadDynamicArrayBuffer( - uint buffer, - ref int capacityBytes, - void* data, - int byteCount) - { - if (byteCount <= 0) - throw new ArgumentOutOfRangeException(nameof(byteCount)); - - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, buffer); - if (capacityBytes < byteCount) - { - int grownCapacity = DynamicBufferCapacity.Grow(capacityBytes, byteCount); - TrackedGlResource.AllocateBufferStorage( - _gl, - GLEnum.ArrayBuffer, - buffer, - capacityBytes, - grownCapacity, - GLEnum.DynamicDraw, - $"growing particle dynamic buffer {buffer} to {grownCapacity} bytes"); - capacityBytes = grownCapacity; - } - - _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)byteCount, data); - } - - /// - /// Campaign V slice V4t: drains the device texture table's dirty runs and - /// (re)binds it at - /// . - /// Still called immediately before every draw call rather than once per - /// pipeline switch, for the reason V2c gave: a run of consecutive - /// mesh-particle sub-batches can pull in a texture whose slot was registered - /// this frame, and the table must be current for each one. - /// - private void FlushAndBindTextureTable() - { - AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable; - device.FlushTextureTable(); - _gl.BindBufferBase( - BufferTargetARB.ShaderStorageBuffer, - AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable, - device.TextureTableGlName); - } - - private void ApplyMeshCullMode(CullMode mode) - { - _gl.FrontFace(FrontFaceDirection.CW); - 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; - } - } - private TranslucencyKind ResolveMeshBlend(ObjectRenderBatch batch) { uint surfaceId = batch.Key.SurfaceId; @@ -1652,38 +865,12 @@ public sealed unsafe partial class ParticleRenderer : IDisposable if (_meshReferences is not null) releases.Add(("mesh-references", _meshReferences.Dispose)); - // Campaign V slice V6l: the RHI arm owns pipelines and two static quad - // buffers and no GL names at all, so it releases through the same - // retryable ledger and then there is nothing else to do. - if (_glContext is null) - { - releases.Add(("rhi-resources", DisposeRhiResources)); - return; - } - - AddTrackedBufferRelease( - releases, - _quadVbo, - 16L * sizeof(float), - "quad-vbo", - "deleting particle quad VBO"); - AddTrackedBufferRelease( - releases, - _quadEbo, - 6L * sizeof(uint), - "quad-ebo", - "deleting particle quad EBO"); - for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++) - { - List frameSets = _dynamicBufferSetsByFrame[frame]; - for (int index = 0; index < frameSets.Count; index++) - AddDynamicBufferSetReleases(releases, frameSets[index], frame, index); - } - - if (_shader is not null) - releases.Add(("billboard-shader", _shader.Dispose)); - if (_meshShader is not null) - releases.Add(("mesh-shader", _meshShader.Dispose)); + // The RHI arm owns pipelines and two static quad buffers and no GL + // names at all, so it releases through the same retryable ledger and + // then there is nothing else to do. The raw-GL release path (tracked + // quad/dynamic-buffer/shader deletions) was deleted at Campaign V + // slice V11. + releases.Add(("rhi-resources", DisposeRhiResources)); } private void RetireEveryResolvedEmitter() @@ -1694,82 +881,9 @@ public sealed unsafe partial class ParticleRenderer : IDisposable _emitterRetirements.CompleteOrThrow(); } - private void AddDynamicBufferSetReleases( - List<(string Name, Action Release)> releases, - DynamicBufferSet set, - int frame, - int index) - { - AddTrackedBufferRelease( - releases, - set.BillboardInstanceVbo, - set.BillboardCapacityBytes, - $"dynamic-{frame}-{index}-billboard-vbo", - "deleting particle billboard instance VBO"); - AddTrackedBufferRelease( - releases, - set.MeshInstanceVbo, - set.MeshCapacityBytes, - $"dynamic-{frame}-{index}-mesh-vbo", - "deleting particle mesh instance VBO"); - AddTrackedVertexArrayRelease( - releases, - set.BillboardVao, - $"dynamic-{frame}-{index}-billboard-vao", - "deleting particle billboard VAO"); - AddTrackedVertexArrayRelease( - releases, - set.MeshVao, - $"dynamic-{frame}-{index}-mesh-vao", - "deleting particle mesh VAO"); - } - - 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)); - } - - private void AddTrackedVertexArrayRelease( - List<(string Name, Action Release)> releases, - uint vertexArray, - string name, - string context) - { - if (vertexArray == 0) - return; - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableVertexArrayDeletion( - _gl, - vertexArray, - context); - releases.Add((name, release.Run)); - } - private void CompleteDispose() { - foreach (List frameSets in _dynamicBufferSetsByFrame) - frameSets.Clear(); - _activeDynamicBufferSet = null; _dynamicFrameStarted = false; - _quadVao = 0; - _instanceVbo = 0; - _meshVao = 0; - _meshInstanceVbo = 0; - _instanceVboCapacityBytes = 0; - _meshInstanceVboCapacityBytes = 0; _particleGfxInfoByEmitter.Clear(); _particleGfxInfoByGfxObj.Clear(); _geometryKindByGfxObj.Clear(); diff --git a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs index 7f3e0889..a20f286f 100644 --- a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs +++ b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs @@ -1,8 +1,5 @@ using System; -using System.Linq; using System.Numerics; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -42,196 +39,42 @@ namespace AcDream.App.Rendering; /// depth write lands only inside the slice region, matching retail's clipped /// fan. /// -/// Self-contained GL state (feedback_render_self_contained_gl_state): -/// sets everything it depends on, restores the frame-global convention on -/// exit, no early-outs between set and restore. +/// The GL arm's inline program (rehomed clip planes, raw uniform +/// locations) and this class's own dynamic VAO/VBO ring were deleted at +/// Campaign V slice V11; the RHI arm compiles +/// Rendering/Shaders/portal_depth.{vert,frag} from committed SPIR-V, +/// and its per-frame fan vertices come from the frame ring instead. /// public sealed partial class PortalDepthMaskRenderer : IDisposable { - /// - /// The GL arm's inline program. Campaign V slice V6l added a SECOND arm that - /// compiles Rendering/Shaders/portal_depth.{vert,frag} — the same body, - /// with its loose uniforms rehomed onto the shared push block and the clip - /// planes onto the TerrainClip UBO — because a Vulkan pipeline needs a - /// named, SPIR-V-compiled pair. This string stays because the GL arm keeps - /// its raw path through to V10 (plan §5.5.6) and because rehoming its clip - /// planes onto UBO binding 2 would clobber the terrain clip block that - /// ClipFrame binds there globally on GL. The two are kept in step by - /// PortalDepthShaderParityTests, and this one is deleted at V11. - /// - internal const string VertSrc = @"#version 430 core -layout(location = 0) in vec3 aPos; -uniform mat4 uViewProjection; -uniform int uPlaneCount; -uniform vec4 uPlanes[8]; -uniform int uForceFarZ; -uniform float uDepthBias; // NDC bias toward the viewer (mark pass only) -uniform float uDepthBiasEyeCapN; // eye-span cap x near plane (#129; see MarkBiasNdc) -out float gl_ClipDistance[8]; -void main() -{ - vec4 clipPos = uViewProjection * vec4(aPos, 1.0); - for (int i = 0; i < 8; i++) - gl_ClipDistance[i] = (i < uPlaneCount) ? dot(uPlanes[i], clipPos) : 1.0; - if (uForceFarZ == 1) - clipPos.z = clipPos.w * 0.99999988; // retail far-z punch constant (0x0059bc90 tail) - else if (uDepthBias > 0.0) - { - // #117 mark-pass bias, #129 eye-space cap. clipPos.w = eye depth d; - // an NDC bias b spans ~b*d*d/near meters of eye depth, so the - // constant-NDC form alone reached METERS at distance (door-shaped - // leaks through hills/houses). Keep in sync with MarkBiasNdc. - float biasNdc = min(uDepthBias, uDepthBiasEyeCapN / max(clipPos.w * clipPos.w, 1e-6)); - clipPos.z -= biasNdc * clipPos.w; - } - gl_Position = clipPos; -}"; - - internal const string FragSrc = @"#version 430 core -void main() { } // depth-only: color writes are masked off by the caller state -"; - - private readonly GL? _glContext; - private GL _gl => _glContext - ?? throw new InvalidOperationException( - "PortalDepthMaskRenderer's GL arm was reached on a backend with no GL context " - + "(campaign plan slice V6l; the RHI arm lives in PortalDepthMaskRenderer.Rhi.cs)."); - - private readonly uint _program; - private readonly int _locViewProjection; - private readonly int _locPlaneCount; - private readonly int _locPlanes; - private readonly int _locForceFarZ; - private readonly int _locDepthBias; - private readonly int _locDepthBiasEyeCapN; private readonly ResourceCleanupGroup _resources; + /// Shared by both arms: the largest fan accepts. private const int MaxFanVerts = 32; - private readonly float[] _scratch = new float[MaxFanVerts * 3]; - private sealed class FrameBufferSet - { - public uint Vao; - public uint Vbo; - public int CapacityBytes; - public int UsedVertices; - } - - private readonly FrameBufferSet?[] _frameBuffers = new FrameBufferSet[3]; - private FrameBufferSet? _activeFrameBuffer; - - internal long DynamicBufferCapacityBytes => - _frameBuffers.Sum(set => (long)(set?.CapacityBytes ?? 0)); - - public PortalDepthMaskRenderer(GL gl) - { - _glContext = gl ?? throw new ArgumentNullException(nameof(gl)); - var resources = new ResourceCleanupGroup(); - try - { - _program = ShaderProgramConstruction.Build( - new GlShaderProgramBuildApi(gl), - VertSrc, - FragSrc); - resources.Add( - "portal-depth program", - () => GlResourceCommand.DeleteProgram( - gl, - _program, - $"delete PortalDepthMask program {_program}")); - - ( - _locViewProjection, - _locPlaneCount, - _locPlanes, - _locForceFarZ, - _locDepthBias, - _locDepthBiasEyeCapN) = GlResourceCommand.Execute( - _gl, - "resolve PortalDepthMask uniforms", - () => ( - _gl.GetUniformLocation(_program, "uViewProjection"), - _gl.GetUniformLocation(_program, "uPlaneCount"), - _gl.GetUniformLocation(_program, "uPlanes"), - _gl.GetUniformLocation(_program, "uForceFarZ"), - _gl.GetUniformLocation(_program, "uDepthBias"), - _gl.GetUniformLocation(_program, "uDepthBiasEyeCapN"))); - - for (int i = 0; i < _frameBuffers.Length; i++) - { - FrameBufferSet set = CreateFrameBufferSet(resources, i); - _frameBuffers[i] = set; - } - } - catch (Exception constructionFailure) - { - resources.RollbackConstructionAndThrow( - "PortalDepthMaskRenderer construction failed and its GL prefix did not cleanly roll back.", - constructionFailure); - throw new System.Diagnostics.UnreachableException(); - } - - _resources = resources; - } + /// + /// The GL arm's per-flight VAO/VBO ring this used to report on was deleted + /// at Campaign V slice V11: the RHI arm draws every fan from a ring + /// allocation that lives until its frame retires, so there is no + /// persistent dynamic-buffer pool left to size. + /// + internal long DynamicBufferCapacityBytes => 0; /// /// Selects the GPU-fenced frame slot and resets its append cursor. Portal /// fans written during one frame occupy distinct ranges, so later portal /// slices never overwrite vertices still referenced by earlier draws. + /// + /// The GL arm's per-flight VAO/VBO ring this used to activate was + /// deleted at Campaign V slice V11 — the RHI arm owns no per-flight ring + /// at all, because every frame ring allocation is already distinct memory + /// that lives until the frame retires — so all this edge carries now is + /// the started latch. /// public void BeginFrame(int frameSlot) { - if ((uint)frameSlot >= (uint)_frameBuffers.Length) - throw new ArgumentOutOfRangeException(nameof(frameSlot)); - // Slice V6l: the RHI arm owns no per-flight VBO ring — every frame ring - // allocation is already distinct memory that lives until the frame - // retires — so all this edge carries there is the started latch. + ArgumentOutOfRangeException.ThrowIfNegative(frameSlot); _rhiFrameStarted = true; - if (_glContext is null) - return; - _activeFrameBuffer = _frameBuffers[frameSlot]; - _activeFrameBuffer!.UsedVertices = 0; - } - - private FrameBufferSet CreateFrameBufferSet( - ResourceCleanupGroup resources, - int frameIndex) - { - uint vao = TrackedGlResource.CreateVertexArray( - _gl, - "PortalDepthMask frame VAO creation"); - RetryableGpuResourceRelease vaoRelease = - TrackedGlResource.CreateRetryableVertexArrayDeletion( - _gl, - vao, - "PortalDepthMask frame VAO disposal"); - resources.Add($"portal-depth frame {frameIndex} VAO", vaoRelease.Run); - - uint vbo = TrackedGlResource.CreateBuffer( - _gl, - "PortalDepthMask frame VBO creation"); - var set = new FrameBufferSet { Vao = vao, Vbo = vbo }; - RetryableGpuResourceRelease vboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - vbo, - () => set.CapacityBytes, - "PortalDepthMask frame VBO disposal"); - resources.Add($"portal-depth frame {frameIndex} VBO", vboRelease.Run); - - GlResourceCommand.Execute(_gl, "configure PortalDepthMask frame buffers", () => - { - _gl.BindVertexArray(set.Vao); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo); - unsafe - { - _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), (void*)0); - } - _gl.EnableVertexAttribArray(0); - _gl.BindVertexArray(0); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); - }); - return set; } /// @@ -303,143 +146,8 @@ void main() { } // depth-only: color writes are masked off by the caller state { if (worldVerts.Length < 3) return; - if (_glContext is null) - { - DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ); - return; - } - - FrameBufferSet frameBuffer = _activeFrameBuffer - ?? throw new InvalidOperationException("BeginFrame must be called before drawing portal depth masks."); - int n = Math.Min(worldVerts.Length, MaxFanVerts); - int planeCount = Math.Min(planes.Length, 8); - int firstVertex = frameBuffer.UsedVertices; - int requiredBytes = checked((firstVertex + n) * 3 * sizeof(float)); - - for (int i = 0; i < n; i++) - { - _scratch[i * 3 + 0] = worldVerts[i].X; - _scratch[i * 3 + 1] = worldVerts[i].Y; - _scratch[i * 3 + 2] = worldVerts[i].Z; - } - - // ---- set state (everything this draw depends on) ---- - _gl.UseProgram(_program); - _gl.Disable(EnableCap.Blend); - _gl.Disable(EnableCap.CullFace); // portal fans face either way - _gl.Disable(EnableCap.ScissorTest); - _gl.Enable(EnableCap.DepthTest); - _gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color - for (int i = 0; i < planeCount; i++) - _gl.Enable(EnableCap.ClipDistance0 + i); - - unsafe - { - var m = viewProjection; - _gl.UniformMatrix4(_locViewProjection, 1, false, (float*)&m); - _gl.Uniform1(_locPlaneCount, planeCount); - if (planeCount > 0) - { - Span p = stackalloc float[planeCount * 4]; - for (int i = 0; i < planeCount; i++) - { - p[i * 4 + 0] = planes[i].X; - p[i * 4 + 1] = planes[i].Y; - p[i * 4 + 2] = planes[i].Z; - p[i * 4 + 3] = planes[i].W; - } - fixed (float* pp = p) - _gl.Uniform4(_locPlanes, (uint)planeCount, pp); - } - - _gl.BindVertexArray(frameBuffer.Vao); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, frameBuffer.Vbo); - if (frameBuffer.CapacityBytes < requiredBytes) - { - int newCapacity = DynamicBufferCapacity.Grow( - frameBuffer.CapacityBytes, - requiredBytes); - TrackedGlResource.AllocateBufferStorage( - _gl, - GLEnum.ArrayBuffer, - frameBuffer.Vbo, - frameBuffer.CapacityBytes, - newCapacity, - GLEnum.DynamicDraw, - "PortalDepthMask frame VBO growth"); - frameBuffer.CapacityBytes = newCapacity; - } - fixed (float* v = _scratch) - _gl.BufferSubData( - BufferTargetARB.ArrayBuffer, - (nint)(firstVertex * 3 * sizeof(float)), - (nuint)(n * 3 * sizeof(float)), - v); - frameBuffer.UsedVertices += n; - - if (!forceFarZ) - { - // ── SEAL: retail-verbatim single pass ── - _gl.DepthFunc(DepthFunction.Always); - _gl.DepthMask(true); - _gl.Uniform1(_locForceFarZ, 0); - _gl.Uniform1(_locDepthBias, 0f); - _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n); - } - else - { - // ── PUNCH pass A: stencil-mark visible aperture pixels ── - _gl.Enable(EnableCap.StencilTest); - _gl.StencilFunc(StencilFunction.Always, 1, 0xFF); - _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); - _gl.StencilMask(0xFF); - _gl.DepthFunc(DepthFunction.Lequal); - _gl.DepthMask(false); - _gl.Uniform1(_locForceFarZ, 0); - _gl.Uniform1(_locDepthBias, PunchMarkDepthBias); - _gl.Uniform1(_locDepthBiasEyeCapN, - PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters); - _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n); - - // ── PUNCH pass B: far-Z write on marked pixels only; - // zero the stencil as we go (self-cleaning) ── - _gl.StencilFunc(StencilFunction.Equal, 1, 0xFF); - _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero); - _gl.DepthFunc(DepthFunction.Always); - _gl.DepthMask(true); - _gl.Uniform1(_locForceFarZ, 1); - _gl.Uniform1(_locDepthBias, 0f); - _gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (uint)n); - - _gl.Disable(EnableCap.StencilTest); - } - - _gl.BindVertexArray(0); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); - } - - // ---- restore the frame-global convention ---- - for (int i = 0; i < planeCount; i++) - _gl.Disable(EnableCap.ClipDistance0 + i); - _gl.ColorMask(true, true, true, true); - _gl.DepthMask(true); - _gl.DepthFunc(DepthFunction.Less); - // Renderers opt into culling locally; the shared frame convention is - // cull-off. Keep this success exit identical to frame-abort recovery. - _gl.Disable(EnableCap.CullFace); - _gl.CullFace(TriangleFace.Back); - _gl.FrontFace(FrontFaceDirection.CW); - _gl.UseProgram(0); + DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ); } - public void Dispose() - { - if (_glContext is null) - { - DisposeRhiResources(); - return; - } - - _resources.RetryCleanup(); - } + public void Dispose() => DisposeRhiResources(); } diff --git a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs index 9ded8cd7..28d43065 100644 --- a/src/AcDream.App/Rendering/PortalTunnelPresentation.cs +++ b/src/AcDream.App/Rendering/PortalTunnelPresentation.cs @@ -12,7 +12,6 @@ using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -24,14 +23,12 @@ namespace AcDream.App.Rendering; /// retained gameplay UI. /// /// Campaign V slice V6m. This was the last raw-GL world-adjacent -/// renderer; it now draws on both arms. Nothing about the scene changed — the -/// same synthetic Setup, the same 40 fps sequence, the same rotation and the -/// same distant light, through the same (already dual-arm) -/// . What forked is where the draw is recorded: -/// GL sets ambient capability state under a and draws -/// into whatever framebuffer is bound, while the RHI arm opens a pass of its own -/// against the backbuffer and publishes it on for -/// the span of the draw, exactly as the two offscreen viewports do +/// renderer, and its GL arm was deleted at slice V11. Nothing about the scene +/// changed — the same synthetic Setup, the same 40 fps sequence, the same +/// rotation and the same distant light, through the same +/// . The RHI arm opens a pass of its own against +/// the backbuffer and publishes it on for the +/// span of the draw, exactly as the two offscreen viewports do /// (, slice V6l). /// public sealed class PortalTunnelPresentation : IDisposable @@ -40,13 +37,6 @@ public sealed class PortalTunnelPresentation : IDisposable public const uint AnimationClientEnum = 0x10000002u; public const uint ClientEnumCategory = 7u; - // UIViewportObject::DrawContent @ 0x006950A5 calls - // RenderDeviceD3D::Clear(4, RGBAColor_Black, 1). Clear @ 0x0059FD30 - // maps retail flag 4 to D3DCLEAR_ZBUFFER only: portal space deliberately - // preserves the black color target established by SceneTool::BeginScene's - // whole-frame Clear(7) while replacing the hidden world viewport. - internal const ClearBufferMask RetailViewportClearMask = ClearBufferMask.DepthBufferBit; - /// /// The colour the RHI arm's pass loads with, and the one value that makes /// its Clear load-op equivalent to GL's depth-only clear. @@ -74,22 +64,17 @@ public sealed class PortalTunnelPresentation : IDisposable private static readonly HashSet AnimatedIds = new() { SyntheticEntityId }; - /// The GL arm's context, or null on a backend that has none. - private readonly GL? _glContext; - /// - /// The world pass scope, or null on the GL arm. - /// - /// 's RHI arm borrows its pass from the - /// scope rather than opening one, so a presentation that opens a pass of its - /// own has to publish it there for the duration of the draw. On the GL arm - /// the dispatcher records against whatever framebuffer is bound, so nothing - /// is published. + /// The world pass scope this presentation's own pass publishes into for + /// the span of the draw. borrows its pass + /// from the scope rather than opening one, so a presentation that opens a + /// pass of its own has to publish it there. The raw-GL arm this used to be + /// optional for was deleted at Campaign V slice V11. /// - private readonly IWorldPassScope? _scope; + private readonly IWorldPassScope _scope; - /// The frame the RHI arm's pass is opened on; null on the GL arm. - private readonly ICurrentGpuFrameSource? _frames; + /// The frame this presentation's own pass is opened on. + private readonly ICurrentGpuFrameSource _frames; private readonly WbDrawDispatcher _dispatcher; private readonly SceneLightingUboBinding _lightUbo; @@ -117,9 +102,8 @@ public sealed class PortalTunnelPresentation : IDisposable private bool _disposed; private PortalTunnelPresentation( - GL? gl, - IWorldPassScope? scope, - ICurrentGpuFrameSource? frames, + IWorldPassScope scope, + ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo, IWbMeshAdapter meshAdapter, @@ -132,7 +116,6 @@ public sealed class PortalTunnelPresentation : IDisposable Action? displayNotice, IDisposable? displayNoticeLifetime) { - _glContext = gl; _scope = scope; _frames = frames; _dispatcher = dispatcher; @@ -173,9 +156,8 @@ public sealed class PortalTunnelPresentation : IDisposable /// Composition is the only caller. /// internal static PortalTunnelPresentation CreateRequired( - GL? gl, - IWorldPassScope? scope, - ICurrentGpuFrameSource? frames, + IWorldPassScope scope, + ICurrentGpuFrameSource frames, IDatReaderWriter dats, IAnimationLoader animationLoader, IAnimationHookSink hookSink, @@ -186,14 +168,8 @@ public sealed class PortalTunnelPresentation : IDisposable IDisposable? displayNoticeLifetime = null, Random? random = null) { - if (gl is null && (scope is null || frames is null)) - { - throw new ArgumentNullException( - nameof(scope), - "A backend without a GL context must supply a world pass scope and a frame source " - + "for portal space to open its pass on."); - } - + ArgumentNullException.ThrowIfNull(scope); + ArgumentNullException.ThrowIfNull(frames); ArgumentNullException.ThrowIfNull(dats); ArgumentNullException.ThrowIfNull(animationLoader); ArgumentNullException.ThrowIfNull(hookSink); @@ -215,7 +191,6 @@ public sealed class PortalTunnelPresentation : IDisposable animation is not null); return new PortalTunnelPresentation( - gl, scope, frames, dispatcher, @@ -335,51 +310,21 @@ public sealed class PortalTunnelPresentation : IDisposable _camera.Aspect = width / (float)height; _camera.UseSmartBoxFov(smartBoxProjection); - if (_glContext is { } gl) - { - DrawGl(gl, width, height); - return; - } - DrawRhi(); } - private void DrawGl(GL gl, int width, int height) - { - using var scope = new GLStateScope(gl); - - gl.Viewport(0, 0, (uint)width, (uint)height); - gl.Disable(EnableCap.ScissorTest); - gl.ClearDepth(1.0); - gl.DepthMask(true); - gl.Clear(RetailViewportClearMask); - - gl.Enable(EnableCap.DepthTest); - gl.DepthFunc(DepthFunction.Less); - gl.Enable(EnableCap.CullFace); - gl.CullFace(TriangleFace.Back); - gl.FrontFace(FrontFaceDirection.Ccw); - gl.Disable(EnableCap.Blend); - - DrawScene(); - } - /// - /// The RHI arm. Every capability the GL arm sets by hand is baked into the - /// dispatcher's pipelines, and the pass sets its own full-attachment - /// viewport, so what remains is the pass itself: a backbuffer pass at the - /// world's sample count that clears colour and depth (see - /// for why re-clearing colour is - /// exact) and is published as the scope's for the span of the draw. + /// Every capability the raw-GL arm used to set by hand (deleted at + /// Campaign V slice V11) is baked into the dispatcher's pipelines, and the + /// pass sets its own full-attachment viewport, so what remains is the + /// pass itself: a backbuffer pass at the world's sample count that clears + /// colour and depth (see for why + /// re-clearing colour is exact) and is published as the scope's for the + /// span of the draw. /// private void DrawRhi() { - IWorldPassScope scope = _scope - ?? throw new InvalidOperationException( - "Portal space reached its RHI arm with no world pass scope."); - IGpuFrame frame = (_frames ?? throw new InvalidOperationException( - "Portal space reached its RHI arm with no frame source.")) - .CurrentFrame + IGpuFrame frame = _frames.CurrentFrame ?? throw new InvalidOperationException( "Portal space requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); @@ -387,11 +332,11 @@ public sealed class PortalTunnelPresentation : IDisposable GpuPassDescription.BackbufferClear( "portal-space", RetailPortalSpaceClearColor, - scope.SampleCount)); + _scope.SampleCount)); // Published AFTER the pass opens and BEFORE the light upload: publishing // resets the frame-global sections, and the distant light installed below // is the one this scene wants rather than the world's. - using IDisposable publication = scope.Publish(encoder); + using IDisposable publication = _scope.Publish(encoder); DrawScene(); } diff --git a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs index 7670d7aa..97db5e00 100644 --- a/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs +++ b/src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs @@ -4,7 +4,6 @@ using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Core.Lighting; using AcDream.Core.World; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; @@ -41,41 +40,31 @@ internal interface IPrivateEntityViewportCamera : ICamera /// nothing depends on inheritance and the GL and Vulkan backends agree about /// what Target: null means. /// -/// The DRAW inside the pass is still raw GL: WbDrawDispatcher keeps -/// its GL arm through to V10 (§5.5.6), so this renderer opens an RHI pass and -/// then lets a raw-GL renderer record into the framebuffer that pass bound. The -/// surrounding is what restores the previous -/// framebuffer, viewport and capability state afterwards, exactly as before. +/// The GL arm this renderer used to open an RHI pass and delegate a +/// raw-GL WbDrawDispatcher into (through V10, §5.5.6) was deleted at +/// Campaign V slice V11: WbDrawDispatcher now records into the pass +/// this renderer publishes on both call sites the same way. /// -internal sealed unsafe class PrivateEntityViewportRenderer : +internal sealed class PrivateEntityViewportRenderer : IUiViewportRenderer, IDisposable { private const uint PrivateLandblockId = 0u; - /// - /// The GL arm's context, or null on a backend that has none. Campaign V - /// slice V6l: both retained-UI viewports render on either arm now. - /// - private readonly GL? _glContext; - - private GL RequireGl => _glContext - ?? throw new InvalidOperationException( - $"The {_diagnosticName} reached its GL arm on a backend with no GL context."); - private readonly IGpuDevice _device; private readonly ICurrentGpuFrameSource _frames; /// - /// The world pass scope, or null on the GL arm. + /// The world pass scope this renderer's own pass publishes into for the + /// span of the draw. WbDrawDispatcher borrows its pass from the + /// scope rather than opening one, so this renderer — which opens a pass of + /// its own to draw into an offscreen target — has to publish it there. /// - /// Slice V6l. WbDrawDispatcher's RHI arm borrows its pass from - /// the scope rather than opening one, so a viewport that opens a pass of its - /// own has to publish it there for the duration of the draw. On the GL arm - /// the dispatcher records against whatever framebuffer is bound, which is - /// what the pass bound, so nothing is published. + /// The raw-GL arm this used to be optional for (the GL dispatcher + /// recorded against whatever framebuffer was already bound, needing no + /// publication) was deleted at Campaign V slice V11. /// - private readonly IWorldPassScope? _scope; + private readonly IWorldPassScope _scope; private readonly WbDrawDispatcher _dispatcher; private readonly SceneLightingUboBinding _lightUbo; @@ -96,8 +85,7 @@ internal sealed unsafe class PrivateEntityViewportRenderer : private SyntheticEntityMeshReferenceOwner? _meshReferences; public PrivateEntityViewportRenderer( - GL? gl, - IWorldPassScope? scope, + IWorldPassScope scope, IGpuDevice device, ICurrentGpuFrameSource frames, WbDrawDispatcher dispatcher, @@ -110,15 +98,10 @@ internal sealed unsafe class PrivateEntityViewportRenderer : { if (renderId == 0u) throw new ArgumentOutOfRangeException(nameof(renderId)); - if (gl is null && scope is null) - { - throw new ArgumentNullException( - nameof(scope), - "A backend without a GL context must publish a world pass scope for the viewport to draw into."); - } - _glContext = gl; - _scope = scope; + _scope = scope ?? throw new ArgumentNullException( + nameof(scope), + "The viewport must publish a world pass scope to draw into."); _device = device ?? throw new ArgumentNullException(nameof(device)); _frames = frames ?? throw new ArgumentNullException(nameof(frames)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); @@ -136,10 +119,11 @@ internal sealed unsafe class PrivateEntityViewportRenderer : } /// - /// A GL framebuffer's colour texture samples bottom-up; a Vulkan render - /// target's does not. See . + /// A GL framebuffer's colour texture used to sample bottom-up; that arm was + /// deleted at Campaign V slice V11, and a Vulkan render target's does not. + /// See . /// - public bool TextureIsBottomUp => _glContext is not null; + public bool TextureIsBottomUp => false; public void SetEntity(WorldEntity? entity) { @@ -222,14 +206,6 @@ internal sealed unsafe class PrivateEntityViewportRenderer : ?? throw new InvalidOperationException( $"The {_diagnosticName} requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); - // The pass's clear is not scissored on either backend, but GL's glClear - // is: a doorway slice earlier in the frame can have left the scissor on, - // and this target is not confined to it. - using GLStateScope? glState = _glContext is { } scissorGl - ? new GLStateScope(scissorGl) - : null; - _glContext?.Disable(EnableCap.ScissorTest); - using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription { Name = _diagnosticName, @@ -246,31 +222,13 @@ internal sealed unsafe class PrivateEntityViewportRenderer : SampleCount = 1, }); - // GL's BeginPass deliberately does not touch viewport or scissor, and - // the renderer that draws inside this pass is still raw GL, so the - // remaining state is set here exactly as it always was. On the RHI arm - // every one of these is baked into the dispatcher's pipelines and the - // pass sets its own full-attachment viewport, so there is nothing to do. - if (_glContext is { } stateGl) - { - stateGl.Viewport(0, 0, (uint)width, (uint)height); - stateGl.Enable(EnableCap.DepthTest); - stateGl.DepthFunc(DepthFunction.Less); - stateGl.Enable(EnableCap.CullFace); - stateGl.CullFace(TriangleFace.Back); - stateGl.FrontFace(FrontFaceDirection.Ccw); - stateGl.Disable(EnableCap.Blend); - } - - // Slice V6l: the dispatcher's RHI arm borrows its pass from the scope, - // so this pass has to BE the scope's for the span of the draw. Published - // after the world phase has closed its own, which is why it does not - // nest; the sections it resets are republished by UploadCreatureLight + // The dispatcher borrows its pass from the scope, so this pass has to + // BE the scope's for the span of the draw. Published after the world + // phase has closed its own, which is why it does not nest; the + // sections it resets are republished by UploadCreatureLight // immediately below, which is the private lighting this view wants // rather than the world's. - using IDisposable? publication = _glContext is null - ? _scope!.Publish(encoder) - : null; + using IDisposable publication = _scope.Publish(encoder); UploadCreatureLight(); diff --git a/src/AcDream.App/Rendering/RenderBootstrap.cs b/src/AcDream.App/Rendering/RenderBootstrap.cs deleted file mode 100644 index 6636a4f5..00000000 --- a/src/AcDream.App/Rendering/RenderBootstrap.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System.Collections.Concurrent; -using AcDream.Content; -using DatReaderWriter; -using Microsoft.Extensions.Logging.Abstractions; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -/// -/// The subset of the production render stack that the UI Studio needs: -/// GL + dats + UiHost + the WB mesh pipeline. Constructed from the same -/// classes and the same order as , minus -/// terrain / sky / physics / streaming. -/// -public sealed record RenderStack( - GL Gl, - IDatReaderWriter Dats, - string ShaderDir, - Wb.BindlessSupport Bindless, - TextureCache TextureCache, - Shader MeshShader, - Wb.WbMeshAdapter MeshAdapter, - Wb.EntitySpawnAdapter EntitySpawnAdapter, - Wb.WbDrawDispatcher DrawDispatcher, - SceneLightingUboBinding LightingUbo, - AcDream.App.UI.UiHost UiHost, - AcDream.App.UI.UiDatFont? VitalsDatFont, - AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable -{ - internal GpuFrameFlightController FrameFlights { get; init; } = null!; - - /// - /// Campaign V slice V4a: the studio's own RHI device (mirrors - /// 's production one — the studio - /// composes its own render stack independently of GameWindow). - /// - internal AcDream.App.Rendering.Gpu.IGpuDevice GpuDevice { get; init; } = null!; - - /// - /// Drives /IGpuFrame.End - /// once per / pair and - /// exposes the open frame to 's . - /// - internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!; - - private ResourceShutdownTransaction? _shutdown; - - internal void BeginFrame() => FrameLifetime.BeginFrame(); - - internal void EndFrame() => FrameLifetime.EndFrame(); - - /// Dispose the GL pieces this stack OWNS (everything created in - /// ). + are caller-owned - /// and NOT disposed here. Called once at studio teardown. - public void Dispose() - { - _shutdown ??= new ResourceShutdownTransaction( - new ResourceShutdownStage("submitted GPU work", - [ - new("frame flight drain", FrameFlights.WaitForSubmittedWork), - ]), - new ResourceShutdownStage("draw frontend", - [ - new("draw dispatcher", DrawDispatcher.Dispose), - ]), - new ResourceShutdownStage("mesh adapter", - [ - new("mesh adapter", MeshAdapter.Dispose), - ]), - new ResourceShutdownStage("remaining render stack", - [ - new("texture cache", TextureCache.Dispose), - new("mesh shader", MeshShader.Dispose), - new("lighting UBO", LightingUbo.Dispose), - new("UI host", UiHost.Dispose), - // GpuDevice's own Dispose routes every resource release through - // FrameFlights as its retirement queue, so it must be disposed - // before FrameFlights below (see GlGpuDevice.Dispose's comment). - new("GPU device (RHI)", GpuDevice.Dispose), - ]), - new ResourceShutdownStage("frame flight owner", - [ - new("frame flights", FrameFlights.Dispose), - ])); - _shutdown.CompleteOrThrow(); - } - - /// - /// Resolves a sprite id (0x06xxxxxx) to a (GL handle, width, height) triple. - /// Copied verbatim from GameWindow's ResolveChrome closure — it calls - /// TextureCache.GetOrUploadRenderSurface(id, out w, out h). - /// - public (uint handle, int width, int height) ResolveChrome(uint spriteId) - { - uint t = TextureCache.GetOrUploadRenderSurface(spriteId, out int w, out int h); - return (t, w, h); - } - - // ── Font cache (per-stack, keyed by FontDid) ───────────────────────────── - - /// - /// Cache of loaded dat fonts keyed by FontDid (0x40000000-range). - /// Populated lazily by . Thread-safe for - /// concurrent reads from the studio render loop; writes happen only - /// during the first load of each distinct FontDid. - /// - private readonly ConcurrentDictionary _fontCache = new(); - - /// - /// Lazily load and cache a dat font by its FontDid. Returns null (and - /// caches null) when the Font DBObj is absent or has no foreground surface — - /// callers fall back to the global font in that case. - /// - /// Pre-seeds (0x40000000) and - /// (0x40000001) from the already-loaded instances - /// to avoid a redundant upload on those two ids. - /// - public AcDream.App.UI.UiDatFont? ResolveDatFont(uint fontDid) - { - return _fontCache.GetOrAdd(fontDid, id => - AcDream.App.UI.UiDatFont.Load(Dats, TextureCache, id)); - } - - /// - /// Pre-seeds the font cache from the two already-loaded font instances - /// (VitalsDatFont = 0x40000000, LargeDatFont = 0x40000001) so that - /// returns them without a redundant GL upload. - /// Called once by after the stack is - /// fully constructed. - /// - internal void SeedFontCache() - { - if (VitalsDatFont is not null) - _fontCache.TryAdd(AcDream.App.UI.UiDatFont.DefaultFontId, VitalsDatFont); - if (LargeDatFont is not null) - _fontCache.TryAdd(0x40000001u, LargeDatFont); - } -} - -/// Options for . -public sealed record RenderBootstrapOptions( - AcDream.UI.Abstractions.Settings.QualitySettings Quality, - string DiagnosticsDirectory); - -/// -/// Constructs the UI Studio's render stack from the production classes, -/// in the same order as . -/// -public static class RenderBootstrap -{ - /// - /// Build the studio's render stack. Throws - /// (same message as GameWindow) if GL_ARB_bindless_texture or - /// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory. - /// - public static RenderStack Create( - GL gl, - IDatReaderWriter dats, - RenderBootstrapOptions opts) - { - // --- Bindless detection (GameWindow ~1701-1723) --- - if (!Wb.BindlessSupport.TryCreate(gl, out var bindless) - || bindless is null - || !bindless.HasShaderDrawParameters(gl)) - { - throw new NotSupportedException( - "acdream requires GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters " + - "(GL 4.3+ with bindless support). Your GPU/driver does not expose these extensions. " + - "If this is unexpected, please file a bug report with your GPU vendor + driver version."); - } - - // --- Shared infra (GameWindow ~1198, ~1211) --- - string shaderDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders"); - var lightingUbo = new SceneLightingUboBinding(gl); - - // --- Mesh shader (GameWindow ~1769-1771) --- - // Campaign V slice V6e: mesh_modern has needed common.glsl since V2 — - // ACDREAM_UBO_SET appears in a layout qualifier and ACDREAM_SAMPLE_ARRAY - // at the sample site, and without the preamble both are undeclared - // identifiers, so this program has failed to link on the Studio path - // since that slice. The world composition (WorldRenderComposition) has - // always passed true; this is the same pair loaded the same way. - var meshShader = new Shader(gl, - Path.Combine(shaderDir, "mesh_modern.vert"), - Path.Combine(shaderDir, "mesh_modern.frag"), - includeCommonPreamble: true); - - // --- TextureCache (GameWindow ~1774) --- - var frameFlights = new GpuFrameFlightController(gl); - // Campaign V slice V4a: the studio composes its own RHI device - // independently of GameWindow/HostInputCameraComposition, mirroring - // that composition's construction (gl + frame flights + shaders dir). - Gpu.IGpuDevice gpuDevice = new Gpu.Gl.GlGpuDevice(gl, frameFlights, shaderDir); - var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice); - var textureCache = new TextureCache( - gl, - gpuDevice, - dats, - bindless, - frameFlights, - opts.DiagnosticsDirectory); - - // --- AnimLoader (GameWindow ~1240) --- - var animLoader = new AcDream.Content.Vfx.RetailAnimationLoader(dats); - - // --- WbMeshAdapter (GameWindow ~2286-2287) --- - var wbLogger = NullLogger.Instance; - var meshAdapter = Wb.WbMeshAdapter.CreateWithLiveDatPreparedAssets( - gl, - gpuDevice, - dats, - wbLogger, - frameFlights); - - // --- SequencerFactory (GameWindow ~2306-2334) --- - var capturedDats = dats; - var capturedAnimLoader = animLoader; - AcDream.Core.Physics.AnimationSequencer SequencerFactory(AcDream.Core.World.WorldEntity e) - { - if (capturedDats is not null && capturedAnimLoader is not null) - { - var setup = capturedDats.Get(e.SourceGfxObjOrSetupId); - if (setup is not null) - { - uint mtableId = (uint)setup.DefaultMotionTable; - if (mtableId != 0) - { - var mtable = capturedDats.Get(mtableId); - if (mtable is not null) - return new AcDream.Core.Physics.AnimationSequencer( - setup, mtable, capturedAnimLoader); - } - // Setup exists but no motion table — no-op sequencer. - return new AcDream.Core.Physics.AnimationSequencer( - setup, - new DatReaderWriter.DBObjs.MotionTable(), - capturedAnimLoader); - } - } - // Complete fallback: empty setup + empty motion table + null loader. - return new AcDream.Core.Physics.AnimationSequencer( - new DatReaderWriter.DBObjs.Setup(), - new DatReaderWriter.DBObjs.MotionTable(), - new NullAnimLoader()); - } - - // --- EntitySpawnAdapter (GameWindow ~2335-2336) --- - var entitySpawnAdapter = new Wb.EntitySpawnAdapter( - textureCache, SequencerFactory, meshAdapter); - - // --- EntityClassificationCache (GameWindow ~217 — field initializer, new()) --- - var classificationCache = new Wb.EntityClassificationCache(); - - // --- TranslucencyFadeManager (GameWindow — field initializer, new()) --- - var translucencyFades = new AcDream.Core.Rendering.TranslucencyFadeManager(); - - // --- WbDrawDispatcher (GameWindow ~2377-2381) --- - var drawDispatcher = new Wb.WbDrawDispatcher( - gl, meshShader, textureCache, meshAdapter, entitySpawnAdapter, - bindless, classificationCache, translucencyFades); - drawDispatcher.AlphaToCoverage = opts.Quality.AlphaToCoverage; - - // --- Vitals dat font (GameWindow ~1820-1822) --- - var vitalsDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache); - - // --- Larger retail font (0x40000001, MaxCharHeight=18) for attribute row text. - // The default font (0x40000000, 16px) renders the row names too small; the 18px - // variant (confirmed in client_portal.dat 2026-06-26) matches the retail character - // window list more closely (≈ icon height ≈ 24px target, 18px is best available). - var largeDatFont = AcDream.App.UI.UiDatFont.Load(dats, textureCache, 0x40000001u); - - // --- UiHost (GameWindow ~1790); pass null for debugFont (only used as - // a fallback BitmapFont for the world-space HUD — not needed for the - // UI Studio, and BitmapFont requires a system font byte array) --- - var uiHost = new AcDream.App.UI.UiHost(gpuDevice, gpuFrameLifetime, shaderDir, defaultFont: null); - - var stack = new RenderStack( - Gl: gl, - Dats: dats, - ShaderDir: shaderDir, - Bindless: bindless, - TextureCache: textureCache, - MeshShader: meshShader, - MeshAdapter: meshAdapter, - EntitySpawnAdapter: entitySpawnAdapter, - DrawDispatcher: drawDispatcher, - LightingUbo: lightingUbo, - UiHost: uiHost, - VitalsDatFont: vitalsDatFont, - LargeDatFont: largeDatFont) - { - FrameFlights = frameFlights, - GpuDevice = gpuDevice, - FrameLifetime = gpuFrameLifetime, - }; - - // Pre-seed the font cache with the two already-uploaded atlas instances - // so ResolveDatFont(0x40000000) and ResolveDatFont(0x40000001) hit the cache - // rather than re-uploading the same GL texture a second time. - stack.SeedFontCache(); - - return stack; - } - - // NullAnimLoader mirrors GameWindow's private NullAnimLoader (GameWindow ~13327-13330). - private sealed class NullAnimLoader : AcDream.Core.Physics.IAnimationLoader - { - public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) => null; - } -} diff --git a/src/AcDream.App/Rendering/RenderFrameGlStateController.cs b/src/AcDream.App/Rendering/RenderFrameGlStateController.cs deleted file mode 100644 index b8a68c4e..00000000 --- a/src/AcDream.App/Rendering/RenderFrameGlStateController.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -internal interface IRenderFrameGlState -{ - void RestoreFrameDefaults(); -} - -internal interface IRenderFrameGlStateApi -{ - void SetCapability(EnableCap capability, bool enabled); - - void ColorMask(bool red, bool green, bool blue, bool alpha); - - void StencilMask(uint mask); - - void DepthMask(bool enabled); - - void DepthFunc(DepthFunction function); - - void CullFace(TriangleFace face); - - void FrontFace(FrontFaceDirection direction); - - void UseProgram(uint program); - - void BindVertexArray(uint vertexArray); - - void BindBuffer(BufferTargetARB target, uint buffer); -} - -internal sealed class SilkRenderFrameGlStateApi : IRenderFrameGlStateApi -{ - private readonly GL _gl; - - public SilkRenderFrameGlStateApi(GL gl) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - } - - public void SetCapability(EnableCap capability, bool enabled) - { - if (enabled) - _gl.Enable(capability); - else - _gl.Disable(capability); - } - - public void ColorMask(bool red, bool green, bool blue, bool alpha) => - _gl.ColorMask(red, green, blue, alpha); - - public void StencilMask(uint mask) => _gl.StencilMask(mask); - - public void DepthMask(bool enabled) => _gl.DepthMask(enabled); - - public void DepthFunc(DepthFunction function) => _gl.DepthFunc(function); - - public void CullFace(TriangleFace face) => _gl.CullFace(face); - - public void FrontFace(FrontFaceDirection direction) => _gl.FrontFace(direction); - - public void UseProgram(uint program) => _gl.UseProgram(program); - - public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray); - - public void BindBuffer(BufferTargetARB target, uint buffer) => - _gl.BindBuffer(target, buffer); -} - -/// -/// Restores the frame-global OpenGL convention shared by frame clear and -/// exceptional world-pass rollback. This prevents a failed doorway scissor or -/// portal depth mask from clipping/color-masking the next frame's clear. -/// -internal sealed class RenderFrameGlStateController : IRenderFrameGlState -{ - private readonly IRenderFrameGlStateApi _gl; - - public RenderFrameGlStateController(IRenderFrameGlStateApi gl) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - } - - public void RestoreFrameDefaults() - { - _gl.SetCapability(EnableCap.ScissorTest, enabled: false); - _gl.SetCapability(EnableCap.StencilTest, enabled: false); - _gl.SetCapability(EnableCap.Blend, enabled: false); - _gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false); - for (int index = 0; index < ClipFrame.MaxPlanes; index++) - { - _gl.SetCapability( - EnableCap.ClipDistance0 + index, - enabled: false); - } - - _gl.ColorMask(true, true, true, true); - _gl.StencilMask(0xFF); - _gl.DepthMask(true); - _gl.DepthFunc(DepthFunction.Less); - _gl.SetCapability(EnableCap.DepthTest, enabled: true); - // Renderers that need face culling establish it locally. The shared - // frame convention is culling off; SkyRenderer restores the state it - // observed, so enabling it here would leak culling into later passes. - _gl.SetCapability(EnableCap.CullFace, enabled: false); - _gl.CullFace(TriangleFace.Back); - _gl.FrontFace(FrontFaceDirection.CW); - _gl.UseProgram(0); - _gl.BindVertexArray(0); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); - } -} diff --git a/src/AcDream.App/Rendering/ResourceCleanupGroup.cs b/src/AcDream.App/Rendering/ResourceCleanupGroup.cs index 1a3b6c5d..0153d719 100644 --- a/src/AcDream.App/Rendering/ResourceCleanupGroup.cs +++ b/src/AcDream.App/Rendering/ResourceCleanupGroup.cs @@ -2,6 +2,38 @@ namespace AcDream.App.Rendering; using System.Runtime.ExceptionServices; +internal interface IRetryableResourceCleanup +{ + bool IsCleanupComplete { get; } + void RetryCleanup(); +} + +/// +/// Thrown when a resource construction failed and the partial-construction +/// rollback it triggered could not fully complete either. Backend-neutral — +/// and ShaderProgramConstruction +/// throw it on both the (deleted, Campaign V slice V11) GL construction path +/// and the Vulkan one. +/// +internal sealed class ResourceConstructionException : AggregateException, + IRetryableResourceCleanup +{ + private readonly IRetryableResourceCleanup _cleanup; + + public ResourceConstructionException( + string message, + IRetryableResourceCleanup cleanup, + IEnumerable failures) + : base(message, failures) + { + _cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup)); + } + + public bool IsCleanupComplete => _cleanup.IsCleanupComplete; + + public void RetryCleanup() => _cleanup.RetryCleanup(); +} + /// /// Reverse-order, all-attempted cleanup owner used while a composite resource /// is still under construction and after it becomes the aggregate owner. @@ -84,7 +116,7 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup } catch (Exception cleanupFailure) { - throw new GlResourceConstructionException( + throw new ResourceConstructionException( message, this, [constructionFailure, cleanupFailure]); @@ -94,3 +126,88 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup throw new InvalidOperationException("Unreachable construction rollback path."); } } + +/// +/// Lifetime root for cleanup work that could not finish before a throwing +/// composition factory returned control. The original exception remains the +/// retry owner; this ledger prevents it and its exact pending names from +/// becoming local-only. +/// +/// Backend-neutral, despite its former name +/// (GlConstructionCleanupLedger, deleted at Campaign V slice V11): it +/// walks any exception chain for — +/// which implements on both +/// backends — and does not itself touch GL. +/// +internal sealed class ResourceConstructionCleanupLedger : IDisposable +{ + private readonly List _pending = []; + private bool _disposing; + + public bool IsComplete => _pending.Count == 0; + + public bool RetainFrom(Exception failure) + { + ArgumentNullException.ThrowIfNull(failure); + bool retained = false; + Visit(failure); + return retained; + + void Visit(Exception current) + { + if (current is IRetryableResourceCleanup cleanup) + { + if (!cleanup.IsCleanupComplete && !_pending.Contains(cleanup)) + _pending.Add(cleanup); + retained = true; + } + + if (current is AggregateException aggregate) + { + foreach (Exception inner in aggregate.InnerExceptions) + Visit(inner); + } + else if (current.InnerException is { } inner) + { + Visit(inner); + } + } + } + + public void Dispose() + { + if (_disposing || _pending.Count == 0) + return; + + _disposing = true; + List? failures = null; + try + { + for (int i = _pending.Count - 1; i >= 0; i--) + { + IRetryableResourceCleanup cleanup = _pending[i]; + try + { + cleanup.RetryCleanup(); + if (cleanup.IsCleanupComplete) + _pending.RemoveAt(i); + } + catch (Exception failure) + { + (failures ??= []).Add(failure); + } + } + } + finally + { + _disposing = false; + } + + if (failures is not null) + { + throw new AggregateException( + "One or more failed resource construction transactions remain pending.", + failures); + } + } +} diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 0a9966fc..f329013c 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -5,7 +5,6 @@ using AcDream.App.Rendering.Wb; using AcDream.Core.Rendering; using AcDream.Core.Vfx; using AcDream.Core.World; -using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace AcDream.App.Rendering; diff --git a/src/AcDream.App/Rendering/SamplerCache.cs b/src/AcDream.App/Rendering/SamplerCache.cs deleted file mode 100644 index 91647bab..00000000 --- a/src/AcDream.App/Rendering/SamplerCache.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -/// -/// Two persistent GL sampler objects (Repeat + ClampToEdge) created once -/// per GL context. Renderers the appropriate -/// one to a texture unit instead of mutating per-texture -/// GL_TEXTURE_WRAP_S/T state — sampler state overrides the -/// texture's own wrap parameters, so two renderers can share the same -/// texture handle but sample it with different wrap modes safely. -/// -/// -/// Ported from -/// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132. -/// Filter modes match 's upload defaults -/// (Linear / Linear, no mipmaps) so binding either sampler doesn't -/// change the visual filtering behavior — only the wrap behavior at -/// UVs outside [0, 1]. -/// -/// -/// -/// Lifetime: created once at GL init, disposed with the GL context. -/// Anything that binds a sampler MUST unbind it (BindSampler(unit, 0)) -/// before yielding to a renderer that doesn't use samplers, otherwise -/// the bound sampler's wrap mode will silently override that renderer's -/// per-texture wrap state. -/// -/// -public sealed class SamplerCache : IDisposable -{ - private readonly GL _gl; - private readonly ResourceCleanupGroup _resources; - - /// Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by . - public uint Wrap { get; } - - /// Sampler with WrapS = WrapT = ClampToEdge. Used by sky meshes whose authored UVs are strictly in [0, 1] to avoid bilinear-filter bleed at seam edges. - public uint Clamp { get; } - - public SamplerCache(GL gl) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - var resources = new ResourceCleanupGroup(); - uint wrap = 0; - uint clamp = 0; - try - { - wrap = GlResourceCommand.CreateName( - _gl, - "repeat sampler", - _gl.GenSampler, - _gl.DeleteSampler); - uint ownedWrap = wrap; - resources.Add( - "repeat sampler", - () => GlResourceCommand.Execute( - _gl, - $"delete repeat sampler {ownedWrap}", - () => _gl.DeleteSampler(ownedWrap))); - Configure(wrap, TextureWrapMode.Repeat, "repeat sampler"); - - clamp = GlResourceCommand.CreateName( - _gl, - "clamp sampler", - _gl.GenSampler, - _gl.DeleteSampler); - uint ownedClamp = clamp; - resources.Add( - "clamp sampler", - () => GlResourceCommand.Execute( - _gl, - $"delete clamp sampler {ownedClamp}", - () => _gl.DeleteSampler(ownedClamp))); - Configure(clamp, TextureWrapMode.ClampToEdge, "clamp sampler"); - } - catch (Exception constructionFailure) - { - resources.RollbackConstructionAndThrow( - "SamplerCache construction failed and its sampler prefix did not cleanly roll back.", - constructionFailure); - } - - Wrap = wrap; - Clamp = clamp; - _resources = resources; - } - - private void Configure(uint sampler, TextureWrapMode wrap, string name) => - GlResourceCommand.Execute(_gl, $"configure {name}", () => - { - _gl.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrap); - _gl.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrap); - _gl.SamplerParameter(sampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear); - _gl.SamplerParameter(sampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear); - }); - - public void Dispose() - { - _resources.RetryCleanup(); - } -} diff --git a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs index 57e20d1e..61602a6a 100644 --- a/src/AcDream.App/Rendering/SceneLightingUboBinding.cs +++ b/src/AcDream.App/Rendering/SceneLightingUboBinding.cs @@ -1,61 +1,39 @@ using System; -using System.Runtime.InteropServices; using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Wb; using AcDream.Core.Lighting; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// -/// GL wrapper that owns the SceneLighting UBO buffer, updates its -/// contents each frame, and keeps it bound at binding=1 so every -/// shader sampling uLights[] / uFogColor / etc reads -/// consistent data without per-shader re-upload. +/// Publishes the SceneLighting UBO each frame so every shader sampling +/// uLights[] / uFogColor / etc reads consistent data without +/// per-shader re-upload. /// -/// -/// Usage (r12 §13.2 + r13 §12.3): -/// -/// Instantiate once at startup, after the GL context exists. -/// Each frame, after , call with a freshly-built . -/// Shader programs that declare layout(std140, binding = 1) uniform SceneLighting { ... } automatically pick up the data. -/// -/// +/// The raw-GL arm this used to have alongside it — a global uniform +/// binding point kept live by a per-flight-slot buffer pool — was deleted at +/// Campaign V slice V11. Vulkan has no such global binding point: a +/// descriptor set is bound per draw, so the upload is a frame ring slice +/// PUBLISHED on , and each world +/// renderer binds it inside the pass after its own binds (plan §5.5.14 item +/// 2). Every allocation within a frame is already distinct memory that lives +/// until the frame retires, which is the property the deleted buffer pool +/// existed to provide when the world, portal-space and paperdoll draws each +/// upload different lighting. /// public sealed unsafe class SceneLightingUboBinding : IDisposable { - private readonly GL? _gl; - private readonly ICurrentGpuFrameSource? _frames; - private readonly WorldFrameSections? _sections; - private uint _ubo; - private readonly List[] _buffersByFrame = [[], [], []]; - private int _frameSlot; - private int _bufferCursor; + private readonly ICurrentGpuFrameSource _frames; + private readonly WorldFrameSections _sections; private bool _frameStarted; - private bool _disposed; - - internal int DynamicBufferCount => _buffersByFrame.Sum(buffers => buffers.Count); - - public SceneLightingUboBinding(GL gl) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - } /// - /// Campaign V slice V6j: the RHI arm. - /// - /// GL keeps the block bound at a global uniform binding point and every - /// shader inherits it. Vulkan has no such point — a descriptor set is bound - /// per draw and the renderer's own binds select the scope this block has to - /// land in — so the upload becomes a frame ring slice PUBLISHED on - /// , and each world renderer - /// binds it inside the pass after its own binds (plan §5.5.14 item 2). - /// - /// The per-flight-slot buffer pool disappears with it: every allocation - /// within a frame is already distinct memory that lives until the frame - /// retires, which is the property the pool existed to provide when the world, - /// portal-space and paperdoll draws each upload different lighting. + /// The GL arm's per-flight-slot buffer pool this used to report on was + /// deleted at Campaign V slice V11: every allocation now comes from a ring + /// that lives until its frame retires, so there is no persistent + /// dynamic-buffer pool left to size. /// + internal int DynamicBufferCount => 0; + internal SceneLightingUboBinding( ICurrentGpuFrameSource frames, WorldFrameSections sections) @@ -71,80 +49,20 @@ public sealed unsafe class SceneLightingUboBinding : IDisposable /// public void BeginFrame(int frameSlot) { - if ((uint)frameSlot >= (uint)_buffersByFrame.Length) - throw new ArgumentOutOfRangeException(nameof(frameSlot)); - - _frameSlot = frameSlot; - _bufferCursor = 0; + ArgumentOutOfRangeException.ThrowIfNegative(frameSlot); _frameStarted = true; } - private void ActivateNextBuffer() - { - if (!_frameStarted) - throw new InvalidOperationException("BeginFrame must be called before uploading scene lighting."); - if (_gl is null) - throw new InvalidOperationException("The RHI arm publishes a ring section rather than a buffer."); - - List buffers = _buffersByFrame[_frameSlot]; - if (_bufferCursor == buffers.Count) - { - uint buffer = TrackedGlResource.CreateBuffer( - _gl, - "SceneLighting frame UBO creation"); - try - { - TrackedGlResource.AllocateBufferStorage( - _gl, - GLEnum.UniformBuffer, - buffer, - 0, - SceneLightingUbo.SizeInBytes, - GLEnum.DynamicDraw, - "SceneLighting frame UBO allocation"); - buffers.Add(buffer); - } - catch - { - TrackedGlResource.DeleteBuffer( - _gl, - buffer, - 0, - "SceneLighting frame UBO rollback"); - throw; - } - } - - _ubo = buffers[_bufferCursor++]; - } - /// /// Push the current frame's UBO contents to the GPU. Cheap (576 bytes) /// so fine to call unconditionally every frame. /// public void Upload(SceneLightingUbo data) - { - if (_gl is null) - { - PublishSection(data); - return; - } - - ActivateNextBuffer(); - _gl.BindBuffer(BufferTargetARB.UniformBuffer, _ubo); - _gl.BufferSubData(BufferTargetARB.UniformBuffer, - (nint)0, (nuint)SceneLightingUbo.SizeInBytes, &data); - _gl.BindBufferBase(BufferTargetARB.UniformBuffer, - SceneLightingUbo.BindingPoint, _ubo); - _gl.BindBuffer(BufferTargetARB.UniformBuffer, 0); - } - - private void PublishSection(SceneLightingUbo data) { if (!_frameStarted) throw new InvalidOperationException("BeginFrame must be called before uploading scene lighting."); - IGpuFrame frame = _frames!.CurrentFrame + IGpuFrame frame = _frames.CurrentFrame ?? throw new InvalidOperationException( "Scene lighting requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); GpuRingAllocation allocation = frame.AllocateRing( @@ -152,30 +70,19 @@ public sealed unsafe class SceneLightingUboBinding : IDisposable GpuRingUsage.Uniform); new ReadOnlySpan(&data, SceneLightingUbo.SizeInBytes) .CopyTo(allocation.Data); - _sections!.SceneLighting = new GpuBufferSection( + _sections.SceneLighting = new GpuBufferSection( allocation.Buffer, allocation.OffsetBytes, (uint)SceneLightingUbo.SizeInBytes); } + /// + /// No-op: this binding owns no GPU resource of its own on the RHI arm — the + /// deleted GL arm's per-flight buffer pool was the only thing to release. + /// Kept as a method so callers that hold this behind an + /// reference (composition's acquisition scope) don't need a special case. + /// public void Dispose() { - if (_disposed) return; - if (_gl is not null) - { - foreach (List buffers in _buffersByFrame) - { - foreach (uint buffer in buffers) - { - TrackedGlResource.DeleteBuffer( - _gl, - buffer, - SceneLightingUbo.SizeInBytes, - "SceneLighting frame UBO disposal"); - } - buffers.Clear(); - } - } - _disposed = true; } } diff --git a/src/AcDream.App/Rendering/Shader.cs b/src/AcDream.App/Rendering/Shader.cs deleted file mode 100644 index 2d3be01c..00000000 --- a/src/AcDream.App/Rendering/Shader.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Numerics; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -public sealed class Shader : IDisposable -{ - private readonly GL _gl; - private readonly Dictionary _uniformLocations = new(StringComparer.Ordinal); - public uint Program { get; private set; } - - public Shader(GL gl, string vertexPath, string fragmentPath) - : this(gl, vertexPath, fragmentPath, includeCommonPreamble: false) - { - } - - /// - /// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when - /// is true, the text of - /// Shaders/common.glsl — sitting alongside - /// — is spliced into both sources right after their leading - /// #version/#extension block. GL has no #include, so this - /// is plain string concatenation at load time rather than a GLSL-level - /// mechanism. Every existing two-argument-path caller is unaffected: the - /// convenience constructor above always passes false. - /// - public Shader(GL gl, string vertexPath, string fragmentPath, bool includeCommonPreamble) - { - _gl = gl; - string vertexSource = File.ReadAllText(vertexPath); - string fragmentSource = File.ReadAllText(fragmentPath); - if (includeCommonPreamble) - { - string? directory = Path.GetDirectoryName(vertexPath); - string commonPath = directory is null - ? "common.glsl" - : Path.Combine(directory, "common.glsl"); - string commonSource = File.ReadAllText(commonPath); - vertexSource = InjectPreamble(vertexSource, commonSource); - fragmentSource = InjectPreamble(fragmentSource, commonSource); - } - Program = ShaderProgramConstruction.Build( - new GlShaderProgramBuildApi(gl), - vertexSource, - fragmentSource); - } - - /// - /// Inserts right after the shader's leading - /// #version/#extension/blank-line block. GLSL requires - /// #version to be the very first statement in the source, so the - /// preamble cannot simply be prepended — it has to land after that block, - /// before the first real declaration. - /// - /// Internal rather than private since slice V6d: GlGpuDevice.CreatePipeline - /// loads RHI shader pairs itself and has to splice the same preamble the - /// same way. Two implementations of "where does the preamble go" is exactly - /// the kind of drift that shows up as one shader silently missing a binding. - /// - internal static string InjectPreamble(string source, string preamble) - { - int insertAt = 0; - int lineStart = 0; - while (lineStart < source.Length) - { - int lineEnd = source.IndexOf('\n', lineStart); - if (lineEnd < 0) - lineEnd = source.Length; - string trimmed = source[lineStart..lineEnd].TrimStart(); - bool isLeadingLine = - trimmed.Length == 0 - || trimmed.StartsWith("#version", StringComparison.Ordinal) - || trimmed.StartsWith("#extension", StringComparison.Ordinal); - if (!isLeadingLine) - break; - - insertAt = lineEnd < source.Length ? lineEnd + 1 : lineEnd; - lineStart = lineEnd + 1; - } - - return string.Concat(source.AsSpan(0, insertAt), preamble, "\n", source.AsSpan(insertAt)); - } - - public void Use() => _gl.UseProgram(Program); - - public unsafe void SetMatrix4(string name, Matrix4x4 m) - { - int loc = GetUniformLocation(name); - _gl.UniformMatrix4(loc, 1, false, (float*)&m); - } - - public void SetInt(string name, int value) - { - int loc = GetUniformLocation(name); - _gl.Uniform1(loc, value); - } - - public void SetFloat(string name, float value) - { - int loc = GetUniformLocation(name); - _gl.Uniform1(loc, value); - } - - public void SetVec3(string name, Vector3 v) - { - int loc = GetUniformLocation(name); - _gl.Uniform3(loc, v.X, v.Y, v.Z); - } - - public void SetVec2(string name, Vector2 v) - { - int loc = GetUniformLocation(name); - _gl.Uniform2(loc, v.X, v.Y); - } - - public void SetVec4(string name, Vector4 v) - { - int loc = GetUniformLocation(name); - _gl.Uniform4(loc, v.X, v.Y, v.Z, v.W); - } - - private int GetUniformLocation(string name) - { - ArgumentException.ThrowIfNullOrWhiteSpace(name); - if (_uniformLocations.TryGetValue(name, out int location)) - return location; - - location = _gl.GetUniformLocation(Program, name); - _uniformLocations.Add(name, location); - return location; - } - - public void Dispose() - { - uint program = Program; - if (program == 0) - return; - - _uniformLocations.Clear(); - GlResourceCommand.DeleteProgram(_gl, program, $"delete Shader program {program}"); - Program = 0; - } -} diff --git a/src/AcDream.App/Rendering/ShaderProgramConstruction.cs b/src/AcDream.App/Rendering/ShaderProgramConstruction.cs deleted file mode 100644 index a01f81ab..00000000 --- a/src/AcDream.App/Rendering/ShaderProgramConstruction.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System.Runtime.ExceptionServices; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -internal interface IShaderProgramBuildApi -{ - uint CreateShader(ShaderType type); - void ShaderSource(uint shader, string source); - void CompileShader(uint shader); - int GetShaderCompileStatus(uint shader); - string GetShaderInfoLog(uint shader); - uint CreateProgram(); - void AttachShader(uint program, uint shader); - void LinkProgram(uint program); - int GetProgramLinkStatus(uint program); - string GetProgramInfoLog(uint program); - void DetachShader(uint program, uint shader); - void DeleteShader(uint shader); - void DeleteProgram(uint program); -} - -internal sealed class GlShaderProgramBuildApi(GL gl) : IShaderProgramBuildApi -{ - public uint CreateShader(ShaderType type) => - GlResourceCommand.CreateName( - gl, - $"{type} shader", - () => gl.CreateShader(type), - gl.DeleteShader); - - public void ShaderSource(uint shader, string source) => - GlResourceCommand.Execute( - gl, - $"upload shader source {shader}", - () => gl.ShaderSource(shader, source)); - - public void CompileShader(uint shader) => - GlResourceCommand.Execute( - gl, - $"compile shader {shader}", - () => gl.CompileShader(shader)); - - public void GetShader(uint shader, ShaderParameterName name, out int value) => - gl.GetShader(shader, name, out value); - - public int GetShaderCompileStatus(uint shader) - { - return GlResourceCommand.Execute( - gl, - $"read shader {shader} compile status", - () => - { - gl.GetShader(shader, ShaderParameterName.CompileStatus, out int value); - return value; - }); - } - - public string GetShaderInfoLog(uint shader) => - GlResourceCommand.Execute( - gl, - $"read shader {shader} info log", - () => gl.GetShaderInfoLog(shader)); - - public uint CreateProgram() => - GlResourceCommand.CreateName( - gl, - "shader program", - gl.CreateProgram, - gl.DeleteProgram); - - public void AttachShader(uint program, uint shader) => - GlResourceCommand.Execute( - gl, - $"attach shader {shader} to program {program}", - () => gl.AttachShader(program, shader)); - - public void LinkProgram(uint program) => - GlResourceCommand.Execute( - gl, - $"link shader program {program}", - () => gl.LinkProgram(program)); - - public int GetProgramLinkStatus(uint program) - { - return GlResourceCommand.Execute( - gl, - $"read shader program {program} link status", - () => - { - gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int value); - return value; - }); - } - - public string GetProgramInfoLog(uint program) => - GlResourceCommand.Execute( - gl, - $"read shader program {program} info log", - () => gl.GetProgramInfoLog(program)); - - public void DetachShader(uint program, uint shader) => - GlResourceCommand.Execute( - gl, - $"detach shader {shader} from program {program}", - () => gl.DetachShader(program, shader)); - - public void DeleteShader(uint shader) => - GlResourceCommand.DeleteShader(gl, shader, $"delete shader {shader}"); - - public void DeleteProgram(uint program) => - GlResourceCommand.DeleteProgram(gl, program, $"delete shader program {program}"); -} - -internal sealed class ShaderProgramCleanup(IShaderProgramBuildApi api) - : IRetryableResourceCleanup -{ - public uint Vertex { get; set; } - public uint Fragment { get; set; } - public uint Program { get; set; } - public bool VertexAttached { get; set; } - public bool FragmentAttached { get; set; } - - public bool IsCleanupComplete => Vertex == 0 && Fragment == 0 && Program == 0; - - public uint TransferProgram() - { - if (Vertex != 0 || Fragment != 0 || Program == 0) - throw new InvalidOperationException("The linked program is not ready for ownership transfer."); - uint program = Program; - Program = 0; - return program; - } - - public List Release(bool includeProgram) - { - var failures = new List(); - Try( - () => - { - if (Program != 0 && Vertex != 0 && VertexAttached) - { - api.DetachShader(Program, Vertex); - VertexAttached = false; - } - }, - failures); - Try( - () => - { - if (Program != 0 && Fragment != 0 && FragmentAttached) - { - api.DetachShader(Program, Fragment); - FragmentAttached = false; - } - }, - failures); - Try( - () => - { - if (Vertex != 0) - { - api.DeleteShader(Vertex); - Vertex = 0; - VertexAttached = false; - } - }, - failures); - Try( - () => - { - if (Fragment != 0) - { - api.DeleteShader(Fragment); - Fragment = 0; - FragmentAttached = false; - } - }, - failures); - if (includeProgram) - { - Try( - () => - { - if (Program != 0) - { - api.DeleteProgram(Program); - Program = 0; - VertexAttached = false; - FragmentAttached = false; - } - }, - failures); - } - - return failures; - } - - public List ReleaseProgramOnly() - { - var failures = new List(); - Try( - () => - { - if (Program != 0) - { - api.DeleteProgram(Program); - Program = 0; - VertexAttached = false; - FragmentAttached = false; - } - }, - failures); - return failures; - } - - public void RetryCleanup() - { - List failures = Release(includeProgram: true); - if (!IsCleanupComplete) - { - throw new AggregateException( - "Shader program cleanup remains incomplete.", - failures.Count == 0 - ? [new InvalidOperationException("Shader cleanup retained names without reporting an operation failure.")] - : failures); - } - } - - private static void Try(Action operation, List failures) - { - try - { - operation(); - } - catch (Exception failure) - { - failures.Add(failure); - } - } -} - -internal static class ShaderProgramConstruction -{ - public static uint Build( - IShaderProgramBuildApi api, - string vertexSource, - string fragmentSource) - { - ArgumentNullException.ThrowIfNull(api); - ArgumentNullException.ThrowIfNull(vertexSource); - ArgumentNullException.ThrowIfNull(fragmentSource); - - var cleanup = new ShaderProgramCleanup(api); - Exception? constructionFailure = null; - - try - { - cleanup.Vertex = CreateShader(api, ShaderType.VertexShader); - Compile(api, cleanup.Vertex, ShaderType.VertexShader, vertexSource); - cleanup.Fragment = CreateShader(api, ShaderType.FragmentShader); - Compile(api, cleanup.Fragment, ShaderType.FragmentShader, fragmentSource); - cleanup.Program = api.CreateProgram(); - if (cleanup.Program == 0) - throw new InvalidOperationException("OpenGL returned no shader program name."); - - api.AttachShader(cleanup.Program, cleanup.Vertex); - cleanup.VertexAttached = true; - api.AttachShader(cleanup.Program, cleanup.Fragment); - cleanup.FragmentAttached = true; - api.LinkProgram(cleanup.Program); - if (api.GetProgramLinkStatus(cleanup.Program) == 0) - throw new InvalidOperationException( - "program link failed: " + api.GetProgramInfoLog(cleanup.Program)); - } - catch (Exception failure) - { - constructionFailure = failure; - } - - List cleanupFailures = cleanup.Release(includeProgram: false); - - if (constructionFailure is null && cleanupFailures.Count == 0) - return cleanup.TransferProgram(); - - cleanupFailures.AddRange(cleanup.ReleaseProgramOnly()); - - if (constructionFailure is not null && cleanupFailures.Count == 0) - ExceptionDispatchInfo.Capture(constructionFailure).Throw(); - - var failures = new List(cleanupFailures.Count + 1); - if (constructionFailure is not null) - failures.Add(constructionFailure); - failures.AddRange(cleanupFailures); - const string message = - "Shader program construction failed and one or more temporary OpenGL names could not be cleanly released."; - if (!cleanup.IsCleanupComplete) - throw new GlResourceConstructionException(message, cleanup, failures); - throw new AggregateException(message, failures); - } - - private static uint CreateShader( - IShaderProgramBuildApi api, - ShaderType type) - { - uint shader = api.CreateShader(type); - if (shader == 0) - throw new InvalidOperationException($"OpenGL returned no {type} name."); - return shader; - } - - private static void Compile( - IShaderProgramBuildApi api, - uint shader, - ShaderType type, - string source) - { - api.ShaderSource(shader, source); - api.CompileShader(shader); - if (api.GetShaderCompileStatus(shader) == 0) - throw new InvalidOperationException( - $"{type} compile failed: " + api.GetShaderInfoLog(shader)); - } -} diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index 058beb7a..38cedc8f 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -9,7 +9,6 @@ using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Sky; @@ -43,46 +42,11 @@ namespace AcDream.App.Rendering.Sky; /// measured clockwise from north. /// /// -public sealed unsafe partial class SkyRenderer : IDisposable +public sealed partial class SkyRenderer : IDisposable { - private readonly GL? _gl; private readonly IDatReaderWriter _dats; - private readonly Shader? _shader; private readonly TextureCache _textures; - private readonly SamplerCache? _samplers; - // Campaign V slice V6e. Two GL objects replace what used to be a run of - // glUniform* calls and a texture/sampler binding on unit 0: - // - // _paramsUbo — the std140 SkyParams block both stages declare. - // _uTextureIndexALoc — the push-constant-named uniform carrying the slot. - // - // The wrap mode is what makes the (texture, wrap) pairing necessary. A - // bindless handle BAKES its sampler state, so the per-submesh - // Repeat-vs-ClampToEdge choice that used to be a glBindSampler call has to be - // a different handle — which is also exactly how the Vulkan table works, - // where an entry is a combined image sampler. ManagedGLTextureArray has - // interned wrap/clamp handle pairs the same way since the world path went - // bindless. - // - // Campaign V slice V6k retired the interim per-renderer GlBindlessHandleTable - // — the last one in the tree — for the device's own retirement-gated table, - // exactly as V4t did for the other four world renderers. The handles are - // still minted here, because the sky is the one world path whose textures it - // produces itself; only the slot allocator moved. - private readonly Wb.BindlessSupport? _bindless; - - /// - /// Campaign V slice V6k: the GL arm's slot allocator. The sky mints its own - /// resident handles — it is the one world path that does — so it registers - /// them into the device's table through V4t's world-handle seam instead of - /// keeping the last private GlBindlessHandleTable in the tree. - /// - private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? _glDevice; - - private readonly Dictionary<(uint Texture, bool Repeat), ulong> _handleByTextureAndWrap = new(); - private uint _paramsUbo; - private int _uTextureIndexALoc = -1; private SkyParams _params; // Lazily-built GPU resources per sky-GfxObj. @@ -125,40 +89,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable public float Near { get; set; } = 0.1f; public float Far { get; set; } = 1_000_000f; - internal SkyRenderer( - GL gl, - IDatReaderWriter dats, - Shader shader, - TextureCache textures, - SamplerCache samplers, - Wb.BindlessSupport bindless, - AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _dats = dats ?? throw new ArgumentNullException(nameof(dats)); - _shader = shader ?? throw new ArgumentNullException(nameof(shader)); - _textures = textures ?? throw new ArgumentNullException(nameof(textures)); - _samplers = samplers ?? throw new ArgumentNullException(nameof(samplers)); - _bindless = bindless ?? throw new ArgumentNullException(nameof(bindless)); - _glDevice = device ?? throw new ArgumentNullException(nameof(device)); - - _uTextureIndexALoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexA"); - - _paramsUbo = Wb.TrackedGlResource.CreateBuffer(_gl, "sky params UBO creation"); - // Fixed size: SkyParams is a 256-byte std140 struct, so the buffer never - // grows and every write is one whole-struct BufferSubData. - _gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo); - Wb.TrackedGlResource.AllocateBufferStorage( - _gl, - BufferTargetARB.UniformBuffer, - _paramsUbo, - 0, - SkyParams.SizeInBytes, - BufferUsageARB.DynamicDraw, - "allocating sky params UBO"); - _gl.BindBuffer(BufferTargetARB.UniformBuffer, 0); - } - /// /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds — /// every SkyObject with Properties & 0x04 == 0). @@ -265,7 +195,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable skyView.M42 = 0f; skyView.M43 = 0f; - _shader?.Use(); // Campaign V slice V6e: the values below used to be individual // glUniform* calls. They now populate the std140 SkyParams block, which // is uploaded once per submesh right before its draw — the same cadence @@ -283,34 +212,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable _params.SunDir = AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(keyframe); - bool wasCullFace = false; - if (_gl is { } gl) - { - gl.BindBufferBase( - BufferTargetARB.UniformBuffer, - AcDream.App.Rendering.Gpu.GpuBindingModel.UniformSkyParams, - _paramsUbo); - - // Save + override GL state. - gl.DepthMask(false); - gl.Disable(EnableCap.DepthTest); - // Save + disable CullFace for the sky pass; restore at the end. - // Mirrors TextRenderer.cs's save/restore pattern. Without this the - // sky pass left CullFace disabled regardless of its prior state, - // which is benign today (the global convention in this codebase is - // off and subsequent renderers manage their own CullFace) but - // would break the moment any future caller assumes back-face - // culling stays on across the sky pass. - wasCullFace = gl.IsEnabled(EnableCap.CullFace); - gl.Disable(EnableCap.CullFace); - gl.Enable(EnableCap.Blend); - // Default blend — overridden per-submesh inside the inner loop. - // Additive surfaces (sun/moon/stars via SurfaceType.Additive = - // 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome - // with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA. - gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - } - // Look up the keyframe's override list so we can apply // SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation // override + transparency fade + luminosity cap. @@ -434,14 +335,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable // mesh pipeline where Surface flags dictate state. On the RHI // arm the same two blend functions are two pipelines, because // Vulkan bakes blend rather than making it dynamic. - if (_gl is { } blendGl) - { - if (sub.IsAdditive) - blendGl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); - else - blendGl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - } - // Emissive source picks the surface's authored Luminosity by // default; the per-keyframe replace data can OVERRIDE // (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches @@ -532,46 +425,9 @@ public sealed unsafe partial class SkyRenderer : IDisposable || obj.TexVelocityX != 0f || obj.TexVelocityY != 0f; uint slot = TextureTableSlot(sub.SurfaceId, needsRepeat); - - if (_gl is { } drawGl) - { - drawGl.ProgramUniform1( - _shader!.Program, - _uTextureIndexALoc, - slot); - - UploadParams(); - FlushAndBindTextureTable(); - - drawGl.BindVertexArray(sub.Vao); - drawGl.DrawElements(PrimitiveType.Triangles, - (uint)sub.IndexCount, - DrawElementsType.UnsignedInt, - (void*)0); - } - else - { - DrawSubMeshRhi(sub, slot); - } + DrawSubMeshRhi(sub, slot); } } - - // Restore GL state expected by the rest of the pipeline. - // - // Slice V6e removed the glBindSampler(0, …) that used to happen here - // and its matching unbind. The sky no longer touches texture unit 0 at - // all — its wrap mode rides inside the bindless handle — so there is - // nothing left to leak onto the next renderer's unit 0. That unbind was - // load-bearing precisely because the binding was global state; a table - // slot is not. - if (_gl is { } restoreGl) - { - restoreGl.Disable(EnableCap.Blend); - restoreGl.DepthMask(true); - restoreGl.Enable(EnableCap.DepthTest); - if (wasCullFace) restoreGl.Enable(EnableCap.CullFace); - restoreGl.BindVertexArray(0); - } } /// @@ -588,57 +444,8 @@ public sealed unsafe partial class SkyRenderer : IDisposable /// handle table in the codebase — the sky's texture set is a fixed handful /// per day group and does not churn. /// - private uint TextureTableSlot(uint surfaceId, bool repeat) - { - if (_gl is null) - return RhiTextureTableSlot(surfaceId, repeat); - - uint textureName = _textures.GetOrUpload(surfaceId); - var key = (textureName, repeat); - if (!_handleByTextureAndWrap.TryGetValue(key, out ulong handle)) - { - handle = _bindless!.GetResidentHandle( - textureName, - repeat ? _samplers!.Wrap : _samplers!.Clamp); - _handleByTextureAndWrap.Add(key, handle); - } - - return _glDevice!.RegisterWorldTextureHandle(handle).Index; - } - - /// - /// Writes the current to its uniform buffer. Called - /// immediately before each draw, which is the cadence the per-submesh - /// glUniform* calls it replaces already had. - /// - private void UploadParams() - { - GL gl = _gl!; - fixed (void* p = &_params) - { - gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo); - gl.BufferSubData( - BufferTargetARB.UniformBuffer, 0, (nuint)SkyParams.SizeInBytes, p); - } - } - - /// - /// Campaign V slice V6k: drains the device texture table's dirty runs and - /// (re)binds it at - /// . - /// Mirrors ParticleRenderer.FlushAndBindTextureTable, including its - /// reason for running per draw rather than per pass: a submesh partway - /// through the sky stack can be the first to ask for a wrap mode. - /// - private void FlushAndBindTextureTable() - { - AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = _glDevice!; - device.FlushTextureTable(); - _gl!.BindBufferBase( - BufferTargetARB.ShaderStorageBuffer, - AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable, - device.TextureTableGlName); - } + private uint TextureTableSlot(uint surfaceId, bool repeat) => + RhiTextureTableSlot(surfaceId, repeat); /// /// Find the entries for the @@ -865,106 +672,9 @@ public sealed unsafe partial class SkyRenderer : IDisposable } } - private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm) - { - if (_gl is null) - return UploadSubMeshRhi(sm); + private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm) => UploadSubMeshRhi(sm); - uint vao = _gl.GenVertexArray(); - _gl.BindVertexArray(vao); - - uint vbo = _gl.GenBuffer(); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo); - fixed (void* p = sm.Vertices) - _gl.BufferData(BufferTargetARB.ArrayBuffer, - (nuint)(sm.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw); - - uint ebo = _gl.GenBuffer(); - _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo); - fixed (void* p = sm.Indices) - _gl.BufferData(BufferTargetARB.ElementArrayBuffer, - (nuint)(sm.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw); - - uint stride = (uint)sizeof(Vertex); - _gl.EnableVertexAttribArray(0); - _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0); - _gl.EnableVertexAttribArray(1); - _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); - _gl.EnableVertexAttribArray(2); - _gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float))); - - _gl.BindVertexArray(0); - - // Classify blend mode from the Surface's flags. Sun/moon/stars with - // `SurfaceType.Additive = 0x10000` get GL_ONE / GL_ONE (their texture - // has a black background and a bright body; additive makes the - // background contribute nothing and the body glow on top of the sky). - // - // NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40` - // as additive, but that flag is present on the sky DOME itself and - // on cloud sheets — turning those additive blew the whole sky to - // white. `Luminous` means "self-illuminated / unshaded" in retail's - // render pipeline, not "additive blend". Only the Additive bit - // toggles the blend mode. - bool isAdditive = sm.Translucency == TranslucencyKind.Additive; - - return new SubMeshGpu - { - Vao = vao, - Vbo = vbo, - Ebo = ebo, - IndexCount = sm.Indices.Length, - SurfaceId = sm.SurfaceId, - IsAdditive = isAdditive, - SurfLuminosity = sm.Luminosity, - SurfDiffuse = sm.Diffuse, - NeedsUvRepeat = sm.NeedsUvRepeat, - SurfOpacity = sm.SurfOpacity, - DisableFog = sm.DisableFog, - }; - } - - public void Dispose() - { - if (_gl is null) - { - DisposeRhi(); - return; - } - - foreach (var subs in _gpuByGfxObj.Values) - { - foreach (var sub in subs) - { - _gl.DeleteBuffer(sub.Vbo); - _gl.DeleteBuffer(sub.Ebo); - _gl.DeleteVertexArray(sub.Vao); - } - } - _gpuByGfxObj.Clear(); - - // Campaign V slice V6e. Residency first: a handle made resident pins - // its texture's GPU address, and the textures themselves belong to - // TextureCache, which outlives this renderer and disposes them itself. - // - // Slice V6k: the device's table entry is retired alongside it, in the - // order V4t established — the table entry goes first, because the slot - // is only recycled once the frames that could still read it retire, - // whereas the handle stops being valid the instant it is non-resident. - foreach (ulong handle in _handleByTextureAndWrap.Values) - { - _glDevice!.ReleaseWorldTextureHandle(handle); - _bindless!.MakeNonResident(handle); - } - _handleByTextureAndWrap.Clear(); - - if (_paramsUbo != 0) - { - Wb.TrackedGlResource.DeleteBuffer( - _gl, _paramsUbo, SkyParams.SizeInBytes, "sky params UBO disposal"); - _paramsUbo = 0; - } - } + public void Dispose() => DisposeRhi(); /// /// Campaign V slice V6e: the CPU mirror of sky.{vert,frag}'s SkyParams @@ -999,14 +709,11 @@ public sealed unsafe partial class SkyRenderer : IDisposable private sealed class SubMeshGpu { - public uint Vao; - public uint Vbo; - public uint Ebo; /// /// Campaign V slice V6k: the RHI arm's vertex source. The sky's meshes are /// built once per GfxObj and never change, so each submesh owns a /// device-local buffer pair rather than taking a ring slice per frame. - /// Null on the GL arm, which uses . + /// The raw-GL arm's VAO/VBO/EBO names were deleted at slice V11. /// public AcDream.App.Rendering.Gpu.IGpuBuffer? VertexBuffer; public AcDream.App.Rendering.Gpu.IGpuBuffer? IndexBuffer; diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs index 1b4286aa..64a73db6 100644 --- a/src/AcDream.App/Rendering/TerrainAtlas.cs +++ b/src/AcDream.App/Rendering/TerrainAtlas.cs @@ -1,13 +1,10 @@ using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; using AcDream.Core.Textures; using DatReaderWriter; using AcDream.Content; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; -using Silk.NET.OpenGL; using DatPixelFormat = DatReaderWriter.Enums.PixelFormat; -using GLPixelFormat = Silk.NET.OpenGL.PixelFormat; namespace AcDream.App.Rendering; @@ -29,12 +26,8 @@ namespace AcDream.App.Rendering; /// The alpha atlas is built but not yet sampled by any shader — that wiring /// lands in Phase 3c.4 along with the shader rewrite. /// -public sealed unsafe class TerrainAtlas : IDisposable +public sealed class TerrainAtlas : IDisposable { - private readonly GL? _gl; - - // --- Terrain atlas (unchanged public API from Phase 2b) --- - public uint GlTexture { get; } // terrain atlas, kept as GlTexture for back-compat with TerrainRenderer public IReadOnlyDictionary TerrainTypeToLayer { get; } public int LayerCount { get; } /// @@ -45,7 +38,6 @@ public sealed unsafe class TerrainAtlas : IDisposable public IReadOnlyList TilingByLayer { get; } // --- Alpha atlas (new in Phase 3c.2) --- - public uint GlAlphaTexture { get; } public int AlphaLayerCount { get; } /// Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries). public IReadOnlyList CornerAlphaLayers { get; } @@ -62,77 +54,12 @@ public sealed unsafe class TerrainAtlas : IDisposable /// RCode for each RoadMap, parallel to . public IReadOnlyList RoadAlphaRCodes { get; } - private readonly Wb.BindlessSupport? _bindless; - private readonly BindlessTexturePair? _bindlessHandles; - private readonly BindlessTextureMutationGuard? _bindlessMutation; - private readonly RestoredTextureBindingMutation _anisotropyBindingMutation = new(); - private ResourceShutdownTransaction? _shutdown; - - private ulong _registeredTerrainHandle; - private ulong _registeredAlphaHandle; - private GpuTextureSlot _terrainSlot = GpuTextureSlot.Unassigned; - private GpuTextureSlot _alphaSlot = GpuTextureSlot.Unassigned; - /// - /// Campaign V slice V4t: the device texture-table slots for the terrain and - /// alpha arrays — the backend-neutral replacement for the raw 64-bit - /// ARB_bindless_texture handles this used to return. Residency still - /// belongs to this atlas (acquired lazily here, released by - /// ); the device owns only the two table entries. - /// - /// Throws if the atlas was - /// constructed without a instance. - /// - /// makes both textures non-resident and - /// re-acquires them, which yields new handles. Re-registering is therefore - /// conditional on the handle actually having changed, and the superseded - /// table entry is retired in the same step — otherwise a quality-preset - /// change would leak a slot holding a non-resident handle. Registration is - /// idempotent, so the common per-draw call is two dictionary lookups. - /// - internal (GpuTextureSlot Terrain, GpuTextureSlot Alpha) GetTextureSlots(GlGpuDevice device) - { - ArgumentNullException.ThrowIfNull(device); - if (_rhi is not null) - { - throw new InvalidOperationException( - "This TerrainAtlas owns IGpuTexture arrays; ask it for TextureSlots, not for GL handles."); - } - - if (_bindless is null) - throw new InvalidOperationException( - "TerrainAtlas was constructed without BindlessSupport; cannot return texture slots."); - - (ulong terrain, ulong alpha) = _bindlessHandles!.Acquire(); - if (terrain != _registeredTerrainHandle) - { - device.ReleaseWorldTextureHandle(_registeredTerrainHandle); - _terrainSlot = device.RegisterWorldTextureHandle(terrain); - _registeredTerrainHandle = terrain; - } - if (alpha != _registeredAlphaHandle) - { - device.ReleaseWorldTextureHandle(_registeredAlphaHandle); - _alphaSlot = device.RegisterWorldTextureHandle(alpha); - _registeredAlphaHandle = alpha; - } - return (_terrainSlot, _alphaSlot); - } - - /// - /// Campaign V slice V6i-2: the backend-neutral arm. When present, both - /// arrays are s the device created and both slots - /// were registered at build time, so is a field - /// read rather than a residency negotiation. and - /// are 0 and no GL handle exists at all. - /// - /// V4t moved the TABLE ENTRY to the device but left CREATION with this - /// class, which is exactly the remainder plan §5.5.11 recorded: "Creating - /// world textures through IGpuTexture is real remaining work and it belongs - /// with the Vulkan world arm, which is the first thing that cannot use a GL - /// handle at all." This is that work, expressed as a second construction - /// path rather than a rewrite, so the GL path's calls are textually - /// unchanged. + /// Campaign V slice V6i-2: both arrays are s the + /// device created, and both slots were registered at build time, so + /// is a field read rather than a residency + /// negotiation. The GL construction path this used to sit alongside is + /// deleted as of Campaign V slice V11. /// private sealed class RhiArrays( IGpuDevice device, @@ -149,30 +76,19 @@ public sealed unsafe class TerrainAtlas : IDisposable public GpuTextureSlot AlphaSlot { get; set; } = GpuTextureSlot.Unassigned; } - private readonly RhiArrays? _rhi; + private readonly RhiArrays _rhi; /// - /// True when this atlas owns arrays rather than raw - /// GL names. The two construction paths are mutually exclusive. - /// - internal bool IsBackendNeutral => _rhi is not null; - - /// - /// The device-table slots for the terrain and alpha arrays on the - /// backend-neutral arm. Registered once at build time and re-registered only - /// by , which changes the sampler. + /// The device-table slots for the terrain and alpha arrays. Registered once + /// at build time and re-registered only by , + /// which changes the sampler. /// internal (GpuTextureSlot Terrain, GpuTextureSlot Alpha) TextureSlots => - _rhi is null - ? throw new InvalidOperationException( - "This TerrainAtlas owns GL names; ask it for GetTextureSlots(GlGpuDevice).") - : (_rhi.TerrainSlot, _rhi.AlphaSlot); + (_rhi.TerrainSlot, _rhi.AlphaSlot); /// /// Retail's terrain arrays are trilinear-filtered with the highest anisotropy - /// the quality preset allows; lowers it. The GL - /// path sets GL_TEXTURE_MAX_ANISOTROPY to 16 at build time, so the - /// backend-neutral path starts at the same value. + /// the quality preset allows; lowers it. /// private const float RetailMaxAnisotropy = 16f; @@ -192,11 +108,7 @@ public sealed unsafe class TerrainAtlas : IDisposable IReadOnlyList sideTCodes, IReadOnlyList roadRCodes) { - _gl = null; - _bindless = null; _rhi = new RhiArrays(device, terrain, alpha, alphaSampler); - GlTexture = 0; - GlAlphaTexture = 0; TerrainTypeToLayer = map; LayerCount = layerCount; TilingByLayer = tilingByLayer; @@ -211,72 +123,6 @@ public sealed unsafe class TerrainAtlas : IDisposable ApplyAnisotropic(RetailMaxAnisotropy); } - private TerrainAtlas( - GL gl, - Wb.BindlessSupport? bindless, - uint glTexture, IReadOnlyDictionary map, int layerCount, - IReadOnlyList tilingByLayer, - uint glAlphaTexture, int alphaLayerCount, - IReadOnlyList cornerLayers, IReadOnlyList sideLayers, IReadOnlyList roadLayers, - IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList roadRCodes) - { - _gl = gl; - _bindless = bindless; - GlTexture = glTexture; - TerrainTypeToLayer = map; - LayerCount = layerCount; - TilingByLayer = tilingByLayer; - GlAlphaTexture = glAlphaTexture; - AlphaLayerCount = alphaLayerCount; - CornerAlphaLayers = cornerLayers; - SideAlphaLayers = sideLayers; - RoadAlphaLayers = roadLayers; - CornerAlphaTCodes = cornerTCodes; - SideAlphaTCodes = sideTCodes; - RoadAlphaRCodes = roadRCodes; - if (bindless is not null) - { - _bindlessHandles = new BindlessTexturePair( - glTexture, - glAlphaTexture, - bindless.GetResidentHandle, - bindless.MakeNonResident); - _bindlessMutation = new BindlessTextureMutationGuard(_bindlessHandles); - } - } - - /// - /// Build the atlas by walking Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc - /// for the mapping from TerrainTextureType to SurfaceTexture id, decoding each - /// to RGBA8, and uploading as layers in a single GL_TEXTURE_2D_ARRAY. - /// - public static TerrainAtlas Build(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null) - { - var textures = new GlTextureConstructionTransaction(new GlTextureNameApi(gl)); - try - { - TerrainAtlas atlas = BuildCore(gl, dats, bindless, textures); - textures.Commit(); - return atlas; - } - catch (Exception constructionFailure) - { - try - { - textures.Rollback(); - } - catch (Exception cleanupFailure) - { - throw new GlResourceConstructionException( - "TerrainAtlas construction failed and its allocated OpenGL texture names did not cleanly roll back.", - textures, - [constructionFailure, cleanupFailure]); - } - - throw; - } - } - /// /// Campaign V slice V6i-2: the decode both construction paths share. /// Splitting it out is what keeps the CPU logic single while the upload @@ -340,108 +186,6 @@ public sealed unsafe class TerrainAtlas : IDisposable return new TerrainLayerDecode(decodedByType, tilingByType, maxW, maxH); } - private static TerrainAtlas BuildCore( - GL gl, - IDatReaderWriter dats, - Wb.BindlessSupport? bindless, - GlTextureConstructionTransaction textures) - { - var region = dats.Get(0x13000000u) - ?? throw new InvalidOperationException("Region dat id 0x13000000 missing"); - - var texMerge = region.TerrainInfo?.LandSurfaces?.TexMerge; - var terrainDesc = texMerge?.TerrainDesc; - if (terrainDesc is null || terrainDesc.Count == 0) - { - Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer"); - return BuildFallback(gl, bindless, textures); - } - - // ---- Terrain atlas (unchanged Phase 2b logic) ---- - TerrainLayerDecode decode = DecodeTerrainLayers(dats, terrainDesc); - Dictionary decodedByType = decode.DecodedByType; - Dictionary tilingByType = decode.TilingByType; - int maxW = decode.MaxWidth, maxH = decode.MaxHeight; - - int layerCount = decodedByType.Count; - var map = new Dictionary(); - uint tex = TrackedTextureConstruction.Create( - textures, - gl, - "upload terrain atlas texture array", - texture => - { - gl.BindTexture(TextureTarget.Texture2DArray, texture); - gl.TexImage3D( - TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, - (uint)maxW, (uint)maxH, (uint)layerCount, - 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); - - int layerIdx = 0; - foreach (var kvp in decodedByType) - { - byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH); - fixed (byte* p = buffer) - { - gl.TexSubImage3D( - TextureTarget.Texture2DArray, 0, - 0, 0, layerIdx, - (uint)maxW, (uint)maxH, 1, - GLPixelFormat.Rgba, PixelType.UnsignedByte, p); - } - map[kvp.Key] = (uint)layerIdx; - layerIdx++; - } - - // A.5 T19: generate mipmaps + trilinear + 16x anisotropic for distant-LB quality. - gl.GenerateMipmap(TextureTarget.Texture2DArray); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); - // GL_TEXTURE_MAX_ANISOTROPY = 0x84FE (GL_EXT_texture_filter_anisotropic / ARB_texture_filter_anisotropic). - gl.TexParameter(TextureTarget.Texture2DArray, (TextureParameterName)0x84FE, 16.0f); - gl.BindTexture(TextureTarget.Texture2DArray, 0); - }); - - var tilingByLayer = TerrainTextureTilingTable.Build( - map.Select(entry => - (entry.Value, tilingByType.TryGetValue(entry.Key, out uint repeatCount) - ? repeatCount - : 1u))); - - Console.WriteLine($"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH} (mipmaps+aniso16x)"); - - // ---- Alpha atlas (new in Phase 3c.2) ---- - // texMerge is guaranteed non-null here: the early return above exited - // if texMerge?.TerrainDesc was null. - var alphaBuild = BuildAlphaAtlas(gl, dats, texMerge!, textures); - - return new TerrainAtlas( - gl, - bindless, - tex, map, layerCount, tilingByLayer, - alphaBuild.gl, alphaBuild.layerCount, - alphaBuild.corner, alphaBuild.side, alphaBuild.road, - alphaBuild.cornerTCodes, alphaBuild.sideTCodes, alphaBuild.roadRCodes); - } - - /// - /// Load corner, side, and road alpha maps from the TexMerge into a second - /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures; - /// expands each alpha byte - /// into all four RGBA channels so the shader can sample from any channel. - /// - /// Layers are appended in TexMerge insertion order: corners first, then - /// sides, then roads. The returned index lists tell - /// TerrainBlending.BuildSurface which layer to cite for each - /// corner/side/road alpha source. - /// - private readonly record struct AlphaAtlasBuildResult( - uint gl, int layerCount, - IReadOnlyList corner, IReadOnlyList side, IReadOnlyList road, - IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList roadRCodes); - /// /// Slice V6i-2: the alpha-map decode, shared by both construction paths for /// the same reason is. @@ -530,100 +274,12 @@ public sealed unsafe class TerrainAtlas : IDisposable decodedMaxH); } - private static AlphaAtlasBuildResult BuildAlphaAtlas( - GL gl, - IDatReaderWriter dats, - DatReaderWriter.Types.TexMerge texMerge, - GlTextureConstructionTransaction textures) - { - AlphaLayerDecode decode = DecodeAlphaLayers(dats, texMerge); - List decoded = decode.Decoded; - List cornerLayers = decode.CornerLayers; - List sideLayers = decode.SideLayers; - List roadLayers = decode.RoadLayers; - List cornerTCodes = decode.CornerTCodes; - List sideTCodes = decode.SideTCodes; - List roadRCodes = decode.RoadRCodes; - - if (decoded.Count == 0) - { - Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback"); - uint fallbackAlpha = TrackedTextureConstruction.Create( - textures, - gl, - "upload fallback terrain alpha texture array", - texture => - { - gl.BindTexture(TextureTarget.Texture2DArray, texture); - gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, - GLPixelFormat.Rgba, PixelType.UnsignedByte, null); - var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; - fixed (byte* p = white) - gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, - GLPixelFormat.Rgba, PixelType.UnsignedByte, p); - gl.BindTexture(TextureTarget.Texture2DArray, 0); - }); - return new AlphaAtlasBuildResult( - fallbackAlpha, 1, - cornerLayers, sideLayers, roadLayers, - cornerTCodes, sideTCodes, roadRCodes); - } - - int aMaxW = decode.MaxWidth, aMaxH = decode.MaxHeight; - - uint glAlpha = TrackedTextureConstruction.Create( - textures, - gl, - "upload terrain alpha texture array", - texture => - { - gl.BindTexture(TextureTarget.Texture2DArray, texture); - gl.TexImage3D( - TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, - (uint)aMaxW, (uint)aMaxH, (uint)decoded.Count, - 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); - - for (int i = 0; i < decoded.Count; i++) - { - var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH); - fixed (byte* p = buffer) - { - gl.TexSubImage3D( - TextureTarget.Texture2DArray, 0, - 0, 0, i, - (uint)aMaxW, (uint)aMaxH, 1, - GLPixelFormat.Rgba, PixelType.UnsignedByte, p); - } - } - - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); - gl.BindTexture(TextureTarget.Texture2DArray, 0); - }); - - Console.WriteLine( - $"AlphaAtlas: {decoded.Count} layers at {aMaxW}x{aMaxH} " - + $"(corners={cornerLayers.Count}, sides={sideLayers.Count}, roads={roadLayers.Count})"); - - return new AlphaAtlasBuildResult( - glAlpha, decoded.Count, - cornerLayers, sideLayers, roadLayers, - cornerTCodes, sideTCodes, roadRCodes); - } - /// - /// Campaign V slice V6i-2: build both arrays through - /// . - /// - /// Same DAT reads, same decode, same layer ordering and the same - /// resize-to-max policy as — only the upload differs, - /// which is the whole point of splitting the decode out. The terrain array + /// Builds both arrays through . The terrain array /// gets a full mip chain ( blits /// it, because RGBA8 is a legal blit destination) and a repeat/anisotropic - /// sampler; the alpha array is single-level and clamped, exactly as the GL - /// texture parameters say. + /// sampler; the alpha array is single-level and clamped, matching what the + /// deleted GL path's texture parameters said. /// internal static TerrainAtlas BuildBackendNeutral(IGpuDevice device, IDatReaderWriter dats) { @@ -826,151 +482,27 @@ public sealed unsafe class TerrainAtlas : IDisposable return dst; } - private static TerrainAtlas BuildFallback( - GL gl, - Wb.BindlessSupport? bindless, - GlTextureConstructionTransaction textures) - { - var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; - uint tex = TrackedTextureConstruction.Create( - textures, - gl, - "upload fallback terrain texture array", - texture => - { - gl.BindTexture(TextureTarget.Texture2DArray, texture); - gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); - fixed (byte* p = white) - gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - gl.BindTexture(TextureTarget.Texture2DArray, 0); - }); - - // Fallback alpha atlas: 1x1 white, no layers tracked - uint alphaTex = TrackedTextureConstruction.Create( - textures, - gl, - "upload fallback alpha texture array", - texture => - { - gl.BindTexture(TextureTarget.Texture2DArray, texture); - gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); - fixed (byte* p = white) - gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); - gl.BindTexture(TextureTarget.Texture2DArray, 0); - }); - - return new TerrainAtlas( - gl, - bindless, - tex, new Dictionary { [0] = 0u }, 1, - TerrainTextureTilingTable.Build(Array.Empty<(uint Layer, uint RepeatCount)>()), - alphaTex, 1, - Array.Empty(), Array.Empty(), Array.Empty(), - Array.Empty(), Array.Empty(), Array.Empty()); - } - /// - /// A.5 T22.5: update GL_TEXTURE_MAX_ANISOTROPY on the terrain atlas at - /// runtime (called by + /// Update terrain-array anisotropy at runtime (called by /// when /// the user changes Quality preset mid-session). Idempotent — calling with /// the same level as the current setting is safe and produces no visual - /// change. The texture must not be resident-bindless when its parameters - /// are mutated; we temporarily make it non-resident if needed. + /// change. /// public void SetAnisotropic(int level) { - if (_rhi is not null) - { - ApplyAnisotropic(level); - Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x"); - return; - } - - GL gl = RequireGl(); - void Mutate() - { - _anisotropyBindingMutation.Execute( - () => unchecked((uint)GlResourceCommand.Execute( - gl, - "read terrain-array binding before anisotropy mutation", - () => gl.GetInteger(GetPName.TextureBinding2DArray))), - binding => GlResourceCommand.Execute( - gl, - "set terrain-array binding for anisotropy mutation", - () => gl.BindTexture(TextureTarget.Texture2DArray, binding)), - GlTexture, - () => GlResourceCommand.Execute( - gl, - "set terrain atlas anisotropy", - () => - { - // GL_TEXTURE_MAX_ANISOTROPY = 0x84FE - gl.TexParameter( - TextureTarget.Texture2DArray, - (TextureParameterName)0x84FE, - (float)level); - })); - } - - if (_bindlessMutation is not null) - _bindlessMutation.Execute(Mutate); - else - Mutate(); - + ApplyAnisotropic(level); Console.WriteLine($"TerrainAtlas: anisotropic updated to {level}x"); } - /// - /// Slice V6i-2: the GL arm's context. The two construction paths are - /// mutually exclusive, so a null here means a backend-neutral atlas reached - /// GL-only code — a programming error, not a runtime condition. - /// - private GL RequireGl() => - _gl ?? throw new InvalidOperationException( - "This TerrainAtlas owns IGpuTexture arrays and has no GL context."); - public void Dispose() { - if (_rhi is not null) - { - // Slice V4t's teardown rule holds on both arms: the device dies with - // its callers, so the table entries are not released here — - // deferring through a possibly-disposed retirement queue would turn - // a clean shutdown into a throw. The images themselves route through - // the device's retirement queue, which is what IGpuTexture.Dispose - // does. - _rhi.Alpha.Dispose(); - _rhi.Terrain.Dispose(); - return; - } - - _shutdown ??= new ResourceShutdownTransaction( - new ResourceShutdownStage( - "terrain atlas bindless residency", - [ - new ResourceShutdownOperation( - "release terrain and alpha handles", - () => _bindlessHandles?.Release()), - ]), - new ResourceShutdownStage( - "terrain atlas textures", - [ - new ResourceShutdownOperation( - "delete terrain texture", - () => GlResourceCommand.DeleteTexture( - RequireGl(), - GlTexture, - $"delete terrain atlas texture {GlTexture}")), - new ResourceShutdownOperation( - "delete alpha texture", - () => GlResourceCommand.DeleteTexture( - RequireGl(), - GlAlphaTexture, - $"delete terrain alpha texture {GlAlphaTexture}")), - ])); - _shutdown.CompleteOrThrow(); + // Slice V4t's teardown rule: the device dies with its callers, so the + // table entries are not released here — deferring through a + // possibly-disposed retirement queue would turn a clean shutdown into a + // throw. The images themselves route through the device's retirement + // queue, which is what IGpuTexture.Dispose does. + _rhi.Alpha.Dispose(); + _rhi.Terrain.Dispose(); } } diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs index 15cd3f7a..9840788f 100644 --- a/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs +++ b/src/AcDream.App/Rendering/TerrainModernRenderer.Rhi.cs @@ -292,7 +292,6 @@ public sealed unsafe partial class TerrainModernRenderer throw; } _tilingBuffer = buffer; - _textureTilingUploaded = true; } encoder.BindUniformBuffer( diff --git a/src/AcDream.App/Rendering/TerrainModernRenderer.cs b/src/AcDream.App/Rendering/TerrainModernRenderer.cs index e28c3a2f..a9a91cfe 100644 --- a/src/AcDream.App/Rendering/TerrainModernRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainModernRenderer.cs @@ -1,23 +1,23 @@ using System.Numerics; using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// -/// Phase N.5b modern terrain dispatcher. Single global VBO/EBO with a slot -/// allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 bytes -/// per slot). Per-frame: build a DrawElementsIndirectCommand array from -/// visible slots, upload, dispatch via glMultiDrawElementsIndirect. Atlas -/// textures bound via bindless handles set per-frame as sampler uniforms. +/// Phase N.5b modern terrain dispatcher. Single global vertex/index arena with +/// a slot allocator (one slot per landblock, 384 verts × 40 bytes = 15,360 +/// bytes per slot). Per-frame: build a DrawElementsIndirectCommand array from +/// visible slots and dispatch via one multi-draw-indirect call. Atlas +/// textures bound via the device's global texture table. /// -/// Total ~6-8 GL calls per frame for terrain regardless of visible -/// landblock count. +/// Campaign V slice V11 deleted the raw-GL submission arm +/// (TerrainModernRenderer.cs's former GL fields/constructor/Draw/Dispose +/// bodies); the RHI arm this file now exclusively hosts the shared allocator +/// and visibility logic for is defined in TerrainModernRenderer.Rhi.cs. /// -public sealed unsafe partial class TerrainModernRenderer : IDisposable +public sealed partial class TerrainModernRenderer : IDisposable { // VertsPerLandblock MUST stay divisible by 6 — terrain_modern.vert uses // `gl_VertexID % 6` to pick the cell-corner index (BL/BR/TR/TL), and @@ -32,12 +32,6 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable private const int IndexSize = sizeof(uint); private const float LandblockSize = LandblockMesh.LandblockSize; // 192 - // Campaign V slice V6j: null on the RHI arm, where every statement below that - // reaches one of these is forked into TerrainModernRenderer.Rhi.cs. The GL arm - // executes exactly what it did before. - private readonly GL? _gl; - private readonly BindlessSupport? _bindless; - private readonly Shader? _shader; private readonly TerrainAtlas _atlas; /// A.5 T22.5: exposes the terrain atlas so callers can update @@ -46,7 +40,6 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable private readonly GpuRetiredTerrainSlotAllocator _alloc; private readonly GpuRetirementLedger _retirementLedger; - private RetryableResourceReleaseLedger? _disposeResources; private bool _disposed; // Per-slot live data (index by slot integer; null entries are unused slots). @@ -55,58 +48,22 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable // Reverse map: landblockId -> slot, for RemoveLandblock and replacement. private readonly Dictionary _idToSlot = new(); - // GPU buffers. - private uint _globalVao; - private uint _globalVbo; - private uint _globalEbo; + // Backing-store capacity bookkeeping, shared with the RHI arm's arena. private long _globalVboCapacityBytes; private long _globalEboCapacityBytes; - private uint _indirectBuffer; - private int _indirectCapacity; - private sealed class DynamicIndirectBuffer - { - public uint Buffer; - public int Capacity; - } - - private readonly List[] _indirectBuffersByFrame = - [[], [], []]; + // Per-GPU-fenced-frame-slot draw bookkeeping, shared with the RHI arm. private int _dynamicFrameSlot; - private int _dynamicBufferCursor; private bool _dynamicFrameStarted; - internal int DynamicIndirectBufferCount => - _indirectBuffersByFrame.Sum(frameBuffers => frameBuffers.Count); - - // Phase U.3: terrain clip UBO (binding=2, terrain_modern.vert TerrainClip). - // The shared one is created + uploaded by the GameWindow-level ClipFrame and - // handed in via SetClipUbo. When 0, we bind a lazily-created no-clip fallback - // (count 0 = ungated) so the shader never reads an unbound UBO at binding=2. - private TerrainClipBufferBinding _sharedClipBinding; - private uint _fallbackClipUbo; - - // Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2) - // became uTextureIndexA/uTextureIndexB (uint table slots) — cached - // uniform locations (matrix uniforms are set by name via Shader.SetMatrix4). - private int _uTextureIndexALoc; - private int _uTextureIndexBLoc; - private bool _textureTilingUploaded; - - // Campaign V slice V6f-2: the 36 per-layer tiling factors used to be a loose - // `uniform float uTexTiling[36]`, which Vulkan GLSL cannot declare at all. - // They now live in the uniform buffer GpuBindingModel reserved - // UniformTerrainTiling (binding 3) for — see terrain_modern.frag for the - // std140 packing and why it is vec4[9] rather than float[36]. - private uint _tilingUbo; - - // Campaign V slice V4t: the interim per-renderer GlBindlessHandleTable is - // retired. TerrainAtlas hands out GpuTextureSlots from the device's one - // table and this renderer flushes and binds that table at - // GpuBindingModel.StorageTextureTable itself, because it still submits - // through raw GL and so never reaches GlGpuDevice.FlushBeforeDraw. Null on - // a backend with no GL device, where this renderer is never constructed. - private readonly GlGpuDevice? _gpuDevice; + /// + /// The dynamic per-frame-slot indirect-command buffer pool this used to + /// report was raw-GL-only bookkeeping, deleted with that arm at Campaign V + /// slice V11. The RHI arm allocates its indirect-command storage from the + /// GPU frame's own upload ring instead, so there is no separate pool to + /// count. + /// + internal int DynamicIndirectBufferCount => 0; // Reusable per-frame buffers. private readonly List _visibleSlots = new(); @@ -126,183 +83,18 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable public void BeginVisibilityFrame() => _visibleCellIds.Clear(); - internal TerrainModernRenderer( - GL gl, - BindlessSupport bindless, - Shader shader, - TerrainAtlas atlas, - GlGpuDevice? gpuDevice, - int initialSlotCapacity = 64) - : this( - gl, - bindless, - shader, - atlas, - gpuDevice, - ImmediateGpuResourceRetirementQueue.Instance, - initialSlotCapacity) - { - } - - internal TerrainModernRenderer( - GL gl, - BindlessSupport bindless, - Shader shader, - TerrainAtlas atlas, - GlGpuDevice? gpuDevice, - IGpuResourceRetirementQueue resourceRetirement, - int initialSlotCapacity = 64) - { - _gl = gl; - _bindless = bindless; - _shader = shader; - _atlas = atlas; - _gpuDevice = gpuDevice; - ArgumentNullException.ThrowIfNull(resourceRetirement); - _retirementLedger = new GpuRetirementLedger(resourceRetirement); - _alloc = new GpuRetiredTerrainSlotAllocator(initialSlotCapacity, resourceRetirement); - _slots = new SlotData?[initialSlotCapacity]; - - _uTextureIndexALoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexA"); - _uTextureIndexBLoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexB"); - - var constructionResources = new ResourceCleanupGroup(); - try - { - // Campaign V slice V6f-2: the tiling UBO. Fixed size — the table is - // 36 immutable floats packed four to a vec4 — so it is allocated - // once here and written once on the first bound draw, which is the - // same cadence the glUniform1fv it replaces already had. - _tilingUbo = TrackedGlResource.CreateBuffer( - _gl, - "creating terrain tiling UBO"); - RetryableGpuResourceRelease tilingUboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - _tilingUbo, - TerrainTextureTilingTable.UniformBufferBytes, - "rolling back terrain tiling UBO"); - constructionResources.Add("terrain tiling UBO", tilingUboRelease.Run); - TrackedGlResource.AllocateBufferStorage( - _gl, - BufferTargetARB.UniformBuffer, - _tilingUbo, - 0, - TerrainTextureTilingTable.UniformBufferBytes, - BufferUsageARB.StaticDraw, - "allocating terrain tiling UBO"); - - _globalVao = TrackedGlResource.CreateVertexArray( - _gl, - "creating terrain global VAO"); - RetryableGpuResourceRelease globalVaoRelease = - TrackedGlResource.CreateRetryableVertexArrayDeletion( - _gl, - _globalVao, - "rolling back terrain global VAO"); - constructionResources.Add( - "terrain global VAO", - globalVaoRelease.Run); - _globalVbo = TrackedGlResource.CreateBuffer( - _gl, - "creating terrain global vertex buffer"); - RetryableGpuResourceRelease globalVboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - _globalVbo, - () => _globalVboCapacityBytes, - "rolling back terrain global vertex buffer"); - constructionResources.Add( - "terrain global vertex buffer", - globalVboRelease.Run); - _globalEbo = TrackedGlResource.CreateBuffer( - _gl, - "creating terrain global index buffer"); - RetryableGpuResourceRelease globalEboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - _globalEbo, - () => _globalEboCapacityBytes, - "rolling back terrain global index buffer"); - constructionResources.Add( - "terrain global index buffer", - globalEboRelease.Run); - AllocateGpuBuffers(initialSlotCapacity); - GlResourceCommand.Execute( - _gl, - "configure terrain global vertex array", - () => ConfigureVao(_globalVao, _globalVbo, _globalEbo)); - constructionResources.TransferAll(); - } - catch (Exception constructionFailure) - { - constructionResources.RollbackConstructionAndThrow( - "TerrainModernRenderer construction failed and its GL prefix did not cleanly roll back.", - constructionFailure); - } - - } - /// - /// Resets the indirect-command submission cursor for a GPU-fenced frame - /// slot. A retail outside view may draw terrain more than once in a frame; - /// each draw receives storage that cannot overwrite an earlier command. + /// Resets the per-GPU-fenced-frame-slot draw state. A retail outside view + /// may draw terrain more than once in a frame. /// public void BeginFrame(int frameSlot) { - if ((uint)frameSlot >= (uint)_indirectBuffersByFrame.Length) - throw new ArgumentOutOfRangeException(nameof(frameSlot)); + ArgumentOutOfRangeException.ThrowIfNegative(frameSlot); _retirementLedger.RetryPendingPublications(); _dynamicFrameSlot = frameSlot; - _dynamicBufferCursor = 0; _dynamicFrameStarted = true; } - private void ActivateNextIndirectBuffer() - { - if (!_dynamicFrameStarted) - throw new InvalidOperationException("BeginFrame must be called before drawing terrain."); - - List frameBuffers = _indirectBuffersByFrame[_dynamicFrameSlot]; - if (_dynamicBufferCursor == frameBuffers.Count) - { - uint buffer = TrackedGlResource.CreateBuffer( - _gl!, - $"creating terrain indirect buffer for frame slot {_dynamicFrameSlot}"); - try - { - frameBuffers.Add(new DynamicIndirectBuffer { Buffer = buffer }); - } - catch - { - TrackedGlResource.DeleteBuffer( - _gl!, - buffer, - 0, - "rolling back terrain indirect buffer"); - throw; - } - } - - DynamicIndirectBuffer active = frameBuffers[_dynamicBufferCursor++]; - _indirectBuffer = active.Buffer; - _indirectCapacity = active.Capacity; - } - - private void PersistIndirectCapacity() - { - _indirectBuffersByFrame[_dynamicFrameSlot][_dynamicBufferCursor - 1].Capacity = - _indirectCapacity; - } - - /// - /// Hand the renderer the current aligned terrain-clip range (binding=2). - /// Each outside-view slice occupies a distinct range in the current - /// GPU-fenced frame's UBO arena. - /// - public void SetClipUbo(TerrainClipBufferBinding sharedClipBinding) => - _sharedClipBinding = sharedClipBinding; - /// /// Two-tier streaming entry point. Accepts a prebuilt mesh from /// built on the worker @@ -343,59 +135,26 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable EnsureCapacity(newCap); } - // Bake worldOrigin into vertex positions; capture min/max Z for AABB. - var bakedVerts = new TerrainVertex[VertsPerLandblock]; - float zMin = float.MaxValue, zMax = float.MinValue; - for (int i = 0; i < VertsPerLandblock; i++) - { - var v = meshData.Vertices[i]; - var worldPos = v.Position + worldOrigin; - bakedVerts[i] = new TerrainVertex(worldPos, v.Normal, v.Data0, v.Data1, v.Data2, v.Data3); - if (worldPos.Z < zMin) zMin = worldPos.Z; - if (worldPos.Z > zMax) zMax = worldPos.Z; - } - if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; } - - // Bake baseVertex into indices on the CPU side (driver-portable pattern). - uint baseVertex = (uint)(slot * VertsPerLandblock); - var bakedIndices = new uint[IndicesPerLandblock]; - for (int i = 0; i < IndicesPerLandblock; i++) - bakedIndices[i] = meshData.Indices[i] + baseVertex; - - // glBufferSubData into the slot's VBO + EBO regions. - nint vboByteOffset = (nint)(slot * VertsPerLandblock * VertexSize); - nint eboByteOffset = (nint)(slot * IndicesPerLandblock * IndexSize); - - if (_gl is null) + // Bake worldOrigin into vertex positions; capture min/max Z for AABB. + var bakedVerts = new TerrainVertex[VertsPerLandblock]; + float zMin = float.MaxValue, zMax = float.MinValue; + for (int i = 0; i < VertsPerLandblock; i++) { - UploadRhiLandblock(slot, bakedVerts, bakedIndices); - } - else - { - fixed (TerrainVertex* p = bakedVerts) - { - TrackedGlResource.UpdateBufferSubData( - _gl, - BufferTargetARB.ArrayBuffer, - _globalVbo, - vboByteOffset, - VertsPerLandblock * VertexSize, - p, - $"uploading terrain vertices for 0x{landblockId:X8}"); + var v = meshData.Vertices[i]; + var worldPos = v.Position + worldOrigin; + bakedVerts[i] = new TerrainVertex(worldPos, v.Normal, v.Data0, v.Data1, v.Data2, v.Data3); + if (worldPos.Z < zMin) zMin = worldPos.Z; + if (worldPos.Z > zMax) zMax = worldPos.Z; } + if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; } - fixed (uint* p = bakedIndices) - { - TrackedGlResource.UpdateBufferSubData( - _gl, - BufferTargetARB.ElementArrayBuffer, - _globalEbo, - eboByteOffset, - IndicesPerLandblock * IndexSize, - p, - $"uploading terrain indices for 0x{landblockId:X8}"); - } - } + // Bake baseVertex into indices on the CPU side (driver-portable pattern). + uint baseVertex = (uint)(slot * VertsPerLandblock); + var bakedIndices = new uint[IndicesPerLandblock]; + for (int i = 0; i < IndicesPerLandblock; i++) + bakedIndices[i] = meshData.Indices[i] + baseVertex; + + UploadRhiLandblock(slot, bakedVerts, bakedIndices); _slots[slot] = new SlotData { @@ -472,128 +231,15 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable } if (_visibleSlots.Count == 0) return; - // Campaign V slice V6j: the command array is built the same way on both - // arms; only where it lands differs. The RHI arm writes it into a frame - // ring slice, which retires the per-frame-slot indirect buffer pool - // structurally — every allocation within a frame is already distinct - // memory that outlives the draw recorded against it. BuildIndirectCommands(); - if (_gl is null) - { - if (!_dynamicFrameStarted) - throw new InvalidOperationException("BeginFrame must be called before drawing terrain."); - DrawRhi(viewProjection, _visibleSlots.Count); - return; - } - - ActivateNextIndirectBuffer(); - - // Grow indirect buffer if needed. - if (_visibleSlots.Count > _indirectCapacity) - { - int grownCapacity = Math.Max(64, _visibleSlots.Count * 2); - TrackedGlResource.AllocateBufferStorage( - _gl, - GLEnum.DrawIndirectBuffer, - _indirectBuffer, - checked((long)_indirectCapacity * sizeof(DrawElementsIndirectCommand)), - checked((long)grownCapacity * sizeof(DrawElementsIndirectCommand)), - GLEnum.DynamicDraw, - "growing terrain indirect command buffer"); - _indirectCapacity = grownCapacity; - } - - // Upload DEIC array. - fixed (DrawElementsIndirectCommand* p = _deicScratch) - { - TrackedGlResource.UpdateBufferSubData( - _gl, - GLEnum.DrawIndirectBuffer, - _indirectBuffer, - 0, - checked((long)_visibleSlots.Count * sizeof(DrawElementsIndirectCommand)), - p, - "uploading terrain indirect commands"); - } - PersistIndirectCapacity(); - - // Bind shader + uniforms + atlas handles. - // Verified Phase W Stage 4 (T4.2): terrain projects from the camera view-proj; - // no separate landscape viewpoint to sync. uViewProjection derives from - // the ICamera passed into this method — the same camera used for all other - // renderers in the unified pipeline. Retail's LScape::update_viewpoint - // pre-positions terrain to the outdoor landcell, but acdream uses the - // unified camera matrix everywhere, so no separate viewpoint divergence can occur. - _shader!.Use(); - UploadTextureTilingOnce(); - // Campaign V slice V6f-2: bind the tiling UBO every draw, not once. GL's - // uniform-buffer binding points are global and shared with the sky's - // params block and the SceneLighting block, so a renderer that runs - // between two terrain draws can take binding 3 out from under us. - // Self-contained state, per feedback_render_self_contained_gl_state. - _gl.BindBufferBase( - BufferTargetARB.UniformBuffer, - Gpu.GpuBindingModel.UniformTerrainTiling, - _tilingUbo); - // Campaign V slice V6f-1: one uViewProjection, matching the field - // GpuPushConstants already carries, instead of the separate uView and - // uProjection the shader used to combine per vertex. viewProjection is - // the same product the visibility pass above already computed. - _shader.SetMatrix4("uViewProjection", viewProjection); - - // Campaign V slice V2b: pass each texture's binding=9 table slot - // instead of the raw uvec2 handle. GLSL reconstructs - // sampler2DArray(ACDREAM_TEXTURE_HANDLE(uTextureIndexA)) at the use - // site — see terrain_modern.frag. Slice V4t: the slots come from the - // device's one table rather than a table private to this renderer. - (GpuTextureSlot terrainSlot, GpuTextureSlot alphaSlot) = - _atlas.GetTextureSlots(GpuDevice); - FlushAndBindTextureTable(); - _gl.ProgramUniform1(_shader.Program, _uTextureIndexALoc, terrainSlot.Index); - _gl.ProgramUniform1(_shader.Program, _uTextureIndexBLoc, alphaSlot.Index); - - // Phase U.3: bind the terrain clip UBO (binding=2). Shared ClipFrame UBO - // when wired, else the no-clip fallback (count 0 = ungated terrain). - BindClipUboBinding2(); - - // #108-residual: retail terrain is SINGLE-SIDED — ACRender::landPolysDraw - // (0x006b7040) draws each land triangle ONLY when the camera is on the - // POSITIVE (upper) side of its plane (Plane::which_side2 vs - // Render::FrameCurrent, zFightTerrainAdjust bias). GL backface culling - // evaluates the same per-triangle eye-side predicate at rasterization. - // LandblockMesh emits every triangle CCW in world XY seen from above - // (LandblockMeshTests winding pin), which the unified camera chain - // (CreateLookAt up=+Z + Numerics perspective) maps to CCW window - // winding from above / CW from below (TerrainCullOrientationTests) — - // so FrontFace(Ccw)+Cull(Back) keeps the top side and culls the - // underside. WB drew the whole world with culling DISABLED - // frame-globally (WB GameScene.cs:841 — an editor camera goes - // underground); inheriting that drew terrain DOUBLE-SIDED, and a - // below-grade eye (cellar ascent) saw the UNDERSIDE of the grade - // sheet through the exit-door aperture — the #108 grass window. - // Self-contained state per feedback_render_self_contained_gl_state; - // the frame-global CW + cull-off baseline is restored after the draw. - _gl.Enable(EnableCap.CullFace); - _gl.CullFace(TriangleFace.Back); - _gl.FrontFace(FrontFaceDirection.Ccw); - - _gl.BindVertexArray(_globalVao); - _gl.MemoryBarrier(MemoryBarrierMask.CommandBarrierBit); - _gl.MultiDrawElementsIndirect( - PrimitiveType.Triangles, DrawElementsType.UnsignedInt, - (void*)0, - (uint)_visibleSlots.Count, - (uint)sizeof(DrawElementsIndirectCommand)); - _gl.BindVertexArray(0); - _gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0); - - _gl.FrontFace(FrontFaceDirection.CW); - _gl.Disable(EnableCap.CullFace); + if (!_dynamicFrameStarted) + throw new InvalidOperationException("BeginFrame must be called before drawing terrain."); + DrawRhi(viewProjection, _visibleSlots.Count); } /// /// Builds this frame's DrawElementsIndirectCommand array from the - /// visible slot list. Pure CPU, identical on both arms. + /// visible slot list. Pure CPU. /// private void BuildIndirectCommands() { @@ -618,298 +264,13 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable if (_disposed) return; _retirementLedger.RetryPendingPublications(); - if (_gl is null) - { - DisposeRhi(); - return; - } - - if (_disposeResources is null) - { - var releases = new List<(string Name, Action Release)>(); - if (_globalVao != 0) - { - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableVertexArrayDeletion( - _gl, - _globalVao, - "deleting terrain global VAO"); - releases.Add(("global-vao", release.Run)); - } - if (_globalVbo != 0) - { - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - _globalVbo, - _globalVboCapacityBytes, - "deleting terrain global vertex buffer"); - releases.Add(("global-vbo", release.Run)); - } - if (_globalEbo != 0) - { - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - _globalEbo, - _globalEboCapacityBytes, - "deleting terrain global index buffer"); - releases.Add(("global-ebo", release.Run)); - } - for (int frame = 0; frame < _indirectBuffersByFrame.Length; frame++) - { - List frameBuffers = _indirectBuffersByFrame[frame]; - for (int index = 0; index < frameBuffers.Count; index++) - { - DynamicIndirectBuffer buffer = frameBuffers[index]; - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - buffer.Buffer, - checked((long)buffer.Capacity * sizeof(DrawElementsIndirectCommand)), - "deleting terrain indirect command buffer"); - releases.Add(($"indirect-{frame}-{index}", release.Run)); - } - } - if (_fallbackClipUbo != 0) - { - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - _fallbackClipUbo, - ClipFrame.TerrainUboBytes, - "deleting terrain fallback clip UBO"); - releases.Add(("fallback-clip-ubo", release.Run)); - } - if (_tilingUbo != 0) - { - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - _tilingUbo, - TerrainTextureTilingTable.UniformBufferBytes, - "deleting terrain tiling UBO"); - releases.Add(("tiling-ubo", release.Run)); - } - _disposeResources = new RetryableResourceReleaseLedger(releases); - } - - ResourceReleaseAttempt attempt = _disposeResources.Advance(); - if (!_disposeResources.IsComplete) - { - throw attempt.ToException( - "One or more terrain GPU resources could not be released."); - } - - _globalVao = 0; - _globalVbo = 0; - _globalEbo = 0; - _globalVboCapacityBytes = 0; - _globalEboCapacityBytes = 0; - foreach (List frameBuffers in _indirectBuffersByFrame) - frameBuffers.Clear(); - _indirectBuffer = 0; - _indirectCapacity = 0; - _dynamicFrameStarted = false; - _fallbackClipUbo = 0; - _disposeResources = null; - _disposed = true; - - if (attempt.HasFailures) - { - throw attempt.ToException( - "Terrain GPU resources released with exceptional committed outcomes."); - } + DisposeRhi(); } // ---------------------------------------------------------------- // Private helpers // ---------------------------------------------------------------- - /// - /// Upload the texture-array adapter for retail's per-surface repeat count. - /// Retail passes TerrainTex::tex_tiling directly to - /// ImgTex::TileCSI / ImgTex::MergeTexture - /// (`TexMerge::CopyAndTile` 0x00503580, `TexMerge::Merge` 0x005038C0). - /// Uniform values persist for the lifetime of this linked shader program, - /// so the immutable atlas table is uploaded on its first bound draw. - /// - private void UploadTextureTilingOnce() - { - if (_textureTilingUploaded) - return; - - if (_atlas.TilingByLayer.Count != TerrainTextureTilingTable.LayerCapacity) - { - throw new InvalidOperationException( - $"Terrain tiling table has {_atlas.TilingByLayer.Count} entries; " + - $"expected {TerrainTextureTilingTable.LayerCapacity}."); - } - - // Campaign V slice V6f-2: one whole-buffer write into the binding=3 - // uniform buffer, replacing the glUniform1fv into the loose array. The - // block is std140, so each value sits at a 16-byte stride with three - // dead words after it; the span is cleared first so those words are - // zero rather than whatever the stack held. - Span block = stackalloc byte[TerrainTextureTilingTable.UniformBufferBytes]; - block.Clear(); - for (int i = 0; i < TerrainTextureTilingTable.LayerCapacity; i++) - { - BitConverter.TryWriteBytes( - block[(i * TerrainTextureTilingTable.UniformElementStrideBytes)..], - _atlas.TilingByLayer[i]); - } - - fixed (byte* p = block) - { - TrackedGlResource.UpdateBufferSubData( - _gl!, - BufferTargetARB.UniformBuffer, - _tilingUbo, - 0, - TerrainTextureTilingTable.UniformBufferBytes, - p, - "uploading terrain tiling UBO"); - } - - _textureTilingUploaded = true; - } - - /// - /// The GL device whose texture table this renderer samples through. - /// Campaign V slice V4t: a terrain renderer without one could not resolve a - /// single texture, so the failure names the composition that built it rather - /// than dereferencing null mid-draw. - /// - private GlGpuDevice GpuDevice => _gpuDevice ?? throw new InvalidOperationException( - "TerrainModernRenderer was constructed without a GL GPU device: its texture " + - "slots come from that device's table (Campaign V slice V4t)."); - - /// - /// Campaign V slice V4t: drains the device texture table's dirty runs and - /// (re)binds it at . - /// Terrain registers at most two slots (the terrain and alpha atlases), so - /// the table is dirty only on the atlas's first draw — but the bind is - /// unconditional, because GL storage-buffer binding points are global and - /// another renderer's binding 9 sits there between two terrain draws. - /// Deleted with the raw-GL world path when the Vulkan world arm lands and - /// this renderer's draws go through the encoder, which binds the same table - /// on every pipeline bind. - /// - private void FlushAndBindTextureTable() - { - GlGpuDevice device = GpuDevice; - device.FlushTextureTable(); - _gl!.BindBufferBase( - GLEnum.ShaderStorageBuffer, - GpuBindingModel.StorageTextureTable, - device.TextureTableGlName); - } - - /// - /// Phase U.3: bind the terrain clip UBO to binding=2. Prefers the shared - /// UBO range (); otherwise lazily - /// creates + binds a no-clip fallback (count 0 = ungated) so the shader never - /// reads an unbound UBO. The fallback is std140-sized to - /// and zero-filled (count 0). - /// - private void BindClipUboBinding2() - { - if (_sharedClipBinding.IsValid) - { - _sharedClipBinding.Bind(_gl!); - return; - } - - if (_fallbackClipUbo == 0) - { - var zero = stackalloc byte[ClipFrame.TerrainUboBytes]; - for (int i = 0; i < ClipFrame.TerrainUboBytes; i++) zero[i] = 0; - uint fallback = TrackedGlResource.CreateBuffer( - _gl!, - "creating terrain fallback clip UBO"); - try - { - TrackedGlResource.AllocateBufferStorage( - _gl!, - BufferTargetARB.UniformBuffer, - fallback, - 0, - ClipFrame.TerrainUboBytes, - BufferUsageARB.DynamicDraw, - zero, - "allocating terrain fallback clip UBO"); - _fallbackClipUbo = fallback; - } - catch - { - TrackedGlResource.DeleteBuffer( - _gl!, - fallback, - 0, - "rolling back terrain fallback clip UBO"); - throw; - } - } - _gl!.BindBufferBase(BufferTargetARB.UniformBuffer, - ClipFrame.TerrainClipUboBinding, _fallbackClipUbo); - } - - private void AllocateGpuBuffers(int capacitySlots) - { - long vboBytes = checked((long)capacitySlots * VertsPerLandblock * VertexSize); - long eboBytes = checked((long)capacitySlots * IndicesPerLandblock * IndexSize); - - TrackedGlResource.AllocateBufferStorage( - _gl!, - BufferTargetARB.ArrayBuffer, - _globalVbo, - _globalVboCapacityBytes, - vboBytes, - BufferUsageARB.DynamicDraw, - "allocating terrain global vertex storage"); - _globalVboCapacityBytes = vboBytes; - - TrackedGlResource.AllocateBufferStorage( - _gl!, - BufferTargetARB.ElementArrayBuffer, - _globalEbo, - _globalEboCapacityBytes, - eboBytes, - BufferUsageARB.DynamicDraw, - "allocating terrain global index storage"); - _globalEboCapacityBytes = eboBytes; - } - - private void ConfigureVao(uint vao, uint vbo, uint ebo) - { - _gl!.BindVertexArray(vao); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo); - _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo); - - uint stride = (uint)VertexSize; - - // location 0: Position - _gl.EnableVertexAttribArray(0); - _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0); - // location 1: Normal - _gl.EnableVertexAttribArray(1); - _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); - // locations 2-5: Data0..Data3 (uvec4 byte attributes) - nint dataOffset = 6 * sizeof(float); - _gl.EnableVertexAttribArray(2); - _gl.VertexAttribIPointer(2, 4, VertexAttribIType.UnsignedByte, stride, (void*)dataOffset); - _gl.EnableVertexAttribArray(3); - _gl.VertexAttribIPointer(3, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 4)); - _gl.EnableVertexAttribArray(4); - _gl.VertexAttribIPointer(4, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 8)); - _gl.EnableVertexAttribArray(5); - _gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12)); - - _gl.BindVertexArray(0); - GLHelpers.ThrowOnResourceError(_gl, "configuring terrain VAO"); - } - internal static void CollectVisibleCells( HashSet destination, uint landblockId, @@ -1038,132 +399,7 @@ public sealed unsafe partial class TerrainModernRenderer : IDisposable { if (newCapacity <= _alloc.Capacity) return; - if (_gl is null) - { - EnsureRhiCapacity(newCapacity); - return; - } - - var grownSlots = new SlotData?[newCapacity]; - Array.Copy(_slots, grownSlots, _slots.Length); - - long newVboBytes = checked((long)newCapacity * VertsPerLandblock * VertexSize); - long newEboBytes = checked((long)newCapacity * IndicesPerLandblock * IndexSize); - uint newVbo = 0; - uint newEbo = 0; - uint newVao = 0; - long allocatedNewVboBytes = 0; - long allocatedNewEboBytes = 0; - bool published = false; - try - { - newVbo = TrackedGlResource.CreateBuffer( - _gl, - "creating grown terrain vertex buffer"); - TrackedGlResource.AllocateBufferStorage( - _gl, - BufferTargetARB.ArrayBuffer, - newVbo, - 0, - newVboBytes, - BufferUsageARB.DynamicDraw, - "allocating grown terrain vertex buffer"); - allocatedNewVboBytes = newVboBytes; - - newEbo = TrackedGlResource.CreateBuffer( - _gl, - "creating grown terrain index buffer"); - TrackedGlResource.AllocateBufferStorage( - _gl, - BufferTargetARB.ElementArrayBuffer, - newEbo, - 0, - newEboBytes, - BufferUsageARB.DynamicDraw, - "allocating grown terrain index buffer"); - allocatedNewEboBytes = newEboBytes; - - GLHelpers.ThrowOnResourceError(_gl, "copying terrain buffers (precondition)"); - _gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalVbo); - _gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newVbo); - _gl.CopyBufferSubData( - CopyBufferSubDataTarget.CopyReadBuffer, - CopyBufferSubDataTarget.CopyWriteBuffer, - 0, - 0, - checked((nuint)_globalVboCapacityBytes)); - _gl.BindBuffer(BufferTargetARB.CopyReadBuffer, _globalEbo); - _gl.BindBuffer(BufferTargetARB.CopyWriteBuffer, newEbo); - _gl.CopyBufferSubData( - CopyBufferSubDataTarget.CopyReadBuffer, - CopyBufferSubDataTarget.CopyWriteBuffer, - 0, - 0, - checked((nuint)_globalEboCapacityBytes)); - GLHelpers.ThrowOnResourceError(_gl, "copying terrain buffers"); - - newVao = TrackedGlResource.CreateVertexArray( - _gl, - "creating grown terrain VAO"); - ConfigureVao(newVao, newVbo, newEbo); - - uint oldVao = _globalVao; - uint oldVbo = _globalVbo; - uint oldEbo = _globalEbo; - long oldVboBytes = _globalVboCapacityBytes; - long oldEboBytes = _globalEboCapacityBytes; - - _globalVao = newVao; - _globalVbo = newVbo; - _globalEbo = newEbo; - _globalVboCapacityBytes = newVboBytes; - _globalEboCapacityBytes = newEboBytes; - _slots = grownSlots; - _alloc.GrowTo(newCapacity); - published = true; - - // Older submitted draws captured the former VAO/buffer bindings. - // Retire the complete old set only after the replacement is valid. - RetryableGpuResourceRelease oldVaoRelease = - TrackedGlResource.CreateRetryableVertexArrayDeletion( - _gl, - oldVao, - "retiring terrain VAO after growth"); - RetryableGpuResourceRelease oldVboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - oldVbo, - oldVboBytes, - "retiring terrain vertex buffer after growth"); - RetryableGpuResourceRelease oldEboRelease = - TrackedGlResource.CreateRetryableBufferDeletion( - _gl, - oldEbo, - oldEboBytes, - "retiring terrain index buffer after growth"); - _retirementLedger.RetireMany( - [oldVaoRelease, oldVboRelease, oldEboRelease]); - } - finally - { - if (!published) - { - TrackedGlResource.DeleteVertexArray( - _gl, - newVao, - "rolling back grown terrain VAO"); - TrackedGlResource.DeleteBuffer( - _gl, - newVbo, - allocatedNewVboBytes, - "rolling back grown terrain vertex buffer"); - TrackedGlResource.DeleteBuffer( - _gl, - newEbo, - allocatedNewEboBytes, - "rolling back grown terrain index buffer"); - } - } + EnsureRhiCapacity(newCapacity); } private sealed class SlotData diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs index 0ef2a366..40a41f6d 100644 --- a/src/AcDream.App/Rendering/TextRenderer.cs +++ b/src/AcDream.App/Rendering/TextRenderer.cs @@ -455,7 +455,8 @@ public sealed class TextRenderer : IDisposable /// into it, and issues one non-indexed draw. Replaces the old growable /// per-flight VBO + BufferSubData pattern: every UI vertex upload is /// now the frame's shared ring, reset once per frame by - /// . + /// + /// (the raw-GL device's equivalent reset was deleted at Campaign V slice V11). /// private static void DrawRing(IGpuFrame frame, IGpuPassEncoder encoder, List buf) { diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs index a7a2c9b0..df9ddada 100644 --- a/src/AcDream.App/Rendering/TextureCache.cs +++ b/src/AcDream.App/Rendering/TextureCache.cs @@ -3,10 +3,8 @@ using AcDream.Core.Textures; using AcDream.Core.World; using AcDream.Content; using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; using DatReaderWriter; using DatReaderWriter.DBObjs; -using Silk.NET.OpenGL; using System.Linq; using PixelFormatId = DatReaderWriter.Enums.PixelFormat; using SurfaceType = DatReaderWriter.Enums.SurfaceType; @@ -14,22 +12,15 @@ using AcDream.App.Rendering.Residency; namespace AcDream.App.Rendering; -public sealed unsafe class TextureCache +public sealed class TextureCache : Wb.IEntityTextureLifetime, IDisposable { - private readonly GL? _gl; private readonly IGpuDevice _device; private readonly IDatReaderWriter _dats; private readonly string _diagnosticsDirectory; - // Handle and decoded dimensions are one atomic cache entry. Keeping them - // in separate dictionaries allowed GetOrUpload(surfaceId) followed by the - // sized overload to upload a second GL texture and orphan the first. - private readonly Dictionary - _surfacesById = new(); private readonly Dictionary<(uint SurfaceId, uint OrigTextureId), (int Width, int Height)> _decodedDimensionsByTexture = new(); - private uint _magentaHandle; /// /// Campaign V slice V4a: one registered plus its @@ -55,7 +46,6 @@ public sealed unsafe class TextureCache // GPU texture objects/slots until process exit. private readonly List _adhocGpuTextures = new(); - private readonly Wb.BindlessSupport? _bindless; private readonly CompositeTextureArrayCache? _compositeTextures; private bool _destinationRevealUploadPriority; @@ -105,12 +95,10 @@ public sealed unsafe class TextureCache // contract), and this convenience overload has no real caller today (both // production construction sites already target the internal overload // below) — kept internal rather than deleted to preserve its shape. - internal TextureCache(GL? gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null) + internal TextureCache(IGpuDevice device, IDatReaderWriter dats) : this( - gl, device, dats, - bindless, ImmediateGpuResourceRetirementQueue.Instance, Path.Combine( Path.GetTempPath(), @@ -119,142 +107,57 @@ public sealed unsafe class TextureCache { } - /// - /// The GL context, or null on a backend that has none. Campaign V slice V6h: - /// the UI path (, ) - /// is entirely -driven and runs on either backend, - /// while the world paths — the legacy Texture2D upload, particle - /// arrays, and the composite/bindless caches — still speak raw GL and are - /// unreachable without it. A null context therefore reaches exactly the same - /// code a null already gated, and every world - /// entry point throws with the slice that owns it named. Removed at V4t, - /// which ports the world texture stack onto the RHI. - /// internal TextureCache( - GL? gl, IGpuDevice device, IDatReaderWriter dats, - Wb.BindlessSupport? bindless, IGpuResourceRetirementQueue retirementQueue, string diagnosticsDirectory, ResidencyBudgetOptions? budgets = null) { budgets ??= ResidencyBudgetOptions.Default; - _gl = gl; - if (gl is null && bindless is not null) - { - throw new ArgumentException( - "Bindless composite/particle texture caches require a GL context.", - nameof(bindless)); - } - _device = device ?? throw new ArgumentNullException(nameof(device)); _dats = dats; - _bindless = bindless; ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory); _diagnosticsDirectory = diagnosticsDirectory; ArgumentNullException.ThrowIfNull(retirementQueue); - if (bindless is not null) - { - var resources = new ResourceCleanupGroup(); - CompositeTextureArrayCache? composite = null; - StandaloneBindlessTextureCache? particles = null; - try - { - composite = new CompositeTextureArrayCache( - gl!, - bindless, - WorldDevice, - retirementQueue, - budgets.CompositeUnownedBytes, - budgets.CompositePhysicalBytes); - resources.Add("composite texture cache", composite.Dispose); - particles = new StandaloneBindlessTextureCache( - new ParticleTextureBackend(this), - retirementQueue, - budgets.StandaloneUnownedBytes, - budgets.StandaloneUnownedEntries); - resources.Add("particle texture cache", particles.Dispose); - resources.TransferAll(); - } - catch (Exception constructionFailure) - { - resources.RollbackConstructionAndThrow( - "TextureCache construction failed and its child-cache prefix did not cleanly roll back.", - constructionFailure); - } - _compositeTextures = composite; - _particleTextures = particles; - } - else + // Campaign V slice V6l: both owner-scoped caches exist on both arms. + // + // Everything about them that matters — sharing equivalent surfaces + // between owners, the bounded unowned LRU, the metered upload budget, + // and retirement behind the frame-flight fence — is already + // backend-neutral; only how one entry is created and destroyed + // differs, which is exactly what the two backend interfaces are for. + var resources = new ResourceCleanupGroup(); + CompositeTextureArrayCache? composite = null; + StandaloneBindlessTextureCache? particles = null; + try { - // Campaign V slice V6l: both owner-scoped caches exist on both arms. - // - // Everything about them that matters — sharing equivalent surfaces - // between owners, the bounded unowned LRU, the metered upload budget, - // and retirement behind the frame-flight fence — is already - // backend-neutral; only how one entry is created and destroyed - // differs, which is exactly what the two backend interfaces are for. - // RhiCompositeTextureArrayBackend is V6i-2's, built and exercised at - // startup since that slice but with no production consumer until now; - // ParticleRhiTextureBackend is this slice's. - var resources = new ResourceCleanupGroup(); - CompositeTextureArrayCache? composite = null; - StandaloneBindlessTextureCache? particles = null; - try - { - composite = new CompositeTextureArrayCache( - new RhiCompositeTextureArrayBackend(device), - retirementQueue, - budgets.CompositeUnownedBytes, - budgets.CompositePhysicalBytes); - resources.Add("composite texture cache", composite.Dispose); - particles = new StandaloneBindlessTextureCache( - new ParticleRhiTextureBackend(this), - retirementQueue, - budgets.StandaloneUnownedBytes, - budgets.StandaloneUnownedEntries); - resources.Add("particle texture cache", particles.Dispose); - resources.TransferAll(); - } - catch (Exception constructionFailure) - { - resources.RollbackConstructionAndThrow( - "TextureCache construction failed and its child-cache prefix did not cleanly roll back.", - constructionFailure); - } - - _compositeTextures = composite; - _particleTextures = particles; + composite = new CompositeTextureArrayCache( + new RhiCompositeTextureArrayBackend(device), + retirementQueue, + budgets.CompositeUnownedBytes, + budgets.CompositePhysicalBytes); + resources.Add("composite texture cache", composite.Dispose); + particles = new StandaloneBindlessTextureCache( + new ParticleRhiTextureBackend(this), + retirementQueue, + budgets.StandaloneUnownedBytes, + budgets.StandaloneUnownedEntries); + resources.Add("particle texture cache", particles.Dispose); + resources.TransferAll(); } + catch (Exception constructionFailure) + { + resources.RollbackConstructionAndThrow( + "TextureCache construction failed and its child-cache prefix did not cleanly roll back.", + constructionFailure); + } + + _compositeTextures = composite; + _particleTextures = particles; } - /// - /// The GL context the world texture paths need. Campaign V slice V6h: a - /// Vulkan-composed cache serves the UI path through - /// alone and never reaches here, so a failure names the slice that owns the - /// port rather than dereferencing null. - /// - private GL Gl => _gl ?? throw new InvalidOperationException( - "This TextureCache owns no GL context: the world texture paths " + - "(Texture2D upload, particle arrays, composite/bindless caches) are " + - "unavailable until Campaign V slice V4t ports them onto the RHI."); - - /// - /// The GL backend's device, for the world texture paths' table - /// registrations (Campaign V slice V4t). Those paths already require a GL - /// context — see — so the same construction that makes - /// non-null makes this cast sound; a Vulkan-composed - /// cache serves only the UI path through and never - /// reaches here. - /// - private GlGpuDevice WorldDevice => _device as GlGpuDevice - ?? throw new InvalidOperationException( - "This TextureCache's device is not the GL backend's: the world " + - "texture paths intern their bindless handles into GlGpuDevice's " + - "texture table (Campaign V slice V4t)."); - internal void RegisterResidencySources(ResidencyManager manager) { ArgumentNullException.ThrowIfNull(manager); @@ -286,44 +189,6 @@ public sealed unsafe class TextureCache BudgetBytes: textures.BudgetBytes); } - /// - /// Get or upload the GL texture handle for a Surface id. Returns a - /// 1x1 magenta fallback if the Surface or its RenderSurface chain is - /// missing or uses an unsupported format. - /// - public uint GetOrUpload(uint surfaceId) - => GetOrUploadSurfaceCore(surfaceId, out _, out _); - - /// - /// Like but also returns the decoded - /// pixel dimensions. UI 9-slice geometry needs the source size to - /// compute slice UVs. Cached alongside the handle. - /// - public uint GetOrUpload(uint surfaceId, out int width, out int height) - => GetOrUploadSurfaceCore(surfaceId, out width, out height); - - private uint GetOrUploadSurfaceCore(uint surfaceId, out int width, out int height) - { - if (_surfacesById.TryGetValue(surfaceId, out var existing)) - { - width = existing.Width; - height = existing.Height; - return existing.Handle; - } - - DecodedTexture decoded = DecodeFromDats( - surfaceId, - origTextureOverride: null, - paletteOverride: null); - if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1") - DumpAlphaHistogram(surfaceId, decoded); - uint h = UploadRgba8(decoded); - _surfacesById.Add(surfaceId, (h, decoded.Width, decoded.Height)); - width = decoded.Width; - height = decoded.Height; - return h; - } - /// /// Upload a UI sprite by its RenderSurface DataId (0x06xxxxxx), decoded /// DIRECTLY (Portal/HighRes → DecodeRenderSurface) rather than through the @@ -491,20 +356,12 @@ public sealed unsafe class TextureCache } /// - /// The identity a UI upload is accounted under. On GL it is the texture's - /// own GL name — unchanged, so the VRAM ledger and the - /// ACDREAM_DUMP_SURFACES histogram key off exactly what they always - /// did. On any other backend there is no such name, so a descending - /// synthetic counter supplies one; it starts at uint.MaxValue because - /// GL hands out small ascending names and the two spaces share the - /// _uploadMetadata dictionary. The value is a dictionary key and a - /// dedup token only — Campaign V slice V6d removed the last draw-time - /// consumer of a raw GL name, so nothing binds it. + /// The identity a UI upload is accounted under. There is no GL name on the + /// Vulkan-only backend, so a descending synthetic counter supplies one; the + /// value is a dictionary key and a dedup token only — Campaign V slice V6d + /// removed the last draw-time consumer of a raw GL name, so nothing binds it. /// - private uint UploadAccountingName(IGpuTexture texture) => - texture is GlGpuTexture glTexture - ? glTexture.GlName - : _nextSyntheticUploadName--; + private uint UploadAccountingName(IGpuTexture texture) => _nextSyntheticUploadName--; private uint _nextSyntheticUploadName = uint.MaxValue; @@ -521,44 +378,6 @@ public sealed unsafe class TextureCache GpuAddressMode.Repeat, MaxAnisotropy: 1f); - /// - /// Alpha-channel histogram for one decoded texture. Used to diagnose - /// "why are clouds not transparent" — if cloud textures come out with - /// alpha = 1.0 everywhere we know the decode path strips the alpha - /// channel somewhere. Printed once per unique surfaceId under - /// ACDREAM_DUMP_SKY=1. Adds ~2ms per texture upload, negligible. - /// - private static void DumpAlphaHistogram(uint surfaceId, DecodedTexture decoded) - { - if (decoded.Rgba8.Length == 0 || decoded.Width == 0 || decoded.Height == 0) - { - System.Console.WriteLine($"[tex-alpha] surf=0x{surfaceId:X8} empty"); - return; - } - int total = decoded.Rgba8.Length / 4; - // Bucket alpha in 10 bins. - var buckets = new int[10]; - int aMin = 255, aMax = 0; - long aSum = 0; - for (int i = 0; i < decoded.Rgba8.Length; i += 4) - { - int a = decoded.Rgba8[i + 3]; - if (a < aMin) aMin = a; - if (a > aMax) aMax = a; - aSum += a; - int b = a * 10 / 256; - if (b > 9) b = 9; - buckets[b]++; - } - float aMean = aSum / (float)total / 255f; - var pct = new string[10]; - for (int i = 0; i < 10; i++) pct[i] = $"{100.0 * buckets[i] / total:F0}%"; - System.Console.WriteLine( - $"[tex-alpha] surf=0x{surfaceId:X8} {decoded.Width}x{decoded.Height} " + - $"a_min={aMin / 255f:F3} a_max={aMax / 255f:F3} a_mean={aMean:F3} " + - $"bins[0-9]={string.Join(",", pct)}"); - } - /// /// Acquires the exact DAT-decoded one-layer texture array for a live /// particle emitter. Equivalent surfaces are shared; the cache ownership @@ -588,81 +407,17 @@ public sealed unsafe class TextureCache origTextureOverride: null, paletteOverride: null); - // Campaign V slice V6l: the RHI arm has no GL name to intern a bindless - // handle from, so the image is created through IGpuDevice and paired - // with a real sampler object. The decode above is the same one the GL - // arm uses, so the pixels are identical; the shader still samples layer - // zero of a one-layer array, which is what a Texture2D registered into - // the table is on Vulkan (VulkanTextureFormatMapping.SampledViewTypeOf). - if (_gl is null) - return AcquireParticleTextureRhi(textures, ownerId, surfaceId, decoded); - - uint name = UploadRgba8AsLayer1Array(decoded); - ulong handle = 0; - try - { - handle = _bindless!.GetResidentHandle(name); - Wb.GLHelpers.ThrowOnResourceError( - Gl, - $"making particle surface 0x{surfaceId:X8} resident"); - GpuTextureSlot slot = WorldDevice.RegisterWorldTextureHandle(handle); - var resource = new StandaloneBindlessTextureResource - { - SurfaceId = surfaceId, - Name = name, - Handle = handle, - Slot = slot, - Bytes = checked((long)decoded.Width * decoded.Height * 4L), - }; - textures.AddAndAcquire(ownerId, resource); - return slot; - } - catch (Exception residencyFailure) - { - List? cleanupFailures = null; - void Attempt(Action cleanup) - { - try { cleanup(); } - catch (Exception ex) { (cleanupFailures ??= []).Add(ex); } - } - - bool residencyReleased = handle == 0; - if (handle != 0) - { - Attempt(() => - { - // Slice V4t: the table entry may or may not have been made - // before the failure. Releasing an unregistered handle is a - // no-op, so this covers both without asking which. - WorldDevice.ReleaseWorldTextureHandle(handle); - _bindless!.MakeNonResident(handle); - Wb.GLHelpers.ThrowOnResourceError( - Gl, - "rolling back particle texture residency"); - residencyReleased = true; - }); - } - if (residencyReleased) - Attempt(() => DeleteUploadedTexture(name)); - if (cleanupFailures is not null) - { - cleanupFailures.Insert(0, residencyFailure); - throw new AggregateException( - "Particle texture residency and rollback both failed.", - cleanupFailures); - } - throw; - } + return AcquireParticleTextureRhi(textures, ownerId, surfaceId, decoded); } /// /// Campaign V slice V6l: one particle surface as a device texture-table - /// slot, owned by the same emitter-scoped cache the GL arm uses. + /// slot, owned by the same emitter-scoped cache. /// - /// Linear/clamped is the filtering the GL arm's own one-layer array - /// upload sets on itself, and a particle sheet's UVs never leave [0,1] — - /// the quad's own texcoords are the unit square — so the wrap mode is not a - /// visible choice, it is just the safe one. + /// Linear/clamped matches the filtering the deleted GL arm's own + /// one-layer array upload set on itself, and a particle sheet's UVs never + /// leave [0,1] — the quad's own texcoords are the unit square — so the + /// wrap mode is not a visible choice, it is just the safe one. /// private GpuTextureSlot AcquireParticleTextureRhi( StandaloneBindlessTextureCache textures, @@ -714,8 +469,9 @@ public sealed unsafe class TextureCache /// Owner-scoped bindless variant for a server-supplied original-texture /// replacement. Stores compatible composites in a pooled Texture2DArray /// and returns its resident handle plus the assigned layer. Equivalent - /// composites are shared until their final live owner leaves. Throws if - /// BindlessSupport wasn't provided. + /// composites are shared until their final live owner leaves. Returns + /// (an empty location) if a composite upload + /// can't start or the decoded size can't be prepared this frame. /// internal BindlessTextureLocation GetOrUploadWithOrigTextureOverrideBindless( uint ownerLocalId, @@ -750,7 +506,8 @@ public sealed unsafe class TextureCache /// top of the texture's default palette before decoding, stores compatible /// composites in a pooled Texture2DArray, and returns its resident handle /// plus the assigned layer. Structural identity is computed once per entity. - /// Throws if BindlessSupport wasn't provided to the constructor. + /// Returns (an empty location) if a composite + /// upload can't start or the decoded size can't be prepared this frame. /// internal BindlessTextureLocation GetOrUploadWithPaletteOverrideBindless( uint ownerLocalId, @@ -853,14 +610,6 @@ public sealed unsafe class TextureCache EnsureCompositeTexturesAvailable().ReleaseOwner(localEntityId); } - private void EnsureBindlessAvailable() - { - if (_bindless is null) - throw new InvalidOperationException( - "TextureCache constructed without BindlessSupport — cannot generate bindless handles. " + - "WbDrawDispatcher requires the bindless-aware ctor overload (pass non-null BindlessSupport)."); - } - /// /// Campaign V slice V6l: no longer gated on bindless. The composite cache is /// constructed on both arms — V6i-2's RHI backend is what serves the one @@ -880,29 +629,9 @@ public sealed unsafe class TextureCache _particleTextures ?? throw new InvalidOperationException( "This TextureCache owns no standalone particle texture cache."); - private sealed class ParticleTextureBackend(TextureCache owner) - : IStandaloneBindlessTextureBackend - { - public void MakeNonResident(StandaloneBindlessTextureResource resource) - { - // Slice V4t: retire the table entry before its handle stops being - // resident. Idempotent, so a retried release stays correct. - owner.WorldDevice.ReleaseWorldTextureHandle(resource.Handle); - owner._bindless!.MakeNonResident(resource.Handle); - Wb.GLHelpers.ThrowOnResourceError( - owner.Gl, - $"releasing particle texture handle {resource.Handle}"); - } - - public void Delete(StandaloneBindlessTextureResource resource) - => owner.DeleteUploadedTexture(resource.Name); - } - /// - /// Campaign V slice V6l: the same ownership boundary on a backend with no - /// bindless handles. The table slot is released first and the image second, - /// which is the same order the GL arm uses and for the same reason — a - /// submitted-but-unretired frame may still sample the slot, and + /// Campaign V slice V6l: the table slot is released first and the image + /// second — a submitted-but-unretired frame may still sample the slot, and /// is what defers its reuse. /// private sealed class ParticleRhiTextureBackend(TextureCache owner) @@ -1043,7 +772,6 @@ public sealed unsafe class TextureCache bucketsByTriple[tripleKey] = bucketsByTriple.GetValueOrDefault(tripleKey) + 1; } - foreach (var kv in _surfacesById) Emit(kv.Key, kv.Value.Handle); _particleTextures?.VisitEntries(resource => Emit(resource.SurfaceId, resource.Name)); _compositeTextures?.VisitEntries((surfaceId, width, height) => { @@ -1191,101 +919,6 @@ public sealed unsafe class TextureCache return UiTextureTableHandle.FromSlot(entry.Slot); } - private uint UploadRgba8(DecodedTexture decoded, bool nearest = false) - { - uint tex = Gl.GenTexture(); - if (tex == 0) - throw new InvalidOperationException("OpenGL did not create a 2D texture."); - try - { - Gl.BindTexture(TextureTarget.Texture2D, tex); - - fixed (byte* p = decoded.Rgba8) - Gl.TexImage2D( - TextureTarget.Texture2D, - 0, - InternalFormat.Rgba8, - (uint)decoded.Width, - (uint)decoded.Height, - 0, - PixelFormat.Rgba, - PixelType.UnsignedByte, - p); - - // Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat - // font's small glyphs. Other surfaces use bilinear. - int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear; - Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter); - Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, filter); - Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); - Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); - Wb.GLHelpers.ThrowOnResourceError( - Gl, - $"uploading 2D RGBA8 texture {decoded.Width}x{decoded.Height}"); - - TrackUploadedTexture(tex, decoded.Width, decoded.Height); - return tex; - } - catch - { - Gl.DeleteTexture(tex); - throw; - } - finally - { - Gl.BindTexture(TextureTarget.Texture2D, 0); - } - } - - /// - /// Variant of that uploads pixel data as a 1-layer - /// Texture2DArray. Required by the WB modern rendering path which samples via - /// sampler2DArray in its bindless shader. Pixel data is identical. - /// - private uint UploadRgba8AsLayer1Array(DecodedTexture decoded) - { - uint tex = Gl.GenTexture(); - if (tex == 0) - throw new InvalidOperationException("OpenGL did not create a one-layer texture array."); - try - { - Gl.BindTexture(TextureTarget.Texture2DArray, tex); - - fixed (byte* p = decoded.Rgba8) - Gl.TexImage3D( - TextureTarget.Texture2DArray, - 0, - InternalFormat.Rgba8, - (uint)decoded.Width, - (uint)decoded.Height, - depth: 1, - border: 0, - PixelFormat.Rgba, - PixelType.UnsignedByte, - p); - - Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); - Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); - Wb.GLHelpers.ThrowOnResourceError( - Gl, - $"uploading one-layer RGBA8 array {decoded.Width}x{decoded.Height}"); - - TrackUploadedTexture(tex, decoded.Width, decoded.Height); - return tex; - } - catch - { - Gl.DeleteTexture(tex); - throw; - } - finally - { - Gl.BindTexture(TextureTarget.Texture2DArray, 0); - } - } - private void TrackUploadedTexture(uint name, int width, int height) { _uploadMetadata[name] = (width, height, "RGBA8_DECODED"); @@ -1294,19 +927,10 @@ public sealed unsafe class TextureCache Wb.GpuMemoryTracker.TrackAllocation(bytes, Wb.GpuResourceType.Texture); } - private void DeleteUploadedTexture(uint name) - { - Gl.DeleteTexture(name); - Wb.GLHelpers.ThrowOnResourceError(Gl, $"deleting uploaded texture {name}"); - UntrackUploadedTexture(name); - } - /// - /// Memory-tracking bookkeeping only, without a raw GL delete — used for - /// the Campaign V slice V4a UI-path entries, - /// whose GL name is released by through - /// the device's own retirement queue rather than by - /// . + /// Memory-tracking bookkeeping only — used for every + /// entry, whose GPU resource is released by + /// through the device's own retirement queue. /// private void UntrackUploadedTexture(uint name) { @@ -1328,17 +952,6 @@ public sealed unsafe class TextureCache _paletteIndexedByTexture.Clear(); - // Legacy Texture2D textures. - foreach (var entry in _surfacesById.Values) - DeleteUploadedTexture(entry.Handle); - _surfacesById.Clear(); - - if (_magentaHandle != 0) - { - DeleteUploadedTexture(_magentaHandle); - _magentaHandle = 0; - } - // RenderSurface (UI sprite) textures — Campaign V slice V4a: each // entry's IGpuTexture.Dispose() releases the underlying GL name // through the device's own retirement queue, so only the memory- diff --git a/src/AcDream.App/Rendering/TrackedTextureConstruction.cs b/src/AcDream.App/Rendering/TrackedTextureConstruction.cs deleted file mode 100644 index 5d19a0a9..00000000 --- a/src/AcDream.App/Rendering/TrackedTextureConstruction.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace AcDream.App.Rendering; - -internal static class TrackedTextureConstruction -{ - public static uint Create( - GlTextureConstructionTransaction transaction, - Action initialize) - { - ArgumentNullException.ThrowIfNull(transaction); - ArgumentNullException.ThrowIfNull(initialize); - uint texture = transaction.Allocate(); - initialize(texture); - return texture; - } - - public static uint Create( - GlTextureConstructionTransaction transaction, - Silk.NET.OpenGL.GL gl, - string context, - Action initialize) - { - ArgumentNullException.ThrowIfNull(transaction); - ArgumentNullException.ThrowIfNull(gl); - ArgumentException.ThrowIfNullOrWhiteSpace(context); - ArgumentNullException.ThrowIfNull(initialize); - - uint texture = transaction.Allocate(); - GlResourceCommand.Execute(gl, context, () => initialize(texture)); - return texture; - } -} diff --git a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs b/src/AcDream.App/Rendering/Wb/BindlessSupport.cs deleted file mode 100644 index cde447c2..00000000 --- a/src/AcDream.App/Rendering/Wb/BindlessSupport.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Silk.NET.OpenGL; -using Silk.NET.OpenGL.Extensions.ARB; -using AcDream.App.Rendering; - -namespace AcDream.App.Rendering.Wb; - -/// -/// Thin wrapper around + capability detection -/// for the modern rendering path. Constructed once at startup via -/// , which returns false if the extension isn't present. -/// -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(out var ext)) - { - support = new BindlessSupport(gl, ext); - return true; - } - support = null; - return false; - } - - /// Get a 64-bit bindless handle for the texture and make it resident. - /// Idempotent: handle is the same for a given texture name. - 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; - } - - /// - /// 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 GlGpuDevice.RegisterTexture, - /// which registers a (texture, sampler) pair per the RHI contract — "the - /// same texture registered with two samplers occupies two slots." The - /// texture-only above cannot express - /// that; ManagedGLTextureArray already calls the equivalent - /// ArbBindlessTexture.GetTextureSamplerHandle directly through - /// OpenGLGraphicsDevice.BindlessExtension, so this simply exposes - /// the same GL entry point through this class for the RHI's use. - /// - 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; - } - - /// Release residency for a handle. Call before deleting the underlying texture. - 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. - - /// Detect GL_ARB_shader_draw_parameters in addition to bindless. - /// N.5's vertex shader uses gl_BaseInstanceARB and gl_DrawIDARB - /// from this extension. - public bool HasShaderDrawParameters(GL gl) - { - return gl.IsExtensionPresent("GL_ARB_shader_draw_parameters"); - } -} diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index f268f899..982206c9 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -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(); - // 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(); - // 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? _pointSnapshot; private sealed class CachedCellLightSet @@ -125,51 +99,17 @@ public sealed unsafe partial class EnvCellRenderer : private readonly List _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[] _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; + /// + /// 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. + /// + 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> _activeSnapshotGlobalGroups = new(); private readonly List _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; - } - /// Resets the per-frame submission cursor for the GPU-fenced slot. 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 : } } - /// - /// Phase U.3: hand the renderer the SHARED per-cell clip-region SSBO - /// (binding=2) created by . 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. - /// - 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? filter, IReadOnlyList? 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 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 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 instances in _activeSnapshotGlobalGroups.Values) @@ -1359,59 +1165,7 @@ public sealed unsafe partial class EnvCellRenderer : } } - private void DeleteDynamicBufferSet(DynamicBufferSet set) - { - List? 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 allInstances, IReadOnlyList 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) - // --------------------------------------------------------------------------- - - /// - /// Campaign V slice V4t: drains the device texture table's dirty runs and - /// (re)binds it at - /// . - /// 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. - /// - 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) - // --------------------------------------------------------------------------- - - /// - /// Bind the per-cell clip-region SSBO to binding=2. Prefers the shared - /// buffer (); otherwise - /// lazily creates + binds a one-slot no-clip fallback (count 0 = pass-all) so - /// the shader never reads an unbound SSBO. - /// - 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 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 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 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)); - } } diff --git a/src/AcDream.App/Rendering/Wb/GLHelpers.cs b/src/AcDream.App/Rendering/Wb/GLHelpers.cs deleted file mode 100644 index 8e0640d6..00000000 --- a/src/AcDream.App/Rendering/Wb/GLHelpers.cs +++ /dev/null @@ -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; - } - - /// - /// Always-on error boundary for resource transactions. Most render-path - /// checks remain Debug-only because glGetError 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. - /// - [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 - /// - /// Checks for OpenGL errors and provides context-specific information - /// - 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 - - - /// - /// Gets detailed information about the current texture state for debugging - /// - 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(); - } - - /// - /// Logs current OpenGL state for debugging - /// - 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()); - } - - /// - /// 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. - /// - 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); - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/GLSLShader.cs b/src/AcDream.App/Rendering/Wb/GLSLShader.cs deleted file mode 100644 index f4eabecf..00000000 --- a/src/AcDream.App/Rendering/Wb/GLSLShader.cs +++ /dev/null @@ -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 _uniformLocations = []; - private Dictionary _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); - }); - } - } - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/GLStateScope.cs b/src/AcDream.App/Rendering/Wb/GLStateScope.cs deleted file mode 100644 index 49abea1c..00000000 --- a/src/AcDream.App/Rendering/Wb/GLStateScope.cs +++ /dev/null @@ -1,230 +0,0 @@ -using Silk.NET.OpenGL; -using System; - -namespace AcDream.App.Rendering.Wb { - /// - /// A RAII scope for saving and restoring OpenGL state. - /// - 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; - - /// - /// Captures the current OpenGL state. - /// - /// - 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); - } - - /// - /// Restores only the scissor state from the scope. - /// - 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]); - } - - /// - /// Restores the captured OpenGL state. - /// - 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; - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs index 8138e068..9211cb68 100644 --- a/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs +++ b/src/AcDream.App/Rendering/Wb/GlobalMeshBuffer.cs @@ -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 vkCmdCopyBuffer. 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 -/// WbDrawDispatcher, EnvCellRenderer and ParticleRenderer -/// still bind // directly -/// on the GL arm. +/// resource handle type moved. /// -/// Campaign V slice V6i-3 made the GL context optional. A backend that has -/// none builds no vertex array and publishes no raw names — , -/// and are 0 there — and its consumers bind +/// 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 /// and 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. +/// encoder, which is the same 32-byte position/normal/texcoord layout +/// expressed as pipeline vertex input. /// 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."); - /// - /// Campaign V slice V4b transitional bridge. The arena owns its stores as - /// , but its consumers — the vertex array object here, - /// and WbDrawDispatcher/EnvCellRenderer/ParticleRenderer - /// through / — are still raw GL until slice - /// V4c. This is the only place that reaches through the interface, and it - /// disappears with those consumers. - /// - 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; } - - /// - /// 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 - /// . - /// - public uint VBO => - _gl is null || _vertexBuffer is null ? 0u : RequireGlBuffer(_vertexBuffer).GlName; - - /// The index store's raw GL name. See . - public uint IBO => - _gl is null || _indexBuffer is null ? 0u : RequireGlBuffer(_indexBuffer).GlName; - /// /// The vertex store as the contract's own handle. This is what a pass - /// encoder binds, and it is live on both arms — is the - /// GL-only expression of the same thing. + /// encoder binds. /// internal IGpuBuffer? VertexStore => _vertexBuffer; /// The index store as the contract's own handle. See . internal IGpuBuffer? IndexStore => _indexBuffer; - /// - /// True once both backing stores exist. The backend-neutral form of the - /// VAO != 0 readiness test the raw-GL draw paths make. - /// + /// True once both backing stores exist. 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 /// /// Copies at most 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 + /// () publishes the destination. /// 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 /// /// The arena's own flight gate — 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 - /// . Stages match - /// TrackedGlResource.CreateRetryableBufferDeletion 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. /// 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; diff --git a/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs b/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs index 0b60bd74..de20127c 100644 --- a/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs +++ b/src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs @@ -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. /// -/// So the coupling is expressed as an interface at exactly that surface -/// and declares it — every member already -/// existed, so the GL arm executes not one changed statement. What this buys is -/// that ObjectMeshManager and WbMeshAdapter no longer NAME a -/// backend, which is the prerequisite for the slice that gives them a second +/// So the coupling is expressed as an interface at exactly that surface, +/// and OpenGLGraphicsDevice declared it — every member already existed, +/// so the GL arm executed not one changed statement. What this buys is that +/// ObjectMeshManager and WbMeshAdapter no longer NAME a backend, +/// which is the prerequisite for the slice that gives them a second /// implementation. /// /// What slice V6i-3 then moved. 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 /// IGpuDevice.CreateBuffer and publishes them as /// GlobalMeshBuffer.VertexStore/IndexStore, which a pass encoder -/// binds; the vertex array is built only where one exists. What still reads -/// is the LEGACY per-mesh upload the N.5 ship amendment made -/// unreachable, and AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice -/// is the second implementation this interface was cut for. +/// binds; the vertex array is built only where one exists. +/// is the +/// second implementation this interface was cut for. +/// +/// Campaign V slice V11 deleted OpenGLGraphicsDevice along +/// with the rest of the raw-GL arm it fronted, so +/// is now +/// the interface's only implementation. 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. /// internal interface IMeshPipelineDevice : IDisposable { diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs deleted file mode 100644 index 7a3b190a..00000000 --- a/src/AcDream.App/Rendering/Wb/ManagedGLFrameBuffer.cs +++ /dev/null @@ -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 { - /// - /// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET. - /// - 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); - } - }); - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs deleted file mode 100644 index 7443fc58..00000000 --- a/src/AcDream.App/Rendering/Wb/ManagedGLIndexBuffer.cs +++ /dev/null @@ -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 { - /// - /// OpenGL index buffer - /// - public unsafe class ManagedGLIndexBuffer : IIndexBuffer { - private uint bufferId; - private readonly OpenGLGraphicsDevice _device; - private void* _mappedPtr; - private GL GL => _device.GL; - - /// - public int Size { get; private set; } - - /// - public BufferUsage Usage { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// Buffer usage - /// The size of the buffer, in bytes - 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); - } - - /// - public void SetData(uint[] data) { - SetData(data.AsSpan()); - } - - /// - public unsafe void SetData(Span 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 mappedSpan = new Span(_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); - } - } - - - /// - public unsafe void SetSubData(Span 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 mappedSpan = new Span((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); - } - } - } - - - /// - 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); - } - } - - /// - public void Bind() { - RenderStateCache.CurrentIBO = 0; - GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId); - GLHelpers.CheckErrors(GL); - } - - /// - 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; - } - }); - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs deleted file mode 100644 index 31e252e4..00000000 --- a/src/AcDream.App/Rendering/Wb/ManagedGLTexture.cs +++ /dev/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; - - /// - public IntPtr NativePtr => (IntPtr)_texture; - - /// - public int Width { get; private set; } - - /// - public int Height { get; private set; } - - public TextureFormat Format => TextureFormat.RGBA8; - - /// - 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; - } - - /// - 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(); - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs deleted file mode 100644 index 4a6e263f..00000000 --- a/src/AcDream.App/Rendering/Wb/ManagedGLTextureArray.cs +++ /dev/null @@ -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; - /// - /// Campaign V slice V6i-2: the device whose one texture table this - /// array's two resident handles are interned into. Before this slice - /// ObjectMeshManager 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 instead. Null only - /// for the legacy OpenGLGraphicsDevice.CreateTextureArrayInternal - /// entry points, which no shared atlas uses. - /// - 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 _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(); - - /// - /// #105 diagnostic: staged layer updates (retained decoded payloads) not yet - /// applied to the GL texture by . 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. - /// - 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? 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 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); - } - - /// - /// 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. - /// - 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; - } - } - - /// - /// 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. - /// - internal bool IsPhysicalRetirementComplete => - Volatile.Read(ref _disposeQueued) != 0 - && Volatile.Read(ref _disposeRelease) is null; - - bool IWorldTextureArray.HasDurableDisposeOwnership => HasDurableDisposeOwnership; - - bool IWorldTextureArray.IsPhysicalRetirementComplete => IsPhysicalRetirementComplete; - - /// - /// Campaign V slice V6i-2: this array's device-table slot for the - /// requested address mode. - /// - /// The interning call is the one ObjectMeshManager made - /// itself before this slice, moved one level down so the caller can be - /// written against instead of against a - /// 64-bit ARB_bindless_texture 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. - /// - 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); - } - - /// - /// Retires both table entries. 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. - /// - 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 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; - } - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs deleted file mode 100644 index ebcc56d2..00000000 --- a/src/AcDream.App/Rendering/Wb/ManagedGLUniformBuffer.cs +++ /dev/null @@ -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 { - /// - /// OpenGL uniform buffer - /// - public unsafe class ManagedGLUniformBuffer : IUniformBuffer { - private uint bufferId; - private readonly OpenGLGraphicsDevice _device; - private GL GL => _device.GL; - - /// - public int Size { get; private set; } - - /// - public BufferUsage Usage { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// Graphics device - /// Buffer usage - /// The size of the buffer, in bytes - 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; - } - - /// - public unsafe void SetData(T[] data) where T : unmanaged { - SetData(data.AsSpan()); - } - - /// - public unsafe void SetData(Span data) where T : unmanaged { - uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf(); - - // 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); - } - } - - /// - public unsafe void SetSubData(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged { - SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements); - } - - /// - public unsafe void SetSubData(Span 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(); - - // 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); - } - } - - /// - /// Sets a single piece of data in the buffer. - /// - public unsafe void SetData(ref T data) where T : unmanaged { - fixed (T* pData = &data) { - SetData(new Span(pData, 1)); - } - } - - /// - /// Binds the buffer to the specified binding point. - /// - /// The binding point to bind to - public void Bind(uint bindingPoint) { - GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId); - GLHelpers.CheckErrors(GL); - } - - /// - public void Bind() { - GL.BindBuffer(GLEnum.UniformBuffer, bufferId); - GLHelpers.CheckErrors(GL); - } - - /// - 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; - } - }); - } - - /// - /// 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. - /// - internal void DisposeImmediately() { - if (bufferId == 0) - return; - - RetryableGpuResourceRelease release = - TrackedGlResource.CreateRetryableBufferDeletion( - GL, - bufferId, - Size, - "rolling back unpublished managed uniform buffer"); - release.Run(); - bufferId = 0; - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs deleted file mode 100644 index 583e82d9..00000000 --- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexArray.cs +++ /dev/null @@ -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); - }); - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs b/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs deleted file mode 100644 index d9a3752a..00000000 --- a/src/AcDream.App/Rendering/Wb/ManagedGLVertexBuffer.cs +++ /dev/null @@ -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 { - /// - /// OpenGL vertex buffer - /// - public unsafe class ManagedGLVertexBuffer : IVertexBuffer { - private uint bufferId; - private readonly OpenGLGraphicsDevice _device; - private void* _mappedPtr; - private GL GL => _device.GL; - - /// - public int Size { get; private set; } - - /// - public BufferUsage Usage { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// Buffer usage - /// The size of the buffer, in bytes - 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); - } - - /// - public unsafe void SetData(T[] data) where T : IVertex { - SetData(data.AsSpan()); - } - - /// - public unsafe void SetData(Span data) where T : IVertex { - uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf(); - - // 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 mappedSpan = new Span(_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 mappedSpan = new Span(mappedPtr, data.Length); - data.CopyTo(mappedSpan); - } - finally { - // Unmap the buffer - GL.UnmapBuffer(GLEnum.ArrayBuffer); - GLHelpers.CheckErrors(GL); - } - } - } - - public unsafe void SetSubData(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex { - SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements); - } - - /// - public unsafe void SetSubData(Span 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(); - - // 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 mappedSpan = new Span((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 mappedSpan = new Span(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; - } - }); - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs index a3b497eb..f670a7ec 100644 --- a/src/AcDream.App/Rendering/Wb/ModernRenderData.cs +++ b/src/AcDream.App/Rendering/Wb/ModernRenderData.cs @@ -1,6 +1,4 @@ using System.Runtime.InteropServices; -using DatReaderWriter.Enums; -using Chorizite.Core.Render; namespace AcDream.App.Rendering.Wb { /// @@ -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; - } } diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs index 4c47d8e9..c7c5a835 100644 --- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs +++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs @@ -28,7 +28,17 @@ namespace AcDream.App.Rendering.Wb /// public class ObjectRenderData { + /// + /// 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 arena has no VAO/VBO + /// concept at all (see ). This + /// is always 0 now; it survives only because + /// WbDrawDispatcher.cs's legacy (non-RHI) dispatcher still reads + /// it into its own dead anyVao bookkeeping. + /// public uint VAO { get; set; } + /// See — always 0 for the same reason. public uint VBO { get; set; } public int VertexCount { get; set; } public List Batches { get; set; } = new(); @@ -76,6 +86,11 @@ namespace AcDream.App.Rendering.Wb /// public class ObjectRenderBatch { + /// See — 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 /. + /// 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 /// private readonly IMeshPipelineDevice _graphicsDevice; - /// - /// The GL context the LEGACY (pre-modern-path) upload bodies write - /// through. - /// - /// Campaign V slice V6i-3 narrowed what still needs it. The modern - /// path's arena upload is 's, and that is - /// now 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 _useModernRendering 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. - /// - 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 /// private readonly AcDream.App.Rendering.Gpu.IGpuDevice _gpuDevice; - /// - /// 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. - /// - 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."); - /// /// Campaign V slice V6i-2: how a shared atlas's physical array is made. /// Composed once; see . @@ -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 /// /// #105 diagnostic: counts staged-but-unflushed texture layer updates across all - /// shared atlases (see ). + /// shared atlases (see ). /// Render thread only — _globalAtlases is render-thread-owned. /// 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(); 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) diff --git a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs b/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs deleted file mode 100644 index 1e926865..00000000 --- a/src/AcDream.App/Rendering/Wb/OpenGLGraphicsDevice.cs +++ /dev/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 { - /// - /// OpenGL graphics device - /// - 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> _glThreadQueue = new(); - private readonly ConcurrentQueue> _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 action) { - _glThreadQueue.Enqueue(action); - } - - internal void QueueGLActionForNextPass(Action 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? 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; } - - /// OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs). - public uint WrapSampler { get; private set; } - /// OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs). - public uint ClampSampler { get; private set; } - internal float MaxSupportedAnisotropy { get; private set; } - - private ManagedGLUniformBuffer? _sceneDataBuffer; - /// Shared SceneData UBO. - 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; - - /// - 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()); - 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); - } - } - - /// - /// Retires a GL resource only after every submitted draw that could - /// reference it has completed on the GPU. - /// - 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(List data) where T : unmanaged { - EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf(), true); - var span = CollectionsMarshal.AsSpan(data); - if (InstanceVBOPtr != null) { - var destSpan = new Span(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()), ptr); - } - } - } - - public void UpdateInstanceBuffer(Span data) where T : unmanaged { - EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf(), true); - if (InstanceVBOPtr != null) { - var destSpan = new Span(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()), ptr); - } - } - } - - /// - 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); - } - - /// - public override IIndexBuffer CreateIndexBuffer(int size, - Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) { - return new ManagedGLIndexBuffer(this, usage, size); - } - - /// - public override IVertexBuffer CreateVertexBuffer(int size, - Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) { - return new ManagedGLVertexBuffer(this, usage, size); - } - - /// - public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) { - return new ManagedGLVertexArray(this, vertexBuffer, format); - } - - /// - 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(); - } - } - - /// - 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 _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(); - } - } - } - - /// - 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); - } - - /// - /// Creates a texture with custom texture parameters. - /// - 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); - } - - /// - 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); - } - - /// - public override ITextureArray - CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) { - return new ManagedGLTextureArray(this, format, width, height, size, _log); - } - - /// - /// Creates a texture array with custom texture parameters. - /// - public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) { - return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams); - } - - /// - 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); - } - - /// - public override void EndFrame() { - } - - /// - 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; - } - } - - /// - 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."); - } - } - - /// - 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); - } - - /// - public override void BindFramebuffer(IFramebuffer? framebuffer) { - uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0; - GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId); - } - - /// - 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? 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); - } - } -} diff --git a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs b/src/AcDream.App/Rendering/Wb/RenderStateCache.cs deleted file mode 100644 index 6453ed7e..00000000 --- a/src/AcDream.App/Rendering/Wb/RenderStateCache.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace AcDream.App.Rendering.Wb; - -/// -/// Tracks currently-bound GL state to skip redundant rebinds across the -/// WB-derived render path. Previously these were static fields on -/// BaseObjectRenderManager in the WorldBuilder.Shared project; inlined -/// here in Phase O-T7 to eliminate the WorldBuilder project reference. -/// -/// Semantics are identical to the WB originals: -/// CurrentAtlas — slot index of the currently bound texture atlas. -/// CurrentVAO — OpenGL name of the currently bound vertex array object. -/// CurrentIBO — OpenGL name of the currently bound index buffer object. -/// Sentinel value 0 means "no valid binding cached." -/// -/// -/// 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. -/// -public static class RenderStateCache -{ - public static uint CurrentAtlas = 0; - public static uint CurrentVAO = 0; - public static uint CurrentIBO = 0; -} diff --git a/src/AcDream.App/Rendering/Wb/TrackedGlResource.cs b/src/AcDream.App/Rendering/Wb/TrackedGlResource.cs deleted file mode 100644 index 6a02e3fd..00000000 --- a/src/AcDream.App/Rendering/Wb/TrackedGlResource.cs +++ /dev/null @@ -1,283 +0,0 @@ -using Silk.NET.OpenGL; -using AcDream.App.Rendering; -using System.Runtime.ExceptionServices; - -namespace AcDream.App.Rendering.Wb; - -/// -/// 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. -/// -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 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 create, - Action 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); - } -} diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 54ebdfbd..e64b042d 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -13,21 +13,20 @@ using AcDream.Core.Terrain; using AcDream.Core.World; using AcDream.App.Rendering.Selection; using DatReaderWriter.Enums; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb; /// /// Draws entities using WB's (a single global -/// VAO/VBO/IBO under modern rendering) with acdream's -/// for bindless texture resolution. Exact pass classification travels with -/// each immutable prepared mesh batch. +/// vertex/index arena under modern rendering) with acdream's +/// for texture resolution. Exact pass classification +/// travels with each immutable prepared mesh batch. /// /// /// Atlas-tier entities (ServerGuid == 0): mesh data comes from WB's /// via . /// Shared textures reuse each batch's WB atlas handle and layer, returning -/// 64-bit resident handles stored in the per-group SSBO. +/// a device texture-table slot stored in the per-group SSBO. /// /// /// @@ -40,33 +39,32 @@ namespace AcDream.App.Rendering.Wb; /// /// /// -/// GL strategy (N.5 — mandatory): glMultiDrawElementsIndirect with SSBOs -/// and GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters. -/// All visible (entity, batch) pairs are bucketed by ; -/// each group becomes one DrawElementsIndirectCommand. Three GPU buffers -/// are uploaded per frame: instance matrices (SSBO binding 0), per-group batch -/// metadata/texture handles (SSBO binding 1), and the indirect draw commands. -/// Opaque world groups remain MDI-batched. Transparent world instances enter -/// so ordinary GfxObj parts and particles share -/// retail's stable far-to-near stream; sealed off-screen consumers retain the -/// immediate transparent MDI path. +/// Draw strategy (Campaign V — Vulkan only): multi-draw-indexed-indirect +/// with SSBOs, recorded through the RHI encoder in the sibling +/// WbDrawDispatcher.Rhi.cs partial. All visible (entity, batch) pairs are +/// bucketed by ; each group becomes one +/// DrawElementsIndirectCommand. Per-frame ring allocations carry instance +/// matrices (binding 0), per-group batch metadata/texture-table slots (binding +/// 1), and the indirect draw commands. Opaque world groups remain MDI-batched. +/// Transparent world instances enter so ordinary +/// GfxObj parts and particles share retail's stable far-to-near stream; sealed +/// off-screen consumers retain the immediate transparent MDI path. /// /// /// -/// Shader: mesh_modern (bindless + gl_DrawIDARB / -/// gl_BaseInstanceARB). Missing bindless/draw-parameters throws -/// at startup — there is no legacy fallback. +/// Shader: mesh_modern, compiled from committed SPIR-V. Missing a +/// mandatory GPU capability (the device texture table, MDI, or SSBOs) throws at +/// renderer construction — there is no legacy fallback. /// /// /// -/// Modern rendering assumption: WB's _useModernRendering path (GL -/// 4.3 + bindless) puts every mesh in a single shared VAO/VBO/IBO and uses -/// FirstIndex + BaseVertex per batch. The dispatcher honors those -/// offsets inside each DrawElementsIndirectCommand via -/// glMultiDrawElementsIndirect. +/// Modern rendering assumption: WB's modern-rendering path puts every +/// mesh in a single shared vertex/index arena and uses FirstIndex + +/// BaseVertex per batch. The dispatcher honors those offsets inside each +/// DrawElementsIndirectCommand via multi-draw-indexed-indirect. /// /// -public sealed unsafe partial class WbDrawDispatcher : IDisposable +public sealed partial class WbDrawDispatcher : IDisposable { /// /// Which subset of entities to walk in a single Draw call. @@ -86,8 +84,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable All, } - private readonly GL? _gl; - private readonly Shader? _shader; private readonly TextureCache _textures; private readonly WbMeshAdapter _meshAdapter; private readonly EntitySpawnAdapter _entitySpawnAdapter; @@ -98,7 +94,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable private readonly RetainedScratchCapacityPolicy _alphaScratchPolicy; private int _scratchPeakUnits; - private readonly BindlessSupport? _bindless; private ICurrentRenderDispatcherObserver? _currentRenderSceneObserver; public readonly record struct DrawStats( @@ -444,99 +439,47 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable /// public bool AlphaToCoverage { get; set; } = true; - // SSBO buffer ids - private uint _instanceSsbo; - private uint _batchSsbo; - private uint _indirectBuffer; - private int _instanceSsboCapacityBytes; - private int _batchSsboCapacityBytes; - private int _indirectBufferCapacityBytes; - - // Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to - // _instanceSsbo. One uint per instance selecting its CellClip slot. In U.3 - // this is ALL ZEROS (every instance → slot 0 → no-clip), so the render is - // identical to pre-U.3. U.4 populates real slot indices. - private uint _clipSlotSsbo; - private int _clipSlotSsboCapacityBytes; + // Phase U.3: per-instance clip-slot data (binding=3 on the RHI ring). One + // uint per instance selecting its CellClip slot. In U.3 this is ALL ZEROS + // (every instance → slot 0 → no-clip), so the render is identical to + // pre-U.3. U.4 populates real slot indices. private uint[] _clipSlotData = new uint[256]; // Fix B (A7 #3): per-OBJECT light selection (minimize_object_lighting). Two - // SSBOs replace the single global nearest-8-to-CAMERA UBO set for point/spot - // lights — see mesh_modern.vert binding=4/5. _globalLightsSsbo (binding=4) - // holds the per-frame point-light snapshot (LightManager.PointSnapshot); - // _instLightSetSsbo (binding=5) holds MaxLightsPerObject int indices per - // instance INTO it (-1 = unused), laid out parallel to _instanceSsbo. - private uint _globalLightsSsbo; - private uint _instLightSetSsbo; - private int _globalLightsSsboCapacityBytes; - private int _instLightSetSsboCapacityBytes; + // ring sections replace the single global nearest-8-to-CAMERA UBO set for + // point/spot lights — see mesh_modern.vert binding=4/5. The global-lights + // section (binding=4) holds the per-frame point-light snapshot + // (LightManager.PointSnapshot); the light-set section (binding=5) holds + // MaxLightsPerObject int indices per instance INTO it (-1 = unused), laid + // out parallel to the instance data. private int[] _lightSetData = new int[256 * LightManager.MaxLightsPerObject]; private float[] _globalLightData = new float[GlobalLightPacker.FloatsPerLight * 16]; // 16 floats (4 vec4) per GlobalLight // #142: per-instance "indoor" flag (binding=6), one uint per instance, parallel - // to _instanceSsbo. 1 = object parented to an EnvCell (skip the sun in the - // shader's uLightingMode==0 branch); 0 = outdoor object (gets the sun). - // Mechanically a clone of _clipSlotData / _clipSlotSsbo. - private uint _instIndoorSsbo; - private int _instIndoorSsboCapacityBytes; + // to the instance data. 1 = object parented to an EnvCell (skip the sun in + // the shader's uLightingMode==0 branch); 0 = outdoor object (gets the sun). + // Mechanically a clone of _clipSlotData. private uint[] _indoorData = new uint[256]; // #188: per-instance opacity multiplier (binding=7), one float per - // instance, parallel to _instanceSsbo. 1.0 = unmodified (the dat's own + // instance, parallel to the instance data. 1.0 = unmodified (the dat's own // material/texture alpha, untouched); < 1.0 multiplies the shader's // sampled alpha for an entity mid-TransparentPartHook fade. Mechanically - // a clone of _indoorData / _instIndoorSsbo, one binding higher. - private uint _instAlphaSsbo; - private int _instAlphaSsboCapacityBytes; + // a clone of _indoorData, one binding higher. private float[] _alphaData = new float[256]; + + private bool _dynamicFrameStarted; + + // Campaign V slice V11: the raw-GL upload path (and its per-frame triple- + // buffered SSBO pool) is gone — the RHI arm's frame ring + // (WbDrawDispatcher.Rhi.cs) owns the equivalent ring allocations now, so + // there is no dynamic buffer set left to count. Kept for the diagnostic + // consumer (RenderFrameDiagnosticSources) that reads this alongside the + // other renderers'. + internal int DynamicBufferSetCount => 0; + // Retail SmartBox click confirmation: per-instance CMaterial luminosity / // diffuse replacement (binding=8), parallel to the transform buffer. - private uint _instSelectionLightingSsbo; - private int _instSelectionLightingSsboCapacityBytes; - - // Campaign V slice V4t (2026-07-28): the interim per-renderer - // GlBindlessHandleTable is retired. Batch data now carries the device's own - // GpuTextureSlot, produced by the texture stack at upload time, so this - // renderer only has to flush and bind that one table at - // GpuBindingModel.StorageTextureTable before each of its raw-GL draws — it - // does not submit through the encoder, so it never reaches - // GlGpuDevice.FlushBeforeDraw. Taken from the mesh adapter rather than - // wired separately: a batch's slot and the table that resolves it must come - // from the same device by construction. - private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable => - _meshAdapter.WorldTextureTable; - - private sealed class DynamicBufferSet - { - public uint InstanceSsbo; - public uint BatchSsbo; - public uint IndirectBuffer; - public uint ClipSlotSsbo; - public uint GlobalLightsSsbo; - public uint InstanceLightSetSsbo; - public uint InstanceIndoorSsbo; - public uint InstanceAlphaSsbo; - public uint InstanceSelectionLightingSsbo; - public int InstanceCapacityBytes; - public int BatchCapacityBytes; - public int IndirectCapacityBytes; - public int ClipSlotCapacityBytes; - public int GlobalLightsCapacityBytes; - public int InstanceLightSetCapacityBytes; - public int InstanceIndoorCapacityBytes; - public int InstanceAlphaCapacityBytes; - public int InstanceSelectionLightingCapacityBytes; - } - - private readonly List[] _dynamicBufferSetsByFrame = - [[], [], []]; - private int _dynamicFrameSlot; - private int _dynamicBufferSetCursor; - private bool _dynamicFrameStarted; - private DynamicBufferSet? _activeDynamicBufferSet; - - internal int DynamicBufferSetCount => - _dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count); private Vector2[] _selectionLightingData = new Vector2[256]; // This frame's point-light snapshot, handed in by GameWindow before Draw via // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1). @@ -554,13 +497,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable private bool _currentEntityIndoor; private Vector2 _currentEntitySelectionLighting = new(0f, 1f); - // Phase U.3: the SHARED per-cell clip-region SSBO (binding=2), owned by the - // GameWindow-level ClipFrame and handed to us via SetClipRegionSsbo. When 0 - // (not yet wired), we bind our OWN fallback no-clip region buffer below so the - // shader never reads an unbound SSBO. The fallback holds exactly slot 0 - // (count 0 = pass-all), matching ClipFrame.NoClip's slot 0. + // Phase U.3: the SHARED per-cell clip-region SSBO (binding=2) id, owned by + // the GL-arm ClipFrame and handed in via SetClipRegionSsbo. Campaign V + // slice V11: the raw-GL world path that read this is gone, and the RHI arm + // binds clip regions from IWorldPassScope.Sections instead (see + // WbDrawDispatcher.Rhi.cs), so this is now write-only — kept because + // GlWorldPassSurface still calls the setter unconditionally. private uint _sharedClipRegionSsbo; - private uint _fallbackClipRegionSsbo; // Phase U.4: per-frame clip-slot routing handed in via SetClipRouting before // each Draw. When _clipRoutingActive is false (the U.3 path / outdoor root / @@ -786,91 +729,28 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable // render-thread only like the walk itself. private static readonly Dictionary _walkRejectCounts = new(); - // CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1. + // CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1. The GPU samples + // are written by the RHI arm's SampleRhiTimers (WbDrawDispatcher.Rhi.cs) + // from the device's own timer pool; the raw-GL query-object ring that used + // to feed them is gone with the raw-GL draw path. private readonly System.Diagnostics.Stopwatch _cpuStopwatch = new(); private readonly long[] _cpuSamples = new long[256]; // microseconds private int _cpuSampleCursor; - // GPU timing uses a ring of 3 query-pair slots so the read of frame N-3's - // result lands when the GPU has finished (~50ms after issue on a typical - // 60fps frame). Ring of 3 is the vendor-neutral choice: NVIDIA drivers with - // triple-buffering+vsync can queue ~3 frames ahead, AMD typically 1-2, - // Intel iGPUs vary. ResultAvailable is the safety guard if the GPU is - // still working when we try to read. - private const int GpuQueryRingDepth = 3; - private readonly uint[] _gpuQueryOpaque = new uint[GpuQueryRingDepth]; - private readonly uint[] _gpuQueryTransparent = new uint[GpuQueryRingDepth]; - // #125: a glGenQueries name does not become a QUERY OBJECT until its first - // glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION. - // The N.6 ring assumed ONE Draw per frame with both passes always non-empty; - // the pview pipeline issues MANY small Draws per frame (landscape slices, - // per-cell buckets, dynamics), where zero-draw passes routinely skip - // BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read then queued an - // InvalidOperation EVERY frame — silently, until WB's diligent texture-path - // glGetError checks ate the stale errors and treated their own successful - // uploads as failures ([wb-error] + sticky drop) and ProcessDirtyUpdates' - // check threw (process death; tower-wbdiag3.log). Track which slots were - // actually begun and only read those. - private readonly bool[] _gpuQueryOpaqueBegun = new bool[GpuQueryRingDepth]; - private readonly bool[] _gpuQueryTransparentBegun = new bool[GpuQueryRingDepth]; - private int _gpuQueryFrameIndex; private readonly long[] _gpuSamples = new long[256]; // microseconds private int _gpuSampleCursor; - private bool _gpuQueriesInitialized; - - // Constructor accessibility is internal because EntityClassificationCache - // is internal — a public ctor with an internal-typed parameter would be - // an inconsistent-accessibility error. The dispatcher is constructed - // exclusively from GameWindow (same assembly), so internal is fine. - internal WbDrawDispatcher( - GL gl, - Shader shader, - TextureCache textures, - WbMeshAdapter meshAdapter, - EntitySpawnAdapter entitySpawnAdapter, - BindlessSupport bindless, - EntityClassificationCache classificationCache, - AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades, - IRetailSelectionRenderSink? selectionSink = null, - RetailAlphaQueue? alphaQueue = null, - long? alphaScratchBudgetBytes = null) - { - ArgumentNullException.ThrowIfNull(gl); - ArgumentNullException.ThrowIfNull(shader); - ArgumentNullException.ThrowIfNull(textures); - ArgumentNullException.ThrowIfNull(meshAdapter); - ArgumentNullException.ThrowIfNull(entitySpawnAdapter); - ArgumentNullException.ThrowIfNull(classificationCache); - ArgumentNullException.ThrowIfNull(translucencyFades); - - _gl = gl; - _shader = shader; - _textures = textures; - _meshAdapter = meshAdapter; - _entitySpawnAdapter = entitySpawnAdapter; - _cache = classificationCache; - _translucencyFades = translucencyFades; - _selectionSink = selectionSink; - _selectionLighting = selectionSink as IRetailSelectionLightingSource; - _alphaQueue = alphaQueue; - _alphaSource = new AlphaDrawSource(this); - long scratchBudget = alphaScratchBudgetBytes - ?? AlphaScratchBudgetProfile.Create( - ResidencyBudgetOptions.Default.AlphaScratchBytes).DispatcherBytes; - _alphaScratchPolicy = - new RetainedScratchCapacityPolicy(scratchBudget); - _bindless = bindless ?? throw new ArgumentNullException(nameof(bindless)); - } /// - /// Selects the fence-protected frame slot and resets its draw-call cursor. - /// Every Draw/alpha preparation in one frame receives a distinct buffer - /// set, so later per-cell submissions cannot overwrite an earlier draw's - /// still-pending SSBO and indirect-command data. + /// Marks the start of a fence-protected frame. + /// is the shared GPU frame-ring slot every renderer in the frame receives + /// (see ) — the RHI arm's own + /// ring allocations (WbDrawDispatcher.Rhi.cs) come from the current + /// IGpuFrame instead, so this dispatcher no longer indexes its own + /// buffer-set pool by it; the parameter is kept so every renderer's + /// BeginFrame call stays uniform. /// public void BeginFrame(int frameSlot) { - if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length) - throw new ArgumentOutOfRangeException(nameof(frameSlot)); + _ = frameSlot; if (_groupFrame == long.MaxValue) throw new InvalidOperationException("Instance-group frame identity was exhausted."); @@ -881,10 +761,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable _groups, _retiredGroupKeys, _groupFrame - 1); - _dynamicFrameSlot = frameSlot; - _dynamicBufferSetCursor = 0; _dynamicFrameStarted = true; - _activeDynamicBufferSet = null; _currentRenderSceneObserver?.BeginDispatcherFrame(); } @@ -908,10 +785,12 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable /// /// Phase U.3: hand the dispatcher the SHARED per-cell clip-region SSBO - /// (binding=2) that created. The - /// dispatcher re-binds it to binding=2 immediately before each MDI so a - /// consumer that touched binding=2 in between can't leave it pointing - /// elsewhere. Pass 0 to fall back to the internal no-clip region buffer. + /// (binding=2) that created. Campaign + /// V slice V11: the raw-GL draw path that rebound this id is gone — + /// GlWorldPassSurface still calls this setter unconditionally, so it + /// is kept as a harmless store; the RHI arm binds clip regions from + /// IWorldPassScope.Sections instead (see + /// WbDrawDispatcher.Rhi.cs). /// public void SetClipRegionSsbo(uint sharedClipRegionSsbo) => _sharedClipRegionSsbo = sharedClipRegionSsbo; @@ -1513,6 +1392,11 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable foreach (InstanceGroup group in _groups.Values) group.ClearPerInstanceData(); + // Campaign V slice V11: no longer read for its own sake (the raw-GL + // VAO bind it fed is gone) — kept only because the packed-oracle + // partial (WbDrawDispatcher.PackedOracle.cs) mirrors this exact + // "first non-zero mesh id" computation and shares ExecuteClassifiedGroups' + // signature with it. uint anyVao = 0; // Project the 5-tuple enumerable into LandblockEntry records for WalkEntities. @@ -2039,8 +1923,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable // histogram exactly as it will be uploaded to binding=3 (grp.Slots) plus the // culled-entity count. Routed draws only (the landscape pass under DrawInside) so the // unrouted per-cell bucket draws don't oscillate the print-on-change signature. - // Emitted BEFORE the anyVao / totalInstances early-outs so an all-culled frame still - // reports (inst=0). + // Emitted BEFORE the MeshSourceReady / totalInstances early-outs so an + // all-culled frame still reports (inst=0). if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive) EmitClipRouteDispatchProbe(probeCulledEntities); @@ -2057,49 +1941,29 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable } /// - /// Whether there is a mesh source to draw from. - /// - /// Campaign V slice V6j: on GL that question is "did we find a vertex - /// array", which answers. The encoder arm has no - /// vertex array — the pipeline owns one shaped by - /// GpuVertexLayout.WorldMesh — so it asks the arena the - /// backend-neutral form of the same question, which V6i-3 published as - /// HasStores. + /// Whether there is a mesh source to draw from. The encoder arm has no + /// vertex array of its own — the pipeline owns one shaped by + /// GpuVertexLayout.WorldMesh — so this asks the shared mesh arena + /// directly, the backend-neutral question V6i-3 published as + /// HasStores. /// - private bool MeshSourceReady(uint anyVao) => - _gl is not null - ? anyVao != 0 - : _meshAdapter.MeshManager?.GlobalBuffer is { HasStores: true }; + private bool MeshSourceReady() => + _meshAdapter.MeshManager?.GlobalBuffer is { HasStores: true }; private bool BeginEntityDispatch( ICamera camera, out Matrix4x4 viewProjection, out Vector3 cameraWorldPosition) { - _shader?.Use(); _selectionLighting?.TickLighting(); _indoorProbeFrameCounter++; viewProjection = camera.View * camera.Projection; - _shader?.SetMatrix4("uViewProjection", viewProjection); - _shader?.SetInt("uLightingMode", 0); - _shader?.SetInt( - "uLightDebug", - RenderingDiagnostics.LightDebugMode); _missRequested.Clear(); bool diagnosticsEnabled = string.Equals( Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal); - if (diagnosticsEnabled && _gl is not null && !_gpuQueriesInitialized) - { - for (int index = 0; index < GpuQueryRingDepth; index++) - { - _gpuQueryOpaque[index] = _gl.GenQuery(); - _gpuQueryTransparent[index] = _gl.GenQuery(); - } - _gpuQueriesInitialized = true; - } _cpuStopwatch.Restart(); cameraWorldPosition = Vector3.Zero; @@ -2108,6 +1972,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable return diagnosticsEnabled; } + /// + /// is no longer read — it survives as a + /// parameter only because WbDrawDispatcher.PackedOracle.cs calls + /// this positionally with its own mirrored classification's + /// PackedRangeClassification.AnyVao and that partial is out of + /// scope for this collapse. + /// private void ExecuteClassifiedGroups( Matrix4x4 vp, Vector3 camPos, @@ -2120,7 +1991,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable bool observeCurrentPath) { // Nothing visible — skip the pass entirely. - if (!MeshSourceReady(anyVao)) + if (!MeshSourceReady()) { LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0); ObserveClassifiedDispatcherSubmission(observeCurrentPath, @@ -2277,230 +2148,18 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable deferTransparent, camPos); - // Campaign V slice V6j: on the encoder arm every per-frame upload below - // is a frame ring slice bound through the borrowed world pass, which - // retires the buffer-set pool structurally. See WbDrawDispatcher.Rhi.cs. - if (_gl is null) - { - SubmitRhi(vp, immediateInstances, totalDraws, diag); - _cpuStopwatch.Stop(); - if (diag) - { - long rhiCpuUs = _cpuStopwatch.ElapsedTicks * 1_000_000L - / System.Diagnostics.Stopwatch.Frequency; - _cpuSamples[_cpuSampleCursor] = rhiCpuUs; - _cpuSampleCursor = (_cpuSampleCursor + 1) % _cpuSamples.Length; - _drawsIssued += _opaqueDrawCount + _transparentDrawCount; - _instancesIssued += totalInstances; - MaybeFlushDiag(); - } - return; - } - - // ── Phase 5: upload four buffers ──────────────────────────────────── - ActivateNextDynamicBufferSet(); - fixed (float* ip = _instanceData) - UploadSsbo(_instanceSsbo, 0, ref _instanceSsboCapacityBytes, - ip, immediateInstances * 16 * sizeof(float)); - - fixed (BatchData* bp = _batchData) - UploadSsbo(_batchSsbo, 1, ref _batchSsboCapacityBytes, - bp, totalDraws * sizeof(BatchData)); - - // Phase U.4: per-instance clip-slot buffer (binding=3), one uint per - // instance, laid out parallel to _instanceData in Phase 3's group loop so - // instanceClipSlot[instanceIndex] tracks Instances[instanceIndex]. On the - // U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to - // U.3); under indoor routing it holds the per-instance slot from - // ResolveEntitySlot. No clear here — Phase 3 wrote exactly immediateInstances - // entries; only [0..immediateInstances) is uploaded, so any stale tail is - // never read by the shader. - fixed (uint* sp = _clipSlotData) - UploadSsbo(_clipSlotSsbo, 3, ref _clipSlotSsboCapacityBytes, - sp, immediateInstances * sizeof(uint)); - - // #142: per-instance indoor flag buffer (binding=6), one uint per instance, - // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances) - // is uploaded — stale tail never read (same guarantee as clip-slot above). - fixed (uint* dp = _indoorData) - UploadSsbo(_instIndoorSsbo, 6, ref _instIndoorSsboCapacityBytes, - dp, immediateInstances * sizeof(uint)); - - // #188: per-instance opacity buffer (binding=7), one float per instance, - // laid out parallel to _instanceData in Phase 3. Only [0..immediateInstances) - // is uploaded — stale tail never read (same guarantee as clip-slot above). - fixed (float* ap = _alphaData) - UploadSsbo(_instAlphaSsbo, 7, ref _instAlphaSsboCapacityBytes, - ap, immediateInstances * sizeof(float)); - - // SmartBox click lighting: x=luminosity, y=diffuse. mesh_modern.vert - // reads this only for the object path (uLightingMode=0), so EnvCell's - // independent mode-1 renderer does not consume this binding. - fixed (Vector2* hp = _selectionLightingData) - UploadSsbo(_instSelectionLightingSsbo, 8, ref _instSelectionLightingSsboCapacityBytes, - hp, immediateInstances * sizeof(float) * 2); - - // Fix B: global point-light buffer (binding=4) + per-instance light-set - // buffer (binding=5). The global buffer is this frame's PointSnapshot; the - // per-instance buffer holds 8 int indices into it per instance, laid out - // parallel to _instanceData in Phase 3. Both bound with ≥1 element so the - // shader never reads an unbound SSBO on a no-lights frame. - UploadGlobalLights(); - fixed (int* lp = _lightSetData) - UploadSsbo(_instLightSetSsbo, 5, ref _instLightSetSsboCapacityBytes, - lp, immediateInstances * LightManager.MaxLightsPerObject * sizeof(int)); - - // Campaign V slice V2 (binding=9): uploads only when ToInput registered - // a genuinely new handle this frame; otherwise just rebinds. - FlushAndBindTextureTable(); - - fixed (DrawElementsIndirectCommand* cp = _indirectCommands) - { - UploadDynamicBuffer( - BufferTargetARB.DrawIndirectBuffer, - _indirectBuffer, - ref _indirectBufferCapacityBytes, - cp, - totalDraws * sizeof(DrawElementsIndirectCommand)); - } - - PersistActiveDynamicBufferCapacities(); - - // Phase U.3: bind the SHARED per-cell clip-region SSBO (binding=2). The - // GameWindow-level ClipFrame already uploaded + bound it this frame; we - // re-bind defensively in case another consumer touched binding=2 since. - // When no shared id is set (0), bind our own no-clip fallback so the - // shader never reads an unbound SSBO at binding=2. - BindClipRegionBinding2(); - - // ── Phase 6: bind global VAO once ─────────────────────────────────── - _gl.BindVertexArray(anyVao); - - if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal)) - _gl.Disable(EnableCap.CullFace); - - // GPU timing: compute this frame's ring slot. We read frame N-3's - // result (the oldest data in the ring) before overwriting it with - // frame N's queries. Hoisted to function scope so both the opaque - // and transparent passes below can reference gpuQuerySlot. See spec - // §3 Q1/Q2 + §4 in - // docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md. - int gpuQuerySlot = _gpuQueryFrameIndex % GpuQueryRingDepth; - // diag is part of the gate so the read/issue/increment trio stays - // symmetric — without it, toggling ACDREAM_WB_DIAG mid-session would - // freeze the frame counter (gated by diag below) while the read kept - // re-reading the same slot, producing duplicate stale samples. - if (diag && _gpuQueriesInitialized && _gpuQueryFrameIndex >= GpuQueryRingDepth) - { - // #125: only read slots whose query objects were actually BEGUN (a - // zero-draw pass skips BeginQuery; reading a never-begun name is - // GL_INVALID_OPERATION). A pass that never ran contributes 0 ns. - ulong opaqueNs = 0, transNs = 0; - bool anyRead = false, allAvailable = true; - if (_gpuQueryOpaqueBegun[gpuQuerySlot]) - { - _gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.ResultAvailable, out int availO); - if (availO != 0) - { - _gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.Result, out opaqueNs); - anyRead = true; - } - else allAvailable = false; - } - if (_gpuQueryTransparentBegun[gpuQuerySlot]) - { - _gl.GetQueryObject(_gpuQueryTransparent[gpuQuerySlot], QueryObjectParameterName.ResultAvailable, out int availT); - if (availT != 0) - { - _gl.GetQueryObject(_gpuQueryTransparent[gpuQuerySlot], QueryObjectParameterName.Result, out transNs); - anyRead = true; - } - else allAvailable = false; - } - // If a begun query isn't available yet the sample is dropped - // silently. MedianMicros computes over the non-zero subset, so - // dropped samples don't poison the median. - if (anyRead && allAvailable) - { - long gpuUs = (long)((opaqueNs + transNs) / 1000UL); - _gpuSamples[_gpuSampleCursor] = gpuUs; - _gpuSampleCursor = (_gpuSampleCursor + 1) % _gpuSamples.Length; - } - } - - // ── Phase 7: opaque pass ───────────────────────────────────────────── - if (_opaqueDrawCount > 0) - { - _gl.Disable(EnableCap.Blend); - _gl.DepthMask(true); - // A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask - // from the alpha written by mesh_modern.frag so foliage edges are - // smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches. - // A.5 T22.5: gated by AlphaToCoverage property so Low/Medium presets - // (no MSAA) skip the unnecessary GL state change. - if (AlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); - _shader!.SetInt("uRenderPass", 0); - // Phase Post-A.5 (ISSUE #52, 2026-05-10): opaque section of - // Batches[] starts at index 0. See uDrawIDOffset comment in - // mesh_modern.vert for why this is needed. - _shader.SetInt("uDrawIDOffset", 0); - _gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer); - if (diag && _gpuQueriesInitialized) - { - _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryOpaque[gpuQuerySlot]); - _gpuQueryOpaqueBegun[gpuQuerySlot] = true; // #125 - } - DrawIndirectRange(0, _opaqueDrawCount); - if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed); - if (AlphaToCoverage) _gl.Disable(EnableCap.SampleAlphaToCoverage); - } - - // ── Phase 8: transparent pass ──────────────────────────────────────── - if (_transparentDrawCount > 0) - { - _gl.Enable(EnableCap.Blend); - _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - _gl.DepthMask(false); - // Phase Post-A.5 (ISSUE #52, 2026-05-10): transparent section of - // Batches[] starts at index _opaqueDrawCount. Without this offset, - // each transparent draw reads BatchData[0..transparentCount) — the - // OPAQUE section — and the lifestone crystal's apparent texture - // flickers to whatever opaque batch sorted first that frame. See - // uDrawIDOffset comment in mesh_modern.vert. - _shader!.SetInt("uDrawIDOffset", _opaqueDrawCount); - // Closed-shell translucent meshes still need culling, but the - // cull side must come from each dat batch just like the opaque - // section. BuildIndirectArrays preserves CullMode in _drawCullModes. - _gl.FrontFace(FrontFaceDirection.CW); - _shader.SetInt("uRenderPass", 1); - if (diag && _gpuQueriesInitialized) - { - _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryTransparent[gpuQuerySlot]); - _gpuQueryTransparentBegun[gpuQuerySlot] = true; // #125 - } - DrawIndirectRange(_opaqueDrawCount, _transparentDrawCount); - if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed); - _gl.DepthMask(true); - _gl.Disable(EnableCap.Blend); - } - - _gl.Disable(EnableCap.CullFace); - _gl.BindVertexArray(0); - + // Campaign V slice V11: every per-frame upload is a frame ring slice + // bound through the borrowed world pass, which retires the buffer-set + // pool structurally. See WbDrawDispatcher.Rhi.cs. + SubmitRhi(vp, immediateInstances, totalDraws, diag); _cpuStopwatch.Stop(); - if (diag) { - long cpuUs = _cpuStopwatch.ElapsedTicks * 1_000_000L / System.Diagnostics.Stopwatch.Frequency; + long cpuUs = _cpuStopwatch.ElapsedTicks * 1_000_000L + / System.Diagnostics.Stopwatch.Frequency; _cpuSamples[_cpuSampleCursor] = cpuUs; _cpuSampleCursor = (_cpuSampleCursor + 1) % _cpuSamples.Length; - - // GPU sample read happens BEFORE issuing the next frame's queries - // (see step 1.3 above). Increment the frame counter here so the - // next call computes a fresh slot. - if (_gpuQueriesInitialized) _gpuQueryFrameIndex++; - - _drawsIssued += _opaqueDrawCount + _transparentDrawCount; + _drawsIssued += _opaqueDrawCount + _transparentDrawCount; _instancesIssued += totalInstances; MaybeFlushDiag(); } @@ -3000,7 +2659,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable return; GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer; - if (global is null || !MeshSourceReady(global.VAO)) + if (global is null || !MeshSourceReady()) return; int count = tokens.Length; @@ -3039,18 +2698,11 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable // One upload per source per sorted alpha scope. RetailAlphaQueue later // draws contiguous ranges from this immutable prepared payload; it must // never overwrite these buffers for every short mesh/particle run. - if (_gl is null) - { - // A ring allocation cannot outlive its frame as a ref struct, but its - // buffer, offset and size can be stored — so the payload is written - // once here and bound many times below without recopying. - PrepareRhiAlphaSections(count); - return; - } - - ActivateNextDynamicBufferSet(); - UploadDeferredAlphaBuffers(count); - PersistActiveDynamicBufferCapacities(); + // + // A ring allocation cannot outlive its frame as a ref struct, but its + // buffer, offset and size can be stored — so the payload is written + // once here and bound many times below without recopying. + PrepareRhiAlphaSections(count); } private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) @@ -3062,60 +2714,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable throw new ArgumentOutOfRangeException(nameof(firstPreparedDraw)); GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer; - if (global is null || !MeshSourceReady(global.VAO)) + if (global is null || !MeshSourceReady()) return; - if (_gl is null) - { - DrawPreparedAlphaBatchRhi(global, firstPreparedDraw, drawCount); - return; - } - - _shader!.Use(); - _shader.SetMatrix4("uViewProjection", _deferredAlphaViewProjection); - _shader.SetInt("uLightingMode", 0); - _shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode); - _shader.SetInt("uRenderPass", 1); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 0, _instanceSsbo); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 1, _batchSsbo); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 3, _clipSlotSsbo); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 4, _globalLightsSsbo); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 5, _instLightSetSsbo); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 6, _instIndoorSsbo); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 7, _instAlphaSsbo); - _gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 8, _instSelectionLightingSsbo); - // Campaign V slice V2: already flushed in UploadDeferredAlphaBuffers - // (this is the same device table as the main draw path); just rebind. - _gl.BindBufferBase( - BufferTargetARB.ShaderStorageBuffer, - AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable, - WorldTextureTable.TextureTableGlName); - BindClipRegionBinding2(); - _gl.BindVertexArray(global.VAO); - _gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer); - _gl.Enable(EnableCap.DepthTest); - _gl.Enable(EnableCap.Blend); - _gl.DepthMask(false); - _gl.FrontFace(FrontFaceDirection.CW); - - int runStart = firstPreparedDraw; - int preparedEnd = firstPreparedDraw + drawCount; - while (runStart < preparedEnd) - { - TranslucencyKind blend = _deferredAlphaKinds[runStart]; - int runEnd = runStart + 1; - while (runEnd < preparedEnd && _deferredAlphaKinds[runEnd] == blend) - runEnd++; - - ApplyRetailBlend(blend); - DrawIndirectRange(runStart, runEnd - runStart); - runStart = runEnd; - } - - _gl.DepthMask(true); - _gl.Disable(EnableCap.Blend); - _gl.Disable(EnableCap.CullFace); - _gl.BindVertexArray(0); + DrawPreparedAlphaBatchRhi(global, firstPreparedDraw, drawCount); } private void EnsureDeferredAlphaCapacity(int count) @@ -3201,60 +2803,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable _deferredAlpha.Capacity = targetCapacity; } - private void UploadDeferredAlphaBuffers(int count) - { - fixed (float* p = _instanceData) - UploadSsbo(_instanceSsbo, 0, ref _instanceSsboCapacityBytes, - p, count * 16 * sizeof(float)); - fixed (BatchData* p = _batchData) - UploadSsbo(_batchSsbo, 1, ref _batchSsboCapacityBytes, - p, count * sizeof(BatchData)); - fixed (uint* p = _clipSlotData) - UploadSsbo(_clipSlotSsbo, 3, ref _clipSlotSsboCapacityBytes, - p, count * sizeof(uint)); - fixed (int* p = _lightSetData) - UploadSsbo(_instLightSetSsbo, 5, ref _instLightSetSsboCapacityBytes, - p, count * LightManager.MaxLightsPerObject * sizeof(int)); - fixed (uint* p = _indoorData) - UploadSsbo(_instIndoorSsbo, 6, ref _instIndoorSsboCapacityBytes, - p, count * sizeof(uint)); - fixed (float* p = _alphaData) - UploadSsbo(_instAlphaSsbo, 7, ref _instAlphaSsboCapacityBytes, - p, count * sizeof(float)); - fixed (Vector2* p = _selectionLightingData) - UploadSsbo(_instSelectionLightingSsbo, 8, ref _instSelectionLightingSsboCapacityBytes, - p, count * sizeof(float) * 2); - UploadGlobalLights(); - // Campaign V slice V2 (binding=9): flush/rebind the device texture table - // before DrawPreparedAlphaBatch, which submits without going through - // here again. - FlushAndBindTextureTable(); - - fixed (DrawElementsIndirectCommand* p = _indirectCommands) - { - UploadDynamicBuffer( - BufferTargetARB.DrawIndirectBuffer, - _indirectBuffer, - ref _indirectBufferCapacityBytes, - p, - count * sizeof(DrawElementsIndirectCommand)); - } - } - - private void ApplyRetailBlend(TranslucencyKind blend) - { - _gl!.BlendFunc( - blend == TranslucencyKind.InvAlpha - ? BlendingFactor.OneMinusSrcAlpha - : BlendingFactor.SrcAlpha, - blend switch - { - TranslucencyKind.Additive => BlendingFactor.One, - TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha, - _ => BlendingFactor.OneMinusSrcAlpha, - }); - } - private static int CompareOpaqueSubmissionOrder(InstanceGroup a, InstanceGroup b) { int cull = ((int)a.CullMode).CompareTo((int)b.CullMode); @@ -3284,282 +2832,6 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable return runs; } - private unsafe void DrawIndirectRange(int startCommand, int commandCount) - { - int end = startCommand + commandCount; - int command = startCommand; - while (command < end) - { - var cullMode = _drawCullModes[command]; - ApplyCullMode(cullMode); - - int runCount = 1; - while (command + runCount < end && _drawCullModes[command + runCount] == cullMode) - runCount++; - - // Each glMultiDrawElementsIndirect call restarts gl_DrawID at 0. - // Because this method splits one logical opaque/transparent pass - // into CullMode runs, the shader must receive the absolute command - // index for this run or it will read BatchData[0] again and bind - // the wrong texture for later runs. - _shader!.SetInt("uDrawIDOffset", command); - _gl!.MultiDrawElementsIndirect( - PrimitiveType.Triangles, - DrawElementsType.UnsignedShort, - (void*)(command * DrawCommandStride), - (uint)runCount, - (uint)DrawCommandStride); - - command += runCount; - } - } - - private void ApplyCullMode(CullMode mode) - { - // WB BaseObjectRenderManager.cs:850-866 applies CullMode per MDI group. - // WB GameScene.cs:843 sets FrontFace(CW) globally; SetCullMode then - // only chooses front/back culling. Keep the same convention here so - // splitting MDI commands by CullMode cannot resurrect stale CCW state. - _gl!.FrontFace(FrontFaceDirection.CW); - 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; - } - } - - private void ActivateNextDynamicBufferSet() - { - if (!_dynamicFrameStarted) - throw new InvalidOperationException("BeginFrame must be called before drawing world entities."); - - List slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot]; - if (_dynamicBufferSetCursor == slotSets.Count) - slotSets.Add(CreateDynamicBufferSet()); - - DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++]; - _activeDynamicBufferSet = set; - _instanceSsbo = set.InstanceSsbo; - _batchSsbo = set.BatchSsbo; - _indirectBuffer = set.IndirectBuffer; - _clipSlotSsbo = set.ClipSlotSsbo; - _globalLightsSsbo = set.GlobalLightsSsbo; - _instLightSetSsbo = set.InstanceLightSetSsbo; - _instIndoorSsbo = set.InstanceIndoorSsbo; - _instAlphaSsbo = set.InstanceAlphaSsbo; - _instSelectionLightingSsbo = set.InstanceSelectionLightingSsbo; - _instanceSsboCapacityBytes = set.InstanceCapacityBytes; - _batchSsboCapacityBytes = set.BatchCapacityBytes; - _indirectBufferCapacityBytes = set.IndirectCapacityBytes; - _clipSlotSsboCapacityBytes = set.ClipSlotCapacityBytes; - _globalLightsSsboCapacityBytes = set.GlobalLightsCapacityBytes; - _instLightSetSsboCapacityBytes = set.InstanceLightSetCapacityBytes; - _instIndoorSsboCapacityBytes = set.InstanceIndoorCapacityBytes; - _instAlphaSsboCapacityBytes = set.InstanceAlphaCapacityBytes; - _instSelectionLightingSsboCapacityBytes = set.InstanceSelectionLightingCapacityBytes; - } - - private DynamicBufferSet CreateDynamicBufferSet() - { - var set = new DynamicBufferSet(); - try - { - set.InstanceSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity instance SSBO"); - set.BatchSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity batch SSBO"); - set.IndirectBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating entity indirect buffer"); - set.ClipSlotSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity clip-slot SSBO"); - set.GlobalLightsSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity global-light SSBO"); - set.InstanceLightSetSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity light-set SSBO"); - set.InstanceIndoorSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity indoor SSBO"); - set.InstanceAlphaSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating entity alpha SSBO"); - set.InstanceSelectionLightingSsbo = TrackedGlResource.CreateBuffer( - _gl!, - "creating entity selection-lighting SSBO"); - return set; - } - catch (Exception creationFailure) - { - try { DeleteDynamicBufferSet(set); } - catch (Exception cleanupFailure) - { - throw new AggregateException( - "Entity dynamic-buffer creation and rollback failed.", - creationFailure, - cleanupFailure); - } - throw; - } - } - - private void DeleteDynamicBufferSet(DynamicBufferSet set) - { - List? failures = null; - void Attempt(uint buffer, int bytes, string name) - { - try { TrackedGlResource.DeleteBuffer(_gl!, buffer, bytes, $"deleting {name}"); } - catch (Exception ex) { (failures ??= []).Add(ex); } - } - - Attempt(set.InstanceSsbo, set.InstanceCapacityBytes, "entity instance SSBO"); - Attempt(set.BatchSsbo, set.BatchCapacityBytes, "entity batch SSBO"); - Attempt(set.IndirectBuffer, set.IndirectCapacityBytes, "entity indirect buffer"); - Attempt(set.ClipSlotSsbo, set.ClipSlotCapacityBytes, "entity clip-slot SSBO"); - Attempt(set.GlobalLightsSsbo, set.GlobalLightsCapacityBytes, "entity global-light SSBO"); - Attempt(set.InstanceLightSetSsbo, set.InstanceLightSetCapacityBytes, "entity light-set SSBO"); - Attempt(set.InstanceIndoorSsbo, set.InstanceIndoorCapacityBytes, "entity indoor SSBO"); - Attempt(set.InstanceAlphaSsbo, set.InstanceAlphaCapacityBytes, "entity alpha SSBO"); - Attempt( - set.InstanceSelectionLightingSsbo, - set.InstanceSelectionLightingCapacityBytes, - "entity selection-lighting SSBO"); - - if (failures is not null) - throw new AggregateException("One or more entity dynamic buffers failed to delete.", failures); - } - - private void PersistActiveDynamicBufferCapacities() - { - DynamicBufferSet set = _activeDynamicBufferSet - ?? throw new InvalidOperationException("No dynamic entity buffer set is active."); - set.InstanceCapacityBytes = _instanceSsboCapacityBytes; - set.BatchCapacityBytes = _batchSsboCapacityBytes; - set.IndirectCapacityBytes = _indirectBufferCapacityBytes; - set.ClipSlotCapacityBytes = _clipSlotSsboCapacityBytes; - set.GlobalLightsCapacityBytes = _globalLightsSsboCapacityBytes; - set.InstanceLightSetCapacityBytes = _instLightSetSsboCapacityBytes; - set.InstanceIndoorCapacityBytes = _instIndoorSsboCapacityBytes; - set.InstanceAlphaCapacityBytes = _instAlphaSsboCapacityBytes; - set.InstanceSelectionLightingCapacityBytes = _instSelectionLightingSsboCapacityBytes; - } - - private unsafe void UploadSsbo( - uint ssbo, - uint binding, - ref int capacityBytes, - void* data, - int byteCount) - { - UploadDynamicBuffer( - BufferTargetARB.ShaderStorageBuffer, - ssbo, - ref capacityBytes, - data, - byteCount); - _gl!.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, binding, ssbo); - } - - private unsafe void UploadDynamicBuffer( - BufferTargetARB target, - uint buffer, - ref int capacityBytes, - void* data, - int byteCount) - { - if (byteCount < 0) - throw new ArgumentOutOfRangeException(nameof(byteCount)); - - _gl!.BindBuffer(target, buffer); - // A render bucket can legitimately contain zero batches (for example the outdoor dynamic - // bucket immediately after auto-entry). Keep the buffer bound for the corresponding SSBO - // binding, but there is no active prefix to allocate or upload and no draw can read it. - if (byteCount == 0) - return; - - if (capacityBytes < byteCount) - { - int grownCapacity = DynamicBufferCapacity.Grow(capacityBytes, byteCount); - TrackedGlResource.AllocateBufferStorage( - _gl, - (GLEnum)target, - buffer, - capacityBytes, - grownCapacity, - GLEnum.DynamicDraw, - $"growing entity dynamic buffer {buffer} to {grownCapacity} bytes"); - capacityBytes = grownCapacity; - } - - _gl.BufferSubData(target, 0, (nuint)byteCount, data); - } - - /// - /// Fix B: pack into the binding=4 global light - /// buffer (one GlobalLight = 4 vec4 = 16 floats, std430 stride 64 bytes, - /// matching mesh_modern.vert's GlobalLight). Always uploads ≥1 element - /// so the shader never reads an unbound SSBO — on a no-lights frame index 0 is - /// a zeroed dummy that no instance set references (all sets are -1). - /// - private unsafe void UploadGlobalLights() - { - int n = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData); - int count = n > 0 ? n : 1; // never zero-size - // Pack guarantees _globalLightData holds at least max(n,1) * FloatsPerLight floats. - fixed (float* gp = _globalLightData) - UploadSsbo(_globalLightsSsbo, 4, ref _globalLightsSsboCapacityBytes, gp, - count * GlobalLightPacker.FloatsPerLight * sizeof(float)); - } - - /// - /// Campaign V slice V4t: drains the device texture table's dirty runs and - /// (re)binds it at - /// . - /// The drain is normally a no-op — a genuinely new slot means a new dat - /// surface or composite override, not a new frame — but the bind is - /// unconditional, because GL storage-buffer binding points are global and - /// another raw-GL renderer's binding 9 sits there between two of these - /// draws. Deleted with the raw-GL world path once these draws go through the - /// encoder, which binds the same table on every pipeline bind. - /// - private void FlushAndBindTextureTable() - { - AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable; - device.FlushTextureTable(); - _gl!.BindBufferBase( - BufferTargetARB.ShaderStorageBuffer, - AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable, - device.TextureTableGlName); - } - - /// - /// Phase U.3: bind the per-cell clip-region SSBO to binding=2. Prefers the - /// shared buffer (set via ); - /// otherwise lazily creates + binds a one-slot no-clip fallback so the shader - /// never reads an unbound SSBO. The fallback's single slot has count 0 - /// (pass-all), matching 's slot 0. - /// - private unsafe void BindClipRegionBinding2() - { - if (_sharedClipRegionSsbo != 0) - { - _gl!.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, - ClipFrame.MeshClipSsboBinding, _sharedClipRegionSsbo); - return; - } - - if (_fallbackClipRegionSsbo == 0) - { - _fallbackClipRegionSsbo = _gl!.GenBuffer(); - // One CellClip slot, all zeros: count 0 ⇒ shader passes every plane. - var zero = stackalloc byte[ClipFrame.CellClipStrideBytes]; - for (int i = 0; i < ClipFrame.CellClipStrideBytes; i++) zero[i] = 0; - _gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, _fallbackClipRegionSsbo); - _gl.BufferData(BufferTargetARB.ShaderStorageBuffer, - (nuint)ClipFrame.CellClipStrideBytes, zero, BufferUsageARB.DynamicDraw); - } - _gl!.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, - ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo); - } - private void MaybeFlushDiag() { long now = Environment.TickCount64; @@ -4154,14 +3426,11 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable { if (_disposeResources is null) { - var releases = new List<(string Name, Action Release)>(); - // Campaign V slice V6j: the encoder arm owns no GL names. Its + // Campaign V slice V11: the RHI arm is the only arm — its // pipelines route their physical free through the device's - // retirement queue, so the ledger below is empty there. - if (_gl is null) - DisposeRhiResources(); - else - BuildDisposeReleases(releases); + // retirement queue, so the release ledger is always empty. + var releases = new List<(string Name, Action Release)>(); + DisposeRhiResources(); _disposeResources = new RetryableResourceReleaseLedger(releases); } @@ -4188,127 +3457,9 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable } } - private void BuildDisposeReleases(List<(string Name, Action Release)> releases) - { - for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++) - { - List frameSets = _dynamicBufferSetsByFrame[frame]; - for (int index = 0; index < frameSets.Count; index++) - AddDynamicBufferSetReleases(releases, frameSets[index], frame, index); - } - - AddRawGlRelease( - releases, - _fallbackClipRegionSsbo, - "fallback-clip-region", - "deleting entity fallback clip SSBO", - _gl!.DeleteBuffer); - - if (!_gpuQueriesInitialized) - return; - for (int i = 0; i < GpuQueryRingDepth; i++) - { - AddRawGlRelease( - releases, - _gpuQueryOpaque[i], - $"opaque-query-{i}", - "deleting entity opaque timing query", - _gl.DeleteQuery); - AddRawGlRelease( - releases, - _gpuQueryTransparent[i], - $"transparent-query-{i}", - "deleting entity transparent timing query", - _gl.DeleteQuery); - } - } - - private void AddDynamicBufferSetReleases( - List<(string Name, Action Release)> releases, - DynamicBufferSet set, - int frame, - int index) - { - AddTrackedBufferRelease(releases, set.InstanceSsbo, set.InstanceCapacityBytes, - $"dynamic-{frame}-{index}-instances", "deleting entity instance SSBO"); - AddTrackedBufferRelease(releases, set.BatchSsbo, set.BatchCapacityBytes, - $"dynamic-{frame}-{index}-batches", "deleting entity batch SSBO"); - AddTrackedBufferRelease(releases, set.IndirectBuffer, set.IndirectCapacityBytes, - $"dynamic-{frame}-{index}-indirect", "deleting entity indirect buffer"); - AddTrackedBufferRelease(releases, set.ClipSlotSsbo, set.ClipSlotCapacityBytes, - $"dynamic-{frame}-{index}-clip-slots", "deleting entity clip-slot SSBO"); - AddTrackedBufferRelease(releases, set.GlobalLightsSsbo, set.GlobalLightsCapacityBytes, - $"dynamic-{frame}-{index}-global-lights", "deleting entity global-light SSBO"); - AddTrackedBufferRelease(releases, set.InstanceLightSetSsbo, set.InstanceLightSetCapacityBytes, - $"dynamic-{frame}-{index}-light-sets", "deleting entity light-set SSBO"); - AddTrackedBufferRelease(releases, set.InstanceIndoorSsbo, set.InstanceIndoorCapacityBytes, - $"dynamic-{frame}-{index}-indoor", "deleting entity indoor SSBO"); - AddTrackedBufferRelease(releases, set.InstanceAlphaSsbo, set.InstanceAlphaCapacityBytes, - $"dynamic-{frame}-{index}-alpha", "deleting entity alpha SSBO"); - AddTrackedBufferRelease( - releases, - set.InstanceSelectionLightingSsbo, - set.InstanceSelectionLightingCapacityBytes, - $"dynamic-{frame}-{index}-selection-lighting", - "deleting entity selection-lighting SSBO"); - } - - 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)); - } - - private void AddRawGlRelease( - List<(string Name, Action Release)> releases, - uint resource, - string name, - string context, - Action delete) - { - if (resource == 0) - return; - var release = new RetryableGpuResourceRelease( - () => GLHelpers.ThrowOnResourceError(_gl!, $"{context} (precondition)"), - () => - { - delete(resource); - GLHelpers.ThrowOnResourceError(_gl!, context); - }); - releases.Add((name, release.Run)); - } - private void CompleteDispose() { - foreach (List frameSets in _dynamicBufferSetsByFrame) - frameSets.Clear(); - _activeDynamicBufferSet = null; _dynamicFrameStarted = false; - _instanceSsbo = 0; - _batchSsbo = 0; - _indirectBuffer = 0; - _clipSlotSsbo = 0; - _globalLightsSsbo = 0; - _instLightSetSsbo = 0; - _instIndoorSsbo = 0; - _instAlphaSsbo = 0; - _instSelectionLightingSsbo = 0; - _fallbackClipRegionSsbo = 0; - Array.Clear(_gpuQueryOpaque); - Array.Clear(_gpuQueryTransparent); - _gpuQueriesInitialized = false; } // ── Public types + helpers for BuildIndirectArrays (Task 9) ───────────── diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs index 04ca35f3..abf93dba 100644 --- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -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; } - /// - /// Campaign V slice V4t: the GL device whose texture table every mesh - /// batch's GpuTextureSlot 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. - /// - 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); diff --git a/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs b/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs index a2207b05..9addda89 100644 --- a/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs +++ b/src/AcDream.App/Rendering/Wb/WorldTextureArray.cs @@ -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 /// ITextureArray implementation over , not a -/// codec." This is that interface. and -/// implement it, and which one exists is -/// decided once at composition by — -/// never per call, so the GL path executes exactly the statements it executed -/// before. +/// codec." This is that interface. ManagedGLTextureArray used to be its +/// GL implementation, alongside ; which one +/// existed was decided once at composition by +/// , never per call. Campaign V slice +/// V11 deleted ManagedGLTextureArray along with the rest of the raw-GL +/// arm, so is now the sole implementation. /// -/// The slot, not the handle, is the seam. Before this slice +/// The slot, not the handle, is the seam. Before V6i-2 /// ObjectMeshManager read BindlessWrapHandle/ /// BindlessClampHandle off the concrete GL array and interned them into /// the device table itself. A 64-bit ARB_bindless_texture handle is -/// unspellable on Vulkan, so the array now answers the question the caller was -/// really asking — — 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. +/// unspellable on Vulkan, so the array answers the question the caller was +/// really asking — — instead: the RHI array +/// registered its two (texture, sampler) pairs at construction and returns a +/// field. /// 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); } /// The retirement queue array layers and images are released through. @@ -132,36 +132,6 @@ internal interface IWorldTextureArrayFactory IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers); } -/// -/// The GL arm. Delegates to the same OpenGLGraphicsDevice entry point -/// called directly before this slice, so the -/// shipping backend's construction is textually unchanged. -/// -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); -} - /// /// The backend-neutral arm. Creates through /// 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; + + /// + /// The expected byte count for one uploaded layer of + /// at x. + /// + 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}"), + }; + } + + /// + /// Validates an upload payload against the format's expected byte count and + /// rejects transfer overrides that contradict it. + /// + 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}."); + } + } } diff --git a/src/AcDream.App/Rendering/WorldPassSurface.cs b/src/AcDream.App/Rendering/WorldPassSurface.cs index b4aee130..e7198b50 100644 --- a/src/AcDream.App/Rendering/WorldPassSurface.cs +++ b/src/AcDream.App/Rendering/WorldPassSurface.cs @@ -1,10 +1,20 @@ using System.Numerics; using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Wb; -using Silk.NET.OpenGL; namespace AcDream.App.Rendering; +/// +/// Restores the frame-global rendering convention shared by frame clear and +/// exceptional world-pass rollback. The raw-GL implementation this contract +/// used to have alongside it (RenderFrameGlStateController) was deleted +/// at Campaign V slice V11; is the only +/// implementation left. +/// +internal interface IRenderFrameGlState +{ + void RestoreFrameDefaults(); +} + /// /// Campaign V slice V6j: the small graphics surface the two world pass executors /// touch directly, expressed once so their ordering logic — which is retail's, @@ -75,97 +85,11 @@ internal interface IWorldPassSurface } /// -/// The GL arm. Every statement below is the one the executor used to issue -/// inline, in the same order, against the same objects. -/// -internal sealed class GlWorldPassSurface : IWorldPassSurface -{ - private readonly GL _gl; - private readonly ClipFrame _clipFrame; - private readonly IRetailPViewFramebufferSource _framebuffer; - private readonly WbDrawDispatcher _entities; - private readonly EnvCellRenderer _envCells; - private readonly TerrainModernRenderer? _terrain; - - public GlWorldPassSurface( - GL gl, - ClipFrame clipFrame, - IRetailPViewFramebufferSource framebuffer, - WbDrawDispatcher entities, - EnvCellRenderer envCells, - TerrainModernRenderer? terrain) - { - _gl = gl ?? throw new ArgumentNullException(nameof(gl)); - _clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame)); - _framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer)); - _entities = entities ?? throw new ArgumentNullException(nameof(entities)); - _envCells = envCells ?? throw new ArgumentNullException(nameof(envCells)); - _terrain = terrain; - } - - public void PrepareClipFrame(int terrainUploadCount) - { - // Allocate every terrain record before issuing the first draw. BufferData - // must not replace the arena while an earlier slice can reference it. - _clipFrame.ReserveTerrainUploads(_gl, terrainUploadCount); - _clipFrame.UploadRegions(_gl); - _entities.SetClipRegionSsbo(_clipFrame.RegionSsbo); - _envCells.SetClipRegionSsbo(_clipFrame.RegionSsbo); - UploadTerrainClip(); - } - - public void SetTerrainClip(ReadOnlySpan planes) - { - _clipFrame.SetTerrainClip(planes); - UploadTerrainClip(); - } - - public void BindTerrainClip() => _clipFrame.BindTerrainClip(_gl); - - public void EnableClipDistances() - { - for (int index = 0; index < ClipFrame.MaxPlanes; index++) - _gl.Enable(EnableCap.ClipDistance0 + index); - } - - public void DisableClipDistances() - { - for (int index = 0; index < ClipFrame.MaxPlanes; index++) - _gl.Disable(EnableCap.ClipDistance0 + index); - } - - public bool BeginScissor(Vector4 ndcAabb) - { - RetailPViewFramebufferSize framebuffer = _framebuffer.Capture(); - var box = NdcScissorRect.ToPixels( - ndcAabb, - framebuffer.Width, - framebuffer.Height); - _gl.Enable(EnableCap.ScissorTest); - _gl.Scissor(box.X, box.Y, (uint)box.Width, (uint)box.Height); - return true; - } - - public void EndScissor() => _gl.Disable(EnableCap.ScissorTest); - - public void ClearInteriorDepth() - { - _gl.Disable(EnableCap.ScissorTest); - _gl.DepthMask(true); - _gl.Clear(ClearBufferMask.DepthBufferBit); - } - - private void UploadTerrainClip() - { - TerrainClipBufferBinding binding = _clipFrame.UploadTerrainClip(_gl); - _terrain?.SetClipUbo(binding); - } -} - -/// -/// The RHI arm. The clip tables become ring sections published on the world pass -/// scope, the scissor becomes dynamic state on the borrowed encoder, and the -/// depth clear becomes a scoped vkCmdClearAttachments. +/// The clip tables are ring sections published on the world pass scope, the +/// scissor is dynamic state on the borrowed encoder, and the depth clear is a +/// scoped vkCmdClearAttachments. The raw-GL implementation this used to +/// sit alongside (GlWorldPassSurface) was deleted at Campaign V slice +/// V11. /// internal sealed class RhiWorldPassSurface : IWorldPassSurface { diff --git a/src/AcDream.App/RuntimeOptions.cs b/src/AcDream.App/RuntimeOptions.cs index 0468b63c..703a14a3 100644 --- a/src/AcDream.App/RuntimeOptions.cs +++ b/src/AcDream.App/RuntimeOptions.cs @@ -59,7 +59,6 @@ public sealed record RuntimeOptions( float FogEndMultiplier, ResidencyBudgetOptions ResidencyBudgets, StreamingWorkBudgetOptions StreamingWorkBudgets, - RenderBackendKind RenderBackend, string? VulkanDeviceOverride, string? VulkanForcedUnsupportedFeature, bool VulkanCapabilityProbe, @@ -147,10 +146,6 @@ public sealed record RuntimeOptions( FogEndMultiplier: TryParseFloat(env("ACDREAM_FOG_END_MULT")) ?? 0.95f, ResidencyBudgets: ResidencyBudgetOptions.Parse(env), StreamingWorkBudgets: StreamingWorkBudgetOptions.Parse(env), - // Campaign V slice V10 flipped this. Unset, empty, or any - // unrecognised value now means Vulkan: it is the shipping backend, - // and a typo must never silently start the backend V11 deletes. - RenderBackend: ParseRenderBackend(env("ACDREAM_RENDER_BACKEND")), // Physical-device override, matched as a decimal index first and then // as a case-insensitive device-name substring. Recorded verbatim in // graphical-capabilities-vulkan.json whether or not it matched. @@ -178,29 +173,6 @@ public sealed record RuntimeOptions( TryParseNonNegativeInt(env("ACDREAM_VULKAN_PROBE_FRAMES")) ?? 0); } - /// - /// Startup backend request. Campaign V slice V10 inverted this: Vulkan is - /// the default, and only the explicit escape-hatch tokens gl and - /// opengl select OpenGL. Everything else — unset, vulkan, or a - /// typo — is Vulkan. - /// - /// - /// The polarity of the typo case is deliberate and it flipped with the - /// default. Before V10 an unrecognised token had to land on GL, because - /// Vulkan was dark and a typo must never silently start a backend that - /// cannot draw. After V10 an unrecognised token must land on Vulkan for the - /// same reason read the other way: GL is the retiring backend that slice V11 - /// deletes, so a typo must never silently pin a process to it. opengl - /// is honoured alongside gl because the escape hatch exists to be - /// found, and the failure mode of a near-miss spelling here is a process - /// that quietly is not on the backend the operator asked to fall back to. - /// - private static RenderBackendKind ParseRenderBackend(string? value) - => string.Equals(value, "gl", StringComparison.OrdinalIgnoreCase) - || string.Equals(value, "opengl", StringComparison.OrdinalIgnoreCase) - ? RenderBackendKind.Gl - : RenderBackendKind.Vulkan; - /// True iff live-mode credentials are present and valid for connecting. public bool HasLiveCredentials => LiveMode && !string.IsNullOrEmpty(LiveUser) && !string.IsNullOrEmpty(LivePass); diff --git a/src/AcDream.App/UI/UiHost.cs b/src/AcDream.App/UI/UiHost.cs index d2b0043b..254fe689 100644 --- a/src/AcDream.App/UI/UiHost.cs +++ b/src/AcDream.App/UI/UiHost.cs @@ -3,7 +3,6 @@ using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using Silk.NET.Input; -using Silk.NET.OpenGL; namespace AcDream.App.UI; diff --git a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs index bf69aeb9..ff2c6b0f 100644 --- a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using AcDream.App.Audio; using AcDream.App.Composition; @@ -19,7 +19,6 @@ using AcDream.Runtime.Gameplay; using DatReaderWriter.DBObjs; using Silk.NET.Input; using Silk.NET.OpenAL; -using Silk.NET.OpenGL; namespace AcDream.App.Tests.Composition; @@ -253,7 +252,7 @@ public sealed class ContentEffectsAudioCompositionTests Poses = new EntityEffectPoseRegistry(); Factory = new Factory(); Publication = new Publication(); - Platform = new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, null!); + Platform = new GameWindowPlatformResult(TestGameWindowGraphics.Instance, null!); Host = (HostInputCameraResult)RuntimeHelpers.GetUninitializedObject( typeof(HostInputCameraResult)); Dependencies = new ContentEffectsAudioDependencies( diff --git a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs index 9c517617..267bb3ee 100644 --- a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using System.Reflection; using AcDream.App.Composition; using AcDream.App.Input; @@ -8,7 +8,6 @@ using AcDream.App.Tests.Rendering.Gpu; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; using Silk.NET.Maths; -using Silk.NET.OpenGL; namespace AcDream.App.Tests.Composition; @@ -95,7 +94,7 @@ public sealed class HostInputCameraCompositionTests IKeyboard keyboard = DispatchProxy.Create(); IMouse mouse = DispatchProxy.Create(); Input = new InputContext(keyboard, mouse); - Platform = new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, Input); + Platform = new GameWindowPlatformResult(TestGameWindowGraphics.Instance, Input); ViewportAspect = new ViewportAspectState(); Framebuffer = new FramebufferResizeController(ViewportAspect); Capture = new CaptureSource(); diff --git a/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs b/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs index dc23169b..93962314 100644 --- a/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs +++ b/tests/AcDream.App.Tests/Composition/TestGameWindowGraphics.cs @@ -1,61 +1,30 @@ -using AcDream.App; using AcDream.App.Composition; -using Silk.NET.Core.Contexts; -using Silk.NET.OpenGL; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Gpu.Vk; namespace AcDream.App.Tests.Composition; /// -/// Campaign V slice V6h: the graphics handle composition tests hand to a phase. +/// The graphics handle composition tests hand to a phase. /// -/// The phases now select their backend arm from -/// rather than from a bare GL reference, -/// so a test that means "compose the OpenGL arm" has to say so. The GL instance -/// is never called: every composition test supplies a stub factory that ignores -/// its context argument, and the loader below would fault if anything did — which -/// is the point. It is a token identifying the arm, not a driver. +/// Campaign V slice V11 deleted the raw-GL arm — GameWindowGraphics +/// no longer has a Backend or Gl member to select between, so this +/// is now a single Vulkan-shaped token: a real +/// (composition phases require one, and it needs no live surface — just a +/// sample count), no live because tests never +/// dereference it. /// internal sealed class TestGameWindowGraphics : GameWindowGraphics { - private readonly GL? _gl; + public static TestGameWindowGraphics Instance { get; } = new(); - private TestGameWindowGraphics(RenderBackendKind backend, GL? gl) + private TestGameWindowGraphics() { - Backend = backend; - _gl = gl; } - /// Selects the OpenGL arm, with a context token no test dereferences. - public static TestGameWindowGraphics OpenGl { get; } = - new(RenderBackendKind.Gl, new GL(new UnusableNativeContext())); - - /// Selects the Vulkan arm: no GL context exists. - public static TestGameWindowGraphics Vulkan { get; } = - new(RenderBackendKind.Vulkan, null); - - public override RenderBackendKind Backend { get; } - - public override GL? Gl => _gl; + public override IWorldPassScope? WorldPassScope { get; } = new VulkanWorldPassScope(sampleCount: 1); public override void Dispose() { } - - private sealed class UnusableNativeContext : INativeContext - { - public nint GetProcAddress(string proc, int? slot = null) => - throw new InvalidOperationException( - $"A composition test called GL entry point '{proc}'. " + - "Test graphics are a backend token, not a driver."); - - public bool TryGetProcAddress(string proc, out nint addr, int? slot = null) - { - addr = 0; - return false; - } - - public void Dispose() - { - } - } } diff --git a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs index 5ff43550..e2ee6328 100644 --- a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Runtime.CompilerServices; using AcDream.App.Composition; using AcDream.App.Rendering; @@ -14,7 +14,6 @@ using AcDream.UI.Abstractions.Settings; using DatReaderWriter.DBObjs; using Silk.NET.Input; using Silk.NET.OpenGL; -using Shader = AcDream.App.Rendering.Shader; namespace AcDream.App.Tests.Composition; @@ -90,15 +89,12 @@ public sealed class WorldRenderCompositionTests } [Theory] - [InlineData("terrain shader", "terrain shader")] [InlineData("scene lighting", "scene lighting")] [InlineData("debug lines", "debug lines")] [InlineData("HUD", "text renderer|debug font")] [InlineData("terrain", "terrain")] - [InlineData("mesh shader", "mesh shader")] [InlineData("WB mesh adapter", "WB mesh adapter")] [InlineData("texture cache", "texture cache")] - [InlineData("sampler cache", "sampler cache")] public void FailedPublicationRollsBackOnlyItsUnpublishedResourcePrefix( string publication, string expectedReleaseOrder) @@ -193,7 +189,7 @@ public sealed class WorldRenderCompositionTests if (point == _failurePoint) throw new InvalidOperationException($"fault at {point}"); }).Compose( - new GameWindowPlatformResult(TestGameWindowGraphics.OpenGl, null!), + new GameWindowPlatformResult(TestGameWindowGraphics.Instance, null!), Content, new SettingsDevToolsResult( QualitySettings.From(QualityPreset.High))); @@ -224,8 +220,6 @@ public sealed class WorldRenderCompositionTests public ResidencyBudgetOptions? TextureBudgets { get; private set; } public ResidencyManager? RegisteredResidency { get; private set; } - public void InitializeGlState(GL gl) { } - public WorldRegionData LoadRegion(IDatReaderWriter dats) => new(Stub(), new float[256]); @@ -233,16 +227,6 @@ public sealed class WorldRenderCompositionTests WorldEnvironmentController environment, Region region) { } - public BindlessSupport RequireBindless(GL gl, Action log) => - Stub(); - - public TerrainAtlas AcquireTerrainAtlas( - IGameRenderResourceLifetime lifetime, - GL gl, - IDatReaderWriter dats, - BindlessSupport bindless) => - lifetime.AcquireTerrainAtlas(() => Atlas); - /// /// Campaign V slice V6i-2: the arm a backend with no GL context takes. /// Returns the same stub atlas through the same lifetime owner, so the @@ -258,7 +242,7 @@ public sealed class WorldRenderCompositionTests /// Recorded rather than run: the exercise needs a real /// to create images through. Its behaviour is /// covered by RhiWorldTextureArrayTests and by the Vulkan - /// composition-host run — see plan §5.5.13. + /// composition-host run — see plan §5.5.13. /// public void ExerciseBackendNeutralWorldTextures( IGpuDevice device, @@ -270,12 +254,6 @@ public sealed class WorldRenderCompositionTests public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) => AnisotropicLevel = level; - public Shader CreateTerrainShader(GL gl, string shadersDirectory) => - Resource("terrain shader"); - - public SceneLightingUboBinding CreateSceneLighting(GL gl) => - Resource("scene lighting"); - public SceneLightingUboBinding CreateBackendNeutralSceneLighting( ICurrentGpuFrameSource frameSource, IWorldPassScope scope) => @@ -294,15 +272,6 @@ public sealed class WorldRenderCompositionTests IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory) => Resource("text renderer"); - public TerrainModernRenderer CreateTerrain( - GL gl, - BindlessSupport bindless, - Shader shader, - TerrainAtlas atlas, - IGpuDevice gpuDevice, - IGpuResourceRetirementQueue retirement) => - Resource("terrain"); - public TerrainModernRenderer CreateBackendNeutralTerrain( IGpuDevice gpuDevice, ICurrentGpuFrameSource frameSource, @@ -323,9 +292,6 @@ public sealed class WorldRenderCompositionTests Stub(), new ConcurrentDictionary()); - public Shader CreateMeshShader(GL gl, string shadersDirectory) => - Resource("mesh shader"); - public WbMeshAdapter CreateMeshAdapter( GL? gl, IGpuDevice device, @@ -339,10 +305,8 @@ public sealed class WorldRenderCompositionTests } public TextureCache CreateTextureCache( - GL gl, IGpuDevice device, IDatReaderWriter dats, - BindlessSupport bindless, IGpuResourceRetirementQueue retirement, string diagnosticsDirectory, ResidencyBudgetOptions budgets) @@ -362,9 +326,6 @@ public sealed class WorldRenderCompositionTests RegisteredResidency = manager; } - public SamplerCache CreateSamplerCache(GL gl) => - Resource("sampler cache"); - public void Release(IDisposable resource) { Releases.Add(_names[resource]); @@ -385,10 +346,6 @@ public sealed class WorldRenderCompositionTests public WbMeshAdapter? MeshAdapter { get; private set; } public TextureCache? TextureCache { get; private set; } - public void PublishBindlessSupport(BindlessSupport value) => - Fail("bindless"); - public void PublishTerrainShader(Shader value) => - Fail("terrain shader"); public void PublishSceneLighting(SceneLightingUboBinding value) => Fail("scene lighting"); public void PublishDebugLines(DebugLineRenderer value) => @@ -407,7 +364,6 @@ public sealed class WorldRenderCompositionTests TerrainBlendingContext blending, ConcurrentDictionary surfaceCache) => Fail("terrain build state"); - public void PublishMeshShader(Shader value) => Fail("mesh shader"); public void PublishWbMeshAdapter(WbMeshAdapter value) { @@ -421,9 +377,6 @@ public sealed class WorldRenderCompositionTests TextureCache = value; } - public void PublishSamplerCache(SamplerCache value) => - Fail("sampler cache"); - private void Fail(string point) { if (string.Equals(failure, point, StringComparison.Ordinal)) diff --git a/tests/AcDream.App.Tests/Platform/GraphicalCapabilityRequirementsTests.cs b/tests/AcDream.App.Tests/Platform/GraphicalCapabilityRequirementsTests.cs deleted file mode 100644 index 741686e1..00000000 --- a/tests/AcDream.App.Tests/Platform/GraphicalCapabilityRequirementsTests.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Text.Json; -using AcDream.App.Platform; - -namespace AcDream.App.Tests.Platform; - -public sealed class GraphicalCapabilityRequirementsTests -{ - [Fact] - public void SupportedModernContextPasses() - { - GraphicalCapabilityRecord capabilities = CreateSupported(); - - Assert.Empty(GraphicalCapabilityRequirements.Evaluate(capabilities)); - } - - [Theory] - [InlineData("bindless")] - [InlineData("draw-parameters")] - [InlineData("mdi")] - [InlineData("ssbo")] - [InlineData("timer")] - [InlineData("depth")] - [InlineData("stencil")] - [InlineData("srgb")] - [InlineData("keyboard")] - [InlineData("mouse")] - public void MissingMandatoryCapabilityIsRejected(string missing) - { - GraphicalCapabilityRecord capabilities = CreateSupported(); - capabilities = missing switch - { - "bindless" => capabilities with - { - HasBindlessTexture = false, - }, - "draw-parameters" => capabilities with - { - HasShaderDrawParameters = false, - }, - "mdi" => capabilities with - { - HasMultiDrawIndirect = false, - }, - "ssbo" => capabilities with - { - HasShaderStorageBuffer = false, - }, - "timer" => capabilities with - { - HasTimerQuery = false, - }, - "depth" => capabilities with - { - Framebuffer = capabilities.Framebuffer with - { - DepthBits = 16, - }, - }, - "stencil" => capabilities with - { - Framebuffer = capabilities.Framebuffer with - { - StencilBits = 0, - }, - }, - "srgb" => capabilities with - { - Framebuffer = capabilities.Framebuffer with - { - FramebufferSrgbApi = false, - }, - }, - "keyboard" => capabilities with - { - Input = capabilities.Input with - { - KeyboardCount = 0, - }, - }, - "mouse" => capabilities with - { - Input = capabilities.Input with - { - MouseCount = 0, - }, - }, - _ => throw new ArgumentOutOfRangeException(nameof(missing)), - }; - - Assert.NotEmpty(GraphicalCapabilityRequirements.Evaluate(capabilities)); - } - - [Fact] - public void AdvertisedBufferStorageRequiresSuccessfulPersistentProbe() - { - GraphicalCapabilityRecord capabilities = CreateSupported() with - { - FunctionProbe = CreateSupported().FunctionProbe with - { - PersistentBufferStorage = false, - }, - }; - - Assert.Contains( - GraphicalCapabilityRequirements.Evaluate(capabilities), - failure => failure.Contains( - "persistent mapping", - StringComparison.Ordinal)); - } - - [Fact] - public void MissingOptionalBufferStorageDoesNotRequireProbe() - { - GraphicalCapabilityRecord capabilities = CreateSupported() with - { - HasBufferStorage = false, - FunctionProbe = CreateSupported().FunctionProbe with - { - PersistentBufferStorage = null, - }, - }; - - Assert.Empty(GraphicalCapabilityRequirements.Evaluate(capabilities)); - } - - [Fact] - public void UnsupportedMessageNamesDriverProtocolFailureAndReport() - { - GraphicalCapabilityRecord capabilities = CreateSupported() with - { - SupportFailures = ["GL_ARB_bindless_texture is required."], - }; - - string message = GraphicalCapabilityGuard.FormatUnsupportedMessage( - capabilities, - "capabilities.json"); - - Assert.Contains("Mesa", message); - Assert.Contains("RadeonSI", message); - Assert.Contains("Wayland", message); - Assert.Contains("GL_ARB_bindless_texture", message); - Assert.Contains( - Path.GetFullPath("capabilities.json"), - message); - } - - [Fact] - public void ReportWriterAtomicallyOverwritesJson() - { - string directory = Path.Combine( - Path.GetTempPath(), - "acdream-capability-tests", - Guid.NewGuid().ToString("N")); - string path = Path.Combine(directory, "capabilities.json"); - try - { - GraphicalCapabilityReportWriter.Write(path, CreateSupported()); - GraphicalCapabilityReportWriter.Write( - path, - CreateSupported() with - { - GlRenderer = "second renderer", - }); - - using JsonDocument report = JsonDocument.Parse( - File.ReadAllText(path)); - Assert.Equal( - "second renderer", - report.RootElement - .GetProperty(nameof(GraphicalCapabilityRecord.GlRenderer)) - .GetString()); - Assert.Equal( - "Wayland", - report.RootElement - .GetProperty(nameof( - GraphicalCapabilityRecord.ActiveDisplayProtocol)) - .GetString()); - Assert.False(File.Exists(path + ".tmp")); - } - finally - { - if (Directory.Exists(directory)) - Directory.Delete(directory, recursive: true); - } - } - - private static GraphicalCapabilityRecord CreateSupported() => new( - DateTimeOffset.UnixEpoch, - "linux-x64", - GraphicalHostOperatingSystem.Linux, - GraphicalDisplayProtocol.Wayland, - GraphicalDisplayProtocol.Wayland, - "test", - "Silk.NET.Windowing.Glfw", - "3.4.0", - "Mesa", - "RadeonSI", - "4.6", - "4.60", - 4, - 6, - 1, - 1, - HasBindlessTexture: true, - HasShaderDrawParameters: true, - HasMultiDrawIndirect: true, - HasShaderStorageBuffer: true, - HasBufferStorage: true, - HasTimerQuery: true, - MaximumShaderStorageBufferBindings: 16, - MaximumUniformBufferBindings: 72, - MaximumTextureSize: 16_384, - MaximumArrayTextureLayers: 2_048, - MaximumCombinedTextureImageUnits: 192, - new GraphicalFramebufferCapabilities( - 8, - 8, - 8, - 8, - 24, - 8, - 1, - 4, - FramebufferSrgbApi: true), - new GraphicalInputCapabilities( - KeyboardCount: 1, - MouseCount: 1, - GamepadCount: 0, - JoystickCount: 0), - new GraphicalWindowCapabilities( - 1280, - 720, - 2560, - 1440, - "test monitor", - 144, - VSync: false), - new GraphicalAudioCapabilities( - Requested: false, - Available: false, - PlaybackSubmitted: false, - DisposalComplete: true, - Backend: "not requested"), - new GraphicalSmokeLifecycleCapabilities( - OwnedWindowCount: 1, - OwnedGlApiCount: 1, - OwnedInputContextCount: 1, - OwnedAudioEngineCount: 0, - ShutdownComplete: false), - [], - new GraphicalFunctionProbeResult( - BindlessTexture: true, - ShaderDrawParameters: true, - MultiDrawIndirect: true, - ShaderStorageBuffer: true, - TimerQuery: true, - SrgbFramebuffer: true, - PersistentBufferStorage: true, - Failures: []), - SupportFailures: []); -} diff --git a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs index 46090dd1..ccb0f221 100644 --- a/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs +++ b/tests/AcDream.App.Tests/Rendering/ClipFrameLayoutTests.cs @@ -1,5 +1,6 @@ using System.Numerics; using AcDream.App.Rendering; +using AcDream.App.Rendering.Gpu; using Xunit; namespace AcDream.App.Tests.Rendering; @@ -41,7 +42,11 @@ public class ClipFrameLayoutTests Assert.Equal(8, ClipFrame.MaxPlanes); Assert.Equal(144, ClipFrame.TerrainUboBytes); // Binding contract: mesh clip regions on SSBO binding=2, terrain on UBO binding=2. - Assert.Equal(2u, ClipFrame.MeshClipSsboBinding); + // The mesh side's binding index moved off ClipFrame at Campaign V slice + // V11 — the RHI arm addresses it through GpuBindingModel.StorageClipRegions + // instead of a raw GL binding constant (see ClipFrame's BeginFrame doc + // comment); the terrain UBO binding is still genuinely shared, so it stays. + Assert.Equal(2u, GpuBindingModel.StorageClipRegions); Assert.Equal(2u, ClipFrame.TerrainClipUboBinding); } diff --git a/tests/AcDream.App.Tests/Rendering/ClipFrameUploadTests.cs b/tests/AcDream.App.Tests/Rendering/ClipFrameUploadTests.cs deleted file mode 100644 index 24704a28..00000000 --- a/tests/AcDream.App.Tests/Rendering/ClipFrameUploadTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using AcDream.App.Rendering; -using Xunit; - -namespace AcDream.App.Tests.Rendering; - -public sealed class ClipFrameUploadTests -{ - [Theory] - [InlineData(1, 144)] - [InlineData(16, 144)] - [InlineData(64, 192)] - [InlineData(256, 256)] - [InlineData(512, 512)] - public void TerrainArena_RecordStride_RespectsDriverAlignment( - int alignment, - int expectedStride) - { - Assert.Equal(expectedStride, ClipFrameArenaLayout.RecordStride(alignment)); - } - - [Fact] - public void TerrainArena_AssignsEverySliceAUniqueOrderedRange() - { - const int stride = 256; - Assert.Equal(0, ClipFrameArenaLayout.RecordOffset(0, stride)); - Assert.Equal(256, ClipFrameArenaLayout.RecordOffset(1, stride)); - Assert.Equal(512, ClipFrameArenaLayout.RecordOffset(2, stride)); - Assert.Equal(1280, ClipFrameArenaLayout.RequiredBytes(5, stride)); - } - - [Fact] - public void UploadState_RegionsOnce_AndTerrainRangesInSubmissionOrder() - { - var state = new ClipFrameUploadState(); - state.BeginFrame(); - state.ValidateRegionsNotUploaded(); - state.MarkRegionsUploaded(); - Assert.True(state.RegionsUploaded); - Assert.Throws(state.ValidateRegionsNotUploaded); - - state.ValidateTerrainReservation(4); - state.CommitTerrainReservation(4); - Assert.Equal(new[] { 0, 1, 2, 3 }, new[] - { - state.NextTerrainRecord(), - state.NextTerrainRecord(), - state.NextTerrainRecord(), - state.NextTerrainRecord(), - }); - Assert.Equal(4, state.TerrainUploaded); - Assert.Throws(() => state.NextTerrainRecord()); - - state.BeginFrame(); - Assert.False(state.RegionsUploaded); - Assert.Equal(0, state.TerrainReserved); - Assert.Equal(0, state.TerrainUploaded); - } - - [Fact] - public void ResourceRing_RetainsAtMostOneResourcePerFencedFrameSlot() - { - var ring = new ClipFrameResourceRing(ClipFrame.FrameSlotCount); - for (int slot = 0; slot < ClipFrame.FrameSlotCount; slot++) - ring.Set(slot, new object()); - - // Pathological slice counts reuse ranges in the frame slot's one arena; - // they cannot add another GL object to the ring. - for (int slice = 0; slice < 10_000; slice++) - Assert.True(ring.TryGet(slice % ClipFrame.FrameSlotCount, out _)); - - Assert.Equal(ClipFrame.FrameSlotCount, ring.Count); - Assert.Throws(() => ring.Set(0, new object())); - } - - [Fact] - public void CapacityPolicy_ShrinksOneOffPathologicalPeakAfterHysteresis() - { - var policy = new ClipBufferCapacityPolicy(); - int peak = policy.SelectCapacity(0, 1_000_000); - Assert.True(peak >= 1_000_000); - - int first = policy.SelectCapacity(peak, 4_096); - int second = policy.SelectCapacity(first, 4_096); - int third = policy.SelectCapacity(second, 4_096); - - Assert.Equal(peak, first); - Assert.Equal(peak, second); - Assert.Equal(4_096, third); - } - - [Fact] - public void CapacityPolicy_OrdinaryDemandJitterCancelsPendingShrink() - { - var policy = new ClipBufferCapacityPolicy(); - int capacity = policy.SelectCapacity(0, 65_536); - capacity = policy.SelectCapacity(capacity, 4_096); - capacity = policy.SelectCapacity(capacity, 20_000); // above 25% utilization - capacity = policy.SelectCapacity(capacity, 4_096); - capacity = policy.SelectCapacity(capacity, 4_096); - - Assert.Equal(65_536, capacity); - } - - [Fact] - public void CapacityTransaction_PublishesOnlySuccessfulResize() - { - int capacity = 4_096; - Assert.Throws(() => - ClipBufferCapacityTransaction.Resize( - ref capacity, - 8_192, - (_, _) => throw new InvalidOperationException("BufferData failed"))); - Assert.Equal(4_096, capacity); - - ClipBufferCapacityTransaction.Resize( - ref capacity, - 8_192, - (previous, next) => - { - Assert.Equal(4_096, previous); - Assert.Equal(8_192, next); - }); - Assert.Equal(8_192, capacity); - - // A later stage failure must not roll accounting back to the old store. - Action laterFailure = () => throw new InvalidOperationException("later bind failed"); - Assert.Throws(laterFailure); - Assert.Equal(8_192, capacity); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs index 174c3bb1..880d4596 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs @@ -138,7 +138,7 @@ public sealed class GameWindowRenderLeafCompositionTests "new ResourceShutdownStage(\"render frontends\"", "Hard(\"portal tunnel\"", "Hard(\"paperdoll viewport\"", - "new ResourceShutdownStage(\"OpenGL context\""); + "new ResourceShutdownStage(\"graphics API context\""); AssertAppearsInOrder( source, "new ResourceShutdownStage(\"frame borrowers\"", @@ -155,7 +155,7 @@ public sealed class GameWindowRenderLeafCompositionTests "new ResourceShutdownStage(\"render frontends\"", "new ResourceShutdownStage(\"input context\"", "platform.Input?.Dispose()", - "new ResourceShutdownStage(\"OpenGL context\""); + "new ResourceShutdownStage(\"graphics API context\""); } [Fact] diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs index f00dc6a4..9faeb3e5 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs @@ -18,7 +18,10 @@ public sealed class GameWindowSlice8BoundaryTests "RuntimeSettingsSnapshot startup = _runtimeSettings.Startup", "_displayFramePacing.InitializeStartup(startup.Display.VSync)", "VSync = startupPacing.UseVSync", - "Samples = startup.Quality.MsaaSamples", + // Campaign V slice V11: the raw-GL "Samples = ..." window option + // is gone — Vulkan takes MSAA as an RHI attachment property, not + // a window attribute. _startupQuality carries it forward instead. + "_startupQuality = startup.Quality;", "Window.Create(options)", "_displayFramePacing.BindSurface(", "_windowCallbacks = SilkWindowCallbackBinding.Create(", @@ -332,7 +335,13 @@ public sealed class GameWindowSlice8BoundaryTests run, "RuntimeSettingsSnapshot startup = _runtimeSettings.Startup", "_displayFramePacing.InitializeStartup(startup.Display.VSync)", - "Samples = startup.Quality.MsaaSamples", + // Campaign V slice V11: Vulkan needs a client-API-less window and + // takes neither MSAA nor the stencil bit count as a window + // attribute (both are RHI attachment properties instead), so the + // raw-GL "Samples = ..." window option this used to assert is + // gone. _startupQuality carries MsaaSamples forward instead, into + // CreateGraphics' VulkanGraphicsContext.Acquire call. + "_startupQuality = startup.Quality;", "Window.Create(options)"); AssertAppearsInOrder( load, @@ -372,9 +381,9 @@ public sealed class GameWindowSlice8BoundaryTests "WorldRenderComposition.cs")); AssertAppearsInOrder( worldPhase, - "TerrainAtlas.Build(gl, dats, bindless)", + "TerrainAtlas.BuildBackendNeutral(device, dats)", "settings.ResolvedQuality.AnisotropicLevel", - "_factory.CreateTerrain("); + "_factory.CreateBackendNeutralTerrain("); AssertAppearsInOrder( shutdown, "Soft(\"settings view model\", () => ingress.Settings.UnbindViewModel())", @@ -471,7 +480,7 @@ public sealed class GameWindowSlice8BoundaryTests "new ResourceShutdownStage(\"frame flight owner\"", "new ResourceShutdownStage(\"content mappings\"", "new ResourceShutdownStage(\"input context\"", - "new ResourceShutdownStage(\"OpenGL context\"", + "new ResourceShutdownStage(\"graphics API context\"", ]; AssertAppearsInOrder(manifest, stages); Assert.Equal(stages.Length, CountOccurrences(manifest, "new ResourceShutdownStage(")); @@ -563,7 +572,6 @@ public sealed class GameWindowSlice8BoundaryTests livePhase, "d.PortalTunnelFallback.AcquirePrepared(", "static tunnel => tunnel.PrepareResources());", - "d.RenderResourceLifetime.AcquireSkyShader(", "new SkyRenderer("); AssertAppearsInOrder( load, @@ -584,13 +592,14 @@ public sealed class GameWindowSlice8BoundaryTests AssertAppearsInOrder( worldPhase, "lifetime.AcquireTerrainAtlas(", - "TerrainAtlas.Build(gl, dats, bindless)", - "TerrainModernRenderer CreateTerrain(", + "TerrainAtlas.BuildBackendNeutral(device, dats)", + "TerrainModernRenderer CreateBackendNeutralTerrain(", // Campaign V slice V6j: terrain is composed on both arms, so its // acquisition is unconditional. The boundary this test pins — that // the atlas is acquired, then the factory names the renderer, then // the renderer is acquired AND published in one step — is unchanged. - "TerrainModernRenderer? terrain = AcquireAndPublish("); + // The raw-GL arm (CreateTerrain) was deleted at slice V11. + "TerrainModernRenderer terrain = AcquireAndPublish("); AssertAppearsInOrder( shutdown, "frame.FrameGraphPublication?.Dispose()", @@ -600,7 +609,6 @@ public sealed class GameWindowSlice8BoundaryTests "render.PortalTunnelFallback.ReleaseFallback();", "render.Sky?.Dispose()", "render.Terrain?.Dispose()", - "render.DedicatedResources.ReleaseSkyShader", "render.DedicatedResources.ReleaseTerrainAtlas", "render.ConstructionCleanup.Dispose", "platform.Graphics?.Dispose()"); @@ -620,11 +628,11 @@ public sealed class GameWindowSlice8BoundaryTests AssertAppearsInOrder( source, "_window.Run();", - "_glConstructionCleanup.RetainFrom(failure);", + "_constructionCleanup.RetainFrom(failure);", "private GameWindowShutdownRoots CaptureShutdownRoots()", - "_glConstructionCleanup)"); + "_constructionCleanup)"); Assert.Contains( - "Hard(\"GL construction ledger\", render.ConstructionCleanup.Dispose)", + "Hard(\"resource construction ledger\", render.ConstructionCleanup.Dispose)", shutdown, StringComparison.Ordinal); } diff --git a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs b/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs deleted file mode 100644 index 51f506af..00000000 --- a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs +++ /dev/null @@ -1,417 +0,0 @@ -using AcDream.App.Rendering; - -namespace AcDream.App.Tests.Rendering; - -public sealed class GlTextureOwnershipTests -{ - [Fact] - public void ConstructionRollbackDeletesAllNamesInReverseOrder() - { - var api = new FakeTextureNameApi(); - var transaction = new GlTextureConstructionTransaction(api); - - Assert.Equal(1u, transaction.Allocate()); - Assert.Equal(2u, transaction.Allocate()); - Assert.Equal(3u, transaction.Allocate()); - - transaction.Rollback(); - transaction.Rollback(); - - Assert.Equal([3u, 2u, 1u], api.DeleteAttempts); - } - - [Fact] - public void ConstructionRollbackAttemptsEveryNameAndReportsFailures() - { - var api = new FakeTextureNameApi { FailingDelete = 2 }; - var transaction = new GlTextureConstructionTransaction(api); - _ = transaction.Allocate(); - _ = transaction.Allocate(); - _ = transaction.Allocate(); - - AggregateException failure = Assert.Throws(transaction.Rollback); - - Assert.Single(failure.InnerExceptions); - Assert.Equal([3u, 2u, 1u], api.DeleteAttempts); - Assert.Equal([3u, 1u], api.Deleted); - - api.FailingDelete = null; - transaction.RetryCleanup(); - - Assert.True(transaction.IsCleanupComplete); - Assert.Equal([3u, 2u, 1u, 2u], api.DeleteAttempts); - Assert.Equal([3u, 1u, 2u], api.Deleted); - } - - [Fact] - public void CommittedNamesAreNeverDeletedByConstructionTransaction() - { - var api = new FakeTextureNameApi(); - var transaction = new GlTextureConstructionTransaction(api); - _ = transaction.Allocate(); - _ = transaction.Allocate(); - - transaction.Commit(); - transaction.Rollback(); - - Assert.Empty(api.DeleteAttempts); - Assert.Throws(() => transaction.Allocate()); - } - - [Fact] - public void TrackedTextureUploadFailureLeavesEveryActualBranchNameInTransaction() - { - var api = new FakeTextureNameApi(); - var transaction = new GlTextureConstructionTransaction(api); - uint first = TrackedTextureConstruction.Create(transaction, _ => { }); - - Assert.Throws(() => - TrackedTextureConstruction.Create( - transaction, - _ => throw new InvalidOperationException("upload failed"))); - transaction.Rollback(); - - Assert.Equal(1u, first); - Assert.Equal([2u, 1u], api.DeleteAttempts); - } - - [Fact] - public void TerrainAtlasRoutesEveryTerrainAlphaAndFallbackUploadThroughTracker() - { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "TerrainAtlas.cs")); - - Assert.Equal(5, CountOccurrences(source, "TrackedTextureConstruction.Create(")); - Assert.Equal( - 5, - System.Text.RegularExpressions.Regex.Matches( - source, - "TrackedTextureConstruction\\.Create\\(\\s*textures,\\s*gl,\\s*\\\"").Count); - Assert.DoesNotContain("textures.Allocate()", source, StringComparison.Ordinal); - } - - [Fact] - public void ProductionGlResourcePathsUseAlwaysOnCheckedCommitBoundaries() - { - string root = FindRepoRoot(); - string textureNames = File.ReadAllText(Path.Combine( - root, "src", "AcDream.App", "Rendering", "GlTextureConstructionTransaction.cs")); - string shaderPrograms = File.ReadAllText(Path.Combine( - root, "src", "AcDream.App", "Rendering", "ShaderProgramConstruction.cs")); - string terrain = File.ReadAllText(Path.Combine( - root, "src", "AcDream.App", "Rendering", "TerrainAtlas.cs")); - // Campaign V slice V4a: TextRenderer's shader/texture creation moved - // from raw GlResourceCommand calls to IGpuDevice.CreatePipeline/ - // CreateTexture, whose own checked-commit construction - // (GlResourceCommand.CreateName / ShaderProgramConstruction.Build, - // predating this slice) is what those now delegate to. Slice V6d then - // removed the white fill texture, so TextRenderer's constructor owns a - // single resource and holds no GL name of any kind — every checked - // commit boundary it depends on lives behind the RHI. - string text = File.ReadAllText(Path.Combine( - root, "src", "AcDream.App", "Rendering", "TextRenderer.cs")); - string bindless = File.ReadAllText(Path.Combine( - root, "src", "AcDream.App", "Rendering", "Wb", "BindlessSupport.cs")); - - Assert.Contains("GlResourceCommand.CreateTexture", textureNames, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.DeleteTexture", textureNames, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.CreateName", shaderPrograms, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.DeleteShader", shaderPrograms, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.DeleteProgram", shaderPrograms, StringComparison.Ordinal); - Assert.Contains("_anisotropyBindingMutation.Execute", terrain, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.DeleteTexture", terrain, StringComparison.Ordinal); - Assert.Contains("_pipeline = device.CreatePipeline(", text, StringComparison.Ordinal); - Assert.DoesNotContain("GlResourceCommand", text, StringComparison.Ordinal); - Assert.DoesNotContain("GlName", text, StringComparison.Ordinal); - Assert.Contains("make bindless handle", bindless, StringComparison.Ordinal); - Assert.Contains("GlResourceCommand.Execute", bindless, StringComparison.Ordinal); - } - - private static void AssertAppearsInOrder(string source, params string[] needles) - { - int cursor = -1; - foreach (string needle in needles) - { - int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal); - Assert.True(next >= 0, $"Missing expected source fragment: {needle}"); - Assert.True(next > cursor, $"Out-of-order source fragment: {needle}"); - cursor = next; - } - } - - [Fact] - public void SecondBindlessAcquireFailureRollsBackFirstHandle() - { - var residency = new FakeResidency { FailingAcquireTexture = 20 }; - var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release); - - Assert.Throws(() => pair.Acquire()); - - Assert.False(pair.IsFullyResident); - Assert.Equal([10u, 20u], residency.AcquireAttempts); - Assert.Equal([1010ul], residency.ReleaseAttempts); - } - - [Fact] - public void FailedPrefixRollbackRemainsOwnedAndRetryDoesNotReacquireIt() - { - var residency = new FakeResidency - { - FailingAcquireTexture = 20, - FailingReleaseHandle = 1010, - }; - var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release); - - Assert.Throws(() => pair.Acquire()); - residency.FailingAcquireTexture = null; - residency.FailingReleaseHandle = null; - - Assert.Equal((1010ul, 1020ul), pair.Acquire()); - - Assert.True(pair.IsFullyResident); - Assert.Equal([10u, 20u, 20u], residency.AcquireAttempts); - Assert.Equal([1010ul], residency.ReleaseAttempts); - } - - [Fact] - public void BindlessReleaseAttemptsBothAndRetriesOnlyPendingHandle() - { - var residency = new FakeResidency(); - var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release); - _ = pair.Acquire(); - residency.FailingReleaseHandle = 1010; - - Assert.Throws(pair.Release); - Assert.False(pair.IsFullyResident); - Assert.True(pair.HasAnyResident); - Assert.Equal([1010ul, 1020ul], residency.ReleaseAttempts); - - residency.FailingReleaseHandle = null; - pair.Release(); - pair.Release(); - - Assert.False(pair.HasAnyResident); - Assert.Equal([1010ul, 1020ul, 1010ul], residency.ReleaseAttempts); - } - - [Fact] - public void MutationFailureStillRestoresThePreviouslyResidentPair() - { - var residency = new FakeResidency(); - var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release); - var guard = new BindlessTextureMutationGuard(pair); - _ = pair.Acquire(); - - Assert.Throws(() => - guard.Execute(() => throw new InvalidOperationException("mutation failed"))); - - Assert.True(pair.IsFullyResident); - Assert.False(guard.RestoreRequired); - Assert.Equal([10u, 20u, 10u, 20u], residency.AcquireAttempts); - } - - [Fact] - public void FailedReacquireKeepsRestoreIntentUntilALaterMutationRetry() - { - var residency = new FakeResidency(); - var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release); - var guard = new BindlessTextureMutationGuard(pair); - _ = pair.Acquire(); - residency.FailingAcquireTexture = 20; - - Assert.Throws(() => guard.Execute(() => { })); - Assert.True(guard.RestoreRequired); - Assert.False(pair.HasAnyResident); - - residency.FailingAcquireTexture = null; - guard.Execute(() => { }); - - Assert.False(guard.RestoreRequired); - Assert.True(pair.IsFullyResident); - Assert.Equal([10u, 20u, 10u, 20u, 10u, 20u], residency.AcquireAttempts); - } - - [Fact] - public void PartialReleaseFailurePreventsMutationAndRestoresPairBeforeThrowing() - { - var residency = new FakeResidency(); - var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release); - var guard = new BindlessTextureMutationGuard(pair); - _ = pair.Acquire(); - residency.FailingReleaseHandle = 1010; - int mutations = 0; - - Assert.Throws(() => guard.Execute(() => mutations++)); - - Assert.Equal(0, mutations); - Assert.True(pair.IsFullyResident); - Assert.False(guard.RestoreRequired); - } - - [Fact] - public void FailedTextureMutationRestoresTheExactPriorBinding() - { - var bindings = new List(); - var owner = new RestoredTextureBindingMutation(); - - InvalidOperationException failure = Assert.Throws(() => - owner.Execute( - () => 77, - bindings.Add, - 42, - () => throw new InvalidOperationException("mutation failed"))); - - Assert.Equal("mutation failed", failure.Message); - Assert.Equal([42u, 77u], bindings); - } - - [Fact] - public void TextureMutationReportsBothMutationAndBindingRestoreFailure() - { - int bindCalls = 0; - var owner = new RestoredTextureBindingMutation(); - - AggregateException failure = Assert.Throws(() => - owner.Execute( - () => 77, - _ => - { - bindCalls++; - if (bindCalls == 2) - throw new InvalidOperationException("restore failed"); - }, - 42, - () => throw new InvalidOperationException("mutation failed"))); - - Assert.Equal(2, failure.InnerExceptions.Count); - Assert.Equal(2, bindCalls); - Assert.True(owner.HasPendingRestore); - } - - [Fact] - public void FailedBindingRestoreRetriesTheOriginalBindingBeforeAnotherMutation() - { - var owner = new RestoredTextureBindingMutation(); - var bindings = new List(); - int restoreFailures = 1; - int mutations = 0; - - Assert.Throws(() => - owner.Execute( - () => 77, - binding => - { - bindings.Add(binding); - if (binding == 77 && restoreFailures-- > 0) - throw new InvalidOperationException("restore failed"); - }, - 42, - () => mutations++)); - - Assert.True(owner.HasPendingRestore); - owner.Execute( - () => 77, - bindings.Add, - 42, - () => mutations++); - - Assert.False(owner.HasPendingRestore); - Assert.Equal([42u, 77u, 77u, 42u, 77u], bindings); - Assert.Equal(2, mutations); - } - - [Fact] - public void ConstructionCleanupLedgerRetainsNestedFailureUntilRetryCompletes() - { - var api = new FakeTextureNameApi { FailingDelete = 1 }; - var transaction = new GlTextureConstructionTransaction(api); - _ = transaction.Allocate(); - AggregateException cleanupFailure = Assert.Throws( - transaction.Rollback); - var constructionFailure = new GlResourceConstructionException( - "synthetic construction failure", - transaction, - [new InvalidOperationException("build failed"), cleanupFailure]); - var ledger = new GlConstructionCleanupLedger(); - - Assert.True(ledger.RetainFrom(constructionFailure)); - Assert.Throws(ledger.Dispose); - Assert.False(ledger.IsComplete); - - api.FailingDelete = null; - ledger.Dispose(); - - Assert.True(ledger.IsComplete); - Assert.True(constructionFailure.IsCleanupComplete); - } - - private sealed class FakeTextureNameApi : IGlTextureNameApi - { - private uint _nextName = 1; - - public uint? FailingDelete { get; set; } - public List DeleteAttempts { get; } = []; - public List Deleted { get; } = []; - - public uint GenTexture() => _nextName++; - - public void DeleteTexture(uint texture) - { - DeleteAttempts.Add(texture); - if (FailingDelete == texture) - throw new InvalidOperationException("delete failed"); - Deleted.Add(texture); - } - } - - private static int CountOccurrences(string source, string value) - { - int count = 0; - int cursor = 0; - while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0) - { - count++; - cursor += value.Length; - } - return count; - } - - private static string FindRepoRoot() - { - DirectoryInfo? directory = new(AppContext.BaseDirectory); - while (directory is not null) - { - if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) - return directory.FullName; - directory = directory.Parent; - } - throw new DirectoryNotFoundException("Could not find AcDream.slnx."); - } - - private sealed class FakeResidency - { - public uint? FailingAcquireTexture { get; set; } - public ulong? FailingReleaseHandle { get; set; } - public List AcquireAttempts { get; } = []; - public List ReleaseAttempts { get; } = []; - - public ulong Acquire(uint texture) - { - AcquireAttempts.Add(texture); - if (FailingAcquireTexture == texture) - throw new InvalidOperationException("acquire failed"); - return 1000ul + texture; - } - - public void Release(ulong handle) - { - ReleaseAttempts.Add(handle); - if (FailingReleaseHandle == handle) - throw new InvalidOperationException("release failed"); - } - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlDirtySlotRunsTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlDirtySlotRunsTests.cs deleted file mode 100644 index 1e67e2da..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlDirtySlotRunsTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -using AcDream.App.Rendering.Gpu.Gl; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -/// -/// The texture table's flush is mapped with GL_MAP_UNSYNCHRONIZED_BIT and -/// GL_MAP_INVALIDATE_RANGE_BIT, so a run this tracker reports must contain -/// nothing but slots that were actually written. A run that swallowed a clean -/// slot in between would let the driver discard a live bindless handle while a -/// submitted draw was reading it — invisible in any pixel gate, and exactly the -/// class of fault the ring rewrite exists to remove. These tests pin that. -/// -public sealed class GlDirtySlotRunsTests -{ - [Fact] - public void NothingMarkedYieldsNoRuns() - { - var runs = new GlDirtySlotRuns(16); - - Assert.False(runs.HasDirtySlots); - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void OneMarkedSlotIsOneRunOfOne() - { - var runs = new GlDirtySlotRuns(16); - runs.Mark(7); - - Assert.True(runs.HasDirtySlots); - Assert.True(runs.TryTakeNextRun(out uint first, out uint count)); - Assert.Equal(7u, first); - Assert.Equal(1u, count); - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void ConsecutiveSlotsMergeIntoOneRun() - { - var runs = new GlDirtySlotRuns(16); - runs.Mark(4); - runs.Mark(5); - runs.Mark(6); - - Assert.True(runs.TryTakeNextRun(out uint first, out uint count)); - Assert.Equal(4u, first); - Assert.Equal(3u, count); - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void AGapBetweenMarkedSlotsSplitsTheRuns() - { - var runs = new GlDirtySlotRuns(64); - runs.Mark(5); - runs.Mark(50); - - Assert.True(runs.TryTakeNextRun(out uint firstStart, out uint firstCount)); - Assert.Equal(5u, firstStart); - Assert.Equal(1u, firstCount); - - Assert.True(runs.TryTakeNextRun(out uint secondStart, out uint secondCount)); - Assert.Equal(50u, secondStart); - Assert.Equal(1u, secondCount); - - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void RunsComeBackInAscendingSlotOrderRegardlessOfMarkOrder() - { - var runs = new GlDirtySlotRuns(64); - runs.Mark(40); - runs.Mark(1); - runs.Mark(41); - runs.Mark(20); - runs.Mark(0); - - Assert.True(runs.TryTakeNextRun(out uint start, out uint count)); - Assert.Equal(0u, start); - Assert.Equal(2u, count); - - Assert.True(runs.TryTakeNextRun(out start, out count)); - Assert.Equal(20u, start); - Assert.Equal(1u, count); - - Assert.True(runs.TryTakeNextRun(out start, out count)); - Assert.Equal(40u, start); - Assert.Equal(2u, count); - - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void MarkingTheSameSlotTwiceStillYieldsOneRun() - { - var runs = new GlDirtySlotRuns(16); - runs.Mark(3); - runs.Mark(3); - - Assert.True(runs.TryTakeNextRun(out uint start, out uint count)); - Assert.Equal(3u, start); - Assert.Equal(1u, count); - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void DrainingLeavesTheTrackerCleanForTheNextFlush() - { - var runs = new GlDirtySlotRuns(16); - runs.Mark(2); - runs.Mark(9); - while (runs.TryTakeNextRun(out _, out _)) - { - } - - Assert.False(runs.HasDirtySlots); - - runs.Mark(11); - Assert.True(runs.TryTakeNextRun(out uint start, out uint count)); - Assert.Equal(11u, start); - Assert.Equal(1u, count); - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void SlotsMarkedAfterAPartialDrainAreStillReported() - { - var runs = new GlDirtySlotRuns(32); - runs.Mark(4); - runs.Mark(20); - - Assert.True(runs.TryTakeNextRun(out uint start, out uint count)); - Assert.Equal(4u, start); - Assert.Equal(1u, count); - - // A draw between two flushes can register another texture. - runs.Mark(21); - - Assert.True(runs.TryTakeNextRun(out start, out count)); - Assert.Equal(20u, start); - Assert.Equal(2u, count); - Assert.False(runs.TryTakeNextRun(out _, out _)); - } - - [Fact] - public void TheLastSlotInTheTableIsAddressable() - { - var runs = new GlDirtySlotRuns(8); - runs.Mark(7); - - Assert.True(runs.TryTakeNextRun(out uint start, out uint count)); - Assert.Equal(7u, start); - Assert.Equal(1u, count); - } - - [Fact] - public void MarkingBeyondCapacityThrows() - { - var runs = new GlDirtySlotRuns(8); - Assert.Throws(() => runs.Mark(8)); - } - - [Fact] - public void ZeroCapacityIsRejected() - { - Assert.Throws(() => new GlDirtySlotRuns(0)); - } - - /// - /// The whole point, stated as one property: every slot a run covers was - /// marked, and every marked slot is covered exactly once. - /// - [Fact] - public void EveryReportedSlotWasMarkedAndEveryMarkedSlotIsReportedOnce() - { - const int Capacity = 200; - var random = new Random(20260727); - var expected = new HashSet(); - var runs = new GlDirtySlotRuns(Capacity); - for (int i = 0; i < 60; i++) - { - uint slot = (uint)random.Next(Capacity); - expected.Add(slot); - runs.Mark(slot); - } - - var reported = new List(); - while (runs.TryTakeNextRun(out uint start, out uint count)) - { - for (uint slot = start; slot < start + count; slot++) - reported.Add(slot); - } - - Assert.Equal(reported.Count, reported.Distinct().Count()); - Assert.Equal(expected.OrderBy(slot => slot), reported); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlEnumMappingTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlEnumMappingTests.cs deleted file mode 100644 index 8cf17908..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlEnumMappingTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; -using Silk.NET.OpenGL; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -public sealed class GlEnumMappingTests -{ - [Fact] - public void VertexShapesMatchTheDeclaredComponentCounts() - { - Assert.Equal(new GlVertexAttributeShape(1, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float1)); - Assert.Equal(new GlVertexAttributeShape(2, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float2)); - Assert.Equal(new GlVertexAttributeShape(3, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float3)); - Assert.Equal(new GlVertexAttributeShape(4, VertexAttribPointerType.Float, false), GlEnumMapping.VertexShapeOf(GpuVertexFormat.Float4)); - Assert.Equal(new GlVertexAttributeShape(4, VertexAttribPointerType.UnsignedByte, true), GlEnumMapping.VertexShapeOf(GpuVertexFormat.UByte4Normalized)); - } - - [Fact] - public void IndexTypesMapToTheirGlSizesAndEnums() - { - Assert.Equal(DrawElementsType.UnsignedShort, GlEnumMapping.DrawElementsTypeOf(GpuIndexType.UInt16)); - Assert.Equal(2, GlEnumMapping.IndexSizeBytesOf(GpuIndexType.UInt16)); - Assert.Equal(DrawElementsType.UnsignedInt, GlEnumMapping.DrawElementsTypeOf(GpuIndexType.UInt32)); - Assert.Equal(4, GlEnumMapping.IndexSizeBytesOf(GpuIndexType.UInt32)); - } - - [Fact] - public void BlendFactorsMatchTheDocumentedFormulas() - { - (BlendingFactor source, BlendingFactor destination) straight = GlEnumMapping.BlendFactorsOf(GpuBlendMode.StraightAlpha); - Assert.Equal((BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha), straight); - - (BlendingFactor source, BlendingFactor destination) additive = GlEnumMapping.BlendFactorsOf(GpuBlendMode.Additive); - Assert.Equal((BlendingFactor.SrcAlpha, BlendingFactor.One), additive); - } - - [Fact] - public void BlendNoneHasNoGlFactors() - { - Assert.Throws(() => GlEnumMapping.BlendFactorsOf(GpuBlendMode.None)); - } - - [Fact] - public void CullNoneHasNoGlCullFaceMode() - { - Assert.Throws(() => GlEnumMapping.CullFaceModeOf(GpuCullMode.None)); - } - - [Fact] - public void DepthCompareOpsMapOneToOne() - { - Assert.Equal(DepthFunction.Never, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Never)); - Assert.Equal(DepthFunction.Less, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Less)); - Assert.Equal(DepthFunction.Lequal, GlEnumMapping.DepthFunctionOf(GpuCompareOp.LessOrEqual)); - Assert.Equal(DepthFunction.Equal, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Equal)); - Assert.Equal(DepthFunction.Greater, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Greater)); - Assert.Equal(DepthFunction.Gequal, GlEnumMapping.DepthFunctionOf(GpuCompareOp.GreaterOrEqual)); - Assert.Equal(DepthFunction.Always, GlEnumMapping.DepthFunctionOf(GpuCompareOp.Always)); - } - - [Fact] - public void FilterCombinationsProduceTheExpectedMinFilter() - { - Assert.Equal(TextureMinFilter.Nearest, GlEnumMapping.MinFilterOf(GpuFilter.Nearest, GpuMipFilter.None)); - Assert.Equal(TextureMinFilter.Linear, GlEnumMapping.MinFilterOf(GpuFilter.Linear, GpuMipFilter.None)); - Assert.Equal(TextureMinFilter.LinearMipmapLinear, GlEnumMapping.MinFilterOf(GpuFilter.Linear, GpuMipFilter.Linear)); - Assert.Equal(TextureMinFilter.NearestMipmapNearest, GlEnumMapping.MinFilterOf(GpuFilter.Nearest, GpuMipFilter.Nearest)); - } - - [Fact] - public void EveryNonNoneEnumValueResolvesForFrontFaceAndWrapMode() - { - Assert.Equal(FrontFaceDirection.Ccw, GlEnumMapping.FrontFaceDirectionOf(GpuFrontFace.CounterClockwise)); - Assert.Equal(FrontFaceDirection.CW, GlEnumMapping.FrontFaceDirectionOf(GpuFrontFace.Clockwise)); - Assert.Equal(TextureWrapMode.Repeat, GlEnumMapping.WrapModeOf(GpuAddressMode.Repeat)); - Assert.Equal(TextureWrapMode.ClampToEdge, GlEnumMapping.WrapModeOf(GpuAddressMode.ClampToEdge)); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTextureFormatMappingTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTextureFormatMappingTests.cs deleted file mode 100644 index c31791bb..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTextureFormatMappingTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; -using Silk.NET.OpenGL; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -public sealed class GlGpuTextureFormatMappingTests -{ - [Fact] - public void Rgba8AndItsRenderTargetVariantShareTheSameGlShape() - { - GlTextureFormatInfo plain = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.Rgba8Unorm); - GlTextureFormatInfo target = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.Rgba8UnormRenderTarget); - - Assert.Equal(plain, target); - Assert.Equal(SizedInternalFormat.Rgba8, plain.SizedInternalFormat); - Assert.Equal(PixelFormat.Rgba, plain.UploadPixelFormat); - Assert.Equal(PixelType.UnsignedByte, plain.UploadPixelType); - Assert.False(plain.IsCompressed); - Assert.Equal(4, plain.LayerByteCount(1, 1)); - Assert.Equal(4 * 16 * 16, plain.LayerByteCount(16, 16)); - } - - [Fact] - public void R8IsOneByteUncompressed() - { - GlTextureFormatInfo info = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.R8Unorm); - Assert.Equal(SizedInternalFormat.R8, info.SizedInternalFormat); - Assert.False(info.IsCompressed); - Assert.Equal(1, info.LayerByteCount(1, 1)); - Assert.Equal(64, info.LayerByteCount(8, 8)); - } - - [Fact] - public void Bc1IsCompressedFourByFourEightByteBlocks() => - AssertBcFormat(GpuTextureFormat.Bc1Unorm, expectedBlockBytes: 8); - - [Fact] - public void Bc2IsCompressedFourByFourSixteenByteBlocks() => - AssertBcFormat(GpuTextureFormat.Bc2Unorm, expectedBlockBytes: 16); - - [Fact] - public void Bc3IsCompressedFourByFourSixteenByteBlocks() => - AssertBcFormat(GpuTextureFormat.Bc3Unorm, expectedBlockBytes: 16); - - private static void AssertBcFormat(GpuTextureFormat format, int expectedBlockBytes) - { - GlTextureFormatInfo info = GlGpuTextureFormatMapping.Resolve(format); - Assert.True(info.IsCompressed); - Assert.Equal(4, info.BlockDimension); - Assert.Equal(expectedBlockBytes, info.BlockOrTexelBytes); - - // A single 4x4 block for a 4x4 texture. - Assert.Equal(expectedBlockBytes, info.LayerByteCount(4, 4)); - // Partial blocks round up: a 5x5 texture needs a 2x2 block grid. - Assert.Equal(expectedBlockBytes * 4, info.LayerByteCount(5, 5)); - } - - [Fact] - public void Depth24Stencil8IsAttachmentShapedNotCompressed() - { - GlTextureFormatInfo info = GlGpuTextureFormatMapping.Resolve(GpuTextureFormat.Depth24Stencil8); - Assert.Equal(SizedInternalFormat.Depth24Stencil8, info.SizedInternalFormat); - Assert.Equal(PixelFormat.DepthStencil, info.UploadPixelFormat); - Assert.Equal(PixelType.UnsignedInt248, info.UploadPixelType); - Assert.False(info.IsCompressed); - } - - [Fact] - public void EveryGpuTextureFormatValueResolvesWithoutThrowing() - { - foreach (GpuTextureFormat format in Enum.GetValues()) - GlGpuTextureFormatMapping.Resolve(format); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTimerPoolTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTimerPoolTests.cs deleted file mode 100644 index 4e2435b1..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlGpuTimerPoolTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -using AcDream.App.Rendering.Gpu.Gl; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -public sealed class GlGpuTimerPoolTests -{ - [Fact] - public void UnsupportedPoolReturnsNoOpScopesAndNeverResolves() - { - var pool = new GlGpuTimerPool(new FakeTimerQueryApi(), isSupported: false); - - using (pool.BeginScope("world")) - { - } - - Assert.False(pool.TryResolve("world", out _)); - } - - [Fact] - public void ScopeNamesGetIndependentDoubleBufferedQueries() - { - var api = new FakeTimerQueryApi(); - var pool = new GlGpuTimerPool(api, isSupported: true); - - using (pool.BeginScope("world")) - { - } - using (pool.BeginScope("ui")) - { - } - - Assert.Equal(4, api.CreatedQueryCount); - } - - [Fact] - public void NestingAScopeBeforeDisposingThePreviousOneThrows() - { - var pool = new GlGpuTimerPool(new FakeTimerQueryApi(), isSupported: true); - pool.BeginScope("world"); - - Assert.Throws(() => pool.BeginScope("ui")); - } - - [Fact] - public void AResolvedResultIsPromotedTheNextTimeTheSameScopeBegins() - { - var api = new FakeTimerQueryApi(); - var pool = new GlGpuTimerPool(api, isSupported: true); - - using (pool.BeginScope("world")) - { - } - // No result available yet — the first slot hasn't been revisited. - Assert.False(pool.TryResolve("world", out _)); - - api.MakeNextResultReady(milliseconds: 1.5); - using (pool.BeginScope("world")) - { - } - - Assert.True(pool.TryResolve("world", out double milliseconds)); - Assert.Equal(1.5, milliseconds); - } - - [Fact] - public void DisposeQueriesDeletesEveryCreatedQuery() - { - var api = new FakeTimerQueryApi(); - var pool = new GlGpuTimerPool(api, isSupported: true); - using (pool.BeginScope("world")) - { - } - - pool.DisposeQueries(); - - Assert.Equal(2, api.DeletedQueryCount); - } - - private sealed class FakeTimerQueryApi : IGlTimerQueryApi - { - private uint _nextQuery = 1; - private bool _nextResultReady; - private double _nextResultMilliseconds; - - public int CreatedQueryCount { get; private set; } - public int DeletedQueryCount { get; private set; } - - public uint CreateQuery() - { - CreatedQueryCount++; - return _nextQuery++; - } - - public void DeleteQuery(uint query) => DeletedQueryCount++; - - public void Begin(uint query) - { - } - - public void End() - { - } - - public void MakeNextResultReady(double milliseconds) - { - _nextResultReady = true; - _nextResultMilliseconds = milliseconds; - } - - public bool TryGetResult(uint query, out double milliseconds) - { - if (_nextResultReady) - { - milliseconds = _nextResultMilliseconds; - _nextResultReady = false; - return true; - } - - milliseconds = 0; - return false; - } - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlPushConstantUniformNamesTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlPushConstantUniformNamesTests.cs deleted file mode 100644 index 21d50c60..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlPushConstantUniformNamesTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -using AcDream.App.Rendering.Gpu.Gl; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -public sealed class GlPushConstantUniformNamesTests -{ - [Fact] - public void EveryDocumentedFieldMapsToItsDocumentedUniformName() - { - Assert.Equal("uViewProjection", GlPushConstantUniformNames.ByFieldName["ViewProjection"]); - Assert.Equal("uDrawIDOffset", GlPushConstantUniformNames.ByFieldName["DrawIdOffset"]); - Assert.Equal("uLightingMode", GlPushConstantUniformNames.ByFieldName["LightingMode"]); - Assert.Equal("uRenderPass", GlPushConstantUniformNames.ByFieldName["RenderPass"]); - Assert.Equal("uLightDebug", GlPushConstantUniformNames.ByFieldName["LightDebug"]); - Assert.Equal("uTextureIndexA", GlPushConstantUniformNames.ByFieldName["TextureIndexA"]); - Assert.Equal("uTextureIndexB", GlPushConstantUniformNames.ByFieldName["TextureIndexB"]); - Assert.Equal("uParamA", GlPushConstantUniformNames.ByFieldName["ParamA"]); - Assert.Equal("uParamB", GlPushConstantUniformNames.ByFieldName["ParamB"]); - } - - [Fact] - public void TableCoversEveryFieldOnTheSharedStructWithNoStaleEntries() - { - // This is the drift guard: if a later slice adds/removes/renames a - // GpuPushConstants field without updating the mapping table, this - // test fails instead of the GL backend silently skipping a uniform. - GlPushConstantUniformNames.AssertMapsEveryField(); - } - - [Fact] - public void TableHasExactlyNineEntries() - { - Assert.Equal(9, GlPushConstantUniformNames.ByFieldName.Count); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs deleted file mode 100644 index c6085a0e..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRenderStateCacheTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -using AcDream.App.Rendering.Gpu; -using AcDream.App.Rendering.Gpu.Gl; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -public sealed class GlRenderStateCacheTests -{ - private static GlRenderStateSnapshot Default(uint program = 1) => new( - program, - GpuBlendMode.None, - DepthTest: true, - DepthWrite: true, - GpuCompareOp.LessOrEqual, - GpuCullMode.Back, - GpuFrontFace.CounterClockwise, - AlphaToCoverage: false, - ColorWrite: true, - // Slice V6l: the stencil dimension. Off is what every pipeline in the - // tree but the portal depth mask asks for. - StencilTest: false, - GpuStencilState.Default); - - [Fact] - public void FirstApplyReportsEveryDimensionChanged() - { - var cache = new GlRenderStateCache(); - GlRenderStateChanges changes = cache.Apply(Default()); - Assert.Equal(GlRenderStateChanges.All, changes); - Assert.True(changes.AnyChange); - } - - [Fact] - public void ReapplyingTheIdenticalSnapshotReportsNoChanges() - { - var cache = new GlRenderStateCache(); - GlRenderStateSnapshot snapshot = Default(); - cache.Apply(snapshot); - - GlRenderStateChanges changes = cache.Apply(snapshot); - - Assert.False(changes.AnyChange); - } - - [Fact] - public void ChangingOnlyCullModeReportsOnlyThatDimension() - { - var cache = new GlRenderStateCache(); - cache.Apply(Default()); - - GlRenderStateChanges changes = cache.Apply(Default() with { Cull = GpuCullMode.None }); - - Assert.True(changes.Cull); - Assert.True(changes.AnyChange); - Assert.False(changes.Program); - Assert.False(changes.Blend); - Assert.False(changes.DepthTest); - Assert.False(changes.DepthWrite); - Assert.False(changes.DepthCompare); - Assert.False(changes.FrontFace); - Assert.False(changes.AlphaToCoverage); - Assert.False(changes.ColorWrite); - Assert.False(changes.StencilTest); - Assert.False(changes.Stencil); - } - - [Fact] - public void ChangingOnlyTheStencilValuesReportsOnlyThatDimension() - { - // Slice V6l. #117's portal punch changes compare, ops, reference and - // masks between its mark pass and its punch pass while the test stays - // enabled, so the two stencil dimensions have to move independently. - var cache = new GlRenderStateCache(); - cache.Apply(Default() with { StencilTest = true }); - - GlRenderStateChanges changes = cache.Apply(Default() with - { - StencilTest = true, - Stencil = GpuStencilState.Default with - { - Compare = GpuCompareOp.Equal, - Pass = GpuStencilOp.Zero, - Reference = 1, - }, - }); - - Assert.True(changes.Stencil); - Assert.False(changes.StencilTest); - Assert.False(changes.DepthTest); - Assert.False(changes.ColorWrite); - } - - [Fact] - public void ChangingTheProgramReportsProgramChangedEvenWhenEveryOtherFieldMatches() - { - var cache = new GlRenderStateCache(); - cache.Apply(Default(program: 1)); - - GlRenderStateChanges changes = cache.Apply(Default(program: 2)); - - Assert.True(changes.Program); - Assert.False(changes.Blend); - } - - [Fact] - public void ResetForcesTheNextApplyToReportEveryDimensionAgain() - { - var cache = new GlRenderStateCache(); - GlRenderStateSnapshot snapshot = Default(); - cache.Apply(snapshot); - - cache.Reset(); - GlRenderStateChanges changes = cache.Apply(snapshot); - - Assert.Equal(GlRenderStateChanges.All, changes); - } - - [Fact] - public void MultipleDimensionsChangingAreAllReported() - { - var cache = new GlRenderStateCache(); - cache.Apply(Default()); - - GlRenderStateChanges changes = cache.Apply(Default() with - { - Blend = GpuBlendMode.StraightAlpha, - DepthWrite = false, - FrontFace = GpuFrontFace.Clockwise, - }); - - Assert.True(changes.Blend); - Assert.True(changes.DepthWrite); - Assert.True(changes.FrontFace); - Assert.False(changes.Cull); - Assert.False(changes.DepthTest); - Assert.False(changes.DepthCompare); - Assert.False(changes.AlphaToCoverage); - Assert.False(changes.ColorWrite); - Assert.False(changes.Program); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRingBufferStateTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRingBufferStateTests.cs deleted file mode 100644 index 4bf01f18..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlRingBufferStateTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using AcDream.App.Rendering.Gpu.Gl; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -public sealed class GlRingBufferStateTests -{ - [Fact] - public void FirstAllocationStartsAtZero() - { - var state = new GlRingBufferState(1024); - uint offset = state.Allocate(64, alignmentBytes: 16); - Assert.Equal(0u, offset); - Assert.Equal(64u, state.AllocatedBytes); - } - - [Fact] - public void SubsequentAllocationsAlignUpToTheRequestedBoundary() - { - var state = new GlRingBufferState(1024); - state.Allocate(12, alignmentBytes: 4); - uint second = state.Allocate(64, alignmentBytes: 256); - Assert.Equal(0u, second % 256); - Assert.True(second >= 12); - } - - [Fact] - public void AllocationsMergeIntoOneDirtyRange() - { - var state = new GlRingBufferState(1024); - state.Allocate(16, alignmentBytes: 4); - state.Allocate(32, alignmentBytes: 4); - - (int start, int length) = state.TakeDirtyRange(); - Assert.Equal(0, start); - Assert.Equal(48, length); - } - - [Fact] - public void TakingTheDirtyRangeClearsItUntilTheNextWrite() - { - var state = new GlRingBufferState(1024); - state.Allocate(16, alignmentBytes: 4); - state.TakeDirtyRange(); - - (int start, int length) = state.TakeDirtyRange(); - Assert.Equal(0, start); - Assert.Equal(0, length); - Assert.False(state.HasDirtyBytes); - } - - [Fact] - public void WritesAfterAFlushExtendANewDirtyRangeWithoutRewindingTheCursor() - { - var state = new GlRingBufferState(1024); - state.Allocate(16, alignmentBytes: 4); - state.TakeDirtyRange(); - - uint secondOffset = state.Allocate(8, alignmentBytes: 4); - Assert.Equal(16u, secondOffset); - - (int start, int length) = state.TakeDirtyRange(); - Assert.Equal(16, start); - Assert.Equal(8, length); - } - - /// - /// The precondition for issuing the ring's writes with - /// GL_MAP_UNSYNCHRONIZED_BIT: within one frame, no flush may cover a byte an - /// earlier flush already handed to the GPU. This walks a frame's worth of - /// mixed-alignment allocations and asserts the ranges are disjoint and - /// ascending. - /// - [Fact] - public void SuccessiveDirtyRangesWithinAFrameNeverOverlap() - { - var state = new GlRingBufferState(64 * 1024); - int previousEnd = 0; - - foreach (int size in new[] { 100, 4, 1024, 36, 7, 512, 3 }) - { - state.Allocate(size, alignmentBytes: 256); - state.Allocate(size, alignmentBytes: 4); - - (int start, int length) = state.TakeDirtyRange(); - Assert.True( - start >= previousEnd, - $"flush [{start}, {start + length}) reaches below the already-flushed {previousEnd} bytes"); - previousEnd = start + length; - Assert.Equal(previousEnd, state.FlushedEndBytes); - } - } - - [Fact] - public void AWriteBelowTheFlushedHighWaterMarkIsRefused() - { - var state = new GlRingBufferState(1024); - state.Allocate(128, alignmentBytes: 4); - state.TakeDirtyRange(); - - Assert.Equal(128, state.FlushedEndBytes); - InvalidOperationException error = Assert.Throws( - () => state.MarkDirty(64, 96)); - Assert.Contains("GL_MAP_UNSYNCHRONIZED_BIT", error.Message, StringComparison.Ordinal); - } - - [Fact] - public void FlushedHighWaterMarkOnlyAdvancesWhenSomethingWasFlushed() - { - var state = new GlRingBufferState(1024); - state.Allocate(48, alignmentBytes: 4); - state.TakeDirtyRange(); - Assert.Equal(48, state.FlushedEndBytes); - - // A draw with nothing newly written must not move the mark, or the next - // allocation's guard would compare against a number no flush produced. - state.TakeDirtyRange(); - Assert.Equal(48, state.FlushedEndBytes); - } - - [Fact] - public void ResetClearsTheFlushedHighWaterMarkSoTheSlotIsWritableAgain() - { - var state = new GlRingBufferState(1024); - state.Allocate(256, alignmentBytes: 4); - state.TakeDirtyRange(); - Assert.Equal(256, state.FlushedEndBytes); - - // BeginFrame has waited on this slot's fence by the time Reset runs, so - // the whole slot is writable from zero again. - state.Reset(); - - Assert.Equal(0, state.FlushedEndBytes); - Assert.Equal(0u, state.Allocate(64, alignmentBytes: 4)); - } - - [Fact] - public void ResetRewindsTheCursorAndClearsDirtyState() - { - var state = new GlRingBufferState(1024); - state.Allocate(64, alignmentBytes: 4); - - state.Reset(); - - Assert.Equal(0u, state.AllocatedBytes); - Assert.False(state.HasDirtyBytes); - Assert.Equal(0u, state.Allocate(4, alignmentBytes: 4)); - } - - [Fact] - public void OverCapacityRequestThrowsRatherThanTruncating() - { - var state = new GlRingBufferState(64); - Assert.Throws(() => state.Allocate(128, alignmentBytes: 4)); - } - - [Fact] - public void AlignmentPushingPastCapacityAlsoThrows() - { - var state = new GlRingBufferState(64); - state.Allocate(60, alignmentBytes: 4); - Assert.Throws(() => state.Allocate(8, alignmentBytes: 4)); - } - - [Theory] - [InlineData(0u, 256u, 0u)] - [InlineData(1u, 256u, 256u)] - [InlineData(255u, 256u, 256u)] - [InlineData(256u, 256u, 256u)] - [InlineData(10u, 1u, 10u)] - public void AlignUpMatchesStandardAlignmentArithmetic(uint value, uint alignment, uint expected) - { - Assert.Equal(expected, GlRingBufferState.AlignUp(value, alignment)); - } - - [Fact] - public void ZeroByteAllocationDoesNotMarkAnythingDirty() - { - var state = new GlRingBufferState(1024); - state.Allocate(0, alignmentBytes: 4); - Assert.False(state.HasDirtyBytes); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlTextureSlotAllocatorTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlTextureSlotAllocatorTests.cs deleted file mode 100644 index e5973468..00000000 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Gl/GlTextureSlotAllocatorTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using AcDream.App.Rendering.Gpu.Gl; - -namespace AcDream.App.Tests.Rendering.Gpu.Gl; - -public sealed class GlTextureSlotAllocatorTests -{ - [Fact] - public void AllocationsBumpSequentiallyFromZero() - { - var allocator = new GlTextureSlotAllocator(capacity: 4); - Assert.Equal(0u, allocator.Allocate()); - Assert.Equal(1u, allocator.Allocate()); - Assert.Equal(2u, allocator.Allocate()); - Assert.Equal(3, allocator.LiveCount); - } - - [Fact] - public void ExhaustingCapacityThrows() - { - var allocator = new GlTextureSlotAllocator(capacity: 2); - allocator.Allocate(); - allocator.Allocate(); - Assert.Throws(() => allocator.Allocate()); - } - - [Fact] - public void ReleasedSlotsAreReusedBeforeBumpingFurther() - { - var allocator = new GlTextureSlotAllocator(capacity: 4); - uint first = allocator.Allocate(); - allocator.Allocate(); - - allocator.Release(first); - Assert.Equal(1, allocator.LiveCount); - - uint reused = allocator.Allocate(); - Assert.Equal(first, reused); - Assert.Equal(2, allocator.LiveCount); - } - - [Fact] - public void ReleasingAnUnallocatedSlotThrows() - { - var allocator = new GlTextureSlotAllocator(capacity: 4); - allocator.Allocate(); - Assert.Throws(() => allocator.Release(3)); - } - - [Fact] - public void FreeingThenExhaustingTheBumpRangeStillThrowsPastCapacity() - { - var allocator = new GlTextureSlotAllocator(capacity: 2); - uint first = allocator.Allocate(); - allocator.Allocate(); - allocator.Release(first); - allocator.Allocate(); - - Assert.Throws(() => allocator.Allocate()); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/GpuResourceRetirementTransactionTests.cs b/tests/AcDream.App.Tests/Rendering/GpuResourceRetirementTransactionTests.cs index 9e1243a8..7a50bacf 100644 --- a/tests/AcDream.App.Tests/Rendering/GpuResourceRetirementTransactionTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GpuResourceRetirementTransactionTests.cs @@ -2,7 +2,6 @@ using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.App.Tests.Rendering.Gpu; -using Silk.NET.OpenGL; namespace AcDream.App.Tests.Rendering; @@ -192,40 +191,6 @@ public sealed class GpuResourceRetirementTransactionTests Assert.Equal(2, queue.Actions.Count); } - [Fact] - public void GlQueue_PersistentNextPassRetryDoesNotStarveOrdinaryWork() - { - var device = new QueueOnlyGraphicsDevice(); - int retryCalls = 0; - int ordinaryCalls = 0; - Action? retry = null; - retry = _ => - { - retryCalls++; - device.QueueGLActionForNextPass(retry!); - }; - device.QueueGLActionForNextPass(retry); - device.QueueGLAction(_ => ordinaryCalls++); - - device.ProcessGLQueue(); - - Assert.Equal(1, retryCalls); - Assert.Equal(1, ordinaryCalls); - Assert.True(device.HasPendingGLWork); - } - - [Fact] - public void GlQueue_ReportsPendingWorkUntilBothGenerationsDrain() - { - var device = new QueueOnlyGraphicsDevice(); - device.QueueGLAction(_ => { }); - - Assert.True(device.HasPendingGLWork); - device.ProcessGLQueue(); - - Assert.False(device.HasPendingGLWork); - } - /// /// Campaign V slice V4b: the mesh arena's staged store is an /// rather than a raw GL name, so the abort ticket @@ -309,46 +274,6 @@ public sealed class GpuResourceRetirementTransactionTests Assert.Equal(1, accountingCalls); } - [Fact] - public void GlobalMeshVaoAccounting_CreateAndRetryableDeleteBalanceExactlyOnce() - { - int baseline = GpuMemoryTracker.VaoCount; - bool allocationOutstanding = true; - GlobalMeshVaoAccounting.TrackAllocation(); - try - { - Assert.Equal(baseline + 1, GpuMemoryTracker.VaoCount); - var release = new RetryableGpuResourceRelease( - () => { }, - () => - { - GlobalMeshVaoAccounting.TrackDeallocation(); - allocationOutstanding = false; - }); - - release.Run(); - release.Run(); - - Assert.Equal(baseline, GpuMemoryTracker.VaoCount); - } - finally - { - if (allocationOutstanding) - GlobalMeshVaoAccounting.TrackDeallocation(); - } - } - - [Fact] - public void GlobalMeshVaoAccounting_InitializationRollbackReturnsToBaseline() - { - int baseline = GpuMemoryTracker.VaoCount; - GlobalMeshVaoAccounting.TrackAllocation(); - - GlobalMeshVaoAccounting.TrackDeallocation(); - - Assert.Equal(baseline, GpuMemoryTracker.VaoCount); - } - private sealed class FailBeforeAcceptQueue : IGpuResourceRetirementQueue { private bool _fail = true; @@ -378,11 +303,4 @@ public sealed class GpuResourceRetirementTransactionTests } } - private sealed class QueueOnlyGraphicsDevice : OpenGLGraphicsDevice - { - public QueueOnlyGraphicsDevice() - : base() - { - } - } } diff --git a/tests/AcDream.App.Tests/Rendering/PortalDepthShaderParityTests.cs b/tests/AcDream.App.Tests/Rendering/PortalDepthShaderParityTests.cs deleted file mode 100644 index 53a5b942..00000000 --- a/tests/AcDream.App.Tests/Rendering/PortalDepthShaderParityTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using AcDream.App.Rendering; -using Xunit; - -namespace AcDream.App.Tests.Rendering; - -/// -/// Campaign V slice V6l: the portal depth mask is the one renderer whose two arms -/// do NOT share a shader source, and this is the tripwire that stops them -/// drifting. -/// -/// Every other RHI arm in the campaign compiles the same GLSL file the GL -/// arm does. This one cannot: its clip planes would have to travel in the -/// TerrainClip uniform block at binding 2, and on GL that binding is held -/// globally by ClipFrame for terrain — a portal draw that rebound it would -/// leave every later terrain draw in the frame reading the wrong region. So the -/// GL arm keeps its inline program, the RHI arm compiles -/// Rendering/Shaders/portal_depth.vert, and the numbers that decide where -/// depth lands are asserted to appear in both. -/// -/// Deleted with the GL arm at V11, when the inline string goes. -/// -public class PortalDepthShaderParityTests -{ - private static string PortalDepthVertexSource() => - File.ReadAllText(Path.Combine( - AppContext.BaseDirectory, - "Rendering", - "Shaders", - "portal_depth.vert")); - - [Fact] - public void BothArmsPunchWithRetailsFarZConstant() - { - // 0.99999988 is retail's own value at the tail of - // D3DPolyRender::DrawPortalPolyInternal (0x0059bc90). It decides where - // the punched depth lands, so the two arms agreeing on it is the whole - // point of this file. - const string FarZ = "0.99999988"; - Assert.Contains(FarZ, PortalDepthMaskRenderer.VertSrc, StringComparison.Ordinal); - Assert.Contains(FarZ, PortalDepthVertexSource(), StringComparison.Ordinal); - } - - [Fact] - public void BothArmsApplyTheSameEyeCappedMarkBias() - { - // #129's capped bias: min(bias, cap / max(w*w, 1e-6)), then z -= bias*w. - // Spelled with different identifier names on the two arms — the RHI arm - // reads the shared push block's uParamA/uParamB — so the assertion is on - // the SHAPE that decides the result rather than on the text. - string rhi = PortalDepthVertexSource(); - Assert.Contains("max(clipPos.w * clipPos.w, 1e-6)", PortalDepthMaskRenderer.VertSrc, StringComparison.Ordinal); - Assert.Contains("max(clipPos.w * clipPos.w, 1e-6)", rhi, StringComparison.Ordinal); - Assert.Contains("clipPos.z -= biasNdc * clipPos.w", PortalDepthMaskRenderer.VertSrc, StringComparison.Ordinal); - Assert.Contains("clipPos.z -= biasNdc * clipPos.w", rhi, StringComparison.Ordinal); - } - - [Fact] - public void BothArmsClipAgainstEightHalfPlanes() - { - // The plane budget is ClipFrame.MaxPlanes and GL's guaranteed - // GL_MAX_CLIP_DISTANCES; a shader that looped to a different number - // would clip a differently-shaped region than the one the CPU published. - Assert.Equal(8, ClipFrame.MaxPlanes); - Assert.Contains("for (int i = 0; i < 8; i++)", PortalDepthMaskRenderer.VertSrc, StringComparison.Ordinal); - Assert.Contains("for (int i = 0; i < 8; i++)", PortalDepthVertexSource(), StringComparison.Ordinal); - } - - [Fact] - public void TheRhiArmReadsTheClipPlanesFromTheSharedTerrainClipBlock() - { - string rhi = PortalDepthVertexSource(); - Assert.Contains("uniform TerrainClip", rhi, StringComparison.Ordinal); - Assert.Contains("binding = 2", rhi, StringComparison.Ordinal); - // And the block is the one ClipFrame already packs for terrain and sky: - // an int count padded to 16 bytes, then eight vec4 planes. - Assert.Equal(144, ClipFrame.TerrainUboBytes); - Assert.Equal(2u, ClipFrame.TerrainClipUboBinding); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/PortalTunnelAssetTests.cs b/tests/AcDream.App.Tests/Rendering/PortalTunnelAssetTests.cs index 0f638770..1a825b82 100644 --- a/tests/AcDream.App.Tests/Rendering/PortalTunnelAssetTests.cs +++ b/tests/AcDream.App.Tests/Rendering/PortalTunnelAssetTests.cs @@ -13,17 +13,6 @@ namespace AcDream.App.Tests.Rendering; public sealed class PortalTunnelAssetTests { - [Fact] - public void PortalViewport_PreservesColorWhileClearingDepthLikeRetail() - { - Assert.Equal( - Silk.NET.OpenGL.ClearBufferMask.DepthBufferBit, - PortalTunnelPresentation.RetailViewportClearMask); - Assert.False( - (PortalTunnelPresentation.RetailViewportClearMask - & Silk.NET.OpenGL.ClearBufferMask.ColorBufferBit) != 0); - } - /// /// Campaign V slice V6m. The RHI arm cannot inherit a bound framebuffer, so /// its pass re-establishes the colour the frame already cleared. That is @@ -40,19 +29,18 @@ public sealed class PortalTunnelAssetTests } /// - /// Campaign V slice V6m. A backend with no GL context draws portal space + /// Campaign V slice V6m (the raw-GL arm deleted at V11): portal space draws /// through a pass of its own, so it must be given somewhere to open one. /// Composition failing loudly here beats a null dereference at the first /// portal transit, which is minutes into a connected run. /// [Fact] - public void PortalSpace_BackendWithoutGlRequiresAPassScopeAndFrameSource() + public void PortalSpace_RequiresAPassScope() { var error = Assert.Throws(() => PortalTunnelPresentation.CreateRequired( - gl: null, - scope: null, - frames: null, + scope: null!, + frames: null!, dats: null!, animationLoader: null!, hookSink: null!, @@ -61,7 +49,6 @@ public sealed class PortalTunnelAssetTests meshAdapter: null!)); Assert.Equal("scope", error.ParamName); - Assert.Contains("world pass scope", error.Message); } [Fact] diff --git a/tests/AcDream.App.Tests/Rendering/RenderFrameGlStateControllerTests.cs b/tests/AcDream.App.Tests/Rendering/RenderFrameGlStateControllerTests.cs deleted file mode 100644 index 1f31ef3f..00000000 --- a/tests/AcDream.App.Tests/Rendering/RenderFrameGlStateControllerTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using AcDream.App.Rendering; -using Silk.NET.OpenGL; - -namespace AcDream.App.Tests.Rendering; - -public sealed class RenderFrameGlStateControllerTests -{ - [Fact] - public void RestoreFrameDefaults_ReestablishesTheCompleteFrameGlobalContract() - { - var api = new RecordingApi(); - var state = new RenderFrameGlStateController(api); - - state.RestoreFrameDefaults(); - - Assert.Equal( - [ - "cap:ScissorTest:False", - "cap:StencilTest:False", - "cap:Blend:False", - "cap:SampleAlphaToMaskSgis:False", - "cap:ClipDistance0:False", - "cap:ClipDistance1:False", - "cap:ClipDistance2:False", - "cap:ClipDistance3:False", - "cap:ClipDistance4:False", - "cap:ClipDistance5:False", - "cap:ClipDistance6:False", - "cap:ClipDistance7:False", - "color-mask:True:True:True:True", - "stencil-mask:255", - "depth-mask:True", - "depth-func:Less", - "cap:DepthTest:True", - "cap:CullFace:False", - "cull:Back", - "front:CW", - "program:0", - "vao:0", - "buffer:ArrayBuffer:0", - ], - api.Calls); - } - - [Fact] - public void PortalDepthMask_SuccessExitRestoresTheSameCullOffConvention() - { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "PortalDepthMaskRenderer.cs")); - int restore = source.IndexOf( - "// ---- restore the frame-global convention ----", - StringComparison.Ordinal); - - Assert.True(restore >= 0, "Missing portal-depth state restore block."); - string restoreBlock = source[restore..]; - Assert.Contains("_gl.Disable(EnableCap.CullFace);", restoreBlock); - Assert.DoesNotContain("_gl.Enable(EnableCap.CullFace);", restoreBlock); - } - - private static string FindRepoRoot() - { - DirectoryInfo? directory = new(AppContext.BaseDirectory); - while (directory is not null) - { - if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) - return directory.FullName; - directory = directory.Parent; - } - - throw new DirectoryNotFoundException("Could not find AcDream.slnx."); - } - - private sealed class RecordingApi : IRenderFrameGlStateApi - { - public List Calls { get; } = []; - - public void SetCapability(EnableCap capability, bool enabled) => - Calls.Add($"cap:{capability}:{enabled}"); - - public void ColorMask(bool red, bool green, bool blue, bool alpha) => - Calls.Add($"color-mask:{red}:{green}:{blue}:{alpha}"); - - public void StencilMask(uint mask) => Calls.Add($"stencil-mask:{mask}"); - - public void DepthMask(bool enabled) => Calls.Add($"depth-mask:{enabled}"); - - public void DepthFunc(DepthFunction function) => - Calls.Add($"depth-func:{function}"); - - public void CullFace(TriangleFace face) => Calls.Add($"cull:{face}"); - - public void FrontFace(FrontFaceDirection direction) => - Calls.Add($"front:{direction}"); - - public void UseProgram(uint program) => Calls.Add($"program:{program}"); - - public void BindVertexArray(uint vertexArray) => Calls.Add($"vao:{vertexArray}"); - - public void BindBuffer(BufferTargetARB target, uint buffer) => - Calls.Add($"buffer:{target}:{buffer}"); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs index dfe2fa26..736ac2b0 100644 --- a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs +++ b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs @@ -58,8 +58,8 @@ public sealed class ResourceCleanupGroupTests throw new InvalidOperationException("release failed"); }); - GlResourceConstructionException failure = - Assert.Throws(() => + ResourceConstructionException failure = + Assert.Throws(() => resources.RollbackConstructionAndThrow( "construction failed", new InvalidOperationException("original failure"))); @@ -74,62 +74,6 @@ public sealed class ResourceCleanupGroupTests Assert.Equal(2, releases); } - [Fact] - public void WorldRenderFoundationConstructorsPublishEveryOwnedPrefix() - { - string root = FindRepoRoot(); - string graphicsDevice = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "Wb", - "OpenGLGraphicsDevice.cs")); - string shader = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "Wb", - "GLSLShader.cs")); - string uniformBuffer = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "Wb", - "ManagedGLUniformBuffer.cs")); - - AssertAppearsInOrder( - graphicsDevice, - "var resources = new ResourceCleanupGroup();", - "InstanceVBO = CreateConstructionBuffer(resources", - "WrapSampler = CreateConstructionSampler(", - "ClampSampler = CreateConstructionSampler(", - "_sceneDataBuffer = new ManagedGLUniformBuffer(", - "resources.Add(", - "\"WB scene-data uniform buffer\"", - "InitializeSharedDebugResources(resources);", - "resources.TransferAll();", - "resources.RollbackConstructionAndThrow("); - AssertAppearsInOrder( - shader, - "ShaderProgramConstruction.Build(", - "resources.Add(\"WB shader program\"", - "configure shader {Name} SceneData binding", - "resources.TransferAll();", - "resources.RollbackConstructionAndThrow("); - AssertAppearsInOrder( - uniformBuffer, - "TrackedGlResource.CreateBuffer(", - "resources.Add(", - "TrackedGlResource.AllocateBufferStorage(", - "resources.TransferAll();", - "resources.RollbackConstructionAndThrow("); - Assert.Contains("internal void DisposeImmediately()", uniformBuffer, - StringComparison.Ordinal); - } - /// /// Campaign V slice V4a moved TextRenderer's constructor off raw /// VAO/VBO/texture GL names and onto two device-owned resources — a @@ -176,49 +120,6 @@ public sealed class ResourceCleanupGroupTests return count; } - [Fact] - public void ReturnedNameIsRetainedWhenPostconditionAndFirstRollbackFail() - { - bool deleteFails = true; - int deleteCalls = 0; - - GlResourceConstructionException failure = - Assert.Throws(() => - GlResourceCommand.CreateNameCore( - "synthetic buffer", - static () => { }, - () => 42, - () => throw new InvalidOperationException("postcondition failed"), - name => - { - Assert.Equal(42u, name); - deleteCalls++; - if (deleteFails) - throw new InvalidOperationException("delete failed"); - })); - - Assert.False(failure.IsCleanupComplete); - Assert.Equal(1, deleteCalls); - - deleteFails = false; - failure.RetryCleanup(); - failure.RetryCleanup(); - - Assert.True(failure.IsCleanupComplete); - Assert.Equal(2, deleteCalls); - } - - private static void AssertAppearsInOrder(string source, params string[] values) - { - int cursor = -1; - foreach (string value in values) - { - int next = source.IndexOf(value, cursor + 1, StringComparison.Ordinal); - Assert.True(next > cursor, $"Missing or out-of-order source fragment: {value}"); - cursor = next; - } - } - private static string FindRepoRoot() { DirectoryInfo? directory = new(AppContext.BaseDirectory); diff --git a/tests/AcDream.App.Tests/Rendering/ShaderProgramConstructionTests.cs b/tests/AcDream.App.Tests/Rendering/ShaderProgramConstructionTests.cs deleted file mode 100644 index f66113ee..00000000 --- a/tests/AcDream.App.Tests/Rendering/ShaderProgramConstructionTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -using AcDream.App.Rendering; -using Silk.NET.OpenGL; - -namespace AcDream.App.Tests.Rendering; - -public sealed class ShaderProgramConstructionTests -{ - [Theory] - [InlineData("ShaderSource:VertexShader", 1, 0)] - [InlineData("CompileShader:VertexShader", 1, 0)] - [InlineData("CreateShader:FragmentShader", 1, 0)] - [InlineData("ShaderSource:FragmentShader", 2, 0)] - [InlineData("CompileShader:FragmentShader", 2, 0)] - [InlineData("CreateProgram", 2, 0)] - [InlineData("AttachShader:VertexShader", 2, 1)] - [InlineData("AttachShader:FragmentShader", 2, 1)] - [InlineData("LinkProgram", 2, 1)] - public void ThrowAfterEachAllocatedNameRollsBackEveryPublishedName( - string failingOperation, - int expectedShaderDeletes, - int expectedProgramDeletes) - { - var api = new FakeShaderApi { ThrowOn = failingOperation }; - - Assert.Throws(() => - ShaderProgramConstruction.Build(api, "vertex", "fragment")); - - Assert.Equal(expectedShaderDeletes, api.DeletedShaders.Count); - Assert.Equal(expectedProgramDeletes, api.DeletedPrograms.Count); - Assert.Equal(api.CreatedShaders, api.DeletedShaders.Order().ToArray()); - Assert.Equal(api.CreatedPrograms, api.DeletedPrograms.Order().ToArray()); - } - - [Fact] - public void CompileFailurePreservesOriginalFailureWhenRollbackIsClean() - { - var api = new FakeShaderApi { VertexCompiles = false }; - - InvalidOperationException failure = Assert.Throws(() => - ShaderProgramConstruction.Build(api, "vertex", "fragment")); - - Assert.Contains("VertexShader compile failed", failure.Message); - Assert.Equal([1u], api.DeletedShaders); - Assert.Empty(api.CreatedPrograms); - } - - [Fact] - public void LinkFailureDetachesAndDeletesBothShadersAndProgram() - { - var api = new FakeShaderApi { ProgramLinks = false }; - - InvalidOperationException failure = Assert.Throws(() => - ShaderProgramConstruction.Build(api, "vertex", "fragment")); - - Assert.Contains("program link failed", failure.Message); - Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls); - Assert.Equal([1u, 2u], api.DeletedShaders); - Assert.Equal([3u], api.DeletedPrograms); - } - - [Fact] - public void SuccessfulBuildCommitsOnlyProgramAndReleasesTemporaryShaders() - { - var api = new FakeShaderApi(); - - uint program = ShaderProgramConstruction.Build(api, "vertex", "fragment"); - - Assert.Equal(3u, program); - Assert.Equal([1u, 2u], api.DeletedShaders); - Assert.Empty(api.DeletedPrograms); - Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls); - } - - [Fact] - public void CleanupFailureStillAttemptsEveryOtherReleaseAndProgramRollback() - { - var api = new FakeShaderApi { ThrowOn = "DetachShader:VertexShader" }; - - AggregateException failure = Assert.Throws(() => - ShaderProgramConstruction.Build(api, "vertex", "fragment")); - - Assert.Single(failure.InnerExceptions); - Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls); - Assert.Equal([1u, 2u], api.DeletedShaders); - Assert.Equal([3u], api.DeletedPrograms); - } - - [Fact] - public void ConstructionAndRollbackFailuresAreReportedTogether() - { - var api = new FakeShaderApi - { - ProgramLinks = false, - ThrowOn = "DeleteShader:VertexShader", - }; - - GlResourceConstructionException failure = Assert.Throws(() => - ShaderProgramConstruction.Build(api, "vertex", "fragment")); - - Assert.Equal(2, failure.InnerExceptions.Count); - Assert.Contains("program link failed", failure.InnerExceptions[0].Message); - Assert.Equal([1u, 2u], api.DeleteShaderAttempts); - Assert.Equal([2u], api.DeletedShaders); - Assert.Equal([3u], api.DeletedPrograms); - - api.ThrowOn = null; - failure.RetryCleanup(); - - Assert.True(failure.IsCleanupComplete); - Assert.Equal([2u, 1u], api.DeletedShaders); - Assert.Equal([1u, 2u, 1u], api.DeleteShaderAttempts); - } - - private sealed class FakeShaderApi : IShaderProgramBuildApi - { - private readonly Dictionary _shaderTypes = []; - private uint _nextName = 1; - - public string? ThrowOn { get; set; } - public bool VertexCompiles { get; init; } = true; - public bool FragmentCompiles { get; init; } = true; - public bool ProgramLinks { get; init; } = true; - public List CreatedShaders { get; } = []; - public List CreatedPrograms { get; } = []; - public List DeleteShaderAttempts { get; } = []; - public List DeletedShaders { get; } = []; - public List DeletedPrograms { get; } = []; - public List DetachCalls { get; } = []; - - public uint CreateShader(ShaderType type) - { - Fail($"CreateShader:{type}"); - uint name = _nextName++; - CreatedShaders.Add(name); - _shaderTypes.Add(name, type); - return name; - } - - public void ShaderSource(uint shader, string source) => - Fail($"ShaderSource:{TypeOf(shader)}"); - - public void CompileShader(uint shader) => - Fail($"CompileShader:{TypeOf(shader)}"); - - public int GetShaderCompileStatus(uint shader) => TypeOf(shader) switch - { - ShaderType.VertexShader => VertexCompiles ? 1 : 0, - ShaderType.FragmentShader => FragmentCompiles ? 1 : 0, - _ => 0, - }; - - public string GetShaderInfoLog(uint shader) => $"bad {TypeOf(shader)}"; - - public uint CreateProgram() - { - Fail("CreateProgram"); - uint name = _nextName++; - CreatedPrograms.Add(name); - return name; - } - - public void AttachShader(uint program, uint shader) => - Fail($"AttachShader:{TypeOf(shader)}"); - - public void LinkProgram(uint program) => Fail("LinkProgram"); - public int GetProgramLinkStatus(uint program) => ProgramLinks ? 1 : 0; - public string GetProgramInfoLog(uint program) => "bad link"; - - public void DetachShader(uint program, uint shader) - { - string operation = $"DetachShader:{TypeOf(shader)}"; - DetachCalls.Add(operation); - Fail(operation); - } - - public void DeleteShader(uint shader) - { - DeleteShaderAttempts.Add(shader); - Fail($"DeleteShader:{TypeOf(shader)}"); - DeletedShaders.Add(shader); - } - - public void DeleteProgram(uint program) - { - Fail("DeleteProgram"); - DeletedPrograms.Add(program); - } - - private ShaderType TypeOf(uint shader) => _shaderTypes[shader]; - - private void Fail(string operation) - { - if (ThrowOn == operation) - throw new InvalidOperationException(operation + " failed"); - } - } -} diff --git a/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs b/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs deleted file mode 100644 index 4e87a29d..00000000 --- a/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System.Reflection; -using AcDream.App.Rendering; -using AcDream.App.Rendering.Gpu.Gl; -using Silk.NET.OpenGL; - -namespace AcDream.App.Tests.Rendering; - -/// -/// The retained UI's whole draw is one RHI pass, and a failure anywhere inside -/// it must not leave GL state behind for the raw-GL world renderers that run in -/// the next frame. Until Campaign V slice V6d, TextRenderer owned a private -/// state scope for that; V6d made the renderer backend-neutral and moved the -/// guarantee onto , which every RHI pass -/// gets. These tests follow it there. -/// -public sealed class TextRendererFailureSafetyTests -{ - [Fact] - public void Flush_CompilesThePassEncoderAsFinallyAroundBothDrawLayers() - { - MethodInfo flush = typeof(TextRenderer).GetMethod(nameof(TextRenderer.Flush))!; - MethodBody body = flush.GetMethodBody()!; - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "TextRenderer.cs")); - - // The encoder's `using` is the finally: disposing it closes the pass, - // which is what restores the ambient capability state the pass changed. - Assert.Contains( - body.ExceptionHandlingClauses, - clause => clause.Flags == ExceptionHandlingClauseOptions.Finally); - AssertAppearsInOrder( - source, - "using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription", - "encoder.BindPipeline(_pipeline);", - "DrawLayer(_spriteSegs,", - "DrawLayer(_overlaySpriteSegs,"); - - // And no raw GL of its own is left: the renderer draws on both backends. - Assert.DoesNotContain("Silk.NET.OpenGL", source, StringComparison.Ordinal); - Assert.DoesNotContain("_gl.", source, StringComparison.Ordinal); - } - - [Fact] - public void FailedDraw_RestoresEveryGlValueMutatedByThePass() - { - var gl = new RecordingGlState - { - DepthWrite = false, - DepthFuncValue = DepthFunction.Greater, - BlendSourceRgb = BlendingFactor.DstAlpha, - BlendDestinationRgb = BlendingFactor.OneMinusDstAlpha, - BlendSourceAlpha = BlendingFactor.One, - BlendDestinationAlpha = BlendingFactor.Zero, - CullFaceMode = TriangleFace.Front, - FrontFaceDirection = FrontFaceDirection.CW, - Program = 17, - VertexArray = 23, - ArrayBuffer = 31, - ActiveUnit = TextureUnit.Texture2, - }; - gl.SetCapability(EnableCap.DepthTest, enabled: true); - gl.SetCapability(EnableCap.Blend, enabled: false); - gl.SetCapability(EnableCap.CullFace, enabled: true); - gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: true); - // Enabled on entry — the world's MSAA. The UI pass turns it off and the - // restore has to put it back, which is the exact dimension the V4a - // revert lost and which nothing covered before this test. - gl.SetCapability(EnableCap.Multisample, enabled: true); - gl.TextureBindings[TextureUnit.Texture0] = 41; - gl.TextureBindings[TextureUnit.Texture2] = 43; - StateSnapshot expected = gl.Capture(); - - Action failedDraw = () => - { - GlAmbientCapabilityState ambient = GlAmbientCapabilityState.Capture(gl); - try - { - gl.SetCapability(EnableCap.DepthTest, enabled: false); - gl.SetCapability(EnableCap.Blend, enabled: true); - gl.SetCapability(EnableCap.CullFace, enabled: false); - gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false); - gl.SetCapability(EnableCap.Multisample, enabled: false); - gl.DepthMask(true); - gl.DepthFunc(DepthFunction.Lequal); - gl.BlendFuncSeparate( - BlendingFactor.SrcAlpha, - BlendingFactor.OneMinusSrcAlpha, - BlendingFactor.SrcAlpha, - BlendingFactor.OneMinusSrcAlpha); - gl.CullFace(TriangleFace.Back); - gl.FrontFace(FrontFaceDirection.Ccw); - gl.UseProgram(101); - gl.BindVertexArray(103); - gl.BindBuffer(BufferTargetARB.ArrayBuffer, 107); - gl.ActiveTexture(TextureUnit.Texture0); - gl.BindTexture(TextureTarget.Texture2D, 109); - throw new InvalidOperationException("draw upload"); - } - finally - { - ambient.Restore(gl); - } - }; - - Assert.Throws(failedDraw); - - Assert.Equal(expected, gl.Capture()); - } - - private readonly record struct StateSnapshot( - bool DepthTest, - bool Blend, - bool Cull, - bool AlphaToCoverage, - bool Multisample, - bool DepthWrite, - DepthFunction DepthFunc, - BlendingFactor BlendSourceRgb, - BlendingFactor BlendDestinationRgb, - BlendingFactor BlendSourceAlpha, - BlendingFactor BlendDestinationAlpha, - TriangleFace CullFaceMode, - FrontFaceDirection FrontFaceDirection, - uint Program, - uint VertexArray, - uint ArrayBuffer, - TextureUnit ActiveUnit, - uint Texture0, - uint Texture2, - bool StencilTest, - StencilFunction StencilFunc, - int StencilReference, - uint StencilValueMask, - uint StencilWriteMask, - Silk.NET.OpenGL.StencilOp StencilFail, - Silk.NET.OpenGL.StencilOp StencilDepthFail, - Silk.NET.OpenGL.StencilOp StencilPass, - bool ColorMaskRed, - bool ColorMaskGreen, - bool ColorMaskBlue, - bool ColorMaskAlpha); - - private sealed class RecordingGlState : IGlAmbientStateApi - { - private readonly Dictionary _capabilities = []; - - public bool DepthWrite { get; set; } - public DepthFunction DepthFuncValue { get; set; } - public BlendingFactor BlendSourceRgb { get; set; } - public BlendingFactor BlendDestinationRgb { get; set; } - public BlendingFactor BlendSourceAlpha { get; set; } - public BlendingFactor BlendDestinationAlpha { get; set; } - public TriangleFace CullFaceMode { get; set; } - public FrontFaceDirection FrontFaceDirection { get; set; } - public uint Program { get; set; } - public uint VertexArray { get; set; } - public uint ArrayBuffer { get; set; } - public TextureUnit ActiveUnit { get; set; } - public Dictionary TextureBindings { get; } = []; - - // Slice V6l added the stencil and colour-mask dimensions to the ambient - // save/restore, for the portal depth mask's sake. GL's own defaults. - public StencilFunction StencilFuncValue { get; set; } = StencilFunction.Always; - public int StencilReference { get; set; } - public uint StencilValueMask { get; set; } = 0xFFFFFFFFu; - public uint StencilWriteMaskValue { get; set; } = 0xFFFFFFFFu; - public Silk.NET.OpenGL.StencilOp StencilFailOp { get; set; } = Silk.NET.OpenGL.StencilOp.Keep; - public Silk.NET.OpenGL.StencilOp StencilDepthFailOp { get; set; } = Silk.NET.OpenGL.StencilOp.Keep; - public Silk.NET.OpenGL.StencilOp StencilPassOp { get; set; } = Silk.NET.OpenGL.StencilOp.Keep; - public bool[] ColorMaskValue { get; } = [true, true, true, true]; - - public StateSnapshot Capture() => new( - IsEnabled(EnableCap.DepthTest), - IsEnabled(EnableCap.Blend), - IsEnabled(EnableCap.CullFace), - IsEnabled(EnableCap.SampleAlphaToCoverage), - IsEnabled(EnableCap.Multisample), - DepthWrite, - DepthFuncValue, - BlendSourceRgb, - BlendDestinationRgb, - BlendSourceAlpha, - BlendDestinationAlpha, - CullFaceMode, - FrontFaceDirection, - Program, - VertexArray, - ArrayBuffer, - ActiveUnit, - TextureBindings.GetValueOrDefault(TextureUnit.Texture0), - TextureBindings.GetValueOrDefault(TextureUnit.Texture2), - IsEnabled(EnableCap.StencilTest), - StencilFuncValue, - StencilReference, - StencilValueMask, - StencilWriteMaskValue, - StencilFailOp, - StencilDepthFailOp, - StencilPassOp, - ColorMaskValue[0], - ColorMaskValue[1], - ColorMaskValue[2], - ColorMaskValue[3]); - - public bool IsEnabled(EnableCap capability) => - _capabilities.GetValueOrDefault(capability); - - public int GetInteger(GetPName parameter) => parameter switch - { - GetPName.BlendSrcRgb => (int)BlendSourceRgb, - GetPName.BlendDstRgb => (int)BlendDestinationRgb, - GetPName.BlendSrcAlpha => (int)BlendSourceAlpha, - GetPName.BlendDstAlpha => (int)BlendDestinationAlpha, - GetPName.DepthFunc => (int)DepthFuncValue, - GetPName.CullFaceMode => (int)CullFaceMode, - GetPName.FrontFace => (int)FrontFaceDirection, - GetPName.CurrentProgram => (int)Program, - GetPName.VertexArrayBinding => (int)VertexArray, - GetPName.ArrayBufferBinding => (int)ArrayBuffer, - GetPName.ActiveTexture => (int)ActiveUnit, - GetPName.TextureBinding2D => - (int)TextureBindings.GetValueOrDefault(ActiveUnit), - GetPName.StencilFunc => (int)StencilFuncValue, - GetPName.StencilRef => StencilReference, - GetPName.StencilValueMask => (int)StencilValueMask, - GetPName.StencilWritemask => (int)StencilWriteMaskValue, - GetPName.StencilFail => (int)StencilFailOp, - GetPName.StencilPassDepthFail => (int)StencilDepthFailOp, - GetPName.StencilPassDepthPass => (int)StencilPassOp, - _ => throw new ArgumentOutOfRangeException(nameof(parameter)), - }; - - public bool[] GetColorMask() => (bool[])ColorMaskValue.Clone(); - - public void StencilFunc(StencilFunction function, int reference, uint mask) - { - StencilFuncValue = function; - StencilReference = reference; - StencilValueMask = mask; - } - - public void StencilOp( - Silk.NET.OpenGL.StencilOp fail, - Silk.NET.OpenGL.StencilOp depthFail, - Silk.NET.OpenGL.StencilOp pass) - { - StencilFailOp = fail; - StencilDepthFailOp = depthFail; - StencilPassOp = pass; - } - - public void StencilMask(uint mask) => StencilWriteMaskValue = mask; - - public void ColorMask(bool red, bool green, bool blue, bool alpha) - { - ColorMaskValue[0] = red; - ColorMaskValue[1] = green; - ColorMaskValue[2] = blue; - ColorMaskValue[3] = alpha; - } - - public bool GetBoolean(GetPName parameter) => - parameter == GetPName.DepthWritemask - ? DepthWrite - : throw new ArgumentOutOfRangeException(nameof(parameter)); - - public void SetCapability(EnableCap capability, bool enabled) => - _capabilities[capability] = enabled; - - public void DepthMask(bool enabled) => DepthWrite = enabled; - - public void DepthFunc(DepthFunction function) => DepthFuncValue = function; - - public void BlendFuncSeparate( - BlendingFactor sourceRgb, - BlendingFactor destinationRgb, - BlendingFactor sourceAlpha, - BlendingFactor destinationAlpha) - { - BlendSourceRgb = sourceRgb; - BlendDestinationRgb = destinationRgb; - BlendSourceAlpha = sourceAlpha; - BlendDestinationAlpha = destinationAlpha; - } - - public void CullFace(TriangleFace face) => CullFaceMode = face; - - public void FrontFace(FrontFaceDirection direction) => FrontFaceDirection = direction; - - public void UseProgram(uint program) => Program = program; - - public void BindVertexArray(uint vertexArray) => VertexArray = vertexArray; - - public void BindBuffer(BufferTargetARB target, uint buffer) - { - Assert.Equal(BufferTargetARB.ArrayBuffer, target); - ArrayBuffer = buffer; - } - - public void ActiveTexture(TextureUnit unit) => ActiveUnit = unit; - - public void BindTexture(TextureTarget target, uint texture) - { - Assert.Equal(TextureTarget.Texture2D, target); - TextureBindings[ActiveUnit] = texture; - } - } - - private static void AssertAppearsInOrder(string source, params string[] needles) - { - int cursor = -1; - foreach (string needle in needles) - { - int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal); - Assert.True(next >= 0, $"Missing expected source fragment: {needle}"); - Assert.True(next > cursor, $"Out-of-order source fragment: {needle}"); - cursor = next; - } - } - - private static string FindRepoRoot() - { - DirectoryInfo? directory = new(AppContext.BaseDirectory); - while (directory is not null) - { - if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) - return directory.FullName; - directory = directory.Parent; - } - - throw new DirectoryNotFoundException("Could not find AcDream.slnx."); - } -} diff --git a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs index 007cb531..d7626792 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs @@ -1,18 +1,78 @@ // Tests for EnvCellRenderer (Phase A8, 2026-05-28). // These cover the pure data-handling portions of EnvCellRenderer. -// The GL-dependent Render() and RenderModernMDIInternal() paths require a -// GL context and are visual-verified at the render frame (Task 10). +// The draw-recording Render() and RenderModernMDIInternal() paths require a +// live GPU device and are visual-verified at the render frame (Task 10). +// +// Campaign V slice V11: the raw-GL constructor (which stored a possibly-null +// GL reference and did no other work) was deleted along with the GL arm. The +// sole remaining constructor builds three real pipelines against IGpuDevice, +// so these "no GL calls" tests now construct through the RHI arm with the +// same lightweight, no-hardware-required fakes the composition tests use: +// RecordingGpuDevice (records but does no driver work), GpuDeviceFrameLifetime +// (never began, so CurrentFrame stays null — fine, since these tests never +// draw), and VulkanWorldPassScope (needs only a sample count, no live surface). +// meshManager can no longer be null either — the RHI constructor throws on a +// null ObjectMeshManager, so tests that don't care about mesh-manager +// behavior get a real one built the same way MeshPipelineDeviceSeamTests +// does: VulkanMeshPipelineDevice (the production Vulkan seam implementation) +// over the same RecordingGpuDevice, plus a no-op IPreparedAssetSource. using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; +using System.Threading; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Gpu; +using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Rendering.Wb; +using AcDream.App.Tests.Rendering.Gpu; +using AcDream.Content; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace AcDream.App.Tests.Rendering.Wb; public class EnvCellRendererTests { + private sealed class NullPreparedAssetSource : IPreparedAssetSource + { + public PreparedAssetSourceStats Stats => default; + + public CacheStats DecodedTextureCacheStats => default; + + public PreparedAssetPresence Probe( + AcDream.Content.Pak.PakAssetType type, + uint sourceFileId) => + PreparedAssetPresence.Missing; + + public PreparedAssetReadResult Read( + in PreparedAssetRequest request, + CancellationToken cancellationToken = default) => + PreparedAssetReadResult.Missing; + + public void Dispose() + { + } + } + + private static ObjectMeshManager CreateMeshManager(RecordingGpuDevice device) => + new( + new VulkanMeshPipelineDevice(device.Retirement), + device, + new NullPreparedAssetSource(), + NullLogger.Instance); + + private static EnvCellRenderer CreateRenderer(ObjectMeshManager? meshManager = null) + { + var device = new RecordingGpuDevice(); + return new EnvCellRenderer( + device, + new GpuDeviceFrameLifetime(device), + new VulkanWorldPassScope(sampleCount: 1), + meshManager ?? CreateMeshManager(device), + new WbFrustum()); + } + [Fact] public void OrderedMdiRanges_CoalesceAdjacentCellsWithIdenticalState() { @@ -57,14 +117,14 @@ public class EnvCellRendererTests } // ----------------------------------------------------------------------- - // GetEnvCellGeomId — verbatim port of WB EnvCellRenderManager.cs:94-103 + // GetEnvCellGeomId — verbatim port of WB EnvCellRenderManager.cs:94-103 // ----------------------------------------------------------------------- [Fact] public void GetEnvCellGeomId_DedupBitSet() { var id = EnvCellRenderer.GetEnvCellGeomId(0x42, 7, new List { 1, 2, 3 }); - // Bit 33 (0x2_0000_0000) must be set — distinguishes dedup geom from per-cell ids. + // Bit 33 (0x2_0000_0000) must be set — distinguishes dedup geom from per-cell ids. Assert.NotEqual(0UL, id & 0x2_0000_0000UL); } @@ -102,40 +162,40 @@ public class EnvCellRendererTests } // ----------------------------------------------------------------------- - // Constructor — pure data, no GL + // Constructor — pure data, no GL // ----------------------------------------------------------------------- [Fact] public void NewRenderer_NeedsPrepareIsTrue() { - // GL and meshManager are null — only valid for pure-data tests (no + // GL and meshManager are null — only valid for pure-data tests (no // Initialize() is called, so no GL calls are made). - var r = new EnvCellRenderer(gl: null!, meshManager: null!, frustum: new WbFrustum()); + var r = CreateRenderer(); Assert.True(r.NeedsPrepare); } [Fact] public void NewRenderer_NotDisposed() { - var r = new EnvCellRenderer(gl: null!, meshManager: null!, frustum: new WbFrustum()); + var r = CreateRenderer(); Assert.False(r.IsDisposed); } // ----------------------------------------------------------------------- - // RemoveLandblock — pure data path + // RemoveLandblock — pure data path // ----------------------------------------------------------------------- [Fact] public void RemoveLandblock_NonExistent_DoesNotThrow() { - var r = new EnvCellRenderer(gl: null!, meshManager: null!, frustum: new WbFrustum()); + var r = CreateRenderer(); // Should silently no-op. r.RemoveLandblock(0xA9B40000u); Assert.True(r.NeedsPrepare); } // ----------------------------------------------------------------------- - // GetEnvCellGeomId — additional edge cases + // GetEnvCellGeomId — additional edge cases // ----------------------------------------------------------------------- [Fact] @@ -157,7 +217,7 @@ public class EnvCellRendererTests Assert.NotEqual(a, b); } - // (Render() requires a GL context — visual-verified in Task 10.) + // (Render() requires a GL context — visual-verified in Task 10.) [Fact] public void GpuInstanceUpload_UsesMeshModernMat4Stride() @@ -212,13 +272,13 @@ public class EnvCellRendererTests { // The bug: WB's GetPooledList clears the list before returning so // the merge phase pattern `gfxDict[k] = list; list.AddRange(...)` - // populates fresh data. The original port omitted Clear() — each + // populates fresh data. The original port omitted Clear() — each // frame's lists grew unbounded with stale data layered on top. // // Reflection-based test that drives the private GetPooledList + // _poolIndex/_listPool fields. If a future refactor removes the // Clear() call, this test fails. - var r = new EnvCellRenderer(gl: null!, meshManager: null!, frustum: new WbFrustum()); + var r = CreateRenderer(); var type = typeof(EnvCellRenderer); var getPooledListMethod = type.GetMethod("GetPooledList", @@ -228,16 +288,16 @@ public class EnvCellRendererTests System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); Assert.NotNull(poolIndexField); - // First call — creates _listPool[0], _poolIndex 0 → 1. + // First call — creates _listPool[0], _poolIndex 0 → 1. var first = (List)getPooledListMethod!.Invoke(r, null)!; first.Add(new InstanceData()); first.Add(new InstanceData()); Assert.Equal(2, first.Count); - // Reset cursor to 0 — simulates the start of the next prepare cycle. + // Reset cursor to 0 — simulates the start of the next prepare cycle. poolIndexField!.SetValue(r, 0); - // Second call — returns _listPool[0] (same as first). With the fix + // Second call — returns _listPool[0] (same as first). With the fix // it should be cleared. Without the fix the list still has 2 items. var second = (List)getPooledListMethod.Invoke(r, null)!; Assert.Same(first, second); // reuses the same instance @@ -249,7 +309,7 @@ public class EnvCellRendererTests { // Sanity check for the fresh-list branch. _poolIndex past _listPool.Count // should produce a brand-new empty list and grow the pool. - var r = new EnvCellRenderer(gl: null!, meshManager: null!, frustum: new WbFrustum()); + var r = CreateRenderer(); var type = typeof(EnvCellRenderer); var getPooledListMethod = type.GetMethod("GetPooledList", @@ -264,8 +324,8 @@ public class EnvCellRendererTests } // ----------------------------------------------------------------------- - // Prepare gate (2026-07-24) — pure camera-tolerance half. - // The tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer + // Prepare gate (2026-07-24) — pure camera-tolerance half. + // The tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer // R-A2 note) but never survive real camera motion. // ----------------------------------------------------------------------- @@ -288,7 +348,7 @@ public class EnvCellRendererTests [Fact] public void CameraApproximatelyEqual_RestJitter_True() { - // The ~36 µm eye rest jitter must NOT dirty the gate. + // The ~36 µm eye rest jitter must NOT dirty the gate. var eye = new Vector3(120.34f, 87.91f, 10.2f); var jittered = eye + new Vector3(36e-6f, -36e-6f, 36e-6f); var a = ViewProjectionFor(eye, Vector3.UnitX); @@ -299,7 +359,7 @@ public class EnvCellRendererTests [Fact] public void CameraApproximatelyEqual_SmallRealRotation_False() { - // 0.05° of yaw — far below one frame of real mouse motion — must dirty. + // 0.05° of yaw — far below one frame of real mouse motion — must dirty. var eye = new Vector3(120.34f, 87.91f, 10.2f); float yaw = 0.05f * MathF.PI / 180f; var a = ViewProjectionFor(eye, Vector3.UnitX); @@ -322,7 +382,7 @@ public class EnvCellRendererTests public void CameraApproximatelyEqual_GlobalScaleCoordinates_TranslationStillDirties() { // AC world coordinates reach ~5e4. A relative tolerance applied to the - // matrix translation row would mask sub-meter motion at that scale — + // matrix translation row would mask sub-meter motion at that scale — // the eye epsilon is absolute precisely so this case stays sharp. var eye = new Vector3(40120.34f, 45087.91f, 110.2f); var moved = eye + new Vector3(0.07f, 0f, 0f); // one walking frame @@ -338,7 +398,7 @@ public class EnvCellRendererTests [Fact] public void NewRenderer_SnapshotGenerationStartsAtZero() { - var r = new EnvCellRenderer(gl: null!, meshManager: null!, frustum: new WbFrustum()); + var r = CreateRenderer(); Assert.Equal(0, r.SnapshotGeneration); } } diff --git a/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs index 8894fe81..0d1e70b8 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs @@ -100,23 +100,6 @@ public sealed class MeshPipelineDeviceSeamTests Assert.False(manager.IsDisposed); } - /// - /// The GL handle table is the emulation the Vulkan backend replaces with set - /// 2, so asking a non-GL pipeline for it is a programming error — and it says - /// which backend it was composed against rather than reporting a cast. - /// - [Fact] - public void TheGlHandleTableIsRefusedByNameRatherThanCast() - { - using var device = new RecordingGpuDevice(); - using ObjectMeshManager manager = Build(device); - - InvalidOperationException failure = - Assert.Throws(() => manager.WorldTextureTable); - Assert.Contains("GL-only", failure.Message, StringComparison.Ordinal); - Assert.Contains(GpuBackendKind.Recording.ToString(), failure.Message, StringComparison.Ordinal); - } - /// /// The one branch the texture stack keeps: a GL pair yields the GL arm, and /// anything else yields the RHI arm. Selection happens once, at construction. @@ -203,9 +186,6 @@ public sealed class MeshPipelineDeviceSeamTests using ObjectMeshManager manager = Build(device, modernPath: true); GlobalMeshBuffer arena = Assert.IsType(manager.GlobalBuffer); - Assert.Equal(0u, arena.VAO); - Assert.Equal(0u, arena.VBO); - Assert.Equal(0u, arena.IBO); Assert.True(arena.HasStores); Assert.NotNull(arena.VertexStore); Assert.NotNull(arena.IndexStore); diff --git a/tests/AcDream.App.Tests/Rendering/Wb/TextureAtlasCapacityTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/TextureAtlasCapacityTests.cs index 44294ea2..9e618d9f 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/TextureAtlasCapacityTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/TextureAtlasCapacityTests.cs @@ -32,7 +32,7 @@ public sealed class TextureAtlasCapacityTests [Fact] public void DirectUploadAcceptsExactRgbaPayload() { - ManagedGLTextureArray.ValidateUploadPayload( + RhiWorldTextureArray.ValidateUploadPayload( TextureFormat.RGBA8, 2, 2, 16, PixelFormat.Rgba, PixelType.UnsignedByte); } @@ -42,7 +42,7 @@ public sealed class TextureAtlasCapacityTests public void DirectUploadRejectsNonExactPayloadLength(int bytes) { Assert.Throws(() => - ManagedGLTextureArray.ValidateUploadPayload( + RhiWorldTextureArray.ValidateUploadPayload( TextureFormat.RGBA8, 2, 2, bytes, PixelFormat.Rgba, PixelType.UnsignedByte)); } @@ -50,27 +50,27 @@ public sealed class TextureAtlasCapacityTests public void DirectUploadRejectsMismatchedTransferTuple() { Assert.Throws(() => - ManagedGLTextureArray.ValidateUploadPayload( + RhiWorldTextureArray.ValidateUploadPayload( TextureFormat.RGBA8, 2, 2, 16, PixelFormat.Rgb, PixelType.UnsignedByte)); Assert.Throws(() => - ManagedGLTextureArray.ValidateUploadPayload( + RhiWorldTextureArray.ValidateUploadPayload( TextureFormat.RGBA8, 2, 2, 16, PixelFormat.Rgba, PixelType.Float)); } [Fact] public void CompressedUploadRejectsUncompressedDescriptorOverride() { - int bytes = ManagedGLTextureArray.CalculateExpectedDataSize(TextureFormat.DXT1, 4, 4); + int bytes = RhiWorldTextureArray.CalculateExpectedDataSize(TextureFormat.DXT1, 4, 4); Assert.Throws(() => - ManagedGLTextureArray.ValidateUploadPayload( + RhiWorldTextureArray.ValidateUploadPayload( TextureFormat.DXT1, 4, 4, bytes, PixelFormat.Rgba, PixelType.UnsignedByte)); } [Fact] public void RgbPayloadUsesTightlyPackedRows() { - Assert.Equal(18, ManagedGLTextureArray.CalculateExpectedDataSize(TextureFormat.RGB8, 3, 2)); - ManagedGLTextureArray.ValidateUploadPayload( + Assert.Equal(18, RhiWorldTextureArray.CalculateExpectedDataSize(TextureFormat.RGB8, 3, 2)); + RhiWorldTextureArray.ValidateUploadPayload( TextureFormat.RGB8, 3, 2, 18, PixelFormat.Rgb, PixelType.UnsignedByte); } } diff --git a/tests/AcDream.App.Tests/RuntimeOptionsTests.cs b/tests/AcDream.App.Tests/RuntimeOptionsTests.cs index f106c469..3970c486 100644 --- a/tests/AcDream.App.Tests/RuntimeOptionsTests.cs +++ b/tests/AcDream.App.Tests/RuntimeOptionsTests.cs @@ -475,75 +475,6 @@ public sealed class RuntimeOptionsTests Assert.Throws(() => RuntimeOptions.Parse(AnyDatDir, null!)); } - /// - /// Campaign V slice V10 flipped the default: Vulkan is the shipping backend - /// and an unset variable must select it. Awaiting the user's cutover - /// sign-off; the one-line rollback restores GL here and in - /// RuntimeOptions.ParseRenderBackend together. - /// - [Fact] - public void RenderBackend_DefaultsToVulkan() - { - Assert.Equal( - RenderBackendKind.Vulkan, - RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).RenderBackend); - } - - [Theory] - [InlineData("vulkan")] - [InlineData("Vulkan")] - [InlineData("VULKAN")] - public void RenderBackend_SelectsVulkanCaseInsensitively(string value) - { - Assert.Equal( - RenderBackendKind.Vulkan, - RuntimeOptions.Parse( - AnyDatDir, - Env(new() { ["ACDREAM_RENDER_BACKEND"] = value })).RenderBackend); - } - - /// - /// The V10 escape hatch, and the whole of it. Both spellings are honoured - /// because a near-miss here strands the operator on the backend they asked - /// to leave. - /// - [Theory] - [InlineData("gl")] - [InlineData("GL")] - [InlineData("Gl")] - [InlineData("opengl")] - [InlineData("OpenGL")] - public void RenderBackend_SelectsGlOnlyForTheEscapeHatchTokens(string value) - { - Assert.Equal( - RenderBackendKind.Gl, - RuntimeOptions.Parse( - AnyDatDir, - Env(new() { ["ACDREAM_RENDER_BACKEND"] = value })).RenderBackend); - } - - /// - /// The mirror image of the pre-V10 rule. A typo used to have to land on GL - /// because Vulkan was dark; it now has to land on Vulkan because GL is the - /// backend slice V11 deletes, and a misspelled fallback that silently works - /// is how a process ends up pinned to it. - /// - [Theory] - [InlineData("")] - [InlineData("vulcan")] - [InlineData("vk")] - [InlineData(" vulkan")] - [InlineData(" gl")] - [InlineData("ogl")] - public void RenderBackend_AnythingElseStaysOnVulkan(string value) - { - Assert.Equal( - RenderBackendKind.Vulkan, - RuntimeOptions.Parse( - AnyDatDir, - Env(new() { ["ACDREAM_RENDER_BACKEND"] = value })).RenderBackend); - } - [Fact] public void VulkanDeviceOverride_IsNullWhenUnsetOrEmpty() { diff --git a/tests/AcDream.Core.Tests/Rendering/TextureCacheBindlessTests.cs b/tests/AcDream.Core.Tests/Rendering/TextureCacheBindlessTests.cs deleted file mode 100644 index 88877f66..00000000 --- a/tests/AcDream.Core.Tests/Rendering/TextureCacheBindlessTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using AcDream.App.Rendering; -using AcDream.App.Rendering.Wb; -using DatReaderWriter; -using Xunit; - -namespace AcDream.Core.Tests.Rendering; - -/// -/// Lightweight unit tests for 's bindless path. -/// We can't construct a real TextureCache in a headless test (it requires a -/// live GL context), so this file documents contracts that future engineers -/// should preserve. Real bindless integration is verified at Task 14's -/// visual gate. -/// -public sealed class TextureCacheBindlessTests -{ - [Fact] - public void Contract_BindlessMethodsThrowWithoutBindlessSupport() - { - // The actual throw lives in TextureCache.EnsureBindlessAvailable - // and is reached only via GL-bound Bindless* method calls. The - // contract is: if the dispatcher (which requires bindless) ever - // gets a TextureCache constructed without BindlessSupport, it - // should fail-fast with InvalidOperationException — NOT silently - // route a draw to handle 0 (which would produce a non-resident - // GPU fault). - // - // This test is a marker. Future engineers: do not weaken - // EnsureBindlessAvailable to swallow the missing dependency. - Assert.True(true, "Contract documented in TextureCache.EnsureBindlessAvailable"); - } -}