Fiche U7b-T1 — Niveau N3 — Modèle prévu lp-cart | utilisé claude-opus-5-5 — Agent lp-cart — Skills sk-protocol, lp-paths (préchargés aussi : ue-cpp-build, lp-commandlets) # U7b-T1 : cartographie de la capture des planches de la tournée Mesure (fenêtre minimisée, UE 5.8) Date : 2026-10-09. Lecture seule : rien n'a été modifié, lancé ni commité. Addon = worktree `D:\Game\LifePlay-Sandbox-5.8`, branche `ue5.8`, HEAD `d1efdd1`, `git status --porcelain` vide. Moteur = `D:\Game\Launcher\Engine\UE_5.8`, 5.8.3, CL 58210709 (`Engine/Build/Build.version`), sources présentes. Dans ce document, `E58` = `D:\Game\Launcher\Engine\UE_5.8\Engine\Source\Runtime`. **Écart à la fiche (méthode § 4)** : aucun outil de recherche web (WebSearch / WebFetch) n'est disponible dans cette session. Les sources moteur 5.8 installées (source primaire, règle d'or de `ue-cpp-build`) les remplacent. Je ne cite aucune URL parce que je n'en ai consulté aucune. Si l'orchestrateur veut un recoupement par la documentation Epic, il faut une session qui a un accès web. ## 1. Où et comment la tournée capture aujourd'hui (établi) ### 1.1 Lancement - Pilote : `Tools\anim\AnimPipeline.ps1`, fonction `Invoke-Sandbox` (l. 300-356). Il écrit `Saved\sandbox_mode.txt = Mesure` et `Saved\sandbox_tour.txt = vignettes planches quitter` (l. 329-331), puis lance `Tools\debug\LifePlay_interaction_sandbox.bat` (l. 306 et 334, `Start-Process cmd.exe ... -WindowStyle Hidden`). Ensuite, il attend que le processus `UnrealEditor` se termine, avec un plafond de 40 min (l. 338-340). - Lanceur `Tools\debug\LifePlay_interaction_sandbox.bat:9` : `UnrealEditor.exe "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 "-ini:Engine:[...]:GameInstanceClass=..."`. Le .bat ne transmet pas d'arguments supplémentaires (`%*` absent). - Le même .bat sert aussi à **l'ouverture interactive** : `Tools\BuildMod.ps1:165` (`Open-AnimTarget -Which sandbox`) et `README.md:53`. Changer son comportement par défaut touche donc aussi l'usage manuel. ### 1.2 Capture (fichier `Plugins\LPSandbox\Source\LPSandbox\LPSandboxMeasureTour.cpp`) - Le mode de capture est unique : `FScreenshotRequest::RequestScreenshot(, false, false)`, c'est-à-dire sans interface et sans suffixe. Il y a **4 appels** (`grep -c` = 4) : - l. 127 : passages automatiques (`TickAuto`) ; - l. 541 : planches (`TickTour`, `bPlancheTour`) ; - l. 561 : vignettes ; - l. 566 : tournée simple. - Les images ne sont pas écrites telles quelles. Elles passent par le délégué `UGameViewportClient::OnScreenshotCaptured()`, branché l. 427 (`SavePlancheFrame`) et l. 433 (`SaveVignette`), débranché l. 458 et 468 (4 occurrences, `grep -c` = 4). Ensuite : - `SavePlancheFrame` (l. 611-619) empile les `TArray` ; - `WritePlanche` (l. 621-681) recadre le carré commun, ramène chaque image à 256 px, encode en JPEG qualité 85 et écrit `.jpg` et `.json`. Il trace `PLANCHE ... (carre N px)` à la l. 679. - Le cadrage se fait dans l'espace écran du viewport : `ProjectWorldLocationToScreen` des os (l. 533-539 pour les planches, l. 552-558 pour les vignettes), appliqué ensuite sur l'image reçue (l. 632-642, 581-591). **Toute solution doit donc garder une image de la même taille que le viewport (1600×900)**, sinon le carré ne tombe plus au même endroit. - L'addon n'a aucune autre voie de capture : `HighResShot`, `SceneCapture` et `ReadPixels` n'apparaissent dans aucun fichier de `Plugins\LPSandbox\Source` (`grep -rln`, code 1). Aucune garde « fenêtre minimisée » n'existe non plus : `IsWindowMinimized`, `RenderOffScreen` et `bAllowMinimize` n'apparaissent ni dans les sources ni dans les outils de l'addon, ni dans `Config` du jeu 5.8. - `LPSandboxMeasureTour.cpp` est **identique** entre `ue5.8` et la branche addon 5.5 `lot/DC3-atelier` (`git diff --stat ue5.8 lot/DC3-atelier -- ` : seuls le .bat et `AnimPipeline.ps1` diffèrent). ### 1.3 Chemin moteur d'une capture (5.8) 1. `FViewport::Draw` continue tant que `SizeX > 0 && SizeY > 0` (`E58\Engine\Private\UnrealClient.cpp:1789`, test l. 1820). Pendant la minimisation, c'est la taille de `FSceneViewport` qui compte (1600×900), pas celle de la fenêtre. 2. `UGameViewportClient::ProcessScreenShots` (`E58\Engine\Private\GameViewportClient.cpp:2369`) passe par la branche sans interface → `GetViewportScreenShot` (`UnrealClient.cpp:2407-2416`) → `FRenderTarget::ReadPixels` (`UnrealClient.cpp:61-79`). Comme le rectangle demandé est vide, il est remplacé par `(0,0,GetSizeXY())`, soit **1600×900**. La lecture porte sur `GetShaderResourceTexture()`. 3. En rendu direct à la fenêtre, la cible du viewport est le back buffer Slate : `Target = ViewportProvider->GetBackBufferResource()` (`E58\Engine\Private\Slate\SceneViewport.cpp:2289-2300`). 4. Les images arrivent ensuite dans le délégué (`GameViewportClient.cpp:2543-2552`). ## 2. Pourquoi la fenêtre minimisée plante ### 2.1 Chaîne établie (sources 5.8 et journal) 1. Minimisation → `WM_SIZE` avec `SIZE_MINIMIZED`, zone client 0×0 → `MessageHandler->OnSizeChanged(..., 0, 0, bWasMinimized)` (`E58\ApplicationCore\Private\Windows\WindowsApplication.cpp:3943-3966`). 2. `FSlateApplication::OnSizeChanged` → `Renderer->RequestResize(Window, 0, 0)` (`E58\Slate\Private\Framework\Application\SlateApplication.cpp:6960-6970`). 3. `FSlateRHIRenderer::RequestResize` → `FSlateViewportInfo::ResizeViewport` (`E58\SlateRHIRenderer\Private\SlateRHIRenderer.cpp:2114-2123`). `ClampExtent` porte la taille à `MIN_VIEWPORT_SIZE` = **8** (l. 78, 287), puis `RHIResizeViewport(8, 8)` (l. 537, 576). Le back buffer `BackBuffer0` mesure alors 8×8. 4. Le viewport de jeu reste à 1600×900 et la capture demande donc une copie de 1600×900 depuis un back buffer de 8×8. `FD3D12DynamicRHI::GetStagingTexture` appelle `CopyTextureRegionChecked` (`E58\D3D12RHI\Private\D3D12RenderTarget.cpp:472`). Le contrôle `RHI_BREADCRUMB_CHECK_SHIPPINGF(CopyBox.right <= SubresourceWidth)` échoue (`E58\D3D12RHI\Private\D3D12Texture.cpp:1551`). 5. Le journal de la 1re tentative U4-T6 (`D:\Game\LifePlay_Backup_5.5\U4-T6\E4_quaternius_echec1.log`) le confirme : - l. 6721-6722 : `Source width out of bounds for: 'BackBuffer0' -> 'None' ... dimensions [8, 8, 1], copying from [0, 0, 0] to [1600, 900, 1]` ; - l. 6739 : callstack `GetStagingTexture ... D3D12RenderTarget.cpp:483` (attribution de ligne d'un appel sur plusieurs lignes, l. 472-483). ### 2.2 Pourquoi 5.5 ne plantait pas - **Établi** : en 5.5, `GetStagingTexture` appelle directement `GraphicsCommandList()->CopyTextureRegion` (`UE_5.5\...\D3D12RenderTarget.cpp:472`). Le texte « out of bounds » ne figure nulle part dans `UE_5.5\...\D3D12Texture.cpp` (`grep -c` = 0). Le contrôle de bornes est donc **nouveau en 5.8**. - **Estimé** : en 5.5, une fenêtre minimisée aurait produit une copie invalide sans erreur (images fausses ou noires) plutôt qu'un plantage. L'assertion 5.8 transforme une corruption silencieuse en arrêt net. - **Estimé** : une fenêtre seulement recouverte, ou un écran verrouillé, ne déclenche pas `WM_SIZE`/`SIZE_MINIMIZED`. Le back buffer garde alors sa taille et la capture ne plante pas. Seule la minimisation est en cause (non essayé). - **Établi** (relance T6-bis, `U4-T6-VAL.md:110-120`) : avec la fenêtre visible, il n'y a aucune assertion et les codes sont 0. ### 2.3 Pas de réglage moteur « rendre même minimisé » - `grep -rli "ForceRenderingWhenMinimized|RenderWhenMinimized"` sur `E58` (*.cpp) ne trouve rien : **ce CVar n'existe pas en 5.8**. - `tick.MinimizedSyncDrawToGPU` (`E58\Engine\Private\GameEngine.cpp:105-109`) ne fait que synchroniser le GPU quand la fenêtre est cachée (l. 2057). Il ne règle pas la taille du back buffer. ## 3. Référence de non-régression : ce qui est comparable (établi) - **Les pixels ne sont déjà pas identiques entre 5.5 et 5.8**, fenêtre visible. Sur les 20 images de freeanims (`U4-REF55\freeanims\sandbox` et `U4-T6\freeanims\sandbox`), **0 sur 20** ont le même SHA-256. Écart de taille : planches -1,4 % à +2,5 %, vignettes +3,5 % à +19 % (`sha256sum`/`stat`, boucle sur les 30 fichiers). Le critère « pixels identiques » est donc hors d'atteinte quelle que soit l'option, y compris l'état actuel. - Trois points sont en revanche identiques : - les `.json` des planches (10/10, SHA-256) ; - les dimensions (20/20 : planches 6144×256, ou 3328×256 pour 13 images ; vignettes 512×512, lues dans l'en-tête par le script `imgdims.py` du bloc-notes, `python -I`) ; - surtout, **la géométrie de recadrage** : lignes `VIGNETTE ... (carre N px a X,Y)` et `PLANCHE ... N images D s (carre N px)`, une fois le chemin et le nombre d'octets retirés : **freeanims 20/20 et quaternius 86/86 identiques 5.5 / 5.8**. Commande : `grep -E "PLANCHE |VIGNETTE "` sur `U4-REF55\E4_.log` et `U4-T6\E4_.log`, `sed` des chemins et des octets, puis `diff`, qui ne signale aucune différence. - **Critère proposé** pour U7b : code 0 ; 0 `Assertion failed` ; géométrie 106/106 identique à `U4-REF55` ; `.json` des planches identiques ; dimensions identiques ; contrôle visuel d'une planche par pack. Cette géométrie dépend de la taille du viewport (1600×900) et de la pose : elle détecte tout changement de résolution ou de cadrage. ## 4. Options | Option | Principe | Points d'appel à changer | Effort | Risque | Non-régression attendue | |---|---|---|---|---|---| | **A. `-RenderOffScreen`** (sans C++) | Pas de fenêtre système : Slate rend dans une texture, donc pas de `WM_SIZE` ni de back buffer 8×8. Même chaîne de capture. | `.bat:9` : transmettre `%*`, ou un .bat dédié au pilote. `AnimPipeline.ps1:334` : ajouter `-RenderOffScreen`. Libellé `:317`. Ne pas changer le défaut du .bat (`BuildMod.ps1:165`). | XS, 2 à 5 lignes, 0 C++ | Faible à moyen (voir 4.1) | Géométrie identique attendue (même viewport, même lecture) : **estimé**. Pixels : variation déjà présente aujourd'hui. | | **B. SceneCapture2D → render target 1600×900 → `ReadPixels`** | Capture hors du back buffer : on lit la texture du render target (`UnrealClient.cpp:61-79`), quel que soit l'état de la fenêtre. | `LPSandboxMeasureTour.cpp` : les 4 `RequestScreenshot` et les 4 branchements du délégué remplacés par un composant de capture qui copie la vue (position, rotation, FOV) du `PlayerCameraManager`. | M à L, environ 100 à 150 lignes C++ ; compilation 5.8, plus 5.5 si le fichier reste commun | Moyen à élevé | Géométrie identique seulement si la taille et le FOV sont copiés exactement. **Rendu différent** : historique TAA/TSR, exposition auto, post-process, drapeaux d'affichage (`SceneCaptureComponent.h:130-132`, `SceneCaptureComponent2D.h:174-192`). Il faut une nouvelle référence visuelle. | | **C1. `bAllowMinimize=False`** (sans C++) | Retire le bouton de réduction : `GetProjectSettingBool` lit `[/Script/EngineSettings.GeneralProjectSettings]` de `GGameIni` (`GameEngine.cpp:539-542, 558, 693`) → pas de `WS_MINIMIZEBOX` (`E58\ApplicationCore\Private\Windows\WindowsWindow.cpp:143-145`). Surcharge possible en ligne de commande : `-ini:Game:[/Script/EngineSettings.GeneralProjectSettings]:bAllowMinimize=False` (syntaxe générique, `E58\Core\Private\Misc\ConfigCacheIni.cpp:2322-2324`). | `.bat:9` ou argument du pilote. | XS | Moyen : couverture incomplète. Win+D, Win+M, clic sur la barre des tâches ou réduction programmée restent possibles (**incertain**, non vérifié). | Pixels et géométrie inchangés (même chemin). | | **C2. Garde dans la tournée** (C++) | Avant chaque `RequestScreenshot`, si `GEngine->GameViewport->GetWindow()->IsWindowMinimized()` (`SWindow.h:758`, `GameViewportClient.h:362`), alors `Restore()` (`SWindow.h:928`), journaliser, reculer l'étape (`--TourStep`, comme l. 516-519) et attendre. | `LPSandboxMeasureTour.cpp` : un point commun avant les l. 127, 541, 561 et 566. | S, environ 15 à 25 lignes | Faible à moyen : la fenêtre reprend le premier plan. Une fenêtre réduite entre le tick et le rendu dans la même image : **estimé** impossible, puisque les messages sont traités en début d'image. | Pixels et géométrie inchangés (même chemin), après au moins une image d'attente une fois la fenêtre restaurée. | ### 4.1 Détail de l'option A (établi dans les sources 5.8, sauf mention) - Sous Windows, `-RenderOffScreen` remplace l'application Windows par l'application nulle (`E58\ApplicationCore\Private\Windows\WindowsPlatformApplicationMisc.cpp:173-176`). Cette voie est prévue sous Windows (`Null\NullPlatformApplicationMisc.cpp:14-20`, commentaire Epic « supported on Linux and Windows »). - La fenêtre est une `FNullWindow` et `IsRenderingOffScreen()` vaut `true` (`Null\NullWindow.cpp:251-254`). Slate crée alors une texture `SlateOffScreenViewportTexture` à la place d'une chaîne d'échange (`SlateRHIRenderer.cpp:321-324, 340-353`), et `GetBackBufferResource` renvoie cette texture (`SlateRHIRenderer.cpp:471-474`). Le viewport D3D12 n'a pas de chaîne d'échange (`E58\D3D12RHI\Private\Windows\WindowsD3D12Viewport.cpp:34`). - Il n'y a ni écran de démarrage (`E58\Launch\Private\LaunchEngineLoop.cpp:3111`) ni fenêtre système, donc ni minimisation ni `WM_SIZE`. Slate dessine quand même la fenêtre (`SlateApplication.cpp:1249`). La taille de la fenêtre n'est pas ajustée à la zone de travail (`SceneViewport.cpp:1718-1720`). - Risques à lever par l'essai décisif : - (a) **taille réelle du viewport**. La zone de travail de l'application nulle part de `r.SetRes` (`Null\NullApplication.cpp:103-112`). La config pose `r.setres:1280x720` (`E4_freeanims.log:523`) avant que `-ResX=1600 -ResY=900` ne s'applique. La géométrie identique prouvera que la taille est bien 1600×900. - (b) **rythme des images** : sans présentation à l'écran, il n'y a ni vsync ni limite d'images. Les attentes de la tournée sont en secondes (`LoadWait` 0,9 s, `CameraWait` 0,5 s, l. 206-207). La géométrie ne doit donc pas bouger, mais la convergence TAA peut changer les pixels (**estimé**). - (c) aucune fenêtre visible pendant la tournée : l'utilisateur ne la voit plus. Le pilote n'attend que le processus, donc c'est sans effet sur lui. - (d) l'application nulle ne reçoit pas de clavier. C'est sans effet : la tournée est pilotée par `sandbox_tour.txt` (l. 290-311) et se ferme par `RequestExit` (l. 356). ## 5. Recommandation 1. **A d'abord**, réservée au pilote. Le .bat transmet `%*`, ou un .bat dédié est créé, et `AnimPipeline.ps1:334` passe `-RenderOffScreen`. Le défaut interactif reste inchangé. Cette option supprime la cause (pas de back buffer de fenêtre), sans C++ et sans nouvelle compilation, sur les deux moteurs. 2. **Si A échoue** (géométrie différente, plantage, viewport ≠ 1600×900) : **C2**, une garde de quelques lignes sur le même chemin de capture, donc une non-régression assurée par construction. 3. **B en dernier recours** seulement : c'est la voie la plus coûteuse, elle change le rendu et impose une nouvelle référence visuelle. 4. C1 n'est utile qu'en complément : elle ne couvre pas tous les moyens de réduire une fenêtre. ## 6. Essai décisif (à faire dans le lot IMPL/VAL, pas ici) 1. **Pré-essai court**, d'une à deux minutes. Reproduire `Invoke-Sandbox` à la main pour un seul clip : `sandbox_pack.ini` du pack freeanims, `sandbox_mode.txt = Mesure`, `sandbox_tour.txt = vignettes planches quitter AS_RightLegKick2` (liste partielle autorisée, l. 294-303). Lancer avec `-RenderOffScreen`. Attendu : code 0, 0 `Assertion failed`, et les lignes `VIGNETTE ... (carre 695 px a 621,173)` et `PLANCHE ... 13 images 1.10 s ... (carre 724 px)`, identiques à U4-REF55. 2. **Essai complet** : `AnimPipeline.ps1 -Pack freeanims -From 5 -To 5` (446 s en 5.8). Comparer la géométrie (20/20), les `.json` des planches (10/10) et les dimensions (20/20) à `U4-REF55\freeanims`, avec le même `diff` qu'au § 3. Puis quaternius (86/86, environ 1210 s). 3. **Essai contraire**, si C2 est retenu : lancer fenêtre visible, la réduire pendant les planches. Attendu : ligne de journal « fenêtre restaurée », code 0, géométrie identique. Sans la garde, le plantage `D3D12Texture.cpp:1551` doit se reproduire : c'est la preuve par l'échec, à documenter avant le correctif. ## 7. Preuves, incertitudes - **Établi** : §§ 1, 2.1, 2.2 (premier point), 2.3, 3 et 4.1 (mécanique), avec les lignes et commandes citées. - **Estimé** : - comportement 5.5 en fenêtre minimisée (corruption silencieuse) ; - fenêtre recouverte ou écran verrouillé sans effet ; - géométrie conservée sous `-RenderOffScreen` ; - absence de course dans C2. - **Incertain** : - taille effective du viewport sous `-RenderOffScreen` avec `r.setres:1280x720` en config (essai 6.1) ; - couverture de C1 face à Win+D / Win+M ; - déterminisme des pixels d'une exécution 5.8 à l'autre (aucune paire 5.8/5.8 conservée) ; - recoupement par la documentation Epic, non fait faute d'outil web.