Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
39bf69f
contiguous query data/filter
Jenya705 Nov 30, 2025
19c7a2a
contiguous iter
Jenya705 Dec 1, 2025
9f391c7
fmt
Jenya705 Dec 1, 2025
564b771
fmt
Jenya705 Dec 1, 2025
667955d
safety comment
Jenya705 Dec 1, 2025
6ad54b0
small fixes
Jenya705 Dec 1, 2025
f3ebbc7
removed contiguous query filter
Jenya705 Dec 1, 2025
d66a0cb
macro
Jenya705 Dec 3, 2025
9e54e50
added example into the table
Jenya705 Dec 3, 2025
c5aa426
example's metadata fix
Jenya705 Dec 3, 2025
cd000c1
example fix
Jenya705 Dec 3, 2025
420dbd5
(Contiguous)QueryData's docs
Jenya705 Dec 3, 2025
2fb9cd3
typo
Jenya705 Dec 3, 2025
59cd6c6
docs fix
Jenya705 Dec 4, 2025
6c359ee
Has contiguous impl
Jenya705 Dec 4, 2025
083f714
thinsliceptr refinements
Jenya705 Dec 4, 2025
3881d63
QueryContiguousIter additions
Jenya705 Dec 4, 2025
7d5ba3d
ThinSlicePtr::cast refinements, test ticks and docs
Jenya705 Dec 9, 2025
8571bd7
typo
Jenya705 Dec 9, 2025
d1d9386
cast refinements
Jenya705 Dec 9, 2025
28d40cc
release notes
Jenya705 Dec 9, 2025
8d64ec4
Merge branch 'main' into main
Jenya705 Dec 9, 2025
f2afb53
format fix
Jenya705 Dec 9, 2025
7477821
typo
Jenya705 Dec 9, 2025
f02d4c9
cast
Jenya705 Dec 10, 2025
0d74581
Owned QueryContiguousIter
Jenya705 Jan 21, 2026
2c6c251
ci fix
Jenya705 Jan 21, 2026
9599bfc
Minor fixes
Jenya705 Jan 21, 2026
6ade0c9
No exactsize
Jenya705 Jan 21, 2026
c9c293d
Contiguous[Ref/Mut]
Jenya705 Jan 22, 2026
495c355
fixes
Jenya705 Jan 22, 2026
7f55e03
debug impls
Jenya705 Jan 22, 2026
888a473
safety comments
Jenya705 Jan 22, 2026
1f765c4
example fix
Jenya705 Jan 22, 2026
bef611e
iterators
Jenya705 Jan 22, 2026
f8286d0
into_iter->iter_mut
Jenya705 Jan 22, 2026
9b14f5f
release note
Jenya705 Jan 22, 2026
250a359
reborrow
Jenya705 Jan 22, 2026
2acfca5
documentation
Jenya705 Jan 23, 2026
42733c5
big->large
Jenya705 Jan 23, 2026
4e1cc10
docs
Jenya705 Jan 23, 2026
6e0441f
docs
Jenya705 Jan 23, 2026
526680b
docs
Jenya705 Jan 23, 2026
31d6f8c
removing unsafe
Jenya705 Jan 23, 2026
ecc13e2
Deref(Mut)
Jenya705 Jan 23, 2026
99d116d
typo
Jenya705 Jan 23, 2026
22612d7
into_inner
Jenya705 Jan 23, 2026
371c52c
ContiguousRef::new
Jenya705 Jan 23, 2026
bddcf57
new ticks methods
Jenya705 Jan 23, 2026
6430029
docs fix
Jenya705 Jan 24, 2026
c4bd5ae
split reverse/bypassing change detection
Jenya705 Jan 24, 2026
6c0c771
warning doc on split
Jenya705 Jan 24, 2026
c8b1548
ContiguousComponentTicks(Mut|Ref) methods
Jenya705 Jan 24, 2026
3ee7dd3
Merge branch 'main' into main
Jenya705 Jan 27, 2026
e941398
Merge branch 'main' into main
Jenya705 Feb 2, 2026
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
47 changes: 47 additions & 0 deletions benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use bevy_ecs::prelude::*;
use glam::*;

#[derive(Component, Copy, Clone)]
struct Transform(Mat4);

#[derive(Component, Copy, Clone)]
struct Position(Vec3);

#[derive(Component, Copy, Clone)]
struct Rotation(Vec3);

#[derive(Component, Copy, Clone)]
struct Velocity(Vec3);

pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>);

impl<'w> Benchmark<'w> {
pub fn new() -> Self {
let mut world = World::new();

world.spawn_batch(core::iter::repeat_n(
(
Transform(Mat4::from_scale(Vec3::ONE)),
Position(Vec3::X),
Rotation(Vec3::X),
Velocity(Vec3::X),
),
10_000,
));

let query = world.query::<(&Velocity, &mut Position)>();
Self(world, query)
}

#[inline(never)]
pub fn run(&mut self) {
let mut iter = self.1.iter_mut(&mut self.0);
for (velocity, (position, mut ticks)) in iter.as_contiguous_iter().unwrap() {
for (v, p) in velocity.iter().zip(position.iter_mut()) {
p.0 += v.0;
}
// to match the iter_simple benchmark
ticks.mark_all_as_updated();
}
}
}
63 changes: 63 additions & 0 deletions benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use bevy_ecs::prelude::*;
use glam::*;

