From d1d87447ae44d85b15e748c5b1cc8ccd310f8740 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 2 Feb 2026 13:12:01 +0100 Subject: feat(3d): Add scaffolding for visual debugging (Task #18a) - Added 'src/3d/visual_debug.h/cc' to implement wireframe rendering. - Integrated VisualDebug into Renderer3D with a static global toggle. - Added '--debug' command-line option to 'demo64k' and 'test_3d_render' to enable wireframes. - Updated 'src/gpu/effects/hybrid_3d_effect.h' to expose the debug setter (reverted later as static method used). - Ensured full cross-platform compatibility (native and Windows) for the new debug module. - All code guarded by STRIP_ALL for final release. --- src/3d/visual_debug.cc | 247 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/3d/visual_debug.cc (limited to 'src/3d/visual_debug.cc') diff --git a/src/3d/visual_debug.cc b/src/3d/visual_debug.cc new file mode 100644 index 0000000..361372b --- /dev/null +++ b/src/3d/visual_debug.cc @@ -0,0 +1,247 @@ +// This file is part of the 64k demo project. +// Implementation of visual debugging tools. + +#include "3d/visual_debug.h" + +#if !defined(STRIP_ALL) + +#include +#include + +// Simple shader for drawing colored lines +static const char* kDebugShaderCode = R"( +struct Uniforms { + viewProj : mat4x4, +} +@group(0) @binding(0) var uniforms : Uniforms; + +struct VertexInput { + @location(0) position : vec3, + @location(1) color : vec3, +} + +struct VertexOutput { + @builtin(position) position : vec4, + @location(0) color : vec3, +} + +@vertex +fn vs_main(in : VertexInput) -> VertexOutput { + var out : VertexOutput; + out.position = uniforms.viewProj * vec4(in.position, 1.0); + out.color = in.color; + return out; +} + +@fragment +fn fs_main(in : VertexOutput) -> @location(0) vec4 { + return vec4(in.color, 1.0); +} +)"; + +void VisualDebug::init(WGPUDevice device, WGPUTextureFormat format) { + device_ = device; + create_pipeline(format); + + // Initial capacity for vertex buffer (e.g., 1024 lines) + vertex_buffer_capacity_ = 1024 * 2 * sizeof(float) * 6; // 2 verts per line, 6 floats per vert + + WGPUBufferDescriptor vb_desc = {}; + vb_desc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; + vb_desc.size = vertex_buffer_capacity_; + vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &vb_desc); + + WGPUBufferDescriptor ub_desc = {}; + ub_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; + ub_desc.size = sizeof(mat4); + uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &ub_desc); +} + +void VisualDebug::shutdown() { + if (pipeline_) wgpuRenderPipelineRelease(pipeline_); + if (bind_group_layout_) wgpuBindGroupLayoutRelease(bind_group_layout_); + if (vertex_buffer_) wgpuBufferRelease(vertex_buffer_); + if (uniform_buffer_) wgpuBufferRelease(uniform_buffer_); + if (bind_group_) wgpuBindGroupRelease(bind_group_); +} + +void VisualDebug::create_pipeline(WGPUTextureFormat format) { + // Bind Group Layout + WGPUBindGroupLayoutEntry bgl_entry = {}; + bgl_entry.binding = 0; + bgl_entry.visibility = WGPUShaderStage_Vertex; + bgl_entry.buffer.type = WGPUBufferBindingType_Uniform; + bgl_entry.buffer.minBindingSize = sizeof(mat4); + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 1; + bgl_desc.entries = &bgl_entry; + bind_group_layout_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + // Pipeline Layout + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &bind_group_layout_; + WGPUPipelineLayout pipeline_layout = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); + + // Shader +#if defined(DEMO_CROSS_COMPILE_WIN32) + WGPUShaderModuleWGSLDescriptor wgsl_desc = {}; + wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor; + wgsl_desc.code = kDebugShaderCode; + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; +#else + WGPUShaderSourceWGSL wgsl_desc = {}; + wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_desc.code = {kDebugShaderCode, strlen(kDebugShaderCode)}; + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; +#endif + WGPUShaderModule shader_module = wgpuDeviceCreateShaderModule(device_, &shader_desc); + + // Vertex State + WGPUVertexAttribute attributes[2]; + // Position + attributes[0].format = WGPUVertexFormat_Float32x3; + attributes[0].offset = 0; + attributes[0].shaderLocation = 0; + // Color + attributes[1].format = WGPUVertexFormat_Float32x3; + attributes[1].offset = sizeof(float) * 3; + attributes[1].shaderLocation = 1; + + WGPUVertexBufferLayout vertex_layout = {}; + vertex_layout.arrayStride = sizeof(float) * 6; + vertex_layout.stepMode = WGPUVertexStepMode_Vertex; + vertex_layout.attributeCount = 2; + vertex_layout.attributes = attributes; + + // Pipeline + WGPURenderPipelineDescriptor pipeline_desc = {}; + pipeline_desc.layout = pipeline_layout; + pipeline_desc.vertex.module = shader_module; +#if defined(DEMO_CROSS_COMPILE_WIN32) + pipeline_desc.vertex.entryPoint = "vs_main"; +#else + pipeline_desc.vertex.entryPoint = {"vs_main", 7}; +#endif + pipeline_desc.vertex.bufferCount = 1; + pipeline_desc.vertex.buffers = &vertex_layout; + + WGPUFragmentState fragment_state = {}; + fragment_state.module = shader_module; +#if defined(DEMO_CROSS_COMPILE_WIN32) + fragment_state.entryPoint = "fs_main"; +#else + fragment_state.entryPoint = {"fs_main", 7}; +#endif + fragment_state.targetCount = 1; + + WGPUColorTargetState color_target = {}; + color_target.format = format; + color_target.writeMask = WGPUColorWriteMask_All; + // Enable simple alpha blending if needed, but opaque lines are fine for now + fragment_state.targets = &color_target; + pipeline_desc.fragment = &fragment_state; + + pipeline_desc.primitive.topology = WGPUPrimitiveTopology_LineList; + pipeline_desc.primitive.cullMode = WGPUCullMode_None; + pipeline_desc.primitive.frontFace = WGPUFrontFace_CCW; + + WGPUDepthStencilState depth_stencil = {}; + depth_stencil.format = WGPUTextureFormat_Depth24Plus; + depth_stencil.depthWriteEnabled = WGPUOptionalBool_False; // Don't write depth + depth_stencil.depthCompare = WGPUCompareFunction_Less; // But do test against it + pipeline_desc.depthStencil = &depth_stencil; + + pipeline_desc.multisample.count = 1; + pipeline_desc.multisample.mask = 0xFFFFFFFF; + + pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &pipeline_desc); + + wgpuPipelineLayoutRelease(pipeline_layout); + wgpuShaderModuleRelease(shader_module); +} + +void VisualDebug::add_box(const vec3& c, const vec3& e, const vec3& color) { + // 8 corners + vec3 p0 = vec3(c.x - e.x, c.y - e.y, c.z - e.z); + vec3 p1 = vec3(c.x + e.x, c.y - e.y, c.z - e.z); + vec3 p2 = vec3(c.x + e.x, c.y + e.y, c.z - e.z); + vec3 p3 = vec3(c.x - e.x, c.y + e.y, c.z - e.z); + vec3 p4 = vec3(c.x - e.x, c.y - e.y, c.z + e.z); + vec3 p5 = vec3(c.x + e.x, c.y - e.y, c.z + e.z); + vec3 p6 = vec3(c.x + e.x, c.y + e.y, c.z + e.z); + vec3 p7 = vec3(c.x - e.x, c.y + e.y, c.z + e.z); + + // 12 edges (each 2 vertices) + DebugLine edges[] = { + {p0, p1, color}, {p1, p2, color}, {p2, p3, color}, {p3, p0, color}, // Front face + {p4, p5, color}, {p5, p6, color}, {p6, p7, color}, {p7, p4, color}, // Back face + {p0, p4, color}, {p1, p5, color}, {p2, p6, color}, {p3, p7, color} // Connecting edges + }; + + for (const auto& l : edges) { + lines_.push_back(l); + } +} + +void VisualDebug::update_buffers(const mat4& view_proj) { + // Update Uniforms + wgpuQueueWriteBuffer(wgpuDeviceGetQueue(device_), uniform_buffer_, 0, &view_proj, sizeof(mat4)); + + // Update Vertices + size_t required_size = lines_.size() * 2 * sizeof(float) * 6; + if (required_size > vertex_buffer_capacity_) { + // Resize buffer + wgpuBufferRelease(vertex_buffer_); + vertex_buffer_capacity_ = required_size * 2; // Double capacity + WGPUBufferDescriptor vb_desc = {}; + vb_desc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; + vb_desc.size = vertex_buffer_capacity_; + vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &vb_desc); + } + + if (required_size > 0) { + std::vector vertex_data; + vertex_data.reserve(lines_.size() * 12); // 2 verts * 6 floats + for (const auto& line : lines_) { + vertex_data.push_back(line.start.x); vertex_data.push_back(line.start.y); vertex_data.push_back(line.start.z); + vertex_data.push_back(line.color.x); vertex_data.push_back(line.color.y); vertex_data.push_back(line.color.z); + + vertex_data.push_back(line.end.x); vertex_data.push_back(line.end.y); vertex_data.push_back(line.end.z); + vertex_data.push_back(line.color.x); vertex_data.push_back(line.color.y); vertex_data.push_back(line.color.z); + } + wgpuQueueWriteBuffer(wgpuDeviceGetQueue(device_), vertex_buffer_, 0, vertex_data.data(), vertex_data.size() * sizeof(float)); + } + + // Re-create bind group if needed (e.g. if uniform buffer changed, though here it's static) + if (!bind_group_) { + WGPUBindGroupEntry bg_entry = {}; + bg_entry.binding = 0; + bg_entry.buffer = uniform_buffer_; + bg_entry.size = sizeof(mat4); + + WGPUBindGroupDescriptor bg_desc = {}; + bg_desc.layout = bind_group_layout_; + bg_desc.entryCount = 1; + bg_desc.entries = &bg_entry; + bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); + } +} + +void VisualDebug::render(WGPURenderPassEncoder pass, const mat4& view_proj) { + if (lines_.empty()) return; + + update_buffers(view_proj); + + wgpuRenderPassEncoderSetPipeline(pass, pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + wgpuRenderPassEncoderSetVertexBuffer(pass, 0, vertex_buffer_, 0, lines_.size() * 2 * sizeof(float) * 6); + wgpuRenderPassEncoderDraw(pass, (uint32_t)lines_.size() * 2, 1, 0, 0); + + lines_.clear(); // Clear for next frame +} + +#endif // !defined(STRIP_ALL) -- cgit v1.2.3