Surface: specialise the material rule per gradient band and surface side

Each chunk's compiled rule is specialised once for every band of y that
its vertical gradients bound, and for blocks below and at or above the
preliminary surface when the rule asks for it. Conditions those facts
decide are folded away, so a deep stone block evaluates only the rules
that can apply there (ore veins, the underground rules) instead of
walking the whole surface tree. Conditions only read the position and
the world, so the blocks are the same.

Surface stage, one thread, seed 1234: 2.45 -> 2.08 ms per chunk.
This commit is contained in:
Kacpherek
2026-10-25 13:38:10 +01:00
parent 6a065fa489
commit dbafda06ed
3 changed files with 244 additions and 3 deletions
@@ -23,6 +23,7 @@ use horizon_core::{
pub use library::MaterialRuleLibrary;
pub use noise::MaterialNoiseSources;
use rule::CompiledRule;
pub use rule::SurfaceVariants;
pub use rule::{MaterialRule, MaterialRuleError};
use std::{
collections::{BTreeMap, BTreeSet},
@@ -299,7 +300,81 @@ enum Kind<R: RandomSource> {
},
Not(Box<Compiled<R>>),
}
/// What a specialised rule may assume about every position it is applied to
/// (`CompiledRule::specialize`): its y lies in `low..=high`, and whether it is
/// at or above the column's preliminary surface level, when known.
#[derive(Clone, Copy, Debug)]
pub struct Facts {
pub low: i32,
pub high: i32,
pub above_surface: Option<bool>,
}
impl<R: RandomSource> Clone for Kind<R>
where
R::Positional: Clone,
{
fn clone(&self) -> Self {
match self {
Self::Simple(condition) => Self::Simple(condition.clone()),
Self::Biome(names) => Self::Biome(names.clone()),
Self::Constant(value) => Self::Constant(*value),
Self::Noise { sampler, min, max } => Self::Noise {
sampler: *sampler,
min: *min,
max: *max,
},
Self::Gradient { low, high, random } => Self::Gradient {
low: *low,
high: *high,
random: random.clone(),
},
Self::Not(child) => Self::Not(child.clone()),
}
}
}
impl<R: RandomSource> Clone for Compiled<R>
where
R::Positional: Clone,
{
fn clone(&self) -> Self {
Self {
kind: self.kind.clone(),
revision: self.revision,
result: self.result,
}
}
}
impl<R: RandomSource> Compiled<R> {
/// The value `facts` decide for this condition at every position they
/// cover. Conditions read the position and the world, never change them,
/// so one decided in advance gives what testing it would.
fn decided(&self, facts: Facts) -> Option<bool> {
match &self.kind {
Kind::Constant(value) => Some(*value),
Kind::Simple(MaterialCondition::AbovePreliminarySurface) => facts.above_surface,
Kind::Gradient { low, high, .. } => {
if facts.high <= *low {
Some(true)
} else if facts.low >= *high {
Some(false)
} else {
None
}
}
Kind::Not(child) => child.decided(facts).map(|value| !value),
_ => None,
}
}
/// The vertical gradients' bounds, and whether the preliminary surface
/// is asked for.
fn inputs(&self, gradients: &mut Vec<(i32, i32)>, surface: &mut bool) {
match &self.kind {
Kind::Gradient { low, high, .. } => gradients.push((*low, *high)),
Kind::Simple(MaterialCondition::AbovePreliminarySurface) => *surface = true,
Kind::Not(child) => child.inputs(gradients, surface),
_ => {}
}
}
fn constant_result(&self) -> Option<bool> {
match &self.kind {
Kind::Constant(value) => Some(*value),
@@ -1,5 +1,7 @@
//! Compiled material rule composition, sharing the condition worker's context.
use super::{Compiled, Context, MaterialCondition, MaterialConditions, MaterialSurfaceQueries};
use super::{
Compiled, Context, Facts, MaterialCondition, MaterialConditions, MaterialSurfaceQueries,
};
use crate::{registry::BlockStateId, worldgen::DecodeError};
use horizon_core::{
density::DensitySampler,
@@ -209,6 +211,161 @@ impl<R: RandomSource> CompiledRule<R> {
}
}
}
impl<R: RandomSource> CompiledRule<R>
where
R::Positional: Clone,
{
/// This rule where `facts` hold: conditions they decide are folded away,
/// with what can no longer produce a block, and a sequence stops at its
/// first child that always produces one. For every position the facts
/// cover it gives the block the whole rule gives.
fn specialize(&self, facts: Facts) -> Self {
match self {
Self::Empty => Self::Empty,
Self::Block(block) => Self::Block(*block),
Self::Bandlands => Self::Bandlands,
Self::OreVein {
blocks,
raw_ore_chance,
densities,
random,
} => Self::OreVein {
blocks: *blocks,
raw_ore_chance: *raw_ore_chance,
densities: *densities,
random: random.clone(),
},
Self::Condition { condition, rule } => match condition.decided(facts) {
Some(false) => Self::Empty,
Some(true) => rule.specialize(facts),
None => match rule.specialize(facts) {
Self::Empty => Self::Empty,
rule => Self::Condition {
condition: condition.clone(),
rule: Box::new(rule),
},
},
},
Self::Sequence(children) => {
let mut result = Vec::new();
for child in children {
let child = child.specialize(facts);
if matches!(child, Self::Empty) {
continue;
}
let last = child.always_produces();
result.push(child);
if last {
break;
}
}
match result.len() {
0 => Self::Empty,
1 => result.pop().unwrap(),
_ => Self::Sequence(result),
}
}
}
}
/// Whether applying this rule never gives `None` (only an error).
fn always_produces(&self) -> bool {
match self {
Self::Block(_) | Self::Bandlands => true,
Self::Sequence(children) => children.iter().any(Self::always_produces),
Self::Empty | Self::OreVein { .. } | Self::Condition { .. } => false,
}
}
fn inputs(&self, gradients: &mut Vec<(i32, i32)>, surface: &mut bool) {
match self {
Self::Condition { condition, rule } => {
condition.inputs(gradients, surface);
rule.inputs(gradients, surface);
}
Self::Sequence(children) => {
for child in children {
child.inputs(gradients, surface);
}
}
_ => {}
}
}
}
/// A rule specialised (`CompiledRule::specialize`) for each band of y between
/// its vertical gradients' bounds, and below and at or above the preliminary
/// surface when the rule asks for it: most blocks of a column, deep stone,
/// then evaluate only the few rules that can apply there.
pub struct SurfaceVariants {
/// The first y of each band after the first, ascending.
starts: Vec<i32>,
/// The rule index per band (two per band, below then above the surface,
/// when `surface`).
rules: Vec<usize>,
pub surface: bool,
}
impl SurfaceVariants {
/// The rule for a block at `y`, `above` the preliminary surface (ignored
/// unless `surface`).
#[inline]
pub fn rule(&self, y: i32, above: bool) -> usize {
let band = self.starts.partition_point(|&start| start <= y);
if self.surface {
self.rules[band * 2 + usize::from(above)]
} else {
self.rules[band]
}
}
}
impl<R: RandomSource, S: MaterialSurfaceQueries> MaterialConditions<R, S>
where
R::Positional: Clone,
{
/// Add the specialised variants of rule `index` (`SurfaceVariants`).
pub fn surface_variants(&mut self, index: usize) -> Result<SurfaceVariants, MaterialRuleError> {
let rule = self
.rules
.get(index)
.ok_or(MaterialRuleError::UnknownRule)?;
let (mut gradients, mut surface) = (Vec::new(), false);
rule.inputs(&mut gradients, &mut surface);
// A gradient is true at and below `low`, random between, false at
// and above `high`: it is decided within each band these bound.
let mut starts: Vec<i32> = gradients
.iter()
.flat_map(|&(low, high)| [low.saturating_add(1), high])
.collect();
starts.sort_unstable();
starts.dedup();
let mut variants = Vec::new();
for band in 0..=starts.len() {
let low = if band == 0 {
i32::MIN
} else {
starts[band - 1]
};
let high = starts.get(band).map_or(i32::MAX, |&next| next - 1);
let sides: &[Option<bool>] = if surface {
&[Some(false), Some(true)]
} else {
&[None]
};
for &above_surface in sides {
let facts = Facts {
low,
high,
above_surface,
};
variants.push(self.rules[index].specialize(facts));
}
}
let first = self.rules.len();
self.rules.extend(variants);
Ok(SurfaceVariants {
starts,
rules: (first..self.rules.len()).collect(),
surface,
})
}
}
impl<R: RandomSource, S: MaterialSurfaceQueries> MaterialConditions<R, S> {
/// Compile all children in order, even after an unconditional block rule.
/// Failed compilation never adds a partial rule to this worker.
@@ -196,7 +196,10 @@ pub fn build_surface<R: RandomSource, S: MaterialSurfaceQueries, B: TerrainBlock
rule: MaterialRule,
inputs: SurfaceInputs<'_, R, S>,
extensions: &MaterialExtensions<R::Positional>,
) -> Result<(), SurfaceError> {
) -> Result<(), SurfaceError>
where
R::Positional: Clone,
{
let min = chunk.min_y();
let max = min + chunk.height() - 1;
if inputs.write_min_y < min
@@ -240,9 +243,13 @@ fn build_columns<R: RandomSource, S: MaterialSurfaceQueries, B: TerrainBlockStat
extensions: &MaterialExtensions<R::Positional>,
write_min_y: i32,
write_max_y: i32,
) -> Result<(), SurfaceError> {
) -> Result<(), SurfaceError>
where
R::Positional: Clone,
{
let min = chunk.min_y();
let rule = worker.add_rule(rule)?;
let variants = worker.surface_variants(rule)?;
let cx = chunk.position().x.wrapping_shl(4);
let cz = chunk.position().z.wrapping_shl(4);
for lx in 0..16 {
@@ -321,6 +328,8 @@ fn build_columns<R: RandomSource, S: MaterialSurfaceQueries, B: TerrainBlockStat
above = above.wrapping_add(1);
worker.update_y(above, y.wrapping_sub(bottom).wrapping_add(1), water, y);
if y >= write_min_y && y <= write_max_y {
let above = variants.surface && y >= worker.minimum_surface_level()?;
let rule = variants.rule(y, above);
if let Some(state) = worker.try_apply(rule, BlockPos::new(x, y, z))? {
column.set_block(y, state)?;
}