fix(hot-path): 回退 query_circle 复用缓冲优化——存在重入覆写 bug

上个提交 35c1742 让 query_circle 复用成员 _query_result 缓冲,实测有重入缺陷:
GDScript 的 PackedArray 返回值是活引用别名(非写时复制副本),当调用方遍历返回
的 hits 期间触发嵌套 query_circle(命中→SpellEvaluator.execute_sub 的区域法术),
嵌套调用的 clear()+append 会就地覆写外层正在遍历的同一缓冲,导致碰撞遍历读到错误
实体 ID。编辑器内已复现:外层期望 [10,20,30],重入后实得 [10,88]。

原优化仅省约 0.064ms/帧(帧预算 ~0.4%),不值得为其做跨 4 处调用点的 out 参数
+ 各自持久缓冲的重入安全改造,故回退为每次新建数组。
BulletManager 无冷数据命中快路径无重入问题,保留。
并加注释说明此处禁止复用缓冲的原因。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-23 16:35:31 +08:00
co-authored by Claude Opus 4.8
parent 7038505880
commit 2a7658e0e3
+7 -9
View File
@@ -14,12 +14,6 @@ var _cs_node: Node = null # SpatialGridCs C# 子节点(就绪后挂载)
var _grid: Array = [] # Array[Array[int]] var _grid: Array = [] # Array[Array[int]]
var _entity_cell: Dictionary = {} # { entity_id → cell_idx } var _entity_cell: Dictionary = {} # { entity_id → cell_idx }
## query_circle 复用结果缓冲(避免每次调用新建 PackedInt32Array
## 由 clear()+append 就地填充后返回;调用方按值(CoW)读取。
## 若调用方在遍历返回值时触发嵌套 query_circle(如命中→SpellEvaluator 区域查询),
## 下一次 clear() 因引用计数>1 触发写时复制,旧结果不被破坏,语义与新建数组等价。
var _query_result: PackedInt32Array = PackedInt32Array()
func _ready() -> void: func _ready() -> void:
_grid.resize(GRID_COLS * GRID_ROWS) _grid.resize(GRID_COLS * GRID_ROWS)
for i in _grid.size(): for i in _grid.size():
@@ -45,7 +39,11 @@ func insert(entity_id: int, pos: Vector2) -> void:
func query_circle(center: Vector2, radius: float) -> PackedInt32Array: func query_circle(center: Vector2, radius: float) -> PackedInt32Array:
if _cs_node and _cs_node.has_method("QueryCircle"): if _cs_node and _cs_node.has_method("QueryCircle"):
return _cs_node.call("QueryCircle", center, radius) return _cs_node.call("QueryCircle", center, radius)
_query_result.clear() # 注意:必须返回新建数组,不可复用成员缓冲。
# 调用方(BulletManager._check_collision)在遍历返回值期间可能触发嵌套 query_circle
# (命中→SpellEvaluator.execute_sub 的区域法术),GDScript 的 PackedArray 返回值是活引用别名,
# 复用成员缓冲会被嵌套调用就地覆写、破坏外层遍历(已实测复现)。
var result := PackedInt32Array()
var min_cx: int = clamp(int((center.x - radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_COLS - 1) var min_cx: int = clamp(int((center.x - radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_COLS - 1)
var max_cx: int = clamp(int((center.x + radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_COLS - 1) var max_cx: int = clamp(int((center.x + radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_COLS - 1)
var min_cy: int = clamp(int((center.y - radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_ROWS - 1) var min_cy: int = clamp(int((center.y - radius + GRID_OFFSET) / CELL_SIZE), 0, GRID_ROWS - 1)
@@ -53,8 +51,8 @@ func query_circle(center: Vector2, radius: float) -> PackedInt32Array:
for cy in range(min_cy, max_cy + 1): for cy in range(min_cy, max_cy + 1):
for cx in range(min_cx, max_cx + 1): for cx in range(min_cx, max_cx + 1):
for eid in _grid[cx + cy * GRID_COLS]: for eid in _grid[cx + cy * GRID_COLS]:
_query_result.append(eid) result.append(eid)
return _query_result return result
## 每帧由 EnemyManager._physics_process 驱动(dirty-list 方案) ## 每帧由 EnemyManager._physics_process 驱动(dirty-list 方案)
func rebuild() -> void: func rebuild() -> void: