Dbg : outillage de debug (BuildDebug, LPDebugSubsystem, lanceurs, événements LoadTxt / LPDataCache), documents 18 et 19

Phase Dbg (CHG-114, 117, 118).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-07 15:49:01 +02:00
parent 14768a96a8
commit b8414af456
20 changed files with 1708 additions and 8 deletions

View file

@ -98,10 +98,10 @@ $Proj = 'D:\Game\LifePlay-master 5.5\LifePlay.uproject'
-archivedirectory=D:\Game\LifePlay_Build_5.5_<nom> -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 `<archivedirectory>\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),

View file

@ -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 <chemin>] [-UE <moteur>] [-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-<Config>.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_<config>_<horodatage>.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=<Saved>/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 <lot> -BuildDir <archive> -Session A|B|C|bugs|gpu|collision
```
Destination : `D:\Game\LifePlay_Backup_5.5\<lot>\Session<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 <dossier ou fichier 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 <texte>` | `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> ms (seuil <seuil>) #<n>`.
É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.

View file

@ -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

View file

@ -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`) |

View file

@ -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;

View file

@ -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();

View file

@ -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<FString> 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<FString> 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<FString, int32> ByClass;
TMap<FString, int64> TrisByClass;
TArray<FCollisionOwner> 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<UStaticMeshComponent>(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<const UBodySetup*> Bodies;
for (TActorIterator<AActor> It(World); It; ++It)
{
const FString ClassName = It->GetClass()->GetName();
TInlineComponentArray<UPrimitiveComponent*> 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<FString> 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<TPair<FString, int32>> Classes;
for (const TPair<FString, int32>& Pair : Tally.ByClass)
{
Classes.Add(Pair);
}
Classes.Sort([](const TPair<FString, int32>& A, const TPair<FString, int32>& 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<FString>& 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

View file

@ -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;
};

View file

@ -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<FArrayProperty>(GameInstance->GetClass()->FindPropertyByName(ModEnabledsName));
const FArrayProperty* To = Settings ? CastField<FArrayProperty>(Settings->GetClass()->FindPropertyByName(ModEnabledsName)) : nullptr;
const FArrayProperty* From = CastField<FArrayProperty>(GameInstance->GetClass()->FindPropertyByName(SettingsModEnabledsName));
const FArrayProperty* To = Settings ? CastField<FArrayProperty>(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);

79
Tools/BuildDebug.ps1 Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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()