Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,4 @@ examples/go/*/sandbox
# Coverage artifacts
.coverage
**/coverage/
.claude/scheduled_tasks.lock
11 changes: 10 additions & 1 deletion codegen/ffi_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3695,6 +3695,15 @@
"return_type": "i32",
"is_unsafe": true
},
"goud_renderer3d_instantiate_plane": {
"source_file": "ffi/renderer3d/primitives.rs",
"params": [
"context_id: GoudContextId",
"source_plane_id: u32"
],
"return_type": "u32",
"is_unsafe": false
},
"goud_renderer3d_is_animation_playing": {
"source_file": "ffi/renderer3d/animation.rs",
"params": [
Expand Down Expand Up @@ -6593,5 +6602,5 @@
"is_unsafe": false
}
},
"total_count": 669
"total_count": 670
}
1 change: 1 addition & 0 deletions codegen/ffi_mapping.json
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@
"renderer_3d": {
"goud_renderer3d_create_cube": {},
"goud_renderer3d_create_plane": {},
"goud_renderer3d_instantiate_plane": {},
"goud_renderer3d_create_sphere": {},
"goud_renderer3d_create_cylinder": {},
"goud_renderer3d_set_object_position": {},
Expand Down
1 change: 1 addition & 0 deletions codegen/gen_ts_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,7 @@ def gen_web_wrapper():
lines.append(" loadModel(_path: string): number { return 0; }")
lines.append(" destroyModel(_modelId: number): boolean { return false; }")
lines.append(" instantiateModel(_sourceModelId: number): number { return 0; }")
lines.append(" instantiatePlane(_sourcePlaneId: number): number { return 0; }")
lines.append(" setModelMaterial(_modelId: number, _meshIndex: number, _materialId: number): boolean { return false; }")
lines.append(" getModelMeshCount(_modelId: number): number { return 0; }")
lines.append(" getModelBoundingBox(_modelId: number): { minX: number; minY: number; minZ: number; maxX: number; maxY: number; maxZ: number } { return { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 }; }")
Expand Down
5 changes: 5 additions & 0 deletions codegen/generated/goud_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -2728,6 +2728,11 @@ uint32_t goud_renderer3d_create_cube(struct GoudContextId context_id, uint32_t t
*/
uint32_t goud_renderer3d_create_plane(struct GoudContextId context_id, uint32_t texture_id, float width, float depth);

/**
* Creates an instance of a source plane primitive.
*/
uint32_t goud_renderer3d_instantiate_plane(struct GoudContextId context_id, uint32_t source_plane_id);

/**
* Creates a 3D sphere object.
*/
Expand Down
14 changes: 14 additions & 0 deletions codegen/goud_sdk.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5055,6 +5055,17 @@
],
"returns": "u32"
},
{
"name": "instantiatePlane",
"doc": "Creates an instanced plane that shares geometry with a source plane (issue #679). All instances of the same source plane render through one instanced draw call regardless of setStaticBatchingEnabled / setInstancingEnabled / setMinInstancesForBatching (those flags govern non-instanced primitives and skinned models, not plane-instance pools). Use one source plane per material to draw multiple materials. Destroying the source plane cascades: the pool is freed and existing instance handles are invalidated.",
"params": [
{
"name": "sourcePlaneId",
"type": "u32"
}
],
"returns": "u32"
},
{
"name": "createSphere",
"doc": "Creates a 3D sphere",
Expand Down Expand Up @@ -13032,6 +13043,9 @@
"createPlane": {
"ffi": "goud_renderer3d_create_plane"
},
"instantiatePlane": {
"ffi": "goud_renderer3d_instantiate_plane"
},
"createSphere": {
"ffi": "goud_renderer3d_create_sphere"
},
Expand Down
5 changes: 3 additions & 2 deletions goud_engine/src/ffi/renderer3d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ pub use postprocess::{
pub use primitives::{
goud_renderer3d_create_cube, goud_renderer3d_create_cylinder, goud_renderer3d_create_plane,
goud_renderer3d_create_sphere, goud_renderer3d_destroy_object,
goud_renderer3d_set_object_position, goud_renderer3d_set_object_rotation,
goud_renderer3d_set_object_scale, goud_renderer3d_set_object_static,
goud_renderer3d_instantiate_plane, goud_renderer3d_set_object_position,
goud_renderer3d_set_object_rotation, goud_renderer3d_set_object_scale,
goud_renderer3d_set_object_static,
};
pub use scene::{
goud_renderer3d_add_light_to_scene, goud_renderer3d_add_model_to_scene,
Expand Down
86 changes: 86 additions & 0 deletions goud_engine/src/ffi/renderer3d/primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,92 @@ pub extern "C" fn goud_renderer3d_create_plane(
.unwrap_or(GOUD_INVALID_OBJECT)
}

/// Creates an instance of a source plane primitive.
///
/// Mirrors `goud_renderer3d_instantiate_model` for primitives. Every instance
/// of the same source plane renders through one instanced draw call, so a
/// terrain of identical-geometry tiles collapses to one batch per source plane
/// (issue #679).
///
/// The returned id behaves like an object id for transform updates
/// (`set_object_position`, `set_object_rotation`, `set_object_scale`,
/// `destroy_object`). Per-instance materials are not supported -- the source
/// plane's material/texture is captured when the first instance is created.
/// Use one source plane per material to draw multiple materials.
///
/// # Source plane visibility
///
/// The source plane is a regular `Object3D`, so it is also drawn by the
/// per-object pass. Each pool therefore costs **one extra per-object draw on
/// top of the instanced draw** (e.g., 9 source planes + 9 pools = 18 draws,
/// not 9). The pool's draw call collapse is still the dominant win for
/// large terrains, but if the source plane is camera-visible callers should
/// either:
///
/// * Position the source plane outside the camera frustum / view region so
/// frustum culling drops it (recommended for terrain templates), or
/// * Use it as the first visible tile (place it where one of the pool's
/// instances would have been) so the per-object draw is not "extra".
///
/// # Lifecycle
///
/// Destroying the source plane via `destroy_object` cascades: the underlying
/// pool's GPU buffers are freed, every existing instance handle is invalidated,
/// and subsequent calls to `instantiate_plane` with the same id return
/// `GOUD_INVALID_OBJECT`. Adding the source plane to a scene via
/// `add_object_to_scene` makes the entire pool visible in that scene; passing
/// an instance handle to `add_object_to_scene` resolves to the source plane.
///
/// # Interaction with batching flags
///
/// Plane-instance pools always render through the instanced path. The
/// renderer's batching flags do **not** gate them:
///
/// - `set_static_batching_enabled` controls the static-batch VBO that combines
/// non-instanced primitives marked with `set_object_static`. It does **not**
/// apply to plane-instance pools and does not need to be enabled to benefit
/// from `instantiate_plane`.
/// - `set_instancing_enabled` enables instanced rendering for skinned model
/// instances above the `min_instances_for_batching` threshold. It does
/// **not** apply to plane-instance pools, which always batch regardless of
/// the flag or instance count.
/// - `set_min_instances_for_batching` is the threshold for skinned model
/// instancing. It is ignored by plane-instance pools.
///
/// Primitives created via `create_plane` (without `instantiate_plane`) still
/// follow the legacy paths: `set_object_static` + `set_static_batching_enabled`
/// places them in the static-batch VBO; otherwise they render via the
/// per-object pass.
///
/// # Arguments
/// * `context_id` - The windowed context
/// * `source_plane_id` - Object id returned by `goud_renderer3d_create_plane`
///
/// # Returns
/// Plane-instance handle on success, GOUD_INVALID_OBJECT on failure.
#[no_mangle]
pub extern "C" fn goud_renderer3d_instantiate_plane(
context_id: GoudContextId,
source_plane_id: u32,
) -> u32 {
if context_id == GOUD_INVALID_CONTEXT_ID {
set_last_error(GoudError::InvalidContext);
return GOUD_INVALID_OBJECT;
}

if let Err(e) = ensure_renderer3d_state(context_id) {
set_last_error(e);
return GOUD_INVALID_OBJECT;
}

with_renderer(context_id, |renderer| {
renderer
.instantiate_plane(source_plane_id)
.unwrap_or(GOUD_INVALID_OBJECT)
})
.unwrap_or(GOUD_INVALID_OBJECT)
}

/// Creates a 3D sphere object.
#[no_mangle]
pub extern "C" fn goud_renderer3d_create_sphere(
Expand Down
20 changes: 20 additions & 0 deletions goud_engine/src/jni/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3242,6 +3242,26 @@ pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_createPlane<'
})
}

#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_instantiatePlane<'local>(
mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>,
contextId: jni::sys::jlong,
sourcePlaneId: jni::sys::jint,
) -> jni::sys::jint {
crate::jni::helpers::catch_jni_panic(&mut env, "Java_com_goudengine_internal_GoudGameNative_instantiatePlane", 0, |env| -> crate::jni::helpers::JniCallResult<jni::sys::jint> {
crate::jni::helpers::prepare_call(env)?;
crate::jni::helpers::clear_last_error();
let result = crate::ffi::renderer3d::goud_renderer3d_instantiate_plane(goud_context_id_from_jlong(contextId), sourcePlaneId as _);
if crate::jni::helpers::last_error_code() != 0 {
let _ = crate::jni::helpers::throw_engine_error(env, "goud_renderer3d_instantiate_plane", Some(result as i64));
return Err(());
}
Ok(result as i32)
})
}

