diff --git a/assets/tests/add_system/read_only_parameters_cannot_be_written.lua b/assets/tests/add_system/read_only_parameters_cannot_be_written.lua new file mode 100644 index 0000000000..eae6e7f619 --- /dev/null +++ b/assets/tests/add_system/read_only_parameters_cannot_be_written.lua @@ -0,0 +1,52 @@ +-- parameters declared with `resource_read` / `component_read` are declared as reads to bevy, +-- so the system may read but must not write them +local ScriptAttachment = import_namespace("bms").ScriptAttachment +local bms = import_namespace("bms") +local world = bms.world +runs = {} +local ResourceType = world.get_type_by_name("test_utils::test_data::TestResourceWithVariousFields") +local ComponentA = world.get_type_by_name("test_utils::test_data::CompWithFromWorldAndComponentData") + +function on_test() + local post_update_schedule = world.get_schedule_by_name("PostUpdate") + local script_attachment = ScriptAttachment.new_entity_script(bms.current_entity(), bms.current_script()) + + local entity = world.spawn() + world.add_default_component(entity, ComponentA) + + world.add_system( + post_update_schedule, + bms.system_builder("my_reading_system", script_attachment) + :resource_read(ResourceType) + :query(world.query():component_read(ComponentA)) + ) + + return true +end + +function my_reading_system(resource, query) + runs[#runs + 1] = "my_reading_system" + local ok, err = pcall(check_accesses, resource, query) + runs[#runs + 1] = ok and "finished" or tostring(err) +end + +function check_accesses(resource, query) + assert(resource.string == "Initial Value", "Expected 'Initial Value', got: " .. resource.string) + assert(#query >= 1, "Expected results, got: " .. #query) + local component = query[1]:components()[1] + assert(type(component[1]) == "string", "Expected to read the component, got: " .. tostring(component[1])) + + local ok, err = pcall(function() resource.string = "changed" end) + assert(not ok and tostring(err):find("annot claim access"), "resource write: " .. tostring(err)) + ok, err = pcall(function() component[1] = "changed" end) + assert(not ok and tostring(err):find("annot claim access"), "component write: " .. tostring(err)) +end + +function on_test_post_update() + return true +end + +function on_test_last() + assert(#runs == 2 and runs[2] == "finished", "the system did not finish, runs: " .. table.concat(runs, ",")) + return true +end diff --git a/crates/bevy_mod_scripting_bindings/src/query.rs b/crates/bevy_mod_scripting_bindings/src/query.rs index 3519302b79..d337c25d9b 100644 --- a/crates/bevy_mod_scripting_bindings/src/query.rs +++ b/crates/bevy_mod_scripting_bindings/src/query.rs @@ -407,6 +407,22 @@ impl std::fmt::Display for ScriptTypeRegistration { } } +/// How [`ScriptQueryBuilder::as_query_state`] declares the accesses of the query. +/// +/// Read and written components match the same entities, the difference is only in the access the query reports. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum QueryAccess { + /// Every component is declared as read, for finding the matching entities, i.e. with + /// [`bevy_ecs::query::QueryState::iter`] on a `&World`, while the actual component accesses are claimed through + /// the world guard. + /// + /// The query state must not be used to write components, or have its access reported to the scheduler. + MatchOnly, + /// The written components are declared as written, for queries whose access is reported to the scheduler, + /// i.e. the queries of script systems. + Declared, +} + #[derive(Clone, Default, Reflect)] #[reflect(opaque)] /// The query builder is used to build ECS queries which retrieve spefific components filtered by specific conditions. @@ -431,6 +447,8 @@ impl std::fmt::Display for ScriptTypeRegistration { pub struct ScriptQueryBuilder { /// The components to query for. pub components: Vec, + /// Whether each of the components is written, components without an entry are written. + writes: Vec, /// Components that must be present. pub with: Vec, /// Components that must not be present. @@ -444,17 +462,38 @@ impl ScriptQueryBuilder { Default::default() } - /// Adds components to the query. + /// Adds components to the query, which can be read and written. pub fn components(&mut self, components: Vec) -> &mut Self { - self.components.extend(components); + for component in components { + self.component(component); + } self } - /// Adds a component to the query. + /// Adds a component to the query, which can be read and written. pub fn component(&mut self, component: ScriptComponentRegistration) -> &mut Self { + self.push_component(component, true) + } + + /// Adds a component to the query, which can only be read. + /// + /// Systems reading a component can run in parallel with each other. + pub fn component_read(&mut self, component: ScriptComponentRegistration) -> &mut Self { + self.push_component(component, false) + } + + fn push_component(&mut self, component: ScriptComponentRegistration, write: bool) -> &mut Self { + // components pushed to the public field directly are written + self.writes.resize(self.components.len(), true); self.components.push(component); + self.writes.push(write); self } + /// Returns true if the component at the given index of [`Self::components`] is written + pub fn is_written(&self, index: usize) -> bool { + self.writes.get(index).copied().unwrap_or(true) + } + /// Adds components to the query that must be present. pub fn with_components(&mut self, with: Vec) -> &mut Self { self.with.extend(with); @@ -479,14 +518,21 @@ impl ScriptQueryBuilder { self } - /// Builds the query into a query state as used in systems. - pub fn as_query_state(&self, world: &mut World) -> QueryState { + /// Builds the query into a query state, declaring its component accesses as given by `access`. + pub fn as_query_state( + &self, + world: &mut World, + access: QueryAccess, + ) -> QueryState { let mut dynamic_query = QueryBuilder::::new(world); // we don't actually want to fetch the data for components now, only figure out - // which entities match the query - // so we might be being slightly overkill - for c in &self.components { - dynamic_query.ref_id(c.component_id()); + // which entities match the query, and declare the access + for (index, c) in self.components.iter().enumerate() { + if access == QueryAccess::Declared && self.is_written(index) { + dynamic_query.mut_id(c.component_id()); + } else { + dynamic_query.ref_id(c.component_id()); + } } for w in &self.with { diff --git a/crates/bevy_mod_scripting_bindings/src/world_extensions.rs b/crates/bevy_mod_scripting_bindings/src/world_extensions.rs index cb397817cf..7b85447110 100644 --- a/crates/bevy_mod_scripting_bindings/src/world_extensions.rs +++ b/crates/bevy_mod_scripting_bindings/src/world_extensions.rs @@ -509,7 +509,8 @@ impl<'w> WorldExtensions for WorldAccessGuard<'w> { query: crate::ScriptQueryBuilder, ) -> Result, InteropError> { self.with_world_mut(|world| { - let mut built_query = query.as_query_state::(world); + let mut built_query = + query.as_query_state::(world, crate::QueryAccess::MatchOnly); let query_result = built_query.iter(world); Ok(query_result diff --git a/crates/bevy_mod_scripting_core/src/extractors.rs b/crates/bevy_mod_scripting_core/src/extractors.rs index 116ef0da9f..75063ca1d7 100644 --- a/crates/bevy_mod_scripting_core/src/extractors.rs +++ b/crates/bevy_mod_scripting_core/src/extractors.rs @@ -143,12 +143,12 @@ pub(crate) fn get_all_access_ids(access: &Access) -> Vec<(ComponentId, bool)> { let mut access_all_write = Access::default(); access_all_write.write_all(); - // read conflicts with each set to figure out the necessary locks + // read conflicts with each set to figure out the necessary locks: + // only writes conflict with reading everything, reads and writes conflict with writing everything + let written = individual_conflicts(access.get_conflicts(&access_all_read)); + let mut read = individual_conflicts(access.get_conflicts(&access_all_write)); - let mut read = individual_conflicts(access.get_conflicts(&access_all_read)); - let written = individual_conflicts(access.get_conflicts(&access_all_write)); - - // remove reads from writes + // remove writes from reads read.difference_with(&written); let mut result = Vec::new(); diff --git a/crates/bevy_mod_scripting_core/src/script_system.rs b/crates/bevy_mod_scripting_core/src/script_system.rs index 6d473ddff9..73e5518588 100644 --- a/crates/bevy_mod_scripting_core/src/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/script_system.rs @@ -14,7 +14,7 @@ use ::{ bevy_ecs::{ component::ComponentId, entity::Entity, - query::{FilteredAccess, FilteredAccessSet, QueryState}, + query::{FilteredAccessSet, QueryState}, reflect::AppTypeRegistry, schedule::SystemSet, system::{System, SystemParamValidationError}, @@ -38,7 +38,7 @@ use bevy_log::{debug, warn, warn_once}; use bevy_mod_scripting_bindings::{ AppScheduleRegistry, AppScriptCallbackRunners, AppScriptComponentRegistry, AppScriptFunctionRegistry, AppScriptTypeCatalogue, CurrentScriptAttachment, InteropError, - IntoScript, ReflectReference, ScriptQueryBuilder, ScriptQueryResult, + IntoScript, QueryAccess, ReflectReference, ScriptQueryBuilder, ScriptQueryResult, ScriptResourceRegistration, V, WorldExtensions, }; use bevy_mod_scripting_display::DisplayProxy; @@ -89,7 +89,10 @@ impl SystemSet for ScriptSystemSet { #[derive(Clone)] enum ScriptSystemParamDescriptor { - Res(ScriptResourceRegistration), + Res { + resource: ScriptResourceRegistration, + write: bool, + }, EntityQuery(ScriptQueryBuilder), } @@ -126,10 +129,21 @@ impl ScriptSystemBuilder { self } - /// Adds a resource access to the system + /// Adds a resource access to the system, the resource can be read and written pub fn resource(&mut self, resource: ScriptResourceRegistration) -> &mut Self { - self.system_params - .push(ScriptSystemParamDescriptor::Res(resource)); + self.system_params.push(ScriptSystemParamDescriptor::Res { + resource, + write: true, + }); + self + } + + /// Adds a read only resource access to the system, systems reading a resource can run in parallel with each other + pub fn resource_read(&mut self, resource: ScriptResourceRegistration) -> &mut Self { + self.system_params.push(ScriptSystemParamDescriptor::Res { + resource, + write: false, + }); self } @@ -847,12 +861,17 @@ impl System for DynamicScriptSystem

{ // start with resources let mut subset = HashSet::::new(); + // the accesses in `subset` which can be written, the others are read only + let mut written = HashSet::::new(); let mut system_params = Vec::with_capacity(self.system_param_descriptors.len()); let mut component_access_set = FilteredAccessSet::new(); let mut initialization_errors = Vec::new(); for param in &self.system_param_descriptors { match param { - ScriptSystemParamDescriptor::Res(res) => { + ScriptSystemParamDescriptor::Res { + resource: res, + write, + } => { let component_id = res.resource_id; let type_id = res.type_registration().type_id(); @@ -862,10 +881,13 @@ impl System for DynamicScriptSystem

{ }; system_params.push(system_param); - let mut access = FilteredAccess::matches_nothing(); - - access.add_write(component_id); - component_access_set.add(access); + // like `Res`/`ResMut`, an access matching nothing would be compatible with any other access + if *write { + component_access_set.add_resource_write(component_id); + written.insert(component_id); + } else { + component_access_set.add_resource_read(component_id); + } if subset.contains(&component_id) { initialization_errors.push(format!( "Duplicate resource access in system: {component_id:?}." @@ -879,7 +901,7 @@ impl System for DynamicScriptSystem

{ .iter() .map(|c| (c.component_id, c.type_registration().type_id())) .collect(); - let query = query.as_query_state::(world); + let query = query.as_query_state::(world, QueryAccess::Declared); // The access carries the query filters, so bevy runs this system in parallel with systems accessing // the same components on other archetypes (i.e. `With` vs `Without`). @@ -887,10 +909,14 @@ impl System for DynamicScriptSystem

{ // matched by this query in `run_unsafe`, see `ArchetypeFilter`. component_access_set.add(query.component_access().clone()); - let new_raids = get_all_access_ids(query.component_access().access()) - .into_iter() - .map(|(a, _)| a) - .collect::>(); + let accesses = get_all_access_ids(query.component_access().access()); + written.extend( + accesses + .iter() + .filter(|(_, write)| *write) + .map(|(id, _)| *id), + ); + let new_raids = accesses.into_iter().map(|(a, _)| a).collect::>(); if !subset.is_disjoint(&new_raids) { initialization_errors @@ -906,8 +932,15 @@ impl System for DynamicScriptSystem

{ } } - let final_subset = - AccessByteSet::from_allowed_list(&subset.iter().map(|c| c.index()).collect::>()); + // the guard must not allow writes which are declared to bevy as reads + let (read_write, read_only): (Vec<_>, Vec<_>) = subset + .iter() + .map(|c| (c.index(), written.contains(c))) + .partition(|(_, write)| *write); + let final_subset = AccessByteSet::from_allowed_lists( + &read_write.into_iter().map(|(c, _)| c).collect::>(), + &read_only.into_iter().map(|(c, _)| c).collect::>(), + ); self.state = Some(ScriptSystemState { type_registry: world.get_resource_or_init::().clone().0, @@ -1077,7 +1110,9 @@ mod test { schedule::{ScheduleLabel, Schedules}, }, }; - use bevy_mod_scripting_bindings::ScriptValue; + use bevy_mod_scripting_bindings::{ + ScriptComponentRegistration, ScriptTypeRegistration, ScriptValue, + }; use test_utils::make_test_plugin; use crate::{ @@ -1291,4 +1326,61 @@ mod test { 1 ); } + + #[derive(bevy_ecs::resource::Resource, Reflect, Default)] + struct AccessTestResource; + + #[derive(bevy_ecs::component::Component, Reflect)] + struct AccessTestComponent; + + fn access_of(world: &mut World, f: impl FnOnce(&mut ScriptSystemBuilder)) -> FilteredAccessSet { + let mut builder = ScriptSystemBuilder::new("test".to_owned().into(), attachment()); + f(&mut builder); + let mut system = DynamicScriptSystem::::new( + builder, + ScriptSystemSet(0), + Arc::new(AtomicBool::new(true)), + ScriptSystems::default(), + ); + match system.initialize(world) { + SystemAccess::Shared(access) => access, + _ => panic!("expected a non-exclusive system"), + } + } + + #[test] + fn read_only_parameters_are_declared_as_reads() { + let mut world = World::new(); + let resource = ScriptResourceRegistration::new( + ScriptTypeRegistration::new(Arc::new(bevy_reflect::TypeRegistration::of::< + AccessTestResource, + >())), + world.init_resource::(), + ); + let component = ScriptComponentRegistration::new( + ScriptTypeRegistration::new(Arc::new(bevy_reflect::TypeRegistration::of::< + AccessTestComponent, + >())), + world.register_component::(), + ); + let reader = |builder: &mut ScriptSystemBuilder| { + let mut query = ScriptQueryBuilder::new(); + query.component_read(component.clone()); + builder.resource_read(resource.clone()).query(query); + }; + let resource_writer = |builder: &mut ScriptSystemBuilder| { + builder.resource(resource.clone()); + }; + let component_writer = |builder: &mut ScriptSystemBuilder| { + let mut query = ScriptQueryBuilder::new(); + query.component(component.clone()); + builder.query(query); + }; + + let a = access_of(&mut world, reader); + let b = access_of(&mut world, reader); + assert!(a.is_compatible(&b), "readers should run in parallel"); + assert!(!a.is_compatible(&access_of(&mut world, resource_writer))); + assert!(!a.is_compatible(&access_of(&mut world, component_writer))); + } } diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 1896946cdd..c7c47f018f 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -1401,6 +1401,25 @@ impl ScriptQueryBuilder { V(builder) } + /// Adds a component to be retrieved by the query, which can only be read. + /// + /// Script systems reading a component can run in parallel with each other, unlike ones writing it. + /// + /// Arguments: + /// * `query`: The query to add the component to + /// * `component`: The component to add + /// Returns: + /// * `query`: The query with the component added + fn component_read( + query: V, + component: V, + ) -> V { + profiling::function_scope!("component_read"); + let mut builder = query.into_inner(); + builder.component_read(component.into_inner()); + V(builder) + } + /// Adds a component to filter the query by. This component will NOT be retrieved. /// /// Arguments: @@ -1630,6 +1649,25 @@ impl ScriptSystemBuilder { builder.into() } + /// Requests the system have read only access to the given resource. The resource will be added to the + /// list of arguments of the callback in the order they're provided. + /// + /// Script systems reading a resource can run in parallel with each other, unlike ones writing it. + /// Arguments: + /// * `builder`: The system builder to add the resource to. + /// * `resource`: The resource to add. + /// Returns: + /// * `builder`: The system builder with the resource added. + fn resource_read( + builder: V, + resource: V, + ) -> V { + profiling::function_scope!("resource_read"); + let mut builder = builder.into_inner(); + builder.resource_read(resource.into_inner()); + builder.into() + } + /// Specifies the system is to run exclusively, meaning it can access anything, but will not run in parallel with other systems. /// Arguments: /// * `builder`: The system builder to make exclusive. diff --git a/crates/bevy_mod_scripting_world/src/access_map.rs b/crates/bevy_mod_scripting_world/src/access_map.rs index ebb61d7435..a3b5cfc6aa 100644 --- a/crates/bevy_mod_scripting_world/src/access_map.rs +++ b/crates/bevy_mod_scripting_world/src/access_map.rs @@ -7,6 +7,8 @@ use bevy_ecs::component::ComponentId; use bevy_platform::collections::HashMap; use parking_lot::Mutex; +use bevy_ecs::schedule::FixedBitSet; + /// Describes access ranges in and outside a bevy world #[derive(Clone)] pub enum WorldAccessRange { @@ -101,8 +103,13 @@ impl AccessBitField { let conflicting_exclusive = one_is_exclusive && count != 0; let aliasing_exclusive_access = (kind_requested == 1) && (count != 0); let access_is_forbidden = self.is_forbidden(); + // another claim would wrap the count to 0, letting a write alias the existing reads + let count_is_full = count == Self::MAX_COUNT; - let allowed = !(conflicting_exclusive || aliasing_exclusive_access || access_is_forbidden); + let allowed = !(conflicting_exclusive + || aliasing_exclusive_access + || access_is_forbidden + || count_is_full); let allowed_u8 = allowed as u8; // Increment count only if allowed @@ -206,6 +213,9 @@ impl AccessBitField { pub struct AccessByteSet { new_capacity_forbidden: bool, data: Vec, + /// Accesses which can only be read, empty unless the set is a subset with read only accesses. + /// Kept outside of the [`AccessBitField`]s so that only write claims pay for checking it. + read_only: FixedBitSet, } #[profiling::all_functions] @@ -216,22 +226,44 @@ impl AccessByteSet { Self { data: Vec::new(), new_capacity_forbidden: false, + read_only: FixedBitSet::new(), } } - /// Generates a new access byte set with the given forbidden list + /// Generates a new access byte set allowing only the given accesses pub fn from_allowed_list(iter: &[usize]) -> Self { - let max_index = iter.iter().max().unwrap_or(&0); + Self::from_allowed_lists(iter, &[]) + } + + /// Generates a new access byte set allowing only the given accesses, the read only ones can't be written. + /// + /// The lists must be disjoint, callers split their accesses by whether they are written. + /// Every other access is forbidden. + pub fn from_allowed_lists(read_write: &[usize], read_only: &[usize]) -> Self { + debug_assert!( + read_only.iter().all(|i| !read_write.contains(i)), + "read only and read write accesses overlap" + ); + let max_index = read_write.iter().chain(read_only).max().unwrap_or(&0); let mut new = Self::new(); new.new_capacity_forbidden = true; new.ensure_capacity(*max_index); - for i in iter { - new.data[*i] = new.data[*i].with_forbidden(false) + for &i in read_write.iter().chain(read_only) { + new.data[i] = new.data[i].with_forbidden(false); + } + for &i in read_only { + new.read_only.grow_and_insert(i); } new } + #[inline] + /// Returns true if the access can only be read + pub fn is_read_only(&self, index: usize) -> bool { + self.read_only.contains(index) + } + fn ensure_capacity(&mut self, index: usize) { if index + 1 > self.data.len() { self.data.resize_with(index + 1, || { @@ -408,6 +440,9 @@ impl AccessMapInner { pub fn claim_component_if_free(&mut self, id: ComponentId, write: bool) -> bool { let idx = id.index(); + if write && self.component_access.is_read_only(idx) { + return false; + } let elem = self.component_access.entry_mut(idx); let (free, new) = elem.claim_if_free(write); *elem = new; @@ -1060,6 +1095,34 @@ mod test { assert!(!subset_access_map.claim_read_access(WorldAccessRange::Global)); } + #[test] + fn subset_map_read_only_accesses_cannot_be_written() { + let subset_access_map = + AccessMap::new_subset(AccessByteSet::from_allowed_lists(&[1], &[2])); + + assert!(!subset_access_map.claim_write_access(TestAccess(2))); + assert!(subset_access_map.claim_read_access(TestAccess(2))); + assert!(subset_access_map.claim_read_access(TestAccess(2))); + assert!(subset_access_map.claim_write_access(TestAccess(1))); + + // the permission survives releasing all accesses + subset_access_map.release_all_accesses(); + assert!(!subset_access_map.claim_write_access(TestAccess(2))); + } + + #[test] + fn access_counts_do_not_overflow() { + let access_map = AccessMap::default(); + let mut claimed = 0; + while access_map.claim_read_access(TestAccess(0)) { + claimed += 1; + assert!(claimed < 1000, "claims never fail"); + } + assert_eq!(claimed, 63); + // all reads are still counted, so writes stay blocked + assert!(!access_map.claim_write_access(TestAccess(0))); + } + #[test] fn subset_map_retains_subset_in_scope() { let subset_access_map = AccessMap::new_subset(AccessByteSet::from_allowed_list(&[1])); diff --git a/crates/bevy_mod_scripting_world/src/archetype_filter.rs b/crates/bevy_mod_scripting_world/src/archetype_filter.rs index a9ff970beb..c62b539ef8 100644 --- a/crates/bevy_mod_scripting_world/src/archetype_filter.rs +++ b/crates/bevy_mod_scripting_world/src/archetype_filter.rs @@ -29,18 +29,20 @@ use bevy_ecs::{archetype::ArchetypeId, component::ComponentId}; +use bevy_ecs::schedule::FixedBitSet; + /// Restricts access to components to entities in a set of archetypes, per component. /// /// Components without a restriction are not affected, i.e. resources and components accessed by exclusive guards. #[derive(Clone, Debug, Default)] pub struct ArchetypeFilter { - /// Restricted components, each with a bitset indexed by [`ArchetypeId::index`]. + /// Restricted components, each with the set of allowed [`ArchetypeId::index`]es. /// Systems restrict a handful of components, so a linear scan beats indexing by component id. - restricted: Vec<(ComponentId, Vec)>, + restricted: Vec<(ComponentId, FixedBitSet)>, } impl ArchetypeFilter { - fn bits_mut(&mut self, component_id: ComponentId) -> &mut Vec { + fn bits_mut(&mut self, component_id: ComponentId) -> &mut FixedBitSet { let index = match self .restricted .iter() @@ -48,7 +50,7 @@ impl ArchetypeFilter { { Some(index) => index, None => { - self.restricted.push((component_id, Vec::new())); + self.restricted.push((component_id, FixedBitSet::new())); self.restricted.len() - 1 } }; @@ -59,12 +61,8 @@ impl ArchetypeFilter { /// /// Once called for a component, the component is restricted to the archetypes allowed this way. pub fn allow(&mut self, component_id: ComponentId, archetype_id: ArchetypeId) { - let bits = self.bits_mut(component_id); - let (word, bit) = (archetype_id.index() / 64, archetype_id.index() % 64); - if word >= bits.len() { - bits.resize(word + 1, 0); - } - bits[word] |= 1 << bit; + self.bits_mut(component_id) + .grow_and_insert(archetype_id.index()); } /// Restricts the given component to no archetypes, until [`Self::allow`] is called for it. @@ -76,9 +74,7 @@ impl ArchetypeFilter { #[inline] pub fn is_allowed(&self, component_id: ComponentId, archetype_id: ArchetypeId) -> bool { match self.restricted.iter().find(|(id, _)| *id == component_id) { - Some((_, bits)) => bits - .get(archetype_id.index() / 64) - .is_some_and(|word| word & (1 << (archetype_id.index() % 64)) != 0), + Some((_, archetypes)) => archetypes.contains(archetype_id.index()), None => true, } } diff --git a/docs/src/ReleaseNotes/0.23-to-0.24.md b/docs/src/ReleaseNotes/0.23-to-0.24.md index d2be51fb19..a64dfd5732 100644 --- a/docs/src/ReleaseNotes/0.23-to-0.24.md +++ b/docs/src/ReleaseNotes/0.23-to-0.24.md @@ -63,6 +63,8 @@ Other changes: - `ScriptAttachedMessage(a)` / `ScriptDetachedMessage(a)` are one message, `ScriptLifecycleMessage::Attached(a)` / `::Detached(a)`, so attachments and detachments are processed in the order they were written. `GetScriptHandle::get_script_handle` returns an `Option` (`None`: the event doesn't wait for its script to load) and `ScriptLifecycleQueue::get_ready`, formerly known as `LoadedWithHandles` yields `(event, Option<(StrongScriptHandle, Language)>)`. - An attachment whose script is still loading holds back the attachments and detachments after it, and callbacks reach scripts in a deterministic order (attachment order, until scripts are removed), see [Determinism](../ScriptPipeline/pipeline.md#determinism). Lua's `math.random` is seeded with `0` in new contexts. - Script systems are identified by attachment and callback label (adding one again replaces it) and are removed with their script. `ScriptSystemSet` holds a recycled slot instead of the callback label, `ScriptSystemBuilder` no longer implements `IntoSystem` (use `ScriptSystemBuilder::build`), and their errors are sent as `ScriptErrorMessage`s instead of being logged. `WorldAccessGuard::new_non_exclusive` takes an `Option>`, restricting component accesses to the given archetypes (`None` for no restriction). +- Script system parameters are declared to Bevy as they're accessed: `resource` and query `component`s as writes (they were declared as reads, or as accessing nothing for resources, while scripts could write them), `resource_read` / `component_read` as reads. Script systems writing the same resources or components no longer run in parallel. `ScriptQueryBuilder::as_query_state` takes a `QueryAccess`: `Declared` declares the written components as written (for queries reported to the scheduler), `MatchOnly` declares all components as read (i.e. for `EntityRef` queries only finding entities). +- `AccessByteSet::from_allowed_lists(read_write, read_only)` creates subsets with read only accesses. At most 63 accesses to the same value can be claimed at once, claims beyond fail (the count used to overflow). - `WorldAccessRange::External` holds the value's owner (`AccessOwner`) instead of an id, and the access map keeps it alive until the claim is released. Build it from the value (`(&allocation_id).into()`); `WorldAccessRange` is no longer `Copy`. ### Language implementations diff --git a/docs/src/ScriptSystems/introduction.md b/docs/src/ScriptSystems/introduction.md index 19136e2958..3055fa86cd 100644 --- a/docs/src/ScriptSystems/introduction.md +++ b/docs/src/ScriptSystems/introduction.md @@ -75,6 +75,14 @@ will create a system which calls the specified callback `my_system` with 2 argum - With `components` access to ComponentA and ComponentB - The `ReflectReference` to `ResourceA` +`resource` and `component` give read and write access. Use `resource_read` and `component_read` for read only access, systems reading the same resources and components can run in parallel, while systems writing them cannot: + +```lua +bms.system_builder("my_system", attachment) + :query(world.query():component_read(ComponentA):component(ComponentB)) + :resource_read(Time) +``` + ## Exclusive systems An exclusive system can be created using the `exclusive` function call on the system builder.