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);
}
}