Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ members = [
"tools/*",
# Bevy's error codes. This is a crate so we can automatically check all of the code blocks.
"errors",
"core/simulatedentity",
"core/simulatedentity_macro",
]
exclude = [
# Integration tests are not part of the workspace
Expand Down
18 changes: 18 additions & 0 deletions core/simulatedentity/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "simulatedentity"
version = "0.1.0"
edition = "2024"

[dependencies]
bevy_app = { path = "../../crates/bevy_app", version = "0.16.0-dev" }
bevy_ecs = { path = "../../crates/bevy_ecs", version = "0.16.0-dev" }
bevy_math = { path = "../../crates/bevy_math", version = "0.16.0-dev" }
bevy_time = { path = "../../crates/bevy_time", version = "0.16.0-dev" }
simulatedentity_macro = { path = "../simulatedentity_macro" }
memoffset = "0.9"

[features]
default = []

[lints]
workspace = true
75 changes: 75 additions & 0 deletions core/simulatedentity/src/dod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#![allow(unsafe_code)]

use bevy_ecs::{archetype::{Archetype, ArchetypeId}, prelude::*};
use bevy_math::Vec4;
use crate::{Position, Velocity};

pub struct DODArray {
pub bytes: Vec<u8>,
}

pub struct DODStorage {
pub pos: DODArray,
pub vel: DODArray,
}

#[derive(Clone, Copy)]
pub struct FieldInfo {
pub offset: usize,
pub size: usize,
}

pub trait DODConvertible: Sized {
const FIELD_LAYOUT: &'static [FieldInfo];
fn to_bytes(&self) -> Vec<u8>;
fn from_bytes(bytes: &[u8]) -> Self;
}

pub fn convert_chunk_to_dod(chunk: &Archetype) -> DODStorage {
let len = chunk.entities().len();
use core::mem::size_of;
let pos_bytes = vec![0u8; len * size_of::<Position>()];
let vel_bytes = vec![0u8; len * size_of::<Velocity>()];
DODStorage {
pos: DODArray { bytes: pos_bytes },
vel: DODArray { bytes: vel_bytes },
}
}

pub fn apply_dod_back(_world: &mut World, _storage: &DODStorage, _id: ArchetypeId) {}

pub fn move_simd(storage: &mut DODStorage, dt: f32) {
move_simd_inner(storage, dt);
}

#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), target_feature = "sse2"))]
fn move_simd_inner(storage: &mut DODStorage, dt: f32) {
unsafe {
use core::arch::x86_64::*;
let len = storage.pos.bytes.len() / 16;
let pos_ptr = storage.pos.bytes.as_mut_ptr() as *mut __m128;
let vel_ptr = storage.vel.bytes.as_mut_ptr() as *mut __m128;
let dt_vec = _mm_set1_ps(dt);
for i in 0..len {
let p = _mm_load_ps(pos_ptr.add(i) as *const f32);
let v = _mm_load_ps(vel_ptr.add(i) as *const f32);
let v_dt = _mm_mul_ps(v, dt_vec);
let res = _mm_add_ps(p, v_dt);
_mm_store_ps(pos_ptr.add(i) as *mut f32, res);
}
}
}

#[cfg(not(all(any(target_arch = "x86", target_arch = "x86_64"), target_feature = "sse2")))]
fn move_simd_inner(storage: &mut DODStorage, dt: f32) {
let len = storage.pos.bytes.len() / 16;
for i in 0..len {
unsafe {
let pos_ptr = storage.pos.bytes.as_mut_ptr() as *mut Vec4;
let vel_ptr = storage.vel.bytes.as_ptr() as *const Vec4;
let pos = &mut *pos_ptr.add(i);
let vel = &*vel_ptr.add(i);
*pos = *pos + (*vel * dt);
}
}
}
104 changes: 104 additions & 0 deletions core/simulatedentity/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#![allow(unsafe_code)]

use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{archetype::ArchetypeId, prelude::*};
use bevy_math::{Vec3, Vec4};
use bevy_time::Time;

pub use simulatedentity_macro::DODConvertible;

pub mod dod;
use dod::{apply_dod_back, convert_chunk_to_dod, move_simd, DODStorage};

#[derive(Component, Clone, Copy, DODConvertible)]
#[repr(C)]
pub struct Position {
pub value: Vec4,
}

impl From<Vec3> for Position {
fn from(v: Vec3) -> Self {
Self { value: v.extend(0.0) }
}
}

#[derive(Component, Clone, Copy, DODConvertible)]
#[repr(C)]
pub struct Velocity {
pub value: Vec4,
}

impl From<Vec3> for Velocity {
fn from(v: Vec3) -> Self {
Self { value: v.extend(0.0) }
}
}

#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct UpdateSimulatedEntities;

pub trait SimulatedEntityCommands {
fn spawn_simulated_entity(&mut self, bundle: impl Bundle) -> Entity;
}

impl<'w, 's> SimulatedEntityCommands for Commands<'w, 's> {
fn spawn_simulated_entity(&mut self, bundle: impl Bundle) -> Entity {
self.spawn(bundle).id()
}
}

#[derive(Default, Resource)]
struct DODStore(pub std::collections::HashMap<ArchetypeId, DODStorage>);

fn update_simulated_entities(world: &mut World) {
let pos_id = world.component_id::<Position>().unwrap();
let vel_id = world.component_id::<Velocity>().unwrap();
let ids: Vec<ArchetypeId> = world
.archetypes()
.iter()
.filter(|a| a.len() as usize >= 1000 && a.contains(pos_id) && a.contains(vel_id))
.map(|a| a.id())
.collect();
let dt = world.resource::<Time>().delta_secs();
world.resource_scope(|world, mut store: Mut<DODStore>| {
for id in &ids {
let arch = world.archetypes().get(*id).unwrap();
let entry = store
.0
.entry(*id)
.or_insert_with(|| convert_chunk_to_dod(arch));
move_simd(entry, dt);
}
store.0.retain(|&id, _| {
world
.archetypes()
.get(id)
.map_or(false, |a| a.len() as usize >= 1000)
});
});
}

fn apply_back_system(world: &mut World) {
let ids: Vec<ArchetypeId> = world.resource::<DODStore>().0.keys().copied().collect();
for id in ids {
world.resource_scope(|world, mut store: Mut<DODStore>| {
if let Some(dod) = store.0.get(&id) {
apply_dod_back(world, dod, id);
}
});
}
}

#[derive(Default)]
pub struct SimulatedEntityPlugin;

impl Plugin for SimulatedEntityPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<DODStore>()
.add_systems(
PostUpdate,
update_simulated_entities.in_set(UpdateSimulatedEntities),
)
.add_systems(PostUpdate, apply_back_system.after(UpdateSimulatedEntities));
}
}
17 changes: 17 additions & 0 deletions core/simulatedentity_macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "simulatedentity_macro"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

