diff --git a/server/src/global/globals.h b/server/src/global/globals.h index 31823a44..ab228475 100644 --- a/server/src/global/globals.h +++ b/server/src/global/globals.h @@ -12,6 +12,7 @@ #include #include #include "engine/core/word_item.h" +#include "ipc/candidate_render_sync.h" #include "window/candidate_view_model.h" namespace GlobalIme @@ -256,7 +257,10 @@ inline void PublishRenderedCandidatePageGeneration(std::uint64_t generation) { { std::lock_guard lock(candidate_render_mutex); - rendered_candidate_page_generation.store(generation, std::memory_order_release); + rendered_candidate_page_generation.store( + FanyImeIpc::AdvanceRenderedCandidateGeneration( + rendered_candidate_page_generation.load(std::memory_order_relaxed), generation), + std::memory_order_release); } candidate_render_cv.notify_all(); } diff --git a/server/src/ipc/candidate_render_sync.h b/server/src/ipc/candidate_render_sync.h index 9f4bac36..ffac3d35 100644 --- a/server/src/ipc/candidate_render_sync.h +++ b/server/src/ipc/candidate_render_sync.h @@ -36,4 +36,13 @@ constexpr bool ShouldWaitForCandidateRender(std::uint64_t rendered, std::uint64_ } return rendered < current; } + +// The render echo only moves forward. WebView2 echoes from async DOM callbacks, so a content-only +// update's callback can land after a dedup of the same content already echoed the newer generation; +// storing the older one left rendered one behind published and every selection waited out the full +// bound. +constexpr std::uint64_t AdvanceRenderedCandidateGeneration(std::uint64_t rendered, std::uint64_t echoed) noexcept +{ + return echoed > rendered ? echoed : rendered; +} } // namespace FanyImeIpc diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index 13fead4d..e6c53990 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -2492,7 +2492,16 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA { CAND_DIAG_LOGF(L"show folded into in-flight fine-tune caret=({},{})", Global::Point[0], Global::Point[1]); CAND_DIAG_LOGF(L"candidate-frame path=folded-inflight content_gen={}", contentGeneration); - InflateCandWnd(str); + // The in-flight FineTune echoes only the page it captured, which may predate this one. Without + // an echo here a digit/space selection right after a fast burst waits out the full render-sync + // timeout, felt as a stall on commit. + InflateCandWnd(str, [contentGeneration, pageGeneration = candidatePage->generation]() { + if (!::is_global_wnd_cand_shown || contentGeneration != g_candidate_content_generation.load()) + { + return; + } + Global::PublishRenderedCandidatePageGeneration(pageGeneration); + }); KillTimer(hwnd, TIMER_ID_CANDIDATE_MOVE_SETTLE); SetTimer(hwnd, TIMER_ID_CANDIDATE_MOVE_SETTLE, kCandidateMoveSettleMs, nullptr); return 0; diff --git a/server/tests/src/test_candidate_render_sync.cpp b/server/tests/src/test_candidate_render_sync.cpp index b2a9edc2..0e3fdbbc 100644 --- a/server/tests/src/test_candidate_render_sync.cpp +++ b/server/tests/src/test_candidate_render_sync.cpp @@ -22,6 +22,23 @@ TEST_CASE(candidate_render_sync_waits_only_when_a_painted_page_lags_the_publishe REQUIRE(!ShouldWaitForCandidateRender(8, 7, false, true)); } +TEST_CASE(candidate_render_echo_never_moves_backwards) +{ + using FanyImeIpc::AdvanceRenderedCandidateGeneration; + using FanyImeIpc::ShouldWaitForCandidateRender; + + // WebView2: a dedup echoes 332 synchronously, then the content-only DOM callback for the same + // content lands and echoes 331. The stale echo must not pull rendered back behind published. + std::uint64_t rendered = 330; + rendered = AdvanceRenderedCandidateGeneration(rendered, 332); + rendered = AdvanceRenderedCandidateGeneration(rendered, 331); + REQUIRE_EQ(rendered, 332u); + REQUIRE(!ShouldWaitForCandidateRender(rendered, 332, false, true)); + + // A newer echo still advances. + REQUIRE_EQ(AdvanceRenderedCandidateGeneration(332, 333), 333u); +} + TEST_CASE(candidate_render_sync_wait_is_bounded_to_a_visible_stall) { // The bound is a UX contract and a transport constraint: a wedged UI thread may delay a