mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
perf(entity-culling): reduce occlusion query overhead
Skip doomed async work while the camera moves, reduce synchronous voxel-plane lookups, and cull dynamic block entities before generating their geometry. Reuse chest state and remove dye-prefix sorting from the block-entity hot path. Co-Authored-By: OpenAI Codex <[email protected]>
This commit is contained in:
1 parent
3ec390a6b8
commit
d87992bdf3
10 files changed
+483
-110
No files matched your search
@@ -271,10 +271,10 @@ pub const DYES: [(&str, u32); 16] = [
|
||||
("black", 0x1D1D21),
|
||||
];
|
||||
fn dye_prefix(name: &str) -> Option<(usize, &str)> {
|
||||
// Longest names first so "light_gray" wins over "gray".
|
||||
let mut order: Vec<usize> = (0..16).collect();
|
||||
order.sort_by_key(|&i| std::cmp::Reverse(DYES[i].0.len()));
|
||||
order.into_iter().find_map(|i| {
|
||||
// Longest names first; this is the same stable order as sorting DYES by
|
||||
// descending prefix length, without a Vec allocation or comparison sort.
|
||||
const ORDER: [usize; 16] = [3, 8, 2, 1, 4, 10, 0, 12, 13, 15, 5, 6, 7, 9, 11, 14];
|
||||
ORDER.into_iter().find_map(|i| {
|
||||
name.strip_prefix(DYES[i].0)
|
||||
.and_then(|rest| rest.strip_prefix('_'))
|
||||
.map(|rest| (i, rest))
|
||||
@@ -833,6 +833,9 @@ pub struct Scene<'a> {
|
||||
/// Changes whenever `glyph` may answer differently (`Font::epoch`): sign
|
||||
/// text laid out under another value is laid out again.
|
||||
pub glyphs: u64,
|
||||
/// Per-block visibility for dynamic, animated, or uncached models. Bounds
|
||||
/// are world-space and conservatively include model animation extents.
|
||||
pub dynamic_visible: Option<&'a dyn Fn([i32; 3], u32, DVec3, DVec3) -> bool>,
|
||||
}
|
||||
|
||||
/// Empty by default (a dedicated server): no state has a block entity model.
|
||||
@@ -1402,6 +1405,167 @@ impl BlockEntityRenderer {
|
||||
fn kind(&self, state: u32) -> &Kind {
|
||||
self.kinds.get(state as usize).unwrap_or(&Kind::None)
|
||||
}
|
||||
fn dynamic_bounds(
|
||||
&self,
|
||||
scene: &Scene,
|
||||
pos: [i32; 3],
|
||||
kind: &Kind,
|
||||
chest_openness: Option<(f32, Option<[i32; 3]>)>,
|
||||
) -> (DVec3, DVec3) {
|
||||
let [x, y, z] = pos.map(f64::from);
|
||||
let mut lo = DVec3::new(x, y, z);
|
||||
let mut hi = DVec3::new(x + 1.0, y + 1.0, z + 1.0);
|
||||
match kind {
|
||||
Kind::Chest { double_with, .. } => {
|
||||
if let Some(offset) = double_with {
|
||||
lo.x += f64::from(offset[0].min(0));
|
||||
lo.z += f64::from(offset[2].min(0));
|
||||
hi.x += f64::from(offset[0].max(0));
|
||||
hi.z += f64::from(offset[2].max(0));
|
||||
}
|
||||
let is_open = chest_openness.map_or_else(
|
||||
|| {
|
||||
(scene.lid)(pos) != 0.0
|
||||
|| double_with.is_some_and(|offset| {
|
||||
let other = [
|
||||
pos[0].saturating_add(offset[0]),
|
||||
pos[1].saturating_add(offset[1]),
|
||||
pos[2].saturating_add(offset[2]),
|
||||
];
|
||||
(scene.lid)(other) != 0.0
|
||||
})
|
||||
},
|
||||
|(open, _)| open != 0.0,
|
||||
);
|
||||
if is_open {
|
||||
// The lid pivots up from the rear edge. Its longest corner
|
||||
// rises by less than one block in the fully open pose.
|
||||
hi.y += 0.8;
|
||||
}
|
||||
}
|
||||
Kind::Shulker { facing, .. } => {
|
||||
let offset = step(facing);
|
||||
lo.x += f64::from(offset[0].min(0)) * 0.52;
|
||||
lo.y += f64::from(offset[1].min(0)) * 0.52;
|
||||
lo.z += f64::from(offset[2].min(0)) * 0.52;
|
||||
hi.x += f64::from(offset[0].max(0)) * 0.52;
|
||||
hi.y += f64::from(offset[1].max(0)) * 0.52;
|
||||
hi.z += f64::from(offset[2].max(0)) * 0.52;
|
||||
}
|
||||
Kind::Banner { wall: false, .. } => {
|
||||
hi.y += 1.0;
|
||||
lo.x -= 0.05;
|
||||
lo.z -= 0.05;
|
||||
hi.x += 0.05;
|
||||
hi.z += 0.05;
|
||||
}
|
||||
Kind::Banner { wall: true, .. } => {
|
||||
lo.y -= 1.0;
|
||||
lo.x -= 0.05;
|
||||
lo.z -= 0.05;
|
||||
hi.x += 0.05;
|
||||
hi.z += 0.05;
|
||||
}
|
||||
Kind::Sign { hanging: true, .. } => {
|
||||
lo.y -= 1.0;
|
||||
lo.x -= 0.15;
|
||||
lo.z -= 0.15;
|
||||
hi.x += 0.15;
|
||||
hi.z += 0.15;
|
||||
}
|
||||
Kind::Sign { hanging: false, .. } => {
|
||||
hi.y += 0.3;
|
||||
lo.x -= 0.02;
|
||||
lo.z -= 0.02;
|
||||
hi.x += 0.02;
|
||||
hi.z += 0.02;
|
||||
}
|
||||
Kind::EnchantingTable | Kind::Lectern { .. } => {
|
||||
// The book and statue models rise above the block's collision
|
||||
// cube; include their full rendered height and rotated corners.
|
||||
hi.y += 1.0;
|
||||
lo.x -= 0.25;
|
||||
lo.z -= 0.25;
|
||||
hi.x += 0.25;
|
||||
hi.z += 0.25;
|
||||
}
|
||||
Kind::CopperGolem { .. } => {
|
||||
// The statue pose rotates around the root, so some variants
|
||||
// extend below the block as well as above it.
|
||||
lo.y -= 1.0;
|
||||
hi.y += 1.0;
|
||||
lo.x -= 0.25;
|
||||
lo.z -= 0.25;
|
||||
hi.x += 0.25;
|
||||
hi.z += 0.25;
|
||||
}
|
||||
Kind::Conduit => {
|
||||
// Its animated cage is centered above the conduit block and
|
||||
// the wind model spans multiple cells. The caller bypasses
|
||||
// voxel occlusion for envelopes this tall, but retains frustum
|
||||
// culling with this conservative volume.
|
||||
lo.x -= 1.0;
|
||||
lo.y -= 1.0;
|
||||
lo.z -= 1.0;
|
||||
hi.x += 1.0;
|
||||
hi.y += 1.0;
|
||||
hi.z += 1.0;
|
||||
}
|
||||
Kind::Bell => {
|
||||
// The bell's pivoted swing moves its corners slightly outside
|
||||
// the block even though the model starts centered in it.
|
||||
lo.x -= 0.1;
|
||||
lo.y -= 0.1;
|
||||
lo.z -= 0.1;
|
||||
hi.x += 0.1;
|
||||
hi.y += 0.1;
|
||||
hi.z += 0.1;
|
||||
}
|
||||
Kind::Skull { .. } => {
|
||||
// Wall-mounted and dragon heads can protrude from their block.
|
||||
lo.x -= 0.25;
|
||||
lo.y -= 0.25;
|
||||
lo.z -= 0.25;
|
||||
hi.x += 0.25;
|
||||
hi.y += 0.25;
|
||||
hi.z += 0.25;
|
||||
}
|
||||
Kind::EndGateway { .. } => {
|
||||
let anim = (scene.anim)(pos);
|
||||
let beam = gateway_beam(anim.gateway, scene.partial, scene.max_y, scene.game_time);
|
||||
let height = f64::from(beam.height.max(0));
|
||||
lo.y -= height;
|
||||
hi.y += height;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
fn is_dynamic_visible(
|
||||
&self,
|
||||
scene: &Scene,
|
||||
shapes: &Arc<BlockEntityShapes>,
|
||||
pos: [i32; 3],
|
||||
state: u32,
|
||||
kind: &Kind,
|
||||
chest_openness: Option<(f32, Option<[i32; 3]>)>,
|
||||
) -> bool {
|
||||
let Some(visible) = scene.dynamic_visible else {
|
||||
return true;
|
||||
};
|
||||
if matches!(kind, Kind::EndGateway { .. }) {
|
||||
return true;
|
||||
}
|
||||
// A pack model may extend outside the vanilla envelope. Keep that
|
||||
// geometry on the uncullable path unless its model bounds are known.
|
||||
if !Arc::ptr_eq(shapes, &self.vanilla) {
|
||||
return true;
|
||||
}
|
||||
let (lo, hi) = self.dynamic_bounds(scene, pos, kind, chest_openness);
|
||||
visible(pos, state, lo, hi)
|
||||
}
|
||||
|
||||
fn cached_entry_is_current(
|
||||
&self,
|
||||
scene: &Scene,
|
||||
@@ -1411,6 +1575,7 @@ impl BlockEntityRenderer {
|
||||
options: CacheOptions,
|
||||
entry: &StaticEntry,
|
||||
section_revision: StaticSectionRevision,
|
||||
chest_openness: Option<(f32, Option<[i32; 3]>)>,
|
||||
) -> bool {
|
||||
if entry.key.state != state
|
||||
|| entry.key.nether != scene.nether
|
||||
@@ -1422,15 +1587,24 @@ impl BlockEntityRenderer {
|
||||
}
|
||||
match kind {
|
||||
Kind::Chest { .. } => {
|
||||
let (open, other) = self.chest_openness(
|
||||
pos,
|
||||
kind,
|
||||
&|p| scene.world.block(p[0], p[1], p[2]).0,
|
||||
scene.lid,
|
||||
);
|
||||
// Double chests depend on the neighboring half's light too;
|
||||
// let those uncommon models take the full key path.
|
||||
open == 0.0 && other.is_none()
|
||||
let (open, other) = chest_openness.unwrap_or_else(|| {
|
||||
self.chest_openness(
|
||||
pos,
|
||||
kind,
|
||||
&|p| scene.world.block(p[0], p[1], p[2]).0,
|
||||
scene.lid,
|
||||
)
|
||||
});
|
||||
if open != 0.0 {
|
||||
return false;
|
||||
}
|
||||
let mut light = scene.world.light(pos[0], pos[1], pos[2]);
|
||||
if let Some(other) = other {
|
||||
let other_light = scene.world.light(other[0], other[1], other[2]);
|
||||
light = (light >> 4).max(other_light >> 4) << 4
|
||||
| (light & 15).max(other_light & 15);
|
||||
}
|
||||
entry.key.light == light
|
||||
}
|
||||
Kind::Shulker { .. } => {
|
||||
let (progress, old) = (scene.anim)(pos).shulker;
|
||||
@@ -1693,6 +1867,17 @@ impl BlockEntityRenderer {
|
||||
}
|
||||
let local = static_local_index(pos, section_min_y);
|
||||
let current_section_revision = active_section_revision.unwrap_or_default();
|
||||
let chest_openness = (cache.is_some()
|
||||
&& options.chests
|
||||
&& matches!(kind, Kind::Chest { .. }))
|
||||
.then(|| {
|
||||
self.chest_openness(
|
||||
pos,
|
||||
kind,
|
||||
&|p| scene.world.block(p[0], p[1], p[2]).0,
|
||||
scene.lid,
|
||||
)
|
||||
});
|
||||
if cache_only && section_revision.is_some() && active_section_revision.is_none() {
|
||||
break 'draw;
|
||||
}
|
||||
@@ -1708,6 +1893,7 @@ impl BlockEntityRenderer {
|
||||
options,
|
||||
&active_entries[index].1,
|
||||
current_section_revision,
|
||||
chest_openness,
|
||||
)
|
||||
{
|
||||
let entry = &mut active_entries[index].1;
|
||||
@@ -1727,7 +1913,9 @@ impl BlockEntityRenderer {
|
||||
..*vertex
|
||||
}));
|
||||
}
|
||||
if matches!(kind, Kind::Banner { .. }) {
|
||||
if matches!(kind, Kind::Banner { .. })
|
||||
&& self.is_dynamic_visible(scene, &shapes, pos, state, kind, None)
|
||||
{
|
||||
let packed = scene.world.light(pos[0], pos[1], pos[2]);
|
||||
let origin =
|
||||
(DVec3::new(f64::from(pos[0]), f64::from(pos[1]), f64::from(pos[2]))
|
||||
@@ -1748,11 +1936,25 @@ impl BlockEntityRenderer {
|
||||
}
|
||||
break 'draw;
|
||||
}
|
||||
if cache_only && chest_openness.is_some_and(|(open, _)| open != 0.0) {
|
||||
if let Some(cache) = &mut cache
|
||||
&& let Ok(index) =
|
||||
active_entries.binary_search_by_key(&local, |(index, _)| *index)
|
||||
{
|
||||
active_entries.remove(index);
|
||||
cache.len -= 1;
|
||||
cache.mark_section_dirty(section);
|
||||
}
|
||||
break 'draw;
|
||||
}
|
||||
let origin = (DVec3::new(f64::from(pos[0]), f64::from(pos[1]), f64::from(pos[2]))
|
||||
- scene.camera)
|
||||
.as_vec3();
|
||||
let mut packed = scene.world.light(pos[0], pos[1], pos[2]);
|
||||
if matches!(kind, Kind::Sign { .. }) && scene.texts.is_none() {
|
||||
if !self.is_dynamic_visible(scene, &shapes, pos, state, kind, None) {
|
||||
break 'draw;
|
||||
}
|
||||
if options.signs {
|
||||
self.sign_text(scene, pos, state, origin, packed, out);
|
||||
} else {
|
||||
@@ -1771,8 +1973,15 @@ impl BlockEntityRenderer {
|
||||
break 'draw;
|
||||
}
|
||||
if let Some(cache) = &mut cache
|
||||
&& let Some(key) =
|
||||
self.static_key(scene, pos, state, kind, options, &mut packed)
|
||||
&& let Some(key) = self.static_key(
|
||||
scene,
|
||||
pos,
|
||||
state,
|
||||
kind,
|
||||
options,
|
||||
&mut packed,
|
||||
chest_openness,
|
||||
)
|
||||
{
|
||||
let slot = active_entries.binary_search_by_key(&local, |(index, _)| *index);
|
||||
let entry = match slot {
|
||||
@@ -1857,7 +2066,9 @@ impl BlockEntityRenderer {
|
||||
..*vertex
|
||||
}));
|
||||
}
|
||||
if matches!(kind, Kind::Banner { .. }) {
|
||||
if matches!(kind, Kind::Banner { .. })
|
||||
&& self.is_dynamic_visible(scene, &shapes, pos, state, kind, None)
|
||||
{
|
||||
self.render_banner(
|
||||
&shapes,
|
||||
scene,
|
||||
@@ -1884,6 +2095,9 @@ impl BlockEntityRenderer {
|
||||
if cache_only {
|
||||
break 'draw;
|
||||
}
|
||||
if !self.is_dynamic_visible(scene, &shapes, pos, state, kind, chest_openness) {
|
||||
break 'draw;
|
||||
}
|
||||
let base = Mat4::from_translation(origin);
|
||||
self.render_one(scene, pos, kind, base, packed, lights, false, true, out);
|
||||
}
|
||||
@@ -1950,6 +2164,7 @@ impl BlockEntityRenderer {
|
||||
kind: &Kind,
|
||||
options: CacheOptions,
|
||||
packed: &mut u8,
|
||||
chest_openness: Option<(f32, Option<[i32; 3]>)>,
|
||||
) -> Option<StaticKey> {
|
||||
let mut sprite_rects = [None; 5];
|
||||
let mut sprite_count = 0;
|
||||
@@ -1967,12 +2182,14 @@ impl BlockEntityRenderer {
|
||||
part,
|
||||
..
|
||||
} if options.chests => {
|
||||
let (open, other) = self.chest_openness(
|
||||
pos,
|
||||
kind,
|
||||
&|p| scene.world.block(p[0], p[1], p[2]).0,
|
||||
scene.lid,
|
||||
);
|
||||
let (open, other) = chest_openness.unwrap_or_else(|| {
|
||||
self.chest_openness(
|
||||
pos,
|
||||
kind,
|
||||
&|p| scene.world.block(p[0], p[1], p[2]).0,
|
||||
scene.lid,
|
||||
)
|
||||
});
|
||||
if open != 0.0 {
|
||||
return None;
|
||||
}
|
||||
@@ -3354,6 +3571,7 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// Compare the cached world path with the original model renderer,
|
||||
// including atlas replacement, current light and animated fallback.
|
||||
@@ -3648,6 +3866,7 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let mut call = TextCall {
|
||||
matrix: Mat4::IDENTITY,
|
||||
@@ -3720,6 +3939,7 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let call = TextCall {
|
||||
matrix: Mat4::IDENTITY,
|
||||
@@ -3835,6 +4055,7 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let check = |scene: &Scene, pos: [i32; 3], state: u32| {
|
||||
let mut cached = Vec::new();
|
||||
@@ -3975,6 +4196,7 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let mut verts = Vec::new();
|
||||
renderer.render_block(&scene, [0; 3], state.0, Mat4::IDENTITY, 0xff, &mut verts);
|
||||
@@ -4060,6 +4282,7 @@ mod tests {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// A closed single chest of `block` drawn by `renderer`: positions and UVs.
|
||||
let draw = |renderer: &BlockEntityRenderer, block: &str, texture: &'static str| {
|
||||
|
||||
@@ -358,6 +358,7 @@ fn batched_callbacks_preserve_entity_ranges_and_allow_immediate_culling() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
// Reverse local-position order to check that cache indexing does not
|
||||
// change the order of emitted entities.
|
||||
@@ -446,6 +447,7 @@ fn static_cache_qualifies_local_positions_by_section_and_drops_old_entries() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
shulker_boxes: true,
|
||||
@@ -500,6 +502,7 @@ fn gpu_static_cache_uses_section_local_chests_and_keeps_visibility_separate() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([100, 70, -32], 0)];
|
||||
let other_block = [([116, 70, -32], 0)];
|
||||
@@ -686,6 +689,7 @@ fn sign_text_cache_can_be_disabled_and_reenabled_without_changing_output() {
|
||||
skull: &|_| None,
|
||||
glyph: &glyph,
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
let enabled = CacheOptions {
|
||||
@@ -739,6 +743,7 @@ fn shulker_cache_tracks_settled_poses_and_does_not_freeze_opening_animation() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
let options = CacheOptions {
|
||||
@@ -840,6 +845,7 @@ fn shulker_cache_covers_all_colors_and_facing_directions() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
shulker_boxes: true,
|
||||
@@ -894,6 +900,7 @@ fn cached_bells_keep_shaking_until_the_animation_ends() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
let options = CacheOptions {
|
||||
@@ -955,6 +962,7 @@ fn cached_closed_chests_match_the_direct_model_and_rebuild_when_opening() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
let options = CacheOptions {
|
||||
@@ -1050,6 +1058,7 @@ fn closed_chest_cache_covers_all_materials_and_atlas_reloads() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
let options = CacheOptions {
|
||||
@@ -1138,6 +1147,7 @@ fn double_chest_cache_bypasses_both_halves_while_either_lid_is_open() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pair = [([0, 64, 0], 0), ([1, 64, 0], 1)];
|
||||
let options = CacheOptions {
|
||||
@@ -1213,6 +1223,7 @@ fn decorated_pot_cache_tracks_sherds_and_bypasses_wobble_animation() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
let options = CacheOptions {
|
||||
@@ -1291,6 +1302,7 @@ fn banner_cache_keeps_the_pole_static_while_the_flag_and_patterns_update() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let blocks = [([0, 64, 0], 0)];
|
||||
let options = CacheOptions {
|
||||
@@ -1357,6 +1369,7 @@ fn cached_wall_banner_poles_use_the_wall_model() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let pos = [0, 64, 0];
|
||||
let options = CacheOptions {
|
||||
@@ -1404,6 +1417,7 @@ fn copper_statue_cache_rebuilds_when_its_pose_state_changes() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
copper_golem_statues: true,
|
||||
@@ -1509,6 +1523,7 @@ fn copper_statue_cache_covers_weathering_waxing_and_all_poses() {
|
||||
skull: &|_| None,
|
||||
glyph: &|_| Glyph::default(),
|
||||
glyphs: 0,
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let options = CacheOptions {
|
||||
copper_golem_statues: true,
|
||||
|
||||
@@ -373,6 +373,7 @@ pub fn main() -> Result<()> {
|
||||
skull: &|_| None,
|
||||
glyph: &|c| atlas.glyph_cell(c),
|
||||
glyphs: atlas.font.epoch(),
|
||||
dynamic_visible: None,
|
||||
};
|
||||
let mut verts = Vec::new();
|
||||
renderer.render_block(&scene, pos, state, Mat4::IDENTITY, packed, &mut verts);
|
||||
|
||||
@@ -377,6 +377,7 @@ use horizon_client::{
|
||||
first_person::{Hand, SwingAnimation},
|
||||
furnace::FurnaceCooking,
|
||||
gfx::{Frame, Renderer},
|
||||
graphics::EntityCullingMode,
|
||||
hud::{HudAtlas, HudBatch, rgba},
|
||||
hud_layout, hud_overlays,
|
||||
items::{ItemStack, Items},
|
||||
@@ -11198,32 +11199,6 @@ impl Game<'_> {
|
||||
laps.lap("eg_shadows");
|
||||
// Block entities (BlockEntityRenderers): chests, shulker boxes, banners, skulls...
|
||||
if let Some(r) = &self.renderer {
|
||||
let lid = |p: [i32; 3]| self.chest_lid(p, alpha as f32);
|
||||
let anim = |p: [i32; 3]| self.block_anim(p, alpha as f32);
|
||||
let scene = BlockEntityScene {
|
||||
camera,
|
||||
orientation: self.camera_orientation(),
|
||||
anim: &anim,
|
||||
texts: None,
|
||||
partial: alpha as f32,
|
||||
game_time: self.world.game_time,
|
||||
max_y: self.world.shape.max_y() - 1,
|
||||
world: &self.world,
|
||||
sprites: &self.gui.atlas.sprites,
|
||||
emissive: &self.gui.atlas.emissive,
|
||||
translucent_sprites: &self.gui.atlas.translucent,
|
||||
atlas_revision: self.gui.atlas.revision,
|
||||
nether: self.dim == Dim::Nether,
|
||||
lid: &lid,
|
||||
sign: &|p| match self.world.block_entities.get(&(p[0], p[1], p[2])) {
|
||||
Some(BlockEntity::Sign(sign)) => Some(sign),
|
||||
_ => None,
|
||||
},
|
||||
banner: &|p| self.banner_layers_at(p),
|
||||
skull: &|p| self.skull_skin_at(p),
|
||||
glyph: &|c| self.gui.atlas.glyph_cell(c),
|
||||
glyphs: self.gui.atlas.font.epoch(),
|
||||
};
|
||||
// Block entities outside the view frustum are skipped (with a
|
||||
// block of margin for models larger than their block).
|
||||
let view = self.view_frustum;
|
||||
@@ -11250,6 +11225,62 @@ impl Game<'_> {
|
||||
v.intersects(o - glam::Vec3::ONE, o + glam::Vec3::splat(2.0))
|
||||
})
|
||||
};
|
||||
let dynamic_visible = |_: [i32; 3], state: u32, lo: glam::DVec3, hi: glam::DVec3| {
|
||||
if off_screen(state) {
|
||||
return true;
|
||||
}
|
||||
if let Some(view) = view
|
||||
&& !view.intersects(
|
||||
(lo - camera - glam::DVec3::splat(0.5)).as_vec3(),
|
||||
(hi - camera + glam::DVec3::splat(0.5)).as_vec3(),
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let name = &*self.res.facts().get(BlockStateId(state)).name;
|
||||
if self.settings.horizon.entity_culling == EntityCullingMode::Off
|
||||
|| self.pack_shadow_casters.0
|
||||
|| name == "minecraft:end_portal"
|
||||
|| name == "minecraft:end_gateway"
|
||||
|| hi.y - lo.y > 2.0
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Culler::aabb_bounds adds 0.05 on each side. Keep its final
|
||||
// query just inside the conservative rendered-model envelope.
|
||||
let bounds = Aabb::new(
|
||||
Vec3::new(lo.x + 0.06, lo.y + 0.06, lo.z + 0.06),
|
||||
Vec3::new(hi.x - 0.06, hi.y - 0.06, hi.z - 0.06),
|
||||
);
|
||||
!self.block_entity_occluded(bounds)
|
||||
};
|
||||
let lid = |p: [i32; 3]| self.chest_lid(p, alpha as f32);
|
||||
let anim = |p: [i32; 3]| self.block_anim(p, alpha as f32);
|
||||
let scene = BlockEntityScene {
|
||||
camera,
|
||||
orientation: self.camera_orientation(),
|
||||
anim: &anim,
|
||||
texts: None,
|
||||
partial: alpha as f32,
|
||||
game_time: self.world.game_time,
|
||||
max_y: self.world.shape.max_y() - 1,
|
||||
world: &self.world,
|
||||
sprites: &self.gui.atlas.sprites,
|
||||
emissive: &self.gui.atlas.emissive,
|
||||
translucent_sprites: &self.gui.atlas.translucent,
|
||||
atlas_revision: self.gui.atlas.revision,
|
||||
nether: self.dim == Dim::Nether,
|
||||
lid: &lid,
|
||||
sign: &|p| match self.world.block_entities.get(&(p[0], p[1], p[2])) {
|
||||
Some(BlockEntity::Sign(sign)) => Some(sign),
|
||||
_ => None,
|
||||
},
|
||||
banner: &|p| self.banner_layers_at(p),
|
||||
skull: &|p| self.skull_skin_at(p),
|
||||
glyph: &|c| self.gui.atlas.glyph_cell(c),
|
||||
glyphs: self.gui.atlas.font.epoch(),
|
||||
dynamic_visible: Some(&dynamic_visible),
|
||||
};
|
||||
let visible = r
|
||||
.visible_block_entities_in_frustum(
|
||||
self.settings.chunk_fade,
|
||||
|
||||
@@ -6917,7 +6917,10 @@ impl Game<'_> {
|
||||
// The coarse mob bounds pay off only in dense async scenes; otherwise,
|
||||
// keep the existing exact mesh-culling path.
|
||||
if self.settings.horizon.entity_culling == EntityCullingMode::Asynchronous
|
||||
&& self.async_occlusion_culler.is_some()
|
||||
&& self
|
||||
.async_occlusion_culler
|
||||
.as_ref()
|
||||
.is_some_and(|culler| culler.camera_is_stationary())
|
||||
&& self.settings.horizon.entity_culling_precheck != 0
|
||||
&& self.mobs.len() >= self.settings.horizon.entity_culling_precheck as usize
|
||||
&& !self.pack_shadow_casters.0
|
||||
|
||||
@@ -68,6 +68,9 @@ impl Game<'_> {
|
||||
EntityCullingMode::Off => false,
|
||||
EntityCullingMode::Asynchronous => {
|
||||
if let Some(culler) = &self.async_occlusion_culler {
|
||||
if !culler.camera_is_stationary() {
|
||||
return false;
|
||||
}
|
||||
let Some(query) = culler.query_camera_bounds(lo, hi) else {
|
||||
return false;
|
||||
};
|
||||
@@ -90,23 +93,31 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let corners = [lo, hi].map(|pos| Vertex {
|
||||
pos: pos.to_array(),
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
});
|
||||
self.entity_hidden_sync(Some(&corners), 0, None)
|
||||
self.entity_hidden_sync_camera_bounds(lo, hi)
|
||||
}
|
||||
}
|
||||
EntityCullingMode::Synchronous => {
|
||||
let corners = [lo, hi].map(|pos| Vertex {
|
||||
pos: pos.to_array(),
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
});
|
||||
self.entity_hidden_sync(Some(&corners), 0, None)
|
||||
}
|
||||
EntityCullingMode::Synchronous => self.entity_hidden_sync_camera_bounds(lo, hi),
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_hidden_sync_camera_bounds(&self, lo: glam::Vec3, hi: glam::Vec3) -> bool {
|
||||
self.sync_occlusion_culler.as_ref().is_some_and(|culler| {
|
||||
let mut occluder = SyncOccluder::new(
|
||||
&self.world,
|
||||
self.res.facts(),
|
||||
self.renderer.as_ref(),
|
||||
&self.streaming,
|
||||
&self.pending_uploads,
|
||||
);
|
||||
culler.hidden_camera_bounds(lo.to_array(), hi.to_array(), |pos| occluder.ready(pos))
|
||||
})
|
||||
}
|
||||
|
||||
/// Pre-model visibility hook for dynamic block-entity geometry.
|
||||
pub(super) fn block_entity_occluded(&self, bounds: Aabb) -> bool {
|
||||
self.occlusion_hidden(None, 0, Some(bounds))
|
||||
}
|
||||
|
||||
/// Cull from the shared entity AABB before generating its model vertices.
|
||||
pub(super) fn entity_cull_decision<T: RenderEntityBounds>(
|
||||
&self,
|
||||
@@ -151,6 +162,9 @@ impl Game<'_> {
|
||||
EntityCullDecision::CheckVertices
|
||||
};
|
||||
};
|
||||
if !culler.camera_is_stationary() {
|
||||
return EntityCullDecision::CheckVertices;
|
||||
}
|
||||
let Some(query) = culler.query_entity_bounds(bounds) else {
|
||||
return EntityCullDecision::CheckVertices;
|
||||
};
|
||||
@@ -203,6 +217,9 @@ impl Game<'_> {
|
||||
EntityCullingMode::Synchronous => self.entity_hidden_sync(vertices, start, bounds),
|
||||
EntityCullingMode::Asynchronous => {
|
||||
if let Some(culler) = &self.async_occlusion_culler {
|
||||
if !culler.camera_is_stationary() {
|
||||
return false;
|
||||
}
|
||||
let Some(query) = (match bounds {
|
||||
Some(bounds) => culler.query_entity_bounds(bounds),
|
||||
None => vertices.and_then(|vertices| culler.query(vertices, start)),
|
||||
|
||||
@@ -2718,6 +2718,7 @@ impl Game<'_> {
|
||||
skull: &|_| None,
|
||||
glyph: &|c| self.gui.atlas.glyph_cell(c),
|
||||
glyphs: self.gui.atlas.font.epoch(),
|
||||
dynamic_visible: None,
|
||||
};
|
||||
self.res
|
||||
.block_entity_render()
|
||||
|
||||
@@ -745,6 +745,8 @@ pub(super) struct AsyncOcclusionCuller {
|
||||
query_cache_stamp: Option<SnapshotKey>,
|
||||
query_cache_camera_cell: Option<[i32; 3]>,
|
||||
camera: [f64; 3],
|
||||
previous_camera_key: Option<[u64; 3]>,
|
||||
camera_stationary: bool,
|
||||
stamp: SnapshotKey,
|
||||
pending: bool,
|
||||
frame: u64,
|
||||
@@ -856,6 +858,8 @@ impl AsyncOcclusionCuller {
|
||||
query_cache_stamp: None,
|
||||
query_cache_camera_cell: None,
|
||||
camera: [0.0; 3],
|
||||
previous_camera_key: None,
|
||||
camera_stationary: true,
|
||||
stamp: SnapshotKey {
|
||||
world_revision: 0,
|
||||
chunks_revision: 0,
|
||||
@@ -901,6 +905,10 @@ impl AsyncOcclusionCuller {
|
||||
stats.previous_chunks_revision = Some(self.stamp.chunks_revision);
|
||||
}
|
||||
let camera_key = self.camera.map(f64::to_bits);
|
||||
self.camera_stationary = self
|
||||
.previous_camera_key
|
||||
.is_none_or(|previous| previous == camera_key);
|
||||
self.previous_camera_key = Some(camera_key);
|
||||
let current_camera_cell = camera_cell(self.camera);
|
||||
let layout_changed = self
|
||||
.query_cache_stamp
|
||||
@@ -946,13 +954,7 @@ impl AsyncOcclusionCuller {
|
||||
let mut stats = self.stats.borrow_mut();
|
||||
stats.stale_camera_jobs = stats.stale_camera_jobs.saturating_add(1);
|
||||
}
|
||||
if results.camera == camera_key {
|
||||
self.cache_query_results(&mut results);
|
||||
} else {
|
||||
// A stale-camera visible answer is fail-open. Never carry a
|
||||
// hidden proof across viewpoints.
|
||||
self.cache_visible_results(&mut results);
|
||||
}
|
||||
self.cache_results_for_camera(&mut results, camera_key);
|
||||
let scratch = std::mem::take(&mut results.scratch_queries);
|
||||
if scratch.capacity() > self.queries.get_mut().capacity() {
|
||||
*self.queries.get_mut() = scratch;
|
||||
@@ -981,6 +983,11 @@ impl AsyncOcclusionCuller {
|
||||
self.queries.get_mut().clear();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn camera_is_stationary(&self) -> bool {
|
||||
self.camera_stationary
|
||||
}
|
||||
|
||||
pub(super) fn query(&self, vertices: &[Vertex], start: usize) -> Option<CullKey> {
|
||||
let scan_started = self.measure_bounds_timing.get().then(Instant::now);
|
||||
let bounds = OcclusionCuller::bounds(self.camera, vertices, start);
|
||||
@@ -1229,10 +1236,17 @@ impl AsyncOcclusionCuller {
|
||||
self.cache_results(results, true, camera_cell);
|
||||
}
|
||||
|
||||
fn cache_visible_results(&mut self, results: &mut CullResults) {
|
||||
// Visible results fail open, so they may follow the current camera
|
||||
// sub-cell without ever hiding geometry.
|
||||
self.cache_results(results, false, self.query_cache_camera_cell);
|
||||
#[inline]
|
||||
fn cache_results_for_camera(&mut self, results: &mut CullResults, camera: [u64; 3]) {
|
||||
if results.camera == camera {
|
||||
self.cache_query_results(results);
|
||||
} else {
|
||||
// Even visible answers suppress a fresh query while their cache
|
||||
// entry is live. Drop stale batches instead of sorting and merging
|
||||
// results that cannot help this viewpoint.
|
||||
results.visible.clear();
|
||||
results.hidden.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_results(
|
||||
@@ -1955,8 +1969,8 @@ pub(super) struct SyncOccluder<'a> {
|
||||
streaming: &'a Streaming,
|
||||
pending_uploads: &'a [((i32, i32, i32), SectionMesh)],
|
||||
bottom_y: i32,
|
||||
last_chunk: Option<(ChunkKey, Option<&'a Chunk>)>,
|
||||
last_section: Option<(SectionKey, bool)>,
|
||||
chunk_slots: [Option<(ChunkKey, Option<&'a Chunk>)>; 4],
|
||||
section_slots: [Option<(SectionKey, bool)>; 4],
|
||||
}
|
||||
|
||||
impl<'a> SyncOccluder<'a> {
|
||||
@@ -1974,19 +1988,20 @@ impl<'a> SyncOccluder<'a> {
|
||||
streaming,
|
||||
pending_uploads,
|
||||
bottom_y: world.bottom_y(),
|
||||
last_chunk: None,
|
||||
last_section: None,
|
||||
chunk_slots: [None; 4],
|
||||
section_slots: [None; 4],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn ready(&mut self, pos: [i32; 3]) -> bool {
|
||||
let chunk_key = (pos[0] >> 4, pos[2] >> 4);
|
||||
let chunk = match self.last_chunk {
|
||||
let chunk_index = (((chunk_key.0 as usize) & 1) << 1) | ((chunk_key.1 as usize) & 1);
|
||||
let chunk = match self.chunk_slots[chunk_index] {
|
||||
Some((key, cached)) if key == chunk_key => cached,
|
||||
_ => {
|
||||
let found = self.world.chunks.get(&chunk_key).map(Arc::as_ref);
|
||||
self.last_chunk = Some((chunk_key, found));
|
||||
self.chunk_slots[chunk_index] = Some((chunk_key, found));
|
||||
found
|
||||
}
|
||||
};
|
||||
@@ -2001,14 +2016,15 @@ impl<'a> SyncOccluder<'a> {
|
||||
return false;
|
||||
};
|
||||
let section = (chunk_key.0, y >> 4, chunk_key.1);
|
||||
match self.last_section {
|
||||
let section_index = (chunk_index ^ (section.1 as usize)) & 3;
|
||||
match self.section_slots[section_index] {
|
||||
Some((key, ready)) if key == section => ready,
|
||||
_ => {
|
||||
let ready = self.renderer.is_some_and(|r| r.has_section(section))
|
||||
&& !self.world.dirty_sections.contains(§ion)
|
||||
&& !self.streaming.is_meshing(section)
|
||||
&& !self.pending_uploads.iter().any(|(key, _)| *key == section);
|
||||
self.last_section = Some((section, ready));
|
||||
self.section_slots[section_index] = Some((section, ready));
|
||||
ready
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ fn stale_world_results_cannot_replace_a_current_proof_in_the_same_camera_cell()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_visible_results_are_reusable_in_the_current_camera_cell() {
|
||||
fn stale_camera_visible_results_are_discarded_for_a_fresh_query() {
|
||||
let mut culler = AsyncOcclusionCuller::new().unwrap();
|
||||
culler.stamp = SnapshotKey {
|
||||
world_revision: 2,
|
||||
@@ -275,11 +275,14 @@ fn stale_visible_results_are_reusable_in_the_current_camera_cell() {
|
||||
visible_entities: 1,
|
||||
deferred_entities: 0,
|
||||
};
|
||||
culler.cache_visible_results(&mut results);
|
||||
assert!(matches!(
|
||||
culler.query_result(¤t_key, QuerySource::EntityAabb),
|
||||
Some((false, None))
|
||||
));
|
||||
culler.cache_results_for_camera(&mut results, current_key.camera);
|
||||
assert!(results.visible.is_empty());
|
||||
assert!(results.hidden.is_empty());
|
||||
assert!(
|
||||
culler
|
||||
.query_result(¤t_key, QuerySource::EntityAabb)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -67,6 +67,29 @@ impl Culler {
|
||||
self.occluded(min, max, opaque)
|
||||
}
|
||||
|
||||
/// Whether camera-relative model bounds are hidden. This avoids building
|
||||
/// synthetic vertices when a renderer already has a conservative AABB.
|
||||
pub fn hidden_camera_bounds(
|
||||
&self,
|
||||
lo: [f32; 3],
|
||||
hi: [f32; 3],
|
||||
opaque: impl FnMut([i32; 3]) -> bool,
|
||||
) -> bool {
|
||||
if self.remaining.get() == 0
|
||||
|| self.camera.into_iter().any(|value| !value.is_finite())
|
||||
|| lo.into_iter().chain(hi).any(|value| !value.is_finite())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let min = std::array::from_fn(|axis| {
|
||||
f64::from(lo[axis].min(hi[axis])) + self.camera[axis] - 0.05
|
||||
});
|
||||
let max = std::array::from_fn(|axis| {
|
||||
f64::from(lo[axis].max(hi[axis])) + self.camera[axis] + 0.05
|
||||
});
|
||||
self.occluded(min, max, opaque)
|
||||
}
|
||||
|
||||
/// World-space bounds expanded for floating-point and rasterization edges.
|
||||
pub fn aabb_bounds(bounds: Aabb) -> Option<([f64; 3], [f64; 3])> {
|
||||
let lo = bounds.min_position();
|
||||
@@ -171,29 +194,45 @@ impl Culler {
|
||||
};
|
||||
cell = next;
|
||||
}
|
||||
// These two reciprocals are invariant for every plane along the ray.
|
||||
let denominator_min = (min[axis] - camera[axis]) * sign;
|
||||
let denominator_max = (max[axis] - camera[axis]) * sign;
|
||||
if !denominator_min.is_finite()
|
||||
|| !denominator_max.is_finite()
|
||||
|| denominator_min.abs() <= EPSILON
|
||||
|| denominator_max.abs() <= EPSILON
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let reciprocal_min = 1.0 / denominator_min;
|
||||
let reciprocal_max = 1.0 / denominator_max;
|
||||
let transverse_min = [
|
||||
min[across[0]] - camera[across[0]],
|
||||
min[across[1]] - camera[across[1]],
|
||||
];
|
||||
let transverse_max = [
|
||||
max[across[0]] - camera[across[0]],
|
||||
max[across[1]] - camera[across[1]],
|
||||
];
|
||||
loop {
|
||||
let depth = (f64::from(cell) + 0.5 - camera[axis]) * sign;
|
||||
if depth >= gap - EPSILON || *remaining == 0 {
|
||||
return false;
|
||||
}
|
||||
let t_min = depth * reciprocal_min;
|
||||
let t_max = depth * reciprocal_max;
|
||||
let mut lo = [f64::INFINITY; 2];
|
||||
let mut hi = [f64::NEG_INFINITY; 2];
|
||||
for corner in 0..8 {
|
||||
let point: [f64; 3] = std::array::from_fn(|a| {
|
||||
if corner & (1 << a) == 0 {
|
||||
min[a]
|
||||
} else {
|
||||
max[a]
|
||||
}
|
||||
});
|
||||
let t = depth / ((point[axis] - camera[axis]) * sign);
|
||||
for (i, &a) in across.iter().enumerate() {
|
||||
let p = camera[a] + t * (point[a] - camera[a]);
|
||||
if !p.is_finite() {
|
||||
for i in 0..2 {
|
||||
let camera_axis = camera[across[i]];
|
||||
for t in [t_min, t_max] {
|
||||
let p_min = camera_axis + t * transverse_min[i];
|
||||
let p_max = camera_axis + t * transverse_max[i];
|
||||
if !p_min.is_finite() || !p_max.is_finite() {
|
||||
return false;
|
||||
}
|
||||
lo[i] = lo[i].min(p);
|
||||
hi[i] = hi[i].max(p);
|
||||
lo[i] = lo[i].min(p_min);
|
||||
hi[i] = hi[i].max(p_max);
|
||||
}
|
||||
}
|
||||
let bounds = [
|
||||
@@ -205,27 +244,51 @@ impl Culler {
|
||||
let [Some(x0), Some(x1), Some(y0), Some(y1)] = bounds else {
|
||||
return false;
|
||||
};
|
||||
let center_x = x0 + ((i64::from(x1) - i64::from(x0)) / 2) as i32;
|
||||
let center_y = y0 + ((i64::from(y1) - i64::from(y0)) / 2) as i32;
|
||||
let mut pos = [0; 3];
|
||||
pos[axis] = cell;
|
||||
pos[across[0]] = x0 + ((i64::from(x1) - i64::from(x0)) / 2) as i32;
|
||||
pos[across[1]] = y0 + ((i64::from(y1) - i64::from(y0)) / 2) as i32;
|
||||
pos[across[0]] = center_x;
|
||||
pos[across[1]] = center_y;
|
||||
*remaining -= 1;
|
||||
if opaque(pos) {
|
||||
let area = (i64::from(x1) - i64::from(x0) + 1)
|
||||
.checked_mul(i64::from(y1) - i64::from(y0) + 1)
|
||||
.and_then(|n| usize::try_from(n).ok());
|
||||
let Some(area) = area.filter(|&n| n <= *remaining) else {
|
||||
let Some(additional) = area.and_then(|n| n.checked_sub(1)) else {
|
||||
return false;
|
||||
};
|
||||
// Reserve the worst-case lookups before walking the rectangle.
|
||||
*remaining -= area;
|
||||
let covered = (x0..=x1).all(|x| {
|
||||
if additional > *remaining {
|
||||
return false;
|
||||
}
|
||||
// Keep Y in the inner loop for X-facing planes. This keeps
|
||||
// consecutive probes in one chunk column. Charge only probes
|
||||
// actually visited when a gap short-circuits the rectangle.
|
||||
let covered = if across[0] == 1 {
|
||||
(y0..=y1).all(|y| {
|
||||
pos[across[0]] = x;
|
||||
pos[across[1]] = y;
|
||||
opaque(pos)
|
||||
(x0..=x1).all(|x| {
|
||||
if x == center_x && y == center_y {
|
||||
return true;
|
||||
}
|
||||
*remaining -= 1;
|
||||
pos[across[0]] = x;
|
||||
opaque(pos)
|
||||
})
|
||||
})
|
||||
});
|
||||
} else {
|
||||
(x0..=x1).all(|x| {
|
||||
pos[across[0]] = x;
|
||||
(y0..=y1).all(|y| {
|
||||
if x == center_x && y == center_y {
|
||||
return true;
|
||||
}
|
||||
*remaining -= 1;
|
||||
pos[across[1]] = y;
|
||||
opaque(pos)
|
||||
})
|
||||
})
|
||||
};
|
||||
if covered {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user