#[derive(Component, Copy, Clone)]
struct Transform(Mat4);

#[derive(Component, Copy, Clone)]
struct Position(Vec3);

#[derive(Component, Copy, Clone)]
struct Rotation(Vec3);

#[derive(Component, Copy, Clone)]
struct Velocity(Vec3);

pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>);

impl<'w> Benchmark<'w> {
pub fn supported() -> bool {
is_x86_feature_detected!("avx2")
}

pub fn new() -> Option<Self> {
if !Self::supported() {
return None;
}

let mut world = World::new();

world.spawn_batch(core::iter::repeat_n(
(
Transform(Mat4::from_scale(Vec3::ONE)),
Position(Vec3::X),
Rotation(Vec3::X),
Velocity(Vec3::X),
),
10_000,
));

let query = world.query::<(&Velocity, &mut Position)>();
Some(Self(world, query))
}

#[inline(never)]
pub fn run(&mut self) {
#[target_feature(enable = "avx2")]
fn exec(position: &mut [Position], velocity: &[Velocity]) {
for i in 0..position.len() {
position[i].0 += velocity[i].0;
}
}

let mut iter = self.1.iter_mut(&mut self.0);
for (velocity, (position, mut ticks)) in iter.as_contiguous_iter().unwrap() {
// SAFETY: checked in new
unsafe {
exec(position, velocity);
}
// to match the iter_simple benchmark
ticks.mark_all_as_updated();
}
}
}
42 changes: 42 additions & 0 deletions benches/benches/bevy_ecs/iteration/iter_simple_no_detection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use bevy_ecs::prelude::*;
use glam::*;

#[derive(Component, Copy, Clone)]
struct Transform(Mat4);

#[derive(Component, Copy, Clone)]
struct Position(Vec3);

#[derive(Component, Copy, Clone)]
struct Rotation(Vec3);

#[derive(Component, Copy, Clone)]
struct Velocity(Vec3);

pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>);

impl<'w> Benchmark<'w> {
pub fn new() -> Self {
let mut world = World::new();

world.spawn_batch(core::iter::repeat_n(
(
Transform(Mat4::from_scale(Vec3::ONE)),
Position(Vec3::X),
Rotation(Vec3::X),
Velocity(Vec3::X),
),
10_000,
));

let query = world.query::<(&Velocity, &mut Position)>();
Self(world, query)
}

#[inline(never)]
pub fn run(&mut self) {
for (velocity, mut position) in self.1.iter_mut(&mut self.0) {
position.bypass_change_detection().0 += velocity.0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use bevy_ecs::prelude::*;
use glam::*;

#[derive(Component, Copy, Clone)]
struct Transform(Mat4);

#[derive(Component, Copy, Clone)]
struct Position(Vec3);

#[derive(Component, Copy, Clone)]
struct Rotation(Vec3);

#[derive(Component, Copy, Clone)]
struct Velocity(Vec3);

pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>);

impl<'w> Benchmark<'w> {
pub fn new() -> Self {
let mut world = World::new();

world.spawn_batch(core::iter::repeat_n(
(
Transform(Mat4::from_scale(Vec3::ONE)),
Position(Vec3::X),
Rotation(Vec3::X),
Velocity(Vec3::X),
),
10_000,
));

let query = world.query::<(&Velocity, &mut Position)>();
Self(world, query)
}

#[inline(never)]
pub fn run(&mut self) {
let mut iter = self.1.iter_mut(&mut self.0);
for (velocity, (position, _ticks)) in iter.as_contiguous_iter().unwrap() {
for (v, p) in velocity.iter().zip(position.iter_mut()) {
p.0 += v.0;
}
}
}
}
26 changes: 26 additions & 0 deletions benches/benches/bevy_ecs/iteration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ mod iter_frag_sparse;
mod iter_frag_wide;
mod iter_frag_wide_sparse;
mod iter_simple;
mod iter_simple_contiguous;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod iter_simple_contiguous_avx2;
mod iter_simple_foreach;
mod iter_simple_foreach_hybrid;
mod iter_simple_foreach_sparse_set;
mod iter_simple_foreach_wide;
mod iter_simple_foreach_wide_sparse_set;
mod iter_simple_no_detection;
mod iter_simple_no_detection_contiguous;
mod iter_simple_sparse_set;
mod iter_simple_system;
mod iter_simple_wide;
Expand Down Expand Up @@ -40,6 +45,27 @@ fn iter_simple(c: &mut Criterion) {
let mut bench = iter_simple::Benchmark::new();
b.iter(move || bench.run());
});
group.bench_function("base_contiguous", |b| {
let mut bench = iter_simple_contiguous::Benchmark::new();
b.iter(move || bench.run());
});
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
if iter_simple_contiguous_avx2::Benchmark::supported() {
group.bench_function("base_contiguous_avx2", |b| {
let mut bench = iter_simple_contiguous_avx2::Benchmark::new().unwrap();
b.iter(move || bench.run());
});
}
}
group.bench_function("base_no_detection", |b| {
let mut bench = iter_simple_no_detection::Benchmark::new();
b.iter(move || bench.run());
});
group.bench_function("base_no_detection_contiguous", |b| {
let mut bench = iter_simple_no_detection_contiguous::Benchmark::new();
b.iter(move || bench.run());
});
group.bench_function("wide", |b| {
let mut bench = iter_simple_wide::Benchmark::new();
b.iter(move || bench.run());
Expand Down
79 changes: 78 additions & 1 deletion crates/bevy_ecs/src/change_detection/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use crate::{
ptr::PtrMut,
resource::Resource,
};
use bevy_ptr::{Ptr, UnsafeCellDeref};
use bevy_ptr::{Ptr, ThinSlicePtr, UnsafeCellDeref};
use core::{
cell::UnsafeCell,
ops::{Deref, DerefMut},
panic::Location,
};
Expand Down Expand Up @@ -463,6 +464,82 @@ impl<'w, T: ?Sized> Mut<'w, T> {
}
}

/// Used by [`Mut`] for [`crate::query::ContiguousQueryData`] to allow marking component's changes
pub struct ContiguousComponentTicks<'w, const MUTABLE: bool> {
added: ThinSlicePtr<'w, UnsafeCell<Tick>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be simpler to convert these to &[Tick] or &mut [Tick] immediately. The ThinSlicePtr optimization is important in storage because we store a lot of them, but during iteration there will usually only be one on the stack.

changed: ThinSlicePtr<'w, UnsafeCell<Tick>>,
changed_by: MaybeLocation<ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>>,
count: usize,
last_run: Tick,
this_run: Tick,
}

