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}