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