chore: auto-commit (3 archivos)

- app.md
- main.cpp
- appicon.ico

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 16:33:23 +02:00
parent 6e52b658a3
commit 98fcd89d5a
3 changed files with 521 additions and 31 deletions
+55 -17
View File
@@ -2,31 +2,39 @@
name: altsnap_jitter_test
lang: cpp
domain: tools
description: "Regression test for multi-viewport window jitter triggered by external window movers (AltSnap on Windows, tiling WMs). Drives glfwSetWindowPos every frame and asserts ImGui viewport tracks the OS pos within 1px."
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: "cpp/apps/altsnap_jitter_test"
dir_path: "apps/altsnap_jitter_test"
repo_url: ""
---
# altsnap_jitter_test
Headless C++ harness para validar el fix de jitter de multi-viewport (AltSnap, tiling WMs).
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
1. Arranca `fn::run_app` con viewports ON (config exacta que reproducia el bug).
2. Tras 8 frames de warmup, mueve la ventana via `glfwSetWindowPos` un step por frame durante 60 frames.
3. Cada frame muestrea pos OS (`glfwGetWindowPos`) y pos ImGui (`GetMainViewport()->Pos`).
4. Cuenta divergencias > 1px entre ambos. Cero divergencias = PASS, exit 0.
5. Reporta tambien clamp del WM (cuando el compositor rechaza la pos pedida).
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:
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
@@ -36,22 +44,52 @@ xvfb-run -a -s "-screen 0 1280x800x24" \
echo "EXIT: $?"
```
Windows (cross-compile + Desktop deploy):
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)
## Output esperado (PASS, Windows)
```
[altsnap_jitter] f=0 target=(200,200) actual=(200,200) vp=(200.0,200.0) sync_d=0 clamp_d=0
[altsnap_jitter] f=10 target=(240,200) actual=(240,200) vp=(240.0,200.0) sync_d=0 clamp_d=0
...
[altsnap_jitter] DONE frames=60 max_sync_divergence=0px max_clamp_divergence=0px bad_sync=0 bad_clamp=0
[altsnap_jitter] PASS
[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
`bad_sync > 0` significa que ImGui viewport->Pos quedo fuera de sincronia con la pos real OS, exactamente la condicion que produce el visible "temblor" cuando AltSnap arrastra la ventana. Ese feedback loop es lo que arregla la patch en `cpp/framework/app_base.cpp` (callback GLFW + per-frame sync de viewports).
- `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<HWND, WNDPROC> 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.