ready-active-safe 0.1.3

Lifecycle engine for externally driven systems
Documentation
//! `OpenXR` session lifecycle.
//!
//! The runtime drives the session through modes.
//! The machine never transitions itself.

#![allow(clippy::print_stdout, clippy::enum_glob_use)]

use ready_active_safe::prelude::*;

/// XR session lifecycle modes.
///
/// These map directly to the `OpenXR` session states, but modeled as
/// lifecycle phases rather than an arbitrary state graph.
#[derive(Debug, Clone, PartialEq, Eq)]
enum SessionMode {
    /// Session created but not yet ready to render.
    Idle,
    /// Runtime is ready. The application should prepare resources.
    Ready,
    /// Frames are being composed but not yet visible.
    Synchronized,
    /// Session is visible to the user.
    Visible,
    /// Session has input focus.
    Focused,
    /// Session is stopping. Release resources.
    Stopping,
}

impl core::fmt::Display for SessionMode {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "{self:?}")
    }
}

/// Events from the XR runtime.
#[derive(Debug)]
enum XrEvent {
    /// The runtime signals a session state change.
    SessionStateChanged(SessionMode),
    /// A frame has been waited on and is available.
    FrameReady,
    /// The user or runtime requests session end.
    RequestExit,
}

/// Commands the application should execute in response.
#[derive(Debug, PartialEq)]
enum XrCommand {
    /// Load shaders, meshes, and textures.
    PrepareResources,
    /// Begin the frame submission loop.
    BeginFrameLoop,
    /// Submit a frame to the compositor.
    SubmitFrame,
    /// Acquire input device state.
    PollInput,
    /// Release GPU resources and end the session.
    ReleaseResources,
    /// Call `xrEndSession`.
    EndSession,
}

struct XrSession;

impl Machine for XrSession {
    type Mode = SessionMode;
    type Event = XrEvent;
    type Command = XrCommand;

    fn initial_mode(&self) -> SessionMode {
        SessionMode::Idle
    }

    fn on_event(
        &self,
        mode: &Self::Mode,
        event: &Self::Event,
    ) -> Decision<Self::Mode, Self::Command> {
        use SessionMode::*;
        use XrCommand::*;
        use XrEvent::*;

        match (mode, event) {
            (Idle, SessionStateChanged(Ready)) => transition(Ready).emit(PrepareResources),
            (Ready, SessionStateChanged(Synchronized)) => {
                transition(Synchronized).emit(BeginFrameLoop)
            }
            (Synchronized, SessionStateChanged(Visible)) => transition(Visible),
            (Visible, SessionStateChanged(Focused)) => transition(Focused).emit(PollInput),
            (Focused, FrameReady) => stay().emit_all([SubmitFrame, PollInput]),
            (Visible, FrameReady) => stay().emit(SubmitFrame),
            (mode, RequestExit) if *mode != Idle && *mode != Stopping => {
                transition(Stopping).emit_all([ReleaseResources, EndSession])
            }
            _ => ignore(),
        }
    }
}

fn main() {
    let session = XrSession;
    let mut mode = session.initial_mode();

    let events = [
        XrEvent::SessionStateChanged(SessionMode::Ready),
        XrEvent::SessionStateChanged(SessionMode::Synchronized),
        XrEvent::SessionStateChanged(SessionMode::Visible),
        XrEvent::SessionStateChanged(SessionMode::Focused),
        XrEvent::FrameReady,
        XrEvent::FrameReady,
        XrEvent::RequestExit,
    ];

    for event in &events {
        let decision = session.decide(&mode, event);
        println!("{mode:?} + {event:?} => {decision}");

        let (next_mode, commands) = decision.apply(mode);
        if !commands.is_empty() {
            println!("  commands: {commands:?}");
        }

        mode = next_mode;
    }

    assert_eq!(mode, SessionMode::Stopping);
}