1use std::{
4 any::TypeId,
5 collections::{HashMap, HashSet},
6 marker::PhantomData,
7};
8
9use bevy_app::{App, Plugin};
10use bevy_ecs::{prelude::*, world::EntityWorldMut};
11use ranim_core::{
12 frame::{EvaluatedFrame, FrameItemId},
13 glam::DAffine3,
14 transformed::Transformed,
15};
16
17use crate::{schedule::RenderSchedule, world::SourceOrder};
18
19pub trait Extract {
27 fn extract(&self, ctx: &mut ExtractContext<'_>);
29}
30
31pub struct ExtractPlugin<T>(PhantomData<fn() -> T>);
38
39impl<T> Default for ExtractPlugin<T> {
40 fn default() -> Self {
41 Self(PhantomData)
42 }
43}
44
45impl<T: Extract + Send + Sync + 'static> Plugin for ExtractPlugin<T> {
46 fn build(&self, app: &mut App) {
47 app.init_resource::<ExtractedEntities<T>>()
48 .add_systems(RenderSchedule::Extract, extract_items::<T>);
49 }
50}
51
52type OutputId = (FrameItemId, usize);
53
54struct OutputEntity {
55 entity: Entity,
56 bundle_type: TypeId,
57}
58
59#[derive(Resource)]
60struct ExtractedEntities<T: Send + Sync + 'static> {
61 entities: HashMap<OutputId, OutputEntity>,
62 seen: HashSet<OutputId>,
63 inputs: HashSet<FrameItemId>,
64 marker: PhantomData<fn() -> T>,
65}
66
67impl<T: Send + Sync + 'static> Default for ExtractedEntities<T> {
68 fn default() -> Self {
69 Self {
70 entities: HashMap::new(),
71 seen: HashSet::new(),
72 inputs: HashSet::new(),
73 marker: PhantomData,
74 }
75 }
76}
77
78pub struct ExtractContext<'w> {
84 world: &'w mut World,
85 entities: &'w mut HashMap<OutputId, OutputEntity>,
86 seen: &'w mut HashSet<OutputId>,
87 source: FrameItemId,
88 order: usize,
89 next_part: &'w mut usize,
90 transform: DAffine3,
91}
92
93impl ExtractContext<'_> {
94 pub fn world_mut(&mut self) -> &mut World {
101 self.world
102 }
103
104 pub fn entity<B: Bundle>(&mut self) -> EntityWorldMut<'_> {
112 self.next_entity(TypeId::of::<B>())
113 }
114
115 pub fn transform(&self) -> DAffine3 {
117 self.transform
118 }
119
120 pub fn with_transform<R>(
122 &mut self,
123 transform: DAffine3,
124 extract: impl FnOnce(&mut ExtractContext<'_>) -> R,
125 ) -> R {
126 let mut child = ExtractContext {
127 world: self.world,
128 entities: self.entities,
129 seen: self.seen,
130 source: self.source,
131 order: self.order,
132 next_part: self.next_part,
133 transform: self.transform * transform,
134 };
135 extract(&mut child)
136 }
137
138 fn next_entity(&mut self, bundle_type: TypeId) -> EntityWorldMut<'_> {
139 let part = *self.next_part;
140 *self.next_part += 1;
141 let id = (self.source, part);
142 self.seen.insert(id);
143
144 let output = self.entities.entry(id).or_insert_with(|| OutputEntity {
145 entity: self.world.spawn_empty().id(),
146 bundle_type,
147 });
148 if output.bundle_type != bundle_type || !self.world.entities().contains(output.entity) {
149 self.world.despawn(output.entity);
150 output.entity = self.world.spawn_empty().id();
151 output.bundle_type = bundle_type;
152 }
153
154 let mut entity = self.world.entity_mut(output.entity);
155 let order = SourceOrder {
156 order: self.order,
157 part,
158 };
159 if entity.get::<SourceOrder>() != Some(&order) {
160 entity.insert(order);
161 }
162 entity
163 }
164}
165
166fn extract_items<T: Extract + Send + Sync + 'static>(world: &mut World) {
167 let _span = crate::cpu_probe::span("reconcile");
168 world.resource_scope(|world, mut state: Mut<ExtractedEntities<T>>| {
169 world.resource_scope(|world, frame: Mut<EvaluatedFrame>| {
170 let state = &mut *state;
171 state.seen.clear();
172 state.inputs.clear();
173 for item in frame.items::<T>() {
174 assert!(
175 state.inputs.insert(item.id),
176 "duplicate frame item id {:?} for `{}`",
177 item.id,
178 std::any::type_name::<T>()
179 );
180 let mut next_part = 0;
181 item.value.extract(&mut ExtractContext {
182 world,
183 entities: &mut state.entities,
184 seen: &mut state.seen,
185 source: item.id,
186 order: item.order,
187 next_part: &mut next_part,
188 transform: DAffine3::IDENTITY,
189 });
190 }
191 state.entities.retain(|id, output| {
192 if state.seen.contains(id) {
193 true
194 } else {
195 world.despawn(output.entity);
196 false
197 }
198 });
199 });
200 });
201}
202
203impl<T: Extract, G: Clone + Into<DAffine3>> Extract for Transformed<T, G> {
204 fn extract(&self, ctx: &mut ExtractContext<'_>) {
205 ctx.with_transform(self.transform.clone().into(), |ctx| self.inner.extract(ctx));
206 }
207}
208
209impl<T: Extract> Extract for Vec<T> {
210 fn extract(&self, ctx: &mut ExtractContext<'_>) {
211 for item in self {
212 item.extract(ctx);
213 }
214 }
215}
216
217impl<T: Extract> Extract for Option<T> {
218 fn extract(&self, ctx: &mut ExtractContext<'_>) {
219 if let Some(item) = self {
220 item.extract(ctx);
221 }
222 }
223}
224
225impl<T: Extract, const N: usize> Extract for [T; N] {
226 fn extract(&self, ctx: &mut ExtractContext<'_>) {
227 for item in self {
228 item.extract(ctx);
229 }
230 }
231}
232
233impl Extract for () {
234 fn extract(&self, _ctx: &mut ExtractContext<'_>) {}
235}
236
237macro_rules! impl_extract_for_tuple {
238 ($(($T:ident, $value:ident)),*) => {
239 impl<$($T: Extract),*> Extract for ($($T,)*) {
240 fn extract(&self, ctx: &mut ExtractContext<'_>) {
241 let ($($value,)*) = self;
242 $($value.extract(ctx);)*
243 }
244 }
245 };
246}
247
248variadics_please::all_tuples!(impl_extract_for_tuple, 1, 15, T, value);
249
250#[cfg(test)]
251mod tests {
252 use bevy_app::SubApp;
253 use bevy_ecs::{query::Changed, schedule::ScheduleLabel};
254 use ranim_core::glam::{DMat4, DVec3};
255
256 use super::*;
257
258 #[derive(Component, Default, Clone, Debug, PartialEq)]
259 struct TestItem {
260 value: f32,
261 transform: ranim_core::glam::Mat4,
262 }
263
264 impl Extract for TestItem {
265 fn extract(&self, ctx: &mut ExtractContext<'_>) {
266 let mut output = self.clone();
267 output.transform = DMat4::from(ctx.transform()).as_mat4() * output.transform;
268 let mut entity = ctx.entity::<Self>();
269 if entity.get::<Self>() != Some(&output) {
270 entity.insert(output);
271 }
272 }
273 }
274
275 #[derive(Component, Default, Clone)]
276 struct OtherItem {
277 _value: u8,
278 }
279
280 impl Extract for OtherItem {
281 fn extract(&self, ctx: &mut ExtractContext<'_>) {
282 ctx.entity::<Self>().insert(self.clone());
283 }
284 }
285
286 fn extraction_app<T: Extract + Send + Sync + 'static>() -> SubApp {
287 let mut app = SubApp::new();
288 app.update_schedule = Some(RenderSchedule::Extract.intern());
289 app.init_resource::<EvaluatedFrame>()
290 .add_plugins(ExtractPlugin::<T>::default());
291 app
292 }
293
294 fn entity<T: Send + Sync + 'static>(app: &SubApp, source: FrameItemId, part: usize) -> Entity {
295 app.world().resource::<ExtractedEntities<T>>().entities[&(source, part)].entity
296 }
297
298 #[test]
299 fn raw_compositions_extract_in_order_and_empty_vectors_remove_outputs() {
300 type Input = Vec<(TestItem, Option<OtherItem>)>;
301 let mut app = extraction_app::<Input>();
302 app.world_mut().resource_mut::<EvaluatedFrame>().push(
303 (7, 0),
304 vec![
305 (TestItem::default(), Some(OtherItem::default())),
306 (TestItem::default(), None),
307 ],
308 );
309 app.update();
310 for part in 0..3 {
311 assert_eq!(
312 app.world()
313 .get::<SourceOrder>(entity::<Input>(&app, (7, 0), part)),
314 Some(&SourceOrder { order: 0, part }),
315 );
316 }
317 let previous = entity::<Input>(&app, (7, 0), 0);
318 app.world_mut()
319 .resource_mut::<EvaluatedFrame>()
320 .update([((7, 0), Input::new())]);
321 app.update();
322 assert!(!app.world().entities().contains(previous));
323 assert!(
324 app.world()
325 .resource::<ExtractedEntities<Input>>()
326 .entities
327 .is_empty()
328 );
329 }
330
331 #[test]
332 fn unchanged_component_outputs_keep_entities_and_changed_tracking() {
333 #[derive(Resource, Default)]
334 struct Changes(Vec<usize>);
335
336 let mut app = extraction_app::<Vec<TestItem>>();
337 app.init_resource::<Changes>().add_systems(
338 RenderSchedule::Extract,
339 (|items: Query<Entity, Changed<TestItem>>, mut changes: ResMut<Changes>| {
340 changes.0.push(items.iter().count());
341 })
342 .after(extract_items::<Vec<TestItem>>),
343 );
344 app.world_mut()
345 .resource_mut::<EvaluatedFrame>()
346 .push((1, 0), vec![TestItem::default()]);
347 app.update();
348 let original = entity::<Vec<TestItem>>(&app, (1, 0), 0);
349 app.update();
350 assert_eq!(entity::<Vec<TestItem>>(&app, (1, 0), 0), original);
351 assert_eq!(app.world().resource::<Changes>().0, [1, 0]);
352
353 let changed = TestItem {
354 value: 2.0,
355 ..Default::default()
356 };
357 app.world_mut()
358 .resource_mut::<EvaluatedFrame>()
359 .update([((1, 0), vec![changed])]);
360 app.update();
361 assert_eq!(app.world().resource::<Changes>().0, [1, 0, 1]);
362 }
363
364 #[test]
365 fn absent_optional_output_cannot_leave_a_previous_bundle_shape() {
366 type Input = (Option<TestItem>, OtherItem);
367 let mut app = extraction_app::<Input>();
368 app.world_mut()
369 .resource_mut::<EvaluatedFrame>()
370 .push((1, 0), (Some(TestItem::default()), OtherItem::default()));
371 app.update();
372 let first = entity::<Input>(&app, (1, 0), 0);
373 let second = entity::<Input>(&app, (1, 0), 1);
374
375 app.world_mut()
376 .resource_mut::<EvaluatedFrame>()
377 .update([((1, 0), (None::<TestItem>, OtherItem::default()))]);
378 app.update();
379 let current = entity::<Input>(&app, (1, 0), 0);
380 assert!(!app.world().entities().contains(first));
381 assert!(!app.world().entities().contains(second));
382 assert!(app.world().get::<OtherItem>(current).is_some());
383 assert!(app.world().get::<TestItem>(current).is_none());
384 }
385
386 #[test]
387 fn nested_transforms_compose_during_extraction_without_changing_raw_input() {
388 type Input = Transformed<Transformed<TestItem, DAffine3>, DAffine3>;
389 let outer = DAffine3::from_translation(DVec3::X * 3.0);
390 let inner = DAffine3::from_scale(DVec3::splat(2.0));
391 let input = Transformed::new(Transformed::new(TestItem::default(), inner), outer);
392 let mut app = extraction_app::<Input>();
393 app.world_mut()
394 .resource_mut::<EvaluatedFrame>()
395 .push((1, 0), input.clone());
396 app.update();
397 app.update();
398
399 let output = app
400 .world()
401 .get::<TestItem>(entity::<Input>(&app, (1, 0), 0))
402 .unwrap();
403 assert_eq!(output.transform, DMat4::from(outer * inner).as_mat4());
404 assert_eq!(
405 app.world().resource::<EvaluatedFrame>().items::<Input>()[0].value,
406 input
407 );
408 }
409
410 #[test]
411 fn a_missing_whole_batch_cleans_only_its_own_entities() {
412 let mut app = extraction_app::<TestItem>();
413 app.add_plugins(ExtractPlugin::<OtherItem>::default());
414 app.world_mut()
415 .resource_mut::<EvaluatedFrame>()
416 .push((1, 0), TestItem::default());
417 app.world_mut()
418 .resource_mut::<EvaluatedFrame>()
419 .push((2, 0), OtherItem::default());
420 app.update();
421 let removed = entity::<TestItem>(&app, (1, 0), 0);
422 let preserved = entity::<OtherItem>(&app, (2, 0), 0);
423
424 let mut frame = EvaluatedFrame::new();
425 frame.push((2, 0), OtherItem::default());
426 app.insert_resource(frame);
427 app.update();
428 assert!(!app.world().entities().contains(removed));
429 assert_eq!(entity::<OtherItem>(&app, (2, 0), 0), preserved);
430 }
431
432 #[test]
433 #[should_panic(expected = "duplicate frame item id (1, 0)")]
434 fn duplicate_input_ids_are_rejected() {
435 let mut app = extraction_app::<TestItem>();
436 app.world_mut()
437 .resource_mut::<EvaluatedFrame>()
438 .update([((1, 0), TestItem::default()), ((1, 0), TestItem::default())]);
439 app.update();
440 }
441}