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:
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user