There’s two fundamental issues here.
Firstly, the iterator is a &'vert mut …struct containing… &'src mut [f32]
and your item type promises &'src mut [f32]
items. That isn’t possible (unless you want to mutate Vectices
itself and give away the references in the p_x
, p_y
, p_z
fields), because a &'short_lived mut &'long_lived mut T
reference-to-reference can only give you a &'short_lived T
access to the inner data.
The other problem is that the iterator will, during iteration, produce multiple references to the inner f32
s which can co-exist during iteration (e.g. if all items are .collect
ed), and while you aren’t producing any aliasing mutable references to the same items in the slices, this is only prevented due to run-time logic on the indices, i.e. because self.cur
always increments and never overflows. The compiler cannot know this logic is correct, so the code cannot possibly compile like that.
Approaches to fix situations like this in Rust are always the two alternatives of
- use unsafe code, or
- use pre-existing safe abstractions.
The pre-existing safe abstractions are of course usually preferable because unsafe code can often easily have subtle soundness bugs in the interface. The pre-existing safe abstraction would be the mutable iterator on slices from the standard library. On the other hand, the downside of using existing iterators of course is that that would duplicate the iteration “index”. These iterators don’t actually work with true “indices” but rather directly increment pointers to the current element, but still, the iterator would become a bit larger, and the result is, probably (?), not the absolute optimum that could be achieved with unsafe code. Compared to your current approach there’s the (presumably significant) advantage that the index-checks from indexing operators can be avoided using standard library iterators.
So let’s try consistently using those iterators, and we’ll get something like
pub struct Vertices<'a> {
pub len: usize,
pub u: &'a [f32],
pub v: &'a [f32],
pub ng_x: &'a [f32],
pub ng_y: &'a [f32],
pub ng_z: &'a [f32],
pub p_x: &'a mut [f32],
pub p_y: &'a mut [f32],
pub p_z: &'a mut [f32],
}
impl Vertices<'_> {
pub fn iter_mut(&mut self) -> VerticesIterMut<'_> {
VerticesIterMut {
u_it: self.u.iter(),
v_it: self.v.iter(),
ng_x_it: self.ng_x.iter(),
ng_y_it: self.ng_y.iter(),
ng_z_it: self.ng_z.iter(),
p_x_it: self.p_x.iter_mut(),
p_y_it: self.p_y.iter_mut(),
p_z_it: self.p_z.iter_mut(),
}
}
}
// I liked the short lifetime names,
// but this `'a` is like `'vert` in your original code
// so the `Item` type below also uses `'vert`, not `'src`
pub struct VerticesIterMut<'a> {
u_it: std::slice::Iter<'a, f32>,
v_it: std::slice::Iter<'a, f32>,
ng_x_it: std::slice::Iter<'a, f32>,
ng_y_it: std::slice::Iter<'a, f32>,
ng_z_it: std::slice::Iter<'a, f32>,
p_x_it: std::slice::IterMut<'a, f32>,
p_y_it: std::slice::IterMut<'a, f32>,
p_z_it: std::slice::IterMut<'a, f32>,
}
impl<'a> Iterator for VerticesIterMut<'a> {
type Item = ([f32; 2], [f32; 3], [&'a mut f32; 3]);
fn next(&mut self) -> Option<Self::Item> {
Some((
[*self.u_it.next()?, *self.v_it.next()?],
[
*self.ng_x_it.next()?,
*self.ng_y_it.next()?,
*self.ng_z_it.next()?,
],
[
self.p_x_it.next()?,
self.p_y_it.next()?,
self.p_z_it.next()?,
],
))
}
}
Glancing back at the number of early-return opportunities from the .next
calls, there certainly are possibly good improvements to possibly be made here if performance really matters; maybe even some using safe code, and probably a lot more opportunity using unsafe
.
Edit: I’m noticing that I haven’t used the len
field in the code above. There’s an argument to be had – if efficient code really matters – that the Vertices
struct will contain way more copies of the “length” (one for each slice) than you might probably want to have, so there’s our first inefficiency already in the first place. The additional len
field you have beyond that does hover appear to be quite clearly overkill. I assume, the code would often assume that all slice lengths are simply the same; if no unsafe code is used, this assumption could be enforced by the API, and would result in panics or logic errors if there was a bug that could result in different-length slices; if unsafe code is used, eliminating all these copies by using raw pointers would be a possible optimization in the representation or Vertices
, too, though from what I remember from watching some videos online that shortly talked about SOA, there might be even more compact representations, if more assumptions are made how the different slices can be layed out relative to each other.