OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_level_collision_scale.gd

148 lines · gd ref:
extends Node
## GATE — DCLevel's collision rebuild at SCALE (2026-10-05: brush edits took ~1.1 s
## with 2480 brushes, and DCLevel.get_stats() put 98% of it in ONE phase: ms_collide
## 1149 of 1172 ms — the clip was 8.6 ms).
##
## Cause (engine source, modules/jolt_physics/objects/jolt_shaped_object_3d.cpp): a
## Jolt body that is IN A SPACE rebuilds its whole compound shape on every add_shape
## (add_shape -> _shapes_changed -> commit_shapes -> build_shapes). _upload_collision
## put the body in the space FIRST and then added one shape per brush: n compound
## builds of up to n shapes — quadratic.
##
##   1. with 2000 brushes, the collision phase of one rebuild is under 150 ms
##   2. it is still CORRECT: a ray onto each of 25 sampled brushes hits the level, and
##      get_brush_id_from_shape(shape) names THAT brush (the shape order is the brush
##      order picking relies on)
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_level_collision_scale.tscn

const N := 2000
var fails := 0
var checks := 0


func _ok(what: String, cond: bool, note: String = "") -> void:
	checks += 1
	if not cond:
		fails += 1
	print("  %-64s %s  %s" % [what, "OK" if cond else "**FAIL**", note])


func _box(lo: Vector3, hi: Vector3) -> Array:
	return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]


func _ready() -> void:
	var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
	gs.route("world:level:*", 1, "peer")
	var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
	if keet != null:
		keet.current_member_id = "gate-author"
	var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
	add_child(map)
	for i in 6:
		await get_tree().process_frame
	await _run(map)
	if checks < 8:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE level_collision_scale: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _run(map: Node3D) -> void:
	var persist: Node = map.get("_persist")
	var level: Node = map.get("level")
	# 2000 1 m boxes on a grid 3 m apart, 40 per row, 100 m away from map_test's own.
	var keys: Array = []
	persist.begin_gesture("Grid")
	for i in N:
		var x := 200.0 + float(i % 40) * 3.0
		var z := 200.0 + float(i / 40) * 3.0
		keys.append(persist.record("brush.create", {"planes": _box(Vector3(x, 0, z), Vector3(x + 1, 1, z + 1)),
			"proj": [], "material_id": 0, "level_id": 0, "sky": false}))
	persist.end_gesture()
	persist.refold()
	var st: Dictionary = level.get_stats()
	var ms := float(st.get("ms_collide", -1.0))
	_ok("1. collision phase at %d+ brushes under 150 ms" % N, ms >= 0.0 and ms < 150.0,
		"%.1f ms (whole rebuild %.1f ms, clip %.1f ms)" % [ms, float(st.get("build_ms", -1)), float(st.get("ms_clip", -1))])

	for i in 3:
		await get_tree().physics_frame
	var space: PhysicsDirectSpaceState3D = (level as Node3D).get_world_3d().direct_space_state
	var hits := 0
	var right := 0
	var bad: Array = []
	for s in 25:
		var i := (s * 79) % N
		var want: int = level.brush_by_key(String(keys[i]))
		var x := 200.0 + float(i % 40) * 3.0 + 0.5
		var z := 200.0 + float(i / 40) * 3.0 + 0.5
		var q := PhysicsRayQueryParameters3D.create(Vector3(x, 5, z), Vector3(x, -1, z))
		var hit: Dictionary = space.intersect_ray(q)
		if hit.is_empty() or hit.get("collider") != level:
			bad.append("%d: no level hit" % i)
			continue
		hits += 1
		var got: int = level.get_brush_id_from_shape(int(hit["shape"]))
		if got == want:
			right += 1
		else:
			bad.append("%d: shape -> brush %d, want %d" % [i, got, want])
	_ok("2. rays onto 25 sampled brushes hit the level", hits == 25, "%d/25" % hits)
	_ok("2. and each hit names THAT brush", right == 25, "%d/25 %s" % [right, str(bad.slice(0, 3))])

	# 3. THE WIREFRAME follows every brush fold; it filled its mesh one vertex per
	#    GDScript call (~60k calls at 2000 boxes, ~35 ms). One array upload instead.
	var bv: Node = map.call("_ensure_brush_view")
	var best := INF
	for k in 3:
		var t0 := Time.get_ticks_usec()
		bv.refresh()
		best = minf(best, (Time.get_ticks_usec() - t0) / 1000.0)
	var segs: int = bv.segment_count()
	_ok("3. the wireframe refresh at %d+ brushes is under 12 ms" % N, best < 12.0, "%.1f ms" % best)
	_ok("3. and it still draws every edge (12 per box)", segs >= N * 12, "%d segments" % segs)

	# 4. THE EDGE CACHE (dc-terrain, keyed by each brush's planes) never serves a stale
	#    outline: move a brush and delete another with the cache warm, then compare the
	#    whole-level edges with a fresh per-brush computation (get_brush_edges_of).
	persist.record("brush.translate", {"brush_key": String(keys[7]), "delta": [0.0, 2.0, 0.0]})
	persist.record("brush.remove", {"brush_key": String(keys[8])})
	persist.refold()
	var cached: PackedVector3Array = level.get_brush_edges()
	var fresh := PackedVector3Array()
	for id: int in (level.get_live_brush_ids() as PackedInt32Array):
		if not bool((level.get_brush(id) as Dictionary).get("clearance", false)):
			fresh.append_array(level.get_brush_edges_of(id))
	_ok("4. after a move and a delete the cached edges equal a fresh computation",
		cached == fresh, "%d vs %d points" % [cached.size(), fresh.size()])

	# 5. THE BOT NAV GRAPH, built on every map entry from every brush as a box. Each
	#    chunk tested every box for every sample: 25.1 s at 1522 boxes (the starter-map
	#    load hitch, 2026-10-05). dc-terrain now hands each chunk only the boxes near it.
	var tn := Time.get_ticks_usec()
	var populated: int = map.nav_rebuild()
	var nav_ms := (Time.get_ticks_usec() - tn) / 1000.0
	_ok("5. the nav graph over %d+ brushes builds in under 2 s" % N, nav_ms < 2000.0 and populated > 0,
		"%.0f ms, %d chunks" % [nav_ms, populated])

	# 6. DENSE: 1500 more boxes 1.2 m apart (the layout of a big built-up Home), so a
	#    chunk holds hundreds. Per-chunk filtering alone left each sample testing all of
	#    them (758 ms at 3500 brushes); dc-terrain's BoxGrid buckets them (170 ms).
	persist.begin_gesture("Dense")
	for i in 1500:
		var x := -40.0 - float(i % 50) * 1.2
		var z := -40.0 - float(i / 50) * 1.2
		persist.record("brush.create", {"planes": _box(Vector3(x, 0, z), Vector3(x + 1, 1, z + 1)),
			"proj": [], "material_id": 0, "level_id": 0, "sky": false})
	persist.end_gesture()
	persist.refold()
	var td := Time.get_ticks_usec()
	var populated2: int = map.nav_rebuild()
	var dense_ms := (Time.get_ticks_usec() - td) / 1000.0
	# 350 ms: ~3x what it measures (115-127 ms), well under the 758 ms before BoxGrid.
	_ok("6. DENSE: the nav graph over %d brushes (1500 packed) builds in under 350 ms" % (N + 1500),
		dense_ms < 350.0 and populated2 > 0, "%.0f ms, %d chunks" % [dense_ms, populated2])