From 21b061b951ec3a65bc479fabeb2d9565e08a807e Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 8 Mar 2026 10:38:19 +0100 Subject: fix: transpose matrices on GPU upload (row-major → column-major) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mini_math mat4 is row-major; WGSL mat4x4f is column-major. Matrices uploaded without transposing were interpreted as their own transpose on the GPU, causing RotatingCube and Renderer3D to render upside-down. - Add gpu_upload_mat4() to post_process_helper for standalone uploads - Add Uniforms::make() to RotatingCube::Uniforms (handles transpose) - Add GlobalUniforms::make() and ObjectData::make() to renderer.h - Update renderer_draw.cc and visual_debug.cc to use make() handoff(Gemini): matrix layout bug fixed across all rasterized effects. Co-Authored-By: Claude Sonnet 4.6 --- src/3d/renderer.h | 13 +++++++++ src/3d/renderer_draw.cc | 57 +++++++++++-------------------------- src/3d/visual_debug.cc | 7 ++--- src/effects/ntsc.wgsl | 5 ++-- src/effects/rotating_cube_effect.cc | 14 ++++----- src/effects/rotating_cube_effect.h | 8 ++++++ src/gpu/post_process_helper.cc | 6 ++++ src/gpu/post_process_helper.h | 4 +++ 8 files changed, 58 insertions(+), 56 deletions(-) (limited to 'src') diff --git a/src/3d/renderer.h b/src/3d/renderer.h index bf3b497..4c31c97 100644 --- a/src/3d/renderer.h +++ b/src/3d/renderer.h @@ -23,6 +23,14 @@ struct GlobalUniforms { vec4 params; // x = num_objects, yzw = padding vec2 resolution; vec2 padding; + + // Transpose matrices: mini_math is row-major, WGSL expects column-major. + static GlobalUniforms make(const mat4& vp, const vec4& cam_pos_time, + const vec4& p, const vec2& res) { + return {mat4::transpose(vp), mat4::transpose(vp.inverse()), + cam_pos_time, p, + res, vec2(0.0f, 0.0f)}; + } }; // Matches the GPU struct layout @@ -33,6 +41,11 @@ struct ObjectData { // params.x = object type (as float), params.y = plane_distance (if // applicable) vec4 params; + + // Transpose matrices: mini_math is row-major, WGSL expects column-major. + static ObjectData make(const mat4& m, const vec4& col, const vec4& p) { + return {mat4::transpose(m), mat4::transpose(m.inverse()), col, p}; + } }; class Renderer3D { diff --git a/src/3d/renderer_draw.cc b/src/3d/renderer_draw.cc index 8a08998..d4a35a8 100644 --- a/src/3d/renderer_draw.cc +++ b/src/3d/renderer_draw.cc @@ -9,63 +9,38 @@ void Renderer3D::update_uniforms(const Scene& scene, const Camera& camera, float time) { - GlobalUniforms globals; - globals.view_proj = camera.get_projection_matrix() * camera.get_view_matrix(); - globals.inv_view_proj = globals.view_proj.inverse(); - globals.camera_pos_time = - vec4(camera.position.x, camera.position.y, camera.position.z, time); - globals.params = + const mat4 vp = camera.get_projection_matrix() * camera.get_view_matrix(); + const GlobalUniforms globals = GlobalUniforms::make( + vp, + vec4(camera.position.x, camera.position.y, camera.position.z, time), vec4((float)std::min((size_t)kMaxObjects, scene.objects.size()), 0.0f, - 0.0f, 0.0f); - globals.resolution = vec2((float)width_, (float)height_); - globals.padding = vec2(0.0f, 0.0f); + 0.0f, 0.0f), + vec2((float)width_, (float)height_)); wgpuQueueWriteBuffer(queue_, global_uniform_buffer_, 0, &globals, sizeof(GlobalUniforms)); std::vector obj_data; for (const auto& obj : scene.objects) { - ObjectData data; - data.model = obj.get_model_matrix(); - - // Calculate Inverse for point transformation - data.inv_model = data.model.inverse(); - - data.color = obj.color; float type_id = 0.0f; switch (obj.type) { - case ObjectType::SPHERE: - type_id = 1.0f; - break; - case ObjectType::BOX: - type_id = 2.0f; - break; - case ObjectType::CUBE: - type_id = 2.0f; - break; // CUBE is same as BOX for shader - case ObjectType::TORUS: - type_id = 3.0f; - break; - case ObjectType::PLANE: - type_id = 4.0f; - break; - case ObjectType::MESH: - type_id = 5.0f; - break; - default: - type_id = 0.0f; - break; + case ObjectType::SPHERE: type_id = 1.0f; break; + case ObjectType::BOX: type_id = 2.0f; break; + case ObjectType::CUBE: type_id = 2.0f; break; // CUBE is same as BOX + case ObjectType::TORUS: type_id = 3.0f; break; + case ObjectType::PLANE: type_id = 4.0f; break; + case ObjectType::MESH: type_id = 5.0f; break; + default: type_id = 0.0f; break; } float plane_distance = 0.0f; if (obj.type == ObjectType::PLANE && obj.shared_user_data) { - // Safely cast shared_user_data to PlaneData* and get distance plane_distance = ((PlaneData*)(obj.shared_user_data.get()))->distance; } - data.params = - vec4(type_id, plane_distance, obj.local_extent.x, obj.local_extent.y); - obj_data.push_back(data); + obj_data.push_back(ObjectData::make( + obj.get_model_matrix(), obj.color, + vec4(type_id, plane_distance, obj.local_extent.x, obj.local_extent.y))); if (obj_data.size() >= kMaxObjects) break; } diff --git a/src/3d/visual_debug.cc b/src/3d/visual_debug.cc index cd4ccce..f6796d5 100644 --- a/src/3d/visual_debug.cc +++ b/src/3d/visual_debug.cc @@ -340,10 +340,9 @@ void VisualDebug::add_trajectory(const std::vector& points, } void VisualDebug::update_buffers(const mat4& view_proj) { - // Update Uniforms - fill entire GlobalUniforms structure - GlobalUniforms uniforms = {}; - uniforms.view_proj = view_proj; - // Other fields zeroed (not used by visual debug shader) + // Update Uniforms - only view_proj used by visual debug shader + const GlobalUniforms uniforms = GlobalUniforms::make( + view_proj, vec4(0, 0, 0, 0), vec4(0, 0, 0, 0), vec2(0, 0)); wgpuQueueWriteBuffer(wgpuDeviceGetQueue(device_), uniform_buffer_, 0, &uniforms, sizeof(GlobalUniforms)); diff --git a/src/effects/ntsc.wgsl b/src/effects/ntsc.wgsl index 6c9fa37..5c27695 100644 --- a/src/effects/ntsc.wgsl +++ b/src/effects/ntsc.wgsl @@ -5,6 +5,7 @@ const vignetteRounding = 160.0f; const vignetteSmoothness = 0.7f; +const fisheyeStrength = vec2f(.1, .24); @group(0) @binding(0) var input_sampler: sampler; @group(0) @binding(1) var input_texture: texture_2d; @@ -13,7 +14,7 @@ const vignetteSmoothness = 0.7f; // Barrel (fisheye) distortion: strength > 0 = barrel, < 0 = pincushion fn fisheye(uv: vec2f, strength: f32) -> vec2f { let r2 = uv * uv; - return uv * 1.05 * (1.0 + vec2f(.1, .24) * strength * r2); + return uv * 1.2 * (1.0 + fisheyeStrength * strength * r2); } fn vignette(uv: vec2f) -> f32 { @@ -32,7 +33,7 @@ fn vignette(uv: vec2f) -> f32 { // Black outside screen edges if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { - discard; // return vec4f(0.0, 0.0, 0.0, 1.0); + return vec4f(0.0, 0.0, 0.0, 1.0); } // Chroma separation (horizontal RGB bleeding) diff --git a/src/effects/rotating_cube_effect.cc b/src/effects/rotating_cube_effect.cc index 099d06c..757d64c 100644 --- a/src/effects/rotating_cube_effect.cc +++ b/src/effects/rotating_cube_effect.cc @@ -5,6 +5,7 @@ #include "effects/shaders.h" #include "gpu/bind_group_builder.h" #include "gpu/gpu.h" +#include "gpu/post_process_helper.h" #include "util/fatal_error.h" RotatingCube::RotatingCube(const GpuContext& ctx, @@ -137,15 +138,10 @@ void RotatingCube::render(WGPUCommandEncoder encoder, const mat4 model = T * R * S; // Update uniforms - const Uniforms uniforms = { - .view_proj = view_proj, - .inv_view_proj = view_proj.inverse(), - .camera_pos_time = - vec4(camera_pos.x, camera_pos.y, camera_pos.z, params.time), - .params = vec4(1.0f, 0.0f, 0.0f, 0.0f), - .resolution = params.resolution, - .aspect_ratio = params.aspect_ratio, - }; + const Uniforms uniforms = Uniforms::make( + view_proj, + vec4(camera_pos.x, camera_pos.y, camera_pos.z, params.time), + vec4(1.0f, 0.0f, 0.0f, 0.0f), params.resolution, params.aspect_ratio); const ObjectData obj_data = { .model = model, diff --git a/src/effects/rotating_cube_effect.h b/src/effects/rotating_cube_effect.h index e773025..920be5c 100644 --- a/src/effects/rotating_cube_effect.h +++ b/src/effects/rotating_cube_effect.h @@ -28,6 +28,14 @@ class RotatingCube : public Effect { vec2 resolution; float aspect_ratio; float _pad; + + // Transpose matrices: mini_math is row-major, WGSL expects column-major. + static Uniforms make(const mat4& vp, const vec4& cam_pos_time, + const vec4& p, const vec2& res, float asp) { + return {mat4::transpose(vp), mat4::transpose(vp.inverse()), + cam_pos_time, p, + res, asp}; + } }; static_assert(sizeof(Uniforms) == 176, "Uniforms size mismatch"); diff --git a/src/gpu/post_process_helper.cc b/src/gpu/post_process_helper.cc index b0121e4..79fda20 100644 --- a/src/gpu/post_process_helper.cc +++ b/src/gpu/post_process_helper.cc @@ -67,6 +67,12 @@ create_post_process_pipeline_simple(WGPUDevice device, WGPUTextureFormat format, return pipeline; } +void gpu_upload_mat4(WGPUQueue queue, WGPUBuffer buffer, size_t offset, + const mat4& m) { + const mat4 t = mat4::transpose(m); + wgpuQueueWriteBuffer(queue, buffer, offset, &t, sizeof(mat4)); +} + // --- PostProcess Implementation Helper --- static GpuBuffer g_dummy_buffer = {nullptr, 0}; diff --git a/src/gpu/post_process_helper.h b/src/gpu/post_process_helper.h index 811d0e4..178a4d5 100644 --- a/src/gpu/post_process_helper.h +++ b/src/gpu/post_process_helper.h @@ -25,3 +25,7 @@ WGPURenderPipeline create_post_process_pipeline_simple(WGPUDevice device, void pp_update_bind_group(WGPUDevice device, WGPURenderPipeline pipeline, WGPUBindGroup* bind_group, WGPUTextureView input_view, GpuBuffer uniforms, GpuBuffer effect_params); + +// Upload a mat4 to a GPU buffer, transposing from C++ row-major to WGSL column-major. +void gpu_upload_mat4(WGPUQueue queue, WGPUBuffer buffer, size_t offset, + const mat4& m); -- cgit v1.2.3