--- name: altsnap_jitter_test lang: cpp domain: tools version: 0.1.0 description: "Regression test for multi-viewport window jitter + iconified survival + Alt+RMB resize + Alt+LMB move. Six phases: p1 main-window sync, p2 AltSnap on main HWND, p3 AltSnap on secondary viewport HWND, p4 iconify+restore preserves floating panels, p5 Alt+RMB consumed by WndProc, p6 Alt+LMB consumed by WndProc." tags: [imgui, test, regression, headless] uses_functions: [] uses_types: [] framework: "imgui" entry_point: "main.cpp" dir_path: "apps/altsnap_jitter_test" repo_url: "" icon: phosphor: "arrows-clockwise" accent: "#dc2626" --- # altsnap_jitter_test Headless C++ harness para validar el subclass anti-jitter del framework (`cpp/framework/app_base.cpp`): movimiento/redimensionado externos sin temblor en la ventana principal y en viewports secundarios, iconified main no pierde paneles flotantes, Alt+RMB resize anywhere, Alt+LMB move anywhere. ## Que hace Seis fases en una sola sesion de `fn::run_app`: 1. **p1.sync** (cross-platform). Warmup 8 frames, mueve la ventana principal via `glfwSetWindowPos` un step por frame durante 60 frames. Muestrea pos OS (`glfwGetWindowPos`) vs pos ImGui (`GetMainViewport()->Pos`). Tolerancia 1px. Cuenta divergencias = `bad_sync`. 2. **p2.altsnap** (Windows). Worker thread fakea `WM_ENTERSIZEMOVE` + burst de 30 `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el HWND principal. Aserta `renders_during==0` (subclass del WndProc principal y gate del main loop activos). 3. **p3.secondary** (Windows). Fuerza creacion de un viewport secundario (panel flotante, `ConfigViewportsNoAutoMerge=true`), localiza su HWND en `pio.Viewports` via `find_secondary_viewport_hwnd`, y repite el bracket sobre el. Valida que el subclass per-frame cubre tambien el HWND secundario. 4. **p4.minimize** (Windows). State machine 4 steps con `kP4SettleFrames=20` entre cada uno. Captura `IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow` + `glfwRestoreWindow`. Asserta los 3 estados vivos AND `renders_iconified > 0` (frame loop sigue activo durante iconify para que los flotantes no se pierdan). 5. **p5.alt_rmb** (Windows). `fn::internal::set_force_alt_for_test(true)` + `SendMessageW(hwnd, WM_RBUTTONDOWN, MK_RBUTTON, MAKELPARAM(40,40))` sincrono mismo-hilo. Asserta `fn::internal::alt_rmb_resize_count()` incrementa en 1. En test mode el handler salta el `PostMessage SC_SIZE` para no atrapar al harness en modal. 6. **p6.alt_lmb** (Windows). Mismo patron para `WM_LBUTTONDOWN`. Asserta `fn::internal::alt_lmb_move_count()` incrementa en 1. PASS = todas las fases con sus deltas positivos. SKIP en Linux para p2-p6. ## Run WSL/Linux (sanity build; p2-p6 skipped): ```bash cd cpp && cmake -B build/linux -DFN_BUILD_TESTS=OFF cmake --build build/linux --target altsnap_jitter_test -j4 xvfb-run -a -s "-screen 0 1280x800x24" \ env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \ build/linux/apps/altsnap_jitter_test/altsnap_jitter_test echo "EXIT: $?" ``` Windows (cross-compile + Desktop deploy + run): ```bash source bash/functions/infra/e2e_run_cpp_windows.sh e2e_run_cpp_windows altsnap_jitter_test ``` ## Output esperado (PASS, Windows) ``` [p1.sync] DONE frames=60 max_sync=0px max_clamp=0px bad_sync=0 bad_clamp=0 [p2.altsnap] DONE renders before=N during=0 after=M [p3.secondary] DONE renders before=N during=0 after=M [p4.minimize] DONE alive(before=1 during=1 after=1) renders_iconified=20 [p5.alt_rmb] DONE alt_resize delta=1 (after=1) sizemove_enter delta=0 (after=2) [p6.alt_lmb] DONE alt_move delta=1 (after=1) [altsnap_jitter] p1=PASS p2=PASS p3=PASS p4=PASS p5=PASS p6=PASS overall=PASS ``` En Linux/xvfb p2-p6 reportan SKIPPED. P1 puede mostrar lag pre-existente bajo xvfb por como X procesa `SetWindowPos`; no es un fallo del fix. ## Criterio de fallo - `p1_bad_sync > 0`: ImGui viewport->Pos quedo fuera de sincronia con la pos OS. Roto el callback GLFW + per-frame sync. - `p2_renders_during > 0`: la app sigue dibujando durante un bracket AltSnap en el HWND principal. Roto el subclass del WndProc principal o la gate del main loop. - `p3_renders_during > 0`: la app sigue dibujando durante un bracket AltSnap en un HWND **secundario** (panel flotante). Roto el escaneo per-frame de `pio.Viewports` que instala el subclass en cada platform window. - `p4 alive(during)=0`: floating panel se cierra/desaparece al minimizar el main. Regresion del fix iconified+secondary. - `p4 renders_iconified == 0`: el iconified-gate volvio a `glfwWaitEvents+continue` global sin chequear secondaries. Floating panels se congelarian. - `p5 alt_resize delta == 0`: Alt+RMB no se consume. Subclass no esta en el chain (`ImGui_ImplGlfw_WndProc` capturo nuestra WndProc como prev y no chainea bien) o flag `force_alt_for_test` no se aplica. - `p6 alt_move delta == 0`: misma raiz que p5 pero para LMB. ## Donde vive el fix `cpp/framework/app_base.cpp`: - GLFW pos/size callbacks (anti-jitter capa 1). - Per-frame viewport sync (capa 2). - `unordered_map g_subclassed` + per-frame `install_sizemove_subclass_hwnd` sobre `pio.Viewports` (capa 3, multi-HWND). - Iconified gate detecta secondary viewports y fall-through si existen. - `WM_LBUTTONDOWN`/`WM_RBUTTONDOWN` Alt-held → `WM_SYSCOMMAND, SC_MOVE|HTCAPTION` / `SC_SIZE|dir`. - `io.ConfigWindowsMoveFromTitleBarOnly = true` (floating panels respetan header-only). - `fn::internal::*` counters expuestos para tests headless. `cpp/framework/app_base.h`: - `namespace fn::internal { sizemove_enter_count(); alt_rmb_resize_count(); alt_lmb_move_count(); rbuttondown_seen_count(); set_force_alt_for_test(bool); }`. ## Notas - `keybd_event(VK_MENU)` NO es fiable para drivear `GetAsyncKeyState` desde tests headless cross-compilados — la sesion de input del proceso no esta foreground. Usa `set_force_alt_for_test(true)` + `SendMessageW` sincrono mismo-hilo. Bypassa kernel-input filter (que dropea silenciosamente `PostMessage(WM_RBUTTONDOWN)` sintetizado). - ImGui_ImplGlfw subclassea el HWND despues que nosotros (vendor `cpp/vendor/imgui/backends/imgui_impl_glfw.cpp` linea ~820, captura `bd->PrevWndProc = our_subclass`). Por eso ImGui llama a nuestro WndProc via `CallWindowProc(prev_wndproc, ...)` y todos los mensajes nos llegan en orden correcto. No re-subclassear (provoca recursion infinita via cycle). - Test mode (`set_force_alt_for_test(true)`) hace que el WndProc cuente pero NO postee `SC_SIZE`/`SC_MOVE` — evita quedarse atrapado en modal sizemove. La parte "entrar al modal" se valida por p2/p3 fakeando `WM_ENTERSIZEMOVE` directamente. ## Capability growth log Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`: - `major`: breaking observable (CLI args, schema BBDD propia, formato wire). - `minor`: feature aditiva (nuevo panel, endpoint, opcion). - `patch`: bugfix sin cambio observable. - v0.1.0 (2026-05-18) — baseline.