From b8414af456eca0b8d3da7c050948f3a7204bb3e1 Mon Sep 17 00:00:00 2001 From: edujiro5972 <336072401+edujiro5972@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:49:01 +0200 Subject: [PATCH] =?UTF-8?q?Dbg=20:=20outillage=20de=20debug=20(BuildDebug,?= =?UTF-8?q?=20LPDebugSubsystem,=20lanceurs,=20=C3=A9v=C3=A9nements=20LoadT?= =?UTF-8?q?xt=20/=20LPDataCache),=20documents=2018=20et=2019?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase Dbg (CHG-114, 117, 118). Co-Authored-By: Claude Opus 5.5 --- Docs/Migration/08_Guide_Developpeur.md | 13 +- Docs/Migration/18_Build_Debug.md | 289 ++++++++++ Docs/Migration/19_Plan_Outillage_Debug.md | 285 ++++++++++ Docs/Migration/README.md | 2 + .../ModdingExtra/Private/LPDataCache.cpp | 9 + .../Private/ModdingExtraBPLibrary.cpp | 6 + Source/LifePlay/LPDebugSubsystem.cpp | 501 ++++++++++++++++++ Source/LifePlay/LPDebugSubsystem.h | 52 ++ Source/LifePlay/LPModSettingsLibrary.cpp | 6 +- Tools/BuildDebug.ps1 | 79 +++ Tools/CollectDebugArtifacts.ps1 | 100 ++++ Tools/SummarizeDebugCsv.ps1 | 63 +++ Tools/debug/LifePlay_debug_bugs.bat | 4 + Tools/debug/LifePlay_debug_collision.bat | 5 + Tools/debug/LifePlay_debug_gpu.bat | 5 + .../LifePlay_debug_sessionA_insights.bat | 7 + Tools/debug/LifePlay_debug_sessionB_mem.bat | 6 + .../debug/LifePlay_debug_sessionC_profil.bat | 5 + Tools/debug/LifePlay_debug_ui.bat | 4 + Tools/debug/LifePlay_debug_ui.ps1 | 275 ++++++++++ 20 files changed, 1708 insertions(+), 8 deletions(-) create mode 100644 Docs/Migration/18_Build_Debug.md create mode 100644 Docs/Migration/19_Plan_Outillage_Debug.md create mode 100644 Source/LifePlay/LPDebugSubsystem.cpp create mode 100644 Source/LifePlay/LPDebugSubsystem.h create mode 100644 Tools/BuildDebug.ps1 create mode 100644 Tools/CollectDebugArtifacts.ps1 create mode 100644 Tools/SummarizeDebugCsv.ps1 create mode 100644 Tools/debug/LifePlay_debug_bugs.bat create mode 100644 Tools/debug/LifePlay_debug_collision.bat create mode 100644 Tools/debug/LifePlay_debug_gpu.bat create mode 100644 Tools/debug/LifePlay_debug_sessionA_insights.bat create mode 100644 Tools/debug/LifePlay_debug_sessionB_mem.bat create mode 100644 Tools/debug/LifePlay_debug_sessionC_profil.bat create mode 100644 Tools/debug/LifePlay_debug_ui.bat create mode 100644 Tools/debug/LifePlay_debug_ui.ps1 diff --git a/Docs/Migration/08_Guide_Developpeur.md b/Docs/Migration/08_Guide_Developpeur.md index 48f0c01..88c1ecc 100644 --- a/Docs/Migration/08_Guide_Developpeur.md +++ b/Docs/Migration/08_Guide_Developpeur.md @@ -98,10 +98,10 @@ $Proj = 'D:\Game\LifePlay-master 5.5\LifePlay.uproject' -archivedirectory=D:\Game\LifePlay_Build_5.5_ -unattended -utf8output -nop4 ``` -- **Pas de `-pak`** : comme la version de l'auteur (`UsePakFile=False`), le jeu est livré en fichiers - nus ; `Modules` doit rester modifiable par les joueurs et les mods. +- Depuis le lot O2 le contenu est en conteneurs IoStore (`UsePakFile`, `bUseIoStore`, `bCompressed` + dans `DefaultGame.ini`). `Modules` reste en fichiers nus. - `-clientconfig=Shipping` pour une version distribuable (ne journalise pas) ; `Development` pour - toute mesure. + toute mesure. Builds de plongée : `Tools\BuildDebug.ps1` (`18_Build_Debug.md`), jamais `-nodebuginfo`. - Contrôle T-PKG-01 : ≥ 8 555 `.uasset` sous `Windows/LifePlay/Content` (8 561 attendus). - Le résultat est dans `\Windows\LifePlay.exe`. @@ -218,8 +218,11 @@ void UMonSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(Tic 4. Pour un état d'exécution (positions, variables), utiliser les options `-LPDump*` et `-LPTrace*` (`09_Commandes_Utiles.md` §4) dans un fichier `.bat` placé à côté de l'exécutable, que l'utilisateur lance à la place de `LifePlay.exe`. -5. Pour un graphe : `LPDumpFunction` sur les deux arbres, puis diff. -6. Pour un écart de comportement du moteur : lire les sources moteur 4.27 et 5.5 sur les cas limites ; +5. Pour une mesure de performance (à-coups, mémoire, non-régression) : build empaquetée via + `Tools\BuildDebug.ps1` et sessions A/B/C (`18_Build_Debug.md`). Pas `UnrealEditor.exe -game` + pour des millisecondes comparables à O0. +6. Pour un graphe : `LPDumpFunction` sur les deux arbres, puis diff. +7. Pour un écart de comportement du moteur : lire les sources moteur 4.27 et 5.5 sur les cas limites ; en faire un test (`LPTestStringCompat`). Inspection en direct : avec l'éditeur ouvert et le serveur MCP lancé (`McpAutomationBridge`, port 3000), diff --git a/Docs/Migration/18_Build_Debug.md b/Docs/Migration/18_Build_Debug.md new file mode 100644 index 0000000..b1b8c49 --- /dev/null +++ b/Docs/Migration/18_Build_Debug.md @@ -0,0 +1,289 @@ +# 18 — Build debug (mesure, pas optimisation) + +Capacité de mesure reproductible après l'audit O0. Le Shipping joueur ne change pas +(`IncludeDebugFiles=False` dans `Config/DefaultGame.ini`, aucun coût runtime : le sous-système +est compilé vide en Shipping). Skill agent : `.cursor/skills/lifeplay-debug-build/`. +Plan de pilotage du lot : [`19_Plan_Outillage_Debug.md`](19_Plan_Outillage_Debug.md). + +## 1. Deux builds + +| Config | Rôle | Comparer les ms absolues | +|---|---|---| +| **Development** | Baseline chiffrée, session C, non-régression vs O0 | Oui | +| **DebugGame** | Plongée (checks, PDB, sessions A et B) | Non — ratios, signatures, piles | +| **Shipping** | Joueurs | Hors sujet | + +Ne pas reconstruire DebugGame à chaque itération. Itérer dans l'éditeur ou en Development ; +reconstruire DebugGame seulement quand les checks, les PDB ou un outil externe l'exigent. + +## 2. Empaqueter + +```powershell +.\Tools\BuildDebug.ps1 [-Config DebugGame|Development] [-ArchiveDir ] [-UE ] [-SkipBuild] +``` + +- Config par défaut : `DebugGame`. +- Archives : `D:\Game\LifePlay_Build_5.5_DebugGame` ou `D:\Game\LifePlay_Build_5.5_Development`. +- Jamais `-nodebuginfo`. Le PDB `Binaries\Win64\*Win64-.pdb` est recopié dans + `Windows\LifePlay\Binaries\Win64` parce que `IncludeDebugFiles` reste `False` (le Shipping + lancé depuis l'éditeur ne doit pas embarquer les PDB). +- Cuisson alignée sur le projet actuel (lot O2) : `-pak -iostore -compressed`. `Modules` reste en + fichiers nus via `DefaultGame.ini`. +- Après l'archive, les lanceurs `Tools/debug/*.bat` sont copiés à côté de `Windows\LifePlay.exe`. + +Journal UAT : `Saved\Logs\portage\BuildDebug__.log`. + +## 3. Lanceurs + +Scénario unique (O0 §5, environ 15 min) : partie avancée → carte de la ville → intérieur peuplé → +extérieur peuplé → scène à plusieurs personnages → quitter normalement. + +| Session | Build | Lanceur | But | +|---|---|---|---| +| A | DebugGame | `LifePlay_debug_sessionA_insights.bat` | À-coups / travail de `GI` (G6) | +| B | DebugGame | `LifePlay_debug_sessionB_mem.bat` | Fuites, physique, collisions | +| C | Development | `LifePlay_debug_sessionC_profil.bat` | Non-régression CSV + LLM | + +Auxiliaires, hors chemin critique : `LifePlay_debug_bugs.bat`, `LifePlay_debug_gpu.bat` (PIX **ou** +RGP, jamais les deux), `LifePlay_debug_collision.bat`. `LifePlay_debug_ui.bat` ouvre une +fenêtre pour cocher ces options (le canal `memory` reste décoché ; `-csvCaptureFrames` n'est +pas proposé). Le `.ps1` du même nom doit rester à côté du `.bat`. + +Flags des sessions : + +| Lanceur | Ligne de commande | +|---|---| +| A | `-trace=cpu,frame,bookmark,loadtime,memory -statnamedevents -tracefile=/Profiling/SessionA.utrace -LPHitchLog` | +| B | `-llm -llmcsv -LPCollisionLog -ExecCmds="LifePlay.MemSnapshot, p.Chaos.StartVDRecording"` | +| C | `-csvGpuStats -llm -llmcsv -ExecCmds="csvprofile repeat=20, csvprofile frames=9000"` | +| bugs | `-LogCmds="LogModdingExtra Verbose"` | +| gpu | `-d3d12` (PIX). RGP : attacher au processus, sans ce lanceur | +| collision | `-LPCollisionLog -ExecCmds="p.Chaos.StartVDRecording, show collision"` | +| ui | fenêtre : les mêmes options, une par une | + +**Piège CSV.** Ne jamais passer `-csvCaptureFrames`. La capture démarre avant l'initialisation du +RHI et le moteur s'arrête (`IsRayTracingAllowed() may only be called once RHI is initialized`). +La session C démarre le CSV par `-ExecCmds`, après le rendu. Mesurer un jeu empaqueté, pas +`UnrealEditor.exe -game`, pour les chiffres comparables à O0. + +## 4. Collecte + +```powershell +.\Tools\CollectDebugArtifacts.ps1 -Lot -BuildDir -Session A|B|C|bugs|gpu|collision +``` + +Destination : `D:\Game\LifePlay_Backup_5.5\\Session\` (journaux, CSV, LLM, `.utrace`, +MemReport, `Profiling\LPDebug`, enregistrements ChaosVD). Le script échoue si rien n'est copié. + +Résumé CSV (session C) : + +```powershell +.\Tools\SummarizeDebugCsv.ps1 -Csv +``` + +Médiane `FrameTime`, images > 50 ms et > 100 ms. Référence O0 (build L7cdj, 36 004 images) : +médiane 16,7 ms ; 159 images > 50 ms ; 112 images > 100 ms ; physique Chaos environ 463 Mo. + +## 5. Runtime non-Shipping + +`ULPDebugSubsystem` (`UGameInstanceSubsystem`), derrière `#if !UE_BUILD_SHIPPING`. + +| Entrée | Effet | +|---|---| +| `-LPHitchLog` | Journal des images ≥ seuil. Sans ce flag, aucun relevé d'image | +| `-LPHitchMs=N` | Seuil en millisecondes (50 par défaut) | +| `-LPCollisionLog` | Échantillon toutes les 2 s, dump dès qu'un trimesh a des triangles, avec l'acteur qui le porte. Inactif sans ce flag | +| `LifePlay.Bookmark ` | `TRACE_BOOKMARK` et ligne `LPDebug bookmark :` | +| `LifePlay.MemSnapshot` | `Saved/Profiling/LPDebug/MemSnapshot_*.txt` + `memreport` | +| `LifePlay.CollisionDump` | `CollisionDump_*.txt` immédiat (console). Le suivi automatique est `-LPCollisionLog`, pas cette commande au démarrage | +| `LifePlay.HitchSummary` | Bilan courant (`LPDebug hitch : N hitch(s) en …`) | + +Un bilan est aussi écrit à 2 minutes et à la fermeture quand `-LPHitchLog` est actif. +Ligne de hitch : `LPDebug hitch : ms (seuil ) #`. + +Événements nommés Insights (macro `TRACE_CPUPROFILER_EVENT_SCOPE`, canal CPU ; en 5.5 il n'y a pas +de macro publique `SCOPED_NAMED_EVENT`) : `LoadTxt`, `LPDataCache` (`TryGet`), `LPDataCache_LoadAll`. +Ils sont vides en Shipping (`CPUPROFILERTRACE_ENABLED` est 0). + +ChaosVD est limité à l'éditeur dans `LifePlay.uproject`. En jeu empaqueté, si +`p.Chaos.StartVDRecording` n'existe pas, `LifePlay.CollisionDump` est la preuve collision. Pour un +enregistrement CVD, ouvrir l'éditeur et attacher le débogueur visuel Chaos au processus, ou jouer +en PIE. + +Options déjà en place, inchangées : `-LPDump*`, `-LPTrace*`, `-LPNoDataCache`, `-LPDataCacheVerify`, +console `LifePlay.LogSummary` et `ModdingExtra.LoadTxtStats`. + +## 6. Gates + +| Étape | Vert seulement si | +|---|---| +| Build | UAT code 0, `LifePlay.exe`, 6 lanceurs à côté, PDB du projet présents | +| Hitch | Au moins une ligne `LPDebug hitch :`, ou `0 hitch(s)` après ≥ 2 min | +| Signet | Ligne `LPDebug bookmark :` | +| Mémoire | Fichier `MemSnapshot_*.txt` non vide | +| Session A | `.utrace` ouvrable, signets alignés sur les hitches, 3 à 5 causes avec ms ou % | +| Session B | Physique en Mo, MemReport ou snapshot, CVD **ou** CollisionDump, delta mémoire | +| Session C | Au moins une tranche de 9 000 images, LLM, médiane ≤ 17 ms (±1) ou écart expliqué, images > 100 ms comparées à O0 (112). Jouer **sans** `-LPHitchLog` | + +Preuves attendues : `Dbg-I4`, `Dbg-I5`, `Dbg-I6` sous `D:\Game\LifePlay_Backup_5.5\`. + +## 7. Agent + +``` +- [ ] Development pour les chiffres, DebugGame pour la plongée +- [ ] BuildDebug.ps1 si l'archive n'existe pas ou si le C++ a changé +- [ ] Une session (A, B ou C), pas un lanceur unique +- [ ] CollectDebugArtifacts.ps1 +- [ ] Insights / SummarizeDebugCsv / ChaosVD selon la session +- [ ] Rien de Tracy, Optick, ASAN, ImGui +``` + +Outils et URLs : `.cursor/skills/lifeplay-debug-build/reference.md`. +Priorité : Unreal Insights → CSV/LLM → ChaosVD → RenderDoc / PIX / suite Radeon / µProf / diagnostics Visual Studio. +Écartés : Tracy, Optick, Superluminal, ASAN, Nsight par défaut (machine AMD). + +## 8. Mesures du 2026-10-04 et décision + +Build DebugGame : `D:\Game\LifePlay_Build_5.5_DebugGame` (UAT 243 s). Preuves : +`D:\Game\LifePlay_Backup_5.5\Dbg-I4` et `Dbg-I5`. Les millisecondes DebugGame ne se comparent +pas à O0. + +### Session A (G6) + +Trace CPU courte ouvrable (`SessionA_cpu.utrace`, 252 Mo, export +`Dbg-I4\GameThreadTimers.txt`) : 5 388 images, 32 à-coups ≥ 50 ms, max **2 389 ms**. +La trace avec canal `memory` (`SessionA.utrace`, 6,4 Go, 89 à-coups en 350 s, max 6,7 s) +a saturé Insights (51 Gio / 63). Les signets `TRACE_BOOKMARK` sont émis dans la même +fonction que chaque ligne `LPDebug hitch` ; le max 2,35 s des timers `FTimerManager` et +`ExecuteUbergraph_City` est le même à-coup que le journal (2 389 ms). + +Temps inclus GameThread, budget de travail `GameEngine Tick` = 14,42 s (l'attente de +cadence, 69,6 s, est à part) : + +| Cause | Inclus | Part du tick moteur | Max une fois | +|---|---:|---:|---:| +| `ExecuteUbergraph_GI` | 5,34 s | 37 % | 2,35 s | +| `FTimerManager` | 3,39 s | 24 % | 2,35 s | +| `ExecuteUbergraph_City` | 2,51 s | 17 % | 2,35 s | +| `ExecuteUbergraph_Person` | 1,73 s | 12 % | 0,30 s | +| `PopulateRaws` | 1,26 s | 9 % | 1,26 s | +| Actions latentes Blueprint | 1,03 s | 7 % | 0,30 s | +| `LoadTxt` | 5,5 ms | — | 0,2 ms | +| `LPDataCache` | 2,6 ms | — | 0,2 ms | + +`LoadTxt` n'est pas la source des à-coups. Le pic de 2,35 s est le graphe de `GI` / `City` +déclenché par le gestionnaire de minuteries. + +### Session B (physique) + +LLM, fin de chargement de `CentralLondon` (textures 651 Mo, proche des 624 Mo d'O0) : + +| Poste | Début | Fin | +|---|---:|---:| +| Physics | 6,3 Mo | **171,6 Mo** | +| Physics/ChaosTrimesh | — | 87,6 Mo | +| TrackedTotal | 843 Mo | 1 888 Mo | + +Delta physique **+165 Mo**. `p.Chaos.StartVDRecording` a écrit +`ChaosVD-20261004_130550.utrace` (172 Mo). Le `CollisionDump` du `ExecCmds` de démarrage +compte 23 primitives et 1 trimesh, sans dire qui le porte. Le détail est au §10. +MemReport et `MemSnapshot_20261004_130550.txt` sont dans `Dbg-I5\SessionB`. + +Les 463 Mo d'O0 ne se retrouvent pas : les collisions décoratives ont été retirées en O9. +La physique reste le second poste derrière les textures, plus le premier. + +### Session C (Development, sans `-LPHitchLog`) + +Build `D:\Game\LifePlay_Build_5.5_Development` (UAT 127 s). Une tranche complète : +`Profile(20261004_131659).csv`, 9 000 images. Preuves : `Dbg-I6\SessionC`. + +| Mesure | Cette tranche | O0 (36 004 images) | +|---|---:|---:| +| Médiane temps d'image | **16,66 ms** | 16,7 ms | +| Médiane thread de jeu | 3,57 ms | 3,9 à 4,4 ms | +| Images > 50 ms | 52 (0,58 %) | 159 (0,44 %) | +| Images > 100 ms | 23 (0,26 %) | 112 (0,31 %) | + +La médiane tient le plafond de 60 images/s (≤ 17 ms). Le taux d'images > 100 ms n'est pas +plus haut qu'en O0. Le parcours n'est pas les 15 minutes du scénario O0 (démarrage puis +`CentralLondon`), donc le décompte brut 23 contre 112 ne se compare pas tel quel. LLM en fin +de cette tranche : textures 605 Mo, physique 89 Mo (la session B, plus longue, monte à 172 Mo). + +### Décision + +Prochain lot d'optimisation : **G6**, pas O10 en premier. O10 reste un lot mémoire +(trimesh ~88 Mo en session B), après G6. Le sous-système debug, journal d'à-coups éteint, +ne déplace pas la médiane Development. La trace Development du §9 nomme la feuille : +ce n'est pas le graphe `GI` qu'il faut réécrire. + +## 9. Trace Development courte (G6, 2026-10-04) + +Build déjà en place : `D:\Game\LifePlay_Build_5.5_Development`. Trace +`Windows\LifePlay\Saved\Profiling\G6Dev.utrace` (464 Mo), canaux +`cpu,frame,bookmark,loadtime`, sans `memory`, avec `-LPHitchLog`. Durée 184 s, arrêt +après le premier à-coup > 500 ms. Copies : `D:\Game\LifePlay_Backup_5.5\Dbg-G6`. + +Un seul à-coup > 500 ms : **3 621 ms** à l'image du clic Continuer (`DelayedStart`, +3,583 s). Le minuteur de 2 s qui rappelle `DelayedStart` dure ~13 µs. Aucun +`FTimerManager_Tick` > 200 ms : le 654 ms de l'image 3137 (CSV I6) n'est pas reproduit +sur ce démarrage automatique. + +Feuille CPU, temps exclusif dans cette image (export `ExportTimerCallees`, thread de jeu) : +`FindCenterOfPolygon` **0,350 s** exclusif / 0,742 s inclus, 49 425 appels (centre de la +boîte englobante, pas le centroïde). `Array_Get` est plus haut en exclusif (0,536 s) mais +c'est un opcode de la machine virtuelle, pas une fonction. `PopulateRaws` et +`ExecuteUbergraph_City` restent des contenants (exclusif négligeable). + +Lecture des ~164 Mo (CSV I6, image 395, colonnes exclusives vides) : le canal `loadtime` +nomme `/Game/Heavy/StreetMap/Static/CentralLondon` et +`/Game/Heavy/StreetMap/CentralLondon`. Disque chaud, l'attente synchrone de cette trace +est 0,325 s (`GetAsset` × 2 → `MainThreadWaitingOnAsyncLoadingThread`). + +Deux lots, baseline = la tranche I6 (médiane 16,66 ms, une image à 6,2 s, une image +TimerManager à 654 ms) : + +- CHG-115 : `FindCenterOfPolygon` en C++, quatre appels de `GI` rebranchés. Pas de + réécriture du graphe, pas de décalage des minuteries. +- CHG-116 : préchargement async de ces deux paquets à l'init de l'instance de jeu. + Séparé du correctif de la feuille. + +Cuisson Development du 2026-10-04 14:26 (`BuildDebug_Development_20261004-142617.log`, +UAT 148 s) contient les deux lots. Partie 14:31–14:46, quittée normalement, sans +`-LPHitchLog` et sans CSV (journal archivé `Dbg-G6p\SessionC`). `Preload ville : +demande envoyee` dès l'init. Aucune erreur `FindCenterOfPolygon`. L'image du clic +Continuer (`StreetMap BeginPlay`, puis `PopulateRaws`) s'étale sur **1 017 ms** de +journal, contre 3 621 ms avant ces lots. Une autre image, à 14:33:02 (entrée +« Central London » / pièce `street`), s'étale sur 1 171 ms. Ce ne sont pas des temps +CSV : la médiane 16,66 ms et le rythme d'O0 restent à remesurer avec le lanceur +de la session C. + +## 10. Rapport collisions (2026-10-04) + +Les capsules des PNJ ne sont pas les 88 Mo. Un seul trimesh les explique, et le dump +de démarrage ne nomme pas son acteur. `-LPCollisionLog` (CHG-117) est dans la cuisson +de 15:16. La partie de 15:19 ne l'a pas activé : `LifePlay.exe` a été lancé sans ce switch. + +| Preuve | Ce qu'elle montre | +|---|---| +| `Dbg-I5\SessionB\Profiling\LPDebug\CollisionDump_20261004_130550.txt` | Monde `CentralLondon`, 23 primitives dont **12 `Person_C`**, 6 `Room_C`, 2 `StreetMap_Static_C`, 1 `StaticMeshActor`. **1 trimesh** unique, 3 formes simples, 0 corps simulé. Pas de nombre de triangles ni d'acteur porteur. | +| `Dbg-I5\SessionB\Profiling\LLM\LLM_Pid12232_2026.10.04-13.05.50_Windows.csv` | 402 échantillons. Maximum : physique **176,7 Mo**, trimesh **93,5 Mo**, `ChaosUpdate` **83,0 Mo**, convexes **0,11 Mo**. Dès l'échantillon 17 le trimesh est déjà à 79,3 Mo. | +| `Dbg-I5\SessionB\Profiling\ChaosVD-20261004_130550.utrace` (180 Mo) | Enregistrement présent, non ouvert : il ne nomme pas le maillage. | +| `Dbg-I6\SessionC\Profiling\CSV\Profile(20261004_131659).csv` | Development, 9 000 images, médiane **16,66 ms**. Image 395 : **6 218 ms** et 164 Mo lus. Image 3137 : **664 ms** dans `TimerManager`. | +| `Dbg-G6p\SessionC\Profiling\G6Dev.utrace` (464 Mo ; original dans le Saved du build Development) | Canaux `cpu,frame,bookmark,loadtime`. Image Continuer **3 621 ms**. Feuille `FindCenterOfPolygon` 0,350 s exclusif. Paquets `/Game/Heavy/StreetMap/Static/CentralLondon` et `/Game/Heavy/StreetMap/CentralLondon`. | +| `Dbg-G6p\SessionC\Logs\LifePlay.log` | Partie 14:31–14:46 avec CHG-115/116. Préchargement envoyé. Image Continuer **1 017 ms** de journal. Entrée dans la ville **1 171 ms**. Pas de relevé de collision (lanceur sans `-LPCollisionLog`). | + +Lecture. Les 12 capsules de PNJ sont des formes simples : les convexes du LLM culminent à 0,11 Mo. Le trimesh (93 Mo) et `ChaosUpdate` (83 Mo) apparaissent ensemble et font l'essentiel des 177 Mo de physique. Le dump de la même session ne voit qu'un trimesh, donc ce poste est un maillage complexe unique, parmi `StreetMap_Static_C`, `Room_C` ou le `StaticMeshActor`, pas une capsule de personnage. `StreetMap` sait construire cette collision (`UseComplexAsSimple`, profil `BlockAll`) ; le défaut du composant est de ne pas la générer, et le dump ne dit pas si l'instance de la ville l'a fait. + +Le CVD et le dump de démarrage ne suffisent pas à désigner l'acteur. Le suivi `-LPCollisionLog` écrit `CollisionTrack_*.txt` (une ligne toutes les 2 s) et un `CollisionDump_*.txt` dès que des triangles sont comptés, avec la classe, l'acteur et le composant. Les lanceurs `LifePlay_debug_sessionB_mem.bat` et `LifePlay_debug_collision.bat` passent ce switch et ne lancent plus `LifePlay.CollisionDump` dans le `ExecCmds` de démarrage. + +Partie de la cuisson 15:16, 15:19–15:27, sortie normale (`Dbg-G6r\LifePlay.log`). Lignes au démarrage : `LPDebug collision : inactif`, `Preload ville : demande envoyee`, aucune erreur `FindCenterOfPolygon`. Une image, à 15:20:00, s'étale sur **3 152 ms** de journal : `StreetMap BeginPlay`, puis les fichiers de monde, puis « Central London », puis le domicile (`home test310 UpdateWall`) et un clic. Environ **2 s** de cette image n'ont presque pas de lignes. Ce n'est pas un temps CSV. Le porteur du trimesh reste non nommé tant que la partie n'est pas lancée par `LifePlay_debug_collision.bat`. + +Partie 15:37–15:50 avec `-LPCollisionLog` (réglage Collisions du lanceur). Sortie normale. 382 échantillons, 4 dumps, maximum **1 206 768** triangles (`Dbg-G6s`). À 7 s, le porteur est nommé : + +| Triangles | Acteur | Composant | Profil | +|---:|---|---|---| +| **1 084 249** | `StreetMap_Static` | `CentralLondon` | `BlockAllDynamic` | +| 122 469 | `Room_C_0` (au pic, 61 primitives) | meubles `fnt_new*`, lampes `ls_lamp*` | `Custom` | +| 0 | 84 `Person_C` | une capsule `CollisionCylinder` partagée | `Custom` | + +Le maillage de rue porte **90 %** des triangles de collision. Les PNJ n'en ont aucun. Preuve : `CollisionDump_20261004_153727.txt`. En fin de partie il reste 1 143 395 triangles. L'enregistrement ChaosVD (299 Mo) est dans le Saved du build. diff --git a/Docs/Migration/19_Plan_Outillage_Debug.md b/Docs/Migration/19_Plan_Outillage_Debug.md new file mode 100644 index 0000000..4ba64eb --- /dev/null +++ b/Docs/Migration/19_Plan_Outillage_Debug.md @@ -0,0 +1,285 @@ +# 19 — Plan d'outillage debug (pilotage) + +> Plan de lot révisé (game manager + senior). Procédure opérationnelle et mesures : +> [`18_Build_Debug.md`](18_Build_Debug.md). Skill agent : +> [`.cursor/skills/lifeplay-debug-build/`](../../.cursor/skills/lifeplay-debug-build/). +> Protocole lots : [`10_Protocole_Operations.md`](10_Protocole_Operations.md). + +**État :** gates I0–I6 mesurés (voir §4 et relevés dans `18`). Prochain lot d'optimisation +proposé : **G6** (pas O10 en premier) — décision dans `18` § mesures. + +--- + +## 1. Brief production (game manager) + +### Mission + +Livrer une **capacité de mesure reproductible** (pipeline + preuves), pas une optimisation joueur. Après ce lot, l'équipe peut décider *quoi* corriger (G6 à-coups, O10 physique) avec des chiffres, pas au feeling. + +### Succès (Definition of Done globale) + +Le lot est **terminé** seulement si : + +1. Un tiers (ou un agent futur) peut packager DebugGame/Development et lancer sessions A/B/C **sans scripts hors dépôt**. +2. Trois jeux de preuves archivés (`Dbg-I4`, `Dbg-I5`, `Dbg-I6`) sous `D:\Game\LifePlay_Backup_5.5\`. +3. Une note chiffrée classe les priorités suivantes (G6 vs O10 vs autre) — **décision**, pas encore le fix. +4. Shipping **inchangé** en comportement joueur (`IncludeDebugFiles=False`, pas de coût runtime Shipping). +5. Skill + `18_Build_Debug.md` alignés sur le livré. + +### Hors scope (explicitement) + +| Inclus | Exclu jusqu'à preuves I4–I6 | +|---|---| +| Scripts packaging / lanceurs / collecte | Optimisation GI / timers / async load | +| Sous-système debug non-Shipping | Refonte collisions ville / LOD / morphs | +| Doc + skill | Montée UE 5.8 (préparer seulement la portabilité) | +| Mesure G6 / Physics | Intégration Tracy / Optick / ASAN / ImGui | +| Outils gratuits documentés | Builds Shipping « debug » pour joueurs | + +### Priorité joueur (pourquoi ce lot) + +| Signal existant (O0/O9) | Impact ressenti | Ce lot livre | +|---|---|---| +| À-coups GameThread (G6) | Stutters irréguliers | Preuve nommée → backlog fix priorisé | +| Physics ~463 Mo (O0) ; ~172 Mo session B post-O9 | Risque machines modestes | Chiffre Physics + CVD → backlog O10 | +| Régime 16,7 ms | OK aujourd'hui | Garde-fou non-régression (I6) | + +**Règle produit :** ne pas ouvrir un lot d'optimisation tant que I4 (hitches) **et** I5 (mémoire) n'ont pas leurs gates verts. + +### Effort et dépendances (ordre de grandeur) + +| Phase | Effort estimé | Qui | Dépendance | +|---|---|---|---| +| I0 skill | 15 min | Agent | — | +| I1 scripts + 1er cook DebugGame | 0,5–1 j (+ cook long) | Agent ; cook machine | UE 5.5, Content/Heavy dispo | +| I2 C++ + compile / smoke | 0,5 j | Agent | I1 ou Editor Development pour smoke | +| I3 doc + sync skill | 0,25 j | Agent | I1–I2 stabilisés | +| I4 / I5 / I6 playtests | 3× ~20–30 min jeu + analyse | **Utilisateur** joue ; agent archive/analyse | Builds prêtes | + +**Chemin critique :** temps de cook DebugGame + disponibilité playtest humain. + +### Risques production + +| Risque | Impact | Mitigation | +|---|---|---| +| Cook DebugGame très long / échec | Bloque I1 | Smoke I2 en Editor d'abord ; `-SkipBuild` / iterate ; un seul cook DebugGame pour I4–I5 | +| Confusion DebugGame vs Dev sur les ms | Fausses régressions | Dual-build documenté ; I6 = Development only vs O0 | +| Instrumentation fuit dans Shipping | Perf / logs joueur | `#if !UE_BUILD_SHIPPING` strict ; pas de changement `DefaultGame.ini` Shipping | +| Scope creep « on corrige GI tout de suite » | Lot interminable | Hors scope ; nouveau lot après I6 | +| Trace Insights trop lourde | Machine saturée (déjà vu O9) | Trace courte ciblée ; channels limités ; bookmarks | +| Playtest non standard | Mesures incomparables | Scénario O0 figé 15 min ; même type de save | + +### Gouvernance + +- Protocole lots : [`10_Protocole_Operations.md`](10_Protocole_Operations.md) (tag, snapshot, journal). +- Skill obligatoire : [`.cursor/skills/lifeplay-debug-build/`](../../.cursor/skills/lifeplay-debug-build/). +- **Go / No-Go** à chaque gate In : critères verts ou stop + re-mesure. +- Commit : uniquement si demandé ; un sous-lot = un objectif mesuré. + +--- + +## 2. Brief technique (senior gameplay / engine) + +### Principes + +1. **Zero cost Shipping** — tout debug derrière `!UE_BUILD_SHIPPING` ; flags CLI opt-in. +2. **Réutiliser** `ULPSessionLogSubsystem` patterns (`FParse`, `FAutoConsoleCommand`, `FName` categories). +3. **Pas de modification Blueprint GI** dans ce lot — C++ subsystem + ModdingExtra hotspots only. +4. **DebugGame rare** — un cook pour plongée ; itération code en Editor/Development. +5. **Portable 5.8** — Insights/ChaosVD/CSV + attach process ; pas de plugin profiler tiers. + +### Architecture cible + +```mermaid +flowchart TB + subgraph mgmt [DoD_produit] + Proof[Preuves_A_B_C] + Decision[Priorite_G6_vs_O10] + end + subgraph pipe [Pipeline] + BuildPs[BuildDebug.ps1] + Bats[Tools/debug_bats] + Collect[CollectDebugArtifacts.ps1] + end + subgraph runtime [Runtime_non_Shipping] + LPDebug[ULPDebugSubsystem] + Named[SCOPED_NAMED_EVENT_LoadTxt_Cache] + SessLog[ULPSessionLogSubsystem_existant] + end + BuildPs --> Bats + Bats --> LPDebug + Bats --> SessLog + Named --> Insights[UnrealInsights] + LPDebug --> Insights + Bats --> Collect + Collect --> Proof + Proof --> Decision +``` + +### Dual-build + +| Config | Rôle | Comparer ms absolus | +|---|---|---| +| **Development** | Baseline chiffrée / I6 vs O0 | Oui | +| **DebugGame** | Plongée I2/I4/I5 (checks, PDB) | Non | +| **Shipping** | Joueurs | Hors lot | + +### Livrables techniques + +| Livrable | Contenu clé | +|---|---| +| [`Tools/BuildDebug.ps1`](../../Tools/BuildDebug.ps1) | `-Config DebugGame\|Development`, pas `-nodebuginfo`, stage des bats | +| [`Tools/CollectDebugArtifacts.ps1`](../../Tools/CollectDebugArtifacts.ps1) | `-Lot -BuildDir -Session` → backup | +| `Tools/debug/*.bat` | Sessions A/B/C + bugs/gpu/collision | +| `ULPDebugSubsystem` | Hitch + bookmark + MemSnapshot + CollisionDump | +| Named events | `LoadTxt`, `LPDataCache` | +| [`18_Build_Debug.md`](18_Build_Debug.md) | Procédure + mesures + Agent/skill | + +### Sessions de capture + +Scénario unique (O0 §5, ~15 min) : save avancée → carte → intérieur → extérieur → multi-PNJ → quit. + +| Session | Build | Gate métier | +|---|---|---| +| A | DebugGame | Top contributeurs hitch **chiffrés** | +| B | DebugGame | Physics Mo + CVD ou dump collision | +| C | Development | Non-régression vs O0 (médiane ~16,7 ms) | + +Auxiliaires hors chemin critique : bugs / gpu (PIX **ou** RGP) / collision. + +### Stack outils (gratuite, pas de trial) + +Insights + Memory Insights, CSV/LLM, ChaosVD, RenderDoc, PIX, Radeon RGP/RMV, µProf, VS Diagnostics. +Écartés : Tracy, Optick, ASAN, Superluminal, Nsight par défaut (machine AMD). + +Détail : [`.cursor/skills/lifeplay-debug-build/reference.md`](../../.cursor/skills/lifeplay-debug-build/reference.md). + +### Pièges techniques connus + +- Jamais `-csvCaptureFrames` (crash RHI). +- Mesurer packaged, pas Editor `-game`, pour I6. +- Shipping `NO_LOGGING` — ne pas s'y fier pour l'analyse. +- Traces Insights lourdes : rester court + bookmarks. +- I6 **sans** `-LPHitchLog` (overhead off by default). + +--- + +## 3. Skills (obligatoire agent) + +| Skill | Usage | +|---|---| +| [`SKILL.md`](../../.cursor/skills/lifeplay-debug-build/SKILL.md) | Lire **avant** tout code (I0) | +| [`reference.md`](../../.cursor/skills/lifeplay-debug-build/reference.md) | Flags, chemins, mapping symptôme→outil | + +Ne pas diverger (mega-lanceur, Tracy, rebuild DebugGame systématique) sans maj skill + ce plan. + +--- + +## 4. Planning itératif mesurable + +### Règle de sortie + +> On ne quitte **In** que si **tous** les critères de mesure sont verts. +> Code sans artefact / impression en jeu = **rouge**. +> Rouge → corriger → **re-mesurer**. Pas de skip vers In+1. + +```mermaid +flowchart LR + I0[I0] --> I1[I1] + I1 --> G1{Gate} + G1 -->|non| I1 + G1 -->|oui| I2[I2] + I2 --> G2{Gate} + G2 -->|non| I2 + G2 -->|oui| I3[I3] + I3 --> G3{Gate} + G3 -->|non| I3 + G3 -->|oui| I4[I4] + I4 --> G4{Gate} + G4 -->|non| I4 + G4 -->|oui| I5[I5] + I5 --> G5{Gate} + G5 -->|non| I5 + G5 -->|oui| I6[I6] + I6 --> G6{Gate} + G6 -->|non| I6 + G6 -->|oui| DoD[DoD_lot] +``` + +### Statut des gates + +| Itération | Objectif | Gate (résumé) | Statut | +|---|---|---|---| +| I0 | Skill | Procédure partagée | Fait | +| I1 | Pipeline scripts | Build + Collect smoke | Fait | +| I2 | `ULPDebugSubsystem` | Hitch / bookmark / MemSnapshot | Fait | +| I3 | Doc 18 + skill sync | Doc = réalité | Fait | +| I4 | Session A | utrace + hitches chiffrés G6 | Fait — voir `18` | +| I5 | Session B | Physics Mo + CVD | Fait — voir `18` | +| I6 | Session C vs O0 | Médiane ≤ 17 ms, hitches | Fait — voir `18` | + +### Détail des itérations + +#### I0 — Alignement + +- **Owner :** Agent +- **Mesure :** skill lu ; scope hors Tracy/mega-lanceur confirmé + +#### I1 — Pipeline (P0 technique) + +- **Livrables :** `BuildDebug.ps1`, `CollectDebugArtifacts.ps1`, bats +- **Gate :** UAT DebugGame OK + exe + bats staged + Collect smoke → backup non vide + +#### I2 — Runtime debug (P0 technique) + +- **Livrables :** `ULPDebugSubsystem` + named events ; Shipping-safe +- **Gate :** compile clean ; hitch log ; Bookmark ; MemSnapshot non vide + +#### I3 — Doc / skill = réalité + +- **Gate :** noms bats/flags identiques doc↔skill↔Tools ; section Agent ; piège CSV documenté + +#### I4 — Mesure G6 hitches (P0 produit) + +- **Owner :** Utilisateur (jeu) + Agent (analyse) +- **Gate :** `.utrace` ; bookmarks/hitch alignés ; top contributeurs avec ms/% ; Collect OK +- **Livrable décision :** causes candidates classées + +#### I5 — Mesure Physics / fuites (P0 produit) + +- **Gate :** Physics Mo ; MemReport ; CVD ou CollisionDump ; delta mémoire ; Collect OK + +#### I6 — Non-régression Development + +- **Gate :** CSV ≥1×9000 frames ; LLM ; tableau vs O0 ; médiane ≤17 ms (±1) ou écart causé ; Collect OK +- **Sans** `-LPHitchLog` + +### Après DoD + +Lots suivants (chacun avec baseline avant/après) : + +1. **Fix G6** (étalement `ExecuteUbergraph_GI` / `City`, `PopulateRaws`) — priorité décidée en I4/I6 +2. O10 collisions / trimesh — après G6 +3. Montée 5.8 — réutiliser cette pipeline + +--- + +## 5. Critères d'acceptation (checklist manager) + +- [x] Aucun changement comportement Shipping joueur +- [x] Scripts versionnés ; plus de dépendance à `LifePlay_profil_O0.bat` hors repo +- [x] I4 + I5 + I6 verts avec backups +- [x] Décision écrite G6 vs O10 (priorité prochain lot) — dans [`18_Build_Debug.md`](18_Build_Debug.md) +- [x] Skill + doc 18 synchronisés +- [ ] Temps cook / playtest consigné au journal (`Journal_Modifications.md`) si publication du lot + +--- + +## 6. Ordre d'exécution (référence) + +1. I0 skill +2. I1 scripts ; cook DebugGame +3. I2 C++ ; recook DebugGame si besoin +4. I3 doc + sync skill +5. I4 → gate → I5 → gate → I6 → gate +6. Revue DoD + proposition du prochain lot d'optimisation diff --git a/Docs/Migration/README.md b/Docs/Migration/README.md index 11968c0..b203721 100644 --- a/Docs/Migration/README.md +++ b/Docs/Migration/README.md @@ -28,6 +28,8 @@ La migration a été reprise le 2026-09-29, après l'échec d'un premier portage | [15_Modules_Optionnels.md](15_Modules_Optionnels.md) | lot | build Shipping réduite aux modules essentiels, modules optionnels dans une archive séparée (`Tools/SplitModules.ps1`) | | [16_H3_Mods_Paquets.md](16_H3_Mods_Paquets.md) | étude, prototype | H3 : assets apportés par les mods (plugins `Mods/`, cuisson DLC contre la version publiée) ; constats, architecture, risques, lots H3a à H3c ; §5 résultats du prototype H3a, §6 outillage H3b (`ModSources/`, `Tools/BuildMod.ps1`), validés ; §7 guide des moddeurs (`Docs/Modding/FullGuides/Creating_Asset_Mods.md`) et publication ; §8 métadonnées, avertissement de version, paquet `lpr_hair_pocolov` (H3d) | | [17_Mods_Ecran_Titre.md](17_Mods_Ecran_Titre.md) | lot | UI15 : gestionnaire de mods depuis l'écran titre (entrée dédiée, retour au menu titre, choix enregistré) | +| [18_Build_Debug.md](18_Build_Debug.md) | mesure | builds DebugGame / Development, sessions Insights, mémoire, CSV ; Shipping inchangé | +| [19_Plan_Outillage_Debug.md](19_Plan_Outillage_Debug.md) | pilotage | plan du lot outillage debug (DoD, hors scope, gates I0–I6, décision G6 vs O10) | | [Journal_Modifications.md](Journal_Modifications.md) | traçabilité | `CHG-001` à `CHG-057`, chacune avec son retour arrière | | [Registre_Erreurs.md](Registre_Erreurs.md) | traçabilité | `ERR-001` à `ERR-037`, statut et lot | | `correctifs/` | relecture | écarts de code 4.27 → 5.5 en patchs (`P01` à `P06`) | diff --git a/Plugins/ModdingExtra/Source/ModdingExtra/Private/LPDataCache.cpp b/Plugins/ModdingExtra/Source/ModdingExtra/Private/LPDataCache.cpp index 0e2e9c0..d1acfa7 100644 --- a/Plugins/ModdingExtra/Source/ModdingExtra/Private/LPDataCache.cpp +++ b/Plugins/ModdingExtra/Source/ModdingExtra/Private/LPDataCache.cpp @@ -12,6 +12,9 @@ #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" +#if !UE_BUILD_SHIPPING +#include "ProfilingDebugging/CpuProfilerTrace.h" +#endif DEFINE_LOG_CATEGORY_STATIC(LogLPDataCache, Log, All); @@ -92,6 +95,9 @@ void FLPDataCache::StartAsync() void FLPDataCache::LoadAll() { +#if !UE_BUILD_SHIPPING + TRACE_CPUPROFILER_EVENT_SCOPE(LPDataCache_LoadAll); +#endif const double Start = FPlatformTime::Seconds(); const FString ContentDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()); const FString ModulesDir = ContentDir / ModulesFolder; @@ -233,6 +239,9 @@ void FLPDataCache::LoadAll() bool FLPDataCache::TryGet(const FString& ContentRelativePath, FString& OutText) { +#if !UE_BUILD_SHIPPING + TRACE_CPUPROFILER_EVENT_SCOPE(LPDataCache); +#endif if (bDisabled || !bReady) { return false; diff --git a/Plugins/ModdingExtra/Source/ModdingExtra/Private/ModdingExtraBPLibrary.cpp b/Plugins/ModdingExtra/Source/ModdingExtra/Private/ModdingExtraBPLibrary.cpp index b90cc27..7bdae01 100644 --- a/Plugins/ModdingExtra/Source/ModdingExtra/Private/ModdingExtraBPLibrary.cpp +++ b/Plugins/ModdingExtra/Source/ModdingExtra/Private/ModdingExtraBPLibrary.cpp @@ -12,6 +12,9 @@ #include "Modules/ModuleManager.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" +#if !UE_BUILD_SHIPPING +#include "ProfilingDebugging/CpuProfilerTrace.h" +#endif // Journal des entrees/sorties de donnees (lot L3 du portage 5.5, remplace l'instrumentation // temporaire LogLPModdingTrace du 2026-09-28). @@ -177,6 +180,9 @@ namespace LPLoadTxtStats bool UModdingExtraBPLibrary::LoadTxt(FString FileNameA, FString& SaveTextA) { +#if !UE_BUILD_SHIPPING + TRACE_CPUPROFILER_EVENT_SCOPE(LoadTxt); +#endif const double StartTime = FPlatformTime::Seconds(); // Couche de donnees (etapes 2 et 3) : fichiers de Modules servis depuis la memoire quand le cache est pret. FLPDataCache& Cache = FLPDataCache::Get(); diff --git a/Source/LifePlay/LPDebugSubsystem.cpp b/Source/LifePlay/LPDebugSubsystem.cpp new file mode 100644 index 0000000..7817958 --- /dev/null +++ b/Source/LifePlay/LPDebugSubsystem.cpp @@ -0,0 +1,501 @@ +// Portage UE 5.5 - lot debug. Voir LPDebugSubsystem.h. + +#include "LPDebugSubsystem.h" + +#include "LifePlay.h" + +#if !UE_BUILD_SHIPPING + +#include "Chaos/TriangleMeshImplicitObject.h" +#include "Components/PrimitiveComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "EngineUtils.h" +#include "HAL/FileManager.h" +#include "HAL/IConsoleManager.h" +#include "HAL/PlatformMemory.h" +#include "Misc/CommandLine.h" +#include "Misc/CoreDelegates.h" +#include "Misc/FileHelper.h" +#include "Misc/Parse.h" +#include "Misc/Paths.h" +#include "PhysicsEngine/BodySetup.h" +#include "ProfilingDebugging/MiscTrace.h" + +namespace +{ + FAutoConsoleCommand* MemSnapshotCommand = nullptr; + FAutoConsoleCommand* CollisionDumpCommand = nullptr; + FAutoConsoleCommand* BookmarkCommand = nullptr; + FAutoConsoleCommand* HitchSummaryCommand = nullptr; + ULPDebugSubsystem* CommandOwner = nullptr; + + ULPDebugSubsystem* FindDebugSubsystem() + { + return CommandOwner; + } + + UWorld* FindPlayWorld() + { + if (!GEngine) + { + return nullptr; + } + if (UWorld* Play = GEngine->GetCurrentPlayWorld()) + { + return Play; + } + for (const FWorldContext& Context : GEngine->GetWorldContexts()) + { + UWorld* World = Context.World(); + if (World && (World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE)) + { + return World; + } + } + return nullptr; + } + + FString DebugDir() + { + const FString Dir = FPaths::ProjectSavedDir() / TEXT("Profiling/LPDebug"); + IFileManager::Get().MakeDirectory(*Dir, true); + return Dir; + } + + void WriteMemSnapshot() + { + const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); + const FString Path = DebugDir() / FString::Printf(TEXT("MemSnapshot_%s.txt"), *Stamp); + const FPlatformMemoryStats Stats = FPlatformMemory::GetStats(); + TArray Lines; + Lines.Add(FString::Printf(TEXT("When=%s"), *FDateTime::Now().ToString())); + Lines.Add(FString::Printf(TEXT("UsedPhysicalMB=%.1f"), Stats.UsedPhysical / (1024.0 * 1024.0))); + Lines.Add(FString::Printf(TEXT("PeakUsedPhysicalMB=%.1f"), Stats.PeakUsedPhysical / (1024.0 * 1024.0))); + Lines.Add(FString::Printf(TEXT("AvailablePhysicalMB=%.1f"), Stats.AvailablePhysical / (1024.0 * 1024.0))); + Lines.Add(FString::Printf(TEXT("UsedVirtualMB=%.1f"), Stats.UsedVirtual / (1024.0 * 1024.0))); + + UWorld* World = FindPlayWorld(); + if (GEngine) + { + const bool bReport = GEngine->Exec(World, TEXT("memreport")); + Lines.Add(FString::Printf(TEXT("memreport=%s"), bReport ? TEXT("ok") : TEXT("echec"))); + } + const FString ReportDir = FPaths::ProjectSavedDir() / TEXT("Profiling/MemReports"); + TArray Reports; + IFileManager::Get().FindFilesRecursive(Reports, *ReportDir, TEXT("*.memreport"), true, false); + if (Reports.Num() > 0) + { + Reports.Sort(); + Lines.Add(FString::Printf(TEXT("NewestMemReport=%s"), *Reports.Last())); + } + Lines.Add(TEXT("PhysicsMB=voir LLM (colonne Physics) ou le MemReport ; ce fichier ne remplace pas la session B")); + FFileHelper::SaveStringArrayToFile(Lines, *Path); + UE_LOG(LogLifePlay, Display, TEXT("LPDebug MemSnapshot : %s"), *Path); + } + + struct FCollisionOwner + { + FString Actor; + FString ClassName; + FString Component; + FString Profile; + int64 Triangles = 0; + int32 TriMeshes = 0; + int32 Simple = 0; + int32 Capsules = 0; + }; + + struct FCollisionTally + { + FString WorldName; + int32 Prim = 0; + int32 Simulating = 0; + int32 QueryOnly = 0; + int32 Persons = 0; + int32 Players = 0; + int32 Street = 0; + int32 UniqueBodies = 0; + int32 UniqueSimple = 0; + int32 UniqueTriMeshes = 0; + int32 UniqueCapsules = 0; + int64 Triangles = 0; + TMap ByClass; + TMap TrisByClass; + TArray Owners; + }; + + int64 CountTriangles(const UBodySetup* Body) + { + int64 Count = 0; + if (!Body) + { + return 0; + } + for (const Chaos::FTriangleMeshImplicitObjectPtr& Mesh : Body->TriMeshGeometries) + { + if (Mesh) + { + Count += Mesh->Elements().GetNumTriangles(); + } + } + return Count; + } + + FString ComponentLabel(const UPrimitiveComponent* Prim) + { + if (const UStaticMeshComponent* MeshComp = Cast(Prim)) + { + if (const UStaticMesh* Mesh = MeshComp->GetStaticMesh()) + { + return Mesh->GetName(); + } + } + return Prim ? Prim->GetName() : FString(); + } + + FCollisionTally GatherCollision() + { + FCollisionTally Tally; + UWorld* World = FindPlayWorld(); + Tally.WorldName = World ? World->GetName() : TEXT("(aucune)"); + if (!World) + { + return Tally; + } + + TSet Bodies; + for (TActorIterator It(World); It; ++It) + { + const FString ClassName = It->GetClass()->GetName(); + TInlineComponentArray Comps; + It->GetComponents(Comps); + for (UPrimitiveComponent* Prim : Comps) + { + if (!Prim || !Prim->IsCollisionEnabled()) + { + continue; + } + ++Tally.Prim; + Tally.ByClass.FindOrAdd(ClassName)++; + if (ClassName.StartsWith(TEXT("Person_C"))) + { + ++Tally.Persons; + } + else if (ClassName.StartsWith(TEXT("PC_C"))) + { + ++Tally.Players; + } + else if (ClassName.Contains(TEXT("StreetMap"))) + { + ++Tally.Street; + } + if (Prim->IsSimulatingPhysics()) + { + ++Tally.Simulating; + } + if (Prim->GetCollisionEnabled() == ECollisionEnabled::QueryOnly) + { + ++Tally.QueryOnly; + } + + UBodySetup* Body = Prim->GetBodySetup(); + if (!Body || Bodies.Contains(Body)) + { + continue; + } + Bodies.Add(Body); + const int64 Tris = CountTriangles(Body); + const int32 Capsules = Body->AggGeom.SphylElems.Num(); + const int32 Simple = Body->AggGeom.GetElementCount(); + Tally.UniqueBodies++; + Tally.UniqueSimple += Simple; + Tally.UniqueCapsules += Capsules; + Tally.UniqueTriMeshes += Body->TriMeshGeometries.Num(); + Tally.Triangles += Tris; + Tally.TrisByClass.FindOrAdd(ClassName) += Tris; + + if (Tris > 0 || Simple > 0) + { + FCollisionOwner& Owner = Tally.Owners.AddDefaulted_GetRef(); + Owner.Actor = It->GetName(); + Owner.ClassName = ClassName; + Owner.Component = ComponentLabel(Prim); + Owner.Profile = Prim->GetCollisionProfileName().ToString(); + Owner.Triangles = Tris; + Owner.TriMeshes = Body->TriMeshGeometries.Num(); + Owner.Simple = Simple; + Owner.Capsules = Capsules; + } + } + } + + Tally.Owners.Sort([](const FCollisionOwner& A, const FCollisionOwner& B) + { + return A.Triangles > B.Triangles; + }); + return Tally; + } + + FString WriteCollisionDump(const TCHAR* Reason, const FCollisionTally* Existing = nullptr) + { + const FCollisionTally Tally = Existing ? *Existing : GatherCollision(); + const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); + const FString Path = DebugDir() / FString::Printf(TEXT("CollisionDump_%s.txt"), *Stamp); + + TArray Lines; + Lines.Add(FString::Printf(TEXT("When=%s"), *FDateTime::Now().ToString())); + Lines.Add(FString::Printf(TEXT("Reason=%s"), Reason)); + Lines.Add(FString::Printf(TEXT("World=%s"), *Tally.WorldName)); + Lines.Add(FString::Printf(TEXT("CollisionPrimitives=%d"), Tally.Prim)); + Lines.Add(FString::Printf(TEXT("PersonPrimitives=%d"), Tally.Persons)); + Lines.Add(FString::Printf(TEXT("PlayerPrimitives=%d"), Tally.Players)); + Lines.Add(FString::Printf(TEXT("StreetMapPrimitives=%d"), Tally.Street)); + Lines.Add(FString::Printf(TEXT("Simulating=%d"), Tally.Simulating)); + Lines.Add(FString::Printf(TEXT("QueryOnly=%d"), Tally.QueryOnly)); + Lines.Add(FString::Printf(TEXT("UniqueBodySetups=%d"), Tally.UniqueBodies)); + Lines.Add(FString::Printf(TEXT("UniqueSimpleShapes=%d"), Tally.UniqueSimple)); + Lines.Add(FString::Printf(TEXT("UniqueCapsules=%d"), Tally.UniqueCapsules)); + Lines.Add(FString::Printf(TEXT("UniqueTriMeshes=%d"), Tally.UniqueTriMeshes)); + Lines.Add(FString::Printf(TEXT("Triangles=%lld"), Tally.Triangles)); + Lines.Add(TEXT("TopClasses:")); + TArray> Classes; + for (const TPair& Pair : Tally.ByClass) + { + Classes.Add(Pair); + } + Classes.Sort([](const TPair& A, const TPair& B) { return A.Value > B.Value; }); + for (int32 i = 0; i < Classes.Num() && i < 15; ++i) + { + const int64 Tris = Tally.TrisByClass.FindRef(Classes[i].Key); + Lines.Add(FString::Printf(TEXT(" %d %s triangles=%lld"), Classes[i].Value, *Classes[i].Key, Tris)); + } + Lines.Add(TEXT("OwnersByTriangles:")); + for (int32 i = 0; i < Tally.Owners.Num() && i < 12; ++i) + { + const FCollisionOwner& Owner = Tally.Owners[i]; + Lines.Add(FString::Printf( + TEXT(" triangles=%lld trimesh=%d simple=%d capsules=%d profile=%s %s %s %s"), + Owner.Triangles, Owner.TriMeshes, Owner.Simple, Owner.Capsules, + *Owner.Profile, *Owner.ClassName, *Owner.Actor, *Owner.Component)); + } + if (Tally.WorldName == TEXT("(aucune)")) + { + Lines.Add(TEXT("Note=monde de jeu absent (appeler la commande une fois la partie chargee)")); + } + Lines.Add(TEXT("Switch=-LPCollisionLog pour echantillonner apres le chargement, pas au ExecCmds de demarrage")); + FFileHelper::SaveStringArrayToFile(Lines, *Path); + UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %d primitives, %d trimesh, %lld triangles (%s) -> %s"), + Tally.Prim, Tally.UniqueTriMeshes, Tally.Triangles, Reason, *Path); + TRACE_BOOKMARK(TEXT("LP Collision %lld tris"), Tally.Triangles); + return Path; + } + + void WriteBookmark(const TArray& Args) + { + const FString Text = Args.Num() > 0 ? FString::Join(Args, TEXT(" ")) : TEXT("manuel"); + UE_LOG(LogLifePlay, Display, TEXT("LPDebug bookmark : %s"), *Text); + TRACE_BOOKMARK(TEXT("LP %s"), *Text); + } +} + +void ULPDebugSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + if (!MemSnapshotCommand) + { + MemSnapshotCommand = new FAutoConsoleCommand( + TEXT("LifePlay.MemSnapshot"), + TEXT("Ecrit Saved/Profiling/LPDebug/MemSnapshot_*.txt et lance memreport."), + FConsoleCommandDelegate::CreateLambda([]() { WriteMemSnapshot(); })); + CollisionDumpCommand = new FAutoConsoleCommand( + TEXT("LifePlay.CollisionDump"), + TEXT("Ecrit un releve des primitives de collision du monde courant."), + FConsoleCommandDelegate::CreateLambda([]() { WriteCollisionDump(TEXT("console")); })); + BookmarkCommand = new FAutoConsoleCommand( + TEXT("LifePlay.Bookmark"), + TEXT("Signet Insights (TRACE_BOOKMARK) et ligne LogLifePlay. Argument : texte du signet."), + FConsoleCommandWithArgsDelegate::CreateStatic(&WriteBookmark)); + HitchSummaryCommand = new FAutoConsoleCommand( + TEXT("LifePlay.HitchSummary"), + TEXT("Ecrit le bilan courant des a-coups (-LPHitchLog)."), + FConsoleCommandDelegate::CreateLambda([]() + { + if (ULPDebugSubsystem* Sub = FindDebugSubsystem()) + { + Sub->LogHitchSummary(TEXT("console")); + } + else + { + UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : sous-systeme absent")); + } + })); + CommandOwner = this; + } + + bHitchLog = FParse::Param(FCommandLine::Get(), TEXT("LPHitchLog")); + HitchMs = 50.f; + FParse::Value(FCommandLine::Get(), TEXT("LPHitchMs="), HitchMs); + bCollisionLog = FParse::Param(FCommandLine::Get(), TEXT("LPCollisionLog")); + if (bHitchLog || bCollisionLog) + { + StartSeconds = FPlatformTime::Seconds(); + LastFrameSeconds = StartSeconds; + } + if (bHitchLog) + { + UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : actif, seuil %.1f ms"), HitchMs); + } + else + { + UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : inactif")); + } + if (bCollisionLog) + { + const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); + CollisionTrackPath = DebugDir() / FString::Printf(TEXT("CollisionTrack_%s.txt"), *Stamp); + FFileHelper::SaveStringToFile( + TEXT("t\tworld\tprim\tpersons\tplayers\tstreet\ttrimesh\ttriangles\n"), + *CollisionTrackPath); + NextCollisionSeconds = FPlatformTime::Seconds() + 2.0; + UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : actif, premier echantillon dans 2 s -> %s"), *CollisionTrackPath); + } + else + { + UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : inactif")); + } + if (bHitchLog || bCollisionLog) + { + FCoreDelegates::OnEndFrame.AddUObject(this, &ULPDebugSubsystem::OnEndFrame); + } +} + +void ULPDebugSubsystem::Deinitialize() +{ + if (bHitchLog) + { + LogHitchSummary(TEXT("fin")); + } + if (bCollisionLog) + { + LogCollisionSummary(TEXT("fin")); + } + if (bHitchLog || bCollisionLog) + { + FCoreDelegates::OnEndFrame.RemoveAll(this); + } + if (CommandOwner == this) + { + CommandOwner = nullptr; + } + Super::Deinitialize(); +} + +void ULPDebugSubsystem::OnEndFrame() +{ + const double Now = FPlatformTime::Seconds(); + if (bCollisionLog && Now >= NextCollisionSeconds) + { + NextCollisionSeconds = Now + 2.0; + SampleCollision(); + } + if (!bHitchLog) + { + return; + } + const double DeltaMs = (Now - LastFrameSeconds) * 1000.0; + LastFrameSeconds = Now; + if (DeltaMs >= HitchMs) + { + ++HitchCount; + MaxHitchMs = FMath::Max(MaxHitchMs, DeltaMs); + if (HitchLines < 100 || (HitchCount % 25) == 0) + { + ++HitchLines; + UE_LOG(LogLifePlay, Warning, TEXT("LPDebug hitch : %.1f ms (seuil %.1f) #%d"), DeltaMs, HitchMs, HitchCount); + TRACE_BOOKMARK(TEXT("LP Hitch %.1f ms"), DeltaMs); + } + } + + if (!bLoggedTwoMinuteSummary && (Now - StartSeconds) >= 120.0) + { + bLoggedTwoMinuteSummary = true; + LogHitchSummary(TEXT("2 min")); + } +} + +void ULPDebugSubsystem::LogHitchSummary(const TCHAR* When) const +{ + const double Elapsed = StartSeconds > 0.0 ? FPlatformTime::Seconds() - StartSeconds : 0.0; + UE_LOG(LogLifePlay, Display, TEXT("LPDebug hitch : %d hitch(s) en %.1f s (seuil %.1f ms), max %.1f ms, %s"), + HitchCount, Elapsed, HitchMs, MaxHitchMs, When); +} + +void ULPDebugSubsystem::SampleCollision() +{ + const FCollisionTally Tally = GatherCollision(); + ++CollisionSamples; + PeakPrim = FMath::Max(PeakPrim, Tally.Prim); + PeakTriangles = FMath::Max(PeakTriangles, Tally.Triangles); + const double Elapsed = StartSeconds > 0.0 ? FPlatformTime::Seconds() - StartSeconds : FPlatformTime::Seconds(); + const FString Line = FString::Printf( + TEXT("%.1f\t%s\t%d\t%d\t%d\t%d\t%d\t%lld\n"), + Elapsed, *Tally.WorldName, Tally.Prim, Tally.Persons, Tally.Players, Tally.Street, + Tally.UniqueTriMeshes, Tally.Triangles); + FFileHelper::SaveStringToFile(Line, *CollisionTrackPath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), FILEWRITE_Append); + UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %.0f s %s prim=%d pnj=%d trimesh=%d triangles=%lld"), + Elapsed, *Tally.WorldName, Tally.Prim, Tally.Persons, Tally.UniqueTriMeshes, Tally.Triangles); + + const bool bGrew = Tally.Triangles >= LastDumpedTriangles + 1000 || Tally.UniqueTriMeshes > LastDumpedTriMeshes; + if (CollisionDumps < 4 && Tally.Triangles > 0 && bGrew) + { + WriteCollisionDump(TEXT("suivi"), &Tally); + ++CollisionDumps; + LastDumpedTriangles = Tally.Triangles; + LastDumpedTriMeshes = Tally.UniqueTriMeshes; + } +} + +void ULPDebugSubsystem::LogCollisionSummary(const TCHAR* When) const +{ + if (CollisionDumps == 0 && PeakPrim > 0) + { + WriteCollisionDump(TEXT("fin")); + } + UE_LOG(LogLifePlay, Display, TEXT("LPDebug collision : %d echantillon(s), %d dump(s), max %d primitives, %lld triangles, %s"), + CollisionSamples, CollisionDumps, PeakPrim, PeakTriangles, When); +} + +#else + +void ULPDebugSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); +} + +void ULPDebugSubsystem::Deinitialize() +{ + Super::Deinitialize(); +} + +void ULPDebugSubsystem::OnEndFrame() +{ +} + +void ULPDebugSubsystem::LogHitchSummary(const TCHAR* When) const +{ + (void)When; +} + +void ULPDebugSubsystem::SampleCollision() +{ +} + +void ULPDebugSubsystem::LogCollisionSummary(const TCHAR* When) const +{ + (void)When; +} + +#endif diff --git a/Source/LifePlay/LPDebugSubsystem.h b/Source/LifePlay/LPDebugSubsystem.h new file mode 100644 index 0000000..28fe888 --- /dev/null +++ b/Source/LifePlay/LPDebugSubsystem.h @@ -0,0 +1,52 @@ +// Portage UE 5.5 - lot debug (Docs/Migration/18_Build_Debug.md). +// +// Mesure opt-in, absente du comportement Shipping : journal des a-coups, signets Insights, +// instantane memoire, releve de collisions. Aucun Blueprint modifie. +// Inactif tant que -LPHitchLog ou -LPCollisionLog n'est pas passe. +// -LPCollisionLog echantillonne la collision du monde joue (pas au ExecCmds de demarrage) +// et ecrit un dump des qu'un trimesh est present, avec le nombre de triangles par acteur. +// Commandes console (non-Shipping) : LifePlay.MemSnapshot, LifePlay.CollisionDump, +// LifePlay.Bookmark, LifePlay.HitchSummary. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "LPDebugSubsystem.generated.h" + +UCLASS() +class LIFEPLAY_API ULPDebugSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + /** Bilan des a-coups. When : "2 min", "fin" ou "console". */ + void LogHitchSummary(const TCHAR* When) const; + +private: + void OnEndFrame(); + void SampleCollision(); + void LogCollisionSummary(const TCHAR* When) const; + + bool bHitchLog = false; + float HitchMs = 50.f; + int32 HitchCount = 0; + int32 HitchLines = 0; + double MaxHitchMs = 0.0; + double StartSeconds = 0.0; + double LastFrameSeconds = 0.0; + bool bLoggedTwoMinuteSummary = false; + + bool bCollisionLog = false; + double NextCollisionSeconds = 0.0; + int32 CollisionSamples = 0; + int32 CollisionDumps = 0; + int32 PeakPrim = 0; + int64 PeakTriangles = 0; + int64 LastDumpedTriangles = -1; + int32 LastDumpedTriMeshes = -1; + FString CollisionTrackPath; +}; diff --git a/Source/LifePlay/LPModSettingsLibrary.cpp b/Source/LifePlay/LPModSettingsLibrary.cpp index 51d0ff9..a3ce00d 100644 --- a/Source/LifePlay/LPModSettingsLibrary.cpp +++ b/Source/LifePlay/LPModSettingsLibrary.cpp @@ -10,7 +10,7 @@ bool ULPModSettingsLibrary::SaveModSelection(UObject* GameInstance) { static const TCHAR* Slot = TEXT("GameSettings"); - static const FName ModEnabledsName(TEXT("ModEnableds")); + static const FName SettingsModEnabledsName(TEXT("ModEnableds")); if (!GameInstance) { return false; @@ -21,8 +21,8 @@ bool ULPModSettingsLibrary::SaveModSelection(UObject* GameInstance) return false; } USaveGame* Settings = UGameplayStatics::LoadGameFromSlot(Slot, 0); - const FArrayProperty* From = CastField(GameInstance->GetClass()->FindPropertyByName(ModEnabledsName)); - const FArrayProperty* To = Settings ? CastField(Settings->GetClass()->FindPropertyByName(ModEnabledsName)) : nullptr; + const FArrayProperty* From = CastField(GameInstance->GetClass()->FindPropertyByName(SettingsModEnabledsName)); + const FArrayProperty* To = Settings ? CastField(Settings->GetClass()->FindPropertyByName(SettingsModEnabledsName)) : nullptr; if (!From || !To || !From->SameType(To)) { UE_LOG(LogLifePlay, Warning, TEXT("Modules : %s.sav illisible ou sans ModEnableds compatible, choix non enregistre"), Slot); diff --git a/Tools/BuildDebug.ps1 b/Tools/BuildDebug.ps1 new file mode 100644 index 0000000..e9e91e8 --- /dev/null +++ b/Tools/BuildDebug.ps1 @@ -0,0 +1,79 @@ +<# +.SYNOPSIS + Empaquette LifePlay en DebugGame ou Development et depose les lanceurs de session. + +.DESCRIPTION + N'utilise jamais -nodebuginfo : les PDB du projet sont recopies dans l'archive + (IncludeDebugFiles reste False dans DefaultGame.ini, le Shipping n'est pas touche). + Les .bat de Tools\debug sont copies a cote de Windows\LifePlay.exe. + +.EXAMPLE + .\Tools\BuildDebug.ps1 + .\Tools\BuildDebug.ps1 -Config Development + .\Tools\BuildDebug.ps1 -Config DebugGame -SkipBuild +#> +param( + [ValidateSet('DebugGame', 'Development')] + [string]$Config = 'DebugGame', + [string]$ArchiveDir = '', + [string]$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine', + [string]$ProjectRoot = (Split-Path $PSScriptRoot -Parent), + [switch]$SkipBuild +) +$ErrorActionPreference = 'Stop' +if (-not $ArchiveDir) { + $ArchiveDir = "D:\Game\LifePlay_Build_5.5_$Config" +} + +$proj = Join-Path $ProjectRoot 'LifePlay.uproject' +$uat = Join-Path $UE 'Build\BatchFiles\RunUAT.bat' +$stamp = Get-Date -Format 'yyyyMMdd-HHmmss' +$logDir = Join-Path $ProjectRoot 'Saved\Logs\portage' +New-Item -ItemType Directory -Force $logDir | Out-Null +$log = Join-Path $logDir "BuildDebug_${Config}_$stamp.log" + +$buildFlag = '' +if (-not $SkipBuild) { $buildFlag = '-build ' } + +# Pas de -nodebuginfo. -pak -iostore -compressed : reglages courants du projet (lot O2). +# Fichier .cmd : PowerShell couperait une ligne UAT mal quotee (meme parade que BuildMod.ps1). +$batch = [IO.Path]::ChangeExtension($log, '.cmd') +$cmd = "@call `"$uat`" BuildCookRun -project=`"$proj`" -platform=Win64 -clientconfig=$Config $buildFlag-cook -iterate -stage -pak -iostore -compressed -archive -archivedirectory=`"$ArchiveDir`" -unattended -utf8output -nop4 > `"$log`" 2>&1" +[IO.File]::WriteAllText($batch, $cmd + "`r`n@exit /b %ERRORLEVEL%`r`n", (New-Object Text.UTF8Encoding $false)) +Write-Host "> $cmd" +$t0 = Get-Date +& cmd /c $batch +$code = $LASTEXITCODE +$dur = [int]((Get-Date) - $t0).TotalSeconds +Write-Host "UAT code=$code duree=${dur}s journal=$log" +if ($code -ne 0) { exit $code } + +$win = Join-Path $ArchiveDir 'Windows' +$exe = Join-Path $win 'LifePlay.exe' +if (-not (Test-Path $exe)) { + $win = Join-Path $ArchiveDir 'WindowsNoEditor' + $exe = Join-Path $win 'LifePlay.exe' +} +if (-not (Test-Path $exe)) { + Write-Error "LifePlay.exe introuvable sous $ArchiveDir" +} + +$batSrc = Join-Path $PSScriptRoot 'debug' +Copy-Item (Join-Path $batSrc '*.bat') $win -Force +Copy-Item (Join-Path $batSrc 'LifePlay_debug_ui.ps1') $win -Force +$staged = @(Get-ChildItem $win -Filter 'LifePlay_debug_*.bat') +Write-Host ("bats staged={0} -> {1}" -f $staged.Count, $win) + +$bin = Join-Path $win 'LifePlay\Binaries\Win64' +New-Item -ItemType Directory -Force $bin | Out-Null +$pdbSrc = Join-Path $ProjectRoot 'Binaries\Win64' +$copied = 0 +if (Test-Path $pdbSrc) { + Get-ChildItem $pdbSrc -Filter "*Win64-$Config.pdb" | ForEach-Object { + Copy-Item $_.FullName $bin -Force + $copied++ + } +} +Write-Host "pdb copies=$copied -> $bin" +Write-Host "archive=$ArchiveDir exe=$exe" +exit 0 diff --git a/Tools/CollectDebugArtifacts.ps1 b/Tools/CollectDebugArtifacts.ps1 new file mode 100644 index 0000000..2d14801 --- /dev/null +++ b/Tools/CollectDebugArtifacts.ps1 @@ -0,0 +1,100 @@ +<# +.SYNOPSIS + Archive les preuves d'une session debug (journal, CSV, LLM, utrace, MemReport, CVD). + +.EXAMPLE + .\Tools\CollectDebugArtifacts.ps1 -Lot Dbg-I4 -BuildDir D:\Game\LifePlay_Build_5.5_DebugGame -Session A +#> +param( + [Parameter(Mandatory)][string]$Lot, + [Parameter(Mandatory)][string]$BuildDir, + [Parameter(Mandatory)][ValidateSet('A', 'B', 'C', 'bugs', 'gpu', 'collision')][string]$Session, + [string]$BackupRoot = 'D:\Game\LifePlay_Backup_5.5' +) +$ErrorActionPreference = 'Stop' + +function Resolve-SavedDir([string]$Root) { + foreach ($rel in @( + 'Windows\LifePlay\Saved', + 'WindowsNoEditor\LifePlay\Saved', + 'LifePlay\Saved', + 'Saved')) { + $p = Join-Path $Root $rel + if (Test-Path $p) { return $p } + } + return $null +} + +$saved = Resolve-SavedDir $BuildDir +if (-not $saved) { + Write-Error "Saved introuvable sous $BuildDir" +} + +$dest = Join-Path $BackupRoot "$Lot\Session$Session" +New-Item -ItemType Directory -Force $dest | Out-Null + +$copied = New-Object System.Collections.Generic.List[string] + +function Copy-Match([string]$RelativeDir, [string]$Filter) { + $from = Join-Path $saved $RelativeDir + if (-not (Test-Path $from)) { return } + $target = Join-Path $dest $RelativeDir + New-Item -ItemType Directory -Force $target | Out-Null + Get-ChildItem $from -Filter $Filter -File -ErrorAction SilentlyContinue | ForEach-Object { + Copy-Item $_.FullName (Join-Path $target $_.Name) -Force + $script:copied.Add((Join-Path $RelativeDir $_.Name)) + } +} + +Copy-Match 'Logs' '*.log' +Copy-Match 'Profiling' '*.utrace' +Copy-Match 'Profiling\CSV' '*.csv' +Copy-Match 'Profiling\LLM' '*.csv' +Copy-Match 'Profiling\LPDebug' '*.txt' + +$reportRoot = Join-Path $saved 'Profiling\MemReports' +if (Test-Path $reportRoot) { + Get-ChildItem $reportRoot -Recurse -Filter '*.memreport' -File | ForEach-Object { + $rel = $_.FullName.Substring($saved.Length).TrimStart('\') + $target = Join-Path $dest $rel + New-Item -ItemType Directory -Force (Split-Path $target -Parent) | Out-Null + Copy-Item $_.FullName $target -Force + $copied.Add($rel) + } +} + +foreach ($dirName in @('ChaosVisualDebugger', 'ChaosVD')) { + $from = Join-Path $saved $dirName + if (Test-Path $from) { + $target = Join-Path $dest $dirName + Copy-Item $from $target -Recurse -Force + $copied.Add($dirName) + } +} + +Get-ChildItem $saved -Recurse -Include '*.utrace', '*.cvd' -File -ErrorAction SilentlyContinue | ForEach-Object { + $rel = $_.FullName.Substring($saved.Length).TrimStart('\') + if ($copied -contains $rel) { return } + $target = Join-Path $dest $rel + New-Item -ItemType Directory -Force (Split-Path $target -Parent) | Out-Null + Copy-Item $_.FullName $target -Force + $copied.Add($rel) +} + +$manifest = Join-Path $dest 'manifest.txt' +@( + "Lot=$Lot" + "Session=$Session" + "BuildDir=$BuildDir" + "Saved=$saved" + "Date=$(Get-Date -Format s)" + "Files=$($copied.Count)" + '' + $copied +) | Set-Content $manifest -Encoding UTF8 + +Write-Host "dest=$dest fichiers=$($copied.Count)" +if ($copied.Count -lt 1) { + Write-Error "Aucun artefact copie depuis $saved" +} +exit 0 diff --git a/Tools/SummarizeDebugCsv.ps1 b/Tools/SummarizeDebugCsv.ps1 new file mode 100644 index 0000000..32c01e3 --- /dev/null +++ b/Tools/SummarizeDebugCsv.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Mediane FrameTime et decompte des images longues d'un CSV du profileur (session C). + +.EXAMPLE + .\Tools\SummarizeDebugCsv.ps1 -Csv D:\Game\LifePlay_Backup_5.5\Dbg-I6\SessionC\Profiling\CSV +#> +param( + [Parameter(Mandatory)][string]$Csv +) +$ErrorActionPreference = 'Stop' + +$files = @() +if (Test-Path $Csv -PathType Container) { + $files = @(Get-ChildItem $Csv -Filter '*.csv' -File) +} elseif (Test-Path $Csv) { + $files = @(Get-Item $Csv) +} +if ($files.Count -lt 1) { Write-Error "CSV introuvable : $Csv" } + +function Median([double[]]$Values) { + $s = @($Values | Sort-Object) + if ($s.Count -eq 0) { return [double]::NaN } + $mid = [int][math]::Floor($s.Count / 2) + if ($s.Count % 2) { return [double]$s[$mid] } + return ([double]$s[$mid - 1] + [double]$s[$mid]) / 2.0 +} + +$frame = New-Object System.Collections.Generic.List[double] +$game = New-Object System.Collections.Generic.List[double] +$over100 = 0 +$over50 = 0 +$physicsMb = $null + +foreach ($file in $files) { + $rows = Import-Csv $file.FullName + $hasFrame = $rows.Count -gt 0 -and ($rows[0].PSObject.Properties.Name -contains 'FrameTime') + if (-not $hasFrame) { continue } + foreach ($row in $rows) { + $ft = 0.0 + if (-not [double]::TryParse($row.FrameTime, [Globalization.NumberStyles]::Float, [Globalization.CultureInfo]::InvariantCulture, [ref]$ft)) { continue } + $frame.Add($ft) + if ($ft -gt 100) { $over100++ } + if ($ft -gt 50) { $over50++ } + if ($row.PSObject.Properties.Name -contains 'GameThreadTime') { + $gt = 0.0 + if ([double]::TryParse($row.GameThreadTime, [Globalization.NumberStyles]::Float, [Globalization.CultureInfo]::InvariantCulture, [ref]$gt)) { $game.Add($gt) } + } + } + if ($rows.Count -gt 0 -and $rows[0].PSObject.Properties.Name -contains 'LLM/Physics/Chaos') { + $last = $rows[$rows.Count - 1].'LLM/Physics/Chaos' + $bytes = 0.0 + if ([double]::TryParse($last, [Globalization.NumberStyles]::Float, [Globalization.CultureInfo]::InvariantCulture, [ref]$bytes)) { $physicsMb = [math]::Round($bytes / 1MB, 1) } + } +} + +if ($frame.Count -lt 1) { Write-Error "Colonne FrameTime absente dans $Csv" } + +$med = Median $frame.ToArray() +$medGame = Median $game.ToArray() +Write-Host ("Fichiers={0} Images={1} MedianeFrameTimeMs={2:N2} MedianeGameThreadMs={3:N2} ImagesGt50ms={4} ImagesGt100ms={5} PhysicsChaosMB={6}" -f ` + $files.Count, $frame.Count, $med, $medGame, $over50, $over100, $physicsMb) +exit 0 diff --git a/Tools/debug/LifePlay_debug_bugs.bat b/Tools/debug/LifePlay_debug_bugs.bat new file mode 100644 index 0000000..a7fd6d0 --- /dev/null +++ b/Tools/debug/LifePlay_debug_bugs.bat @@ -0,0 +1,4 @@ +@echo off +rem Auxiliaire bugs. Inactif par defaut : -LPDump* et -LPTrace* (voir 09_Commandes_Utiles.md). +setlocal +start "" "%~dp0LifePlay.exe" -LogCmds="LogModdingExtra Verbose" diff --git a/Tools/debug/LifePlay_debug_collision.bat b/Tools/debug/LifePlay_debug_collision.bat new file mode 100644 index 0000000..a29656a --- /dev/null +++ b/Tools/debug/LifePlay_debug_collision.bat @@ -0,0 +1,5 @@ +@echo off +rem Auxiliaire collision. -LPCollisionLog echantillonne apres le chargement +rem (pas de LifePlay.CollisionDump au demarrage : il partait avant la ville). +setlocal +start "" "%~dp0LifePlay.exe" -LPCollisionLog -ExecCmds="p.Chaos.StartVDRecording, show collision" diff --git a/Tools/debug/LifePlay_debug_gpu.bat b/Tools/debug/LifePlay_debug_gpu.bat new file mode 100644 index 0000000..4220a3b --- /dev/null +++ b/Tools/debug/LifePlay_debug_gpu.bat @@ -0,0 +1,5 @@ +@echo off +rem Auxiliaire GPU : ce lanceur prepare PIX (D3D12). +rem Radeon RGP : lancer LifePlay.exe sans ce fichier et attacher le processus. Jamais PIX et RGP ensemble. +setlocal +start "" "%~dp0LifePlay.exe" -d3d12 diff --git a/Tools/debug/LifePlay_debug_sessionA_insights.bat b/Tools/debug/LifePlay_debug_sessionA_insights.bat new file mode 100644 index 0000000..1f193bb --- /dev/null +++ b/Tools/debug/LifePlay_debug_sessionA_insights.bat @@ -0,0 +1,7 @@ +@echo off +rem Session A — Insights (build DebugGame). Scenario O0, environ 15 min. +rem Les millisecondes absolues ne se comparent pas a une build Development. +setlocal +set "SAVED=%~dp0LifePlay\Saved\Profiling" +if not exist "%SAVED%" mkdir "%SAVED%" +start "" "%~dp0LifePlay.exe" -trace=cpu,frame,bookmark,loadtime,memory -statnamedevents -tracefile="%SAVED%\SessionA.utrace" -LPHitchLog diff --git a/Tools/debug/LifePlay_debug_sessionB_mem.bat b/Tools/debug/LifePlay_debug_sessionB_mem.bat new file mode 100644 index 0000000..4e7d7b4 --- /dev/null +++ b/Tools/debug/LifePlay_debug_sessionB_mem.bat @@ -0,0 +1,6 @@ +@echo off +rem Session B — memoire et collisions (build DebugGame). +rem ChaosVD : p.Chaos.StartVDRecording. Si la commande est absente (plugin editeur seul), +rem LifePlay.CollisionDump reste la preuve. Ne pas combiner PIX et RGP. +setlocal +start "" "%~dp0LifePlay.exe" -llm -llmcsv -LPCollisionLog -ExecCmds="LifePlay.MemSnapshot, p.Chaos.StartVDRecording" diff --git a/Tools/debug/LifePlay_debug_sessionC_profil.bat b/Tools/debug/LifePlay_debug_sessionC_profil.bat new file mode 100644 index 0000000..25575c8 --- /dev/null +++ b/Tools/debug/LifePlay_debug_sessionC_profil.bat @@ -0,0 +1,5 @@ +@echo off +rem Session C — non-regression CSV (build Development seulement). Sans -LPHitchLog. +rem Jamais -csvCaptureFrames : la capture demarre avant le RHI et le moteur s'arrete. +setlocal +start "" "%~dp0LifePlay.exe" -csvGpuStats -llm -llmcsv -ExecCmds="csvprofile repeat=20, csvprofile frames=9000" diff --git a/Tools/debug/LifePlay_debug_ui.bat b/Tools/debug/LifePlay_debug_ui.bat new file mode 100644 index 0000000..872afac --- /dev/null +++ b/Tools/debug/LifePlay_debug_ui.bat @@ -0,0 +1,4 @@ +@echo off +rem Fenetre de choix des options de mesure. Le script .ps1 doit rester a cote de ce .bat. +setlocal +powershell -NoProfile -STA -ExecutionPolicy Bypass -File "%~dp0LifePlay_debug_ui.ps1" diff --git a/Tools/debug/LifePlay_debug_ui.ps1 b/Tools/debug/LifePlay_debug_ui.ps1 new file mode 100644 index 0000000..e517a61 --- /dev/null +++ b/Tools/debug/LifePlay_debug_ui.ps1 @@ -0,0 +1,275 @@ +# Lanceur graphique des options de mesure (Docs/Migration/18_Build_Debug.md). +# Double-clic : LifePlay_debug_ui.bat. Ne propose jamais -csvCaptureFrames. + +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +$ErrorActionPreference = 'Stop' + +function Find-GameExes { + $here = Split-Path -Parent $PSCommandPath + $list = New-Object System.Collections.Generic.List[string] + $local = Join-Path $here 'LifePlay.exe' + if (Test-Path -LiteralPath $local) { $list.Add($local) } + foreach ($p in @( + 'D:\Game\LifePlay_Build_5.5_Development\Windows\LifePlay.exe', + 'D:\Game\LifePlay_Build_5.5_DebugGame\Windows\LifePlay.exe')) { + if ((Test-Path -LiteralPath $p) -and -not $list.Contains($p)) { $list.Add($p) } + } + return $list +} + +$script:Applying = $false + +$form = New-Object System.Windows.Forms.Form +$form.Text = 'LifePlay - options de mesure' +$form.StartPosition = 'CenterScreen' +$form.FormBorderStyle = 'FixedDialog' +$form.MaximizeBox = $false +$form.ClientSize = New-Object System.Drawing.Size(560, 640) +$form.Font = New-Object System.Drawing.Font('Segoe UI', 9) + +$y = 12 +$lblGame = New-Object System.Windows.Forms.Label +$lblGame.Text = 'Jeu' +$lblGame.Location = New-Object System.Drawing.Point(16, $y) +$lblGame.AutoSize = $true +$form.Controls.Add($lblGame) + +$cmbExe = New-Object System.Windows.Forms.ComboBox +$cmbExe.DropDownStyle = 'DropDownList' +$cmbExe.Location = New-Object System.Drawing.Point(16, 32) +$cmbExe.Width = 420 +$form.Controls.Add($cmbExe) + +$btnBrowse = New-Object System.Windows.Forms.Button +$btnBrowse.Text = 'Parcourir' +$btnBrowse.Location = New-Object System.Drawing.Point(444, 30) +$btnBrowse.Width = 100 +$form.Controls.Add($btnBrowse) + +$lblPreset = New-Object System.Windows.Forms.Label +$lblPreset.Text = 'Reglage' +$lblPreset.Location = New-Object System.Drawing.Point(16, 68) +$lblPreset.AutoSize = $true +$form.Controls.Add($lblPreset) + +$cmbPreset = New-Object System.Windows.Forms.ComboBox +$cmbPreset.DropDownStyle = 'DropDownList' +$cmbPreset.Location = New-Object System.Drawing.Point(16, 88) +$cmbPreset.Width = 528 +[void]$cmbPreset.Items.AddRange(@( + 'Personnalise', + 'Collisions', + 'Memoire et physique (session B)', + 'CSV 9000 images (session C)', + 'Insights court, sans memory', + 'Journal d''a-coups' + )) +$cmbPreset.SelectedIndex = 1 +$form.Controls.Add($cmbPreset) + +function New-Check([string]$Text, [int]$Top) { + $c = New-Object System.Windows.Forms.CheckBox + $c.Text = $Text + $c.Location = New-Object System.Drawing.Point(16, $Top) + $c.AutoSize = $true + $c.Width = 520 + return $c +} + +$chkHitch = New-Check 'Journal d''a-coups (-LPHitchLog)' 128 +$chkCol = New-Check 'Suivi des collisions (-LPCollisionLog), apres le chargement' 156 +$chkCvd = New-Check 'Enregistrement ChaosVD (p.Chaos.StartVDRecording)' 184 +$chkShow = New-Check 'Afficher les collisions (show collision)' 212 +$chkSnap = New-Check 'Instantane memoire au demarrage (LifePlay.MemSnapshot)' 240 +$chkLlm = New-Check 'Memoire LLM (-llm -llmcsv)' 268 +$chkCsv = New-Check 'CSV 9000 images, demarre apres le rendu (pas -csvCaptureFrames)' 296 +$chkTrace = New-Check 'Trace Insights : cpu, frame, bookmark, loadtime' 324 +$chkMemCh = New-Check 'Canal memory (plusieurs Go, Insights sature au-dela)' 352 +$chkVerbose = New-Check 'Journal des mods verbeux (LogModdingExtra)' 380 +$chkPix = New-Check 'PIX / D3D12 (-d3d12). Ne pas attacher RGP en meme temps.' 408 +foreach ($c in @($chkHitch, $chkCol, $chkCvd, $chkShow, $chkSnap, $chkLlm, $chkCsv, $chkTrace, $chkMemCh, $chkVerbose, $chkPix)) { + $form.Controls.Add($c) +} + +$lblMs = New-Object System.Windows.Forms.Label +$lblMs.Text = 'Seuil (ms)' +$lblMs.Location = New-Object System.Drawing.Point(300, 130) +$lblMs.AutoSize = $true +$form.Controls.Add($lblMs) +$txtMs = New-Object System.Windows.Forms.TextBox +$txtMs.Text = '50' +$txtMs.Location = New-Object System.Drawing.Point(370, 126) +$txtMs.Width = 48 +$form.Controls.Add($txtMs) + +$txtCmd = New-Object System.Windows.Forms.TextBox +$txtCmd.Location = New-Object System.Drawing.Point(16, 448) +$txtCmd.Size = New-Object System.Drawing.Size(528, 110) +$txtCmd.Multiline = $true +$txtCmd.ReadOnly = $true +$txtCmd.ScrollBars = 'Vertical' +$txtCmd.Font = New-Object System.Drawing.Font('Consolas', 8) +$form.Controls.Add($txtCmd) + +$lblWarn = New-Object System.Windows.Forms.Label +$lblWarn.AutoSize = $false +$lblWarn.Location = New-Object System.Drawing.Point(16, 564) +$lblWarn.Size = New-Object System.Drawing.Size(360, 36) +$form.Controls.Add($lblWarn) + +$btnLaunch = New-Object System.Windows.Forms.Button +$btnLaunch.Text = 'Lancer' +$btnLaunch.Location = New-Object System.Drawing.Point(388, 592) +$btnLaunch.Width = 156 +$btnLaunch.Height = 32 +$form.Controls.Add($btnLaunch) +$form.AcceptButton = $btnLaunch + +function Get-SelectedExe { + if ($cmbExe.SelectedItem) { return [string]$cmbExe.SelectedItem } + return '' +} + +function Build-Args { + $exe = Get-SelectedExe + $parts = New-Object System.Collections.Generic.List[string] + if ($chkHitch.Checked) { + $parts.Add('-LPHitchLog') + $ms = 50 + [void][int]::TryParse($txtMs.Text.Trim(), [ref]$ms) + if ($ms -gt 0 -and $ms -ne 50) { $parts.Add("-LPHitchMs=$ms") } + } + if ($chkCol.Checked) { $parts.Add('-LPCollisionLog') } + if ($chkLlm.Checked) { $parts.Add('-llm'); $parts.Add('-llmcsv') } + if ($chkCsv.Checked) { $parts.Add('-csvGpuStats') } + if ($chkTrace.Checked -and $exe) { + $channels = 'cpu,frame,bookmark,loadtime' + if ($chkMemCh.Checked) { $channels += ',memory' } + $parts.Add("-trace=$channels") + $parts.Add('-statnamedevents') + $saved = Join-Path (Split-Path -Parent $exe) 'LifePlay\Saved\Profiling' + $file = Join-Path $saved ('UI_{0:yyyyMMdd_HHmmss}.utrace' -f (Get-Date)) + $parts.Add("-tracefile=`"$file`"") + } + if ($chkPix.Checked) { $parts.Add('-d3d12') } + if ($chkVerbose.Checked) { $parts.Add('-LogCmds="LogModdingExtra Verbose"') } + + $exec = New-Object System.Collections.Generic.List[string] + if ($chkCsv.Checked) { + $exec.Add('csvprofile repeat=20') + $exec.Add('csvprofile frames=9000') + } + if ($chkSnap.Checked) { $exec.Add('LifePlay.MemSnapshot') } + if ($chkCvd.Checked) { $exec.Add('p.Chaos.StartVDRecording') } + if ($chkShow.Checked) { $exec.Add('show collision') } + if ($exec.Count -gt 0) { $parts.Add('-ExecCmds="' + ($exec -join ', ') + '"') } + return $parts +} + +function Update-Preview { + $chkMemCh.Enabled = $chkTrace.Checked + $txtMs.Enabled = $chkHitch.Checked + if (-not $chkTrace.Checked) { $chkMemCh.Checked = $false } + $exe = Get-SelectedExe + $parts = Build-Args + if (-not $exe) { + $txtCmd.Text = 'LifePlay.exe introuvable.' + $btnLaunch.Enabled = $false + } + else { + $txtCmd.Text = "`"$exe`"`r`n" + ($parts -join "`r`n") + $btnLaunch.Enabled = $true + } + $notes = New-Object System.Collections.Generic.List[string] + if ($chkCsv.Checked -and $chkHitch.Checked) { + $notes.Add('La session CSV de non-regression se joue sans journal d''a-coups.') + } + if ($chkMemCh.Checked) { + $notes.Add('Le canal memory a deja produit une trace de 6,4 Go.') + } + if ($exe -match 'DebugGame' -and $chkCsv.Checked) { + $notes.Add('Les ms DebugGame ne se comparent pas a Development.') + } + $lblWarn.Text = ($notes -join ' ') +} + +function Apply-Preset([string]$Name) { + $map = @{ + 'Collisions' = @{ H = $false; C = $true; V = $true; S = $true; M = $false; L = $false; P = $false; T = $false; Mem = $false; Verb = $false; Pix = $false } + 'Memoire et physique (session B)' = @{ H = $false; C = $true; V = $true; S = $false; M = $true; L = $true; P = $false; T = $false; Mem = $false; Verb = $false; Pix = $false } + 'CSV 9000 images (session C)' = @{ H = $false; C = $false; V = $false; S = $false; M = $false; L = $true; P = $true; T = $false; Mem = $false; Verb = $false; Pix = $false } + 'Insights court, sans memory' = @{ H = $true; C = $false; V = $false; S = $false; M = $false; L = $false; P = $false; T = $true; Mem = $false; Verb = $false; Pix = $false } + 'Journal d''a-coups' = @{ H = $true; C = $false; V = $false; S = $false; M = $false; L = $false; P = $false; T = $false; Mem = $false; Verb = $false; Pix = $false } + } + if (-not $map.ContainsKey($Name)) { return } + $p = $map[$Name] + $script:Applying = $true + $chkHitch.Checked = $p.H + $chkCol.Checked = $p.C + $chkCvd.Checked = $p.V + $chkShow.Checked = $p.S + $chkSnap.Checked = $p.M + $chkLlm.Checked = $p.L + $chkCsv.Checked = $p.P + $chkTrace.Checked = $p.T + $chkMemCh.Checked = $p.Mem + $chkVerbose.Checked = $p.Verb + $chkPix.Checked = $p.Pix + $script:Applying = $false + Update-Preview +} + +function Mark-Custom { + if ($script:Applying) { return } + $script:Applying = $true + $cmbPreset.SelectedItem = 'Personnalise' + $script:Applying = $false + Update-Preview +} + +foreach ($exe in (Find-GameExes)) { [void]$cmbExe.Items.Add($exe) } +if ($cmbExe.Items.Count -gt 0) { $cmbExe.SelectedIndex = 0 } + +$cmbExe.Add_SelectedIndexChanged({ Update-Preview }) +$cmbPreset.Add_SelectedIndexChanged({ + if ($script:Applying) { return } + Apply-Preset ([string]$cmbPreset.SelectedItem) + }) +foreach ($c in @($chkHitch, $chkCol, $chkCvd, $chkShow, $chkSnap, $chkLlm, $chkCsv, $chkTrace, $chkMemCh, $chkVerbose, $chkPix)) { + $c.Add_CheckedChanged({ Mark-Custom }) +} +$txtMs.Add_TextChanged({ Update-Preview }) + +$btnBrowse.Add_Click({ + $dlg = New-Object System.Windows.Forms.OpenFileDialog + $dlg.Filter = 'LifePlay.exe|LifePlay.exe' + $dlg.Title = 'Choisir LifePlay.exe' + if ($dlg.ShowDialog() -eq 'OK') { + if (-not $cmbExe.Items.Contains($dlg.FileName)) { [void]$cmbExe.Items.Add($dlg.FileName) } + $cmbExe.SelectedItem = $dlg.FileName + } + }) + +$btnLaunch.Add_Click({ + $exe = Get-SelectedExe + if (-not $exe -or -not (Test-Path -LiteralPath $exe)) { + [System.Windows.Forms.MessageBox]::Show('LifePlay.exe introuvable.', 'LifePlay') + return + } + if ($chkTrace.Checked) { + $saved = Join-Path (Split-Path -Parent $exe) 'LifePlay\Saved\Profiling' + New-Item -ItemType Directory -Force -Path $saved | Out-Null + } + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $exe + $psi.WorkingDirectory = Split-Path -Parent $exe + $psi.UseShellExecute = $true + $psi.Arguments = (Build-Args) -join ' ' + [void][System.Diagnostics.Process]::Start($psi) + $form.Close() + }) + +Apply-Preset 'Collisions' +[void]$form.ShowDialog()