#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_com_goudengine_internal_GoudGameNative_createSphere<'local>(
Expand Down
16 changes: 16 additions & 0 deletions goud_engine/src/libs/graphics/renderer3d/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ pub struct Renderer3D {
/// Object IDs that were actually included in the static batch (not overflowed).
/// Used to distinguish batched objects from overflow objects that need individual draws.
pub(in crate::libs::graphics::renderer3d) static_batched_ids: FxHashSet<u32>,
/// Pools of instanced plane primitives, keyed by `instanced_mesh_id`.
/// One pool per source plane; every instance through `instantiate_plane`
/// goes into the corresponding pool's `InstancedMesh`, collapsing to a
/// single instanced draw call per source plane (#679).
pub(in crate::libs::graphics::renderer3d) plane_instance_pools:
FxHashMap<u32, super::core_plane_instances::PlaneInstancePool>,
/// Reverse lookup from a plane-instance handle to `(instanced_mesh_id, slot)`.
pub(in crate::libs::graphics::renderer3d) plane_instance_index: FxHashMap<u32, (u32, usize)>,
/// Reverse lookup from a source plane object id to its pool's `instanced_mesh_id`.
pub(in crate::libs::graphics::renderer3d) source_plane_to_pool: FxHashMap<u32, u32>,
/// Reusable scratch buffer of dirty pool ids (avoids per-frame alloc).
pub(in crate::libs::graphics::renderer3d) scratch_dirty_plane_pool_ids: Vec<u32>,
}

