mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Chunks beside old ones are decorated, their features spilling into the old chunks
A chunk with a final neighbour (a chunk of an older world, loaded from the save) used to be left undecorated: no trees, ores or structure pieces in the strip along the old chunks. The streaming now decorates it with the final neighbour's blocks in the region and writes what the features changed into the final chunk, as vanilla's features write into the chunks around them. The structure starts of an old chunk are not made again from the seed. test_blending_parity.py --phase full compares the decorated chunks around old ones with vanilla. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
5b5503fce0
commit
5d1038557f
11 files changed
+185
-21
No files matched your search
@@ -70,6 +70,23 @@ pub trait ChunkGenerator: Send + Sync + 'static {
|
||||
}
|
||||
/// Decorate `region[4]`; the region is row-major (dz, dx) around it.
|
||||
fn decorate(&self, _worker: &mut Self::Worker, _region: &mut [Proto; 9]) {}
|
||||
/// Whether a chunk next to final ones (loaded from the save, as the
|
||||
/// chunks of a world an older version made) is decorated, its features
|
||||
/// spilling into them (`proto_beside_final`, `final_writes`), as vanilla's
|
||||
/// are; otherwise it is left undecorated.
|
||||
fn decorates_beside_final(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// The proto chunk of a final chunk, for a decoration region to hold: its
|
||||
/// blocks are `snapshot`'s.
|
||||
fn proto_beside_final(&self, _worker: &mut Self::Worker, _snapshot: &Chunk) -> Option<Proto> {
|
||||
None
|
||||
}
|
||||
/// What decoration wrote into the proto chunk of a final chunk: the blocks
|
||||
/// that differ from `snapshot`, as `([x, y, z], state)`.
|
||||
fn final_writes(&self, _snapshot: &Chunk, _proto: &Proto) -> Vec<([i32; 3], u32)> {
|
||||
Vec::new()
|
||||
}
|
||||
fn finish(&self, proto: Proto) -> Chunk;
|
||||
/// Whether `proto` has been decorated (a saved one can come back decorated).
|
||||
fn is_decorated(&self, _proto: &Proto) -> bool {
|
||||
@@ -170,7 +187,10 @@ struct MeshSettings {
|
||||
}
|
||||
enum Job {
|
||||
Generate(i32, i32),
|
||||
Decorate(i32, i32, Box<[Proto; 9]>),
|
||||
/// A region to decorate, with the final chunks in it (region index and
|
||||
/// the chunk as it is): their slots hold nothing until the worker makes
|
||||
/// their proto chunks.
|
||||
Decorate(i32, i32, Box<[Proto; 9]>, Vec<(usize, Chunk)>),
|
||||
Finish(Proto),
|
||||
Light(i32, i32, Box<Region>, u64, bool),
|
||||
Mesh(i32, i32, i32, Box<Region>, u64, MeshSettings),
|
||||
@@ -185,7 +205,9 @@ pub enum Done {
|
||||
Generated(Chunk),
|
||||
/// A proto chunk, and whether it was decorated (loaded so from the save).
|
||||
Proto(i32, i32, Proto, bool),
|
||||
Decorated(i32, i32, Box<[Proto; 9]>),
|
||||
/// A decorated region, the region indices of its final chunks and what
|
||||
/// decoration wrote into them.
|
||||
Decorated(i32, i32, Box<[Proto; 9]>, Vec<usize>, Vec<([i32; 3], u32)>),
|
||||
Lit(i32, i32, Vec<crate::chunk::Light>, u64),
|
||||
Meshed(i32, i32, i32, SectionMesh, u64),
|
||||
Saved(Vec<((i32, i32), Proto)>),
|
||||
@@ -303,6 +325,8 @@ pub struct Streaming {
|
||||
queue_basis: (((i32, i32), bool, Option<Band>), Vec<((i32, i32), i32)>),
|
||||
finishing: FxHashSet<(i32, i32)>,
|
||||
decorates: bool,
|
||||
/// `ChunkGenerator::decorates_beside_final`.
|
||||
saves_beside_final: bool,
|
||||
lighting: FxHashMap<(i32, i32), u64>,
|
||||
meshing: FxHashMap<(i32, i32, i32), u64>,
|
||||
/// Chunks whose light is current.
|
||||
@@ -598,9 +622,25 @@ impl Streaming {
|
||||
Done::Proto(x, z, proto, decorated)
|
||||
}
|
||||
},
|
||||
Job::Decorate(x, z, mut region) => {
|
||||
Job::Decorate(x, z, mut region, finals) => {
|
||||
// The final chunks' proto chunks, made here: a
|
||||
// generated proto chunk holding their blocks.
|
||||
for (i, snapshot) in &finals {
|
||||
if let Some(proto) =
|
||||
generator.proto_beside_final(&mut worker, snapshot)
|
||||
{
|
||||
region[*i] = proto;
|
||||
}
|
||||
}
|
||||
generator.decorate(&mut worker, &mut region);
|
||||
Done::Decorated(x, z, region)
|
||||
let mut writes = Vec::new();
|
||||
for (i, snapshot) in &finals {
|
||||
writes
|
||||
.extend(generator.final_writes(snapshot, ®ion[*i]));
|
||||
region[*i] = Box::new(());
|
||||
}
|
||||
let finals = finals.into_iter().map(|(i, _)| i).collect();
|
||||
Done::Decorated(x, z, region, finals, writes)
|
||||
}
|
||||
Job::Finish(proto) => Done::Generated(generator.finish(proto)),
|
||||
Job::Save(protos) => {
|
||||
@@ -672,6 +712,7 @@ impl Streaming {
|
||||
queue_basis: Default::default(),
|
||||
finishing: Default::default(),
|
||||
decorates: generator.decorates(),
|
||||
saves_beside_final: generator.decorates_beside_final(),
|
||||
lighting: Default::default(),
|
||||
meshing: Default::default(),
|
||||
lit: Default::default(),
|
||||
@@ -818,16 +859,32 @@ impl Streaming {
|
||||
let mut queue = self.shared.queue.lock().unwrap();
|
||||
rank_again(&mut queue, |x, z| self.distance(x, z));
|
||||
}
|
||||
fn receive_decorated(&mut self, x: i32, z: i32, region: [Proto; 9]) {
|
||||
fn receive_decorated(
|
||||
&mut self,
|
||||
x: i32,
|
||||
z: i32,
|
||||
region: [Proto; 9],
|
||||
finals: Vec<usize>,
|
||||
writes: Vec<([i32; 3], u32)>,
|
||||
world: &mut World,
|
||||
) {
|
||||
self.generation_scan.exhausted = false;
|
||||
self.decorating -= 1;
|
||||
self.decorated.insert((x, z));
|
||||
for (i, proto) in region.into_iter().enumerate() {
|
||||
// A final chunk keeps its own chunk: only what decoration wrote
|
||||
// into it goes there.
|
||||
if finals.contains(&i) {
|
||||
continue;
|
||||
}
|
||||
let key = (x + (i % 3) as i32 - 1, z + (i / 3) as i32 - 1);
|
||||
self.light_views.remove(&key);
|
||||
self.protos.insert(key, Some(proto));
|
||||
self.written.insert(key);
|
||||
}
|
||||
for ([bx, by, bz], state) in writes {
|
||||
let _ = world.set(bx, by, bz, horizon_world::registry::BlockStateId(state));
|
||||
}
|
||||
}
|
||||
/// Saved proto chunks come back; those now out of range are dropped by
|
||||
/// the next `update`.
|
||||
@@ -1048,7 +1105,9 @@ impl Streaming {
|
||||
}
|
||||
}
|
||||
}
|
||||
Done::Decorated(x, z, region) => self.receive_decorated(x, z, *region),
|
||||
Done::Decorated(x, z, region, finals, writes) => {
|
||||
self.receive_decorated(x, z, *region, finals, writes, world)
|
||||
}
|
||||
Done::Generated(chunk) => self.receive_generated(chunk, world),
|
||||
Done::Saved(protos) => self.receive_saved(protos),
|
||||
Done::Lit(x, z, sections, seq) => {
|
||||
@@ -1283,22 +1342,33 @@ impl Streaming {
|
||||
}
|
||||
let keys: Vec<(i32, i32)> =
|
||||
(0..9).map(|i| (x + (i % 3) - 1, z + (i / 3) - 1)).collect();
|
||||
// A final neighbour cannot receive writes: skip decoration. Only
|
||||
// worlds saved before proto chunks were saved get here (their
|
||||
// A final neighbour cannot be generated again: a generator that
|
||||
// can decorates beside it (a chunk of an older world), writing
|
||||
// into it as vanilla's features do; the others skip decoration
|
||||
// (worlds saved before proto chunks were saved get here: their
|
||||
// final chunks' neighbours are generated again).
|
||||
if keys.iter().any(|k| world.chunks.contains_key(k)) {
|
||||
let finals: Vec<(usize, Chunk)> = keys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, k)| Some((i, (**world.chunks.get(k)?).clone())))
|
||||
.collect();
|
||||
if !finals.is_empty() && !self.saves_beside_final {
|
||||
self.decorated.insert((x, z));
|
||||
continue;
|
||||
}
|
||||
if !keys
|
||||
.iter()
|
||||
.all(|k| matches!(self.protos.get(k), Some(Some(_))))
|
||||
{
|
||||
if !keys.iter().enumerate().all(|(i, k)| {
|
||||
finals.iter().any(|(f, _)| *f == i)
|
||||
|| matches!(self.protos.get(k), Some(Some(_)))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let region: Vec<Proto> = keys
|
||||
.iter()
|
||||
.map(|k| {
|
||||
.enumerate()
|
||||
.map(|(i, k)| {
|
||||
if finals.iter().any(|(f, _)| *f == i) {
|
||||
return Box::new(()) as Proto;
|
||||
}
|
||||
self.light_views.remove(k);
|
||||
self.protos
|
||||
.insert(*k, None)
|
||||
@@ -1311,7 +1381,7 @@ impl Streaming {
|
||||
Err(_) => continue,
|
||||
};
|
||||
self.decorating += 1;
|
||||
self.submit(Some((x, z)), 1, Job::Decorate(x, z, region));
|
||||
self.submit(Some((x, z)), 1, Job::Decorate(x, z, region, finals));
|
||||
}
|
||||
}
|
||||
self.schedule_generation(world, workers);
|
||||
@@ -1553,7 +1623,9 @@ impl Streaming {
|
||||
while self.decorating > 0 || !self.finishing.is_empty() || self.saving > 0 {
|
||||
let left = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
match self.shared.done.recv_timeout(left) {
|
||||
Some(Done::Decorated(x, z, region)) => self.receive_decorated(x, z, *region),
|
||||
Some(Done::Decorated(x, z, region, finals, writes)) => {
|
||||
self.receive_decorated(x, z, *region, finals, writes, world)
|
||||
}
|
||||
Some(Done::Generated(chunk)) => self.receive_generated(chunk, world),
|
||||
Some(Done::Saved(protos)) => self.receive_saved(protos),
|
||||
Some(other) => self.deferred.push(other),
|
||||
@@ -1581,7 +1653,9 @@ impl Streaming {
|
||||
let started = std::time::Instant::now();
|
||||
while let Some(done) = self.shared.done.try_recv() {
|
||||
match done {
|
||||
Done::Decorated(x, z, region) => self.receive_decorated(x, z, *region),
|
||||
Done::Decorated(x, z, region, finals, writes) => {
|
||||
self.receive_decorated(x, z, *region, finals, writes, world)
|
||||
}
|
||||
Done::Generated(chunk) => self.receive_generated(chunk, world),
|
||||
Done::Saved(protos) => self.receive_saved(protos),
|
||||
other => self.deferred.push(other),
|
||||
|
||||
@@ -4165,6 +4165,23 @@ impl<G: ChunkGenerator> ChunkGenerator for Persistent<G> {
|
||||
fn decorates(&self) -> bool {
|
||||
self.inner.decorates()
|
||||
}
|
||||
fn decorates_beside_final(&self) -> bool {
|
||||
self.inner.decorates_beside_final()
|
||||
}
|
||||
fn proto_beside_final(
|
||||
&self,
|
||||
worker: &mut G::Worker,
|
||||
snapshot: &Chunk,
|
||||
) -> Option<crate::streaming::Proto> {
|
||||
self.inner.proto_beside_final(worker, snapshot)
|
||||
}
|
||||
fn final_writes(
|
||||
&self,
|
||||
snapshot: &Chunk,
|
||||
proto: &crate::streaming::Proto,
|
||||
) -> Vec<([i32; 3], u32)> {
|
||||
self.inner.final_writes(snapshot, proto)
|
||||
}
|
||||
fn decorate(&self, worker: &mut G::Worker, region: &mut [crate::streaming::Proto; 9]) {
|
||||
self.inner.decorate(worker, region);
|
||||
}
|
||||
|
||||
@@ -572,6 +572,47 @@ impl ChunkGenerator for VanillaTerrain {
|
||||
fn decorates(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn decorates_beside_final(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// A final chunk's proto chunk: terrain generated again with the final
|
||||
/// chunk's blocks over it, so that features read and write it as they do
|
||||
/// the chunks around it (`ChunkAccess` of a full chunk in the region).
|
||||
fn proto_beside_final(&self, worker: &mut GeneratorWorker, snapshot: &Chunk) -> Option<Proto> {
|
||||
let mut proto = self.proto(worker, snapshot.x, snapshot.z);
|
||||
self.restore_proto(
|
||||
&mut proto,
|
||||
SavedProto {
|
||||
column: snapshot.clone(),
|
||||
decorated: true,
|
||||
block_entities: Vec::new(),
|
||||
entities: Vec::new(),
|
||||
},
|
||||
);
|
||||
Some(proto)
|
||||
}
|
||||
fn final_writes(&self, snapshot: &Chunk, proto: &Proto) -> Vec<([i32; 3], u32)> {
|
||||
let Some(VanillaProto::Ready(chunk)) = proto.downcast_ref::<VanillaProto>() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (bx, bz) = (snapshot.x * 16, snapshot.z * 16);
|
||||
let (lo, hi) = (
|
||||
chunk.terrain.min_y().max(snapshot.min_y()),
|
||||
(chunk.terrain.min_y() + chunk.terrain.height()).min(snapshot.max_y()),
|
||||
);
|
||||
let mut writes = Vec::new();
|
||||
for y in lo..hi {
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
let state = chunk.block(bx + x, y, bz + z).0;
|
||||
if state != snapshot.get(x, y, z) {
|
||||
writes.push(([bx + x, y, bz + z], state));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
writes
|
||||
}
|
||||
fn decorate(&self, worker: &mut GeneratorWorker, region: &mut [Proto; 9]) {
|
||||
let taken: Vec<Box<VanillaProto>> = region
|
||||
.iter_mut()
|
||||
|
||||
@@ -389,6 +389,12 @@ pub fn main() -> Result<(), Error> {
|
||||
.collect(),
|
||||
};
|
||||
for (x, z) in order {
|
||||
// An old chunk is not decorated, only written into.
|
||||
if old_rect.is_some_and(|[ox0, oz0, ox1, oz1]| {
|
||||
(ox0..=ox1).contains(&x) && (oz0..=oz1).contains(&z)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let mut region = Vec::with_capacity(9);
|
||||
for dz in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
|
||||
@@ -1793,6 +1793,12 @@ impl<R: RandomSource> super::structure::start::StartContext for StructureQueries
|
||||
where
|
||||
R::Positional: Clone + Send + Sync,
|
||||
{
|
||||
fn is_old_chunk(&self, chunk: ChunkPos) -> bool {
|
||||
self.pipeline
|
||||
.old_world
|
||||
.get()
|
||||
.is_some_and(|world| world.is_old_chunk_around(chunk, 0))
|
||||
}
|
||||
fn seed(&self) -> i64 {
|
||||
self.pipeline.seed
|
||||
}
|
||||
|
||||
@@ -291,6 +291,9 @@ impl StructureState {
|
||||
if let Some(found) = self.starts.get((chunk.x, chunk.z)) {
|
||||
return found;
|
||||
}
|
||||
if context.is_old_chunk(chunk) {
|
||||
return Starts::default();
|
||||
}
|
||||
// Ring searches enlist other workers and wait for them: never hold a
|
||||
// chunk-work lock while helping, even when this chunk has no starts.
|
||||
let rings = self.rings_for(context, chunk);
|
||||
|
||||
@@ -813,6 +813,12 @@ impl NoiseColumn {
|
||||
|
||||
/// Biome and height queries for one structure generation attempt.
|
||||
pub trait StartContext: JigsawContext {
|
||||
/// A chunk an older version generated: its structure starts are the ones
|
||||
/// it saved, which are in its blocks, not the ones this generator would
|
||||
/// make there.
|
||||
fn is_old_chunk(&self, _chunk: ChunkPos) -> bool {
|
||||
false
|
||||
}
|
||||
fn seed(&self) -> i64;
|
||||
fn sea_level(&self) -> i32;
|
||||
/// ChunkGenerator.getBaseColumn as heightmap opacity, indexed from min_y.
|
||||
|
||||
@@ -466,6 +466,7 @@ public final class VanillaChunkGenerationOracle {
|
||||
for (int[] at : order) {
|
||||
if (phase.equals("terrain")) break;
|
||||
int x = at[0], z = at[1];
|
||||
if (oldChunks.contains(ChunkPos.pack(x, z))) continue; // an old chunk is not decorated, only written into
|
||||
var chunk = chunkAt.apply(x, z);
|
||||
var region = regionFor.apply(ChunkStatus.FEATURES, chunk);
|
||||
generator.applyBiomeDecoration(region, chunk, OracleLevel.structures.forWorldGenRegion(region));
|
||||
|
||||
@@ -89,6 +89,14 @@ def main():
|
||||
g.raw('open', 'Dst')
|
||||
g('wait_ready', seconds=400)
|
||||
g('command', command='gamemode spectator')
|
||||
# The chunk strip next to the old chunks is decorated like any other: it has trees.
|
||||
logs = 0
|
||||
for z0 in range(-32, 32, 16): # a request holds at most 32768 blocks
|
||||
counts = g.raw('blocks', 0, 60, z0, 15, 187, z0 + 15).get('counts', {})
|
||||
logs += sum(n for block, n in counts.items() if block.endswith('_log'))
|
||||
print(label, 'logs in the new chunk strip at the border:', logs)
|
||||
if label == 'blended':
|
||||
assert logs > 0, 'no tree grew in the chunk strip beside the old chunks'
|
||||
for shot, x, y, z, yaw, pitch in SHOTS:
|
||||
g('command', command=f'tp @s {x} {y} {z} {yaw} {pitch}')
|
||||
g.wait(500)
|
||||
|
||||
@@ -93,8 +93,8 @@ Blender.blendDensity port crates/horizon-world/src/worldgen/blending.rs::Blender
|
||||
Blender.getBlendingDataValue inline crates/horizon-world/src/worldgen/blending.rs::Blender::blending_value
|
||||
Blender.getBiomeResolver inline crates/horizon-world/src/worldgen/blending.rs::Blender::blend_biome the generator asks `blend_biome` before its own biome source
|
||||
Blender.blendBiome port crates/horizon-world/src/worldgen/blending.rs::Blender::blend_biome
|
||||
Blender.generateBorderTicks missing the leaves and fluids of the old/new chunk border are not marked for post-processing yet
|
||||
Blender.generateBorderTick missing part of generateBorderTicks
|
||||
Blender.generateBorderTicks skip:other does something only for a chunk that has blending data and is being generated again (retrogen); Horizon never regenerates an old chunk
|
||||
Blender.generateBorderTick skip:other does something only for a chunk that has blending data and is being generated again (retrogen); Horizon never regenerates an old chunk
|
||||
Blender.createAroundOldChunksCarvingMaskFilter inline crates/horizon-world/src/worldgen/blending.rs::OldChunkDistance::around the carving filter is `Blender::carving_filter`
|
||||
Blender.makeOldChunkDistanceGetter inline crates/horizon-world/src/worldgen/blending.rs::OldChunkDistance::distance
|
||||
Blender.makeOffsetOldChunkDistanceGetter inline crates/horizon-world/src/worldgen/blending.rs::OldChunkDistance::around
|
||||
|
||||
|
@@ -35,9 +35,11 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--filter', help='regex over the case labels')
|
||||
parser.add_argument('--limit', type=int, default=6, help='block differences shown per chunk')
|
||||
parser.add_argument('--phase', choices=['terrain', 'full'], default='terrain',
|
||||
help='full: with the features, trees and the rest spilling into the old chunks')
|
||||
args = parser.parse_args()
|
||||
import re
|
||||
os.environ['HORIZON_PHASE'] = 'terrain'
|
||||
os.environ['HORIZON_PHASE'] = args.phase
|
||||
oracle = base.Oracle()
|
||||
binary = base.rust_binary()
|
||||
names = base.state_names()
|
||||
@@ -48,7 +50,7 @@ def main():
|
||||
if args.filter and not re.search(args.filter, label):
|
||||
continue
|
||||
os.environ['HORIZON_OLD_CHUNKS'] = f'{old_seed}:{old}'
|
||||
tag = f'blending-{seed}-{old_seed}-{old.replace(",", "_")}'
|
||||
tag = f'blending-{args.phase}-{seed}-{old_seed}-{old.replace(",", "_")}'
|
||||
vanilla_path, rust_path = base.build_dir() / f'vanilla-{tag}.txt', base.build_dir() / f'rust-{tag}.txt'
|
||||
with ThreadPoolExecutor(2) as pool:
|
||||
rust_run = pool.submit(base.run_rust, binary, rust_path, seed, 'overworld', [rect])
|
||||
|
||||
Reference in new issue
Block a user