feat(simulation): T-1151 window par_iter + T-1150 granularity carrier (five touch points + aliasing tests)

T-1151: build_district_window_layer dispatches one Rayon task per row
(pure derive_window_cell via derive_at_metres), scattered row-major into
the flat arrays; a cfg(test) serial path backs the bit-identical
parallel-vs-serial golden.

T-1150: serde-default window_granularity (1=district, 4=quarter) +
window_min_wl_m on AtlasLayerRequest — additive, no sixth demux shape,
old frames decode unchanged (tested). Quarter mode = full
reclassification at 512m spacing over the same world rect ((4n)x(4n)
cells); WIRE_CAP_CELLS=4096 enforces n*granularity <= cap (quarter
clamps n to 16, the design doc's worked example). Granularity + min_wl
key ALL five touch points: DistrictWindowLayer echo, server FIFO-256
cache key (now a 5-tuple), per-connection coalescing key, client
request codec (omitted-at-default wire fields), client LRU key.

Mandatory aliasing regressions on both ends: identical (body, center, n)
at granularity 1 vs 4 produce distinct cache entries and correct
per-granularity payload shapes (server, 3-thread queue to avoid the
AnalyzeBody thread contention found while writing it) and distinct
client cache keys (gdUnit). Replay fixture regenerated — the layer
struct grew two echoed fields (231->254 bytes, content verified).

Client requests stay district-granularity by default — quarter requests
arrive with T-1153's rung selection.
This commit is contained in:
2026-07-22 00:27:40 +02:00
parent afff859a2c
commit 3e87fd5b4f
14 changed files with 1027 additions and 118 deletions
@@ -1,13 +1,22 @@
extends RefCounted
## Client-side LRU cache for DistrictWindowLayer responses (T-1138, D-226
## T-1124 amendment §4 "Client cache policy").
## T-1124 amendment §4 "Client cache policy"; extended T-1150 for the
## granularity/min_wl axes).
##
## Keyed on (body_id, center, n) — D-227's determinism guarantee (same seed +
## body + position -> same derived output, always) means a previously-fetched
## window is valid FOREVER for that body+seed. This is an LRU-evict-only
## cache: no freshness check, no TTL, no invalidation path at all. The only
## reason an entry ever leaves is capacity pressure.
## Keyed on (body_id, center, n, granularity, min_wl_m) — D-227's determinism
## guarantee (same seed + body + position + derivation params -> same derived
## output, always) means a previously-fetched window is valid FOREVER for
## that body+seed. This is an LRU-evict-only cache: no freshness check, no
## TTL, no invalidation path at all. The only reason an entry ever leaves is
## capacity pressure.
##
## granularity/min_wl_m default to DISTRICT_GRANULARITY/0 (district spacing,
## no octave cutoff) — every pre-T-1150 caller that doesn't pass them keeps
## its existing key shape and cache behavior unchanged. This is the client
## half of the mandatory aliasing fix (T-1150 design doc §3): a
## quarter-granularity request and a district-granularity request at the
## identical (body, center, n) MUST NOT collide on the same cache slot.
##
## Godot's Dictionary preserves insertion order, so "move to the end on
## touch, evict from the front on overflow" is the whole LRU implementation —
@@ -20,6 +29,10 @@ extends RefCounted
const DEFAULT_MAX_ENTRIES: int = 24
## Mirrors the server's WINDOW_GRANULARITY_DISTRICT (layer_proxy.rs) — the
## default granularity every pre-T-1150 caller implicitly requests.
const DISTRICT_GRANULARITY: int = 1
var _max_entries: int = DEFAULT_MAX_ENTRIES
var _entries: Dictionary = {} # key String -> DistrictWindowLayer Dictionary
@@ -28,19 +41,34 @@ func _init(max_entries: int = DEFAULT_MAX_ENTRIES) -> void:
_max_entries = maxi(1, max_entries)
## Build the cache key from the three fields D-227 makes sufficient:
## body_id (which world+body), center (a [row, col] pair or Vector2i), and n
## (window side length). String-keyed rather than a nested Dictionary/Array
## key — Godot Dictionary keys compare by value for primitives but a
## consistent stringification sidesteps any Vector2i-vs-Array identity
## mismatch between what a caller happens to hand in.
static func make_key(body_id: String, center: Vector2i, n: int) -> String:
return "%s:%d,%d:%d" % [body_id, center.x, center.y, n]
## Build the cache key from the five fields D-227 + T-1150 make sufficient:
## body_id (which world+body), center (a [row, col] pair or Vector2i), n
## (window extent in districts), granularity (district=1 / quarter=4), and
## min_wl_m (the octave cutoff, 0 = none). String-keyed rather than a nested
## Dictionary/Array key — Godot Dictionary keys compare by value for
## primitives but a consistent stringification sidesteps any
## Vector2i-vs-Array identity mismatch between what a caller happens to hand
## in.
static func make_key(
body_id: String,
center: Vector2i,
n: int,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
) -> String:
return "%s:%d,%d:%d:%d:%d" % [body_id, center.x, center.y, n, granularity, min_wl_m]
## True if a window is already cached for this exact (body, center, n).
func has(body_id: String, center: Vector2i, n: int) -> bool:
return _entries.has(make_key(body_id, center, n))
## True if a window is already cached for this exact (body, center, n,
## granularity, min_wl_m).
func has(
body_id: String,
center: Vector2i,
n: int,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
) -> bool:
return _entries.has(make_key(body_id, center, n, granularity, min_wl_m))
## Fetch a cached window, touching it (move-to-most-recently-used). Returns
@@ -48,8 +76,14 @@ func has(body_id: String, center: Vector2i, n: int) -> bool:
## response, which is a different concept (§1: an as-yet-underived window is
## carried as `district_window: None` inside a `Ready` AtlasLayerResponse,
## not a cache state).
func get_window(body_id: String, center: Vector2i, n: int) -> Variant:
var key := make_key(body_id, center, n)
func get_window(
body_id: String,
center: Vector2i,
n: int,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
) -> Variant:
var key := make_key(body_id, center, n, granularity, min_wl_m)
if not _entries.has(key):
return null
var value: Variant = _entries[key]
@@ -61,8 +95,15 @@ func get_window(body_id: String, center: Vector2i, n: int) -> Variant:
## Store a window, evicting the least-recently-used entry(ies) if over
## capacity. Overwriting an existing key also counts as a touch.
func put(body_id: String, center: Vector2i, n: int, window: Dictionary) -> void:
var key := make_key(body_id, center, n)
func put(
body_id: String,
center: Vector2i,
n: int,
window: Dictionary,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
) -> void:
var key := make_key(body_id, center, n, granularity, min_wl_m)
if _entries.has(key):
_entries.erase(key)
_entries[key] = window
@@ -41,11 +41,20 @@ const DEBOUNCE_DELAY: float = 0.15 # 150ms, §4/§5
const RETRY_DELAY: float = 0.5 # matches atlas_generation_proxy.gd's GEN_RETRY_DELAY
const MAX_RETRIES: int = 20 # ~10s ceiling, matches atlas_generation_proxy.gd's GEN_MAX_RETRIES
## T-1150 struct/key plumbing: this viewer only ever REQUESTS district
## granularity today (requesting quarter is T-1153's job) — these constants
## exist so the cache key / staleness guard below are granularity-aware from
## day one, not bolted on later.
const DEFAULT_GRANULARITY: int = AtlasWindowCache.DISTRICT_GRANULARITY
const DEFAULT_MIN_WL_M: int = 0
var _owner = null # AtlasWindowViewer (untyped to avoid cyclic ref)
var _cache = null # AtlasWindowCache
var _body_id: String = ""
var _center: Vector2i = Vector2i.ZERO
var _n: int = DISTRICT_WINDOW_DEFAULT_N
var _granularity: int = DEFAULT_GRANULARITY
var _min_wl_m: int = DEFAULT_MIN_WL_M
var _pending: bool = false
var _retries: int = 0
var _debounce_timer: Timer = null
@@ -89,9 +98,11 @@ func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEF
_body_id = body_id
_center = center
_n = n
_granularity = DEFAULT_GRANULARITY
_min_wl_m = DEFAULT_MIN_WL_M
_debounce_timer.stop() # a direct request supersedes any pending debounced one
var cached: Variant = _cache.get_window(body_id, center, n)
var cached: Variant = _cache.get_window(body_id, center, n, _granularity, _min_wl_m)
if cached != null:
_pending = false
_retries = 0
@@ -100,7 +111,7 @@ func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEF
_pending = true
_retries = 0
SimBridge.request_atlas_layers(body_id, "Topography", center, n)
SimBridge.request_atlas_layers(body_id, "Topography", center, n, _granularity, _min_wl_m)
## Pan-triggered re-request (§4/§5: "150ms after the last drag-release, not
@@ -112,6 +123,8 @@ func request_debounced(body_id: String, center: Vector2i, n: int = DISTRICT_WIND
_body_id = body_id
_center = center
_n = n
_granularity = DEFAULT_GRANULARITY
_min_wl_m = DEFAULT_MIN_WL_M
_debounce_timer.start()
@@ -122,10 +135,11 @@ func _on_debounce_timeout() -> void:
## Handle an AtlasLayerResponse (routed by the owning viewer from its own
## SimBridge.atlas_layers_received subscription — this object has no signal
## connection of its own, matching atlas_generation_proxy.gd's on_response()
## shape). Ignores responses for a stale body/center/n (the player panned or
## navigated away while a request was in flight) — the echoed center/n IS the
## staleness guard (§2), compared here against what THIS object most recently
## asked for.
## shape). Ignores responses for a stale body/center/n/granularity/min_wl_m
## (the player panned or navigated away while a request was in flight, or a
## different rung's derive answers a request for a different rung, T-1150) —
## the echoed fields ARE the staleness guard (§2, extended T-1150), compared
## here against what THIS object most recently asked for.
func on_response(response: Dictionary) -> void:
if str(response.get("body_id", "")) != _body_id:
return
@@ -150,12 +164,19 @@ func on_response(response: Dictionary) -> void:
var w: Dictionary = window
var echoed_center := _vec_from_center(w.get("center", [0, 0]))
var echoed_n := int(w.get("n", 0))
if echoed_center != _center or echoed_n != _n:
return # stale — answers a window we've since panned away from (§2)
var echoed_granularity := int(w.get("granularity", AtlasWindowCache.DISTRICT_GRANULARITY))
var echoed_min_wl_m := int(w.get("min_wl_m", 0))
if (
echoed_center != _center
or echoed_n != _n
or echoed_granularity != _granularity
or echoed_min_wl_m != _min_wl_m
):
return # stale — answers a window we've since panned away from, or a different rung (§2/T-1150)
_pending = false
_retries = 0
_cache.put(_body_id, _center, _n, w)
_cache.put(_body_id, _center, _n, w, _granularity, _min_wl_m)
window_ready.emit(w)
@@ -164,7 +185,9 @@ func _schedule_retry() -> void:
timer.timeout.connect(
func() -> void:
if _pending:
SimBridge.request_atlas_layers(_body_id, "Topography", _center, _n)
SimBridge.request_atlas_layers(
_body_id, "Topography", _center, _n, _granularity, _min_wl_m
)
)