// StaticBatchGroup is defined in core_static_batch.rs
Expand Down Expand Up @@ -364,6 +376,10 @@ impl Renderer3D {
static_batch_groups: Vec::new(),
static_batch_vertex_count: 0,
static_batched_ids: FxHashSet::default(),
plane_instance_pools: FxHashMap::default(),
plane_instance_index: FxHashMap::default(),
source_plane_to_pool: FxHashMap::default(),
scratch_dirty_plane_pool_ids: Vec::new(),
})
}
}
16 changes: 16 additions & 0 deletions goud_engine/src/libs/graphics/renderer3d/core/object_transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use cgmath::Vector3;
#[allow(missing_docs)]
impl Renderer3D {
pub fn set_object_position(&mut self, id: u32, x: f32, y: f32, z: f32) -> bool {
if self.try_update_plane_instance_transform(id, Some(Vector3::new(x, y, z)), None, None) {
return true;
}
let ok = self.mutate_object(id, |obj| {
obj.position = Vector3::new(x, y, z);
});
Expand All @@ -17,13 +20,19 @@ impl Renderer3D {
ok
}
pub fn set_object_rotation(&mut self, id: u32, x: f32, y: f32, z: f32) -> bool {
if self.try_update_plane_instance_transform(id, None, Some(Vector3::new(x, y, z)), None) {
return true;
}
let ok = self.mutate_object(id, |obj| obj.rotation = Vector3::new(x, y, z));
if ok && self.objects.get(&id).is_some_and(|o| o.is_static) {
self.static_batch_dirty = true;
}
ok
}
pub fn set_object_scale(&mut self, id: u32, x: f32, y: f32, z: f32) -> bool {
if self.try_update_plane_instance_transform(id, None, None, Some(Vector3::new(x, y, z))) {
return true;
}
let ok = self.mutate_object(id, |obj| {
obj.scale = Vector3::new(x, y, z);
});
Expand Down Expand Up @@ -57,11 +66,18 @@ impl Renderer3D {
}

pub fn remove_object(&mut self, id: u32) -> bool {
if self.try_remove_plane_instance(id) {
return true;
}
if let Some(obj) = self.objects.remove(&id) {
if obj.is_static {
self.static_batch_dirty = true;
}
self.backend.destroy_buffer(obj.buffer);
// If this object was the source of a plane-instance pool, tear
// the whole pool down so the source-plane id cannot route future
// instances into a stale pool (#679 review feedback).
self.destroy_plane_pool_for_source(id);
true
} else {
false
Expand Down
Loading
Loading