diff --git a/docs/adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md b/docs/adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md index 36584fd..b16a99b 100644 --- a/docs/adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md +++ b/docs/adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md @@ -68,6 +68,13 @@ System, das einen Durchlauf über dieselbe Gegnermenge macht, kostet höchstens sein) und der Turm steht fest in der Mitte — es gibt keinen Grund für eine Hashmap. Der Zellindex ist zwei Divisionen und zwei `floor`. Gegner außerhalb des Gitters werden in die Randzelle geklemmt. +**Nachtrag vom 2026-08-11 (#16): je Zelle ein `Vec<(Entity, Vec2)>`, nicht `Vec`.** Die +Position wird neben der `Entity` mitgeführt. Der Grund ist die weiche Separation aus §5: sie schreibt +`Transform` und liest dabei die `Transform`s ihrer Nachbarn. Mit bloßen `Entity`s müsste sie dieselbe +Gegner-Query gleichzeitig lesend und schreibend halten — in Bevy nicht ausleihbar, und die Auswege +(`ParamSet`, ein zweiter Puffer-Durchlauf) kosten mehr als das eine `Vec2` je Bucket-Eintrag. Die +Entscheidung ändert sich dadurch nicht; die Ablage tut es. + Das Grid ist **kein** `Component` an der Turm-Entity: Es beschreibt nicht den Turm, sondern die Welt, und es überlebt keinen Run-Wechsel als Zustand — es wird ohnehin jeden Frame neu gebaut. Damit bleibt die Ablageregel aus ADR-0002 §6 unberührt. @@ -118,6 +125,38 @@ schiebt eine `Entity` in einen `Vec`. Die 10 ns sind eine großzügige Obergrenze, weil sie den teureren Nachbarn als Maßstab nimmt. +#### Nachtrag vom 2026-08-11 (nachgemessen in #16): der Maßstab war falsch, die Schranke hält + +Die Rechnung oben ist beim Bau des Grids nachgemessen worden — Release-Profil, `HARD_ENEMY_CAP` +Gegner, Median über 20 Läufe je System (`the_rebuild_stays_cheaper_than_move_enemies_at_the_hard_cap` +in `src/spatial.rs`): + +| System | gemessen bei 10 000 Gegnern | +|---|---| +| `move_enemies` | **0,021 ms** | +| `rebuild_spatial_grid` | **0,057 ms** | + +**Der Rebuild ist rund 2,7× teurer als `move_enemies`, nicht billiger.** Der Absatz oben zählt +Arithmetik — zwei Divisionen und zwei `floor` gegen eine Normalisierung mit Wurzel — und übersieht +die Kosten, die hier dominieren: `move_enemies` ist ein **linearer** Lauf über eine Tabelle, der +Rebuild schreibt in ~7 000 verstreute `Vec`s und zahlt Cache-Misses, die keine FLOP-Zählung sieht. +Der `Vec`-Push, den der Absatz als „Rest der Ersparnis" abtut, **ist** der Kostenpunkt. + +Was das ändert und was nicht: + +- **Die Entscheidung bleibt.** Sie stand nie auf dem Vergleich, sondern auf der absoluten Schranke: + 0,057 ms sind **0,34 % des Frame-Budgets** und liegen unter den hier veranschlagten ≤0,10 ms. Die + Logik gesamt bleibt bei ≤0,18 ms. +- **Das inkrementelle Update wird dadurch nicht attraktiver.** Es würde denselben verstreuten + Schreibzugriff bezahlen, sobald ein Gegner die Zelle wechselt — und bei radialer Konvergenz + wechseln ständig welche —, plus Rückindex und Entfernen aus der Mitte eines `Vec`. Der Preis, der + hier wehtut, ist genau der, den es nicht einspart. +- **Der Maßstab „billiger als `move_enemies`" ist gestrichen.** Er war eine Bequemlichkeit, kein + Argument. Revisions-Auslöser 1 unten ist entsprechend auf die absolute Schranke umgestellt. + +Zu messen ist erneut nach dem Wechsel auf echte Texturen und mit dem HUD — dieselbe Auflage, unter +der die Zahlen aus #4 stehen. + **Was ein inkrementelles Update dagegen einspart, ist nach oben durch dieselben ≤0,10 ms begrenzt — und es spart nicht einmal die, denn den Zellindex muss es genauso je Gegner ausrechnen, um einen Zellwechsel überhaupt zu erkennen.** Der Rest der Ersparnis ist der `Vec`-Push. Dagegen steht: ein @@ -280,10 +319,13 @@ und die Auslöse-Bedingungen, wo sie sichtbarer wäre. Diese Entscheidung wird neu getroffen, sobald **eines** der folgenden Ereignisse eintritt: -1. **Der Rebuild ist nicht mehr der billigere Durchlauf.** Eine Messung mit dem Aufbau aus - `examples/perf_spike.rs` bei `HARD_ENEMY_CAP` zeigt `rebuild_spatial_grid` teurer als - `move_enemies` im selben Frame. Die Entscheidung gegen das inkrementelle Update steht und fällt - mit dieser Annahme; ist sie widerlegt, wird sie neu getroffen — nicht vorher. +1. **Der Rebuild sprengt seine absolute Schranke.** Eine Messung bei `HARD_ENEMY_CAP` zeigt + `rebuild_spatial_grid` über **0,10 ms**. Die Entscheidung gegen das inkrementelle Update steht und + fällt mit dieser Schranke; ist sie gerissen, wird sie neu getroffen — nicht vorher. + *Fassung bis 2026-08-11: „teurer als `move_enemies` im selben Frame". Dieser Maßstab ist in #16 + widerlegt worden (Nachtrag zu §3) — der Rebuild war von Anfang an teurer, ohne dass die + Entscheidung dadurch fällig wäre. Er ist durch die Schranke ersetzt, auf der das Argument + tatsächlich steht.* 2. **Radien-Spreizung.** Ein Konsument braucht einen Abfrageradius kleiner als eine halbe Zelle oder größer als ‹20› Zellkanten. Dann trägt **eine** Auflösung nicht mehr für alle Abfragen, und zwei Auflösungen oder eine hierarchische Struktur sind zu prüfen. diff --git a/docs/spec-vertical-slice.md b/docs/spec-vertical-slice.md index 549eba2..36f60e1 100644 --- a/docs/spec-vertical-slice.md +++ b/docs/spec-vertical-slice.md @@ -141,9 +141,11 @@ Auslöse-System und die Projektil-Kollision bedient. Sprung-zu-Welle auf die höchste im Debug-Panel erreichbare Welle. - Bei 10 000 gleichzeitigen Gegnern bleibt die Frametime unter 16,67 ms (Nachmessung mit dem Aufbau aus `examples/perf_spike.rs`, nach dem Wechsel auf echte Sprites zu wiederholen). -- Der Rebuild des `SpatialGrid` kostet bei 10 000 Gegnern **nicht mehr als `move_enemies` im selben - Frame** (ADR-0004 §3 — die Entscheidung gegen ein inkrementelles Update steht auf dieser Annahme; - reißt sie, ist die ADR fällig, nicht die Zahl). +- Der Rebuild des `SpatialGrid` kostet bei 10 000 Gegnern **höchstens 0,10 ms** (ADR-0004 §3 — die + Entscheidung gegen ein inkrementelles Update steht auf dieser Schranke; reißt sie, ist die ADR + fällig, nicht die Zahl). *Bis 2026-08-11 stand hier „nicht mehr als `move_enemies` im selben + Frame"; die Messung in #16 hat diesen Maßstab widerlegt — gemessen 0,057 ms gegen 0,021 ms —, ohne + die Schranke zu reißen. Siehe den Nachtrag zu ADR-0004 §3.* - Kein Gegner bewegt sich in einem Frame vom Turm **weg**, solange ihn keine Fähigkeit dorthin drückt — die Separationsklemmung macht das konstruktiv unmöglich, und der Turmring pulsiert sichtbar nicht.