impl<'w> ContiguousComponentTicks<'w, true> {
/// Returns mutable changed ticks slice
pub fn get_changed_ticks_mut(&mut self) -> &mut [Tick] {
// SAFETY: `changed` slice is `self.count` long, aliasing rules are uphold by `new`.
unsafe { self.changed.as_mut_slice(self.count) }
}

/// Marks all components as updated
pub fn mark_all_as_updated(&mut self) {
let this_run = self.this_run;

for i in 0..self.count {
// SAFETY: `changed_by` slice is `self.count` long, aliasing rules are uphold by `new`
self.changed_by
.map(|v| unsafe { v.get_unchecked(i).deref_mut() })
.assign(MaybeLocation::caller());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to push the loop inside the self.changed_by.map so that it doesn't leave an empty loop when location tracking is disabled. Although the compiler will probably remove it anyway, so it might not matter.

Suggested change
for i in 0..self.count {
// SAFETY: `changed_by` slice is `self.count` long, aliasing rules are uphold by `new`
self.changed_by
.map(|v| unsafe { v.get_unchecked(i).deref_mut() })
.assign(MaybeLocation::caller());
}
self.changed_by.map(|v| {
for i in 0..self.count {
// SAFETY: `changed_by` slice is `self.count` long, aliasing rules are uphold by `new`
*unsafe { v.get_unchecked(i).deref_mut() } = Location::caller();
}
});


for t in self.get_changed_ticks_mut() {
*t = this_run;
}
}
}

impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> {
pub(crate) unsafe fn new(
added: ThinSlicePtr<'w, UnsafeCell<Tick>>,
changed: ThinSlicePtr<'w, UnsafeCell<Tick>>,
changed_by: MaybeLocation<ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>>,
count: usize,
last_run: Tick,
this_run: Tick,
) -> Self {
Self {
added,
changed,
count,
changed_by,
last_run,
this_run,
}
}

/// Returns immutable changed ticks slice
pub fn get_changed_ticks(&self) -> &[Tick] {
// SAFETY: `self.changed` is `self.count` long
unsafe { self.changed.cast::<Tick>().as_slice(self.count) }
}

/// Returns immutable added ticks slice
pub fn get_added_ticks(&self) -> &[Tick] {
// SAFETY: `self.added` is `self.count` long
unsafe { self.added.cast::<Tick>().as_slice(self.count) }
}

/// Returns the last tick system ran
pub fn last_run(&self) -> Tick {
self.last_run
}

/// Returns the current tick
pub fn this_run(&self) -> Tick {
self.this_run
}
}

impl<'w, T: ?Sized> From<Mut<'w, T>> for Ref<'w, T> {
fn from(mut_ref: Mut<'w, T>) -> Self {
Self {
Expand Down
Loading
Loading