diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08e8904ce..00d5fcb2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1142,7 +1142,7 @@ jobs: name: Android Build Pipeline runs-on: ubuntu-latest needs: [preflight] - timeout-minutes: 15 + timeout-minutes: 25 steps: - uses: actions/checkout@v6 diff --git a/goud_engine/src/libs/graphics/renderer3d/core_model_instances.rs b/goud_engine/src/libs/graphics/renderer3d/core_model_instances.rs index 2d3880393..6eae1b6e5 100644 --- a/goud_engine/src/libs/graphics/renderer3d/core_model_instances.rs +++ b/goud_engine/src/libs/graphics/renderer3d/core_model_instances.rs @@ -29,9 +29,15 @@ impl Renderer3D { let mut instance_material_ids = Vec::with_capacity(source.mesh_material_ids.len()); for (i, &src_obj_id) in source.mesh_object_ids.iter().enumerate() { - let (src_buffer, vertex_count, texture_id, src_bounds) = + let (src_buffer, vertex_count, texture_id, src_bounds, src_vertices) = match self.objects.get(&src_obj_id) { - Some(o) => (o.buffer, o.vertex_count, o.texture_id, o.bounds), + Some(o) => ( + o.buffer, + o.vertex_count, + o.texture_id, + o.bounds, + o.vertices.clone(), + ), None => continue, }; @@ -68,7 +74,7 @@ impl Renderer3D { Object3D { buffer, vertex_count, - vertices: Vec::new(), + vertices: src_vertices, position: Vector3::new(0.0, 0.0, 0.0), rotation: Vector3::new(0.0, 0.0, 0.0), scale: Vector3::new(1.0, 1.0, 1.0), diff --git a/goud_engine/src/libs/graphics/renderer3d/core_models/mod.rs b/goud_engine/src/libs/graphics/renderer3d/core_models/mod.rs index 8da685da0..8cf8c9bc4 100644 --- a/goud_engine/src/libs/graphics/renderer3d/core_models/mod.rs +++ b/goud_engine/src/libs/graphics/renderer3d/core_models/mod.rs @@ -127,7 +127,13 @@ impl Renderer3D { Object3D { buffer, vertex_count: tri_vert_count as i32, - vertices: Vec::new(), + // CPU-side vertex copy for static batching (8 FPV layout). + // Skinned models use 16 FPV and are never static-batched. + vertices: if is_skinned { + Vec::new() + } else { + verts.clone() + }, position: Vector3::new(0.0, 0.0, 0.0), rotation: Vector3::new(0.0, 0.0, 0.0), scale: Vector3::new(1.0, 1.0, 1.0), diff --git a/goud_engine/src/libs/graphics/renderer3d/tests.rs b/goud_engine/src/libs/graphics/renderer3d/tests.rs index 27e3d3e49..bdf99219b 100644 --- a/goud_engine/src/libs/graphics/renderer3d/tests.rs +++ b/goud_engine/src/libs/graphics/renderer3d/tests.rs @@ -379,3 +379,37 @@ fn test_scene_filtering_limits_rendered_objects() { renderer.render(None); assert_eq!(renderer.stats().draw_calls, 2); } + +/// Regression test for #630: primitives marked static must still render via +/// the static batch path instead of disappearing. +#[test] +fn test_static_primitive_renders_via_batch() { + let mut renderer = make_renderer(); + let cube = renderer.create_primitive(PrimitiveCreateInfo { + primitive_type: PrimitiveType::Cube, + width: 1.0, + height: 1.0, + depth: 1.0, + segments: 1, + texture_id: 0, + }); + assert_ne!(cube, 0); + + // Dynamic path: one draw call, one visible object. + renderer.render(None); + assert_eq!(renderer.stats().draw_calls, 1); + assert_eq!(renderer.stats().visible_objects, 1); + + // Mark static: should render via static batch, not dynamic pass. + assert!(renderer.set_object_static(cube, true)); + renderer.render(None); + let stats = renderer.stats(); + assert!( + stats.draw_calls >= 1, + "static object must produce at least one draw call" + ); + assert_eq!( + stats.visible_objects, 0, + "static object should not appear in dynamic pass" + ); +}