using AcDream.App.Rendering.Gpu; using AcDream.Core.Lighting; namespace AcDream.App.Rendering; /// /// A buffer range reduced to the three values a bind needs. /// /// is a ref struct — deliberately, /// so nothing can outlive the frame's memory — but the buffer reference plus its /// offset and size are ordinary values and stay valid for as long as that memory /// does. That is what lets one writer publish a section and several renderers /// bind it later in the same frame without recopying. /// internal readonly record struct GpuBufferSection( IGpuBuffer? Buffer, uint OffsetBytes, uint SizeBytes) { public bool IsValid => Buffer is not null && SizeBytes > 0; } /// /// Campaign V slice V6j: the three sections GL binds frame-globally and Vulkan /// cannot. /// /// On GL the SceneLighting UBO (set 1 binding 1), the per-cell clip regions /// (set 0 binding 2) and the terrain clip block (set 1 binding 2) are each bound /// once, to a global binding point, and every consumer inherits them. Vulkan has /// no global binding points: a descriptor set is bound per draw, and a renderer /// that binds its own buffers selects the descriptor scope those sections have to /// land in (plan §5.5.14 item 2). So the writers PUBLISH here and each renderer /// binds them inside the pass, after its own binds. /// /// Borrowed for the frame that publishes it — the sections are ring slices /// and die when the frame retires. /// internal sealed class WorldFrameSections { /// Set 1 binding 1 — SceneLighting. public GpuBufferSection SceneLighting { get; set; } /// Set 0 binding 2 — the per-cell CellClip table. public GpuBufferSection ClipRegions { get; set; } /// Set 1 binding 2 — TerrainClip. public GpuBufferSection TerrainClip { get; set; } public void Reset() { SceneLighting = default; ClipRegions = default; TerrainClip = default; } } /// /// Campaign V slice V6j: the one render pass every world renderer records into, /// borrowed rather than opened. /// /// Why they cannot each open one. Under MSAA the frame's backbuffer /// pass renders into a multisampled scratch image and RESOLVES it into the /// swapchain image, storing DONT_CARE into the scratch — so a second pass /// declaring Load would load undefined contents and lose everything the /// first drew. The Vulkan backend also permits only one open pass per frame. /// V4c's shape, where WbDrawDispatcher and EnvCellRenderer each /// bracketed their own Load/Store pass, is therefore not available /// on this backend (plan §5.5.12 item 5, §5.5.14 item 1). /// /// So the world-scene phase opens the pass, publishes the encoder here for /// the duration of WorldSceneRenderer, and every renderer borrows it. /// internal interface IWorldPassScope { /// /// Sample count of the pass, and therefore of every pipeline recorded into /// it. Vulkan requires a pipeline's rasterizationSamples to match the /// pass, and alpha-to-coverage does nothing at one sample — which is why /// V4c's blanket SampleCount = 1 is not portable (plan §5.5.14 item 6). /// int SampleCount { get; } /// The open encoder, or null outside the world phase. IGpuPassEncoder? CurrentEncoder { get; } /// The open encoder, or a composition error if the phase is not bracketing. IGpuPassEncoder RequireEncoder(); /// Colour-attachment width in pixels, for scissor and clear rectangles. int AttachmentWidth { get; } /// Colour-attachment height in pixels. int AttachmentHeight { get; } /// /// Retail's interior depth clear, scoped to the live pass. /// /// RetailPViewPassExecutor issues glClear(GL_DEPTH_BUFFER_BIT) /// between the landscape slice and the interior cells. Splitting the pass to /// get a depth load-op is exactly what the resolve forbids, so the backend /// records vkCmdClearAttachments instead — reached through here, so the /// pinned contract stays frozen and the backend-only verb stays inside the /// backend (plan §5.5.14 item 3). /// void ClearInteriorDepth(); /// The frame-global sections every renderer in this pass rebinds. WorldFrameSections Sections { get; } /// /// Publishes as the pass every world renderer /// borrows, for the duration of the returned scope. /// /// Campaign V slice V6l lifted this onto the interface for the /// offscreen viewports. The world-scene phase is still the only publisher of /// the frame's backbuffer pass, but the paperdoll and creature-appraisal /// views open a pass of their OWN — a real render target, after the world /// pass has closed — and then ask WbDrawDispatcher to draw one entity /// into it. The dispatcher borrows its pass from here, so the viewport has to /// be able to say which pass "here" means. /// /// Publications do not nest: the backend permits one open pass at a /// time, so a nested publication could only mean two owners believe they hold /// the frame. /// IDisposable Publish(IGpuPassEncoder encoder); } /// /// Campaign V slice V6j: binds the frame-global sections inside a renderer's own /// draw, which is the only place they can go on Vulkan. /// /// Each helper falls back to a zeroed ring slice when nothing published the /// section. That is the same rule the GL arm already states for its own bindings /// — bind at least one element so the shader never reads an unbound buffer — /// applied to the three sections whose publisher runs outside the renderer. /// internal static class WorldFrameSectionBinding { internal static void BindSceneLighting( IGpuPassEncoder encoder, WorldFrameSections sections, IGpuFrame frame) { GpuBufferSection section = sections.SceneLighting; if (!section.IsValid) { section = Zeroed( frame, SceneLightingUbo.SizeInBytes, GpuRingUsage.Uniform); } encoder.BindUniformBuffer( (uint)SceneLightingUbo.BindingPoint, section.Buffer!, section.OffsetBytes, section.SizeBytes); } internal static void BindClipRegions( IGpuPassEncoder encoder, WorldFrameSections sections, IGpuFrame frame) { GpuBufferSection section = sections.ClipRegions; if (!section.IsValid) { // Slot 0 zeroed is retail's "no clip": count 0 means ungated. section = Zeroed( frame, ClipFrame.CellClipStrideBytes, GpuRingUsage.Storage); } encoder.BindStorageBuffer( GpuBindingModel.StorageClipRegions, section.Buffer!, section.OffsetBytes, section.SizeBytes); } internal static void BindTerrainClip( IGpuPassEncoder encoder, WorldFrameSections sections, IGpuFrame frame) { GpuBufferSection section = sections.TerrainClip; if (!section.IsValid) { section = Zeroed( frame, ClipFrame.TerrainUboBytes, GpuRingUsage.Uniform); } encoder.BindUniformBuffer( ClipFrame.TerrainClipUboBinding, section.Buffer!, section.OffsetBytes, section.SizeBytes); } private static GpuBufferSection Zeroed( IGpuFrame frame, int byteCount, GpuRingUsage usage) { GpuRingAllocation allocation = frame.AllocateRing(byteCount, usage); allocation.Data.Clear(); return new GpuBufferSection( allocation.Buffer, allocation.OffsetBytes, (uint)byteCount); } }