using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.Core.Lighting;
using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb;
///
/// Campaign V slice V6j: the dungeon-shell renderer's RHI submission arm.
///
/// V4c's content, re-landed as a second arm rather than a replacement —
/// see 's RHI file for
/// why the fork exists and where it is confined.
///
/// Two structural differences from V4c. It records into the pass
/// VulkanWorldScenePhase opened rather than opening
/// "envcell-shells" of its own, because the frame's one backbuffer pass
/// resolves. And there is no binding-9 texture table: V4t moved the slot onto
/// the device, and on Vulkan that table is set 2, which the encoder binds.
///
public sealed unsafe partial class EnvCellRenderer
{
private readonly IGpuDevice? _device;
private readonly ICurrentGpuFrameSource? _frames;
private readonly IWorldPassScope? _scope;
private IGpuPipeline? _opaquePipeline;
private IGpuPipeline? _alphaPipeline;
private IGpuPipeline? _additivePipeline;
///
/// The RHI arm's constructor. It also completes Initialize's job: the
/// three pipelines ARE this renderer's program, so there is no second step
/// and no Shader to hand in.
///
internal EnvCellRenderer(
IGpuDevice device,
ICurrentGpuFrameSource frames,
IWorldPassScope scope,
ObjectMeshManager meshManager,
WbFrustum frustum)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
_meshManager = meshManager ?? throw new ArgumentNullException(nameof(meshManager));
_frustum = frustum ?? throw new ArgumentNullException(nameof(frustum));
_opaquePipeline = CreateShellPipeline(
device, "envcell-opaque", GpuBlendMode.None, depthWrite: true, scope.SampleCount);
_alphaPipeline = CreateShellPipeline(
device, "envcell-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, scope.SampleCount);
_additivePipeline = CreateShellPipeline(
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
_initialized = true;
}
///
/// One pipeline per blend state the shell pass uses. Everything else is
/// shared: mesh_modern, the 32-byte world-mesh vertex, triangle lists,
/// back-face culling with clockwise front faces.
///
/// Depth compare is Less, not the contract's LessOrEqual
/// default. The world frame runs under GL_LESS and this renderer never
/// called glDepthFunc, so it inherited it; baking LessOrEqual
/// would change which of two coplanar retail surfaces wins.
///
private static IGpuPipeline CreateShellPipeline(
IGpuDevice device,
string name,
GpuBlendMode blend,
bool depthWrite,
int sampleCount) =>
device.CreatePipeline(new GpuPipelineDescription
{
Name = name,
Shaders = new GpuShaderSet("mesh_modern"),
VertexLayout = GpuVertexLayout.WorldMesh,
Topology = GpuPrimitiveTopology.TriangleList,
Blend = blend,
Depth = new GpuDepthState(Test: true, Write: depthWrite, GpuCompareOp.Less),
Cull = GpuCullMode.Back,
FrontFace = GpuFrontFace.Clockwise,
AlphaToCoverage = false,
ColorWrite = true,
SampleCount = sampleCount,
});
///
/// Writes this pass's sections into the frame ring and records the same
/// per-group multi-draw runs the GL arm issues, in the same order.
///
private void SubmitRhi(
List allInstances,
WbRenderPass renderPass,
int totalDraws,
int uniqueInstanceCount)
{
IWorldPassScope scope = _scope!;
IGpuPassEncoder encoder = scope.RequireEncoder();
IGpuFrame frame = _frames!.CurrentFrame
?? throw new InvalidOperationException(
"EnvCellRenderer requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
GlobalMeshBuffer mesh = _meshManager.GlobalBuffer
?? throw new InvalidOperationException("The shared mesh arena is not published.");
if (_gpuInstanceTransforms.Length < uniqueInstanceCount)
{
Array.Resize(
ref _gpuInstanceTransforms,
Math.Max(_gpuInstanceTransforms.Length * 2, uniqueInstanceCount));
}
for (int i = 0; i < uniqueInstanceCount; i++)
_gpuInstanceTransforms[i] = allInstances[i].Transform;
// Phase U.4: per-instance clip slots, laid out parallel to the transforms
// so instanceClipSlot[BaseInstance + gl_InstanceID] tracks Instances[].
if (_clipSlotData.Length < uniqueInstanceCount)
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
if (_cellIdToSlot is null
|| AcDream.Core.Rendering.RenderingDiagnostics.ClipDebugNoShellTrim)
{
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
}
else
{
for (int i = 0; i < uniqueInstanceCount; i++)
{
_clipSlotData[i] =
_cellIdToSlot.TryGetValue(allInstances[i].CellId, out int slot)
? (uint)slot
: 0u;
}
}
// A7 Fix D (D-2): per-instance 8-int light set, keyed on the cell each
// shell instance belongs to.
int lightStride = LightManager.MaxLightsPerObject;
if (_lightSetData.Length < uniqueInstanceCount * lightStride)
{
_lightSetData = new int[Math.Max(
_lightSetData.Length * 2,
uniqueInstanceCount * lightStride)];
}
for (int i = 0; i < uniqueInstanceCount; i++)
{
int[] cellSet = GetCellLightSet(allInstances[i].CellId);
Array.Copy(cellSet, 0, _lightSetData, i * lightStride, lightStride);
}
if (renderPass == WbRenderPass.Opaque
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled)
{
EmitSeamDrawProbe(_renderDrawCalls, allInstances, _seamProbeFilter);
}
int lightCount = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData);
int globalLightUploadCount = lightCount > 0 ? lightCount : 1;
var pushConstants = new GpuPushConstants
{
ViewProjection = _lastViewProjection,
DrawIdOffset = 0,
// A7 Fix D D-3/D-4: EnvCell bake — wrap points, no sun.
LightingMode = 1,
RenderPass = (int)renderPass,
LightDebug = AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode,
TextureIndexA = 0,
TextureIndexB = 0,
ParamA = 0f,
ParamB = 0f,
};
// Bind the pass's base pipeline first so the ring binds land on a live
// program; the per-range switches below rebind the mesh with it.
IGpuPipeline basePipeline = renderPass == WbRenderPass.Transparent
? _alphaPipeline!
: _opaquePipeline!;
BindPipelineWithMesh(encoder, basePipeline, mesh);
encoder.SetPushConstants(in pushConstants);
BindRingSection(
encoder, frame, GpuBindingModel.StorageInstances,
_gpuInstanceTransforms.AsSpan(0, uniqueInstanceCount));
BindRingSection(
encoder, frame, GpuBindingModel.StorageBatches,
_modernBatches.AsSpan(0, totalDraws));
BindRingSection(
encoder, frame, GpuBindingModel.StorageClipSlots,
_clipSlotData.AsSpan(0, uniqueInstanceCount));
BindRingSection(
encoder, frame, GpuBindingModel.StorageGlobalLights,
_globalLightData.AsSpan(
0,
globalLightUploadCount * GlobalLightPacker.FloatsPerLight));
BindRingSection(
encoder, frame, GpuBindingModel.StorageInstanceLightSets,
_lightSetData.AsSpan(0, uniqueInstanceCount * lightStride));
// The frame-global sections, bound after this renderer's own binds
// because those binds are what select the descriptor scope.
AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions(
encoder, scope.Sections, frame);
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
encoder, scope.Sections, frame);
GpuRingAllocation commands = frame.AllocateRing(
totalDraws * sizeof(DrawElementsIndirectCommand),
GpuRingUsage.Indirect);
MemoryMarshal.AsBytes(_commands.AsSpan(0, totalDraws)).CopyTo(commands.Data);
IGpuBuffer commandBuffer = commands.Buffer;
uint commandBase = commands.OffsetBytes;
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
{
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
int groupIndex = drawRange.GroupIndex;
var cullMode = (CullMode)(groupIndex % 4);
// Phase A8 visual-gate evidence: cell meshes use CullMode.Landblock
// uniformly, but the room surfaces need to be visible from inside.
// Render cell polys double-sided, exactly as the GL arm does.
if (cullMode == CullMode.Landblock) cullMode = CullMode.None;
bool isAdditive = groupIndex >= 4;
if (renderPass == WbRenderPass.Transparent)
{
// Blend state is the pipeline's; switching variants mid-pass has
// to re-establish the mesh, which is vertex-array state.
BindPipelineWithMesh(
encoder,
isAdditive ? _additivePipeline! : _alphaPipeline!,
mesh);
}
// Must follow the pipeline bind: BindPipeline re-issues the
// pipeline's own cull/front-face/depth-write defaults.
SetCullMode(encoder, cullMode);
pushConstants.RenderPass = isAdditive
? (int)renderPass | 0x100
: (int)renderPass;
pushConstants.DrawIdOffset = drawRange.FirstCommand;
encoder.SetPushConstants(in pushConstants);
encoder.MultiDrawIndexedIndirect(
commandBuffer,
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
(uint)drawRange.CommandCount,
(uint)sizeof(DrawElementsIndirectCommand));
}
}
private void BindPipelineWithMesh(
IGpuPassEncoder encoder,
IGpuPipeline pipeline,
GlobalMeshBuffer mesh)
{
encoder.BindPipeline(pipeline);
encoder.BindVertexBuffer(
0,
mesh.VertexStore ?? throw new InvalidOperationException(
"The shared mesh arena has no vertex store."),
0);
encoder.BindIndexBuffer(
mesh.IndexStore ?? throw new InvalidOperationException(
"The shared mesh arena has no index store."),
0,
GpuIndexType.UInt16);
}
///
/// WB BaseObjectRenderManager.cs:850-866 applies CullMode per MDI
/// group; WB GameScene.cs:843 sets FrontFace(CW) globally. Both are
/// dynamic state in core Vulkan 1.3, so they stay per-run calls.
///
private static void SetCullMode(IGpuPassEncoder encoder, CullMode mode)
{
encoder.SetFrontFace(GpuFrontFace.Clockwise);
switch (mode)
{
case CullMode.None:
encoder.SetCullMode(GpuCullMode.None);
break;
case CullMode.Clockwise:
encoder.SetCullMode(GpuCullMode.Front);
break;
case CullMode.CounterClockwise:
case CullMode.Landblock:
encoder.SetCullMode(GpuCullMode.Back);
break;
}
}
///
/// Reserves this frame's ring, copies into it, and binds the slice. A
/// logically empty section still reserves one element so the bound range is
/// never zero-length — the "bind at least one element so the shader never
/// reads an unbound SSBO" rule the light buffers already stated.
///
private static void BindRingSection(
IGpuPassEncoder encoder,
IGpuFrame frame,
uint binding,
ReadOnlySpan data)
where T : unmanaged
{
int elementBytes = sizeof(T);
int byteCount = Math.Max(data.Length * elementBytes, elementBytes);
GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Storage);
if (!data.IsEmpty)
data.CopyTo(allocation.AsSpan());
encoder.BindStorageBuffer(
binding,
allocation.Buffer,
allocation.OffsetBytes,
(uint)byteCount);
}
private void DisposeRhiResources()
{
_opaquePipeline?.Dispose();
_opaquePipeline = null;
_alphaPipeline?.Dispose();
_alphaPipeline = null;
_additivePipeline?.Dispose();
_additivePipeline = null;
}
}