namespace AcDream.App.Rendering.Gpu.Vk; /// /// Campaign V slice V6a: one frame's recording context on Vulkan. /// /// Thin on purpose. The frame owns no Vulkan objects: the command buffer, /// the ring and the timeline all belong to , which /// keeps them alive across frames and hands this object the slot index they are /// addressed by. What is here is the contract's shape — one open pass at a time, /// idempotent . /// /// being idempotent with Dispose is /// load-bearing rather than tidy: a renderer that throws mid-frame must still /// close its flight slot, or the ring never rewinds and the next /// BeginFrame waits on a timeline value nothing will ever signal. /// internal sealed class VulkanGpuFrame : IGpuFrame { private readonly VulkanGpuDevice _device; private IGpuPassEncoder? _openPass; private bool _ended; internal VulkanGpuFrame(VulkanGpuDevice device, int slotIndex, long serial) { _device = device ?? throw new ArgumentNullException(nameof(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."); } IGpuPassEncoder encoder = _device.BeginPass(this, description); _openPass = encoder; return encoder; } internal void ClosePass(IGpuPassEncoder encoder) { if (ReferenceEquals(_openPass, encoder)) _openPass = null; } public void End() { if (_ended) return; _ended = true; _device.EndFrame(this); } public void Dispose() => End(); }