Skip to main content

saturnus_objects/
screen.rs

1//! What has the keyboard: the stack (the ROM's top-level outer loop), an
2//! open command line, a halted program, or an application's own loop (an
3//! input form, a choose box, a message box, the graphics screen), read
4//! from RAM without pressing a key; the display only where RAM gave no
5//! sign (wiki: hardware/ui-state).
6//!
7//! The signals, observed on the 48SX (ROM J), 48GX (ROM R) and 49G (ROM
8//! 2.10 and 1.19-6) while the ROM waits for a key:
9//!
10//! - The RPL return stack (between the saved `HeapStart` and the saved B):
11//!   its bottom entries are the system outer loop's own frame; while the
12//!   stack has the keyboard, the sixth entry from the top (under the key
13//!   wait's four entries and the calling loop's own return into it) is
14//!   that same outer-loop frame. Any other loop (a
15//!   parameterized outer loop: forms, choose and message boxes, the
16//!   graphics screen, `INPUT`, `WAIT`, the interactive stack; the 48's
17//!   equation writer) puts its own frame there. A halted program leaves
18//!   its frames on the return stack with a new outer loop above them, so
19//!   each halted program adds one more copy of the outer-loop frame.
20//! - The temporary environments (RPLMAN 13.1): input forms bind named
21//!   temporaries (`IF` on the 48GX, `IfMenu`, `CurrentField`, ... on the
22//!   49G), the 49G's choose box binds `NbElm` and others.
23//! - The display: the stack display's GROB pointer equals the PICT
24//!   pointer while the graphics screen shows.
25//! - The 48GX's choose and message boxes bind only unnamed temporaries,
26//!   so they are told apart on the display: a framed box with a two-dot
27//!   shadow, a choose box with a dark (inverted) row inside it.
28
29use saturnus_core::{Machine, Model};
30use serde::Serialize;
31
32use crate::cmdline::{self, EditorLayout};
33use crate::error::Error;
34use crate::object::{Memory, Object, decode_at};
35
36type Result<T, E = Error> = std::result::Result<T, E>;
37
38/// Most return-stack entries believed (more means the pointers are not
39/// the ROM's).
40const MAX_DEPTH: u32 = 4096;
41/// Most temporary environments walked.
42const MAX_ENVIRONMENTS: usize = 64;
43/// Most temporaries read per environment.
44const MAX_BINDINGS: u32 = 512;
45/// Entries the key wait itself puts on the return stack, above the loop
46/// that called it.
47const KEY_WAIT: u32 = 4;
48/// The entry from the top (1-based) that holds the calling loop's frame:
49/// under the key wait's entries and the loop's own return into the wait.
50const LOOP_FRAME: u32 = KEY_WAIT + 2;
51
52/// Where a model's ROM keeps what the screen state is read from (wiki:
53/// hardware/ui-state). The `*_ptr` fields hold 5-nibble pointers.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub struct ScreenLayout {
56    /// The bottom of the return stack (`HeapStart` in the 1991 address
57    /// list).
58    pub return_bottom_ptr: u32,
59    /// The saved B, the return stack's top (`FramePtr`).
60    pub return_top_ptr: u32,
61    /// The newest temporary environment (`local_vars`).
62    pub environments_ptr: u32,
63    /// The GROB the display shows below the header (`Stk_56line_GROB_ptr`).
64    pub shown_grob_ptr: u32,
65    /// PICT (`graph_GROB_ptr`).
66    pub pict_ptr: u32,
67    /// Return-stack entries at the stack with no program halted.
68    pub base_depth: u32,
69    /// Which entry from the bottom (1 = the bottom one) is the outer
70    /// loop's frame.
71    pub outer_frame: u32,
72}
73
74impl ScreenLayout {
75    /// 48SX (ROM J): the 1991 address list's locations.
76    pub const HP48SX: ScreenLayout = ScreenLayout {
77        return_bottom_ptr: 0x7056F,
78        return_top_ptr: 0x70574,
79        environments_ptr: 0x70583,
80        shown_grob_ptr: 0x7055B,
81        pict_ptr: 0x70565,
82        base_depth: 8,
83        outer_frame: 3,
84    };
85    /// 48GX (ROM R): the 48SX's plus #1017F.
86    pub const HP48GX: ScreenLayout = ScreenLayout {
87        return_bottom_ptr: 0x806EE,
88        return_top_ptr: 0x806F3,
89        environments_ptr: 0x80702,
90        shown_grob_ptr: 0x806DA,
91        pict_ptr: 0x806E4,
92        base_depth: 8,
93        outer_frame: 3,
94    };
95    /// 49G (ROM 2.10): the 48GX's addresses; one more entry at the bottom.
96    pub const HP49G: ScreenLayout = ScreenLayout {
97        base_depth: 9,
98        outer_frame: 4,
99        ..Self::HP48GX
100    };
101
102    /// The layout of `model`, or `None` where there is none (as
103    /// [`EditorLayout::of`]).
104    pub fn of(model: Model) -> Option<ScreenLayout> {
105        match model {
106            Model::Hp48sx => Some(Self::HP48SX),
107            Model::Hp48gx => Some(Self::HP48GX),
108            Model::Hp49g => Some(Self::HP49G),
109            Model::Hp38g
110            | Model::Hp39g
111            | Model::Hp40g
112            | Model::Hp42s
113            | Model::Hp27s
114            | Model::Hp17bii
115            | Model::Hp17b
116            | Model::Hp28s
117            | Model::Hp19bii => None,
118        }
119    }
120}
121
122/// What has the keyboard.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
124#[serde(rename_all = "camelCase")]
125pub enum Kind {
126    /// The stack, no command line open, no program halted.
127    Stack,
128    /// The stack with a command line open.
129    CommandLine,
130    /// The stack, with one or more programs halted (HALT, PROMPT).
131    Halted,
132    /// An input form (48GX, 49G).
133    InputForm,
134    /// A choose box.
135    ChooseBox,
136    /// A message box (MSGBOX, the 49G's error box).
137    Message,
138    /// The graphics screen (PICTURE, PVIEW, a plot).
139    Graphics,
140    /// Another loop reads the keys with the command line open: a program
141    /// waits for input (INPUT), or an editor uses the line (the 49G's
142    /// equation writer).
143    Input,
144    /// Another loop has the keyboard (the interactive stack, the equation
145    /// or matrix writer, WAIT), or the state could not be read.
146    Unknown,
147}
148
149/// Where a [`Screen::kind`] came from.
150#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
151#[serde(rename_all = "camelCase")]
152pub enum Source {
153    /// RAM.
154    Memory,
155    /// The display's dots.
156    Display,
157}
158
159/// The `uiState` protocol answer.
160#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
161#[serde(rename_all = "camelCase")]
162pub struct Screen {
163    /// What has the keyboard.
164    pub kind: Kind,
165    /// Where `kind` came from.
166    pub from: Source,
167    /// The stack's own loop reads the keys (also with a command line open
168    /// or a program halted).
169    pub stack_in_control: bool,
170    /// Programs halted (HALT, PROMPT), waiting for CONT or KILL.
171    pub halted: u32,
172    /// The editor's command line is open (at the stack, in a form's
173    /// field, or for INPUT).
174    pub command_line: bool,
175}
176
177fn field(mem: &dyn Memory, addr: u32, len: u32) -> Result<u32> {
178    let mut v = 0;
179    for i in (0..len).rev() {
180        v = (v << 4) | u32::from(mem.nibble(addr + i).ok_or(Error::Unreadable(addr + i))?);
181    }
182    Ok(v)
183}
184
185/// The return stack as the ROM left it.
186#[derive(Clone, Debug, PartialEq, Eq)]
187pub struct ReturnStack {
188    /// The entries, top (most recent) first.
189    pub entries: Vec<u32>,
190}
191
192impl ReturnStack {
193    /// Read it through `layout`; `None` when the pointers are not the
194    /// ROM's.
195    pub fn read(mem: &dyn Memory, layout: &ScreenLayout) -> Result<Option<ReturnStack>> {
196        let bottom = field(mem, layout.return_bottom_ptr, 5)?;
197        let top = field(mem, layout.return_top_ptr, 5)?;
198        if top <= bottom || (top - bottom) % 5 != 0 || (top - bottom) / 5 > MAX_DEPTH {
199            return Ok(None);
200        }
201        let depth = (top - bottom) / 5;
202        let mut entries = Vec::with_capacity(depth as usize);
203        for i in 1..=depth {
204            entries.push(field(mem, top - 5 * i, 5)?);
205        }
206        Ok(Some(ReturnStack { entries }))
207    }
208
209    /// Entry `i` from the top, 1-based.
210    fn top(&self, i: u32) -> Option<u32> {
211        self.entries.get(i.checked_sub(1)? as usize).copied()
212    }
213
214    /// Entry `j` from the bottom, 1-based.
215    fn bottom(&self, j: u32) -> Option<u32> {
216        let n = self.entries.len();
217        self.entries.get(n.checked_sub(j as usize)?).copied()
218    }
219
220    /// Whether the stack's outer loop reads the keys, and how many
221    /// programs are halted below it; `None` when the entries do not have
222    /// the outer loop's shape at the bottom.
223    pub fn control(&self, layout: &ScreenLayout) -> Option<(bool, u32)> {
224        if (self.entries.len() as u32) < layout.base_depth || self.bottom(1)? != 0 {
225            return None;
226        }
227        let outer = self.bottom(layout.outer_frame)?;
228        if outer == 0 {
229            return None;
230        }
231        let copies = self.entries.iter().filter(|&&e| e == outer).count() as u32;
232        let in_control = self.top(LOOP_FRAME)? == outer;
233        Some((in_control, copies.saturating_sub(1)))
234    }
235}
236
237/// The names of the temporaries bound in the newest environments, down to
238/// and including the first that binds `SavedUI` (the parameterized outer
239/// loop's saved state: what the innermost loop bound); only the newest
240/// environment when none does. Names lose a leading `'`.
241pub fn innermost_names(mem: &dyn Memory, layout: &ScreenLayout) -> Result<Vec<String>> {
242    let mut envs: Vec<Vec<String>> = Vec::new();
243    let mut a = field(mem, layout.environments_ptr, 5)?;
244    for _ in 0..MAX_ENVIRONMENTS {
245        let link = field(mem, a, 5)?;
246        if link == 0 {
247            break;
248        }
249        let mut names = Vec::new();
250        let mut p = a + 10;
251        let mut n = 0;
252        while p + 10 <= a + link && n < MAX_BINDINGS {
253            let name = field(mem, p, 5)?;
254            if let Ok(Object::LocalName { value }) = decode_at(name, mem) {
255                let value = value.trim_start_matches('\'');
256                if !value.is_empty() {
257                    names.push(value.to_string());
258                }
259            }
260            p += 10;
261            n += 1;
262        }
263        let saved = names.iter().any(|n| n == "SavedUI");
264        envs.push(names);
265        if saved {
266            return Ok(envs.concat());
267        }
268        a = a.wrapping_add(link) & 0xFFFFF;
269    }
270    Ok(envs.into_iter().next().unwrap_or_default())
271}
272
273/// A framed box with a two-dot shadow on the display (the 48GX's and the
274/// 49G's message and choose boxes): `Some(true)` when a row inside it is
275/// dark (a choose box's highlighted item or title rule), `Some(false)`
276/// without, `None` when there is no such box.
277pub fn framed_box(pixels: &[Vec<bool>]) -> Option<bool> {
278    let h = pixels.len();
279    let dark = |x: usize, y: usize| {
280        pixels
281            .get(y)
282            .and_then(|r| r.get(x))
283            .copied()
284            .unwrap_or(false)
285    };
286    for (y0, row) in pixels.iter().enumerate().take(h.saturating_sub(4)) {
287        let w = row.len();
288        let mut x = 0;
289        while x < w {
290            if !dark(x, y0) {
291                x += 1;
292                continue;
293            }
294            let x0 = x;
295            while x < w && dark(x, y0) {
296                x += 1;
297            }
298            let x1 = x - 1;
299            if x1 < x0 + 30 {
300                continue;
301            }
302            // The bottom border: the first row under the top whose dots
303            // run from the left edge to the shadow.
304            let Some(y1) =
305                (y0 + 3..h.saturating_sub(1)).find(|&y| (x0..=x1 + 1).all(|x| dark(x, y)))
306            else {
307                continue;
308            };
309            let edges = (y0 + 1..y1)
310                .all(|y| dark(x0, y) && dark(x1, y) && dark(x1 + 1, y) && !dark(x0 + 1, y));
311            let shadow = (x0 + 1..=x1 + 1).all(|x| dark(x, y1 + 1)) && !dark(x0, y1 + 1);
312            if !edges || !shadow {
313                continue;
314            }
315            let inner = (x0 + 2..=x1.saturating_sub(2)).count().max(1);
316            let banded = (y0 + 1..y1).any(|y| {
317                (x0 + 2..=x1.saturating_sub(2))
318                    .filter(|&x| dark(x, y))
319                    .count()
320                    * 10
321                    >= inner * 9
322            });
323            return Some(banded);
324        }
325    }
326    None
327}
328
329/// Read the screen state through the layouts, with `pixels` the display
330/// for the boxes RAM does not name.
331pub fn read(
332    mem: &dyn Memory,
333    layout: &ScreenLayout,
334    editor: &EditorLayout,
335    pixels: &[Vec<bool>],
336) -> Result<Screen> {
337    let command_line = cmdline::read(mem, editor)?.active;
338    let unknown = Screen {
339        kind: Kind::Unknown,
340        from: Source::Memory,
341        stack_in_control: false,
342        halted: 0,
343        command_line,
344    };
345    let Some(rs) = ReturnStack::read(mem, layout)? else {
346        return Ok(unknown);
347    };
348    let Some((in_control, halted)) = rs.control(layout) else {
349        return Ok(unknown);
350    };
351    let at = |kind, from| Screen {
352        kind,
353        from,
354        stack_in_control: in_control,
355        halted,
356        command_line,
357    };
358    if in_control {
359        let kind = if command_line {
360            Kind::CommandLine
361        } else if halted > 0 {
362            Kind::Halted
363        } else {
364            Kind::Stack
365        };
366        return Ok(at(kind, Source::Memory));
367    }
368    if field(mem, layout.shown_grob_ptr, 5)? == field(mem, layout.pict_ptr, 5)? {
369        return Ok(at(Kind::Graphics, Source::Memory));
370    }
371    let names = innermost_names(mem, layout)?;
372    let has = |n: &str| names.iter().any(|m| m == n);
373    if has("IF") || has("IfMenu") || has("CurrentField") {
374        return Ok(at(Kind::InputForm, Source::Memory));
375    }
376    if has("NbElm") {
377        return Ok(at(Kind::ChooseBox, Source::Memory));
378    }
379    if has("PlotEnv") {
380        return Ok(at(Kind::Graphics, Source::Memory));
381    }
382    match framed_box(pixels) {
383        Some(true) => return Ok(at(Kind::ChooseBox, Source::Display)),
384        Some(false) => return Ok(at(Kind::Message, Source::Display)),
385        None => {}
386    }
387    if command_line {
388        return Ok(at(Kind::Input, Source::Memory));
389    }
390    Ok(at(Kind::Unknown, Source::Memory))
391}
392
393/// The screen state of `machine` (48SX, 48GX, 49G).
394pub fn screen(machine: &Machine) -> Result<Screen> {
395    let model = machine.model();
396    let editor = EditorLayout::require(model)?;
397    let layout = ScreenLayout::of(model).ok_or(Error::NoCommandLine(model))?;
398    read(machine, &layout, &editor, &machine.lcd().pixels)
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use std::collections::HashMap;
405
406    #[derive(Default)]
407    struct Ram(HashMap<u32, u8>);
408
409    impl Memory for Ram {
410        fn nibble(&self, addr: u32) -> Option<u8> {
411            Some(self.0.get(&addr).copied().unwrap_or(0))
412        }
413    }
414
415    impl Ram {
416        fn put(&mut self, addr: u32, value: u32, len: u32) {
417            for i in 0..len {
418                self.0.insert(addr + i, ((value >> (4 * i)) & 15) as u8);
419            }
420        }
421
422        /// A return stack, bottom entry first, at #71000.
423        fn returns(&mut self, l: &ScreenLayout, bottom_first: &[u32]) {
424            self.put(l.return_bottom_ptr, 0x71000, 5);
425            for (i, e) in bottom_first.iter().enumerate() {
426                self.put(0x71000 + 5 * i as u32, *e, 5);
427            }
428            self.put(l.return_top_ptr, 0x71000 + 5 * bottom_first.len() as u32, 5);
429        }
430    }
431
432    const KEYS: [u32; 4] = [0x4204B, 0x420AF, 0x42190, 0x421C2];
433
434    fn stack(extra: &[u32]) -> Vec<u32> {
435        let mut v = vec![0, 0x38598, 0x385B6];
436        v.extend_from_slice(extra);
437        v.extend_from_slice(&KEYS);
438        v
439    }
440
441    fn read_sx(ram: &Ram) -> Screen {
442        read(ram, &ScreenLayout::HP48SX, &EditorLayout::HP48SX, &[]).unwrap()
443    }
444
445    #[test]
446    fn the_return_stack_tells_the_stack_from_a_loop_and_counts_halts() {
447        // The 48SX's return stack as observed (wiki: hardware/ui-state).
448        let l = ScreenLayout::HP48SX;
449        let mut ram = Ram::default();
450        ram.put(l.shown_grob_ptr, 0x86907, 5);
451        ram.put(l.pict_ptr, 0xCE017, 5);
452        ram.returns(&l, &stack(&[0x385CA]));
453        let s = read_sx(&ram);
454        assert_eq!(
455            (s.kind, s.stack_in_control, s.halted),
456            (Kind::Stack, true, 0)
457        );
458        // A halted program: its frames, then a new outer loop.
459        ram.returns(
460            &l,
461            &stack(&[0x385D4, 0x3A807, 0x40BC4, 0x143DE, 0x385B6, 0x385CA]),
462        );
463        let s = read_sx(&ram);
464        assert_eq!(
465            (s.kind, s.stack_in_control, s.halted),
466            (Kind::Halted, true, 1)
467        );
468        // A parameterized outer loop (the interactive stack): no names, no box.
469        ram.returns(
470            &l,
471            &stack(&[
472                0x385D4, 0x40DBB, 0x43757, 0x4374D, 0x38AF5, 0x38B3B, 0x38B22,
473            ]),
474        );
475        let s = read_sx(&ram);
476        assert_eq!((s.kind, s.stack_in_control), (Kind::Unknown, false));
477        // With the command line open in that loop: INPUT.
478        ram.put(EditorLayout::HP48SX.line, 2, 1);
479        ram.put(EditorLayout::HP48SX.text_end_ptr, 0x7FEE3, 5);
480        ram.put(EditorLayout::HP48SX.stack_end_ptr, 0x7FEB3, 5);
481        assert_eq!(read_sx(&ram).kind, Kind::Input);
482        // The graphics screen: the shown GROB is PICT.
483        ram.put(l.shown_grob_ptr, 0xCE017, 5);
484        assert_eq!(read_sx(&ram).kind, Kind::Graphics);
485        // Pointers that are not the ROM's.
486        ram.put(l.return_top_ptr, 0x70FFF, 5);
487        assert_eq!(read_sx(&ram).kind, Kind::Unknown);
488    }
489
490    #[test]
491    fn named_temporaries_name_the_loop() {
492        let l = ScreenLayout::HP48GX;
493        let mut ram = Ram::default();
494        ram.returns(&l, &stack(&[0x385D4, 0x38AF5, 0x38B3B, 0x38B22]));
495        ram.put(l.shown_grob_ptr, 1, 5);
496        // Local names: prolog #02E6D, length, characters.
497        let lam = |ram: &mut Ram, at: u32, name: &str| {
498            ram.put(at, 0x02E6D, 5);
499            ram.put(at + 5, name.len() as u32, 2);
500            for (i, b) in name.bytes().enumerate() {
501                ram.put(at + 7 + 2 * i as u32, u32::from(b), 2);
502            }
503        };
504        lam(&mut ram, 0x90000, "IF");
505        lam(&mut ram, 0x90100, "SavedUI");
506        // Two environments of one binding each, then the end.
507        ram.put(l.environments_ptr, 0xA0000, 5);
508        ram.put(0xA0000, 20, 5);
509        ram.put(0xA000A, 0x90000, 5);
510        ram.put(0xA0014, 20, 5);
511        ram.put(0xA001E, 0x90100, 5);
512        assert_eq!(innermost_names(&ram, &l).unwrap(), ["IF", "SavedUI"]);
513        let s = read(&ram, &l, &EditorLayout::HP48GX, &[]).unwrap();
514        assert_eq!((s.kind, s.from), (Kind::InputForm, Source::Memory));
515        // A box opened over the form binds its own environment above:
516        // the form's names are below its SavedUI.
517        ram.put(l.environments_ptr, 0xA1000, 5);
518        ram.put(0xA1000, 20, 5);
519        ram.put(0xA100A, 0x90100, 5);
520        ram.put(0xA1014, 0, 5);
521        assert_eq!(innermost_names(&ram, &l).unwrap(), ["SavedUI"]);
522    }
523
524    fn boxed(band: bool) -> Vec<Vec<bool>> {
525        let mut p = vec![vec![false; 131]; 64];
526        let (x0, x1, y0, y1) = (17, 112, 15, 44);
527        p[y0][x0..=x1].fill(true);
528        for row in p.iter_mut().take(y1).skip(y0 + 1) {
529            row[x0] = true;
530            row[x1] = true;
531            row[x1 + 1] = true;
532        }
533        p[y1][x0..=x1 + 1].fill(true);
534        p[y1 + 1][x0 + 1..=x1 + 1].fill(true);
535        if band {
536            p[20][x0 + 2..x1 - 1].fill(true);
537        }
538        p
539    }
540
541    #[test]
542    fn a_shadowed_frame_is_a_box() {
543        assert_eq!(framed_box(&boxed(false)), Some(false));
544        assert_eq!(framed_box(&boxed(true)), Some(true));
545        assert_eq!(framed_box(&vec![vec![false; 131]; 64]), None);
546        // A filled area is not a frame.
547        assert_eq!(framed_box(&vec![vec![true; 131]; 64]), None);
548    }
549}