perf(renderer3d): optimize 3D rendering pipeline for 44% FPS improvement - #668
Conversation
Optimize the renderer3d hot paths to improve character_sandbox performance from ~40 FPS to ~59 FPS with 280 animated agents, shadows enabled. Key changes: - Replace O(total_vertices) shadow bounds computation with O(num_objects) bounding sphere approach — reduces shadow_build from 11.4ms to 0.1ms - Eliminate per-frame heap allocations: bone matrix clones, uniform struct clones, lights Vec allocation, animation player ID collection - Migrate renderer3d HashMaps to FxHashMap (rustc-hash) for faster integer key lookups in hot paths - Pre-cache material sort keys and visible object draw data to reduce HashMap lookups per draw call - Add #[inline] to hot animation math, unroll mat4_mul for auto-vectorization, optimize quaternion normalization (reciprocal multiply) - Cache skinned mesh model matrices with dirty flag - Reuse wgpu pipeline key Vecs across frames via scratch buffers - Expose FramePhaseTimings to C# SDK for per-phase profiling - Add animation timing instrumentation (anim_eval, bone_pack, bone_upload) Regression fixes included: - Fix memory leak: revert uniform buffer growth from *2 to next_power_of_two() and deduplicate per-shader buffer recreation - Fix frustum culling: remove broken cached bounds (initialized in local space), revert to correct inline world-space computation - Add regression tests for both buffer growth stability and culling correctness Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Claude finished @aram-devdocs's task in 3m 24s —— View job Code Review: perf(renderer3d) — 44% FPS OptimizationVerdict: ✅ APPROVED WITH WARNINGS This is a well-executed, high-impact performance PR. The optimization strategy is sound, the implementation is careful, and the measured results (40 → 59 FPS, shadow_build -99%) are impressive. A few warnings should be addressed before or shortly after merge. BlockersNone. Warnings (should fix, non-blocking)
Warning DetailsW1/W2 — File size limits Both files grew over the project's stated 500-line limit with this PR's additions. Consider extracting the scratch-buffer management and visible-object caching into a dedicated submodule (e.g., W3 — Unsafe raw pointer in // SAFETY: we just ensured `cached_fallback_names` is `Some`.
let names = self.cached_fallback_names.as_ref().unwrap();
// Take a raw pointer to avoid the borrow conflict with `&mut self`.
let names_ptr = names as *const Vec<BonePropertyNames>;
// SAFETY: `update_with_names` does not modify `cached_fallback_names`.
let names_ref = unsafe { &*names_ptr };
self.update_with_names(dt, skeleton, animations, names_ref);The SAFETY comments are correct as written today, but this pattern is fragile: if anyone refactors // Option 1: pass by cloning (acceptable for lazy-init data)
let names = self.cached_fallback_names.as_ref().unwrap().clone();
self.update_with_names(dt, skeleton, animations, &names);
// Option 2: restructure update_with_names to take names as a slice
// so we can borrow names and call as a free functionW4 — SDK tests unchecked on the PR checklist The PR modifies Please confirm these pass locally and check the boxes before merge. SDK tests live at:
W5 — Silent data drop in for triangle in mesh.vertices.chunks_exact(24) {
debug_assert!(
mesh.vertices.len() % 24 == 0,
"mesh vertex buffer length {} is not a multiple of 24",
mesh.vertices.len()
);
for triangle in mesh.vertices.chunks_exact(24) {Positive CalloutsExcellent patterns observed:
Minor Observations (informational, no action required)
|
Extract object_transforms.rs from core/mod.rs (516→369 lines) and skinned_render.rs from render/mod.rs (540→413 lines) to satisfy CI file size check. Pure structural change, no logic modifications. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- W3: Replace unsafe raw pointer in animation/mod.rs with safe take/restore pattern for cached_fallback_names - W4: Fix TypeScript SDK — add IFramePhaseTimings interface to codegen (ts_node_shared.py, ts_node_interface.py, ts_node_wrapper.py, gen_ts_web.py). All SDK tests now pass. - W5: Add debug_assert for vertex buffer alignment in shadow.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Overview
Type: perf
Summary:
Optimize the renderer3d hot paths to improve character_sandbox performance from ~40 FPS to ~59 FPS with 280 animated agents and shadows enabled. The shadow bounds computation was the primary bottleneck (11.4ms/frame), reduced to 0.1ms by switching from per-vertex to per-object bounding sphere approach.
Related Issues:
Changes Made
Engine Core (
goud_engine/src/)Rendering pipeline optimizations:
shadow.rsrender/mod.rsskinned_mesh.rs,core_skinned.rsrender/shadow_render.rsAnimation optimizations:
#[inline]to hot math functions inanimation_sampling.rsmat4_mulfor LLVM auto-vectorizationanimation/mod.rsHashMap migration:
rustc-hashdependency, migrate ~13 hot-path HashMaps to FxHashMap incore/mod.rs,core_model_animation/mod.rs,render_instanced_skinned.rs,scene.rs,shadow.rs,render_helpers.rs,core_models/lifecycle.rsuniforms.rspipeline key hashingBackend optimizations:
frame.rs,shadow_pass.rs*2tonext_power_of_two()and deduplicate per-shader recreation inuniforms.rs,shadow_pass.rsProfiling instrumentation:
anim_eval,bone_pack,bone_uploadphase timings toframe_timing.rsFramePhaseTimingsvia FFI inffi/types.rs,ffi/renderer/metrics.rsRegression fixes:
*2growth strategyRegression tests:
frustum.rs: Test world-space bounds at non-origin positions, scaled objectsuniforms.rs: Test buffer growth stabilizes at power-of-two sizesFFI Layer (
goud_engine/src/ffi/)shadowPassUs,animEvalUs,bonePackUs,boneUploadUsfields toFfiFramePhaseTimingsgoud_renderer_get_frame_phase_timingsto include new fieldsC# SDK (
sdks/csharp/)GetFramePhaseTimings()method on GoudGameFramePhaseTimingsstruct with new fieldsFfiFramePhaseTimingsP/Invoke structPython SDK (
sdks/python/)TypeScript SDK (
sdks/typescript/)Codegen Pipeline (
codegen/)shadowPassUsfield,animEvalUs/bonePackUs/boneUploadUsfields,getFramePhaseTimingsmethodgetFramePhaseTimingsProc Macros (
goud_engine_macros/)No changes
Tools (
tools/)No changes
WASM (
goud_engine/src/wasm/)No changes
Examples (
examples/)character_sandbox/Program.cs: Added phase timing display to live HUD and profile report outputDocumentation
No changes
Architectural Compliance
#[no_mangle] extern "C"and#[repr(C)]where neededTesting
cargo testpasses (4779 passed, 0 failed)cargo clippy -- -D warningsis cleancargo fmt --all -- --checkpasses./codegen.shsucceeded)Code Quality
todo!()orunimplemented!()in production code#[allow(unused)]without justification commentResult, notunwrap()/expect()in library codeDocumentation
AGENTS.mdfiles (if architecture changed)Breaking Changes
None — all changes are internal optimizations. FFI struct has new fields appended (backward compatible).
FfiFramePhaseTimingshas 3 new fields appended (ABI-safe)GetFramePhaseTimings()method added (additive)Version Bump
Bump type: patch
Justification: Internal performance optimization, no API changes
Security
unsafeblocksPerformance
Baseline (280 agents, shadows, no-vsync): 40.8 FPS, 14.3ms render
After optimization: 58.6 FPS (+44%), 2.8ms render (-80%)
Key phase timing improvements:
Scale test: 560 agents (400 NPCs + 160 animals) runs at 71.4 FPS.
Deployment
Reviewer Notes
compute_light_space_matrixfrom iterating all vertices to using bounding spheres — this alone accounts for most of the 11ms reductionCopyderive additions toLight,FogConfig,SkyboxConfig,GridConfigenable pass-by-value instead of clone — these are small structs*2→next_power_of_two()+ shader dedup🤖 Generated with Claude Code