mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11645 from vegetableovosh/fix/11214-chunk-refresh
Fix/11214 chunk refresh
This commit is contained in:
4 files changed
+108
-42
No files matched your search
@@ -1126,18 +1126,29 @@ impl Streaming {
|
|||||||
pub fn lod_source(&self) -> Arc<dyn LodSource> {
|
pub fn lod_source(&self) -> Arc<dyn LodSource> {
|
||||||
self.saves.clone()
|
self.saves.clone()
|
||||||
}
|
}
|
||||||
/// Resource packs reloaded (`LevelRenderer.allChanged`): mesh with the new
|
/// Resource packs reloaded (`LevelExtractor.allChanged`): mesh with the new
|
||||||
/// models and sprites, and rebuild every loaded section. Meshes in flight
|
/// models and sprites, and rebuild every loaded section. Meshes in flight
|
||||||
/// were made with the old resources; their results are dropped.
|
/// were made with the old resources; their results are dropped.
|
||||||
pub fn reload_resources(&mut self, world: &mut World, context: Arc<WorkerContext>) {
|
pub fn reload_resources(&mut self, world: &mut World, context: Arc<WorkerContext>) {
|
||||||
*self.shared.context.write().unwrap() = context;
|
*self.shared.context.write().unwrap() = context;
|
||||||
|
self.reload_sections(world);
|
||||||
|
}
|
||||||
|
/// `LevelExtractor.allChanged`: discard mesh work made before the reload and
|
||||||
|
/// rebuild every loaded, lit section.
|
||||||
|
pub fn reload_sections(&mut self, world: &mut World) {
|
||||||
self.meshing.clear();
|
self.meshing.clear();
|
||||||
self.compile_started.clear();
|
self.compile_started.clear();
|
||||||
|
Self::mark_loaded_sections_dirty(world, &self.lit);
|
||||||
|
}
|
||||||
|
/// The resident sections `LevelExtractor.allChanged` queues again. An
|
||||||
|
/// unlit column will queue itself when its light job completes.
|
||||||
|
fn mark_loaded_sections_dirty(world: &mut World, lit: &FxHashSet<(i32, i32)>) {
|
||||||
for &(x, z) in world.chunks.keys() {
|
for &(x, z) in world.chunks.keys() {
|
||||||
if self.lit.contains(&(x, z)) {
|
if !lit.contains(&(x, z)) {
|
||||||
for sy in 0..world.layout().sections() as i32 {
|
continue;
|
||||||
world.dirty_sections.insert((x, sy, z));
|
}
|
||||||
}
|
for sy in 0..world.layout().sections() as i32 {
|
||||||
|
world.dirty_sections.insert((x, sy, z));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4462,3 +4473,38 @@ mod cover_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod reload_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reload_marks_every_section_of_loaded_lit_columns() {
|
||||||
|
let mut world = World::new(horizon_world::registry::BlockStateId(0), 0);
|
||||||
|
world.chunks.insert((2, -3), Arc::new(Chunk::empty(2, -3)));
|
||||||
|
world.chunks.insert((4, 5), Arc::new(Chunk::empty(4, 5)));
|
||||||
|
let lit = FxHashSet::from_iter([(2, -3), (99, 99)]);
|
||||||
|
|
||||||
|
Streaming::mark_loaded_sections_dirty(&mut world, &lit);
|
||||||
|
|
||||||
|
let sections = world.layout().sections() as i32;
|
||||||
|
assert_eq!(world.dirty_sections.len(), sections as usize);
|
||||||
|
for sy in 0..sections {
|
||||||
|
assert!(world.dirty_sections.contains(&(2, sy, -3)));
|
||||||
|
assert!(!world.dirty_sections.contains(&(4, sy, 5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reload_keeps_existing_work_and_ignores_unloaded_lit_columns() {
|
||||||
|
let mut world = World::new(horizon_world::registry::BlockStateId(0), 0);
|
||||||
|
world.chunks.insert((4, 5), Arc::new(Chunk::empty(4, 5)));
|
||||||
|
world.dirty_sections.insert((4, 3, 5));
|
||||||
|
let lit = FxHashSet::from_iter([(99, 99)]);
|
||||||
|
|
||||||
|
Streaming::mark_loaded_sections_dirty(&mut world, &lit);
|
||||||
|
|
||||||
|
assert_eq!(world.dirty_sections.len(), 1);
|
||||||
|
assert!(world.dirty_sections.contains(&(4, 3, 5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15481,27 +15481,30 @@ impl Game<'_> {
|
|||||||
// LocalPlayer.refreshChatAbilities: the chat shows what the Chat option allows.
|
// LocalPlayer.refreshChatAbilities: the chat shows what the Chat option allows.
|
||||||
let abilities = self.chat_abilities();
|
let abilities = self.chat_abilities();
|
||||||
self.ui.chat.set_abilities(&self.gui.atlas.font, abilities);
|
self.ui.chat.set_abilities(&self.gui.atlas.font, abilities);
|
||||||
|
let render_distance_changed = self.streaming.radius != self.settings.render_distance;
|
||||||
self.streaming.radius = self.settings.render_distance;
|
self.streaming.radius = self.settings.render_distance;
|
||||||
// Options.biomeBlendRadius, ambientOcclusion, cutoutLeaves and the
|
// Options.renderDistance, biomeBlendRadius, ambientOcclusion,
|
||||||
// leaf-culling mode: LevelExtractor.allChanged when they change.
|
// cutoutLeaves and the leaf-culling mode: LevelExtractor.allChanged
|
||||||
|
// when they change.
|
||||||
let leaf_culling = self.effective_leaf_culling();
|
let leaf_culling = self.effective_leaf_culling();
|
||||||
let (s, st) = (&self.settings, &self.streaming);
|
let (s, st) = (&self.settings, &self.streaming);
|
||||||
let remesh = (
|
let reload = render_distance_changed
|
||||||
s.biome_blend,
|
|| (
|
||||||
s.smooth_lighting,
|
s.biome_blend,
|
||||||
s.cutout_leaves,
|
s.smooth_lighting,
|
||||||
leaf_culling,
|
s.cutout_leaves,
|
||||||
) != (
|
leaf_culling,
|
||||||
st.biome_blend,
|
) != (
|
||||||
st.smooth_lighting,
|
st.biome_blend,
|
||||||
st.cutout_leaves,
|
st.smooth_lighting,
|
||||||
st.leaf_culling,
|
st.cutout_leaves,
|
||||||
);
|
st.leaf_culling,
|
||||||
|
);
|
||||||
self.streaming.biome_blend = self.settings.biome_blend;
|
self.streaming.biome_blend = self.settings.biome_blend;
|
||||||
self.streaming.smooth_lighting = self.settings.smooth_lighting;
|
self.streaming.smooth_lighting = self.settings.smooth_lighting;
|
||||||
self.streaming.cutout_leaves = self.settings.cutout_leaves;
|
self.streaming.cutout_leaves = self.settings.cutout_leaves;
|
||||||
self.streaming.leaf_culling = leaf_culling;
|
self.streaming.leaf_culling = leaf_culling;
|
||||||
if remesh {
|
if reload {
|
||||||
self.all_sections_changed();
|
self.all_sections_changed();
|
||||||
}
|
}
|
||||||
self.audio.master = self.settings.master_volume;
|
self.audio.master = self.settings.master_volume;
|
||||||
@@ -15522,13 +15525,15 @@ impl Game<'_> {
|
|||||||
self.all_sections_changed();
|
self.all_sections_changed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// `LevelExtractor.allChanged`: every loaded section is rebuilt.
|
/// `LevelExtractor.allChanged`: displayed sections and mesh work are
|
||||||
|
/// discarded, then every loaded section is rebuilt.
|
||||||
fn all_sections_changed(&mut self) {
|
fn all_sections_changed(&mut self) {
|
||||||
for &(x, z) in self.world.chunks.keys() {
|
self.clear_lod();
|
||||||
for sy in 0..self.world.layout().height() / 16 {
|
self.pending_uploads.clear();
|
||||||
self.world.dirty_sections.insert((x, sy, z));
|
if let Some(renderer) = self.renderer.as_mut() {
|
||||||
}
|
renderer.clear_sections(self.world.layout());
|
||||||
}
|
}
|
||||||
|
self.streaming.reload_sections(&mut self.world);
|
||||||
}
|
}
|
||||||
/// Whether precipitation at a column is snow (Biome.coldEnoughToSnow:
|
/// Whether precipitation at a column is snow (Biome.coldEnoughToSnow:
|
||||||
/// height adjusted temperature) or none (no precipitation).
|
/// height adjusted temperature) or none (no precipitation).
|
||||||
|
|||||||
@@ -73,14 +73,17 @@ fn columns_loaded_past_the_render_distance_mesh_when_they_come_in_range() {
|
|||||||
streaming.radius = 4;
|
streaming.radius = 4;
|
||||||
let mut world = World::new(BlockStateId(0), 0);
|
let mut world = World::new(BlockStateId(0), 0);
|
||||||
let mut meshed = HashSet::new();
|
let mut meshed = HashSet::new();
|
||||||
let mut settle_at = |streaming: &mut Streaming, center: (i32, i32)| {
|
let settle_at = |streaming: &mut Streaming,
|
||||||
|
world: &mut World,
|
||||||
|
center: (i32, i32),
|
||||||
|
meshed: &mut HashSet<(i32, i32, i32)>| {
|
||||||
streaming.center = center;
|
streaming.center = center;
|
||||||
let deadline = Instant::now() + Duration::from_secs(60);
|
let deadline = Instant::now() + Duration::from_secs(60);
|
||||||
let mut quiet = 0;
|
let mut quiet = 0;
|
||||||
while quiet < 40 {
|
while quiet < 40 {
|
||||||
assert!(Instant::now() < deadline, "streaming never settled");
|
assert!(Instant::now() < deadline, "streaming never settled");
|
||||||
let before = (world.chunks.len(), meshed.len());
|
let before = (world.chunks.len(), meshed.len());
|
||||||
let (uploads, _) = streaming.update(&mut world);
|
let (uploads, _) = streaming.update(world);
|
||||||
meshed.extend(uploads.iter().map(|(key, _)| *key));
|
meshed.extend(uploads.iter().map(|(key, _)| *key));
|
||||||
// As the game does once they are uploaded: a worker frees them,
|
// As the game does once they are uploaded: a worker frees them,
|
||||||
// and nothing comes back (`pending` empties).
|
// and nothing comes back (`pending` empties).
|
||||||
@@ -92,25 +95,35 @@ fn columns_loaded_past_the_render_distance_mesh_when_they_come_in_range() {
|
|||||||
};
|
};
|
||||||
// Columns up to three chunks past the render distance load around the
|
// Columns up to three chunks past the render distance load around the
|
||||||
// first position; the second brings some of them within it.
|
// first position; the second brings some of them within it.
|
||||||
settle_at(&mut streaming, (0, 0));
|
settle_at(&mut streaming, &mut world, (0, 0), &mut meshed);
|
||||||
settle_at(&mut streaming, (3, 0));
|
settle_at(&mut streaming, &mut world, (3, 0), &mut meshed);
|
||||||
let (cx, cz): (i32, i32) = (3, 0);
|
let (cx, cz): (i32, i32) = (3, 0);
|
||||||
let mut checked = 0;
|
let all_in_range = |meshed: &HashSet<(i32, i32, i32)>| {
|
||||||
for x in cx - 4..=cx + 4 {
|
let mut checked = 0;
|
||||||
for z in cz - 4..=cz + 4 {
|
for x in cx - 4..=cx + 4 {
|
||||||
if (x - cx).pow(2) + (z - cz).pow(2) > 16 {
|
for z in cz - 4..=cz + 4 {
|
||||||
continue;
|
if (x - cx).pow(2) + (z - cz).pow(2) > 16 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for sy in 0..HEIGHT / 16 {
|
||||||
|
assert!(
|
||||||
|
meshed.contains(&(x, sy, z)),
|
||||||
|
"section {x} {sy} {z} is in range but was never meshed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
checked += 1;
|
||||||
}
|
}
|
||||||
for sy in 0..HEIGHT / 16 {
|
|
||||||
assert!(
|
|
||||||
meshed.contains(&(x, sy, z)),
|
|
||||||
"section {x} {sy} {z} is in range but was never meshed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
checked += 1;
|
|
||||||
}
|
}
|
||||||
}
|
checked
|
||||||
assert_eq!(checked, 49);
|
};
|
||||||
|
assert_eq!(all_in_range(&meshed), 49);
|
||||||
|
|
||||||
|
// `LevelExtractor.allChanged` (F3+A and a changed render distance) drops
|
||||||
|
// every displayed section. Streaming must produce all of them again.
|
||||||
|
meshed.clear();
|
||||||
|
streaming.reload_sections(&mut world);
|
||||||
|
settle_at(&mut streaming, &mut world, (3, 0), &mut meshed);
|
||||||
|
assert_eq!(all_in_range(&meshed), 49);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A one-block cave under the sky: a stone floor at y 62 with a glowstone,
|
/// A one-block cave under the sky: a stone floor at y 62 with a glowstone,
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ impl InterpolatedSampler {
|
|||||||
/// increment once per Y. The X lerps at a level of corners are the top of one
|
/// increment once per Y. The X lerps at a level of corners are the top of one
|
||||||
/// cell and the bottom of the next (the same lerps of the same samples), so they
|
/// cell and the bottom of the next (the same lerps of the same samples), so they
|
||||||
/// are computed once.
|
/// are computed once.
|
||||||
|
// The replacements Clippy suggests require Rust 1.87 and 1.88, above our 1.85 MSRV.
|
||||||
|
#[allow(clippy::manual_is_multiple_of, clippy::chunks_exact_to_as_chunks)]
|
||||||
fn fill_whole_cells(
|
fn fill_whole_cells(
|
||||||
&self,
|
&self,
|
||||||
output: &mut [f32],
|
output: &mut [f32],
|
||||||
|
|||||||
Reference in new issue
Block a user