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++
|
// 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,
|
// app shell. The harness runs two phases inside one fn::run_app session:
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Linux/WSL: runs the same loop. With xvfb the window manager honors
|
// Phase 1 — Sync test (cross-platform).
|
||||||
// glfwSetWindowPos, so the test is meaningful in CI. On a real WSL X
|
// Drives the OS window with glfwSetWindowPos every frame and asserts
|
||||||
// display some compositors clamp positions; the test logs and accepts
|
// ImGui's main viewport->Pos tracks the actual OS pos within 1px. This
|
||||||
// "OS clamped" frames as long as ImGui tracks the clamped value.
|
// 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
|
// Phase 2 — AltSnap simulation (Windows-only, no-op on Linux).
|
||||||
// the Desktop apps dir, launched via cmd.exe; this is where the original
|
// A worker thread reproduces AltSnap's exact wire format:
|
||||||
// AltSnap symptom lives, so any divergence here is the real regression.
|
// 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 "app_base.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
#include <GLFW/glfw3.h>
|
#include <GLFW/glfw3.h>
|
||||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
|
||||||
#ifdef _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 <GLFW/glfw3native.h>
|
||||||
|
#include <thread>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -32,35 +49,92 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct Sample {
|
// ----- Phase 1 (sync test) tunables -----
|
||||||
int frame = 0;
|
constexpr int kWarmupFrames = 8;
|
||||||
int target_x = 0;
|
constexpr int kSyncFrames = 60;
|
||||||
int target_y = 0;
|
constexpr int kStepPx = 4;
|
||||||
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
|
|
||||||
constexpr int kBaseX = 200;
|
constexpr int kBaseX = 200;
|
||||||
constexpr int kBaseY = 200;
|
constexpr int kBaseY = 200;
|
||||||
constexpr float kSyncTolerance = 1.0f; // ImGui viewport vs OS pos
|
constexpr float kSyncTolerance = 1.0f;
|
||||||
constexpr int kClampTolerance = 6; // OS-applied vs target (compositor clamp ok up to this)
|
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;
|
GLFWwindow* g_window = nullptr;
|
||||||
int g_frame = 0;
|
int g_frame = 0;
|
||||||
int g_max_sync = 0;
|
|
||||||
int g_max_clamp = 0;
|
// Phase 1 metrics
|
||||||
int g_bad_sync = 0; // frames where vp diverges from OS pos > tolerance
|
int g_p1_max_sync = 0;
|
||||||
int g_bad_clamp = 0; // frames where OS pos diverges from target > clamp tolerance
|
int g_p1_max_clamp = 0;
|
||||||
bool g_done = false;
|
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() {
|
void render() {
|
||||||
// Locate the main window once. ImGui_ImplGlfw stashes it on the main
|
// Bump render counter first thing — Phase 2 measures this.
|
||||||
// viewport's PlatformHandle right after init.
|
g_render_counter.fetch_add(1, std::memory_order_acq_rel);
|
||||||
|
|
||||||
if (!g_window) {
|
if (!g_window) {
|
||||||
if (ImGuiViewport* vp = ImGui::GetMainViewport()) {
|
if (ImGuiViewport* vp = ImGui::GetMainViewport()) {
|
||||||
g_window = (GLFWwindow*)vp->PlatformHandle;
|
g_window = (GLFWwindow*)vp->PlatformHandle;
|
||||||
@@ -73,54 +147,83 @@ void render() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Phase 1 — sync test
|
||||||
|
// -----------------------------------------------------------------
|
||||||
int rel = g_frame - kWarmupFrames;
|
int rel = g_frame - kWarmupFrames;
|
||||||
if (rel < kTestFrames) {
|
if (rel < kSyncFrames) {
|
||||||
// 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.
|
|
||||||
int tx = kBaseX + rel * kStepPx;
|
int tx = kBaseX + rel * kStepPx;
|
||||||
int ty = kBaseY;
|
int ty = kBaseY;
|
||||||
glfwSetWindowPos(g_window, tx, ty);
|
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;
|
int ax = 0, ay = 0;
|
||||||
glfwGetWindowPos(g_window, &ax, &ay);
|
glfwGetWindowPos(g_window, &ax, &ay);
|
||||||
ImVec2 vp = ImGui::GetMainViewport()->Pos;
|
ImVec2 vp = ImGui::GetMainViewport()->Pos;
|
||||||
|
|
||||||
int sync_dx = (int)std::abs(vp.x - (float)ax);
|
int sync_d = std::max((int)std::abs(vp.x - (float)ax),
|
||||||
int sync_dy = (int)std::abs(vp.y - (float)ay);
|
(int)std::abs(vp.y - (float)ay));
|
||||||
int clamp_dx = std::abs(ax - tx);
|
int clamp_d = std::max(std::abs(ax - tx), std::abs(ay - ty));
|
||||||
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);
|
|
||||||
|
|
||||||
if ((float)sync_d > kSyncTolerance) ++g_bad_sync;
|
if ((float)sync_d > kSyncTolerance) ++g_p1_bad_sync;
|
||||||
if (clamp_d > kClampTolerance) ++g_bad_clamp;
|
if (clamp_d > kClampTolerance) ++g_p1_bad_clamp;
|
||||||
g_max_sync = std::max(g_max_sync, sync_d);
|
g_p1_max_sync = std::max(g_p1_max_sync, sync_d);
|
||||||
g_max_clamp = std::max(g_max_clamp, clamp_d);
|
g_p1_max_clamp = std::max(g_p1_max_clamp, clamp_d);
|
||||||
|
|
||||||
// Verbose first/last frame for log readability.
|
if (rel == 0 || rel == kSyncFrames - 1 || (rel % 10 == 0)) {
|
||||||
if (rel == 0 || rel == kTestFrames - 1 || (rel % 10 == 0)) {
|
|
||||||
std::fprintf(stdout,
|
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);
|
rel, tx, ty, ax, ay, vp.x, vp.y, sync_d, clamp_d);
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
}
|
}
|
||||||
++g_frame;
|
++g_frame;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!g_p1_done) {
|
||||||
if (!g_done) {
|
|
||||||
std::fprintf(stdout,
|
std::fprintf(stdout,
|
||||||
"[altsnap_jitter] DONE frames=%d max_sync_divergence=%dpx max_clamp_divergence=%dpx bad_sync=%d bad_clamp=%d\n",
|
"[p1.sync] DONE frames=%d max_sync=%dpx max_clamp=%dpx bad_sync=%d bad_clamp=%d\n",
|
||||||
kTestFrames, g_max_sync, g_max_clamp, g_bad_sync, g_bad_clamp);
|
kSyncFrames, g_p1_max_sync, g_p1_max_clamp, g_p1_bad_sync, g_p1_bad_clamp);
|
||||||
std::fflush(stdout);
|
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);
|
glfwSetWindowShouldClose(g_window, GLFW_TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,8 +236,8 @@ int main(int argc, char** argv) {
|
|||||||
cfg.title = "altsnap_jitter_test";
|
cfg.title = "altsnap_jitter_test";
|
||||||
cfg.width = 800;
|
cfg.width = 800;
|
||||||
cfg.height = 600;
|
cfg.height = 600;
|
||||||
cfg.vsync = false; // run as fast as possible
|
cfg.vsync = false;
|
||||||
cfg.viewports = true; // exact config that exhibits the bug
|
cfg.viewports = true;
|
||||||
cfg.init_gl_loader = false;
|
cfg.init_gl_loader = false;
|
||||||
|
|
||||||
int rc = fn::run_app(cfg, render);
|
int rc = fn::run_app(cfg, render);
|
||||||
@@ -143,10 +246,19 @@ int main(int argc, char** argv) {
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass criterion: every measured frame must have ImGui viewport tracking
|
// Pass criteria:
|
||||||
// the OS-reported window pos within 1px. A frame where the OS clamped
|
// p1_bad_sync == 0 (existing GLFW callback fix still works)
|
||||||
// the position (compositor) is fine as long as ImGui follows the clamp.
|
// p2_renders_during == 0 (subclass + main-loop gate kicked in)
|
||||||
bool pass = (g_bad_sync == 0);
|
// p2_renders_after > 0 (rendering resumed after EXITSIZEMOVE)
|
||||||
std::fprintf(stdout, "[altsnap_jitter] %s\n", pass ? "PASS" : "FAIL");
|
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;
|
return pass ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user