Files
altsnap_jitter_test/main.cpp
T
egutierrez 181c4f3dd6 chore: auto-commit (1 archivos)
- main.cpp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:30:27 +02:00

265 lines
9.6 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 (Windows-only, no-op on Linux).
// A worker thread reproduces AltSnap's exact wire format:
// 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.
//
// Linux/WSL: only Phase 1 runs. With xvfb the WM honors glfwSetWindowPos,
// so the sync test is meaningful. Phase 2 returns 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;
#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);
}
#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 and exit.
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);
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;
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)
// p2_renders_after > 0 (rendering resumed after EXITSIZEMOVE)
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 pass = p1_pass && p2_pass;
std::fprintf(stdout,
"[altsnap_jitter] p1=%s p2=%s overall=%s\n",
p1_pass ? "PASS" : "FAIL",
g_p2_skipped ? "SKIP" : (p2_pass ? "PASS" : "FAIL"),
pass ? "PASS" : "FAIL");
return pass ? 0 : 1;
}