using System.Collections.Immutable; using System.Runtime.InteropServices; using AcDream.App.Rendering.Gpu; using AcDream.Core.Meshing; using AcDream.Core.Terrain; using AcDream.Content; using DatReaderWriter; namespace AcDream.App.Rendering.Sky; /// /// Campaign V slice V6k: the sky's RHI submission arm. /// /// This is V4f's content, landed as a SECOND arm rather than a replacement, /// for the reason §5.5.6 gave: NVIDIA rendered the V4c binary 10/10 where AMD's /// GL stack did not, so GL keeps its raw world path through to V10 as a /// documented fork confined to the submission seam and the RHI world path ships /// on Vulkan. Every GL statement in the sibling file is the one it always issued; /// everything here runs only when there is no GL context. /// /// Three things differ from the GL arm, each because Vulkan bakes what GL /// sets. The per-submesh blend function is two PIPELINES — additive for /// sun/moon/stars, straight alpha for the dome and cloud sheets — because core /// Vulkan 1.3 does not make blend dynamic. The SkyParams block is a ring /// slice taken per draw rather than one buffer rewritten per draw, because a ring /// allocation is distinct memory that lives until the frame retires and a /// rewritten buffer is read at EXECUTION time, not record time (plan §5.5.8's /// second recorded gap). And the pass is BORROWED from /// , because the frame's one backbuffer pass /// resolves and a second pass could not load what it left. /// /// The sky is the first Vulkan consumer of set 1 binding 4. Plan §5.5.8 /// recorded that UniformSkyParams was missing from the uniform set layout /// and V6i-2 added it; nothing had ever bound it until now. /// public sealed unsafe partial class SkyRenderer { private readonly IGpuDevice? _device; private readonly ICurrentGpuFrameSource? _frames; private readonly IWorldPassScope? _scope; private IGpuPipeline? _alphaPipeline; private IGpuPipeline? _additivePipeline; /// /// Slot cache for the RHI arm, keyed the same way the GL arm keys its /// bindless handles: one entry per (surface, wrap mode) pair, because a /// Vulkan table entry is a combined image sampler and the dome sampled /// CLAMP_TO_EDGE is a different entry from a cloud sheet sampled REPEAT. /// private readonly Dictionary<(uint SurfaceId, bool Repeat), GpuTextureSlot> _slotBySurfaceAndWrap = new(); /// /// The sky vertex layout, taken from itself. /// /// The stride is sizeof(Vertex) — 36 bytes, not the 32 the world /// mesh uses. carries a fourth member, /// TerrainLayer, which sky.vert does not declare and the GL arm /// never described to a glVertexAttribPointer; it is still part of the /// record's footprint, and getting the stride wrong scatters the dome's /// vertices into noise while leaving the frame otherwise plausible. The GL arm /// says sizeof(Vertex) and so does this. /// internal static readonly GpuVertexLayout SkyVertexLayout = GpuVertexLayout.Interleaved( strideBytes: (uint)sizeof(Vertex), ImmutableArray.Create( new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0), new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12), new GpuVertexAttribute(2, GpuVertexFormat.Float2, 24))); /// /// The RHI arm's constructor. No GL context, no Shader, no /// SamplerCache and no BindlessSupport: the two pipelines /// compile sky from the committed SPIR-V, and the wrap mode is which /// sampler the slot was registered with. /// internal SkyRenderer( IGpuDevice device, ICurrentGpuFrameSource frames, IWorldPassScope scope, IDatReaderWriter dats, TextureCache textures) { _device = device ?? throw new ArgumentNullException(nameof(device)); _frames = frames ?? throw new ArgumentNullException(nameof(frames)); _scope = scope ?? throw new ArgumentNullException(nameof(scope)); _dats = dats ?? throw new ArgumentNullException(nameof(dats)); _textures = textures ?? throw new ArgumentNullException(nameof(textures)); _alphaPipeline = CreateSkyPipeline("sky-alpha", GpuBlendMode.StraightAlpha); try { _additivePipeline = CreateSkyPipeline("sky-additive", GpuBlendMode.Additive); } catch { _alphaPipeline.Dispose(); _alphaPipeline = null; throw; } } /// /// One sky pipeline. Depth is off in both directions and culling is off, which /// is exactly the GL arm's state bracket — the sky must never occlude scene /// geometry and its meshes are drawn from both sides. /// private IGpuPipeline CreateSkyPipeline(string name, GpuBlendMode blend) => _device!.CreatePipeline(new GpuPipelineDescription { Name = name, Shaders = new GpuShaderSet("sky"), VertexLayout = SkyVertexLayout, Topology = GpuPrimitiveTopology.TriangleList, Blend = blend, Depth = GpuDepthState.Disabled, Cull = GpuCullMode.None, FrontFace = GpuFrontFace.CounterClockwise, AlphaToCoverage = false, ColorWrite = true, SampleCount = _scope!.SampleCount, }); /// /// Uploads one submesh into its own device-local vertex/index buffer pair. /// Sky meshes are built once per GfxObj and never move, so this is the same /// "upload once, draw many frames" shape the GL arm's static-draw VBOs have. /// private SubMeshGpu UploadSubMeshRhi(GfxObjSubMesh sm) { IGpuDevice device = _device!; ReadOnlySpan vertexBytes = MemoryMarshal.AsBytes(sm.Vertices); ReadOnlySpan indexBytes = MemoryMarshal.AsBytes(sm.Indices); IGpuBuffer vertices = device.CreateBuffer(new GpuBufferDescription( $"sky-vertices-0x{sm.SurfaceId:X8}", Math.Max(vertexBytes.Length, 32), GpuBufferUsage.Vertex | GpuBufferUsage.TransferDestination, GpuMemoryResidency.DeviceLocal)); IGpuBuffer indices; try { indices = device.CreateBuffer(new GpuBufferDescription( $"sky-indices-0x{sm.SurfaceId:X8}", Math.Max(indexBytes.Length, 4), GpuBufferUsage.Index | GpuBufferUsage.TransferDestination, GpuMemoryResidency.DeviceLocal)); } catch { vertices.Dispose(); throw; } try { if (!vertexBytes.IsEmpty) vertices.Upload(0, vertexBytes); if (!indexBytes.IsEmpty) indices.Upload(0, indexBytes); } catch { indices.Dispose(); vertices.Dispose(); throw; } return new SubMeshGpu { VertexBuffer = vertices, IndexBuffer = indices, IndexCount = sm.Indices.Length, SurfaceId = sm.SurfaceId, IsAdditive = sm.Translucency == TranslucencyKind.Additive, SurfLuminosity = sm.Luminosity, SurfDiffuse = sm.Diffuse, NeedsUvRepeat = sm.NeedsUvRepeat, SurfOpacity = sm.SurfOpacity, DisableFog = sm.DisableFog, }; } /// /// The (surface, wrap) table slot on a backend with no GL texture name. The /// decode is TextureCache's, so the pixels are the same ones the GL arm /// samples; what differs is that the texture is created through /// and paired with a real sampler /// object rather than a bindless handle. /// private uint RhiTextureTableSlot(uint surfaceId, bool repeat) { var key = (surfaceId, repeat); if (!_slotBySurfaceAndWrap.TryGetValue(key, out GpuTextureSlot slot)) { slot = _textures.RegisterWorldSurface(surfaceId, repeat); _slotBySurfaceAndWrap.Add(key, slot); } return slot.Index; } /// /// Records one submesh into the borrowed world pass. /// /// Order matters for the same reason it does in terrain's arm: /// BindPipeline re-issues the pipeline's own fixed state, and the /// frame-global sections are bound AFTER this renderer's own binds because /// those binds are what select the descriptor scope the sections must land in /// (plan §5.5.14 item 2). /// private void DrawSubMeshRhi(SubMeshGpu sub, uint textureSlot) { if (sub.IndexCount == 0 || sub.VertexBuffer is null || sub.IndexBuffer is null) return; IWorldPassScope scope = _scope!; IGpuPassEncoder encoder = scope.RequireEncoder(); IGpuFrame frame = _frames!.CurrentFrame ?? throw new InvalidOperationException( "SkyRenderer requires an open IGpuFrame (see GpuDeviceFrameLifetime)."); encoder.BindPipeline(sub.IsAdditive ? _additivePipeline! : _alphaPipeline!); var pushConstants = new GpuPushConstants { // uViewProjection is unread by sky.vert — the sky carries its own // camera-anchored view and dome projection in SkyParams — so the // block's only live member here is the texture slot. ViewProjection = System.Numerics.Matrix4x4.Identity, DrawIdOffset = 0, LightingMode = 0, RenderPass = 0, LightDebug = 0, TextureIndexA = textureSlot, TextureIndexB = 0, ParamA = 0f, ParamB = 0f, }; encoder.SetPushConstants(in pushConstants); encoder.BindVertexBuffer(0, sub.VertexBuffer, 0); encoder.BindIndexBuffer(sub.IndexBuffer, 0, GpuIndexType.UInt32); GpuRingAllocation parameters = frame.AllocateRing( SkyParams.SizeInBytes, GpuRingUsage.Uniform); MemoryMarshal.Write(parameters.Data, in _params); encoder.BindUniformBuffer( GpuBindingModel.UniformSkyParams, parameters.Buffer, parameters.OffsetBytes, SkyParams.SizeInBytes); WorldFrameSectionBinding.BindSceneLighting(encoder, scope.Sections, frame); WorldFrameSectionBinding.BindTerrainClip(encoder, scope.Sections, frame); encoder.DrawIndexed((uint)sub.IndexCount, 1, 0, 0, 0); } private void DisposeRhi() { List? failures = null; void Attempt(Action action) { try { action(); } catch (Exception error) { (failures ??= []).Add(error); } } foreach (List subs in _gpuByGfxObj.Values) { foreach (SubMeshGpu sub in subs) { Attempt(() => sub.VertexBuffer?.Dispose()); Attempt(() => sub.IndexBuffer?.Dispose()); } } _gpuByGfxObj.Clear(); // The textures themselves belong to TextureCache, which outlives this // renderer and releases both the image and its table slot itself; the // slot cache here is a lookup, not an ownership record. _slotBySurfaceAndWrap.Clear(); Attempt(() => _alphaPipeline?.Dispose()); _alphaPipeline = null; Attempt(() => _additivePipeline?.Dispose()); _additivePipeline = null; if (failures is not null) throw new AggregateException("The sky renderer's RHI resources did not fully release.", failures); } }