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:
@@ -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)?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user