Merge pull request #11645 from vegetableovosh/fix/11214-chunk-refresh

Fix/11214 chunk refresh
This commit is contained in:
rdbt authored and GitHub committed 2026-11-07 12:33:47 +00:00
commit 7d8bb9e69b
4 files changed
+108 -42

No files matched your search

+51 -5
View File
@@ -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)));
}
}
+24 -19
View File
@@ -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).
+31 -18
View File
@@ -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],