fix(world): geteilte Requisiten-Geometrie beim Chunk-Abbau verschonen

Ruling 62: PropLayer-Geometrien (Palme, Laternenteile, Stammkollider) sind
Singletons über alle Chunks; #unload gab bei jedem Nachbarabbau ihre
GPU-Puffer frei. mesh.userData.sharedGeometry markiert sie, #unload
überspringt sie jetzt beim Entsorgen. Ruling 63: geschlossene Ringe
(Laufzeitform) waren in keiner Testvorrichtung gepinnt, obwohl der Code sie
bereits korrekt behandelte. Ruling 64: der Zweig `if (this.props)` bekommt
direkte Testabdeckung statt nur der manuellen Probe aus Fix-Runde 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-13 01:58:06 +02:00
1 parent d6a2f6d269
commit 02d0f36580
4 files changed
+152 -25

No files matched your search

+10
View File
@@ -168,10 +168,17 @@ export class PropLayer {
const trunk = new Mesh(this.trunkGeometry);
trunk.visible = false;
trunk.position.set(p.x, 3, p.z);
// sharedGeometry: this.trunkGeometry ist wie this.palmGeometry ein einziges,
// von PropLayer.load() gehaltenes Objekt für alle Chunks (Ruling 62).
trunk.userData.sharedGeometry = true;
colliders.push(trunk);
});
mesh.instanceMatrix.needsUpdate = true;
mesh.userData.excludeFromCollider = true;
// sharedGeometry: this.palmGeometry entsteht einmal in load() und wird von jedem
// Chunk instanziert — #unload() in streaming.js darf es nicht mit entsorgen,
// solange andere Chunks noch darauf zeigen (Ruling 62, Fix-Runde 2).
mesh.userData.sharedGeometry = true;
visual.add(mesh);
}
this.#buildLamps(chunk, visual);
@@ -197,6 +204,9 @@ export class PropLayer {
const mesh = new InstancedMesh(part.geometry, part.material, points.length);
mesh.castShadow = true;
mesh.userData.excludeFromCollider = true;
// sharedGeometry: part.geometry gehört this.lampParts, einmalig aus load()
// (Ruling 62, Fix-Runde 2 — dieselbe Begründung wie bei den Palmen oben).
mesh.userData.sharedGeometry = true;
points.forEach((p, i) => {
dummy.position.set(p.x, 0, p.z);
dummy.rotation.set(0, p.angle, 0);
+7 -1
View File
@@ -115,7 +115,13 @@ export class ChunkStreamer {
// Ruling 51: collider-lose Chunks (offenes Wasser) haben kein handle abzumelden.
if (entry.handle) this.player.removeCollider(entry.handle);
this.scene.remove(entry.group);
entry.group.traverse((o) => o.geometry?.dispose());
// Ruling 62: this.palmGeometry, das Palmenmaterial und die Laternenteile entstehen
// einmal in PropLayer.load() und werden von jedem Chunk benutzt (props.visual landet
// hier in derselben Gruppe). Ohne die Wache würde jeder Chunk-Abbau ihre GPU-Puffer
// freigeben, während Nachbarchunks sie noch anzeigen — kein Absturz (dispose() lässt
// die CPU-Daten stehen, der nächste Zugriff lädt still neu hoch), aber unnötige Arbeit
// bei jedem Abbau. mesh.userData.sharedGeometry markiert das, analog excludeFromCollider.
entry.group.traverse((o) => { if (o.geometry && !o.userData.sharedGeometry) o.geometry.dispose(); });
this.active.delete(k);
}