Skip to main content

concinnity_core/ecs/
access.rs

1// A system's declared data access: which components and resources it reads and
2// writes, and whether it must run alone. The scheduler runs two systems
3// concurrently only when their accesses do not conflict. A conflict is a
4// read-write or write-write overlap on the same component or resource (two
5// readers never conflict), or either system being exclusive.
6//
7// Components and resources use independent id spaces, each a ComponentMask, so
8// a component id and a resource id never collide even though both are small
9// integers.
10//
11// A declaration is only as good as its accuracy, so debug builds check it: the
12// sibling `access_check` module collects what each context accessor actually
13// touched and asserts it against the set declared here.
14
15use crate::ecs::mask::{ComponentId, ComponentMask};
16
17#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
18/// A system's declared data access.
19pub struct Access {
20    component_reads: ComponentMask,
21    component_writes: ComponentMask,
22    resource_reads: ComponentMask,
23    resource_writes: ComponentMask,
24    exclusive: bool,
25}
26
27impl Access {
28    /// An empty declaration: no components, no resources, not exclusive.
29    pub fn new() -> Access {
30        Access::default()
31    }
32
33    /// Declare the components `step` reads.
34    pub fn reads_components(mut self, components: ComponentMask) -> Access {
35        self.component_reads = components;
36        self
37    }
38
39    /// Declare the components `step` writes.
40    pub fn writes_components(mut self, components: ComponentMask) -> Access {
41        self.component_writes = components;
42        self
43    }
44
45    /// Declare the resources `step` reads.
46    pub fn reads_resources(mut self, resources: ComponentMask) -> Access {
47        self.resource_reads = resources;
48        self
49    }
50
51    /// Declare the resources `step` writes.
52    pub fn writes_resources(mut self, resources: ComponentMask) -> Access {
53        self.resource_writes = resources;
54        self
55    }
56
57    /// Mark the system as exclusive: it conflicts with every other system and so
58    /// never runs concurrently. Used for a system that touches non-shareable
59    /// state the access masks do not model (e.g. a main-thread-only backend).
60    pub fn exclusive(mut self) -> Access {
61        self.exclusive = true;
62        self
63    }
64
65    /// Whether the system was marked exclusive.
66    pub fn is_exclusive(self) -> bool {
67        self.exclusive
68    }
69
70    /// The combined access of two declarations: reads, writes, and exclusivity
71    /// union. Used to fold a data-dependent system's per-program accesses into
72    /// its schedule-visible declaration.
73    pub fn union(self, other: Access) -> Access {
74        Access {
75            component_reads: self.component_reads.merged(other.component_reads),
76            component_writes: self.component_writes.merged(other.component_writes),
77            resource_reads: self.resource_reads.merged(other.resource_reads),
78            resource_writes: self.resource_writes.merged(other.resource_writes),
79            exclusive: self.exclusive || other.exclusive,
80        }
81    }
82
83    /// Whether a read of this component id is within the declaration (a
84    /// declared write implies read permission; exclusive allows everything).
85    pub fn may_read_component(self, id: ComponentId) -> bool {
86        self.exclusive || self.component_reads.contains(id) || self.component_writes.contains(id)
87    }
88
89    /// Whether a write of this component id is within the declaration.
90    pub fn may_write_component(self, id: ComponentId) -> bool {
91        self.exclusive || self.component_writes.contains(id)
92    }
93
94    /// Whether a read of this resource id is within the declaration (a
95    /// declared write implies read permission).
96    pub fn may_read_resource(self, id: ComponentId) -> bool {
97        self.exclusive || self.resource_reads.contains(id) || self.resource_writes.contains(id)
98    }
99
100    /// Whether a write of this resource id is within the declaration.
101    pub fn may_write_resource(self, id: ComponentId) -> bool {
102        self.exclusive || self.resource_writes.contains(id)
103    }
104
105    /// Whether this system can run concurrently with `other`.
106    pub fn conflicts_with(self, other: Access) -> bool {
107        self.exclusive
108            || other.exclusive
109            || overlaps(
110                self.component_reads,
111                self.component_writes,
112                other.component_reads,
113                other.component_writes,
114            )
115            || overlaps(
116                self.resource_reads,
117                self.resource_writes,
118                other.resource_reads,
119                other.resource_writes,
120            )
121    }
122}
123
124// A read-write or write-write overlap within one id space. Read-read overlap is
125// not a conflict.
126fn overlaps(
127    a_reads: ComponentMask,
128    a_writes: ComponentMask,
129    b_reads: ComponentMask,
130    b_writes: ComponentMask,
131) -> bool {
132    !a_writes.is_disjoint(b_reads)
133        || !a_writes.is_disjoint(b_writes)
134        || !a_reads.is_disjoint(b_writes)
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::ecs::mask::{ComponentId, ComponentMask};
141
142    fn mask(ids: &[u8]) -> ComponentMask {
143        let mut m = ComponentMask::EMPTY;
144        for &id in ids {
145            m.insert(ComponentId::new(id));
146        }
147        m
148    }
149
150    #[test]
151    fn two_readers_do_not_conflict() {
152        let a = Access::new().reads_components(mask(&[1, 2]));
153        let b = Access::new().reads_components(mask(&[2, 3]));
154        assert!(!a.conflicts_with(b));
155        assert!(!b.conflicts_with(a));
156    }
157
158    #[test]
159    fn read_write_overlap_conflicts() {
160        let reader = Access::new().reads_components(mask(&[2]));
161        let writer = Access::new().writes_components(mask(&[2]));
162        assert!(reader.conflicts_with(writer));
163        // Conflict is symmetric.
164        assert!(writer.conflicts_with(reader));
165    }
166
167    #[test]
168    fn write_write_overlap_conflicts() {
169        let a = Access::new().writes_components(mask(&[5]));
170        let b = Access::new().writes_components(mask(&[5, 6]));
171        assert!(a.conflicts_with(b));
172    }
173
174    #[test]
175    fn disjoint_access_does_not_conflict() {
176        let a = Access::new()
177            .reads_components(mask(&[1]))
178            .writes_components(mask(&[2]));
179        let b = Access::new()
180            .reads_components(mask(&[3]))
181            .writes_components(mask(&[4]));
182        assert!(!a.conflicts_with(b));
183    }
184
185    #[test]
186    fn exclusive_conflicts_with_everything() {
187        let solo = Access::new().exclusive();
188        let empty = Access::new();
189        assert!(solo.conflicts_with(empty));
190        assert!(empty.conflicts_with(solo));
191        // Even another exclusive.
192        assert!(solo.conflicts_with(Access::new().exclusive()));
193    }
194
195    #[test]
196    fn resource_and_component_spaces_are_independent() {
197        // Same numeric id in different spaces must not collide.
198        let writes_component_7 = Access::new().writes_components(mask(&[7]));
199        let reads_resource_7 = Access::new().reads_resources(mask(&[7]));
200        assert!(!writes_component_7.conflicts_with(reads_resource_7));
201
202        // A genuine resource read-write overlap does conflict.
203        let writes_resource_7 = Access::new().writes_resources(mask(&[7]));
204        assert!(reads_resource_7.conflicts_with(writes_resource_7));
205    }
206
207    #[test]
208    fn component_conflict_holds_despite_disjoint_resources() {
209        let a = Access::new()
210            .writes_components(mask(&[1]))
211            .reads_resources(mask(&[10]));
212        let b = Access::new()
213            .reads_components(mask(&[1]))
214            .reads_resources(mask(&[11]));
215        assert!(a.conflicts_with(b));
216    }
217
218    #[test]
219    fn empty_access_never_conflicts() {
220        let a = Access::new();
221        let b = Access::new()
222            .reads_components(mask(&[1]))
223            .writes_components(mask(&[2]));
224        assert!(!a.conflicts_with(b));
225        assert!(!b.conflicts_with(a));
226    }
227}