98fcd89d5a
- app.md - main.cpp - appicon.ico Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
721 lines
29 KiB
C++
721 lines
29 KiB
C++
// 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 <GLFW/glfw3.h>
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#define GLFW_EXPOSE_NATIVE_WIN32
|
|
#include <GLFW/glfw3native.h>
|
|
#include <thread>
|
|
#endif
|
|
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
|
|
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<int> g_render_counter{0}; // bumped every render() call
|
|
std::atomic<bool> g_p2_started{false};
|
|
std::atomic<bool> 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<bool> g_p3_started{false};
|
|
std::atomic<bool> 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<bool> g_p4_started{false};
|
|
std::atomic<bool> 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<bool> g_p5_started{false};
|
|
std::atomic<bool> 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<bool> g_p6_started{false};
|
|
std::atomic<bool> 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;
|
|
}
|