1#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct Timing {
20 pub dot_clock_hz: f64,
22 pub h_total: u32,
23 pub h_active: u32,
24 pub h_blank_start: u32,
25 pub h_blank_end: u32,
26 pub h_retrace_start: u32,
27 pub h_retrace_end: u32,
28 pub v_total: u32,
29 pub v_active: u32,
30 pub v_blank_start: u32,
31 pub v_blank_end: u32,
32 pub v_retrace_start: u32,
33 pub v_retrace_end: u32,
34}
35
36impl Timing {
37 pub fn normalized(mut self) -> Self {
39 self.h_total = self.h_total.max(8);
40 self.h_active = self.h_active.clamp(1, self.h_total);
41 self.h_blank_start = self.h_blank_start.clamp(self.h_active, self.h_total);
42 self.h_blank_end = self.h_blank_end.clamp(self.h_blank_start, self.h_total);
43 self.h_retrace_start = self.h_retrace_start.min(self.h_total);
44 self.h_retrace_end = self.h_retrace_end.clamp(self.h_retrace_start, self.h_total);
45 self.v_total = self.v_total.max(2);
46 self.v_active = self.v_active.clamp(1, self.v_total);
47 self.v_blank_start = self.v_blank_start.clamp(self.v_active, self.v_total);
48 self.v_blank_end = self.v_blank_end.clamp(self.v_blank_start, self.v_total);
49 self.v_retrace_start = self.v_retrace_start.min(self.v_total);
50 self.v_retrace_end = self.v_retrace_end.clamp(self.v_retrace_start, self.v_total);
51 if self.dot_clock_hz.is_nan() || self.dot_clock_hz <= 1.0 {
52 self.dot_clock_hz = 1.0;
53 }
54 self
55 }
56
57 pub fn line_rate_hz(&self) -> f64 {
59 self.dot_clock_hz / self.h_total as f64
60 }
61
62 pub fn refresh_hz(&self) -> f64 {
64 self.line_rate_hz() / self.v_total as f64
65 }
66
67 pub fn line_ns(&self) -> f64 {
69 1e9 / self.line_rate_hz()
70 }
71
72 pub fn frame_ns(&self) -> f64 {
74 1e9 / self.refresh_hz()
75 }
76
77 pub(crate) fn frame_dots(&self) -> u64 {
78 self.h_total as u64 * self.v_total as u64
79 }
80
81 fn left_border(&self) -> u32 {
82 self.h_total - self.h_blank_end
83 }
84
85 fn top_border(&self) -> u32 {
86 self.v_total - self.v_blank_end
87 }
88
89 pub fn visible_size(&self) -> (u32, u32) {
91 (
92 self.left_border() + self.h_blank_start,
93 self.top_border() + self.v_blank_start,
94 )
95 }
96}
97
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub struct Beam {
101 pub frame: u64,
103 pub line: u32,
105 pub dot: u32,
107 pub display_active: bool,
109 pub h_retrace: bool,
110 pub v_retrace: bool,
111 pub h_blank: bool,
112 pub v_blank: bool,
113}
114
115pub(crate) trait Scanout {
117 fn frame_start(&mut self) {}
119 fn line_start(&mut self, _line: u32) {}
121 fn vretrace_start(&mut self) {}
123 fn render_active(&mut self, line: u32, x0: u32, out: &mut [[u8; 4]]);
125 fn border(&self) -> [u8; 4];
127}
128
129#[derive(Clone, Copy, Debug)]
131pub struct FrameView<'a> {
132 pub width: u32,
134 pub height: u32,
135 pub rgba: &'a [u8],
137 pub active: [u32; 4],
139 pub number: u64,
141}
142
143pub(crate) struct Crt {
144 timing: Timing,
145 dots_per_ns: f64,
146 epoch_ns: f64,
148 painted: u64,
150 back: Vec<[u8; 4]>,
151 front: Vec<[u8; 4]>,
152 width: u32,
153 height: u32,
154 frames: u64,
155}
156
157impl Crt {
158 pub fn new(timing: Timing, now_ns: f64) -> Self {
159 let timing = timing.normalized();
160 let (width, height) = timing.visible_size();
161 let n = (width * height) as usize;
162 Self {
163 dots_per_ns: timing.dot_clock_hz / 1e9,
164 timing,
165 epoch_ns: now_ns,
166 painted: 0,
167 back: vec![[0, 0, 0, 255]; n],
168 front: vec![[0, 0, 0, 255]; n],
169 width,
170 height,
171 frames: 0,
172 }
173 }
174
175 pub fn timing(&self) -> &Timing {
176 &self.timing
177 }
178
179 pub fn dot_at(&self, now_ns: f64) -> u64 {
181 let d = (now_ns - self.epoch_ns) * self.dots_per_ns;
182 if d <= 0.0 { 0 } else { d as u64 }
183 }
184
185 pub fn time_of(&self, dot: u64) -> f64 {
187 self.epoch_ns + dot as f64 / self.dots_per_ns
188 }
189
190 pub fn beam_at(&self, now_ns: f64) -> Beam {
191 self.beam_of(self.dot_at(now_ns))
192 }
193
194 pub fn beam_of(&self, g: u64) -> Beam {
195 let t = &self.timing;
196 let fd = t.frame_dots();
197 let in_frame = g % fd;
198 let line = (in_frame / t.h_total as u64) as u32;
199 let dot = (in_frame % t.h_total as u64) as u32;
200 Beam {
201 frame: g / fd,
202 line,
203 dot,
204 display_active: line < t.v_active && dot < t.h_active,
205 h_retrace: dot >= t.h_retrace_start && dot < t.h_retrace_end,
206 v_retrace: line >= t.v_retrace_start && line < t.v_retrace_end,
207 h_blank: dot >= t.h_blank_start && dot < t.h_blank_end,
208 v_blank: line >= t.v_blank_start && line < t.v_blank_end,
209 }
210 }
211
212 pub fn next_dot(&self, from: u64, line: u32, dot: u32) -> u64 {
214 let t = &self.timing;
215 let fd = t.frame_dots();
216 let target =
217 line.min(t.v_total - 1) as u64 * t.h_total as u64 + dot.min(t.h_total - 1) as u64;
218 let base = from - from % fd;
219 let cand = base + target;
220 if cand >= from { cand } else { cand + fd }
221 }
222
223 pub fn retime<C: Scanout>(&mut self, card: &mut C, timing: Timing, now_ns: f64) {
225 let timing = timing.normalized();
226 if timing == self.timing {
227 return;
228 }
229 self.advance(card, self.dot_at(now_ns));
230 let b = self.beam_at(now_ns);
231 let line = b.line.min(timing.v_total - 1);
232 let dot = b.dot.min(timing.h_total - 1);
233 self.timing = timing;
234 self.dots_per_ns = timing.dot_clock_hz / 1e9;
235 let g = line as u64 * timing.h_total as u64 + dot as u64;
236 self.epoch_ns = now_ns - g as f64 / self.dots_per_ns;
237 self.painted = g;
238 let (w, h) = timing.visible_size();
239 if (w, h) != (self.width, self.height) {
240 self.width = w;
241 self.height = h;
242 let n = (w * h) as usize;
243 self.back = vec![[0, 0, 0, 255]; n];
244 self.front = vec![[0, 0, 0, 255]; n];
245 }
246 }
247
248 pub fn reset<C: Scanout>(&mut self, card: &mut C, timing: Timing, now_ns: f64) {
250 let frames = self.frames;
251 *self = Crt::new(timing, now_ns);
252 self.frames = frames;
253 let _ = card;
254 }
255
256 pub fn frame(&self) -> FrameView<'_> {
257 let t = &self.timing;
258 FrameView {
259 width: self.width,
260 height: self.height,
261 rgba: self.front.as_flattened(),
262 active: [t.left_border(), t.top_border(), t.h_active, t.v_active],
263 number: self.frames,
264 }
265 }
266
267 pub fn live(&self) -> FrameView<'_> {
269 let mut f = self.frame();
270 f.rgba = self.back.as_flattened();
271 f
272 }
273
274 pub fn frames_completed(&self) -> u64 {
275 self.frames
276 }
277
278 pub fn advance<C: Scanout>(&mut self, card: &mut C, target: u64) {
280 if target <= self.painted {
281 return;
282 }
283 let t = self.timing;
284 let fd = t.frame_dots();
285 if target - self.painted > 2 * fd {
287 let start = (target / fd).saturating_sub(1) * fd;
288 if start > self.painted {
289 self.painted = start;
290 }
291 }
292 let ht = t.h_total as u64;
293 while self.painted < target {
294 let in_frame = self.painted % fd;
295 let line = (in_frame / ht) as u32;
296 let x = (in_frame % ht) as u32;
297 if x == 0 {
298 if line == 0 {
299 card.frame_start();
300 }
301 card.line_start(line);
302 if line == t.v_retrace_start {
303 card.vretrace_start();
304 }
305 if line == t.v_blank_start {
306 std::mem::swap(&mut self.back, &mut self.front);
307 self.frames += 1;
308 }
309 }
310 let x1 = (x as u64 + (target - self.painted)).min(ht) as u32;
311 self.paint_segment(card, line, x, x1);
312 self.painted += (x1 - x) as u64;
313 }
314 }
315
316 fn row_of_line(&self, line: u32) -> Option<u32> {
317 let t = &self.timing;
318 if line < t.v_blank_start {
319 Some(t.top_border() + line)
320 } else if line >= t.v_blank_end {
321 Some(line - t.v_blank_end)
322 } else {
323 None
324 }
325 }
326
327 fn paint_segment<C: Scanout>(&mut self, card: &mut C, line: u32, x0: u32, x1: u32) {
328 let t = self.timing;
329 let w = self.width as usize;
330 let lb = t.left_border();
331 if x0 < t.h_blank_start
333 && let Some(row) = self.row_of_line(line)
334 {
335 let row_off = row as usize * w + lb as usize;
336 let a0 = x0;
337 let a1 = x1.min(t.h_blank_start);
338 if line < t.v_active {
339 let e = a1.min(t.h_active);
340 if a0 < e {
341 let out = &mut self.back[row_off + a0 as usize..row_off + e as usize];
342 card.render_active(line, a0, out);
343 }
344 let b0 = a0.max(t.h_active);
345 if b0 < a1 {
346 let c = card.border();
347 self.back[row_off + b0 as usize..row_off + a1 as usize].fill(c);
348 }
349 } else if a0 < a1 {
350 let c = card.border();
351 self.back[row_off + a0 as usize..row_off + a1 as usize].fill(c);
352 }
353 }
354 if x1 > t.h_blank_end {
356 let next = (line + 1) % t.v_total;
357 if let Some(row) = self.row_of_line(next) {
358 let s = x0.max(t.h_blank_end) - t.h_blank_end;
359 let e = x1 - t.h_blank_end;
360 let off = row as usize * w;
361 let c = card.border();
362 self.back[off + s as usize..off + e as usize].fill(c);
363 }
364 }
365 }
366}