Skip to main content

pixel8_console/
lib.rs

1//! The Pixel8 console as a library: the shell (boot prompt, mode machine,
2//! run loop), the five editors, build orchestration and web export —
3//! everything except presentation and input, which each frontend supplies.
4//!
5//! Two frontends drive it: the `pixel8` binary in this crate — whose
6//! windowed frontend (winit + wgpu) sits behind the default-on `window`
7//! feature, while its headless subcommands always build — and the
8//! `pixel8-tui` crate (sixel/half-block terminal rendering). Frontends
9//! that don't want the GPU stack depend on this crate with
10//! `default-features = false`; everything a frontend needs — ticking the
11//! [`shell::Shell`], feeding it keys and mouse state, presenting the
12//! framebuffer it draws — is exported here.
13
14pub mod builder;
15mod clipboard;
16mod editor;
17#[cfg(feature = "window")]
18pub mod gpu;
19pub mod shell;
20pub mod ui;
21mod watch;
22pub mod webexport;
23
24use std::{
25    path::{Path, PathBuf},
26    time::{Duration, Instant},
27};
28
29/// One tick's wall-clock budget at a given rate (30 normally, 60 while a
30/// 60 fps cart runs).
31pub fn frame_duration(fps: u32) -> Duration {
32    Duration::from_nanos(1_000_000_000 / fps.max(1) as u64)
33}
34
35/// Paces a fixed-rate tick loop against the wall clock, one pass at a time.
36///
37/// A frontend alternates between handling input and running the ticks that are due. Each pass
38/// reads the clock once and runs the ticks owed at that instant, however long they take to run;
39/// whatever falls due meanwhile waits for the next pass, after the frontend has handled the input
40/// that arrived in between — a key release, `Esc`, a resize. A pass that asked the clock again
41/// after every tick would keep finding another one due for as long as ticks run slower than real
42/// time (a heavy cart, an unoptimized build), and would never hand control back.
43pub struct TickPacer {
44    next: Instant,
45}
46
47impl TickPacer {
48    /// A pacer whose first tick is due at `now`.
49    pub fn new(now: Instant) -> Self {
50        Self { next: now }
51    }
52
53    /// Whether a tick is due by `now`, the instant the calling pass began, counting it as run if
54    /// so.
55    ///
56    /// A pass asks with the same `now` until the answer is `false`, running one tick per `true`.
57    /// A pass still more than ten frames behind after its first tick drops the rest of the
58    /// backlog rather than running it back to back: that tick is all it runs, and the next falls
59    /// due a frame after `now`.
60    pub fn tick_due(&mut self, now: Instant, frame: Duration) -> bool {
61        if now < self.next {
62            return false;
63        }
64        self.next += frame;
65        // Too far behind to catch up without stalling the frontend: drop the backlog.
66        if now > self.next + frame * 10 {
67            self.next = now + frame;
68        }
69        true
70    }
71
72    /// The instant the next tick falls due.
73    pub fn next_tick(&self) -> Instant {
74        self.next
75    }
76}
77
78/// Where the `pixel8` SDK crate lives, for generated project manifests.
79/// Defaults to this source tree; override with PIXEL8_SDK for installs.
80pub fn sdk_path() -> PathBuf {
81    if let Ok(p) = std::env::var("PIXEL8_SDK") {
82        return PathBuf::from(p);
83    }
84    Path::new(env!("CARGO_MANIFEST_DIR")).join("../pixel8")
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn a_pass_runs_only_the_ticks_already_due() {
93        let t0 = Instant::now();
94        let frame = Duration::from_millis(10);
95        let mut pacer = TickPacer::new(t0);
96        // 2.5 frames behind: three ticks are owed, then none, however many
97        // more times the pass asks with the same `now`.
98        let now = t0 + frame * 2 + frame / 2;
99        let mut ticks = 0;
100        while pacer.tick_due(now, frame) {
101            ticks += 1;
102        }
103        assert_eq!(ticks, 3);
104        assert!(!pacer.tick_due(now, frame));
105    }
106
107    #[test]
108    fn a_long_stall_skips_ahead_with_a_single_tick() {
109        let t0 = Instant::now();
110        let frame = Duration::from_millis(10);
111        let mut pacer = TickPacer::new(t0);
112        // Far past the 10-frame grace period.
113        let now = t0 + frame * 15;
114        assert!(pacer.tick_due(now, frame));
115        assert_eq!(pacer.next_tick(), now + frame);
116        assert!(!pacer.tick_due(now, frame));
117    }
118
119    #[test]
120    fn next_tick_reports_the_scheduled_instant() {
121        let t0 = Instant::now();
122        let frame = Duration::from_millis(10);
123        let mut pacer = TickPacer::new(t0);
124        assert_eq!(pacer.next_tick(), t0);
125        assert!(pacer.tick_due(t0, frame));
126        assert_eq!(pacer.next_tick(), t0 + frame);
127    }
128}