OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / 06f3cf8

#127: selection centre / size and group labels through DCLevel's narrow reads (-120 ms a fold at a bundle)
selection_centre / selection_size and OutlinerModel._group_label called get_brush (~60 us) per brush for one field each. Now DCLevel.get_brushes_bounds / count_clearance (dc-terrain 2510f64), proved bit-equal to the old loops by gate_brush_narrow_reads (+ ci entry). A/B on bundle20: outliner listener 181.8 -> 61.2 ms; rows() 44 -> 1.3 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
06f3cf831e77c07983ac567944b7ab9febbd9483
by plankatron <fleeky@gmail.com> · 2026-10-19T19:44:54.000Z

Changed files (4) +155 -23

.pear-git/ci.yaml +4 -0
……@@ -1130,6 +1130,10 @@ steps:
11301130 # A bundle between two face sets (#120 L3 §14.6): islands, seeded ends on the faces,
11311131 # free end handles, slack; N cables + a Bundle collection in one gesture; Shift+Ctrl+C.
11321132 run: "HYPERTPS_DAEMON_TCP=127.0.0.1:1 /storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . res://scenes/test/gate_cable_bundle.tscn"
1133 - name: gate-brush-narrow-reads
1134 # DCLevel.get_brushes_bounds / count_clearance answer bit-for-bit what the get_brush
1135 # loops they replaced did (hyper-games #127: the organizer's per-fold reads).
1136 run: "/storage/projects/code/godot/nvidia-rtx-godot-v3/bin/godot.linuxbsd.editor.x86_64 --headless --path . --script res://scenes/test/gate_brush_narrow_reads.gd"
11331137 - name: gate-curve-overlay
11341138 # Curves (wire, points, held handles) are an editor overlay: hidden in play mode, the
11351139 # Curves switch (Shift+Alt+K) and the master switch hide them in build (2026-10-18).
scenes/game/outliner_model.gd +3 -4
……@@ -434,10 +434,9 @@ func _shape_label(p: Node, dot: String) -> String:
434434## with a clearance volume is a passage, one without is a plain solid. Cheaper
435435## and more honest than journalling a label that could disagree with the brushes.
436436func _group_label(members: PackedInt32Array) -> String:
437 var clearance := 0
438 for b: int in members:
439 if bool((level.get_brush(b) as Dictionary).get("clearance", false)):
440 clearance += 1
437 # One narrow read, not a get_brush per member: every fold labels every group, and
438 # that was ~40 ms per pass at a 20-cable bundle (#127).
439 var clearance: int = level.count_clearance(members)
441440 var solids: int = members.size() - clearance
442441 if clearance > 0:
443442 return "passage (%d + cut)" % solids
scenes/maps/map_dc_level.gd +6 -19
……@@ -6359,20 +6359,13 @@ func world_scale() -> float:
63596359
63606360
63616361## The selection's bounding box, for the organizer's size fields.
6362## One narrow read (DCLevel.get_brushes_bounds), not a get_brush per brush: the organizer
6363## asks on every fold, and a 668-brush selection cost ~80 ms that way (#127).
63626364func selection_size() -> Vector3:
63636365 if _sel_brushes.is_empty() or level == null:
63646366 return Vector3.ZERO
6365 var lo := Vector3.INF
6366 var hi := -Vector3.INF
6367 var any := false
6368 for b: int in _sel_brushes:
6369 var d: Dictionary = level.get_brush(b)
6370 if not d.has("aabb_min"):
6371 continue
6372 lo = lo.min(d["aabb_min"] as Vector3)
6373 hi = hi.max(d["aabb_max"] as Vector3)
6374 any = true
6375 return (hi - lo) if any else Vector3.ZERO
6367 var bb: Dictionary = level.get_brushes_bounds(PackedInt32Array(_sel_brushes))
6368 return ((bb["aabb_max"] as Vector3) - (bb["aabb_min"] as Vector3)) if int(bb["count"]) > 0 else Vector3.ZERO
63766369
63776370
63786371## Select every live brush in the level — the organizer's Select all.
……@@ -6392,14 +6385,8 @@ func selection_centre() -> Vector3:
63926385func _selection_centre() -> Vector3:
63936386 if _sel_brushes.is_empty() or level == null:
63946387 return Vector3.ZERO
6395 var sum := Vector3.ZERO
6396 var n := 0
6397 for b: int in _sel_brushes:
6398 var d: Dictionary = level.get_brush(b)
6399 if d.has("center"):
6400 sum += d["center"] as Vector3
6401 n += 1
6402 return sum / float(n) if n > 0 else Vector3.ZERO
6388 # The mean of the brushes' centres, as get_brush's "center" summed (#127: one read).
6389 return level.get_brushes_bounds(PackedInt32Array(_sel_brushes))["center"]
64036390
64046391
64056392func _clear_face_selection() -> void:
scenes/test/gate_brush_narrow_reads.gd +142 -0
……@@ -0,0 +1,142 @@
1extends SceneTree
2## DCLevel's NARROW READS equal the get_brush loops they replace (hyper-games #127).
3##
4## "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_brush_narrow_reads.gd
5##
6## get_brushes_bounds / count_clearance exist because the organizer read one field of
7## get_brush (~60 us a brush) for every brush on every fold. They must answer EXACTLY
8## what those loops answered — the same floats, not merely close — so the reference
9## below is each loop as it stood (map_dc_level selection_centre / selection_size,
10## OutlinerModel._group_label), run on the same ids:
11## all live brushes (60 boxes at non-round offsets, so the centre sum carries rounding),
12## a TOMBSTONED one (get_brush still answers it), an unknown id, a repeat, the empty set,
13## and reversed order (the centre is summed in the order given).
14## Anti-absence: the level must hold clearance AND sky brushes and the tombstone must be
15## in the set, or the comparisons prove nothing.
16
17var _failures: Array[String] = []
18var _frame := 0
19
20
21func _check(label: String, ok: bool, detail: String) -> void:
22 if not ok:
23 _failures.append("%s (%s)" % [label, detail])
24 print(" %-52s %s %s" % [label, "OK " if ok else "FAIL", detail])
25
26
27static func _box(lo: Vector3, hi: Vector3) -> PackedFloat32Array:
28 return PackedFloat32Array([
29 -1, 0, 0, -lo.x, 1, 0, 0, hi.x,
30 0, -1, 0, -lo.y, 0, 1, 0, hi.y,
31 0, 0, -1, -lo.z, 0, 0, 1, hi.z,
32 ])
33
34
35## map_dc_level.selection_centre's loop, verbatim but for its inputs.
36static func _ref_centre(level: Node, ids: Array) -> Vector3:
37 var sum := Vector3.ZERO
38 var n := 0
39 for b: int in ids:
40 var d: Dictionary = level.get_brush(b)
41 if d.has("center"):
42 sum += d["center"] as Vector3
43 n += 1
44 return sum / float(n) if n > 0 else Vector3.ZERO
45
46
47## map_dc_level.selection_size's loop, verbatim but for its inputs.
48static func _ref_size(level: Node, ids: Array) -> Vector3:
49 var lo := Vector3.INF
50 var hi := -Vector3.INF
51 var any := false
52 for b: int in ids:
53 var d: Dictionary = level.get_brush(b)
54 if not d.has("aabb_min"):
55 continue
56 lo = lo.min(d["aabb_min"] as Vector3)
57 hi = hi.max(d["aabb_max"] as Vector3)
58 any = true
59 return (hi - lo) if any else Vector3.ZERO
60
61
62## OutlinerModel._group_label's count, verbatim but for its inputs.
63static func _ref_clearance(level: Node, ids: PackedInt32Array) -> int:
64 var clearance := 0
65 for b: int in ids:
66 if bool((level.get_brush(b) as Dictionary).get("clearance", false)):
67 clearance += 1
68 return clearance
69
70
71func _compare(level: Node, label: String, ids: Array) -> void:
72 var p := PackedInt32Array(ids)
73 var bb: Dictionary = level.get_brushes_bounds(p)
74 var c: Vector3 = bb["center"]
75 var sz: Vector3 = (bb["aabb_max"] as Vector3) - (bb["aabb_min"] as Vector3) if int(bb["count"]) > 0 else Vector3.ZERO
76 var rc := _ref_centre(level, ids)
77 var rs := _ref_size(level, ids)
78 _check("%s: centre bit-equal" % label, c == rc, "%s vs %s" % [str(c), str(rc)])
79 _check("%s: size bit-equal" % label, sz == rs, "%s vs %s" % [str(sz), str(rs)])
80 var k: int = level.count_clearance(p)
81 var rk := _ref_clearance(level, p)
82 _check("%s: clearance count" % label, k == rk, "%d vs %d" % [k, rk])
83
84
85func _process(_delta: float) -> bool:
86 _frame += 1
87 if _frame < 3:
88 return false
89 print("brush narrow reads gate")
90 var holder := Node3D.new()
91 root.add_child(holder)
92 var level := ClassDB.instantiate("DCLevel") as Node3D
93 holder.add_child(level)
94 if not level.has_method("get_brushes_bounds") or not level.has_method("count_clearance"):
95 _check("the loaded dc-terrain has the narrow reads", false, "rebuild the .so")
96 return _finish()
97 var rng := RandomNumberGenerator.new()
98 rng.seed = 127
99 for i in 60:
100 var lo := Vector3(rng.randf_range(-300, 300), rng.randf_range(-40, 40), rng.randf_range(-300, 300))
101 var hi := lo + Vector3(rng.randf_range(0.03, 9), rng.randf_range(0.03, 9), rng.randf_range(0.03, 9))
102 # Every 7th a clearance volume, every 11th sky: both flags present among the ids.
103 level.add_brush(_box(lo, hi), 0, PackedInt32Array(), 0, i % 11 == 3, "", i % 7 == 2)
104 level.rebuild()
105 var live: PackedInt32Array = level.get_live_brush_ids()
106 var dead := live[5]
107 level.remove_brush(dead)
108 live = level.get_live_brush_ids()
109 var all: Array = Array(live)
110 all.append(dead)
111 var sky := 0
112 for b: int in all:
113 sky += 1 if bool((level.get_brush(b) as Dictionary).get("sky", false)) else 0
114 _check("anti-absence: clearance brushes among the ids", _ref_clearance(level, PackedInt32Array(all)) > 0,
115 "%d" % _ref_clearance(level, PackedInt32Array(all)))
116 _check("anti-absence: sky brushes among the ids", sky > 0, "%d" % sky)
117 _check("anti-absence: the tombstone still answers get_brush", (level.get_brush(dead) as Dictionary).has("center"),
118 "brush %d" % dead)
119 _check("anti-absence: the tombstone is not live", not live.has(dead), "brush %d" % dead)
120 _compare(level, "all + tombstone", all)
121 var rev := all.duplicate()
122 rev.reverse()
123 _compare(level, "reversed", rev)
124 _compare(level, "with unknown id + repeat", [live[0], 999999, live[0], live[1]])
125 _compare(level, "empty", [])
126 _compare(level, "unknown only", [999999])
127 var bb: Dictionary = level.get_brushes_bounds(PackedInt32Array())
128 _check("empty: count 0, zero vectors", int(bb["count"]) == 0 and bb["center"] == Vector3.ZERO
129 and bb["aabb_min"] == Vector3.ZERO and bb["aabb_max"] == Vector3.ZERO, str(bb))
130 return _finish()
131
132
133func _finish() -> bool:
134 if _failures.is_empty():
135 print("PASS brush narrow reads")
136 quit(0)
137 else:
138 print("FAIL brush narrow reads: %d" % _failures.size())
139 for f in _failures:
140 print(" - " + f)
141 quit(1)
142 return true