28 KiB
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) :
- Le process Léa Windows démarre DPI-unaware (défaut Python 3.x sur Windows sans
SetProcessDpiAwarenessContextexplicite). mss.mss()au premier appel passe parctypes.windll.user32et modifie implicitement le DPI context du thread (effet de bord internemsspour pouvoir capturer en pixels physiques sur écran HiDPI).- À partir de là,
MONITORINFO.rcMonitorpeut 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). - 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)
# 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
- Déclarer le DPI awareness TÔT dans le process (avant tout
import mss, avantfrom PyQt5 import ...). Idéalement dansmain.pydu client Léa, ligne 1-5. - Une fois en V2,
mss.monitors[i]['width'/'height']est cohérent avec les coords composite Windows (logique = physique pour le process). - 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.
- Pour cliquer en pixels physiques : utiliser
SendInput(ctypes.windll.user32.SendInput) plutôt quepyautogui.click—pyautoguire-divise par le DPI scale. - 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
# 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=0correspond 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),PrintWindowretourne 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_windowfait déjà danscapturer.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 :
- Les clics côté Léa Windows partent, mais ne sont pas vus par l'host Linux.
- Pas de feedback d'erreur — Léa croit avoir cliqué.
- La capture côté Léa Windows continue à montrer l'écran AVANT le clic (puisque le compositor host n'a rien repeint).
- → 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 (!), avecmonitor_indexconfigurable - Pour RDR2 / fullscreen exclusive DirectX : capture via window-handle ciblé par
pygetwindow+msssur 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) :
- Toutes les 5 s, capture pHash de l'écran complet.
- 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.
- Notifier serveur via heartbeat enrichi :
remote_session_status: "frozen_suspected". - Côté serveur, basculer en
replay_pausedautomatique, 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) :
# 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 :
# 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" :
- Pause replay (
replay_paused=True) + bulle Léa "Connexion NoMachine figée détectée". - Dialog VWB côté Dom :
[Restart NoMachine] [J'ai débloqué, reprendre] [Stop]. - Tracer le freeze dans
data/runner_captures/freeze_events.jsonlpour 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_pendingcô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 — root cause DPI shift
- BoboTiG/python-mss#30 — monitors does not correspond with screen resolution
- BoboTiG/python-mss#108 — combined monitor image and monitors incorrect
- BoboTiG/python-mss#49 — secondary screen Windows 8.1
- BoboTiG/python-mss#257 — scaling factor 2 applied
- Lightrun answer — GetSystemMetrics wrong screen resolution
DXGI / dxcam / windows-capture
- ra1nty/DXcam — high-performance Python DXGI Desktop Duplication, updated 2026
- DXcam Releases
- dxcam PyPI
- NiiightmareXD/windows-capture — Rust+Python DXGI+WGC
- windows-capture PyPI
- RootKit-Org/BetterCam — DXcam fork 240+Hz
- Microsoft Learn — Desktop Duplication API
- microsoft/Windows-classic-samples — DXGIDesktopDuplication
- Kyle Fu — Python Fast Screen Capture benchmark
- ScreenshotOne — How to Capture Desktop Screen with DXcam in Python
- SerpentAI/D3DShot — historique, quasi abandonné
DPI awareness Windows
- Microsoft Learn — SetProcessDpiAwarenessContext
- Microsoft Learn — Setting default DPI awareness for a process
- Microsoft Learn — DPI_AWARENESS_CONTEXT handle
- Win32 DPI And Monitor Scaling — gist marler8997
- Python issue 33656 — IDLE DPI awareness on Windows
NoMachine freeze (forum officiel)
- NoMachine stops accepting mouse and keyboard input
- Mouse click not working
- NoMachine connects but doesn't accept keyboard or mouse input
- Inputs suddenly stopped working
- Mouse click unresponsive until Alt-key pressed
- Mouse freeze after forwarding
Alternatives remote desktop
- Bundl — RustDesk vs Parsec 2026
- QuantVPS — Parsec vs RDP vs Rustdesk
- Ultimate Systems Blog — AnyDesk vs Parsec vs RustDesk Showdown
- Fileion — AnyDesk vs RustDesk 2026
- AlternativeTo — NoMachine Alternatives
- Cendio ThinLinc — NoMachine Alternative
- TechTarget — Windows 11 Remote Desktop freezing
- Windows Forum — RDP Freezing 24H2
Citrix / RDP / accessibility tree
- Citrix Docs — Graphics policy settings
- Citrix Docs — Visual display policy
- Citrix CTX202687 — HDX Graphics Modes DCR/Thinwire/H.264
- UiPath — Citrix Automation
Cradle / agent computer use
- BAAI-Agents/Cradle
- arXiv 2403.03186 — Cradle: Empowering Foundation Agents Towards General Computer Control
- Cradle project page
- CursorTouch/Windows-MCP — MCP Server for Computer Use in Windows
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).