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:
@@ -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| §ion.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(§ion_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}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user