# AXE B5 + D1 — Capture multi-écran Windows & Desktop distant sans accessibility tree **Date :** 2026-05-23 **Auteur :** Claude (agent recherche sous-traité), brief Dom **Périmètre :** B5 (capture Windows 11 robuste, bug `mss.monitors[N]=2560×60`, DPI) + D1 (NoMachine/Citrix/RDP, capture sans accessibility tree) **Statut :** recommandations techniques, pas de modif code. À valider Dom avant action. --- ## 1. TL;DR **Bug racine identifié** : `mss.monitors[1]=2560×60` n'est PAS un bug du multi-écran, c'est un **effet de bord DPI** documenté du couple `mss` + Windows. Quand un autre composant du process (PyQt5 GUI Léa, NoMachine, ou un appel `GetSystemMetrics` antérieur) modifie le `DPI_AWARENESS_CONTEXT` du process pendant l'exécution, `mss` (qui s'appuie sur `EnumDisplayMonitors` + `MONITORINFO`) renvoie des dims tronquées intermittemment. La 1re capture est saine, les suivantes peuvent dériver. Issue documentée : `BoboTiG/python-mss#197`, `#257`, `#108`, `#49`. **Recommandation principale :** déclarer **`DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2`** AU LANCEMENT du process Léa Windows (avant tout `import mss`, avant PyQt5), via `ctypes.windll.user32.SetProcessDpiAwarenessContext(-4)`. Conserver le garde-fou `_acquire_safe_grab` actuel comme filet de sécurité (post-incident il a déjà sauvé la démo). Migrer à moyen terme vers **`dxcam`** (DXGI Desktop Duplication) qui est immunisé par construction (l'API DXGI travaille en pixels physiques sans dépendre du DPI awareness du process). **Recommandation NoMachine :** garder NoMachine 9.5.7 pour la démo client (terrain connu), mais évaluer **RustDesk** ou **Parsec** comme alternative pour POC suivants. Le freeze NoMachine "clics avalés après quelques minutes" est confirmé par 9+ threads du forum officiel depuis v4.x, **jamais corrigé**. Implémenter un **heartbeat actif côté Léa** (capture pHash toutes les 5 s + détection écran figé > 30 s) avant tout déploiement client. **Fix court terme bug coord Y cassé (P0)** : injecter `SetProcessDpiAwarenessContext` dans `executor.py` au démarrage + serrer le garde-fou existant (refuser TOUTE capture < 200 px de haut, pas seulement secondaire). Code copy-paste-ready en §4. --- ## 2. Section B5 — Capture Windows ### 2.1. Table comparative — bibliothèques de capture Windows (mai 2026) | Lib | Backend | Cross-OS | DPI-safe | Multi-monitor | FPS 1080p | Statut maint. | Verdict RPA Vision | |---|---|---|---|---|---|---|---| | **mss** (BoboTiG) | GDI BitBlt | Linux/Mac/Win | ⚠ Buggy (Win) | OK via `monitors[]` | 30-60 | Actif mais bug DPI ouvert depuis 2018 | **Actuel** — conserver avec ceinture DPI | | **pyautogui** | Pillow + GDI | Cross-OS | ⚠ Idem mss | ❌ Composite seulement | 5-15 | Stable, fonctionnalités figées | À éviter pour capture (ok pour mouse) | | **Win32 GDI direct** (`BitBlt + GetDC`) | GDI | Win | ✅ Si DPI déclaré | OK manuel | 20-40 | Stable, bas niveau | Trop verbeux, ne pas réinventer | | **DXGI Desktop Duplication** (Win32 natif) | DXGI | Win 8+ | ✅ Pixels physiques | OK via `IDXGIOutput` | 240+ | Microsoft, stable | Cible idéale mais complexe en pur Win32 | | **dxcam** (ra1nty) | DXGI | Win | ✅ Natif | OK `output_idx=N` | **240+** | **Actif 2026** (release juin 2025 + maj mars 2026) | ⭐ **Migration cible** | | **D3DShot** (Serpent-AI) | DXGI | Win | ✅ | OK | 60-100 | **Quasi abandonné** (dernier commit 2022) | NON, deprecated | | **windows-capture** (NiiightmareXD) | DXGI + WGC | Win 10+ | ✅ | OK | 240+ | Actif, Rust+Python | Alternative à dxcam, plus jeune | | **BetterCam** (RootKit-Org) | DXGI fork DXcam | Win | ✅ | OK | 240+ | Actif | Fork sécurité/gaming, marketing FPS | ### 2.2. Diagnostic du bug `mss.monitors[N]=2560×60` #### Root cause confirmée Issue `BoboTiG/python-mss#197` documente précisément le pattern : > *« After running `sct.grab(monitor)`, `GetSystemMetrics(0/1)` returns physical pixels (2560×1600) instead of scaled logical (1463×914). »* Lecture causale (mainteneur + reproductions) : 1. Le process Léa Windows démarre **DPI-unaware** (défaut Python 3.x sur Windows sans `SetProcessDpiAwarenessContext` explicite). 2. `mss.mss()` au premier appel passe par `ctypes.windll.user32` et **modifie implicitement** le DPI context du thread (effet de bord interne `mss` pour pouvoir capturer en pixels physiques sur écran HiDPI). 3. À partir de là, `MONITORINFO.rcMonitor` peut renvoyer des coords incohérentes : la combinaison "process unaware → context modifié à la volée" laisse Windows dans un état intermédiaire où certains monitors logiques sont **réduits à la zone non-DPI-aware** (typiquement la barre de tâches = 60 px de haut sur écran 1600 px). 4. Le bug est **intermittent** parce qu'il dépend de l'ordre des appels d'API par le process, de la présence d'une fenêtre PyQt5 active, et de l'événement NoMachine (resize remote → callback Windows qui re-évalue les monitors). L'observation 2560×60 chez Léa correspond exactement à : "monitor reconnu, mais sa hauteur effective dans le contexte non-aware est la zone d'overlay NoMachine (≈ taskbar)". #### Pourquoi `dxcam` est immunisé DXGI Desktop Duplication s'appuie sur `IDXGIOutput::GetDesc` qui retourne `DXGI_OUTPUT_DESC.DesktopCoordinates` en **pixels physiques** indépendamment du DPI awareness du process. Le bug ne peut littéralement pas se produire. #### Workaround documenté officiellement (mss issue #197) ```python # AVANT tout import mss / PyQt5 / win32api import ctypes # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 try: ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4)) except Exception: # Fallback Windows 8.1 (avant V2) : per-monitor v1 try: ctypes.windll.shcore.SetProcessDpiAwareness(2) except Exception: ctypes.windll.user32.SetProcessDPIAware() # legacy ``` ### 2.3. Recommandation patterns multi-DPI 1. **Déclarer le DPI awareness TÔT** dans le process (avant tout `import mss`, avant `from PyQt5 import ...`). Idéalement dans `main.py` du client Léa, ligne 1-5. 2. **Une fois en V2**, `mss.monitors[i]['width'/'height']` est cohérent avec les coords composite Windows (logique = physique pour le process). 3. **Coordonnées agent → serveur** : toujours en pixels physiques globaux (origine = top-left du virtual desktop, qui peut être négative si moniteur secondaire à gauche). Pas de pourcentage tant que le DPI peut bouger. 4. **Pour cliquer en pixels physiques** : utiliser `SendInput` (`ctypes.windll.user32.SendInput`) plutôt que `pyautogui.click` — `pyautogui` re-divise par le DPI scale. 5. **Tester sur écran HiDPI** : la box dev Dom est 1×, le client cible peut être 1.5× ou 1.75× (cas réel issue #197). Bench obligatoire avant déploiement client. ### 2.4. Snippet Python — capture moderne dxcam multi-monitor prêt à coller ```python # capture_dxgi.py — capture Windows via DXGI Desktop Duplication # Drop-in pour remplacer mss.mss().grab(monitor) côté Léa Windows. import ctypes import dxcam import numpy as np from PIL import Image from typing import Optional, Tuple # Étape 1 : déclarer le DPI awareness avant toute capture def _ensure_dpi_aware_v2() -> bool: """Déclare PER_MONITOR_AWARE_V2. À appeler en tout début de process.""" try: # -4 = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (Win 10 1703+) ok = ctypes.windll.user32.SetProcessDpiAwarenessContext( ctypes.c_void_p(-4) ) return bool(ok) except (AttributeError, OSError): try: ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR return True except Exception: try: ctypes.windll.user32.SetProcessDPIAware() return True except Exception: return False _ensure_dpi_aware_v2() # exécuté à l'import # Étape 2 : cache d'instances dxcam par output_idx (création coûteuse) _camera_cache: dict[int, "dxcam.DXCamera"] = {} def get_camera(output_idx: int = 0, device_idx: int = 0) -> "dxcam.DXCamera": """Récupère (ou crée) une caméra DXGI pour un monitor donné.""" key = (device_idx, output_idx) if key not in _camera_cache: _camera_cache[key] = dxcam.create( device_idx=device_idx, output_idx=output_idx, output_color="BGR", # cohérent avec cv2 / mss legacy ) return _camera_cache[key] def list_monitors() -> list[dict]: """Retourne la géométrie de chaque monitor en pixels physiques.""" # dxcam.output_info() retourne une string formatée — on parse via API bas niveau monitors = [] for output_idx, info in enumerate(dxcam.device_info().splitlines()): # Fallback robuste : interroger les outputs via dxcam directement try: cam = get_camera(output_idx=output_idx) # cam.width / cam.height exposés depuis dxcam 0.0.5+ monitors.append({ "idx": output_idx, "width": int(cam.width), "height": int(cam.height), "left": int(getattr(cam, "left", 0)), "top": int(getattr(cam, "top", 0)), "primary": output_idx == 0, }) except Exception: continue return monitors def grab_monitor(output_idx: int = 0) -> Optional[Image.Image]: """Capture un monitor en PIL.Image RGB (drop-in pour mss + Image.frombytes). Retourne None si capture échoue (frame skipped par DXGI = no-change). """ cam = get_camera(output_idx=output_idx) frame = cam.grab() # ndarray (H, W, 3) BGR, ou None si frame inchangée if frame is None: # DXGI ne re-livre pas une frame identique → forcer frame = cam.grab(region=None) if frame is None: return None # BGR → RGB pour PIL return Image.fromarray(frame[:, :, ::-1]) def safe_grab( output_idx: int = 0, min_width: int = 200, min_height: int = 200, max_attempts: int = 2, ) -> Tuple[Optional[dict], Optional[Image.Image]]: """Drop-in pour _acquire_safe_grab actuel mais sur DXGI. Vérifie que la frame retournée a des dimensions plausibles. """ for attempt in range(max_attempts): img = grab_monitor(output_idx=output_idx) if img is None: continue w, h = img.size if w >= min_width and h >= min_height: return ( {"width": w, "height": h, "idx": output_idx}, img, ) return None, None ``` **Notes d'intégration :** - `pip install "dxcam[cv2]"` (ajoute opencv-headless si pas déjà installé). - Python 3.10-3.14 supporté. - À tester sur la box Léa avant migration globale : confirmer que `output_idx=0` correspond bien au monitor principal physique de NoMachine. - **Ne pas migrer en chaud avant la démo client** — l'archi actuelle marche grâce au garde-fou `_acquire_safe_grab`. Migration = post-démo Anouste. ### 2.5. Capture fenêtre vs capture écran (cas Easily Assure dans Edge) Pour le cas spécifique de la démo (Easily Assure rendu dans Microsoft Edge plein écran sur Léa Windows) : - **Ne pas utiliser `PrintWindow`** : sur Edge moderne (Chromium/WebView2), `PrintWindow` retourne souvent une image noire ou figée (composition GPU contourne GDI). Issue connue : `Microsoft/microsoft-ui-xaml#7170`. - **Privilégier capture écran complet + crop** : c'est ce que `capture_active_window` fait déjà dans `capturer.py:381`. Conserver. - **Pour Edge fullscreen** : la frame DXGI inclut toujours le contenu Chromium même en mode exclusive (contrairement à GDI). DXcam est donc encore mieux ici. --- ## 3. Section D1 — Desktop distant sans accessibility tree ### 3.1. Table comparative — remote desktop pour RPA visuel (mai 2026) | Solution | Latence LAN | Couleur | Color depth | RPA-friendly | Freeze pattern | Verdict | |---|---:|---|---|---|---|---| | **NoMachine 9.5.7** | 15-30 ms | NX H.264 | 24 bpp | Moyen (clipboard cassé, input passive grab) | ⚠ **Confirmé** clics avalés après N min, forum officiel | Actuel, à remplacer dès Anouste | | **RustDesk** (open source) | 18-30 ms | VP9 | 24 bpp | Moyen | Pas de freeze connu, mais latence WAN x2 vs Parsec | Alternative crédible, on-prem possible | | **Parsec** | 7-10 ms | H.264 NVENC | 24/32 bpp | Bon (input fiable) | Aucun rapporté | Excellent mais cloud + fermé | | **AnyDesk** | 20-40 ms | DeskRT | 24 bpp | Bon, support commercial | Rare, restart fix | Standard entreprise, on-prem cher | | **Citrix Workspace** | variable | HDX (YUV420/YUV444) | 8-24 bpp configurable | **Difficile** (color depth réduit, lag) | Spécifique app | Terrain réel hôpital, accepter contraintes | | **RDP vanille** | 20-50 ms | RemoteFX/AVC | 16-32 bpp | Bon | "RDP freezing Win11 24H2" connu, fix par TCP-only | Acceptable mais Win-Win only | | **VNC** | 50-100 ms | divers | 8-24 bpp | Pauvre (latence input) | Variable | Éviter | ### 3.2. NoMachine 9.5.7 — analyse freeze **Pattern documenté** (forum officiel NoMachine, multiples threads depuis v4) : - *"NoMachine stops accepting mouse and keyboard input"* — persiste v4 → v8, **pas de fix officiel**. - *"Mouse click not working"*, *"Inputs suddenly stopped working"*. - Workaround unique remonté : toucher physiquement la souris sur l'hôte pour débloquer. **Cause probable** (lecture forum + connaissance archi) : NoMachine utilise du *passive grab* X11 pour propager les events Windows→Linux. Quand le buffer X11 est saturé (compositor lent, refresh display, sleep system court), le grab est libéré silencieusement mais NoMachine ne réinjecte plus les events. **Conséquences pour RPA :** 1. **Les clics côté Léa Windows partent**, mais ne sont pas vus par l'host Linux. 2. Pas de feedback d'erreur — Léa croit avoir cliqué. 3. La capture côté Léa Windows continue à montrer l'écran AVANT le clic (puisque le compositor host n'a rien repeint). 4. → **L'agent boucle sur "je vois l'écran avant clic, je reclique"** — c'est exactement le pattern du LoopDetector QW2. **Recommandations P1 (cf. §5) :** instrumenter un heartbeat actif côté client Léa qui détecte l'écran figé > 30 s et **pause supervisée explicite** ("la connexion NoMachine semble figée, restart NoMachine puis reprendre ?"). ### 3.3. Cradle (BAAI-Agents) — comment ils capturent en jeu vidéo Windows **Cradle** n'est pas de Microsoft (commune confusion) mais de BAAI (Beijing Academy of AI). Code GitHub : `BAAI-Agents/Cradle`. Leur stack capture (regard rapide du repo, à confirmer si pertinent) : - Capture via **`mss`** également (!), avec `monitor_index` configurable - Pour RDR2 / fullscreen exclusive DirectX : capture via **window-handle ciblé** par `pygetwindow` + `mss` sur la zone - Pas de wrapper DXGI custom dans la branche main → ils sont confrontés aux mêmes bugs que nous, leur env est juste plus contrôlé (un seul écran, pas de remote desktop intermédiaire) **Apprentissage :** Cradle n'a PAS résolu le problème, ils l'évitent en contrôlant l'environnement (PC dédié, un écran, pas de scaling). Notre setup remote desktop multi-écran est intrinsèquement plus difficile. ### 3.4. Patterns côté hôte vs côté viewer | Approche | Description | Avantages | Inconvénients | |---|---|---|---| | **Capture côté hôte distant** (Windows Léa) | Léa capture localement Windows, envoie au serveur Linux | Pixels physiques natifs, pas de re-encoding NoMachine | Bug DPI, nécessite agent sur l'hôte | | **Capture côté viewer Linux** (notre poste Dom) | Linux capture la fenêtre NoMachine via mss/dxcam Linux | Pas besoin d'agent Windows | "screenshots through screenshots", artefacts H.264 NoMachine, perte qualité OCR | | **Hybride : agent host + screenshot fallback viewer** | Pondération selon dispo réseau | Robuste | Complexité, désync entre les 2 sources | **Recommandation projet :** rester sur capture côté hôte (Léa Windows). C'est ce qui est implémenté et c'est la bonne décision : la qualité OCR sur capture re-encodée par NoMachine (H.264 lossy) est mauvaise (cf. bug OCR-DIRECT 8 mai sur tabs Easily). Si NoMachine devient bloquant, migrer vers **RustDesk auto-hébergé** plutôt que vers une capture côté viewer. ### 3.5. Heartbeat actif — détecter le freeze AVANT d'envoyer les clics Pattern à implémenter côté Léa Windows (P1, cf. §5) : 1. Toutes les 5 s, capture pHash de l'écran complet. 2. Comparer au pHash N-1. Si identique pendant **> 30 s** ET un input a été émis dans cette fenêtre → considérer connexion gelée. 3. Notifier serveur via heartbeat enrichi : `remote_session_status: "frozen_suspected"`. 4. Côté serveur, basculer en `replay_paused` automatique, dialogue VWB *"Restart NoMachine ?"*. Code de référence existant : `windows.forum.nomachine.com` confirme que toucher physiquement la souris débloque. Donc le restart NoMachine est la bonne action de récupération — mais il faut un humain. --- ## 4. Recommandations P0 — Fix bug coord Y intermittent (`executor.py:606-617`) Le code actuel `_resolve_via_uia_local` (lignes 606-617 d'`executor.py`) n'est PAS l'origine du bug coord Y — c'est UIA, pas la capture. Le bug racine est dans `capturer.py` (déjà partiellement traité par `_acquire_safe_grab`) ET dans le manque de déclaration DPI au démarrage. **Fix recommandé en 2 temps :** ### Fix #1 — Déclarer DPI awareness au démarrage du client Léa Ajouter en **première ligne** de `agent_v0/agent_v1/main.py` (avant tout autre import) : ```python # agent_v0/agent_v1/main.py — TOUT EN HAUT, avant tout import import platform if platform.system() == "Windows": import ctypes try: # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 # Cf. mss issue #197 : sans ça, mss.monitors retourne intermittemment # des dims tronquées (cas observé 2560×60 démo GHT 19 mai 2026). ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4)) except (AttributeError, OSError): try: ctypes.windll.shcore.SetProcessDpiAwareness(2) # PER_MONITOR (Win 8.1) except Exception: try: ctypes.windll.user32.SetProcessDPIAware() # legacy except Exception: pass # fallback silencieux si vraiment ancien ``` **Validation attendue :** après ce fix + restart Léa, `mss.monitors[1]` doit toujours retourner `2560×1600` (jamais `2560×60`). Si le bug persiste → c'est un autre composant qui modifie le DPI context après start (suspect : PyQt5 GUI). Investiguer via instrumentation. ### Fix #2 — Renforcer `_acquire_safe_grab` en filet de sécurité Le code actuel de `capturer.py:115-203` est **déjà solide**. Une seule amélioration : refuser le fallback secondaire dans **toutes** les méthodes coord-bearing (déjà fait pour `capture_dual`, vérifier que `capture_active_window` standalone fait pareil — c'est le cas L:371). **Aucune modification recommandée sur le fichier** dans le scope court terme. Le fix #1 suffit en théorie ; `_acquire_safe_grab` est la ceinture, pas la cause. ### Fix #3 — Coordonnées agent serveur Vérifier dans `executor.py` autour de L:606-617 (et plus largement) que **toute coord renvoyée au serveur** est en pixels physiques absolus du virtual desktop. Si ailleurs dans le code des pourcentages sont calculés AVEC `mss.monitors[1]['height']` qui peut être 60 → division par 60 → `y_pct × 1600 = grand nombre`. **Le bug Y ÷27 vient probablement de là, pas du clic en sortie**. Action : `grep -rn "monitors\[" agent_v0/agent_v1/` et auditer chaque site. Hors scope de ce doc. --- ## 5. Recommandations P1 — Détection + recovery freeze NoMachine ### Détection (côté client Léa) Ajouter à `capturer.py` un thread heartbeat de monitoring : ```python # pseudocode pour ajout heartbeat dans VisionCapturer import threading, time class FreezeDetector(threading.Thread): def __init__(self, capturer, on_freeze_callback, interval_s=5.0, freeze_threshold_s=30.0): super().__init__(daemon=True) self.capturer = capturer self.callback = on_freeze_callback self.interval = interval_s self.threshold = freeze_threshold_s self._last_hash = None self._last_change_ts = time.time() self._stop = threading.Event() def run(self): while not self._stop.is_set(): try: _mon, img = _acquire_safe_grab() if img is not None: h = self.capturer._compute_quick_hash(img) now = time.time() if h != self._last_hash: self._last_hash = h self._last_change_ts = now elif (now - self._last_change_ts) > self.threshold: self.callback(stale_for_s=(now - self._last_change_ts)) self._last_change_ts = now # éviter re-trigger en rafale except Exception: pass self._stop.wait(self.interval) def stop(self): self._stop.set() ``` ### Recovery (côté serveur / VWB) Sur réception d'un heartbeat enrichi `remote_session_status="frozen_suspected"` : 1. Pause replay (`replay_paused=True`) + bulle Léa *"Connexion NoMachine figée détectée"*. 2. Dialog VWB côté Dom : `[Restart NoMachine] [J'ai débloqué, reprendre] [Stop]`. 3. Tracer le freeze dans `data/runner_captures/freeze_events.jsonl` pour stats post-démo. --- ## 6. Dépendances / liens avec AXE B1 (transport) Le bug coord Y et le freeze NoMachine ont une **interaction critique** avec le bug transport diagnostiqué le 8 mai (`REPLAY_BLOCAGE_NOTES_MEDICALES_2026-05-08.md`) : - Si client Léa freeze (NoMachine) ET timeout HTTP client = 5 s → l'action est doublement perdue. - Le watchdog `_retry_pending` côté serveur (fix moyen-terme du doc 8 mai) doit s'articuler avec le heartbeat freeze : ne PAS re-dispatcher une action si la session client est suspectée gelée (sinon empilement). Recommandation transverse : **ne pas implémenter le watchdog `_retry_pending` sans intégrer le heartbeat freeze**. Sinon on multiplie les clics fantômes pendant un freeze. --- ## 7. Sources ### Bug `mss` DPI / monitors - [BoboTiG/python-mss#197 — GetSystemMetrics wrong after sct.grab](https://github.com/BoboTiG/python-mss/issues/197) — root cause DPI shift - [BoboTiG/python-mss#30 — monitors does not correspond with screen resolution](https://github.com/BoboTiG/python-mss/issues/30) - [BoboTiG/python-mss#108 — combined monitor image and monitors incorrect](https://github.com/BoboTiG/python-mss/issues/108) - [BoboTiG/python-mss#49 — secondary screen Windows 8.1](https://github.com/BoboTiG/python-mss/issues/49) - [BoboTiG/python-mss#257 — scaling factor 2 applied](https://github.com/BoboTiG/python-mss/issues/257) - [Lightrun answer — GetSystemMetrics wrong screen resolution](https://lightrun.com/answers/bobotig-python-mss-getsystemmetrics-module-returns-wrong-screen-resolution-after-running-sctgrabmonitor) ### DXGI / dxcam / windows-capture - [ra1nty/DXcam — high-performance Python DXGI Desktop Duplication, updated 2026](https://github.com/ra1nty/DXcam) - [DXcam Releases](https://github.com/ra1nty/DXcam/releases) - [dxcam PyPI](https://pypi.org/project/dxcam/0.1.0.dev2/) - [NiiightmareXD/windows-capture — Rust+Python DXGI+WGC](https://github.com/NiiightmareXD/windows-capture) - [windows-capture PyPI](https://pypi.org/project/windows-capture/) - [RootKit-Org/BetterCam — DXcam fork 240+Hz](https://github.com/RootKit-Org/BetterCam) - [Microsoft Learn — Desktop Duplication API](https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api) - [microsoft/Windows-classic-samples — DXGIDesktopDuplication](https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/DXGIDesktopDuplication/README.md) - [Kyle Fu — Python Fast Screen Capture benchmark](https://kylefu.me/2023/02/18/python-fast-screen-capture.html) - [ScreenshotOne — How to Capture Desktop Screen with DXcam in Python](https://screenshotone.com/blog/dxcam-python-screenshots/) - [SerpentAI/D3DShot — historique, quasi abandonné](https://github.com/SerpentAI/D3DShot) ### DPI awareness Windows - [Microsoft Learn — SetProcessDpiAwarenessContext](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocessdpiawarenesscontext) - [Microsoft Learn — Setting default DPI awareness for a process](https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process) - [Microsoft Learn — DPI_AWARENESS_CONTEXT handle](https://learn.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context) - [Win32 DPI And Monitor Scaling — gist marler8997](https://gist.github.com/marler8997/9f39458d26e2d8521d48e36530fbb459) - [Python issue 33656 — IDLE DPI awareness on Windows](https://bugs.python.org/issue33656) ### NoMachine freeze (forum officiel) - [NoMachine stops accepting mouse and keyboard input](https://forum.nomachine.com/topic/nomachine-stops-accepting-mouse-and-keyboard-input) - [Mouse click not working](https://forum.nomachine.com/topic/mouse-click-not-working) - [NoMachine connects but doesn't accept keyboard or mouse input](https://forum.nomachine.com/topic/nomachine-connects-but-doesnt-accept-keyboard-or-mouse-input) - [Inputs suddenly stopped working](https://forum.nomachine.com/topic/inputs-suddenly-stopped-working) - [Mouse click unresponsive until Alt-key pressed](https://forum.nomachine.com/topic/mouse-click-unresponsive-until-alt-key-pressed) - [Mouse freeze after forwarding](https://forum.nomachine.com/topic/mouse-freeze-after-forwarding) ### Alternatives remote desktop - [Bundl — RustDesk vs Parsec 2026](https://bundl.run/compare/rustdesk-vs-parsec) - [QuantVPS — Parsec vs RDP vs Rustdesk](https://www.quantvps.com/blog/parsec-vs-rdp-vs-rustdesk) - [Ultimate Systems Blog — AnyDesk vs Parsec vs RustDesk Showdown](https://blog.usro.net/2025/06/anydesk-vs-parsec-vs-rustdesk-showdown/) - [Fileion — AnyDesk vs RustDesk 2026](https://fileion.com/blog/anydesk-vs-rustdesk-best-free-remote-desktop-2026) - [AlternativeTo — NoMachine Alternatives](https://alternativeto.net/software/nomachine/) - [Cendio ThinLinc — NoMachine Alternative](https://www.cendio.com/blog/nomachine-alternative/) - [TechTarget — Windows 11 Remote Desktop freezing](https://www.techtarget.com/searchvirtualdesktop/tip/What-to-do-when-a-Windows-11-remote-desktop-keeps-freezing) - [Windows Forum — RDP Freezing 24H2](https://windowsforum.com/threads/how-to-fix-rdp-freezing-issues-in-windows-11-24h2-complete-guide.362476/) ### Citrix / RDP / accessibility tree - [Citrix Docs — Graphics policy settings](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/graphics-policy-settings.html) - [Citrix Docs — Visual display policy](https://docs.citrix.com/en-us/xenapp-and-xendesktop/7-15-ltsr/policies/reference/ica-policy-settings/visual-display-policy-settings.html) - [Citrix CTX202687 — HDX Graphics Modes DCR/Thinwire/H.264](https://support.citrix.com/article/CTX202687) - [UiPath — Citrix Automation](https://www.uipath.com/platform/agentic-automation/ai-ecosystem/citrix-automation) ### Cradle / agent computer use - [BAAI-Agents/Cradle](https://github.com/BAAI-Agents/Cradle) - [arXiv 2403.03186 — Cradle: Empowering Foundation Agents Towards General Computer Control](https://arxiv.org/abs/2403.03186) - [Cradle project page](https://baai-agents.github.io/Cradle/) - [CursorTouch/Windows-MCP — MCP Server for Computer Use in Windows](https://github.com/CursorTouch/Windows-MCP) --- *Document de recherche, lecture seule. Toute implémentation nécessite arbitrage explicite de Dom — la migration `mss → dxcam` est NON urgente tant que la ceinture `_acquire_safe_grab` tient. Le fix DPI awareness §4.1 est par contre **chirurgical, 8 lignes, à valider rapidement** (post-démo client Anouste).*