This commit is contained in:
33333-33333 2026-06-27 19:22:38 +09:00
commit 129c07183a
51 changed files with 5711 additions and 1122 deletions

View file

@ -23,6 +23,13 @@
bedConflictRuns: 0,
bedConflictSkips: 0,
};
this.spatialDirtyReasonCountsThisFrame = {};
this.spatialRebuildReasonCountsThisFrame = {};
this.deferredSpatialReadsThisFrame = 0;
this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} };
this.nearbyQueryStatsThisFrame = { obstacleRectHits: 0, obstacleRectMisses: 0, obstacleRectBypass: 0, obstacleRectBuilt: 0 };
this.constraintDirtyStatsThisFrame = { marked: 0, coalesced: 0 };
this._solidObstacleRectQueryCache = new Map();
},
spendScheduledWork(kind = "ai", urgent = false) {
@ -77,6 +84,9 @@
rebuildsTotal: this.spatialRebuildsTotal || 0,
partialRebuildsTotal: this.spatialPartialRebuildsTotal || 0,
lastRebuildReason: this.lastSpatialRebuildReason || "",
dirtyReasons: this.spatialDirtyReasonCountsThisFrame || {},
rebuildReasons: this.spatialRebuildReasonCountsThisFrame || {},
deferredReads: this.deferredSpatialReadsThisFrame || 0,
},
render: {
drawList: (this.drawList || []).length,
@ -105,6 +115,10 @@
stats: this.workStats || null,
},
scheduler: this._itemUpdateSchedulerStats || null,
creatures: this._creatureUpdateStats || null,
terrain: this.terrainDirtyStatsThisFrame || null,
nearby: this.nearbyQueryStatsThisFrame || null,
constraintDirty: this.constraintDirtyStatsThisFrame || null,
perf: window.TarinaiPerf?.snapshot?.() || null,
};
},
@ -146,10 +160,32 @@
ensureSpatial(reason = "read") {
if (!this.spatial) return false;
if (!this.spatialDirty && (this.spatialVersion || 0) > 0) return false;
// Rebuild at most once per frame. Queries after movement may use the previous
// frame's grid until the next update-start; this is acceptable for this game
// and avoids repeated full-grid rebuilds under dense scenes.
if ((this.spatialRebuildsThisFrame || 0) > 0 && (this.spatialVersion || 0) > 0) return false;
// During the creature pass, Tarinai movement marks only the Tarinai bucket
// dirty many times. Obstacle/static item cells are still valid, so spatial
// reads can use the frame-start Tarinai grid and collapse those invalidations
// into one post-creature rebuild. Do not defer static/dynamic/ant dirtiness:
// those buckets are used by collision and contact queries and need fresh data.
const inDeferredPhase = (this.deferSpatialRebuildDepth || 0) > 0 && (this.spatialVersion || 0) > 0;
const canDeferTarinaiOnly = inDeferredPhase
&& this.spatialTarinaiDirty
&& !this.spatialStaticItemsDirty
&& !this.spatialDynamicItemsDirty
&& !this.spatialAntDirty;
const canDeferMobileOnly = inDeferredPhase
&& String(this.deferSpatialRebuildMode || "") === "mobile"
&& !this.spatialStaticItemsDirty
&& (this.spatialDynamicItemsDirty || this.spatialTarinaiDirty || this.spatialAntDirty);
if (canDeferTarinaiOnly || canDeferMobileOnly) {
this.deferredSpatialDirtyReason = reason;
this.deferredSpatialReadsThisFrame = (this.deferredSpatialReadsThisFrame || 0) + 1;
return false;
}
// Collision/contact queries must not read a previous-grid position after
// moving static/dynamic obstacles in the same frame. Partial rebuilds are
// already split by static/dynamic/tarinai/ant buckets, so keep those reads
// correct here and let dirty flags decide how much work is required.
return this.rebuildSpatial(true, reason);
},
@ -231,6 +267,8 @@
this.spatialRebuildsThisFrame = (this.spatialRebuildsThisFrame || 0) + 1;
this.spatialRebuildsTotal = (this.spatialRebuildsTotal || 0) + 1;
this.lastSpatialRebuildReason = reason;
if (!this.spatialRebuildReasonCountsThisFrame) this.spatialRebuildReasonCountsThisFrame = {};
this.spatialRebuildReasonCountsThisFrame[String(reason || "manual")] = (this.spatialRebuildReasonCountsThisFrame[String(reason || "manual")] || 0) + 1;
return true;
},