render: extract static M2 batch materializer

This commit is contained in:
2026-07-18 02:00:55 +04:00
parent 0fd052923d
commit 252927d90c
12 changed files with 593 additions and 31 deletions
@@ -0,0 +1,56 @@
class_name M2StaticBatchMaterializer
extends RefCounted
## Builds and attaches one static M2 MultiMesh batch from an ordered transform
## slice. All Resource and SceneTree mutation belongs on the main thread.
## Returns the attached MultiMeshInstance3D, or null for invalid/empty input.
## The supplied parent owns the returned node and its MultiMesh Resource.
## Transform bounds are a build-planner precondition and are not clamped.
func materialize_batch(
m2_parent_root: Node3D,
relative_path: String,
mesh: Mesh,
transforms: Array,
start_index: int,
instance_count: int,
batch_serial: int,
visibility_range_end: float,
visibility_range_margin: float,
cast_shadows: bool
) -> MultiMeshInstance3D:
if (
m2_parent_root == null
or mesh == null
or transforms.is_empty()
or instance_count <= 0
):
return null
var multimesh := MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.mesh = mesh
multimesh.instance_count = instance_count
for batch_offset in instance_count:
multimesh.set_instance_transform(
batch_offset,
transforms[start_index + batch_offset]
)
var multimesh_instance := MultiMeshInstance3D.new()
multimesh_instance.name = "%s_%d" % [
relative_path.get_file().get_basename(),
batch_serial,
]
multimesh_instance.multimesh = multimesh
multimesh_instance.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
if visibility_range_end > 0.0:
multimesh_instance.visibility_range_end = visibility_range_end
multimesh_instance.visibility_range_end_margin = visibility_range_margin
m2_parent_root.add_child(multimesh_instance)
return multimesh_instance
@@ -0,0 +1 @@
uid://bgiw27rkh8fbn
+17 -22
View File
@@ -60,6 +60,9 @@ const M2_PLACEMENT_GROUPER_SCRIPT := preload(
const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_batch_planner.gd"
)
const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
"res://src/render/m2/m2_static_batch_materializer.gd"
)
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
)
@@ -273,6 +276,7 @@ var _m2_unique_placement_registry := (
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
var _m2_static_batch_materializer := M2_STATIC_BATCH_MATERIALIZER_SCRIPT.new()
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()
var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new()
var _m2_prototype_cache_state := M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()
@@ -4396,29 +4400,20 @@ func _materialize_m2_group_batch(
start: int,
count: int,
serial: int) -> void:
if transforms.is_empty() or count <= 0 or mesh == null:
return
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.mesh = mesh
mm.instance_count = count
for i in count:
mm.set_instance_transform(i, transforms[start + i])
var mmi := MultiMeshInstance3D.new()
mmi.name = "%s_%d" % [rel_path.get_file().get_basename(), serial]
mmi.multimesh = mm
mmi.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if m2_cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
var multimesh_instance := _m2_static_batch_materializer.materialize_batch(
m2_root,
rel_path,
mesh,
transforms,
start,
count,
serial,
m2_visibility_range,
CHUNK_SIZE,
m2_cast_shadows
)
if m2_visibility_range > 0.0:
mmi.visibility_range_end = m2_visibility_range
mmi.visibility_range_end_margin = CHUNK_SIZE
m2_root.add_child(mmi)
_set_editor_owner_recursive(mmi)
if multimesh_instance != null:
_set_editor_owner_recursive(multimesh_instance)
func _materialize_m2_animated_batch(
@@ -0,0 +1,255 @@
extends SceneTree
## Synthetic MultiMesh identity, transform order, render settings, ownership
## boundary and bounded-timing regression for static M2 batch materialization.
const MATERIALIZER_SCRIPT := preload(
"res://src/render/m2/m2_static_batch_materializer.gd"
)
const MATERIALIZER_PATH := "res://src/render/m2/m2_static_batch_materializer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_batch_contract(failures)
_verify_zero_visibility_contract(failures)
_verify_invalid_inputs(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_STATIC_BATCH_MATERIALIZER: %s" % failure)
quit(1)
return
print(
"M2_STATIC_BATCH_MATERIALIZER PASS cases=11 instances=10000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_batch_contract(failures: Array[String]) -> void:
var materializer = MATERIALIZER_SCRIPT.new()
var parent_root := Node3D.new()
get_root().add_child(parent_root)
var mesh := ArrayMesh.new()
var transforms: Array = [
Transform3D(Basis.IDENTITY, Vector3.ZERO),
Transform3D(Basis.IDENTITY, Vector3(1.0, 2.0, 3.0)),
Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0)),
]
var multimesh_instance: MultiMeshInstance3D = materializer.materialize_batch(
parent_root,
"World\\Doodads\\StaticTree.m2",
mesh,
transforms,
1,
2,
7,
345.0,
16.0,
false
)
_expect_true(multimesh_instance != null, "valid batch returned", failures)
_expect_same(multimesh_instance.get_parent(), parent_root, "batch attached once", failures)
_expect_equal(parent_root.get_child_count(), 1, "one parent child", failures)
_expect_string_equal(multimesh_instance.name, "StaticTree_7", "batch name", failures)
_expect_equal(multimesh_instance.multimesh.transform_format, MultiMesh.TRANSFORM_3D, "3D transform format", failures)
_expect_same(multimesh_instance.multimesh.mesh, mesh, "exact Mesh identity", failures)
_expect_equal(multimesh_instance.multimesh.instance_count, 2, "instance count", failures)
_expect_equal(
multimesh_instance.cast_shadow,
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
"shadow mode",
failures
)
_expect_float_equal(multimesh_instance.visibility_range_end, 345.0, "visibility end", failures)
_expect_float_equal(multimesh_instance.visibility_range_end_margin, 16.0, "visibility margin", failures)
parent_root.free()
func _verify_zero_visibility_contract(failures: Array[String]) -> void:
var materializer = MATERIALIZER_SCRIPT.new()
var parent_root := Node3D.new()
var multimesh_instance: MultiMeshInstance3D = materializer.materialize_batch(
parent_root,
"world/static.m2",
ArrayMesh.new(),
[Transform3D.IDENTITY],
0,
1,
0,
0.0,
99.0,
true
)
_expect_float_equal(multimesh_instance.visibility_range_end, 0.0, "disabled visibility unchanged", failures)
_expect_equal(
multimesh_instance.cast_shadow,
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
"enabled shadow mode",
failures
)
parent_root.free()
func _verify_invalid_inputs(failures: Array[String]) -> void:
var materializer = MATERIALIZER_SCRIPT.new()
var parent_root := Node3D.new()
var mesh := ArrayMesh.new()
var transforms: Array = [Transform3D.IDENTITY]
_expect_true(
materializer.materialize_batch(
null, "a.m2", mesh, transforms, 0, 1, 0, 0.0, 0.0, true
) == null,
"null parent rejected",
failures
)
_expect_true(
materializer.materialize_batch(
parent_root, "a.m2", null, transforms, 0, 1, 0, 0.0, 0.0, true
) == null,
"null Mesh rejected",
failures
)
_expect_true(
materializer.materialize_batch(
parent_root, "a.m2", mesh, [], 0, 1, 0, 0.0, 0.0, true
) == null,
"empty transforms rejected",
failures
)
_expect_true(
materializer.materialize_batch(
parent_root, "a.m2", mesh, transforms, 0, 0, 0, 0.0, 0.0, true
) == null,
"zero count rejected",
failures
)
_expect_equal(parent_root.get_child_count(), 0, "invalid input has no attachment", failures)
parent_root.free()
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var loader_adapter_source := _source_between(
loader_source,
"func _materialize_m2_group_batch(",
"func _materialize_m2_animated_batch(",
failures
)
_expect_true(
loader_source.contains("M2_STATIC_BATCH_MATERIALIZER_SCRIPT.new()"),
"loader composes materializer",
failures
)
_expect_equal(
loader_adapter_source.count("_m2_static_batch_materializer.materialize_batch("),
1,
"loader delegates once",
failures
)
_expect_false(loader_adapter_source.contains("MultiMesh.new()"), "loader adapter omits MultiMesh construction", failures)
_expect_false(loader_adapter_source.contains("set_instance_transform("), "loader adapter omits transform upload", failures)
_expect_true(loader_adapter_source.contains("_set_editor_owner_recursive(multimesh_instance)"), "loader retains Editor ownership", failures)
for required_materializer_rule in [
"MultiMesh.TRANSFORM_3D",
"multimesh.mesh = mesh",
"multimesh.instance_count = instance_count",
"multimesh.set_instance_transform(",
"transforms[start_index + batch_offset]",
"m2_parent_root.add_child(multimesh_instance)",
]:
_expect_true(
materializer_source.contains(required_materializer_rule),
"materializer owns %s" % required_materializer_rule,
failures
)
for forbidden_dependency in [
"ResourceLoader.",
"FileAccess.",
"WorkerThreadPool.",
"RenderingServer.",
".owner =",
"_m2_build_jobs",
"_render_budget_scheduler",
]:
_expect_false(
materializer_source.contains(forbidden_dependency),
"materializer omits %s ownership" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var materializer = MATERIALIZER_SCRIPT.new()
var parent_root := Node3D.new()
get_root().add_child(parent_root)
var transforms: Array = []
for instance_index in 10000:
transforms.append(
Transform3D(Basis.IDENTITY, Vector3(float(instance_index), 0.0, 0.0))
)
var started_microseconds := Time.get_ticks_usec()
var multimesh_instance: MultiMeshInstance3D = materializer.materialize_batch(
parent_root,
"world/timing.m2",
ArrayMesh.new(),
transforms,
0,
10000,
1,
0.0,
0.0,
true
)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_equal(multimesh_instance.multimesh.instance_count, 10000, "timing batch complete", failures)
_expect_true(elapsed_milliseconds < 1000.0, "10000 transforms under one second", failures)
parent_root.free()
return elapsed_milliseconds
func _source_between(
source: String,
start_marker: String,
end_marker: String,
failures: Array[String]
) -> String:
var start_offset := source.find(start_marker)
var end_offset := source.find(end_marker, start_offset + start_marker.length())
if start_offset < 0 or end_offset <= start_offset:
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
return ""
return source.substr(start_offset, end_offset - start_offset)
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
if not condition:
failures.append(label)
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
_expect_true(not condition, label, failures)
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
if actual != expected:
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
func _expect_float_equal(actual: float, expected: float, label: String, failures: Array[String]) -> void:
if not is_equal_approx(actual, expected):
failures.append("%s expected=%.6f actual=%.6f" % [label, expected, actual])
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
if actual != expected:
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
if not is_same(actual, expected):
failures.append(label)
@@ -0,0 +1 @@
uid://hdpp6alfpaa3