Merge pull request #507 from ludovic111/perf/collision-scan-empty

Collision scans: skip blocks with no shape, build the entity shape lazily (−6 % tick time)
This commit is contained in:
Ludovic Marie
2026-10-16 20:01:11 +02:00
committed by GitHub
4 changed files with 48 additions and 16 deletions
+12 -11
View File
@@ -209,21 +209,22 @@ where
)
.unwrap_or(false)
}
impl BlockCollisionWorld for SceneCollisions<'_, '_> {
impl<'w> BlockCollisionWorld for SceneCollisions<'w, '_> {
type Error = io::Error;
type Chunk = ChunkPos;
fn collision_chunk(&mut self, p: ChunkPos) -> io::Result<Option<ChunkPos>> {
Ok(self.world.chunks.contains_key(&(p.x, p.z)).then_some(p))
/// The resident column, looked up once per chunk the scan enters.
type Chunk = &'w crate::chunk::Chunk;
fn collision_chunk(&mut self, p: ChunkPos) -> io::Result<Option<Self::Chunk>> {
Ok(self.world.chunks.get(&(p.x, p.z)).map(|chunk| &**chunk))
}
fn collision_block(&mut self, chunk: ChunkPos, p: BlockPos) -> io::Result<CollisionBlock> {
let chunk = self
.world
.chunks
.get(&(chunk.x, chunk.z))
.ok_or_else(|| io::Error::other("resident collision chunk disappeared"))?;
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))
}
fn is_suffocating(&mut self, _: ChunkPos, _: CollisionBlock, _: BlockPos) -> io::Result<bool> {
fn is_suffocating(
&mut self,
_: Self::Chunk,
_: CollisionBlock,
_: BlockPos,
) -> io::Result<bool> {
Ok(false)
}
fn context_shape(
@@ -192,6 +192,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
state: BlockStateId(i as u32),
large_collision_shape: v[1].as_bool().unwrap(),
moving_piston: v[2].as_bool().unwrap(),
empty_shape: false,
},
shape: shape(v[0].as_u64().unwrap()),
suffocating: v[3].as_bool().unwrap(),
+9 -2
View File
@@ -98,8 +98,6 @@ impl<'r> BlockCollisionRegistry<'r> {
.into_iter()
.any(|axis| shape.min(axis) < 0.0 || shape.max(axis) > 1.0)
});
scan_flags
.push(u8::from(large) | (u8::from(state.name == "minecraft:moving_piston") << 1));
if let Some(rule) = ScaffoldingCollision::from_state(state)? {
contextual.insert(id, ContextualRule::Scaffolding(rule));
} else if let Some(rule) = LiquidCollision::from_state(state)? {
@@ -107,6 +105,14 @@ impl<'r> BlockCollisionRegistry<'r> {
} else if state.name == "minecraft:powder_snow" {
contextual.insert(id, ContextualRule::PowderSnow);
}
// No contextual rule and an empty cached shape: empty everywhere.
let empty = !contextual.contains_key(&id)
&& cached.cached_shape(id)?.is_some_and(VoxelShape::is_empty);
scan_flags.push(
u8::from(large)
| (u8::from(state.name == "minecraft:moving_piston") << 1)
| (u8::from(empty) << 2),
);
if cached.cached_shape(id)?.is_none() {
if let Some(rule) =
DynamicSupport::from_state(cached.registry().state(id).unwrap())?
@@ -138,6 +144,7 @@ impl<'r> BlockCollisionRegistry<'r> {
state,
large_collision_shape: flags & 1 != 0,
moving_piston: flags & 2 != 0,
empty_shape: flags & 4 != 0,
})
}
pub fn collision_shape<W: DynamicCollisionWorld>(
@@ -11,6 +11,10 @@ pub struct CollisionBlock {
pub state: BlockStateId,
pub large_collision_shape: bool,
pub moving_piston: bool,
/// The shape is empty under every context (air, plants): a scan skips
/// the block without asking for its shape. Providers whose shapes come
/// from elsewhere leave it false.
pub empty_shape: bool,
}
pub trait BlockCollisionWorld {
@@ -72,7 +76,9 @@ pub struct BlockCollisionScan<'w, 'c, W: BlockCollisionWorld> {
world: &'w mut W,
context: CollisionContext<'c>,
bounds: Aabb,
entity_shape: VoxelShape,
/// `Shapes.create(box)`, built the first time a block's shape is not
/// the unit cube (finite bounds always make one).
entity_shape: Option<VoxelShape>,
only_suffocating: bool,
start: BlockPos,
sizes: [usize; 3],
@@ -120,7 +126,7 @@ impl<'w, 'c, W: BlockCollisionWorld> BlockCollisionScan<'w, 'c, W> {
world,
context,
bounds,
entity_shape: VoxelShape::from_box(bounds).map_err(CollisionScanError::Shape)?,
entity_shape: None,
only_suffocating,
start,
sizes: sizes.map(|s| s as usize),
@@ -185,6 +191,9 @@ impl<'w, 'c, W: BlockCollisionWorld> BlockCollisionScan<'w, 'c, W> {
if kind == 1 && !block.large_collision_shape || kind == 2 && !block.moving_piston {
continue;
}
if block.empty_shape {
continue;
}
let shape = self
.world
.context_shape(block, p, &self.context)
@@ -201,10 +210,24 @@ impl<'w, 'c, W: BlockCollisionWorld> BlockCollisionScan<'w, 'c, W> {
}
shape.moved(offset)
} else {
// Shapes whose bounds miss the box share no volume with it:
// joinIsNotEmpty(AND) is false without moving them.
if !shape
.bounds()
.is_some_and(|b| b.move_by(offset).intersects(self.bounds))
{
continue;
}
let moved = shape.moved(offset);
let entity_shape = match &self.entity_shape {
Some(shape) => shape,
None => self.entity_shape.insert(
VoxelShape::from_box(self.bounds).map_err(CollisionScanError::Shape)?,
),
};
if moved.is_empty()
|| !moved
.join_is_not_empty(&self.entity_shape, ShapeOp::AND)
.join_is_not_empty(entity_shape, ShapeOp::AND)
.map_err(CollisionScanError::Shape)?
{
continue;