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/renderer.cc | 25 +++++ src/3d/renderer.h | 13 +++ src/3d/visual_debug.cc | 247 ++++++++++++++++++++++++++++++++++++++++++++ src/3d/visual_debug.h | 49 +++++++++ src/main.cc | 3 + src/tests/test_3d_render.cc | 13 ++- 6 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 src/3d/visual_debug.cc create mode 100644 src/3d/visual_debug.h (limited to 'src') diff --git a/src/3d/renderer.cc b/src/3d/renderer.cc index db9d73d..adc9a5f 100644 --- a/src/3d/renderer.cc +++ b/src/3d/renderer.cc @@ -7,6 +7,10 @@ #include #include +#if !defined(STRIP_ALL) +bool Renderer3D::s_debug_enabled_ = false; +#endif + static const char* kShaderCode = R"( struct GlobalUniforms { view_proj: mat4x4, @@ -242,9 +246,17 @@ void Renderer3D::init(WGPUDevice device, WGPUQueue queue, create_default_resources(); create_pipeline(); + +#if !defined(STRIP_ALL) + visual_debug_.init(device_, format_); +#endif } void Renderer3D::shutdown() { +#if !defined(STRIP_ALL) + visual_debug_.shutdown(); +#endif + if (default_sampler_) wgpuSamplerRelease(default_sampler_); if (pipeline_) @@ -482,6 +494,19 @@ void Renderer3D::draw(WGPURenderPassEncoder pass, const Scene& scene, if (instance_count > 0) { wgpuRenderPassEncoderDraw(pass, 36, instance_count, 0, 0); } + +#if !defined(STRIP_ALL) + if (s_debug_enabled_) { + for (const auto& obj : scene.objects) { + // Simple AABB approximation from scale + visual_debug_.add_box(obj.position, obj.scale, vec3(1.0f, 1.0f, 0.0f)); // Yellow boxes + } + + // Calculate ViewProj matrix for the debug renderer + mat4 view_proj = camera.get_projection_matrix() * camera.get_view_matrix(); + visual_debug_.render(pass, view_proj); + } +#endif } void Renderer3D::render(const Scene& scene, const Camera& camera, float time, diff --git a/src/3d/renderer.h b/src/3d/renderer.h index 8cb379b..453daf2 100644 --- a/src/3d/renderer.h +++ b/src/3d/renderer.h @@ -9,6 +9,10 @@ #include "gpu/gpu.h" #include +#if !defined(STRIP_ALL) +#include "3d/visual_debug.h" +#endif + // Matches the GPU struct layout struct GlobalUniforms { mat4 view_proj; @@ -28,6 +32,10 @@ class Renderer3D { void init(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format); void shutdown(); +#if !defined(STRIP_ALL) + static void SetDebugEnabled(bool enabled) { s_debug_enabled_ = enabled; } +#endif + // Renders the scene to the given texture view (Convenience: creates a pass) void render(const Scene& scene, const Camera& camera, float time, @@ -75,4 +83,9 @@ class Renderer3D { // Max objects capacity static const int kMaxObjects = 100; + +#if !defined(STRIP_ALL) + VisualDebug visual_debug_; + static bool s_debug_enabled_; +#endif }; 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) diff --git a/src/3d/visual_debug.h b/src/3d/visual_debug.h new file mode 100644 index 0000000..c757de3 --- /dev/null +++ b/src/3d/visual_debug.h @@ -0,0 +1,49 @@ +// This file is part of the 64k demo project. +// It provides visual debugging tools, such as wireframe rendering. +// This entire module is excluded when STRIP_ALL is defined. + +#pragma once + +#if !defined(STRIP_ALL) + +#include "util/mini_math.h" +#include "gpu/gpu.h" +#include + +struct DebugLine { + vec3 start; + vec3 end; + vec3 color; +}; + +class VisualDebug { +public: + void init(WGPUDevice device, WGPUTextureFormat format); + void shutdown(); + + // Queue a wireframe box for rendering this frame + void add_box(const vec3& center, const vec3& extent, const vec3& color); + + // Render all queued primitives and clear the queue + void render(WGPURenderPassEncoder pass, const mat4& view_proj); + +private: + WGPUDevice device_ = nullptr; + WGPURenderPipeline pipeline_ = nullptr; + WGPUBindGroupLayout bind_group_layout_ = nullptr; + + std::vector lines_; + + // Uniform buffer for ViewProjection matrix + WGPUBuffer uniform_buffer_ = nullptr; + WGPUBindGroup bind_group_ = nullptr; + + // Vertex buffer for line segments + WGPUBuffer vertex_buffer_ = nullptr; + size_t vertex_buffer_capacity_ = 0; + + void create_pipeline(WGPUTextureFormat format); + void update_buffers(const mat4& view_proj); +}; + +#endif // !defined(STRIP_ALL) diff --git a/src/main.cc b/src/main.cc index 2ece549..406b900 100644 --- a/src/main.cc +++ b/src/main.cc @@ -7,6 +7,7 @@ #include "audio/synth.h" #include "generated/assets.h" // Include generated asset header #include "gpu/gpu.h" +#include "3d/renderer.h" #include "platform.h" #include "util/math.h" #include @@ -149,6 +150,8 @@ int main(int argc, char** argv) { width_ptr = &custom_width; height_ptr = &custom_height; } + } else if (strcmp(argv[i], "--debug") == 0) { + Renderer3D::SetDebugEnabled(true); } } #else diff --git a/src/tests/test_3d_render.cc b/src/tests/test_3d_render.cc index a87897f..003eeaf 100644 --- a/src/tests/test_3d_render.cc +++ b/src/tests/test_3d_render.cc @@ -171,9 +171,20 @@ void gen_periodic_noise(uint8_t* buffer, int w, int h, const float* params, procedural::make_periodic(buffer, w, h, p_params, 1); } -int main() { +int main(int argc, char** argv) { printf("Running 3D Renderer Test...\n"); +#if !defined(STRIP_ALL) + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--debug") == 0) { + Renderer3D::SetDebugEnabled(true); + } + } +#else + (void)argc; + (void)argv; +#endif + PlatformState platform_state = {}; platform_init(&platform_state, false, nullptr, nullptr); -- cgit v1.2.3