Compare commits

...
Author SHA1 Message Date
Ludovic Marie 1e38d2f298 Merge branch 'perf/idle-generator-scratch' into HEAD 2026-10-30 02:14:10 +01:00
Ludovic Marie d7e8c1cc92 Merge remote-tracking branch 'origin/perf/shared-advancement-tree' into bench/sn-w4-ladder 2026-10-30 02:10:58 +01:00
Ludovic Marie f5ea7f154d Merge remote-tracking branch 'origin/perf/shared-feature-block-tags' into bench/sn-w4-ladder 2026-10-30 02:10:58 +01:00
Ludovic Marie b0ef41c9c2 Merge remote-tracking branch 'origin/perf/shared-face-visibility' into bench/sn-w4-ladder 2026-10-30 02:10:57 +01:00
Ludovic Marie 9c642a280b Merge remote-tracking branch 'origin/perf/dedicated-no-sprite-pixels' into bench/sn-w4-ladder 2026-10-30 02:10:57 +01:00
Ludovic Marie 6095d4880c Merge remote-tracking branch 'origin/perf/dedicated-font-atlas' into bench/sn-w4-ladder 2026-10-30 02:10:57 +01:00
Ludovic MarieandClaude Opus 5.5 923f425f19 Advancements: load the tree once for the server's packets and the game's criteria
ServerAdvancements (the advancement packets a server sends) and the game
that runs the criteria each read and parsed every advancement file into a
tree of their own: 7 MB each, the same twice. Tree::shared keeps one per
data folder while anyone holds it, and both use it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:20:01 +01:00
Ludovic MarieandClaude Opus 5.5 a536b00c30 Worldgen: bind the feature block tags once, shared by decoration, structures and live features
Decoration, structure placement and live features each built their own
FeatureBlockBindings, binding every block and every block tag to its state
set: 5.5 MB each, the same three times. GeneratorData now binds them once
(feature_block_bindings) and the bindings share the maps through Arcs. Each
decorated region also cloned the whole tag map; it now shares it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:48:07 +01:00
Ludovic MarieandClaude Opus 5.5 23c5335910 Block entity rendering: reuse the meshes' face visibility instead of loading it again
BlockEntityRenderer loaded occlusion.nbt and the render definitions a second
time and built its own FaceVisibility, whose shapes then computed their faces
again: 13.6 MB of face shapes, twice. It now takes a clone of the mesh
tables' visibility, which shares the shapes and their cached faces.
Probes and tests that load the renderer alone still load their own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:18:41 +01:00
Ludovic MarieandClaude Opus 5.5 998f8f4755 Chunk workers: give freed memory back once every worker is idle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 00:14:28 +01:00
Ludovic MarieandClaude Opus 5.5 bd2774b133 Chunk workers: free the generator's scratch buffers after 5 s with nothing to do
Each streaming worker keeps its generator's density scratch buffers and
lookup caches between jobs, megabytes per worker, for as long as the world
is open: about 51 MB on the Mac mini's dedicated server with nobody on. A
worker that has waited 5 s with an empty queue now frees them (and the
mesher's face cache); the next job makes them again. Results do not change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:44:29 +01:00
Ludovic MarieandClaude Opus 5.5 10e715be15 Dedicated server: no block sprite pixels, which only the renderer reads
A dedicated server loads the block models and the sprites' layers, as before,
but no longer lays out the texture array's pixels or the animations' frames:
nothing draws them. The integrated server shares the window's resources and
is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:36:59 +01:00
Ludovic MarieandClaude Opus 5.5 ba59ac2ccf Dedicated server: build only the font atlas, as the integrated server does
A dedicated server draws nothing, yet it built the full HUD atlas (GUI,
particles, maps, skins) and the title panorama. The integrated server's
game already holds only the font, which books and chat measure with, and a
one-pixel panorama; both now come from UiResources::server, and the
dedicated server's synchronous load uses it too.

Idle, fresh world, 3 interleaved runs: RSS 836/836/841 -> 771/792/765 MB,
ready 5.05 -> 4.32 s (median).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:06:19 +01:00
18 changed files with 243 additions and 52 deletions

No files matched your search

+13 -1
View File
@@ -192,9 +192,17 @@ impl Assets {
pub fn reload(&self, root: &Path) -> Result<RenderAssets> {
RenderAssets::load(root, self)
}
/// [`Assets::reload`] for a server, which draws nothing: the models and
/// the sprites' layers, without the textures' pixels.
pub fn reload_for_server(&self, root: &Path) -> Result<RenderAssets> {
RenderAssets::load_with(root, self, false)
}
}
impl RenderAssets {
pub fn load(root: &Path, assets: &Assets) -> Result<Self> {
Self::load_with(root, assets, true)
}
fn load_with(root: &Path, assets: &Assets, pixels: bool) -> Result<Self> {
let registry = assets.registry;
let mut sprites = crate::sprites::Sprites::with_disk_cache(root);
for name in MATERIALS {
@@ -218,7 +226,11 @@ impl RenderAssets {
sprites.insert(root, &format!("minecraft:block/destroy_stage_{stage}"))?;
}
sprites.save_disk_cache();
sprites.finish();
if pixels {
sprites.finish();
} else {
sprites.finish_without_pixels();
}
// `finish` grows the buffer by doubling: keep only the bytes in use.
sprites.pixels.shrink_to_fit();
Ok(Self {
@@ -865,13 +865,23 @@ const VIEW_DISTANCE: f64 = 64.0;
impl BlockEntityRenderer {
pub fn load(root: &Path, registry: &BlockRegistry) -> crate::Result<Self> {
let visibility = crate::models::load_visibility(root, registry)?;
Self::load_with_visibility(root, registry, visibility)
}
/// [`BlockEntityRenderer::load`] over the face visibility the meshes use
/// (`MeshTables::visibility`): a clone shares its shapes and their faces.
pub fn load_with_visibility(
root: &Path,
registry: &BlockRegistry,
visibility: FaceVisibility,
) -> crate::Result<Self> {
let all: Value = serde_json::from_slice(&std::fs::read(root.join("entity-models.json"))?)?;
let models = MODELS
.iter()
.filter_map(|(key, layer)| all.get(*layer).map(|v| (*key, ModelPart::parse(v))))
.collect();
let mut renderer = Self::new(models, registry);
renderer.visibility = Some(Mutex::new(crate::models::load_visibility(root, registry)?));
renderer.visibility = Some(Mutex::new(visibility));
// Sherd items: provides_pottery_pattern -> decorated_pot_pattern asset id.
if let Ok(bytes) = std::fs::read(root.join("items.json")) {
let items: Value = serde_json::from_slice(&bytes)?;
@@ -159,6 +159,14 @@ impl Sprites {
/// up to a power of two and limited to [`MAX_ARRAY_BYTES`]. Smaller
/// textures are scaled up by whole pixels, so UVs stay per layer.
pub fn finish(&mut self) {
self.lay_out(true);
}
/// [`Sprites::finish`] for a server, which draws nothing: the layers'
/// size, but neither their pixels nor the animations' frames.
pub fn finish_without_pixels(&mut self) {
self.lay_out(false);
}
fn lay_out(&mut self, pixels: bool) {
let layers = self.native.len().max(1) as u64;
let largest = self.native.iter().map(|n| n.0).max().unwrap_or(SPRITE_SIZE);
self.minimum_size = self.native.iter().map(|n| n.0).min().unwrap_or(SPRITE_SIZE);
@@ -166,6 +174,12 @@ impl Sprites {
while size > SPRITE_SIZE && layers * u64::from(size * size) * 4 > MAX_ARRAY_BYTES {
size /= 2;
}
if !pixels {
self.pixels = Vec::new();
self.animations = Vec::new();
self.size = size;
return;
}
let mut resized = Vec::with_capacity(self.native.len());
self.pixels.clear();
for (from, frames, initial) in &self.native {
+42 -2
View File
@@ -16,11 +16,15 @@ use std::{
collections::{BinaryHeap, HashSet, VecDeque},
sync::{
Arc, Condvar, Mutex, MutexGuard, RwLock, TryLockError,
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
},
thread,
};
/// How long a worker waits with nothing to do before it gives back its
/// generator's scratch buffers ([`ChunkGenerator::idle`]).
pub const IDLE_RELEASE: std::time::Duration = std::time::Duration::from_secs(5);
/// A chunk under generation, opaque to the streamer.
pub type Proto = Box<dyn std::any::Any + Send>;
/// Chunk generation in vanilla's stages: a neighbour-independent proto chunk
@@ -57,6 +61,9 @@ pub trait ChunkGenerator: Send + Sync + 'static {
/// What a loaded chunk needs from the generator before the game uses it,
/// on the worker that loaded it.
fn prepare_loaded(&self, _worker: &mut Self::Worker, _chunk: &Chunk) {}
/// The worker has had nothing to do for [`IDLE_RELEASE`]: give back what
/// it keeps between jobs only to go faster.
fn idle(&self, _worker: &mut Self::Worker) {}
fn proto(&self, worker: &mut Self::Worker, x: i32, z: i32) -> Proto;
fn decorates(&self) -> bool {
false
@@ -220,6 +227,9 @@ struct Shared {
/// packs reload; each mesh job takes the current one.
context: RwLock<Arc<WorkerContext>>,
done: Mailbox,
/// Workers that have freed their buffers ([`IDLE_RELEASE`]) and taken no
/// job since: the last of them gives the freed memory back to the OS.
released: AtomicUsize,
}
/// Finished jobs, from the workers to the game thread, in the order they
/// finished. Not an `mpsc` channel: its receiver waits for a sender it finds
@@ -513,7 +523,9 @@ impl Streaming {
stop: AtomicBool::new(false),
context: RwLock::new(context.clone()),
done: Mailbox::default(),
released: AtomicUsize::new(0),
});
let workers = threads.max(1);
let light_tables = context.light.clone();
let saves: Arc<dyn Saves> = generator.clone();
let threads = (0..threads.max(1))
@@ -534,14 +546,42 @@ impl Streaming {
loop {
let job = {
let mut queue = shared.queue.lock().unwrap();
let mut released = false;
loop {
if shared.stop.load(Ordering::Relaxed) {
return;
}
if let Some(job) = queue.pop() {
if released {
shared.released.fetch_sub(1, Ordering::Relaxed);
}
break job.job;
}
queue = shared.ready.wait(queue).unwrap();
if released {
queue = shared.ready.wait(queue).unwrap();
continue;
}
let (woken, wait) =
shared.ready.wait_timeout(queue, IDLE_RELEASE).unwrap();
queue = woken;
if wait.timed_out() && queue.is_empty() {
// A server nobody is on, a client standing still:
// free the generator's buffers (and the mesher's
// face cache) until the next job needs them.
drop(queue);
generator.idle(&mut worker);
faces = None;
released = true;
// The allocator keeps freed pages until memory
// runs short: once every worker has freed its own,
// hand them back.
if shared.released.fetch_add(1, Ordering::Relaxed) + 1
== workers
{
horizon_threads::release_free_memory();
}
queue = shared.queue.lock().unwrap();
}
}
};
let done = match job {
+21 -1
View File
@@ -5,7 +5,8 @@
use serde_json::Value;
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
path::Path,
path::{Path, PathBuf},
sync::{Arc, Mutex, Weak},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -66,6 +67,25 @@ fn frame(v: &Value) -> Frame {
}
}
impl Tree {
/// [`Tree::load`], shared while anyone holds it: a server's advancement
/// packets (`ServerAdvancements`) and its game's criteria read the same
/// files, and each kept its own copy (7 MB).
pub fn shared(data: &Path) -> Arc<Tree> {
static TREES: Mutex<Vec<(PathBuf, Weak<Tree>)>> = Mutex::new(Vec::new());
// Held while loading: a second reader waits for the first one's tree.
let mut trees = TREES.lock().unwrap_or_else(|e| e.into_inner());
trees.retain(|(_, tree)| tree.strong_count() > 0);
if let Some(tree) = trees
.iter()
.find(|(path, _)| path == data)
.and_then(|(_, tree)| tree.upgrade())
{
return tree;
}
let tree = Arc::new(Tree::load(data));
trees.push((data.to_path_buf(), Arc::downgrade(&tree)));
tree
}
/// Loads `<data>/<namespace>/advancement/**/*.json`.
pub fn load(data: &Path) -> Tree {
let mut tree = Tree::default();
+46 -8
View File
@@ -1336,6 +1336,17 @@ impl UiResources {
panorama: Arc::new(horizon_client::gfx::panorama::Faces::load(root)),
})
}
/// What a server's game holds, since it draws nothing: the font, which books
/// and chat measure with, and a one-pixel panorama.
pub fn server(root: &std::path::Path) -> Result<Self> {
Ok(Self {
atlas: HudAtlas::fonts(root)?,
panorama: Arc::new(horizon_client::gfx::panorama::Faces {
size: 1,
pixels: [0u8; 4].repeat(6),
}),
})
}
}
/// What a resource pack reload replaces (`Minecraft.reloadResourcePacks`):
/// block models and sprites with their meshing tables, item models,
@@ -15010,6 +15021,7 @@ fn load_resources(
settings: &Settings,
started: Instant,
menu: bool,
headless: bool,
progress: &Progress,
publish: &mut dyn FnMut(UiResources, &std::path::Path),
) -> Result<Loaded> {
@@ -15021,7 +15033,16 @@ fn load_resources(
let mut loaded_gui = None;
let mut ui_ready = false;
let mut pack_error = None;
let ui = match UiResources::load(&root, vanilla_root) {
// A dedicated server builds only the font (UiResources::server), as the
// integrated server's game does: the full atlas and panorama are 144 MB.
let load_ui = |root: &std::path::Path| {
if headless {
UiResources::server(root)
} else {
UiResources::load(root, vanilla_root)
}
};
let ui = match load_ui(&root) {
Ok(ui) => Some(ui),
// Minecraft.rollbackResourcePacks: the packs' textures don't fit, start
// with the game's own resources.
@@ -15047,8 +15068,16 @@ fn load_resources(
ui_ready = true;
}
progress.set(0);
// Nor the block sprites' pixels, which only the renderer reads (32 MB).
let reload = |assets: &Assets, root: &std::path::Path| {
if headless {
assets.reload_for_server(root)
} else {
assets.reload(root)
}
};
let (assets, render) = match Assets::load(&root).and_then(|assets| {
let render = assets.reload(&root)?;
let render = reload(&assets, &root)?;
Ok((assets, render))
}) {
Ok(bundle) => bundle,
@@ -15059,7 +15088,7 @@ fn load_resources(
pack_error = Some(e.to_string());
root = vanilla_root.to_path_buf();
let assets = Assets::load(vanilla_root)?;
let render = assets.reload(vanilla_root)?;
let render = reload(&assets, vanilla_root)?;
(assets, render)
}
Err(e) => {
@@ -15073,7 +15102,7 @@ fn load_resources(
let render = Arc::new(render);
// The menu bundle must come from the same root as the world bundle.
if !ui_ready || root != requested_root {
let ui = UiResources::load(&root, vanilla_root)?;
let ui = load_ui(&root)?;
if menu {
horizon_client::telemetry::mark_ui_ready();
log::info!(
@@ -15170,6 +15199,9 @@ fn load_resources(
prepared_at.elapsed().as_millis()
);
}
// The block entity renderer's face visibility: the meshes' own, whose
// shapes already hold their faces, not a second load of the same tables.
let visibility = pack.context.tables.visibility.clone();
let rest_loader = if menu {
let resources = resources.clone();
let root = root.clone();
@@ -15184,6 +15216,7 @@ fn load_resources(
&assets,
&behaviour_json,
&gameplay_projection,
visibility,
&progress,
started,
)
@@ -15211,6 +15244,7 @@ fn load_resources(
&assets,
&behaviour_json,
&gameplay_projection,
visibility,
progress,
started,
)?;
@@ -15238,6 +15272,7 @@ fn load_world_rest(
assets: &Assets,
behaviour_json: &serde_json::Value,
gameplay_projection: &[horizon_client::blocks::GameplayProjection],
visibility: horizon_world::block_render::FaceVisibility,
progress: &Progress,
started: Instant,
) -> Result<WorldRest> {
@@ -15273,10 +15308,12 @@ fn load_world_rest(
recipes,
transformers,
providers: Providers::load(root),
block_entity_render: horizon_client::block_entity_render::BlockEntityRenderer::load(
root,
registry_owner.registry,
)?,
block_entity_render:
horizon_client::block_entity_render::BlockEntityRenderer::load_with_visibility(
root,
registry_owner.registry,
visibility,
)?,
animate,
fog,
music: horizon_client::music::MusicTables::load(root),
@@ -16143,6 +16180,7 @@ pub fn run() -> Result<()> {
&early_settings,
started,
false,
options.dedicated.is_some(),
&Progress::new(),
&mut |_, _| {},
)?;
@@ -115,7 +115,7 @@ pub(super) struct AdvState {
pub(super) announced: Vec<Text>,
/// The tree being read on another thread since the world opened
/// (`prefetch_advancement_tree`), taken by the first `advancement_tree`.
pub(super) pending_tree: Option<std::thread::JoinHandle<Tree>>,
pub(super) pending_tree: Option<std::thread::JoinHandle<Arc<Tree>>>,
/// `inventory_changed`'s criteria by item, built for `tree`.
inventory_index: Option<(Arc<Tree>, Arc<adv::InventoryIndex>)>,
}
@@ -261,9 +261,9 @@ impl Game<'_> {
if self.adv.tree.list.is_empty() {
let tree = match self.adv.pending_tree.take().map(|h| h.join()) {
Some(Ok(tree)) => tree,
_ => Tree::load(&self.options.resources.join("data")),
_ => Tree::shared(&self.options.resources.join("data")),
};
self.adv.tree = Arc::new(tree);
self.adv.tree = tree;
// With the tree, while the world loads: built on the first
// inventory change it cost that tick ~10 ms.
self.inventory_index();
@@ -299,7 +299,7 @@ impl Game<'_> {
let data = self.options.resources.join("data");
self.adv.pending_tree = std::thread::Builder::new()
.name("horizon-advancements".into())
.spawn(move || Tree::load(&data))
.spawn(move || Tree::shared(&data))
.ok();
}
pub(super) fn load_advancements(&mut self) {
+1 -10
View File
@@ -19,7 +19,6 @@ use super::{
use horizon_client::{
Result,
audio::Audio,
hud::HudAtlas,
lan::{Lan, MAX_PLAYERS, NetConfig, hub::HubConfig},
settings::Settings,
title::WorldOptions,
@@ -850,15 +849,7 @@ fn server_game(
options: Options,
settings: Settings,
) -> Result<Game<'_>> {
// It draws nothing: only the font, which books and chat measure with.
let atlas = HudAtlas::fonts(&options.resources)?;
let gui = UiResources {
atlas,
panorama: Arc::new(horizon_client::gfx::panorama::Faces {
size: 1,
pixels: [0u8; 4].repeat(6),
}),
};
let gui = UiResources::server(&options.resources)?;
let audio = Audio::silent(&options.resources);
Game::new(res, pack, gui, options, settings, audio, None)
}
@@ -251,6 +251,7 @@ pub(super) fn spawn_loader(
&settings,
started,
true,
false,
&progress,
&mut |gui, root| {
let _ = sender.send(LoaderMsg::Ui(gui, root.to_path_buf()));
@@ -16,7 +16,7 @@ use std::{
};
pub struct ServerAdvancements {
tree: Tree,
tree: std::sync::Arc<Tree>,
kids: Vec<Vec<usize>>,
layout: Vec<Option<(i32, f32)>>,
/// Recipe files by id.
@@ -35,7 +35,7 @@ pub struct FirstTick {
impl ServerAdvancements {
pub fn load(resources: &Path) -> Self {
let tree = Tree::load(&resources.join("data"));
let tree = Tree::shared(&resources.join("data"));
let kids = tree.children();
let layout = tree.layout();
let mut recipes = BTreeMap::new();
+3
View File
@@ -4112,6 +4112,9 @@ impl<G: ChunkGenerator> ChunkGenerator for Persistent<G> {
fn prepare_loaded(&self, worker: &mut G::Worker, chunk: &Chunk) {
self.inner.prepare_loaded(worker, chunk);
}
fn idle(&self, worker: &mut G::Worker) {
self.inner.idle(worker);
}
fn proto(&self, worker: &mut G::Worker, x: i32, z: i32) -> crate::streaming::Proto {
let mut proto = self.inner.proto(worker, x, z);
if let Some(saved) = self
+3
View File
@@ -496,6 +496,9 @@ impl ChunkGenerator for VanillaTerrain {
fn lod_sampler(self: Arc<Self>) -> Box<dyn LodSampler> {
Box::new(VanillaLodSampler::new(self))
}
fn idle(&self, worker: &mut GeneratorWorker) {
worker.release();
}
fn lod_surface(
&self,
worker: &mut GeneratorWorker,
@@ -14,6 +14,11 @@ use std::{
sync::Arc,
};
/// Block states by block name and by tag name ([`FeatureBlockBindings::bind_tags`]).
pub type BoundBlockTags = (
Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
);
pub struct FeatureBlockBindings<'r> {
registry: &'r BlockRegistry,
pub(crate) simple_blocks: Option<(
@@ -27,8 +32,8 @@ pub struct FeatureBlockBindings<'r> {
biomes: Option<Arc<super::biome_predicate::BiomePredicateBindings>>,
pub(crate) fluids: super::fluid_predicate::FluidPredicateBindings,
pub(crate) codec: BlockStateJsonCodec<'r>,
pub(crate) blocks: BTreeMap<String, Arc<BTreeSet<BlockStateId>>>,
pub(crate) tags: BTreeMap<String, Arc<BTreeSet<BlockStateId>>>,
pub(crate) blocks: Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
pub(crate) tags: Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
pub(crate) air: [BlockStateId; 3],
tree_blocks: std::sync::OnceLock<Arc<super::tree_blocks::TreeBlocks>>,
pub(crate) structures: Option<Arc<super::structure::StructureResources>>,
@@ -126,8 +131,15 @@ impl<'r> FeatureBlockBindings<'r> {
integers: BTreeSet<(String, String)>,
tags: &BTreeMap<String, Vec<String>>,
) -> Result<Self, DecodeError> {
let codec = BlockStateJsonCodec::new(registry, integers)
.map_err(|e| DecodeError::Registry(e.to_string()))?;
let bound = Self::bind_tags(registry, tags)?;
Self::with_bound_tags(registry, integers, bound)
}
/// Every block's states and every tag's, by name: what `with_tags` binds,
/// to share between the bindings made over the same registry and tags.
pub fn bind_tags(
registry: &BlockRegistry,
tags: &BTreeMap<String, Vec<String>>,
) -> Result<BoundBlockTags, DecodeError> {
let mut blocks = BTreeMap::<String, BTreeSet<BlockStateId>>::new();
for id in 0..registry.len() {
let id = BlockStateId(id as u32);
@@ -153,6 +165,25 @@ impl<'r> FeatureBlockBindings<'r> {
return Err(DecodeError::Field("duplicate block tag"));
}
}
Ok((
Arc::new(
blocks
.into_iter()
.map(|(name, states)| (name, Arc::new(states)))
.collect(),
),
Arc::new(bound_tags),
))
}
/// [`FeatureBlockBindings::with_tags`] over tags already bound by
/// [`FeatureBlockBindings::bind_tags`].
pub fn with_bound_tags(
registry: &'r BlockRegistry,
integers: BTreeSet<(String, String)>,
(blocks, tags): BoundBlockTags,
) -> Result<Self, DecodeError> {
let codec = BlockStateJsonCodec::new(registry, integers)
.map_err(|e| DecodeError::Registry(e.to_string()))?;
let get = |name| {
registry
.default_state(name)
@@ -167,11 +198,8 @@ impl<'r> FeatureBlockBindings<'r> {
occlusion_faces: None,
can_occlude: None,
fluids: super::fluid_predicate::FluidPredicateBindings::new(registry)?,
blocks: blocks
.into_iter()
.map(|(name, states)| (name, Arc::new(states)))
.collect(),
tags: bound_tags,
blocks,
tags,
tree_blocks: std::sync::OnceLock::new(),
structures: None,
air: [
@@ -28,7 +28,7 @@ use std::{
struct RegionBlockQueries {
fluids: super::fluid_predicate::FluidPredicateBindings,
tags: BTreeMap<String, Arc<BTreeSet<BlockStateId>>>,
tags: Arc<BTreeMap<String, Arc<BTreeSet<BlockStateId>>>>,
}
#[derive(Debug)]
@@ -184,6 +184,9 @@ pub struct GeneratorData {
biome_carvers: Vec<Vec<Arc<WorldCarver>>>,
carving_blocks: CarvingBlocks,
pub(super) block_tags: BTreeMap<String, Vec<String>>,
/// `block_tags` bound to block states, once for the feature bindings of
/// decoration, structures and live features (`feature_block_bindings`).
bound_block_tags: std::sync::OnceLock<super::block_feature::BoundBlockTags>,
pub(super) biome_tags: BTreeMap<String, Vec<String>>,
pub(super) feature_resources: Option<FeatureResources>,
/// Templates, processor lists and per-state template transforms.
@@ -557,6 +560,7 @@ impl GeneratorData {
feature_resources,
ring_cache: Mutex::new(Vec::new()),
decorations: Mutex::new(Vec::new()),
bound_block_tags: std::sync::OnceLock::new(),
}))
}
/// The concentric ring positions (strongholds) of the generator for
@@ -676,6 +680,21 @@ impl GeneratorData {
pub(super) fn integers(&self) -> &BTreeSet<(String, String)> {
&self.integers
}
/// Feature block bindings over this data's blocks and block tags. The tags'
/// state sets are bound once and shared: each binding held 5 MB of them.
pub(super) fn feature_block_bindings(
&self,
) -> Result<super::block_feature::FeatureBlockBindings<'static>, DecodeError> {
use super::block_feature::FeatureBlockBindings;
let bound = match self.bound_block_tags.get() {
Some(bound) => bound.clone(),
None => {
let bound = FeatureBlockBindings::bind_tags(self.registry, &self.block_tags)?;
self.bound_block_tags.get_or_init(|| bound).clone()
}
};
FeatureBlockBindings::with_bound_tags(self.registry, self.integers.clone(), bound)
}
}
/// A generated chunk with its quart biomes. Blocks and heightmaps live in
@@ -3071,6 +3090,22 @@ impl GeneratorWorker {
pub fn set_structure_mobs(&mut self, mobs: Arc<dyn StructureMobs>) {
self.state.structure_mobs = Some(mobs);
}
/// Give back the density scratch buffers and the lookup caches, which only
/// speed up the next chunks (a generating worker holds megabytes of them).
/// Results do not change: they are made again on demand.
pub fn release(&mut self) {
let state = &mut self.state;
for context in [
&mut state.terrain_context,
&mut state.biome_context,
&mut state.carver_context,
] {
context.clear_scratch();
}
state.carver_biomes = HashMap::new();
state.structure_biomes = HashMap::new();
state.heights = HashMap::new();
}
}
#[cfg(test)]
@@ -134,10 +134,10 @@ impl Decoration {
.collect(),
&data.biome_tags,
)?);
let mut bindings =
FeatureBlockBindings::with_tags(registry, data.integers().clone(), &data.block_tags)?
.with_fluid_tags(&resources.fluid_tags)?
.with_biomes(biomes.clone());
let mut bindings = data
.feature_block_bindings()?
.with_fluid_tags(&resources.fluid_tags)?
.with_biomes(biomes.clone());
if let Some(structures) = &data.structures {
bindings = bindings.with_structures(structures.clone());
}
@@ -203,11 +203,7 @@ impl StructureState {
for (i, def) in registry.structures.iter().enumerate() {
by_step[def.step].push(i);
}
let mut bindings = FeatureBlockBindings::with_tags(
data.registry(),
data.integers().clone(),
&data.block_tags,
)?;
let mut bindings = data.feature_block_bindings()?;
// Processors that test what a block is made of read canOcclude.
if let Some(blocks) = data
.feature_resources
+4 -4
View File
@@ -103,10 +103,10 @@ impl LiveFeatures {
.collect(),
&data.biome_tags,
)?);
let mut bindings =
FeatureBlockBindings::with_tags(registry, data.integers().clone(), &data.block_tags)?
.with_fluid_tags(&resources.fluid_tags)?
.with_biomes(biomes.clone());
let mut bindings = data
.feature_block_bindings()?
.with_fluid_tags(&resources.fluid_tags)?
.with_biomes(biomes.clone());
if let Some(structures) = &data.structures {
bindings = bindings.with_structures(structures.clone());
}