fix(client): T-1153 round 6 — zoom floor at the canonical frame; reset guard purely edge-triggered
The round-5 per-tick zoom comparison inside the reset guard was level- triggered: at the canonical frame every continued zoom-out tick drifted _view_zoom a hair below fit, read as 'not there', and re-fired enter_orbital — tile set torn down/recreated per tick, in-flight responses orphaned (black entry), server flooded (889 of 897 responses in one drive phase). The drift concern now lives in _zoom_at as a ZOOM FLOOR (once at the canonical frame, zoom-out clamps at fit); the guard keys only on fields that never drift (_held_center/_held_granularity_v2/ _tile_mode) — genuinely edge-triggered. Three shared helpers keep the guard and the floor on one definition of 'canonical frame'. Anti-storm regression drives 100 real _zoom_at ticks past the first reset and asserts tile-set child node identities never change; the black-entry repro interleaves Lendel's six wire-accurate tile responses with continued ticks and asserts all six held. Both revert-verified with the exact live symptoms. Round-5 reset tests pass unchanged. Live- verified twice: exactly 6 requests at entry and at reset, no storm.
This commit is contained in:
@@ -372,12 +372,16 @@ func test_reset_after_crossing_out_and_back_snaps_to_the_canonical_fit_zoom() ->
|
||||
## zoom (0.004, chosen below the fit zoom) regardless of the reset. This
|
||||
## test reproduces that shape directly: continue zooming out PAST the point
|
||||
## where the reset first re-enters tile mode, all the way to a target zoom
|
||||
## BELOW the fit value. Before the second live-round-5 fix, `_view_zoom`
|
||||
## drifted back down from the fit value on every subsequent `_zoom_at()`
|
||||
## tick (ordinary multiplicative scaling doesn't care that a reset just
|
||||
## happened) while the mode/center/granularity guard read "already
|
||||
## canonical" and silently let it drift, landing on whatever the LOOP's
|
||||
## target zoom happened to be instead of the fit value.
|
||||
## BELOW the fit value. Before the round-5 fix, `_view_zoom` drifted back
|
||||
## down from the fit value on every subsequent `_zoom_at()` tick while the
|
||||
## mode/center/granularity guard read "already canonical" and silently let
|
||||
## it drift, landing on whatever the LOOP's target zoom happened to be
|
||||
## instead of the fit value. **Live round 6 update:** the MECHANISM that
|
||||
## now holds this assertion changed — `_zoom_at()`'s own zoom FLOOR (not a
|
||||
## re-firing reset) is what keeps `_view_zoom` pinned at fit through
|
||||
## continued zoom-out ticks; see `_maybe_reset_to_canonical_frame()`'s own
|
||||
## doc for why re-firing on every tick caused a request storm. This test's
|
||||
## own assertions are unchanged — only the doc below was updated to match.
|
||||
func test_reset_resnaps_even_after_continued_zoom_out_past_the_first_reset() -> void:
|
||||
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
||||
add_child(v)
|
||||
@@ -420,14 +424,175 @@ func test_reset_resnaps_even_after_continued_zoom_out_past_the_first_reset() ->
|
||||
(
|
||||
"post-reset _view_zoom (%.6f) must equal the canonical fit zoom (%.6f) even"
|
||||
+ " though the wheel gesture continued past the point where the reset first"
|
||||
+ " fired (target was %.6f, BELOW the fit zoom) — the guard must re-fire on"
|
||||
+ " every subsequent tick where the zoom has drifted away from the fit value,"
|
||||
+ " not just once"
|
||||
+ " fired (target was %.6f, BELOW the fit zoom) — _zoom_at()'s own zoom floor"
|
||||
+ " must keep pinning it at fit through every subsequent tick, not just once"
|
||||
)
|
||||
% [v._view_zoom, fit_zoom, target_zoom]
|
||||
).is_equal_approx(fit_zoom, 0.000001)
|
||||
|
||||
|
||||
## Live round 6's ANTI-STORM test — the exact repro the coordinator's live
|
||||
## drive caught: drive a REAL continued zoom-out gesture (via `_zoom_at()`,
|
||||
## the same call path the live drive uses — NOT calling
|
||||
## `_maybe_reset_to_canonical_frame()` directly with unchanged state, which
|
||||
## trivially can't reproduce the drift the storm depends on) many ticks past
|
||||
## the point where the reset first fires — asserts ZERO additional tile-set
|
||||
## entries occur across the WHOLE gesture. Spies on `AtlasWindowTileSet`'s
|
||||
## own child `AtlasWindowRequest` node INSTANCES (captured right after the
|
||||
## FIRST reset) — a fresh `enter_orbital()` call tears down (`queue_free()`s)
|
||||
## every one of them and creates BRAND NEW ones, so "the same node
|
||||
## instances are still alive and still the tile set's children after 100
|
||||
## more ticks" is a direct, non-invasive proxy for "the reset never fired
|
||||
## again" — no new production instrumentation needed. Before the round-6
|
||||
## fix, `_zoom_at()`'s continued multiplicative zoom-out drifted `_view_zoom`
|
||||
## below fit on every subsequent tick, the level-triggered guard read "not
|
||||
## already there" every time, and `enter_orbital()` fired repeatedly:
|
||||
## tearing down and recreating the tile set (and its 6 request nodes) every
|
||||
## tick — exactly the "889 of 897 wire responses arrived during one
|
||||
## zoom-out phase" storm.
|
||||
func test_reset_evaluated_repeatedly_at_canonical_frame_issues_zero_additional_requests() -> void:
|
||||
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
||||
add_child(v)
|
||||
v.size = Vector2(1600.0, 900.0)
|
||||
var radius_km := 6238.4 # GJ380c (Lendel) — a tiling body, the live-repro shape
|
||||
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
|
||||
assert_bool(v.is_tile_mode()).is_true()
|
||||
|
||||
# Zoom IN far enough to leave tile mode, then zoom back OUT past the
|
||||
# first reset — same shape as the round-5 continued-zoom-out test, but
|
||||
# this time spying on the tile set across the WHOLE remaining gesture
|
||||
# instead of only checking the final zoom value.
|
||||
var cursor_pos := Vector2(1100.0, 300.0) # matches tmp_drive_ladder.gd's own aim point
|
||||
for _i in range(60):
|
||||
v._zoom_at(cursor_pos, 1.15)
|
||||
if not v.is_tile_mode():
|
||||
break
|
||||
assert_bool(v.is_tile_mode()).is_false()
|
||||
|
||||
for _i in range(200):
|
||||
v._zoom_at(cursor_pos, 1.0 / 1.05)
|
||||
if v.is_tile_mode():
|
||||
break
|
||||
assert_bool(v.is_tile_mode()).override_failure_message(
|
||||
"sanity: the first reset must have fired before spying on the tile set"
|
||||
).is_true()
|
||||
|
||||
var tile_set = v.get_tile_set()
|
||||
var original_requests: Array = tile_set.get_children()
|
||||
assert_int(original_requests.size()).override_failure_message(
|
||||
"sanity: the first reset must have created real tile-request child nodes to spy on"
|
||||
).is_greater(0)
|
||||
|
||||
# Continue the SAME zoom-out gesture 100 MORE ticks past the first
|
||||
# reset — a real wheel gesture has no way to stop exactly at the reset
|
||||
# point, and holding the wheel down (or residual scroll momentum) keeps
|
||||
# sending ticks. None of these must tear down/recreate the tile set.
|
||||
for _i in range(100):
|
||||
v._zoom_at(cursor_pos, 1.0 / 1.05)
|
||||
|
||||
var current_requests: Array = tile_set.get_children()
|
||||
assert_int(current_requests.size()).override_failure_message(
|
||||
"the tile set's child count must be unchanged after 100 more continued"
|
||||
+ " zoom-out ticks — a changed count means teardown/recreate happened"
|
||||
).is_equal(original_requests.size())
|
||||
for i in range(original_requests.size()):
|
||||
assert_bool(is_instance_valid(original_requests[i])).override_failure_message(
|
||||
"original tile-request node #%d must still be alive — a storm would have"
|
||||
+ " queue_free()'d it and created a fresh one" % i
|
||||
).is_true()
|
||||
assert_bool(is_same(original_requests[i], current_requests[i])).override_failure_message(
|
||||
(
|
||||
"tile-request node #%d must be the SAME instance as right after the"
|
||||
+ " first reset — a different object at the same index means the tile"
|
||||
+ " set was torn down and recreated (a storm), even if the count"
|
||||
+ " coincidentally matches"
|
||||
)
|
||||
% i
|
||||
).is_true()
|
||||
|
||||
|
||||
## Live round 6's BLACK-ENTRY repro: enter_orbital(), then deliver the six
|
||||
## wire-accurate tile responses WHILE a REAL continued zoom-out gesture (via
|
||||
## `_zoom_at()`, matching the live drive's actual input shape — a held
|
||||
## wheel-down keeps sending ticks concurrently with responses streaming in
|
||||
## from the server) is in flight — asserts all six are accepted and HELD
|
||||
## (tile set stable throughout, no teardown between delivery and the final
|
||||
## assertion). Before the round-6 fix, the level-triggered guard fired on
|
||||
## every zoom-out tick once `_view_zoom` drifted below fit, tearing down the
|
||||
## tile set mid-delivery and orphaning responses addressed to now-freed
|
||||
## request nodes — nothing ever accumulated, and the mosaic stayed black
|
||||
## even though the server dutifully answered every request.
|
||||
func test_six_tile_responses_survive_concurrent_reset_evaluation_and_are_held() -> void:
|
||||
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
||||
add_child(v)
|
||||
v.size = Vector2(1600.0, 900.0)
|
||||
var radius_km := 6238.4 # GJ380c (Lendel) — 6 tiles, the live-repro shape
|
||||
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
|
||||
assert_bool(v.is_tile_mode()).is_true()
|
||||
|
||||
var tile_set = v.get_tile_set()
|
||||
var tiles: Array = tile_set.get_tiles()
|
||||
assert_int(tiles.size()).override_failure_message(
|
||||
"sanity: Lendel must produce Lendel's own real tile count (6) for this"
|
||||
+ " repro to be faithful, not a smaller synthetic count"
|
||||
).is_equal(6)
|
||||
|
||||
# Same continued zoom-out gesture as the anti-storm test above — leave
|
||||
# tile mode, cross back into it (the first reset), then KEEP sending
|
||||
# zoom-out ticks (a real held wheel has no way to stop exactly at the
|
||||
# reset point). Responses are delivered interleaved with these ticks,
|
||||
# exactly matching the live drive's concurrent shape.
|
||||
var cursor_pos := Vector2(1100.0, 300.0)
|
||||
for _i in range(60):
|
||||
v._zoom_at(cursor_pos, 1.15)
|
||||
if not v.is_tile_mode():
|
||||
break
|
||||
assert_bool(v.is_tile_mode()).is_false()
|
||||
for _i in range(200):
|
||||
v._zoom_at(cursor_pos, 1.0 / 1.05)
|
||||
if v.is_tile_mode():
|
||||
break
|
||||
assert_bool(v.is_tile_mode()).override_failure_message(
|
||||
"sanity: the first reset must have fired before delivering responses"
|
||||
).is_true()
|
||||
|
||||
for i in range(tiles.size()):
|
||||
var center: Vector2i = tiles[i]["center"]
|
||||
var tile_window: Dictionary = {
|
||||
"center": [center.x, center.y],
|
||||
"n": AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION,
|
||||
"granularity": SERVER_LEGACY_GRANULARITY_REGION_SENTINEL,
|
||||
"granularity_v2": "Region",
|
||||
"morphology": PackedByteArray([8, 14, 0, 1]),
|
||||
"elev_q": PackedByteArray([40, 90, 5, 60]),
|
||||
"temp_dc": [120, 95, -32768, 60],
|
||||
"moisture_q": PackedByteArray([50, 30, 90, 20]),
|
||||
"vegetation": PackedByteArray([2, 1, 6, 3]),
|
||||
"glaciation": PackedByteArray([0, 0, 1, 2]),
|
||||
}
|
||||
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", tile_window))
|
||||
# Interleave several MORE continued zoom-out ticks, matching the live
|
||||
# drive's per-frame cadence — none of these must tear anything down.
|
||||
for _tick in range(5):
|
||||
v._zoom_at(cursor_pos, 1.0 / 1.05)
|
||||
|
||||
var final_tiles: Array = tile_set.get_tiles()
|
||||
assert_int(final_tiles.size()).override_failure_message(
|
||||
"the tile set must still have all 6 tile slots — a storm mid-delivery"
|
||||
+ " would have torn it down and rebuilt it with fresh (unfulfilled) slots"
|
||||
).is_equal(6)
|
||||
for i in range(final_tiles.size()):
|
||||
assert_that(final_tiles[i]["window"]).override_failure_message(
|
||||
(
|
||||
"tile #%d's window must be HELD (non-null) — all six wire-accurate"
|
||||
+ " responses delivered during a concurrent continued zoom-out gesture"
|
||||
+ " must survive to be accepted, not be silently dropped by an"
|
||||
+ " orphaning teardown"
|
||||
)
|
||||
% i
|
||||
).is_not_null()
|
||||
|
||||
|
||||
## Not fully zoomed out (a normal District-rung view) must NOT trigger the
|
||||
## reset — only reaching the top of the ladder resets, not every zoom step.
|
||||
func test_reset_to_canonical_frame_does_not_fire_when_not_fully_zoomed_out() -> void:
|
||||
|
||||
Reference in New Issue
Block a user