OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / _probe_fold_split.gd

250 lines · gd ref:
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)])