Skip to main content

loam_runtime/
session.rs

1use loam_shape::polytope::Polytope4Topology;
2
3use crate::bridge::{
4    Bridge, BridgeError, BridgeSpec, Drag, DragError, DragRelease, VELOCITY_SAMPLES,
5};
6use crate::command::{
7    Command, CommandResult, Commands, Dispatch, Outcome, Rejection, Request, RequestId,
8};
9use crate::domain::{
10    presented_alpha, ChartCommand, ChartPoint, DomainBuilder, DomainError, DomainHandle, DomainId,
11    DomainSnapshot, DomainSpace, Domains,
12};
13use crate::entity::{Entities, EntitiesSnapshot, Epoch, RuntimeId, SceneId};
14use crate::input::Input;
15use crate::phase::{Ctx, Order, Phase, PhaseError, Phases, Step, System, SystemEntry, Tick};
16use crate::relation::{LinkId, Relation, RelationSnapshot};
17use crate::store::{Owner, SchemaId, StoreField};
18use crate::stores::Stores;
19use crate::view::{ImageRay, Pick, Rigid, Vec3, ViewRecords, ViewTarget, Views, ViewsSnapshot};
20
21const RELEASE_STALE_SECONDS: f64 = 0.12;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct SimConfig {
25    pub fixed_hz: u32,
26    pub max_ticks_per_frame: u32,
27}
28
29impl SimConfig {
30    pub fn new(fixed_hz: u32, max_ticks_per_frame: u32) -> Result<Self, SimConfigError> {
31        let config = Self {
32            fixed_hz,
33            max_ticks_per_frame,
34        };
35        config.check()?;
36        Ok(config)
37    }
38
39    pub fn check(&self) -> Result<(), SimConfigError> {
40        if self.max_ticks_per_frame == 0 {
41            return Err(SimConfigError::NoCatchUp);
42        }
43        Ok(())
44    }
45
46    /// `None` when `fixed_hz` is zero, which stops the simulation.
47    pub fn dt(&self) -> Option<f32> {
48        (self.fixed_hz > 0).then(|| 1.0 / self.fixed_hz as f32)
49    }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum SimConfigError {
54    NoCatchUp,
55}
56
57impl std::fmt::Display for SimConfigError {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Self::NoCatchUp => f.write_str("max_ticks_per_frame must be at least one"),
61        }
62    }
63}
64
65impl std::error::Error for SimConfigError {}
66
67impl Default for SimConfig {
68    fn default() -> Self {
69        Self {
70            fixed_hz: 60,
71            max_ticks_per_frame: 4,
72        }
73    }
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
77pub struct Stamp {
78    pub tick: Tick,
79    pub sequence: u64,
80}
81
82/// What one boundary applied; the session grows its tables here and nowhere else.
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub struct Growth {
85    pub commands: usize,
86    pub spawned: usize,
87    pub despawned: usize,
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
91pub struct PreparedId(u32);
92
93impl PreparedId {
94    pub fn index(self) -> usize {
95        self.0 as usize
96    }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100pub struct MaterialId(u32);
101
102impl MaterialId {
103    pub fn index(self) -> usize {
104        self.0 as usize
105    }
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
109pub struct PaletteId(u32);
110
111impl PaletteId {
112    pub fn index(self) -> usize {
113        self.0 as usize
114    }
115}
116
117#[derive(Clone, Debug, PartialEq)]
118pub enum PreparedGeometry {
119    Lines4 {
120        segments: Vec<[[f32; 4]; 2]>,
121    },
122    Lines3 {
123        segments: Vec<[[f32; 3]; 2]>,
124    },
125    /// Edges for the wireframe and cells for the section, at canonical coordinates times `scale`.
126    Polytope4 {
127        polytope: loam_shape::polytope::Polytope4,
128        scale: f32,
129    },
130}
131
132impl PreparedGeometry {
133    pub fn edges_of(topology: &Polytope4Topology, scale: f32) -> Self {
134        let segments = topology
135            .edges
136            .iter()
137            .map(|&[i, j]| {
138                [
139                    (topology.vertices[i as usize] * scale).to_array(),
140                    (topology.vertices[j as usize] * scale).to_array(),
141                ]
142            })
143            .collect();
144        Self::Lines4 { segments }
145    }
146
147    /// Chart distance from the origin to the farthest vertex.
148    pub fn bounding_radius(&self) -> f32 {
149        fn farthest<'a>(points: impl Iterator<Item = &'a [f32]>) -> f32 {
150            points
151                .map(|point| point.iter().map(|c| c * c).sum::<f32>().sqrt())
152                .fold(0.0, f32::max)
153        }
154        match self {
155            Self::Lines4 { segments } => {
156                farthest(segments.iter().flatten().map(|point| point.as_slice()))
157            }
158            Self::Lines3 { segments } => {
159                farthest(segments.iter().flatten().map(|point| point.as_slice()))
160            }
161            Self::Polytope4 { polytope, scale } => {
162                polytope
163                    .topology()
164                    .vertices
165                    .iter()
166                    .map(|vertex| vertex.length())
167                    .fold(0.0, f32::max)
168                    * scale
169            }
170        }
171    }
172}
173
174#[derive(Clone, Copy, Debug, PartialEq)]
175pub enum Material {
176    Lines { color: [f32; 4], width_px: f32 },
177    Flat { color: [f32; 4] },
178}
179
180impl Material {
181    pub fn lines(color: [f32; 4], width_px: f32) -> Self {
182        Self::Lines { color, width_px }
183    }
184
185    pub fn flat(color: [f32; 4]) -> Self {
186        Self::Flat { color }
187    }
188}
189
190#[derive(Clone, Copy)]
191pub struct Library<'a> {
192    pub(crate) geometry: &'a [PreparedGeometry],
193    pub(crate) materials: &'a [Material],
194    pub(crate) palettes: &'a [Vec<[f32; 4]>],
195}
196
197impl Library<'_> {
198    pub(crate) fn line_style(&self, material: MaterialId) -> ([f32; 4], f32) {
199        match self.materials.get(material.index()) {
200            Some(Material::Lines { color, width_px }) => (*color, *width_px),
201            Some(Material::Flat { color }) => (*color, 1.0),
202            None => ([1.0; 4], 1.0),
203        }
204    }
205
206    pub(crate) fn palette(&self, palette: PaletteId) -> &[[f32; 4]] {
207        self.palettes
208            .get(palette.index())
209            .map_or(&[][..], Vec::as_slice)
210    }
211}
212
213#[derive(Default)]
214pub(crate) struct Assets {
215    prepared: Vec<PreparedGeometry>,
216    materials: Vec<Material>,
217    palettes: Vec<Vec<[f32; 4]>>,
218}
219
220impl Assets {
221    pub(crate) fn prepare(&mut self, geometry: PreparedGeometry) -> PreparedId {
222        self.prepared.push(geometry);
223        PreparedId((self.prepared.len() - 1) as u32)
224    }
225
226    pub(crate) fn prepared(&self, id: PreparedId) -> Option<&PreparedGeometry> {
227        self.prepared.get(id.index())
228    }
229
230    pub(crate) fn add_palette(&mut self, colors: Vec<[f32; 4]>) -> PaletteId {
231        self.palettes.push(colors);
232        PaletteId((self.palettes.len() - 1) as u32)
233    }
234
235    pub(crate) fn palette(&self, id: PaletteId) -> Option<&[[f32; 4]]> {
236        self.palettes.get(id.index()).map(Vec::as_slice)
237    }
238
239    pub(crate) fn add_material(&mut self, material: Material) -> MaterialId {
240        self.materials.push(material);
241        MaterialId((self.materials.len() - 1) as u32)
242    }
243
244    pub(crate) fn material(&self, id: MaterialId) -> Option<&Material> {
245        self.materials.get(id.index())
246    }
247
248    pub(crate) fn geometry(&self) -> &[PreparedGeometry] {
249        &self.prepared
250    }
251
252    pub(crate) fn library(&self) -> Library<'_> {
253        Library {
254            geometry: &self.prepared,
255            materials: &self.materials,
256            palettes: &self.palettes,
257        }
258    }
259}
260
261pub struct PublishedView {
262    pub domain: DomainId,
263    pub target: ViewTarget,
264    pub placement: Rigid,
265    pub records: ViewRecords,
266}
267
268#[derive(Default)]
269pub struct Publication {
270    pub views: Vec<PublishedView>,
271    pub stamp: Stamp,
272    /// The clamped alpha the records were drawn at.
273    pub alpha: f32,
274    source: Option<SceneId>,
275    content: u64,
276}
277
278impl Publication {
279    /// Changes only when a publish alters what a frame draws; a restamp alone leaves it as it was.
280    pub fn content(&self) -> u64 {
281        self.content
282    }
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub enum PublishError {
287    Borrowed,
288    Phase(PhaseError),
289}
290
291impl From<PhaseError> for PublishError {
292    fn from(error: PhaseError) -> Self {
293        Self::Phase(error)
294    }
295}
296
297pub struct Records {
298    idle: Option<Publication>,
299}
300
301impl Default for Records {
302    fn default() -> Self {
303        Self {
304            idle: Some(Publication::default()),
305        }
306    }
307}
308
309impl Records {
310    pub fn publish<A: Stores>(&mut self, session: &mut Session<A>) -> Result<Stamp, PublishError> {
311        self.publish_at(session, 1.0)
312    }
313
314    pub fn publish_at<A: Stores>(
315        &mut self,
316        session: &mut Session<A>,
317        alpha: f32,
318    ) -> Result<Stamp, PublishError> {
319        let buffer = self.idle.as_mut().ok_or(PublishError::Borrowed)?;
320        session.publish_at(buffer, alpha)?;
321        Ok(buffer.stamp)
322    }
323
324    /// Until the buffer is released, [`Self::publish`] returns [`PublishError::Borrowed`].
325    pub fn lend(&mut self) -> Option<Publication> {
326        self.idle.take()
327    }
328
329    pub fn release(&mut self, publication: Publication) {
330        self.idle = Some(publication);
331    }
332}
333
334pub struct SessionSnapshot<A: Stores> {
335    runtime: RuntimeId,
336    app: A::Snapshot,
337    entities: EntitiesSnapshot,
338    domains: Vec<DomainSnapshot>,
339    views: ViewsSnapshot,
340    bridges: RelationSnapshot<Bridge>,
341    tick: Tick,
342    config: SimConfig,
343    next_request: RequestId,
344}
345
346#[derive(Clone, Copy, Debug, PartialEq, Eq)]
347pub enum RestoreError {
348    Pending,
349    Unfinished(Phase),
350    NoInitial,
351    ForeignRuntime,
352    Schema(SchemaId),
353    Domain(DomainId),
354    /// A domain's world refused its snapshot before anything was touched.
355    #[cfg(feature = "physics")]
356    Edit(loam_physics::EditError),
357}
358
359impl std::fmt::Display for RestoreError {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        match self {
362            Self::Pending => f.write_str("deferred mutation is still pending"),
363            Self::Unfinished(phase) => write!(f, "the {phase:?} phase is unfinished"),
364            Self::NoInitial => f.write_str("no initial snapshot was captured"),
365            Self::ForeignRuntime => f.write_str("the snapshot belongs to another runtime"),
366            Self::Schema(schema) => write!(f, "no stored snapshot for {}", schema.name()),
367            Self::Domain(domain) => write!(f, "{domain} refused the snapshot"),
368            #[cfg(feature = "physics")]
369            Self::Edit(error) => std::fmt::Display::fmt(error, f),
370        }
371    }
372}
373
374#[derive(Default)]
375pub(crate) struct Manipulation {
376    drag: Option<Drag>,
377}
378
379impl Manipulation {
380    pub(crate) fn dragging(&self) -> Option<Drag> {
381        self.drag
382    }
383
384    pub(crate) fn grab<A: Stores>(
385        &mut self,
386        domains: &Domains,
387        views: &Views,
388        prepared: &[PreparedGeometry],
389        commands: &mut Commands<A>,
390        ndc: [f32; 2],
391        time: f64,
392    ) -> Result<Pick, DragError> {
393        let pick = domains
394            .pick_lifted(views, prepared, ndc)
395            .ok_or(DragError::NoPick)?;
396        let domain = domains
397            .get(pick.domain)
398            .ok_or(DomainError::UnknownDomain(pick.domain))?;
399        let into = views
400            .to_root(pick.image)
401            .and_then(|to| to.rigid())
402            .ok_or(DomainError::Unsupported("image space"))?
403            .inverse();
404        let center = domain
405            .image_of(pick.view, pick.entity)
406            .ok_or(DomainError::Stale(pick.entity))?;
407        let eye = views
408            .get(views.root())
409            .ok_or(DomainError::Unsupported("image space"))?
410            .eye;
411        let plane = into.apply(pick.image_point);
412        let normal = into.direction(eye.forward);
413        let depth =
414            (Vec3::from(plane) - Vec3::from(into.apply(eye.position))).dot(Vec3::from(normal));
415        let hit = pick.hit.ok_or(DomainError::Unsupported("view ray lift"))?;
416        if let Some(held) = self.drag.take() {
417            commands.submit(Command::Chart(
418                held.domain,
419                ChartCommand::Release {
420                    entity: held.entity,
421                },
422            ));
423        }
424        self.drag = Some(Drag {
425            entity: pick.entity,
426            domain: pick.domain,
427            view: pick.view,
428            image: pick.image,
429            plane,
430            normal,
431            depth,
432            center,
433            at: plane,
434            samples: [(plane, time); VELOCITY_SAMPLES],
435            sampled: 1,
436            opened: time,
437        });
438        commands.submit(Command::Chart(
439            pick.domain,
440            ChartCommand::Grab {
441                entity: pick.entity,
442                point: hit,
443            },
444        ));
445        Ok(pick)
446    }
447
448    pub(crate) fn drag<A: Stores>(
449        &mut self,
450        domains: &Domains,
451        views: &Views,
452        commands: &mut Commands<A>,
453        ndc: [f32; 2],
454        time: f64,
455    ) -> Result<ChartPoint, DragError> {
456        let mut drag = self.drag.ok_or(DragError::NotGrabbed)?;
457        let domain = domains
458            .get(drag.domain)
459            .ok_or(DomainError::UnknownDomain(drag.domain))?;
460        let name = domain
461            .view(drag.view)
462            .ok_or(DomainError::UnknownView(drag.view))?
463            .name;
464        let ray = views
465            .ray(drag.image, ndc)
466            .ok_or(DomainError::Unsupported("image space"))?;
467        let eye = views
468            .get(views.root())
469            .ok_or(DomainError::Unsupported("image space"))?
470            .eye;
471        let into = views
472            .to_root(drag.image)
473            .and_then(|to| to.rigid())
474            .ok_or(DomainError::Unsupported("image space"))?
475            .inverse();
476        let forward = into.direction(eye.forward);
477        let plane = drag.plane_at(into.apply(eye.position), forward);
478        let at = Drag::meet(&ray, plane, forward).ok_or(DragError::Ambiguous(name))?;
479        let point = domain.lift_origin(
480            drag.view,
481            &ImageRay {
482                origin: drag.moved(at),
483                direction: drag.normal,
484            },
485        )?;
486        drag.sample(at, time);
487        self.drag = Some(drag);
488        commands.submit(Command::Chart(
489            drag.domain,
490            ChartCommand::Move {
491                entity: drag.entity,
492                point,
493            },
494        ));
495        Ok(point)
496    }
497
498    fn finish<A: Stores>(
499        commands: &mut Commands<A>,
500        drag: Drag,
501        velocity: [f32; 3],
502    ) -> DragRelease {
503        commands.submit(Command::Chart(
504            drag.domain,
505            ChartCommand::Release {
506                entity: drag.entity,
507            },
508        ));
509        drag.released(velocity)
510    }
511
512    pub(crate) fn release<A: Stores>(&mut self, commands: &mut Commands<A>) -> Option<DragRelease> {
513        let drag = self.drag.take()?;
514        Some(Self::finish(commands, drag, drag.velocity()))
515    }
516
517    pub(crate) fn release_at<A: Stores>(
518        &mut self,
519        commands: &mut Commands<A>,
520        time: f64,
521    ) -> Option<DragRelease> {
522        let drag = self.drag.take()?;
523        let age = time - drag.newest_time();
524        let velocity = if (0.0..=RELEASE_STALE_SECONDS).contains(&age) {
525            drag.velocity()
526        } else {
527            [0.0; 3]
528        };
529        Some(Self::finish(commands, drag, velocity))
530    }
531
532    pub(crate) fn cancel<A: Stores>(&mut self, commands: &mut Commands<A>) -> Option<DragRelease> {
533        let drag = self.drag.take()?;
534        Some(Self::finish(commands, drag, [0.0; 3]))
535    }
536
537    pub(crate) fn clear(&mut self) {
538        self.drag = None;
539    }
540}
541
542/// The simulation entry `Session::new` registers; `system_at` places app entries around it.
543pub const DOMAIN_STEP: &str = "domain step";
544
545/// A CPU value with no GPU or window object; `Send`, and it builds for wasm32.
546pub struct Session<A: Stores> {
547    pub app: A,
548    domains: Domains,
549    views: Views,
550    bridges: Relation<Bridge>,
551    manipulation: Manipulation,
552    phases: Phases<A>,
553    commands: Commands<A>,
554    batch: Vec<Request<A>>,
555    results: Vec<CommandResult>,
556    input: Input,
557    assets: Assets,
558    config: SimConfig,
559    tick: Tick,
560    sequence: u64,
561    initial: Option<SessionSnapshot<A>>,
562    restored: bool,
563    unfinished: Option<Phase>,
564    phase_error: Option<PhaseError>,
565}
566
567impl<A: Stores> Session<A> {
568    pub fn new(mut app: A, config: SimConfig) -> Self {
569        let scene = SceneId {
570            runtime: RuntimeId::allocate(),
571            epoch: Epoch::default(),
572        };
573        app.bind(scene, Owner::new());
574        let mut phases = Phases::new();
575        phases.push(
576            Phase::Simulation,
577            SystemEntry::new(DOMAIN_STEP, |ctx: Ctx<'_, A>| {
578                for domain in ctx.domains.iter_mut() {
579                    domain.step(ctx.step)?;
580                }
581                Ok(())
582            }),
583        );
584        let mut bridges = Relation::new();
585        StoreField::bind(&mut bridges, scene, Owner::new());
586        Self {
587            app,
588            domains: Domains::new(scene.runtime),
589            views: Views::new(),
590            bridges,
591            manipulation: Manipulation::default(),
592            phases,
593            commands: Commands::new(scene),
594            batch: Vec::new(),
595            results: Vec::new(),
596            input: Input::default(),
597            assets: Assets::default(),
598            config,
599            tick: Tick::default(),
600            sequence: 0,
601            initial: None,
602            restored: false,
603            unfinished: None,
604            phase_error: None,
605        }
606    }
607
608    pub fn config(&self) -> SimConfig {
609        self.config
610    }
611
612    pub fn scene(&self) -> SceneId {
613        self.commands.entities().scene()
614    }
615
616    pub fn current_tick(&self) -> Tick {
617        self.tick
618    }
619
620    pub fn faulted_phase(&self) -> Option<Phase> {
621        self.unfinished
622    }
623
624    pub fn phase_error(&self) -> Option<PhaseError> {
625        self.phase_error
626    }
627
628    fn unfinished_error(&self) -> Option<PhaseError> {
629        self.unfinished.map(|phase| {
630            self.phase_error.unwrap_or_else(|| {
631                PhaseError::unnamed(
632                    phase,
633                    DomainError::Unsupported("unfinished phase requires restore"),
634                )
635            })
636        })
637    }
638
639    pub fn register_domain<S: DomainSpace>(
640        &mut self,
641        builder: DomainBuilder<S>,
642    ) -> DomainHandle<S> {
643        let domain = builder.build(self.domains.next_id(), self.scene());
644        let handle = domain.handle();
645        self.domains.push(Box::new(domain));
646        handle
647    }
648
649    pub fn domains(&self) -> &Domains {
650        &self.domains
651    }
652
653    pub fn domains_mut(&mut self) -> &mut Domains {
654        &mut self.domains
655    }
656
657    pub fn views(&self) -> &Views {
658        &self.views
659    }
660
661    pub fn views_mut(&mut self) -> &mut Views {
662        &mut self.views
663    }
664
665    pub fn entities(&self) -> &Entities {
666        self.commands.entities()
667    }
668
669    pub fn submit(&mut self, command: Command<A>) -> RequestId {
670        self.commands.submit(command)
671    }
672
673    pub fn prepare(&mut self, geometry: PreparedGeometry) -> PreparedId {
674        self.assets.prepare(geometry)
675    }
676
677    pub fn prepared(&self, id: PreparedId) -> Option<&PreparedGeometry> {
678        self.assets.prepared(id)
679    }
680
681    /// Two colors per prepared segment, start then end, read in the prepared geometry's own order; a segment past the palette's end keeps the material color.
682    pub fn add_palette(&mut self, colors: Vec<[f32; 4]>) -> PaletteId {
683        self.assets.add_palette(colors)
684    }
685
686    pub fn palette(&self, id: PaletteId) -> Option<&[[f32; 4]]> {
687        self.assets.palette(id)
688    }
689
690    pub fn add_material(&mut self, material: Material) -> MaterialId {
691        self.assets.add_material(material)
692    }
693
694    pub fn material(&self, id: MaterialId) -> Option<&Material> {
695        self.assets.material(id)
696    }
697
698    pub fn system(&mut self, phase: Phase, name: &'static str, system: impl System<A>) {
699        self.phases.push(phase, SystemEntry::new(name, system));
700    }
701
702    /// `None` when no entry of that phase has the named anchor.
703    pub fn system_at(
704        &mut self,
705        phase: Phase,
706        order: Order,
707        name: &'static str,
708        system: impl System<A>,
709    ) -> Option<()> {
710        self.phases
711            .insert(phase, order, SystemEntry::new(name, system))
712    }
713
714    pub fn entries(&self, phase: Phase) -> &[SystemEntry<A>] {
715        self.phases.entries(phase)
716    }
717
718    pub fn dispatch<R>(&mut self, f: impl FnOnce(&mut Dispatch<'_, A>) -> R) -> R {
719        let result = {
720            let mut dispatch = Dispatch::new(
721                &mut self.app,
722                &mut self.domains,
723                &mut self.views,
724                self.commands.entities_mut(),
725                &mut self.bridges,
726                &mut self.assets,
727            );
728            f(&mut dispatch)
729        };
730        self.domains.synchronize();
731        result
732    }
733
734    pub fn bridges(&self) -> &Relation<Bridge> {
735        &self.bridges
736    }
737
738    pub fn bridge(&mut self, spec: BridgeSpec) -> Result<LinkId, BridgeError> {
739        if self.entities().resolve(spec.anchor).is_none() {
740            return Err(BridgeError::Domain(DomainError::Stale(spec.anchor)));
741        }
742        let summary = {
743            let source = self
744                .domains
745                .get(spec.source)
746                .ok_or(BridgeError::UnknownDomain(spec.source))?;
747            source
748                .view(spec.view)
749                .ok_or(BridgeError::UnknownView(spec.view))?
750        };
751        if spec.placement.rigid().is_none() {
752            return Err(BridgeError::Nonlinear(summary.name));
753        }
754        if self.entities().resolve(summary.eye).is_none() {
755            return Err(BridgeError::Domain(DomainError::Stale(summary.eye)));
756        }
757        let image = self
758            .views
759            .place(spec.into, spec.placement)
760            .ok_or(BridgeError::UnknownImage(spec.into))?;
761        let link = self.bridges.link(
762            self.commands.entities(),
763            spec.anchor,
764            summary.eye,
765            Bridge {
766                source: spec.source,
767                view: spec.view,
768                image,
769            },
770        );
771        let link = match link {
772            Ok(link) => link,
773            Err(error) => {
774                self.views.unplace(image);
775                return Err(BridgeError::Link(error));
776            }
777        };
778        let source = self
779            .domains
780            .facade(spec.source)
781            .ok_or(BridgeError::UnknownDomain(spec.source))?;
782        source.retarget(spec.view, image)?;
783        Ok(link)
784    }
785
786    pub fn dragging(&self) -> Option<Drag> {
787        self.manipulation.dragging()
788    }
789
790    pub fn grab(&mut self, ndc: [f32; 2], time: f64) -> Result<Pick, DragError> {
791        self.manipulation.grab(
792            &self.domains,
793            &self.views,
794            self.assets.geometry(),
795            &mut self.commands,
796            ndc,
797            time,
798        )
799    }
800
801    pub fn drag(&mut self, ndc: [f32; 2], time: f64) -> Result<ChartPoint, DragError> {
802        self.manipulation
803            .drag(&self.domains, &self.views, &mut self.commands, ndc, time)
804    }
805
806    pub fn release(&mut self) -> Option<DragRelease> {
807        self.manipulation.release(&mut self.commands)
808    }
809
810    pub fn release_at(&mut self, time: f64) -> Option<DragRelease> {
811        self.manipulation.release_at(&mut self.commands, time)
812    }
813
814    pub fn cancel_drag(&mut self) -> Option<DragRelease> {
815        self.manipulation.cancel(&mut self.commands)
816    }
817
818    pub fn boundary(&mut self, input: Input) -> Result<Growth, PhaseError> {
819        self.input = input;
820        if let Some(error) = self.unfinished_error() {
821            return Err(error);
822        }
823        self.unfinished = Some(Phase::Dispatch);
824        self.results.clear();
825        let mut growth = Growth::default();
826        self.commit(&mut growth);
827        let step = Step {
828            tick: self.tick,
829            dt: self.config.dt().unwrap_or(0.0),
830        };
831        let mut dispatched = Ok(());
832        for index in 0..self.phases.entries(Phase::Dispatch).len() {
833            if let Err(error) = self.run_entry(Phase::Dispatch, index, step) {
834                dispatched = Err(error);
835                break;
836            }
837            self.commit(&mut growth);
838        }
839        self.restored = false;
840        dispatched?;
841        self.app.boundary(Owner::new());
842        for domain in self.domains.iter_mut() {
843            domain.boundary();
844        }
845        self.unfinished = None;
846        if self.initial.is_none() {
847            match self.snapshot() {
848                Ok(initial) => self.initial = Some(initial),
849                Err(cause) => {
850                    let error =
851                        PhaseError::system(Phase::Dispatch, "initial", DomainError::Restore(cause));
852                    self.unfinished = Some(Phase::Dispatch);
853                    self.phase_error = Some(error);
854                    return Err(error);
855                }
856            }
857        }
858        Ok(growth)
859    }
860
861    pub fn tick(&mut self) -> Result<(), PhaseError> {
862        if let Some(error) = self.unfinished_error() {
863            return Err(error);
864        }
865        let Some(dt) = self.config.dt() else {
866            return Ok(());
867        };
868        self.unfinished = Some(Phase::Simulation);
869        let step = Step {
870            tick: self.tick,
871            dt,
872        };
873        for domain in self.domains.iter_mut() {
874            domain.begin_tick();
875        }
876        let mut stepped = Ok(());
877        for index in 0..self.phases.entries(Phase::Simulation).len() {
878            stepped = self.run_entry(Phase::Simulation, index, step);
879            if stepped.is_err() {
880                break;
881            }
882        }
883        for domain in self.domains.iter_mut() {
884            domain.end_tick();
885        }
886        stepped?;
887        self.tick = Tick(self.tick.0 + 1);
888        self.unfinished = None;
889        Ok(())
890    }
891
892    pub fn publish(&mut self, into: &mut Publication) -> Result<(), PhaseError> {
893        self.publish_at(into, 1.0)
894    }
895
896    /// Draws each pose `alpha` of the way from where it stood before the last tick to where it stands now, with `alpha` clamped to [0, 1] and NaN taken as 1.
897    pub fn publish_at(&mut self, into: &mut Publication, alpha: f32) -> Result<(), PhaseError> {
898        if let Some(error) = self.unfinished_error() {
899            return Err(error);
900        }
901        let alpha = presented_alpha(alpha);
902        let step = Step {
903            tick: self.tick,
904            dt: self.config.dt().unwrap_or(0.0),
905        };
906        self.unfinished = Some(Phase::Publication);
907        self.run_phase(Phase::Publication, step)?;
908        self.domains.synchronize();
909        for domain in self.domains.iter_mut() {
910            domain.present(alpha);
911        }
912        let scene = self.scene();
913        let reset = into.source != Some(scene);
914        if reset {
915            *into = Publication::default();
916        }
917        let sequence = self.sequence.wrapping_add(1);
918        let stamp = Stamp {
919            tick: self.tick,
920            sequence,
921        };
922        let extracted = (|| {
923            let library = self.assets.library();
924            let mut changed = reset;
925            let mut count = 0;
926            for domain in self.domains.owned() {
927                for &target in domain.views() {
928                    let Some(placement) =
929                        self.views.to_root(target.image).and_then(|to| to.rigid())
930                    else {
931                        continue;
932                    };
933                    let held = into
934                        .views
935                        .iter()
936                        .position(|view| view.domain == domain.id() && view.target == target);
937                    match held {
938                        Some(index) if index != count => {
939                            into.views.swap(index, count);
940                            changed = true;
941                        }
942                        Some(_) => {}
943                        None => {
944                            into.views.insert(
945                                count,
946                                PublishedView {
947                                    domain: domain.id(),
948                                    target,
949                                    placement,
950                                    records: ViewRecords::default(),
951                                },
952                            );
953                            changed = true;
954                        }
955                    }
956                    let view = &mut into.views[count];
957                    changed |= view.placement != placement;
958                    view.placement = placement;
959                    domain.publish(target.view, library, &mut view.records, stamp, alpha)?;
960                    changed |= view.records.built == stamp;
961                    count += 1;
962                }
963            }
964            changed |= into.views.len() != count;
965            into.views.truncate(count);
966            into.stamp = stamp;
967            if changed {
968                into.content = sequence;
969            }
970            into.alpha = alpha;
971            Ok::<(), DomainError>(())
972        })();
973        if let Err(error) = extracted {
974            *into = Publication::default();
975            let error = PhaseError::unnamed(Phase::Publication, error);
976            self.phase_error = Some(error);
977            return Err(error);
978        }
979        into.source = Some(scene);
980        self.sequence = sequence;
981        self.unfinished = None;
982        Ok(())
983    }
984
985    fn run_phase(&mut self, phase: Phase, step: Step) -> Result<(), PhaseError> {
986        for index in 0..self.phases.entries(phase).len() {
987            self.run_entry(phase, index, step)?;
988        }
989        Ok(())
990    }
991
992    /// Call after the frame's last tick; systems see an empty input until the next boundary.
993    pub fn take_input(&mut self) -> Input {
994        std::mem::take(&mut self.input)
995    }
996
997    pub fn results(&self) -> &[CommandResult] {
998        &self.results
999    }
1000
1001    pub fn pick(&self, ndc: [f32; 2]) -> Option<Pick> {
1002        self.domains.pick(&self.views, self.assets.geometry(), ndc)
1003    }
1004
1005    /// `Unfinished` while a phase is incomplete and `Pending` while deferred mutation remains.
1006    pub fn snapshot(&self) -> Result<SessionSnapshot<A>, RestoreError> {
1007        if let Some(phase) = self.unfinished {
1008            return Err(RestoreError::Unfinished(phase));
1009        }
1010        if !self.commands.is_empty() || self.entities().has_reservations() {
1011            return Err(RestoreError::Pending);
1012        }
1013        Ok(SessionSnapshot {
1014            runtime: self.scene().runtime,
1015            app: self.app.snapshot(),
1016            entities: self.entities().snapshot(),
1017            domains: self
1018                .domains
1019                .owned()
1020                .map(|domain| domain.snapshot())
1021                .collect(),
1022            views: self.views.snapshot(),
1023            bridges: StoreField::snapshot(&self.bridges),
1024            tick: self.tick,
1025            config: self.config,
1026            next_request: self.commands.next_request(),
1027        })
1028    }
1029
1030    /// Validates ownership and every domain before it cancels pending commands and advances the epoch; a failure after that faults the session until a later restore succeeds.
1031    pub fn restore(&mut self, from: &SessionSnapshot<A>) -> Result<(), RestoreError> {
1032        if from.runtime != self.scene().runtime {
1033            return Err(RestoreError::ForeignRuntime);
1034        }
1035        if from.domains.len() != self.domains.len() {
1036            let first = from.domains.len().min(self.domains.len());
1037            return Err(RestoreError::Domain(DomainId::new(first)));
1038        }
1039        for (domain, snapshot) in self.domains.owned().zip(&from.domains) {
1040            domain.check_restore(snapshot)?;
1041        }
1042        self.commands.cancel_into(&mut self.results);
1043        self.commands.restore(&from.entities, from.next_request);
1044        let scene = self.scene();
1045        for (domain, snapshot) in self.domains.iter_mut().zip(&from.domains) {
1046            if let Err(error) = domain.restore(snapshot, scene) {
1047                self.unfinished = Some(Phase::Dispatch);
1048                self.phase_error = Some(PhaseError::system(
1049                    Phase::Dispatch,
1050                    "restore",
1051                    DomainError::Restore(error),
1052                ));
1053                return Err(error);
1054            }
1055        }
1056        self.app.restore(&from.app, scene, Owner::new());
1057        self.views.restore(&from.views);
1058        StoreField::restore(&mut self.bridges, &from.bridges, scene, Owner::new());
1059        self.manipulation.clear();
1060        self.tick = from.tick;
1061        self.config = from.config;
1062        self.unfinished = None;
1063        self.phase_error = None;
1064        Ok(())
1065    }
1066
1067    pub fn set_initial(&mut self) -> Result<(), RestoreError> {
1068        self.initial = Some(self.snapshot()?);
1069        Ok(())
1070    }
1071
1072    pub fn reset(&mut self) -> Result<(), RestoreError> {
1073        let Some(initial) = self.initial.take() else {
1074            return Err(RestoreError::NoInitial);
1075        };
1076        let result = self.restore(&initial);
1077        self.initial = Some(initial);
1078        self.restored = true;
1079        result
1080    }
1081
1082    fn commit(&mut self, growth: &mut Growth) {
1083        let mut batch = std::mem::take(&mut self.batch);
1084        self.commands.drain_into(&mut batch);
1085        for request in batch.drain(..) {
1086            let despawn = matches!(request.command, Command::Despawn(_));
1087            let name = request.command.name();
1088            let outcome = match request.command {
1089                Command::Reset if self.restored => Ok(Outcome::Done),
1090                Command::Reset => {
1091                    let unfinished = self.unfinished;
1092                    let phase_error = self.phase_error;
1093                    let outcome = self
1094                        .reset()
1095                        .map(|()| Outcome::Done)
1096                        .map_err(Rejection::Restore);
1097                    if outcome.is_ok() {
1098                        self.unfinished = unfinished;
1099                        self.phase_error = phase_error;
1100                    }
1101                    outcome
1102                }
1103                command => self.dispatch(|dispatch| dispatch.apply(command)),
1104            };
1105            growth.commands += 1;
1106            match outcome {
1107                Ok(Outcome::Spawned(_)) => growth.spawned += 1,
1108                Ok(Outcome::Done) if despawn => growth.despawned += 1,
1109                _ => {}
1110            }
1111            self.results.push(CommandResult {
1112                request: request.id,
1113                name,
1114                outcome,
1115            });
1116        }
1117        self.batch = batch;
1118    }
1119
1120    fn run_entry(&mut self, phase: Phase, index: usize, step: Step) -> Result<(), PhaseError> {
1121        let Session {
1122            app,
1123            domains,
1124            views,
1125            phases,
1126            commands,
1127            results,
1128            input,
1129            assets,
1130            manipulation,
1131            phase_error,
1132            ..
1133        } = self;
1134        let Some(system) = phases.entries_mut(phase).get_mut(index) else {
1135            return Ok(());
1136        };
1137        let name = system.name();
1138        let result = system.run(Ctx {
1139            app,
1140            domains,
1141            views,
1142            commands,
1143            results: results.as_slice(),
1144            input,
1145            prepared: assets.geometry(),
1146            step,
1147            manipulation,
1148        });
1149        domains.synchronize();
1150        result.map_err(|cause| {
1151            let error = PhaseError::system(phase, name, cause);
1152            *phase_error = Some(error);
1153            error
1154        })
1155    }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160    use crate::view::Vec4;
1161    use loam_math::{EuclideanR4, Space};
1162    use loam_time::alloc::bytes_allocated_by;
1163
1164    use super::*;
1165    use crate::command::SpawnBundle;
1166    use crate::domain::{Instance, Pose};
1167    use crate::entity::Entity;
1168    use crate::store::{LogCapacity, Store};
1169    use crate::view::{DepthEnvelope, DomainRay, ImageRay, ViewMapping, ViewSpec};
1170
1171    crate::stores! {
1172        #[derive(Default)]
1173        pub struct Quiet {}
1174    }
1175
1176    #[test]
1177    fn a_caught_system_unwind_keeps_the_unfinished_phase_blocked() {
1178        let mut session = Session::new(Quiet::default(), SimConfig::default());
1179        session.system(Phase::Simulation, "panic", |_ctx: Ctx<'_, Quiet>| {
1180            panic!("system panic")
1181        });
1182        let unwind = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| session.tick()));
1183        assert!(unwind.is_err());
1184
1185        let mut publication = Publication::default();
1186        assert_eq!(
1187            session.publish(&mut publication),
1188            Err(PhaseError {
1189                phase: Phase::Simulation,
1190                system: None,
1191                cause: DomainError::Unsupported("unfinished phase requires restore"),
1192            })
1193        );
1194        assert_eq!(session.faulted_phase(), Some(Phase::Simulation));
1195    }
1196
1197    fn shaded_segments(
1198        shading: crate::domain::EdgeShading,
1199        at_w: f32,
1200        line_style: Option<(f32, f32)>,
1201        sectioned: bool,
1202        prepare: impl FnOnce(&mut Session<Quiet>) -> (PreparedId, MaterialId),
1203    ) -> Vec<crate::view::SegmentRecord> {
1204        use crate::view::{Eye, Section4, Vec4};
1205
1206        let mut session = Session::new(Quiet::default(), SimConfig::default());
1207        let r4 = session
1208            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1209        let (geometry, material) = prepare(&mut session);
1210        let root = session.views().root();
1211        session
1212            .dispatch(|d| -> Result<(), Rejection> {
1213                let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1214                let mut instance = Instance::new(geometry, material).shaded(shading);
1215                if let Some((width_px, opacity)) = line_style {
1216                    instance = instance.line_style(width_px, opacity);
1217                }
1218                if sectioned {
1219                    instance = instance.sectioned(material);
1220                }
1221                d.spawn(
1222                    SpawnBundle::new()
1223                        .at(r4, Pose::at(Vec4::new(0.0, 0.0, -4.0, at_w)))
1224                        .instance(instance),
1225                )?;
1226                d.domains
1227                    .typed(r4)?
1228                    .add_view(ViewSpec::new(root, eye, Section4 { w: at_w }))?;
1229                Ok(())
1230            })
1231            .expect("the view registered");
1232        session.views_mut().root_mut().eye = Eye::default();
1233        let mut records = Records::default();
1234        records.publish(&mut session).expect("published");
1235        let publication = records.lend().expect("the buffer is free");
1236        publication.views[0].records.segments().to_vec()
1237    }
1238
1239    #[test]
1240    fn the_tesseract_cut_at_w_zero_publishes_the_square_caps_of_its_six_straddling_cells() {
1241        use crate::view::{Eye, Section4, Vec4, ViewId};
1242
1243        const SIZE: f32 = 0.7;
1244        const LIFT: f32 = 0.2;
1245        let mut session = Session::new(Quiet::default(), SimConfig::default());
1246        let r4 = session
1247            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1248        let geometry = session.prepare(PreparedGeometry::Polytope4 {
1249            polytope: loam_shape::polytope::Polytope4::Tesseract,
1250            scale: SIZE,
1251        });
1252        let body = session.add_material(Material::lines([1.0; 4], 1.0));
1253        let cut = session.add_material(Material::lines([1.0, 0.85, 0.35, 1.0], 2.0));
1254        let root = session.views().root();
1255        let view = session
1256            .dispatch(|d| -> Result<ViewId, Rejection> {
1257                let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1258                d.spawn(
1259                    SpawnBundle::new()
1260                        .at(r4, Pose::at(Vec4::new(0.0, 0.0, -4.0, 0.0)))
1261                        .instance(Instance::new(geometry, body).sectioned(cut)),
1262                )?;
1263                d.domains
1264                    .typed(r4)?
1265                    .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
1266                    .map_err(Rejection::from)
1267            })
1268            .expect("the view registered");
1269        session.views_mut().root_mut().eye = Eye::default();
1270
1271        let mut records = Records::default();
1272        let read = |session: &mut Session<Quiet>, records: &mut Records| {
1273            records.publish(session).expect("published");
1274            let publication = records.lend().expect("the buffer is free");
1275            let view = &publication.views[0];
1276            let read = (
1277                view.records.triangles().len(),
1278                view.records.segments().len(),
1279                view.records.built(),
1280            );
1281            records.release(publication);
1282            read
1283        };
1284
1285        let (triangles, segments, built) = read(&mut session, &mut records);
1286        assert_eq!(
1287            triangles, 24,
1288            "the six cells that straddle w = 0 each cut to a square, and each square fans around its centroid into four triangles, not {triangles}"
1289        );
1290        assert_eq!(
1291            segments,
1292            32 + 24,
1293            "the wireframe's 32 edges and the cut cube's 24 perimeter segments came to {segments}"
1294        );
1295        assert_eq!(
1296            read(&mut session, &mut records).2,
1297            built,
1298            "an unchanged source and view rebuilt the section"
1299        );
1300
1301        session
1302            .dispatch(|d| -> Result<(), Rejection> {
1303                d.domains
1304                    .typed(r4)?
1305                    .view_mut(view)
1306                    .ok_or(Rejection::Unsupported("view"))?
1307                    .set_mapping(Section4 { w: LIFT });
1308                Ok(())
1309            })
1310            .expect("the slice moved");
1311        let (lifted, _, moved) = read(&mut session, &mut records);
1312        assert_ne!(
1313            moved, built,
1314            "a new slice left the section at its old build"
1315        );
1316        assert_eq!(
1317            lifted, 24,
1318            "the cut at w = {LIFT} still meets six cells, not {lifted}"
1319        );
1320    }
1321
1322    #[test]
1323    fn a_depth_shaded_segment_reads_its_color_from_the_endpoints_own_w_not_the_bodys() {
1324        use crate::domain::EdgeShading;
1325
1326        const EXTENT: f32 = 0.5;
1327        const BACK: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
1328        const FRONT: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
1329        let shading = EdgeShading::Depth {
1330            back: BACK,
1331            front: FRONT,
1332            extent: EXTENT,
1333        };
1334        let prepare = |session: &mut Session<Quiet>| {
1335            (
1336                session.prepare(PreparedGeometry::Lines4 {
1337                    segments: vec![[[0.0; 4], [0.0, 0.0, 0.0, EXTENT]]],
1338                }),
1339                session.add_material(Material::lines([1.0; 4], 1.0)),
1340            )
1341        };
1342
1343        for lifted in [0.0, 3.0] {
1344            let segments = shaded_segments(shading, lifted, None, false, prepare);
1345            assert_eq!(
1346                segments[0].start_color,
1347                [0.5, 0.0, 0.5, 1.0],
1348                "the midpoint of the depth ramp is wrong for a body at w {lifted}"
1349            );
1350            assert_eq!(
1351                segments[0].end_color, FRONT,
1352                "the far endpoint of the depth ramp is wrong for a body at w {lifted}"
1353            );
1354        }
1355    }
1356
1357    #[test]
1358    fn a_palette_colors_each_segment_by_its_prepared_index_and_no_shading_keeps_the_material() {
1359        use crate::domain::EdgeShading;
1360
1361        const COLORS: [[f32; 4]; 4] = [
1362            [1.0, 0.0, 0.0, 1.0],
1363            [1.0, 1.0, 0.0, 1.0],
1364            [0.0, 1.0, 0.0, 1.0],
1365            [0.0, 1.0, 1.0, 1.0],
1366        ];
1367        const MATERIAL: [f32; 4] = [0.25, 0.5, 0.75, 1.0];
1368        let two_edges = vec![
1369            [[0.0; 4], [0.5, 0.0, 0.0, 0.0]],
1370            [[0.0; 4], [0.0, 0.5, 0.0, 0.0]],
1371        ];
1372
1373        let painted = shaded_segments(EdgeShading::Material, 0.0, None, false, |session| {
1374            (
1375                session.prepare(PreparedGeometry::Lines4 {
1376                    segments: two_edges.clone(),
1377                }),
1378                session.add_material(Material::lines(MATERIAL, 1.0)),
1379            )
1380        });
1381        assert_eq!(
1382            painted.iter().map(|s| s.start_color).collect::<Vec<_>>(),
1383            [MATERIAL, MATERIAL],
1384            "an instance with no shading lost the material's line color"
1385        );
1386
1387        let mut id = None;
1388        let painted = shaded_segments(
1389            EdgeShading::Palette(PaletteId(0)),
1390            0.0,
1391            None,
1392            false,
1393            |session: &mut Session<Quiet>| {
1394                id = Some(session.add_palette(COLORS.to_vec()));
1395                (
1396                    session.prepare(PreparedGeometry::Lines4 {
1397                        segments: two_edges,
1398                    }),
1399                    session.add_material(Material::lines(MATERIAL, 1.0)),
1400                )
1401            },
1402        );
1403        assert_eq!(id, Some(PaletteId(0)));
1404        assert_eq!(
1405            painted
1406                .iter()
1407                .flat_map(|s| [s.start_color, s.end_color])
1408                .collect::<Vec<_>>(),
1409            COLORS,
1410            "the palette did not follow the prepared segment endpoints in order"
1411        );
1412    }
1413
1414    #[test]
1415    fn an_instance_line_style_replaces_the_material_width_and_opacity() {
1416        let [segment] = shaded_segments(
1417            crate::domain::EdgeShading::Material,
1418            0.0,
1419            Some((2.5, 0.4)),
1420            false,
1421            |session| {
1422                (
1423                    session.prepare(PreparedGeometry::Lines4 {
1424                        segments: vec![[[0.0; 4], [0.5, 0.0, 0.0, 0.0]]],
1425                    }),
1426                    session.add_material(Material::lines([0.2, 0.4, 0.6, 0.8], 1.0)),
1427                )
1428            },
1429        )[..] else {
1430            panic!("the line did not publish as one segment");
1431        };
1432        assert_eq!(segment.width_px, 2.5);
1433        assert_eq!(segment.start_color, [0.2, 0.4, 0.6, 0.4]);
1434        assert_eq!(segment.end_color, [0.2, 0.4, 0.6, 0.4]);
1435
1436        let sectioned = shaded_segments(
1437            crate::domain::EdgeShading::Material,
1438            0.0,
1439            Some((2.5, 0.4)),
1440            true,
1441            |session| {
1442                (
1443                    session.prepare(PreparedGeometry::Polytope4 {
1444                        polytope: loam_shape::polytope::Polytope4::Tesseract,
1445                        scale: 0.7,
1446                    }),
1447                    session.add_material(Material::lines([0.2, 0.4, 0.6, 0.8], 1.0)),
1448                )
1449            },
1450        );
1451        assert!(sectioned.len() > 32, "the section edge did not publish");
1452        assert!(sectioned[..sectioned.len() - 32].iter().all(|segment| {
1453            segment.width_px == 2.5
1454                && segment.start_color == [0.2, 0.4, 0.6, 0.4]
1455                && segment.end_color == [0.2, 0.4, 0.6, 0.4]
1456        }));
1457    }
1458
1459    #[test]
1460    fn a_grab_takes_the_view_it_can_lift_while_a_plain_pick_keeps_the_nearer_one() {
1461        use crate::view::{Eye, Projection4, Section4, Vec4, ViewId};
1462        use loam_math::EuclideanR4;
1463
1464        const DEPTH: f32 = 4.0;
1465        const FOCAL: f32 = 2.5;
1466        const AT_W: f32 = -2.5;
1467
1468        let mut session = Session::new(Quiet::default(), SimConfig::default());
1469        let r4 = session
1470            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1471        let edges = session.prepare(PreparedGeometry::Lines4 {
1472            segments: vec![[[0.5, 0.0, 0.0, 0.0], [-0.5, 0.0, 0.0, 0.0]]],
1473        });
1474        let white = session.add_material(Material::lines([1.0; 4], 1.0));
1475        let root = session.views().root();
1476        let (section, projection) = session
1477            .dispatch(|d| -> Result<(ViewId, ViewId), Rejection> {
1478                let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1479                d.spawn(
1480                    SpawnBundle::new()
1481                        .at(r4, Pose::at(Vec4::new(0.0, 0.0, -DEPTH, AT_W)))
1482                        .instance(Instance::new(edges, white)),
1483                )?;
1484                let domain = d.domains.typed(r4)?;
1485                let section = domain.add_view(ViewSpec::new(root, eye, Section4 { w: AT_W }))?;
1486                let projection =
1487                    domain.add_view(ViewSpec::new(root, eye, Projection4 { focal: FOCAL }))?;
1488                Ok((section, projection))
1489            })
1490            .expect("the two views registered");
1491        session.views_mut().root_mut().eye = Eye::default();
1492
1493        let picked = session
1494            .pick([0.0, 0.0])
1495            .expect("both views cover the origin");
1496        assert_eq!(
1497            picked.view, projection,
1498            "the plain pick lost the nearer projection layer"
1499        );
1500        let grabbed = session.grab([0.0, 0.0], 0.0).expect("the section lifts");
1501        assert_eq!(
1502            grabbed.view, section,
1503            "the grab resolved to a view it cannot lift, so a drag is a coin flip"
1504        );
1505    }
1506
1507    #[test]
1508    fn a_section_pick_passes_through_a_body_the_slice_misses() {
1509        use crate::view::{Eye, Section4};
1510        use loam_shape::polytope::Polytope4;
1511
1512        const SCALE: f32 = 0.45;
1513
1514        let mut session = Session::new(Quiet::default(), SimConfig::default());
1515        let r4 = session
1516            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1517        let tesseract = session.prepare(PreparedGeometry::Polytope4 {
1518            polytope: Polytope4::Tesseract,
1519            scale: SCALE,
1520        });
1521        let white = session.add_material(Material::lines([1.0; 4], 1.0));
1522        let root = session.views().root();
1523        let cut = session
1524            .dispatch(|d| -> Result<Entity, Rejection> {
1525                let eye = d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))?;
1526                d.spawn(
1527                    SpawnBundle::new()
1528                        .at(r4, Pose::at(Vec4::new(0.0, 0.0, -3.0, 0.3)))
1529                        .instance(Instance::new(tesseract, white)),
1530                )?;
1531                let cut = d.spawn(
1532                    SpawnBundle::new()
1533                        .at(r4, Pose::at(Vec4::new(0.0, 0.0, -5.0, 0.0)))
1534                        .instance(Instance::new(tesseract, white)),
1535                )?;
1536                d.domains
1537                    .typed(r4)?
1538                    .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))?;
1539                Ok(cut)
1540            })
1541            .expect("the scene built");
1542        session.views_mut().root_mut().eye = Eye::default();
1543
1544        let picked = session
1545            .pick([0.0, 0.0])
1546            .expect("the slice cuts the far tesseract");
1547        assert_eq!(
1548            picked.entity, cut,
1549            "the pick took the near tesseract, whose section is empty"
1550        );
1551        let face = picked.hit.expect("a section pick lifts").coordinates[2];
1552        assert!(
1553            (face - (-5.0 + 0.5 * SCALE)).abs() < 1e-4,
1554            "the hit is at z = {face}, off the tesseract's face"
1555        );
1556    }
1557
1558    crate::stores! {
1559        #[derive(Default)]
1560        pub struct Churn {
1561            counters: Store<u32>,
1562            pool: Value<Vec<Entity>>,
1563        }
1564    }
1565
1566    #[test]
1567    fn warmed_tick_and_boundary_allocate_nothing_outside_reported_growth() {
1568        let mut session = Session::new(Churn::default(), SimConfig::default());
1569        let r4 = session
1570            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1571        let pool = session.dispatch(|d| {
1572            (0..8)
1573                .map(|value| {
1574                    d.spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)).row(value))
1575                        .unwrap()
1576                })
1577                .collect()
1578        });
1579        session.app.pool.set(pool);
1580        session.system(Phase::Simulation, "churn", |ctx: Ctx<'_, Churn>| {
1581            for (_, counter) in ctx.app.counters.iter_mut() {
1582                *counter += 1;
1583            }
1584            let pool = ctx.app.pool.get_mut();
1585            let retired = pool.swap_remove(0);
1586            ctx.commands.submit(Command::Despawn(retired));
1587            let fresh = ctx.commands.spawn(SpawnBundle::new()).unwrap();
1588            pool.push(fresh.entity);
1589            Ok(())
1590        });
1591        let cycle = |session: &mut Session<Churn>| {
1592            session.tick().unwrap();
1593            session.boundary(Input::default()).unwrap()
1594        };
1595        for _ in 0..16 {
1596            cycle(&mut session);
1597        }
1598
1599        let bytes = bytes_allocated_by(|| {
1600            for _ in 0..16 {
1601                let growth = cycle(&mut session);
1602                assert_eq!(
1603                    growth,
1604                    Growth {
1605                        commands: 2,
1606                        spawned: 1,
1607                        despawned: 1,
1608                    }
1609                );
1610            }
1611            let quiet = session.boundary(Input::default()).unwrap();
1612            assert_eq!(quiet, Growth::default());
1613        })
1614        .expect("the counting allocator is installed");
1615        assert_eq!(
1616            bytes, 0,
1617            "16 warmed cycles asked the allocator for {bytes} bytes"
1618        );
1619        assert_eq!(session.entities().len(), 8);
1620    }
1621
1622    crate::stores! {
1623        #[derive(Default)]
1624        pub struct Tagged {
1625            tags: Store<u32>,
1626        }
1627    }
1628
1629    #[test]
1630    fn a_warmed_restore_allocates_beyond_the_rows_it_copies() {
1631        use crate::domain::{Field, FieldKind};
1632        use crate::field::FieldOp;
1633        use crate::view::Vec4;
1634
1635        let mut session = Session::new(Tagged::default(), SimConfig::default());
1636        let r4 = session.register_domain(
1637            DomainBuilder::new("r4", EuclideanR4)
1638                .tracked(LogCapacity::default())
1639                .fields(),
1640        );
1641        session.dispatch(|d| {
1642            let at = |x: f32| SpawnBundle::new().at(r4, Pose::at(Vec4::new(x, 0.0, 0.0, 0.0)));
1643            let left = d.spawn(at(-1.0).row(1u32)).unwrap();
1644            let right = d.spawn(at(1.0).row(2u32)).unwrap();
1645            let union = d.spawn(at(0.0).row(3u32)).unwrap();
1646            for operand in [left, right] {
1647                d.attach_field(
1648                    r4,
1649                    operand,
1650                    Field {
1651                        kind: FieldKind::ExactDistance,
1652                        op: FieldOp::HyperSphere { radius: 1.0 },
1653                        operands: Vec::new(),
1654                    },
1655                )
1656                .unwrap();
1657            }
1658            d.attach_field(
1659                r4,
1660                union,
1661                Field {
1662                    kind: FieldKind::ExactDistance,
1663                    op: FieldOp::Union,
1664                    operands: vec![left, right],
1665                },
1666            )
1667            .unwrap();
1668        });
1669        let snapshot = session.snapshot().unwrap();
1670        for _ in 0..8 {
1671            session.restore(&snapshot).unwrap();
1672        }
1673
1674        let bytes = bytes_allocated_by(|| {
1675            session.restore(&snapshot).unwrap();
1676        })
1677        .expect("the counting allocator is installed");
1678        let copied = (size_of::<Entity>() * 2) as u64;
1679        assert_eq!(
1680            bytes, copied,
1681            "a warmed restore of three rows asked the allocator for {bytes} bytes, not the {copied} its operand list copies"
1682        );
1683        session
1684            .domains_mut()
1685            .facade(r4.id())
1686            .unwrap()
1687            .compile_fields()
1688            .expect("the restored operands still name live rows");
1689    }
1690
1691    crate::stores! {
1692        pub struct Shown {
1693            scores: Store<u32>,
1694        }
1695    }
1696
1697    struct Flat;
1698
1699    impl ViewMapping<EuclideanR4> for Flat {
1700        fn name(&self) -> &'static str {
1701            "flat"
1702        }
1703
1704        fn image_point(
1705            &self,
1706            eye: &Pose<EuclideanR4>,
1707            point: <EuclideanR4 as Space>::Point,
1708        ) -> Option<[f32; 3]> {
1709            let relative = point - eye.point;
1710            Some([relative.x, relative.y, relative.z])
1711        }
1712
1713        fn lift(
1714            &self,
1715            _eye: &Pose<EuclideanR4>,
1716            _ray: &ImageRay,
1717        ) -> Option<DomainRay<EuclideanR4>> {
1718            None
1719        }
1720
1721        fn depth_envelope(&self) -> DepthEnvelope {
1722            DepthEnvelope {
1723                near: 0.0,
1724                far: 1.0,
1725            }
1726        }
1727    }
1728
1729    #[test]
1730    fn a_foreign_publication_buffer_does_not_restamp_the_previous_sessions_records() {
1731        let build = |score: u32, offset: f32| {
1732            let shown = Shown {
1733                scores: Store::tracked(LogCapacity::default()),
1734            };
1735            let mut session = Session::new(shown, SimConfig::default());
1736            let r4 = session.register_domain(
1737                DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()),
1738            );
1739            let geometry = session.prepare(PreparedGeometry::Lines4 {
1740                segments: vec![[[offset, 0.0, 0.0, 0.0], [offset + 1.0, 0.0, 0.0, 0.0]]],
1741            });
1742            let material = session.add_material(Material::flat([1.0; 4]));
1743            let root = session.views().root();
1744            session.dispatch(|dispatch| {
1745                let eye = dispatch
1746                    .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
1747                    .unwrap();
1748                dispatch
1749                    .spawn(
1750                        SpawnBundle::new()
1751                            .at(r4, Pose::at(Vec4::ZERO))
1752                            .instance(Instance::new(geometry, material))
1753                            .row(score),
1754                    )
1755                    .unwrap();
1756                dispatch
1757                    .domains
1758                    .typed(r4)
1759                    .unwrap()
1760                    .add_view(ViewSpec::new(root, eye, Flat))
1761                    .unwrap();
1762            });
1763            session
1764        };
1765        let mut first = build(11, 0.0);
1766        let mut second = build(22, 4.0);
1767        let mut publication = Publication::default();
1768
1769        first.publish(&mut publication).unwrap();
1770        assert_eq!(publication.views[0].records.segments()[0].start[0], 0.0);
1771        second.publish(&mut publication).unwrap();
1772        assert_eq!(publication.views[0].records.segments()[0].start[0], 4.0);
1773    }
1774
1775    #[test]
1776    fn warmed_publish_allocates() {
1777        let shown = Shown {
1778            scores: Store::tracked(LogCapacity::default()),
1779        };
1780        let mut session = Session::new(shown, SimConfig::default());
1781        let r4 = session
1782            .register_domain(DomainBuilder::new("r4", EuclideanR4).tracked(LogCapacity::default()));
1783        let geometry = session.prepare(PreparedGeometry::Lines4 {
1784            segments: Vec::new(),
1785        });
1786        let material = session.add_material(Material::flat([1.0; 4]));
1787        let root = session.views().root();
1788        session.dispatch(|d| {
1789            let eye = d
1790                .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::ZERO)))
1791                .unwrap();
1792            d.domains
1793                .typed(r4)
1794                .unwrap()
1795                .add_view(ViewSpec::new(root, eye, Flat))
1796                .unwrap();
1797            for value in 0..8 {
1798                d.spawn(
1799                    SpawnBundle::new()
1800                        .at(r4, Pose::at(Vec4::ZERO))
1801                        .instance(Instance::new(geometry, material))
1802                        .row(value),
1803                )
1804                .unwrap();
1805            }
1806        });
1807        session.system(Phase::Simulation, "churn", move |ctx: Ctx<'_, Shown>| {
1808            let domain = ctx.domains.typed(r4).unwrap();
1809            for (entity, score) in ctx.app.scores.iter_mut() {
1810                *score += 1;
1811                let mut point = domain.poses().get(entity).unwrap().point;
1812                point.x += ctx.step.dt;
1813                domain.set_point(entity, point).unwrap();
1814            }
1815            Ok(())
1816        });
1817        let mut publication = Publication::default();
1818        let cycle = |session: &mut Session<Shown>, publication: &mut Publication| {
1819            session.tick().unwrap();
1820            session.boundary(Input::default()).unwrap();
1821            session.publish(publication).unwrap();
1822        };
1823        for _ in 0..4 {
1824            cycle(&mut session, &mut publication);
1825        }
1826
1827        let bytes = bytes_allocated_by(|| {
1828            for _ in 0..16 {
1829                cycle(&mut session, &mut publication);
1830            }
1831        })
1832        .expect("the counting allocator is installed");
1833        assert_eq!(
1834            bytes, 0,
1835            "16 warmed publishes asked the allocator for {bytes} bytes"
1836        );
1837        assert_eq!(publication.views[0].records.instances.rows().len(), 8);
1838    }
1839}