extends Node
## PROBE (not a gate) — WHERE A FULL REFOLD'S TIME GOES (hyper-games #127). One room per
## process, PROBE_CASE:
## bundle20 a 20-cable bundle (as _probe_cable_fold_time)
## bundle40 the same with 40 cables — the RULER CHECK: the cable-attributable stages
## should about double (a ruler that does not scale is not measuring cables)
## spheres20 20 spheres at 32 x 16 (the user's "high poly faceted spheres")
## Then the brush half of LevelPersistence.refold, STAGE BY STAGE as refold runs it
## (records partition -> ShapeFold.expand -> level.apply_journal -> level.rebuild), each
## the median of 3, plus refold() itself the same way for the total. No ops / mods in
## these rooms, so those stages are empty (checked).
## DIGEST: a hash of every live brush's planes after the fold — the correctness golden for
## a change that must not alter geometry (same digest before and after it).
##
## RUN: PROBE_CASE=bundle20 CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . res://scenes/test/_probe_fold_split.tscn
const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd")
const OpFold := preload("res://scenes/maps/op_fold.gd")
const ModFold := preload("res://scenes/maps/mod_fold.gd")
const CollectionFold := preload("res://scenes/maps/collection_fold.gd")
var persist: Node
var level: Node
var map: Node3D
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 = "probe"
get_tree().root.get_node("/root/GameMode").set_mode("build")
map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
persist = map.get("_persist")
level = map.get("level")
var case := OS.get_environment("PROBE_CASE") if OS.get_environment("PROBE_CASE") != "" else "bundle20"
match case:
"bundle20", "bundle40":
_bundle(20 if case == "bundle20" else 40)
"spheres20":
_spheres(int(OS.get_environment("PROBE_N")) if OS.get_environment("PROBE_N") != "" else 20)
_:
print("PROBE unknown case %s" % case)
get_tree().quit(1)
return
print("PROBE step: room built %d ms" % Time.get_ticks_msec())
persist.refold()
# PROBE_DUMP=<file>: write the planes of every brush with > 64 planes (nx ny nz d per
# line, a blank line between brushes) — the input for a standalone libdc benchmark.
if OS.get_environment("PROBE_DUMP") != "":
var fo := FileAccess.open(OS.get_environment("PROBE_DUMP"), FileAccess.WRITE)
for b: int in (level.get_live_brush_ids() as PackedInt32Array):
var pl: PackedFloat32Array = level.get_brush(b)["planes"]
if pl.size() / 4 > 64:
for i in pl.size() / 4:
fo.store_line("%.9f %.9f %.9f %.9f" % [pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2], pl[i * 4 + 3]])
fo.store_line("")
fo.close()
print("PROBE dumped to %s" % OS.get_environment("PROBE_DUMP"))
get_tree().quit(0)
return
print("PROBE step: first refold done %d ms" % Time.get_ticks_msec())
_split(case)
get_tree().quit(0)
func _bundle(n: int) -> void:
var A := {"pts": PackedVector3Array([Vector3(400, 0, 300), Vector3(400, 0, 302), Vector3(400, 2, 302), Vector3(400, 2, 300)]), "n": Vector3.RIGHT}
var B := {"pts": PackedVector3Array([Vector3(410, 0, 300), Vector3(410, 2, 300), Vector3(410, 2, 302), Vector3(410, 0, 302)]), "n": Vector3.LEFT}
map.cable_bundle([A, B], {"count": n, "random": true, "seed": 1, "tension": 1.0, "jitter": 0.3, "slack": 1.0},
{"radius": 0.03})
func _spheres(n: int) -> void:
persist.begin_gesture("spheres")
for k in n:
persist.record("shape.create", {"type": "sphere", "gv": 1, "params": {"segments": 32, "rings": 16},
"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(300 + (k % 5) * 6, 0, 300 + (k / 5) * 6))),
"size": [4, 4, 4], "material_id": 2, "level_id": 0})
persist.end_gesture()
func _med(ts: Array) -> float:
ts.sort()
return ts[ts.size() / 2]
func _split(case: String) -> void:
var part := []
var ex := []
var ap := []
var rb := []
var tot := []
var brush_recs: Array = []
var extra := 0
var reps := int(OS.get_environment("PROBE_REPS")) if OS.get_environment("PROBE_REPS") != "" else 3
var phases := {}
for rep in reps:
var t0 := Time.get_ticks_usec()
brush_recs = []
var shape_recs: Array = []
var curve_recs: Array = []
extra = 0
for r: Dictionary in persist.records(false):
var rk := String(r.get("kind", ""))
if LevelPersistence.CurveFold.handles(rk):
curve_recs.append(r)
elif CollectionFold.handles(rk):
continue
elif ShapeFold.handles(rk):
shape_recs.append(r)
elif OpFold.handles(rk) or ModFold.handles(rk):
extra += 1
else:
brush_recs.append(r)
print("PROBE step: rep %d partitioned %d ms" % [rep, Time.get_ticks_msec()])
var curves := LevelPersistence.CurveFold.live(curve_recs)
part.append(float(Time.get_ticks_usec() - t0) / 1000.0)
t0 = Time.get_ticks_usec()
brush_recs.append_array(ShapeFold.expand(shape_recs, {}, curves))
ex.append(float(Time.get_ticks_usec() - t0) / 1000.0)
t0 = Time.get_ticks_usec()
print("PROBE step: rep %d expanded %d ms" % [rep, Time.get_ticks_msec()])
level.apply_journal(brush_recs)
print("PROBE step: rep %d applied %d ms" % [rep, Time.get_ticks_msec()])
ap.append(float(Time.get_ticks_usec() - t0) / 1000.0)
t0 = Time.get_ticks_usec()
level.rebuild()
rb.append(float(Time.get_ticks_usec() - t0) / 1000.0)
var st: Dictionary = level.get_stats()
for k: String in ["ms_split", "ms_clip", "ms_mesh", "ms_upload", "ms_collide", "ms_pocket", "build_ms"]:
(phases.get_or_add(k, []) as Array).append(float(st.get(k, -1.0)))
for rep2 in reps:
persist.set("_brush_ver_folded", -1) # force the brush half
persist.set("_ix_tail_ok", false)
var t1 := Time.get_ticks_usec()
persist.refold()
tot.append(float(Time.get_ticks_usec() - t1) / 1000.0)
var ids: PackedInt32Array = level.get_live_brush_ids()
var planes := 0
# THE DIGEST HASHES WHAT face_winding PRODUCES: every live brush's face windings (as
# DCLevel.get_face_winding returns them), the brushes ordered by those bytes — never by
# key, whose seq is time-derived and differs run to run.
var blobs: Array = []
# FACE CORNERS, summed — the digest's bits move with any rounding, this mostly with the
# SHAPE. spheres20 sits ~300 m out: before the brush frames (hyper-games #123) world
# float32 wound ~7330 corners per sphere; in its frame ~4800, the 4760–4797 the same
# planes give moved to the origin (dc-terrain precision audit §12).
var corners := 0
for b: int in ids:
var pl: PackedFloat32Array = level.get_brush(b)["planes"]
planes += pl.size() / 4
var bb := PackedByteArray()
for f in pl.size() / 4:
var w := level.get_face_winding(b, f) as PackedVector3Array
corners += w.size()
bb.append_array(w.to_byte_array())
bb.append(255)
blobs.append(bb)
print("PROBE %s: %d face corners over the live brushes" % [case, corners])
blobs.sort_custom(func(x: PackedByteArray, y: PackedByteArray) -> bool: return x.hex_encode() < y.hex_encode())
var h := HashingContext.new()
h.start(HashingContext.HASH_SHA256)
for bb2: PackedByteArray in blobs:
h.update(bb2)
print("PROBE %s: %d live brushes, %d planes, %d brush records, ops/mods %d" % [case, ids.size(), planes, brush_recs.size(), extra])
print("PROBE %s: partition %.1f | ShapeFold.expand %.1f | apply_journal %.1f | rebuild %.1f | (sum %.1f) | refold() %.1f ms"
% [case, _med(part), _med(ex), _med(ap), _med(rb), _med(part) + _med(ex) + _med(ap) + _med(rb), _med(tot)])
var ph := []
for k2: String in phases:
ph.append("%s %.1f" % [k2, _med(phases[k2])])
print("PROBE %s: rebuild phases (get_stats, median): %s" % [case, ", ".join(ph)])
# THE REST OF refold(): the objects half, and the `folded` listeners (the editor's
# outliner, curve view, panels … redraw on every fold).
var ob := []
var em := []
var lf: Dictionary = persist.get("_last_fold")
for rep3 in reps:
var t2 := Time.get_ticks_usec()
var objs: Object = persist.get("objects")
if objs != null:
objs.apply_journal(persist.call("_object_records"))
ob.append(float(Time.get_ticks_usec() - t2) / 1000.0)
t2 = Time.get_ticks_usec()
persist.folded.emit(lf)
em.append(float(Time.get_ticks_usec() - t2) / 1000.0)
var lis: Array = []
for c: Dictionary in persist.folded.get_connections():
lis.append("%s.%s" % [str((c["callable"] as Callable).get_object()).get_slice(":", 0), (c["callable"] as Callable).get_method()])
print("PROBE %s: objects.apply_journal %.1f | folded.emit %.1f ms | listeners %s" % [case, _med(ob), _med(em), lis])
var parts := {}
for rep4 in reps:
var cs: Array = [["brush_view", Callable(map, "_refresh_brush_view")], ["reselect", Callable(map, "_reselect_shape")],
["curves", Callable(map, "_refresh_curves")]]
var ol: Variant = map.get("_outliner")
if ol != null and is_instance_valid(ol):
cs.append(["outliner", Callable(ol, "refresh")])
for c2: Dictionary in persist.folded.get_connections():
var cb: Callable = c2["callable"]
if cb.get_method() == "_on_folded":
cs.append(["history", cb.bind(lf)])
for pr: Array in cs:
var t3 := Time.get_ticks_usec()
(pr[1] as Callable).call()
(parts.get_or_add(String(pr[0]), []) as Array).append(float(Time.get_ticks_usec() - t3) / 1000.0)
var pl2 := []
for k3: String in parts:
pl2.append("%s %.1f" % [k3, _med(parts[k3])])
print("PROBE %s: folded listener parts (median): %s" % [case, ", ".join(pl2)])
print("PROBE %s: digest %s" % [case, h.finish().hex_encode().substr(0, 16)])
_edit_split(case, reps)
## ONE CURVE EDITED — what a drag on a cable's point pays (map.set_curve_point: its record,
## then the room's refold, listeners and all). The first curve's point 1 moves 5 cm up and
## back, an ODD count of edits (2 x reps + 1), so the room ends 5 cm off where it began,
## and its digest is the golden for a cache that must never answer for stale inputs.
func _edit_split(case: String, reps: int) -> void:
var cs: Dictionary = map.curve_states()
if cs.is_empty():
return
var dots: Array = cs.keys()
dots.sort()
var cd: String = dots[0]
var ts := []
for e in reps * 2 + 1:
var q: Vector3 = (map.curve_states()[cd]["points"] as Array)[1]["p"]
var t0 := Time.get_ticks_usec()
map.set_curve_point(cd, 1, {"p": q + Vector3(0, 0.05 if e % 2 == 0 else -0.05, 0)})
ts.append(float(Time.get_ticks_usec() - t0) / 1000.0)
var blobs: Array = []
for b: int in (level.get_live_brush_ids() as PackedInt32Array):
var pl: PackedFloat32Array = level.get_brush(b)["planes"]
var bb := PackedByteArray()
for f in pl.size() / 4:
bb.append_array((level.get_face_winding(b, f) as PackedVector3Array).to_byte_array())
bb.append(255)
blobs.append(bb)
blobs.sort_custom(func(x: PackedByteArray, y: PackedByteArray) -> bool: return x.hex_encode() < y.hex_encode())
var h := HashingContext.new()
h.start(HashingContext.HASH_SHA256)
for bb2: PackedByteArray in blobs:
h.update(bb2)
print("PROBE %s: one curve point edited (set_curve_point, refold + listeners) median %.1f ms of %d, digest after %s"
% [case, _med(ts), ts.size(), h.finish().hex_encode().substr(0, 16)])