perf: lag nelle zone di rilievo (terreno Z-displaced) #240

Closed
opened 2026-08-01 13:22:08 +02:00 by panda · 3 comments
Collaborator

Muoversi/teletrasportarsi nelle zone di rilievo autorato di asterra (le 64 elevation-chunk: il massiccio vulcanico ma anche i foothill rocciosi) lagga.

Correla col rilievo, non con l'animazione lava — anche il foothill roccioso (Z~1 vicino a Z≥20) lagga, quindi non è la lava. Sospetto: overdraw dalla mesh del terreno spostata in altezza (celle a Z diversi si sovrappongono) + ricostruzione mesh / ricalcolo finestra blend mentre la vista si muove.

Pre-esistente, non causato dalla base-Z (#238): un baseline uniforme non aggiunge overdraw — solo le differenze di Z (le montagne autorate) lo fanno; il rilievo c'era già (in 7b vedevamo solo lo spawn piatto).

Da profilare: è CPU (mesh/GroundZAt per-frame) o GPU (overdraw)? Confrontare con main/pre-relief. Possibili leve: limitare/cullare lo ZMargin, cache della mesh a vista ferma, ridurre l'overdraw.

Muoversi/teletrasportarsi nelle **zone di rilievo autorato** di asterra (le 64 elevation-chunk: il massiccio vulcanico ma anche i foothill rocciosi) **lagga**. **Correla col rilievo, non con l'animazione lava** — anche il foothill roccioso (Z~1 vicino a Z≥20) lagga, quindi non è la lava. Sospetto: **overdraw** dalla mesh del terreno spostata in altezza (celle a Z diversi si sovrappongono) + ricostruzione mesh / ricalcolo finestra blend mentre la vista si muove. **Pre-esistente**, non causato dalla base-Z (#238): un baseline uniforme non aggiunge overdraw — solo le *differenze* di Z (le montagne autorate) lo fanno; il rilievo c'era già (in 7b vedevamo solo lo spawn piatto). Da profilare: è CPU (mesh/`GroundZAt` per-frame) o GPU (overdraw)? Confrontare con `main`/pre-relief. Possibili leve: limitare/cullare lo `ZMargin`, cache della mesh a vista ferma, ridurre l'overdraw.
Author
Collaborator

Root cause CONFERMATA (profiling)

Strumentato il client (tempi per-fase del Draw + dt reale tra frame + IsRunningSlowly), misurato in 3 zone:

Zona blend (GroundBlend.Draw) frame note
Spawn (piatto) ~2.1 ms ~2.5 ms baseline
Vetta vulcanica (Z uniforme ~49) ~0.9 ms ~1.3 ms Z alto ma senza gradiente in vista
Foothill (piano Z1 accanto a collina Z≥20) ~11.8 ms ~12 ms maxDt ~20 ms → sfiora il vsync

ensureWin≈0, buildMesh≈0.5ms → gli ~11 ms sono nel draw dello shader (DrawUserIndexedPrimitives). drops≈0, slow=0, ma il frame a ~12 ms sfiora il budget vsync (16.6 ms) → slip occasionali = micro-stutter, peggiore muovendosi (la camera scorre e tiene più gradiente in vista).

Esperimento decisivo — appiattita la mesh del blend (ToScreen(x,y,0) invece di GroundZAt), stesso identico punto (foothill 7523,4887):

Mesh blend frame
Z-displaced (reale) 11.8 ms 12 ms
Appiattita (Z=0) 2.8 ms 3.2 ms

~4x solo togliendo il dislivello. Causa = overdraw dalla mesh Z-spostata.

Meccanismo

Il ground SDF è disegnato come una mesh finestrata con BlendState.Opaque + DepthStencilState.None (nessun depth-test). Con il Z, celle a quote diverse si sovrappongono sullo schermo; senza depth-buffer, lo shader SDF (costoso per-pixel: warp + noise + campionamento di più palette/weight/atlas) gira su tutti i frammenti sovrapposti, anche quelli occlusi dietro il terreno più alto. Costo ∝ gradiente di Z in vista (per questo la vetta a Z uniforme è economica e il piano-accanto-a-collina è caro). Non c'entrano né la lava animata né la base-Z (uno shift uniforme non aggiunge gradiente).

Fix (direzione)

Renderizzare solo il visibile — scartare i frammenti occlusi:

  • Depth-test: dare al ground un depth-buffer e far scrivere allo shader una profondità iso (es. dalla Y-schermo), con depth-test attivo → l'early-Z scarta i pixel occlusi → overdraw ~eliminato. È la via pulita e diretta.
  • (Alternative: cull CPU delle celle occluse; ridurre il costo per-pixel dello shader.)

Nota per #239 (hillshade): l'hillshade aggiunge costo per-pixel al ground, quindi conviene sistemare l'overdraw (depth-test) prima o insieme.

## Root cause CONFERMATA (profiling) Strumentato il client (tempi per-fase del Draw + dt reale tra frame + `IsRunningSlowly`), misurato in 3 zone: | Zona | blend (GroundBlend.Draw) | frame | note | |------|----|----|----| | Spawn (piatto) | ~2.1 ms | ~2.5 ms | baseline | | Vetta vulcanica (Z uniforme ~49) | ~0.9 ms | ~1.3 ms | **Z alto ma senza gradiente in vista** | | Foothill (piano Z1 **accanto** a collina Z≥20) | **~11.8 ms** | ~12 ms | maxDt ~20 ms → sfiora il vsync | `ensureWin≈0`, `buildMesh≈0.5ms` → gli ~11 ms sono nel **draw dello shader** (`DrawUserIndexedPrimitives`). `drops≈0, slow=0`, ma il frame a ~12 ms **sfiora il budget vsync (16.6 ms)** → slip occasionali = **micro-stutter**, peggiore muovendosi (la camera scorre e tiene più gradiente in vista). **Esperimento decisivo** — appiattita la mesh del blend (`ToScreen(x,y,0)` invece di `GroundZAt`), stesso identico punto (foothill 7523,4887): | Mesh | blend | frame | |------|----|----| | Z-displaced (reale) | 11.8 ms | 12 ms | | **Appiattita (Z=0)** | **2.8 ms** | 3.2 ms | → **~4x** solo togliendo il dislivello. **Causa = overdraw dalla mesh Z-spostata.** ## Meccanismo Il ground SDF è disegnato come **una mesh finestrata** con `BlendState.Opaque` + **`DepthStencilState.None`** (nessun depth-test). Con il Z, celle a quote diverse si **sovrappongono** sullo schermo; senza depth-buffer, lo **shader SDF (costoso per-pixel: warp + noise + campionamento di più palette/weight/atlas)** gira su **tutti** i frammenti sovrapposti, anche quelli occlusi dietro il terreno più alto. Costo ∝ **gradiente di Z in vista** (per questo la vetta a Z uniforme è economica e il piano-accanto-a-collina è caro). Non c'entrano né la lava animata né la base-Z (uno shift uniforme non aggiunge gradiente). ## Fix (direzione) **Renderizzare solo il visibile** — scartare i frammenti occlusi: - **Depth-test**: dare al ground un depth-buffer e far scrivere allo shader una profondità iso (es. dalla Y-schermo), con depth-test attivo → l'early-Z scarta i pixel occlusi → overdraw ~eliminato. È la via pulita e diretta. - (Alternative: cull CPU delle celle occluse; ridurre il costo per-pixel dello shader.) Nota per #239 (hillshade): l'hillshade **aggiunge** costo per-pixel al ground, quindi conviene sistemare l'overdraw (depth-test) **prima o insieme**.
Author
Collaborator

Tentativo 1 — depth-test single-pass: FALLITO (scartato)

Provato: depth nel vertice (ordine iso da gx+gy), draw near→far, DepthStencilState LessEqual + write; depth-buffer sul back-buffer e sul render-target ghost.

Esito, misurato allo stesso punto (foothill 7523,4887):

  • perf peggiore: blend ~11.8ms → ~14.2ms (+overhead depth, nessun guadagno);
  • visivo rotto: terreno quasi tutto nero (frammenti visibili scartati).

Perché fallisce (evidenza, non ipotesi): su DesktopGL/OpenGL con effetto SM3 (MojoShader→GLSL vecchio), l'early-Z non si attiva automaticamente (serve layout(early_fragment_tests), non emettibile via MGCB/SM3). Quindi il depth-test è late (dopo lo shader): lo shader paga comunque tutto l'overdraw, più l'overhead del depth. Un single-pass depth-test non può aiutare qui, a prescindere dal sign del depth.

Approccio affidabile — depth pre-pass (2 pass)

  1. Pass 1: disegna la mesh depth-only con un pixel-shader banale (color-write off) → riempie il depth-buffer a costo ~nullo (vertex-bound).
  2. Pass 2: ridisegna con DepthFunc = Equal (no depth-write) + lo shader SDF vero → ogni pixel viene shadato una sola volta (solo il visibile), overdraw eliminato indipendentemente dall'early-Z.

Costo: richiede un cambio allo shader (ground_blend.fx con una tecnica depth-only) + rebuild MGCB dell'.xnb (vedi memoria monogame-shaders). Cambio più grande e con più rischio del previsto.

Nota di sequenza

#239 (hillshade) rilavora comunque lo stesso ground shader. Conviene fare l'overdraw-fix (pre-pass) insieme all'hillshade in un'unica rilavorazione del ground — un solo cambio shader/MGCB, testato insieme — invece di due interventi separati sullo stesso .fx. Il lag attuale è micro-stutter (nessun drop netto), quindi tollerabile nel frattempo.

## Tentativo 1 — depth-test single-pass: FALLITO (scartato) Provato: depth nel vertice (ordine iso da `gx+gy`), draw near→far, `DepthStencilState` LessEqual + write; depth-buffer sul back-buffer e sul render-target ghost. **Esito, misurato allo stesso punto (foothill 7523,4887):** - perf **peggiore**: blend ~11.8ms → **~14.2ms** (+overhead depth, nessun guadagno); - visivo **rotto**: terreno quasi tutto nero (frammenti visibili scartati). **Perché fallisce (evidenza, non ipotesi):** su **DesktopGL/OpenGL** con effetto **SM3** (MojoShader→GLSL vecchio), l'**early-Z non si attiva** automaticamente (serve `layout(early_fragment_tests)`, non emettibile via MGCB/SM3). Quindi il depth-test è **late** (dopo lo shader): lo shader paga **comunque** tutto l'overdraw, più l'overhead del depth. Un single-pass depth-test non può aiutare qui, a prescindere dal sign del depth. ## Approccio affidabile — depth pre-pass (2 pass) 1. **Pass 1**: disegna la mesh **depth-only** con un pixel-shader banale (color-write off) → riempie il depth-buffer a costo ~nullo (vertex-bound). 2. **Pass 2**: ridisegna con `DepthFunc = Equal` (no depth-write) + lo shader SDF vero → **ogni pixel viene shadato una sola volta** (solo il visibile), overdraw eliminato **indipendentemente** dall'early-Z. Costo: richiede un **cambio allo shader** (`ground_blend.fx` con una tecnica depth-only) + **rebuild MGCB** dell'`.xnb` (vedi memoria `monogame-shaders`). Cambio più grande e con più rischio del previsto. ## Nota di sequenza **#239 (hillshade) rilavora comunque lo stesso ground shader.** Conviene fare l'overdraw-fix (pre-pass) **insieme** all'hillshade in un'unica rilavorazione del ground — un solo cambio shader/MGCB, testato insieme — invece di due interventi separati sullo stesso `.fx`. Il lag attuale è micro-stutter (nessun drop netto), quindi tollerabile nel frattempo.
Author
Collaborator

Marco aiutami invece di farti le unghie a tema pokemon 💅

Risolta: deferred shading (fix overdraw zone di rilievo) su main (#243).

Marco aiutami invece di farti le unghie a tema pokemon 💅 Risolta: deferred shading (fix overdraw zone di rilievo) su main (#243).
panda closed this issue 2026-08-02 00:47:16 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo#240
No description provided.