From f6fc95d0460ce51b8453c63beab4271da159adf7 Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:19:07 +0000 Subject: [PATCH] fix: prevent tile worker starvation on unchanged map model --- lib/tdeck_ui/UI/LXMF/MapScreen.cpp | 4 ++- lib/tdeck_ui/UI/LXMF/MapScreenPresenter.h | 1 + .../build_scripts/test_map_screen_contract.py | 11 ++++++++ tests/native/test_map_screen_presenter.cpp | 27 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp index 1a9508b0..933247e2 100644 --- a/lib/tdeck_ui/UI/LXMF/MapScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/MapScreen.cpp @@ -620,7 +620,9 @@ void MapScreen::updateModel(const Pyxis::MapView::Request& request) { presenter_.recenter(true, request.local_location)) { center_initialized_ = true; } - requests_released_ = false; + if (!presenter_.frameBuiltForCurrentEpoch()) { + requests_released_ = false; + } (void)presenter_.buildFrame(request); unlockState(); } diff --git a/lib/tdeck_ui/UI/LXMF/MapScreenPresenter.h b/lib/tdeck_ui/UI/LXMF/MapScreenPresenter.h index fe9fc3ce..075c05ff 100644 --- a/lib/tdeck_ui/UI/LXMF/MapScreenPresenter.h +++ b/lib/tdeck_ui/UI/LXMF/MapScreenPresenter.h @@ -102,6 +102,7 @@ public: std::uint32_t zoom() const { return zoom_; } std::uint32_t generation() const { return generation_; } std::uint32_t frameEpoch() const { return frame_epoch_; } + bool frameBuiltForCurrentEpoch() const { return frame_built_for_epoch_; } std::size_t requestCount() const { return request_count_; } std::size_t completionCount() const { return completion_count_; } diff --git a/tests/build_scripts/test_map_screen_contract.py b/tests/build_scripts/test_map_screen_contract.py index 6ad16e26..2ea37e2f 100644 --- a/tests/build_scripts/test_map_screen_contract.py +++ b/tests/build_scripts/test_map_screen_contract.py @@ -77,6 +77,17 @@ def test_worker_predecodes_and_render_path_has_no_io(): assert "MAX_COMPLETIONS_PER_TICK = 1" in text(UI / "MapScreen.h") +def test_unchanged_model_does_not_starve_released_tile_requests(): + source = text(UI / "MapScreen.cpp") + presenter = text(UI / "MapScreenPresenter.h") + update = function_body(source, "void MapScreen::updateModel(") + assert "frameBuiltForCurrentEpoch" in presenter + guard = update.index("if (!presenter_.frameBuiltForCurrentEpoch())") + revoke = update.index("requests_released_ = false") + build = update.index("presenter_.buildFrame(request)") + assert guard < revoke < build + + def test_selected_pack_is_the_only_sd_tile_source(): source = text(UI / "MapScreen.cpp") header = text(UI / "MapScreen.h") diff --git a/tests/native/test_map_screen_presenter.cpp b/tests/native/test_map_screen_presenter.cpp index af568122..ac0db32e 100644 --- a/tests/native/test_map_screen_presenter.cpp +++ b/tests/native/test_map_screen_presenter.cpp @@ -67,6 +67,32 @@ void fixedCapacityAndDedupe() { } } +void frameBuildLifecycleKeepsReleasedWorkAvailable() { + MapScreenPresenter presenter; + presenter.show(); + CHECK(!presenter.frameBuiltForCurrentEpoch()); + CHECK(presenter.buildFrame(requestAt(0.0, 0.0, 3)) == + Pyxis::MapView::Result::OK); + CHECK(presenter.frameBuiltForCurrentEpoch()); + + // Rebuilding the unchanged UI model must not revoke the worker's access + // to requests that were released after the first render of this epoch. + CHECK(presenter.buildFrame(requestAt(0.0, 0.0, 3)) == + Pyxis::MapView::Result::OK); + CHECK(presenter.frameBuiltForCurrentEpoch()); + + CHECK(presenter.zoomBy(1)); + CHECK(!presenter.frameBuiltForCurrentEpoch()); + CHECK(presenter.buildFrame(requestAt(0.0, 0.0, 4)) == + Pyxis::MapView::Result::OK); + CHECK(presenter.frameBuiltForCurrentEpoch()); + + presenter.invalidateTiles(); + CHECK(!presenter.frameBuiltForCurrentEpoch()); + presenter.hide(); + CHECK(!presenter.frameBuiltForCurrentEpoch()); +} + void staleCompletionsRejectedAndAcceptedOnce() { MapScreenPresenter presenter; presenter.show(); @@ -285,6 +311,7 @@ void deterministicHundredThousandOperationStress() { int main() { fixedCapacityAndDedupe(); + frameBuildLifecycleKeepsReleasedWorkAvailable(); staleCompletionsRejectedAndAcceptedOnce(); newestFrameReusesSlotsAndPurgesOldRequests(); generationPanZoomAndRecenterBounds();