Skip to main content

nightshade_api/
app.rs

1//! The caller-owned main loop: [`open`] a window, then [`frame`] it yourself.
2
3use crate::runner::ApiState;
4use nightshade::prelude::*;
5use nightshade::run::pump::{PumpShell, pump_frame, pump_shell_new, pump_shell_ready};
6
7/// Window settings for [`open_with`]. Plain data with sensible defaults.
8pub struct Window {
9    pub title: String,
10    pub size: Option<(u32, u32)>,
11}
12
13impl Default for Window {
14    fn default() -> Self {
15        Self {
16            title: "nightshade".to_string(),
17            size: None,
18        }
19    }
20}
21
22/// A running engine you drive one [`frame`] at a time.
23///
24/// `world` is the real engine world, yours to read and mutate between frames.
25/// `frame_limit` stops the loop after that many frames, and is seeded from
26/// the `NIGHTSHADE_API_FRAMES` environment variable so examples double as
27/// smoke tests.
28pub struct App {
29    pub world: World,
30    pub frame_limit: Option<u32>,
31    frames_rendered: u32,
32    shell: PumpShell,
33}
34
35/// Opens a window with the default settings and returns once the renderer is
36/// ready and the standard scene defaults (sky, sun, grid, orbit camera) are in
37/// place.
38pub fn open() -> App {
39    open_with(Window::default())
40}
41
42/// Opens a window with the given settings. See [`open`].
43pub fn open_with(window: Window) -> App {
44    let mut stages = crate::runner::build_api_frame_stages();
45    nightshade::app::remove_frame_system(
46        &mut stages,
47        nightshade::ecs::input::systems::reset_mouse_system,
48    );
49    nightshade::app::remove_frame_system(
50        &mut stages,
51        nightshade::ecs::input::systems::reset_keyboard_system,
52    );
53    nightshade::app::remove_frame_system(
54        &mut stages,
55        nightshade::ecs::input::systems::reset_touch_system,
56    );
57    let state = ApiState::<()> {
58        setup: None,
59        update: None,
60        data: None,
61        clears_draw_pools: false,
62        frame_limit: None,
63        frames_rendered: 0,
64        stages,
65    };
66    let mut shell =
67        pump_shell_new(Box::new(state)).expect("failed to create the engine event loop");
68    shell
69        .context
70        .world
71        .res_mut::<nightshade::ecs::window::resources::Window>()
72        .title = window.title;
73    shell
74        .context
75        .world
76        .res_mut::<nightshade::ecs::window::resources::Window>()
77        .initial_size = window.size;
78
79    let mut pumps_without_window = 0;
80    while !pump_shell_ready(&shell) {
81        if !pump_frame(&mut shell) {
82            break;
83        }
84        let window_exists = shell
85            .context
86            .world
87            .res::<nightshade::ecs::window::resources::Window>()
88            .handle
89            .is_some();
90        if window_exists && shell.context.initialized && shell.context.renderer.is_none() {
91            panic!("failed to create the renderer, see the log for the error");
92        }
93        if !window_exists {
94            pumps_without_window += 1;
95            if pumps_without_window > 10000 {
96                panic!("failed to create the window, see the log for the error");
97            }
98        }
99    }
100
101    let mut world = World::default();
102    std::mem::swap(&mut world, &mut shell.context.world);
103
104    let frame_limit = crate::runner::frame_limit_from_environment();
105
106    App {
107        world,
108        frame_limit,
109        frames_rendered: 0,
110        shell,
111    }
112}
113
114/// Builds a scene and saves it as a png, with no visible window. The window
115/// and renderer come up hidden, `setup` runs against the same defaults
116/// [`open`] applies, the scene settles for half a second of frames so
117/// streamed textures and lighting captures land, then the frame is captured
118/// to `path`.
119///
120/// ```ignore
121/// render_image(1920, 1080, "scene.png", |world| {
122///     spawn_floor(world, 10.0);
123///     let gem = spawn_torus(world, vec3(0.0, 1.5, 0.0));
124///     set_emissive(world, gem, [0.3, 0.8, 1.0], 8.0);
125/// });
126/// ```
127pub fn render_image(
128    width: u32,
129    height: u32,
130    path: impl Into<std::path::PathBuf>,
131    setup: impl FnOnce(&mut World),
132) {
133    let state = ApiState::<()> {
134        setup: None,
135        update: None,
136        data: None,
137        clears_draw_pools: false,
138        frame_limit: None,
139        frames_rendered: 0,
140        stages: crate::runner::build_api_frame_stages(),
141    };
142    let mut shell =
143        pump_shell_new(Box::new(state)).expect("failed to create the engine event loop");
144    shell
145        .context
146        .world
147        .res_mut::<nightshade::ecs::window::resources::Window>()
148        .start_hidden = true;
149    shell
150        .context
151        .world
152        .res_mut::<nightshade::ecs::window::resources::Window>()
153        .initial_size = Some((width, height));
154
155    while !pump_shell_ready(&shell) {
156        if !pump_frame(&mut shell) {
157            return;
158        }
159    }
160
161    setup(&mut shell.context.world);
162
163    for _ in 0..30 {
164        tick_hidden(&mut shell);
165    }
166    crate::environment::screenshot(&mut shell.context.world, path.into());
167    for _ in 0..10 {
168        tick_hidden(&mut shell);
169    }
170}
171
172fn tick_hidden(shell: &mut PumpShell) {
173    let context = &mut shell.context;
174    let Some(renderer) = context.renderer.as_mut() else {
175        return;
176    };
177    if let Some(next_state) = tick_offscreen(&mut context.world, context.state.as_mut(), renderer) {
178        context.state = next_state;
179    }
180}
181
182/// Pumps events and renders one frame. Returns false when the window closes,
183/// escape exits, or the frame limit is reached.
184///
185/// Everything drawn with the `draw_` functions since the previous call is
186/// visible for this frame and cleared afterward. Edge triggered input
187/// ([`key_pressed`](crate::prelude::key_pressed),
188/// [`mouse_clicked`](crate::prelude::mouse_clicked), the mouse deltas)
189/// reflects this frame's events and stays readable until the next call. The
190/// per frame input reset that normally runs inside the engine's frame
191/// schedule runs here instead, before pumping, so those flags survive the
192/// gap where your code runs.
193pub fn frame(app: &mut App) -> bool {
194    if let Some(limit) = app.frame_limit
195        && app.frames_rendered >= limit
196    {
197        return false;
198    }
199
200    nightshade::ecs::input::systems::reset_mouse_system(&mut app.world);
201    nightshade::ecs::input::systems::reset_keyboard_system(&mut app.world);
202    nightshade::ecs::input::systems::reset_touch_system(&mut app.world);
203
204    std::mem::swap(&mut app.world, &mut app.shell.context.world);
205    let alive = pump_frame(&mut app.shell);
206    std::mem::swap(&mut app.world, &mut app.shell.context.world);
207
208    crate::draw::clear_draw_pools(&mut app.world);
209    app.frames_rendered += 1;
210
211    alive
212}