OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / 2ac6cc8

#120 L4 §15 step 7: placed-asset fold cost measured (_probe_asset_fold)
N placements of a 50-box asset vs the same geometry as owned shapes, equal brush counts: refold 19.5/20.9 ms (N=1), 130/105 (N=10), 394/375 (N=40); AssetFold.expand ~1.9 ms per 60-brush placement; moving one placement refolds the whole room like moving one shape (the #128 refold-scope lever). L4 §15 steps 1-7 done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2ac6cc8ddc9faa217c404c7f95859dd3c20e82e5
by plankatron <fleeky@gmail.com> · 2026-10-19T23:42:08.000Z

Changed files (3) +125 -2

docs/node-tree-and-stacks.md +17 -2
……@@ -1371,8 +1371,23 @@ Save as asset is menu-only for now).
13711371 only). `gate_asset_thumb` (9), WINDOWED (the headless dummy renders nothing): a real
13721372 render (colours, the middle is the thing), the overlay hash, both libraries, the tile,
13731373 and a late thumbnail never clobbers a newer save. 2 mutants killed. **DONE 2026-10-19.**
13747. **Measure**: fold cost of N placements of a 50-shape asset (the _probe_fold_split
1375 method) — the follow-up perf issue is #128.
13747. **Measure** (`_probe_asset_fold`, 2026-10-19, tabertron, keetd quiet but load ~4 —
1375 indicative): N placements of a 50-box asset (every 5th mirrored = 60 brushes each)
1376 vs the SAME geometry recorded as the user's own shapes. Equal brush counts in both
1377 (the ruler: the same work), median of 3:
1378
1379 | N | brushes | refold (placed / owned) | AssetFold.expand | move one (placed / owned) |
1380 |---|---|---|---|---|
1381 | 1 | 60 | 19.5 / 20.9 ms | 1.8 ms | 28.2 / 23.7 ms |
1382 | 10 | 600 | 130.3 / 104.8 ms | 21.3 ms | 130.6 / 125.4 ms |
1383 | 40 | 2400 | 393.9 / 374.5 ms | 75.0 ms | 590.9 / 522.7 ms |
1384
1385 - Instancing costs about the same as owning the records (+5–25 %); the expansion is
1386 linear, ~1.9 ms per 60-brush placement.
1387 - Moving ONE placement refolds the WHOLE room, exactly as moving one shape does —
1388 the per-edit cost grows with the room, not the edit. That is #128's "refold scope"
1389 lever (refold only what the edit touches), not an asset problem.
1390 **DONE 2026-10-19.**
13761391
13771392### 15.9 Not in v1
13781393
scenes/test/_probe_asset_fold.gd +102 -0
……@@ -0,0 +1,102 @@
1extends Node
2## PROBE (not a gate) — WHAT PLACED ASSETS COST THE FOLD (#120 L4 §15.8 step 7). One room
3## per process, PROBE_N placements (default 10) of a 50-shape asset ("Kit": 50 boxes in a
4## 10 x 5 grid, every 5th with a mirror), then, each the median of PROBE_REPS (3):
5## refold() the whole fold, forced (no skip, no tail)
6## AssetFold.expand the placements' expansion alone
7## move one placement asset.xform + its refold (what a G on one costs)
8## and the SAME geometry as ordinary shapes — N copies of the 50 boxes recorded directly — for
9## the reference: what instancing adds over owning the records.
10## DIGESTS: the placements' brushes vs the copies' (offset), so the two rooms are the same.
11##
12## RUN: PROBE_N=10 CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . res://scenes/test/_probe_asset_fold.tscn
13
14var map: Node3D
15var persist: Node
16var level: Node
17
18
19func _ready() -> void:
20 var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
21 gs.route("world:level:*", 1, "peer")
22 var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
23 if keet != null:
24 keet.current_member_id = "probe"
25 get_tree().root.get_node("/root/GameMode").set_mode("build")
26 map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
27 add_child(map)
28 for i in 6:
29 await get_tree().process_frame
30 persist = map.get("_persist")
31 level = map.get("level")
32 var n := int(OS.get_environment("PROBE_N")) if OS.get_environment("PROBE_N") != "" else 10
33 var reps := int(OS.get_environment("PROBE_REPS")) if OS.get_environment("PROBE_REPS") != "" else 3
34 var mode := OS.get_environment("PROBE_MODE") if OS.get_environment("PROBE_MODE") != "" else "assets"
35 var O0 := Vector3(1000, 0, 1000)
36 # The asset: 50 boxes, every 5th mirrored.
37 persist.begin_gesture("kit")
38 var members: Array = []
39 for i in 50:
40 var at := O0 + Vector3((i % 10) * 3.0, 0, (i / 10) * 3.0)
41 var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
42 "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)), "size": [2, 2, 2], "material_id": 2, "level_id": 0})
43 if i % 5 == 0:
44 persist.record("mod.create", {"target": d, "mod": "mirror", "params": {"axis": "x", "offset": 0.0}})
45 members.append("g:" + d)
46 var kit: String = persist.record("col.create", {"name": "Kit", "parent": ""})
47 persist.record("col.link", {"target": kit, "members": members})
48 persist.end_gesture()
49 persist.refold()
50 var lib: Node = map.asset_library()
51 lib.save_collection(kit, "Kit", true, false)
52 var base_brushes := (level.get_live_brush_ids() as PackedInt32Array).size()
53 var dots: Array = []
54 persist.begin_gesture("place")
55 for k in n:
56 var f := Transform3D(Basis.IDENTITY, O0 + Vector3(0, 0, 40 + k * 20))
57 if mode == "assets":
58 dots.append(persist.record("asset.place", {"asset": lib.rows()[0]["oid"], "name": "Kit", "frame": SceneObjects.xform_to(f)}))
59 else:
60 # The reference: the same 50 boxes (+ mirrors) recorded as the user's own shapes.
61 for i2 in 50:
62 var at2 := f.origin + Vector3((i2 % 10) * 3.0, 0, (i2 / 10) * 3.0) - Vector3(0, 0, 0)
63 var d2: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
64 "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at2)), "size": [2, 2, 2], "material_id": 2, "level_id": 0})
65 if i2 % 5 == 0:
66 persist.record("mod.create", {"target": d2, "mod": "mirror", "params": {"axis": "x", "offset": 0.0}})
67 persist.end_gesture()
68 persist.refold()
69 var brushes := (level.get_live_brush_ids() as PackedInt32Array).size() - base_brushes
70 var tf := []
71 for r in reps:
72 persist.set("_brush_ver_folded", -1)
73 persist.set("_ix_tail_ok", false)
74 var t0 := Time.get_ticks_usec()
75 persist.refold()
76 tf.append(float(Time.get_ticks_usec() - t0) / 1000.0)
77 var te := []
78 if mode == "assets":
79 var arecs: Array = (persist.records(false) as Array).filter(func(x: Dictionary) -> bool: return String(x["kind"]).begins_with("asset."))
80 for r2 in reps:
81 var t1 := Time.get_ticks_usec()
82 LevelPersistence.AssetFold.expand(arecs, persist.get("_asset_docs"), persist._parse_as)
83 te.append(float(Time.get_ticks_usec() - t1) / 1000.0)
84 var tm := []
85 for r3 in reps:
86 var t2 := Time.get_ticks_usec()
87 persist.begin_gesture("move")
88 if mode == "assets":
89 persist.record("asset.xform", {"target": dots[0], "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, O0 + Vector3(r3 + 1, 0, 40)))})
90 else:
91 persist.record("shape.xform", {"target": String(members[0]).substr(2), "frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, O0 + Vector3(r3 + 1, 0, 0)))})
92 persist.end_gesture()
93 persist.refold()
94 tm.append(float(Time.get_ticks_usec() - t2) / 1000.0)
95 print("PROBE %s N=%d: %d brushes from them | refold() %.1f ms | AssetFold.expand %s | move one %.1f ms"
96 % [mode, n, brushes, _med(tf), ("%.1f ms" % _med(te)) if not te.is_empty() else "—", _med(tm)])
97 get_tree().quit(0)
98
99
100func _med(ts: Array) -> float:
101 ts.sort()
102 return ts[ts.size() / 2]
scenes/test/_probe_asset_fold.tscn +6 -0
……@@ -0,0 +1,6 @@
1[gd_scene load_steps=2 format=3]
2
3[ext_resource type="Script" path="res://scenes/test/_probe_asset_fold.gd" id="1"]
4
5[node name="AssetFoldProbe" type="Node"]
6script = ExtResource("1")