[dependencies]
syn = { version = "2", features = ["derive", "full"] }
quote = "1"
proc-macro2 = "1"
memoffset = "0.9"
proc-macro-crate = "3"

[lints]
workspace = true
52 changes: 52 additions & 0 deletions core/simulatedentity_macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use proc_macro_crate::{crate_name, FoundCrate};
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(DODConvertible)]
pub fn derive_dod_convertible(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let crate_path = match crate_name("simulatedentity") {
Ok(FoundCrate::Itself) => quote!(crate),
Ok(FoundCrate::Name(name)) => {
let ident = format_ident!("{}", name);
quote!(#ident)
}
Err(_) => quote!(simulatedentity),
};
let fields = if let syn::Data::Struct(s) = input.data {
s.fields
} else {
panic!("DODConvertible can only be derived for structs");
};

let field_infos = fields.iter().map(|f| {
let ident = f.ident.as_ref().expect("named fields required");
let ty = &f.ty;
quote! {
#crate_path::dod::FieldInfo {
offset: ::memoffset::offset_of!(#name, #ident),
size: ::core::mem::size_of::<#ty>(),
}
}
});

let expanded = quote! {
impl #crate_path::dod::DODConvertible for #name {
const FIELD_LAYOUT: &'static [#crate_path::dod::FieldInfo] = &[#(#field_infos),*];
fn to_bytes(&self) -> Vec<u8> {
unsafe {
let ptr = self as *const Self as *const u8;
::core::slice::from_raw_parts(ptr, ::core::mem::size_of::<Self>()).to_vec()
}
}
fn from_bytes(bytes: &[u8]) -> Self {
assert_eq!(bytes.len(), ::core::mem::size_of::<Self>());
unsafe { *(bytes.as_ptr() as *const Self) }
}
}
};

TokenStream::from(expanded)
}