all_is_cubes/block/
block_def.rs

1#![allow(
2    elided_lifetimes_in_paths,
3    clippy::needless_pass_by_value,
4    reason = "Bevy systems"
5)]
6
7use alloc::sync::Arc;
8use bevy_ecs::change_detection::DetectChangesMut;
9use core::{fmt, mem, ops};
10
11use bevy_ecs::prelude as ecs;
12use bevy_ecs::schedule::IntoScheduleConfigs as _;
13
14use crate::block::{self, Block, BlockChange, EvalBlockError, InEvalError, MinEval};
15use crate::listen::{self, Gate, IntoListener as _, Listener, Notifier};
16use crate::time;
17use crate::transaction::{self, Equal, Transaction};
18use crate::universe::{self, HandleVisitor, ReadTicket, VisitHandles};
19
20#[cfg(doc)]
21use crate::block::{EvaluatedBlock, Primitive};
22#[cfg(doc)]
23use crate::universe::Universe;
24
25// -------------------------------------------------------------------------------------------------
26
27type EvalResult = Result<MinEval, EvalBlockError>;
28
29/// Contains a [`Block`] and can be stored in a [`Universe`].
30/// Together with [`Primitive::Indirect`], this allows mutation of a block definition such
31/// that all its existing usages follow.
32///
33/// To perform such a mutation, use [`BlockDefTransaction`].
34///
35/// Additionally, it caches the results of block evaluation to improve performance.
36/// Note that this cache only updates when the owning [`Universe`] is being stepped, or when
37/// a direct mutation to this [`BlockDef`] is performed, not when the contained [`Block`]
38/// sends a change notification.
39#[derive(bevy_ecs::component::Component)]
40#[require(BlockDefNextValue)]
41pub struct BlockDef {
42    state: BlockDefState,
43
44    /// Notifier of changes to this `BlockDef`'s evaluation result, either via transaction or via
45    /// the contained block itself changing.
46    ///
47    /// Note that this fires only when the cache is refreshed, not when the underlying block sends
48    /// a change notification.
49    notifier: Arc<Notifier<BlockChange>>,
50}
51
52/// Subset of [`BlockDef`] that is constructed anew when its block is replaced.
53// TODO(ecs): make this private once the system fn types are no longer exposed
54pub(crate) struct BlockDefState {
55    /// The current value.
56    block: Block,
57
58    /// Cache of evaluation results.
59    ///
60    /// If the current value is an `Err`, then it is also the case that `cache_dirty` may not have
61    /// a listener hooked up.
62    ///
63    /// Design rationale for caching and this particular arrangement of caching:
64    ///
65    /// * Deduplicating evaluation calculations, when a block is in multiple spaces,
66    ///   is wrapped with different modifiers, or is removed and reinserted.
67    /// * Moving the cost of evaluation to a consistent, deferred point.
68    /// * Fewer chains of forwarded notifications, improving data and instruction cache locality.
69    /// * Breaking data dependency cycles, so that if a `Space` contains itself
70    ///   via a block definition, this results in iterative convergence rather than an error.
71    cache: EvalResult,
72
73    /// Whether the cache needs to be updated.
74    cache_dirty: listen::Flag,
75
76    /// Whether we have successfully installed a listener on `self.block`.
77    listeners_ok: bool,
78
79    /// Gate with which to interrupt previous listening to a contained block.
80    #[expect(unused, reason = "used only for its `Drop` behavior")]
81    block_listen_gate: Gate,
82}
83
84impl BlockDef {
85    /// Constructs a new [`BlockDef`] that stores the given block (which may be replaced
86    /// in the future).
87    pub fn new(read_ticket: ReadTicket<'_>, block: Block) -> Self {
88        BlockDef {
89            state: BlockDefState::new(block, read_ticket),
90            notifier: Arc::new(Notifier::new()),
91        }
92    }
93
94    /// Returns the current block value.
95    ///
96    /// Note that if you wish to get the [`EvaluatedBlock`] result, you should obtain the cached
97    /// value by calling `BlockDef.evaluate()`, or by using a [`Primitive::Indirect`],
98    /// not by calling `.block().evaluate()`, which is not cached.
99    pub fn block(&self) -> &Block {
100        &self.state.block
101    }
102
103    /// Returns the current cached evaluation of the current block value.
104    ///
105    /// This returns the same success or error as `Block::from(handle_to_self).evaluate()` would,
106    /// not the same as `.block().evaluate()` would.
107    pub fn evaluate(
108        &self,
109        read_ticket: ReadTicket<'_>,
110    ) -> Result<block::EvaluatedBlock, EvalBlockError> {
111        let filter = block::EvalFilter::new(read_ticket);
112        block::finish_evaluation(
113            self.block().clone(),
114            filter.budget.get(),
115            {
116                // This decrement makes the cost consistent with evaluating a
117                // block with Primitive::Indirect.
118                block::Budget::decrement_components(&filter.budget).unwrap();
119
120                self.evaluate_impl(&filter)
121            },
122            &filter,
123        )
124    }
125
126    /// Implementation of block evaluation used by a [`Primitive::Indirect`] pointing to this.
127    pub(super) fn evaluate_impl(
128        &self,
129        filter: &block::EvalFilter<'_>,
130    ) -> Result<MinEval, InEvalError> {
131        let &block::EvalFilter {
132            read_ticket: _,
133            skip_eval,
134            ref listener,
135            budget: _, // already accounted in the caller
136        } = filter;
137
138        if let Some(listener) = listener {
139            <BlockDef as listen::Listen>::listen(self, listener.clone());
140        }
141
142        if skip_eval {
143            // In this case, don't use the cache, because it might contain an error, which
144            // would imply the *listen* part also failed, which it did not.
145            Ok(block::AIR_EVALUATED_MIN)
146        } else {
147            // TODO: Rework the `MinEval` type or the signatures of evaluation internals
148            // so that we can benefit from caching the `EvaluatedBlock` and not just the `MinEval`.
149            self.state
150                .cache
151                .clone()
152                .map_err(EvalBlockError::into_internal_error_for_block_def)
153        }
154    }
155}
156
157impl BlockDefState {
158    #[inline]
159    fn new(block: Block, read_ticket: ReadTicket<'_>) -> Self {
160        let cache_dirty = listen::Flag::new(false);
161        let (block_listen_gate, block_listener) =
162            Listener::<BlockChange>::gate(cache_dirty.listener());
163
164        let cache = block
165            .evaluate2(&block::EvalFilter {
166                read_ticket,
167                skip_eval: false,
168                listener: Some(block_listener.into_listener()),
169                budget: Default::default(),
170            })
171            .map(MinEval::from);
172
173        BlockDefState {
174            listeners_ok: cache.is_ok(),
175
176            block,
177            cache,
178            cache_dirty,
179            block_listen_gate,
180        }
181    }
182}
183
184impl fmt::Debug for BlockDef {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        // TODO: Consider printing the cache, but only if it wouldn't be redundant?
187        let Self {
188            state:
189                BlockDefState {
190                    block,
191                    cache: _,
192                    cache_dirty,
193                    listeners_ok,
194                    block_listen_gate: _,
195                },
196            notifier,
197        } = self;
198        f.debug_struct("BlockDef")
199            .field("block", &block)
200            .field("cache_dirty", &cache_dirty)
201            .field("listeners_ok", &listeners_ok)
202            .field("notifier", &notifier)
203            .finish_non_exhaustive()
204    }
205}
206
207/// Registers a listener for whenever the result of evaluation of this block definition changes.
208/// Note that this only occurs when the owning [`Universe`] is being stepped.
209impl listen::Listen for BlockDef {
210    type Msg = BlockChange;
211    type Listener = <Notifier<Self::Msg> as listen::Listen>::Listener;
212
213    fn listen_raw(&self, listener: Self::Listener) {
214        self.notifier.listen_raw(listener)
215    }
216}
217
218impl AsRef<Block> for BlockDef {
219    fn as_ref(&self) -> &Block {
220        &self.state.block
221    }
222}
223
224impl VisitHandles for BlockDef {
225    fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
226        let Self {
227            state:
228                BlockDefState {
229                    block,
230                    // Not 100% sure we shouldn't visit the cache too, but
231                    // it's not serialized, at least, which is a sign that no.
232                    cache: _,
233                    cache_dirty: _,
234                    listeners_ok: _,
235                    block_listen_gate: _,
236                },
237            notifier: _,
238        } = self;
239        block.visit_handles(visitor);
240    }
241}
242
243universe::impl_universe_member_for_single_component_type!(BlockDef);
244
245impl transaction::Transactional for BlockDef {
246    type Transaction = BlockDefTransaction;
247}
248
249#[cfg(feature = "arbitrary")]
250impl<'a> arbitrary::Arbitrary<'a> for BlockDef {
251    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
252        Ok(BlockDef::new(ReadTicket::stub(), Block::arbitrary(u)?))
253    }
254
255    fn size_hint(depth: usize) -> (usize, Option<usize>) {
256        // We don't need to bother with try_size_hint() because Block short-circuits recursion
257        Block::size_hint(depth)
258    }
259}
260
261/// A [`Transaction`] which replaces (or checks) the [`Block`] stored in a [`BlockDef`].
262#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
263#[must_use]
264pub struct BlockDefTransaction {
265    // TODO: This struct is the second occurrence (the first is space::CubeTransaction) of a "assign to a mutable location" transaction. If we figure out how to have conveniently _composable_ transactions then we should have an `impl Transaction<Target = &mut T> for Assign<T>` transaction (targeting `&mut` to discourage use otherwise).
266    /// Block that must already be present.
267    old: Equal<Block>,
268    /// Block to be written in.
269    new: Equal<Block>,
270}
271
272impl BlockDefTransaction {
273    /// Returns a transaction which fails if the current value of the [`BlockDef`] is not
274    /// equal to `old`.
275    pub fn expect(old: Block) -> Self {
276        Self {
277            old: Equal(Some(old)),
278            new: Equal(None),
279        }
280    }
281
282    /// Returns a transaction which replaces the current value of the [`BlockDef`] with `new`.
283    pub fn overwrite(new: Block) -> Self {
284        Self {
285            old: Equal(None),
286            new: Equal(Some(new)),
287        }
288    }
289
290    /// Returns a transaction which replaces the value of the [`BlockDef`] with `new`,
291    /// if it is equal to `old`, and otherwise fails.
292    pub fn replace(old: Block, new: Block) -> Self {
293        Self {
294            old: Equal(Some(old)),
295            new: Equal(Some(new)),
296        }
297    }
298}
299
300#[expect(missing_debug_implementations)]
301#[doc(hidden)] // would be impl Trait if we could
302pub struct Check {
303    /// May be `None` if the transaction has no new block and was only comparing the old block.
304    new_state: Option<BlockDefState>,
305}
306
307impl Transaction for BlockDefTransaction {
308    type Target = BlockDef;
309    type Context<'a> = ReadTicket<'a>;
310    type CommitCheck = Check;
311    type Output = transaction::NoOutput;
312    type Mismatch = BlockDefMismatch;
313
314    fn check(
315        &self,
316        target: &BlockDef,
317        read_ticket: Self::Context<'_>,
318    ) -> Result<Self::CommitCheck, Self::Mismatch> {
319        // Check the transaction precondition
320        self.old.check(&target.state.block).map_err(|_| BlockDefMismatch::Unexpected)?;
321
322        // Perform evaluation now, while we have the read ticket.
323        //
324        // Evaluation errors are not transaction errors, to ensure that editing the world is always
325        // possible even when in a situation with multiple errors that need to be fixed separately.
326        //
327        // TODO: Maybe add an option to the transaction to be strict?
328        Ok(Check {
329            new_state: self.new.0.clone().map(|block| BlockDefState::new(block, read_ticket)),
330        })
331    }
332
333    fn commit(
334        self,
335        target: &mut BlockDef,
336        check: Self::CommitCheck,
337        _outputs: &mut dyn FnMut(Self::Output),
338    ) -> Result<(), transaction::CommitError> {
339        match (self.new, check.new_state) {
340            (Equal(Some(_)), Some(new_state)) => {
341                target.state = new_state;
342                target.notifier.notify(&BlockChange::new());
343            }
344            (Equal(None), None) => {}
345            _ => panic!("BlockDefTransaction check value is inconsistent"),
346        }
347        Ok(())
348    }
349}
350
351impl universe::TransactionOnEcs for BlockDefTransaction {
352    type WriteQueryData = &'static mut Self::Target;
353
354    fn check(
355        &self,
356        target: &BlockDef,
357        read_ticket: ReadTicket<'_>,
358    ) -> Result<Self::CommitCheck, Self::Mismatch> {
359        Transaction::check(self, target, read_ticket)
360    }
361
362    fn commit(
363        self,
364        mut target: ecs::Mut<'_, BlockDef>,
365        check: Self::CommitCheck,
366    ) -> Result<(), transaction::CommitError> {
367        Transaction::commit(self, &mut *target, check, &mut transaction::no_outputs)
368    }
369}
370
371impl transaction::Merge for BlockDefTransaction {
372    type MergeCheck = ();
373    type Conflict = BlockDefConflict;
374
375    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
376        let conflict = BlockDefConflict {
377            old: self.old.check_merge(&other.old).is_err(),
378            new: self.new.check_merge(&other.new).is_err(),
379        };
380
381        if (conflict
382            != BlockDefConflict {
383                old: false,
384                new: false,
385            })
386        {
387            Err(conflict)
388        } else {
389            Ok(())
390        }
391    }
392
393    fn commit_merge(&mut self, other: Self, (): Self::MergeCheck) {
394        let Self { old, new } = self;
395        old.commit_merge(other.old, ());
396        new.commit_merge(other.new, ());
397    }
398}
399
400/// Transaction precondition error type for a [`BlockDefTransaction`].
401#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
402#[non_exhaustive]
403pub enum BlockDefMismatch {
404    /// old definition not as expected
405    Unexpected,
406}
407
408/// Transaction conflict error type for a [`BlockDefTransaction`].
409// ---
410// TODO: this is identical to `CubeConflict` but for the names
411#[derive(Clone, Copy, Debug, Eq, PartialEq)]
412#[non_exhaustive]
413pub struct BlockDefConflict {
414    /// The transactions have conflicting preconditions (`old` blocks).
415    pub(crate) old: bool,
416    /// The transactions are attempting to replace the existing block with different `new` blocks.
417    pub(crate) new: bool,
418}
419
420impl core::error::Error for BlockDefMismatch {}
421impl core::error::Error for BlockDefConflict {}
422
423impl fmt::Display for BlockDefConflict {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        match *self {
426            BlockDefConflict {
427                old: true,
428                new: false,
429            } => write!(f, "different preconditions for BlockDef"),
430            BlockDefConflict {
431                old: false,
432                new: true,
433            } => write!(f, "cannot write different blocks to the same BlockDef"),
434            BlockDefConflict {
435                old: true,
436                new: true,
437            } => write!(f, "different preconditions (with write)"),
438            BlockDefConflict {
439                old: false,
440                new: false,
441            } => unreachable!(),
442        }
443    }
444}
445
446#[derive(Clone, Copy, Debug, Default, PartialEq)]
447pub(crate) struct BlockDefStepInfo {
448    /// A cache update was attempted.
449    attempted: usize,
450    /// A cache update succeeded.
451    updated: usize,
452    /// A cache update failed because of a [`HandleError::InUse`] conflict.
453    was_in_use: usize,
454}
455
456impl ops::Add for BlockDefStepInfo {
457    type Output = Self;
458    #[inline]
459    fn add(self, rhs: Self) -> Self::Output {
460        Self {
461            attempted: self.attempted + rhs.attempted,
462            updated: self.updated + rhs.updated,
463            was_in_use: self.was_in_use + rhs.was_in_use,
464        }
465    }
466}
467
468impl ops::AddAssign for BlockDefStepInfo {
469    #[inline]
470    fn add_assign(&mut self, other: Self) {
471        *self = *self + other;
472    }
473}
474
475impl manyfmt::Fmt<crate::util::StatusText> for BlockDefStepInfo {
476    fn fmt(&self, fmt: &mut fmt::Formatter<'_>, _: &crate::util::StatusText) -> fmt::Result {
477        let Self {
478            attempted,
479            updated,
480            was_in_use,
481        } = self;
482        write!(
483            fmt,
484            "{attempted} attempted, {updated} updated, {was_in_use} were in use"
485        )
486    }
487}
488
489// -------------------------------------------------------------------------------------------------
490
491#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, bevy_ecs::schedule::SystemSet)]
492pub(crate) struct BlockDefUpdateSet;
493
494/// Install systems related to keeping [`BlockDef`]s updated.
495pub(crate) fn add_block_def_systems(world: &mut ecs::World) {
496    let mut schedules = world.resource_mut::<ecs::Schedules>();
497    schedules.add_systems(
498        time::schedule::Synchronize,
499        (update_phase_1, update_phase_2).chain().in_set(BlockDefUpdateSet),
500    );
501}
502
503/// When updating block definitions, this temporarily stores the value that should be written
504/// into the `BlockDef` component.
505///
506#[derive(ecs::Component, Default)]
507// TODO(ecs): make this private once the system fn types are no longer exposed
508pub(crate) enum BlockDefNextValue {
509    #[default]
510    None,
511    NewEvaluation(EvalResult),
512    /// Used when the prior attempt to add listeners failed.
513    NewState(BlockDefState),
514}
515
516/// ECS system function that looks for `BlockDef`s needing reevaluation, then writes the new
517/// evaluations into `BlockDefNextValue`.
518#[allow(clippy::needless_pass_by_value)]
519pub(crate) fn update_phase_1(
520    mut info_collector: ecs::ResMut<universe::InfoCollector<BlockDefStepInfo>>,
521    mut defs: ecs::Query<(&BlockDef, &mut BlockDefNextValue)>,
522    data_sources: universe::QueryBlockDataSources,
523) {
524    let mq = data_sources.get();
525    let read_ticket = ReadTicket::from_queries(&mq);
526    let mut info = BlockDefStepInfo::default();
527    // TODO(ecs): parallel iter
528    for (def, mut next) in defs.iter_mut() {
529        debug_assert!(
530            matches!(*next, BlockDefNextValue::None),
531            "BlockDefNextValue should have been cleared",
532        );
533
534        if !def.state.listeners_ok {
535            info.attempted += 1;
536            // If there was an evaluation error, then we may also be missing listeners.
537            // Start over.
538            *next = BlockDefNextValue::NewState(BlockDefState::new(
539                def.state.block.clone(),
540                read_ticket,
541            ));
542            info.updated += 1;
543        } else if def.state.cache_dirty.get_and_clear() {
544            // We have a cached value, but it is stale.
545
546            info.attempted += 1;
547
548            let new_cache = def
549                .state
550                .block
551                .evaluate2(&block::EvalFilter {
552                    read_ticket,
553                    skip_eval: false,
554                    listener: None, // we already have a listener installed
555                    budget: Default::default(),
556                })
557                .map(MinEval::from);
558
559            // Write the new cache data *unless* it is a transient error.
560            if !matches!(new_cache, Err(ref e) if e.is_transient()) && new_cache != def.state.cache
561            {
562                *next = BlockDefNextValue::NewEvaluation(new_cache);
563                info.updated += 1;
564            }
565        }
566
567        if info.attempted > 0 && matches!(def.state.cache, Err(ref e) if e.is_transient()) {
568            info.was_in_use += 1;
569        }
570    }
571    info_collector.record(info);
572}
573
574/// ECS system function that moves new evaluations from `BlockDefNextValue` to `BlockDef`.
575///
576/// This system being separate resolves the borrow conflict between different `BlockDef`s reading
577/// each other (possibly circularly) and writing themselves.
578pub(crate) fn update_phase_2(
579    mut defs: ecs::Query<
580        '_,
581        '_,
582        (&mut BlockDef, &mut BlockDefNextValue),
583        ecs::Changed<BlockDefNextValue>,
584    >,
585) {
586    defs.par_iter_mut().for_each(|(mut def, mut next)| {
587        // By bypassing change detection, we avoid detecting this consumption of the change.
588        // (This means that change detection no longer strictly functions as change detection,
589        // but that is okay because `BlockDefNextValue` is *only* for this.)
590        match mem::take(next.bypass_change_detection()) {
591            BlockDefNextValue::NewEvaluation(result) => {
592                def.state.cache = result;
593                def.notifier.notify(&BlockChange::new());
594            }
595            BlockDefNextValue::NewState(result) => {
596                def.state = result;
597                def.notifier.notify(&BlockChange::new());
598            }
599            BlockDefNextValue::None => {}
600        }
601    });
602}
603
604// -------------------------------------------------------------------------------------------------
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609    use crate::math::Rgba;
610    use crate::universe::Universe;
611    use pretty_assertions::assert_eq;
612
613    /// Quick more-than-nothing test for [`BlockDef::evaluate()`] being the same as more usual
614    /// options.
615    ///
616    /// TODO: Test its behavior on failure.
617    #[test]
618    fn evaluate_equivalence() {
619        let mut universe = Universe::new();
620        let block = Block::builder().color(Rgba::new(1.0, 0.0, 0.0, 1.0)).build();
621
622        let eval_bare = block.evaluate(universe.read_ticket()).unwrap();
623        let block_def = BlockDef::new(universe.read_ticket(), block.clone());
624        let eval_def = block_def.evaluate(universe.read_ticket()).unwrap();
625        let block_def_handle = universe.insert_anonymous(block_def);
626        let indirect_block = Block::from(block_def_handle);
627        let eval_indirect = indirect_block.evaluate(universe.read_ticket()).unwrap();
628
629        assert_eq!(
630            block::EvaluatedBlock {
631                block: indirect_block,
632                ..eval_def.clone()
633            },
634            eval_indirect,
635            "BlockDef::evaluate() same except for block as Primitive::Indirect"
636        );
637        assert_eq!(
638            block::EvaluatedBlock {
639                block,
640                cost: eval_bare.cost,
641                ..eval_def
642            },
643            eval_bare,
644            "BlockDef::evaluate() same except for block and cost as the def block"
645        );
646    }
647}