Nach #348 rechnet eine gemalte Zelle Zellindex, Tile-Liste und Kollision nur noch in ihrer Umgebung neu. Was jetzt noch mit der Map-Größe wächst, ist das Mesh: Im CPU-Profil beim Malen auf 120x120 (Kopie von Garden of Sacrifices) ist TilemapField.sync mit rund 10 % des Main-Threads der größte Posten, der noch an der Map hängt.
Ohne Malen liegt die Szene bei 55–57 fps. Die Lücke auf 120x120 ist also noch deutlich.
Was heute passiert
Eine gemalte Zelle gibt der Ebene eine neue layer.tiles-Liste (seit #348 stückweise geflickt, die einzelnen TileDraw-Einträge bleiben dieselben Objekte).
groupByTile (src/viewport/tilemapMesh.ts) hängt am Listen-Objekt. Eine neue Liste wird komplett neu gruppiert, jede Gruppe ist ein neues Array.
TilemapField.meshFor (src/viewport/tilemap.ts) vergleicht tiles per Identität. Damit wird jedes Mesh der Ebene neu gebaut (tileMesh + neue MeshGeometry), nicht nur das des gemalten Tiles.
Bei einer Ebene mit Y-Sortierung (layerRows) gilt das je Zeile. Ob unberührte Zeilen ihre Liste behalten, ist zu prüfen.
Idee
Gruppen stückweise:groupByTile übernimmt bei einer geflickten Liste die Gruppen der Vorgänger-Liste und baut nur die Gruppen der Tiles neu, deren Zellen sich geändert haben (die geänderten Schlüssel gibt es seit #348 über cellsChangedSince). Unberührte Gruppen bleiben dasselbe Array, ihr Mesh wird nicht angefasst. Das allein sollte den Großteil bringen.
Geometrie stückweise (nur falls 1 nicht reicht): Im Mesh des gemalten Tiles nur die Quads der geänderten Zellen schreiben, statt positions/uvs/indices neu zu erzeugen. Das heißt: feste Slots je Zelle oder Lücken als degenerierte Quads, Puffer mit Reserve, buffer.update() statt neuer MeshGeometry. Das ist der eigentliche Umbau an der Pixi-Geometrie.
Abnahme
Pinselstrich auf 120x120 kommt nahe an die fps von 60x60 heran (gleiche Sonde wie #347/#348).
TilemapField.builds steigt pro gemalter Zelle um höchstens die Zahl der betroffenen Tiles, nicht um alle Tiles der Ebene.
Vergleichstest: stückweise gebaute Meshes ergeben dieselben Vertices wie ein frischer Bau (wie tilemap-patch.test.ts bei #348).
e2e 39 + 40 grün.
Bezug
#347, #348 (Vorarbeit), #284 (Mesh nur bei Änderung neu bauen)
## Warum
Nach #348 rechnet eine gemalte Zelle Zellindex, Tile-Liste und Kollision nur noch in ihrer Umgebung neu. Was jetzt noch mit der Map-Größe wächst, ist das **Mesh**: Im CPU-Profil beim Malen auf 120x120 (Kopie von *Garden of Sacrifices*) ist `TilemapField.sync` mit rund **10 % des Main-Threads** der größte Posten, der noch an der Map hängt.
Gemessen bei #348 (Pinselstrich, stein2):
| Map | vorher | nach #348 |
|---|---|---|
| 60x60 | 42,3 fps | 48,8 fps |
| 120x120 | 27,9 fps, p95 92 ms | 41,9 fps, p95 52 ms |
Ohne Malen liegt die Szene bei 55–57 fps. Die Lücke auf 120x120 ist also noch deutlich.
## Was heute passiert
- Eine gemalte Zelle gibt der Ebene eine neue `layer.tiles`-Liste (seit #348 stückweise geflickt, die einzelnen `TileDraw`-Einträge bleiben dieselben Objekte).
- `groupByTile` (`src/viewport/tilemapMesh.ts`) hängt am Listen-Objekt. Eine neue Liste wird komplett neu gruppiert, **jede** Gruppe ist ein neues Array.
- `TilemapField.meshFor` (`src/viewport/tilemap.ts`) vergleicht `tiles` per Identität. Damit wird **jedes Mesh der Ebene** neu gebaut (`tileMesh` + neue `MeshGeometry`), nicht nur das des gemalten Tiles.
- Bei einer Ebene mit Y-Sortierung (`layerRows`) gilt das je Zeile. Ob unberührte Zeilen ihre Liste behalten, ist zu prüfen.
## Idee
1. **Gruppen stückweise:** `groupByTile` übernimmt bei einer geflickten Liste die Gruppen der Vorgänger-Liste und baut nur die Gruppen der Tiles neu, deren Zellen sich geändert haben (die geänderten Schlüssel gibt es seit #348 über `cellsChangedSince`). Unberührte Gruppen bleiben dasselbe Array, ihr Mesh wird nicht angefasst. Das allein sollte den Großteil bringen.
2. **Geometrie stückweise (nur falls 1 nicht reicht):** Im Mesh des gemalten Tiles nur die Quads der geänderten Zellen schreiben, statt `positions`/`uvs`/`indices` neu zu erzeugen. Das heißt: feste Slots je Zelle oder Lücken als degenerierte Quads, Puffer mit Reserve, `buffer.update()` statt neuer `MeshGeometry`. Das ist der eigentliche Umbau an der Pixi-Geometrie.
## Abnahme
- Pinselstrich auf 120x120 kommt nahe an die fps von 60x60 heran (gleiche Sonde wie #347/#348).
- `TilemapField.builds` steigt pro gemalter Zelle um höchstens die Zahl der betroffenen Tiles, nicht um alle Tiles der Ebene.
- Vergleichstest: stückweise gebaute Meshes ergeben dieselben Vertices wie ein frischer Bau (wie `tilemap-patch.test.ts` bei #348).
- e2e 39 + 40 grün.
## Bezug
- #347, #348 (Vorarbeit), #284 (Mesh nur bei Änderung neu bauen)
Erledigt mit df6dbe9 (Wiki e33da89, Tilemaps). Stufe 1 hat gereicht, Stufe 2 braucht es nicht.
Umsetzung
Eine geflickte Tile-Liste merkt sich ihre Herkunft (tilesPatch): aus welcher Liste sie entstanden ist und welche Einträge weg- bzw. dazugekommen sind. Eine Nachbarzelle, die gleich aussieht, behält ihren Eintrag. Ändert sich am Bild nichts, kommt dieselbe Liste zurück.
groupByTile und layerRows flicken die Gruppen der vorigen Liste (patchedGroups). Eine unberührte Gruppe bleibt dasselbe Array, meshFor baut ihr Mesh also nicht neu. Bei Ebenen, die nach Y sortiert sind, gilt das für jede Zeile und die Gruppen darin.
Ein Tile, das auf der Ebene noch nicht vorkommt, rechnet die Ebene weiter ganz neu (wie seit #348).
Gemessen
Gleiche Sonde wie bei #348 (Pinselstrich auf einer Kopie von Garden of Sacrifices, stein2):
60x60 vorher
60x60 jetzt
120x120 vorher
120x120 jetzt
Neu gebaute Meshes pro Frame
21,9
0,94
40,7
1,22
fps beim Malen
49,4
51,1
42,0
45,8
p95 eines Frames
44,7 ms
40,2 ms
53,3 ms
44,9 ms
Malen kostet auf beiden Größen jetzt gleich viel, etwa 10 fps unter derselben Ansicht ohne Malen (60,9 bzw. 55,3 fps). Vorher waren es auf 120x120 13,4 fps.
Der Rest-Abstand zwischen den Größen ist das Zeichnen der größeren Map, er ist auch ohne Malen da. Das Abnahmekriterium „120x120 nahe an 60x60“ ist damit beim Malen erfüllt, absolut liegen 120x120 noch 5 fps darunter.
Im CPU-Profil auf 120x120 taucht die Tilemap-Geometrie nicht mehr auf. Oben steht jetzt die Auflösung der Szene (expand/resolveObjects, collectHolderItems).
Tests
Gate auf melektaus-atx grün: 312 Dateien, 4832 Tests.
Der Zufallsweg aus #348 vergleicht nach jedem der 400 Schritte auch Gruppen und Zeilen (samt Sort-Offsets) mit einer frischen Gruppierung und zählt, dass Gruppen wirklich übernommen werden. Dazu ein fester Test: Eine gemalte Zelle lässt die Gruppe eines anderen Tiles und alle anderen Zeilen unverändert.
Gegenproben, alle drei rot: entfernte Einträge bleiben in der Gruppe, Zeilen nach der ersten Zelle statt nach der Zeilennummer sortiert, groupByTile übergeht den Patch.
e2e 39 + 40 auf melektaus-atx: ALL PASSED, 107 Checks.
Den CI-Lauf zu df6dbe9 habe ich nicht abgewartet.
Erledigt mit `df6dbe9` (Wiki `e33da89`, *Tilemaps*). Stufe 1 hat gereicht, Stufe 2 braucht es nicht.
## Umsetzung
- **Eine geflickte Tile-Liste merkt sich ihre Herkunft** (`tilesPatch`): aus welcher Liste sie entstanden ist und welche Einträge weg- bzw. dazugekommen sind. Eine Nachbarzelle, die gleich aussieht, behält ihren Eintrag. Ändert sich am Bild nichts, kommt dieselbe Liste zurück.
- **`groupByTile` und `layerRows` flicken die Gruppen der vorigen Liste** (`patchedGroups`). Eine unberührte Gruppe bleibt dasselbe Array, `meshFor` baut ihr Mesh also nicht neu. Bei Ebenen, die nach Y sortiert sind, gilt das für jede Zeile und die Gruppen darin.
- Ein Tile, das auf der Ebene noch nicht vorkommt, rechnet die Ebene weiter ganz neu (wie seit #348).
## Gemessen
Gleiche Sonde wie bei #348 (Pinselstrich auf einer Kopie von *Garden of Sacrifices*, stein2):
| | 60x60 vorher | 60x60 jetzt | 120x120 vorher | 120x120 jetzt |
|---|---|---|---|---|
| Neu gebaute Meshes pro Frame | 21,9 | 0,94 | 40,7 | 1,22 |
| fps beim Malen | 49,4 | 51,1 | 42,0 | 45,8 |
| p95 eines Frames | 44,7 ms | 40,2 ms | 53,3 ms | 44,9 ms |
- Malen kostet auf beiden Größen jetzt gleich viel, etwa 10 fps unter derselben Ansicht ohne Malen (60,9 bzw. 55,3 fps). Vorher waren es auf 120x120 13,4 fps.
- Der Rest-Abstand zwischen den Größen ist das Zeichnen der größeren Map, er ist auch ohne Malen da. Das Abnahmekriterium „120x120 nahe an 60x60“ ist damit beim Malen erfüllt, absolut liegen 120x120 noch 5 fps darunter.
- Im CPU-Profil auf 120x120 taucht die Tilemap-Geometrie nicht mehr auf. Oben steht jetzt die Auflösung der Szene (`expand`/`resolveObjects`, `collectHolderItems`).
## Tests
- Gate auf `melektaus-atx` grün: 312 Dateien, 4832 Tests.
- Der Zufallsweg aus #348 vergleicht nach jedem der 400 Schritte auch Gruppen und Zeilen (samt Sort-Offsets) mit einer frischen Gruppierung und zählt, dass Gruppen wirklich übernommen werden. Dazu ein fester Test: Eine gemalte Zelle lässt die Gruppe eines anderen Tiles und alle anderen Zeilen unverändert.
- Gegenproben, alle drei rot: entfernte Einträge bleiben in der Gruppe, Zeilen nach der ersten Zelle statt nach der Zeilennummer sortiert, `groupByTile` übergeht den Patch.
- e2e 39 + 40 auf `melektaus-atx`: ALL PASSED, 107 Checks.
- Den CI-Lauf zu `df6dbe9` habe ich nicht abgewartet.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Warum
Nach #348 rechnet eine gemalte Zelle Zellindex, Tile-Liste und Kollision nur noch in ihrer Umgebung neu. Was jetzt noch mit der Map-Größe wächst, ist das Mesh: Im CPU-Profil beim Malen auf 120x120 (Kopie von Garden of Sacrifices) ist
TilemapField.syncmit rund 10 % des Main-Threads der größte Posten, der noch an der Map hängt.Gemessen bei #348 (Pinselstrich, stein2):
Ohne Malen liegt die Szene bei 55–57 fps. Die Lücke auf 120x120 ist also noch deutlich.
Was heute passiert
layer.tiles-Liste (seit #348 stückweise geflickt, die einzelnenTileDraw-Einträge bleiben dieselben Objekte).groupByTile(src/viewport/tilemapMesh.ts) hängt am Listen-Objekt. Eine neue Liste wird komplett neu gruppiert, jede Gruppe ist ein neues Array.TilemapField.meshFor(src/viewport/tilemap.ts) vergleichttilesper Identität. Damit wird jedes Mesh der Ebene neu gebaut (tileMesh+ neueMeshGeometry), nicht nur das des gemalten Tiles.layerRows) gilt das je Zeile. Ob unberührte Zeilen ihre Liste behalten, ist zu prüfen.Idee
groupByTileübernimmt bei einer geflickten Liste die Gruppen der Vorgänger-Liste und baut nur die Gruppen der Tiles neu, deren Zellen sich geändert haben (die geänderten Schlüssel gibt es seit #348 übercellsChangedSince). Unberührte Gruppen bleiben dasselbe Array, ihr Mesh wird nicht angefasst. Das allein sollte den Großteil bringen.positions/uvs/indicesneu zu erzeugen. Das heißt: feste Slots je Zelle oder Lücken als degenerierte Quads, Puffer mit Reserve,buffer.update()statt neuerMeshGeometry. Das ist der eigentliche Umbau an der Pixi-Geometrie.Abnahme
TilemapField.buildssteigt pro gemalter Zelle um höchstens die Zahl der betroffenen Tiles, nicht um alle Tiles der Ebene.tilemap-patch.test.tsbei #348).Bezug
Erledigt mit
df6dbe9(Wikie33da89, Tilemaps). Stufe 1 hat gereicht, Stufe 2 braucht es nicht.Umsetzung
tilesPatch): aus welcher Liste sie entstanden ist und welche Einträge weg- bzw. dazugekommen sind. Eine Nachbarzelle, die gleich aussieht, behält ihren Eintrag. Ändert sich am Bild nichts, kommt dieselbe Liste zurück.groupByTileundlayerRowsflicken die Gruppen der vorigen Liste (patchedGroups). Eine unberührte Gruppe bleibt dasselbe Array,meshForbaut ihr Mesh also nicht neu. Bei Ebenen, die nach Y sortiert sind, gilt das für jede Zeile und die Gruppen darin.Gemessen
Gleiche Sonde wie bei #348 (Pinselstrich auf einer Kopie von Garden of Sacrifices, stein2):
expand/resolveObjects,collectHolderItems).Tests
melektaus-atxgrün: 312 Dateien, 4832 Tests.groupByTileübergeht den Patch.melektaus-atx: ALL PASSED, 107 Checks.df6dbe9habe ich nicht abgewartet.