1use 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
38const MAX_DEPTH: u32 = 4096;
41const MAX_ENVIRONMENTS: usize = 64;
43const MAX_BINDINGS: u32 = 512;
45const KEY_WAIT: u32 = 4;
48const LOOP_FRAME: u32 = KEY_WAIT + 2;
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub struct ScreenLayout {
56 pub return_bottom_ptr: u32,
59 pub return_top_ptr: u32,
61 pub environments_ptr: u32,
63 pub shown_grob_ptr: u32,
65 pub pict_ptr: u32,
67 pub base_depth: u32,
69 pub outer_frame: u32,
72}
73
74impl ScreenLayout {
75 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 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 pub const HP49G: ScreenLayout = ScreenLayout {
97 base_depth: 9,
98 outer_frame: 4,
99 ..Self::HP48GX
100 };
101
102 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
124#[serde(rename_all = "camelCase")]
125pub enum Kind {
126 Stack,
128 CommandLine,
130 Halted,
132 InputForm,
134 ChooseBox,
136 Message,
138 Graphics,
140 Input,
144 Unknown,
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
151#[serde(rename_all = "camelCase")]
152pub enum Source {
153 Memory,
155 Display,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
161#[serde(rename_all = "camelCase")]
162pub struct Screen {
163 pub kind: Kind,
165 pub from: Source,
167 pub stack_in_control: bool,
170 pub halted: u32,
172 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#[derive(Clone, Debug, PartialEq, Eq)]
187pub struct ReturnStack {
188 pub entries: Vec<u32>,
190}
191
192impl ReturnStack {
193 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 fn top(&self, i: u32) -> Option<u32> {
211 self.entries.get(i.checked_sub(1)? as usize).copied()
212 }
213
214 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 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
237pub 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
273pub 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 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
329pub 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
393pub 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 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 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 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 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 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 ram.put(l.shown_grob_ptr, 0xCE017, 5);
484 assert_eq!(read_sx(&ram).kind, Kind::Graphics);
485 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 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 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 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 assert_eq!(framed_box(&vec![vec![true; 131]; 64]), None);
548 }
549}