chore: auto-commit (1 archivos)
- main.cpp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +1,47 @@
|
||||
// altsnap_jitter_test — automated regression test for the multi-viewport
|
||||
// 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 test spawns the standard fn::run_app with viewports ON,
|
||||
// then drives the main GLFW window with glfwSetWindowPos every frame to
|
||||
// emulate an external mover (AltSnap, tiling WM, snap-assist, ...). For
|
||||
// each frame it samples the actual OS window position and ImGui's main
|
||||
// viewport->Pos, and asserts both stay in sync within a 1-pixel tolerance
|
||||
// after a 1-frame settle window. Without the anti-jitter patch in
|
||||
// app_base.cpp the two diverge and the test exits 1.
|
||||
// app shell. The harness runs two phases inside one fn::run_app session:
|
||||
//
|
||||
// Linux/WSL: runs the same loop. With xvfb the window manager honors
|
||||
// glfwSetWindowPos, so the test is meaningful in CI. On a real WSL X
|
||||
// display some compositors clamp positions; the test logs and accepts
|
||||
// "OS clamped" frames as long as ImGui tracks the clamped value.
|
||||
// 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.
|
||||
//
|
||||
// Windows (target): cross-compiled via build_cpp_windows, deployed to
|
||||
// the Desktop apps dir, launched via cmd.exe; this is where the original
|
||||
// AltSnap symptom lives, so any divergence here is the real regression.
|
||||
// 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>
|
||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
||||
#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>
|
||||
@@ -32,35 +49,92 @@
|
||||
|
||||
namespace {
|
||||
|
||||
struct Sample {
|
||||
int frame = 0;
|
||||
int target_x = 0;
|
||||
int target_y = 0;
|
||||
int actual_x = 0;
|
||||
int actual_y = 0;
|
||||
float vp_x = 0.0f;
|
||||
float vp_y = 0.0f;
|
||||
};
|
||||
|
||||
constexpr int kWarmupFrames = 8; // wait for the platform to settle
|
||||
constexpr int kTestFrames = 60; // measured frames
|
||||
constexpr int kStepPx = 4; // px / frame
|
||||
// ----- 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; // ImGui viewport vs OS pos
|
||||
constexpr int kClampTolerance = 6; // OS-applied vs target (compositor clamp ok up to this)
|
||||
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;
|
||||
int g_max_sync = 0;
|
||||
int g_max_clamp = 0;
|
||||
int g_bad_sync = 0; // frames where vp diverges from OS pos > tolerance
|
||||
int g_bad_clamp = 0; // frames where OS pos diverges from target > clamp tolerance
|
||||
bool g_done = false;
|
||||
|
||||
// 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() {
|
||||
// Locate the main window once. ImGui_ImplGlfw stashes it on the main
|
||||
// viewport's PlatformHandle right after init.
|
||||
// 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;
|
||||
@@ -73,54 +147,83 @@ void render() {
|
||||
return;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Phase 1 — sync test
|
||||
// -----------------------------------------------------------------
|
||||
int rel = g_frame - kWarmupFrames;
|
||||
if (rel < kTestFrames) {
|
||||
// Drive the OS window from inside the render loop. Mimics what an
|
||||
// external mover (AltSnap, tiling WM) would do: a fresh SetWindowPos
|
||||
// every tick. With the fix in place ImGui's main viewport->Pos must
|
||||
// follow within the same frame because of the GLFW pos callback.
|
||||
if (rel < kSyncFrames) {
|
||||
int tx = kBaseX + rel * kStepPx;
|
||||
int ty = kBaseY;
|
||||
glfwSetWindowPos(g_window, tx, ty);
|
||||
|
||||
// Sample AFTER the SetWindowPos. The callback fires synchronously on
|
||||
// most platforms; if not, the per-frame sync pass in app_base.cpp
|
||||
// covers it before this render() call on the next frame. We measure
|
||||
// both here for diagnostics.
|
||||
int ax = 0, ay = 0;
|
||||
glfwGetWindowPos(g_window, &ax, &ay);
|
||||
ImVec2 vp = ImGui::GetMainViewport()->Pos;
|
||||
|
||||
int sync_dx = (int)std::abs(vp.x - (float)ax);
|
||||
int sync_dy = (int)std::abs(vp.y - (float)ay);
|
||||
int clamp_dx = std::abs(ax - tx);
|
||||
int clamp_dy = std::abs(ay - ty);
|
||||
int sync_d = std::max(sync_dx, sync_dy);
|
||||
int clamp_d = std::max(clamp_dx, clamp_dy);
|
||||
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_bad_sync;
|
||||
if (clamp_d > kClampTolerance) ++g_bad_clamp;
|
||||
g_max_sync = std::max(g_max_sync, sync_d);
|
||||
g_max_clamp = std::max(g_max_clamp, clamp_d);
|
||||
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);
|
||||
|
||||
// Verbose first/last frame for log readability.
|
||||
if (rel == 0 || rel == kTestFrames - 1 || (rel % 10 == 0)) {
|
||||
if (rel == 0 || rel == kSyncFrames - 1 || (rel % 10 == 0)) {
|
||||
std::fprintf(stdout,
|
||||
"[altsnap_jitter] f=%d target=(%d,%d) actual=(%d,%d) vp=(%.1f,%.1f) sync_d=%d clamp_d=%d\n",
|
||||
"[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_done) {
|
||||
if (!g_p1_done) {
|
||||
std::fprintf(stdout,
|
||||
"[altsnap_jitter] DONE frames=%d max_sync_divergence=%dpx max_clamp_divergence=%dpx bad_sync=%d bad_clamp=%d\n",
|
||||
kTestFrames, g_max_sync, g_max_clamp, g_bad_sync, g_bad_clamp);
|
||||
"[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_done = true;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -133,8 +236,8 @@ int main(int argc, char** argv) {
|
||||
cfg.title = "altsnap_jitter_test";
|
||||
cfg.width = 800;
|
||||
cfg.height = 600;
|
||||
cfg.vsync = false; // run as fast as possible
|
||||
cfg.viewports = true; // exact config that exhibits the bug
|
||||
cfg.vsync = false;
|
||||
cfg.viewports = true;
|
||||
cfg.init_gl_loader = false;
|
||||
|
||||
int rc = fn::run_app(cfg, render);
|
||||
@@ -143,10 +246,19 @@ int main(int argc, char** argv) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Pass criterion: every measured frame must have ImGui viewport tracking
|
||||
// the OS-reported window pos within 1px. A frame where the OS clamped
|
||||
// the position (compositor) is fine as long as ImGui follows the clamp.
|
||||
bool pass = (g_bad_sync == 0);
|
||||
std::fprintf(stdout, "[altsnap_jitter] %s\n", pass ? "PASS" : "FAIL");
|
||||
// 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user