docs: correct the rebuild-cost yardstick in ADR-0004 and the spec

Measured while building #16: at 10 000 enemies the grid rebuild costs
0.057 ms against 0.021 ms for move_enemies — 2.7x more, not less. ADR-0004
§3 counted arithmetic and missed what dominates: move_enemies is a linear
table walk, the rebuild writes into ~7000 scattered buckets.

The decision does not change. It never rested on the comparison but on the
absolute bound of 0.10 ms, and 0.057 ms is 0.34 % of the frame budget; an
incremental update would pay the same scattered write on every cell
crossing. Revision trigger 1 and the acceptance criterion of spec feature 3
are restated on that bound, which is what the runnable check now asserts.

Also records the second deviation the build produced: a cell holds
(Entity, Vec2) rather than Entity, because soft separation writes Transform
while reading its neighbours' positions.
This commit is contained in:
2026-08-11 09:03:43 +02:00
parent 0f93a939c0
commit f1b85862da
2 changed files with 51 additions and 7 deletions

View File

@@ -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 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. 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<Entity>`.** 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 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. 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. 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. 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 — **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 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 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: 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 1. **Der Rebuild sprengt seine absolute Schranke.** Eine Messung bei `HARD_ENEMY_CAP` zeigt
`examples/perf_spike.rs` bei `HARD_ENEMY_CAP` zeigt `rebuild_spatial_grid` teurer als `rebuild_spatial_grid` über **0,10 ms**. Die Entscheidung gegen das inkrementelle Update steht und
`move_enemies` im selben Frame. Die Entscheidung gegen das inkrementelle Update steht und fällt fällt mit dieser Schranke; ist sie gerissen, wird sie neu getroffen — nicht vorher.
mit dieser Annahme; ist sie widerlegt, 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 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 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. Auflösungen oder eine hierarchische Struktur sind zu prüfen.

View File

@@ -141,9 +141,11 @@ Auslöse-System und die Projektil-Kollision bedient.
Sprung-zu-Welle auf die höchste im Debug-Panel erreichbare Welle. 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 - 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). 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 - Der Rebuild des `SpatialGrid` kostet bei 10 000 Gegnern **höchstens 0,10 ms** (ADR-0004 §3 — die
Frame** (ADR-0004 §3 — die Entscheidung gegen ein inkrementelles Update steht auf dieser Annahme; Entscheidung gegen ein inkrementelles Update steht auf dieser Schranke; reißt sie, ist die ADR
reißt sie, ist die ADR fällig, nicht die Zahl). 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 - 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 drückt — die Separationsklemmung macht das konstruktiv unmöglich, und der Turmring pulsiert
sichtbar nicht. sichtbar nicht.