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