feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b70b9832ff
commit
8a7a0837e1
121 changed files with 1243 additions and 19840 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,79 +1,32 @@
|
|||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: the graphics ownership that platform acquisition
|
||||
/// publishes, once per backend.
|
||||
/// The graphics ownership that platform acquisition publishes.
|
||||
///
|
||||
/// <para><see cref="GameWindowPlatformAcquisition"/> was already generic over its
|
||||
/// graphics type; only the call sites pinned <c>GL</c>. 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.</para>
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// <para>Vulkan is the only backend as of Campaign V slice V11: the raw-GL
|
||||
/// implementation (<c>OpenGlGameWindowGraphics</c>) and the members that only
|
||||
/// existed to branch on it (<c>Backend</c>, <c>Gl</c>, <c>RequireGl</c>) were
|
||||
/// deleted along with it. This class stays as the seam
|
||||
/// <see cref="GameWindowPlatformAcquisition"/> 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.</para>
|
||||
/// </summary>
|
||||
internal abstract class GameWindowGraphics : IDisposable
|
||||
{
|
||||
/// <summary>Which backend this handle owns.</summary>
|
||||
public abstract RenderBackendKind Backend { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public virtual GL? Gl => null;
|
||||
|
||||
/// <summary>The live Vulkan context, or null on any other backend.</summary>
|
||||
/// <summary>The live Vulkan context.</summary>
|
||||
public virtual VulkanGraphicsContext? Vulkan => null;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6j: the backend's world-pass seam, or null where the
|
||||
/// world renderers open their own passes.
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// The backend's world-pass seam. Every remaining renderer records into the
|
||||
/// pass this publishes rather than opening its own.
|
||||
/// </summary>
|
||||
public virtual AcDream.App.Rendering.IWorldPassScope? WorldPassScope => null;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public GL RequireGl(string owner) =>
|
||||
Gl ?? throw new InvalidOperationException(
|
||||
$"'{owner}' requires the OpenGL context, but the {Backend} backend is active.");
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
|
||||
/// <summary>OpenGL ownership: the Silk.NET <see cref="GL"/> context itself.</summary>
|
||||
internal sealed class OpenGlGameWindowGraphics : GameWindowGraphics
|
||||
{
|
||||
public OpenGlGameWindowGraphics(GL gl) =>
|
||||
Context = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
|
||||
/// <summary>The owned context. <see cref="GameWindowGraphics.Gl"/> is the borrowed view.</summary>
|
||||
public GL Context { get; }
|
||||
|
||||
public override RenderBackendKind Backend => RenderBackendKind.Gl;
|
||||
|
||||
public override GL? Gl => Context;
|
||||
|
||||
public override void Dispose() => Context.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vulkan ownership: the instance, surface, device, swapchain and RHI device
|
||||
/// that <see cref="VulkanGraphicsContext"/> 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;
|
||||
|
||||
/// <summary>The concrete scope, for the phase that publishes the encoder on it.</summary>
|
||||
|
|
|
|||
|
|
@ -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<IMouse> 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<HostInputCameraCompositionPoint>? _faultInjection;
|
||||
private IHostInputCameraCompositionFactory _factory =
|
||||
new RetailHostInputCameraCompositionFactory();
|
||||
new VulkanHostInputCameraCompositionFactory();
|
||||
|
||||
public HostInputCameraCompositionPhase(
|
||||
HostInputCameraDependencies dependencies,
|
||||
|
|
@ -253,17 +167,13 @@ internal sealed class HostInputCameraCompositionPhase :
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static IHostInputCameraCompositionFactory DefaultFactoryFor(
|
||||
GameWindowGraphics graphics) =>
|
||||
graphics.Backend == RenderBackendKind.Vulkan
|
||||
? new VulkanHostInputCameraCompositionFactory()
|
||||
: new RetailHostInputCameraCompositionFactory();
|
||||
new VulkanHostInputCameraCompositionFactory();
|
||||
|
||||
public HostInputCameraResult Compose(
|
||||
GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -8,28 +8,22 @@ using Silk.NET.Input;
|
|||
namespace AcDream.App.Composition;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: the Vulkan arm of the host phase.
|
||||
///
|
||||
/// <para>This is the whole of the backend fork at composition Phase 1. Only the
|
||||
/// four graphics members differ from
|
||||
/// <see cref="RetailHostInputCameraCompositionFactory"/>; 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.</para>
|
||||
/// Campaign V slice V6h: the Vulkan arm of the host phase — now the only arm,
|
||||
/// the raw-GL <c>RetailHostInputCameraCompositionFactory</c> 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.
|
||||
///
|
||||
/// <para><b>What is absent, and why.</b> 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
|
||||
/// <see cref="WorldRenderDiagnostics"/>: it is a raw-GL state tripwire, and
|
||||
/// <see cref="WorldRenderDiagnostics"/>: 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.</para>
|
||||
/// </summary>
|
||||
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<IMouse> 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.");
|
||||
|
||||
/// <summary>
|
||||
/// Vulkan sets the viewport per pass from the pass extent, so there is no
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
/// <summary>
|
||||
/// The render foundation the later phases build on.
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// <para>Every member here is backend-neutral. The raw-GL-only members this
|
||||
/// record used to carry (<c>Bindless</c>, <c>TerrainShader</c>, <c>MeshShader</c>,
|
||||
/// <c>Samplers</c>) were deleted at Campaign V slice V11 along with the GL arm
|
||||
/// that was their only producer.</para>
|
||||
/// </summary>
|
||||
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<uint, SurfaceInfo> 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<string> log);
|
||||
TerrainAtlas AcquireTerrainAtlas(
|
||||
IGameRenderResourceLifetime lifetime,
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless);
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the terrain atlas built through
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/> rather than raw GL.
|
||||
/// Same DATs, same decode, same layer ordering — only the upload differs.
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.IGpuDevice"/>. The raw-GL arm this
|
||||
/// used to fork from (<c>AcquireTerrainAtlas</c>) 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.
|
||||
/// </summary>
|
||||
TerrainAtlas AcquireBackendNeutralTerrainAtlas(
|
||||
IGameRenderResourceLifetime lifetime,
|
||||
|
|
@ -120,12 +104,11 @@ internal interface IWorldRenderCompositionFactory
|
|||
Action<string> log);
|
||||
|
||||
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
||||
Shader CreateTerrainShader(GL gl, string shadersDirectory);
|
||||
SceneLightingUboBinding CreateSceneLighting(GL gl);
|
||||
/// <summary>
|
||||
/// 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 (<c>CreateSceneLighting</c>) was deleted at slice V11.
|
||||
/// </summary>
|
||||
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);
|
||||
/// <summary>
|
||||
/// Campaign V slice V6j: terrain's RHI arm. No GL context, no linked
|
||||
/// program — the pipeline compiles <c>terrain_modern</c> 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 (<c>CreateTerrain</c>) was deleted at slice V11.
|
||||
/// </summary>
|
||||
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<string> 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<uint, SurfaceInfo>());
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private T? AcquireAndPublishIf<T>(
|
||||
bool supported,
|
||||
CompositionAcquisitionScope scope,
|
||||
string name,
|
||||
Func<T> factory,
|
||||
Action<T> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, CaptureStatus> _status =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public FrameScreenshotController(
|
||||
GL gl,
|
||||
string directory,
|
||||
Action<string>? log = null)
|
||||
: this(
|
||||
CreateReader(gl),
|
||||
directory,
|
||||
log)
|
||||
{
|
||||
}
|
||||
|
||||
private static Func<int, int, byte[]> CreateReader(GL gl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
var surface = new GlDefaultFramebufferSurface(gl);
|
||||
return (width, height) => ReadDefaultFramebuffer(surface, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="ReadDefaultFramebuffer(IDefaultFramebufferSurface, int, int)"/>.
|
||||
/// </summary>
|
||||
internal static byte[] ReadDefaultFramebuffer(GL gl, int width, int height)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
return ReadDefaultFramebuffer(
|
||||
new GlDefaultFramebufferSurface(gl),
|
||||
width,
|
||||
height);
|
||||
}
|
||||
|
||||
internal FrameScreenshotController(
|
||||
Func<int, int, byte[]> readRgba,
|
||||
string directory,
|
||||
|
|
@ -186,7 +151,7 @@ internal sealed class FrameScreenshotController
|
|||
/// <summary>
|
||||
/// Blits the whole colour buffer from the bound read framebuffer to the
|
||||
/// bound draw framebuffer with <c>GL_NEAREST</c> and identical rectangles
|
||||
/// — the multisample resolve.
|
||||
/// — the multisample resolve.
|
||||
/// </summary>
|
||||
void BlitColorNearest(int width, int height);
|
||||
|
||||
|
|
@ -194,7 +159,7 @@ internal sealed class FrameScreenshotController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the default framebuffer — framebuffer name 0, the backbuffer —
|
||||
/// Reads the default framebuffer — framebuffer name 0, the backbuffer —
|
||||
/// and nothing else.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
|
|
@ -204,13 +169,13 @@ internal sealed class FrameScreenshotController
|
|||
/// offscreen target the previous renderer left bound. The frame this runs in
|
||||
/// draws several: <c>PrivateEntityViewportRenderer</c> 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This was latent for as long as something else rebound framebuffer 0 often
|
||||
/// enough to mask it (before Campaign V slice V4c, GL <c>BeginPass</c> 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.
|
||||
/// </para>
|
||||
|
|
@ -218,15 +183,15 @@ internal sealed class FrameScreenshotController
|
|||
/// <b>Multisampling.</b> The window is created with the quality preset's
|
||||
/// MSAA sample count, so the default framebuffer is normally 4x multisampled,
|
||||
/// and <c>glReadPixels</c> against a multisampled read framebuffer is
|
||||
/// <i>undefined</i> per the GL spec (GL 4.6 §18.2: an INVALID_OPERATION is
|
||||
/// <i>undefined</i> 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 <c>GL_NEAREST</c>, which is the defined resolve — and reads that.
|
||||
/// and <c>GL_NEAREST</c>, 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.
|
||||
/// </para>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string> 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<string> Extensions,
|
||||
GraphicalFunctionProbeResult FunctionProbe,
|
||||
IReadOnlyList<string> SupportFailures)
|
||||
{
|
||||
internal bool IsSupported => SupportFailures.Count == 0;
|
||||
}
|
||||
|
||||
internal static class GraphicalCapabilityRequirements
|
||||
{
|
||||
internal static IReadOnlyList<string> Evaluate(
|
||||
GraphicalCapabilityRecord capabilities)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(capabilities);
|
||||
var failures = new List<string>();
|
||||
|
||||
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<string> 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<string> 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]<int>)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";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,513 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string>();
|
||||
|
||||
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<string> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,3 +187,58 @@ internal static class GraphicalWindowBackendConfigurator
|
|||
DefaultPathResolver.BaseDirectoryResolver);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the GLFW platform actually selected at runtime (as opposed to the one
|
||||
/// requested — GLFW's <c>Automatic</c> hint can resolve to either X11 or
|
||||
/// Wayland). Moved here from the deleted (Campaign V slice V11) raw-GL
|
||||
/// <c>GraphicalCapabilityRecord.cs</c>: <see cref="VulkanGraphicsContext"/>
|
||||
/// depends on this for its own capability report, so it survived the GL arm
|
||||
/// that used to sit alongside it.
|
||||
/// </summary>
|
||||
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]<int>)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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
namespace AcDream.App;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V5: which rendering backend the graphical host starts.
|
||||
///
|
||||
/// Slice V10 flipped the default. <see cref="Vulkan"/> is what an unset
|
||||
/// <c>ACDREAM_RENDER_BACKEND</c> now selects; <see cref="Gl"/> remains reachable
|
||||
/// for one slice by setting that variable to <c>gl</c>, and slice V11 deletes it
|
||||
/// along with the escape hatch.
|
||||
///
|
||||
/// This enum is public only because <see cref="RuntimeOptions"/> is public and
|
||||
/// exposes it as a property. The backend-neutral
|
||||
/// <c>AcDream.App.Rendering.Gpu.GpuBackendKind</c> 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.
|
||||
/// </summary>
|
||||
public enum RenderBackendKind
|
||||
{
|
||||
/// <summary>
|
||||
/// OpenGL 4.3 core + bindless + MDI. The escape hatch after slice V10,
|
||||
/// reachable only by <c>ACDREAM_RENDER_BACKEND=gl</c>, deleted at V11.
|
||||
/// </summary>
|
||||
Gl,
|
||||
|
||||
/// <summary>Vulkan 1.3 core. The default backend as of Campaign V slice V10.</summary>
|
||||
Vulkan,
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily removes bindless residency around texture-state mutation while
|
||||
/// retaining the obligation to restore the pair across failed mutation or
|
||||
/// failed reacquisition attempts.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class BindlessTexturePair(
|
||||
uint firstTexture,
|
||||
uint secondTexture,
|
||||
Func<uint, ulong> acquire,
|
||||
Action<ulong> 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<Exception>? 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame container + uploader for the SHARED clip data: the binding=2 mesh
|
||||
/// SSBO (one <c>CellClip</c> per slot, slot 0 reserved no-clip) and the binding=2
|
||||
/// terrain UBO (the single OutsideView region). See the file header for the exact
|
||||
/// Per-frame container for the SHARED clip data: the binding=2 mesh SSBO (one
|
||||
/// <c>CellClip</c> per slot, slot 0 reserved no-clip) and the binding=2 terrain
|
||||
/// UBO (the single OutsideView region). See the file header for the exact
|
||||
/// std430 / std140 byte layout. Per-instance slot buffers (binding=3) are owned by
|
||||
/// each renderer, not here.
|
||||
/// </summary>
|
||||
|
|
@ -61,13 +63,12 @@ public sealed class ClipFrame : IDisposable
|
|||
/// coincidence of the 16-byte vec4 rule, but a DIFFERENT layout family.</summary>
|
||||
public const int TerrainUboBytes = 16 + MaxPlanes * 16; // 144
|
||||
|
||||
/// <summary>SSBO binding index for the shared per-cell clip regions
|
||||
/// (mesh_modern.vert binding=2).</summary>
|
||||
public const uint MeshClipSsboBinding = 2;
|
||||
|
||||
/// <summary>UBO binding index for the terrain OutsideView clip region
|
||||
/// (terrain_modern.vert binding=2). UBO namespace — distinct from the SSBO
|
||||
/// binding=2 above.</summary>
|
||||
/// (terrain_modern.vert binding=2). Read directly by both the RHI world-pass
|
||||
/// section binder (<c>WorldFrameSectionBinding.BindTerrainClip</c>) and
|
||||
/// <c>PortalDepthMaskRenderer</c>, so unlike the mesh SSBO binding (which the
|
||||
/// RHI arm addresses through its own <c>GpuBindingModel.StorageClipRegions</c>
|
||||
/// instead) this one is still genuinely shared.</summary>
|
||||
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<RegionBuffer> _regionBuffers =
|
||||
new(FrameSlotCount);
|
||||
private readonly ClipFrameResourceRing<TerrainBufferArena> _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;
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>RhiWorldPassSurface.Publish</c>), which lives until its frame retires,
|
||||
/// so there is no persistent dynamic-buffer pool left to size.
|
||||
/// </summary>
|
||||
internal int DynamicBufferSetCount => 0;
|
||||
|
||||
private ClipFrame(byte[] regionBytes, int slotCount)
|
||||
{
|
||||
|
|
@ -156,9 +116,10 @@ public sealed class ClipFrame : IDisposable
|
|||
/// <summary>
|
||||
/// 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 <c>_clipFrame</c> in
|
||||
/// GameWindow is reset + re-packed every frame by <see cref="ClipFrameAssembler"/>,
|
||||
/// then uploaded through one SSBO and one terrain arena per fenced frame slot.
|
||||
/// frame. The single long-lived <c>_clipFrame</c> in GameWindow is reset +
|
||||
/// re-packed every frame by <see cref="ClipFrameAssembler"/>, then published
|
||||
/// through one frame-ring allocation per section (see
|
||||
/// <c>RhiWorldPassSurface.PrepareClipFrame</c>).
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
|
|
@ -175,35 +136,18 @@ public sealed class ClipFrame : IDisposable
|
|||
Array.Clear(_terrainBytes);
|
||||
}
|
||||
|
||||
/// <summary>The shared mesh-clip SSBO id, or 0 before the first
|
||||
/// <see cref="UploadRegions"/>. Renderers may bind this directly if they don't
|
||||
/// receive it via a parameter; <see cref="UploadRegions"/> already binds it to
|
||||
/// <see cref="MeshClipSsboBinding"/>.</summary>
|
||||
public uint RegionSsbo => _regionSsbo;
|
||||
|
||||
/// <summary>The terrain-clip UBO id, or 0 before the first
|
||||
/// <see cref="UploadTerrainClip"/>. The buffer alone does not identify the
|
||||
/// active range; consumers should use <see cref="TerrainBinding"/>.</summary>
|
||||
public uint TerrainUbo => _terrainUbo;
|
||||
|
||||
/// <summary>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.</summary>
|
||||
public TerrainClipBufferBinding TerrainBinding => _terrainBinding;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>BeginFrame</c>, 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -272,266 +216,14 @@ public sealed class ClipFrame : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility entry point for one-region/one-terrain callers. Complex
|
||||
/// PView frames should reserve their complete terrain sequence, upload the
|
||||
/// regions once, then call <see cref="UploadTerrainClip"/> 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 <see cref="ClipFrame"/>
|
||||
/// kept as <see cref="IDisposable"/>) so <c>GameWindowLifetime</c>'s ordered
|
||||
/// shutdown doesn't need a special case for this one resource.
|
||||
/// </summary>
|
||||
public unsafe void UploadShared(GL gl)
|
||||
{
|
||||
ReserveTerrainUploads(gl, 1);
|
||||
UploadRegions(gl);
|
||||
UploadTerrainClip(gl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates the current frame slot's terrain arena before any draw uses it.
|
||||
/// The arena has one alignment-safe range per subsequent
|
||||
/// <see cref="UploadTerrainClip"/> call, so later slices never overwrite
|
||||
/// uniform data referenced by earlier GPU commands.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Uploads and binds the immutable clip-region table once for the
|
||||
/// assembled frame.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>Uploads the current terrain clip bytes into the next unique
|
||||
/// aligned arena record and installs that range at UBO binding 2.</summary>
|
||||
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
|
|||
/// <summary>
|
||||
/// The packed std430 region table for slots 0..<see cref="SlotCount"/>-1.
|
||||
///
|
||||
/// <para>Campaign V slice V6j: the GL arm hands these to
|
||||
/// <c>glBufferSubData</c> 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.</para>
|
||||
/// <para>The raw-GL arm this used to feed via <c>glBufferSubData</c> against
|
||||
/// a renderer-owned SSBO was deleted at Campaign V slice V11; the RHI arm
|
||||
/// (<c>RhiWorldPassSurface.PrepareClipFrame</c>) now copies these bytes
|
||||
/// straight into a frame ring slice and publishes the range. The packing
|
||||
/// above stays backend-neutral regardless.</para>
|
||||
/// </summary>
|
||||
internal ReadOnlySpan<byte> RegionBytes =>
|
||||
_regionBytes.AsSpan(0, _slotCount * CellClipStrideBytes);
|
||||
|
|
@ -594,230 +286,3 @@ public sealed class ClipFrame : IDisposable
|
|||
/// <summary>Test seam: the packed std140 terrain UBO bytes.</summary>
|
||||
internal ReadOnlySpan<byte> TerrainBytesForTest => TerrainBytes;
|
||||
}
|
||||
|
||||
/// <summary>A single std140 terrain-clip record within a frame-slot UBO arena.</summary>
|
||||
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
|
||||
{
|
||||
/// <summary>Publishes capacity only after BufferData succeeds, and before
|
||||
/// any later upload/bind operation can throw.</summary>
|
||||
public static void Resize(
|
||||
ref int publishedCapacityBytes,
|
||||
int targetCapacityBytes,
|
||||
Action<int, int> allocate)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(publishedCapacityBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(targetCapacityBytes, 1);
|
||||
ArgumentNullException.ThrowIfNull(allocate);
|
||||
|
||||
int previousCapacityBytes = publishedCapacityBytes;
|
||||
allocate(previousCapacityBytes, targetCapacityBytes);
|
||||
publishedCapacityBytes = targetCapacityBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fixed-cardinality resource ownership indexed by the same frame
|
||||
/// slots protected by <see cref="GpuFrameFlightController"/>.</summary>
|
||||
internal sealed class ClipFrameResourceRing<T>(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<T> 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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pure sequencing state for one ClipFrame render submission.</summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Exception>? 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
using AcDream.App.Diagnostics;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Production render-transaction measurement adapter. CPU frame boundaries
|
||||
/// remain owned by <see cref="FrameProfiler"/> while this adapter places the
|
||||
/// GPU query around only the GL-producing render phases.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -12,15 +12,9 @@ internal interface IGameRenderResourceLifetime
|
|||
internal sealed class GameRenderResourceLifetime : IGameRenderResourceLifetime
|
||||
{
|
||||
private readonly OwnedResourceSlot<TerrainAtlas> _terrainAtlas = new();
|
||||
private readonly OwnedResourceSlot<Shader> _skyShader = new();
|
||||
|
||||
public TerrainAtlas AcquireTerrainAtlas(Func<TerrainAtlas> factory) =>
|
||||
_terrainAtlas.Acquire(factory);
|
||||
|
||||
public Shader AcquireSkyShader(Func<Shader> factory) =>
|
||||
_skyShader.Acquire(factory);
|
||||
|
||||
public void ReleaseTerrainAtlas() => _terrainAtlas.Release();
|
||||
|
||||
public void ReleaseSkyShader() => _skyShader.Release();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
/// <summary>Phase N.5b: terrain_modern.vert/.frag program. Owned by
|
||||
/// <see cref="_terrain"/> at draw time but allocated + disposed here.</summary>
|
||||
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;
|
||||
/// <summary>Phase N.4+: WB-backed rendering pipeline adapter. Always non-null
|
||||
/// after <c>OnLoad</c> completes (modern path is mandatory as of N.5).</summary>
|
||||
|
|
@ -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;
|
||||
/// <summary>Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
|
||||
/// support. Required at startup — missing bindless throws
|
||||
/// <see cref="NotSupportedException"/> in <c>OnLoad</c>.</summary>
|
||||
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<int>(1280, 720),
|
||||
Title = "acdream — Vulkan",
|
||||
VSync = startupPacing.UseVSync,
|
||||
}
|
||||
: WindowOptions.Default with
|
||||
{
|
||||
Size = new Vector2D<int>(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<int>(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 :
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: how the UI probe reads a completed frame.
|
||||
///
|
||||
/// <para><see cref="FrameScreenshotController"/> applies the bottom-up flip
|
||||
/// <c>glReadPixels</c> needs, so GL hands it the raw read and Vulkan — whose
|
||||
/// <see cref="IGpuDevice.CaptureBackbuffer"/> is documented top-left-origin —
|
||||
/// pre-flips so the two cancel. Routing GL through the same
|
||||
/// <c>CaptureBackbuffer</c> seam would double-flip, which is exactly the kind
|
||||
/// of "usually right" instrument §5.5 of the campaign plan spent three
|
||||
/// slices removing.</para>
|
||||
/// How the UI probe reads a completed frame.
|
||||
/// <see cref="IGpuDevice.CaptureBackbuffer"/> is documented top-left-origin,
|
||||
/// so <see cref="FrameScreenshotController"/> 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.
|
||||
/// </summary>
|
||||
private static Func<int, int, byte[]> 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);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanGraphicsContext.Acquire"/>,
|
||||
/// throwing <see cref="NotSupportedException"/> into the same exit-code-4
|
||||
/// contract <c>Program.cs</c> publishes for GL.
|
||||
/// contract <c>Program.cs</c> publishes. The raw-GL fork this used to make
|
||||
/// was deleted at Campaign V slice V11.
|
||||
/// </summary>
|
||||
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<GameWindowGraphics, IInputContext> 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<GameWindowGraphics, IInputContext>,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
]));
|
||||
|
|
|
|||
|
|
@ -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<Exception> failures)
|
||||
: base(message, failures)
|
||||
{
|
||||
_cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup));
|
||||
}
|
||||
|
||||
public bool IsCleanupComplete => _cleanup.IsCleanupComplete;
|
||||
|
||||
public void RetryCleanup() => _cleanup.RetryCleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class GlConstructionCleanupLedger : IDisposable
|
||||
{
|
||||
private readonly List<IRetryableResourceCleanup> _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<Exception>? 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
using System.Runtime.ExceptionServices;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<T>(GL gl, string context, Func<T> 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<uint> create,
|
||||
Action<uint> 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<uint> create,
|
||||
Action postcondition,
|
||||
Action<uint> 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<uint> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class GlTextureConstructionTransaction(IGlTextureNameApi api)
|
||||
: IRetryableResourceCleanup
|
||||
{
|
||||
private readonly List<uint> _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<Exception>? 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();
|
||||
}
|
||||
|
|
@ -1,313 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// The narrow slice of GL that <see cref="GlAmbientCapabilityState"/> 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 <c>ITextRenderGlStateApi</c>, which
|
||||
/// <c>TextRenderer</c> 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.
|
||||
/// </summary>
|
||||
internal interface IGlAmbientStateApi
|
||||
{
|
||||
bool IsEnabled(EnableCap capability);
|
||||
|
||||
int GetInteger(GetPName parameter);
|
||||
|
||||
bool GetBoolean(GetPName parameter);
|
||||
|
||||
/// <summary>The four colour-mask channels, in RGBA order.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every ambient GL capability/binding a <see cref="GlGpuPipeline"/> 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks which texture-table slots changed since the last flush and gives them
|
||||
/// back as maximal runs of <i>consecutive</i> dirty slots.
|
||||
///
|
||||
/// <para><b>Why runs, and not one merged range.</b> 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
|
||||
/// <c>GL_MAP_INVALIDATE_RANGE_BIT</c>, which lets the driver discard the whole
|
||||
/// range's contents while it is mapped, and <c>GL_MAP_UNSYNCHRONIZED_BIT</c>,
|
||||
/// 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.</para>
|
||||
///
|
||||
/// <para><b>Why a dirty slot is safe to write unsynchronized.</b> Two producers
|
||||
/// touch a slot: <c>GlGpuDevice.RegisterTexture</c>, which writes a slot fresh
|
||||
/// from <see cref="GlTextureSlotAllocator"/> and therefore one no batch has ever
|
||||
/// indexed; and <c>ReleaseTextureSlot</c>'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
|
||||
/// <c>GL_MAP_UNSYNCHRONIZED_BIT</c> makes.</para>
|
||||
///
|
||||
/// <para>GL-free by design, like <see cref="GlRingBufferState"/> and
|
||||
/// <see cref="GlTextureSlotAllocator"/>, 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.</para>
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the lowest remaining run of consecutive dirty slots, clearing it,
|
||||
/// and reports its first slot and length. Returns <c>false</c> once none
|
||||
/// remain, so a caller drains with a <c>while</c> loop.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>One vertex attribute's GL shape: component count, element type, and whether integer values normalize to [0,1]/[-1,1].</summary>
|
||||
/// <summary>
|
||||
/// How one vertex attribute reaches GL. <paramref name="Integer"/> selects
|
||||
/// <c>glVertexAttribIPointer</c> over <c>glVertexAttribPointer</c>: GL requires
|
||||
/// the integer entry point for an integer shader input (<c>uvec4</c> and friends)
|
||||
/// and leaves the value undefined otherwise.
|
||||
/// </summary>
|
||||
internal readonly record struct GlVertexAttributeShape(
|
||||
int ComponentCount,
|
||||
VertexAttribPointerType Type,
|
||||
bool Normalized,
|
||||
bool Integer = false);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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}."),
|
||||
};
|
||||
|
||||
/// <summary>Slice V6l: the stencil comparison, which shares GL's depth-function enum values.</summary>
|
||||
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}."),
|
||||
};
|
||||
|
||||
/// <summary>Slice V6l: what a stencil outcome does to the stored value.</summary>
|
||||
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}."),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// A plain GL buffer object. Deliberately not one of the existing
|
||||
/// <c>ManagedGL*</c> wrappers: those implement Chorizite's <c>IVertexBuffer</c>/
|
||||
/// <c>IIndexBuffer</c> (generic-over-<c>IVertex</c>, single-usage) and are
|
||||
/// constructed against <see cref="OpenGLGraphicsDevice"/>, which the campaign
|
||||
/// explicitly sheds — <see cref="GlGpuDevice"/> is a fresh root. One GL buffer
|
||||
/// object already serves every <see cref="GpuBufferUsage"/> combination (the
|
||||
/// usage only matters at bind time), so a single small class covers the whole
|
||||
/// <see cref="IGpuBuffer"/> surface without forking per usage.
|
||||
///
|
||||
/// Allocated once at the description's size via <c>glBufferData</c> 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:
|
||||
/// <see cref="Upload"/> is an ordinary synchronized <c>glBufferSubData</c>, used
|
||||
/// for one-off and streaming writes the driver must order for us (the mesh
|
||||
/// arena, texture staging); <see cref="WriteRangeUnsynchronized"/> 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 <c>glBufferData</c> usage hint follows
|
||||
/// <see cref="GpuBufferDescription.Residency"/>:
|
||||
/// <see cref="GpuMemoryResidency.DeviceLocal"/> is written rarely and read by
|
||||
/// many draws, so it takes <c>StaticDraw</c>; the host-writable rings and
|
||||
/// tables are rewritten every frame and take <c>DynamicDraw</c>. 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.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>The physical GL buffer name. For bind calls issued by <see cref="GlGpuPassEncoder"/>.</summary>
|
||||
internal uint GlName => _name;
|
||||
|
||||
private static BufferUsageARB UsageHintFor(GpuMemoryResidency residency) =>
|
||||
residency == GpuMemoryResidency.DeviceLocal
|
||||
? BufferUsageARB.StaticDraw
|
||||
: BufferUsageARB.DynamicDraw;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the physical buffer on the calling thread instead of deferring it
|
||||
/// through the device's retirement queue the way <see cref="Dispose"/> does.
|
||||
///
|
||||
/// Campaign V slice V4b: the mesh arena (<c>GlobalMeshBuffer</c>) already gates
|
||||
/// every arena delete behind its own <c>GpuRetirementLedger</c> and decrements
|
||||
/// its <c>MaximumPhysicalArenaBytes</c> 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 <see cref="Dispose"/>) is a no-op.
|
||||
/// </summary>
|
||||
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<byte> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes <paramref name="data"/> at <paramref name="offsetBytes"/> through
|
||||
/// <c>glMapBufferRange</c> with
|
||||
/// <c>GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT</c>
|
||||
/// — the canonical GL upload-ring idiom, and the only write path the
|
||||
/// per-frame ring uses.
|
||||
///
|
||||
/// <para><b>Why not <see cref="Upload"/>.</b> A partial <c>glBufferSubData</c>
|
||||
/// 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.</para>
|
||||
///
|
||||
/// <para><b>The precondition is the caller's to prove.</b>
|
||||
/// <c>GL_MAP_UNSYNCHRONIZED_BIT</c> means the driver inserts no wait, so the
|
||||
/// caller asserts that no GL command already submitted and not yet complete
|
||||
/// reads any byte of <c>[offsetBytes, offsetBytes + data.Length)</c>. The ring
|
||||
/// has that invariant on both axes: within a frame the allocation cursor only
|
||||
/// moves forward and <see cref="GlRingBufferState"/> refuses a write below the
|
||||
/// flushed high-water mark, and across frames a slot is only rewritten after
|
||||
/// <c>GpuFrameFlightController.BeginFrame</c> has waited on the fence for the
|
||||
/// last frame that used it. <c>GL_MAP_INVALIDATE_RANGE_BIT</c> is likewise
|
||||
/// only sound because every byte of the mapped range is then written.</para>
|
||||
///
|
||||
/// <para>A <c>false</c> return from <c>glUnmapBuffer</c> 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.</para>
|
||||
/// </summary>
|
||||
internal unsafe void WriteRangeUnsynchronized(long offsetBytes, ReadOnlySpan<byte> 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<byte>(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<byte> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// OpenGL 4.3 implementation of <see cref="IGpuDevice"/> — Campaign V slice V1.
|
||||
/// See <c>docs/plans/2026-07-27-vulkan-campaign.md</c> §3 for the pinned
|
||||
/// contract this backend fills and §5 for what this slice covers.
|
||||
///
|
||||
/// Deliberately NOT derived from Chorizite's <c>BaseGraphicsDevice</c>/
|
||||
/// <see cref="OpenGLGraphicsDevice"/> — this is a fresh root, which is one of
|
||||
/// the things Campaign V sheds. It owns its own <see cref="BindlessSupport"/>
|
||||
/// instance (created in the constructor) rather than sharing the legacy WB
|
||||
/// render path's; both simply wrap the same stateless
|
||||
/// <c>GL_ARB_bindless_texture</c> extension, so two instances coexist safely,
|
||||
/// and it lets this device be constructed the moment a GL context and a
|
||||
/// <see cref="GpuFrameFlightController"/> exist — no dependency on when the
|
||||
/// legacy path happens to detect bindless support during composition.
|
||||
///
|
||||
/// <para><b>Ring capacity.</b> Each flight slot gets one managed staging
|
||||
/// <c>byte[]</c> and one same-sized GL buffer, both fixed at construction
|
||||
/// (default 16 MiB — see <see cref="DefaultRingCapacityBytesPerSlot"/>). 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. <see cref="GlRingBufferState"/> is the pure
|
||||
/// piece that enforces this.</para>
|
||||
///
|
||||
/// <para><b>Flush discipline.</b> Ring bytes and the texture-handle table are
|
||||
/// both flushed immediately before every
|
||||
/// <c>Draw</c>/<c>DrawIndexed</c>/<c>MultiDrawIndexedIndirect</c> — see
|
||||
/// <see cref="FlushBeforeDraw"/> — never at bind time, so a renderer that
|
||||
/// writes after binding still uploads correctly. Both flushes go through
|
||||
/// <see cref="GlGpuBuffer.WriteRangeUnsynchronized"/>, i.e.
|
||||
/// <c>glMapBufferRange</c> with
|
||||
/// <c>GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_INVALIDATE_RANGE_BIT</c>,
|
||||
/// rather than <c>glBufferSubData</c>: 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 <c>glBufferSubData</c> 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 <see cref="GlDirtySlotRuns"/>).</para>
|
||||
/// </summary>
|
||||
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<GpuSamplerDescription, GlGpuSampler> _samplers = [];
|
||||
private readonly List<Action> _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;
|
||||
|
||||
/// <summary>
|
||||
/// GL name of the buffer emulating the global texture table
|
||||
/// (<see cref="GpuBindingModel.StorageTextureTable"/>). Slice V6d:
|
||||
/// <see cref="GlGpuPassEncoder.BindPipeline"/> 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.
|
||||
/// </summary>
|
||||
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<byte> data = byteCount == 0
|
||||
? Span<byte>.Empty
|
||||
: _ringStaging[slotIndex].AsSpan((int)offset, byteCount);
|
||||
return new GpuRingAllocation(_ringBuffers[slotIndex], offset, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="GlGpuBuffer.WriteRangeUnsynchronized"/>). 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains every dirty run of the texture table into its GL buffer. Called by
|
||||
/// <see cref="FlushBeforeDraw"/> for RHI draws, and directly by the still-raw-GL
|
||||
/// world renderers before their own draws — see the V4t remarks on
|
||||
/// <see cref="RegisterWorldTextureHandle"/>.
|
||||
/// </summary>
|
||||
internal void FlushTextureTable()
|
||||
{
|
||||
while (_textureTableDirtySlots.TryTakeNextRun(out uint firstSlot, out uint slotCount))
|
||||
{
|
||||
ReadOnlySpan<byte> 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<ulong, GpuTextureSlot> _worldTextureSlotsByHandle = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="ReleaseWorldTextureHandle"/> retires it, which
|
||||
/// is what lets a cache call this per draw rather than tracking the slot.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="ReleaseTextureSlot"/>. A handle that was never
|
||||
/// registered is a no-op, so a cache may call this unconditionally on its
|
||||
/// teardown path.
|
||||
/// </summary>
|
||||
internal void ReleaseWorldTextureHandle(ulong residentHandle)
|
||||
{
|
||||
if (residentHandle == 0)
|
||||
return;
|
||||
if (!_worldTextureSlotsByHandle.Remove(residentHandle, out GpuTextureSlot slot))
|
||||
return;
|
||||
ReleaseTextureSlot(slot);
|
||||
}
|
||||
|
||||
/// <summary>Live world-handle registrations. Diagnostics and tests only.</summary>
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// One frame's recording context on the GL backend. Ring allocations are
|
||||
/// forwarded to the device's per-slot <see cref="GlRingBufferState"/>;
|
||||
/// passes are single-level (no nesting, matching every acdream renderer
|
||||
/// today) and enforced here.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Records one pass's draw work. Binding calls translate almost mechanically
|
||||
/// to GL (a storage/uniform binding is <c>glBindBufferRange</c>, an indexed
|
||||
/// draw is <c>glDrawElementsInstancedBaseVertexBaseInstance</c>, and so on);
|
||||
/// the two pieces of real logic are the render-state diff applied on
|
||||
/// <see cref="BindPipeline"/> / the dynamic setters, and the "flush dirty
|
||||
/// ring + texture-table bytes immediately before every draw" discipline
|
||||
/// described on <see cref="GlGpuDevice"/>.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles <see cref="GpuPipelineDescription.Shaders"/> through the existing
|
||||
/// <see cref="ShaderProgramConstruction"/> (the same compiler every other GL
|
||||
/// shader in the codebase uses — this is not a second compiler) and owns one
|
||||
/// VAO shaped by <see cref="GpuPipelineDescription.VertexLayout"/>.
|
||||
///
|
||||
/// 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 <see cref="GlGpuPassEncoder.BindVertexBuffer"/>
|
||||
/// 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 <c>glVertexAttribPointer</c>.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Applies a <see cref="GpuPushConstants"/> value to the currently bound GL
|
||||
/// program by uniform name (<see cref="GlPushConstantUniformNames"/>),
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class GlGpuPushConstantBinder
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Dictionary<uint, ProgramLocations> _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}");
|
||||
}
|
||||
|
||||
/// <summary>Drops cached locations for a deleted program. Call before its GL name is reused.</summary>
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// An offscreen colour(+depth) FBO. The colour attachment is a
|
||||
/// <see cref="GlGpuTexture"/> so it can be registered into the texture table
|
||||
/// after the pass; the depth/stencil attachment (when requested) is a plain
|
||||
/// renderbuffer, mirroring <c>ManagedGLFramebuffer</c>'s approach — nothing
|
||||
/// ever samples depth for the targets this slice's contract describes
|
||||
/// (paperdoll, creature appraisal, portal masking).
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// One GL sampler object. <see cref="GlGpuDevice.CreateSampler"/>
|
||||
/// de-duplicates by <see cref="GpuSamplerDescription"/> value, so a given
|
||||
/// wrap/filter combination is only ever backed by one physical sampler name —
|
||||
/// the same pattern <c>SamplerCache</c> already uses for its two fixed
|
||||
/// samplers, generalized to the full description space the RHI exposes.
|
||||
/// </summary>
|
||||
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}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// A GL texture (2D or 2D array) allocated with immutable storage
|
||||
/// (<c>glTexStorage2D/3D</c>) so every mip level is defined the moment the
|
||||
/// object is created, regardless of upload order — mirroring
|
||||
/// <c>ManagedGLTextureArray</c>'s allocation strategy without inheriting its
|
||||
/// Chorizite/<see cref="OpenGLGraphicsDevice"/> 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 <see cref="IGpuSampler"/> a draw later
|
||||
/// binds. The bound sampler object overrides actual filtering at draw time
|
||||
/// (same override rule <c>SamplerCache</c> already documents), so this
|
||||
/// default never affects the rendered image.
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>The physical GL texture name. Used by <see cref="GlGpuDevice.RegisterTexture"/> to obtain a bindless handle.</summary>
|
||||
internal uint GlName => _name;
|
||||
|
||||
internal GLEnum Target => _target;
|
||||
|
||||
public void Upload(int mipLevel, int layer, ReadOnlySpan<byte> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// GL format triple for a <see cref="GpuTextureFormat"/>: the sized internal
|
||||
/// format used at allocation (<c>glTexStorage2D/3D</c>), the upload
|
||||
/// format/type for uncompressed uploads, and whether uploads go through
|
||||
/// <c>glCompressedTexSubImage2D/3D</c> instead.
|
||||
///
|
||||
/// <see cref="GpuTextureFormat"/> is acdream's own enum, not the Chorizite
|
||||
/// <c>TextureFormat</c> the existing <c>TextureFormatExtensions</c> 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.
|
||||
/// </summary>
|
||||
internal readonly record struct GlTextureFormatInfo(
|
||||
SizedInternalFormat SizedInternalFormat,
|
||||
PixelFormat UploadPixelFormat,
|
||||
PixelType UploadPixelType,
|
||||
bool IsCompressed,
|
||||
int BlockOrTexelBytes,
|
||||
int BlockDimension)
|
||||
{
|
||||
/// <summary>Bytes required for one full, uncompressed mip layer at the given dimensions, or one compressed layer rounded up to whole blocks.</summary>
|
||||
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}."),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Seam over the four GL calls a <c>TimeElapsed</c> timer scope needs, mirroring
|
||||
/// <c>IGpuFenceApi</c>'s role for <see cref="GpuFrameFlightController"/>: it lets
|
||||
/// <see cref="GlGpuTimerPool"/>'s double-buffering and result-promotion logic run
|
||||
/// under a unit test with no live GL context.
|
||||
/// </summary>
|
||||
internal interface IGlTimerQueryApi
|
||||
{
|
||||
uint CreateQuery();
|
||||
void DeleteQuery(uint query);
|
||||
void Begin(uint query);
|
||||
void End();
|
||||
|
||||
/// <summary>Non-blocking: false when the result for <paramref name="query"/> is not yet available (or the query was never begun).</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IGpuTimerPool"/> backed by <c>TimeElapsed</c> queries. Core GL
|
||||
/// only allows one <c>GL_TIME_ELAPSED</c> query active at a time (the
|
||||
/// restriction is per target, not per query object), so scopes must not
|
||||
/// nest — <see cref="BeginScope"/> throws if a previous scope in the same
|
||||
/// pass hasn't been disposed yet, exactly as <c>WbDrawDispatcher</c> 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.
|
||||
/// </summary>
|
||||
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<string, ScopeState> _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; }
|
||||
|
||||
/// <summary>
|
||||
/// Begins (or continues) the named scope. Returns a disposable that ends
|
||||
/// the query; dispose it before beginning another scope in the same pass.
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// The single source of truth mapping each <see cref="GpuPushConstants"/>
|
||||
/// field to the GLSL uniform name the GL backend binds it to — the names
|
||||
/// documented on the fields themselves. <see cref="GlGpuPushConstantBinder"/>
|
||||
/// reads these constants (not string literals of its own) when it calls
|
||||
/// <c>GL.GetUniformLocation</c>, and <see cref="AssertMapsEveryField"/> lets a
|
||||
/// test prove the table cannot silently drop a field that a later slice adds
|
||||
/// to the shared struct.
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// Field name (as declared on <see cref="GpuPushConstants"/>) to GLSL
|
||||
/// uniform name, built from the same constants the binder applies with —
|
||||
/// so the binder and this completeness table can never disagree.
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, string> ByFieldName { get; } =
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
[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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Throws if <see cref="GpuPushConstants"/> 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.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Everything <see cref="GlGpuPipeline"/> bakes plus the handful of fields
|
||||
/// core Vulkan 1.3 (and this backend) makes dynamic per draw.
|
||||
/// <see cref="Program"/> is the GL program name, included so a pipeline
|
||||
/// switch is itself a tracked dimension.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>Which GL state calls are needed to move from the previous snapshot to the new one.</summary>
|
||||
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;
|
||||
|
||||
/// <summary>Every dimension reported changed — used for the first apply after a reset.</summary>
|
||||
internal static GlRenderStateChanges All { get; } =
|
||||
new(true, true, true, true, true, true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure GL-free state cache: given the previously applied
|
||||
/// <see cref="GlRenderStateSnapshot"/> and a newly desired one, reports which
|
||||
/// dimensions actually changed so <see cref="GlGpuPassEncoder"/> only issues
|
||||
/// the GL calls that matter. Starts with no baseline, so the very first
|
||||
/// <see cref="Apply"/> 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).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Discards the cached baseline — the next <see cref="Apply"/> reports every dimension changed.</summary>
|
||||
public void Reset() => _last = null;
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// 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. <see cref="GlGpuFrame"/>'s device owns one instance per flight slot
|
||||
/// and pairs it with a managed staging <c>byte[]</c> and a real GL buffer; the
|
||||
/// staging array receives every <see cref="GpuRingAllocation.Data"/> write, and
|
||||
/// immediately before each <c>Draw</c>/<c>DrawIndexed</c>/
|
||||
/// <c>MultiDrawIndexedIndirect</c> the device writes exactly the dirty range
|
||||
/// into the GL buffer through
|
||||
/// <see cref="GlGpuBuffer.WriteRangeUnsynchronized"/> and calls
|
||||
/// <see cref="TakeDirtyRange"/> to reset the watermark. The allocation cursor
|
||||
/// itself only resets at <see cref="Reset"/> (once per <c>BeginFrame</c>) —
|
||||
/// 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.
|
||||
///
|
||||
/// <para><b>The forward-only invariant is load-bearing, not incidental.</b> The
|
||||
/// device's per-flush write is mapped with <c>GL_MAP_UNSYNCHRONIZED_BIT</c>,
|
||||
/// 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 <see cref="Allocate"/> 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,
|
||||
/// <see cref="MarkDirty"/> refuses a write below <see cref="FlushedEndBytes"/>:
|
||||
/// 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 <c>GpuFrameFlightController</c>, which waits on a
|
||||
/// slot's fence in <c>BeginFrame</c> before this state is <see cref="Reset"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Bytes handed out since the last <see cref="Reset"/>.</summary>
|
||||
public uint AllocatedBytes => _cursor;
|
||||
|
||||
/// <summary>
|
||||
/// The exclusive end of the bytes already handed to the GPU this frame.
|
||||
/// Every subsequent write must start at or above it.
|
||||
/// </summary>
|
||||
public int FlushedEndBytes => _flushedEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Reserves <paramref name="byteCount"/> bytes aligned to
|
||||
/// <paramref name="alignmentBytes"/>, 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Starts a new frame: rewinds the allocation cursor. Dirty state is untouched — a flush always runs before this is called.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
_cursor = 0;
|
||||
_dirtyStart = -1;
|
||||
_dirtyEnd = -1;
|
||||
_flushedEnd = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the byte range written since the last flush (start, length),
|
||||
/// or (0, 0) when nothing is dirty, and clears the watermark while
|
||||
/// advancing <see cref="FlushedEndBytes"/> over it.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Records that <c>[start, end)</c> was written. Internal rather than
|
||||
/// private for the same reason <see cref="AlignUp"/> is: the forward-only
|
||||
/// guard below is unreachable through <see cref="Allocate"/> by
|
||||
/// construction, so proving it fires at all needs a direct call.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
/// <summary>
|
||||
/// Pure bump-plus-free-list allocator for the GL texture table (the storage
|
||||
/// buffer of bindless handles at <see cref="GpuBindingModel.StorageTextureTable"/>).
|
||||
///
|
||||
/// GL-free by design: it knows nothing about bindless handles, retirement
|
||||
/// queues, or the SSBO itself. <see cref="GlGpuDevice.RegisterTexture"/> calls
|
||||
/// <see cref="Allocate"/> to get a slot to write a handle into;
|
||||
/// <see cref="GlGpuDevice.ReleaseTextureSlot"/> defers the call to
|
||||
/// <see cref="Release"/> through the device's <c>IGpuResourceRetirementQueue</c>
|
||||
/// so a freed slot is never handed back out while a submitted frame could
|
||||
/// still be reading the old handle at that index.
|
||||
/// </summary>
|
||||
internal sealed class GlTextureSlotAllocator
|
||||
{
|
||||
private readonly uint _capacity;
|
||||
private readonly Stack<uint> _freeList = new();
|
||||
private uint _nextBumpSlot;
|
||||
|
||||
public GlTextureSlotAllocator(uint capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfZero(capacity);
|
||||
_capacity = capacity;
|
||||
}
|
||||
|
||||
/// <summary>Slots currently handed out (bumped or reused) and not yet released.</summary>
|
||||
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++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,6 @@ internal enum GpuBackendKind
|
|||
/// <summary>Test double — records calls, owns no driver objects.</summary>
|
||||
Recording,
|
||||
|
||||
/// <summary>OpenGL 4.3 + bindless/MDI. Deleted at Campaign V slice V11.</summary>
|
||||
OpenGl,
|
||||
|
||||
/// <summary>Vulkan 1.3 core.</summary>
|
||||
Vulkan,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -185,12 +185,11 @@ public sealed unsafe partial class ParticleRenderer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when mesh particles can be submitted at all. The GL arm answers with
|
||||
/// its second <c>Shader</c>, 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
|
||||
/// <c>Shader</c> answer was deleted at Campaign V slice V11; only the RHI
|
||||
/// pipelines remain, built exactly when a shared mesh arena exists.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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.</para>
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// <para>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
|
||||
/// <c>Rendering/Shaders/portal_depth.{vert,frag}</c> from committed SPIR-V,
|
||||
/// and its per-frame fan vertices come from the frame ring instead.</para>
|
||||
/// </summary>
|
||||
public sealed partial class PortalDepthMaskRenderer : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The GL arm's inline program. Campaign V slice V6l added a SECOND arm that
|
||||
/// compiles <c>Rendering/Shaders/portal_depth.{vert,frag}</c> — the same body,
|
||||
/// with its loose uniforms rehomed onto the shared push block and the clip
|
||||
/// planes onto the <c>TerrainClip</c> 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
|
||||
/// <c>ClipFrame</c> binds there globally on GL. The two are kept in step by
|
||||
/// <c>PortalDepthShaderParityTests</c>, and this one is deleted at V11.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Shared by both arms: the largest fan <see cref="DrawDepthFan"/> accepts.</summary>
|
||||
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;
|
||||
}
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal long DynamicBufferCapacityBytes => 0;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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<float> 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
/// <para><b>Campaign V slice V6m.</b> 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)
|
||||
/// <see cref="WbDrawDispatcher"/>. What forked is where the draw is recorded:
|
||||
/// GL sets ambient capability state under a <see cref="GLStateScope"/> and draws
|
||||
/// into whatever framebuffer is bound, while the RHI arm opens a pass of its own
|
||||
/// against the backbuffer and publishes it on <see cref="IWorldPassScope"/> 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
|
||||
/// <see cref="WbDrawDispatcher"/>. The RHI arm opens a pass of its own against
|
||||
/// the backbuffer and publishes it on <see cref="IWorldPassScope"/> for the
|
||||
/// span of the draw, exactly as the two offscreen viewports do
|
||||
/// (<see cref="PrivateEntityViewportRenderer"/>, slice V6l).</para>
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The colour the RHI arm's pass loads with, and the one value that makes
|
||||
/// its <c>Clear</c> load-op equivalent to GL's depth-only clear.
|
||||
|
|
@ -74,22 +64,17 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
|
||||
private static readonly HashSet<uint> AnimatedIds = new() { SyntheticEntityId };
|
||||
|
||||
/// <summary>The GL arm's context, or null on a backend that has none.</summary>
|
||||
private readonly GL? _glContext;
|
||||
|
||||
/// <summary>
|
||||
/// The world pass scope, or null on the GL arm.
|
||||
///
|
||||
/// <para><see cref="WbDrawDispatcher"/>'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.</para>
|
||||
/// The world pass scope this presentation's own pass publishes into for
|
||||
/// the span of the draw. <see cref="WbDrawDispatcher"/> 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.
|
||||
/// </summary>
|
||||
private readonly IWorldPassScope? _scope;
|
||||
private readonly IWorldPassScope _scope;
|
||||
|
||||
/// <summary>The frame the RHI arm's pass is opened on; null on the GL arm.</summary>
|
||||
private readonly ICurrentGpuFrameSource? _frames;
|
||||
/// <summary>The frame this presentation's own pass is opened on.</summary>
|
||||
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<string?>? 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.</para>
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="RetailPortalSpaceClearColor"/> 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 <see cref="RetailPortalSpaceClearColor"/> for why
|
||||
/// re-clearing colour is exact) and is published as the scope's for the
|
||||
/// span of the draw.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <c>Target: null</c> means.</para>
|
||||
///
|
||||
/// <para>The DRAW inside the pass is still raw GL: <c>WbDrawDispatcher</c> 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 <see cref="GLStateScope"/> is what restores the previous
|
||||
/// framebuffer, viewport and capability state afterwards, exactly as before.</para>
|
||||
/// <para>The GL arm this renderer used to open an RHI pass and delegate a
|
||||
/// raw-GL <c>WbDrawDispatcher</c> into (through V10, §5.5.6) was deleted at
|
||||
/// Campaign V slice V11: <c>WbDrawDispatcher</c> now records into the pass
|
||||
/// this renderer publishes on both call sites the same way.</para>
|
||||
/// </summary>
|
||||
internal sealed unsafe class PrivateEntityViewportRenderer :
|
||||
internal sealed class PrivateEntityViewportRenderer :
|
||||
IUiViewportRenderer,
|
||||
IDisposable
|
||||
{
|
||||
private const uint PrivateLandblockId = 0u;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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. <c>WbDrawDispatcher</c> 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.
|
||||
///
|
||||
/// <para>Slice V6l. <c>WbDrawDispatcher</c>'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.</para>
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
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 :
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// A GL framebuffer's colour texture samples bottom-up; a Vulkan render
|
||||
/// target's does not. See <see cref="IUiViewportRenderer.TextureIsBottomUp"/>.
|
||||
/// 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 <see cref="IUiViewportRenderer.TextureIsBottomUp"/>.
|
||||
/// </summary>
|
||||
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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="GameWindow.OnLoad"/>, minus
|
||||
/// terrain / sky / physics / streaming.
|
||||
/// </summary>
|
||||
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!;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4a: the studio's own RHI device (mirrors
|
||||
/// <see cref="HostInputCameraComposition"/>'s production one — the studio
|
||||
/// composes its own render stack independently of GameWindow).
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.IGpuDevice GpuDevice { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Drives <see cref="AcDream.App.Rendering.Gpu.IGpuDevice.BeginFrame"/>/<c>IGpuFrame.End</c>
|
||||
/// once per <see cref="BeginFrame"/>/<see cref="EndFrame"/> pair and
|
||||
/// exposes the open frame to <see cref="UiHost"/>'s <see cref="TextRenderer"/>.
|
||||
/// </summary>
|
||||
internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!;
|
||||
|
||||
private ResourceShutdownTransaction? _shutdown;
|
||||
|
||||
internal void BeginFrame() => FrameLifetime.BeginFrame();
|
||||
|
||||
internal void EndFrame() => FrameLifetime.EndFrame();
|
||||
|
||||
/// <summary>Dispose the GL pieces this stack OWNS (everything created in
|
||||
/// <see cref="RenderBootstrap.Create"/>). <see cref="Dats"/> + <see cref="Gl"/> are caller-owned
|
||||
/// and NOT disposed here. Called once at studio teardown.</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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) ─────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Cache of loaded dat fonts keyed by FontDid (0x40000000-range).
|
||||
/// Populated lazily by <see cref="ResolveDatFont"/>. Thread-safe for
|
||||
/// concurrent reads from the studio render loop; writes happen only
|
||||
/// during the first load of each distinct FontDid.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<uint, AcDream.App.UI.UiDatFont?> _fontCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>Pre-seeds <see cref="VitalsDatFont"/> (0x40000000) and
|
||||
/// <see cref="LargeDatFont"/> (0x40000001) from the already-loaded instances
|
||||
/// to avoid a redundant upload on those two ids.</para>
|
||||
/// </summary>
|
||||
public AcDream.App.UI.UiDatFont? ResolveDatFont(uint fontDid)
|
||||
{
|
||||
return _fontCache.GetOrAdd(fontDid, id =>
|
||||
AcDream.App.UI.UiDatFont.Load(Dats, TextureCache, id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-seeds the font cache from the two already-loaded font instances
|
||||
/// (VitalsDatFont = 0x40000000, LargeDatFont = 0x40000001) so that
|
||||
/// <see cref="ResolveDatFont"/> returns them without a redundant GL upload.
|
||||
/// Called once by <see cref="RenderBootstrap.Create"/> after the stack is
|
||||
/// fully constructed.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Options for <see cref="RenderBootstrap.Create"/>.</summary>
|
||||
public sealed record RenderBootstrapOptions(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings Quality,
|
||||
string DiagnosticsDirectory);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the UI Studio's render stack from the production classes,
|
||||
/// in the same order as <see cref="GameWindow.OnLoad"/>.
|
||||
/// </summary>
|
||||
public static class RenderBootstrap
|
||||
{
|
||||
/// <summary>
|
||||
/// Build the studio's render stack. Throws <see cref="NotSupportedException"/>
|
||||
/// (same message as GameWindow) if GL_ARB_bindless_texture or
|
||||
/// GL_ARB_shader_draw_parameters are absent — the modern path is mandatory.
|
||||
/// </summary>
|
||||
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<Wb.WbMeshAdapter>.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<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
{
|
||||
uint mtableId = (uint)setup.DefaultMotionTable;
|
||||
if (mtableId != 0)
|
||||
{
|
||||
var mtable = capturedDats.Get<DatReaderWriter.DBObjs.MotionTable>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,38 @@ namespace AcDream.App.Rendering;
|
|||
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
internal interface IRetryableResourceCleanup
|
||||
{
|
||||
bool IsCleanupComplete { get; }
|
||||
void RetryCleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a resource construction failed and the partial-construction
|
||||
/// rollback it triggered could not fully complete either. Backend-neutral —
|
||||
/// <see cref="ResourceCleanupGroup"/> and <c>ShaderProgramConstruction</c>
|
||||
/// throw it on both the (deleted, Campaign V slice V11) GL construction path
|
||||
/// and the Vulkan one.
|
||||
/// </summary>
|
||||
internal sealed class ResourceConstructionException : AggregateException,
|
||||
IRetryableResourceCleanup
|
||||
{
|
||||
private readonly IRetryableResourceCleanup _cleanup;
|
||||
|
||||
public ResourceConstructionException(
|
||||
string message,
|
||||
IRetryableResourceCleanup cleanup,
|
||||
IEnumerable<Exception> failures)
|
||||
: base(message, failures)
|
||||
{
|
||||
_cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup));
|
||||
}
|
||||
|
||||
public bool IsCleanupComplete => _cleanup.IsCleanupComplete;
|
||||
|
||||
public void RetryCleanup() => _cleanup.RetryCleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>Backend-neutral, despite its former name
|
||||
/// (<c>GlConstructionCleanupLedger</c>, deleted at Campaign V slice V11): it
|
||||
/// walks any exception chain for <see cref="IRetryableResourceCleanup"/> —
|
||||
/// which <see cref="ResourceConstructionException"/> implements on both
|
||||
/// backends — and does not itself touch GL.</para>
|
||||
/// </summary>
|
||||
internal sealed class ResourceConstructionCleanupLedger : IDisposable
|
||||
{
|
||||
private readonly List<IRetryableResourceCleanup> _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<Exception>? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
using System;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Two persistent GL sampler objects (Repeat + ClampToEdge) created once
|
||||
/// per GL context. Renderers <see cref="GL.BindSampler"/> the appropriate
|
||||
/// one to a texture unit instead of mutating per-texture
|
||||
/// <c>GL_TEXTURE_WRAP_S/T</c> 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.
|
||||
///
|
||||
/// <para>
|
||||
/// Ported from
|
||||
/// <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132</c>.
|
||||
/// Filter modes match <see cref="TextureCache"/>'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].
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Lifetime: created once at GL init, disposed with the GL context.
|
||||
/// Anything that binds a sampler MUST unbind it (<c>BindSampler(unit, 0)</c>)
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class SamplerCache : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly ResourceCleanupGroup _resources;
|
||||
|
||||
/// <summary>Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by <see cref="TextureCache"/>.</summary>
|
||||
public uint Wrap { get; }
|
||||
|
||||
/// <summary>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.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// GL wrapper that owns the SceneLighting UBO buffer, updates its
|
||||
/// contents each frame, and keeps it bound at binding=1 so every
|
||||
/// shader sampling <c>uLights[]</c> / <c>uFogColor</c> / etc reads
|
||||
/// consistent data without per-shader re-upload.
|
||||
/// Publishes the SceneLighting UBO each frame so every shader sampling
|
||||
/// <c>uLights[]</c> / <c>uFogColor</c> / etc reads consistent data without
|
||||
/// per-shader re-upload.
|
||||
///
|
||||
/// <para>
|
||||
/// Usage (r12 §13.2 + r13 §12.3):
|
||||
/// <list type="number">
|
||||
/// <item><description>Instantiate once at startup, after the GL context exists.</description></item>
|
||||
/// <item><description>Each frame, after <see cref="LightManager.Tick"/>, call <see cref="Upload"/> with a freshly-built <see cref="SceneLightingUbo"/>.</description></item>
|
||||
/// <item><description>Shader programs that declare <c>layout(std140, binding = 1) uniform SceneLighting { ... }</c> automatically pick up the data.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>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 <see cref="WorldFrameSections.SceneLighting"/>, 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.</para>
|
||||
/// </summary>
|
||||
public sealed unsafe class SceneLightingUboBinding : IDisposable
|
||||
{
|
||||
private readonly GL? _gl;
|
||||
private readonly ICurrentGpuFrameSource? _frames;
|
||||
private readonly WorldFrameSections? _sections;
|
||||
private uint _ubo;
|
||||
private readonly List<uint>[] _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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6j: the RHI arm.
|
||||
///
|
||||
/// <para>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
|
||||
/// <see cref="WorldFrameSections.SceneLighting"/>, and each world renderer
|
||||
/// binds it inside the pass after its own binds (plan §5.5.14 item 2).</para>
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal int DynamicBufferCount => 0;
|
||||
|
||||
internal SceneLightingUboBinding(
|
||||
ICurrentGpuFrameSource frames,
|
||||
WorldFrameSections sections)
|
||||
|
|
@ -71,80 +49,20 @@ public sealed unsafe class SceneLightingUboBinding : IDisposable
|
|||
/// </summary>
|
||||
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<uint> 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++];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push the current frame's UBO contents to the GPU. Cheap (576 bytes)
|
||||
/// so fine to call unconditionally every frame.
|
||||
/// </summary>
|
||||
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<byte>(&data, SceneLightingUbo.SizeInBytes)
|
||||
.CopyTo(allocation.Data);
|
||||
_sections!.SceneLighting = new GpuBufferSection(
|
||||
_sections.SceneLighting = new GpuBufferSection(
|
||||
allocation.Buffer,
|
||||
allocation.OffsetBytes,
|
||||
(uint)SceneLightingUbo.SizeInBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="IDisposable"/>
|
||||
/// reference (composition's acquisition scope) don't need a special case.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (_gl is not null)
|
||||
{
|
||||
foreach (List<uint> buffers in _buffersByFrame)
|
||||
{
|
||||
foreach (uint buffer in buffers)
|
||||
{
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
buffer,
|
||||
SceneLightingUbo.SizeInBytes,
|
||||
"SceneLighting frame UBO disposal");
|
||||
}
|
||||
buffers.Clear();
|
||||
}
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, int> _uniformLocations = new(StringComparer.Ordinal);
|
||||
public uint Program { get; private set; }
|
||||
|
||||
public Shader(GL gl, string vertexPath, string fragmentPath)
|
||||
: this(gl, vertexPath, fragmentPath, includeCommonPreamble: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when
|
||||
/// <paramref name="includeCommonPreamble"/> is true, the text of
|
||||
/// <c>Shaders/common.glsl</c> — sitting alongside <paramref name="vertexPath"/>
|
||||
/// — is spliced into both sources right after their leading
|
||||
/// <c>#version</c>/<c>#extension</c> block. GL has no <c>#include</c>, 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts <paramref name="preamble"/> right after the shader's leading
|
||||
/// <c>#version</c>/<c>#extension</c>/blank-line block. GLSL requires
|
||||
/// <c>#version</c> 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: <c>GlGpuDevice.CreatePipeline</c>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Exception> Release(bool includeProgram)
|
||||
{
|
||||
var failures = new List<Exception>();
|
||||
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<Exception> ReleaseProgramOnly()
|
||||
{
|
||||
var failures = new List<Exception>();
|
||||
Try(
|
||||
() =>
|
||||
{
|
||||
if (Program != 0)
|
||||
{
|
||||
api.DeleteProgram(Program);
|
||||
Program = 0;
|
||||
VertexAttached = false;
|
||||
FragmentAttached = false;
|
||||
}
|
||||
},
|
||||
failures);
|
||||
return failures;
|
||||
}
|
||||
|
||||
public void RetryCleanup()
|
||||
{
|
||||
List<Exception> 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<Exception> 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<Exception> 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<Exception>(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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>GlBindlessHandleTable</c> in the tree.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
|
||||
/// every <c>SkyObject</c> with <c>Properties & 0x04 == 0</c>).
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the current <see cref="SkyParams"/> to its uniform buffer. Called
|
||||
/// immediately before each draw, which is the cadence the per-submesh
|
||||
/// glUniform* calls it replaces already had.
|
||||
/// </summary>
|
||||
private void UploadParams()
|
||||
{
|
||||
GL gl = _gl!;
|
||||
fixed (void* p = &_params)
|
||||
{
|
||||
gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo);
|
||||
gl.BufferSubData(
|
||||
BufferTargetARB.UniformBuffer, 0, (nuint)SkyParams.SizeInBytes, p);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6k: drains the device texture table's dirty runs and
|
||||
/// (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// Mirrors <c>ParticleRenderer.FlushAndBindTextureTable</c>, including its
|
||||
/// reason for running per draw rather than per pass: a submesh partway
|
||||
/// through the sky stack can be the first to ask for a wrap mode.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Find the <see cref="SkyObjectReplaceData"/> 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();
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6e: the CPU mirror of sky.{vert,frag}'s <c>SkyParams</c>
|
||||
|
|
@ -999,14 +709,11 @@ public sealed unsafe partial class SkyRenderer : IDisposable
|
|||
|
||||
private sealed class SubMeshGpu
|
||||
{
|
||||
public uint Vao;
|
||||
public uint Vbo;
|
||||
public uint Ebo;
|
||||
/// <summary>
|
||||
/// 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 <see cref="Vao"/>.
|
||||
/// The raw-GL arm's VAO/VBO/EBO names were deleted at slice V11.
|
||||
/// </summary>
|
||||
public AcDream.App.Rendering.Gpu.IGpuBuffer? VertexBuffer;
|
||||
public AcDream.App.Rendering.Gpu.IGpuBuffer? IndexBuffer;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
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<uint, uint> TerrainTypeToLayer { get; }
|
||||
public int LayerCount { get; }
|
||||
/// <summary>
|
||||
|
|
@ -45,7 +38,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
public IReadOnlyList<float> TilingByLayer { get; }
|
||||
|
||||
// --- Alpha atlas (new in Phase 3c.2) ---
|
||||
public uint GlAlphaTexture { get; }
|
||||
public int AlphaLayerCount { get; }
|
||||
/// <summary>Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries).</summary>
|
||||
public IReadOnlyList<byte> CornerAlphaLayers { get; }
|
||||
|
|
@ -62,77 +54,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
/// <summary>RCode for each RoadMap, parallel to <see cref="RoadAlphaLayers"/>.</summary>
|
||||
public IReadOnlyList<uint> 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;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: the device texture-table slots for the terrain and
|
||||
/// alpha arrays — the backend-neutral replacement for the raw 64-bit
|
||||
/// <c>ARB_bindless_texture</c> handles this used to return. Residency still
|
||||
/// belongs to this atlas (acquired lazily here, released by
|
||||
/// <see cref="Dispose"/>); the device owns only the two table entries.
|
||||
///
|
||||
/// <para>Throws <see cref="InvalidOperationException"/> if the atlas was
|
||||
/// constructed without a <see cref="Wb.BindlessSupport"/> instance.</para>
|
||||
///
|
||||
/// <para><see cref="SetAnisotropic"/> 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.</para>
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the backend-neutral arm. When present, both
|
||||
/// arrays are <see cref="IGpuTexture"/>s the device created and both slots
|
||||
/// were registered at build time, so <see cref="TextureSlots"/> is a field
|
||||
/// read rather than a residency negotiation. <see cref="GlTexture"/> and
|
||||
/// <see cref="GlAlphaTexture"/> are 0 and no GL handle exists at all.
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// Campaign V slice V6i-2: both arrays are <see cref="IGpuTexture"/>s the
|
||||
/// device created, and both slots were registered at build time, so
|
||||
/// <see cref="TextureSlots"/> 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// True when this atlas owns <see cref="IGpuTexture"/> arrays rather than raw
|
||||
/// GL names. The two construction paths are mutually exclusive.
|
||||
/// </summary>
|
||||
internal bool IsBackendNeutral => _rhi is not null;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="SetAnisotropic"/>, which changes the sampler.
|
||||
/// The device-table slots for the terrain and alpha arrays. Registered once
|
||||
/// at build time and re-registered only by <see cref="SetAnisotropic"/>,
|
||||
/// which changes the sampler.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Retail's terrain arrays are trilinear-filtered with the highest anisotropy
|
||||
/// the quality preset allows; <see cref="SetAnisotropic"/> lowers it. The GL
|
||||
/// path sets <c>GL_TEXTURE_MAX_ANISOTROPY</c> to 16 at build time, so the
|
||||
/// backend-neutral path starts at the same value.
|
||||
/// the quality preset allows; <see cref="SetAnisotropic"/> lowers it.
|
||||
/// </summary>
|
||||
private const float RetailMaxAnisotropy = 16f;
|
||||
|
||||
|
|
@ -192,11 +108,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
IReadOnlyList<uint> sideTCodes,
|
||||
IReadOnlyList<uint> 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<uint, uint> map, int layerCount,
|
||||
IReadOnlyList<float> tilingByLayer,
|
||||
uint glAlphaTexture, int alphaLayerCount,
|
||||
IReadOnlyList<byte> cornerLayers, IReadOnlyList<byte> sideLayers, IReadOnlyList<byte> roadLayers,
|
||||
IReadOnlyList<uint> cornerTCodes, IReadOnlyList<uint> sideTCodes, IReadOnlyList<uint> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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<Region>(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<uint, DecodedTexture> decodedByType = decode.DecodedByType;
|
||||
Dictionary<uint, uint> tilingByType = decode.TilingByType;
|
||||
int maxW = decode.MaxWidth, maxH = decode.MaxHeight;
|
||||
|
||||
int layerCount = decodedByType.Count;
|
||||
var map = new Dictionary<uint, uint>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
/// <see cref="SurfaceDecoder.DecodeRenderSurface"/> 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
|
||||
/// <c>TerrainBlending.BuildSurface</c> which layer to cite for each
|
||||
/// corner/side/road alpha source.
|
||||
/// </summary>
|
||||
private readonly record struct AlphaAtlasBuildResult(
|
||||
uint gl, int layerCount,
|
||||
IReadOnlyList<byte> corner, IReadOnlyList<byte> side, IReadOnlyList<byte> road,
|
||||
IReadOnlyList<uint> cornerTCodes, IReadOnlyList<uint> sideTCodes, IReadOnlyList<uint> roadRCodes);
|
||||
|
||||
/// <summary>
|
||||
/// Slice V6i-2: the alpha-map decode, shared by both construction paths for
|
||||
/// the same reason <see cref="DecodeTerrainLayers"/> 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<DecodedTexture> decoded = decode.Decoded;
|
||||
List<byte> cornerLayers = decode.CornerLayers;
|
||||
List<byte> sideLayers = decode.SideLayers;
|
||||
List<byte> roadLayers = decode.RoadLayers;
|
||||
List<uint> cornerTCodes = decode.CornerTCodes;
|
||||
List<uint> sideTCodes = decode.SideTCodes;
|
||||
List<uint> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: build both arrays through
|
||||
/// <see cref="IGpuDevice"/>.
|
||||
///
|
||||
/// <para>Same DAT reads, same decode, same layer ordering and the same
|
||||
/// resize-to-max policy as <see cref="Build"/> — only the upload differs,
|
||||
/// which is the whole point of splitting the decode out. The terrain array
|
||||
/// Builds both arrays through <see cref="IGpuDevice"/>. The terrain array
|
||||
/// gets a full mip chain (<see cref="IGpuTexture.GenerateMipChain"/> 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.</para>
|
||||
/// sampler; the alpha array is single-level and clamped, matching what the
|
||||
/// deleted GL path's texture parameters said.
|
||||
/// </summary>
|
||||
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<uint, uint> { [0] = 0u }, 1,
|
||||
TerrainTextureTilingTable.Build(Array.Empty<(uint Layer, uint RepeatCount)>()),
|
||||
alphaTex, 1,
|
||||
Array.Empty<byte>(), Array.Empty<byte>(), Array.Empty<byte>(),
|
||||
Array.Empty<uint>(), Array.Empty<uint>(), Array.Empty<uint>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="AcDream.App.Settings.RuntimeSettingsController.ReapplyQualityPreset"/> 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.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,7 +292,6 @@ public sealed unsafe partial class TerrainModernRenderer
|
|||
throw;
|
||||
}
|
||||
_tilingBuffer = buffer;
|
||||
_textureTilingUploaded = true;
|
||||
}
|
||||
|
||||
encoder.BindUniformBuffer(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>Campaign V slice V11 deleted the raw-GL submission arm
|
||||
/// (<c>TerrainModernRenderer.cs</c>'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 <c>TerrainModernRenderer.Rhi.cs</c>.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>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<uint, int> _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<DynamicIndirectBuffer>[] _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;
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal int DynamicIndirectBufferCount => 0;
|
||||
|
||||
// Reusable per-frame buffers.
|
||||
private readonly List<int> _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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<DynamicIndirectBuffer> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void SetClipUbo(TerrainClipBufferBinding sharedClipBinding) =>
|
||||
_sharedClipBinding = sharedClipBinding;
|
||||
|
||||
/// <summary>
|
||||
/// Two-tier streaming entry point. Accepts a prebuilt mesh from
|
||||
/// <see cref="LandblockStreamResult.Loaded.MeshData"/> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds this frame's <c>DrawElementsIndirectCommand</c> array from the
|
||||
/// visible slot list. Pure CPU, identical on both arms.
|
||||
/// visible slot list. Pure CPU.
|
||||
/// </summary>
|
||||
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<DynamicIndirectBuffer> 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<DynamicIndirectBuffer> 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
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Upload the texture-array adapter for retail's per-surface repeat count.
|
||||
/// Retail passes <c>TerrainTex::tex_tiling</c> directly to
|
||||
/// <c>ImgTex::TileCSI</c> / <c>ImgTex::MergeTexture</c>
|
||||
/// (`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.
|
||||
/// </summary>
|
||||
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<byte> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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).");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: drains the device texture table's dirty runs and
|
||||
/// (re)binds it at <see cref="GpuBindingModel.StorageTextureTable"/>.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
GlGpuDevice device = GpuDevice;
|
||||
device.FlushTextureTable();
|
||||
_gl!.BindBufferBase(
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
GpuBindingModel.StorageTextureTable,
|
||||
device.TextureTableGlName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: bind the terrain clip UBO to binding=2. Prefers the shared
|
||||
/// <see cref="ClipFrame"/> UBO range (<see cref="SetClipUbo"/>); otherwise lazily
|
||||
/// creates + binds a no-clip fallback (count 0 = ungated) so the shader never
|
||||
/// reads an unbound UBO. The fallback is std140-sized to
|
||||
/// <see cref="ClipFrame.TerrainUboBytes"/> and zero-filled (count 0).
|
||||
/// </summary>
|
||||
private void BindClipUboBinding2()
|
||||
{
|
||||
if (_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<uint> 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
|
||||
|
|
|
|||
|
|
@ -455,7 +455,8 @@ public sealed class TextRenderer : IDisposable
|
|||
/// into it, and issues one non-indexed draw. Replaces the old growable
|
||||
/// per-flight VBO + <c>BufferSubData</c> pattern: every UI vertex upload is
|
||||
/// now the frame's shared ring, reset once per frame by
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.Gl.GlGpuDevice.BeginFrame"/>.
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanGpuDevice.BeginFrame"/>
|
||||
/// (the raw-GL device's equivalent reset was deleted at Campaign V slice V11).
|
||||
/// </summary>
|
||||
private static void DrawRing(IGpuFrame frame, IGpuPassEncoder encoder, List<float> buf)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<uint, (uint Handle, int Width, int Height)>
|
||||
_surfacesById = new();
|
||||
private readonly Dictionary<(uint SurfaceId, uint OrigTextureId), (int Width, int Height)>
|
||||
_decodedDimensionsByTexture = new();
|
||||
private uint _magentaHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4a: one registered <see cref="IGpuTexture"/> plus its
|
||||
|
|
@ -55,7 +46,6 @@ public sealed unsafe class TextureCache
|
|||
// GPU texture objects/slots until process exit.
|
||||
private readonly List<GpuUiTextureEntry> _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
|
|||
{
|
||||
}
|
||||
|
||||
/// <param name="gl">
|
||||
/// The GL context, or null on a backend that has none. Campaign V slice V6h:
|
||||
/// the UI path (<see cref="GetOrUploadRenderSurface"/>, <see cref="UploadRgba8"/>)
|
||||
/// is entirely <see cref="IGpuDevice"/>-driven and runs on either backend,
|
||||
/// while the world paths — the legacy <c>Texture2D</c> 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 <paramref name="bindless"/> 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.
|
||||
/// </param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL context the world texture paths need. Campaign V slice V6h: a
|
||||
/// Vulkan-composed cache serves the UI path through <see cref="IGpuDevice"/>
|
||||
/// alone and never reaches here, so a failure names the slice that owns the
|
||||
/// port rather than dereferencing null.
|
||||
/// </summary>
|
||||
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.");
|
||||
|
||||
/// <summary>
|
||||
/// The GL backend's device, for the world texture paths' table
|
||||
/// registrations (Campaign V slice V4t). Those paths already require a GL
|
||||
/// context — see <see cref="Gl"/> — so the same construction that makes
|
||||
/// <see cref="_gl"/> non-null makes this cast sound; a Vulkan-composed
|
||||
/// cache serves only the UI path through <see cref="IGpuDevice"/> and never
|
||||
/// reaches here.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public uint GetOrUpload(uint surfaceId)
|
||||
=> GetOrUploadSurfaceCore(surfaceId, out _, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="GetOrUpload(uint)"/> but also returns the decoded
|
||||
/// pixel dimensions. UI 9-slice geometry needs the source size to
|
||||
/// compute slice UVs. Cached alongside the handle.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>ACDREAM_DUMP_SURFACES</c> 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 <c>uint.MaxValue</c> because
|
||||
/// GL hands out small ascending names and the two spaces share the
|
||||
/// <c>_uploadMetadata</c> 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.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>ACDREAM_DUMP_SKY=1</c>. Adds ~2ms per texture upload, negligible.
|
||||
/// </summary>
|
||||
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)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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<Exception>? 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
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
|
||||
/// <see langword="default"/> (an empty location) if a composite upload
|
||||
/// can't start or the decoded size can't be prepared this frame.
|
||||
/// </summary>
|
||||
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 <see langword="default"/> (an empty location) if a composite
|
||||
/// upload can't start or the decoded size can't be prepared this frame.
|
||||
/// </summary>
|
||||
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).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="IGpuDevice.ReleaseTextureSlot"/> is what defers its reuse.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="UploadRgba8"/> 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Memory-tracking bookkeeping only, without a raw GL delete — used for
|
||||
/// the Campaign V slice V4a UI-path <see cref="IGpuTexture"/> entries,
|
||||
/// whose GL name is released by <see cref="IGpuTexture.Dispose"/> through
|
||||
/// the device's own retirement queue rather than by
|
||||
/// <see cref="DeleteUploadedTexture"/>.
|
||||
/// Memory-tracking bookkeeping only — used for every <see cref="IGpuTexture"/>
|
||||
/// entry, whose GPU resource is released by <see cref="IGpuTexture.Dispose"/>
|
||||
/// through the device's own retirement queue.
|
||||
/// </summary>
|
||||
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-
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal static class TrackedTextureConstruction
|
||||
{
|
||||
public static uint Create(
|
||||
GlTextureConstructionTransaction transaction,
|
||||
Action<uint> 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<uint> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.OpenGL.Extensions.ARB;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Thin wrapper around <see cref="ArbBindlessTexture"/> + capability detection
|
||||
/// for the modern rendering path. Constructed once at startup via
|
||||
/// <see cref="TryCreate"/>, which returns false if the extension isn't present.
|
||||
/// </summary>
|
||||
public sealed class BindlessSupport
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly ArbBindlessTexture _ext;
|
||||
|
||||
private BindlessSupport(GL gl, ArbBindlessTexture extension)
|
||||
{
|
||||
_gl = gl;
|
||||
_ext = extension;
|
||||
}
|
||||
|
||||
public static bool TryCreate(GL gl, out BindlessSupport? support)
|
||||
{
|
||||
if (gl.TryGetExtension<ArbBindlessTexture>(out var ext))
|
||||
{
|
||||
support = new BindlessSupport(gl, ext);
|
||||
return true;
|
||||
}
|
||||
support = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Get a 64-bit bindless handle for the texture and make it resident.
|
||||
/// Idempotent: handle is the same for a given texture name.</summary>
|
||||
public ulong GetResidentHandle(uint textureName)
|
||||
{
|
||||
ulong h = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"get bindless handle for texture {textureName}",
|
||||
() => _ext.GetTextureHandle(textureName));
|
||||
if (h == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"OpenGL returned no bindless handle for texture {textureName}.");
|
||||
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {h} residency",
|
||||
() => _ext.IsTextureHandleResident(h));
|
||||
if (!resident)
|
||||
{
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {h} resident",
|
||||
() => _ext.MakeTextureHandleResident(h));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a 64-bit bindless handle combining a texture with an EXPLICIT
|
||||
/// sampler object (rather than the texture's own baked sampler state) and
|
||||
/// make it resident. Idempotent per (texture, sampler) pair.
|
||||
///
|
||||
/// Added for Campaign V slice V1's <c>GlGpuDevice.RegisterTexture</c>,
|
||||
/// which registers a (texture, sampler) pair per the RHI contract — "the
|
||||
/// same texture registered with two samplers occupies two slots." The
|
||||
/// texture-only <see cref="GetResidentHandle(uint)"/> above cannot express
|
||||
/// that; <c>ManagedGLTextureArray</c> already calls the equivalent
|
||||
/// <c>ArbBindlessTexture.GetTextureSamplerHandle</c> directly through
|
||||
/// <c>OpenGLGraphicsDevice.BindlessExtension</c>, so this simply exposes
|
||||
/// the same GL entry point through this class for the RHI's use.
|
||||
/// </summary>
|
||||
public ulong GetResidentHandle(uint textureName, uint samplerName)
|
||||
{
|
||||
ulong h = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"get bindless handle for texture {textureName} + sampler {samplerName}",
|
||||
() => _ext.GetTextureSamplerHandle(textureName, samplerName));
|
||||
if (h == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"OpenGL returned no bindless handle for texture {textureName} + sampler {samplerName}.");
|
||||
}
|
||||
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {h} residency",
|
||||
() => _ext.IsTextureHandleResident(h));
|
||||
if (!resident)
|
||||
{
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {h} resident",
|
||||
() => _ext.MakeTextureHandleResident(h));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>Release residency for a handle. Call before deleting the underlying texture.</summary>
|
||||
public void MakeNonResident(ulong handle)
|
||||
{
|
||||
bool resident = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"query bindless handle {handle} residency before release",
|
||||
() => _ext.IsTextureHandleResident(handle));
|
||||
if (!resident)
|
||||
return;
|
||||
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"make bindless handle {handle} non-resident",
|
||||
() => _ext.MakeTextureHandleNonResident(handle));
|
||||
}
|
||||
|
||||
// Phase N.5b note: a `SetSamplerHandleUniform` wrapper was added in T6
|
||||
// and removed when terrain rendering surfaced GL_INVALID_OPERATION on
|
||||
// NVIDIA Windows for the `uniform sampler2DArray` + glProgramUniformHandleARB
|
||||
// combination. The replacement pattern (uvec2 handle uniform + GLSL
|
||||
// sampler-from-handle constructor — see terrain_modern.frag) lives at the
|
||||
// call site via plain `_gl.ProgramUniform2(program, loc, low, high)`. If
|
||||
// you re-introduce a sampler-handle helper, restrict it to drivers known
|
||||
// to accept the direct sampler-uniform path.
|
||||
|
||||
/// <summary>Detect <c>GL_ARB_shader_draw_parameters</c> in addition to bindless.
|
||||
/// N.5's vertex shader uses <c>gl_BaseInstanceARB</c> and <c>gl_DrawIDARB</c>
|
||||
/// from this extension.</summary>
|
||||
public bool HasShaderDrawParameters(GL gl)
|
||||
{
|
||||
return gl.IsExtensionPresent("GL_ARB_shader_draw_parameters");
|
||||
}
|
||||
}
|
||||
|
|
@ -25,20 +25,15 @@ using System.Runtime.CompilerServices;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DatReaderWriter.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
public sealed unsafe partial class EnvCellRenderer :
|
||||
public sealed partial class EnvCellRenderer :
|
||||
IDisposable,
|
||||
IEnvCellLandblockPublisher
|
||||
{
|
||||
private readonly object _publicationOwner = new();
|
||||
|
||||
// Campaign V slice V6j: null on the RHI arm. Every GL statement below is
|
||||
// reached only when this is non-null; the encoder arm lives in
|
||||
// EnvCellRenderer.Rhi.cs and the GL arm is unchanged.
|
||||
private readonly GL? _gl;
|
||||
private readonly ObjectMeshManager _meshManager;
|
||||
private readonly WbFrustum _frustum;
|
||||
|
||||
|
|
@ -52,14 +47,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
private readonly object _renderLock = new();
|
||||
private EnvCellVisibilitySnapshot _activeSnapshot = new();
|
||||
|
||||
// Shader (set by caller via Initialize).
|
||||
// Uses acdream's legacy Shader type (not WB's GLSLShader) to match the
|
||||
// existing wire-in pattern in GameWindow.cs where _meshShader is loaded
|
||||
// for mesh_modern.{vert,frag} and shared across multiple consumers.
|
||||
// API mapping: Bind() -> Use(), SetUniform(s, int) -> SetInt(s, int),
|
||||
// SetUniform(s, Vector4) -> SetVec4(s, Vector4).
|
||||
private AcDream.App.Rendering.Shader? _shader;
|
||||
|
||||
// Phase U.4 root-cause fix: the view-projection captured in PrepareRenderBatches,
|
||||
// re-uploaded by Render() so the cell-shell pass is self-contained and does NOT
|
||||
// inherit WbDrawDispatcher's uViewProjection (which the opaque pass would read one
|
||||
|
|
@ -84,35 +71,22 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
|
||||
// Modern-MDI scratch buffers (single slot — we re-upload every frame).
|
||||
// WB BaseObjectRenderManager.cs:43-48: _scratchMdiCommandBuffers, _scratchModernBatchBuffers, _modernInstanceBuffers
|
||||
// We collapse the ring-of-3 to a single slot since we have no persistent/consolidated draws.
|
||||
private uint _mdiCommandBuffer;
|
||||
private int _mdiCommandCapacity;
|
||||
private uint _modernInstanceBuffer;
|
||||
private int _modernInstanceCapacity;
|
||||
private uint _modernBatchBuffer;
|
||||
private int _modernBatchCapacity;
|
||||
// mesh_modern.vert's SSBO InstanceData is only mat4 transform. The CPU
|
||||
// InstanceData below also carries CellId/Flags for filtering, so upload a
|
||||
// packed transform array instead of the 80-byte CPU struct.
|
||||
private Matrix4x4[] _gpuInstanceTransforms = Array.Empty<Matrix4x4>();
|
||||
|
||||
// Phase U.3: per-instance clip-slot SSBO (binding=3), parallel to
|
||||
// _modernInstanceBuffer. One uint per instance selecting its CellClip slot,
|
||||
// indexed by the same BaseInstance + gl_InstanceID the shader uses for
|
||||
// binding=0. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. U.4 populates real slots.
|
||||
private uint _clipSlotBuffer;
|
||||
private int _clipSlotCapacity;
|
||||
// Phase U.3: per-instance clip-slot data, parallel to _gpuInstanceTransforms.
|
||||
// One uint per instance selecting its CellClip slot, indexed by the same
|
||||
// BaseInstance + gl_InstanceID the shader uses for binding=0. ALL ZEROS ⇒
|
||||
// slot 0 ⇒ no-clip.
|
||||
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||
|
||||
// A7 Fix D (D-2): this renderer owns its lighting (self-contained GL state,
|
||||
// like uViewProjection) instead of reading the SSBO 4/5 WbDrawDispatcher last
|
||||
// left bound. binding=4 = global point-light snapshot (same data/indices as the
|
||||
// dispatcher, via GlobalLightPacker); binding=5 = 8 int indices per instance.
|
||||
private uint _globalLightsSsbo; // binding=4
|
||||
private int _globalLightsCapacity;
|
||||
// A7 Fix D (D-2): this renderer owns its lighting (self-contained state,
|
||||
// like uViewProjection) instead of reading whatever WbDrawDispatcher last
|
||||
// bound. Global point-light snapshot (same data/indices as the dispatcher,
|
||||
// via GlobalLightPacker) plus 8 int indices per instance.
|
||||
private float[] _globalLightData = new float[AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * 16];
|
||||
private uint _instLightSetSsbo; // binding=5
|
||||
private int _instLightSetCapacity;
|
||||
private int[] _lightSetData = new int[1024 * AcDream.Core.Lighting.LightManager.MaxLightsPerObject];
|
||||
private System.Collections.Generic.IReadOnlyList<AcDream.Core.Lighting.LightSource>? _pointSnapshot;
|
||||
private sealed class CachedCellLightSet
|
||||
|
|
@ -125,51 +99,17 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
private readonly List<uint> _cellLightRemovalScratch = new();
|
||||
private int _lightFrameGeneration;
|
||||
|
||||
private sealed class DynamicBufferSet
|
||||
{
|
||||
public uint MdiCommandBuffer;
|
||||
public uint ModernInstanceBuffer;
|
||||
public uint ModernBatchBuffer;
|
||||
public uint ClipSlotBuffer;
|
||||
public uint GlobalLightsSsbo;
|
||||
public uint InstanceLightSetSsbo;
|
||||
public int MdiCommandCapacity;
|
||||
public int ModernInstanceCapacity;
|
||||
public int ModernBatchCapacity;
|
||||
public int ClipSlotCapacity;
|
||||
public int GlobalLightsCapacity;
|
||||
public int InstanceLightSetCapacity;
|
||||
}
|
||||
|
||||
private readonly List<DynamicBufferSet>[] _dynamicBufferSetsByFrame =
|
||||
[[], [], []];
|
||||
// Per-GPU-fenced-frame-slot draw bookkeeping.
|
||||
private int _dynamicFrameSlot;
|
||||
private int _dynamicBufferSetCursor;
|
||||
private bool _dynamicFrameStarted;
|
||||
private DynamicBufferSet? _activeDynamicBufferSet;
|
||||
|
||||
internal int DynamicBufferSetCount =>
|
||||
_dynamicBufferSetsByFrame.Sum(frameSets => frameSets.Count);
|
||||
|
||||
// Phase U.3: SHARED per-cell clip-region SSBO (binding=2) handed in via
|
||||
// SetClipRegionSsbo (the GameWindow-level ClipFrame buffer). When 0, we bind
|
||||
// our own one-slot no-clip fallback so the shader never reads an unbound SSBO.
|
||||
private uint _sharedClipRegionSsbo;
|
||||
private uint _fallbackClipRegionSsbo;
|
||||
|
||||
// Campaign V slice V4t (2026-07-28): the interim per-renderer
|
||||
// GlBindlessHandleTable is retired. ObjectRenderBatch already carries the
|
||||
// device's own GpuTextureSlot, so this renderer shares WbDrawDispatcher's
|
||||
// table — the device's — and only has to flush and bind it before its own
|
||||
// raw-GL draws. That also removes the V2 caveat that two renderers could
|
||||
// legitimately number the same texture differently: there is now one
|
||||
// numbering, and it is the one the mesh manager assigned at upload.
|
||||
private AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
(_meshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"EnvCellRenderer was constructed without a mesh manager: its texture " +
|
||||
"slots come from that manager's GL device table (Campaign V slice V4t)."))
|
||||
.WorldTextureTable;
|
||||
/// <summary>
|
||||
/// The dynamic per-frame-slot SSBO pool this used to report was raw-GL-only
|
||||
/// bookkeeping, deleted with that arm at Campaign V slice V11. The RHI arm
|
||||
/// allocates its storage from the GPU frame's own upload ring instead, so
|
||||
/// there is no separate pool to count.
|
||||
/// </summary>
|
||||
internal int DynamicBufferSetCount => 0;
|
||||
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
|
|
@ -197,11 +137,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
private readonly Dictionary<ulong, List<InstanceData>> _activeSnapshotGlobalGroups = new();
|
||||
private readonly List<ulong> _activeSnapshotGlobalGfxObjIds = new();
|
||||
|
||||
// Static render-state tracking — matches WB BaseObjectRenderManager.cs:24-28.
|
||||
// Shared across all manager instances on the same GL context.
|
||||
private static uint _currentVao;
|
||||
private static CullMode? _currentCullMode;
|
||||
|
||||
public bool NeedsPrepare { get; private set; } = true;
|
||||
|
||||
// --- Prepare gate (2026-07-24) -------------------------------------------
|
||||
|
|
@ -302,31 +237,19 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor + Initialize
|
||||
// Constructor
|
||||
// Campaign V slice V11: the raw-GL constructor + Initialize(Shader) two-step
|
||||
// are deleted. EnvCellRenderer.Rhi.cs's constructor is now the class's sole
|
||||
// constructor — it builds the three shell pipelines itself, so there is no
|
||||
// second initialization step.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
public EnvCellRenderer(GL gl, ObjectMeshManager meshManager, WbFrustum frustum)
|
||||
{
|
||||
_gl = gl;
|
||||
_meshManager = meshManager;
|
||||
_frustum = frustum;
|
||||
}
|
||||
|
||||
public void Initialize(AcDream.App.Rendering.Shader shader)
|
||||
{
|
||||
_shader = shader;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <summary>Resets the per-frame submission cursor for the GPU-fenced slot.</summary>
|
||||
public void BeginFrame(int frameSlot)
|
||||
{
|
||||
if ((uint)frameSlot >= (uint)_dynamicBufferSetsByFrame.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(frameSlot));
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
|
||||
_dynamicFrameSlot = frameSlot;
|
||||
_dynamicBufferSetCursor = 0;
|
||||
_dynamicFrameStarted = true;
|
||||
_activeDynamicBufferSet = null;
|
||||
if (++_lightFrameGeneration == 0)
|
||||
{
|
||||
_cellLightSetCache.Clear();
|
||||
|
|
@ -334,15 +257,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: hand the renderer the SHARED per-cell clip-region SSBO
|
||||
/// (binding=2) created by <see cref="ClipFrame.UploadShared"/>. The renderer
|
||||
/// re-binds it to binding=2 immediately before its MDI. Pass 0 to fall back to
|
||||
/// the internal one-slot no-clip region buffer.
|
||||
/// </summary>
|
||||
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
||||
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
|
||||
|
||||
// Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
|
||||
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
|
||||
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
|
||||
|
|
@ -975,18 +889,14 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
HashSet<uint>? filter,
|
||||
IReadOnlyList<uint>? orderedCellIds)
|
||||
{
|
||||
// WB EnvCellRenderManager.cs:400:
|
||||
// WB EnvCellRenderManager.cs:400: the RHI arm's three pipelines are built
|
||||
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
||||
// answers whether this renderer is ready to draw.
|
||||
if (!_initialized) return;
|
||||
// Campaign V slice V6j: the RHI arm has no linked program to check — the
|
||||
// pipeline it draws with was built at construction and the same readiness
|
||||
// question is answered by _initialized alone.
|
||||
if (_gl is not null && (_shader is null || _shader.Program == 0)) return;
|
||||
|
||||
lock (_renderLock)
|
||||
{
|
||||
var snapshot = _activeSnapshot;
|
||||
// WB EnvCellRenderManager.cs:403-404:
|
||||
_shader?.Use();
|
||||
// FIX 2026-05-28 (pool aliasing root cause): mirror WB
|
||||
// EnvCellRenderManager.cs:405 — restore the pool cursor to the
|
||||
// high-water mark Prepare's merge phase reached, so any
|
||||
|
|
@ -997,43 +907,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
// mid-Render. See docs/research/2026-05-28-a8-env-cell-renderer-audit-findings.md.
|
||||
_poolIndex = snapshot.PostPreparePoolIndex;
|
||||
|
||||
// FIX 2026-05-28: invalidate static GL-state caches at start of Render.
|
||||
// Mirrors WB EnvCellRenderManager.cs:404-410:
|
||||
// CurrentVAO = 0; CurrentIBO = 0; CurrentAtlas = 0;
|
||||
// CurrentInstanceBuffer = 0; CurrentCullMode = null;
|
||||
//
|
||||
// These caches let SetCullMode / BindVertexArray skip redundant GL
|
||||
// calls when the state is already correct. BUT: between two Render()
|
||||
// invocations, OTHER consumers (WbDrawDispatcher, terrain, the
|
||||
// RenderInsideOutAcdream stencil pipeline) change the actual GL
|
||||
// state without updating these caches. The cache then lies, and
|
||||
// the per-batch SetCullMode in RenderModernMDIInternal skips its
|
||||
// glCullFace call — leaving stale cull state from the prior
|
||||
// consumer. For a cottage with mixed CullMode batches, half the
|
||||
// walls end up culled and the user sees "missing walls".
|
||||
//
|
||||
// Forcing the cache to null/0 at entry guarantees each Render call
|
||||
// re-establishes the GL state it expects.
|
||||
_currentVao = 0;
|
||||
_currentCullMode = null;
|
||||
|
||||
// WB EnvCellRenderManager.cs:406-409: uniform state setup.
|
||||
_shader?.SetInt("uRenderPass", (int)renderPass);
|
||||
_shader?.SetInt("uFilterByCell", 0);
|
||||
_shader?.SetInt("uLightingMode", 1); // A7 Fix D D-3/D-4: EnvCell bake (wrap points, no sun)
|
||||
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
|
||||
_shader?.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
|
||||
|
||||
// Phase U.4 ROOT-CAUSE FIX (cell-shell flicker / "transparent walls when
|
||||
// moving"): upload uViewProjection HERE rather than inheriting it from
|
||||
// WbDrawDispatcher. The opaque shell pass runs BEFORE the dispatcher's
|
||||
// Draw (GameWindow ~7411 vs ~7418, the only other setter), so without
|
||||
// this the opaque shells used the PREVIOUS frame's matrix — a stale
|
||||
// gl_Position against this frame's clip planes → pose-dependent clipping,
|
||||
// worst while moving. Same self-contained-GL-state precedent as the
|
||||
// 2026-05-28 cull-state cache fix above.
|
||||
_shader?.SetMatrix4("uViewProjection", _lastViewProjection);
|
||||
|
||||
List<InstanceData> allInstances = _renderInstances;
|
||||
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls =
|
||||
_renderDrawCalls;
|
||||
|
|
@ -1146,7 +1019,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
if (_drawCallRanges.Count == 0 && drawCalls.Count > 0)
|
||||
_drawCallRanges.Add(new DrawCallRange(0, drawCalls.Count));
|
||||
RenderModernMDIInternal(
|
||||
_shader,
|
||||
drawCalls,
|
||||
allInstances,
|
||||
_drawCallRanges,
|
||||
|
|
@ -1155,20 +1027,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
|
||||
// WB EnvCellRenderManager.cs:486-510: selection/hover highlights — DROPPED (no editor state).
|
||||
|
||||
// WB EnvCellRenderManager.cs:506-509: cleanup.
|
||||
_shader?.SetVec4("uHighlightColor", new System.Numerics.Vector4(0, 0, 0, 0));
|
||||
_shader?.SetInt("uRenderPass", (int)renderPass);
|
||||
_gl?.BindVertexArray(0);
|
||||
_currentVao = 0;
|
||||
|
||||
// No cull restore at exit, matching WB's manager pattern: the
|
||||
// last SetCullMode call reflects actual GL state, and the next
|
||||
// Render call invalidates `_currentCullMode` before issuing its
|
||||
// own per-batch state. The Landblock->None override below can
|
||||
// intentionally leave cull disabled for the following IndoorPass,
|
||||
// preserving the shipped Gate #5 baseline while deeper evidence is
|
||||
// gathered.
|
||||
|
||||
// Update frame stats for probe emission at the call site.
|
||||
_lastFrameStats.CellsRendered = orderedCellIds?.Count
|
||||
?? filter?.Count
|
||||
|
|
@ -1285,58 +1143,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
// issues glMultiDrawElementsIndirect.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private void ActivateNextDynamicBufferSet()
|
||||
{
|
||||
if (!_dynamicFrameStarted)
|
||||
throw new InvalidOperationException("BeginFrame must be called before drawing EnvCells.");
|
||||
|
||||
List<DynamicBufferSet> slotSets = _dynamicBufferSetsByFrame[_dynamicFrameSlot];
|
||||
if (_dynamicBufferSetCursor == slotSets.Count)
|
||||
slotSets.Add(CreateDynamicBufferSet());
|
||||
|
||||
DynamicBufferSet set = slotSets[_dynamicBufferSetCursor++];
|
||||
_activeDynamicBufferSet = set;
|
||||
_mdiCommandBuffer = set.MdiCommandBuffer;
|
||||
_modernInstanceBuffer = set.ModernInstanceBuffer;
|
||||
_modernBatchBuffer = set.ModernBatchBuffer;
|
||||
_clipSlotBuffer = set.ClipSlotBuffer;
|
||||
_globalLightsSsbo = set.GlobalLightsSsbo;
|
||||
_instLightSetSsbo = set.InstanceLightSetSsbo;
|
||||
_mdiCommandCapacity = set.MdiCommandCapacity;
|
||||
_modernInstanceCapacity = set.ModernInstanceCapacity;
|
||||
_modernBatchCapacity = set.ModernBatchCapacity;
|
||||
_clipSlotCapacity = set.ClipSlotCapacity;
|
||||
_globalLightsCapacity = set.GlobalLightsCapacity;
|
||||
_instLightSetCapacity = set.InstanceLightSetCapacity;
|
||||
}
|
||||
|
||||
private DynamicBufferSet CreateDynamicBufferSet()
|
||||
{
|
||||
var set = new DynamicBufferSet();
|
||||
try
|
||||
{
|
||||
set.MdiCommandBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell MDI buffer");
|
||||
set.ModernInstanceBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell instance SSBO");
|
||||
set.ModernBatchBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell batch SSBO");
|
||||
set.ClipSlotBuffer = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell clip-slot SSBO");
|
||||
set.GlobalLightsSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell global-light SSBO");
|
||||
set.InstanceLightSetSsbo = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell light-set SSBO");
|
||||
return set;
|
||||
}
|
||||
catch (Exception creationFailure)
|
||||
{
|
||||
try { DeleteDynamicBufferSet(set); }
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"EnvCell dynamic-buffer creation and rollback failed.",
|
||||
creationFailure,
|
||||
cleanupFailure);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildUnfilteredGroups(EnvCellVisibilitySnapshot snapshot)
|
||||
{
|
||||
foreach (List<InstanceData> instances in _activeSnapshotGlobalGroups.Values)
|
||||
|
|
@ -1359,59 +1165,7 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
private void DeleteDynamicBufferSet(DynamicBufferSet set)
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
void Attempt(uint buffer, long bytes, string name)
|
||||
{
|
||||
try { TrackedGlResource.DeleteBuffer(_gl!, buffer, bytes, $"deleting {name}"); }
|
||||
catch (Exception ex) { (failures ??= []).Add(ex); }
|
||||
}
|
||||
|
||||
Attempt(
|
||||
set.MdiCommandBuffer,
|
||||
(long)set.MdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
"EnvCell MDI buffer");
|
||||
Attempt(
|
||||
set.ModernInstanceBuffer,
|
||||
(long)set.ModernInstanceCapacity * sizeof(Matrix4x4),
|
||||
"EnvCell instance SSBO");
|
||||
Attempt(
|
||||
set.ModernBatchBuffer,
|
||||
(long)set.ModernBatchCapacity * sizeof(ModernBatchData),
|
||||
"EnvCell batch SSBO");
|
||||
Attempt(set.ClipSlotBuffer, (long)set.ClipSlotCapacity * sizeof(uint), "EnvCell clip-slot SSBO");
|
||||
Attempt(
|
||||
set.GlobalLightsSsbo,
|
||||
(long)set.GlobalLightsCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
"EnvCell global-light SSBO");
|
||||
Attempt(
|
||||
set.InstanceLightSetSsbo,
|
||||
(long)set.InstanceLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
"EnvCell light-set SSBO");
|
||||
|
||||
if (failures is not null)
|
||||
throw new AggregateException("One or more EnvCell dynamic buffers failed to delete.", failures);
|
||||
}
|
||||
|
||||
private void PersistActiveDynamicBufferCapacities()
|
||||
{
|
||||
DynamicBufferSet set = _activeDynamicBufferSet
|
||||
?? throw new InvalidOperationException("No dynamic EnvCell buffer set is active.");
|
||||
set.MdiCommandCapacity = _mdiCommandCapacity;
|
||||
set.ModernInstanceCapacity = _modernInstanceCapacity;
|
||||
set.ModernBatchCapacity = _modernBatchCapacity;
|
||||
set.ClipSlotCapacity = _clipSlotCapacity;
|
||||
set.GlobalLightsCapacity = _globalLightsCapacity;
|
||||
set.InstanceLightSetCapacity = _instLightSetCapacity;
|
||||
}
|
||||
|
||||
private void RenderModernMDIInternal(
|
||||
AcDream.App.Rendering.Shader? shader,
|
||||
List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> drawCalls,
|
||||
List<InstanceData> allInstances,
|
||||
IReadOnlyList<DrawCallRange> drawCallRanges,
|
||||
|
|
@ -1423,26 +1177,11 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
int passIdx = (int)renderPass;
|
||||
if (passIdx < 0 || passIdx > 2) return;
|
||||
|
||||
// §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
|
||||
// Without the global VAO nothing can draw, and returning AFTER the pass state
|
||||
// was established leaked it (same early-out shape as the totalDraws==0 leak —
|
||||
// see the comment on the state-establish block below).
|
||||
// Campaign V slice V6j: the RHI arm has no vertex array — the pipeline
|
||||
// owns one shaped by GpuVertexLayout.WorldMesh — so its readiness test is
|
||||
// the backend-neutral HasStores the arena publishes (V6i-3).
|
||||
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
|
||||
if (_gl is not null)
|
||||
{
|
||||
if (globalVao == 0) return;
|
||||
}
|
||||
else if (_meshManager.GlobalBuffer is not { HasStores: true })
|
||||
{
|
||||
// the backend-neutral HasStores flag the mesh arena publishes (V6i-3).
|
||||
if (_meshManager.GlobalBuffer is not { HasStores: true })
|
||||
return;
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:715-716:
|
||||
shader?.Use();
|
||||
shader?.SetInt("uFilterByCell", 0);
|
||||
|
||||
// WB BaseObjectRenderManager.cs:718-740: count the pass-filtered batches.
|
||||
// A normal render has one range. The ordered transparent-shell path has
|
||||
|
|
@ -1479,120 +1218,16 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
// WB BaseObjectRenderManager.cs:743:
|
||||
if (totalDraws == 0) return;
|
||||
int uniqueInstanceCount = allInstances.Count;
|
||||
|
||||
// Campaign V slice V6j: the encoder arm owns no buffer pool and no
|
||||
// imperative state bracket. Every per-frame section is a ring slice, so
|
||||
// there is nothing to activate or grow, and blend plus depth-write are
|
||||
// baked into the three shell pipelines rather than set here.
|
||||
if (_gl is not null)
|
||||
{
|
||||
ActivateNextDynamicBufferSet();
|
||||
|
||||
// Phase U.4 ROOT-CAUSE FIX (cell-shell "transparent walls / only bluish
|
||||
// background, flickering when moving"): establish this pass's BLEND + DepthMask
|
||||
// state OURSELVES rather than inheriting it. Mirror the working WbDrawDispatcher
|
||||
// passes (Disable(Blend)+DepthMask(true) opaque; Enable(Blend)+DepthMask(false)
|
||||
// transparent). Restored to opaque defaults at the end of the draw loop so a
|
||||
// Transparent pass can't leak into later draws.
|
||||
//
|
||||
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
|
||||
// totalDraws==0 early-out above. It used to run before the batch grouping, so a
|
||||
// Transparent pass over a cell whose batches are ALL opaque (a plain cottage
|
||||
// interior) set Blend-on/DepthMask-off and then returned at the count check
|
||||
// WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's
|
||||
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
|
||||
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
|
||||
// whole screen dropped to the fog-tinted clear color — onset-locked to the
|
||||
// building-flood merge (the first frame a flooded building shell draws), holding
|
||||
// until camera rotation dropped the cell from the flood. From here down every
|
||||
// path reaches the end-of-pass restore.
|
||||
if (renderPass == WbRenderPass.Transparent)
|
||||
{
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.DepthMask(true);
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:745-759: resize buffers if needed.
|
||||
if (totalDraws > _mdiCommandCapacity)
|
||||
{
|
||||
int grownMdiCapacity = Math.Max(_mdiCommandCapacity * 2, totalDraws);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.DrawIndirectBuffer,
|
||||
_mdiCommandBuffer,
|
||||
(long)_mdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
(long)grownMdiCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell MDI buffer to {grownMdiCapacity} commands");
|
||||
_mdiCommandCapacity = grownMdiCapacity;
|
||||
|
||||
int grownBatchCapacity = grownMdiCapacity;
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_modernBatchBuffer,
|
||||
(long)_modernBatchCapacity * sizeof(ModernBatchData),
|
||||
(long)grownBatchCapacity * sizeof(ModernBatchData),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell batch SSBO to {grownBatchCapacity} batches");
|
||||
_modernBatchCapacity = grownBatchCapacity;
|
||||
}
|
||||
|
||||
if (uniqueInstanceCount > _modernInstanceCapacity)
|
||||
{
|
||||
int grownInstanceCapacity = Math.Max(_modernInstanceCapacity * 2, uniqueInstanceCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_modernInstanceBuffer,
|
||||
(long)_modernInstanceCapacity * sizeof(Matrix4x4),
|
||||
(long)grownInstanceCapacity * sizeof(Matrix4x4),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell instance SSBO to {grownInstanceCapacity} instances");
|
||||
_modernInstanceCapacity = grownInstanceCapacity;
|
||||
}
|
||||
|
||||
// Phase U.3: keep the clip-slot buffer (binding=3) sized to the
|
||||
// instance prefix so instanceClipSlot[BaseInstance + gl_InstanceID]
|
||||
// is always in range. It owns an independent committed capacity so a
|
||||
// failed allocation can never publish the instance buffer's growth as
|
||||
// if both resources had succeeded.
|
||||
if (uniqueInstanceCount > _clipSlotCapacity)
|
||||
{
|
||||
int grownClipCapacity = Math.Max(_clipSlotCapacity * 2, uniqueInstanceCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_clipSlotBuffer,
|
||||
(long)_clipSlotCapacity * sizeof(uint),
|
||||
(long)grownClipCapacity * sizeof(uint),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell clip-slot SSBO to {grownClipCapacity} instances");
|
||||
_clipSlotCapacity = grownClipCapacity;
|
||||
}
|
||||
|
||||
if (uniqueInstanceCount > _instLightSetCapacity)
|
||||
{
|
||||
int grownLightSetCapacity = Math.Max(_instLightSetCapacity * 2, uniqueInstanceCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_instLightSetSsbo,
|
||||
(long)_instLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
(long)grownLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell light-set SSBO to {grownLightSetCapacity} instances");
|
||||
_instLightSetCapacity = grownLightSetCapacity;
|
||||
}
|
||||
}
|
||||
// imperative state bracket. Every per-frame section is a ring slice
|
||||
// allocated fresh in SubmitRhi, and blend plus depth-write are baked
|
||||
// into the three shell pipelines rather than set here. The frame-
|
||||
// started invariant this used to enforce via ActivateNextDynamicBufferSet
|
||||
// still matters (it gates the light-frame-generation cache), so it is
|
||||
// checked directly.
|
||||
if (!_dynamicFrameStarted)
|
||||
throw new InvalidOperationException("BeginFrame must be called before drawing EnvCells.");
|
||||
|
||||
// WB BaseObjectRenderManager.cs:761-762: grow scratch arrays.
|
||||
if (_commands.Length < totalDraws)
|
||||
|
|
@ -1684,189 +1319,7 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
if (_gl is null)
|
||||
{
|
||||
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// WB BaseObjectRenderManager.cs:784-805 upload. Retain capacity and
|
||||
// update the active prefix so portal frames cannot enqueue an unbounded
|
||||
// chain of retired driver allocations.
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||
fixed (DrawElementsIndirectCommand* ptr = _commands)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.DrawIndirectBuffer, 0,
|
||||
(nuint)(totalDraws * sizeof(DrawElementsIndirectCommand)), ptr);
|
||||
}
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernInstanceBuffer);
|
||||
if (_gpuInstanceTransforms.Length < uniqueInstanceCount)
|
||||
Array.Resize(ref _gpuInstanceTransforms, Math.Max(_gpuInstanceTransforms.Length * 2, uniqueInstanceCount));
|
||||
for (int i = 0; i < uniqueInstanceCount; i++)
|
||||
_gpuInstanceTransforms[i] = allInstances[i].Transform;
|
||||
fixed (Matrix4x4* ptr = _gpuInstanceTransforms)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * sizeof(Matrix4x4)), ptr);
|
||||
}
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _modernBatchBuffer);
|
||||
fixed (ModernBatchData* ptr = _modernBatches)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(totalDraws * sizeof(ModernBatchData)), ptr);
|
||||
}
|
||||
|
||||
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
|
||||
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
|
||||
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
|
||||
// the map (shouldn't happen — the Render filter is the map's keys) and the
|
||||
// U.3 path both map to slot 0 (no-clip). allInstances is laid out in the
|
||||
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
|
||||
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
|
||||
if (_clipSlotData.Length < uniqueInstanceCount)
|
||||
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
|
||||
// #176 stripe-hunt isolation (ACDREAM_CLIP_DEBUG=1): force every shell
|
||||
// instance to slot 0 (no-clip) — retail draws cell shells WHOLE.
|
||||
if (_cellIdToSlot is null
|
||||
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
|
||||
{
|
||||
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < uniqueInstanceCount; i++)
|
||||
_clipSlotData[i] = _cellIdToSlot.TryGetValue(allInstances[i].CellId, out int slot)
|
||||
? (uint)slot : 0u;
|
||||
}
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
|
||||
fixed (uint* ptr = _clipSlotData)
|
||||
{
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * sizeof(uint)), ptr);
|
||||
}
|
||||
|
||||
// A7 Fix D (D-2): per-instance 8-int light set, parallel to the transforms,
|
||||
// keyed on the cell each shell instance belongs to (mirrors _clipSlotData).
|
||||
int lightStride = AcDream.Core.Lighting.LightManager.MaxLightsPerObject;
|
||||
if (_lightSetData.Length < uniqueInstanceCount * lightStride)
|
||||
_lightSetData = new int[System.Math.Max(_lightSetData.Length * 2, uniqueInstanceCount * lightStride)];
|
||||
for (int i = 0; i < uniqueInstanceCount; i++)
|
||||
{
|
||||
int[] cellSet = GetCellLightSet(allInstances[i].CellId);
|
||||
System.Array.Copy(cellSet, 0, _lightSetData, i * lightStride, lightStride);
|
||||
}
|
||||
|
||||
// #176 seam-draw probe: emitted HERE (not in Render) so the per-cell light
|
||||
// sets read through the just-cleared cache against THIS frame's
|
||||
// _pointSnapshot — the exact data the SSBO upload below carries.
|
||||
if (renderPass == WbRenderPass.Opaque
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
|
||||
EmitSeamDrawProbe(drawCalls, allInstances, _seamProbeFilter);
|
||||
|
||||
// A7 Fix D (D-2): upload binding=4 (global lights) + binding=5 (per-instance set).
|
||||
int lightCount = AcDream.Core.Lighting.GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
|
||||
int glUploadCount = lightCount > 0 ? lightCount : 1;
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _globalLightsSsbo);
|
||||
if (glUploadCount > _globalLightsCapacity)
|
||||
{
|
||||
int grownGlobalLightCapacity = Math.Max(_globalLightsCapacity * 2, glUploadCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_globalLightsSsbo,
|
||||
(long)_globalLightsCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
(long)grownGlobalLightCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
GLEnum.DynamicDraw,
|
||||
$"growing EnvCell global-light SSBO to {grownGlobalLightCapacity} lights");
|
||||
_globalLightsCapacity = grownGlobalLightCapacity;
|
||||
}
|
||||
fixed (float* gp = _globalLightData)
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(glUploadCount * AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight * sizeof(float)), gp);
|
||||
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _instLightSetSsbo);
|
||||
fixed (int* lp = _lightSetData)
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0,
|
||||
(nuint)(uniqueInstanceCount * lightStride * sizeof(int)), lp);
|
||||
|
||||
PersistActiveDynamicBufferCapacities();
|
||||
|
||||
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
|
||||
// (globalVao validated at the top of the method — a return here would leak the
|
||||
// pass state established above.)
|
||||
if (_currentVao != globalVao)
|
||||
{
|
||||
_gl.BindVertexArray(globalVao);
|
||||
_currentVao = globalVao;
|
||||
}
|
||||
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 0, _modernInstanceBuffer);
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 1, _modernBatchBuffer);
|
||||
// Phase U.3: per-instance clip slots (binding=3) + shared clip regions
|
||||
// (binding=2, via the GameWindow ClipFrame or our no-clip fallback).
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 3, _clipSlotBuffer);
|
||||
BindClipRegionBinding2();
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 4, _globalLightsSsbo); // A7 Fix D (D-2)
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 5, _instLightSetSsbo); // A7 Fix D (D-2)
|
||||
FlushAndBindTextureTable(); // Campaign V slice V2 (binding=9)
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||
|
||||
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
|
||||
|
||||
// WB BaseObjectRenderManager.cs:821-847: issue per-group multi-draw calls.
|
||||
// The ranges retain ordered-cell boundaries, so transparent geometry
|
||||
// stays far-to-near even though all command data was uploaded once.
|
||||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
int groupIndex = drawRange.GroupIndex;
|
||||
var cullMode = (CullMode)(groupIndex % 4);
|
||||
// Phase A8 visual-gate evidence: cell meshes use CullMode.Landblock
|
||||
// uniformly, but the room surfaces need to be visible from inside
|
||||
// under acdream's current global winding state. Render cell polys
|
||||
// double-sided while the architectural cause is isolated.
|
||||
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
|
||||
if (_currentCullMode != cullMode)
|
||||
{
|
||||
SetCullMode(cullMode);
|
||||
}
|
||||
|
||||
bool isAdditive = groupIndex >= 4;
|
||||
if (isAdditive)
|
||||
{
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
shader!.SetInt("uRenderPass", (int)renderPass | 0x100);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
shader!.SetInt("uRenderPass", (int)renderPass);
|
||||
}
|
||||
|
||||
shader!.SetInt("uDrawIDOffset", drawRange.FirstCommand);
|
||||
_gl.MultiDrawElementsIndirect(
|
||||
PrimitiveType.Triangles,
|
||||
DrawElementsType.UnsignedShort,
|
||||
(void*)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
||||
(uint)drawRange.CommandCount,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
|
||||
// Phase U.4: leave a clean opaque-default render state (mirrors WbDrawDispatcher's
|
||||
// post-transparent restore) so a Transparent pass's Blend-on / DepthMask-off does
|
||||
// not leak into particles or the next frame's draws.
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.DepthMask(true);
|
||||
|
||||
// WB BaseObjectRenderManager.cs:845-847:
|
||||
shader!.SetInt("uDrawIDOffset", 0);
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, 0);
|
||||
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
||||
}
|
||||
|
||||
internal static void AppendMdiDrawRange(
|
||||
|
|
@ -1994,116 +1447,6 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
System.Console.WriteLine($"[seam-blk] t={now} changed={(changed ? 1 : 0)}{sig}");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SetCullMode
|
||||
// Verbatim copy of WB BaseObjectRenderManager.cs:850-866.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private void SetCullMode(CullMode mode)
|
||||
{
|
||||
_currentCullMode = mode;
|
||||
switch (mode)
|
||||
{
|
||||
case CullMode.None:
|
||||
_gl!.Disable(EnableCap.CullFace);
|
||||
break;
|
||||
case CullMode.Clockwise:
|
||||
_gl!.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Front);
|
||||
break;
|
||||
case CullMode.CounterClockwise:
|
||||
case CullMode.Landblock:
|
||||
_gl!.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Back);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FlushAndBindTextureTable (Campaign V slice V2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: drains the device texture table's dirty runs and
|
||||
/// (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// A genuinely new slot is rare — new dat surfaces/atlases, not every frame
|
||||
/// — but the bind is unconditional, because GL's storage-buffer binding
|
||||
/// points are global and another raw-GL renderer's binding 9 sits there
|
||||
/// between two of these draws.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = WorldTextureTable;
|
||||
device.FlushTextureTable();
|
||||
_gl!.BindBufferBase(
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
device.TextureTableGlName);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BindClipRegionBinding2 (Phase U.3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Bind the per-cell clip-region SSBO to binding=2. Prefers the shared
|
||||
/// <see cref="ClipFrame"/> buffer (<see cref="SetClipRegionSsbo"/>); otherwise
|
||||
/// lazily creates + binds a one-slot no-clip fallback (count 0 = pass-all) so
|
||||
/// the shader never reads an unbound SSBO.
|
||||
/// </summary>
|
||||
private void BindClipRegionBinding2()
|
||||
{
|
||||
if (_sharedClipRegionSsbo != 0)
|
||||
{
|
||||
_gl!.BindBufferBase(GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _sharedClipRegionSsbo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fallbackClipRegionSsbo == 0)
|
||||
{
|
||||
uint fallback = TrackedGlResource.CreateBuffer(_gl!, "creating EnvCell fallback clip SSBO");
|
||||
bool allocated = false;
|
||||
try
|
||||
{
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl!,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
fallback,
|
||||
0,
|
||||
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
|
||||
GLEnum.DynamicDraw,
|
||||
"allocating EnvCell fallback clip SSBO");
|
||||
allocated = true;
|
||||
// One CellClip slot, all zeros: count 0 ⇒ shader passes every plane.
|
||||
Span<byte> zero = stackalloc byte[AcDream.App.Rendering.ClipFrame.CellClipStrideBytes];
|
||||
zero.Clear();
|
||||
fixed (byte* p = zero)
|
||||
{
|
||||
_gl!.BufferSubData(
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
0,
|
||||
(nuint)zero.Length,
|
||||
p);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(_gl, "initializing EnvCell fallback clip SSBO");
|
||||
_fallbackClipRegionSsbo = fallback;
|
||||
}
|
||||
catch
|
||||
{
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl!,
|
||||
fallback,
|
||||
allocated ? AcDream.App.Rendering.ClipFrame.CellClipStrideBytes : 0,
|
||||
"rolling back EnvCell fallback clip SSBO");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
_gl!.BindBufferBase(GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.ClipFrame.MeshClipSsboBinding, _fallbackClipRegionSsbo);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// List pool (GetPooledList)
|
||||
// Copied from WB ObjectRenderManagerBase (pattern).
|
||||
|
|
@ -2154,67 +1497,12 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
{
|
||||
("prepare-scratch", _prepareScratch.Dispose),
|
||||
};
|
||||
// Campaign V slice V6j: the encoder arm owns no GL names — its
|
||||
// pipelines route their physical free through the device's
|
||||
// retirement queue — so the ledger below holds only the scratch.
|
||||
if (_gl is null)
|
||||
DisposeRhiResources();
|
||||
// Campaign V slice V11: the raw-GL arm's dynamic buffer-set pool is
|
||||
// deleted along with it. The RHI arm's pipelines route their
|
||||
// physical free through the device's own retirement queue, so the
|
||||
// ledger above holds only the scratch.
|
||||
DisposeRhiResources();
|
||||
|
||||
for (int frame = 0; frame < _dynamicBufferSetsByFrame.Length; frame++)
|
||||
{
|
||||
List<DynamicBufferSet> frameSets = _dynamicBufferSetsByFrame[frame];
|
||||
for (int index = 0; index < frameSets.Count; index++)
|
||||
{
|
||||
DynamicBufferSet set = frameSets[index];
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.MdiCommandBuffer,
|
||||
(long)set.MdiCommandCapacity * sizeof(DrawElementsIndirectCommand),
|
||||
$"dynamic-{frame}-{index}-mdi",
|
||||
"deleting EnvCell MDI buffer");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.ModernInstanceBuffer,
|
||||
(long)set.ModernInstanceCapacity * sizeof(Matrix4x4),
|
||||
$"dynamic-{frame}-{index}-instances",
|
||||
"deleting EnvCell instance SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.ModernBatchBuffer,
|
||||
(long)set.ModernBatchCapacity * sizeof(ModernBatchData),
|
||||
$"dynamic-{frame}-{index}-batches",
|
||||
"deleting EnvCell batch SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.ClipSlotBuffer,
|
||||
(long)set.ClipSlotCapacity * sizeof(uint),
|
||||
$"dynamic-{frame}-{index}-clip-slots",
|
||||
"deleting EnvCell clip-slot SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.GlobalLightsSsbo,
|
||||
(long)set.GlobalLightsCapacity
|
||||
* AcDream.Core.Lighting.GlobalLightPacker.FloatsPerLight
|
||||
* sizeof(float),
|
||||
$"dynamic-{frame}-{index}-global-lights",
|
||||
"deleting EnvCell global-light SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
set.InstanceLightSetSsbo,
|
||||
(long)set.InstanceLightSetCapacity
|
||||
* AcDream.Core.Lighting.LightManager.MaxLightsPerObject
|
||||
* sizeof(int),
|
||||
$"dynamic-{frame}-{index}-light-sets",
|
||||
"deleting EnvCell light-set SSBO");
|
||||
}
|
||||
}
|
||||
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
_fallbackClipRegionSsbo,
|
||||
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
|
||||
"fallback-clip-region",
|
||||
"deleting EnvCell fallback clip SSBO");
|
||||
_disposeResources = new RetryableResourceReleaseLedger(releases);
|
||||
}
|
||||
|
||||
|
|
@ -2225,17 +1513,7 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
"One or more EnvCell renderer resources could not be released.");
|
||||
}
|
||||
|
||||
foreach (List<DynamicBufferSet> frameSets in _dynamicBufferSetsByFrame)
|
||||
frameSets.Clear();
|
||||
_activeDynamicBufferSet = null;
|
||||
_dynamicFrameStarted = false;
|
||||
_mdiCommandBuffer = 0;
|
||||
_modernInstanceBuffer = 0;
|
||||
_modernBatchBuffer = 0;
|
||||
_clipSlotBuffer = 0;
|
||||
_globalLightsSsbo = 0;
|
||||
_instLightSetSsbo = 0;
|
||||
_fallbackClipRegionSsbo = 0;
|
||||
_disposeResources = null;
|
||||
IsDisposed = true;
|
||||
|
||||
|
|
@ -2250,22 +1528,4 @@ public sealed unsafe partial class EnvCellRenderer :
|
|||
_disposing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTrackedBufferRelease(
|
||||
List<(string Name, Action Release)> releases,
|
||||
uint buffer,
|
||||
long capacityBytes,
|
||||
string name,
|
||||
string context)
|
||||
{
|
||||
if (buffer == 0)
|
||||
return;
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl!,
|
||||
buffer,
|
||||
capacityBytes,
|
||||
context);
|
||||
releases.Add((name, release.Run));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.Core.Native;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public static class GLHelpers {
|
||||
public static OpenGLGraphicsDevice? Device { get; set; }
|
||||
public static ILogger? Logger { get; set; }
|
||||
|
||||
public static void Init(OpenGLGraphicsDevice device, ILogger logger) {
|
||||
Logger = logger;
|
||||
Device = device;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always-on error boundary for resource transactions. Most render-path
|
||||
/// checks remain Debug-only because <c>glGetError</c> is a synchronous
|
||||
/// driver call; allocation/upload code must not publish CPU state after
|
||||
/// OpenGL reported OOM, context loss, or a rejected transfer in Release.
|
||||
/// Call exactly once before committing each transaction.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ThrowOnResourceError(GL gl, string context) {
|
||||
GLEnum error = gl.GetError();
|
||||
if (error == GLEnum.NoError)
|
||||
return;
|
||||
|
||||
var errors = new System.Text.StringBuilder();
|
||||
do {
|
||||
if (errors.Length != 0)
|
||||
errors.Append(", ");
|
||||
errors.Append(error).Append(" (").Append(GetErrorDetails(error)).Append(')');
|
||||
error = gl.GetError();
|
||||
} while (error != GLEnum.NoError);
|
||||
|
||||
string message = $"OpenGL resource transaction failed: {errors}. Context: {context}";
|
||||
Logger?.LogError(message);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static bool _loggedVersion = false;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrors(GL gl, bool logErrors = false, [CallerMemberName] string callerName = "",
|
||||
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
|
||||
var error = gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
if (!_loggedVersion) {
|
||||
_loggedVersion = true;
|
||||
var version = gl.GetStringS(GLEnum.Version);
|
||||
var vendor = gl.GetStringS(GLEnum.Vendor);
|
||||
var renderer = gl.GetStringS(GLEnum.Renderer);
|
||||
Logger?.LogInformation($"GL Version: {version}, Vendor: {vendor}, Renderer: {renderer}");
|
||||
}
|
||||
string errorDetails = GetErrorDetails(error);
|
||||
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
|
||||
|
||||
var program = (uint)gl.GetInteger(GLEnum.CurrentProgram);
|
||||
var vao = gl.GetInteger(GLEnum.VertexArrayBinding);
|
||||
var activeTex = gl.GetInteger(GLEnum.ActiveTexture);
|
||||
var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
string extraInfo = "";
|
||||
if (program != 0) {
|
||||
bool isProgram = gl.IsProgram(program);
|
||||
gl.GetProgram(program, GLEnum.LinkStatus, out int linkStatus);
|
||||
gl.GetProgram(program, GLEnum.DeleteStatus, out int deleteStatus);
|
||||
gl.GetProgram(program, GLEnum.ValidateStatus, out int validateStatus);
|
||||
extraInfo = $", IsProg: {isProgram}, Link: {linkStatus}, Del: {deleteStatus}, Valid: {validateStatus}";
|
||||
}
|
||||
|
||||
string message = $"OpenGL Error: {error} ({errorDetails}) at {location}. Thread: {threadId}, Program: {program}{extraInfo}, VAO: {vao}, ActiveTex: {activeTex}";
|
||||
|
||||
Logger?.LogError(message);
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
#else
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrors(GL gl, bool logErrors = false, string callerName = "",
|
||||
string callerFile = "", int callerLine = 0) {
|
||||
}
|
||||
#endif
|
||||
|
||||
public static string GetErrorDetails(GLEnum error) {
|
||||
return error switch {
|
||||
GLEnum.InvalidEnum => "Invalid enum - An unacceptable value is specified for an enumerated argument",
|
||||
GLEnum.InvalidValue => "Invalid value - A numeric argument is out of range",
|
||||
GLEnum.InvalidOperation =>
|
||||
"Invalid operation - The specified operation is not allowed in the current state",
|
||||
GLEnum.StackOverflow => "Stack overflow - An operation would cause an internal stack to overflow",
|
||||
GLEnum.StackUnderflow => "Stack underflow - An operation would cause an internal stack to underflow",
|
||||
GLEnum.OutOfMemory => "Out of memory - There is not enough memory left to execute the command",
|
||||
GLEnum.InvalidFramebufferOperation =>
|
||||
"Invalid framebuffer operation - The framebuffer object is not complete",
|
||||
GLEnum.ContextLost => "Context lost - The OpenGL context has been lost due to a graphics card reset",
|
||||
_ => "Unknown error"
|
||||
};
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// Checks for OpenGL errors and provides context-specific information
|
||||
/// </summary>
|
||||
public static void CheckErrorsWithContext(GL gl, string context, [CallerMemberName] string callerName = "",
|
||||
[CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) {
|
||||
var error = gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
string errorDetails = GetErrorDetails(error);
|
||||
string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}";
|
||||
string message = $"OpenGL Error: {error} ({errorDetails})\nContext: {context}\nLocation: {location}";
|
||||
|
||||
Logger?.LogError(message);
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
#else
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CheckErrorsWithContext(GL gl, string context, string callerName = "",
|
||||
string callerFile = "", int callerLine = 0) {
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed information about the current texture state for debugging
|
||||
/// </summary>
|
||||
public static string GetTextureDebugInfo(GL gl, GLEnum target) {
|
||||
var info = new System.Text.StringBuilder();
|
||||
info.AppendLine($"Texture Debug Info for {target}:");
|
||||
|
||||
try {
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureWidth, out int width);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureHeight, out int height);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureDepthExt, out int depth);
|
||||
gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0,
|
||||
GetTextureParameter.TextureInternalFormat, out int format);
|
||||
|
||||
info.AppendLine($" Dimensions: {width}x{height}x{depth}");
|
||||
info.AppendLine($" Internal Format: {(InternalFormat)format}");
|
||||
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMinFilter, out int minFilter);
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMagFilter, out int magFilter);
|
||||
info.AppendLine($" Min Filter: {(TextureMinFilter)minFilter}");
|
||||
info.AppendLine($" Mag Filter: {(TextureMagFilter)magFilter}");
|
||||
|
||||
// Get max mipmap level
|
||||
gl.GetTexParameter(target, GetTextureParameter.TextureMaxLevelSgis, out int maxLevel);
|
||||
info.AppendLine($" Max Level: {maxLevel}");
|
||||
|
||||
// Check completeness
|
||||
int maxMipLevel = (int)Math.Floor(Math.Log2(Math.Max(width, height)));
|
||||
info.AppendLine($" Calculated Max Mip Level: {maxMipLevel}");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
info.AppendLine($" Error getting texture info: {ex.Message}");
|
||||
}
|
||||
|
||||
return info.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs current OpenGL state for debugging
|
||||
/// </summary>
|
||||
public static void LogGLState(GL gl, string context = "") {
|
||||
var state = new System.Text.StringBuilder();
|
||||
state.AppendLine($"=== OpenGL State ({context}) ===");
|
||||
|
||||
try {
|
||||
state.AppendLine(
|
||||
$"Active Texture Unit: GL_TEXTURE{gl.GetInteger(GetPName.ActiveTexture) - (int)GLEnum.Texture0}");
|
||||
state.AppendLine($"Bound 2D Array Texture: {gl.GetInteger(GetPName.TextureBinding2DArray)}");
|
||||
state.AppendLine($"Current Program: {gl.GetInteger(GetPName.CurrentProgram)}");
|
||||
|
||||
gl.GetInteger(GetPName.MaxTextureSize, out int maxTexSize);
|
||||
state.AppendLine($"Max Texture Size: {maxTexSize}");
|
||||
|
||||
gl.GetInteger(GetPName.Max3DTextureSize, out int max3DSize);
|
||||
state.AppendLine($"Max 3D Texture Size: {max3DSize}");
|
||||
|
||||
gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maxLayers);
|
||||
state.AppendLine($"Max Array Texture Layers: {maxLayers}");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
state.AppendLine($"Error getting GL state: {ex.Message}");
|
||||
}
|
||||
|
||||
state.AppendLine("======================");
|
||||
Logger?.LogInformation(state.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicit defaults to prevent Avalonia state leakage into our custom rendering pipeline.
|
||||
/// Call this at the start of complex render cycles immediately inside a GLStateScope.
|
||||
/// </summary>
|
||||
public static void SetupDefaultRenderState(GL gl) {
|
||||
gl.BindSampler(0, 0);
|
||||
gl.BindSampler(1, 0);
|
||||
gl.BindSampler(2, 0);
|
||||
|
||||
gl.ActiveTexture(TextureUnit.Texture1);
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
gl.ActiveTexture(TextureUnit.Texture2);
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
gl.ActiveTexture(TextureUnit.Texture0); // End on Texture0
|
||||
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
|
||||
gl.BindVertexArray(0);
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
|
||||
gl.UseProgram(0);
|
||||
|
||||
gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
|
||||
gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
|
||||
|
||||
gl.Disable(EnableCap.StencilTest);
|
||||
gl.BlendColor(0, 0, 0, 0);
|
||||
gl.PolygonMode(GLEnum.FrontAndBack, PolygonMode.Fill);
|
||||
|
||||
// Disable Avalonia/Skia specific states
|
||||
gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
gl.Disable(EnableCap.SampleAlphaToOne);
|
||||
gl.Disable(EnableCap.Multisample);
|
||||
gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
|
||||
gl.LineWidth(1.0f);
|
||||
gl.PolygonOffset(0f, 0f);
|
||||
gl.Disable(EnableCap.PolygonOffsetFill);
|
||||
gl.Disable((EnableCap)GLEnum.ProgramPointSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using AcDream.App.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
|
||||
public unsafe class GLSLShader : BaseShader, IDisposable {
|
||||
private OpenGLGraphicsDevice _device;
|
||||
private Dictionary<string, int> _uniformLocations = [];
|
||||
private Dictionary<int, object> _uniformValues = [];
|
||||
private readonly object _lock = new();
|
||||
private GL GL => _device.GL;
|
||||
public uint Program { get; protected set; }
|
||||
|
||||
public bool HasUniform(string name) {
|
||||
lock (_lock) {
|
||||
return GetUniformLocation(Program, name) != -1;
|
||||
}
|
||||
}
|
||||
|
||||
public GLSLShader(OpenGLGraphicsDevice device, string name, string vertSource, string fragSource, ILogger log) : base(name, vertSource, fragSource, log) {
|
||||
_device = device;
|
||||
|
||||
Load(vertSource, fragSource);
|
||||
}
|
||||
|
||||
public GLSLShader(OpenGLGraphicsDevice device, string name, string shaderDirectory, ILogger log) : base(name, shaderDirectory, log) {
|
||||
_device = device;
|
||||
|
||||
Load();
|
||||
}
|
||||
|
||||
public override void Dispose() {
|
||||
Unload();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
private int GetUniformLocation(uint program, string name) {
|
||||
lock (_lock) {
|
||||
if (!_uniformLocations.ContainsKey(name)) {
|
||||
_uniformLocations.Add(name, GL.GetUniformLocation(program, name));
|
||||
}
|
||||
return _uniformLocations[name];
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Matrix4x4 m) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation(Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Matrix4x4 mCached && mCached == m) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = m;
|
||||
|
||||
GL.UniformMatrix4(loc, 1, false, (float*)&m);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, int v) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is int vCached && vCached == v) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = v;
|
||||
|
||||
GL.Uniform1(loc, v);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector2 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector2 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform2(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector3 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector3 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform3(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void SetUniform(string location, Vector3[] vecs) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
fixed (float* v = &vecs[0].X) {
|
||||
GL.Uniform3(loc, (uint)vecs.Length, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, Vector4 vec) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is Vector4 vCached && vCached == vec) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = vec;
|
||||
|
||||
GL.Uniform4(loc, vec);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, float v) {
|
||||
lock (_lock) {
|
||||
int loc = GetUniformLocation((uint)Program, location);
|
||||
if (loc == -1) return;
|
||||
|
||||
if (_uniformValues.TryGetValue(loc, out var val) && val is float vCached && vCached == v) {
|
||||
return;
|
||||
}
|
||||
_uniformValues[loc] = v;
|
||||
|
||||
GL.Uniform1(loc, v);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetUniform(string location, float[] vs) {
|
||||
lock (_lock) {
|
||||
fixed (float* v = &vs[0]) {
|
||||
GL.Uniform1(GetUniformLocation((uint)Program, location), (uint)vs.Length, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Load(string vertShaderSource, string fragShaderSource) {
|
||||
|
||||
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
|
||||
_log.LogError($"Shader {Name} has no source code!");
|
||||
throw new InvalidOperationException($"Shader {Name} has no source code.");
|
||||
}
|
||||
|
||||
if (_device.HasOpenGL43 && _device.HasBindless) {
|
||||
string replacement = "#version 430 core\n#extension GL_ARB_bindless_texture : require";
|
||||
vertShaderSource = vertShaderSource.Replace("#version 330 core", replacement);
|
||||
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
|
||||
}
|
||||
|
||||
var resources = new ResourceCleanupGroup();
|
||||
uint prog = 0;
|
||||
bool accountingPublished = false;
|
||||
try {
|
||||
prog = ShaderProgramConstruction.Build(
|
||||
new GlShaderProgramBuildApi(GL),
|
||||
vertShaderSource,
|
||||
fragShaderSource);
|
||||
uint ownedProgram = prog;
|
||||
var unpublishedProgramRelease = new RetryableGpuResourceRelease(
|
||||
() => GlResourceCommand.DeleteProgram(
|
||||
GL,
|
||||
ownedProgram,
|
||||
$"delete unpublished WB shader program {ownedProgram}"),
|
||||
() => {
|
||||
if (accountingPublished)
|
||||
{
|
||||
GpuMemoryTracker.TrackResourceDeallocation(
|
||||
GpuResourceType.Shader);
|
||||
}
|
||||
});
|
||||
resources.Add("WB shader program", unpublishedProgramRelease.Run);
|
||||
|
||||
// Bind SceneData uniform block to point 0 if it exists.
|
||||
GlResourceCommand.Execute(GL, $"configure shader {Name} SceneData binding", () => {
|
||||
uint sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
|
||||
if (sceneDataIndex != uint.MaxValue)
|
||||
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
|
||||
});
|
||||
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
|
||||
accountingPublished = true;
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
_log.LogError(constructionFailure, "Failed to construct shader {ShaderName}", Name);
|
||||
resources.RollbackConstructionAndThrow(
|
||||
$"Shader {Name} construction failed and its GL program did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
|
||||
|
||||
if (Program != 0) {
|
||||
Unload();
|
||||
}
|
||||
_uniformLocations.Clear();
|
||||
_uniformValues.Clear();
|
||||
|
||||
Program = prog;
|
||||
ProgramId = prog;
|
||||
NeedsLoad = false;
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public override void Bind() {
|
||||
lock (_lock) {
|
||||
SetActive();
|
||||
if (Program != 0) {
|
||||
GL.UseProgram((uint)Program);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Unbind() {
|
||||
lock (_lock) {
|
||||
GL.UseProgram(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Unload() {
|
||||
lock (_lock) {
|
||||
if (Program != 0) {
|
||||
var prog = Program;
|
||||
Program = 0;
|
||||
ProgramId = 0;
|
||||
_device.QueueGLAction(gl => {
|
||||
gl.DeleteProgram(prog);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Shader);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// A RAII scope for saving and restoring OpenGL state.
|
||||
/// </summary>
|
||||
public unsafe struct GLStateScope : IDisposable {
|
||||
private readonly GL _gl;
|
||||
private fixed int _viewport[4];
|
||||
private bool _scissorTest;
|
||||
private fixed int _scissorBox[4];
|
||||
private bool _depthTest;
|
||||
private int _depthFunc;
|
||||
private bool _depthMask;
|
||||
private bool _cullFace;
|
||||
private int _cullFaceMode;
|
||||
private int _frontFace;
|
||||
private bool _blend;
|
||||
private int _blendSrc;
|
||||
private int _blendDst;
|
||||
private int _blendEquation;
|
||||
|
||||
// Extended state
|
||||
private int _blendSrcAlpha;
|
||||
private int _blendDstAlpha;
|
||||
private int _blendEquationAlpha;
|
||||
private fixed byte _colorMask[4];
|
||||
private fixed float _clearColor[4];
|
||||
private float _clearDepth;
|
||||
private int _currentProgram;
|
||||
private int _vertexArrayBinding;
|
||||
private int _arrayBufferBinding;
|
||||
private int _elementArrayBufferBinding;
|
||||
private int _activeTexture;
|
||||
private int _textureBinding2D;
|
||||
private bool _stencilTest;
|
||||
private int _stencilFunc;
|
||||
private int _stencilRef;
|
||||
private int _stencilValueMask;
|
||||
private int _stencilFail;
|
||||
private int _stencilPassDepthFail;
|
||||
private int _stencilPassDepthPass;
|
||||
private int _stencilWritemask;
|
||||
private int _unpackAlignment;
|
||||
private int _packAlignment;
|
||||
|
||||
private int _drawFramebufferBinding;
|
||||
|
||||
// Skia / Avalonia extra state protections
|
||||
private fixed float _blendColor[4];
|
||||
private int _polygonMode;
|
||||
private bool _sampleAlphaToCoverage;
|
||||
private bool _multisample;
|
||||
private bool _primitiveRestart;
|
||||
private int _readFramebufferBinding;
|
||||
private int _uniformBufferBinding0;
|
||||
private float _lineWidth;
|
||||
private bool _programPointSize;
|
||||
private int _samplerBinding0;
|
||||
private int _samplerBinding1;
|
||||
private int _samplerBinding2;
|
||||
private int _unpackRowLength;
|
||||
private int _unpackSkipRows;
|
||||
private int _unpackSkipPixels;
|
||||
private bool _sampleAlphaToOne;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current OpenGL state.
|
||||
/// </summary>
|
||||
/// <param name="gl"></param>
|
||||
public GLStateScope(GL gl) {
|
||||
_gl = gl;
|
||||
_isDisposed = false;
|
||||
|
||||
fixed (int* v = _viewport) _gl.GetInteger(GetPName.Viewport, v);
|
||||
_scissorTest = _gl.IsEnabled(EnableCap.ScissorTest);
|
||||
fixed (int* s = _scissorBox) _gl.GetInteger(GetPName.ScissorBox, s);
|
||||
|
||||
_depthTest = _gl.IsEnabled(EnableCap.DepthTest);
|
||||
_gl.GetInteger(GetPName.DepthFunc, out _depthFunc);
|
||||
byte depthMask = 0;
|
||||
_gl.GetBoolean((GetPName)GLEnum.DepthWritemask, (bool*)&depthMask);
|
||||
_depthMask = depthMask != 0;
|
||||
|
||||
_cullFace = _gl.IsEnabled(EnableCap.CullFace);
|
||||
_gl.GetInteger(GetPName.CullFaceMode, out _cullFaceMode);
|
||||
_gl.GetInteger(GetPName.FrontFace, out _frontFace);
|
||||
|
||||
_blend = _gl.IsEnabled(EnableCap.Blend);
|
||||
_gl.GetInteger(GetPName.BlendSrcRgb, out _blendSrc);
|
||||
_gl.GetInteger(GetPName.BlendDstRgb, out _blendDst);
|
||||
_gl.GetInteger(GetPName.BlendSrcAlpha, out _blendSrcAlpha);
|
||||
_gl.GetInteger(GetPName.BlendDstAlpha, out _blendDstAlpha);
|
||||
_gl.GetInteger(GetPName.BlendEquationRgb, out _blendEquation);
|
||||
_gl.GetInteger(GetPName.BlendEquationAlpha, out _blendEquationAlpha);
|
||||
|
||||
fixed (byte* c = _colorMask) _gl.GetBoolean((GetPName)GLEnum.ColorWritemask, (bool*)c);
|
||||
fixed (float* cc = _clearColor) _gl.GetFloat(GetPName.ColorClearValue, cc);
|
||||
_gl.GetFloat(GetPName.DepthClearValue, out _clearDepth);
|
||||
|
||||
_gl.GetInteger(GetPName.CurrentProgram, out _currentProgram);
|
||||
_gl.GetInteger(GetPName.VertexArrayBinding, out _vertexArrayBinding);
|
||||
_gl.GetInteger(GetPName.ArrayBufferBinding, out _arrayBufferBinding);
|
||||
_gl.GetInteger(GetPName.ElementArrayBufferBinding, out _elementArrayBufferBinding);
|
||||
|
||||
_gl.GetInteger(GetPName.ActiveTexture, out _activeTexture);
|
||||
_gl.GetInteger(GetPName.TextureBinding2D, out _textureBinding2D);
|
||||
|
||||
_stencilTest = _gl.IsEnabled(EnableCap.StencilTest);
|
||||
_gl.GetInteger(GetPName.StencilFunc, out _stencilFunc);
|
||||
_gl.GetInteger(GetPName.StencilRef, out _stencilRef);
|
||||
_gl.GetInteger(GetPName.StencilValueMask, out _stencilValueMask);
|
||||
_gl.GetInteger(GetPName.StencilFail, out _stencilFail);
|
||||
_gl.GetInteger(GetPName.StencilPassDepthFail, out _stencilPassDepthFail);
|
||||
_gl.GetInteger(GetPName.StencilPassDepthPass, out _stencilPassDepthPass);
|
||||
_gl.GetInteger(GetPName.StencilWritemask, out _stencilWritemask);
|
||||
|
||||
_gl.GetInteger(GetPName.UnpackAlignment, out _unpackAlignment);
|
||||
_gl.GetInteger(GetPName.PackAlignment, out _packAlignment);
|
||||
|
||||
_gl.GetInteger(GetPName.DrawFramebufferBinding, out _drawFramebufferBinding);
|
||||
|
||||
fixed (float* bc = _blendColor) _gl.GetFloat(GetPName.BlendColor, bc);
|
||||
_gl.GetInteger(GetPName.PolygonMode, out _polygonMode);
|
||||
_sampleAlphaToCoverage = _gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
|
||||
_multisample = _gl.IsEnabled(EnableCap.Multisample);
|
||||
_primitiveRestart = _gl.IsEnabled((EnableCap)GLEnum.PrimitiveRestart);
|
||||
_gl.GetInteger(GetPName.ReadFramebufferBinding, out _readFramebufferBinding);
|
||||
|
||||
_gl.GetInteger(GetPName.UniformBufferBinding, out _uniformBufferBinding0);
|
||||
|
||||
_gl.GetFloat(GetPName.LineWidth, out _lineWidth);
|
||||
_programPointSize = _gl.IsEnabled((EnableCap)GLEnum.ProgramPointSize);
|
||||
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding0);
|
||||
_gl.ActiveTexture(TextureUnit.Texture1);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding1);
|
||||
_gl.ActiveTexture(TextureUnit.Texture2);
|
||||
_gl.GetInteger((GetPName)GLEnum.SamplerBinding, out _samplerBinding2);
|
||||
_gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackRowLength, out _unpackRowLength);
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackSkipRows, out _unpackSkipRows);
|
||||
_gl.GetInteger((GetPName)GLEnum.UnpackSkipPixels, out _unpackSkipPixels);
|
||||
_sampleAlphaToOne = _gl.IsEnabled(EnableCap.SampleAlphaToOne);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores only the scissor state from the scope.
|
||||
/// </summary>
|
||||
public void RestoreScissor() {
|
||||
if (_scissorTest) _gl.Enable(EnableCap.ScissorTest);
|
||||
else _gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.Scissor(_scissorBox[0], _scissorBox[1], (uint)_scissorBox[2], (uint)_scissorBox[3]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the captured OpenGL state.
|
||||
/// </summary>
|
||||
public void Dispose() {
|
||||
if (_isDisposed) return;
|
||||
|
||||
// Restoring state
|
||||
if (_currentProgram != 0) _gl.UseProgram((uint)_currentProgram); else _gl.UseProgram(0);
|
||||
|
||||
_gl.BindVertexArray((uint)_vertexArrayBinding);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBufferBinding);
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, (uint)_elementArrayBufferBinding);
|
||||
_gl.BindBuffer(GLEnum.UniformBuffer, (uint)_uniformBufferBinding0);
|
||||
|
||||
_gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, (uint)_textureBinding2D);
|
||||
|
||||
if (_stencilTest) _gl.Enable(EnableCap.StencilTest); else _gl.Disable(EnableCap.StencilTest);
|
||||
_gl.StencilFunc((StencilFunction)_stencilFunc, _stencilRef, (uint)_stencilValueMask);
|
||||
_gl.StencilOp((StencilOp)_stencilFail, (StencilOp)_stencilPassDepthFail, (StencilOp)_stencilPassDepthPass);
|
||||
_gl.StencilMask((uint)_stencilWritemask);
|
||||
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, _unpackAlignment);
|
||||
_gl.PixelStore(PixelStoreParameter.PackAlignment, _packAlignment);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackRowLength, _unpackRowLength);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackSkipRows, _unpackSkipRows);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, _unpackSkipPixels);
|
||||
|
||||
_gl.ClearColor(_clearColor[0], _clearColor[1], _clearColor[2], _clearColor[3]);
|
||||
_gl.ClearDepth(_clearDepth);
|
||||
|
||||
_gl.Viewport(_viewport[0], _viewport[1], (uint)_viewport[2], (uint)_viewport[3]);
|
||||
RestoreScissor();
|
||||
|
||||
if (_depthTest) _gl.Enable(EnableCap.DepthTest); else _gl.Disable(EnableCap.DepthTest);
|
||||
_gl.DepthFunc((DepthFunction)_depthFunc);
|
||||
_gl.DepthMask(_depthMask);
|
||||
|
||||
if (_cullFace) _gl.Enable(EnableCap.CullFace); else _gl.Disable(EnableCap.CullFace);
|
||||
_gl.CullFace((TriangleFace)_cullFaceMode);
|
||||
_gl.FrontFace((FrontFaceDirection)_frontFace);
|
||||
|
||||
if (_blend) _gl.Enable(EnableCap.Blend); else _gl.Disable(EnableCap.Blend);
|
||||
_gl.BlendFuncSeparate((BlendingFactor)_blendSrc, (BlendingFactor)_blendDst, (BlendingFactor)_blendSrcAlpha, (BlendingFactor)_blendDstAlpha);
|
||||
_gl.BlendEquationSeparate((BlendEquationModeEXT)_blendEquation, (BlendEquationModeEXT)_blendEquationAlpha);
|
||||
_gl.BlendColor(_blendColor[0], _blendColor[1], _blendColor[2], _blendColor[3]);
|
||||
|
||||
_gl.ColorMask(_colorMask[0] != 0, _colorMask[1] != 0, _colorMask[2] != 0, _colorMask[3] != 0);
|
||||
|
||||
_gl.PolygonMode(GLEnum.FrontAndBack, (PolygonMode)_polygonMode);
|
||||
|
||||
if (_sampleAlphaToCoverage) _gl.Enable(EnableCap.SampleAlphaToCoverage); else _gl.Disable(EnableCap.SampleAlphaToCoverage);
|
||||
if (_sampleAlphaToOne) _gl.Enable(EnableCap.SampleAlphaToOne); else _gl.Disable(EnableCap.SampleAlphaToOne);
|
||||
if (_multisample) _gl.Enable(EnableCap.Multisample); else _gl.Disable(EnableCap.Multisample);
|
||||
if (_primitiveRestart) _gl.Enable((EnableCap)GLEnum.PrimitiveRestart); else _gl.Disable((EnableCap)GLEnum.PrimitiveRestart);
|
||||
if (_programPointSize) _gl.Enable((EnableCap)GLEnum.ProgramPointSize); else _gl.Disable((EnableCap)GLEnum.ProgramPointSize);
|
||||
|
||||
_gl.LineWidth(_lineWidth);
|
||||
|
||||
_gl.BindSampler(0, (uint)_samplerBinding0);
|
||||
_gl.BindSampler(1, (uint)_samplerBinding1);
|
||||
_gl.BindSampler(2, (uint)_samplerBinding2);
|
||||
|
||||
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, (uint)_drawFramebufferBinding);
|
||||
_gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, (uint)_readFramebufferBinding);
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using AcDream.Content;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Gl;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
|
|
@ -53,15 +51,6 @@ internal sealed class GlobalMeshMigrationAbortTicket
|
|||
public void Advance() => _release.Run();
|
||||
}
|
||||
|
||||
internal static class GlobalMeshVaoAccounting
|
||||
{
|
||||
public static void TrackAllocation() =>
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
|
||||
|
||||
public static void TrackDeallocation() =>
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
|
||||
}
|
||||
|
||||
internal enum GlobalMeshCapacityResult
|
||||
{
|
||||
Ready,
|
||||
|
|
@ -82,19 +71,15 @@ internal enum GlobalMeshCapacityResult
|
|||
/// backend implements with <c>vkCmdCopyBuffer</c>. The reclaimable-range
|
||||
/// allocator, growth quanta, budgeted incremental migration, retirement-ledger
|
||||
/// gating and the dual-generation physical ceiling are unchanged; only the
|
||||
/// resource handle type moved. The vertex array object stays raw GL because a
|
||||
/// VAO has no RHI equivalent (Vulkan bakes vertex input into the pipeline) and
|
||||
/// <c>WbDrawDispatcher</c>, <c>EnvCellRenderer</c> and <c>ParticleRenderer</c>
|
||||
/// still bind <see cref="VAO"/>/<see cref="VBO"/>/<see cref="IBO"/> directly
|
||||
/// on the GL arm.
|
||||
/// resource handle type moved.
|
||||
///
|
||||
/// <para>Campaign V slice V6i-3 made the GL context optional. A backend that has
|
||||
/// none builds no vertex array and publishes no raw names — <see cref="VAO"/>,
|
||||
/// <see cref="VBO"/> and <see cref="IBO"/> are 0 there — and its consumers bind
|
||||
/// <para>Campaign V slice V6i-3 made the GL context (and its vertex array
|
||||
/// object, which has no RHI equivalent — Vulkan bakes vertex input into the
|
||||
/// pipeline) optional; Campaign V slice V11 deleted the GL arm entirely, so
|
||||
/// this arena now only ever builds the two backing stores. Its consumers bind
|
||||
/// <see cref="VertexStore"/> and <see cref="IndexStore"/> through the pass
|
||||
/// encoder instead, which is the same 32-byte position/normal/texcoord layout
|
||||
/// expressed as pipeline vertex input. Everything above the handle — the
|
||||
/// allocator, the migration, the ledger — is one body on both arms.</para>
|
||||
/// encoder, which is the same 32-byte position/normal/texcoord layout
|
||||
/// expressed as pipeline vertex input.</para>
|
||||
/// </summary>
|
||||
public sealed class GlobalMeshBuffer : IDisposable
|
||||
{
|
||||
|
|
@ -113,10 +98,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
internal const int MaximumIndexCapacity =
|
||||
(int)(MaximumIndexBufferBytes / sizeof(ushort));
|
||||
|
||||
// Retained only for the vertex array object and its attribute layout, which
|
||||
// the RHI has no verb for, and null on a backend with no such object. It is
|
||||
// retired with the raw-GL dispatcher.
|
||||
private readonly GL? _gl;
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly GpuRetirementLedger _retirementLedger;
|
||||
private readonly GpuRetiredRangeAllocator _vertices;
|
||||
|
|
@ -134,20 +115,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
store ?? throw new InvalidOperationException(
|
||||
"The global mesh arena has no live backing store.");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4b transitional bridge. The arena owns its stores as
|
||||
/// <see cref="IGpuBuffer"/>, but its consumers — the vertex array object here,
|
||||
/// and <c>WbDrawDispatcher</c>/<c>EnvCellRenderer</c>/<c>ParticleRenderer</c>
|
||||
/// through <see cref="VBO"/>/<see cref="IBO"/> — are still raw GL until slice
|
||||
/// V4c. This is the only place that reaches through the interface, and it
|
||||
/// disappears with those consumers.
|
||||
/// </summary>
|
||||
private static GlGpuBuffer RequireGlBuffer(IGpuBuffer buffer) =>
|
||||
buffer as GlGpuBuffer
|
||||
?? throw new NotSupportedException(
|
||||
"The global mesh arena requires a GL-backed buffer while its draw paths "
|
||||
+ "still bind raw GL names (Campaign V slice V4c retires that requirement).");
|
||||
|
||||
private enum BufferKind
|
||||
{
|
||||
Vertices,
|
||||
|
|
@ -167,35 +134,16 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
public long CopiedBytes { get; set; }
|
||||
}
|
||||
|
||||
public uint VAO { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The vertex store's raw GL name, or 0 on a backend with no GL context.
|
||||
/// Transitional: the GL draw paths still bind the arena themselves, so the
|
||||
/// arena keeps publishing the backend name of the buffer it now owns as an
|
||||
/// <see cref="IGpuBuffer"/>.
|
||||
/// </summary>
|
||||
public uint VBO =>
|
||||
_gl is null || _vertexBuffer is null ? 0u : RequireGlBuffer(_vertexBuffer).GlName;
|
||||
|
||||
/// <summary>The index store's raw GL name. See <see cref="VBO"/>.</summary>
|
||||
public uint IBO =>
|
||||
_gl is null || _indexBuffer is null ? 0u : RequireGlBuffer(_indexBuffer).GlName;
|
||||
|
||||
/// <summary>
|
||||
/// The vertex store as the contract's own handle. This is what a pass
|
||||
/// encoder binds, and it is live on both arms — <see cref="VAO"/> is the
|
||||
/// GL-only expression of the same thing.
|
||||
/// encoder binds.
|
||||
/// </summary>
|
||||
internal IGpuBuffer? VertexStore => _vertexBuffer;
|
||||
|
||||
/// <summary>The index store as the contract's own handle. See <see cref="VertexStore"/>.</summary>
|
||||
internal IGpuBuffer? IndexStore => _indexBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// True once both backing stores exist. The backend-neutral form of the
|
||||
/// <c>VAO != 0</c> readiness test the raw-GL draw paths make.
|
||||
/// </summary>
|
||||
/// <summary>True once both backing stores exist.</summary>
|
||||
internal bool HasStores => _vertexBuffer is not null && _indexBuffer is not null;
|
||||
internal long UploadCount { get; private set; }
|
||||
internal long UploadedBytes { get; private set; }
|
||||
|
|
@ -268,9 +216,8 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
newBuffers);
|
||||
}
|
||||
|
||||
internal GlobalMeshBuffer(GL? gl, IGpuDevice device, IGpuResourceRetirementQueue retirement)
|
||||
internal GlobalMeshBuffer(IGpuDevice device, IGpuResourceRetirementQueue retirement)
|
||||
{
|
||||
_gl = gl;
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
ArgumentNullException.ThrowIfNull(retirement);
|
||||
_retirementLedger = new GpuRetirementLedger(retirement);
|
||||
|
|
@ -295,47 +242,20 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
| GpuBufferUsage.TransferDestination,
|
||||
GpuMemoryResidency.DeviceLocal);
|
||||
|
||||
private unsafe void InitBuffers()
|
||||
private void InitBuffers()
|
||||
{
|
||||
uint vao = 0;
|
||||
IGpuBuffer? vbo = null;
|
||||
IGpuBuffer? ibo = null;
|
||||
long vertexBytes = (long)_vertices.Capacity * VertexPositionNormalTexture.Size;
|
||||
long indexBytes = (long)_indices.Capacity * sizeof(ushort);
|
||||
bool vaoTracked = false;
|
||||
bool vertexTracked = false;
|
||||
bool indexTracked = false;
|
||||
|
||||
try
|
||||
{
|
||||
// The vertex array is the one object here with no RHI equivalent —
|
||||
// Vulkan bakes vertex input into the pipeline — so a backend with no
|
||||
// GL context builds the two stores and nothing else.
|
||||
if (_gl is { } gl)
|
||||
{
|
||||
gl.GenVertexArrays(1, out vao);
|
||||
if (vao == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create the global mesh-buffer objects.");
|
||||
}
|
||||
|
||||
vbo = _device.CreateBuffer(DescribeStore(BufferKind.Vertices, vertexBytes, _storeGeneration));
|
||||
ibo = _device.CreateBuffer(DescribeStore(BufferKind.Indices, indexBytes, _storeGeneration));
|
||||
|
||||
if (_gl is { } glBind)
|
||||
{
|
||||
glBind.BindVertexArray(vao);
|
||||
glBind.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(vbo).GlName);
|
||||
ConfigureVertexAttributes(glBind);
|
||||
|
||||
glBind.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(ibo).GlName);
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
glBind,
|
||||
$"creating global mesh buffers ({vertexBytes} vertex bytes, {indexBytes} index bytes)");
|
||||
|
||||
GlobalMeshVaoAccounting.TrackAllocation();
|
||||
vaoTracked = true;
|
||||
}
|
||||
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GpuMemoryTracker.TrackAllocation(vertexBytes, GpuResourceType.Buffer);
|
||||
vertexTracked = true;
|
||||
|
|
@ -343,25 +263,15 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
GpuMemoryTracker.TrackAllocation(indexBytes, GpuResourceType.Buffer);
|
||||
indexTracked = true;
|
||||
|
||||
VAO = vao;
|
||||
_vertexBuffer = vbo;
|
||||
_indexBuffer = ibo;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Construction rollback: nothing was ever submitted, so the physical
|
||||
// stores are released on the spot rather than deferred. Pattern-matched
|
||||
// rather than RequireGlBuffer'd so a non-GL store could never raise a
|
||||
// cast failure that masks the original construction exception.
|
||||
if (ibo is GlGpuBuffer stagedIndexStore)
|
||||
stagedIndexStore.DeleteRetired("rolling back the global index arena buffer");
|
||||
else
|
||||
ibo?.Dispose();
|
||||
if (vbo is GlGpuBuffer stagedVertexStore)
|
||||
stagedVertexStore.DeleteRetired("rolling back the global vertex arena buffer");
|
||||
else
|
||||
vbo?.Dispose();
|
||||
if (vao != 0) _gl!.DeleteVertexArray(vao);
|
||||
// stores are released on the spot rather than deferred.
|
||||
ibo?.Dispose();
|
||||
vbo?.Dispose();
|
||||
if (indexTracked)
|
||||
{
|
||||
GpuMemoryTracker.TrackDeallocation(indexBytes, GpuResourceType.Buffer);
|
||||
|
|
@ -372,25 +282,8 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
GpuMemoryTracker.TrackDeallocation(vertexBytes, GpuResourceType.Buffer);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
}
|
||||
if (vaoTracked)
|
||||
GlobalMeshVaoAccounting.TrackDeallocation();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gl?.BindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void ConfigureVertexAttributes(GL gl)
|
||||
{
|
||||
int stride = VertexPositionNormalTexture.Size;
|
||||
gl.EnableVertexAttribArray(0);
|
||||
gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
|
||||
gl.EnableVertexAttribArray(1);
|
||||
gl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
|
||||
gl.EnableVertexAttribArray(2);
|
||||
gl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
}
|
||||
|
||||
internal GlobalMeshAllocation UploadMesh(
|
||||
|
|
@ -673,9 +566,9 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Copies at most <paramref name="maximumCopyBytes"/> of the immutable
|
||||
/// live prefix into the staged backing store. The active VAO continues to
|
||||
/// reference the old store until the final chunk succeeds, then one atomic
|
||||
/// VAO rebind publishes the destination.
|
||||
/// live prefix into the staged backing store. Draws continue to reference
|
||||
/// the old store until the final chunk succeeds, then one atomic field swap
|
||||
/// (<see cref="CommitMigration"/>) publishes the destination.
|
||||
/// </summary>
|
||||
internal GlobalMeshMaintenanceStep AdvanceMigration(long maximumCopyBytes)
|
||||
{
|
||||
|
|
@ -819,48 +712,9 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
private void CommitMigration(BufferMigration migration)
|
||||
{
|
||||
// The atomic publication step is a VAO rebind on GL and nothing at all
|
||||
// on a backend whose vertex source is a per-draw encoder bind: the field
|
||||
// swap below IS the publication there, and the next pass reads the new
|
||||
// store. The rollback arm exists for the same reason it did — a failed
|
||||
// rebind must leave the vertex array pointing at the live store.
|
||||
if (_gl is { } gl)
|
||||
{
|
||||
try
|
||||
{
|
||||
gl.BindVertexArray(VAO);
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
|
||||
ConfigureVertexAttributes(gl);
|
||||
}
|
||||
else
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.NewBuffer).GlName);
|
||||
}
|
||||
GLHelpers.ThrowOnResourceError(gl, $"publishing staged {migration.Kind} arena buffer");
|
||||
}
|
||||
catch
|
||||
{
|
||||
gl.BindVertexArray(VAO);
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
|
||||
ConfigureVertexAttributes(gl);
|
||||
}
|
||||
else
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ElementArrayBuffer, RequireGlBuffer(migration.OldBuffer).GlName);
|
||||
}
|
||||
gl.BindVertexArray(0);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
gl.BindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
// The atomic publication step is nothing at all here: the vertex source
|
||||
// is a per-draw pass-encoder bind, so the field swap below IS the
|
||||
// publication, and the next pass reads the new store.
|
||||
if (migration.Kind == BufferKind.Vertices)
|
||||
{
|
||||
_vertexBuffer = migration.NewBuffer;
|
||||
|
|
@ -908,11 +762,10 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
/// <summary>
|
||||
/// The arena's own flight gate — <see cref="_retirementLedger"/> and the abort
|
||||
/// ticket — already proves no submitted frame can reference the store, so the
|
||||
/// physical delete runs here rather than being deferred a second time by
|
||||
/// <see cref="IGpuBuffer.Dispose"/>. Stages match
|
||||
/// <c>TrackedGlResource.CreateRetryableBufferDeletion</c> exactly: precondition,
|
||||
/// mutation-with-validation, byte accounting, then resource-count accounting,
|
||||
/// so a driver failure re-issues only the delete and never double-counts.
|
||||
/// physical delete runs here rather than being deferred a second time.
|
||||
/// Stages: precondition (no-op), mutation (dispose), byte accounting, then
|
||||
/// resource-count accounting, so a failure re-issues only the delete and
|
||||
/// never double-counts.
|
||||
/// </summary>
|
||||
private RetryableGpuResourceRelease CreateRetryableStoreDeletion(
|
||||
IGpuBuffer buffer,
|
||||
|
|
@ -921,24 +774,9 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(buffer);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
|
||||
GL? gl = _gl;
|
||||
return new RetryableGpuResourceRelease(
|
||||
() =>
|
||||
{
|
||||
if (gl is not null)
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
},
|
||||
// On GL the delete runs here rather than through IGpuBuffer.Dispose
|
||||
// because the arena's own flight gate has already proven no submitted
|
||||
// frame can reference the store. A backend with no GL context has no
|
||||
// second deferral to skip: Dispose IS its retirement-queued release.
|
||||
() =>
|
||||
{
|
||||
if (gl is not null)
|
||||
RequireGlBuffer(buffer).DeleteRetired(context);
|
||||
else
|
||||
buffer.Dispose();
|
||||
},
|
||||
() => { },
|
||||
() => buffer.Dispose(),
|
||||
() =>
|
||||
{
|
||||
if (capacityBytes != 0)
|
||||
|
|
@ -1020,16 +858,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
releases.Add(("staged-migration-buffer", release.Run));
|
||||
}
|
||||
|
||||
if (VAO != 0 && _gl is { } vaoGl)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
||||
vaoGl,
|
||||
VAO,
|
||||
$"deleting global mesh vertex array {VAO}",
|
||||
GlobalMeshVaoAccounting.TrackDeallocation);
|
||||
releases.Add(("global-vao", release.Run));
|
||||
}
|
||||
if (_vertexBuffer is { } vertexStore)
|
||||
{
|
||||
RetryableGpuResourceRelease release =
|
||||
|
|
@ -1058,7 +886,6 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|
||||
_migration = null;
|
||||
_migrationAbort = null;
|
||||
VAO = 0;
|
||||
_vertexBuffer = null;
|
||||
_indexBuffer = null;
|
||||
_disposeResources = null;
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// retirement queue, the shared instance VBO, and two capability flags. Seven
|
||||
/// members out of a 760-line class.</para>
|
||||
///
|
||||
/// <para>So the coupling is expressed as an interface at exactly that surface
|
||||
/// and <see cref="OpenGLGraphicsDevice"/> declares it — every member already
|
||||
/// existed, so the GL arm executes not one changed statement. What this buys is
|
||||
/// that <c>ObjectMeshManager</c> and <c>WbMeshAdapter</c> no longer NAME a
|
||||
/// backend, which is the prerequisite for the slice that gives them a second
|
||||
/// <para>So the coupling is expressed as an interface at exactly that surface,
|
||||
/// and <c>OpenGLGraphicsDevice</c> declared it — every member already existed,
|
||||
/// so the GL arm executed not one changed statement. What this buys is that
|
||||
/// <c>ObjectMeshManager</c> and <c>WbMeshAdapter</c> no longer NAME a backend,
|
||||
/// which is the prerequisite for the slice that gives them a second
|
||||
/// implementation.</para>
|
||||
///
|
||||
/// <para><b>What slice V6i-3 then moved.</b> V6i-2 left the upload bodies raw —
|
||||
|
|
@ -27,10 +27,16 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// GL but not RUN. The arena now builds its stores through
|
||||
/// <c>IGpuDevice.CreateBuffer</c> and publishes them as
|
||||
/// <c>GlobalMeshBuffer.VertexStore</c>/<c>IndexStore</c>, which a pass encoder
|
||||
/// binds; the vertex array is built only where one exists. What still reads
|
||||
/// <see cref="Gl"/> is the LEGACY per-mesh upload the N.5 ship amendment made
|
||||
/// unreachable, and <c>AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice</c>
|
||||
/// is the second implementation this interface was cut for.</para>
|
||||
/// binds; the vertex array is built only where one exists.
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice"/> is the
|
||||
/// second implementation this interface was cut for.</para>
|
||||
///
|
||||
/// <para><b>Campaign V slice V11</b> deleted <c>OpenGLGraphicsDevice</c> along
|
||||
/// with the rest of the raw-GL arm it fronted, so
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice"/> is now
|
||||
/// the interface's only implementation. <see cref="Gl"/> always answers null
|
||||
/// there; removing it (and the legacy per-mesh upload bodies it alone still
|
||||
/// gated) is Campaign V's package/shader cleanup slice, not this one.</para>
|
||||
/// </summary>
|
||||
internal interface IMeshPipelineDevice : IDisposable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Implementation of a framebuffer for OpenGL ES 3.0 using Silk.NET.
|
||||
/// </summary>
|
||||
public class ManagedGLFramebuffer : IFramebuffer {
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL _gl => _device.GL;
|
||||
private readonly uint _fboId;
|
||||
private readonly uint _depthStencilRenderbuffer; // 0 if not used
|
||||
private readonly ITexture _texture;
|
||||
private readonly int _width;
|
||||
private readonly int _height;
|
||||
|
||||
public ITexture Texture => _texture;
|
||||
public IntPtr NativeHandle => new IntPtr(_fboId);
|
||||
|
||||
public ManagedGLFramebuffer(OpenGLGraphicsDevice device, ITexture texture, int width, int height, bool hasDepthStencil) {
|
||||
_device = device;
|
||||
_texture = texture;
|
||||
_width = width;
|
||||
_height = height;
|
||||
|
||||
// Generate and bind the framebuffer
|
||||
_fboId = _gl.GenFramebuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.FBO);
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fboId);
|
||||
|
||||
// Attach the texture as the color attachment
|
||||
_gl.FramebufferTexture2D(
|
||||
FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.ColorAttachment0,
|
||||
TextureTarget.Texture2D,
|
||||
(uint)texture.NativePtr.ToInt32(),
|
||||
0
|
||||
);
|
||||
|
||||
// Create and attach a depth-stencil renderbuffer if requested
|
||||
if (true || hasDepthStencil) {
|
||||
_depthStencilRenderbuffer = _gl.GenRenderbuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.RBO);
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthStencilRenderbuffer);
|
||||
_gl.RenderbufferStorage(
|
||||
RenderbufferTarget.Renderbuffer,
|
||||
InternalFormat.Depth24Stencil8,
|
||||
(uint)width,
|
||||
(uint)height
|
||||
);
|
||||
_gl.FramebufferRenderbuffer(
|
||||
FramebufferTarget.Framebuffer,
|
||||
FramebufferAttachment.DepthStencilAttachment,
|
||||
RenderbufferTarget.Renderbuffer,
|
||||
_depthStencilRenderbuffer
|
||||
);
|
||||
GpuMemoryTracker.TrackAllocation(_width * _height * 4, GpuResourceType.RBO); // Depth24Stencil8 is 4 bytes per pixel
|
||||
}
|
||||
|
||||
// Check framebuffer completeness
|
||||
var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
|
||||
if (status != GLEnum.FramebufferComplete) {
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
_gl.DeleteFramebuffer(_fboId);
|
||||
if (_depthStencilRenderbuffer != 0) {
|
||||
_gl.DeleteRenderbuffer(_depthStencilRenderbuffer);
|
||||
}
|
||||
throw new InvalidOperationException($"Framebuffer creation failed: {status}");
|
||||
}
|
||||
|
||||
var error = _gl.GetError();
|
||||
if (error != GLEnum.NoError) {
|
||||
throw new InvalidOperationException($"OpenGL error during framebuffer setup: {error}");
|
||||
}
|
||||
|
||||
// Unbind the framebuffer
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
var fboId = _fboId;
|
||||
var depthStencilRenderbuffer = _depthStencilRenderbuffer;
|
||||
var width = _width;
|
||||
var height = _height;
|
||||
|
||||
_device.QueueGLAction(gl => {
|
||||
if (fboId != 0) {
|
||||
gl.DeleteFramebuffer(fboId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.FBO);
|
||||
}
|
||||
if (depthStencilRenderbuffer != 0) {
|
||||
gl.DeleteRenderbuffer(depthStencilRenderbuffer);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.RBO);
|
||||
GpuMemoryTracker.TrackDeallocation(width * height * 4, GpuResourceType.RBO);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Silk.NET.OpenGL;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL index buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLIndexBuffer : IIndexBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private void* _mappedPtr;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLIndexBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLIndexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device;
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Allocate the buffer with the specified size but no initial data
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_device.HasBufferStorage) {
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ElementArrayBuffer, (uint)Size, (void*)0, flags);
|
||||
_mappedPtr = GL.MapBufferRange(GLEnum.ElementArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)Size, (void*)0, Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetData(uint[] data) {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData(Span<uint> data) {
|
||||
uint dataSize = (uint)data.Length * sizeof(uint);
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<uint> mappedSpan = new Span<uint>(_mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[0]) {
|
||||
GL.BufferData(GLEnum.ElementArrayBuffer, dataSize, (void*)dataPtr, Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData(Span<uint> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
|
||||
|
||||
if (dataSizeBytes == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're not trying to write past the end of the buffer
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<uint> mappedSpan = new Span<uint>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
|
||||
GL.BufferSubData(
|
||||
GLEnum.ElementArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
(void*)dataPtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData(uint[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * sizeof(uint);
|
||||
|
||||
if (dataSizeBytes == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we're not trying to write past the end of the buffer
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
fixed (uint* dataPtr = &data[sourceOffsetElements]) {
|
||||
GL.BufferSubData(
|
||||
GLEnum.ElementArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
(void*)dataPtr);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Bind() {
|
||||
RenderStateCache.CurrentIBO = 0;
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.ElementArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
_mappedPtr = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public unsafe class ManagedGLTexture : ITexture {
|
||||
private uint _texture;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
|
||||
private GL GL => (_device as OpenGLGraphicsDevice).GL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr NativePtr => (IntPtr)_texture;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Width { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Height { get; private set; }
|
||||
|
||||
public TextureFormat Format => TextureFormat.RGBA8;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ManagedGLTexture(OpenGLGraphicsDevice device, byte[]? source, int width, int height, TextureParameters? texParams = null) {
|
||||
var p = texParams ?? TextureParameters.Default;
|
||||
_device = device;
|
||||
_texture = GL.GenTexture();
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
Width = width;
|
||||
Height = height;
|
||||
GL.BindTexture(GLEnum.Texture2D, _texture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
|
||||
if (_device.HasTextureStorage) {
|
||||
GL.TexStorage2D(GLEnum.Texture2D, (uint)mipLevels, GLEnum.Rgba8, (uint)width, (uint)height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
else {
|
||||
GL.TexImage2D(GLEnum.Texture2D, 0, (int)InternalFormat.Rgba8, (uint)width, (uint)height, 0, PixelFormat.Rgba, (PixelType)0x1401, (void*)0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMinFilter, (int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2D, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (p.EnableAnisotropicFiltering && _device.RenderSettings.EnableAnisotropicFiltering)
|
||||
{
|
||||
if (_device.MaxSupportedAnisotropy > 0)
|
||||
{
|
||||
GL.TexParameter(GLEnum.Texture2D, GLEnum.TextureMaxAnisotropy,
|
||||
_device.MaxSupportedAnisotropy);
|
||||
}
|
||||
}
|
||||
|
||||
if (p.EnableMipmaps) {
|
||||
GL.GenerateMipmap(GLEnum.Texture2D);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindTexture(GLEnum.Texture2D, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(CalculateSize(), GpuResourceType.Texture);
|
||||
|
||||
}
|
||||
|
||||
private long CalculateSize() {
|
||||
int maxDimension = Math.Max(Width, Height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
long totalSize = 0;
|
||||
|
||||
for (int i = 0; i < mipLevels; i++) {
|
||||
int w = Math.Max(1, Width >> i);
|
||||
int h = Math.Max(1, Height >> i);
|
||||
totalSize += (long)w * h * 4;
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ManagedGLTexture(OpenGLGraphicsDevice device, string file) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetData(Rectangle rectangle, byte[] data) {
|
||||
if (_texture == 0) return;
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
|
||||
GL.GetInteger(GLEnum.TextureBinding2D, out int oldBinding);
|
||||
GL.BindTexture(GLEnum.Texture2D, _texture);
|
||||
|
||||
fixed (byte* ptr = data) {
|
||||
GL.TexSubImage2D(
|
||||
GLEnum.Texture2D,
|
||||
0, // level
|
||||
rectangle.X,
|
||||
rectangle.Y,
|
||||
(uint)rectangle.Width,
|
||||
(uint)rectangle.Height,
|
||||
PixelFormat.Rgba,
|
||||
PixelType.UnsignedByte,
|
||||
ptr
|
||||
);
|
||||
}
|
||||
|
||||
// Generate mipmaps if needed
|
||||
GL.GenerateMipmap(GLEnum.Texture2D);
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2D, (uint)oldBinding);
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Bind(int slot = 0) {
|
||||
if (slot == 0) {
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
}
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
||||
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture(targetTextureUnit);
|
||||
}
|
||||
|
||||
GL.BindSampler((uint)slot, 0);
|
||||
GL.BindTexture(GLEnum.Texture2D, (uint)NativePtr);
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2D, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected void ReleaseTexture() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_texture != 0) {
|
||||
GL.DeleteTexture(_texture);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
GpuMemoryTracker.TrackDeallocation(CalculateSize(), GpuResourceType.Texture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
_texture = 0;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
ReleaseTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,705 +0,0 @@
|
|||
using AcDream.Core.Rendering.Wb;
|
||||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
// Use our extracted TextureHelpers (T3), not the WB original — disambiguate explicitly
|
||||
using TextureHelpers = AcDream.Core.Rendering.Wb.TextureHelpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public class ManagedGLTextureArray : ITextureArray, IWorldTextureArray {
|
||||
private readonly bool[] _usedLayers;
|
||||
private readonly GL GL;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the device whose one texture table this
|
||||
/// array's two resident handles are interned into. Before this slice
|
||||
/// <c>ObjectMeshManager</c> read the handles off this object and did the
|
||||
/// interning itself; a 64-bit bindless handle cannot cross to Vulkan, so
|
||||
/// the array now answers <see cref="ResolveSlot"/> instead. Null only
|
||||
/// for the legacy <c>OpenGLGraphicsDevice.CreateTextureArrayInternal</c>
|
||||
/// entry points, which no shared atlas uses.
|
||||
/// </summary>
|
||||
private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? _worldTextureTable;
|
||||
private static int _nextId = 0;
|
||||
private bool _needsMipmapRegeneration = false;
|
||||
private readonly bool _isCompressed;
|
||||
private int _mipmapDirtyCount = 0;
|
||||
private readonly object _mipmapLock = new object();
|
||||
private readonly List<TextureLayerUpdate> _pendingUpdates = new();
|
||||
private int _disposeQueued;
|
||||
private int _disposePublicationQueued;
|
||||
private int _disposeRetirementAccepted;
|
||||
private RetryableGpuResourceRelease? _disposeRelease;
|
||||
|
||||
private struct TextureLayerUpdate {
|
||||
public int Layer;
|
||||
public required byte[] Data;
|
||||
public PixelFormat? UploadPixelFormat;
|
||||
public PixelType? UploadPixelType;
|
||||
}
|
||||
|
||||
public int Slot { get; } = _nextId++;
|
||||
public int Width { get; private set; }
|
||||
public int Height { get; private set; }
|
||||
public int Size { get; private set; }
|
||||
public TextureFormat Format { get; private set; }
|
||||
public nint NativePtr { get; private set; }
|
||||
public ulong BindlessWrapHandle { get; private set; }
|
||||
public ulong BindlessClampHandle { get; private set; }
|
||||
public long TotalSizeInBytes => CalculateTotalSize();
|
||||
|
||||
/// <summary>
|
||||
/// #105 diagnostic: staged layer updates (retained decoded payloads) not yet
|
||||
/// applied to the GL texture by <see cref="ProcessDirtyUpdates"/>. Layers with
|
||||
/// a pending update sample UNDEFINED content (TexStorage3D contents) until the
|
||||
/// flush runs — a stuck non-zero count at standstill is the white-walls mechanism.
|
||||
/// </summary>
|
||||
public int PendingUpdateCount {
|
||||
get { lock (_mipmapLock) { return _pendingUpdates.Count; } }
|
||||
}
|
||||
|
||||
public ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
|
||||
int size, ILogger logger, TextureParameters? texParams = null)
|
||||
: this(graphicsDevice, format, width, height, size, logger, worldTextureTable: null, texParams) {
|
||||
}
|
||||
|
||||
internal ManagedGLTextureArray(OpenGLGraphicsDevice graphicsDevice, TextureFormat format, int width, int height,
|
||||
int size, ILogger logger,
|
||||
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? worldTextureTable,
|
||||
TextureParameters? texParams = null) {
|
||||
_worldTextureTable = worldTextureTable;
|
||||
var p = texParams ?? TextureParameters.Default;
|
||||
if (width <= 0 || height <= 0 || size <= 0) {
|
||||
throw new ArgumentException($"Invalid texture array dimensions: {width}x{height}x{size}");
|
||||
}
|
||||
|
||||
Format = format;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Size = size;
|
||||
_usedLayers = new bool[size];
|
||||
_device = graphicsDevice;
|
||||
GL = graphicsDevice.GL;
|
||||
_logger = logger;
|
||||
_isCompressed = IsCompressedFormat(format);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
uint textureName = 0;
|
||||
ulong wrapHandle = 0;
|
||||
ulong clampHandle = 0;
|
||||
bool textureTracked = false;
|
||||
bool textureBytesTracked = false;
|
||||
bool wrapResident = false;
|
||||
bool clampResident = false;
|
||||
long textureBytes = CalculateTotalSize();
|
||||
|
||||
try {
|
||||
textureName = GL.GenTexture();
|
||||
if (textureName == 0)
|
||||
throw new InvalidOperationException("Failed to generate texture array.");
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Texture);
|
||||
textureTracked = true;
|
||||
|
||||
GL.BindTexture(GLEnum.Texture2DArray, textureName);
|
||||
|
||||
int maxDimension = Math.Max(width, height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
|
||||
GL.TexStorage3D(GLEnum.Texture2DArray, (uint)mipLevels, format.ToGL(), (uint)width, (uint)height,
|
||||
(uint)size);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMinFilter,
|
||||
(int)p.MinFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMaxLevel, mipLevels - 1);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureMagFilter, (int)p.MagFilter);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapS, (int)p.WrapS);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureWrapT, (int)p.WrapT);
|
||||
|
||||
if (p.EnableAnisotropicFiltering
|
||||
&& graphicsDevice.RenderSettings.EnableAnisotropicFiltering
|
||||
&& graphicsDevice.MaxSupportedAnisotropy > 0) {
|
||||
GL.TexParameter(
|
||||
GLEnum.Texture2DArray,
|
||||
GLEnum.TextureMaxAnisotropy,
|
||||
graphicsDevice.MaxSupportedAnisotropy);
|
||||
}
|
||||
|
||||
if (format == TextureFormat.A8) {
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleR, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleG, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleB, (int)GLEnum.One);
|
||||
GL.TexParameter(GLEnum.Texture2DArray, TextureParameterName.TextureSwizzleA, (int)GLEnum.Red);
|
||||
}
|
||||
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
GL,
|
||||
$"creating texture array {format} {width}x{height}x{size} ({mipLevels} mip levels)");
|
||||
GpuMemoryTracker.TrackAllocation(textureBytes, GpuResourceType.Texture);
|
||||
textureBytesTracked = true;
|
||||
|
||||
if (_device.HasBindless && _device.BindlessExtension != null) {
|
||||
wrapHandle = _device.BindlessExtension.GetTextureSamplerHandle(textureName, _device.WrapSampler);
|
||||
clampHandle = _device.BindlessExtension.GetTextureSamplerHandle(textureName, _device.ClampSampler);
|
||||
_device.BindlessExtension.MakeTextureHandleResident(wrapHandle);
|
||||
wrapResident = true;
|
||||
_device.BindlessExtension.MakeTextureHandleResident(clampHandle);
|
||||
clampResident = true;
|
||||
GLHelpers.ThrowOnResourceError(GL, "making texture-array sampler handles resident");
|
||||
}
|
||||
|
||||
NativePtr = (nint)textureName;
|
||||
BindlessWrapHandle = wrapHandle;
|
||||
BindlessClampHandle = clampHandle;
|
||||
}
|
||||
catch (Exception constructionFailure) {
|
||||
// Constructor failure cannot use Dispose: the object was never
|
||||
// published and queued teardown would make retries accumulate
|
||||
// invalid resident handles. Attempt every independent cleanup.
|
||||
List<Exception>? cleanupFailures = null;
|
||||
void Attempt(Action cleanup) {
|
||||
try { cleanup(); }
|
||||
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
|
||||
}
|
||||
if (_device.BindlessExtension != null) {
|
||||
if (clampResident)
|
||||
Attempt(() => {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(clampHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back clamp texture-array handle");
|
||||
clampResident = false;
|
||||
});
|
||||
if (wrapResident)
|
||||
Attempt(() => {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(wrapHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back wrap texture-array handle");
|
||||
wrapResident = false;
|
||||
});
|
||||
}
|
||||
// Deleting a texture while either bindless sampler handle is
|
||||
// still resident is undefined. A pre-commit residency failure
|
||||
// therefore retains the texture instead of risking a driver
|
||||
// reset during constructor rollback.
|
||||
if (textureName != 0 && !clampResident && !wrapResident)
|
||||
Attempt(() => {
|
||||
GL.DeleteTexture(textureName);
|
||||
GLHelpers.ThrowOnResourceError(GL, "rolling back texture array");
|
||||
if (textureBytesTracked)
|
||||
GpuMemoryTracker.TrackDeallocation(textureBytes, GpuResourceType.Texture);
|
||||
if (textureTracked)
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
});
|
||||
if (cleanupFailures is not null) {
|
||||
cleanupFailures.Insert(0, constructionFailure);
|
||||
throw new AggregateException(
|
||||
"Texture-array construction and rollback both failed.",
|
||||
cleanupFailures);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
finally {
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public long CalculateTotalSize() {
|
||||
int maxDimension = Math.Max(Width, Height);
|
||||
int mipLevels = (int)Math.Floor(Math.Log2(maxDimension)) + 1;
|
||||
long layerSize = GetExpectedDataSize();
|
||||
long totalSize = 0;
|
||||
|
||||
for (int i = 0; i < mipLevels; i++) {
|
||||
int w = Math.Max(1, Width >> i);
|
||||
int h = Math.Max(1, Height >> i);
|
||||
if (_isCompressed) {
|
||||
totalSize += TextureHelpers.GetCompressedLayerSize(w, h, Format) * Size;
|
||||
}
|
||||
else {
|
||||
totalSize += (long)w * h * (layerSize / (Width * Height)) * Size;
|
||||
}
|
||||
}
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
private static bool IsCompressedFormat(TextureFormat format) {
|
||||
return format == TextureFormat.DXT1 ||
|
||||
format == TextureFormat.DXT3 ||
|
||||
format == TextureFormat.DXT5;
|
||||
}
|
||||
|
||||
public void Bind(int slot = 0) {
|
||||
if (NativePtr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
GL.GetInteger(GLEnum.ActiveTexture, out int oldActiveTexture);
|
||||
GLEnum targetTextureUnit = GLEnum.Texture0 + slot;
|
||||
bool changedUnit = (GLEnum)oldActiveTexture != targetTextureUnit;
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture(targetTextureUnit);
|
||||
}
|
||||
|
||||
GL.BindSampler((uint)slot, 0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
|
||||
if (changedUnit) {
|
||||
GL.ActiveTexture((GLEnum)oldActiveTexture);
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(byte[] data) {
|
||||
return AddLayer(data, null, null);
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
||||
for (int i = 0; i < _usedLayers.Length; i++) {
|
||||
if (!_usedLayers[i]) {
|
||||
UpdateLayerInternal(i, data, uploadPixelFormat, uploadPixelType);
|
||||
_usedLayers[i] = true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No free layers available in texture array (Slot={Slot}, Size={Width}x{Height}x{Size}).");
|
||||
}
|
||||
|
||||
public unsafe int AddLayer(Span<byte> data) {
|
||||
return AddLayer(data.ToArray());
|
||||
}
|
||||
|
||||
public void UpdateLayer(int layer, byte[] data) {
|
||||
UpdateLayer(layer, data, null, null);
|
||||
}
|
||||
|
||||
public void UpdateLayer(int layer, byte[] data, PixelFormat? uploadPixelFormat, PixelType? uploadPixelType) {
|
||||
UpdateLayerInternal(layer, data, uploadPixelFormat, uploadPixelType);
|
||||
_usedLayers[layer] = true;
|
||||
}
|
||||
|
||||
private unsafe void UpdateLayerInternal(int layer, byte[] data, PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType) {
|
||||
if (NativePtr == 0) {
|
||||
throw new InvalidOperationException("Texture array not created.");
|
||||
}
|
||||
|
||||
if (layer < 0 || layer >= Size) {
|
||||
throw new ArgumentOutOfRangeException(nameof(layer),
|
||||
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
||||
}
|
||||
|
||||
ValidateUploadPayload(
|
||||
Format,
|
||||
Width,
|
||||
Height,
|
||||
data.Length,
|
||||
uploadPixelFormat,
|
||||
uploadPixelType);
|
||||
|
||||
lock (_mipmapLock) {
|
||||
// Retain the immutable decoded payload until the once-per-frame
|
||||
// atlas flush. The former per-atlas PBO permanently reserved
|
||||
// several MiB for every array and duplicated each upload
|
||||
// through BufferSubData before TexSubImage3D.
|
||||
var update = new TextureLayerUpdate {
|
||||
Layer = layer,
|
||||
Data = data,
|
||||
UploadPixelFormat = uploadPixelFormat,
|
||||
UploadPixelType = uploadPixelType
|
||||
};
|
||||
int existingIndex = _pendingUpdates.FindLastIndex(pending => pending.Layer == layer);
|
||||
if (existingIndex >= 0)
|
||||
_pendingUpdates[existingIndex] = update;
|
||||
else
|
||||
_pendingUpdates.Add(update);
|
||||
|
||||
_needsMipmapRegeneration = true;
|
||||
if (existingIndex < 0)
|
||||
_mipmapDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public long ProcessDirtyUpdates() {
|
||||
lock (_mipmapLock) {
|
||||
return ProcessDirtyUpdatesInternal(generateMipmaps: true);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe long ProcessDirtyUpdatesInternal(bool generateMipmaps) {
|
||||
if (_pendingUpdates.Count == 0
|
||||
&& (!generateMipmaps || !_needsMipmapRegeneration)) return 0;
|
||||
|
||||
long generatedBytes = 0;
|
||||
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// This runs in WbMeshAdapter.Tick before any draw pass. Establish
|
||||
// the upload phase's canonical texture state directly instead of
|
||||
// synchronously querying driver state for every dirty array.
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
RenderStateCache.CurrentAtlas = 0;
|
||||
|
||||
bool mipmapWorkCompleted = false;
|
||||
try {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, (uint)NativePtr);
|
||||
|
||||
if (_pendingUpdates.Count > 0) {
|
||||
// A non-zero pixel-unpack binding changes pointer arguments
|
||||
// into byte offsets. Direct client-memory uploads therefore
|
||||
// establish the canonical zero binding once for the batch.
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackRowLength, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackSkipRows, 0);
|
||||
GL.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0);
|
||||
|
||||
foreach (var update in _pendingUpdates) {
|
||||
fixed (byte* data = update.Data) {
|
||||
if (_isCompressed) {
|
||||
var internalFormat = Format.ToCompressedGL();
|
||||
GL.CompressedTexSubImage3D(
|
||||
GLEnum.Texture2DArray,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
update.Layer,
|
||||
(uint)Width,
|
||||
(uint)Height,
|
||||
1,
|
||||
internalFormat,
|
||||
(uint)update.Data.Length,
|
||||
data);
|
||||
}
|
||||
else {
|
||||
var pixelFormat = update.UploadPixelFormat ?? Format.ToPixelFormat();
|
||||
var pixelType = update.UploadPixelType ?? Format.ToPixelType();
|
||||
GL.TexSubImage3D(
|
||||
GLEnum.Texture2DArray,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
update.Layer,
|
||||
(uint)Width,
|
||||
(uint)Height,
|
||||
1,
|
||||
pixelFormat,
|
||||
pixelType,
|
||||
data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (generateMipmaps && _needsMipmapRegeneration && _mipmapDirtyCount > 0) {
|
||||
if (_isCompressed) {
|
||||
_logger.LogDebug("Skipping automatic mipmap generation for compressed texture array (Slot={Slot})", Slot);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// Width, height and format were validated when the
|
||||
// immutable storage was allocated. Re-reading them
|
||||
// here forced three CPU/GPU synchronization points
|
||||
// for every dirty atlas without adding safety.
|
||||
GL.GenerateMipmap(GLEnum.Texture2DArray);
|
||||
generatedBytes = TotalSizeInBytes;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogWarning(ex, "Failed to generate mipmaps for texture array (Slot={Slot}); retaining upload state for retry.", Slot);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release builds must observe transfer/OOM/context errors
|
||||
// before the pending offsets and dirty mip state are cleared.
|
||||
// One check covers every layer in this array plus its single
|
||||
// mip generation, keeping the synchronization cost bounded by
|
||||
// dirty arrays rather than uploaded textures.
|
||||
GLHelpers.ThrowOnResourceError(
|
||||
GL,
|
||||
$"committing texture-array updates (Slot={Slot}, Layers={_pendingUpdates.Count})");
|
||||
mipmapWorkCompleted = generateMipmaps
|
||||
&& _needsMipmapRegeneration
|
||||
&& _mipmapDirtyCount > 0;
|
||||
}
|
||||
finally {
|
||||
GL.BindBuffer(GLEnum.PixelUnpackBuffer, 0);
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
}
|
||||
|
||||
// Commit CPU-side completion only after glGetError confirms the
|
||||
// uploads/mipmap work succeeded. If the driver rejects an
|
||||
// operation, the retained payloads and dirty flags remain intact and the
|
||||
// atlas stays in ObjectMeshManager's dirty set for a later retry.
|
||||
_pendingUpdates.Clear();
|
||||
if (mipmapWorkCompleted) {
|
||||
_mipmapDirtyCount = 0;
|
||||
_needsMipmapRegeneration = false;
|
||||
}
|
||||
return generatedBytes;
|
||||
}
|
||||
|
||||
private void ClearLayerForMipmap(int layer) {
|
||||
// Upload a single black/transparent pixel to make layer defined
|
||||
byte[] clearData = new byte[GetExpectedDataSize()];
|
||||
Array.Clear(clearData, 0, clearData.Length); // Zero-fill (black/transparent)
|
||||
UpdateLayerInternal(layer, clearData, null, null);
|
||||
}
|
||||
|
||||
private int GetExpectedDataSize() {
|
||||
return CalculateExpectedDataSize(Format, Width, Height);
|
||||
}
|
||||
|
||||
internal static int CalculateExpectedDataSize(TextureFormat format, int width, int height) {
|
||||
if (IsCompressedFormat(format))
|
||||
return TextureHelpers.GetCompressedLayerSize(width, height, format);
|
||||
|
||||
return format switch {
|
||||
TextureFormat.RGBA8 => checked(width * height * 4),
|
||||
TextureFormat.RGB8 => checked(width * height * 3),
|
||||
TextureFormat.A8 => checked(width * height),
|
||||
TextureFormat.Rgba32f => checked(width * height * 16),
|
||||
_ => throw new NotSupportedException($"Unsupported format {format}")
|
||||
};
|
||||
}
|
||||
|
||||
internal static void ValidateUploadPayload(
|
||||
TextureFormat format,
|
||||
int width,
|
||||
int height,
|
||||
int dataLength,
|
||||
PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType) {
|
||||
int expectedBytes = CalculateExpectedDataSize(format, width, height);
|
||||
if (dataLength != expectedBytes) {
|
||||
throw new ArgumentException(
|
||||
$"Texture-array layer payload has {dataLength} bytes; expected exactly {expectedBytes} "
|
||||
+ $"for {format} {width}x{height}.",
|
||||
nameof(dataLength));
|
||||
}
|
||||
|
||||
if (IsCompressedFormat(format)) {
|
||||
if (uploadPixelFormat.HasValue || uploadPixelType.HasValue)
|
||||
throw new ArgumentException("Compressed texture uploads cannot specify pixel format/type overrides.");
|
||||
return;
|
||||
}
|
||||
|
||||
PixelFormat expectedFormat = format.ToPixelFormat();
|
||||
PixelType expectedType = format.ToPixelType();
|
||||
if ((uploadPixelFormat ?? expectedFormat) != expectedFormat
|
||||
|| (uploadPixelType ?? expectedType) != expectedType) {
|
||||
throw new ArgumentException(
|
||||
$"Upload descriptor {uploadPixelFormat}/{uploadPixelType} does not match "
|
||||
+ $"the {expectedFormat}/{expectedType} transfer required by {format}.");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveLayer(int layer) {
|
||||
if (layer < 0 || layer >= Size) {
|
||||
throw new ArgumentOutOfRangeException(nameof(layer),
|
||||
$"Layer index {layer} is out of range [0, {Size - 1}] (Slot={Slot}).");
|
||||
}
|
||||
|
||||
if (!_usedLayers[layer]) {
|
||||
throw new InvalidOperationException($"Layer {layer} is already free (Slot={Slot}).");
|
||||
}
|
||||
|
||||
_usedLayers[layer] = false;
|
||||
|
||||
// An unreferenced layer needs no clear or whole-array mip
|
||||
// regeneration before AddTexture overwrites it on reuse.
|
||||
}
|
||||
|
||||
public bool IsLayerUsed(int layer) {
|
||||
if (layer < 0 || layer >= Size) return false;
|
||||
return _usedLayers[layer];
|
||||
}
|
||||
|
||||
public int GetUsedLayerCount() {
|
||||
return _usedLayers.Count(x => x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True once disposal is durably owned by a queued GL publication,
|
||||
/// the frame-retirement queue, or a completed retained release. A
|
||||
/// caller may only commit its own logical disposal after this becomes
|
||||
/// true; otherwise a synchronous enqueue failure still needs retry.
|
||||
/// </summary>
|
||||
internal bool HasDurableDisposeOwnership {
|
||||
get {
|
||||
if (Volatile.Read(ref _disposeQueued) == 0)
|
||||
return false;
|
||||
return Volatile.Read(ref _disposePublicationQueued) != 0
|
||||
|| Volatile.Read(ref _disposeRetirementAccepted) != 0
|
||||
|| Volatile.Read(ref _disposeRelease) is null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True only after every retained bindless-handle, GL-name, and memory
|
||||
/// accounting release stage has completed. Logical disposal can become
|
||||
/// durable earlier while the frame fence still owns the physical array.
|
||||
/// </summary>
|
||||
internal bool IsPhysicalRetirementComplete =>
|
||||
Volatile.Read(ref _disposeQueued) != 0
|
||||
&& Volatile.Read(ref _disposeRelease) is null;
|
||||
|
||||
bool IWorldTextureArray.HasDurableDisposeOwnership => HasDurableDisposeOwnership;
|
||||
|
||||
bool IWorldTextureArray.IsPhysicalRetirementComplete => IsPhysicalRetirementComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: this array's device-table slot for the
|
||||
/// requested address mode.
|
||||
///
|
||||
/// <para>The interning call is the one <c>ObjectMeshManager</c> made
|
||||
/// itself before this slice, moved one level down so the caller can be
|
||||
/// written against <see cref="IWorldTextureArray"/> instead of against a
|
||||
/// 64-bit <c>ARB_bindless_texture</c> handle that has no Vulkan
|
||||
/// spelling. It is idempotent by handle, which is why it stays a per-batch
|
||||
/// call rather than becoming cached state — exactly as before.</para>
|
||||
/// </summary>
|
||||
AcDream.App.Rendering.Gpu.GpuTextureSlot IWorldTextureArray.ResolveSlot(bool wrapping) {
|
||||
if (_worldTextureTable is null)
|
||||
return AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
||||
ulong handle = wrapping ? _retiredWrapHandle : _retiredClampHandle;
|
||||
if (handle == 0)
|
||||
handle = wrapping ? BindlessWrapHandle : BindlessClampHandle;
|
||||
return _worldTextureTable.RegisterWorldTextureHandle(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires both table entries. <see cref="Dispose"/> zeroes the public
|
||||
/// handle properties, so the values are captured there and read from the
|
||||
/// captures here — this is called after physical retirement completes,
|
||||
/// which is necessarily after Dispose.
|
||||
/// </summary>
|
||||
public void ReleaseTextureSlots() {
|
||||
if (_worldTextureTable is null)
|
||||
return;
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(_retiredWrapHandle);
|
||||
_worldTextureTable.ReleaseWorldTextureHandle(_retiredClampHandle);
|
||||
_retiredWrapHandle = 0;
|
||||
_retiredClampHandle = 0;
|
||||
}
|
||||
|
||||
private ulong _retiredWrapHandle;
|
||||
private ulong _retiredClampHandle;
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void GenerateMipmaps() {
|
||||
_needsMipmapRegeneration = true;
|
||||
lock (_mipmapLock) {
|
||||
_mipmapDirtyCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (Interlocked.CompareExchange(ref _disposeQueued, 1, 0) != 0) {
|
||||
ScheduleDisposeRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
uint textureName = (uint)NativePtr;
|
||||
ulong bindlessWrapHandle = BindlessWrapHandle;
|
||||
ulong bindlessClampHandle = BindlessClampHandle;
|
||||
long textureBytes = CalculateTotalSize();
|
||||
|
||||
// Slice V6i-2: the handles the two table entries are keyed by. The
|
||||
// properties are zeroed below, so ReleaseTextureSlots — which runs
|
||||
// only once physical retirement completes — reads these captures.
|
||||
_retiredWrapHandle = bindlessWrapHandle;
|
||||
_retiredClampHandle = bindlessClampHandle;
|
||||
|
||||
NativePtr = 0;
|
||||
BindlessWrapHandle = 0;
|
||||
BindlessClampHandle = 0;
|
||||
|
||||
_disposeRelease = new RetryableGpuResourceRelease(
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessWrapHandle != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing wrap texture-array handle (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessWrapHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(bindlessWrapHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing wrap texture-array handle");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessClampHandle != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing clamp texture-array handle (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (_device.BindlessExtension != null && bindlessClampHandle != 0) {
|
||||
_device.BindlessExtension.MakeTextureHandleNonResident(bindlessClampHandle);
|
||||
GLHelpers.ThrowOnResourceError(GL, "releasing clamp texture-array handle");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName} (precondition)");
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0) {
|
||||
GL.DeleteTexture(textureName);
|
||||
GLHelpers.ThrowOnResourceError(GL, $"deleting texture array {textureName}");
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(textureBytes, GpuResourceType.Texture);
|
||||
},
|
||||
() => {
|
||||
if (textureName != 0)
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Texture);
|
||||
},
|
||||
() => _disposeRelease = null);
|
||||
|
||||
ScheduleDisposeRelease();
|
||||
}
|
||||
|
||||
private void ScheduleDisposeRelease(bool forNextPass = false) {
|
||||
RetryableGpuResourceRelease? release = _disposeRelease;
|
||||
if (release is null || release.IsComplete || Volatile.Read(ref _disposeRetirementAccepted) != 0)
|
||||
return;
|
||||
if (Interlocked.CompareExchange(ref _disposePublicationQueued, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
try {
|
||||
Action<GL> publish = GL => {
|
||||
Volatile.Write(ref _disposePublicationQueued, 0);
|
||||
try {
|
||||
_device.RetireGpuResource(release.Run);
|
||||
Volatile.Write(ref _disposeRetirementAccepted, 1);
|
||||
}
|
||||
catch {
|
||||
// Retire may fail before accepting the callback, or an
|
||||
// immediate queue may surface a partial release. The
|
||||
// release cursor makes this next-pass retry exact.
|
||||
ScheduleDisposeRelease(forNextPass: true);
|
||||
throw;
|
||||
}
|
||||
};
|
||||
if (forNextPass)
|
||||
_device.QueueGLActionForNextPass(publish);
|
||||
else
|
||||
_device.QueueGLAction(publish);
|
||||
}
|
||||
catch {
|
||||
Volatile.Write(ref _disposePublicationQueued, 0);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Runtime.InteropServices;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
|
||||
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL uniform buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLUniformBuffer : IUniformBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLUniformBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="device">Graphics device</param>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
||||
uint buffer = 0;
|
||||
bool allocated = false;
|
||||
try {
|
||||
buffer = TrackedGlResource.CreateBuffer(
|
||||
GL,
|
||||
"creating managed uniform buffer");
|
||||
uint ownedBuffer = buffer;
|
||||
RetryableGpuResourceRelease unpublishedBufferRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
GL,
|
||||
ownedBuffer,
|
||||
() => allocated ? Size : 0,
|
||||
"rolling back managed uniform buffer");
|
||||
resources.Add(
|
||||
"managed uniform buffer",
|
||||
unpublishedBufferRelease.Run);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
GL,
|
||||
GLEnum.UniformBuffer,
|
||||
buffer,
|
||||
0,
|
||||
Size,
|
||||
GLEnum.DynamicDraw,
|
||||
"allocating managed uniform buffer");
|
||||
allocated = true;
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"ManagedGLUniformBuffer construction failed and its GL buffer did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
bufferId = buffer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(T[] data) where T : unmanaged {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(Span<T> data) where T : unmanaged {
|
||||
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
fixed (T* ptr = data) {
|
||||
GL.BufferSubData(GLEnum.UniformBuffer, 0, (nuint)dataSize, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
|
||||
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : unmanaged {
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Validate buffer bounds
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
fixed (T* ptr = data.Slice(sourceOffsetElements, lengthElements)) {
|
||||
GL.BufferSubData(GLEnum.UniformBuffer, (nint)destinationOffsetBytes, (nuint)dataSizeBytes, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a single piece of data in the buffer.
|
||||
/// </summary>
|
||||
public unsafe void SetData<T>(ref T data) where T : unmanaged {
|
||||
fixed (T* pData = &data) {
|
||||
SetData(new Span<T>(pData, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the buffer to the specified binding point.
|
||||
/// </summary>
|
||||
/// <param name="bindingPoint">The binding point to bind to</param>
|
||||
public void Bind(uint bindingPoint) {
|
||||
GL.BindBufferBase(GLEnum.UniformBuffer, bindingPoint, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Bind() {
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases an unpublished constructor-owned buffer synchronously on
|
||||
/// the GL thread. This is deliberately separate from ordinary queued
|
||||
/// disposal so an enclosing constructor can prove rollback before it
|
||||
/// propagates its failure.
|
||||
/// </summary>
|
||||
internal void DisposeImmediately() {
|
||||
if (bufferId == 0)
|
||||
return;
|
||||
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
GL,
|
||||
bufferId,
|
||||
Size,
|
||||
"rolling back unpublished managed uniform buffer");
|
||||
release.Run();
|
||||
bufferId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VertexAttribType = Silk.NET.OpenGL.VertexAttribType;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
public unsafe class ManagedGLVertexArray : IVertexArray {
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private GL GL => _device.GL;
|
||||
private uint _vaoId = 0;
|
||||
|
||||
public ManagedGLVertexArray(OpenGLGraphicsDevice device, IVertexBuffer buffer, VertexFormat format) {
|
||||
_device = device;
|
||||
|
||||
// Generate the vertex array
|
||||
_vaoId = GL.GenVertexArray();
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_vaoId == 0) {
|
||||
throw new Exception("Failed to generate vertex array.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO);
|
||||
|
||||
SetVertexBuffer(buffer, format);
|
||||
}
|
||||
|
||||
public void SetVertexBuffer(IVertexBuffer buffer, VertexFormat format) {
|
||||
GL.BindVertexArray(_vaoId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
buffer.Bind();
|
||||
for (int i = 0; i < format.Attributes.Length; i++) {
|
||||
var attr = format.Attributes[i];
|
||||
GL.EnableVertexAttribArray((uint)i);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.VertexAttribPointer((uint)i, attr.Size, Convert(attr.Type), attr.Normalized, (uint)format.Stride, attr.Offset);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
GL.BindVertexArray(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
private GLEnum Convert(Chorizite.Core.Render.Enums.VertexAttribType type) => type switch {
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Float => GLEnum.Float,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Int => GLEnum.Int,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedInt => GLEnum.UnsignedInt,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.UnsignedByte => GLEnum.UnsignedByte,
|
||||
Chorizite.Core.Render.Enums.VertexAttribType.Byte => GLEnum.Byte,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
public void Bind() {
|
||||
GL.BindVertexArray(_vaoId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindVertexArray(0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (_vaoId != 0) {
|
||||
GL.DeleteVertexArray(_vaoId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
|
||||
_vaoId = 0;
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System.Buffers;
|
||||
using System.Runtime.InteropServices;
|
||||
using BufferUsage = Chorizite.Core.Render.Enums.BufferUsage;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL vertex buffer
|
||||
/// </summary>
|
||||
public unsafe class ManagedGLVertexBuffer : IVertexBuffer {
|
||||
private uint bufferId;
|
||||
private readonly OpenGLGraphicsDevice _device;
|
||||
private void* _mappedPtr;
|
||||
private GL GL => _device.GL;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public BufferUsage Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedGLVertexBuffer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLVertexBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device;
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
if (bufferId == 0) {
|
||||
throw new Exception("Failed to generate vertex buffer.");
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Allocate the buffer with the specified size but no initial data
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
if (_device.HasBufferStorage) {
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ArrayBuffer, (uint)Size, (void*)0, flags);
|
||||
_mappedPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)Size, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(uint)Size,
|
||||
(void*)0, // No initial data
|
||||
Usage.ToGL());
|
||||
}
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(T[] data) where T : IVertex {
|
||||
SetData(data.AsSpan());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetData<T>(Span<T> data) where T : IVertex {
|
||||
uint dataSize = (uint)data.Length * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Ensure the buffer size is sufficient
|
||||
if (dataSize > Size) {
|
||||
throw new ArgumentException($"Data size ({dataSize} bytes) exceeds buffer size ({Size} bytes).");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<T> mappedSpan = new Span<T>(_mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Map the buffer for writing
|
||||
void* mappedPtr = GL.MapBufferRange(
|
||||
GLEnum.ArrayBuffer,
|
||||
0, // offset
|
||||
dataSize,
|
||||
MapBufferAccessMask.WriteBit | MapBufferAccessMask.InvalidateBufferBit // Overwrite entire buffer
|
||||
);
|
||||
|
||||
if (mappedPtr == null) {
|
||||
throw new Exception("Failed to map buffer for writing.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Copy data directly to mapped memory
|
||||
Span<T> mappedSpan = new Span<T>(mappedPtr, data.Length);
|
||||
data.CopyTo(mappedSpan);
|
||||
}
|
||||
finally {
|
||||
// Unmap the buffer
|
||||
GL.UnmapBuffer(GLEnum.ArrayBuffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void SetSubData<T>(T[] data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
|
||||
SetSubData(data.AsSpan(), destinationOffsetBytes, sourceOffsetElements, lengthElements);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void SetSubData<T>(Span<T> data, int destinationOffsetBytes, int sourceOffsetElements = 0, int lengthElements = 0) where T : IVertex {
|
||||
if (Usage != BufferUsage.Dynamic) {
|
||||
throw new InvalidOperationException("Cannot update a buffer that is not dynamic.");
|
||||
}
|
||||
|
||||
if (lengthElements <= 0) {
|
||||
lengthElements = data.Length - sourceOffsetElements;
|
||||
}
|
||||
|
||||
uint dataSizeBytes = (uint)lengthElements * (uint)Marshal.SizeOf<T>();
|
||||
|
||||
// Validate buffer bounds
|
||||
if (destinationOffsetBytes + dataSizeBytes > Size) {
|
||||
throw new ArgumentException($"Update would exceed buffer size. Buffer size: {Size}, Update range: {destinationOffsetBytes} to {destinationOffsetBytes + dataSizeBytes}");
|
||||
}
|
||||
|
||||
if (_mappedPtr != null) {
|
||||
Span<T> mappedSpan = new Span<T>((byte*)_mappedPtr + destinationOffsetBytes, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Map the specific range of the buffer
|
||||
void* mappedPtr = GL.MapBufferRange(
|
||||
GLEnum.ArrayBuffer,
|
||||
destinationOffsetBytes,
|
||||
dataSizeBytes,
|
||||
MapBufferAccessMask.WriteBit // Write access for partial update
|
||||
);
|
||||
|
||||
if (mappedPtr == null) {
|
||||
throw new Exception("Failed to map buffer for writing.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Copy the specified range of data to the mapped memory
|
||||
Span<T> mappedSpan = new Span<T>(mappedPtr, lengthElements);
|
||||
data.Slice(sourceOffsetElements, lengthElements).CopyTo(mappedSpan);
|
||||
}
|
||||
finally {
|
||||
// Unmap the buffer
|
||||
GL.UnmapBuffer(GLEnum.ArrayBuffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind() {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public unsafe void Dispose() {
|
||||
_device.QueueGLAction(GL => {
|
||||
if (bufferId != 0) {
|
||||
GL.DeleteBuffer(bufferId);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GpuMemoryTracker.TrackDeallocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = 0;
|
||||
_mappedPtr = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using DatReaderWriter.Enums;
|
||||
using Chorizite.Core.Render;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
|
|
@ -19,17 +17,4 @@ namespace AcDream.App.Rendering.Wb {
|
|||
public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
|
||||
}
|
||||
|
||||
public struct LandblockMdiCommand {
|
||||
public ulong SortKey;
|
||||
public ulong ObjectId;
|
||||
public DrawElementsIndirectCommand Command;
|
||||
public ModernBatchData BatchData;
|
||||
public uint TextureIndex;
|
||||
public ManagedGLTextureArray Atlas;
|
||||
public uint VAO;
|
||||
public uint IBO;
|
||||
public bool IsTransparent;
|
||||
public bool IsAdditive;
|
||||
public bool HasWrappingUVs;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,17 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
public class ObjectRenderData
|
||||
{
|
||||
/// <summary>
|
||||
/// Campaign V slice V11 deleted the per-mesh raw-GL vertex array/buffer
|
||||
/// this used to carry — Vulkan bakes vertex input into the pipeline and
|
||||
/// the shared <see cref="GlobalMeshBuffer"/> arena has no VAO/VBO
|
||||
/// concept at all (see <see cref="GlobalMeshBuffer.VertexStore"/>). This
|
||||
/// is always 0 now; it survives only because
|
||||
/// <c>WbDrawDispatcher.cs</c>'s legacy (non-RHI) dispatcher still reads
|
||||
/// it into its own dead <c>anyVao</c> bookkeeping.
|
||||
/// </summary>
|
||||
public uint VAO { get; set; }
|
||||
/// <summary>See <see cref="VAO"/> — always 0 for the same reason.</summary>
|
||||
public uint VBO { get; set; }
|
||||
public int VertexCount { get; set; }
|
||||
public List<ObjectRenderBatch> Batches { get; set; } = new();
|
||||
|
|
@ -76,6 +86,11 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
public class ObjectRenderBatch
|
||||
{
|
||||
/// <summary>See <see cref="ObjectRenderData.VAO"/> — Campaign V slice
|
||||
/// V11 deleted the legacy per-batch raw-GL index buffer this used to
|
||||
/// carry. Always 0 now; every batch's actual index range lives in the
|
||||
/// shared arena via <see cref="FirstIndex"/>/<see cref="BaseVertex"/>.
|
||||
/// </summary>
|
||||
public uint IBO { get; set; }
|
||||
public int IndexCount { get; set; }
|
||||
public TextureAtlasManager Atlas { get; set; } = null!;
|
||||
|
|
@ -124,26 +139,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
private readonly IMeshPipelineDevice _graphicsDevice;
|
||||
|
||||
/// <summary>
|
||||
/// The GL context the LEGACY (pre-modern-path) upload bodies write
|
||||
/// through.
|
||||
///
|
||||
/// <para>Campaign V slice V6i-3 narrowed what still needs it. The modern
|
||||
/// path's arena upload is <see cref="GlobalMeshBuffer"/>'s, and that is
|
||||
/// now <see cref="AcDream.App.Rendering.Gpu.IGpuBuffer"/> work on both
|
||||
/// arms; what remains raw is the per-mesh VAO/VBO/IBO construction the
|
||||
/// N.5 ship amendment made unreachable — missing bindless or
|
||||
/// draw-parameters throws at startup, so <c>_useModernRendering</c> is
|
||||
/// true in every shipping configuration. The accessor therefore survives
|
||||
/// as the guard on genuinely dead code rather than as a blocker, and it
|
||||
/// is deleted with that code.</para>
|
||||
/// </summary>
|
||||
private GL RequireGl() =>
|
||||
_graphicsDevice.Gl
|
||||
?? throw new InvalidOperationException(
|
||||
"The mesh pipeline's legacy per-mesh vertex-array upload is raw GL and this "
|
||||
+ "device has no context. The modern path is mandatory (N.5 ship amendment), "
|
||||
+ "so reaching this is a composition error rather than a backend gap.");
|
||||
private readonly IPreparedAssetSource _preparedAssets;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
|
@ -158,20 +153,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
private readonly AcDream.App.Rendering.Gpu.IGpuDevice _gpuDevice;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the downcast moved here from the constructor.
|
||||
/// Only the raw-GL world renderers reach this — the Vulkan backend binds
|
||||
/// set 2 and never touches the handle table — so a Vulkan-composed mesh
|
||||
/// pipeline can now be CONSTRUCTED, and only a caller that genuinely
|
||||
/// needs a GL handle table fails, naming why.
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
_gpuDevice as AcDream.App.Rendering.Gpu.Gl.GlGpuDevice
|
||||
?? throw new InvalidOperationException(
|
||||
"The GL bindless handle table was requested from a mesh pipeline composed against "
|
||||
+ $"the {_gpuDevice.Backend} backend. It is GL-only emulation of the Vulkan texture "
|
||||
+ "table and is deleted with the raw-GL world path.");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: how a shared atlas's physical array is made.
|
||||
/// Composed once; see <see cref="IWorldTextureArrayFactory"/>.
|
||||
|
|
@ -339,7 +320,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
public GlobalMeshBuffer? GlobalBuffer { get; }
|
||||
private readonly bool _useModernRendering;
|
||||
internal (int RenderData, int AtlasArrays, int UnusedLru, long EstimatedBytes) Diagnostics
|
||||
{
|
||||
get
|
||||
|
|
@ -527,11 +507,16 @@ namespace AcDream.App.Rendering.Wb
|
|||
_stagedMeshData = new MeshUploadStagingQueue(
|
||||
budgets.MeshStagingEntries,
|
||||
budgets.MeshStagingBytes);
|
||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||
if (_useModernRendering)
|
||||
// The modern path is mandatory (N.5 ship amendment) and Campaign V
|
||||
// slice V11 deleted the only other backend, so a production
|
||||
// IMeshPipelineDevice always reports both flags true. The gate
|
||||
// survives because AcDream.App.Tests.Rendering.Wb.
|
||||
// MeshPipelineDeviceSeamTests exercises a device that reports
|
||||
// neither, to prove the arena is genuinely optional rather than
|
||||
// dereferenced unconditionally.
|
||||
if (_graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless)
|
||||
{
|
||||
GlobalBuffer = new GlobalMeshBuffer(
|
||||
_graphicsDevice.Gl,
|
||||
gpuDevice,
|
||||
_graphicsDevice.ResourceRetirement);
|
||||
}
|
||||
|
|
@ -680,7 +665,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
/// <summary>
|
||||
/// #105 diagnostic: counts staged-but-unflushed texture layer updates across all
|
||||
/// shared atlases (see <see cref="ManagedGLTextureArray.PendingUpdateCount"/>).
|
||||
/// shared atlases (see <see cref="IWorldTextureArray.PendingUpdateCount"/>).
|
||||
/// Render thread only — <c>_globalAtlases</c> is render-thread-owned.
|
||||
/// </summary>
|
||||
public (int PendingUpdates, int ArraysWithPending, int TotalArrays) GetPendingTextureUpdateStats()
|
||||
|
|
@ -901,7 +886,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
private long GetReclaimableBytes(ObjectRenderData data)
|
||||
{
|
||||
if (_useModernRendering && data.GlobalAllocation is { } allocation)
|
||||
if (data.GlobalAllocation is { } allocation)
|
||||
{
|
||||
return checked(
|
||||
(long)allocation.Vertices.Length * VertexPositionNormalTexture.Size
|
||||
|
|
@ -1989,16 +1974,10 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
#region Private: GPU Upload
|
||||
|
||||
private unsafe ObjectRenderData? UploadGfxObjMeshData(ObjectMeshData meshData)
|
||||
private ObjectRenderData? UploadGfxObjMeshData(ObjectMeshData meshData)
|
||||
{
|
||||
if (meshData.Vertices.Length == 0) return null;
|
||||
|
||||
// Resolved lazily since Campaign V slice V6i-3: every reader below
|
||||
// is inside a !_useModernRendering branch, and the modern path is
|
||||
// mandatory, so a backend with no GL context uploads meshes here
|
||||
// without ever asking for one.
|
||||
GL? gl = _graphicsDevice.Gl;
|
||||
uint vao = 0, vbo = 0;
|
||||
var modernIndexBatches = meshData.TextureBatches.Values
|
||||
.SelectMany(batches => batches)
|
||||
.Where(batch => batch.Indices.Count != 0)
|
||||
|
|
@ -2007,62 +1986,23 @@ namespace AcDream.App.Rendering.Wb
|
|||
GlobalMeshAllocation? globalAllocation = null;
|
||||
var renderBatches = new List<ObjectRenderBatch>();
|
||||
var acquiredTextures = new List<(TextureAtlasManager Atlas, TextureKey Key)>();
|
||||
var legacyIndexBuffers = new List<(uint Name, int Bytes)>();
|
||||
|
||||
try
|
||||
{
|
||||
if (_useModernRendering)
|
||||
{
|
||||
// One mesh owns one vertex range and one contiguous index
|
||||
// range. The former append path duplicated the full vertex
|
||||
// array per material and never reclaimed evicted ranges.
|
||||
vao = GlobalBuffer!.VAO;
|
||||
vbo = GlobalBuffer!.VBO;
|
||||
}
|
||||
else
|
||||
{
|
||||
GL legacyGl = RequireGl();
|
||||
legacyGl.GenVertexArrays(1, out vao);
|
||||
legacyGl.BindVertexArray(vao);
|
||||
|
||||
legacyGl.GenBuffers(1, out vbo);
|
||||
legacyGl.BindBuffer(GLEnum.ArrayBuffer, vbo);
|
||||
fixed (VertexPositionNormalTexture* ptr = meshData.Vertices)
|
||||
{
|
||||
legacyGl.BufferData(GLEnum.ArrayBuffer, (nuint)(meshData.Vertices.Length * VertexPositionNormalTexture.Size), ptr, GLEnum.StaticDraw);
|
||||
}
|
||||
GpuMemoryTracker.TrackAllocation(meshData.Vertices.Length * VertexPositionNormalTexture.Size, GpuResourceType.Buffer);
|
||||
|
||||
int stride = VertexPositionNormalTexture.Size;
|
||||
// Position (location 0)
|
||||
legacyGl.EnableVertexAttribArray(0);
|
||||
legacyGl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint)stride, (void*)0);
|
||||
// Normal (location 1)
|
||||
legacyGl.EnableVertexAttribArray(1);
|
||||
legacyGl.VertexAttribPointer(1, 3, GLEnum.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
|
||||
// TexCoord (location 2)
|
||||
legacyGl.EnableVertexAttribArray(2);
|
||||
legacyGl.VertexAttribPointer(2, 2, GLEnum.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
|
||||
// Instance data (shared VBO)
|
||||
legacyGl.BindBuffer(GLEnum.ArrayBuffer, _graphicsDevice.InstanceVBO);
|
||||
for (uint i = 0; i < 4; i++)
|
||||
{
|
||||
var loc = 3 + i;
|
||||
legacyGl.EnableVertexAttribArray(loc);
|
||||
legacyGl.VertexAttribPointer(loc, 4, GLEnum.Float, false, (uint)sizeof(InstanceData), (void*)(i * 16));
|
||||
legacyGl.VertexAttribDivisor(loc, 1);
|
||||
}
|
||||
legacyGl.EnableVertexAttribArray(8);
|
||||
legacyGl.VertexAttribIPointer(8, 1, GLEnum.UnsignedInt, (uint)sizeof(InstanceData), (void*)64);
|
||||
legacyGl.VertexAttribDivisor(8, 1);
|
||||
}
|
||||
|
||||
// Allocate the shared vertex/index range before acquiring texture
|
||||
// references. A buffer-growth failure therefore leaves every atlas
|
||||
// untouched; later failures still roll this allocation back below.
|
||||
if (_useModernRendering && modernIndexBatches.Length != 0)
|
||||
globalAllocation = GlobalBuffer!.UploadMesh(meshData.Vertices, modernIndexBatches);
|
||||
//
|
||||
// GlobalBuffer is null only for a test double that reports no
|
||||
// modern-path capability (MeshPipelineDeviceSeamTests); every
|
||||
// production IMeshPipelineDevice is Vulkan-backed and reports both
|
||||
// flags true (N.5 ship amendment; Campaign V slice V11 deleted the
|
||||
// only other backend). There is no longer a per-mesh vertex array
|
||||
// or vertex/index buffer to build here — Vulkan bakes vertex input
|
||||
// into the pipeline, and the shared arena's stores are bound once
|
||||
// per pass (see WbDrawDispatcher.Rhi.cs's BindPipelineWithMesh).
|
||||
if (GlobalBuffer is not null && modernIndexBatches.Length != 0)
|
||||
globalAllocation = GlobalBuffer.UploadMesh(meshData.Vertices, modernIndexBatches);
|
||||
|
||||
foreach (var (format, batches) in meshData.TextureBatches)
|
||||
{
|
||||
|
|
@ -2070,7 +2010,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
if (batch.Indices.Count == 0) continue;
|
||||
|
||||
uint ibo = 0;
|
||||
TextureAtlasManager? atlasManager = null;
|
||||
int textureIndex = 0;
|
||||
uint firstIndex = 0;
|
||||
|
|
@ -2128,24 +2067,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
if (uploadsNewLayer)
|
||||
_dirtyAtlases.Add(atlasManager);
|
||||
|
||||
if (_useModernRendering)
|
||||
{
|
||||
ibo = GlobalBuffer!.IBO;
|
||||
}
|
||||
else
|
||||
{
|
||||
GL legacyGl = RequireGl();
|
||||
legacyGl.GenBuffers(1, out ibo);
|
||||
legacyGl.BindBuffer(GLEnum.ElementArrayBuffer, ibo);
|
||||
var indexArray = batch.Indices.ToArray();
|
||||
fixed (ushort* iptr = indexArray)
|
||||
{
|
||||
legacyGl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(indexArray.Length * sizeof(ushort)), iptr, GLEnum.StaticDraw);
|
||||
}
|
||||
GpuMemoryTracker.TrackAllocation(indexArray.Length * sizeof(ushort), GpuResourceType.Buffer);
|
||||
legacyIndexBuffers.Add((ibo, indexArray.Length * sizeof(ushort)));
|
||||
}
|
||||
|
||||
// Campaign V slice V4t interned the atlas's resident
|
||||
// handle into the device's one texture table here and
|
||||
// carried the slot. Slice V6i-2 asks the array for the
|
||||
|
|
@ -2159,7 +2080,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
renderBatches.Add(new ObjectRenderBatch
|
||||
{
|
||||
IBO = ibo,
|
||||
IndexCount = batch.Indices.Count,
|
||||
Atlas = atlasManager!,
|
||||
TextureIndex = textureIndex,
|
||||
|
|
@ -2178,7 +2098,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
}
|
||||
|
||||
if (_useModernRendering && globalAllocation is not null)
|
||||
if (globalAllocation is not null)
|
||||
{
|
||||
if (renderBatches.Count != globalAllocation.BatchFirstIndices.Count)
|
||||
{
|
||||
|
|
@ -2197,8 +2117,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
+ renderBatches.Sum(b => (long)b.IndexCount * sizeof(ushort)));
|
||||
var renderData = new ObjectRenderData
|
||||
{
|
||||
VAO = vao,
|
||||
VBO = vbo,
|
||||
VertexCount = meshData.Vertices.Length,
|
||||
Batches = renderBatches,
|
||||
GlobalAllocation = globalAllocation,
|
||||
|
|
@ -2209,26 +2127,17 @@ namespace AcDream.App.Rendering.Wb
|
|||
CPUEdgeLines = meshData.EdgeLines,
|
||||
MemorySize = geometryBytes,
|
||||
NonArenaGpuBytes = CalculateNonArenaGeometryBytes(
|
||||
_useModernRendering,
|
||||
GlobalBuffer is not null,
|
||||
geometryBytes),
|
||||
};
|
||||
|
||||
if (!_useModernRendering)
|
||||
{
|
||||
RequireGl().BindVertexArray(0);
|
||||
}
|
||||
return renderData;
|
||||
}
|
||||
catch (Exception uploadFailure)
|
||||
{
|
||||
RetryableResourceReleaseLedger rollback = CreateUploadRollback(
|
||||
meshData,
|
||||
gl,
|
||||
vao,
|
||||
vbo,
|
||||
globalAllocation,
|
||||
acquiredTextures,
|
||||
legacyIndexBuffers);
|
||||
acquiredTextures);
|
||||
ResourceReleaseAttempt attempt = rollback.Advance();
|
||||
if (!rollback.IsComplete)
|
||||
{
|
||||
|
|
@ -2248,13 +2157,8 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
private RetryableResourceReleaseLedger CreateUploadRollback(
|
||||
ObjectMeshData meshData,
|
||||
GL? gl,
|
||||
uint vao,
|
||||
uint vbo,
|
||||
GlobalMeshAllocation? globalAllocation,
|
||||
IReadOnlyList<(TextureAtlasManager Atlas, TextureKey Key)> acquiredTextures,
|
||||
IReadOnlyList<(uint Name, int Bytes)> legacyIndexBuffers)
|
||||
IReadOnlyList<(TextureAtlasManager Atlas, TextureKey Key)> acquiredTextures)
|
||||
{
|
||||
var releases = new List<(string Name, Action Release)>();
|
||||
|
||||
|
|
@ -2282,35 +2186,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
acquiredTextures[releaseIndex].Key)));
|
||||
}
|
||||
|
||||
if (!_useModernRendering)
|
||||
{
|
||||
GL legacyGl = gl ?? RequireGl();
|
||||
for (int i = 0; i < legacyIndexBuffers.Count; i++)
|
||||
{
|
||||
int bufferIndex = i;
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{bufferIndex}-delete",
|
||||
() => legacyGl.DeleteBuffer(legacyIndexBuffers[bufferIndex].Name)));
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{bufferIndex}-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
legacyIndexBuffers[bufferIndex].Bytes,
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
|
||||
if (vbo != 0)
|
||||
{
|
||||
releases.Add(("legacy-vertex-buffer-delete", () => legacyGl.DeleteBuffer(vbo)));
|
||||
releases.Add((
|
||||
"legacy-vertex-buffer-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
meshData.Vertices.Length * VertexPositionNormalTexture.Size,
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
if (vao != 0)
|
||||
releases.Add(("legacy-vertex-array-delete", () => legacyGl.DeleteVertexArray(vao)));
|
||||
}
|
||||
|
||||
return new RetryableResourceReleaseLedger(releases);
|
||||
}
|
||||
|
||||
|
|
@ -2447,48 +2322,14 @@ namespace AcDream.App.Rendering.Wb
|
|||
return null;
|
||||
|
||||
var releases = new List<(string Name, Action Release)>();
|
||||
if (_useModernRendering)
|
||||
if (data.GlobalAllocation is { } allocation)
|
||||
{
|
||||
if (data.GlobalAllocation is { } allocation)
|
||||
{
|
||||
releases.Add((
|
||||
"global-index-range",
|
||||
() => GlobalBuffer!.ReleaseIndexRange(allocation)));
|
||||
releases.Add((
|
||||
"global-vertex-range",
|
||||
() => GlobalBuffer!.ReleaseVertexRange(allocation)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GL gl = RequireGl();
|
||||
if (data.VAO != 0)
|
||||
releases.Add(("legacy-vertex-array-delete", () => gl.DeleteVertexArray(data.VAO)));
|
||||
if (data.VBO != 0)
|
||||
{
|
||||
releases.Add(("legacy-vertex-buffer-delete", () => gl.DeleteBuffer(data.VBO)));
|
||||
releases.Add((
|
||||
"legacy-vertex-buffer-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
data.VertexCount * VertexPositionNormalTexture.Size,
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.Batches.Count; i++)
|
||||
{
|
||||
int batchIndex = i;
|
||||
ObjectRenderBatch batch = data.Batches[batchIndex];
|
||||
if (batch.IBO == 0)
|
||||
continue;
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{batchIndex}-delete",
|
||||
() => gl.DeleteBuffer(data.Batches[batchIndex].IBO)));
|
||||
releases.Add((
|
||||
$"legacy-index-buffer-{batchIndex}-accounting",
|
||||
() => GpuMemoryTracker.TrackDeallocation(
|
||||
data.Batches[batchIndex].IndexCount * sizeof(ushort),
|
||||
GpuResourceType.Buffer)));
|
||||
}
|
||||
releases.Add((
|
||||
"global-index-range",
|
||||
() => GlobalBuffer!.ReleaseIndexRange(allocation)));
|
||||
releases.Add((
|
||||
"global-vertex-range",
|
||||
() => GlobalBuffer!.ReleaseVertexRange(allocation)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.Batches.Count; i++)
|
||||
|
|
@ -2841,7 +2682,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
"One or more texture atlases could not be disposed.",
|
||||
failures!);
|
||||
|
||||
if (_useModernRendering && GlobalBuffer is not null)
|
||||
if (GlobalBuffer is not null)
|
||||
Capture(ref failures, GlobalBuffer.Dispose);
|
||||
|
||||
if (failures is not null)
|
||||
|
|
|
|||
|
|
@ -1,776 +0,0 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using AcDream.App.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
|
||||
using IUniformBuffer = Chorizite.OpenGLSDLBackend.IUniformBuffer;
|
||||
using Silk.NET.OpenGL.Extensions.ARB;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using PolygonMode = Silk.NET.OpenGL.PolygonMode;
|
||||
using PrimitiveType = Silk.NET.OpenGL.PrimitiveType;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// OpenGL graphics device
|
||||
/// </summary>
|
||||
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice, IMeshPipelineDevice {
|
||||
private readonly ILogger _log;
|
||||
private readonly DebugRenderSettings _renderSettings;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement;
|
||||
|
||||
public GL GL { get; }
|
||||
public DebugRenderSettings RenderSettings => _renderSettings;
|
||||
|
||||
private readonly ConcurrentQueue<Action<GL>> _glThreadQueue = new();
|
||||
private readonly ConcurrentQueue<Action<GL>> _nextGlThreadQueue = new();
|
||||
|
||||
internal bool HasPendingGLWork =>
|
||||
!_glThreadQueue.IsEmpty || !_nextGlThreadQueue.IsEmpty;
|
||||
|
||||
// Campaign V slice V6i-2: IMeshPipelineDevice. Every member below already
|
||||
// existed under a GL-specific name; these are aliases, not behaviour, so
|
||||
// the shipping backend executes exactly the statements it executed
|
||||
// before. See the interface for what the mesh pipeline actually needs
|
||||
// and what still has to move before it has a second implementation.
|
||||
GL? IMeshPipelineDevice.Gl => GL;
|
||||
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue IMeshPipelineDevice.ResourceRetirement =>
|
||||
_resourceRetirement;
|
||||
|
||||
bool IMeshPipelineDevice.HasPendingWork => HasPendingGLWork;
|
||||
|
||||
void IMeshPipelineDevice.ProcessQueue() => ProcessGLQueue();
|
||||
|
||||
public void QueueGLAction(Action<GL> action) {
|
||||
_glThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
internal void QueueGLActionForNextPass(Action<GL> action) {
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_nextGlThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
public void ProcessGLQueue() {
|
||||
// Retry the prior pass before ordinary work (notably sampler
|
||||
// deletion), but process only the captured generation so a
|
||||
// persistent driver failure cannot spin this frame forever.
|
||||
int retryCount = _nextGlThreadQueue.Count;
|
||||
for (int i = 0; i < retryCount && _nextGlThreadQueue.TryDequeue(out Action<GL>? retry); i++) {
|
||||
try {
|
||||
retry(GL);
|
||||
} catch (Exception ex) {
|
||||
_log.LogError(ex, "Error processing retryable GL queue action");
|
||||
}
|
||||
}
|
||||
// Normal actions retain drain-to-empty semantics because teardown
|
||||
// actions intentionally enqueue dependent atlas releases here.
|
||||
// A persistent retryable error must not starve unrelated uploads
|
||||
// and releases forever: the retry generation remains bounded to
|
||||
// one attempt per pass, while ordinary work still makes progress.
|
||||
while (_glThreadQueue.TryDequeue(out var action)) {
|
||||
try {
|
||||
action(GL);
|
||||
} catch (Exception ex) {
|
||||
_log.LogError(ex, "Error processing GL queue action");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasBindless { get; private set; }
|
||||
public bool HasOpenGL43 { get; private set; }
|
||||
public bool HasBufferStorage { get; private set; }
|
||||
public bool HasTextureStorage { get; private set; }
|
||||
public ArbBindlessTexture? BindlessExtension { get; private set; }
|
||||
|
||||
public uint InstanceVBO { get; private set; }
|
||||
public void* InstanceVBOPtr { get; private set; }
|
||||
|
||||
public uint SharedQuadVBO { get; private set; }
|
||||
public uint SharedDebugVAO { get; private set; }
|
||||
public uint SharedDebugInstanceVBO { get; private set; }
|
||||
|
||||
/// <summary>OpenGL sampler object with TextureWrapMode.Repeat (for meshes with wrapping UVs).</summary>
|
||||
public uint WrapSampler { get; private set; }
|
||||
/// <summary>OpenGL sampler object with TextureWrapMode.ClampToEdge (for meshes without wrapping UVs).</summary>
|
||||
public uint ClampSampler { get; private set; }
|
||||
internal float MaxSupportedAnisotropy { get; private set; }
|
||||
|
||||
private ManagedGLUniformBuffer? _sceneDataBuffer;
|
||||
/// <summary>Shared SceneData UBO.</summary>
|
||||
public ManagedGLUniformBuffer SceneDataBuffer => _sceneDataBuffer!;
|
||||
|
||||
private SceneData _currentSceneData;
|
||||
public SceneData CurrentSceneData => _currentSceneData;
|
||||
|
||||
public void SetSceneData(ref SceneData data) {
|
||||
_currentSceneData = data;
|
||||
SceneDataBuffer.SetData(ref data);
|
||||
}
|
||||
|
||||
private int _instanceBufferCapacity = 0;
|
||||
private int _instanceBufferStride = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IntPtr NativeDevice { get; }
|
||||
|
||||
protected OpenGLGraphicsDevice() : base() {
|
||||
_log = null!;
|
||||
_renderSettings = null!;
|
||||
_resourceRetirement = null!;
|
||||
GL = null!;
|
||||
}
|
||||
|
||||
public OpenGLGraphicsDevice(GL gl, ILogger log, DebugRenderSettings renderSettings, bool allowBindless = true)
|
||||
: this(gl, log, renderSettings, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance, allowBindless) {
|
||||
}
|
||||
|
||||
internal OpenGLGraphicsDevice(
|
||||
GL gl,
|
||||
ILogger log,
|
||||
DebugRenderSettings renderSettings,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||
bool allowBindless = true) : base() {
|
||||
_log = log;
|
||||
_renderSettings = renderSettings;
|
||||
_resourceRetirement = resourceRetirement ?? throw new ArgumentNullException(nameof(resourceRetirement));
|
||||
|
||||
GL = gl;
|
||||
GLHelpers.Init(this, log);
|
||||
|
||||
try {
|
||||
GL.GetInteger(GLEnum.MajorVersion, out int major);
|
||||
GL.GetInteger(GLEnum.MinorVersion, out int minor);
|
||||
HasOpenGL43 = major > 4 || (major == 4 && minor >= 3);
|
||||
HasTextureStorage = major > 4 || (major == 4 && minor >= 2) || GL.IsExtensionPresent("GL_ARB_texture_storage");
|
||||
HasBufferStorage = major > 4 || (major == 4 && minor >= 4) || GL.IsExtensionPresent("GL_ARB_buffer_storage");
|
||||
|
||||
if (allowBindless && GL.TryGetExtension(out ArbBindlessTexture ext)) {
|
||||
BindlessExtension = ext;
|
||||
HasBindless = true;
|
||||
} else {
|
||||
HasBindless = false;
|
||||
}
|
||||
} catch {
|
||||
HasOpenGL43 = false;
|
||||
HasBindless = false;
|
||||
}
|
||||
|
||||
var resources = new ResourceCleanupGroup();
|
||||
try {
|
||||
InstanceVBO = CreateConstructionBuffer(resources, "WB instance buffer");
|
||||
|
||||
// Query this immutable device limit once. Atlas construction can
|
||||
// happen hundreds of times during portal streaming; repeating a
|
||||
// driver GetFloat for every texture serialized the upload burst.
|
||||
if (renderSettings.EnableAnisotropicFiltering) {
|
||||
MaxSupportedAnisotropy = GlResourceCommand.Execute(
|
||||
GL,
|
||||
"query maximum texture anisotropy",
|
||||
() => {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
return Math.Max(0f, maxAniso);
|
||||
});
|
||||
}
|
||||
|
||||
WrapSampler = CreateConstructionSampler(
|
||||
resources,
|
||||
TextureWrapMode.Repeat,
|
||||
"WB repeat sampler");
|
||||
ClampSampler = CreateConstructionSampler(
|
||||
resources,
|
||||
TextureWrapMode.ClampToEdge,
|
||||
"WB clamp sampler");
|
||||
|
||||
_sceneDataBuffer = new ManagedGLUniformBuffer(
|
||||
this,
|
||||
BufferUsage.Dynamic,
|
||||
Marshal.SizeOf<SceneData>());
|
||||
ManagedGLUniformBuffer ownedSceneDataBuffer = _sceneDataBuffer;
|
||||
resources.Add(
|
||||
"WB scene-data uniform buffer",
|
||||
ownedSceneDataBuffer.DisposeImmediately);
|
||||
|
||||
InitializeSharedDebugResources(resources);
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"OpenGLGraphicsDevice construction failed and its GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retires a GL resource only after every submitted draw that could
|
||||
/// reference it has completed on the GPU.
|
||||
/// </summary>
|
||||
internal void RetireGpuResource(Action release) => _resourceRetirement.Retire(release);
|
||||
|
||||
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
|
||||
_resourceRetirement;
|
||||
|
||||
private uint CreateConstructionBuffer(ResourceCleanupGroup resources, string name) {
|
||||
uint buffer = GlResourceCommand.CreateName(GL, name, GL.GenBuffer, GL.DeleteBuffer);
|
||||
resources.Add(
|
||||
name,
|
||||
() => GlResourceCommand.DeleteBuffer(
|
||||
GL,
|
||||
buffer,
|
||||
$"delete {name} {buffer}"));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private uint CreateConstructionSampler(
|
||||
ResourceCleanupGroup resources,
|
||||
TextureWrapMode wrapMode,
|
||||
string name) {
|
||||
uint sampler = GlResourceCommand.CreateName(GL, name, GL.GenSampler, GL.DeleteSampler);
|
||||
resources.Add(
|
||||
name,
|
||||
() => GlResourceCommand.Execute(
|
||||
GL,
|
||||
$"delete {name} {sampler}",
|
||||
() => GL.DeleteSampler(sampler)));
|
||||
GlResourceCommand.Execute(GL, $"configure {name}", () => {
|
||||
GL.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrapMode);
|
||||
GL.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrapMode);
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
SamplerParameterI.MinFilter,
|
||||
(int)TextureMinFilter.LinearMipmapLinear);
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
SamplerParameterI.MagFilter,
|
||||
(int)TextureMagFilter.Linear);
|
||||
if (MaxSupportedAnisotropy > 0)
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
GLEnum.TextureMaxAnisotropy,
|
||||
MaxSupportedAnisotropy);
|
||||
});
|
||||
return sampler;
|
||||
}
|
||||
|
||||
private void InitializeSharedDebugResources(ResourceCleanupGroup resources) {
|
||||
// Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
|
||||
float[] quadVertices = {
|
||||
0.0f, -0.5f,
|
||||
1.0f, -0.5f,
|
||||
1.0f, 0.5f,
|
||||
0.0f, -0.5f,
|
||||
1.0f, 0.5f,
|
||||
0.0f, 0.5f
|
||||
};
|
||||
|
||||
SharedQuadVBO = CreateConstructionBuffer(resources, "WB shared debug quad buffer");
|
||||
SharedDebugInstanceVBO = CreateConstructionBuffer(
|
||||
resources,
|
||||
"WB shared debug instance buffer");
|
||||
SharedDebugVAO = GlResourceCommand.CreateName(
|
||||
GL,
|
||||
"WB shared debug vertex array",
|
||||
GL.GenVertexArray,
|
||||
GL.DeleteVertexArray);
|
||||
uint ownedDebugVao = SharedDebugVAO;
|
||||
resources.Add(
|
||||
"WB shared debug vertex array",
|
||||
() => GlResourceCommand.DeleteVertexArray(
|
||||
GL,
|
||||
ownedDebugVao,
|
||||
$"delete WB shared debug vertex array {ownedDebugVao}"));
|
||||
|
||||
GlResourceCommand.Execute(GL, "configure WB shared debug resources", () => {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
fixed (float* pQuad = quadVertices) {
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(nuint)(quadVertices.Length * sizeof(float)),
|
||||
pQuad,
|
||||
GLEnum.StaticDraw);
|
||||
}
|
||||
|
||||
// Initial capacity for debug instances.
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(nuint)(1024 * 44),
|
||||
(void*)0,
|
||||
GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
|
||||
|
||||
GL.BindVertexArray(SharedDebugVAO);
|
||||
|
||||
// Quad Pos attribute (location 0)
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
|
||||
|
||||
// Instance attributes
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
uint lineInstanceSize = 44;
|
||||
|
||||
// aStart (location 1)
|
||||
GL.EnableVertexAttribArray(1);
|
||||
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
|
||||
GL.VertexAttribDivisor(1, 1);
|
||||
|
||||
// aEnd (location 2)
|
||||
GL.EnableVertexAttribArray(2);
|
||||
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12);
|
||||
GL.VertexAttribDivisor(2, 1);
|
||||
|
||||
// aColor (location 3)
|
||||
GL.EnableVertexAttribArray(3);
|
||||
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24);
|
||||
GL.VertexAttribDivisor(3, 1);
|
||||
|
||||
// aThickness (location 4)
|
||||
GL.EnableVertexAttribArray(4);
|
||||
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40);
|
||||
GL.VertexAttribDivisor(4, 1);
|
||||
|
||||
GL.BindVertexArray(0);
|
||||
});
|
||||
}
|
||||
|
||||
public void EnsureInstanceBufferCapacity(int count, int stride, bool forceOrphan = false) {
|
||||
if (count <= _instanceBufferCapacity && !forceOrphan) return;
|
||||
|
||||
if (_instanceBufferCapacity > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(_instanceBufferCapacity * _instanceBufferStride);
|
||||
}
|
||||
|
||||
_instanceBufferCapacity = Math.Max(count, 256);
|
||||
_instanceBufferStride = stride;
|
||||
|
||||
if (HasBufferStorage) {
|
||||
if (InstanceVBO != 0) {
|
||||
GL.DeleteBuffer(InstanceVBO);
|
||||
}
|
||||
GL.GenBuffers(1, out uint instanceVbo);
|
||||
InstanceVBO = instanceVbo;
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
var flags = BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit;
|
||||
GL.BufferStorage(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride), (void*)0, flags);
|
||||
InstanceVBOPtr = GL.MapBufferRange(GLEnum.ArrayBuffer, 0, (nuint)(_instanceBufferCapacity * _instanceBufferStride), MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(_instanceBufferCapacity * _instanceBufferStride),
|
||||
(void*)null, GLEnum.DynamicDraw);
|
||||
InstanceVBOPtr = null;
|
||||
}
|
||||
GpuMemoryTracker.TrackAllocation(_instanceBufferCapacity * _instanceBufferStride);
|
||||
}
|
||||
|
||||
public void UpdateInstanceBuffer<T>(List<T> data) where T : unmanaged {
|
||||
EnsureInstanceBufferCapacity(data.Count, Marshal.SizeOf<T>(), true);
|
||||
var span = CollectionsMarshal.AsSpan(data);
|
||||
if (InstanceVBOPtr != null) {
|
||||
var destSpan = new Span<T>(InstanceVBOPtr, data.Count);
|
||||
span.CopyTo(destSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
fixed (T* ptr = span) {
|
||||
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Count * Marshal.SizeOf<T>()), ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateInstanceBuffer<T>(Span<T> data) where T : unmanaged {
|
||||
EnsureInstanceBufferCapacity(data.Length, Marshal.SizeOf<T>(), true);
|
||||
if (InstanceVBOPtr != null) {
|
||||
var destSpan = new Span<T>(InstanceVBOPtr, data.Length);
|
||||
data.CopyTo(destSpan);
|
||||
} else {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, InstanceVBO);
|
||||
fixed (T* ptr = data) {
|
||||
GL.BufferSubData(GLEnum.ArrayBuffer, 0, (nuint)(data.Length * Marshal.SizeOf<T>()), ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear(ColorVec color, ClearFlags flags, float depth, int stencil) {
|
||||
GL.ClearColor(color.R, color.G, color.B, color.A);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.Clear((uint)Convert(flags));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IIndexBuffer CreateIndexBuffer(int size,
|
||||
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
|
||||
return new ManagedGLIndexBuffer(this, usage, size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IVertexBuffer CreateVertexBuffer(int size,
|
||||
Chorizite.Core.Render.Enums.BufferUsage usage = Chorizite.Core.Render.Enums.BufferUsage.Static) {
|
||||
return new ManagedGLVertexBuffer(this, usage, size);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IVertexArray CreateArrayBuffer(IVertexBuffer vertexBuffer, VertexFormat format) {
|
||||
return new ManagedGLVertexArray(this, vertexBuffer, format);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DrawElements(Chorizite.Core.Render.Enums.PrimitiveType type, int numElements, int indiceOffset = 0) {
|
||||
GL.DrawElements(Convert(type), (uint)numElements, GLEnum.UnsignedInt, (void*)(indiceOffset * sizeof(uint)));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
public override IShader CreateShader(string name, string vertexCode, string fragmentCode) {
|
||||
var key = $"{GL.GetHashCode()}_{name}_{vertexCode.GetHashCode()}_{fragmentCode.GetHashCode()}";
|
||||
|
||||
while (true) {
|
||||
if (_shaderCache.TryGetValue(key, out var existing)) {
|
||||
if (existing is SharedShader shared && shared.TryIncrement()) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
var inner = new GLSLShader(this, name, vertexCode, fragmentCode, _log);
|
||||
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
|
||||
|
||||
if (_shaderCache.TryAdd(key, newShader)) {
|
||||
return newShader;
|
||||
}
|
||||
|
||||
// Someone else added it first, dispose ours and try again
|
||||
newShader.DisposeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IShader CreateShader(string name, string shaderDirectory) {
|
||||
var key = $"{GL.GetHashCode()}_{name}";
|
||||
|
||||
while (true) {
|
||||
if (_shaderCache.TryGetValue(key, out var existing)) {
|
||||
if (existing is SharedShader shared && shared.TryIncrement()) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
var inner = new GLSLShader(this, name, shaderDirectory, _log);
|
||||
var newShader = new SharedShader(inner, () => _shaderCache.TryRemove(key, out _));
|
||||
|
||||
if (_shaderCache.TryAdd(key, newShader)) {
|
||||
return newShader;
|
||||
}
|
||||
|
||||
// Someone else added it first, dispose ours and try again
|
||||
newShader.DisposeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, IShader> _shaderCache = new();
|
||||
|
||||
private class SharedShader : IShader, IDisposable {
|
||||
private readonly IShader _shader;
|
||||
private readonly Action _onDispose;
|
||||
private int _refCount = 1;
|
||||
|
||||
public string Name => _shader.Name;
|
||||
public uint ProgramId => _shader.ProgramId;
|
||||
|
||||
public SharedShader(IShader shader, Action onDispose) {
|
||||
_shader = shader;
|
||||
_onDispose = onDispose;
|
||||
}
|
||||
|
||||
public bool TryIncrement() {
|
||||
while (true) {
|
||||
int current = _refCount;
|
||||
if (current <= 0) return false;
|
||||
if (Interlocked.CompareExchange(ref _refCount, current + 1, current) == current) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Bind() => _shader.Bind();
|
||||
public void Unbind() => _shader.Unbind();
|
||||
public void Load(string vertexSource, string fragmentSource) => _shader.Load(vertexSource, fragmentSource);
|
||||
|
||||
public void SetUniform(string name, int value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, float value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Vector2 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Vector3 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Vector4 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, Matrix4x4 value) => _shader.SetUniform(name, value);
|
||||
public void SetUniform(string name, float[] values) => _shader.SetUniform(name, values);
|
||||
|
||||
public void DisposeInternal() {
|
||||
_refCount = 0;
|
||||
(_shader as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
if (Interlocked.Decrement(ref _refCount) == 0) {
|
||||
(_shader as IDisposable)?.Dispose();
|
||||
_onDispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITexture
|
||||
CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data = null) {
|
||||
if (format != TextureFormat.RGBA8) {
|
||||
throw new NotImplementedException($"Texture format {format} is not supported.");
|
||||
}
|
||||
|
||||
return new ManagedGLTexture(this, data, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture with custom texture parameters.
|
||||
/// </summary>
|
||||
public ITexture CreateTextureInternal(TextureFormat format, int width, int height, byte[]? data, TextureParameters texParams) {
|
||||
if (format != TextureFormat.RGBA8) {
|
||||
throw new NotImplementedException($"Texture format {format} is not supported.");
|
||||
}
|
||||
return new ManagedGLTexture(this, data, width, height, texParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITexture? CreateTextureInternal(TextureFormat format, string filename) {
|
||||
if (format != TextureFormat.RGBA8) {
|
||||
throw new NotImplementedException($"Texture format {format} is not supported.");
|
||||
}
|
||||
|
||||
return new ManagedGLTexture(this, filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ITextureArray
|
||||
CreateTextureArrayInternal(TextureFormat format, int width, int height, int size) {
|
||||
return new ManagedGLTextureArray(this, format, width, height, size, _log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture array with custom texture parameters.
|
||||
/// </summary>
|
||||
public ITextureArray CreateTextureArrayInternal(TextureFormat format, int width, int height, int size, TextureParameters texParams) {
|
||||
return new ManagedGLTextureArray(this, format, width, height, size, _log, texParams);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void BeginFrame() {
|
||||
GL.Viewport(Viewport.X, Viewport.Y, (uint)Viewport.Width, (uint)Viewport.Height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void EndFrame() {
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetRenderStateInternal(RenderState state, bool enabled) {
|
||||
switch (state) {
|
||||
case RenderState.AlphaBlend:
|
||||
if (enabled) GL.Enable(EnableCap.Blend);
|
||||
else GL.Disable(EnableCap.Blend);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.DepthTest:
|
||||
if (enabled) GL.Enable(EnableCap.DepthTest);
|
||||
else GL.Disable(EnableCap.DepthTest);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.ScissorTest:
|
||||
if (enabled) GL.Enable(EnableCap.ScissorTest);
|
||||
else GL.Disable(EnableCap.ScissorTest);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.DepthWrite:
|
||||
if (enabled) GL.DepthMask(true);
|
||||
else GL.DepthMask(false);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
break;
|
||||
case RenderState.Fog:
|
||||
break;
|
||||
case RenderState.Lighting:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetBlendFactorInternal(BlendFactor srcBlendFactor, BlendFactor dstBlendFactor) {
|
||||
GL.BlendFunc(Convert(srcBlendFactor), Convert(dstBlendFactor));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetScissorRectInternal(Rectangle scissor) {
|
||||
var gtop = (int)Viewport.Height - scissor.Y - scissor.Height;
|
||||
GL.Scissor(scissor.X, gtop, (uint)scissor.Width, (uint)scissor.Height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetViewportInternal(Rectangle viewport) {
|
||||
GL.Viewport(viewport.X, viewport.Y, (uint)viewport.Width, (uint)viewport.Height);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetPolygonModeInternal(Chorizite.Core.Render.Enums.PolygonMode polygonMode) {
|
||||
GL.PolygonMode(GLEnum.FrontAndBack, Convert(polygonMode));
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
protected override void SetCullModeInternal(CullMode cullMode) {
|
||||
switch (cullMode) {
|
||||
case CullMode.None:
|
||||
GL.Disable(EnableCap.CullFace);
|
||||
break;
|
||||
case CullMode.Front:
|
||||
GL.Enable(EnableCap.CullFace);
|
||||
GL.CullFace(GLEnum.Front);
|
||||
break;
|
||||
case CullMode.Back:
|
||||
GL.Enable(EnableCap.CullFace);
|
||||
GL.CullFace(GLEnum.Back);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private GLEnum Convert(Chorizite.Core.Render.Enums.PolygonMode mode) {
|
||||
switch (mode) {
|
||||
case Chorizite.Core.Render.Enums.PolygonMode.Fill:
|
||||
return GLEnum.Fill;
|
||||
case Chorizite.Core.Render.Enums.PolygonMode.Line:
|
||||
return GLEnum.Line;
|
||||
case Chorizite.Core.Render.Enums.PolygonMode.Point:
|
||||
return GLEnum.Point;
|
||||
default:
|
||||
return GLEnum.Fill;
|
||||
}
|
||||
}
|
||||
|
||||
private GLEnum Convert(ClearFlags flags) {
|
||||
GLEnum mask = 0;
|
||||
|
||||
if ((flags & ClearFlags.Color) == ClearFlags.Color) mask |= GLEnum.ColorBufferBit;
|
||||
if ((flags & ClearFlags.Depth) == ClearFlags.Depth) mask |= GLEnum.DepthBufferBit;
|
||||
if ((flags & ClearFlags.Stencil) == ClearFlags.Stencil) mask |= GLEnum.StencilBufferBit;
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
private GLEnum Convert(BlendFactor factor) {
|
||||
switch (factor) {
|
||||
case BlendFactor.One:
|
||||
return GLEnum.One;
|
||||
case BlendFactor.SrcAlpha:
|
||||
return GLEnum.SrcAlpha;
|
||||
case BlendFactor.OneMinusSrcAlpha:
|
||||
return GLEnum.OneMinusSrcAlpha;
|
||||
case BlendFactor.DstAlpha:
|
||||
return GLEnum.DstAlpha;
|
||||
case BlendFactor.OneMinusDstAlpha:
|
||||
return GLEnum.OneMinusDstAlpha;
|
||||
default:
|
||||
return GLEnum.One;
|
||||
}
|
||||
}
|
||||
|
||||
private PrimitiveType Convert(Chorizite.Core.Render.Enums.PrimitiveType type) {
|
||||
switch (type) {
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.PointList:
|
||||
return PrimitiveType.Points;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.LineList:
|
||||
return PrimitiveType.Lines;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.LineStrip:
|
||||
return PrimitiveType.LineStrip;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleList:
|
||||
return PrimitiveType.Triangles;
|
||||
case Chorizite.Core.Render.Enums.PrimitiveType.TriangleStrip:
|
||||
return PrimitiveType.TriangleStrip;
|
||||
default:
|
||||
throw new NotImplementedException($"Primitive type {type} is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IFramebuffer CreateFramebuffer(ITexture texture, int width, int height,
|
||||
bool hasDepthStencil = true) {
|
||||
if (texture == null) {
|
||||
throw new ArgumentNullException(nameof(texture));
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new ArgumentException("Width and height must be positive.");
|
||||
}
|
||||
|
||||
return new ManagedGLFramebuffer(this, texture, width, height, hasDepthStencil);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void BindFramebuffer(IFramebuffer? framebuffer) {
|
||||
uint fboId = framebuffer != null ? (uint)framebuffer.NativeHandle.ToInt32() : 0;
|
||||
GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose() {
|
||||
var instanceVBO = InstanceVBO;
|
||||
var instanceBufferCapacity = _instanceBufferCapacity;
|
||||
var instanceBufferStride = _instanceBufferStride;
|
||||
var wrapSampler = WrapSampler;
|
||||
var clampSampler = ClampSampler;
|
||||
|
||||
var sharedQuadVbo = SharedQuadVBO;
|
||||
var sharedDebugInstanceVbo = SharedDebugInstanceVBO;
|
||||
var sharedDebugVao = SharedDebugVAO;
|
||||
|
||||
QueueGLAction(gl => {
|
||||
if (sharedQuadVbo != 0) gl.DeleteBuffer(sharedQuadVbo);
|
||||
if (sharedDebugInstanceVbo != 0) gl.DeleteBuffer(sharedDebugInstanceVbo);
|
||||
if (sharedDebugVao != 0) gl.DeleteVertexArray(sharedDebugVao);
|
||||
if (instanceVBO != 0) {
|
||||
gl.DeleteBuffer(instanceVBO);
|
||||
if (instanceBufferCapacity > 0) {
|
||||
GpuMemoryTracker.TrackDeallocation(instanceBufferCapacity * instanceBufferStride);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Bindless texture-array retirements embed these samplers in their
|
||||
// resident handles. Ordinary GL work must keep flowing when one
|
||||
// retry is sick, but sampler deletion itself is dependency-ordered
|
||||
// behind the retry queue. Requeue into the next generation (never
|
||||
// the drain-to-empty ordinary queue) to remain one attempt/frame.
|
||||
Action<GL>? deleteSamplersWhenSafe = null;
|
||||
deleteSamplersWhenSafe = gl => {
|
||||
if (!_nextGlThreadQueue.IsEmpty) {
|
||||
QueueGLActionForNextPass(deleteSamplersWhenSafe!);
|
||||
return;
|
||||
}
|
||||
if (wrapSampler != 0)
|
||||
gl.DeleteSampler(wrapSampler);
|
||||
if (clampSampler != 0)
|
||||
gl.DeleteSampler(clampSampler);
|
||||
};
|
||||
QueueGLActionForNextPass(deleteSamplersWhenSafe);
|
||||
|
||||
InstanceVBO = 0;
|
||||
InstanceVBOPtr = null;
|
||||
WrapSampler = 0;
|
||||
ClampSampler = 0;
|
||||
_sceneDataBuffer?.Dispose();
|
||||
_sceneDataBuffer = null;
|
||||
}
|
||||
|
||||
public override IUniformBuffer CreateUniformBuffer(BufferUsage usage, int size) {
|
||||
return (IUniformBuffer)new ManagedGLUniformBuffer(this, usage, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks currently-bound GL state to skip redundant rebinds across the
|
||||
/// WB-derived render path. Previously these were static fields on
|
||||
/// <c>BaseObjectRenderManager</c> in the WorldBuilder.Shared project; inlined
|
||||
/// here in Phase O-T7 to eliminate the WorldBuilder project reference.
|
||||
///
|
||||
/// Semantics are identical to the WB originals:
|
||||
/// <c>CurrentAtlas</c> — slot index of the currently bound texture atlas.
|
||||
/// <c>CurrentVAO</c> — OpenGL name of the currently bound vertex array object.
|
||||
/// <c>CurrentIBO</c> — OpenGL name of the currently bound index buffer object.
|
||||
/// Sentinel value 0 means "no valid binding cached."
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// All accesses must occur on the render thread. GL state binding is
|
||||
/// not thread-safe; these sentinels are written immediately after the
|
||||
/// corresponding glBind* call and read by the next dispatch on the
|
||||
/// same thread.
|
||||
/// </remarks>
|
||||
public static class RenderStateCache
|
||||
{
|
||||
public static uint CurrentAtlas = 0;
|
||||
public static uint CurrentVAO = 0;
|
||||
public static uint CurrentIBO = 0;
|
||||
}
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
using Silk.NET.OpenGL;
|
||||
using AcDream.App.Rendering;
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Always-on transaction boundary and accounting for raw dynamic GL objects.
|
||||
/// Growth keeps the published CPU capacity unchanged until BufferData succeeds;
|
||||
/// OpenGL leaves the previous data store intact when allocation reports an
|
||||
/// error, so the caller can continue using the old capacity or unwind.
|
||||
/// </summary>
|
||||
internal static unsafe class TrackedGlResource
|
||||
{
|
||||
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
|
||||
GL gl,
|
||||
uint buffer,
|
||||
long capacityBytes,
|
||||
string context) =>
|
||||
CreateRetryableBufferDeletion(
|
||||
gl,
|
||||
buffer,
|
||||
() => capacityBytes,
|
||||
context);
|
||||
|
||||
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
|
||||
GL gl,
|
||||
uint buffer,
|
||||
Func<long> capacityBytes,
|
||||
string context)
|
||||
{
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
ArgumentNullException.ThrowIfNull(capacityBytes);
|
||||
return new RetryableGpuResourceRelease(
|
||||
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
|
||||
() =>
|
||||
{
|
||||
gl.DeleteBuffer(buffer);
|
||||
// Per the GL error contract, a command which generates an
|
||||
// error does not change object state. Keep mutation and its
|
||||
// validation in one retryable stage so that failed deletion
|
||||
// is issued again, while later accounting remains untouched.
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
long bytes = capacityBytes();
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(bytes);
|
||||
if (bytes != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(bytes, GpuResourceType.Buffer);
|
||||
},
|
||||
() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer));
|
||||
}
|
||||
|
||||
public static RetryableGpuResourceRelease CreateRetryableVertexArrayDeletion(
|
||||
GL gl,
|
||||
uint vertexArray,
|
||||
string context,
|
||||
Action? trackDeallocation = null)
|
||||
{
|
||||
if (vertexArray == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(vertexArray));
|
||||
return new RetryableGpuResourceRelease(
|
||||
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
|
||||
() =>
|
||||
{
|
||||
gl.DeleteVertexArray(vertexArray);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
},
|
||||
trackDeallocation
|
||||
?? (() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO)));
|
||||
}
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
BufferTargetARB target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
BufferUsageARB usage,
|
||||
string context)
|
||||
=> AllocateBufferStorage(
|
||||
gl,
|
||||
(GLEnum)target,
|
||||
buffer,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
(GLEnum)usage,
|
||||
context);
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
BufferTargetARB target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
BufferUsageARB usage,
|
||||
void* data,
|
||||
string context)
|
||||
=> AllocateBufferStorage(
|
||||
gl,
|
||||
(GLEnum)target,
|
||||
buffer,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
(GLEnum)usage,
|
||||
data,
|
||||
context);
|
||||
|
||||
public static uint CreateBuffer(GL gl, string context)
|
||||
{
|
||||
return CreateTrackedName(
|
||||
gl,
|
||||
"buffer",
|
||||
context,
|
||||
gl.GenBuffer,
|
||||
name => GlResourceCommand.DeleteBuffer(
|
||||
gl,
|
||||
name,
|
||||
$"rollback buffer {name} after failed {context}"),
|
||||
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer));
|
||||
}
|
||||
|
||||
public static uint CreateVertexArray(GL gl, string context)
|
||||
{
|
||||
return CreateTrackedName(
|
||||
gl,
|
||||
"vertex-array",
|
||||
context,
|
||||
gl.GenVertexArray,
|
||||
name => GlResourceCommand.DeleteVertexArray(
|
||||
gl,
|
||||
name,
|
||||
$"rollback vertex array {name} after failed {context}"),
|
||||
() => GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.VAO));
|
||||
}
|
||||
|
||||
private static uint CreateTrackedName(
|
||||
GL gl,
|
||||
string resourceName,
|
||||
string context,
|
||||
Func<uint> create,
|
||||
Action<uint> rollback,
|
||||
Action publishAccounting)
|
||||
{
|
||||
uint name = GlResourceCommand.CreateName(
|
||||
gl,
|
||||
$"{resourceName} for {context}",
|
||||
create,
|
||||
rollback);
|
||||
var cleanup = new ResourceCleanupGroup();
|
||||
cleanup.Add($"{resourceName} name {name}", () => rollback(name));
|
||||
try
|
||||
{
|
||||
publishAccounting();
|
||||
return name;
|
||||
}
|
||||
catch (Exception publicationFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
cleanup.RetryCleanup();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new GlResourceConstructionException(
|
||||
$"Publishing {resourceName} accounting failed and GL name {name} could not be released.",
|
||||
cleanup,
|
||||
[publicationFailure, cleanupFailure]);
|
||||
}
|
||||
|
||||
ExceptionDispatchInfo.Capture(publicationFailure).Throw();
|
||||
throw new InvalidOperationException("Unreachable resource-publication path.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
GLEnum target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
GLEnum usage,
|
||||
string context)
|
||||
=> AllocateBufferStorage(
|
||||
gl,
|
||||
target,
|
||||
buffer,
|
||||
previousBytes,
|
||||
newBytes,
|
||||
usage,
|
||||
null,
|
||||
context);
|
||||
|
||||
public static void AllocateBufferStorage(
|
||||
GL gl,
|
||||
GLEnum target,
|
||||
uint buffer,
|
||||
long previousBytes,
|
||||
long newBytes,
|
||||
GLEnum usage,
|
||||
void* data,
|
||||
string context)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(previousBytes);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(newBytes, 1);
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.BindBuffer(target, buffer);
|
||||
gl.BufferData(target, checked((nuint)newBytes), data, usage);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
|
||||
long delta = checked(newBytes - previousBytes);
|
||||
if (delta > 0)
|
||||
GpuMemoryTracker.TrackAllocation(delta, GpuResourceType.Buffer);
|
||||
else if (delta < 0)
|
||||
GpuMemoryTracker.TrackDeallocation(-delta, GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
public static void UpdateBufferSubData(
|
||||
GL gl,
|
||||
BufferTargetARB target,
|
||||
uint buffer,
|
||||
nint byteOffset,
|
||||
long byteCount,
|
||||
void* data,
|
||||
string context)
|
||||
=> UpdateBufferSubData(
|
||||
gl,
|
||||
(GLEnum)target,
|
||||
buffer,
|
||||
byteOffset,
|
||||
byteCount,
|
||||
data,
|
||||
context);
|
||||
|
||||
public static void UpdateBufferSubData(
|
||||
GL gl,
|
||||
GLEnum target,
|
||||
uint buffer,
|
||||
nint byteOffset,
|
||||
long byteCount,
|
||||
void* data,
|
||||
string context)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(byteOffset);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(byteCount, 1);
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
ArgumentNullException.ThrowIfNull(data);
|
||||
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.BindBuffer(target, buffer);
|
||||
gl.BufferSubData(target, byteOffset, checked((nuint)byteCount), data);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
}
|
||||
|
||||
public static void DeleteBuffer(GL gl, uint buffer, long capacityBytes, string context)
|
||||
{
|
||||
if (buffer == 0)
|
||||
return;
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.DeleteBuffer(buffer);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
if (capacityBytes != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(capacityBytes, GpuResourceType.Buffer);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer);
|
||||
}
|
||||
|
||||
public static void DeleteVertexArray(GL gl, uint vertexArray, string context)
|
||||
{
|
||||
if (vertexArray == 0)
|
||||
return;
|
||||
GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)");
|
||||
gl.DeleteVertexArray(vertexArray);
|
||||
GLHelpers.ThrowOnResourceError(gl, context);
|
||||
GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.VAO);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -200,39 +200,15 @@ public sealed class WbMeshAdapter
|
|||
{
|
||||
// Campaign V slice V6i-3: WHICH mesh-pipeline device is decided
|
||||
// here, once, and it is the only place in the mesh pipeline that
|
||||
// names a backend. The GL arm is unchanged — same construction,
|
||||
// same queue-drain guarantee on rollback. A backend with no context
|
||||
// gets the RHI arm, whose queue is empty by construction because
|
||||
// Vulkan resource work is recorded or retirement-queued rather than
|
||||
// deferred onto a context-owning thread.
|
||||
if (gl is { } context)
|
||||
{
|
||||
var openGl = new OpenGLGraphicsDevice(
|
||||
context,
|
||||
logger,
|
||||
new DebugRenderSettings(),
|
||||
resourceRetirement);
|
||||
graphicsDevice = openGl;
|
||||
var graphicsDeviceRelease = new RetryableGpuResourceRelease(
|
||||
openGl.Dispose,
|
||||
() =>
|
||||
{
|
||||
openGl.ProcessGLQueue();
|
||||
if (openGl.HasPendingGLWork)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"WB graphics-device construction cleanup still has queued GL work.");
|
||||
}
|
||||
});
|
||||
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rhiDevice = new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(
|
||||
resourceRetirement);
|
||||
graphicsDevice = rhiDevice;
|
||||
resources.Add("WB graphics device", rhiDevice.Dispose);
|
||||
}
|
||||
// names a backend. The raw-GL arm (OpenGLGraphicsDevice) was
|
||||
// deleted at Campaign V slice V11; the RHI arm's queue is empty by
|
||||
// construction because Vulkan resource work is recorded or
|
||||
// retirement-queued rather than deferred onto a context-owning
|
||||
// thread.
|
||||
var rhiDevice = new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(
|
||||
resourceRetirement);
|
||||
graphicsDevice = rhiDevice;
|
||||
resources.Add("WB graphics device", rhiDevice.Dispose);
|
||||
if (resolvedPreparedAssets is null)
|
||||
{
|
||||
resolvedPreparedAssets = new DatPreparedAssetSource(
|
||||
|
|
@ -267,19 +243,6 @@ public sealed class WbMeshAdapter
|
|||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V4t: the GL device whose texture table every mesh
|
||||
/// batch's <c>GpuTextureSlot</c> indexes. The world renderers this adapter
|
||||
/// feeds flush and bind that table before their raw-GL draws, and taking it
|
||||
/// from here rather than from a second composition wire is what makes "the
|
||||
/// slot and the table came from the same device" true by construction.
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
(_meshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"An initialized mesh adapter is required for the world texture table."))
|
||||
.WorldTextureTable;
|
||||
|
||||
internal void RegisterResidencySources(ResidencyManager manager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manager);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Gpu.Gl;
|
||||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
using AcDream.Core.Rendering.Wb;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
|
|
@ -15,21 +15,21 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// deliberately left CREATION with the caches — plan §5.5.11 records why, and
|
||||
/// §5.5.12 item 1 hands the remainder forward: "the missing piece is an
|
||||
/// <c>ITextureArray</c> implementation over <see cref="IGpuTexture"/>, not a
|
||||
/// codec." This is that interface. <see cref="ManagedGLTextureArray"/> and
|
||||
/// <see cref="RhiWorldTextureArray"/> implement it, and which one exists is
|
||||
/// decided once at composition by <see cref="IWorldTextureArrayFactory"/> —
|
||||
/// never per call, so the GL path executes exactly the statements it executed
|
||||
/// before.</para>
|
||||
/// codec." This is that interface. <c>ManagedGLTextureArray</c> used to be its
|
||||
/// GL implementation, alongside <see cref="RhiWorldTextureArray"/>; which one
|
||||
/// existed was decided once at composition by
|
||||
/// <see cref="IWorldTextureArrayFactory"/>, never per call. Campaign V slice
|
||||
/// V11 deleted <c>ManagedGLTextureArray</c> along with the rest of the raw-GL
|
||||
/// arm, so <see cref="RhiWorldTextureArray"/> is now the sole implementation.</para>
|
||||
///
|
||||
/// <para><b>The slot, not the handle, is the seam.</b> Before this slice
|
||||
/// <para><b>The slot, not the handle, is the seam.</b> Before V6i-2
|
||||
/// <c>ObjectMeshManager</c> read <c>BindlessWrapHandle</c>/
|
||||
/// <c>BindlessClampHandle</c> off the concrete GL array and interned them into
|
||||
/// the device table itself. A 64-bit <c>ARB_bindless_texture</c> handle is
|
||||
/// unspellable on Vulkan, so the array now answers the question the caller was
|
||||
/// really asking — <see cref="ResolveSlot"/> — and each implementation gets
|
||||
/// there its own way: the GL array interns its resident handle (the same
|
||||
/// idempotent call, one level down), while the RHI array registered its two
|
||||
/// (texture, sampler) pairs at construction and returns a field.</para>
|
||||
/// unspellable on Vulkan, so the array answers the question the caller was
|
||||
/// really asking — <see cref="ResolveSlot"/> — instead: the RHI array
|
||||
/// registered its two (texture, sampler) pairs at construction and returns a
|
||||
/// field.</para>
|
||||
/// </summary>
|
||||
internal interface IWorldTextureArray : IDisposable
|
||||
{
|
||||
|
|
@ -116,9 +116,9 @@ internal interface IWorldTextureArrayFactory
|
|||
ArgumentNullException.ThrowIfNull(graphicsDevice);
|
||||
ArgumentNullException.ThrowIfNull(gpuDevice);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
return graphicsDevice is OpenGLGraphicsDevice gl && gpuDevice is GlGpuDevice table
|
||||
? new GlWorldTextureArrayFactory(gl, table, logger)
|
||||
: new RhiWorldTextureArrayFactory(gpuDevice);
|
||||
// The GL arm this used to select between was deleted at Campaign V
|
||||
// slice V11; the RHI arm is the only one left.
|
||||
return new RhiWorldTextureArrayFactory(gpuDevice);
|
||||
}
|
||||
|
||||
/// <summary>The retirement queue array layers and images are released through.</summary>
|
||||
|
|
@ -132,36 +132,6 @@ internal interface IWorldTextureArrayFactory
|
|||
IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL arm. Delegates to the same <c>OpenGLGraphicsDevice</c> entry point
|
||||
/// <see cref="TextureAtlasManager"/> called directly before this slice, so the
|
||||
/// shipping backend's construction is textually unchanged.
|
||||
/// </summary>
|
||||
internal sealed class GlWorldTextureArrayFactory(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
GlGpuDevice worldTextureTable,
|
||||
ILogger logger) : IWorldTextureArrayFactory
|
||||
{
|
||||
private readonly OpenGLGraphicsDevice _graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
private readonly GlGpuDevice _worldTextureTable = worldTextureTable
|
||||
?? throw new ArgumentNullException(nameof(worldTextureTable));
|
||||
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
public IGpuResourceRetirementQueue Retirement => _graphicsDevice.ResourceRetirement;
|
||||
|
||||
public IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers) =>
|
||||
new ManagedGLTextureArray(
|
||||
_graphicsDevice,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
layers,
|
||||
_logger,
|
||||
_worldTextureTable,
|
||||
TextureParameters.ClampToEdge);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The backend-neutral arm. Creates through <see cref="IGpuDevice.CreateTexture"/>
|
||||
/// and registers both address modes into the device's one texture table, so an
|
||||
|
|
@ -353,11 +323,7 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
|
|||
ArgumentNullException.ThrowIfNull(data);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(layer);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, Size);
|
||||
// The GL array validates the payload against the format's expected byte
|
||||
// count and rejects transfer overrides that contradict it. Reusing that
|
||||
// validator rather than writing a second one keeps the two arms agreeing
|
||||
// on what a well-formed layer is.
|
||||
ManagedGLTextureArray.ValidateUploadPayload(
|
||||
ValidateUploadPayload(
|
||||
SourceFormat,
|
||||
_width,
|
||||
_height,
|
||||
|
|
@ -504,4 +470,65 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
|
|||
+ "and is not part of GpuTextureDescription. Campaign V's world-draw slice owns "
|
||||
+ "extending the contract or proving no such atlas exists."),
|
||||
};
|
||||
|
||||
private static bool IsCompressedFormat(TextureFormat format) =>
|
||||
format is TextureFormat.DXT1 or TextureFormat.DXT3 or TextureFormat.DXT5;
|
||||
|
||||
/// <summary>
|
||||
/// The expected byte count for one uploaded layer of <paramref name="format"/>
|
||||
/// at <paramref name="width"/>x<paramref name="height"/>.
|
||||
/// </summary>
|
||||
internal static int CalculateExpectedDataSize(TextureFormat format, int width, int height)
|
||||
{
|
||||
if (IsCompressedFormat(format))
|
||||
return TextureHelpers.GetCompressedLayerSize(width, height, format);
|
||||
|
||||
return format switch
|
||||
{
|
||||
TextureFormat.RGBA8 => checked(width * height * 4),
|
||||
TextureFormat.RGB8 => checked(width * height * 3),
|
||||
TextureFormat.A8 => checked(width * height),
|
||||
TextureFormat.Rgba32f => checked(width * height * 16),
|
||||
_ => throw new NotSupportedException($"Unsupported format {format}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an upload payload against the format's expected byte count and
|
||||
/// rejects transfer overrides that contradict it.
|
||||
/// </summary>
|
||||
internal static void ValidateUploadPayload(
|
||||
TextureFormat format,
|
||||
int width,
|
||||
int height,
|
||||
int dataLength,
|
||||
PixelFormat? uploadPixelFormat,
|
||||
PixelType? uploadPixelType)
|
||||
{
|
||||
int expectedBytes = CalculateExpectedDataSize(format, width, height);
|
||||
if (dataLength != expectedBytes)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Texture-array layer payload has {dataLength} bytes; expected exactly {expectedBytes} "
|
||||
+ $"for {format} {width}x{height}.",
|
||||
nameof(dataLength));
|
||||
}
|
||||
|
||||
if (IsCompressedFormat(format))
|
||||
{
|
||||
if (uploadPixelFormat.HasValue || uploadPixelType.HasValue)
|
||||
throw new ArgumentException("Compressed texture uploads cannot specify pixel format/type overrides.");
|
||||
return;
|
||||
}
|
||||
|
||||
PixelFormat expectedFormat = format.ToPixelFormat();
|
||||
PixelType expectedType = format.ToPixelType();
|
||||
if ((uploadPixelFormat ?? expectedFormat) != expectedFormat
|
||||
|| (uploadPixelType ?? expectedType) != expectedType)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Upload descriptor {uploadPixelFormat}/{uploadPixelType} does not match "
|
||||
+ $"the {expectedFormat}/{expectedType} transfer required by {format}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 (<c>RenderFrameGlStateController</c>) was deleted
|
||||
/// at Campaign V slice V11; <see cref="NullRenderFrameGlState"/> is the only
|
||||
/// implementation left.
|
||||
/// </summary>
|
||||
internal interface IRenderFrameGlState
|
||||
{
|
||||
void RestoreFrameDefaults();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL arm. Every statement below is the one the executor used to issue
|
||||
/// inline, in the same order, against the same objects.
|
||||
/// </summary>
|
||||
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<Vector4> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>vkCmdClearAttachments</c>.
|
||||
/// 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 <c>vkCmdClearAttachments</c>. The raw-GL implementation this used to
|
||||
/// sit alongside (<c>GlWorldPassSurface</c>) was deleted at Campaign V slice
|
||||
/// V11.
|
||||
/// </summary>
|
||||
internal sealed class RhiWorldPassSurface : IWorldPassSurface
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Startup backend request. Campaign V slice V10 inverted this: Vulkan is
|
||||
/// the default, and only the explicit escape-hatch tokens <c>gl</c> and
|
||||
/// <c>opengl</c> select OpenGL. Everything else — unset, <c>vulkan</c>, or a
|
||||
/// typo — is Vulkan.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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. <c>opengl</c>
|
||||
/// is honoured alongside <c>gl</c> 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.
|
||||
/// </remarks>
|
||||
private static RenderBackendKind ParseRenderBackend(string? value)
|
||||
=> string.Equals(value, "gl", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "opengl", StringComparison.OrdinalIgnoreCase)
|
||||
? RenderBackendKind.Gl
|
||||
: RenderBackendKind.Vulkan;
|
||||
|
||||
/// <summary>True iff live-mode credentials are present and valid for connecting.</summary>
|
||||
public bool HasLiveCredentials =>
|
||||
LiveMode && !string.IsNullOrEmpty(LiveUser) && !string.IsNullOrEmpty(LivePass);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.OpenGl, null!);
|
||||
Platform = new GameWindowPlatformResult<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.Instance, null!);
|
||||
Host = (HostInputCameraResult)RuntimeHelpers.GetUninitializedObject(
|
||||
typeof(HostInputCameraResult));
|
||||
Dependencies = new ContentEffectsAudioDependencies(
|
||||
|
|
|
|||
|
|
@ -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<IKeyboard, NullDeviceProxy>();
|
||||
IMouse mouse = DispatchProxy.Create<IMouse, NullDeviceProxy>();
|
||||
Input = new InputContext(keyboard, mouse);
|
||||
Platform = new GameWindowPlatformResult<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.OpenGl, Input);
|
||||
Platform = new GameWindowPlatformResult<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.Instance, Input);
|
||||
ViewportAspect = new ViewportAspectState();
|
||||
Framebuffer = new FramebufferResizeController(ViewportAspect);
|
||||
Capture = new CaptureSource();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6h: the graphics handle composition tests hand to a phase.
|
||||
/// The graphics handle composition tests hand to a phase.
|
||||
///
|
||||
/// <para>The phases now select their backend arm from
|
||||
/// <see cref="GameWindowGraphics"/> rather than from a bare <c>GL</c> 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.</para>
|
||||
/// <para>Campaign V slice V11 deleted the raw-GL arm — <c>GameWindowGraphics</c>
|
||||
/// no longer has a <c>Backend</c> or <c>Gl</c> member to select between, so this
|
||||
/// is now a single Vulkan-shaped token: a real <see cref="VulkanWorldPassScope"/>
|
||||
/// (composition phases require one, and it needs no live surface — just a
|
||||
/// sample count), no live <see cref="VulkanGraphicsContext"/> because tests never
|
||||
/// dereference it.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Selects the OpenGL arm, with a context token no test dereferences.</summary>
|
||||
public static TestGameWindowGraphics OpenGl { get; } =
|
||||
new(RenderBackendKind.Gl, new GL(new UnusableNativeContext()));
|
||||
|
||||
/// <summary>Selects the Vulkan arm: no GL context exists.</summary>
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GameWindowGraphics, IInputContext>(TestGameWindowGraphics.OpenGl, null!),
|
||||
new GameWindowPlatformResult<GameWindowGraphics, IInputContext>(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<Region>(), new float[256]);
|
||||
|
||||
|
|
@ -233,16 +227,6 @@ public sealed class WorldRenderCompositionTests
|
|||
WorldEnvironmentController environment,
|
||||
Region region) { }
|
||||
|
||||
public BindlessSupport RequireBindless(GL gl, Action<string> log) =>
|
||||
Stub<BindlessSupport>();
|
||||
|
||||
public TerrainAtlas AcquireTerrainAtlas(
|
||||
IGameRenderResourceLifetime lifetime,
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless) =>
|
||||
lifetime.AcquireTerrainAtlas(() => Atlas);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="IGpuDevice"/> to create images through. Its behaviour is
|
||||
/// covered by <c>RhiWorldTextureArrayTests</c> and by the Vulkan
|
||||
/// composition-host run — see plan §5.5.13.
|
||||
/// composition-host run  see plan §5.5.13.
|
||||
/// </summary>
|
||||
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<Shader>("terrain shader");
|
||||
|
||||
public SceneLightingUboBinding CreateSceneLighting(GL gl) =>
|
||||
Resource<SceneLightingUboBinding>("scene lighting");
|
||||
|
||||
public SceneLightingUboBinding CreateBackendNeutralSceneLighting(
|
||||
ICurrentGpuFrameSource frameSource,
|
||||
IWorldPassScope scope) =>
|
||||
|
|
@ -294,15 +272,6 @@ public sealed class WorldRenderCompositionTests
|
|||
IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory) =>
|
||||
Resource<TextRenderer>("text renderer");
|
||||
|
||||
public TerrainModernRenderer CreateTerrain(
|
||||
GL gl,
|
||||
BindlessSupport bindless,
|
||||
Shader shader,
|
||||
TerrainAtlas atlas,
|
||||
IGpuDevice gpuDevice,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
Resource<TerrainModernRenderer>("terrain");
|
||||
|
||||
public TerrainModernRenderer CreateBackendNeutralTerrain(
|
||||
IGpuDevice gpuDevice,
|
||||
ICurrentGpuFrameSource frameSource,
|
||||
|
|
@ -323,9 +292,6 @@ public sealed class WorldRenderCompositionTests
|
|||
Stub<TerrainBlendingContext>(),
|
||||
new ConcurrentDictionary<uint, SurfaceInfo>());
|
||||
|
||||
public Shader CreateMeshShader(GL gl, string shadersDirectory) =>
|
||||
Resource<Shader>("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<SamplerCache>("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<uint, SurfaceInfo> 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))
|
||||
|
|
|
|||
|
|
@ -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: []);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<InvalidOperationException>(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<InvalidOperationException>(() => 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<object>(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<InvalidOperationException>(() => 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<InvalidOperationException>(() =>
|
||||
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<InvalidOperationException>(laterFailure);
|
||||
Assert.Equal(8_192, capacity);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue