OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps commits / a7efb8e

#127: cable pieces cached by content (one curve edit in a 20-cable room 534 -> 283 ms)
Every fold regenerated every cable (its curve is followed). world_pieces now answers from a cache keyed on exactly what it reads: the curve's points + closed, the params, the profile curve's points + closed; a hash picks the slot, a deep compare confirms it. gate_cable_gen C1-C4 change each input alone; mutants dropping any one of the four inputs from the key fail it (the cable gates before C1-C4 passed a key without the path's points). Digests before / after 7 curve edits unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a7efb8e70f0f50c878ea6192c86bb01a6fd619da
by plankatron <fleeky@gmail.com> · 2026-10-19T19:44:54.000Z

Changed files (2) +59 -0

scenes/maps/shapes/param/param_cable.gd +26 -0
……@@ -98,7 +98,33 @@ func params() -> Array:
9898## and the resolved params `p`. Empty with no curve or a curve of fewer than 2 points.
9999## `profile` = the profile curve's state when `p.profile` is custom; with no usable
100100## outline (no curve, open, too few points) the cable folds round.
101##
102## CACHED BY CONTENT (hyper-games #127): every fold re-expands every shape, and a cable's
103## curve is followed, so one curve edit re-generated EVERY cable in the room — ~250 ms for
104## a 20-cable bundle, most of it the 2 cm sampling. The pieces depend on nothing but the
105## curve's points + closed, the params and the profile curve's, so those ARE the key —
106## not the curve's ver, which two authors' edits at one seq can share. A hash picks the
107## slot; a deep compare of the inputs confirms it, so a collision is a miss, not a wrong
108## cable. The pieces are SHARED: callers read them and must not write into them.
109const CACHE_MAX := 1024
110static var _cache: Dictionary = {} # hash -> [inputs, pieces]
111
112
101113func world_pieces(curve: Dictionary, p: Dictionary, profile: Dictionary = {}) -> Array:
114 var inputs := [curve.get("points", []), bool(curve.get("closed", false)), p,
115 profile.get("points", []), bool(profile.get("closed", false))]
116 var h := inputs.hash()
117 var hit: Variant = _cache.get(h)
118 if hit != null and (hit as Array)[0] == inputs:
119 return (hit as Array)[1]
120 var out := _world_pieces(curve, p, profile)
121 if _cache.size() >= CACHE_MAX:
122 _cache.clear() # a room's worth of cables is far below this
123 _cache[h] = [inputs.duplicate(true), out]
124 return out
125
126
127func _world_pieces(curve: Dictionary, p: Dictionary, profile: Dictionary = {}) -> Array:
102128 var out: Array = []
103129 if curve.is_empty() or (curve.get("points", []) as Array).size() < 2:
104130 return out
scenes/test/gate_cable_gen.gd +33 -0
……@@ -331,3 +331,36 @@ func _run() -> void:
331331 ids.append(String(r["id"]))
332332 _ok("S9 strands tabled: no Strands / Spread / Twist row in the Shape panel", not ids.has("p.strands")
333333 and not ids.has("p.spread") and not ids.has("p.twist") and ids.has("p.radius"), ids)
334 _cache_checks()
335
336
337## THE PIECES CACHE (#127): world_pieces answers from a content-keyed cache. Each input it
338## reads changes ALONE here, with the params left as they were (the case a key that missed
339## an input would get wrong: a dropped curve went undetected by every cable gate), and the
340## answer must equal a fresh, uncached _world_pieces of that input — and differ from before.
341## C1 the same input twice: the SAME array (the cache is live, so C2–C4 test it)
342## C2 the path curve's points moved
343## C3 the path curve closed
344## C4 the profile curve's points moved, and the profile curve closed / opened
345func _cache_checks() -> void:
346 var a := _curve([Vector3(0, 0.5, 0), Vector3(5, 0.5, 2), Vector3(10, 0.5, 0)])
347 var p: Dictionary = gen.resolve({"radius": 0.1})
348 var first: Array = gen.world_pieces(a, p)
349 _ok("C1 cache: the same input twice -> the same pieces array", is_same(first, gen.world_pieces(a, p)))
350 var b := _curve([Vector3(0, 0.5, 0), Vector3(5, 1.5, 2), Vector3(10, 0.5, 0)])
351 var pb: Array = gen.world_pieces(b, p)
352 _ok("C2 path points moved: = uncached, != before", pb == gen._world_pieces(b, p) and pb != first, pb.size())
353 # The SAME points, only `closed` flipped (_curve(..., true) re-derives the handles, so
354 # the points would differ too and hide a key that dropped `closed`).
355 var c := {"points": a["points"], "closed": true}
356 var pc: Array = gen.world_pieces(c, p)
357 _ok("C3 path closed: = uncached, != before", pc == gen._world_pieces(c, p) and pc != first, pc.size())
358 var pcust: Dictionary = gen.resolve({"radius": 0.1, "profile": "custom"})
359 var sq := _curve([Vector3(100, 0, 0), Vector3(101, 0, 0), Vector3(101, 1, 0), Vector3(100, 1, 0)], true)
360 var kite := _curve([Vector3(100, 0, 0), Vector3(101, 0, 0), Vector3(102, 2, 0), Vector3(100, 1, 0)], true)
361 var sq_open := _curve([Vector3(100, 0, 0), Vector3(101, 0, 0), Vector3(101, 1, 0), Vector3(100, 1, 0)], false)
362 var ps: Array = gen.world_pieces(a, pcust, sq)
363 var pk: Array = gen.world_pieces(a, pcust, kite)
364 var po: Array = gen.world_pieces(a, pcust, sq_open)
365 _ok("C4 profile points moved: = uncached, != before", pk == gen._world_pieces(a, pcust, kite) and pk != ps)
366 _ok("C4 profile opened (folds round): = uncached, != before", po == gen._world_pieces(a, pcust, sq_open) and po != ps)