Files
altsnap_jitter_test/main.cpp
T
2026-05-09 18:11:20 +02:00

153 lines
5.7 KiB
C++

// altsnap_jitter_test — automated regression test 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.
//
// 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.
//
// 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.
#include "app_base.h"
#include "imgui.h"
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#ifdef _WIN32
#include <GLFW/glfw3native.h>
#endif
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <algorithm>
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
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)
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;
void render() {
// Locate the main window once. ImGui_ImplGlfw stashes it on the main
// viewport's PlatformHandle right after init.
if (!g_window) {
if (ImGuiViewport* vp = ImGui::GetMainViewport()) {
g_window = (GLFWwindow*)vp->PlatformHandle;
}
}
if (!g_window) return;
if (g_frame < kWarmupFrames) {
++g_frame;
return;
}
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.
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);
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);
// Verbose first/last frame for log readability.
if (rel == 0 || rel == kTestFrames - 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",
rel, tx, ty, ax, ay, vp.x, vp.y, sync_d, clamp_d);
std::fflush(stdout);
}
++g_frame;
return;
}
if (!g_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);
std::fflush(stdout);
g_done = 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; // run as fast as possible
cfg.viewports = true; // exact config that exhibits the bug
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 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");
return pass ? 0 : 1;
}