Files
tactical-shooter/docs/performance-budget.md
T
shawn e385eae0f5 feat: performance budget profiling setup
- docs/performance-budget.md: 60fps/2GB VRAM budget with CPU, GPU,
  memory, and draw-call targets (P95<16.6ms, P99<50ms)
- scripts/profiling/: SceneTree + Node profilers, test scenes
- scripts/generate_occluders.gd: auto-generate OccluderInstance3D
  from CSG wall pieces (128^3 voxel occlusion culling)
- scripts/convert_csg_to_mesh.gd: CSG->StaticMesh baker with LOD
  slots (LOD1@15m, LOD2@30m)
- project.godot: occlusion culling + LOD settings enabled
- modular scene UV2 data from lightmapping pass
- rcon_command_handler.gd conflict resolved (kept upstream plugin cmd)
- Fixed profiler_node.gd warmup frame bug (60 frames, not 3)
- Fixed convert_csg_to_mesh.gd LOD API (add_lod + distance)
2026-07-01 19:01:03 -04:00

185 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Performance Budget & Profiling Methodology
> **Target hardware:** GTX 1050 / RX 560 class (2 GB VRAM)
> **Engine:** Godot 4.7 Forward+ renderer
> **Frame target:** 60 fps (16.6 ms per frame)
---
## 1. Frame Budget (16.6 ms)
| Phase | Budget | Notes |
|---------------|--------|-------|
| GPU total | 8.0 ms | Forward+ opaque pass, transparent, sky, tonemap |
| CPU — physics | 1.5 ms | 128-tick, capsule sweep, raycasts |
| CPU — render | 1.5 ms | Culling, sorting, draw-call submission |
| CPU — gameplay| 3.0 ms | Input, animation, network interpolation |
| **Headroom** | **2.6 ms** | Buffer for frametime variance (≈15%) |
### Bottleneck detection thresholds
| Metric | Green | Yellow | Red |
|----------------|------------|---------------|---------------|
| GPU frame ms | < 5.0 | 5.07.5 | > 7.5 |
| CPU frame ms | < 10.0 | 10.013.0 | > 13.0 |
| Draw calls | < 1200 | 12001800 | > 2000 |
| Triangles / frame | < 300K | 300K600K | > 1M |
| VRAM used | < 1.2 GB | 1.21.7 GB | > 1.8 GB |
| Main RAM used | < 2.0 GB | 2.03.0 GB | > 3.5 GB |
---
## 2. Rendering Budget
### Triangle Count
| Asset type | LOD0 tris | LOD1 tris (50%) | LOD2 tris (25%) | Instances/map |
|-----------------|-----------|-----------------|-----------------|---------------|
| Wall panel | 800 | 400 | 200 | ~600 |
| Floor slab | 800 | 400 | 200 | ~400 |
| Pillar | 400 | 200 | 100 | ~80 |
| Doorway / window| 1200 | 600 | 300 | ~60 |
| Accent panel | 400 | 200 | 100 | ~40 |
**Budget calculation** (max visible):
- Walls ≈ 50 visible × 800 = 40K (LOD0)
- Floors ≈ 20 visible × 800 = 16K (LOD0)
- Details ≈ 30 visible × 400 = 12K (LOD0)
- **Total LOD0 tris ≈ 68K** — well within budget
- With LOD + occlusion, visible tris on target GPU < 200K
### Draw Calls
| Pass | Draw calls | Notes |
|---------------|------------|-------|
| Opaque solids | 600800 | Per-object, one per visible piece |
| Shadows | 200400 | Single directional cascade |
| Transparent | 50100 | Reflection probes, lightmap |
| UI | 100200 | HUD, minimap, scoreboard |
| **Total** | **9501500** | Well within 2000 target |
### VRAM Budget
| Category | Size | Notes |
|----------------------|----------|-------|
| Texture pool (1K × 7 mats) | 75 MB | 28 PBR maps |
| Lightmap atlas | 64128 MB| 2048 × 2048, DXT5 |
| Shadow map | 16 MB | 2048 × 2048 atlas |
| Mesh geometry | 2040 MB | LOD0LOD2 |
| Reflection probe cubemaps | 8 MB | 8 × 512 cubemaps |
| Audio + misc | 50 MB | |
| **Total base** | **~300 MB** | |
| **Headroom** | **1.7 GB** | For gameplay assets, streaming |
---
## 3. LOD Strategy
### CSG-based pieces (current)
Since the modular kit uses CSG nodes (procedural geometry), traditional mesh LOD is not directly supported. The equivalent optimizations are:
| Technique | Benefit | How |
|----------------------------|---------|-----|
| **Visibility ranges** | Skip rendering past cutoff | `visibility_range_end` on each CSGBox3D/Combiner |
| **Occlusion culling** | Skip behind-occluder geometry | OccluderInstance3D on major walls |
| **CSG → Mesh baking** | Replace procedural with static mesh | `convert_csg_to_mesh.gd` tool script |
| **Mesh LOD after baking** | Traditional LOD on static meshes | Godot ImporterMesh LOD |
### Visibility Range Thresholds (CSG phase)
| Asset | Hide beyond | Rationale |
|---------------------|-------------|-----------|
| Wall panels | 50 m | OC sightlines rarely exceed 40 m |
| Floor slabs | 60 m | Visible as ground plane further |
| Pillars | 30 m | Thin silhouette, cull earlier |
| Beams | 30 m | Ceiling detail, only visible indoors |
| Accent panels | 20 m | Small team-color decals |
| Doorway / window | 40 m | Opening shapes visible at medium range |
### Mesh LOD Targets (baked mesh phase)
| LOD | Distance | Tris % | Notes |
|-----|----------|--------|-------|
| 0 | 015 m | 100% | Full detail, panel gaps |
| 1 | 1540 m | 50% | Collapse bevels, remove small holes |
| 2 | 4080 m | 25% | Planar collapse, aggressive decimate |
| 3 | 80 m+ | CULL | Not visible at competitive sightlines |
---
## 4. Occlusion Culling
### Implementation
- **OccluderInstance3D** nodes auto-generated from wall CSGBox3D geometry
- **Voxel resolution**: 128³ (good balance of accuracy vs memory)
- **Generation script**: `client/scripts/generate_occluders.gd``@tool` script
- Activation: Scene tree → Add OccluderInstance3D → Set occluder shape to box matching wall extents
### Occluder Coverage
| Occluder origin | Covers | Shape |
|----------------------|-----------------------|--------|
| Exterior walls | Block view of outdoor | Box |
| Interior dividers | Lane separation | Box |
| Pillar clusters | Mid-range detail | Box |
Expected culling benefit: **3050% reduction in visible geometry** on a 3-lane map with interior dividers.
---
## 5. Profiling Methodology
### Toolchain
| Tool | Use |
|------|-----|
| Godot Editor Debugger | Real-time frame profiler, monitor, 3D scene debug |
| `profile_scene.gd` | Headless frame-time capture |
| GPU vendor tools (NVIDIA NSight / Radeon GPA) | GPU-bound profiling |
| Frame debugger (Godot built-in) | Draw-call inspection |
### Profiling Workflow
1. **CPU baseline**: Run `profile_scene.gd --headless` on target hardware
2. **GPU baseline**: Capture 1000+ frames with editor profiler, record p50/p95/p99
3. **Draw call audit**: Use frame debugger to count and classify draw calls
4. **Occlusion test**: Toggle OccluderInstance3D visibility, measure FPS delta
5. **LOD validation**: Check LOD transitions with debug visualization
6. **Thermal test**: 30-minute continuous gameplay, log frame-time variance
### Pass/Fail Criteria
- **PASS**: holds 60 fps on target hardware for 95% of frames across all 3 maps
- **ACCEPTABLE**: drops to 4555 fps during intense firefights but recovers within 2 s
- **FAIL**: sustained < 45 fps or > 100 ms frame-time spike
---
## 6. Reference Frames
### `kit_demo.tscn` (indoor single-room, 5.12 × 5.12 m)
- ~11 CSG pieces, 8 materials
- Estimated draw calls: ~2030
- Estimated triangles: ~8K12K
- VRAM: ~80 MB (materials + lightmap)
- **Expected: > 200 fps on GTX 1050**
### `template_map.tscn` (3-lane showroom, 20 × 16 m)
- ~25 CSG pieces + prefabs
- Estimated draw calls: ~150250
- Estimated triangles: ~40K60K
- VRAM: ~200 MB (materials + lightmap + probes)
- **Expected: 90120 fps on GTX 1050**
### Full competitive map (target, ~80 × 60 m)
- ~800 CSG pieces (when built from modular kit)
- Estimated draw calls: ~1500 (with LOD + occlusion)
- Estimated triangles: ~200K400K
- VRAM: ~500 MB
- **Target: 60 fps on GTX 1050**