concinnity-core 0.19.1

Runtime vocabulary for the Concinnity engine: GPU layouts, ECS components, registry, CPU kernels
Documentation
// A system's declared data access: which components and resources it reads and
// writes, and whether it must run alone. The scheduler runs two systems
// concurrently only when their accesses do not conflict. A conflict is a
// read-write or write-write overlap on the same component or resource (two
// readers never conflict), or either system being exclusive.
//
// Components and resources use independent id spaces, each a ComponentMask, so
// a component id and a resource id never collide even though both are small
// integers.
//
// A declaration is only as good as its accuracy, so debug builds check it: the
// sibling `access_check` module collects what each context accessor actually
// touched and asserts it against the set declared here.

use crate::ecs::mask::{ComponentId, ComponentMask};

#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
/// A system's declared data access.
pub struct Access {
    component_reads: ComponentMask,
    component_writes: ComponentMask,
    resource_reads: ComponentMask,
    resource_writes: ComponentMask,
    exclusive: bool,
}

impl Access {
    /// An empty declaration: no components, no resources, not exclusive.
    pub fn new() -> Access {
        Access::default()
    }

    /// Declare the components `step` reads.
    pub fn reads_components(mut self, components: ComponentMask) -> Access {
        self.component_reads = components;
        self
    }

    /// Declare the components `step` writes.
    pub fn writes_components(mut self, components: ComponentMask) -> Access {
        self.component_writes = components;
        self
    }

    /// Declare the resources `step` reads.
    pub fn reads_resources(mut self, resources: ComponentMask) -> Access {
        self.resource_reads = resources;
        self
    }

    /// Declare the resources `step` writes.
    pub fn writes_resources(mut self, resources: ComponentMask) -> Access {
        self.resource_writes = resources;
        self
    }

    /// Mark the system as exclusive: it conflicts with every other system and so
    /// never runs concurrently. Used for a system that touches non-shareable
    /// state the access masks do not model (e.g. a main-thread-only backend).
    pub fn exclusive(mut self) -> Access {
        self.exclusive = true;
        self
    }

    /// Whether the system was marked exclusive.
    pub fn is_exclusive(self) -> bool {
        self.exclusive
    }

    /// The combined access of two declarations: reads, writes, and exclusivity
    /// union. Used to fold a data-dependent system's per-program accesses into
    /// its schedule-visible declaration.
    pub fn union(self, other: Access) -> Access {
        Access {
            component_reads: self.component_reads.merged(other.component_reads),
            component_writes: self.component_writes.merged(other.component_writes),
            resource_reads: self.resource_reads.merged(other.resource_reads),
            resource_writes: self.resource_writes.merged(other.resource_writes),
            exclusive: self.exclusive || other.exclusive,
        }
    }

    /// Whether a read of this component id is within the declaration (a
    /// declared write implies read permission; exclusive allows everything).
    pub fn may_read_component(self, id: ComponentId) -> bool {
        self.exclusive || self.component_reads.contains(id) || self.component_writes.contains(id)
    }

    /// Whether a write of this component id is within the declaration.
    pub fn may_write_component(self, id: ComponentId) -> bool {
        self.exclusive || self.component_writes.contains(id)
    }

    /// Whether a read of this resource id is within the declaration (a
    /// declared write implies read permission).
    pub fn may_read_resource(self, id: ComponentId) -> bool {
        self.exclusive || self.resource_reads.contains(id) || self.resource_writes.contains(id)
    }

    /// Whether a write of this resource id is within the declaration.
    pub fn may_write_resource(self, id: ComponentId) -> bool {
        self.exclusive || self.resource_writes.contains(id)
    }

    /// Whether this system can run concurrently with `other`.
    pub fn conflicts_with(self, other: Access) -> bool {
        self.exclusive
            || other.exclusive
            || overlaps(
                self.component_reads,
                self.component_writes,
                other.component_reads,
                other.component_writes,
            )
            || overlaps(
                self.resource_reads,
                self.resource_writes,
                other.resource_reads,
                other.resource_writes,
            )
    }
}

// A read-write or write-write overlap within one id space. Read-read overlap is
// not a conflict.
fn overlaps(
    a_reads: ComponentMask,
    a_writes: ComponentMask,
    b_reads: ComponentMask,
    b_writes: ComponentMask,
) -> bool {
    !a_writes.is_disjoint(b_reads)
        || !a_writes.is_disjoint(b_writes)
        || !a_reads.is_disjoint(b_writes)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ecs::mask::{ComponentId, ComponentMask};

    fn mask(ids: &[u8]) -> ComponentMask {
        let mut m = ComponentMask::EMPTY;
        for &id in ids {
            m.insert(ComponentId::new(id));
        }
        m
    }

    #[test]
    fn two_readers_do_not_conflict() {
        let a = Access::new().reads_components(mask(&[1, 2]));
        let b = Access::new().reads_components(mask(&[2, 3]));
        assert!(!a.conflicts_with(b));
        assert!(!b.conflicts_with(a));
    }

    #[test]
    fn read_write_overlap_conflicts() {
        let reader = Access::new().reads_components(mask(&[2]));
        let writer = Access::new().writes_components(mask(&[2]));
        assert!(reader.conflicts_with(writer));
        // Conflict is symmetric.
        assert!(writer.conflicts_with(reader));
    }

    #[test]
    fn write_write_overlap_conflicts() {
        let a = Access::new().writes_components(mask(&[5]));
        let b = Access::new().writes_components(mask(&[5, 6]));
        assert!(a.conflicts_with(b));
    }

    #[test]
    fn disjoint_access_does_not_conflict() {
        let a = Access::new()
            .reads_components(mask(&[1]))
            .writes_components(mask(&[2]));
        let b = Access::new()
            .reads_components(mask(&[3]))
            .writes_components(mask(&[4]));
        assert!(!a.conflicts_with(b));
    }

    #[test]
    fn exclusive_conflicts_with_everything() {
        let solo = Access::new().exclusive();
        let empty = Access::new();
        assert!(solo.conflicts_with(empty));
        assert!(empty.conflicts_with(solo));
        // Even another exclusive.
        assert!(solo.conflicts_with(Access::new().exclusive()));
    }

    #[test]
    fn resource_and_component_spaces_are_independent() {
        // Same numeric id in different spaces must not collide.
        let writes_component_7 = Access::new().writes_components(mask(&[7]));
        let reads_resource_7 = Access::new().reads_resources(mask(&[7]));
        assert!(!writes_component_7.conflicts_with(reads_resource_7));

        // A genuine resource read-write overlap does conflict.
        let writes_resource_7 = Access::new().writes_resources(mask(&[7]));
        assert!(reads_resource_7.conflicts_with(writes_resource_7));
    }

    #[test]
    fn component_conflict_holds_despite_disjoint_resources() {
        let a = Access::new()
            .writes_components(mask(&[1]))
            .reads_resources(mask(&[10]));
        let b = Access::new()
            .reads_components(mask(&[1]))
            .reads_resources(mask(&[11]));
        assert!(a.conflicts_with(b));
    }

    #[test]
    fn empty_access_never_conflicts() {
        let a = Access::new();
        let b = Access::new()
            .reads_components(mask(&[1]))
            .writes_components(mask(&[2]));
        assert!(!a.conflicts_with(b));
        assert!(!b.conflicts_with(a));
    }
}