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
+14 -2
View File
@@ -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)
+15 -1
View File
@@ -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)
+8 -3
View File
@@ -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
+28
View File
@@ -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],
+61
View File
@@ -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
)
)
+38 -12
View File
@@ -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,
}
}
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -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());
+4
View File
@@ -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();
+2
View File
@@ -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");
+3
View File
@@ -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