From a564bc80abc67984fbbcc5770fbcbe8c46ec1e9b Mon Sep 17 00:00:00 2001 From: makspll Date: Thu, 22 Oct 2026 17:44:40 +0000 Subject: [PATCH 1/3] perf: entities as plain values, query results as plain lists - `ScriptValue::Entity(Entity)`: entities never allocate, `V` converts to and from it, entity fields read as values. Lua (`LuaEntity`) and Rune (`RuneEntity`) resolve its methods through `static_index(Entity)` - query results convert to lists of the entity followed by the components (`result[1]`, `result[2]` in Lua) instead of allocated references, the `entity()` / `components()` script functions are removed - `script_systems` macro benchmarks: iterating 1000 query results goes from 2.08ms to 1.27ms --- .../script_systems/query_system.lua | 16 ++++++ .../script_systems/query_system_iterate.lua | 19 +++++++ assets/scripts/game_of_life.lua | 2 +- assets/scripts/game_of_life.rn | 2 +- ...tional_components_and_disjoint_queries.lua | 12 ++-- ...read_only_parameters_cannot_be_written.lua | 2 +- ...ess_component_outside_query_archetypes.lua | 6 +- .../add_system/system_with_parameters.lua | 8 +-- .../add_system/system_with_parameters.rn | 8 +-- .../tests/query/dynamic_component_query.lua | 2 +- assets/tests/query/dynamic_component_query.rn | 2 +- .../query/query_can_access_components.lua | 14 ++--- .../query/query_can_access_components.rn | 12 ++-- .../query_returns_all_entities_matching.lua | 2 +- .../query_returns_all_entities_matching.rn | 2 +- .../script_callbacks/lua/script_callbacks.lua | 2 +- .../script_callbacks/rune/script_callbacks.rn | 2 +- benches/benchmarks.rs | 28 +++++++++- .../src/function/from.rs | 18 ++++++ .../src/function/from_ref.rs | 2 + .../src/function/into.rs | 14 ++++- .../src/function/into_ref.rs | 2 + .../bevy_mod_scripting_bindings/src/query.rs | 35 +++++++++++- .../src/script_value.rs | 6 ++ .../src/script_system.rs | 4 +- .../bevy_mod_scripting_functions/src/core.rs | 43 ++------------ .../src/lib.rs | 8 +-- .../src/focus.rs | 2 +- .../src/bindings/reference.rs | 39 ++++++++++++- .../src/bindings/script_value.rs | 6 +- .../bevy_mod_scripting_rune/src/types.rs | 42 +++++++++++++- .../bevy_mod_scripting_rune/src/value.rs | 9 ++- .../src/lib.rs | 56 +++++++++++++++++++ .../AddingLanguages/necessary-features.md | 3 +- docs/src/ReleaseNotes/0.23-to-0.24.md | 12 ++++ docs/src/ScriptSystems/introduction.md | 11 +++- 36 files changed, 358 insertions(+), 95 deletions(-) create mode 100644 assets/macro_benchmarks/script_systems/query_system.lua create mode 100644 assets/macro_benchmarks/script_systems/query_system_iterate.lua diff --git a/assets/macro_benchmarks/script_systems/query_system.lua b/assets/macro_benchmarks/script_systems/query_system.lua new file mode 100644 index 0000000000..69eee6927f --- /dev/null +++ b/assets/macro_benchmarks/script_systems/query_system.lua @@ -0,0 +1,16 @@ +-- a non-exclusive script system querying two components with an empty callback, measures building and passing the payload +local bms = import_namespace("bms") +local world = bms.world +local ComponentA = world.get_type_by_name("test_utils::test_data::CompWithFromWorldAndComponentData") +local ComponentB = world.get_type_by_name("test_utils::test_data::CompWithDefaultAndComponentData") + +function on_script_loaded() + world.add_system( + world.get_schedule_by_name("Update"), + bms.system_builder("bench_system", bms.ScriptAttachment.new_static_script(bms.current_script())) + :query(world.query():component(ComponentA):component_read(ComponentB)) + ) +end + +function bench_system(results) +end diff --git a/assets/macro_benchmarks/script_systems/query_system_iterate.lua b/assets/macro_benchmarks/script_systems/query_system_iterate.lua new file mode 100644 index 0000000000..c79fe974c9 --- /dev/null +++ b/assets/macro_benchmarks/script_systems/query_system_iterate.lua @@ -0,0 +1,19 @@ +-- a non-exclusive script system querying two components, measures building and passing the payload +local bms = import_namespace("bms") +local world = bms.world +local ComponentA = world.get_type_by_name("test_utils::test_data::CompWithFromWorldAndComponentData") +local ComponentB = world.get_type_by_name("test_utils::test_data::CompWithDefaultAndComponentData") + +function on_script_loaded() + world.add_system( + world.get_schedule_by_name("Update"), + bms.system_builder("bench_system", bms.ScriptAttachment.new_static_script(bms.current_script())) + :query(world.query():component(ComponentA):component_read(ComponentB)) + ) +end + +function bench_system(results) + for _, result in ipairs(results) do + local _, _ = result[2], result[3] + end +end diff --git a/assets/scripts/game_of_life.lua b/assets/scripts/game_of_life.lua index 020c917e37..c2655511ca 100644 --- a/assets/scripts/game_of_life.lua +++ b/assets/scripts/game_of_life.lua @@ -9,7 +9,7 @@ math.randomseed(os.time()) function fetch_life_state() -- find the first entity with life state local i, v = next(world.query():component(LifeState):build()) - return v:components()[1] + return v[2] end function on_script_loaded() diff --git a/assets/scripts/game_of_life.rn b/assets/scripts/game_of_life.rn index 366b207986..58e5b541d8 100644 --- a/assets/scripts/game_of_life.rn +++ b/assets/scripts/game_of_life.rn @@ -11,7 +11,7 @@ fn settings_type(world) { fn fetch_life_state(world) { // find the first entity with life state for result in world.query().component(life_state_type(world)).build() { - return result.components()[0]; + return result[1]; } } diff --git a/assets/tests/add_system/optional_components_and_disjoint_queries.lua b/assets/tests/add_system/optional_components_and_disjoint_queries.lua index 66595aec10..4b4ff23109 100644 --- a/assets/tests/add_system/optional_components_and_disjoint_queries.lua +++ b/assets/tests/add_system/optional_components_and_disjoint_queries.lua @@ -41,12 +41,12 @@ end function optional_system(query) local with_b, without_b = 0, 0 for _, result in pairs(query) do - local components = result:components() - assert(components[1] ~= nil, "the required component should be present") - if components[2] == nil then + -- the entity, then the components + assert(result[2] ~= nil, "the required component should be present") + if result[3] == nil then without_b = without_b + 1 else - assert(type(components[2][1]) == "string", "the optional component should be readable") + assert(type(result[3][1]) == "string", "the optional component should be readable") with_b = with_b + 1 end end @@ -57,8 +57,8 @@ end function disjoint_system(with_b, without_b) assert(#with_b >= 1 and #without_b >= 1, "expected results for both queries") -- `A` is written on entities with `B`, but only read on the others - with_b[1]:components()[1][1] = "written" - local ok, err = pcall(function() without_b[1]:components()[1][1] = "written" end) + with_b[1][2][1] = "written" + local ok, err = pcall(function() without_b[1][2][1] = "written" end) assert(not ok and tostring(err):find("writing the component", 1, true), tostring(err)) runs[#runs + 1] = "disjoint_system" end 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 index eae6e7f619..2460ade818 100644 --- a/assets/tests/add_system/read_only_parameters_cannot_be_written.lua +++ b/assets/tests/add_system/read_only_parameters_cannot_be_written.lua @@ -33,7 +33,7 @@ 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] + local component = query[1][2] assert(type(component[1]) == "string", "Expected to read the component, got: " .. tostring(component[1])) local ok, err = pcall(function() resource.string = "changed" end) diff --git a/assets/tests/add_system/system_cannot_access_component_outside_query_archetypes.lua b/assets/tests/add_system/system_cannot_access_component_outside_query_archetypes.lua index 0ce3229e9b..468177d2e8 100644 --- a/assets/tests/add_system/system_cannot_access_component_outside_query_archetypes.lua +++ b/assets/tests/add_system/system_cannot_access_component_outside_query_archetypes.lua @@ -38,8 +38,8 @@ end function check_accesses(query) assert(#query == 1, "Expected 1 result, got: " .. #query) local matched = query[1] - assert(matched:components()[1][1] == "Default", "the matched component should be accessible") - assert(world.get_component(matched:entity(), ComponentA)[1] == "Default", + assert(matched[2][1] == "Default", "the matched component should be accessible") + assert(world.get_component(matched[1], ComponentA)[1] == "Default", "the matched component should be accessible through the world too") assert_throws(function() @@ -49,7 +49,7 @@ function check_accesses(query) -- `with` filters by `B` without accessing it assert_throws(function() - local component = world.get_component(matched:entity(), ComponentB) + local component = world.get_component(matched[1], ComponentB) local _ = component[1] end, ".*annot claim access.*") end diff --git a/assets/tests/add_system/system_with_parameters.lua b/assets/tests/add_system/system_with_parameters.lua index 8bd83e9064..f92bebd305 100644 --- a/assets/tests/add_system/system_with_parameters.lua +++ b/assets/tests/add_system/system_with_parameters.lua @@ -45,10 +45,10 @@ function my_parameterised_system(resourceA, query, resourceB) assert(resourceB.string == "Initial Value", "Expected 'Initial Value', got: " .. resourceB.string) assert(#query == 2, "Expected 3 results, got: " .. #query) for i, result in pairs(query) do - components = result:components() - assert(#components == 2, "Expected 2 components, got " .. #components) - local componentA = components[1] - local componentB = components[2] + -- the entity, then the components + assert(#result == 3, "Expected the entity and 2 components, got " .. #result) + local componentA = result[2] + local componentB = result[3] assert(componentA[1] == "Default", "Expected 'Default', got: " .. componentA[1]) assert(componentB[1] == "Default", "Expected 'Default', got: " .. componentB[1]) end diff --git a/assets/tests/add_system/system_with_parameters.rn b/assets/tests/add_system/system_with_parameters.rn index e032898885..9a7c441a36 100644 --- a/assets/tests/add_system/system_with_parameters.rn +++ b/assets/tests/add_system/system_with_parameters.rn @@ -47,10 +47,10 @@ pub fn my_parameterised_system(resourceA, query, resourceB) { assert!(resourceB.string == "Initial Value", "Expected 'Initial Value', got: {}", resourceB.string); assert!(query.len() == 2, "Expected 2 results, got: {}", query.len()); for result in query { - let components = result.components(); - assert!(components.len() == 2, "Expected 2 components, got {}", components.len()); - let componentA = components[0]; - let componentB = components[1]; + // the entity, then the components + assert!(result.len() == 3, "Expected the entity and 2 components, got {}", result.len()); + let componentA = result[1]; + let componentB = result[2]; assert!(componentA[0] == "Default", "Expected 'Default', got: {}", componentA[0]); assert!(componentB[0] == "Default", "Expected 'Default', got: {}", componentB[0]); } diff --git a/assets/tests/query/dynamic_component_query.lua b/assets/tests/query/dynamic_component_query.lua index 8f85fcc986..65cb2abd13 100644 --- a/assets/tests/query/dynamic_component_query.lua +++ b/assets/tests/query/dynamic_component_query.lua @@ -6,7 +6,7 @@ world.add_default_component(entity_a, NewComponent) local found_entities = {} for i, result in pairs(world.query():component(NewComponent):build()) do - table.insert(found_entities, result:entity()) + table.insert(found_entities, result[1]) end assert(#found_entities == 1, "Expected 1 entities, got " .. #found_entities) diff --git a/assets/tests/query/dynamic_component_query.rn b/assets/tests/query/dynamic_component_query.rn index a3225ef7a2..6812d012de 100644 --- a/assets/tests/query/dynamic_component_query.rn +++ b/assets/tests/query/dynamic_component_query.rn @@ -7,7 +7,7 @@ pub fn main() { let found_entities = []; for result in world.query().component(NewComponent).build() { - found_entities.push(result.entity()); + found_entities.push(result[0]); } assert!(found_entities.len() == 1, "Expected 1 entities, got {}", found_entities.len()); diff --git a/assets/tests/query/query_can_access_components.lua b/assets/tests/query/query_can_access_components.lua index 9e60adf5c1..66fa91f793 100644 --- a/assets/tests/query/query_can_access_components.lua +++ b/assets/tests/query/query_can_access_components.lua @@ -16,13 +16,13 @@ local query_result = world.query():component(componentA):component(componentA):c assert(#query_result == 1, "Expected 1 result, got " .. #query_result) for i, result in pairs(query_result) do - assert(result:entity():index() == entity_a:index(), - "Expected entity_a, got " .. result:entity():index()) - components = result:components() - assert(#components == 3, "Expected 3 components, got " .. #components) - A = components[1] - B = components[2] - C = components[3] + assert(result[1]:index() == entity_a:index(), + "Expected entity_a, got " .. result[1]:index()) + -- the entity, then the components + assert(#result == 4, "Expected the entity and 3 components, got " .. #result) + A = result[2] + B = result[3] + C = result[4] assert(A[1] == "Default", "Expected 'Default', got: " .. A[1]) assert(B[1] == "Default", "Expected 'Default', got: " .. B[1]) assert(C.strings[1] == "asd", "Expected 'asd', got: " .. C.strings[1]) diff --git a/assets/tests/query/query_can_access_components.rn b/assets/tests/query/query_can_access_components.rn index 93401a75cb..9a8f3725c4 100644 --- a/assets/tests/query/query_can_access_components.rn +++ b/assets/tests/query/query_can_access_components.rn @@ -15,12 +15,12 @@ pub fn main() { assert!(query_result.len() == 1, "Expected 1 result, got {}", query_result.len()); for result in query_result { - assert!(result.entity().index() == entity_a.index(), "Expected entity_a, got {}", result.entity().index()); - let components = result.components(); - assert!(components.len() == 3, "Expected 3 components, got {}", components.len()); - let A = components[0]; - let B = components[1]; - let C = components[2]; + // the entity, then the components + assert!(result[0].index() == entity_a.index(), "Expected entity_a, got {}", result[0].index()); + assert!(result.len() == 4, "Expected the entity and 3 components, got {}", result.len()); + let A = result[1]; + let B = result[2]; + let C = result[3]; assert!(A[0] == "Default", "Expected 'Default', got: {}", A[0]); assert!(B[0] == "Default", "Expected 'Default', got: {}", B[0]); assert!(C.strings[0] == "asd", "Expected 'asd', got: {}", C.strings[0]); diff --git a/assets/tests/query/query_returns_all_entities_matching.lua b/assets/tests/query/query_returns_all_entities_matching.lua index 3d39fd5ffc..6b0648422f 100644 --- a/assets/tests/query/query_returns_all_entities_matching.lua +++ b/assets/tests/query/query_returns_all_entities_matching.lua @@ -15,7 +15,7 @@ world.add_default_component(entity_b, component_without) local found_entities = {} for i, result in pairs(world.query():component(component_with):without(component_without):build()) do - table.insert(found_entities, result:entity()) + table.insert(found_entities, result[1]) end assert(#found_entities == 3, "Expected 3 entities, got " .. #found_entities) diff --git a/assets/tests/query/query_returns_all_entities_matching.rn b/assets/tests/query/query_returns_all_entities_matching.rn index 38eaf0f381..cb76053fe5 100644 --- a/assets/tests/query/query_returns_all_entities_matching.rn +++ b/assets/tests/query/query_returns_all_entities_matching.rn @@ -16,7 +16,7 @@ pub fn main() { let found_entities = []; for result in world.query().component(component_with).without(component_without).build() { - found_entities.push(result.entity()); + found_entities.push(result[0]); } assert!(found_entities.len() == 3, "Expected 3 entities, got {}", found_entities.len()); diff --git a/assets/tests/script_callbacks/lua/script_callbacks.lua b/assets/tests/script_callbacks/lua/script_callbacks.lua index 8fd08219ec..c35e7bb009 100644 --- a/assets/tests/script_callbacks/lua/script_callbacks.lua +++ b/assets/tests/script_callbacks/lua/script_callbacks.lua @@ -10,7 +10,7 @@ end -- the other entity running this script local function other_entity() for _, result in pairs(world.query():component(world.get_type_by_name("bevy_mod_scripting_core::script::ScriptComponent")):build()) do - local e = result:entity() + local e = result[1] if e:index() ~= bms.current_entity():index() then return e end diff --git a/assets/tests/script_callbacks/rune/script_callbacks.rn b/assets/tests/script_callbacks/rune/script_callbacks.rn index 4e9a06c04f..4c692e0d19 100644 --- a/assets/tests/script_callbacks/rune/script_callbacks.rn +++ b/assets/tests/script_callbacks/rune/script_callbacks.rn @@ -9,7 +9,7 @@ fn other_entity() { let world = bms.world; let script_component = world.get_type_by_name("bevy_mod_scripting_core::script::ScriptComponent"); for result in world.query().component(script_component).build() { - let e = result.entity(); + let e = result[0]; if e.index() != bms.current_entity().index() { return e; } diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index a9ddd264df..d29f7b78ce 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -18,7 +18,8 @@ use regex::Regex; use script_integration_test_harness::{ make_test_lua_plugin, perform_benchmark_with_generator, run_lua_benchmark, run_plugin_callback_command_benchmark, run_plugin_callback_event_benchmark, - run_plugin_script_load_benchmark, test_functions::rand::Rng, + run_plugin_script_load_benchmark, run_plugin_script_system_benchmark, + test_functions::rand::Rng, }; #[cfg(feature = "rune")] use script_integration_test_harness::{make_test_rune_plugin, run_rune_benchmark}; @@ -307,6 +308,30 @@ fn callback_benchmarks(criterion: &mut Criterion) { run_plugin_callback_command_benchmark(make_test_rune_plugin, "command Rune", rune, &mut group); } +/// benchmarks running a script system querying the components of many entities, measures building and passing the payload +fn script_system_benchmarks(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("script_systems"); + let lua = include_str!("../assets/macro_benchmarks/script_systems/query_system.lua"); + let lua_iterate = + include_str!("../assets/macro_benchmarks/script_systems/query_system_iterate.lua"); + for entity_count in [10, 100, 1000] { + run_plugin_script_system_benchmark( + make_test_lua_plugin, + &format!("query {entity_count} entities Lua"), + lua, + entity_count, + &mut group, + ); + run_plugin_script_system_benchmark( + make_test_lua_plugin, + &format!("query and iterate {entity_count} entities Lua"), + lua_iterate, + entity_count, + &mut group, + ); + } +} + pub fn benches() { maybe_with_profiler(|_profiler| { let mut criterion: criterion::Criterion<_> = (criterion::Criterion::default()) @@ -333,6 +358,7 @@ pub fn benches() { conversion_benchmarks(&mut criterion); script_load_benchmarks(&mut criterion); callback_benchmarks(&mut criterion); + script_system_benchmarks(&mut criterion); }); } criterion_main!(benches); diff --git a/crates/bevy_mod_scripting_bindings/src/function/from.rs b/crates/bevy_mod_scripting_bindings/src/function/from.rs index 5587f7ce0b..e4a9c11f48 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/from.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/from.rs @@ -6,6 +6,7 @@ use crate::{ EnumVariantRef, ReflectReference, ScriptValue, construct::construct_reference, error::InteropError, script_value::VariadicTuple, }; +use bevy_ecs::entity::Entity; use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{FromReflect, Reflect}; @@ -276,6 +277,11 @@ impl FromScript for V { }, } } + ScriptValue::Entity(entity) if TypeId::of::() == TypeId::of::() => { + T::from_reflect(&entity).map(V).ok_or_else(|| { + InteropError::failed_from_reflect(Some(TypeId::of::()), "entity".to_owned()) + }) + } _ => Err(InteropError::value_mismatch( std::any::TypeId::of::(), value, @@ -284,6 +290,18 @@ impl FromScript for V { } } +#[profiling::all_functions] +impl FromScript for Entity { + type This<'w> = Self; + fn from_script(value: ScriptValue, world: WorldGuard) -> Result { + match value { + ScriptValue::Entity(entity) => Ok(entity), + // i.e. an entity field of a component + value => V::::from_script(value, world).map(|v| v.0), + } + } +} + /// A wrapper around a reference to a value of type `T`. /// /// This can be used to retrieve a reference out of a [`ScriptValue::Reference`] corresponding to the type `T`. diff --git a/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs b/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs index b610498222..9715ceb248 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs @@ -4,6 +4,7 @@ use crate::{ FromScript, ScriptValue, error::InteropError, match_by_type, reflection_extensions::TypeInfoExtensions, }; +use bevy_ecs::entity::Entity; use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::{ PartialReflect, ReflectKind, @@ -61,6 +62,7 @@ impl FromScriptErased for Box { tr : PathBuf => return ::from_script(value, world).map(|a| Box::new(a) as _), ts : OsString=> return ::from_script(value, world).map(|a| Box::new(a) as _), tsv: ScriptValue => return ::from_script(value, world).map(|a| Box::new(a) as _), + tent: Entity => return ::from_script(value, world).map(|a| Box::new(a) as _), tn : () => return <()>::from_script(value, world).map(|a| Box::new(a) as _) } ); diff --git a/crates/bevy_mod_scripting_bindings/src/function/into.rs b/crates/bevy_mod_scripting_bindings/src/function/into.rs index 518fd5eba7..b70a691e3b 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/into.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/into.rs @@ -2,10 +2,11 @@ use super::{DynamicScriptFunction, DynamicScriptFunctionMut, Union, V}; use crate::{ReflectReference, ScriptValue, VariadicTuple, error::InteropError}; +use bevy_ecs::entity::Entity; use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; -use std::{borrow::Cow, collections::VecDeque, ffi::OsString, path::PathBuf}; +use std::{any::Any, borrow::Cow, collections::VecDeque, ffi::OsString, path::PathBuf}; /// Converts a value into a [`ScriptValue`]. pub trait IntoScript { @@ -114,6 +115,10 @@ impl IntoScript for ReflectReference { #[profiling::all_functions] impl IntoScript for V { fn into_script(self, _world: WorldGuard) -> Result { + // entities are passed by value, the check is resolved at compile time + if let Some(entity) = (&self.0 as &dyn Any).downcast_ref::() { + return Ok(ScriptValue::Entity(*entity)); + } // one heap allocation, holding the value and its reference counts Ok(ScriptValue::Reference(ReflectReference::new_allocated( self.0, @@ -121,6 +126,13 @@ impl IntoScript for V { } } +#[profiling::all_functions] +impl IntoScript for Entity { + fn into_script(self, _world: WorldGuard) -> Result { + Ok(ScriptValue::Entity(self)) + } +} + #[profiling::all_functions] impl IntoScript for Option { fn into_script(self, world: WorldGuard) -> Result { diff --git a/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs b/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs index c0fafb3223..5d58df776e 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs @@ -2,6 +2,7 @@ use std::{borrow::Cow, ffi::OsString, path::PathBuf}; +use bevy_ecs::entity::Entity; use bevy_mod_scripting_display::OrFakeId; use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::PartialReflect; @@ -112,6 +113,7 @@ fn into_script_erased(r: &dyn PartialReflect, world: WorldGuard) -> Result return value(downcast_into_value!(r, PathBuf).clone().into_script(world)), ts : OsString=> return value(downcast_into_value!(r, OsString).clone().into_script(world)), tsv: ScriptValue=> return Ok(Erased::Value(downcast_into_value!(r, ScriptValue).clone())), + tent: Entity => return Ok(Erased::Value(ScriptValue::Entity(downcast_into_value!(r, Entity)))), tn : () => return Ok(Erased::Value(ScriptValue::Unit)) } ); diff --git a/crates/bevy_mod_scripting_bindings/src/query.rs b/crates/bevy_mod_scripting_bindings/src/query.rs index 95c01768a5..8745b8c455 100644 --- a/crates/bevy_mod_scripting_bindings/src/query.rs +++ b/crates/bevy_mod_scripting_bindings/src/query.rs @@ -5,7 +5,8 @@ use bevy_mod_scripting_world::WorldGuard; use super::{DynamicComponent, ReflectReference}; use crate::{ - PartialReflectExt, ScriptValue, WorldExtensions, + GetTypeDependencies, IntoScript, PartialReflectExt, ScriptValue, ThroughTypeInfo, TypedThrough, + WorldExtensions, error::InteropError, function::{ from::{Union, V}, @@ -25,7 +26,7 @@ use ::{ PartialReflect, Reflect, ReflectFromReflect, TypeRegistration, std_traits::ReflectDefault, }, }; -use std::{any::TypeId, sync::Arc}; +use std::{any::TypeId, collections::VecDeque, sync::Arc}; /// A reference to a type which is not a `Resource` or `Component`. /// @@ -666,9 +667,39 @@ impl ScriptQueryBuilder { #[derive(Clone, Reflect)] #[reflect(opaque)] /// A result from a query. +/// +/// Scripts receive it as a plain list, the entity followed by the components in the order they were added to the query, +/// i.e. `{entity, a, b}` in Lua, with nil for absent optional components. Nothing is allocated for the result itself. pub struct ScriptQueryResult { /// The entity that matched the query. pub entity: Entity, /// The components that matched the query, `None` for absent optional components. pub components: Vec>, } + +impl IntoScript for ScriptQueryResult { + fn into_script(self, _world: WorldGuard) -> Result { + let mut values = VecDeque::with_capacity(self.components.len() + 1); + values.push_back(ScriptValue::Entity(self.entity)); + values.extend( + self.components + .into_iter() + .map(|c| c.map_or(ScriptValue::Unit, ScriptValue::Reference)), + ); + Ok(ScriptValue::List(values)) + } +} + +impl TypedThrough for ScriptQueryResult { + fn through_type_info() -> ThroughTypeInfo { + // the entity followed by the components + as TypedThrough>::through_type_info() + } +} + +impl GetTypeDependencies for ScriptQueryResult { + type Underlying = Self; + fn register_type_dependencies(registry: &mut bevy_reflect::TypeRegistry) { + registry.register::(); + } +} diff --git a/crates/bevy_mod_scripting_bindings/src/script_value.rs b/crates/bevy_mod_scripting_bindings/src/script_value.rs index 943824d1e6..c9f58a1c2e 100644 --- a/crates/bevy_mod_scripting_bindings/src/script_value.rs +++ b/crates/bevy_mod_scripting_bindings/src/script_value.rs @@ -1,6 +1,7 @@ //! This module contains the `ScriptValue` enum which is used to pass values between scripting languages and Rust. use crate::error::InteropError; +use bevy_ecs::entity::Entity; use bevy_mod_scripting_derive::DebugWithTypeInfo; use bevy_mod_scripting_display::{DisplayWithTypeInfo, ReflectDisplayWithTypeInfo, WithTypeInfo}; use bevy_mod_scripting_world::WorldGuard; @@ -43,6 +44,8 @@ pub enum ScriptValue { /// /// Unlike a reference, a type carries no data, so it never allocates. Type(TypeId), + /// Represents an entity, passed by value so it never allocates, scripts call the functions of `Entity` on it. + Entity(Entity), /// A dynamic script function possibly storing state. Preffer using the [`ScriptValue::Function`] variant instead if possible. FunctionMut(DynamicScriptFunctionMut), /// A stateless dynamic script function @@ -111,6 +114,7 @@ impl DisplayWithTypeInfo for ScriptValue { } ScriptValue::Reference(v) => v.display_with_type_info(f, type_info_provider), ScriptValue::Type(v) => v.display_with_type_info(f, type_info_provider), + ScriptValue::Entity(v) => write!(f, "{v}"), ScriptValue::FunctionMut(v) => v.display_with_type_info(f, type_info_provider), ScriptValue::Function(v) => v.display_with_type_info(f, type_info_provider), ScriptValue::Error(e) => e.display_with_type_info(f, type_info_provider), @@ -130,6 +134,7 @@ impl PartialEq for ScriptValue { (Self::Map(l0), Self::Map(r0)) => l0 == r0, (Self::Reference(l0), Self::Reference(r0)) => l0 == r0, (Self::Type(l0), Self::Type(r0)) => l0 == r0, + (Self::Entity(l0), Self::Entity(r0)) => l0 == r0, // explicitly don't compare functions and errors _ => false, } @@ -158,6 +163,7 @@ impl ScriptValue { ScriptValue::Tuple(_) => "Tuple".to_owned(), ScriptValue::Reference(_) => "Reference".to_owned(), ScriptValue::Type(_) => "Type".to_owned(), + ScriptValue::Entity(_) => "Entity".to_owned(), ScriptValue::FunctionMut(_) => "FunctionMut".to_owned(), ScriptValue::Function(_) => "Function".to_owned(), ScriptValue::Error(_) => "Error".to_owned(), diff --git a/crates/bevy_mod_scripting_core/src/script_system.rs b/crates/bevy_mod_scripting_core/src/script_system.rs index 7e3f271583..c28beeb82f 100644 --- a/crates/bevy_mod_scripting_core/src/script_system.rs +++ b/crates/bevy_mod_scripting_core/src/script_system.rs @@ -41,7 +41,7 @@ use bevy_mod_scripting_bindings::{ AppScheduleRegistry, AppScriptCallbackRunners, AppScriptComponentRegistry, AppScriptFunctionRegistry, AppScriptTypeCatalogue, CurrentScriptAttachment, InteropError, IntoScript, ReflectReference, ScriptComponentRegistration, ScriptQueryBuilder, - ScriptResourceRegistration, V, WorldExtensions, + ScriptResourceRegistration, WorldExtensions, }; use bevy_mod_scripting_display::DisplayProxy; use bevy_mod_scripting_script::ScriptAttachment; @@ -1185,7 +1185,7 @@ impl System for DynamicScriptSystem

{ .filter_map(|entity| { let entity = world.get_entity(entity).ok()?; // Safety: the components with change filters are declared as read - unsafe { builder.result_for(entity, last_run, this_run) }.map(V) + unsafe { builder.result_for(entity, last_run, this_run) } }) .collect::>(); diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 45d404324a..70ac5faaa4 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -1528,6 +1528,9 @@ impl ScriptQueryBuilder { /// Builds the query and retrieves the entities and component references. /// + /// Each result is a list of the entity followed by the components in the order they were added to the query, + /// i.e. `local entity, a, b = result[1], result[2], result[3]` in Lua, absent optional components are nil. + /// /// Arguments: /// * `ctxt`: The function call context. /// * `query`: The query to build. @@ -1536,46 +1539,11 @@ impl ScriptQueryBuilder { fn build( ctxt: FunctionCallContext, query: V, - ) -> Result>, InteropError> { + ) -> Result, InteropError> { profiling::function_scope!("build"); let world = ctxt.world()?; let builder = query.into_inner(); - let result = world.query(builder)?; - let result = result.into_iter().map(V).collect::>(); - Ok(result) - } -} - -#[script_bindings( - remote, - bms_bindings_path = "bevy_mod_scripting_bindings", - name = "script_query_result_functions", - core, - namespace = "bms" -)] -impl ScriptQueryResult { - /// Retrieves the entity from the query result. - /// - /// Arguments: - /// * `query`: The query result to retrieve the entity from. - /// Returns: - /// * `entity`: The entity from the query result. - fn entity(query: R) -> V { - profiling::function_scope!("entity"); - V::new(query.entity) - } - - /// Retrieves the components from the query result. - /// - /// These are ordered by the order they were added to the query, absent optional components are nil. - /// - /// Arguments: - /// * `query`: The query result to retrieve the components from. - /// Returns: - /// * `components`: The components from the query result. - fn components(query: R) -> Vec> { - profiling::function_scope!("components"); - query.components.to_vec() + Ok(world.query(builder)?.into()) } } @@ -2374,7 +2342,6 @@ pub fn register_core_functions(app: &mut App) { register_script_event_registration_functions(world); register_script_query_builder_functions(world); - register_script_query_result_functions(world); register_reflect_schedule_functions(world); register_reflect_system_functions(world); diff --git a/crates/bindings/bevy_mod_scripting_bevy_ecs_bindings/src/lib.rs b/crates/bindings/bevy_mod_scripting_bevy_ecs_bindings/src/lib.rs index 572e7719ab..c4fb2c4a38 100644 --- a/crates/bindings/bevy_mod_scripting_bevy_ecs_bindings/src/lib.rs +++ b/crates/bindings/bevy_mod_scripting_bevy_ecs_bindings/src/lib.rs @@ -53,19 +53,19 @@ impl Plugin for BevyEcsBindingsPlugin { )] impl Entity { /// The index of the entity. Indices of despawned entities are reused, so compare whole entities to tell them apart. - fn index(_self: R) -> u32 { + fn index(_self: V) -> u32 { _self.index_u32() } /// How many times the index of this entity has been reused. - fn generation(_self: R) -> u32 { + fn generation(_self: V) -> u32 { _self.generation().to_bits() } /// The entity as a single number, which can be turned back into the entity with [`from_bits`](Self::from_bits). /// /// Only meaningful while the app is running, don't save it. - fn to_bits(_self: R) -> u64 { + fn to_bits(_self: V) -> u64 { _self.to_bits() } @@ -75,7 +75,7 @@ impl Entity { } /// Whether two entities are the same. - fn eq(_self: R, other: R) -> bool { + fn eq(_self: V, other: V) -> bool { *_self == *other } } diff --git a/crates/bindings/bevy_mod_scripting_bevy_input_bindings/src/focus.rs b/crates/bindings/bevy_mod_scripting_bevy_input_bindings/src/focus.rs index 0c823439f8..87aaddf7f3 100644 --- a/crates/bindings/bevy_mod_scripting_bevy_input_bindings/src/focus.rs +++ b/crates/bindings/bevy_mod_scripting_bevy_input_bindings/src/focus.rs @@ -15,7 +15,7 @@ use bevy_mod_scripting_derive::script_bindings; )] impl InputFocus { /// Gives the focus to the entity, so it receives keyboard input. - fn set(mut _self: M, entity: R) { + fn set(mut _self: M, entity: V) { _self.set(*entity, FocusCause::Navigated); } diff --git a/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs b/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs index 2939fb9dda..4f17695556 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs @@ -1,6 +1,9 @@ use std::{any::TypeId, borrow::Cow}; -use bevy_mod_scripting_bindings::{ReflectReference, WorldGuard, dispatch, error::InteropError}; +use bevy_ecs::entity::Entity; +use bevy_mod_scripting_bindings::{ + ReflectReference, ScriptValue, WorldGuard, dispatch, error::InteropError, +}; use bevy_mod_scripting_bindings_domain::ScriptOperatorNames; use bevy_mod_scripting_world::ThreadWorldContainer; use mlua::{FromLua, IntoLua, MetaMethod, UserData, UserDataMethods}; @@ -177,3 +180,37 @@ impl UserData for LuaTypeRef { ); } } + +/// An entity, i.e. what [`bevy_mod_scripting_bindings::ScriptValue::Entity`] is in Lua. Passed by value, its methods are the functions of `Entity`, i.e. `entity:index()`. +#[derive(Debug, Clone, Copy, PartialEq, mlua::FromLua)] +pub struct LuaEntity(pub Entity); + +impl UserData for LuaEntity { + fn add_methods>(m: &mut T) { + m.add_meta_function( + MetaMethod::Index, + |lua, (_, key): (LuaEntity, LuaScriptValue)| { + profiling::function_scope!("MetaMethod::Index"); + let member = with_world(|world| { + dispatch::static_index( + world, + TypeId::of::(), + key.into(), + LUA_CALLER_CONTEXT, + ) + })?; + match member { + // `entity:method()` passes the entity as the first argument + ScriptValue::Function(function) => registered_function_into_lua(lua, function), + other => LuaScriptValue(other).into_lua(lua), + } + }, + ); + m.add_meta_function(MetaMethod::Eq, |_, (a, b): (LuaEntity, LuaEntity)| { + Ok(a.0 == b.0) + }); + m.add_meta_function(MetaMethod::ToString, |_, self_: LuaEntity| { + Ok(self_.0.to_string()) + }); + } +} diff --git a/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs b/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs index b319e90962..2d3b0a3c9c 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs @@ -15,7 +15,7 @@ use mlua::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, MultiValue, Value}; use crate::{IntoMluaError, LuaContextAppData}; -use super::reference::{LuaReflectReference, LuaTypeRef}; +use super::reference::{LuaEntity, LuaReflectReference, LuaTypeRef}; /// A wrapper around many [`ScriptValue`]'s, used where the potential to return/receive many values separately arises. pub struct MultiLuaScriptValue(pub VecDeque); @@ -168,6 +168,9 @@ impl FromLua for LuaScriptValue { if let Ok(type_ref) = ud.borrow::() { return Ok(LuaScriptValue(ScriptValue::Type(type_ref.0))); } + if let Ok(entity) = ud.borrow::() { + return Ok(LuaScriptValue(ScriptValue::Entity(entity.0))); + } let ud = ud.borrow::().map_err(|e| { mlua::Error::FromLuaConversionError { from: "UserData", @@ -275,6 +278,7 @@ impl IntoLua for LuaScriptValue { ScriptValue::String(s) => Value::String(lua.create_string(s.as_ref())?), ScriptValue::Reference(r) => LuaReflectReference::from(r).into_lua(lua)?, ScriptValue::Type(type_id) => LuaTypeRef(type_id).into_lua(lua)?, + ScriptValue::Entity(entity) => LuaEntity(entity).into_lua(lua)?, ScriptValue::Error(script_error) => return Err(mlua::Error::external(script_error)), ScriptValue::Function(function) => lua .create_function(move |lua, args: MultiLuaScriptValue| { diff --git a/crates/languages/bevy_mod_scripting_rune/src/types.rs b/crates/languages/bevy_mod_scripting_rune/src/types.rs index 0fcf983209..10b28f814a 100644 --- a/crates/languages/bevy_mod_scripting_rune/src/types.rs +++ b/crates/languages/bevy_mod_scripting_rune/src/types.rs @@ -6,6 +6,7 @@ //! the index protocol, which gets the name as a value. use std::{any::TypeId, borrow::Cow, cmp::Ordering, collections::BTreeSet, sync::Arc}; +use bevy_ecs::entity::Entity; use bevy_mod_scripting_bindings::{ InteropError, ReflectReference, ScriptValue, WorldGuard, dispatch::{self, Member}, @@ -54,6 +55,11 @@ pub struct RuneRef(pub ReflectReference); #[rune(item = ::bms, name = Type)] pub struct RuneType(pub TypeId); +/// An entity, passed by value, its methods are the functions of `Entity`, i.e. `entity.index()`, see [`ScriptValue::Entity`]. +#[derive(Any, Clone, Copy)] +#[rune(item = ::bms, name = Entity)] +pub struct RuneEntity(pub Entity); + /// The values of a namespace, as returned by `import_namespace`, whose functions can be called with the method syntax. #[derive(Any, Clone)] #[rune(item = ::bms, name = Namespace)] @@ -182,6 +188,18 @@ fn call_member_inner( RUNE_CALLER_CONTEXT, )?; call_value(member, args)? + } else if let Ok(this) = receiver.borrow_ref::() { + let member = dispatch::static_index( + world()?, + TypeId::of::(), + ScriptValue::String(name.to_owned().into()), + RUNE_CALLER_CONTEXT, + )?; + // the entity is the first argument of its methods + let mut with_receiver = Vec::with_capacity(args.len() + 1); + with_receiver.push(ScriptValue::Entity(this.0)); + with_receiver.extend(args); + call_value(member, with_receiver)? } else if let Ok(this) = receiver.borrow_ref::() { let member = this.0.get(name).cloned().ok_or_else(|| { InteropError::external(crate::error::RuneError(format!( @@ -360,6 +378,7 @@ pub fn register_types( ) -> Result<(), ContextError> { module.ty::()?; module.ty::()?; + module.ty::()?; module.ty::()?; module.ty::()?; module.ty::()?; @@ -419,6 +438,20 @@ pub fn register_types( .map(|other| other.0 == this.0) .unwrap_or(false) })?; + module.associated_function(&Protocol::PARTIAL_EQ, |this: &RuneEntity, other: Value| { + other + .borrow_ref::() + .map(|other| other.0 == this.0) + .unwrap_or(false) + })?; + module.associated_function( + &Protocol::DISPLAY_FMT, + |this: &RuneEntity, f: &mut Formatter| rune::vm_write!(f, "{}", this.0), + )?; + module.associated_function( + &Protocol::DEBUG_FMT, + |this: &RuneEntity, f: &mut Formatter| rune::vm_write!(f, "{:?}", this.0), + )?; module.associated_function( &Protocol::PARTIAL_CMP, |this: &RuneRef, other: Value| -> VmResult> { @@ -515,7 +548,12 @@ pub fn register_types( for &name in names { // `v.name(..)` let convert = converter(name); - for container in [Container::Ref, Container::Type, Container::Namespace] { + for container in [ + Container::Ref, + Container::Type, + Container::Entity, + Container::Namespace, + ] { let builder = module.raw_function( name, call_raw(move |values: &[Value]| call_member_inner(name, convert, values)), @@ -523,6 +561,7 @@ pub fn register_types( match container { Container::Ref => builder.build_associated::()?, Container::Type => builder.build_associated::()?, + Container::Entity => builder.build_associated::()?, Container::Namespace => builder.build_associated::()?, }; } @@ -567,5 +606,6 @@ pub fn register_types( enum Container { Ref, Type, + Entity, Namespace, } diff --git a/crates/languages/bevy_mod_scripting_rune/src/value.rs b/crates/languages/bevy_mod_scripting_rune/src/value.rs index 0195ea4774..051a8fda35 100644 --- a/crates/languages/bevy_mod_scripting_rune/src/value.rs +++ b/crates/languages/bevy_mod_scripting_rune/src/value.rs @@ -14,7 +14,7 @@ use rune::{ use crate::{ error::{IntoInteropError, RuneError, describe_vm_error}, - types::{RuneFunction, RuneNamespace, RuneRef, RuneType, ScriptFunction}, + types::{RuneEntity, RuneFunction, RuneNamespace, RuneRef, RuneType, ScriptFunction}, }; /// The context for calling a function from Rune @@ -65,6 +65,7 @@ pub fn to_rune(value: ScriptValue) -> Result { } ScriptValue::Reference(reference) => new_value(RuneRef(reference))?, ScriptValue::Type(type_id) => new_value(RuneType(type_id))?, + ScriptValue::Entity(entity) => new_value(RuneEntity(entity))?, ScriptValue::Function(function) => function_to_rune(function, None)?, ScriptValue::FunctionMut(function) => { new_value(RuneFunction(ScriptFunction::Mut(function)))? @@ -162,6 +163,12 @@ pub fn from_rune(value: &Value) -> Result { .map_err(IntoInteropError::to_bms_error)?; return Ok(ScriptValue::Type(type_ref.0)); } + if hash == ::HASH { + let entity = value + .borrow_ref::() + .map_err(IntoInteropError::to_bms_error)?; + return Ok(ScriptValue::Entity(entity.0)); + } if hash == ::HASH { let function = value .borrow_ref::() diff --git a/crates/testing_crates/script_integration_test_harness/src/lib.rs b/crates/testing_crates/script_integration_test_harness/src/lib.rs index dec46075af..42bf1e6846 100644 --- a/crates/testing_crates/script_integration_test_harness/src/lib.rs +++ b/crates/testing_crates/script_integration_test_harness/src/lib.rs @@ -510,6 +510,62 @@ pub fn run_plugin_callback_command_benchmark< }); } +/// Benchmarks running `Update`, in which the script adds a script system querying the components of the given amount of entities. +pub fn run_plugin_script_system_benchmark< + P: IntoScriptPluginParams + Plugin, + F: Fn() -> P, + M: Measurement, +>( + plugin_maker: F, + benchmark_id: &str, + content: &str, + entity_count: usize, + criterion: &mut criterion::BenchmarkGroup, +) { + use test_utils::test_data::{ + CompWithDefaultAndComponentData, CompWithFromWorldAndComponentData, + }; + + let mut app = setup_integration_test(|_, _| {}); + install_test_plugin(&mut app, false); + app.add_plugins(plugin_maker()); + app.finish(); + app.cleanup(); + let world = app.world_mut(); + for _ in 0..entity_count { + world.spawn(( + CompWithFromWorldAndComponentData("a".into()), + CompWithDefaultAndComponentData("b".into()), + )); + } + let id = world + .get_resource_or_init::>() + .add(ScriptAsset { + content: content.as_bytes().into(), + language: P::LANGUAGE, + }); + AttachScript::

::new(ScriptAttachment::StaticScript(id)).apply(world); + // loads the script, which adds the system, which runs once + app.update(); + let world = app.world_mut(); + let errors = world + .resource::>() + .iter_current_update_messages() + .collect::>(); + assert!(errors.is_empty(), "benchmark setup failed: {errors:?}"); + let added = world + .resource::() + .get(Update) + .and_then(|schedule| schedule.systems().ok()) + .is_some_and(|mut systems| systems.any(|(_, s)| s.name().to_string() == "bench_system")); + assert!(added, "the script did not add its system"); + + let span = tracing::trace_span!("bench_fn"); + criterion.bench_function(benchmark_id, |b| { + b.iter(|| span.in_scope(|| world.run_schedule(Update))); + }); +} + pub fn perform_benchmark_with_generator< M: Measurement, I, diff --git a/docs/src/Development/AddingLanguages/necessary-features.md b/docs/src/Development/AddingLanguages/necessary-features.md index 0c0dc8d301..908488a6f9 100644 --- a/docs/src/Development/AddingLanguages/necessary-features.md +++ b/docs/src/Development/AddingLanguages/necessary-features.md @@ -4,9 +4,10 @@ In order for a language to be called "implemented" in BMS, it needs to support t ## Values -Every `ScriptValue` variant needs a counterpart in the language, in both directions. Two of them are handles rather than data: +Every `ScriptValue` variant needs a counterpart in the language, in both directions. Three of them are handles rather than data: - `ScriptValue::Reference`: a reference to a reflected value, usually a userdata/custom type wrapping `ReflectReference`. - `ScriptValue::Type`: a type itself, i.e. what scripts use to reach a type's static members. It carries only a `TypeId` and never allocates. +- `ScriptValue::Entity`: an entity, passed by value so it never allocates. Its methods are the functions of `Entity`, found with `dispatch::static_index(world, TypeId::of::(), ..)` and called with the entity as the first argument, and two entities are equal if they are the same entity. Every context gets the builtins as globals, nothing else: `bevy_mod_scripting_bindings::builtins::builtins()` (`import_namespace`, `export_namespace`) plus the language's own `register_callback`: diff --git a/docs/src/ReleaseNotes/0.23-to-0.24.md b/docs/src/ReleaseNotes/0.23-to-0.24.md index fb59d28e5b..8c523fbcb3 100644 --- a/docs/src/ReleaseNotes/0.23-to-0.24.md +++ b/docs/src/ReleaseNotes/0.23-to-0.24.md @@ -30,6 +30,17 @@ BMS' functions moved to `bms`: `construct`, `system_builder`, `run_script_callba + bms.current_script() ``` +Query results are plain lists of the entity followed by the components, `result:entity()` and `result:components()` are removed: + +```diff + for _, result in pairs(world.query():component(Transform):build()) do +- local entity, transform = result:entity(), result:components()[1] ++ local entity, transform = result[1], result[2] + end +``` + +Entities are plain values (`ScriptValue::Entity`), compared with `==` and with the same methods (`entity:index()`), also when read from a component field. + Other changes: - `world.get_type_by_name` only takes full paths, i.e. `"bevy_transform::components::transform::Transform"` or `"script.my_game.Enemy"`. - Numbers work on the left of operators: `2 * v`, `1 - v` (dispatched to `mul`, `rsub`, ...), in Rhai too. @@ -64,6 +75,7 @@ Other changes: - 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 `declare_writes`, `false` declares all components as read (i.e. for `EntityRef` queries). +- `ScriptValue::Entity(Entity)` is new, language implementations convert it to a value whose methods are the functions of `Entity` (see `LuaEntity` / `RuneEntity`). `V` converts to and from it, `R` / `M` only accept references, take `V` instead. `ScriptQueryResult` converts to a list instead of a reference, its script functions are removed. - `ScriptQueryResult::components` is a `Vec>`, `None` for absent optional components. `ArchetypeFilter::allow` / `is_allowed` and `WorldAccessGuard::check_archetype_access` take whether the component is written. - `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`. diff --git a/docs/src/ScriptSystems/introduction.md b/docs/src/ScriptSystems/introduction.md index ed7e1a0d41..1dc69f5d62 100644 --- a/docs/src/ScriptSystems/introduction.md +++ b/docs/src/ScriptSystems/introduction.md @@ -102,10 +102,17 @@ bms.system_builder("my_system") ``` will create a system which calls the specified callback `my_system` with 2 arguments: -- The `ScriptQueryResult` for the first query - - With `components` access to ComponentA and ComponentB +- The results of the query, each a list of the entity followed by the components in the order they were added: `{entity, a, b}` - The `ReflectReference` to `ResourceA` +```lua +function my_system(results, resource_a) + for _, result in ipairs(results) do + local entity, a, b = result[1], result[2], result[3] + end +end +``` + `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 From 08491093ca8b0b46c5073af3ca6f5b2a4c37823d Mon Sep 17 00:00:00 2001 From: makspll Date: Thu, 22 Oct 2026 20:25:02 +0000 Subject: [PATCH 2/3] perf: generic inline values instead of entity-specific ones - `ScriptValue::Value(InlineValue)` replaces `ScriptValue::Entity`: `V` stores any `T` of at most 16 bytes without drop glue inline, decided at compile time, with a per-type vtable (type id, reflect, promotion) - languages hold it in one generic wrapper (`LuaValue` / `RuneValue`) and promote it in place to an allocated reference before anything which needs a place (fields, assignments, mutating methods, iteration), so scripts can't tell values from references; field reads stay references - `R` holds a copy of an inline value (exact type only, like references), `dispatch::binary`/`unary` take inline operands, `dispatch::index_value` finds non-mutating methods - functions matching `ScriptValue::Reference` on a `ScriptValue` argument accept values (`eq`, `EnumVariantRef::eq`, `trigger_event`) --- .../bindings/transform/value_semantics.lua | 38 +++ .../bindings/transform/value_semantics.rn | 40 +++ .../src/dispatch.rs | 109 +++--- .../src/function/from.rs | 65 ++-- .../src/function/from_ref.rs | 12 +- .../src/function/into.rs | 26 +- .../src/function/into_ref.rs | 2 - .../src/inline_value.rs | 184 ++++++++++ crates/bevy_mod_scripting_bindings/src/lib.rs | 2 + .../bevy_mod_scripting_bindings/src/query.rs | 2 +- .../src/script_value.rs | 16 +- .../src/derive_tests/mod.rs | 5 +- .../bevy_mod_scripting_functions/src/core.rs | 17 +- .../src/bindings/reference.rs | 202 ++++++++--- .../src/bindings/script_value.rs | 8 +- .../bevy_mod_scripting_rune/src/types.rs | 323 ++++++++++++++---- .../bevy_mod_scripting_rune/src/value.rs | 14 +- .../AddingLanguages/necessary-features.md | 5 +- docs/src/ReleaseNotes/0.23-to-0.24.md | 4 +- 19 files changed, 852 insertions(+), 222 deletions(-) create mode 100644 assets/tests/bindings/transform/value_semantics.lua create mode 100644 assets/tests/bindings/transform/value_semantics.rn create mode 100644 crates/bevy_mod_scripting_bindings/src/inline_value.rs diff --git a/assets/tests/bindings/transform/value_semantics.lua b/assets/tests/bindings/transform/value_semantics.lua new file mode 100644 index 0000000000..cad1fa38a9 --- /dev/null +++ b/assets/tests/bindings/transform/value_semantics.lua @@ -0,0 +1,38 @@ +-- small values (i.e. `Vec3`, `Entity`) are stored inline instead of allocated, scripts must not be able to tell +local world = import_namespace("bms").world +local Transform = import_namespace("transform").Transform +local Vec3 = import_namespace("math").Vec3 + +-- two variables holding the same value see each other's changes, like for allocated values +local a = Vec3.new(1, 2, 3) +local b = a +b.x = 5 +assert(a.x == 5, "aliases share the value") + +-- assigning a value copies it into the place, reads of fields are places +local t = Transform.from_xyz(1, 2, 3) +local v = Vec3.new(7, 8, 9) +t.translation = v +v.x = 10 +assert(t.translation.x == 7, "assignment copies the value") +t.translation.y = 1 +assert(t.translation.y == 1, "fields are places") +local translation = t.translation +translation.z = 4 +assert(t.translation.z == 4, "a field read into a variable is still the place") + +-- functions, operators and display work on values +assert(a:length() > 0, "methods") +assert((a + Vec3.new(1, 0, 0)).x == 6, "operators") +assert(a == Vec3.new(5, 2, 3), "equality") +assert(tostring(Vec3.new(1, 2, 3)) ~= nil, "display") + +-- entities returned by functions and read from fields are equal +local parent = world.spawn() +local child = world.spawn() +world.push_children(parent, { child }) +local child_of = world.get_component(child, world.get_type_by_name("bevy_ecs::hierarchy::ChildOf")) +assert(child_of[1] == parent, "field == value") +assert(parent == child_of[1], "value == field") +assert(child_of:parent() == parent, "function result == value") +assert(world.get_parent(child) == parent, "function results are equal") diff --git a/assets/tests/bindings/transform/value_semantics.rn b/assets/tests/bindings/transform/value_semantics.rn new file mode 100644 index 0000000000..3766feee0b --- /dev/null +++ b/assets/tests/bindings/transform/value_semantics.rn @@ -0,0 +1,40 @@ +// small values (i.e. `Vec3`, `Entity`) are stored inline instead of allocated, scripts must not be able to tell +pub fn main() { + let world = import_namespace("bms").world; + let Transform = import_namespace("transform").Transform; + let Vec3 = import_namespace("math").Vec3; + + // two variables holding the same value see each other's changes, like for allocated values + let a = Vec3.new(1.0, 2.0, 3.0); + let b = a; + b.x = 5.0; + assert!(a.x == 5.0, "aliases share the value"); + + // assigning a value copies it into the place, reads of fields are places + let t = Transform.from_xyz(1.0, 2.0, 3.0); + let v = Vec3.new(7.0, 8.0, 9.0); + t.translation = v; + v.x = 10.0; + assert!(t.translation.x == 7.0, "assignment copies the value"); + t.translation.y = 1.0; + assert!(t.translation.y == 1.0, "fields are places"); + let translation = t.translation; + translation.z = 4.0; + assert!(t.translation.z == 4.0, "a field read into a variable is still the place"); + + // functions, operators and display work on values + assert!(a.length() > 0.0, "methods"); + assert!((a + Vec3.new(1.0, 0.0, 0.0)).x == 6.0, "operators"); + assert!(a == Vec3.new(5.0, 2.0, 3.0), "equality"); + assert!(`${Vec3.new(1.0, 2.0, 3.0)}` != "", "display"); + + // entities returned by functions and read from fields are equal + let parent = world.spawn(); + let child = world.spawn(); + world.push_children(parent, [child]); + let child_of = world.get_component(child, world.get_type_by_name("bevy_ecs::hierarchy::ChildOf")); + assert!(child_of[0] == parent, "field == value"); + assert!(parent == child_of[0], "value == field"); + assert!(child_of.parent() == parent, "function result == value"); + assert!(world.get_parent(child) == parent, "function results are equal"); +} diff --git a/crates/bevy_mod_scripting_bindings/src/dispatch.rs b/crates/bevy_mod_scripting_bindings/src/dispatch.rs index 7955820004..ef5285e64b 100644 --- a/crates/bevy_mod_scripting_bindings/src/dispatch.rs +++ b/crates/bevy_mod_scripting_bindings/src/dispatch.rs @@ -13,8 +13,9 @@ use bevy_mod_scripting_display::OrFakeId; use bevy_mod_scripting_world::WorldGuard; use crate::{ - DynamicScriptFunction, EnumVariantRef, FunctionCallContext, FunctionScope, InteropError, - ReflectReference, ScriptValue, WorldExtensions, world_extensions::FunctionLookups, + DynamicScriptFunction, EnumVariantRef, FunctionCallContext, FunctionScope, InlineValue, + InteropError, ReflectReference, ScriptValue, ThroughTypeInfo, WorldExtensions, + world_extensions::FunctionLookups, }; /// What indexing a value by a name found. @@ -133,9 +134,38 @@ pub fn static_index( )) } +/// What indexing an inline value by a name found, see [`index_value`]. +pub enum ValueMember { + /// A function of the value's type which doesn't mutate it, called with a copy of the value, i.e. `v:length()` in Lua. + Method(DynamicScriptFunction), + /// Anything else needs the value to have a place: the language promotes it in place with + /// [`crate::InlineValue::into_reference`] and indexes the reference with [`index`]. + NeedsPlace, +} + +/// Indexes an inline value, see [`crate::InlineValue`] for why only some members can be reached without a place. +pub fn index_value(world: WorldGuard, value: &InlineValue, name: &str) -> ValueMember { + profiling::function_scope!("dispatch::index_value"); + match world.lookup_static_member(value.type_id(), name) { + Ok(ScriptValue::Function(func)) => { + let mutates = func + .info + .arg_info + .first() + .is_some_and(|arg| matches!(arg.type_info, Some(ThroughTypeInfo::Mut(_)))); + if mutates { + ValueMember::NeedsPlace + } else { + ValueMember::Method(func) + } + } + _ => ValueMember::NeedsPlace, + } +} + /// Applies a binary operator, i.e. `v * 2`. /// -/// The operator's function is called on the operand which is a reference. +/// The operator's function is called on the operand which is a reference or an inline value. /// When that is the right one, the operator's reflected function is called instead, see [`ScriptOperatorNames::reflected_script_function_name`]. pub fn binary( world: WorldGuard, @@ -145,50 +175,51 @@ pub fn binary( ctx: FunctionCallContext, ) -> Result { profiling::function_scope!("dispatch::binary"); - let (receiver, other, name) = match (lhs, rhs) { - (ScriptValue::Reference(receiver), other) => { - (receiver, other, operator.script_function_name()) - } - (other, ScriptValue::Reference(receiver)) => { - match operator.reflected_script_function_name() { - Some(name) => (receiver, other, name), - None => { - return Err(InteropError::str( - "the right operand of this operator can't be a value", - )); - } + let receiver_type = |world: &WorldGuard, receiver: &ScriptValue| match receiver { + ScriptValue::Reference(r) => r.tail_type_id(world.clone()).map(|t| Some(t.or_fake_id())), + ScriptValue::Value(v) => Ok(Some(v.type_id())), + _ => Ok(None), + }; + let (receiver, other, name, type_id) = match receiver_type(&world, &lhs)? { + Some(type_id) => (lhs, rhs, operator.script_function_name(), type_id), + None => match ( + receiver_type(&world, &rhs)?, + operator.reflected_script_function_name(), + ) { + (Some(type_id), Some(name)) => (rhs, lhs, name, type_id), + (Some(_), None) => { + return Err(InteropError::str( + "the right operand of this operator can't be a value", + )); } - } - _ => { - return Err(InteropError::str( - "neither operand of this operator is a value", - )); - } + (None, _) => { + return Err(InteropError::str( + "neither operand of this operator is a value", + )); + } + }, }; - let type_id = receiver.tail_type_id(world.clone())?.or_fake_id(); - world.call_operator( - type_id, - name, - vec![ScriptValue::Reference(receiver), other], - ctx, - ) + world.call_operator(type_id, name, vec![receiver, other], ctx) } -/// Applies a unary operator to a reference, i.e. `-v`. +/// Applies a unary operator to a reference or an inline value, i.e. `-v`. pub fn unary( world: WorldGuard, operator: ScriptOperatorNames, - value: ReflectReference, + value: ScriptValue, ctx: FunctionCallContext, ) -> Result { profiling::function_scope!("dispatch::unary"); - let type_id = value.tail_type_id(world.clone())?.or_fake_id(); - world.call_operator( - type_id, - operator.script_function_name(), - vec![ScriptValue::Reference(value)], - ctx, - ) + let type_id = match &value { + ScriptValue::Reference(r) => r.tail_type_id(world.clone())?.or_fake_id(), + ScriptValue::Value(v) => v.type_id(), + _ => { + return Err(InteropError::str( + "the operand of this operator is not a value", + )); + } + }; + world.call_operator(type_id, operator.script_function_name(), vec![value], ctx) } /// The length of a value, i.e. `#v` in Lua. @@ -203,8 +234,8 @@ pub fn len( profiling::function_scope!("dispatch::len"); match value { ScriptValue::List(list) => Ok(ScriptValue::Integer(list.len() as i64)), - ScriptValue::Reference(reference) => { - unary(world, ScriptOperatorNames::Length, reference, ctx) + value @ (ScriptValue::Reference(_) | ScriptValue::Value(_)) => { + unary(world, ScriptOperatorNames::Length, value, ctx) } _ => Ok(ScriptValue::Unit), } diff --git a/crates/bevy_mod_scripting_bindings/src/function/from.rs b/crates/bevy_mod_scripting_bindings/src/function/from.rs index e4a9c11f48..58694d1644 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/from.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/from.rs @@ -3,10 +3,9 @@ use super::bound::BoundFields; use super::script_function::{DynamicScriptFunction, DynamicScriptFunctionMut}; use crate::{ - EnumVariantRef, ReflectReference, ScriptValue, construct::construct_reference, + EnumVariantRef, InlineValue, ReflectReference, ScriptValue, construct::construct_reference, error::InteropError, script_value::VariadicTuple, }; -use bevy_ecs::entity::Entity; use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{FromReflect, Reflect}; @@ -174,6 +173,8 @@ impl FromScript for ReflectReference { fn from_script(value: ScriptValue, _world: WorldGuard) -> Result { match value { ScriptValue::Reference(r) => Ok(r), + // an allocated copy, languages promote values before functions which might mutate them get them + ScriptValue::Value(v) => Ok(v.into_reference()), _ => Err(InteropError::value_mismatch( std::any::TypeId::of::(), value, @@ -277,11 +278,7 @@ impl FromScript for V { }, } } - ScriptValue::Entity(entity) if TypeId::of::() == TypeId::of::() => { - T::from_reflect(&entity).map(V).ok_or_else(|| { - InteropError::failed_from_reflect(Some(TypeId::of::()), "entity".to_owned()) - }) - } + ScriptValue::Value(value) => inline_value_into::(&value).map(V), _ => Err(InteropError::value_mismatch( std::any::TypeId::of::(), value, @@ -290,15 +287,20 @@ impl FromScript for V { } } -#[profiling::all_functions] -impl FromScript for Entity { - type This<'w> = Self; - fn from_script(value: ScriptValue, world: WorldGuard) -> Result { - match value { - ScriptValue::Entity(entity) => Ok(entity), - // i.e. an entity field of a component - value => V::::from_script(value, world).map(|v| v.0), - } +/// Copies an inline value into a `T`, directly if it is one, otherwise through reflection. +fn inline_value_into(value: &InlineValue) -> Result { + match value.downcast::() { + Some(value) => Ok(value), + None => T::from_reflect(value.as_partial_reflect()).ok_or_else(|| { + InteropError::failed_from_reflect( + Some(TypeId::of::()), + format!( + "Expected '{}' but received a '{}'", + std::any::type_name::(), + value.type_name() + ), + ) + }), } } @@ -307,15 +309,25 @@ impl FromScript for Entity { /// This can be used to retrieve a reference out of a [`ScriptValue::Reference`] corresponding to the type `T`. /// Before downcasting the reference, it will claim write access to the object to ensure that the reference is valid. /// +/// An inline [`ScriptValue::Value`] is copied instead, it has no place to borrow from. +/// /// However, the access is NOT released when the `Mut` is dropped. This is not unsafe but can lead to deadlocks if not released later. /// The script function calling mechanism will take care of releasing all accesses claimed during the function call. -pub struct R<'w, T>(pub &'w T); +pub struct R<'w, T>(RInner<'w, T>); + +enum RInner<'w, T> { + Borrowed(&'w T), + Owned(T), +} impl Deref for R<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { - self.0 + match &self.0 { + RInner::Borrowed(value) => value, + RInner::Owned(value) => value, + } } } @@ -335,7 +347,7 @@ impl FromScript for R<'_, T> { let ref_ = unsafe { reflect_reference.reflect_unsafe_non_empty(world.clone()) }?; match ref_.try_downcast_ref::() { - Some(cast) => Ok(R(cast)), + Some(cast) => Ok(R(RInner::Borrowed(cast))), None => match variant_as_value::(&reflect_reference, &world) { Some(value) => Self::from_script(ScriptValue::Reference(value), world), None => Err(InteropError::type_mismatch( @@ -351,6 +363,13 @@ impl FromScript for R<'_, T> { )) } } + // like a reference, only the exact type, a conversion could take another type for this one in a union + ScriptValue::Value(value) => value + .downcast::() + .map(|v| R(RInner::Owned(v))) + .ok_or_else(|| { + InteropError::type_mismatch(std::any::TypeId::of::(), Some(value.type_id())) + }), _ => Err(InteropError::value_mismatch( std::any::TypeId::of::(), value, @@ -361,7 +380,7 @@ impl FromScript for R<'_, T> { impl<'a, T> From<&'a T> for R<'a, T> { fn from(value: &'a T) -> Self { - R(value) + R(RInner::Borrowed(value)) } } @@ -420,6 +439,12 @@ impl FromScript for M<'_, T> { )) } } + // languages promote inline values before calling functions which mutate them, so this is a value passed + // as another argument, there is no place to write the change to + ScriptValue::Value(value) => Err(InteropError::string(format!( + "a '{}' value can't be mutated by a function it is passed to, only one called on it", + value.type_name() + ))), _ => Err(InteropError::value_mismatch( std::any::TypeId::of::(), value, diff --git a/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs b/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs index 9715ceb248..725b2770be 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/from_ref.rs @@ -4,7 +4,6 @@ use crate::{ FromScript, ScriptValue, error::InteropError, match_by_type, reflection_extensions::TypeInfoExtensions, }; -use bevy_ecs::entity::Entity; use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::{ PartialReflect, ReflectKind, @@ -62,7 +61,6 @@ impl FromScriptErased for Box { tr : PathBuf => return ::from_script(value, world).map(|a| Box::new(a) as _), ts : OsString=> return ::from_script(value, world).map(|a| Box::new(a) as _), tsv: ScriptValue => return ::from_script(value, world).map(|a| Box::new(a) as _), - tent: Entity => return ::from_script(value, world).map(|a| Box::new(a) as _), tn : () => return <()>::from_script(value, world).map(|a| Box::new(a) as _) } ); @@ -145,6 +143,16 @@ impl FromScriptErased for Box { ScriptValue::Reference(reflect_reference) => { reflect_reference.to_owned_value(world.clone()) } + // a copy of the value, like `to_owned_value` makes of the value of a reference + ScriptValue::Value(value) => { + let value = value.as_partial_reflect(); + let copy: Box = match value.reflect_clone() { + Ok(copy) => copy.into_partial_reflect(), + Err(_) => PartialReflect::to_dynamic(value) + .map_err(|e| InteropError::string(e.to_string()))?, + }; + Ok(copy) + } value => Err(InteropError::value_mismatch(target, value)), } } diff --git a/crates/bevy_mod_scripting_bindings/src/function/into.rs b/crates/bevy_mod_scripting_bindings/src/function/into.rs index b70a691e3b..f8ef0362f4 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/into.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/into.rs @@ -1,12 +1,11 @@ //! Implementations of the [`IntoScript`] trait for various types. use super::{DynamicScriptFunction, DynamicScriptFunctionMut, Union, V}; -use crate::{ReflectReference, ScriptValue, VariadicTuple, error::InteropError}; -use bevy_ecs::entity::Entity; +use crate::{InlineValue, ReflectReference, ScriptValue, VariadicTuple, error::InteropError}; use bevy_mod_scripting_world::WorldGuard; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; -use std::{any::Any, borrow::Cow, collections::VecDeque, ffi::OsString, path::PathBuf}; +use std::{borrow::Cow, collections::VecDeque, ffi::OsString, path::PathBuf}; /// Converts a value into a [`ScriptValue`]. pub trait IntoScript { @@ -115,21 +114,14 @@ impl IntoScript for ReflectReference { #[profiling::all_functions] impl IntoScript for V { fn into_script(self, _world: WorldGuard) -> Result { - // entities are passed by value, the check is resolved at compile time - if let Some(entity) = (&self.0 as &dyn Any).downcast_ref::() { - return Ok(ScriptValue::Entity(*entity)); + // small values are stored inline, the check is resolved at compile time, see `InlineValue` + match InlineValue::new(self.0) { + Ok(value) => Ok(ScriptValue::Value(value)), + // one heap allocation, holding the value and its reference counts + Err(value) => Ok(ScriptValue::Reference(ReflectReference::new_allocated( + value, + ))), } - // one heap allocation, holding the value and its reference counts - Ok(ScriptValue::Reference(ReflectReference::new_allocated( - self.0, - ))) - } -} - -#[profiling::all_functions] -impl IntoScript for Entity { - fn into_script(self, _world: WorldGuard) -> Result { - Ok(ScriptValue::Entity(self)) } } diff --git a/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs b/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs index 5d58df776e..c0fafb3223 100644 --- a/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs +++ b/crates/bevy_mod_scripting_bindings/src/function/into_ref.rs @@ -2,7 +2,6 @@ use std::{borrow::Cow, ffi::OsString, path::PathBuf}; -use bevy_ecs::entity::Entity; use bevy_mod_scripting_display::OrFakeId; use bevy_mod_scripting_world::WorldGuard; use bevy_reflect::PartialReflect; @@ -113,7 +112,6 @@ fn into_script_erased(r: &dyn PartialReflect, world: WorldGuard) -> Result return value(downcast_into_value!(r, PathBuf).clone().into_script(world)), ts : OsString=> return value(downcast_into_value!(r, OsString).clone().into_script(world)), tsv: ScriptValue=> return Ok(Erased::Value(downcast_into_value!(r, ScriptValue).clone())), - tent: Entity => return Ok(Erased::Value(ScriptValue::Entity(downcast_into_value!(r, Entity)))), tn : () => return Ok(Erased::Value(ScriptValue::Unit)) } ); diff --git a/crates/bevy_mod_scripting_bindings/src/inline_value.rs b/crates/bevy_mod_scripting_bindings/src/inline_value.rs new file mode 100644 index 0000000000..9b05670be3 --- /dev/null +++ b/crates/bevy_mod_scripting_bindings/src/inline_value.rs @@ -0,0 +1,184 @@ +//! Small values passed to scripts without allocating, see [`InlineValue`]. +//! +//! Values returned to scripts by value ([`crate::V`]) are owned by the script. Instead of allocating them, +//! small values without drop glue are stored inline in the [`crate::ScriptValue`], decided at compile time by [`is_inline`], +//! so no type registry lookups are needed. +//! +//! This is only a storage optimization, scripts can't tell inline values from allocated ones: +//! language implementations hold the inline value in their wrapper and promote it in place to an allocation +//! ([`InlineValue::into_reference`]) before any operation which needs a place, i.e. accessing or assigning a field +//! or calling a function which mutates it. Reads of fields and elements always return references into the place. +use std::{ + any::TypeId, + mem::{MaybeUninit, align_of, needs_drop, size_of}, +}; + +use bevy_mod_scripting_display::DebugWithTypeInfo; +use bevy_mod_scripting_world::WorldAccessGuard; +use bevy_reflect::{PartialReflect, Reflect}; + +use crate::ReflectReference; + +/// The maximum size of values stored inline, in bytes. +pub const INLINE_CAPACITY: usize = 16; + +/// Whether values of the type are stored inline when passed to scripts by value, see [`InlineValue`]. +/// +/// Types with drop glue are never inline: copies of the bytes would be dropped more than once. +pub const fn is_inline() -> bool { + size_of::() <= INLINE_CAPACITY && align_of::() <= align_of::() && !needs_drop::() +} + +/// The operations on an inline value of a given type, monomorphized when the value is created. +struct InlineVTable { + type_id: fn() -> TypeId, + type_name: fn() -> &'static str, + as_reflect: unsafe fn(*const ()) -> *const dyn Reflect, + into_reference: unsafe fn(*const ()) -> ReflectReference, +} + +unsafe fn as_reflect(data: *const ()) -> *const dyn Reflect { + data as *const T as *const dyn Reflect +} + +unsafe fn into_reference(data: *const ()) -> ReflectReference { + // Safety: the caller ensures the data holds a `T`, which has no drop glue, so reading a copy is fine + ReflectReference::new_allocated(unsafe { (data as *const T).read() }) +} + +trait InlineVTableOf: Reflect + Sized { + const VTABLE: InlineVTable = InlineVTable { + type_id: TypeId::of::, + type_name: std::any::type_name::, + as_reflect: as_reflect::, + into_reference: into_reference::, + }; +} + +impl InlineVTableOf for T {} + +/// A small value without drop glue stored inline, with the operations on its type, see the [module docs](self). +#[derive(Clone, Copy)] +pub struct InlineValue { + data: MaybeUninit<[u64; 2]>, + vtable: &'static InlineVTable, +} + +// Safety: only `Reflect` types are stored, which are `Send` and `Sync` +unsafe impl Send for InlineValue {} +// Safety: only `Reflect` types are stored, which are `Send` and `Sync` +unsafe impl Sync for InlineValue {} + +impl InlineValue { + /// Stores the value inline, or returns it if the type isn't [inline](is_inline). + /// + /// The check is a constant, the branch not taken is removed at compile time. + #[inline] + pub fn new(value: T) -> Result { + if !is_inline::() { + return Err(value); + } + let mut data = MaybeUninit::<[u64; 2]>::uninit(); + // Safety: the type fits in and is at most as aligned as the data, checked above + unsafe { (data.as_mut_ptr() as *mut T).write(value) }; + Ok(Self { + data, + vtable: &::VTABLE, + }) + } + + /// The type of the value + pub fn type_id(&self) -> TypeId { + (self.vtable.type_id)() + } + + /// The name of the type of the value + pub fn type_name(&self) -> &'static str { + (self.vtable.type_name)() + } + + /// The value + pub fn as_reflect(&self) -> &dyn Reflect { + // Safety: the vtable belongs to the type of the data + unsafe { &*(self.vtable.as_reflect)(self.data.as_ptr() as *const ()) } + } + + /// The value as a partial reflect + pub fn as_partial_reflect(&self) -> &dyn PartialReflect { + self.as_reflect().as_partial_reflect() + } + + /// A copy of the value if it's a `T` + pub fn downcast(&self) -> Option { + // Safety: the data holds a `T`, which has no drop glue, so reading a copy is fine + (self.type_id() == TypeId::of::()) + .then(|| unsafe { (self.data.as_ptr() as *const T).read() }) + } + + /// Moves the value into an allocation, giving it a place, see the [module docs](self). + pub fn into_reference(self) -> ReflectReference { + // Safety: the vtable belongs to the type of the data + unsafe { (self.vtable.into_reference)(self.data.as_ptr() as *const ()) } + } +} + +impl std::fmt::Debug for InlineValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_partial_reflect().debug(f) + } +} + +impl DebugWithTypeInfo for InlineValue { + fn to_string_with_type_info( + &self, + f: &mut std::fmt::Formatter<'_>, + _type_info_provider: Option<&WorldAccessGuard>, + ) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl PartialEq for InlineValue { + fn eq(&self, other: &Self) -> bool { + self.type_id() == other.type_id() + && self + .as_partial_reflect() + .reflect_partial_eq(other.as_partial_reflect()) + .unwrap_or(false) + } +} + +#[cfg(test)] +mod test { + use bevy_ecs::entity::Entity; + + use super::*; + + #[derive(Reflect, Clone)] + struct Big([u64; 3]); + + #[test] + fn small_types_without_drop_glue_are_inline() { + assert!(is_inline::()); + assert!(is_inline::<[f32; 4]>()); + assert!(!is_inline::()); + assert!(!is_inline::()); + assert!(InlineValue::new(Big([0; 3])).is_err()); + } + + #[test] + fn inline_values_keep_their_value_and_type() { + let entity = Entity::from_raw_u32(7).unwrap(); + let value = InlineValue::new(entity).unwrap_or_else(|_| panic!("entities are inline")); + assert_eq!(value.type_id(), TypeId::of::()); + assert_eq!(value.downcast::(), Some(entity)); + assert_eq!(value.downcast::(), None); + assert_eq!(value, value); + assert_eq!( + value.as_reflect().downcast_ref::().copied(), + Some(entity) + ); + let reference = value.into_reference(); + assert_eq!(reference.base.type_id, TypeId::of::()); + } +} diff --git a/crates/bevy_mod_scripting_bindings/src/lib.rs b/crates/bevy_mod_scripting_bindings/src/lib.rs index 441ffb7a20..f7df7e94f7 100644 --- a/crates/bevy_mod_scripting_bindings/src/lib.rs +++ b/crates/bevy_mod_scripting_bindings/src/lib.rs @@ -11,6 +11,7 @@ pub mod enum_variant; pub mod error; pub mod function; pub mod globals; +pub mod inline_value; pub mod observer; pub mod path; pub mod query; @@ -31,6 +32,7 @@ pub use enum_variant::*; pub use error::*; pub use function::*; pub use globals::*; +pub use inline_value::*; // pub use pretty_print::*; pub use bevy_mod_scripting_world::*; pub use conversions::*; diff --git a/crates/bevy_mod_scripting_bindings/src/query.rs b/crates/bevy_mod_scripting_bindings/src/query.rs index 8745b8c455..d0c767af20 100644 --- a/crates/bevy_mod_scripting_bindings/src/query.rs +++ b/crates/bevy_mod_scripting_bindings/src/query.rs @@ -680,7 +680,7 @@ pub struct ScriptQueryResult { impl IntoScript for ScriptQueryResult { fn into_script(self, _world: WorldGuard) -> Result { let mut values = VecDeque::with_capacity(self.components.len() + 1); - values.push_back(ScriptValue::Entity(self.entity)); + values.push_back(V(self.entity).into_script(_world)?); values.extend( self.components .into_iter() diff --git a/crates/bevy_mod_scripting_bindings/src/script_value.rs b/crates/bevy_mod_scripting_bindings/src/script_value.rs index c9f58a1c2e..ec580cdaeb 100644 --- a/crates/bevy_mod_scripting_bindings/src/script_value.rs +++ b/crates/bevy_mod_scripting_bindings/src/script_value.rs @@ -1,7 +1,6 @@ //! This module contains the `ScriptValue` enum which is used to pass values between scripting languages and Rust. use crate::error::InteropError; -use bevy_ecs::entity::Entity; use bevy_mod_scripting_derive::DebugWithTypeInfo; use bevy_mod_scripting_display::{DisplayWithTypeInfo, ReflectDisplayWithTypeInfo, WithTypeInfo}; use bevy_mod_scripting_world::WorldGuard; @@ -10,7 +9,7 @@ use bevy_reflect::Reflect; use std::{any::TypeId, borrow::Cow, collections::VecDeque}; use super::{ - ReflectReference, + InlineValue, ReflectReference, function::script_function::{DynamicScriptFunction, DynamicScriptFunctionMut}, }; @@ -44,8 +43,11 @@ pub enum ScriptValue { /// /// Unlike a reference, a type carries no data, so it never allocates. Type(TypeId), - /// Represents an entity, passed by value so it never allocates, scripts call the functions of `Entity` on it. - Entity(Entity), + /// A small value owned by the script, stored inline instead of allocated, see [`InlineValue`]. + /// + /// Scripts can't tell it from an allocated [`ScriptValue::Reference`], language implementations promote it to one + /// with [`InlineValue::into_reference`] for anything which needs a place, i.e. accessing its fields. + Value(InlineValue), /// A dynamic script function possibly storing state. Preffer using the [`ScriptValue::Function`] variant instead if possible. FunctionMut(DynamicScriptFunctionMut), /// A stateless dynamic script function @@ -114,7 +116,7 @@ impl DisplayWithTypeInfo for ScriptValue { } ScriptValue::Reference(v) => v.display_with_type_info(f, type_info_provider), ScriptValue::Type(v) => v.display_with_type_info(f, type_info_provider), - ScriptValue::Entity(v) => write!(f, "{v}"), + ScriptValue::Value(v) => write!(f, "{v:?}"), ScriptValue::FunctionMut(v) => v.display_with_type_info(f, type_info_provider), ScriptValue::Function(v) => v.display_with_type_info(f, type_info_provider), ScriptValue::Error(e) => e.display_with_type_info(f, type_info_provider), @@ -134,7 +136,7 @@ impl PartialEq for ScriptValue { (Self::Map(l0), Self::Map(r0)) => l0 == r0, (Self::Reference(l0), Self::Reference(r0)) => l0 == r0, (Self::Type(l0), Self::Type(r0)) => l0 == r0, - (Self::Entity(l0), Self::Entity(r0)) => l0 == r0, + (Self::Value(l0), Self::Value(r0)) => l0 == r0, // explicitly don't compare functions and errors _ => false, } @@ -163,7 +165,7 @@ impl ScriptValue { ScriptValue::Tuple(_) => "Tuple".to_owned(), ScriptValue::Reference(_) => "Reference".to_owned(), ScriptValue::Type(_) => "Type".to_owned(), - ScriptValue::Entity(_) => "Entity".to_owned(), + ScriptValue::Value(_) => "Value".to_owned(), ScriptValue::FunctionMut(_) => "FunctionMut".to_owned(), ScriptValue::Function(_) => "Function".to_owned(), ScriptValue::Error(_) => "Error".to_owned(), diff --git a/crates/bevy_mod_scripting_core/src/derive_tests/mod.rs b/crates/bevy_mod_scripting_core/src/derive_tests/mod.rs index a2ba8d3e5e..374db3be9f 100644 --- a/crates/bevy_mod_scripting_core/src/derive_tests/mod.rs +++ b/crates/bevy_mod_scripting_core/src/derive_tests/mod.rs @@ -96,10 +96,9 @@ mod tests { FunctionCallContext::new(bevy_mod_scripting_asset::Language::Lua), ) .unwrap(); + // small values are returned inline let out: MyThing = match out { - bevy_mod_scripting_bindings::ScriptValue::Reference(reflect_reference) => { - reflect_reference.downcast(world.clone()).unwrap() - } + bevy_mod_scripting_bindings::ScriptValue::Value(value) => value.downcast().unwrap(), _ => panic!("invalid return"), }; assert_eq!(out.0, 42) diff --git a/crates/bevy_mod_scripting_functions/src/core.rs b/crates/bevy_mod_scripting_functions/src/core.rs index 70ac5faaa4..5e41d581c1 100644 --- a/crates/bevy_mod_scripting_functions/src/core.rs +++ b/crates/bevy_mod_scripting_functions/src/core.rs @@ -361,6 +361,9 @@ impl World { Some(ScriptValue::Reference(value)) => { world.trigger_event(registration.into_inner(), &value) } + Some(ScriptValue::Value(value)) => { + world.trigger_event(registration.into_inner(), &value.into_reference()) + } _ => Err(InteropError::string(format!( "triggering the event `{}` needs an event value, i.e. built with `construct`", registration.short_name() @@ -763,8 +766,11 @@ impl ReflectReference { ) -> Result { profiling::function_scope!("eq"); let world = ctxt.world()?; - let ScriptValue::Reference(other) = other else { - return Ok(false); + let other = match other { + ScriptValue::Reference(other) => other, + // only read, a copy will do + ScriptValue::Value(other) => other.into_reference(), + _ => return Ok(false), }; if let Ok(variant) = other.downcast::(world.clone()) { return value_is_variant(&world, &reference, variant); @@ -1165,8 +1171,11 @@ impl EnumVariantRef { ) -> Result { profiling::function_scope!("eq"); let world = ctxt.world()?; - let ScriptValue::Reference(other) = other else { - return Ok(false); + let other = match other { + ScriptValue::Reference(other) => other, + // only read, a copy will do + ScriptValue::Value(other) => other.into_reference(), + _ => return Ok(false), }; if let Ok(other_variant) = other.downcast::(world.clone()) { return Ok(other_variant == *variant); diff --git a/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs b/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs index 4f17695556..27916310dc 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/bindings/reference.rs @@ -1,8 +1,9 @@ use std::{any::TypeId, borrow::Cow}; -use bevy_ecs::entity::Entity; use bevy_mod_scripting_bindings::{ - ReflectReference, ScriptValue, WorldGuard, dispatch, error::InteropError, + InlineValue, ReflectReference, ScriptValue, WorldGuard, + dispatch::{self, ValueMember}, + error::InteropError, }; use bevy_mod_scripting_bindings_domain::ScriptOperatorNames; use bevy_mod_scripting_world::ThreadWorldContainer; @@ -61,6 +62,33 @@ fn binary_operator( } } +/// Indexes a reference, methods come back unbound, `v:method()` passes the value itself. +fn index_reference( + lua: &mlua::Lua, + reference: &ReflectReference, + key: mlua::Value, +) -> mlua::Result { + let member = match &key { + // names and the receiver are only copied for field access + mlua::Value::String(name) => { + let name = name.to_str()?; + with_world(|world| { + dispatch::index_name(world, Cow::Borrowed(reference), &*name, LUA_CALLER_CONTEXT) + })? + } + _ => { + let key = LuaScriptValue::from_lua(key, lua)?; + with_world(|world| { + dispatch::index(world, reference.clone(), key.into(), LUA_CALLER_CONTEXT) + })? + } + }; + match member { + dispatch::Member::Method(func) => registered_function_into_lua(lua, func), + member => LuaScriptValue(member.into()).into_lua(lua), + } +} + impl UserData for LuaReflectReference { fn add_methods>(m: &mut T) { // methods come back unbound, `v:method()` passes the value itself @@ -68,30 +96,7 @@ impl UserData for LuaReflectReference { MetaMethod::Index, |lua, (self_, key): (mlua::UserDataRef, mlua::Value)| { profiling::function_scope!("MetaMethod::Index"); - let member = match &key { - // names and the receiver are only copied for field access - mlua::Value::String(name) => { - let name = name.to_str()?; - with_world(|world| { - dispatch::index_name( - world, - Cow::Borrowed(&self_.0), - &*name, - LUA_CALLER_CONTEXT, - ) - })? - } - _ => { - let key = LuaScriptValue::from_lua(key, lua)?; - with_world(|world| { - dispatch::index(world, self_.0.clone(), key.into(), LUA_CALLER_CONTEXT) - })? - } - }; - match member { - dispatch::Member::Method(func) => registered_function_into_lua(lua, func), - member => LuaScriptValue(member.into()).into_lua(lua), - } + index_reference(lua, &self_.0, key) }, ); @@ -130,7 +135,7 @@ impl UserData for LuaReflectReference { dispatch::unary( world, ScriptOperatorNames::Negation, - self_.0, + ScriptValue::Reference(self_.0), LUA_CALLER_CONTEXT, ) }) @@ -181,36 +186,143 @@ impl UserData for LuaTypeRef { } } -/// An entity, i.e. what [`bevy_mod_scripting_bindings::ScriptValue::Entity`] is in Lua. Passed by value, its methods are the functions of `Entity`, i.e. `entity:index()`. -#[derive(Debug, Clone, Copy, PartialEq, mlua::FromLua)] -pub struct LuaEntity(pub Entity); +/// A small value owned by the script, i.e. what [`ScriptValue::Value`] is in Lua, see [`InlineValue`]. +/// +/// Stored inline until something needs it to have a place (accessing a field, calling a function which mutates it), +/// then promoted in place to an allocated reference, so scripts can't tell it from a [`LuaReflectReference`]. +#[derive(Debug, Clone, mlua::FromLua)] +pub enum LuaValue { + /// Not promoted yet + Inline(InlineValue), + /// Promoted, behaves like a [`LuaReflectReference`] + Promoted(ReflectReference), +} + +impl LuaValue { + /// The value to pass to a function + pub fn to_script_value(&self) -> ScriptValue { + match self { + LuaValue::Inline(value) => ScriptValue::Value(*value), + LuaValue::Promoted(reference) => ScriptValue::Reference(reference.clone()), + } + } +} + +impl LuaValue { + /// Promotes the value in place if needed, and returns its reference. + fn promote(&mut self) -> ReflectReference { + let reference = match self { + LuaValue::Inline(value) => value.into_reference(), + LuaValue::Promoted(reference) => return reference.clone(), + }; + *self = LuaValue::Promoted(reference.clone()); + reference + } +} -impl UserData for LuaEntity { +impl UserData for LuaValue { fn add_methods>(m: &mut T) { m.add_meta_function( MetaMethod::Index, - |lua, (_, key): (LuaEntity, LuaScriptValue)| { + |lua, (mut self_, key): (mlua::UserDataRefMut, mlua::Value)| { profiling::function_scope!("MetaMethod::Index"); - let member = with_world(|world| { - dispatch::static_index( + let inline = match &*self_ { + LuaValue::Inline(value) => *value, + // the same as a reference + LuaValue::Promoted(reference) => return index_reference(lua, reference, key), + }; + if let mlua::Value::String(name) = &key { + let name = name.to_str()?; + let member = + with_world(|world| Ok(dispatch::index_value(world, &inline, &name)))?; + if let ValueMember::Method(function) = member { + // `v:method()` passes the value as the first argument + return registered_function_into_lua(lua, function); + } + } + let reference = self_.promote(); + index_reference(lua, &reference, key) + }, + ); + + m.add_meta_function( + MetaMethod::NewIndex, + |_, + (mut self_, key, value): ( + mlua::UserDataRefMut, + LuaScriptValue, + LuaScriptValue, + )| { + profiling::function_scope!("MetaMethod::NewIndex"); + let reference = self_.promote(); + with_world(|world| { + dispatch::set_index( world, - TypeId::of::(), + reference, key.into(), + value.into(), LUA_CALLER_CONTEXT, ) - })?; - match member { - // `entity:method()` passes the entity as the first argument - ScriptValue::Function(function) => registered_function_into_lua(lua, function), - other => LuaScriptValue(other).into_lua(lua), - } + }) }, ); - m.add_meta_function(MetaMethod::Eq, |_, (a, b): (LuaEntity, LuaEntity)| { - Ok(a.0 == b.0) + + for (metamethod, operator) in [ + (MetaMethod::Add, ScriptOperatorNames::Addition), + (MetaMethod::Sub, ScriptOperatorNames::Subtraction), + (MetaMethod::Mul, ScriptOperatorNames::Multiplication), + (MetaMethod::Div, ScriptOperatorNames::Division), + (MetaMethod::Mod, ScriptOperatorNames::Remainder), + (MetaMethod::Pow, ScriptOperatorNames::Exponentiation), + (MetaMethod::Eq, ScriptOperatorNames::Equality), + (MetaMethod::Lt, ScriptOperatorNames::LessThanComparison), + ] { + m.add_meta_function(metamethod, binary_operator(operator)); + } + + m.add_meta_function(MetaMethod::Unm, |_, self_: LuaScriptValue| { + profiling::function_scope!("MetaMethod::Unm"); + with_world(|world| { + dispatch::unary( + world, + ScriptOperatorNames::Negation, + self_.into(), + LUA_CALLER_CONTEXT, + ) + }) + .map(LuaScriptValue) + }); + + m.add_meta_function(MetaMethod::Len, |_lua, self_: LuaScriptValue| { + profiling::function_scope!("MetaMethod::Len"); + with_world(|world| dispatch::len(world, self_.into(), LUA_CALLER_CONTEXT)) + .map(LuaScriptValue) }); - m.add_meta_function(MetaMethod::ToString, |_, self_: LuaEntity| { - Ok(self_.0.to_string()) + + #[cfg(any( + feature = "lua54", + feature = "lua53", + feature = "lua52", + feature = "luajit52", + ))] + m.add_meta_function( + MetaMethod::Pairs, + |_, mut self_: mlua::UserDataRefMut| { + profiling::function_scope!("MetaMethod::Pairs"); + let reference = self_.promote(); + with_world(|world| dispatch::iter(world, reference, LUA_CALLER_CONTEXT)) + .map(LuaScriptValue) + }, + ); + + m.add_meta_function(MetaMethod::ToString, |_, self_: LuaValue| { + profiling::function_scope!("MetaMethod::ToString"); + // only reads the value, a copy will do + let reference = match self_ { + LuaValue::Inline(value) => value.into_reference(), + LuaValue::Promoted(reference) => reference, + }; + with_world(|world| dispatch::display(world, reference, LUA_CALLER_CONTEXT)) }); } } diff --git a/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs b/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs index 2d3b0a3c9c..e6e82edaf4 100644 --- a/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs +++ b/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs @@ -15,7 +15,7 @@ use mlua::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, MultiValue, Value}; use crate::{IntoMluaError, LuaContextAppData}; -use super::reference::{LuaEntity, LuaReflectReference, LuaTypeRef}; +use super::reference::{LuaReflectReference, LuaTypeRef, LuaValue}; /// A wrapper around many [`ScriptValue`]'s, used where the potential to return/receive many values separately arises. pub struct MultiLuaScriptValue(pub VecDeque); @@ -168,8 +168,8 @@ impl FromLua for LuaScriptValue { if let Ok(type_ref) = ud.borrow::() { return Ok(LuaScriptValue(ScriptValue::Type(type_ref.0))); } - if let Ok(entity) = ud.borrow::() { - return Ok(LuaScriptValue(ScriptValue::Entity(entity.0))); + if let Ok(value) = ud.borrow::() { + return Ok(LuaScriptValue(value.to_script_value())); } let ud = ud.borrow::().map_err(|e| { mlua::Error::FromLuaConversionError { @@ -278,7 +278,7 @@ impl IntoLua for LuaScriptValue { ScriptValue::String(s) => Value::String(lua.create_string(s.as_ref())?), ScriptValue::Reference(r) => LuaReflectReference::from(r).into_lua(lua)?, ScriptValue::Type(type_id) => LuaTypeRef(type_id).into_lua(lua)?, - ScriptValue::Entity(entity) => LuaEntity(entity).into_lua(lua)?, + ScriptValue::Value(value) => LuaValue::Inline(value).into_lua(lua)?, ScriptValue::Error(script_error) => return Err(mlua::Error::external(script_error)), ScriptValue::Function(function) => lua .create_function(move |lua, args: MultiLuaScriptValue| { diff --git a/crates/languages/bevy_mod_scripting_rune/src/types.rs b/crates/languages/bevy_mod_scripting_rune/src/types.rs index 10b28f814a..79dd8341cf 100644 --- a/crates/languages/bevy_mod_scripting_rune/src/types.rs +++ b/crates/languages/bevy_mod_scripting_rune/src/types.rs @@ -6,9 +6,8 @@ //! the index protocol, which gets the name as a value. use std::{any::TypeId, borrow::Cow, cmp::Ordering, collections::BTreeSet, sync::Arc}; -use bevy_ecs::entity::Entity; use bevy_mod_scripting_bindings::{ - InteropError, ReflectReference, ScriptValue, WorldGuard, + InlineValue, InteropError, ReflectReference, ScriptValue, WorldGuard, dispatch::{self, Member}, function::script_function::{DynamicScriptFunction, DynamicScriptFunctionMut}, }; @@ -55,10 +54,71 @@ pub struct RuneRef(pub ReflectReference); #[rune(item = ::bms, name = Type)] pub struct RuneType(pub TypeId); -/// An entity, passed by value, its methods are the functions of `Entity`, i.e. `entity.index()`, see [`ScriptValue::Entity`]. -#[derive(Any, Clone, Copy)] -#[rune(item = ::bms, name = Entity)] -pub struct RuneEntity(pub Entity); +/// A small value owned by the script, see [`ScriptValue::Value`] and [`InlineValue`]. +/// +/// Stored inline until something needs it to have a place (accessing a field, calling a function which mutates it), +/// then promoted in place to an allocated reference, so scripts can't tell it from a [`RuneRef`]. +#[derive(Any, Clone)] +#[rune(item = ::bms, name = Value)] +pub struct RuneValue(pub RuneValueState); + +/// The storage of a [`RuneValue`] +#[derive(Clone)] +pub enum RuneValueState { + /// Not promoted yet + Inline(InlineValue), + /// Promoted, behaves like a [`RuneRef`] + Promoted(ReflectReference), +} + +impl RuneValue { + /// The value to pass to a function + pub fn to_script_value(&self) -> ScriptValue { + match &self.0 { + RuneValueState::Inline(value) => ScriptValue::Value(*value), + RuneValueState::Promoted(reference) => ScriptValue::Reference(reference.clone()), + } + } + + /// Promotes the value in place if needed, and returns its reference. + pub fn promote(&mut self) -> RuneRef { + let reference = match &self.0 { + RuneValueState::Inline(value) => value.into_reference(), + RuneValueState::Promoted(reference) => return RuneRef(reference.clone()), + }; + self.0 = RuneValueState::Promoted(reference.clone()); + RuneRef(reference) + } + + /// A reference to the value for reading it, a copy if it wasn't promoted. + fn read_reference(&self) -> ReflectReference { + match &self.0 { + RuneValueState::Inline(value) => value.into_reference(), + RuneValueState::Promoted(reference) => reference.clone(), + } + } +} + +/// The member of an inline value which doesn't need a place, see [`dispatch::index_value`]. +fn inline_method(this: &RuneValue, name: &str) -> Result, InteropError> { + let RuneValueState::Inline(value) = &this.0 else { + return Ok(None); + }; + match dispatch::index_value(world()?, value, name) { + // the value is not one of the arguments of the function a script calls + dispatch::ValueMember::Method(func) => { + function_to_rune(func.curry(ScriptValue::Value(*value)), Some(1)).map(Some) + } + dispatch::ValueMember::NeedsPlace => Ok(None), + } +} + +fn get_value_member(this: &mut RuneValue, name: &str) -> Result { + match inline_method(this, name)? { + Some(method) => Ok(method), + None => get_ref_member(&this.promote(), name), + } +} /// The values of a namespace, as returned by `import_namespace`, whose functions can be called with the method syntax. #[derive(Any, Clone)] @@ -188,18 +248,42 @@ fn call_member_inner( RUNE_CALLER_CONTEXT, )?; call_value(member, args)? - } else if let Ok(this) = receiver.borrow_ref::() { - let member = dispatch::static_index( - world()?, - TypeId::of::(), - ScriptValue::String(name.to_owned().into()), - RUNE_CALLER_CONTEXT, - )?; - // the entity is the first argument of its methods - let mut with_receiver = Vec::with_capacity(args.len() + 1); - with_receiver.push(ScriptValue::Entity(this.0)); - with_receiver.extend(args); - call_value(member, with_receiver)? + } else if let Ok(mut this) = receiver.borrow_mut::() { + let inline = match &this.0 { + RuneValueState::Inline(value) => Some(*value), + RuneValueState::Promoted(_) => None, + }; + let method = match inline { + Some(value) => match dispatch::index_value(world()?, &value, name) { + dispatch::ValueMember::Method(func) => Some((func, ScriptValue::Value(value))), + dispatch::ValueMember::NeedsPlace => None, + }, + None => None, + }; + let (member, receiver) = match method { + Some((func, value)) => (Member::Method(func), value), + None => { + let reference = this.promote().0; + let member = dispatch::index_name( + world()?, + Cow::Borrowed(&reference), + name.to_owned(), + RUNE_CALLER_CONTEXT, + )?; + (member, ScriptValue::Reference(reference)) + } + }; + drop(this); + match member { + // the value is the first argument of its methods + Member::Method(func) => { + let mut with_receiver = Vec::with_capacity(args.len() + 1); + with_receiver.push(receiver); + with_receiver.extend(args); + func.call(with_receiver, RUNE_CALLER_CONTEXT)? + } + Member::Static(value) => call_value(value, args)?, + } } else if let Ok(this) = receiver.borrow_ref::() { let member = this.0.get(name).cloned().ok_or_else(|| { InteropError::external(crate::error::RuneError(format!( @@ -297,30 +381,24 @@ const COMPOUND_ASSIGNMENTS: [(&Protocol, ScriptOperatorNames); 5] = [ /// How many fields of tuples can be accessed with `v.0`, `v.1`, .. the others with `v[16]` etc. const TUPLE_FIELDS: usize = 16; -fn binary(operator: ScriptOperatorNames, this: &RuneRef, other: &Value) -> VmResult { +fn binary(operator: ScriptOperatorNames, this: ScriptValue, other: &Value) -> VmResult { vm((|| { let rhs = from_rune(other)?; - let out = dispatch::binary( - world()?, - operator, - ScriptValue::Reference(this.0.clone()), - rhs, - RUNE_CALLER_CONTEXT, - )?; + let out = dispatch::binary(world()?, operator, this, rhs, RUNE_CALLER_CONTEXT)?; to_rune(out) })()) } -fn same_reference(this: &RuneRef, other: &Value) -> Result, InteropError> { - // like in Lua, values of different kinds are never equal - if other.borrow_ref::().is_err() { +fn same_reference(this: ScriptValue, other: &Value) -> Result, InteropError> { + // like in Lua, values of different kinds are never equal, references and inline values are the same kind + if other.borrow_ref::().is_err() && other.borrow_ref::().is_err() { return Ok(Some(false)); } let rhs = from_rune(other)?; let out = dispatch::binary( world()?, ScriptOperatorNames::Equality, - ScriptValue::Reference(this.0.clone()), + this, rhs, RUNE_CALLER_CONTEXT, )?; @@ -330,25 +408,50 @@ fn same_reference(this: &RuneRef, other: &Value) -> Result, Interop }) } -fn less_than(this: &RuneRef, other: &Value) -> Result { +fn less_than(this: ScriptValue, other: &Value) -> Result { let rhs = from_rune(other)?; let out = dispatch::binary( world()?, ScriptOperatorNames::LessThanComparison, - ScriptValue::Reference(this.0.clone()), + this, rhs, RUNE_CALLER_CONTEXT, )?; Ok(matches!(out, ScriptValue::Bool(true))) } +fn partial_cmp(this: ScriptValue, other: &Value) -> Result, InteropError> { + if same_reference(this.clone(), other)? == Some(true) { + return Ok(Some(Ordering::Equal)); + } + if less_than(this.clone(), other)? { + return Ok(Some(Ordering::Less)); + } + // greater, when the other is a reference or value which is less than this one + let other_value = from_rune(other)?; + if !matches!( + other_value, + ScriptValue::Reference(_) | ScriptValue::Value(_) + ) { + return Ok(None); + } + let this = to_rune(this)?; + Ok(less_than(other_value, &this)?.then_some(Ordering::Greater)) +} + macro_rules! operators { ($module:expr, $(($protocol:ident, $operator:ident)),* $(,)?) => { $( $module.associated_function( &Protocol::$protocol, |this: &RuneRef, other: Value| { - binary(ScriptOperatorNames::$operator, this, &other) + binary(ScriptOperatorNames::$operator, ScriptValue::Reference(this.0.clone()), &other) + }, + )?; + $module.associated_function( + &Protocol::$protocol, + |this: &RuneValue, other: Value| { + binary(ScriptOperatorNames::$operator, this.to_script_value(), &other) }, )?; )* @@ -378,7 +481,7 @@ pub fn register_types( ) -> Result<(), ContextError> { module.ty::()?; module.ty::()?; - module.ty::()?; + module.ty::()?; module.ty::()?; module.ty::()?; module.ty::()?; @@ -407,6 +510,30 @@ pub fn register_types( })()) }, )?; + module.associated_function(&Protocol::INDEX_GET, |this: &mut RuneValue, key: Value| { + vm((|| match from_rune(&key)? { + ScriptValue::String(name) => get_value_member(this, &name), + key => { + let this = this.promote(); + let member = dispatch::index(world()?, this.0.clone(), key, RUNE_CALLER_CONTEXT)?; + member_to_rune(member, Some(&this.0)) + } + })()) + })?; + module.associated_function( + &Protocol::INDEX_SET, + |this: &mut RuneValue, key: Value, value: Value| { + vm((|| { + dispatch::set_index( + world()?, + this.promote().0, + from_rune(&key)?, + from_rune(&value)?, + RUNE_CALLER_CONTEXT, + ) + })()) + }, + )?; module.associated_function(&Protocol::INDEX_GET, |this: &RuneType, key: Value| { vm((|| match from_rune(&key)? { ScriptValue::String(name) => get_type_member(this, &name), @@ -430,7 +557,13 @@ pub fn register_types( ); module.associated_function(&Protocol::PARTIAL_EQ, |this: &RuneRef, other: Value| { - vm(same_reference(this, &other).map(|eq| eq.unwrap_or(false))) + vm( + same_reference(ScriptValue::Reference(this.0.clone()), &other) + .map(|eq| eq.unwrap_or(false)), + ) + })?; + module.associated_function(&Protocol::PARTIAL_EQ, |this: &RuneValue, other: Value| { + vm(same_reference(this.to_script_value(), &other).map(|eq| eq.unwrap_or(false))) })?; module.associated_function(&Protocol::PARTIAL_EQ, |this: &RuneType, other: Value| { other @@ -438,40 +571,17 @@ pub fn register_types( .map(|other| other.0 == this.0) .unwrap_or(false) })?; - module.associated_function(&Protocol::PARTIAL_EQ, |this: &RuneEntity, other: Value| { - other - .borrow_ref::() - .map(|other| other.0 == this.0) - .unwrap_or(false) - })?; - module.associated_function( - &Protocol::DISPLAY_FMT, - |this: &RuneEntity, f: &mut Formatter| rune::vm_write!(f, "{}", this.0), - )?; + module.associated_function( - &Protocol::DEBUG_FMT, - |this: &RuneEntity, f: &mut Formatter| rune::vm_write!(f, "{:?}", this.0), + &Protocol::PARTIAL_CMP, + |this: &RuneRef, other: Value| -> VmResult> { + vm(partial_cmp(ScriptValue::Reference(this.0.clone()), &other)) + }, )?; module.associated_function( &Protocol::PARTIAL_CMP, - |this: &RuneRef, other: Value| -> VmResult> { - vm((|| { - if same_reference(this, &other)? == Some(true) { - return Ok(Some(Ordering::Equal)); - } - if less_than(this, &other)? { - return Ok(Some(Ordering::Less)); - } - // greater, when the other is a reference which is less than this one - match other.borrow_ref::() { - Ok(other) => { - let this_value = Value::new(RuneRef(this.0.clone())) - .map_err(crate::error::IntoInteropError::to_bms_error)?; - Ok(less_than(&other, &this_value)?.then_some(Ordering::Greater)) - } - Err(_) => Ok(None), - } - })()) + |this: &RuneValue, other: Value| -> VmResult> { + vm(partial_cmp(this.to_script_value(), &other)) }, )?; @@ -484,6 +594,15 @@ pub fn register_types( })(), ) })?; + module.associated_function(&Protocol::INTO_ITER, |this: &mut RuneValue| { + vm( + (|| match dispatch::iter(world()?, this.promote().0, RUNE_CALLER_CONTEXT)? { + ScriptValue::FunctionMut(f) => Ok(RuneIter(ScriptFunction::Mut(f))), + ScriptValue::Function(f) => Ok(RuneIter(ScriptFunction::Plain(f))), + _ => Err(InteropError::str("iterating returned no function")), + })(), + ) + })?; module.associated_function(&Protocol::NEXT, |this: &mut RuneIter| { vm((|| match this.0.call(Vec::new())? { ScriptValue::Unit => Ok(None), @@ -511,6 +630,30 @@ pub fn register_types( rune::vm_write!(f, "{}", text) })?; + // only read the value, a copy will do + module.associated_function( + &Protocol::DISPLAY_FMT, + |this: &RuneValue, f: &mut Formatter| { + let text = rune::vm_try!(vm(world().and_then(|world| dispatch::display( + world, + this.read_reference(), + RUNE_CALLER_CONTEXT + )))); + rune::vm_write!(f, "{}", text) + }, + )?; + module.associated_function( + &Protocol::DEBUG_FMT, + |this: &RuneValue, f: &mut Formatter| { + let text = rune::vm_try!(vm(world().and_then(|world| dispatch::debug( + world, + this.read_reference(), + RUNE_CALLER_CONTEXT + )))); + rune::vm_write!(f, "{}", text) + }, + )?; + // functions of BMS are called with `f.call(..)` module .raw_function( @@ -543,6 +686,22 @@ pub fn register_types( vm(assign_member(operator, this, key(), &value)) })?; } + module.index_function(&Protocol::GET, index, move |this: &mut RuneValue| { + vm((|| { + let this = this.promote(); + let member = dispatch::index(world()?, this.0.clone(), key(), RUNE_CALLER_CONTEXT)?; + member_to_rune(member, Some(&this.0)) + })()) + })?; + for (protocol, operator) in COMPOUND_ASSIGNMENTS { + module.index_function( + protocol, + index, + move |this: &mut RuneValue, value: Value| { + vm(assign_member(operator, &this.promote(), key(), &value)) + }, + )?; + } } for &name in names { @@ -551,7 +710,7 @@ pub fn register_types( for container in [ Container::Ref, Container::Type, - Container::Entity, + Container::Value, Container::Namespace, ] { let builder = module.raw_function( @@ -561,7 +720,7 @@ pub fn register_types( match container { Container::Ref => builder.build_associated::()?, Container::Type => builder.build_associated::()?, - Container::Entity => builder.build_associated::()?, + Container::Value => builder.build_associated::()?, Container::Namespace => builder.build_associated::()?, }; } @@ -595,6 +754,34 @@ pub fn register_types( module.field_function(&Protocol::GET, name, move |this: &RuneType| { vm(get_type_member(this, name)) })?; + module.field_function(&Protocol::GET, name, move |this: &mut RuneValue| { + vm(get_value_member(this, name)) + })?; + module.field_function( + &Protocol::SET, + name, + move |this: &mut RuneValue, value: Value| { + vm((|| { + dispatch::set_index( + world()?, + this.promote().0, + ScriptValue::String(name.to_owned().into()), + from_rune(&value)?, + RUNE_CALLER_CONTEXT, + ) + })()) + }, + )?; + for (protocol, operator) in COMPOUND_ASSIGNMENTS { + module.field_function(protocol, name, move |this: &mut RuneValue, value: Value| { + vm(assign_member( + operator, + &this.promote(), + ScriptValue::String(name.to_owned().into()), + &value, + )) + })?; + } module.field_function(&Protocol::GET, name, move |this: &RuneNamespace| { vm(get_namespace_member(this, name)) })?; @@ -606,6 +793,6 @@ pub fn register_types( enum Container { Ref, Type, - Entity, + Value, Namespace, } diff --git a/crates/languages/bevy_mod_scripting_rune/src/value.rs b/crates/languages/bevy_mod_scripting_rune/src/value.rs index 051a8fda35..d1cd459b0d 100644 --- a/crates/languages/bevy_mod_scripting_rune/src/value.rs +++ b/crates/languages/bevy_mod_scripting_rune/src/value.rs @@ -14,7 +14,9 @@ use rune::{ use crate::{ error::{IntoInteropError, RuneError, describe_vm_error}, - types::{RuneEntity, RuneFunction, RuneNamespace, RuneRef, RuneType, ScriptFunction}, + types::{ + RuneFunction, RuneNamespace, RuneRef, RuneType, RuneValue, RuneValueState, ScriptFunction, + }, }; /// The context for calling a function from Rune @@ -65,7 +67,7 @@ pub fn to_rune(value: ScriptValue) -> Result { } ScriptValue::Reference(reference) => new_value(RuneRef(reference))?, ScriptValue::Type(type_id) => new_value(RuneType(type_id))?, - ScriptValue::Entity(entity) => new_value(RuneEntity(entity))?, + ScriptValue::Value(value) => new_value(RuneValue(RuneValueState::Inline(value)))?, ScriptValue::Function(function) => function_to_rune(function, None)?, ScriptValue::FunctionMut(function) => { new_value(RuneFunction(ScriptFunction::Mut(function)))? @@ -163,11 +165,11 @@ pub fn from_rune(value: &Value) -> Result { .map_err(IntoInteropError::to_bms_error)?; return Ok(ScriptValue::Type(type_ref.0)); } - if hash == ::HASH { - let entity = value - .borrow_ref::() + if hash == ::HASH { + let inline = value + .borrow_ref::() .map_err(IntoInteropError::to_bms_error)?; - return Ok(ScriptValue::Entity(entity.0)); + return Ok(inline.to_script_value()); } if hash == ::HASH { let function = value diff --git a/docs/src/Development/AddingLanguages/necessary-features.md b/docs/src/Development/AddingLanguages/necessary-features.md index 908488a6f9..8942d4bcf2 100644 --- a/docs/src/Development/AddingLanguages/necessary-features.md +++ b/docs/src/Development/AddingLanguages/necessary-features.md @@ -7,7 +7,10 @@ In order for a language to be called "implemented" in BMS, it needs to support t Every `ScriptValue` variant needs a counterpart in the language, in both directions. Three of them are handles rather than data: - `ScriptValue::Reference`: a reference to a reflected value, usually a userdata/custom type wrapping `ReflectReference`. - `ScriptValue::Type`: a type itself, i.e. what scripts use to reach a type's static members. It carries only a `TypeId` and never allocates. -- `ScriptValue::Entity`: an entity, passed by value so it never allocates. Its methods are the functions of `Entity`, found with `dispatch::static_index(world, TypeId::of::(), ..)` and called with the entity as the first argument, and two entities are equal if they are the same entity. +- `ScriptValue::Value`: a small value owned by the script (`InlineValue`, i.e. an `Entity` or a `Vec3`), stored inline instead of allocated. Scripts must not be able to tell it from a reference: + - its methods are found with `dispatch::index_value`, `ValueMember::Method` functions are called with the value as the first argument + - anything else (`ValueMember::NeedsPlace`: fields, assignments, functions which mutate it, iteration) needs a place: promote the value in place with `InlineValue::into_reference` and continue as for a reference, so variables sharing the value see the change + - operators (`dispatch::binary`, `unary`, `len`) take it directly, printing can use a copy Every context gets the builtins as globals, nothing else: `bevy_mod_scripting_bindings::builtins::builtins()` (`import_namespace`, `export_namespace`) plus the language's own `register_callback`: diff --git a/docs/src/ReleaseNotes/0.23-to-0.24.md b/docs/src/ReleaseNotes/0.23-to-0.24.md index 8c523fbcb3..7e91ba59c4 100644 --- a/docs/src/ReleaseNotes/0.23-to-0.24.md +++ b/docs/src/ReleaseNotes/0.23-to-0.24.md @@ -39,8 +39,6 @@ Query results are plain lists of the entity followed by the components, `result: end ``` -Entities are plain values (`ScriptValue::Entity`), compared with `==` and with the same methods (`entity:index()`), also when read from a component field. - Other changes: - `world.get_type_by_name` only takes full paths, i.e. `"bevy_transform::components::transform::Transform"` or `"script.my_game.Enemy"`. - Numbers work on the left of operators: `2 * v`, `1 - v` (dispatched to `mul`, `rsub`, ...), in Rhai too. @@ -75,7 +73,7 @@ Other changes: - 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 `declare_writes`, `false` declares all components as read (i.e. for `EntityRef` queries). -- `ScriptValue::Entity(Entity)` is new, language implementations convert it to a value whose methods are the functions of `Entity` (see `LuaEntity` / `RuneEntity`). `V` converts to and from it, `R` / `M` only accept references, take `V` instead. `ScriptQueryResult` converts to a list instead of a reference, its script functions are removed. +- `V` returns small values without drop glue (at most 16 bytes, i.e. `Entity`, `Vec3`) as the new `ScriptValue::Value(InlineValue)` instead of an allocated reference, decided at compile time (`is_inline`). Scripts can't tell the difference, but Rust code inspecting returned `ScriptValue`s must handle `Value`, and functions taking a `ScriptValue` should accept it where they accept a `Reference` (`InlineValue::into_reference` makes an allocated copy). `V` and `R` accept inline values (`R` holds a copy, its field is private), `M` only for the receiver of a method, language implementations promote values before calling those, see [Necessary Features](../Development/AddingLanguages/necessary-features.md). `ScriptQueryResult` converts to a list instead of a reference, its script functions are removed. - `ScriptQueryResult::components` is a `Vec>`, `None` for absent optional components. `ArchetypeFilter::allow` / `is_allowed` and `WorldAccessGuard::check_archetype_access` take whether the component is written. - `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`. From 3bf0796552bc2bd786c42bbf6019ed0f647d3630 Mon Sep 17 00:00:00 2001 From: makspll Date: Thu, 22 Oct 2026 20:58:16 +0000 Subject: [PATCH 3/3] fix: the Ref/Mut conversion benches convert references, small values are inline --- benches/benchmarks.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index d29f7b78ce..343c23ef16 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -235,12 +235,13 @@ fn conversion_benchmarks(criterion: &mut Criterion) { BatchSize::SmallInput, ); + // references, small values are inline and have no place to borrow from perform_benchmark_with_generator( "Ref::from", - &|rng, w| { - V::new(ReflectyVal(rng.random::())) - .into_script(w) - .unwrap() + &|rng, _| { + ScriptValue::Reference(ReflectReference::new_allocated(ReflectyVal( + rng.random::(), + ))) }, &|w, i| { R::::from_script(i, w).unwrap(); @@ -251,10 +252,10 @@ fn conversion_benchmarks(criterion: &mut Criterion) { perform_benchmark_with_generator( "Mut::from", - &|rng, w| { - V::new(ReflectyVal(rng.random::())) - .into_script(w) - .unwrap() + &|rng, _| { + ScriptValue::Reference(ReflectReference::new_allocated(ReflectyVal( + rng.random::(), + ))) }, &|w, i| { M::::from_script(i, w).unwrap();