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:
@@ -432,12 +432,24 @@ func send_named_action(action_name: String, action_data: Variant = null) -> void
|
||||
## not a specific whole-body layer to be cached first). Omitted by every
|
||||
## whole-body-layer caller (show_body()'s existing request), so their wire
|
||||
## traffic is byte-unchanged.
|
||||
##
|
||||
## window_granularity/window_min_wl_m (T-1150): struct/key plumbing for the
|
||||
## zoom-ladder quarter rung — district (0/omitted) stays the default for
|
||||
## every caller in this codebase today; requesting quarter granularity is
|
||||
## T-1153's job, not wired here.
|
||||
func request_atlas_layers(
|
||||
body_id: String, up_to: String = "Topography", window_center: Variant = null, window_n: int = 0
|
||||
body_id: String,
|
||||
up_to: String = "Topography",
|
||||
window_center: Variant = null,
|
||||
window_n: int = 0,
|
||||
window_granularity: int = 0,
|
||||
window_min_wl_m: int = 0
|
||||
) -> void:
|
||||
if test_mode or _bridge == null or state != ConnectionState.CONNECTED:
|
||||
return
|
||||
var bytes := Protocol.encode_atlas_layer_request(body_id, up_to, window_center, window_n)
|
||||
var bytes := Protocol.encode_atlas_layer_request(
|
||||
body_id, up_to, window_center, window_n, window_granularity, window_min_wl_m
|
||||
)
|
||||
if bytes.is_empty():
|
||||
return
|
||||
var err: int = _bridge.send_message(bytes)
|
||||
|
||||
@@ -31,18 +31,32 @@ class_name AtlasMapProtocol
|
||||
## [1, DISTRICT_WINDOW_MAX_N] itself and never trusts the wire value; the
|
||||
## client-side default/cap constants (DISTRICT_WINDOW_DEFAULT_N/MAX_N) live on
|
||||
## the regional-window viewer, not duplicated into the codec.
|
||||
##
|
||||
## `window_granularity`/`window_min_wl_m` (T-1150): the derivation-granularity
|
||||
## axis (district=1/omitted vs. quarter=4) and the octave cutoff, in whole
|
||||
## metres. Both OMITTED (not sent as 0) when at their default — this is
|
||||
## struct/key plumbing only (T-1150 scope): no caller in this codebase
|
||||
## requests quarter granularity yet (that's T-1153); this function just makes
|
||||
## it possible to ask, byte-compatible with every existing caller that
|
||||
## doesn't pass them.
|
||||
static func encode_atlas_layer_request(
|
||||
mp,
|
||||
body_id: String,
|
||||
up_to: String = "Topography",
|
||||
window_center: Variant = null,
|
||||
window_n: int = 0
|
||||
window_n: int = 0,
|
||||
window_granularity: int = 0,
|
||||
window_min_wl_m: int = 0
|
||||
) -> PackedByteArray:
|
||||
var msg := {"body_id": body_id, "up_to": up_to}
|
||||
if window_center != null:
|
||||
var center: Vector2i = window_center
|
||||
msg["window_center"] = [center.x, center.y]
|
||||
msg["window_n"] = window_n
|
||||
if window_granularity != 0:
|
||||
msg["window_granularity"] = window_granularity
|
||||
if window_min_wl_m != 0:
|
||||
msg["window_min_wl_m"] = window_min_wl_m
|
||||
var result = mp.encode(msg)
|
||||
if result.status != null:
|
||||
push_error("Protocol: encode_atlas_layer_request failed: %s" % result.status)
|
||||
|
||||
@@ -780,14 +780,19 @@ static func encode_request_bookmark_catalog() -> PackedByteArray:
|
||||
## windowed district-resolution regional-map query — see
|
||||
## atlas_map_protocol.gd's encode_atlas_layer_request doc for the wire shape.
|
||||
## Omitted callers (every whole-body-layer call site predating T-1138) are
|
||||
## byte-unchanged.
|
||||
## byte-unchanged. window_granularity/window_min_wl_m (T-1150): same
|
||||
## byte-compatibility contract, see atlas_map_protocol.gd.
|
||||
static func encode_atlas_layer_request(
|
||||
body_id: String,
|
||||
up_to: String = "Topography",
|
||||
window_center: Variant = null,
|
||||
window_n: int = 0
|
||||
window_n: int = 0,
|
||||
window_granularity: int = 0,
|
||||
window_min_wl_m: int = 0
|
||||
) -> PackedByteArray:
|
||||
return _amp().encode_atlas_layer_request(_mp(), body_id, up_to, window_center, window_n)
|
||||
return _amp().encode_atlas_layer_request(
|
||||
_mp(), body_id, up_to, window_center, window_n, window_granularity, window_min_wl_m
|
||||
)
|
||||
|
||||
|
||||
## Decode an AtlasLayerResponse (#969, D-225). Returns a Dictionary
|
||||
|
||||
Binary file not shown.
@@ -214,6 +214,32 @@ func test_encode_atlas_layer_request_carries_window_params() -> void:
|
||||
assert_that(decoded.value.get("window_n")).is_equal(32)
|
||||
|
||||
|
||||
## T-1150: window_granularity/window_min_wl_m are OMITTED (not sent as 0)
|
||||
## when at their default — a windowed request that doesn't pass them (every
|
||||
## pre-T-1150 window caller) is byte-identical to pre-T-1150 wire traffic,
|
||||
## same contract as window_center/window_n's own default-omission above.
|
||||
func test_encode_atlas_layer_request_omits_granularity_and_min_wl_by_default() -> void:
|
||||
var bytes := Protocol.encode_atlas_layer_request(
|
||||
"GJ1c", "Topography", Vector2i(140, 260), 32
|
||||
)
|
||||
var decoded = Messagepack.decode(bytes)
|
||||
assert_bool(decoded.value.has("window_granularity")).is_false()
|
||||
assert_bool(decoded.value.has("window_min_wl_m")).is_false()
|
||||
|
||||
|
||||
## T-1150: a quarter-granularity request with an octave cutoff carries both
|
||||
## new fields verbatim, unclamped (the server owns
|
||||
## resolve_window_granularity()/clamp_window_n() — never trusted from the
|
||||
## wire, same posture as window_n).
|
||||
func test_encode_atlas_layer_request_carries_granularity_and_min_wl() -> void:
|
||||
var bytes := Protocol.encode_atlas_layer_request(
|
||||
"GJ1c", "Topography", Vector2i(140, 260), 32, 4, 512
|
||||
)
|
||||
var decoded = Messagepack.decode(bytes)
|
||||
assert_that(decoded.value.get("window_granularity")).is_equal(4)
|
||||
assert_that(decoded.value.get("window_min_wl_m")).is_equal(512)
|
||||
|
||||
|
||||
## §2: district_window is a distinct payload (echoes center/n for the
|
||||
## client's staleness guard) but the codec passthrough is the same shape as
|
||||
## every sibling layer — raw.get(), no reshaping. Field types follow the
|
||||
@@ -224,6 +250,8 @@ func test_atlas_response_district_window_passthrough() -> void:
|
||||
var window := {
|
||||
"center": [140, 260],
|
||||
"n": 32,
|
||||
"granularity": 4, # T-1150: quarter granularity, passed through same as every other field
|
||||
"min_wl_m": 512,
|
||||
"morphology": PackedByteArray([8, 14, 0, 5]),
|
||||
"elev_q": PackedByteArray([40, 62, 5, 88]),
|
||||
"temp_dc": [120, 95, AtlasOverlayColors.REGION_TEMP_NONE_DC, 60],
|
||||
|
||||
@@ -119,3 +119,64 @@ func test_clear_empties_the_cache() -> void:
|
||||
cache.clear()
|
||||
assert_int(cache.size()).is_equal(0)
|
||||
assert_bool(cache.has("GJ1c", Vector2i(0, 0), 32)).is_false()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# granularity / min_wl_m (T-1150, zoom ladder design doc §3 aliasing risk)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## **MANDATORY aliasing regression (client half, T-1150):** a granularity-4
|
||||
## (quarter) key and a granularity-1 (district) key at the IDENTICAL
|
||||
## (body_id, center, n) must be DISTINCT cache keys — this is what prevents a
|
||||
## quarter-spacing request from silently reading (or overwriting) a
|
||||
## district-spacing window's cache entry, and vice versa.
|
||||
func test_make_key_distinguishes_granularity_at_identical_body_center_n() -> void:
|
||||
var k_district := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY
|
||||
)
|
||||
var k_quarter := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 4)
|
||||
assert_str(k_district).is_not_equal(k_quarter)
|
||||
|
||||
|
||||
## Same aliasing risk, the other new axis: two requests identical except for
|
||||
## `min_wl_m` (the octave cutoff) must not collide either — different cutoffs
|
||||
## are different derived payloads (T-1149/T-1150).
|
||||
func test_make_key_distinguishes_min_wl_m_at_identical_body_center_n_granularity() -> void:
|
||||
var k_uncut := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0
|
||||
)
|
||||
var k_cut := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 512
|
||||
)
|
||||
assert_str(k_uncut).is_not_equal(k_cut)
|
||||
|
||||
|
||||
## Omitting granularity/min_wl_m (every pre-T-1150 call site) must produce the
|
||||
## SAME key as passing the explicit district/no-cutoff defaults — byte/string
|
||||
## compatibility for existing callers, not just "doesn't crash".
|
||||
func test_omitted_granularity_and_min_wl_m_match_explicit_district_defaults() -> void:
|
||||
var k_omitted := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
|
||||
var k_explicit := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0
|
||||
)
|
||||
assert_str(k_omitted).is_equal(k_explicit)
|
||||
|
||||
|
||||
## End-to-end through put()/get_window()/has() (not just make_key() in
|
||||
## isolation): a quarter-granularity window and a district-granularity window
|
||||
## at the identical (body, center, n) must both be independently retrievable,
|
||||
## neither one clobbering or masking the other.
|
||||
func test_district_and_quarter_windows_coexist_at_identical_body_center_n() -> void:
|
||||
var cache := AtlasWindowCache.new()
|
||||
var district_window := {"granularity": AtlasWindowCache.DISTRICT_GRANULARITY, "id": "district"}
|
||||
var quarter_window := {"granularity": 4, "id": "quarter"}
|
||||
|
||||
cache.put("GJ1c", Vector2i(10, 20), 32, district_window, AtlasWindowCache.DISTRICT_GRANULARITY)
|
||||
cache.put("GJ1c", Vector2i(10, 20), 32, quarter_window, 4)
|
||||
|
||||
assert_int(cache.size()).is_equal(2)
|
||||
assert_that(
|
||||
cache.get_window("GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY)
|
||||
).is_equal(district_window)
|
||||
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32, 4)).is_equal(quarter_window)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -203,10 +203,17 @@ pub enum GenWorkItem {
|
||||
/// reason `AnalyzeBody.body_params` is boxed.
|
||||
body_params: Box<BodyParams>,
|
||||
/// Window centre + side length in districts. `n` is ALREADY clamped to
|
||||
/// `[1, DISTRICT_WINDOW_MAX_N]` by the caller (`handle_atlas_request`)
|
||||
/// `[1, DISTRICT_WINDOW_MAX_N]` AND the granularity-aware
|
||||
/// `WIRE_CAP_CELLS` ceiling by the caller (`handle_atlas_request`)
|
||||
/// before this item is built — never trusted from the wire again here.
|
||||
center: DistrictPos,
|
||||
n: u32,
|
||||
/// Derivation granularity (T-1150) — `WINDOW_GRANULARITY_DISTRICT` (1)
|
||||
/// or `WINDOW_GRANULARITY_QUARTER` (4). Already resolved via
|
||||
/// `resolve_window_granularity` by the caller.
|
||||
granularity: u32,
|
||||
/// Octave cutoff in whole metres (T-1149/T-1150), `0` = no cutoff.
|
||||
min_wl_m: u32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -218,14 +225,23 @@ impl GenWorkItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Coalescing key for `DeriveWindow` items only — `(connection, body)`.
|
||||
/// Coalescing key for `DeriveWindow` items only — `(connection, body,
|
||||
/// granularity)` (T-1150, design doc §3 [SOFT] recommendation, extending
|
||||
/// T-1137's `(connection, body)`). `granularity` is part of the key so an
|
||||
/// in-flight district-spacing (granularity 1) pan-burst is never
|
||||
/// superseded by an unrelated quarter-spacing (granularity 4) request for
|
||||
/// the same connection+body, and vice versa — the two rungs are separate
|
||||
/// in-flight derives, not competing updates to the same one.
|
||||
/// `None` for every other variant (they don't coalesce this way).
|
||||
pub fn window_supersede_key(&self) -> Option<(ConnectionId, &str)> {
|
||||
pub fn window_supersede_key(&self) -> Option<(ConnectionId, &str, u32)> {
|
||||
if let GenWorkItem::DeriveWindow {
|
||||
body_id, conn_id, ..
|
||||
body_id,
|
||||
conn_id,
|
||||
granularity,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
Some((*conn_id, body_id))
|
||||
Some((*conn_id, body_id, *granularity))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -403,11 +419,15 @@ impl GenerationQueue {
|
||||
}
|
||||
|
||||
/// Submit a `DeriveWindow` item with per-connection coalescing (D-226
|
||||
/// T-1124 amendment §1, "recommended"): if a `DeriveWindow` item for the
|
||||
/// SAME `(connection, body)` is still sitting in the pending queue
|
||||
/// (not yet dispatched to a Rayon worker), it is replaced in place by the
|
||||
/// new one — a pan-burst that queues several window requests for the same
|
||||
/// connection+body before the first is dispatched collapses to one derive.
|
||||
/// T-1124 amendment §1, "recommended"; extended T-1150 to key on
|
||||
/// granularity too): if a `DeriveWindow` item for the SAME `(connection,
|
||||
/// body, granularity)` is still sitting in the pending queue (not yet
|
||||
/// dispatched to a Rayon worker), it is replaced in place by the new one
|
||||
/// — a pan-burst that queues several window requests for the same
|
||||
/// connection+body+granularity before the first is dispatched collapses
|
||||
/// to one derive. A district-spacing and quarter-spacing request for the
|
||||
/// same connection+body do NOT coalesce with each other — they're
|
||||
/// separate in-flight derives, not competing updates to the same rung.
|
||||
///
|
||||
/// Deliberately does **not** attempt to cancel an item already dispatched
|
||||
/// to a Rayon worker (no cancellation channel exists, and the amendment
|
||||
@@ -419,12 +439,12 @@ impl GenerationQueue {
|
||||
/// `window_supersede_key()` returns `None`).
|
||||
pub fn submit_window(&self, item: GenWorkItem, priority: GenPriority) {
|
||||
if let Some(key) = item.window_supersede_key() {
|
||||
let key = (key.0, key.1.to_string());
|
||||
let key = (key.0, key.1.to_string(), key.2);
|
||||
let mut pending = self.pending.lock().unwrap();
|
||||
pending.retain(|q| {
|
||||
q.item
|
||||
.window_supersede_key()
|
||||
.map(|k| (k.0, k.1.to_string()) != key)
|
||||
.map(|k| (k.0, k.1.to_string(), k.2) != key)
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let pos = pending
|
||||
@@ -747,6 +767,8 @@ fn run_work_item(
|
||||
body_params,
|
||||
center,
|
||||
n,
|
||||
granularity,
|
||||
min_wl_m,
|
||||
} => match load_heightmap_png(heightmap_path, body_id, *sea_level) {
|
||||
Ok(hm) => {
|
||||
// Same GRID_W×GRID_H downsample AnalyzeBody applies (D-202) — the
|
||||
@@ -780,6 +802,8 @@ fn run_work_item(
|
||||
*center,
|
||||
*n,
|
||||
&climate,
|
||||
*granularity,
|
||||
*min_wl_m,
|
||||
);
|
||||
GenCompletion::WindowDerived {
|
||||
body_id: body_id.clone(),
|
||||
@@ -1137,6 +1161,8 @@ mod tests {
|
||||
}),
|
||||
center,
|
||||
n: 4,
|
||||
granularity: crate::atlas::layer_proxy::WINDOW_GRANULARITY_DISTRICT,
|
||||
min_wl_m: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+753
-68
File diff suppressed because it is too large
Load Diff
@@ -413,7 +413,10 @@ fn drain_generation_completions(
|
||||
// NEXT poll (the existing D-225 re-request loop) hits
|
||||
// `handle_atlas_request`'s window branch, which finds this
|
||||
// entry via `DistrictWindowCache::get` and serves it.
|
||||
window_cache.insert((body_id, layer.center, layer.n), *layer);
|
||||
window_cache.insert(
|
||||
(body_id, layer.center, layer.n, layer.granularity, layer.min_wl_m),
|
||||
*layer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -904,6 +907,8 @@ mod tests {
|
||||
up_to: CascadeLayer::Topography,
|
||||
window_center: None,
|
||||
window_n: 0,
|
||||
window_granularity: 0,
|
||||
window_min_wl_m: 0,
|
||||
},
|
||||
)]));
|
||||
world.insert_resource(AtlasResponseBuffer::default());
|
||||
|
||||
@@ -1201,6 +1201,8 @@ mod inbound_tests {
|
||||
up_to: CascadeLayer::Topography,
|
||||
window_center: None,
|
||||
window_n: 0,
|
||||
window_granularity: 0,
|
||||
window_min_wl_m: 0,
|
||||
};
|
||||
let frame = rmp_serde::to_vec_named(&req).unwrap();
|
||||
assert!(
|
||||
@@ -1246,6 +1248,8 @@ mod inbound_tests {
|
||||
up_to: CascadeLayer::Topography,
|
||||
window_center: None,
|
||||
window_n: 0,
|
||||
window_granularity: 0,
|
||||
window_min_wl_m: 0,
|
||||
})
|
||||
.unwrap();
|
||||
let star_map_frame = rmp_serde::to_vec_named(&StarMapRequest { star_map: true }).unwrap();
|
||||
|
||||
@@ -371,6 +371,8 @@ fn single_tick_drains_all_ready_inbound_frames() {
|
||||
up_to: CascadeLayer::Topography,
|
||||
window_center: None,
|
||||
window_n: 0,
|
||||
window_granularity: 0,
|
||||
window_min_wl_m: 0,
|
||||
};
|
||||
let payload = rmp_serde::to_vec_named(&req).expect("failed to serialize");
|
||||
write_framed(&mut stream, &payload).expect("write atlas frame");
|
||||
|
||||
@@ -7,6 +7,7 @@ use settled_reach_server::atlas::layer_proxy::{
|
||||
AtlasLayerResponse, AtlasLayerStatus, DistrictWindowLayer, QuarterFootprintEntry,
|
||||
QuarterFootprintLayer, RegionGridLayer, RoadGraphEdge, RoadGraphLayer, RoadGraphNode,
|
||||
SettlementEntry, SettlementLayer, SettlementSizeClass, REGION_TEMP_NONE_DC,
|
||||
WINDOW_GRANULARITY_DISTRICT,
|
||||
};
|
||||
use settled_reach_server::atlas::region_profile::{SeasonPhase, WeatherState};
|
||||
use settled_reach_server::atlas::road_graph::RoadNodeKind;
|
||||
@@ -725,6 +726,8 @@ fn generate_atlas_layer_response_fixtures() {
|
||||
let window = DistrictWindowLayer {
|
||||
center: (10, -5),
|
||||
n: 2,
|
||||
granularity: WINDOW_GRANULARITY_DISTRICT,
|
||||
min_wl_m: 0,
|
||||
morphology: vec![0, 8, 14, 16], // OpenOcean, AlluvialPlain, Alpine, Wetland
|
||||
elev_q: vec![0, 45, 98, 60],
|
||||
temp_dc: vec![205, 150, REGION_TEMP_NONE_DC, 80], // 20.5°C, 15.0°C, airless sentinel, 8.0°C
|
||||
|
||||
Reference in New Issue
Block a user