use std::num::NonZeroU64;
use bevy::{
ecs::{component::Component, reflect::ReflectComponent, world::Mut},
log::trace,
platform::collections::HashSet,
reflect::Reflect,
};
use serde::{Deserialize, Serialize};
use crate::{graph::Value, ToWgslString, ValueType};
#[derive(Debug, Clone, PartialEq, Hash, Reflect, Serialize, Deserialize)]
pub struct Property {
name: String,
default_value: Value,
}
impl Property {
#[inline]
pub fn new(name: impl Into<String>, default_value: impl Into<Value>) -> Self {
Self {
name: name.into(),
default_value: default_value.into(),
}
}
#[inline]
pub fn name(&self) -> &str {
self.name.as_ref()
}
#[inline]
pub fn default_value(&self) -> &Value {
&self.default_value
}
#[inline]
pub fn value_type(&self) -> ValueType {
self.default_value.value_type()
}
#[inline]
pub fn size(&self) -> usize {
self.default_value.value_type().size()
}
}
impl ToWgslString for Property {
fn to_wgsl_string(&self) -> String {
format!("properties[properties_array_index].{}", self.name)
}
}
#[derive(Debug, Clone, Reflect)]
pub(crate) struct PropertyInstance {
pub def: Property,
pub value: Value,
}
#[derive(Debug, Default, Clone, Component, Reflect)]
#[reflect(Component)]
pub struct EffectProperties {
properties: Vec<PropertyInstance>,
}
impl EffectProperties {
pub fn with_properties(
mut self,
properties: impl IntoIterator<Item = (String, Value)>,
) -> Self {
let iter = properties.into_iter();
for (name, value) in iter {
if let Some(index) = self.properties.iter().position(|p| p.def.name() == name) {
assert_eq!(
self.properties[index].value.value_type(),
value.value_type(),
"Trying to overwrite existing property '{}' with value of type {:?}, but property has type {:?}",
name,
value.value_type(),
self.properties[index].value.value_type()
);
self.properties[index].value = value;
} else {
self.properties.push(PropertyInstance {
def: Property::new(name, value),
value,
});
}
}
self
}
#[allow(dead_code)] pub(crate) fn properties(&self) -> &[PropertyInstance] {
&self.properties
}
pub fn get_stored(&self, name: &str) -> Option<Value> {
self.properties
.iter()
.find(|prop| prop.def.name() == name)
.map(|prop| prop.value)
}
pub fn set(&mut self, name: &str, value: Value) {
if let Some(index) = self
.properties
.iter()
.position(|prop| prop.def.name() == name)
{
let prop = &mut self.properties[index];
assert_eq!(
prop.def.value_type(),
value.value_type(),
"Cannot assign value of type {:?} to property '{}' of type {:?}",
value.value_type(),
prop.def.name(),
prop.def.value_type()
);
prop.value = value;
} else {
self.properties.push(PropertyInstance {
def: Property::new(name, value),
value,
});
}
}
pub fn set_if_changed<'p>(
mut this: Mut<'p, EffectProperties>,
name: &str,
value: Value,
) -> Mut<'p, EffectProperties> {
if let Some(index) = this
.properties
.iter()
.position(|prop| prop.def.name() == name)
{
let prop = &this.properties[index];
assert_eq!(
prop.def.value_type(),
value.value_type(),
"Cannot assign value of type {:?} to property '{}' of type {:?}",
value.value_type(),
prop.def.name(),
prop.def.value_type()
);
if prop.value != value {
this.properties[index].value = value;
}
} else {
this.properties.push(PropertyInstance {
def: Property::new(name, value),
value,
});
}
this
}
pub(crate) fn update(
mut this: Mut<'_, EffectProperties>,
asset_properties: &[Property],
is_added: bool,
) {
trace!(
"Updating effect properties from asset (is_added: {})",
is_added
);
let mut new_props = vec![];
let mut intersect = HashSet::new();
for prop in asset_properties {
if this.properties.iter().any(|p| p.def.name() == prop.name()) {
intersect.insert(prop.name());
continue;
}
new_props.push(PropertyInstance {
def: prop.clone(),
value: *prop.default_value(),
});
}
if intersect.len() != this.properties.len() {
this.properties
.retain(|prop| intersect.contains(prop.def.name()));
}
if !new_props.is_empty() {
this.properties.append(&mut new_props);
}
}
pub(crate) fn serialize(&self, layout: &PropertyLayout) -> Vec<u8> {
let size = layout.cpu_size() as usize;
let mut data = vec![0; size];
for property in &self.properties {
if let Some(offset) = layout.offset(property.def.name()) {
let offset = offset as usize;
let size = property.def.size();
let src = property.value.as_bytes();
debug_assert_eq!(src.len(), size);
let dst = &mut data[offset..offset + size];
dst.copy_from_slice(src);
}
}
data
}
}
#[derive(Clone)]
struct PropertyLayoutEntry {
property: Property,
offset: u32,
}
impl PartialEq for PropertyLayoutEntry {
fn eq(&self, other: &Self) -> bool {
self.property.name() == other.property.name()
&& self.property.value_type() == other.property.value_type()
&& self.offset == other.offset
}
}
impl Eq for PropertyLayoutEntry {}
impl std::hash::Hash for PropertyLayoutEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.property.name().hash(state);
self.property.value_type().hash(state);
self.offset.hash(state);
}
}
impl std::fmt::Debug for PropertyLayoutEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"(+{}) {}: {}",
self.offset,
self.property.name(),
self.property.value_type().to_wgsl_string(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PropertyLayout {
layout: Vec<PropertyLayoutEntry>,
}
impl PropertyLayout {
pub const fn empty() -> Self {
Self { layout: vec![] }
}
pub fn new<'a>(iter: impl IntoIterator<Item = &'a Property>) -> Self {
let mut properties = iter.into_iter().collect::<Vec<_>>();
properties.sort_unstable_by_key(|prop| prop.size());
let properties = properties;
let mut layout = vec![];
let mut offset = 0;
let index4 = properties.partition_point(|prop| prop.size() < 16);
for &prop in properties.iter().skip(index4) {
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
offset += 16;
layout.push(entry);
}
let index2 = properties.partition_point(|prop| prop.size() < 8);
let num1 = index2;
let index3 = properties.partition_point(|prop| prop.size() < 12);
let num2 = (index2..index3).len();
let num3 = (index3..index4).len();
let num_pairs = num1.min(num3);
for i in 0..num_pairs {
let prop = properties[index3 + i];
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
offset += 12;
layout.push(entry);
let prop = properties[i];
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
offset += 4;
layout.push(entry);
}
let index1 = num_pairs;
let index3 = index3 + num_pairs;
let num1 = num1 - num_pairs;
let num3 = num3 - num_pairs;
for i in 0..(num2 / 2) {
for j in 0..2 {
let prop = properties[index2 + i * 2 + j];
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
offset += 8;
layout.push(entry);
}
}
let index2 = index2 + (num2 / 2) * 2;
let num2 = num2 % 2;
if num3 > num1 {
debug_assert_eq!(num1, 0);
for i in 0..num3 {
let prop = properties[index3 + i];
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
offset += 16;
layout.push(entry);
}
if num2 > 0 {
debug_assert_eq!(num2, 1);
let prop = properties[index2];
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
layout.push(entry);
}
} else {
debug_assert_eq!(num3, 0);
if num2 > 0 {
debug_assert_eq!(num2, 1);
let prop = properties[index2];
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
offset += 8;
layout.push(entry);
}
for i in 0..num1 {
let prop = properties[index1 + i];
let entry = PropertyLayoutEntry {
property: prop.clone(),
offset,
};
offset += 4;
layout.push(entry);
}
}
Self { layout }
}
pub fn is_empty(&self) -> bool {
self.layout.is_empty()
}
pub fn cpu_size(&self) -> u32 {
if self.layout.is_empty() {
0
} else {
let last_entry = self.layout.last().unwrap();
last_entry.offset + last_entry.property.size() as u32
}
}
pub fn align(&self) -> usize {
if self.layout.is_empty() {
0
} else {
self.layout
.iter()
.map(|entry| entry.property.value_type().align())
.max()
.unwrap()
}
}
pub fn properties(&self) -> impl Iterator<Item = (u32, &Property)> {
self.layout
.iter()
.map(|entry| (entry.offset, &entry.property))
}
pub fn min_binding_size(&self) -> NonZeroU64 {
assert!(
!self.layout.is_empty(),
"Cannot compute min binding size for empty property layout."
);
let size = self.cpu_size() as usize;
let align = self.align();
NonZeroU64::new(size.next_multiple_of(align) as u64).unwrap()
}
pub fn contains(&self, name: &str) -> bool {
self.layout.iter().any(|entry| entry.property.name == name)
}
pub fn generate_property_struct_code(&self) -> Option<String> {
if self.layout.is_empty() {
return None;
}
let content = self
.layout
.iter()
.map(|entry| {
format!(
" {}: {},",
entry.property.name(),
entry.property.value_type().to_wgsl_string()
)
})
.fold("struct Properties {\n".to_string(), |mut a, b| {
a.reserve(b.len() + 1);
a.push_str(&b);
a.push('\n');
a
})
+ "}\n";
Some(content)
}
pub(crate) fn offset(&self, name: &str) -> Option<u32> {
self.layout.iter().find_map(|entry| {
if entry.property.name == name {
Some(entry.offset)
} else {
None
}
})
}
}
impl Default for PropertyLayout {
fn default() -> Self {
PropertyLayout::empty()
}
}
#[cfg(test)]
mod tests {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use bevy::{
ecs::change_detection::{MaybeLocation, Tick},
math::{Vec2, Vec3, Vec4},
};
use super::*;
#[test]
fn property_basic() {
let value = Value::Scalar(3_f32.into());
let p = Property::new("my_prop", value);
assert_eq!(p.name(), "my_prop");
assert_eq!(*p.default_value(), value);
assert_eq!(p.value_type(), value.value_type());
assert_eq!(p.size(), value.value_type().size());
assert_eq!(
p.to_wgsl_string(),
format!("properties[properties_array_index].{}", p.name())
);
}
#[test]
fn property_serde() {
let p = Property::new("my_prop", Value::Scalar(3_f32.into()));
let s = ron::to_string(&p).unwrap();
println!("property: {:?}", s);
let p_serde: Property = ron::from_str(&s).unwrap();
assert_eq!(p_serde, p);
}
fn hash_ple(ple: &PropertyLayoutEntry) -> u64 {
let mut hasher = DefaultHasher::default();
ple.hash(&mut hasher);
hasher.finish()
}
#[test]
fn property_layout_entry() {
let prop1 = Property::new("my_prop", Vec3::NEG_X);
let entry1 = PropertyLayoutEntry {
property: prop1,
offset: 16,
};
assert_eq!(
format!("{:?}", entry1),
"(+16) my_prop: vec3<f32>".to_string()
);
assert_eq!(entry1, entry1);
assert_eq!(hash_ple(&entry1), hash_ple(&entry1));
let prop1b = Property::new("my_prop", Vec3::X);
let entry1b = PropertyLayoutEntry {
property: prop1b,
offset: 16,
};
assert_eq!(
format!("{:?}", entry1b),
"(+16) my_prop: vec3<f32>".to_string()
);
assert_eq!(entry1, entry1b);
assert_eq!(hash_ple(&entry1), hash_ple(&entry1b));
let prop2 = Property::new("other_prop", Vec3::Y);
let entry2 = PropertyLayoutEntry {
property: prop2,
offset: 16,
};
assert_eq!(
format!("{:?}", entry2),
"(+16) other_prop: vec3<f32>".to_string()
);
assert_ne!(entry1, entry2);
assert_ne!(hash_ple(&entry1), hash_ple(&entry2));
assert_ne!(entry1b, entry2);
assert_ne!(hash_ple(&entry1b), hash_ple(&entry2));
let prop3 = Property::new("my_prop", 3.4_f32);
let entry3 = PropertyLayoutEntry {
property: prop3,
offset: 16,
};
assert_eq!(format!("{:?}", entry3), "(+16) my_prop: f32".to_string());
assert_ne!(entry1, entry3);
assert_ne!(hash_ple(&entry1), hash_ple(&entry3));
assert_ne!(entry1b, entry3);
assert_ne!(hash_ple(&entry1b), hash_ple(&entry3));
let prop4 = Property::new("my_prop", Vec3::NEG_X);
let entry4 = PropertyLayoutEntry {
property: prop4,
offset: 24,
};
assert_eq!(
format!("{:?}", entry4),
"(+24) my_prop: vec3<f32>".to_string()
);
assert_ne!(entry1, entry4);
assert_ne!(hash_ple(&entry1), hash_ple(&entry4));
assert_ne!(entry1b, entry4);
assert_ne!(hash_ple(&entry1b), hash_ple(&entry4));
}
#[test]
fn layout_empty() {
let l = PropertyLayout::empty();
assert!(l.is_empty());
assert_eq!(l.cpu_size(), 0);
assert_eq!(l.align(), 0);
assert_eq!(l.properties().next(), None);
let s = l.generate_property_struct_code();
assert!(s.is_none());
}
#[test]
#[should_panic]
fn layout_empty_min_binding_size_panic() {
let l = PropertyLayout::empty();
_ = l.min_binding_size();
}
#[test]
fn layout_valid() {
let prop1 = Property::new("f32", 3.4_f32);
let prop2 = Property::new("vec3", Vec3::ZERO);
let prop3 = Property::new("vec2", Vec2::NEG_Y);
let prop4 = Property::new("vec4", Vec4::Y);
let layout = PropertyLayout::new([&prop1, &prop2, &prop3, &prop4]);
assert!(!layout.is_empty());
assert_eq!(layout.cpu_size(), 40);
assert_eq!(layout.align(), 16);
assert_eq!(layout.min_binding_size(), NonZeroU64::new(48).unwrap());
let mut it = layout.properties();
assert_eq!(it.next(), Some((0, &prop4)));
assert_eq!(it.next(), Some((16, &prop2)));
assert_eq!(it.next(), Some((28, &prop1)));
assert_eq!(it.next(), Some((32, &prop3)));
assert_eq!(it.next(), None);
let s = layout.generate_property_struct_code();
assert_eq!(
s,
Some(
r#"struct Properties {
vec4: vec4<f32>,
vec3: vec3<f32>,
f32: f32,
vec2: vec2<f32>,
}
"#
.to_string()
)
);
}
#[test]
fn layout_padding_vec3() {
let prop1 = Property::new("vec4a", Vec4::Y);
let prop2 = Property::new("vec3b", Vec3::ZERO);
let prop3 = Property::new("vec3c", Vec3::ONE);
let layout = PropertyLayout::new([&prop1, &prop2, &prop3]);
assert!(!layout.is_empty());
assert_eq!(layout.cpu_size(), 44); assert_eq!(layout.align(), 16);
assert_eq!(layout.min_binding_size(), NonZeroU64::new(48).unwrap());
let mut it = layout.properties();
assert_eq!(it.next(), Some((0, &prop1)));
assert_eq!(it.next(), Some((16, &prop2)));
assert_eq!(it.next(), Some((32, &prop3)));
assert_eq!(it.next(), None);
let s = layout.generate_property_struct_code();
assert_eq!(
s,
Some(
r#"struct Properties {
vec4a: vec4<f32>,
vec3b: vec3<f32>,
vec3c: vec3<f32>,
}
"#
.to_string()
)
);
}
#[test]
fn layout_tail_332() {
let prop1 = Property::new("vec2", Vec2::NEG_Y);
let prop2 = Property::new("vec3a", Vec3::ZERO);
let prop3 = Property::new("vec3b", Vec3::NEG_X);
let layout = PropertyLayout::new([&prop1, &prop2, &prop3]);
assert!(!layout.is_empty());
assert_eq!(layout.cpu_size(), 40);
assert_eq!(layout.align(), 16);
assert_eq!(layout.min_binding_size(), NonZeroU64::new(48).unwrap());
let mut it = layout.properties();
assert_eq!(it.next(), Some((0, &prop2)));
assert_eq!(it.next(), Some((16, &prop3)));
assert_eq!(it.next(), Some((32, &prop1)));
assert_eq!(it.next(), None);
let s = layout.generate_property_struct_code();
assert_eq!(
s,
Some(
r#"struct Properties {
vec3a: vec3<f32>,
vec3b: vec3<f32>,
vec2: vec2<f32>,
}
"#
.to_string()
)
);
}
#[test]
fn layout_tail_32() {
let prop1 = Property::new("vec2", Vec2::NEG_Y);
let prop2 = Property::new("vec3", Vec3::ZERO);
let layout = PropertyLayout::new([&prop1, &prop2]);
assert!(!layout.is_empty());
assert_eq!(layout.cpu_size(), 24);
assert_eq!(layout.align(), 16);
assert_eq!(layout.min_binding_size(), NonZeroU64::new(32).unwrap());
let mut it = layout.properties();
assert_eq!(it.next(), Some((0, &prop2)));
assert_eq!(it.next(), Some((16, &prop1)));
assert_eq!(it.next(), None);
let s = layout.generate_property_struct_code();
assert_eq!(
s,
Some(
r#"struct Properties {
vec3: vec3<f32>,
vec2: vec2<f32>,
}
"#
.to_string()
)
);
}
#[test]
fn layout_tail_21() {
let prop1 = Property::new("f32", 3.4_f32);
let prop2 = Property::new("vec2", Vec2::NEG_Y);
let layout = PropertyLayout::new([&prop1, &prop2]);
assert!(!layout.is_empty());
assert_eq!(layout.cpu_size(), 12);
assert_eq!(layout.align(), 8);
assert_eq!(layout.min_binding_size(), NonZeroU64::new(16).unwrap());
let mut it = layout.properties();
assert_eq!(it.next(), Some((0, &prop2)));
assert_eq!(it.next(), Some((8, &prop1)));
assert_eq!(it.next(), None);
let s = layout.generate_property_struct_code();
assert_eq!(
s,
Some(
r#"struct Properties {
vec2: vec2<f32>,
f32: f32,
}
"#
.to_string()
)
);
}
#[test]
fn effect_properties_with_properties() {
let ep = EffectProperties::default()
.with_properties([
("a".to_string(), 3.0.into()),
("b".to_string(), Vec3::ZERO.into()),
])
.with_properties([
("a".to_string(), 7.0.into()),
("c".to_string(), Vec2::ONE.into()),
]);
assert_eq!(ep.properties.len(), 3);
assert_eq!(ep.properties[0].def.name(), "a");
assert_eq!(
ep.properties[0].def.default_value(),
&Value::Scalar(3.0.into())
);
assert_eq!(ep.properties[0].value, 7.0.into());
assert_eq!(ep.properties[1].def.name(), "b");
assert_eq!(
ep.properties[1].def.default_value(),
&Value::Vector(Vec3::ZERO.into())
);
assert_eq!(ep.properties[1].value, Vec3::ZERO.into());
assert_eq!(ep.properties[2].def.name(), "c");
assert_eq!(
ep.properties[2].def.default_value(),
&Value::Vector(Vec2::ONE.into())
);
assert_eq!(ep.properties[2].value, Vec2::ONE.into());
}
#[test]
#[should_panic]
fn effect_properties_with_properties_type_mismatch() {
let _ = EffectProperties::default()
.with_properties([("a".to_string(), 3.0.into())])
.with_properties([("a".to_string(), Vec2::ONE.into())]);
}
#[test]
fn effect_properties_get_stored() {
let ep = EffectProperties::default()
.with_properties([
("a".to_string(), 3.0.into()),
("b".to_string(), Vec3::ZERO.into()),
])
.with_properties([
("a".to_string(), 7.0.into()),
("c".to_string(), Vec2::ONE.into()),
]);
assert!(ep.get_stored("a").is_some());
assert!(ep.get_stored("b").is_some());
assert!(ep.get_stored("c").is_some());
assert!(ep.get_stored("x").is_none());
}
#[test]
fn effect_properties_set() {
let mut ep = EffectProperties::default().with_properties([
("a".to_string(), 3.0.into()),
("b".to_string(), Vec3::ZERO.into()),
]);
ep.set("a", 7.0.into());
ep.set("x", 3.0.into());
}
#[test]
#[should_panic]
fn effect_properties_set_type_mismatch() {
let mut ep = EffectProperties::default().with_properties([("a".to_string(), 3.0.into())]);
ep.set("a", Vec3::ZERO.into());
}
#[test]
fn effect_properties_update_empty() {
let mut ep = EffectProperties::default();
let mut added = Tick::new(0);
let last_changed_prev = Tick::new(0u32.wrapping_sub(1u32));
let mut last_changed = last_changed_prev;
let last_run = last_changed;
let this_run = added;
let asset_properties = vec![];
{
let mut caller = MaybeLocation::caller();
let this = Mut::new(
&mut ep,
&mut added,
&mut last_changed,
last_run,
this_run,
caller.as_mut(),
);
let is_added = true;
EffectProperties::update(this, &asset_properties, is_added);
}
assert!(ep.properties.is_empty());
assert_eq!(last_changed, last_changed_prev); }
#[test]
fn effect_properties_update_added() {
let mut ep = EffectProperties::default();
let mut added = Tick::new(0);
let last_changed_prev = Tick::new(0u32.wrapping_sub(1u32));
let mut last_changed = last_changed_prev;
let last_run = last_changed;
let this_run = added;
let asset_properties = vec![Property::new("prop1", 32.)];
{
let mut caller = MaybeLocation::caller();
let this = Mut::new(
&mut ep,
&mut added,
&mut last_changed,
last_run,
this_run,
caller.as_mut(),
);
let is_added = true;
EffectProperties::update(this, &asset_properties, is_added);
}
assert_eq!(ep.properties.len(), 1);
assert_eq!(ep.properties[0].def, asset_properties[0]);
assert_eq!(last_changed, this_run); }
#[test]
fn effect_properties_update_removed() {
let mut ep = EffectProperties::default();
ep.set("unknown", 3.into());
let mut added = Tick::new(0);
let last_changed_prev = Tick::new(0u32.wrapping_sub(1u32));
let mut last_changed = last_changed_prev;
let last_run = last_changed;
let this_run = added;
let asset_properties = vec![];
{
let mut caller = MaybeLocation::caller();
let this = Mut::new(
&mut ep,
&mut added,
&mut last_changed,
last_run,
this_run,
caller.as_mut(),
);
let is_added = true;
EffectProperties::update(this, &asset_properties, is_added);
}
assert!(ep.properties.is_empty());
assert_eq!(last_changed, this_run); }
#[test]
fn effect_properties_update_override() {
let mut ep = EffectProperties::default();
ep.set("prop1", 5_f32.into());
let mut added = Tick::new(0);
let last_changed_prev = Tick::new(0u32.wrapping_sub(1u32));
let mut last_changed = last_changed_prev;
let last_run = last_changed;
let this_run = added;
let asset_properties = vec![Property::new("prop1", 32.)];
{
let mut caller = MaybeLocation::caller();
let this = Mut::new(
&mut ep,
&mut added,
&mut last_changed,
last_run,
this_run,
caller.as_mut(),
);
let is_added = true;
EffectProperties::update(this, &asset_properties, is_added);
}
assert_eq!(ep.properties.len(), 1);
assert_eq!(ep.properties[0].def.name(), asset_properties[0].name());
assert_eq!(ep.properties[0].value, 5_f32.into());
assert_eq!(last_changed, last_changed_prev); }
#[test]
fn effect_properties_update_mixed() {
let mut ep = EffectProperties::default();
ep.set("prop1", 5_f32.into());
let mut added = Tick::new(0);
let last_changed_prev = Tick::new(0u32.wrapping_sub(1u32));
let mut last_changed = last_changed_prev;
let last_run = last_changed;
let this_run = added;
let asset_properties = vec![Property::new("prop1", 32.), Property::new("prop2", false)];
{
let mut caller = MaybeLocation::caller();
let this = Mut::new(
&mut ep,
&mut added,
&mut last_changed,
last_run,
this_run,
caller.as_mut(),
);
let is_added = true;
EffectProperties::update(this, &asset_properties, is_added);
}
assert_eq!(ep.properties.len(), 2);
assert_eq!(ep.properties[0].def.name(), asset_properties[0].name());
assert_eq!(ep.properties[0].value, 5_f32.into());
assert_eq!(ep.properties[1].def, asset_properties[1]);
assert_eq!(last_changed, this_run); }
#[test]
fn effect_properties_serialize() {
let ep = EffectProperties::default().with_properties([
("a".to_string(), 3.0.into()),
("b".to_string(), Vec3::ONE.into()),
]);
let layout = PropertyLayout::new(ep.properties().iter().map(|pi| &pi.def));
let blob = ep.serialize(&layout);
assert_eq!(blob.len(), layout.cpu_size() as usize);
let pi_a = &ep.properties()[0];
let size = pi_a.def.size();
assert_eq!(size, 4); let offset = layout.offset(pi_a.def.name()).unwrap() as usize;
let raw = &blob[offset..offset + size];
#[allow(unsafe_code)]
let raw_ref: &[u8; 4] = unsafe { std::mem::transmute(&[3.0_f32]) };
assert_eq!(raw, raw_ref);
let pi_b = &ep.properties()[1];
let size = pi_b.def.size();
assert_eq!(size, 12); let offset = layout.offset(pi_b.def.name()).unwrap() as usize;
let raw = &blob[offset..offset + size];
#[allow(unsafe_code)]
let raw_ref: &[u8; 12] = unsafe { std::mem::transmute(&[1_f32, 1_f32, 1_f32]) };
assert_eq!(raw, raw_ref);
}
}