// altsnap_jitter_test — automated regression tests for the multi-viewport // jitter that AltSnap (and other Win32 window movers) trigger on the C++ // app shell. The harness runs two phases inside one fn::run_app session: // // Phase 1 — Sync test (cross-platform). // Drives the OS window with glfwSetWindowPos every frame and asserts // ImGui's main viewport->Pos tracks the actual OS pos within 1px. This // catches the original "ImGui Pos lags 1 frame, UpdatePlatformWindows // reapplies stale value" bug fixed by the GLFW pos callback in // cpp/framework/app_base.cpp. // // Phase 2 — AltSnap simulation on the MAIN window (Windows-only). // A worker thread reproduces AltSnap's exact wire format on the main // HWND: // 1. PostMessage(hwnd, WM_ENTERSIZEMOVE) — fake bracket open // 2. Burst of SetWindowPos(SWP_ASYNCWINDOWPOS) — async drag steps // 3. PostMessage(hwnd, WM_EXITSIZEMOVE) — fake bracket close // During the bracket the framework's WndProc subclass must observe // ENTERSIZEMOVE and the main loop must STOP rendering / swapping // buffers (mirroring native title-bar drag, where DefWindowProc blocks // the app thread). The test counts render() invocations during the // bracket: zero = pass, anything >0 = the fix didn't engage and the // visible "grab and release" jitter would still appear. // // Phase 3 — AltSnap simulation on a SECONDARY viewport (Windows-only). // With ConfigViewportsNoAutoMerge ON we force ImGui to allocate a // dedicated platform window (HWND) for a panel positioned outside the // main viewport, exactly like the user dragging a tab out of the // dockspace. The same WM_ENTERSIZEMOVE / SetWindowPos burst / // WM_EXITSIZEMOVE sequence is then sent to that secondary HWND. The // framework must subclass every viewport HWND (not just main) so the // bracket is observed and rendering pauses globally — otherwise the // "grab and release" jitter the user reported when AltSnap moves a // floating panel would still appear. Pass criteria mirror Phase 2. // // Linux/WSL: only Phase 1 runs. With xvfb the WM honors glfwSetWindowPos, // so the sync test is meaningful. Phases 2 and 3 return immediately. // // Windows (target): cross-compiled via build_cpp_windows, deployed to the // Desktop apps dir, launched via cmd.exe; this is where AltSnap lives, so // any Phase 2 failure here is the real regression. #include "app_base.h" #include "imgui.h" #include #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #define GLFW_EXPOSE_NATIVE_WIN32 #include #include #endif #include #include #include #include #include namespace { // ----- Phase 1 (sync test) tunables ----- constexpr int kWarmupFrames = 8; constexpr int kSyncFrames = 60; constexpr int kStepPx = 4; constexpr int kBaseX = 200; constexpr int kBaseY = 200; constexpr float kSyncTolerance = 1.0f; constexpr int kClampTolerance = 6; // ----- Phase 2 (AltSnap sim) tunables ----- constexpr int kAltSnapSteps = 30; // SetWindowPos calls in the burst constexpr int kAltSnapStepMs = 16; // delay between calls (~60 Hz) constexpr int kAltSnapStepPx = 6; constexpr int kAltSnapBaseX = 500; constexpr int kAltSnapBaseY = 200; // ----- shared state ----- GLFWwindow* g_window = nullptr; int g_frame = 0; // Phase 1 metrics int g_p1_max_sync = 0; int g_p1_max_clamp = 0; int g_p1_bad_sync = 0; int g_p1_bad_clamp = 0; bool g_p1_done = false; // Phase 2 state std::atomic g_render_counter{0}; // bumped every render() call std::atomic g_p2_started{false}; std::atomic g_p2_done{false}; int g_p2_renders_before = 0; int g_p2_renders_during = 0; int g_p2_renders_after = 0; bool g_p2_skipped = false; // ----- Phase 3 (secondary viewport AltSnap sim) tunables ----- constexpr int kP3InitFrames = 30; // frames waiting for the platform window to materialize constexpr int kP3SecondaryX = 1400; // outside the 800x600 main viewport constexpr int kP3SecondaryY = 200; constexpr int kP3SecondaryW = 320; constexpr int kP3SecondaryH = 220; // Phase 3 state std::atomic g_p3_started{false}; std::atomic g_p3_done{false}; int g_p3_init_frames = 0; int g_p3_renders_before = 0; int g_p3_renders_during = 0; int g_p3_renders_after = 0; bool g_p3_skipped = false; bool g_p3_show_window = true; // when true, render() draws the floating panel #ifdef _WIN32 HWND g_p3_secondary_hwnd = nullptr; #endif // ----- Phase 4 (iconified main, floating panels must survive) ----- constexpr int kP4SettleFrames = 20; // frames between each step of the dance std::atomic g_p4_started{false}; std::atomic g_p4_done{false}; int g_p4_step = 0; int g_p4_renders_iconified = 0; bool g_p4_secondary_alive_before = false; bool g_p4_secondary_alive_during = false; bool g_p4_secondary_alive_after = false; bool g_p4_skipped = false; // ----- Phase 5 (Alt + RMB triggers native resize modal) ----- constexpr int kP5SettleFrames = 20; std::atomic g_p5_started{false}; std::atomic g_p5_done{false}; int g_p5_step = 0; int g_p5_alt_resize_count_before = 0; int g_p5_alt_resize_count_after = 0; int g_p5_sizemove_enter_before = 0; int g_p5_sizemove_enter_after = 0; bool g_p5_skipped = false; // ----- Phase 6 (Alt + LMB triggers native move modal) ----- constexpr int kP6SettleFrames = 20; std::atomic g_p6_started{false}; std::atomic g_p6_done{false}; int g_p6_step = 0; int g_p6_alt_move_before = 0; int g_p6_alt_move_after = 0; bool g_p6_skipped = false; #ifdef _WIN32 void altsnap_worker(HWND hwnd) { // Wait briefly so the render loop is firmly running and the test app's // counters are stable before we start counting. Sleep(50); g_p2_renders_before = g_render_counter.load(std::memory_order_acquire); // Open the fake sizemove bracket. AltSnap uses PostMessage so the // target thread processes the message in its own pump. SendMessage // would re-enter and is harder to reason about. PostMessageW(hwnd, WM_ENTERSIZEMOVE, 0, 0); // Give the target thread one tick to observe the bracket open BEFORE // we measure the "during" baseline. Sleep(kAltSnapStepMs); int during_baseline = g_render_counter.load(std::memory_order_acquire); // Burst SetWindowPos calls exactly like AltSnap's MoveResizeWindowNow_ // path: SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOACTIVATE // | SWP_ASYNCWINDOWPOS. The async flag is the key — calls return // immediately and the moves arrive on the target's queue. UINT flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS; for (int i = 0; i < kAltSnapSteps; ++i) { int x = kAltSnapBaseX + i * kAltSnapStepPx; int y = kAltSnapBaseY; SetWindowPos(hwnd, nullptr, x, y, 0, 0, flags); Sleep(kAltSnapStepMs); } int during_after = g_render_counter.load(std::memory_order_acquire); g_p2_renders_during = during_after - during_baseline; // Close the fake bracket. The framework should clear in_sizemove and // resume rendering on the next iteration of its main loop. PostMessageW(hwnd, WM_EXITSIZEMOVE, 0, 0); // Give the target thread time to render a few frames after the close. Sleep(200); g_p2_renders_after = g_render_counter.load(std::memory_order_acquire) - during_after; g_p2_done.store(true, std::memory_order_release); } // Worker that targets a secondary viewport HWND (Phase 3). Identical wire // shape to altsnap_worker — only the HWND differs. void altsnap_worker_secondary(HWND hwnd) { Sleep(50); g_p3_renders_before = g_render_counter.load(std::memory_order_acquire); PostMessageW(hwnd, WM_ENTERSIZEMOVE, 0, 0); Sleep(kAltSnapStepMs); int during_baseline = g_render_counter.load(std::memory_order_acquire); UINT flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS; for (int i = 0; i < kAltSnapSteps; ++i) { int x = kP3SecondaryX + i * kAltSnapStepPx; int y = kP3SecondaryY; SetWindowPos(hwnd, nullptr, x, y, 0, 0, flags); Sleep(kAltSnapStepMs); } int during_after = g_render_counter.load(std::memory_order_acquire); g_p3_renders_during = during_after - during_baseline; PostMessageW(hwnd, WM_EXITSIZEMOVE, 0, 0); Sleep(200); g_p3_renders_after = g_render_counter.load(std::memory_order_acquire) - during_after; g_p3_done.store(true, std::memory_order_release); } // Walk ImGui's PlatformIO viewports and return the first HWND that is NOT // the main window's HWND. Returns nullptr if no secondary platform window // has been materialized yet. HWND find_secondary_viewport_hwnd(HWND main_hwnd) { ImGuiPlatformIO& pio = ImGui::GetPlatformIO(); for (int i = 0; i < pio.Viewports.Size; ++i) { ImGuiViewport* vp = pio.Viewports[i]; if (!vp || !vp->PlatformHandle) continue; GLFWwindow* gw = (GLFWwindow*)vp->PlatformHandle; HWND h = glfwGetWin32Window(gw); if (h && h != main_hwnd) return h; } return nullptr; } #endif // _WIN32 void render() { // Bump render counter first thing — Phase 2 measures this. g_render_counter.fetch_add(1, std::memory_order_acq_rel); if (!g_window) { if (ImGuiViewport* vp = ImGui::GetMainViewport()) { g_window = (GLFWwindow*)vp->PlatformHandle; } } if (!g_window) return; if (g_frame < kWarmupFrames) { ++g_frame; return; } // ----------------------------------------------------------------- // Phase 1 — sync test // ----------------------------------------------------------------- int rel = g_frame - kWarmupFrames; if (rel < kSyncFrames) { int tx = kBaseX + rel * kStepPx; int ty = kBaseY; glfwSetWindowPos(g_window, tx, ty); int ax = 0, ay = 0; glfwGetWindowPos(g_window, &ax, &ay); ImVec2 vp = ImGui::GetMainViewport()->Pos; int sync_d = std::max((int)std::abs(vp.x - (float)ax), (int)std::abs(vp.y - (float)ay)); int clamp_d = std::max(std::abs(ax - tx), std::abs(ay - ty)); if ((float)sync_d > kSyncTolerance) ++g_p1_bad_sync; if (clamp_d > kClampTolerance) ++g_p1_bad_clamp; g_p1_max_sync = std::max(g_p1_max_sync, sync_d); g_p1_max_clamp = std::max(g_p1_max_clamp, clamp_d); if (rel == 0 || rel == kSyncFrames - 1 || (rel % 10 == 0)) { std::fprintf(stdout, "[p1.sync] f=%d target=(%d,%d) actual=(%d,%d) vp=(%.1f,%.1f) sync_d=%d clamp_d=%d\n", rel, tx, ty, ax, ay, vp.x, vp.y, sync_d, clamp_d); std::fflush(stdout); } ++g_frame; return; } if (!g_p1_done) { std::fprintf(stdout, "[p1.sync] DONE frames=%d max_sync=%dpx max_clamp=%dpx bad_sync=%d bad_clamp=%d\n", kSyncFrames, g_p1_max_sync, g_p1_max_clamp, g_p1_bad_sync, g_p1_bad_clamp); std::fflush(stdout); g_p1_done = true; } // ----------------------------------------------------------------- // Phase 2 — AltSnap simulation (Windows only) // ----------------------------------------------------------------- #ifdef _WIN32 if (!g_p2_started.exchange(true, std::memory_order_acq_rel)) { HWND hwnd = glfwGetWin32Window(g_window); if (!hwnd) { g_p2_skipped = true; g_p2_done.store(true, std::memory_order_release); } else { std::fprintf(stdout, "[p2.altsnap] starting worker — bracket=ENTERSIZEMOVE -> %d SetWindowPos -> EXITSIZEMOVE\n", kAltSnapSteps); std::fflush(stdout); std::thread(altsnap_worker, hwnd).detach(); } } #else if (!g_p2_started.exchange(true, std::memory_order_acq_rel)) { g_p2_skipped = true; g_p2_done.store(true, std::memory_order_release); } #endif if (!g_p2_done.load(std::memory_order_acquire)) { ++g_frame; return; } // Phase 2 finished — log once. static bool s_p2_logged = false; if (!s_p2_logged) { if (g_p2_skipped) { std::fprintf(stdout, "[p2.altsnap] SKIPPED (non-Windows)\n"); } else { std::fprintf(stdout, "[p2.altsnap] DONE renders before=%d during=%d after=%d (during should be 0)\n", g_p2_renders_before, g_p2_renders_during, g_p2_renders_after); } std::fflush(stdout); s_p2_logged = true; } // ----------------------------------------------------------------- // Phase 3 — AltSnap simulation on a SECONDARY viewport (Windows only) // ----------------------------------------------------------------- #ifdef _WIN32 // Always render the floating panel while p3 is active so the platform // window keeps existing and the worker can target it. if (g_p3_show_window && !g_p3_done.load(std::memory_order_acquire)) { ImGui::SetNextWindowPos(ImVec2((float)kP3SecondaryX, (float)kP3SecondaryY), ImGuiCond_Once); ImGui::SetNextWindowSize(ImVec2((float)kP3SecondaryW, (float)kP3SecondaryH), ImGuiCond_Once); if (ImGui::Begin("p3_secondary", nullptr, ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse)) { ImGui::TextUnformatted("floating panel under AltSnap test"); } ImGui::End(); } if (!g_p3_started.load(std::memory_order_acquire)) { // Give the backend kP3InitFrames to materialize the secondary // platform window (UpdatePlatformWindows runs once per frame, and // the GLFW backend needs a couple of cycles). if (g_p3_init_frames < kP3InitFrames) { ++g_p3_init_frames; ++g_frame; return; } HWND main_hwnd = glfwGetWin32Window(g_window); HWND sec_hwnd = find_secondary_viewport_hwnd(main_hwnd); if (!sec_hwnd) { std::fprintf(stdout, "[p3.secondary] SKIPPED — backend never created a secondary platform window " "(ConfigViewportsNoAutoMerge maybe disabled?)\n"); std::fflush(stdout); g_p3_skipped = true; g_p3_done.store(true, std::memory_order_release); g_p3_started.store(true, std::memory_order_release); } else { g_p3_secondary_hwnd = sec_hwnd; std::fprintf(stdout, "[p3.secondary] starting worker — bracket=ENTERSIZEMOVE -> %d SetWindowPos -> EXITSIZEMOVE " "on secondary HWND=%p\n", kAltSnapSteps, (void*)sec_hwnd); std::fflush(stdout); g_p3_started.store(true, std::memory_order_release); std::thread(altsnap_worker_secondary, sec_hwnd).detach(); } } if (!g_p3_done.load(std::memory_order_acquire)) { ++g_frame; return; } #else if (!g_p3_started.exchange(true, std::memory_order_acq_rel)) { g_p3_skipped = true; g_p3_done.store(true, std::memory_order_release); } #endif // Phase 3 finished — log once. static bool s_p3_logged = false; if (!s_p3_logged) { if (g_p3_skipped) { std::fprintf(stdout, "[p3.secondary] SKIPPED\n"); } else { std::fprintf(stdout, "[p3.secondary] DONE renders before=%d during=%d after=%d (during should be 0)\n", g_p3_renders_before, g_p3_renders_during, g_p3_renders_after); } std::fflush(stdout); s_p3_logged = true; } // ----------------------------------------------------------------- // Phase 4 — minimize main, floating panel must survive // ----------------------------------------------------------------- // Keep drawing the secondary panel from p3 so the floating viewport // keeps existing across the dance. We never set g_p3_show_window=false. #ifdef _WIN32 if (g_p3_show_window) { ImGui::SetNextWindowPos(ImVec2((float)kP3SecondaryX, (float)kP3SecondaryY), ImGuiCond_Once); ImGui::SetNextWindowSize(ImVec2((float)kP3SecondaryW, (float)kP3SecondaryH), ImGuiCond_Once); if (ImGui::Begin("p3_secondary", nullptr, ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoCollapse)) { ImGui::TextUnformatted("floating panel under AltSnap + minimize test"); } ImGui::End(); } if (!g_p4_done.load(std::memory_order_acquire)) { // Multi-step state machine. Each step waits kP4SettleFrames before // advancing so the OS / WM have time to react. // step 0: snapshot "alive before" // step 1..N: iconify main // step 2: snapshot "alive while iconified" + count renders during // step 3: restore main // step 4: snapshot "alive after restore" -> done static int s_frame_at_step = 0; static int s_renders_at_step = 0; if (!g_p4_started.exchange(true, std::memory_order_acq_rel)) { s_frame_at_step = g_frame; s_renders_at_step = g_render_counter.load(std::memory_order_acquire); g_p4_step = 0; std::fprintf(stdout, "[p4.minimize] start — iconify dance\n"); std::fflush(stdout); } if (g_frame - s_frame_at_step >= kP4SettleFrames) { HWND main_hwnd = glfwGetWin32Window(g_window); HWND sec_hwnd = find_secondary_viewport_hwnd(main_hwnd); switch (g_p4_step) { case 0: g_p4_secondary_alive_before = (sec_hwnd != nullptr) && IsWindow(sec_hwnd); glfwIconifyWindow(g_window); break; case 1: // Mid-iconified probe: still alive? g_p4_secondary_alive_during = (sec_hwnd != nullptr) && IsWindow(sec_hwnd); g_p4_renders_iconified = g_render_counter.load(std::memory_order_acquire) - s_renders_at_step; break; case 2: glfwRestoreWindow(g_window); break; case 3: g_p4_secondary_alive_after = (sec_hwnd != nullptr) && IsWindow(sec_hwnd); g_p4_done.store(true, std::memory_order_release); break; } s_frame_at_step = g_frame; s_renders_at_step = g_render_counter.load(std::memory_order_acquire); ++g_p4_step; } ++g_frame; return; } #else if (!g_p4_started.exchange(true, std::memory_order_acq_rel)) { g_p4_skipped = true; g_p4_done.store(true, std::memory_order_release); } #endif static bool s_p4_logged = false; if (!s_p4_logged) { if (g_p4_skipped) { std::fprintf(stdout, "[p4.minimize] SKIPPED (non-Windows)\n"); } else { std::fprintf(stdout, "[p4.minimize] DONE alive(before=%d during=%d after=%d) renders_iconified=%d\n", g_p4_secondary_alive_before ? 1 : 0, g_p4_secondary_alive_during ? 1 : 0, g_p4_secondary_alive_after ? 1 : 0, g_p4_renders_iconified); } std::fflush(stdout); s_p4_logged = true; } // ----------------------------------------------------------------- // Phase 5 — Alt + RMB triggers native resize modal (Windows only) // ----------------------------------------------------------------- #ifdef _WIN32 if (!g_p5_done.load(std::memory_order_acquire)) { static int s_frame_at_step5 = 0; if (!g_p5_started.exchange(true, std::memory_order_acq_rel)) { s_frame_at_step5 = g_frame; g_p5_alt_resize_count_before = fn::internal::alt_rmb_resize_count(); g_p5_sizemove_enter_before = fn::internal::sizemove_enter_count(); std::fprintf(stdout, "[p5.alt_rmb] start — counters before alt=%d sizemove_enter=%d\n", g_p5_alt_resize_count_before, g_p5_sizemove_enter_before); std::fflush(stdout); } if (g_frame - s_frame_at_step5 >= kP5SettleFrames) { switch (g_p5_step) { case 0: { // Synchronous same-thread dispatch through the full // WndProc chain. No marshalling, no input-injection // filter to worry about. The framework's WndProc // observes force_alt_for_test, increments the counter, // and (in test mode) skips emitting SC_SIZE so the // headless harness doesn't get stuck in Windows' modal // resize loop. The modal entry itself is covered by p2 // (which fakes WM_ENTERSIZEMOVE directly). HWND hwnd = glfwGetWin32Window(g_window); fn::internal::set_force_alt_for_test(true); SendMessageW(hwnd, WM_RBUTTONDOWN, MK_RBUTTON, MAKELPARAM(40, 40)); fn::internal::set_force_alt_for_test(false); break; } case 1: { g_p5_alt_resize_count_after = fn::internal::alt_rmb_resize_count(); g_p5_sizemove_enter_after = fn::internal::sizemove_enter_count(); std::fprintf(stdout, "[p5.alt_rmb] diag rbuttondown_seen=%d\n", fn::internal::rbuttondown_seen_count()); std::fflush(stdout); g_p5_done.store(true, std::memory_order_release); break; } } s_frame_at_step5 = g_frame; ++g_p5_step; } ++g_frame; return; } #else if (!g_p5_started.exchange(true, std::memory_order_acq_rel)) { g_p5_skipped = true; g_p5_done.store(true, std::memory_order_release); } #endif static bool s_p5_logged = false; if (!s_p5_logged) { if (g_p5_skipped) { std::fprintf(stdout, "[p5.alt_rmb] SKIPPED (non-Windows)\n"); } else { std::fprintf(stdout, "[p5.alt_rmb] DONE alt_resize delta=%d (after=%d) sizemove_enter delta=%d (after=%d)\n", g_p5_alt_resize_count_after - g_p5_alt_resize_count_before, g_p5_alt_resize_count_after, g_p5_sizemove_enter_after - g_p5_sizemove_enter_before, g_p5_sizemove_enter_after); } std::fflush(stdout); s_p5_logged = true; } // ----------------------------------------------------------------- // Phase 6 — Alt + LMB triggers native MOVE modal (Windows only) // ----------------------------------------------------------------- #ifdef _WIN32 if (!g_p6_done.load(std::memory_order_acquire)) { static int s_frame_at_step6 = 0; if (!g_p6_started.exchange(true, std::memory_order_acq_rel)) { s_frame_at_step6 = g_frame; g_p6_alt_move_before = fn::internal::alt_lmb_move_count(); std::fprintf(stdout, "[p6.alt_lmb] start — counter before alt_lmb_move=%d\n", g_p6_alt_move_before); std::fflush(stdout); } if (g_frame - s_frame_at_step6 >= kP6SettleFrames) { switch (g_p6_step) { case 0: { // Same harness shape as p5: synchronous same-thread // SendMessage with force_alt enabled. Framework consumes // the click, increments g_alt_lmb_move_count, and (in // test mode) skips emitting SC_MOVE so the harness // doesn't enter Windows' move modal. HWND hwnd = glfwGetWin32Window(g_window); fn::internal::set_force_alt_for_test(true); SendMessageW(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(60, 60)); fn::internal::set_force_alt_for_test(false); break; } case 1: { g_p6_alt_move_after = fn::internal::alt_lmb_move_count(); g_p6_done.store(true, std::memory_order_release); break; } } s_frame_at_step6 = g_frame; ++g_p6_step; } ++g_frame; return; } #else if (!g_p6_started.exchange(true, std::memory_order_acq_rel)) { g_p6_skipped = true; g_p6_done.store(true, std::memory_order_release); } #endif static bool s_p6_logged = false; if (!s_p6_logged) { if (g_p6_skipped) { std::fprintf(stdout, "[p6.alt_lmb] SKIPPED (non-Windows)\n"); } else { std::fprintf(stdout, "[p6.alt_lmb] DONE alt_move delta=%d (after=%d)\n", g_p6_alt_move_after - g_p6_alt_move_before, g_p6_alt_move_after); } std::fflush(stdout); s_p6_logged = true; } glfwSetWindowShouldClose(g_window, GLFW_TRUE); } } // namespace int main(int argc, char** argv) { (void)argc; (void)argv; fn::AppConfig cfg; cfg.title = "altsnap_jitter_test"; cfg.width = 800; cfg.height = 600; cfg.vsync = false; cfg.viewports = true; cfg.init_gl_loader = false; cfg.about = {"altsnap_jitter_test", "0.1.0", "Smoke test anti-jitter (AltSnap, snap-assist) para fn::run_app."}; cfg.log = {"altsnap_jitter_test.log", 1}; cfg.auto_dockspace = false; // test no necesita dockspace // pre_frame runs after ImGui::NewFrame each tick. We flip // ConfigViewportsNoAutoMerge ON exactly once so Phase 3's floating panel // is guaranteed to land on its own platform window (HWND) instead of // auto-merging into the main viewport. Done in pre_frame (not in main()) // because ImGui::CreateContext / GetIO are not valid yet when AppConfig // is built. cfg.pre_frame = []() { static bool s_done = false; if (s_done) return; ImGui::GetIO().ConfigViewportsNoAutoMerge = true; s_done = true; }; int rc = fn::run_app(cfg, render); if (rc != 0) { std::fprintf(stderr, "[altsnap_jitter] run_app returned %d\n", rc); return rc; } // Pass criteria: // p1_bad_sync == 0 (existing GLFW callback fix still works) // p2_renders_during == 0 (subclass + main-loop gate kicked in on main HWND) // p2_renders_after > 0 (rendering resumed after EXITSIZEMOVE) // p3_renders_during == 0 (subclass installed on secondary viewport HWND too) // p3_renders_after > 0 (rendering resumed after EXITSIZEMOVE on secondary) // p4: secondary alive before AND during iconify AND after restore. // Renders during iconified > 0 (frame loop kept going for floaters). // p5: alt_rmb_resize_count incremented (Alt+RMB consumed by WndProc) AND // sizemove_enter_count incremented (Windows entered SC_SIZE modal). bool p1_pass = (g_p1_bad_sync == 0); bool p2_pass = g_p2_skipped || (g_p2_renders_during == 0 && g_p2_renders_after > 0); bool p3_pass = g_p3_skipped || (g_p3_renders_during == 0 && g_p3_renders_after > 0); bool p4_pass = g_p4_skipped || (g_p4_secondary_alive_before && g_p4_secondary_alive_during && g_p4_secondary_alive_after && g_p4_renders_iconified > 0); // p5: in headless test mode the framework intentionally skips posting // SC_SIZE (to avoid the modal trapping the test thread), so we only // assert the Alt+RMB counter incremented. The real SC_SIZE -> modal // sizemove path is exercised by p2/p3 which fake WM_ENTERSIZEMOVE // directly. int p5_alt_delta = g_p5_alt_resize_count_after - g_p5_alt_resize_count_before; int p5_sizemove_delta = g_p5_sizemove_enter_after - g_p5_sizemove_enter_before; (void)p5_sizemove_delta; bool p5_pass = g_p5_skipped || (p5_alt_delta >= 1); int p6_alt_delta = g_p6_alt_move_after - g_p6_alt_move_before; bool p6_pass = g_p6_skipped || (p6_alt_delta >= 1); bool pass = p1_pass && p2_pass && p3_pass && p4_pass && p5_pass && p6_pass; std::fprintf(stdout, "[altsnap_jitter] p1=%s p2=%s p3=%s p4=%s p5=%s p6=%s overall=%s\n", p1_pass ? "PASS" : "FAIL", g_p2_skipped ? "SKIP" : (p2_pass ? "PASS" : "FAIL"), g_p3_skipped ? "SKIP" : (p3_pass ? "PASS" : "FAIL"), g_p4_skipped ? "SKIP" : (p4_pass ? "PASS" : "FAIL"), g_p5_skipped ? "SKIP" : (p5_pass ? "PASS" : "FAIL"), g_p6_skipped ? "SKIP" : (p6_pass ? "PASS" : "FAIL"), pass ? "PASS" : "FAIL"); return pass ? 0 : 1; }