using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Gpu;
namespace AcDream.App.Rendering;
///
/// Minimal line renderer for visualizing collision shapes, bounding boxes,
/// and other debug geometry. Collect lines each frame via
/// / , then call
/// to upload + draw them.
///
/// Campaign V slice V4a: the debug_line shader compiles through
/// (
/// topology, depth disabled to match this renderer's "visible through
/// geometry" intent), and each Flush's vertex data comes from a per-frame
/// ring allocation instead of the old single respecialized VBO.
///
/// Campaign V slice V6d removed the last GL dependency. The shader's
/// separate uView/uProjection pair was set directly against the
/// compiled GL program because the pinned block
/// carries one combined matrix and there is no verb for arbitrary named
/// uniforms. That was never portable — Vulkan has no default uniform block at
/// all — so the shader converged on uViewProjection and
/// multiplies the two matrices on the CPU. The product
/// therefore rounds once per frame instead of once per vertex; these lines are
/// diagnostic-only geometry that draws when collision wireframes are switched
/// on, so nothing user-visible depends on those bits.
///
/// Vertex format is (vec3 pos, vec3 color) = 24 bytes per vertex.
///
public sealed class DebugLineRenderer : IDisposable
{
// internal: slice V6l's stride-equals-the-uploaded-record gate asserts the
// layout against the producer's own float count rather than a literal.
internal const int FloatsPerVertex = 6;
private const int VertexStrideBytes = FloatsPerVertex * sizeof(float);
internal static readonly GpuVertexLayout VertexLayout = GpuVertexLayout.Interleaved(
strideBytes: VertexStrideBytes,
[
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0),
new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12),
]);
private readonly ICurrentGpuFrameSource _frameSource;
private readonly IGpuPipeline _pipeline;
private readonly List _buffer = new(4096);
private int _vertexCount;
// internal, not public: IGpuDevice/ICurrentGpuFrameSource are internal
// types (the pinned RHI contract). DebugLineRenderer stays public — only
// construction is restricted.
internal DebugLineRenderer(IGpuDevice device, ICurrentGpuFrameSource frameSource, string shaderDir)
{
ArgumentNullException.ThrowIfNull(device);
_frameSource = frameSource ?? throw new ArgumentNullException(nameof(frameSource));
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
_pipeline = device.CreatePipeline(new GpuPipelineDescription
{
Name = "debug-line",
Shaders = new GpuShaderSet("debug_line"),
VertexLayout = VertexLayout,
Topology = GpuPrimitiveTopology.LineList,
Blend = GpuBlendMode.None,
// Retail debug lines draw through geometry (the old Flush disabled
// depth testing for the draw and restored whatever was ambient
// before it — GlGpuPassEncoder now does that restore generically;
// see its class comment).
Depth = GpuDepthState.Disabled,
Cull = GpuCullMode.None,
AlphaToCoverage = false,
ColorWrite = true,
SampleCount = 1,
});
}
/// Clear accumulated lines. Call at the start of each frame.
public void Begin()
{
_buffer.Clear();
_vertexCount = 0;
}
public void AddLine(Vector3 a, Vector3 b, Vector3 color)
{
_buffer.Add(a.X); _buffer.Add(a.Y); _buffer.Add(a.Z);
_buffer.Add(color.X); _buffer.Add(color.Y); _buffer.Add(color.Z);
_buffer.Add(b.X); _buffer.Add(b.Y); _buffer.Add(b.Z);
_buffer.Add(color.X); _buffer.Add(color.Y); _buffer.Add(color.Z);
_vertexCount += 2;
}
///
/// Draw a cylinder as 2 polygon rings (base + top) connected by 4
/// vertical line segments at 0/90/180/270 degrees.
///
public void AddCylinder(Vector3 basePos, float radius, float height, Vector3 color)
{
const int segments = 16;
Vector3 top = basePos + new Vector3(0, 0, height);
// Ring vertices
var baseRing = new Vector3[segments];
var topRing = new Vector3[segments];
for (int i = 0; i < segments; i++)
{
float theta = i * (MathF.PI * 2f / segments);
float cx = MathF.Cos(theta) * radius;
float cy = MathF.Sin(theta) * radius;
baseRing[i] = new Vector3(basePos.X + cx, basePos.Y + cy, basePos.Z);
topRing[i] = new Vector3(top.X + cx, top.Y + cy, top.Z);
}
// Base ring
for (int i = 0; i < segments; i++)
AddLine(baseRing[i], baseRing[(i + 1) % segments], color);
// Top ring
for (int i = 0; i < segments; i++)
AddLine(topRing[i], topRing[(i + 1) % segments], color);
// 4 vertical connectors
for (int i = 0; i < 4; i++)
{
int idx = i * (segments / 4);
AddLine(baseRing[idx], topRing[idx], color);
}
}
///
/// Draw an axis-aligned box as 12 edges.
///
public void AddBox(Vector3 min, Vector3 max, Vector3 color)
{
Vector3[] c =
{
new(min.X, min.Y, min.Z),
new(max.X, min.Y, min.Z),
new(max.X, max.Y, min.Z),
new(min.X, max.Y, min.Z),
new(min.X, min.Y, max.Z),
new(max.X, min.Y, max.Z),
new(max.X, max.Y, max.Z),
new(min.X, max.Y, max.Z),
};
// Bottom
AddLine(c[0], c[1], color); AddLine(c[1], c[2], color);
AddLine(c[2], c[3], color); AddLine(c[3], c[0], color);
// Top
AddLine(c[4], c[5], color); AddLine(c[5], c[6], color);
AddLine(c[6], c[7], color); AddLine(c[7], c[4], color);
// Verticals
AddLine(c[0], c[4], color); AddLine(c[1], c[5], color);
AddLine(c[2], c[6], color); AddLine(c[3], c[7], color);
}
/// Upload + draw all accumulated lines.
public void Flush(Matrix4x4 view, Matrix4x4 projection)
{
if (_vertexCount == 0) return;
IGpuFrame frame = _frameSource.CurrentFrame
?? throw new InvalidOperationException(
"DebugLineRenderer.Flush requires an open IGpuFrame (see GpuDeviceFrameLifetime) — " +
"the host must drive IGpuDevice.BeginFrame() before rendering debug lines.");
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
{
Name = "debug-line",
Color = new GpuColorAttachment(
Target: null,
Load: GpuLoadOp.Load,
Store: GpuStoreOp.Store,
ClearColor: default),
Depth = null,
SampleCount = 1,
});
encoder.BindPipeline(_pipeline);
// The GLSL used to evaluate uProjection * uView per vertex. System.Numerics
// is row-vector convention and GL/Vulkan read the 16 floats as column-major,
// which transposes; so the CPU-side equivalent of that product is
// view * projection, in that order.
GpuPushConstants constants = GpuPushConstants.Default;
constants.ViewProjection = view * projection;
encoder.SetPushConstants(constants);
int byteCount = _buffer.Count * sizeof(float);
GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex);
System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_buffer).CopyTo(allocation.AsSpan());
encoder.BindVertexBuffer(0, allocation.Buffer, allocation.OffsetBytes);
encoder.Draw((uint)_vertexCount, 1, 0, 0);
}
public void Dispose()
{
_pipeline.Dispose();
}
}