mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Optimize async entity occlusion hot paths
Co-Authored-By: OpenAI Codex <[email protected]>
This commit is contained in:
1 parent
4e069b1e21
commit
37380065b7
7 files changed
+314
-13
No files matched your search
@@ -2845,7 +2845,7 @@ struct Game<'a> {
|
||||
/// This frame's view frustum, for culling entities and block entities.
|
||||
view_frustum: Option<horizon_client::gfx::Frustum>,
|
||||
sync_occlusion_culler: Option<horizon_client::entity_culling::Culler>,
|
||||
async_occlusion_culler: Option<occlusion_culling::AsyncOcclusionCuller>,
|
||||
async_occlusion_culler: Option<Box<occlusion_culling::AsyncOcclusionCuller>>,
|
||||
async_occlusion_culling_failed: bool,
|
||||
/// `Renderer::pack_shadow_casters` for this frame's entities: (keep what the culler hides,
|
||||
/// build the player's model in first person); (false, false) without a shader pack.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! breeding, conversions, drops), projectiles, player interactions
|
||||
//! (feeding, shearing, milking, spawn eggs, throwing) and rendering.
|
||||
use super::{
|
||||
Dim, Game, Hand, InteractTarget, dispenser, native_mob_draw::NativePlayerRender,
|
||||
native_projectiles::Impact, view_vector,
|
||||
Dim, Game, Hand, InteractTarget, dispenser, entity_culling::EntityCullDecision,
|
||||
native_mob_draw::NativePlayerRender, native_projectiles::Impact, view_vector,
|
||||
};
|
||||
use horizon_assets::item_model::DisplayContext;
|
||||
use horizon_client::{
|
||||
@@ -12,6 +12,7 @@ use horizon_client::{
|
||||
arrows::EntityHit,
|
||||
blocks::Fluid,
|
||||
collisions::SceneCollisions,
|
||||
graphics::EntityCullingMode,
|
||||
health::{HurtSource, Hurting, Knock, knockback_canceled},
|
||||
items::ItemStack,
|
||||
mesher::Vertex,
|
||||
@@ -159,6 +160,7 @@ impl ProjectileWorld for ProjWorld<'_, '_> {
|
||||
/// `ProjectileUtil.getEntityHitResult` can strike it (`ThrownEnderpearl.onHit`).
|
||||
/// `WIND_CHARGE_IDS + j` is `projectiles[j]` during `tick_projectiles`, below
|
||||
/// the dragon's reserved ids.
|
||||
const ASYNC_MOB_AABB_PRECHECK_MIN: usize = 512;
|
||||
const WIND_CHARGE_IDS: u32 = super::dragon::SPECIAL - 0x10000;
|
||||
|
||||
/// The index of the wind charge a `wind_charge_hitboxes` id stands for.
|
||||
@@ -6367,6 +6369,26 @@ impl Game<'_> {
|
||||
}
|
||||
|
||||
pub(super) fn mob_geometry(&self, camera: glam::DVec3, alpha: f64, out: &mut Vec<Vertex>) {
|
||||
// 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.mobs.len() >= ASYNC_MOB_AABB_PRECHECK_MIN
|
||||
&& !self.pack_shadow_casters.0
|
||||
&& horizon_client::mod_api::mods().is_empty()
|
||||
{
|
||||
self.mob_geometry_inner::<true>(camera, alpha, out);
|
||||
} else {
|
||||
self.mob_geometry_inner::<false>(camera, alpha, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn mob_geometry_inner<const PRECHECK: bool>(
|
||||
&self,
|
||||
camera: glam::DVec3,
|
||||
alpha: f64,
|
||||
out: &mut Vec<Vertex>,
|
||||
) {
|
||||
let atlas = &self.gui.atlas;
|
||||
let lookup = |name: &str| atlas.sprites.get(name).map(|r| [r.u0, r.v0, r.u1, r.v1]);
|
||||
let scale = self.view_scale();
|
||||
@@ -6437,6 +6459,29 @@ impl Game<'_> {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let culling = if PRECHECK
|
||||
&& outline.is_none()
|
||||
&& !is_player
|
||||
&& mob.kind.id.starts_with("minecraft:")
|
||||
&& [
|
||||
&mob.extra.head,
|
||||
&mob.extra.chest,
|
||||
&mob.extra.legs,
|
||||
&mob.extra.feet,
|
||||
&mob.extra.main_hand,
|
||||
&mob.extra.off_hand,
|
||||
]
|
||||
.iter()
|
||||
.all(|item| item.is_none())
|
||||
&& horizon_client::mobs::guardian::beam(mob, alpha as f32).is_none()
|
||||
{
|
||||
self.entity_cull_decision(mob, alpha)
|
||||
} else {
|
||||
EntityCullDecision::CheckVertices
|
||||
};
|
||||
if culling.hides_entity() {
|
||||
continue;
|
||||
}
|
||||
self.mark_entity(entity_start, &mob.kind.id);
|
||||
self.rendered_mobs.set(self.rendered_mobs.get() + 1);
|
||||
let light = self.world.light(
|
||||
@@ -6621,6 +6666,7 @@ impl Game<'_> {
|
||||
// Glowing outlines and a guardian's beam remain visible independently
|
||||
// of whether the body is hidden behind terrain.
|
||||
if outline.is_none()
|
||||
&& culling.checks_vertices()
|
||||
&& horizon_client::mobs::guardian::beam(mob, alpha as f32).is_none()
|
||||
{
|
||||
// What the GPU draws of the mob counts by its bounds' corners.
|
||||
|
||||
@@ -245,6 +245,29 @@ pub(super) trait Entity: EntityRules {
|
||||
pub(super) trait RenderEntityBounds {
|
||||
fn render_id(&self) -> u32;
|
||||
fn render_bounds(&self, alpha: f64) -> Aabb;
|
||||
fn render_glowing(&self, game: &Game<'_>) -> bool {
|
||||
game.entity_glowing(self.render_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderEntityBounds for Mob {
|
||||
fn render_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn render_bounds(&self, alpha: f64) -> Aabb {
|
||||
// A loose rotation-independent envelope for vanilla model animations.
|
||||
// Standing dimensions keep sleeping poses from shrinking it; equipment
|
||||
// and custom rendering bypass this pre-check at the call site.
|
||||
let (width, height, _) = self.awake_dims();
|
||||
let radius = width.max(height) * 2.5 + 3.0;
|
||||
let at = self.previous.lerp(self.position, f64::from(alpha as f32));
|
||||
Aabb::of_size(at, Vec3::new(radius * 2.0, radius * 2.0, radius * 2.0))
|
||||
}
|
||||
|
||||
fn render_glowing(&self, _game: &Game<'_>) -> bool {
|
||||
self.is_currently_glowing()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! moving_render_entity_bounds {
|
||||
|
||||
@@ -58,7 +58,7 @@ impl Game<'_> {
|
||||
) -> EntityCullDecision {
|
||||
if self.settings.horizon.entity_culling == EntityCullingMode::Off
|
||||
|| self.pack_shadow_casters.0
|
||||
|| self.entity_glowing(entity.render_id())
|
||||
|| entity.render_glowing(self)
|
||||
{
|
||||
return EntityCullDecision::Bypass;
|
||||
}
|
||||
@@ -116,9 +116,7 @@ impl Game<'_> {
|
||||
vertices: &mut Vec<Vertex>,
|
||||
start: usize,
|
||||
) {
|
||||
if decision.checks_vertices()
|
||||
&& !self.pack_shadow_casters.0
|
||||
&& !self.entity_glowing(entity.render_id())
|
||||
if decision.checks_vertices() && !self.pack_shadow_casters.0 && !entity.render_glowing(self)
|
||||
{
|
||||
self.cull_entity_vertices(vertices, start);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type ChunkKey = (i32, i32);
|
||||
type SectionKey = (i32, i32, i32);
|
||||
const MAX_SNAPSHOT_CHUNKS: i64 = 4_096;
|
||||
const MAX_OCCLUSION_RESULT_CACHE_ENTRIES: usize = 16_384;
|
||||
const RESULT_CACHE_IDLE_FRAMES: u64 = 2;
|
||||
const OCCLUSION_LOOKUP_BUDGET: usize = 8_192;
|
||||
const OPACITY_GRID_SIZE: usize = 1_024;
|
||||
const MAX_OCCLUSION_RAY_STEPS: usize = 512;
|
||||
@@ -475,6 +476,7 @@ struct CachedQueryResult {
|
||||
stamp: SnapshotKey,
|
||||
camera_cell: Option<[i32; 3]>,
|
||||
proof: Option<OcclusionProof>,
|
||||
last_used_frame: Cell<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -489,6 +491,7 @@ struct AsyncCullingStats {
|
||||
entity_mesh_cache_misses: u64,
|
||||
result_cache_evictions: u64,
|
||||
result_cache_capacity_evictions: u64,
|
||||
result_cache_idle_evictions: u64,
|
||||
result_cache_invalidation_replacements: u64,
|
||||
cache_miss_empty_set: u64,
|
||||
cache_miss_key_mismatch: u64,
|
||||
@@ -563,6 +566,7 @@ pub(super) struct AsyncOcclusionCuller {
|
||||
camera: [f64; 3],
|
||||
stamp: SnapshotKey,
|
||||
pending: bool,
|
||||
frame: u64,
|
||||
proof_validation_sample_counter: Cell<u64>,
|
||||
}
|
||||
|
||||
@@ -640,7 +644,9 @@ impl AsyncOcclusionCuller {
|
||||
cache_merge: Vec::new(),
|
||||
stats: RefCell::new(AsyncCullingStats::default()),
|
||||
proof_validation_cache: RefCell::new(FrameProofValidationCache::default()),
|
||||
measure_bounds_timing: Cell::new(false),
|
||||
measure_bounds_timing: Cell::new(
|
||||
std::env::var("HORIZON_OCCLUSION_PROFILE").is_ok_and(|v| v == "1"),
|
||||
),
|
||||
query_cache_stamp: None,
|
||||
query_cache_camera_cell: None,
|
||||
camera: [0.0; 3],
|
||||
@@ -650,6 +656,7 @@ impl AsyncOcclusionCuller {
|
||||
bottom_y: 0,
|
||||
},
|
||||
pending: false,
|
||||
frame: 0,
|
||||
proof_validation_sample_counter: Cell::new(0),
|
||||
})
|
||||
}
|
||||
@@ -661,6 +668,7 @@ impl AsyncOcclusionCuller {
|
||||
}
|
||||
|
||||
fn begin_frame_inner(&mut self, camera: glam::DVec3, world: &World) {
|
||||
self.frame = self.frame.saturating_add(1);
|
||||
self.proof_validation_cache.get_mut().clear();
|
||||
self.camera = camera.to_array();
|
||||
self.stamp = SnapshotKey {
|
||||
@@ -826,7 +834,10 @@ impl AsyncOcclusionCuller {
|
||||
&& self
|
||||
.query_cache_camera_cell
|
||||
.is_some_and(|cell| entry.camera_cell == Some(cell));
|
||||
(exact || reusable_visible).then(|| (entry.proof.is_some(), entry.proof.as_ref()))
|
||||
(exact || reusable_visible).then(|| {
|
||||
entry.last_used_frame.set(self.frame);
|
||||
(entry.proof.is_some(), entry.proof.as_ref())
|
||||
})
|
||||
});
|
||||
let miss_reason = result.is_none().then(|| {
|
||||
cache_miss_reason(
|
||||
@@ -987,6 +998,7 @@ impl AsyncOcclusionCuller {
|
||||
stamp: results.stamp,
|
||||
camera_cell: result_camera_cell,
|
||||
proof: None,
|
||||
last_used_frame: Cell::new(self.frame),
|
||||
}));
|
||||
} else {
|
||||
results.visible.clear();
|
||||
@@ -1017,6 +1029,7 @@ impl AsyncOcclusionCuller {
|
||||
stamp: results.stamp,
|
||||
camera_cell: result_camera_cell,
|
||||
proof: Some(proof),
|
||||
last_used_frame: Cell::new(self.frame),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -1045,6 +1058,7 @@ impl AsyncOcclusionCuller {
|
||||
let mut updates_iter = updates.drain(..).peekable();
|
||||
let mut invalidation_replacements = 0_u64;
|
||||
let mut capacity_evictions = 0_u64;
|
||||
let mut idle_evictions = 0_u64;
|
||||
while existing_iter.peek().is_some() || updates_iter.peek().is_some() {
|
||||
let entry = match (existing_iter.peek(), updates_iter.peek()) {
|
||||
(Some(old), Some(new)) => {
|
||||
@@ -1068,7 +1082,12 @@ impl AsyncOcclusionCuller {
|
||||
(None, Some(_)) => updates_iter.next().unwrap(),
|
||||
(None, None) => unreachable!(),
|
||||
};
|
||||
if merged.len() < MAX_OCCLUSION_RESULT_CACHE_ENTRIES {
|
||||
// Animated mesh extrema often never repeat. Drop their unused
|
||||
// answers during this existing merge, while frequently queried
|
||||
// AABBs and mesh bounds retain their entries without extra scans.
|
||||
if self.frame.saturating_sub(entry.last_used_frame.get()) > RESULT_CACHE_IDLE_FRAMES {
|
||||
idle_evictions = idle_evictions.saturating_add(1);
|
||||
} else if merged.len() < MAX_OCCLUSION_RESULT_CACHE_ENTRIES {
|
||||
merged.push(entry);
|
||||
} else {
|
||||
capacity_evictions = capacity_evictions.saturating_add(1);
|
||||
@@ -1080,7 +1099,11 @@ impl AsyncOcclusionCuller {
|
||||
let mut stats = self.stats.borrow_mut();
|
||||
stats.result_cache_evictions = stats
|
||||
.result_cache_evictions
|
||||
.saturating_add(capacity_evictions);
|
||||
.saturating_add(capacity_evictions)
|
||||
.saturating_add(idle_evictions);
|
||||
stats.result_cache_idle_evictions = stats
|
||||
.result_cache_idle_evictions
|
||||
.saturating_add(idle_evictions);
|
||||
stats.result_cache_capacity_evictions = stats
|
||||
.result_cache_capacity_evictions
|
||||
.saturating_add(capacity_evictions);
|
||||
@@ -1279,6 +1302,8 @@ impl AsyncOcclusionCuller {
|
||||
"entity_budget_exhausted_jobs": stats.entity_budget_exhausted_jobs,
|
||||
"result_cache_slots": MAX_OCCLUSION_RESULT_CACHE_ENTRIES,
|
||||
"result_cache_entries": self.query_results.len(),
|
||||
"profiling_enabled": self.measure_bounds_timing.get(),
|
||||
"state_bytes": std::mem::size_of::<Self>(),
|
||||
});
|
||||
value["entity_aabb_cache_hits"] = serde_json::json!(stats.entity_aabb_cache_hits);
|
||||
value["entity_aabb_cache_misses"] = serde_json::json!(stats.entity_aabb_cache_misses);
|
||||
@@ -1286,6 +1311,7 @@ impl AsyncOcclusionCuller {
|
||||
value["entity_mesh_cache_misses"] = serde_json::json!(stats.entity_mesh_cache_misses);
|
||||
value["result_cache_capacity_evictions"] =
|
||||
serde_json::json!(stats.result_cache_capacity_evictions);
|
||||
value["result_cache_idle_evictions"] = serde_json::json!(stats.result_cache_idle_evictions);
|
||||
value["result_cache_invalidation_replacements"] =
|
||||
serde_json::json!(stats.result_cache_invalidation_replacements);
|
||||
value["cache_miss_empty_set"] = serde_json::json!(stats.cache_miss_empty_set);
|
||||
@@ -1322,7 +1348,6 @@ impl AsyncOcclusionCuller {
|
||||
|
||||
pub(super) fn reset_stats(&self) {
|
||||
*self.stats.borrow_mut() = AsyncCullingStats::default();
|
||||
self.measure_bounds_timing.set(true);
|
||||
self.proof_validation_sample_counter.set(0);
|
||||
}
|
||||
}
|
||||
@@ -1606,7 +1631,7 @@ impl Game<'_> {
|
||||
if wants_async {
|
||||
if self.async_occlusion_culler.is_none() && !self.async_occlusion_culling_failed {
|
||||
match AsyncOcclusionCuller::new() {
|
||||
Ok(culler) => self.async_occlusion_culler = Some(culler),
|
||||
Ok(culler) => self.async_occlusion_culler = Some(Box::new(culler)),
|
||||
Err(error) => {
|
||||
log::warn!("Horizon: cannot start world-occlusion-culling worker: {error}");
|
||||
self.async_occlusion_culling_failed = true;
|
||||
@@ -1771,3 +1796,7 @@ mod tests {
|
||||
drop(live);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "occlusion_culling_precheck_tests.rs"]
|
||||
mod precheck_tests;
|
||||
@@ -0,0 +1,201 @@
|
||||
use super::*;
|
||||
use crate::game::entity::RenderEntityBounds;
|
||||
use horizon_client::mobs::{Mob, MobData, MobTypes, Species, render};
|
||||
use horizon_core::geometry::Vec3;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn vanilla_models_and_animations_stay_inside_the_precheck_envelope() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.local/horizon-client/26.3");
|
||||
if !root.join("entity-models.json").exists() {
|
||||
eprintln!("no prepared resources: skipped model coverage");
|
||||
return;
|
||||
}
|
||||
let types = MobTypes::load(&root).unwrap();
|
||||
let data = MobData {
|
||||
states: Vec::new(),
|
||||
placements: Default::default(),
|
||||
heightmaps: Default::default(),
|
||||
animations: MobData::load_animations(&root),
|
||||
villager_hats: Default::default(),
|
||||
cem: Default::default(),
|
||||
random_textures: Default::default(),
|
||||
emissive: Default::default(),
|
||||
blinks: Default::default(),
|
||||
biome_tags: Default::default(),
|
||||
entity_tags: Default::default(),
|
||||
pois: Default::default(),
|
||||
species_placements: Default::default(),
|
||||
};
|
||||
let camera = glam::DVec3::new(3.0, 62.0, -4.0);
|
||||
let mut checked = 0;
|
||||
for kind in types.types.values() {
|
||||
for sample in 0..12 {
|
||||
let mut mob = Mob::new(1, Arc::new(kind.clone()), Vec3::new(1.5, 64.0, 3.25), 37.0);
|
||||
mob.previous = Vec3::new(1.25, 64.25, 3.0);
|
||||
mob.age = sample * 37;
|
||||
mob.limb_speed = 0.8;
|
||||
mob.limb_position = sample as f32 * 1.7;
|
||||
mob.head_pitch = if sample % 2 == 0 { 85.0 } else { -85.0 };
|
||||
mob.extra.age = if sample >= 6 { -100 } else { 0 };
|
||||
mob.death_time = if sample % 3 == 0 { 10 } else { 0 };
|
||||
mob.extra.flag = sample % 2 == 0;
|
||||
mob.extra.size = match mob.species() {
|
||||
Species::Slime | Species::MagmaCube | Species::SulfurCube | Species::Phantom => {
|
||||
if sample % 2 == 0 {
|
||||
1
|
||||
} else {
|
||||
4
|
||||
}
|
||||
}
|
||||
_ => mob.extra.size,
|
||||
};
|
||||
if sample % 3 == 1 {
|
||||
mob.custom_name = Some("Dinnerbone".into());
|
||||
}
|
||||
for partial in [0.0, 0.4, 1.0] {
|
||||
let bounds = mob.render_bounds(f64::from(partial));
|
||||
let lo = bounds.min_position();
|
||||
let hi = bounds.max_position();
|
||||
let lo = [lo.x, lo.y, lo.z];
|
||||
let hi = [hi.x, hi.y, hi.z];
|
||||
let mut vertices = Vec::new();
|
||||
render::render_with(
|
||||
&mob,
|
||||
partial,
|
||||
camera,
|
||||
&|_| Some([0.0, 0.0, 1.0, 1.0]),
|
||||
0xF0F0,
|
||||
&data,
|
||||
&[],
|
||||
&mut vertices,
|
||||
);
|
||||
for vertex in &vertices {
|
||||
for axis in 0..3 {
|
||||
let world = f64::from(vertex.pos[axis]) + camera[axis];
|
||||
assert!(
|
||||
world >= lo[axis] - 1.0e-4 && world <= hi[axis] + 1.0e-4,
|
||||
"{} sample {sample} partial {partial}: {world} outside {lo:?}..{hi:?}",
|
||||
kind.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
checked += usize::from(!vertices.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
checked > 1_000,
|
||||
"insufficient real model coverage: {checked}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hole_inside_a_coarse_envelope_remains_visible() {
|
||||
let camera = [0.0; 3];
|
||||
let min = [7.95, -3.0, -3.0];
|
||||
let max = [10.05, 3.0, 3.0];
|
||||
assert!(OcclusionCuller::new(camera, 8192).occluded(min, max, |p| p[0] == 4));
|
||||
assert!(
|
||||
!OcclusionCuller::new(camera, 8192).occluded(min, max, |p| p[0] == 4 && p != [4, 1, 0])
|
||||
);
|
||||
assert!(!OcclusionCuller::new(camera, 0).occluded(min, max, |p| p[0] == 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_cache_entries_require_the_same_camera_and_bounds() {
|
||||
let mut culler = AsyncOcclusionCuller::new().unwrap();
|
||||
let bounds = Aabb::new(Vec3::new(8.0, -1.0, -1.0), Vec3::new(10.0, 1.0, 1.0));
|
||||
let original = culler.query_entity_bounds(bounds).unwrap();
|
||||
culler.query_results.push(CachedQueryResult {
|
||||
key: original,
|
||||
stamp: culler.stamp,
|
||||
camera_cell: Some([0; 3]),
|
||||
proof: Some(OcclusionProof {
|
||||
sections: Vec::new(),
|
||||
chunks: Vec::new(),
|
||||
}),
|
||||
last_used_frame: Cell::new(0),
|
||||
});
|
||||
assert!(matches!(
|
||||
culler.query_result(&original, QuerySource::EntityAabb),
|
||||
Some((true, _))
|
||||
));
|
||||
let moved = culler
|
||||
.query_entity_bounds(bounds.move_by(Vec3::new(0.01, 0.0, 0.0)))
|
||||
.unwrap();
|
||||
assert!(
|
||||
culler
|
||||
.query_result(&moved, QuerySource::EntityAabb)
|
||||
.is_none()
|
||||
);
|
||||
culler.camera[0] = 0.01;
|
||||
let moved_camera = culler.query_entity_bounds(bounds).unwrap();
|
||||
assert!(
|
||||
culler
|
||||
.query_result(&moved_camera, QuerySource::EntityAabb)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resetting_benchmark_counters_preserves_the_profiling_choice() {
|
||||
let culler = AsyncOcclusionCuller::new().unwrap();
|
||||
for enabled in [false, true] {
|
||||
culler.measure_bounds_timing.set(enabled);
|
||||
culler.reset_stats();
|
||||
assert_eq!(culler.stats_value()["profiling_enabled"], enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inactive_answers_expire_while_frequently_used_proofs_stay_cached() {
|
||||
let mut culler = AsyncOcclusionCuller::new().unwrap();
|
||||
let bounds = |x| Aabb::new(Vec3::new(x, -1.0, -1.0), Vec3::new(x + 1.0, 1.0, 1.0));
|
||||
let hot = culler.query_entity_bounds(bounds(8.0)).unwrap();
|
||||
let cold = culler.query_entity_bounds(bounds(12.0)).unwrap();
|
||||
let fresh = culler.query_entity_bounds(bounds(16.0)).unwrap();
|
||||
for key in [hot, cold] {
|
||||
culler.query_results.push(CachedQueryResult {
|
||||
key,
|
||||
stamp: culler.stamp,
|
||||
camera_cell: Some([0; 3]),
|
||||
proof: Some(OcclusionProof {
|
||||
sections: Vec::new(),
|
||||
chunks: Vec::new(),
|
||||
}),
|
||||
last_used_frame: Cell::new(0),
|
||||
});
|
||||
}
|
||||
culler.frame = 10;
|
||||
assert!(matches!(
|
||||
culler.query_result(&hot, QuerySource::EntityAabb),
|
||||
Some((true, _))
|
||||
));
|
||||
culler.cache_query_results(&mut CullResults {
|
||||
stamp: culler.stamp,
|
||||
camera: culler.camera.map(f64::to_bits),
|
||||
generation: 0,
|
||||
cancelled: false,
|
||||
hidden: Vec::new(),
|
||||
visible: vec![fresh],
|
||||
scratch_queries: Vec::new(),
|
||||
worker_ns: 0,
|
||||
hidden_entities: 0,
|
||||
visible_entities: 1,
|
||||
deferred_entities: 0,
|
||||
});
|
||||
assert!(matches!(
|
||||
culler.query_result(&hot, QuerySource::EntityAabb),
|
||||
Some((true, _))
|
||||
));
|
||||
assert!(
|
||||
culler
|
||||
.query_result(&cold, QuerySource::EntityAabb)
|
||||
.is_none()
|
||||
);
|
||||
assert!(matches!(
|
||||
culler.query_result(&fresh, QuerySource::EntityAabb),
|
||||
Some((false, _))
|
||||
));
|
||||
}
|
||||
@@ -74,6 +74,10 @@ cargo run --release -p horizon-client -- --render-distance 12
|
||||
|
||||
Development builds that link quickly: `cargo build --profile fast -p horizon-client`.
|
||||
|
||||
`HORIZON_OCCLUSION_PROFILE=1` enables detailed entity-occlusion phase and bounds-scan timings
|
||||
in `horizon-ctl stats` and `bench`. The normal benchmark keeps these CPU-clock reads disabled;
|
||||
query, cache-hit and cull counters remain available without profiling.
|
||||
|
||||
Optional Windows allocator build features are documented in
|
||||
[Windows allocator experiments](windows-allocators.md).
|
||||
|
||||
|
||||
Reference in new issue
Block a user