Merge pull request #1977 from ludovic111/perf/collision-scan-edges

🤖 Agent: requeued after a runner-setup parity failure (subtitle: missing sounds.json on the runner); 30/31 harnesses passed, the 56 selected passed locally. Queued by perf-next-2.
This commit is contained in:
Ludovic Marie
2026-10-26 04:33:56 +01:00
committed by GitHub
4 changed files with 201 additions and 28 deletions
+18
View File
@@ -92,6 +92,13 @@ impl<W: BlockCollisionWorld> BlockCollisionWorld for EntityCollisions<'_, W> {
) -> std::result::Result<bool, Self::Error> {
self.blocks.is_suffocating(chunk, block, p)
}
fn may_reach_out(
&mut self,
chunk: Self::Chunk,
section_y: i32,
) -> std::result::Result<bool, Self::Error> {
self.blocks.may_reach_out(chunk, section_y)
}
fn context_shape(
&mut self,
block: CollisionBlock,
@@ -262,6 +269,17 @@ impl<'w> BlockCollisionWorld for SceneCollisions<'w, '_> {
fn collision_block(&mut self, chunk: Self::Chunk, p: BlockPos) -> io::Result<CollisionBlock> {
self.binding.collision_block(chunk.block(p.x, p.y, p.z))
}
/// From the section's palette, without reading its blocks; a palette
/// of the whole registry reads every cell.
fn may_reach_out(&mut self, chunk: Self::Chunk, section_y: i32) -> io::Result<bool> {
let blocks = usize::try_from(section_y - (crate::chunk::MIN_Y >> 4))
.ok()
.and_then(|s| chunk.sections.get(s))
.map(|section| &section.blocks);
Ok(blocks.is_none_or(|blocks| {
blocks.is_global() || blocks.maybe_has(|v| self.binding.reaches_out(BlockStateId(v)))
}))
}
fn is_suffocating(
&mut self,
_: Self::Chunk,
@@ -158,6 +158,13 @@ impl<'r> BlockCollisionRegistry<'r> {
empty_shape: flags & 4 != 0,
})
}
/// Whether a scan keeps `state` in the cells around its box: a large
/// collision shape or a moving piston. States outside the registry count.
pub fn reaches_out(&self, state: BlockStateId) -> bool {
self.scan_flags
.get(state.0 as usize)
.is_none_or(|flags| flags & 3 != 0)
}
pub fn collision_shape<W: DynamicCollisionWorld>(
&self,
world: &mut W,
@@ -41,6 +41,13 @@ pub trait BlockCollisionWorld {
position: BlockPos,
context: &CollisionContext<'_>,
) -> Result<VoxelShape, Self::Error>;
/// Whether a block of the chunk's section `section_y` (block y >> 4) may
/// have a large collision shape or be a moving piston: the only blocks
/// the scan keeps outside the box's own cells. False lets the scan pass
/// that section's outer cells without reading them; true reads them all.
fn may_reach_out(&mut self, _chunk: Self::Chunk, _section_y: i32) -> Result<bool, Self::Error> {
Ok(true)
}
}
#[derive(Debug)]
@@ -88,6 +95,8 @@ pub struct BlockCollisionScan<'w, 'c, W: BlockCollisionWorld> {
count: usize,
budget: usize,
chunk: Option<(ChunkPos, W::Chunk)>,
/// `may_reach_out` of the last section an outer cell was in.
reach: Option<(ChunkPos, i32, bool)>,
failed: bool,
}
impl<'w, 'c, W: BlockCollisionWorld> BlockCollisionScan<'w, 'c, W> {
@@ -137,16 +146,36 @@ impl<'w, 'c, W: BlockCollisionWorld> BlockCollisionScan<'w, 'c, W> {
count,
budget: candidate_budget,
chunk: None,
reach: None,
failed: false,
})
}
/// Move the cursor `n` cells further along its row; `n` must not pass
/// the row's end.
fn skip(&mut self, n: usize) {
if n == 0 {
return;
}
let [width, height, _] = self.sizes;
self.index += n;
self.cursor[0] += n;
if self.cursor[0] == width {
self.cursor[0] = 0;
self.cursor[1] += 1;
if self.cursor[1] == height {
self.cursor[1] = 0;
self.cursor[2] += 1;
}
}
}
fn find_next(&mut self) -> Result<Option<BlockCollisionHit>, CollisionScanError<W::Error>> {
while self.index < self.count {
if self.index >= self.budget {
return Err(CollisionScanError::VisitBudget);
}
let [width, height, _] = self.sizes;
let [width, height, depth] = self.sizes;
let relative = self.cursor;
self.index += 1;
self.cursor[0] += 1;
@@ -184,6 +213,41 @@ impl<'w, 'c, W: BlockCollisionWorld> BlockCollisionScan<'w, 'c, W> {
let Some(chunk) = chunk else {
continue;
};
// An outer cell only counts for a block reaching out of its own
// cell: most sections hold none, so their outer cells go unread
// (the result is the same, with a fifth of the block reads for a
// mob's box).
if kind != 0 {
let section_y = p.y >> 4;
let reaches = match self.reach {
Some((at, y, reaches)) if at == chunk_position && y == section_y => reaches,
_ => {
let reaches = self
.world
.may_reach_out(chunk, section_y)
.map_err(CollisionScanError::World)?;
self.reach = Some((chunk_position, section_y, reaches));
reaches
}
};
if !reaches {
// On a row along the box's bottom, top or sides every
// cell is outer: the rest of it in this chunk is passed
// at once (never beyond the budget, which still ends the
// scan where it would have).
if relative[1] == 0
|| relative[1] == height - 1
|| relative[2] == 0
|| relative[2] == depth - 1
{
let rest = (width - 1 - relative[0])
.min(15 - (p.x & 15) as usize)
.min(self.budget.saturating_sub(self.index));
self.skip(rest);
}
continue;
}
}
let block = self
.world
.collision_block(chunk, p)
@@ -112,6 +112,8 @@ fn invalid_or_excessive_domains_fail_before_world_queries() {
/// and has an empty shape.
struct Recording {
cells: Vec<BlockPos>,
/// Sections (block y >> 4) whose blocks never reach out of their cell.
contained: Vec<i32>,
}
impl BlockCollisionWorld for Recording {
type Error = io::Error;
@@ -139,6 +141,40 @@ impl BlockCollisionWorld for Recording {
) -> io::Result<VoxelShape> {
unreachable!()
}
fn may_reach_out(&mut self, _: (), section_y: i32) -> io::Result<bool> {
Ok(!self.contained.contains(&section_y))
}
}
/// The cells the scan reads for `bounds` (Cursor3D order: x fastest, then
/// y, then z, without the 8 corners), less the outer cells of `contained`.
fn expected_cells(bounds: Aabb, contained: &[i32]) -> Vec<BlockPos> {
let (min, max) = (bounds.min_position(), bounds.max_position());
let start = BlockPos::containing(Vec3::new(min.x - 1.0e-7, min.y - 1.0e-7, min.z - 1.0e-7))
.offset(-1, -1, -1);
let end = BlockPos::containing(Vec3::new(max.x + 1.0e-7, max.y + 1.0e-7, max.z + 1.0e-7))
.offset(1, 1, 1);
let sizes = [
end.x - start.x + 1,
end.y - start.y + 1,
end.z - start.z + 1,
];
let mut expected = Vec::new();
for i in 0..sizes[0] * sizes[1] * sizes[2] {
let r = [
i % sizes[0],
i / sizes[0] % sizes[1],
i / sizes[0] / sizes[1],
];
let edges = (0..3)
.filter(|&a| r[a] == 0 || r[a] == sizes[a] - 1)
.count();
let p = start.offset(r[0], r[1], r[2]);
if edges < 3 && (edges == 0 || !contained.contains(&(p.y >> 4))) {
expected.push(p);
}
}
expected
}
#[test]
@@ -150,7 +186,10 @@ fn cells_follow_the_cursor_order_without_corners() {
Aabb::new(Vec3::new(-0.3, 0.0, -0.3), Vec3::new(0.3, 0.9, 0.3)),
];
for bounds in boxes {
let mut world = Recording { cells: Vec::new() };
let mut world = Recording {
cells: Vec::new(),
contained: Vec::new(),
};
let hits = BlockCollisionScan::new(
&mut world,
CollisionContext::empty(false),
@@ -161,31 +200,76 @@ fn cells_follow_the_cursor_order_without_corners() {
.unwrap()
.count();
assert_eq!(hits, 0);
// Cursor3D: x fastest, then y, then z, skipping the 8 corners.
let (min, max) = (bounds.min_position(), bounds.max_position());
let start = BlockPos::containing(Vec3::new(min.x - 1.0e-7, min.y - 1.0e-7, min.z - 1.0e-7))
.offset(-1, -1, -1);
let end = BlockPos::containing(Vec3::new(max.x + 1.0e-7, max.y + 1.0e-7, max.z + 1.0e-7))
.offset(1, 1, 1);
let sizes = [
end.x - start.x + 1,
end.y - start.y + 1,
end.z - start.z + 1,
];
let mut expected = Vec::new();
for i in 0..sizes[0] * sizes[1] * sizes[2] {
let r = [
i % sizes[0],
i / sizes[0] % sizes[1],
i / sizes[0] / sizes[1],
];
let edges = (0..3)
.filter(|&a| r[a] == 0 || r[a] == sizes[a] - 1)
.count();
if edges < 3 {
expected.push(start.offset(r[0], r[1], r[2]));
}
}
assert_eq!(world.cells, expected);
assert_eq!(world.cells, expected_cells(bounds, &[]));
}
}
/// Outer cells are read only in sections that may hold a block reaching
/// out of its cell; the box's own cells always are.
#[test]
fn outer_cells_of_contained_sections_are_not_read() {
// A mob crossing from section 3 into section 4, and an item in section 0.
let boxes = [
(
Aabb::new(Vec3::new(0.2, 63.5, 0.3), Vec3::new(0.8, 65.3, 0.9)),
vec![3],
),
(
Aabb::new(Vec3::new(0.2, 63.5, 0.3), Vec3::new(0.8, 65.3, 0.9)),
vec![3, 4],
),
(
Aabb::new(Vec3::new(4.4, 2.0, -7.6), Vec3::new(4.65, 2.25, -7.35)),
vec![0],
),
// Across a chunk's edge: the rest of a row is passed only up to it.
(
Aabb::new(Vec3::new(14.2, 70.5, -0.4), Vec3::new(17.8, 71.5, 0.4)),
vec![4],
),
];
for (bounds, contained) in boxes {
let mut world = Recording {
cells: Vec::new(),
contained: contained.clone(),
};
let hits = BlockCollisionScan::new(
&mut world,
CollisionContext::empty(false),
bounds,
false,
1_000_000,
)
.unwrap()
.count();
assert_eq!(hits, 0);
assert_eq!(world.cells, expected_cells(bounds, &contained));
assert!(world.cells.len() < expected_cells(bounds, &[]).len());
}
}
/// Passing outer cells at once still stops at the candidate budget, as
/// visiting them one by one does.
#[test]
fn passed_cells_count_toward_the_budget() {
let bounds = Aabb::new(Vec3::new(0.2, 63.5, 0.3), Vec3::new(0.8, 65.3, 0.9));
for budget in 0..80 {
let scan = |contained: Vec<i32>| {
let mut world = Recording {
cells: Vec::new(),
contained,
};
BlockCollisionScan::new(
&mut world,
CollisionContext::empty(false),
bounds,
false,
budget,
)
.unwrap()
.map(|hit| hit.map(|_| ()).map_err(|e| e.to_string()))
.collect::<Vec<_>>()
};
assert_eq!(scan(vec![3, 4]), scan(Vec::new()), "budget {budget}");
}
}