#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Timing {
pub dot_clock_hz: f64,
pub h_total: u32,
pub h_active: u32,
pub h_blank_start: u32,
pub h_blank_end: u32,
pub h_retrace_start: u32,
pub h_retrace_end: u32,
pub v_total: u32,
pub v_active: u32,
pub v_blank_start: u32,
pub v_blank_end: u32,
pub v_retrace_start: u32,
pub v_retrace_end: u32,
}
impl Timing {
pub fn normalized(mut self) -> Self {
self.h_total = self.h_total.max(8);
self.h_active = self.h_active.clamp(1, self.h_total);
self.h_blank_start = self.h_blank_start.clamp(self.h_active, self.h_total);
self.h_blank_end = self.h_blank_end.clamp(self.h_blank_start, self.h_total);
self.h_retrace_start = self.h_retrace_start.min(self.h_total);
self.h_retrace_end = self.h_retrace_end.clamp(self.h_retrace_start, self.h_total);
self.v_total = self.v_total.max(2);
self.v_active = self.v_active.clamp(1, self.v_total);
self.v_blank_start = self.v_blank_start.clamp(self.v_active, self.v_total);
self.v_blank_end = self.v_blank_end.clamp(self.v_blank_start, self.v_total);
self.v_retrace_start = self.v_retrace_start.min(self.v_total);
self.v_retrace_end = self.v_retrace_end.clamp(self.v_retrace_start, self.v_total);
if self.dot_clock_hz.is_nan() || self.dot_clock_hz <= 1.0 {
self.dot_clock_hz = 1.0;
}
self
}
pub fn line_rate_hz(&self) -> f64 {
self.dot_clock_hz / self.h_total as f64
}
pub fn refresh_hz(&self) -> f64 {
self.line_rate_hz() / self.v_total as f64
}
pub fn line_ns(&self) -> f64 {
1e9 / self.line_rate_hz()
}
pub fn frame_ns(&self) -> f64 {
1e9 / self.refresh_hz()
}
pub(crate) fn frame_dots(&self) -> u64 {
self.h_total as u64 * self.v_total as u64
}
fn left_border(&self) -> u32 {
self.h_total - self.h_blank_end
}
fn top_border(&self) -> u32 {
self.v_total - self.v_blank_end
}
pub fn visible_size(&self) -> (u32, u32) {
(
self.left_border() + self.h_blank_start,
self.top_border() + self.v_blank_start,
)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Beam {
pub frame: u64,
pub line: u32,
pub dot: u32,
pub display_active: bool,
pub h_retrace: bool,
pub v_retrace: bool,
pub h_blank: bool,
pub v_blank: bool,
}
pub(crate) trait Scanout {
fn frame_start(&mut self) {}
fn line_start(&mut self, _line: u32) {}
fn vretrace_start(&mut self) {}
fn render_active(&mut self, line: u32, x0: u32, out: &mut [[u8; 4]]);
fn border(&self) -> [u8; 4];
}
#[derive(Clone, Copy, Debug)]
pub struct FrameView<'a> {
pub width: u32,
pub height: u32,
pub rgba: &'a [u8],
pub active: [u32; 4],
pub number: u64,
}
pub(crate) struct Crt {
timing: Timing,
dots_per_ns: f64,
epoch_ns: f64,
painted: u64,
back: Vec<[u8; 4]>,
front: Vec<[u8; 4]>,
width: u32,
height: u32,
frames: u64,
}
impl Crt {
pub fn new(timing: Timing, now_ns: f64) -> Self {
let timing = timing.normalized();
let (width, height) = timing.visible_size();
let n = (width * height) as usize;
Self {
dots_per_ns: timing.dot_clock_hz / 1e9,
timing,
epoch_ns: now_ns,
painted: 0,
back: vec![[0, 0, 0, 255]; n],
front: vec![[0, 0, 0, 255]; n],
width,
height,
frames: 0,
}
}
pub fn timing(&self) -> &Timing {
&self.timing
}
pub fn dot_at(&self, now_ns: f64) -> u64 {
let d = (now_ns - self.epoch_ns) * self.dots_per_ns;
if d <= 0.0 { 0 } else { d as u64 }
}
pub fn time_of(&self, dot: u64) -> f64 {
self.epoch_ns + dot as f64 / self.dots_per_ns
}
pub fn beam_at(&self, now_ns: f64) -> Beam {
self.beam_of(self.dot_at(now_ns))
}
pub fn beam_of(&self, g: u64) -> Beam {
let t = &self.timing;
let fd = t.frame_dots();
let in_frame = g % fd;
let line = (in_frame / t.h_total as u64) as u32;
let dot = (in_frame % t.h_total as u64) as u32;
Beam {
frame: g / fd,
line,
dot,
display_active: line < t.v_active && dot < t.h_active,
h_retrace: dot >= t.h_retrace_start && dot < t.h_retrace_end,
v_retrace: line >= t.v_retrace_start && line < t.v_retrace_end,
h_blank: dot >= t.h_blank_start && dot < t.h_blank_end,
v_blank: line >= t.v_blank_start && line < t.v_blank_end,
}
}
pub fn next_dot(&self, from: u64, line: u32, dot: u32) -> u64 {
let t = &self.timing;
let fd = t.frame_dots();
let target =
line.min(t.v_total - 1) as u64 * t.h_total as u64 + dot.min(t.h_total - 1) as u64;
let base = from - from % fd;
let cand = base + target;
if cand >= from { cand } else { cand + fd }
}
pub fn retime<C: Scanout>(&mut self, card: &mut C, timing: Timing, now_ns: f64) {
let timing = timing.normalized();
if timing == self.timing {
return;
}
self.advance(card, self.dot_at(now_ns));
let b = self.beam_at(now_ns);
let line = b.line.min(timing.v_total - 1);
let dot = b.dot.min(timing.h_total - 1);
self.timing = timing;
self.dots_per_ns = timing.dot_clock_hz / 1e9;
let g = line as u64 * timing.h_total as u64 + dot as u64;
self.epoch_ns = now_ns - g as f64 / self.dots_per_ns;
self.painted = g;
let (w, h) = timing.visible_size();
if (w, h) != (self.width, self.height) {
self.width = w;
self.height = h;
let n = (w * h) as usize;
self.back = vec![[0, 0, 0, 255]; n];
self.front = vec![[0, 0, 0, 255]; n];
}
}
pub fn reset<C: Scanout>(&mut self, card: &mut C, timing: Timing, now_ns: f64) {
let frames = self.frames;
*self = Crt::new(timing, now_ns);
self.frames = frames;
let _ = card;
}
pub fn frame(&self) -> FrameView<'_> {
let t = &self.timing;
FrameView {
width: self.width,
height: self.height,
rgba: self.front.as_flattened(),
active: [t.left_border(), t.top_border(), t.h_active, t.v_active],
number: self.frames,
}
}
pub fn live(&self) -> FrameView<'_> {
let mut f = self.frame();
f.rgba = self.back.as_flattened();
f
}
pub fn frames_completed(&self) -> u64 {
self.frames
}
pub fn advance<C: Scanout>(&mut self, card: &mut C, target: u64) {
if target <= self.painted {
return;
}
let t = self.timing;
let fd = t.frame_dots();
if target - self.painted > 2 * fd {
let start = (target / fd).saturating_sub(1) * fd;
if start > self.painted {
self.painted = start;
}
}
let ht = t.h_total as u64;
while self.painted < target {
let in_frame = self.painted % fd;
let line = (in_frame / ht) as u32;
let x = (in_frame % ht) as u32;
if x == 0 {
if line == 0 {
card.frame_start();
}
card.line_start(line);
if line == t.v_retrace_start {
card.vretrace_start();
}
if line == t.v_blank_start {
std::mem::swap(&mut self.back, &mut self.front);
self.frames += 1;
}
}
let x1 = (x as u64 + (target - self.painted)).min(ht) as u32;
self.paint_segment(card, line, x, x1);
self.painted += (x1 - x) as u64;
}
}
fn row_of_line(&self, line: u32) -> Option<u32> {
let t = &self.timing;
if line < t.v_blank_start {
Some(t.top_border() + line)
} else if line >= t.v_blank_end {
Some(line - t.v_blank_end)
} else {
None
}
}
fn paint_segment<C: Scanout>(&mut self, card: &mut C, line: u32, x0: u32, x1: u32) {
let t = self.timing;
let w = self.width as usize;
let lb = t.left_border();
if x0 < t.h_blank_start
&& let Some(row) = self.row_of_line(line)
{
let row_off = row as usize * w + lb as usize;
let a0 = x0;
let a1 = x1.min(t.h_blank_start);
if line < t.v_active {
let e = a1.min(t.h_active);
if a0 < e {
let out = &mut self.back[row_off + a0 as usize..row_off + e as usize];
card.render_active(line, a0, out);
}
let b0 = a0.max(t.h_active);
if b0 < a1 {
let c = card.border();
self.back[row_off + b0 as usize..row_off + a1 as usize].fill(c);
}
} else if a0 < a1 {
let c = card.border();
self.back[row_off + a0 as usize..row_off + a1 as usize].fill(c);
}
}
if x1 > t.h_blank_end {
let next = (line + 1) % t.v_total;
if let Some(row) = self.row_of_line(next) {
let s = x0.max(t.h_blank_end) - t.h_blank_end;
let e = x1 - t.h_blank_end;
let off = row as usize * w;
let c = card.border();
self.back[off + s as usize..off + e as usize].fill(c);
}
}
}
}