153 lines
5.7 KiB
C++
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;
|
|
}
|