using System.Numerics; using AcDream.App.Rendering.Gpu; namespace AcDream.App.Rendering; /// /// Restores the frame-global rendering convention shared by frame clear and /// exceptional world-pass rollback. The raw-GL implementation this contract /// used to have alongside it (RenderFrameGlStateController) was deleted /// at Campaign V slice V11; is the only /// implementation left. /// internal interface IRenderFrameGlState { void RestoreFrameDefaults(); } /// /// Campaign V slice V6j: the small graphics surface the two world pass executors /// touch directly, expressed once so their ordering logic — which is retail's, /// and heavily tested — is written once for both backends. /// /// Everything else those executors do is delegation to a renderer. What is /// left is exactly this: the clip-frame publication, the doorway scissor, /// gl_ClipDistance enablement, and retail's interior depth clear. Four /// concerns, each of which genuinely differs between GL and Vulkan, and none of /// which is expressible on the pinned RHI contract. /// internal interface IWorldPassSurface { /// /// Publishes this frame's per-cell clip-region table and terrain clip block, /// and routes both to the renderers that read them. /// /// is how many distinct terrain /// clip blocks the frame will issue. GL reserves that many arena records /// before the first draw, because reallocating the arena while an earlier /// slice can still reference it is the hazard the reservation exists for. The /// RHI arm ignores it: a ring allocation is distinct memory by construction /// and lives until the frame retires. /// void PrepareClipFrame(int terrainUploadCount); /// Replaces the terrain clip planes and republishes the block. void SetTerrainClip(ReadOnlySpan planes); /// /// Re-asserts the terrain clip block at its binding. /// /// GL needs this because binding points are global and the sky and mesh /// shaders read the same uniform binding between two terrain slices. On the /// RHI arm every consumer binds the published section inside the pass, so /// there is no ambient binding to re-assert and this is a no-op. /// void BindTerrainClip(); /// /// Enables every gl_ClipDistance slot. /// /// Vulkan activates every element the shader declares and has no /// enable, so this is a no-op there — and that is safe rather than a /// divergence: all three world vertex shaders already write 1.0 /// ("keep everything") into every slot past the active count, so a frame with /// no clip planes clips nothing on either backend (plan §5.5.14 item 4). /// void EnableClipDistances(); /// Disables every gl_ClipDistance slot. See . void DisableClipDistances(); /// /// Scissors to a doorway slice's screen-space bounding box. Returns whether a /// scissor is now active, so the caller can pair it with . /// bool BeginScissor(Vector4 ndcAabb); /// Restores the full drawable rectangle. void EndScissor(); /// /// Retail's interior depth clear, between the landscape slice and the /// interior cells (PView::DrawCells @ 0x005A4840). /// void ClearInteriorDepth(); } /// /// The clip tables are ring sections published on the world pass scope, the /// scissor is dynamic state on the borrowed encoder, and the depth clear is a /// scoped vkCmdClearAttachments. The raw-GL implementation this used to /// sit alongside (GlWorldPassSurface) was deleted at Campaign V slice /// V11. /// internal sealed class RhiWorldPassSurface : IWorldPassSurface { private readonly IWorldPassScope _scope; private readonly ICurrentGpuFrameSource _frames; private readonly ClipFrame _clipFrame; private readonly IRetailPViewFramebufferSource _framebuffer; public RhiWorldPassSurface( IWorldPassScope scope, ICurrentGpuFrameSource frames, ClipFrame clipFrame, IRetailPViewFramebufferSource framebuffer) { _scope = scope ?? throw new ArgumentNullException(nameof(scope)); _frames = frames ?? throw new ArgumentNullException(nameof(frames)); _clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame)); _framebuffer = framebuffer ?? throw new ArgumentNullException(nameof(framebuffer)); } public void PrepareClipFrame(int terrainUploadCount) { // The reservation count has no RHI counterpart: each publication below // takes its own ring slice, which is distinct memory that outlives every // draw recorded against it in this frame. _ = terrainUploadCount; _scope.Sections.ClipRegions = Publish( _clipFrame.RegionBytes, GpuRingUsage.Storage); PublishTerrainClip(); } public void SetTerrainClip(ReadOnlySpan planes) { _clipFrame.SetTerrainClip(planes); PublishTerrainClip(); } /// No-op: there is no ambient binding to re-assert. See the interface. public void BindTerrainClip() { } /// No-op: Vulkan activates every declared clip distance. See the interface. public void EnableClipDistances() { } /// No-op: Vulkan activates every declared clip distance. See the interface. public void DisableClipDistances() { } public bool BeginScissor(Vector4 ndcAabb) { RetailPViewFramebufferSize framebuffer = _framebuffer.Capture(); var box = NdcScissorRect.ToPixels( ndcAabb, framebuffer.Width, framebuffer.Height); // GL convention on the way in; the backend performs its own Y flip. _scope.RequireEncoder().SetScissor(box.X, box.Y, box.Width, box.Height); return true; } public void EndScissor() => _scope.RequireEncoder().SetScissor( 0, 0, _scope.AttachmentWidth, _scope.AttachmentHeight); public void ClearInteriorDepth() { // The GL arm drops the scissor before clearing so the clear covers the // whole target; vkCmdClearAttachments takes its own rectangle and is not // scissored, so the same coverage comes for free — but the scissor still // has to come off, because the interior cells drawn after it are not // confined to the doorway slice that was active. EndScissor(); _scope.ClearInteriorDepth(); } private void PublishTerrainClip() => _scope.Sections.TerrainClip = Publish( _clipFrame.TerrainBytes, GpuRingUsage.Uniform); private GpuBufferSection Publish(ReadOnlySpan data, GpuRingUsage usage) { IGpuFrame frame = _frames.CurrentFrame ?? throw new InvalidOperationException( "The world clip frame requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); // A logically empty table still reserves one slot so the bound range is // never zero-length — the same rule the light buffers already state. int byteCount = Math.Max(data.Length, ClipFrame.CellClipStrideBytes); GpuRingAllocation allocation = frame.AllocateRing(byteCount, usage); allocation.Data.Clear(); if (!data.IsEmpty) data.CopyTo(allocation.Data); return new GpuBufferSection( allocation.Buffer, allocation.OffsetBytes, (uint)byteCount); } } /// /// Campaign V slice V6j: the frame-default restore on a backend with no ambient /// state to restore. /// /// Both executors call /// when aborting a failed frame. On Vulkan every piece of state that call /// re-establishes is either baked into a pipeline or set per draw, so there is /// nothing to put back and saying so is more honest than composing a GL /// controller that would have no context to talk to. /// internal sealed class NullRenderFrameGlState : IRenderFrameGlState { public static NullRenderFrameGlState Instance { get; } = new(); private NullRenderFrameGlState() { } public void RestoreFrameDefaults() { } } /// /// Campaign V slice V6j: the GL state reader on a backend with no GL state. /// /// WorldRenderDiagnostics reads live GL state for explicitly enabled /// probes only. Every snapshot below is the truthful answer for a Vulkan frame — /// there is no ambient capability state to sample — which keeps every other /// diagnostic the class emits (render signature, PView input, out-stage routing, /// phantom objects) working unchanged on both backends. /// internal sealed class NullRenderGlStateReader : IRenderGlStateReader { public static NullRenderGlStateReader Instance { get; } = new(); private NullRenderGlStateReader() { } public RenderGlStateSnapshot CaptureState() => default; public RenderGlScissorSnapshot CaptureScissor() => default; }