Skip to main content

game_toolkit_dosvideo/
machine.rs

1//! The machine: a clock, a bus, one display adapter and the bits of a PC a game talks to.
2
3use crate::cga::{Cga, CgaKind, CgaMonitor};
4use crate::crt::{Beam, Crt, FrameView, Scanout, Timing};
5use crate::hercules::Hercules;
6use crate::input::{Keyboard, Mouse};
7use crate::palette::Phosphor;
8use crate::speaker::PcSpeaker;
9use crate::svga::{Svga, VbeMode};
10use crate::vga::{Vga, VgaKind};
11use game_toolkit_dosbus::{Channel, Tick};
12
13/// Which display adapter is installed.
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub enum Adapter {
16    /// IBM VGA (1987): mode 13h, Mode X, 16-color planar modes, text.
17    Vga,
18    /// IBM EGA (1984) with 256 KiB.
19    Ega,
20    /// IBM CGA (1981).
21    Cga(CgaMonitor),
22    /// Tandy 1000 / PCjr graphics (1983/84).
23    Tandy,
24    /// Hercules Graphics Card (1982) on a monochrome monitor.
25    Hercules(Phosphor),
26    /// A VESA VBE 2.0 SVGA card with this much video memory.
27    Svga { memory_kib: u32 },
28}
29
30/// How long the CPU waits for the video card on each access, in nanoseconds.
31///
32/// Video memory on the ISA bus was *slow*, and how slow decided whether a full-screen copy fit
33/// into the vertical blank or tore. Every [`Dos::poke`] / [`Dos::outb`] advances the clock by
34/// these amounts.
35#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct BusTiming {
37    /// One 8-bit video memory access.
38    pub mem8_ns: f64,
39    /// One 16-bit access (two 8-bit cycles on an 8-bit bus).
40    pub mem16_ns: f64,
41    /// One 32-bit access.
42    pub mem32_ns: f64,
43    /// One I/O port access.
44    pub io_ns: f64,
45}
46
47impl BusTiming {
48    /// Free: video memory as fast as system RAM. Tearing then only comes from *when* you draw.
49    /// Port accesses still cost a little ([`MIN_IO_NS`]), so status polling loops terminate.
50    pub const INSTANT: Self = Self {
51        mem8_ns: 0.0,
52        mem16_ns: 0.0,
53        mem32_ns: 0.0,
54        io_ns: MIN_IO_NS,
55    };
56    /// An 8-bit card in a 4.77 MHz PC/XT (CGA, Hercules, early EGA): about 1 MB/s.
57    pub const ISA8: Self = Self {
58        mem8_ns: 1000.0,
59        mem16_ns: 2000.0,
60        mem32_ns: 4000.0,
61        io_ns: 1000.0,
62    };
63    /// A 16-bit ISA VGA in a 286/386: 2-4 MB/s, a full mode 13h frame takes longer than the
64    /// vertical blank.
65    pub const ISA16: Self = Self {
66        mem8_ns: 500.0,
67        mem16_ns: 500.0,
68        mem32_ns: 1000.0,
69        io_ns: 800.0,
70    };
71    /// A VESA Local Bus card in a 486: ~30 MB/s.
72    pub const VLB: Self = Self {
73        mem8_ns: 100.0,
74        mem16_ns: 100.0,
75        mem32_ns: 120.0,
76        io_ns: 300.0,
77    };
78    /// A PCI card in a Pentium: ~60 MB/s.
79    pub const PCI: Self = Self {
80        mem8_ns: 50.0,
81        mem16_ns: 50.0,
82        mem32_ns: 60.0,
83        io_ns: 200.0,
84    };
85}
86
87/// The least an I/O port access costs, whatever the [`BusTiming`] says. Without it a
88/// `while (inp(0x3DA) & 8)` loop would never see time pass.
89pub const MIN_IO_NS: f64 = 100.0;
90
91/// Common CPU clock rates, for [`Dos::cpu`].
92pub mod cpu {
93    /// IBM PC / XT, 8088.
94    pub const XT_8088: f64 = 4_772_727.0;
95    /// IBM AT, 286.
96    pub const AT_286: f64 = 8_000_000.0;
97    pub const I386DX_33: f64 = 33_000_000.0;
98    pub const I486DX2_66: f64 = 66_000_000.0;
99    pub const PENTIUM_100: f64 = 100_000_000.0;
100}
101
102/// Machine configuration.
103#[derive(Clone, Copy, Debug)]
104pub struct MachineConfig {
105    pub adapter: Adapter,
106    pub bus: BusTiming,
107    /// CPU clock in Hz, for [`Dos::cpu`].
108    pub cpu_hz: f64,
109    /// Snow on a CGA in 80-column text when touching memory during display.
110    pub cga_snow: bool,
111}
112
113impl Default for MachineConfig {
114    fn default() -> Self {
115        Self {
116            adapter: Adapter::Vga,
117            bus: BusTiming::VLB,
118            cpu_hz: cpu::I486DX2_66,
119            cga_snow: true,
120        }
121    }
122}
123
124impl MachineConfig {
125    pub fn new(adapter: Adapter) -> Self {
126        let mut c = Self {
127            adapter,
128            ..Default::default()
129        };
130        if matches!(
131            adapter,
132            Adapter::Cga(_) | Adapter::Tandy | Adapter::Hercules(_)
133        ) {
134            c.bus = BusTiming::ISA8;
135            c.cpu_hz = cpu::XT_8088;
136        }
137        if adapter == Adapter::Ega {
138            c.bus = BusTiming::ISA16;
139            c.cpu_hz = cpu::AT_286;
140        }
141        c
142    }
143
144    pub fn with_bus(mut self, bus: BusTiming) -> Self {
145        self.bus = bus;
146        self
147    }
148
149    pub fn with_cpu(mut self, hz: f64) -> Self {
150        self.cpu_hz = hz;
151        self
152    }
153}
154
155/// The installed adapter, for read-only inspection.
156pub enum Video {
157    Vga(Box<Vga>),
158    Cga(Box<Cga>),
159    Hercules(Box<Hercules>),
160    Svga(Box<Svga>),
161}
162
163macro_rules! with_card {
164    ($video:expr, $c:ident => $e:expr) => {
165        match $video {
166            Video::Vga($c) => $e,
167            Video::Cga($c) => $e,
168            Video::Hercules($c) => $e,
169            Video::Svga($c) => $e,
170        }
171    };
172}
173
174impl Video {
175    fn timing(&mut self) -> Timing {
176        with_card!(self, c => c.timing())
177    }
178
179    fn take_dirty(&mut self) -> bool {
180        match self {
181            Video::Vga(c) => std::mem::take(&mut c.timing_dirty),
182            Video::Cga(c) => std::mem::take(&mut c.timing_dirty),
183            Video::Hercules(c) => std::mem::take(&mut c.timing_dirty),
184            Video::Svga(c) => c.take_dirty(),
185        }
186    }
187
188    /// Pixel resolution of the current mode.
189    pub fn resolution(&self) -> (u32, u32) {
190        with_card!(self, c => c.logical_resolution())
191    }
192
193    /// `(columns, rows, scanlines per row)` in a text mode.
194    pub fn text_geometry(&self) -> Option<(u32, u32, u32)> {
195        match self {
196            Video::Vga(c) => c.text_geometry(),
197            Video::Cga(c) => c.text_geometry(),
198            Video::Hercules(c) => c.text_geometry(),
199            Video::Svga(c) => {
200                if c.vbe.is_some() {
201                    None
202                } else {
203                    c.vga.text_geometry()
204                }
205            }
206        }
207    }
208
209    pub fn as_vga(&self) -> Option<&Vga> {
210        match self {
211            Video::Vga(v) => Some(v),
212            Video::Svga(s) => Some(&s.vga),
213            _ => None,
214        }
215    }
216
217    pub fn as_cga(&self) -> Option<&Cga> {
218        match self {
219            Video::Cga(c) => Some(c),
220            _ => None,
221        }
222    }
223
224    pub fn as_hercules(&self) -> Option<&Hercules> {
225        match self {
226            Video::Hercules(h) => Some(h),
227            _ => None,
228        }
229    }
230
231    pub fn as_svga(&self) -> Option<&Svga> {
232        match self {
233            Video::Svga(s) => Some(s),
234            _ => None,
235        }
236    }
237
238    fn bios_mode(&self) -> u8 {
239        match self {
240            Video::Vga(v) => v.bios_mode,
241            Video::Cga(c) => c.bios_mode,
242            Video::Hercules(h) => h.bios_mode,
243            Video::Svga(s) => s.vga.bios_mode,
244        }
245    }
246}
247
248/// 1.193182 MHz: the PC crystal (315/22 MHz) divided by 12.
249pub const PIT_HZ: f64 = Tick::HZ_NUMERATOR as f64 / (Tick::HZ_DENOMINATOR * 12) as f64;
250
251/// A DOS PC with one display adapter.
252///
253/// All interaction goes through the clock: port and memory accesses cost bus time, [`Dos::cpu`]
254/// charges CPU cycles, and the `wait_*` functions busy-wait (in zero real time) until the beam
255/// gets somewhere. The picture is painted as the beam passes, so *when* you change something
256/// decides where on screen the change appears.
257pub struct Dos {
258    now: f64,
259    crt: Crt,
260    video: Video,
261    bus: BusTiming,
262    cpu_hz: f64,
263    keyboard: Keyboard,
264    mouse: Mouse,
265    /// Timer interrupts counted up to `timer_taken` and not yet taken.
266    timer_pending: u64,
267    /// Machine time up to which timer interrupts are counted.
268    timer_taken: Tick,
269    exit: bool,
270    adapter: Adapter,
271    /// BIOS teletype cursor (column, row).
272    pub(crate) tty: (u32, u32),
273    pub(crate) speaker: PcSpeaker,
274}
275
276impl Dos {
277    pub fn new(config: MachineConfig) -> Self {
278        let mut video = match config.adapter {
279            Adapter::Vga => Video::Vga(Box::new(Vga::new(VgaKind::Vga))),
280            Adapter::Ega => Video::Vga(Box::new(Vga::new(VgaKind::Ega))),
281            Adapter::Cga(m) => {
282                let mut c = Cga::new(CgaKind::Cga);
283                c.set_monitor(m);
284                c.snow = config.cga_snow;
285                Video::Cga(Box::new(c))
286            }
287            Adapter::Tandy => Video::Cga(Box::new(Cga::new(CgaKind::Tandy))),
288            Adapter::Hercules(p) => Video::Hercules(Box::new(Hercules::new(p))),
289            Adapter::Svga { memory_kib } => {
290                Video::Svga(Box::new(Svga::new(memory_kib as usize * 1024)))
291            }
292        };
293        let timing = video.timing();
294        video.take_dirty();
295        let mut dos = Self {
296            now: 0.0,
297            crt: Crt::new(timing, 0.0),
298            video,
299            bus: config.bus,
300            cpu_hz: config.cpu_hz,
301            keyboard: Keyboard::default(),
302            mouse: Mouse::default(),
303            timer_pending: 0,
304            timer_taken: Tick::ZERO,
305            exit: false,
306            adapter: config.adapter,
307            tty: (0, 0),
308            speaker: PcSpeaker::new(),
309        };
310        let mode = dos.video.bios_mode();
311        dos.mouse
312            .reset_for_mode(mode, matches!(config.adapter, Adapter::Hercules(_)));
313        dos
314    }
315
316    pub fn adapter(&self) -> Adapter {
317        self.adapter
318    }
319
320    /// The display adapter, for inspection (registers, video memory).
321    pub fn video(&self) -> &Video {
322        &self.video
323    }
324
325    // ---------------------------------------------------------------------------------------
326    // Time
327
328    /// Machine time in nanoseconds since power-on.
329    pub fn now_ns(&self) -> f64 {
330        self.now
331    }
332
333    /// Machine time in crystal ticks, for the sound hardware.
334    pub(crate) fn now_tick(&self) -> Tick {
335        Tick::from_secs_f64(self.now * 1e-9)
336    }
337
338    /// Machine time in seconds since power-on.
339    pub fn now(&self) -> f64 {
340        self.now * 1e-9
341    }
342
343    /// Spend `cycles` CPU clock cycles (your code's cost, if you want it modeled).
344    pub fn cpu(&mut self, cycles: u64) {
345        self.now += cycles as f64 * 1e9 / self.cpu_hz;
346    }
347
348    pub fn cpu_hz(&self) -> f64 {
349        self.cpu_hz
350    }
351
352    pub fn set_cpu_hz(&mut self, hz: f64) {
353        self.cpu_hz = hz.max(1.0);
354    }
355
356    pub fn bus(&self) -> BusTiming {
357        self.bus
358    }
359
360    /// Swap the bus timing at run time (to compare ISA vs. local bus).
361    pub fn set_bus(&mut self, bus: BusTiming) {
362        self.bus = bus;
363    }
364
365    /// Busy-wait for `us` microseconds.
366    pub fn delay_us(&mut self, us: f64) {
367        self.now += us * 1000.0;
368    }
369
370    /// Advance the clock to `t_ns` if it is in the future (an idle CPU).
371    pub fn idle_until(&mut self, t_ns: f64) {
372        if t_ns > self.now {
373            self.now = t_ns;
374        }
375        self.sync();
376    }
377
378    // ---------------------------------------------------------------------------------------
379    // Beam
380
381    /// Paint the screen up to the current beam position.
382    pub fn sync(&mut self) {
383        let target = self.crt.dot_at(self.now);
384        with_card!(&mut self.video, c => self.crt.advance(&mut **c, target));
385    }
386
387    fn check_timing(&mut self) {
388        if self.video.take_dirty() {
389            let t = self.video.timing();
390            with_card!(&mut self.video, c => self.crt.retime(&mut **c, t, self.now));
391        }
392    }
393
394    /// Where the beam is right now.
395    pub fn beam(&self) -> Beam {
396        self.crt.beam_at(self.now)
397    }
398
399    /// Raster timing of the current mode.
400    pub fn timing(&self) -> Timing {
401        *self.crt.timing()
402    }
403
404    /// Refresh rate of the current mode.
405    pub fn refresh_hz(&self) -> f64 {
406        self.crt.timing().refresh_hz()
407    }
408
409    /// Pixel resolution of the current mode.
410    pub fn resolution(&self) -> (u32, u32) {
411        self.video.resolution()
412    }
413
414    fn wait_dot(&mut self, line: u32, dot: u32) {
415        let from = self.crt.dot_at(self.now) + 1;
416        let g = self.crt.next_dot(from, line, dot);
417        self.now = self
418            .now
419            .max(self.crt.time_of(g) + 0.5 / (self.crt.timing().dot_clock_hz * 1e-9));
420        self.sync();
421    }
422
423    /// The classic `while (inp(0x3DA) & 8); while (!(inp(0x3DA) & 8));`: wait until the *start*
424    /// of the next vertical retrace (if one is in progress, wait for the one after it).
425    pub fn wait_vretrace(&mut self) {
426        let t = *self.crt.timing();
427        self.wait_dot(t.v_retrace_start, 0);
428    }
429
430    /// Wait until vertical retrace ends.
431    pub fn wait_vretrace_end(&mut self) {
432        if self.beam().v_retrace {
433            let t = *self.crt.timing();
434            self.wait_dot(t.v_retrace_end, 0);
435        }
436    }
437
438    /// Wait for the start of vertical *blanking* (the bottom border ends), a little earlier than
439    /// retrace: the earliest moment to start changing the picture invisibly.
440    pub fn wait_vblank(&mut self) {
441        let t = *self.crt.timing();
442        self.wait_dot(t.v_blank_start, 0);
443    }
444
445    /// Wait for the first active line of the next frame.
446    pub fn wait_frame_start(&mut self) {
447        self.wait_dot(0, 0);
448    }
449
450    /// Wait until the beam starts scanline `line` (0 = first active line).
451    pub fn wait_line(&mut self, line: u32) {
452        self.wait_dot(line, 0);
453    }
454
455    /// Wait until the beam reaches the end of the active part of the current line (the start of
456    /// horizontal blanking, where a palette change is invisible).
457    pub fn wait_hblank(&mut self) {
458        let b = self.beam();
459        let t = *self.crt.timing();
460        self.wait_dot(b.line, t.h_active);
461    }
462
463    /// Wait until the beam reaches dot `dot` of line `line`.
464    pub fn wait_beam(&mut self, line: u32, dot: u32) {
465        self.wait_dot(line, dot);
466    }
467
468    /// Frames scanned out since power-on.
469    pub fn frames(&self) -> u64 {
470        self.crt.frames_completed()
471    }
472
473    // ---------------------------------------------------------------------------------------
474    // Output
475
476    /// The last completely scanned-out frame.
477    pub fn frame(&self) -> FrameView<'_> {
478        self.crt.frame()
479    }
480
481    /// The picture as painted so far, including the half-painted current frame (call
482    /// [`Dos::sync`] first).
483    pub fn live_frame(&self) -> FrameView<'_> {
484        self.crt.live()
485    }
486
487    // ---------------------------------------------------------------------------------------
488    // Ports
489
490    /// `OUT port, AL`.
491    pub fn outb(&mut self, port: u16, v: u8) {
492        self.now += self.bus.io_ns.max(MIN_IO_NS);
493        self.sync();
494        match port {
495            // The PIT and port 0x61 live with the speaker.
496            0x40 | 0x43 => {
497                // Reprogramming channel 0 restarts its edge count: collect the interrupts
498                // that already fired first.
499                self.count_timer_ticks();
500                self.speaker.outb(self.now_tick(), port, v);
501            }
502            0x41 | 0x42 | 0x61 => self.speaker.outb(self.now_tick(), port, v),
503            _ => with_card!(&mut self.video, c => c.port_out(port, v)),
504        }
505        self.check_timing();
506    }
507
508    /// `OUT port, AX`: low byte to `port`, high byte to `port + 1`. With an index/data pair
509    /// this sets a register in one go: `outw(0x3C4, 0x0F02)` writes 0x0F to sequencer reg 2.
510    pub fn outw(&mut self, port: u16, v: u16) {
511        self.outb(port, v as u8);
512        self.outb(port + 1, (v >> 8) as u8);
513    }
514
515    /// `IN AL, port`.
516    pub fn inb(&mut self, port: u16) -> u8 {
517        self.now += self.bus.io_ns.max(MIN_IO_NS);
518        let beam = self.crt.beam_at(self.now);
519        match port {
520            0x60 => self.keyboard.port60(),
521            0x40..=0x43 | 0x61 => self.speaker.inb(self.now_tick(), port),
522            _ => {
523                self.sync();
524                with_card!(&mut self.video, c => c.port_in(port, &beam))
525            }
526        }
527    }
528
529    // ---------------------------------------------------------------------------------------
530    // Memory
531
532    fn mem_write_raw(&mut self, addr: u32, v: u8) {
533        let beam = self.crt.beam_at(self.now);
534        match &mut self.video {
535            Video::Vga(c) => {
536                c.mem_write(addr, v);
537            }
538            Video::Cga(c) => {
539                c.mem_write(addr, v, &beam);
540            }
541            Video::Hercules(c) => {
542                c.mem_write(addr, v);
543            }
544            Video::Svga(c) => {
545                c.mem_write(addr, v);
546            }
547        }
548    }
549
550    fn mem_read_raw(&mut self, addr: u32) -> u8 {
551        let beam = self.crt.beam_at(self.now);
552        match &mut self.video {
553            Video::Vga(c) => c.mem_read(addr),
554            Video::Cga(c) => c.mem_read(addr, &beam),
555            Video::Hercules(c) => c.mem_read(addr),
556            Video::Svga(c) => c.mem_read(addr),
557        }
558        .unwrap_or(0xFF)
559    }
560
561    /// Write a byte to a physical address (`0xA0000` is `A000:0000`).
562    pub fn poke(&mut self, addr: u32, v: u8) {
563        self.now += self.bus.mem8_ns;
564        self.sync();
565        self.mem_write_raw(addr, v);
566    }
567
568    /// Read a byte. On EGA/VGA this also loads the latches.
569    pub fn peek(&mut self, addr: u32) -> u8 {
570        self.now += self.bus.mem8_ns;
571        self.sync();
572        self.mem_read_raw(addr)
573    }
574
575    /// Write a little-endian word in one bus cycle.
576    pub fn pokew(&mut self, addr: u32, v: u16) {
577        self.now += self.bus.mem16_ns;
578        self.sync();
579        self.mem_write_raw(addr, v as u8);
580        self.mem_write_raw(addr + 1, (v >> 8) as u8);
581    }
582
583    pub fn peekw(&mut self, addr: u32) -> u16 {
584        self.now += self.bus.mem16_ns;
585        self.sync();
586        let lo = self.mem_read_raw(addr) as u16;
587        lo | (self.mem_read_raw(addr + 1) as u16) << 8
588    }
589
590    /// Write a little-endian dword in one bus cycle (a 386's `MOV [mem], EAX`).
591    pub fn poke32(&mut self, addr: u32, v: u32) {
592        self.now += self.bus.mem32_ns;
593        self.sync();
594        for (i, b) in v.to_le_bytes().into_iter().enumerate() {
595            self.mem_write_raw(addr + i as u32, b);
596        }
597    }
598
599    /// Copy bytes into video memory using 32-bit moves (`REP MOVSD`), with bus timing.
600    pub fn poke_slice(&mut self, addr: u32, data: &[u8]) {
601        self.write_block(addr, data.len(), |i| data[i]);
602    }
603
604    /// Fill video memory with a byte (`REP STOSD`).
605    pub fn fill(&mut self, addr: u32, len: usize, v: u8) {
606        self.write_block(addr, len, |_| v);
607    }
608
609    fn write_block(&mut self, addr: u32, len: usize, byte: impl Fn(usize) -> u8) {
610        // Timing granularity: 16 bytes per step, so the beam can overtake a copy mid-way.
611        const STEP: usize = 16;
612        let per_byte = self.bus.mem32_ns / 4.0;
613        if per_byte == 0.0 {
614            self.sync();
615            for i in 0..len {
616                self.mem_write_raw(addr + i as u32, byte(i));
617            }
618            return;
619        }
620        let mut i = 0;
621        while i < len {
622            let n = STEP.min(len - i);
623            self.now += per_byte * n as f64;
624            self.sync();
625            for k in i..i + n {
626                self.mem_write_raw(addr + k as u32, byte(k));
627            }
628            i += n;
629        }
630    }
631
632    /// Read bytes from video memory, with bus timing.
633    pub fn peek_slice(&mut self, addr: u32, out: &mut [u8]) {
634        self.now += self.bus.mem32_ns / 4.0 * out.len() as f64;
635        self.sync();
636        for (i, o) in out.iter_mut().enumerate() {
637            *o = self.mem_read_raw(addr + i as u32);
638        }
639    }
640
641    /// Copy within video memory one byte at a time (`REP MOVSB` from video to video memory).
642    /// On EGA/VGA with write mode 1 each byte read loads all four latches and each write stores
643    /// them, so this moves four planes per byte: the fast tile copy of EGA-era scrollers.
644    pub fn copy_vram(&mut self, src: u32, dst: u32, len: usize) {
645        for i in 0..len as u32 {
646            self.now += self.bus.mem8_ns * 2.0;
647            self.sync();
648            let v = self.mem_read_raw(src + i);
649            self.mem_write_raw(dst + i, v);
650        }
651    }
652
653    // ---------------------------------------------------------------------------------------
654    // BIOS
655
656    /// `INT 10h, AH=00h`: set a BIOS video mode. Bit 7 keeps video memory. Returns `false` if
657    /// the adapter has no such mode (the real BIOS would silently do something odd).
658    ///
659    /// | Adapter | Modes |
660    /// |---------|-------|
661    /// | VGA / SVGA | 00h-03h, 0Dh, 0Eh, 10h, 12h, 13h |
662    /// | EGA | 02h, 03h, 0Dh, 0Eh, 10h |
663    /// | CGA | 00h-06h |
664    /// | Tandy | 00h-06h, 08h, 09h, 0Ah |
665    /// | Hercules | 07h (graphics: [`Dos::hercules_graphics`]) |
666    pub fn set_mode(&mut self, mode: u8) -> bool {
667        self.now += 50_000.0; // the BIOS takes its time
668        self.sync();
669        let ok = match &mut self.video {
670            Video::Vga(v) => v.set_mode(mode),
671            Video::Cga(c) => c.set_mode(mode),
672            Video::Hercules(h) => h.set_mode(mode),
673            Video::Svga(s) => s.set_vga_mode(mode),
674        };
675        if ok {
676            self.after_mode_set();
677        }
678        ok
679    }
680
681    fn after_mode_set(&mut self) {
682        self.video.take_dirty();
683        let t = self.video.timing();
684        with_card!(&mut self.video, c => {
685            self.crt.reset(&mut **c, t, self.now);
686            c.vretrace_start();
687        });
688        self.tty = (0, 0);
689        let mode = self.video.bios_mode();
690        self.mouse
691            .reset_for_mode(mode, matches!(self.adapter, Adapter::Hercules(_)));
692        if mode == 0xFF {
693            let (w, h) = self.video.resolution();
694            self.mouse.range = [0, w as i32 - 1, 0, h as i32 - 1];
695        }
696    }
697
698    /// The current BIOS mode number (`INT 10h, AH=0Fh`), `0xFF` in a VBE mode.
699    pub fn mode(&self) -> u8 {
700        self.video.bios_mode()
701    }
702
703    /// VBE function 02h: set an SVGA mode. Returns `None` without an SVGA card or for an
704    /// unknown mode.
705    pub fn vbe_set_mode(&mut self, number: u16) -> Option<VbeMode> {
706        self.now += 100_000.0;
707        self.sync();
708        let Video::Svga(s) = &mut self.video else {
709            return None;
710        };
711        let m = s.set_vbe_mode(number)?;
712        self.after_mode_set();
713        Some(m)
714    }
715
716    /// VBE function 05h: select the 64 KiB bank visible at `A000:0000`. Through the BIOS this
717    /// is a far call and costs a few microseconds, which adds up per scanline.
718    pub fn vbe_set_bank(&mut self, bank: u32) {
719        self.now += 2_000.0;
720        self.sync();
721        if let Video::Svga(s) = &mut self.video
722            && let Some(v) = &mut s.vbe
723        {
724            v.bank = bank;
725        }
726    }
727
728    /// Current VBE bank.
729    pub fn vbe_bank(&self) -> u32 {
730        match &self.video {
731            Video::Svga(s) => s.vbe.as_ref().map_or(0, |v| v.bank),
732            _ => 0,
733        }
734    }
735
736    /// Bytes per scanline in the current VBE mode.
737    pub fn vbe_pitch(&self) -> u32 {
738        match &self.video {
739            Video::Svga(s) => s.vbe.as_ref().map_or(0, |v| v.pitch),
740            _ => 0,
741        }
742    }
743
744    /// VBE function 06h: set the logical scanline length in pixels (for a virtual screen wider
745    /// than the display).
746    pub fn vbe_set_scanline(&mut self, pixels: u32) {
747        self.now += 2_000.0;
748        self.sync();
749        if let Video::Svga(s) = &mut self.video
750            && let Some(v) = &mut s.vbe
751        {
752            v.pitch = pixels * v.mode.bytes_per_pixel();
753        }
754    }
755
756    /// VBE function 07h: set the display start (first visible pixel) for panning and page
757    /// flipping. Takes effect at the next vertical retrace; `wait` waits for it (subfunction
758    /// 80h), like a page flip should.
759    pub fn vbe_set_display_start(&mut self, x: u32, y: u32, wait: bool) {
760        self.now += 2_000.0;
761        self.sync();
762        if let Video::Svga(s) = &mut self.video
763            && let Some(v) = &mut s.vbe
764        {
765            v.start = y * v.pitch + x * v.mode.bytes_per_pixel();
766        }
767        if wait {
768            self.wait_vretrace();
769        }
770    }
771
772    /// VBE function 08h: switch the DAC between 6 and 8 bits per component.
773    pub fn vbe_set_dac_width(&mut self, bits: u8) -> u8 {
774        self.sync();
775        if let Video::Svga(s) = &mut self.video {
776            s.vga.dac_8bit = bits >= 8;
777            s.vga.refresh_dac_all();
778            return if s.vga.dac_8bit { 8 } else { 6 };
779        }
780        6
781    }
782
783    /// The current VBE mode.
784    pub fn vbe_mode(&self) -> Option<VbeMode> {
785        match &self.video {
786            Video::Svga(s) => s.vbe.as_ref().map(|v| v.mode),
787            _ => None,
788        }
789    }
790
791    /// Program the Hercules card for 720x348 graphics on `page` (0 or 1). The configuration
792    /// switch is set to allow graphics and (for page 1) the second page; there is no BIOS
793    /// support, so this is what every Hercules game shipped its own version of.
794    pub fn hercules_graphics(&mut self, page: u8) -> bool {
795        if !matches!(self.video, Video::Hercules(_)) {
796            return false;
797        }
798        self.outb(0x3BF, 0x03);
799        self.outb(0x3B8, 0x02);
800        for (i, v) in crate::hercules::HGC_GRAPHICS_CRTC.iter().enumerate() {
801            self.outb(0x3B4, i as u8);
802            self.outb(0x3B5, *v);
803        }
804        self.outb(0x3B8, 0x0A | if page & 1 != 0 { 0x80 } else { 0 });
805        if let Video::Hercules(h) = &mut self.video {
806            h.bios_mode = 0x7F;
807        }
808        self.mouse.range = [0, 719, 0, 347];
809        true
810    }
811
812    /// Back to Hercules text mode.
813    pub fn hercules_text(&mut self) -> bool {
814        if !matches!(self.video, Video::Hercules(_)) {
815            return false;
816        }
817        self.outb(0x3B8, 0x00);
818        for (i, v) in crate::hercules::HGC_TEXT_CRTC.iter().enumerate() {
819            self.outb(0x3B4, i as u8);
820            self.outb(0x3B5, *v);
821        }
822        self.outb(0x3B8, 0x28);
823        if let Video::Hercules(h) = &mut self.video {
824            h.bios_mode = 7;
825        }
826        true
827    }
828
829    /// Change the CGA monitor (RGB vs. composite) at run time.
830    pub fn cga_set_monitor(&mut self, m: CgaMonitor) {
831        self.sync();
832        if let Video::Cga(c) = &mut self.video {
833            c.set_monitor(m);
834        }
835    }
836
837    /// Composite tint knob.
838    pub fn cga_set_composite_hue(&mut self, degrees: f32) {
839        self.sync();
840        if let Video::Cga(c) = &mut self.video {
841            c.composite_hue = degrees;
842        }
843    }
844
845    /// Change the Hercules phosphor color.
846    pub fn hercules_set_phosphor(&mut self, p: Phosphor) {
847        self.sync();
848        if let Video::Hercules(h) = &mut self.video {
849            h.phosphor = p;
850        }
851    }
852
853    /// Load a font into VGA/EGA character map `map` (like `INT 10h, AX=1110h`), for custom
854    /// character sets in text mode. The font height should match the mode's character height.
855    pub fn load_font(&mut self, font: &crate::font::Font, map: usize) {
856        self.now += 20_000.0;
857        self.sync();
858        match &mut self.video {
859            Video::Vga(v) => v.load_font(font, map),
860            Video::Svga(s) => s.vga.load_font(font, map),
861            _ => {}
862        }
863    }
864
865    // ---------------------------------------------------------------------------------------
866    // Timer
867
868    /// Reprogram the timer interrupt rate (the PIT channel 0 divisor), like games did for a
869    /// steady game tick.
870    pub fn set_timer_hz(&mut self, hz: f64) {
871        // A count of 0 means 65536.
872        let d = (PIT_HZ / hz.max(18.2)).round().clamp(1.0, 65536.0) as u32 as u16;
873        self.outb(0x43, 0x36); // channel 0, low then high byte, mode 3
874        self.outb(0x40, d as u8);
875        self.outb(0x40, (d >> 8) as u8);
876    }
877
878    /// Current timer interrupt rate.
879    pub fn timer_hz(&self) -> f64 {
880        PIT_HZ / self.speaker.pit().channel(Channel::Ch0).divisor() as f64
881    }
882
883    /// Timer interrupts since the timer was last programmed.
884    pub fn timer_ticks(&self) -> u64 {
885        // Edges only count since channel 0 was last programmed.
886        self.speaker.pit().irq0_edges(Tick::ZERO, self.now_tick())
887    }
888
889    /// Timer interrupts since the last call: run that many fixed game ticks, as an `INT 8`
890    /// handler would have.
891    pub fn take_timer_ticks(&mut self) -> u64 {
892        self.count_timer_ticks();
893        std::mem::take(&mut self.timer_pending)
894    }
895
896    fn count_timer_ticks(&mut self) {
897        let now = self.now_tick();
898        self.timer_pending += self.speaker.pit().irq0_edges(self.timer_taken, now);
899        self.timer_taken = now;
900    }
901
902    /// BIOS tick count (18.2 Hz since midnight, here since power-on).
903    pub fn bios_ticks(&self) -> u64 {
904        (self.now * 1e-9 * PIT_HZ / 65536.0) as u64
905    }
906
907    // ---------------------------------------------------------------------------------------
908    // Input
909
910    pub fn keyboard(&self) -> &Keyboard {
911        &self.keyboard
912    }
913
914    pub fn keyboard_mut(&mut self) -> &mut Keyboard {
915        &mut self.keyboard
916    }
917
918    /// Is the key with this scancode held?
919    pub fn key(&self, scan: u8) -> bool {
920        self.keyboard.down(scan)
921    }
922
923    /// `kbhit()`.
924    pub fn kbhit(&self) -> bool {
925        self.keyboard.hit()
926    }
927
928    /// `getch()`, without blocking.
929    pub fn getch(&mut self) -> Option<crate::input::KeyPress> {
930        self.keyboard.read()
931    }
932
933    pub fn mouse(&self) -> &Mouse {
934        &self.mouse
935    }
936
937    pub fn mouse_mut(&mut self) -> &mut Mouse {
938        &mut self.mouse
939    }
940
941    /// The mouse position converted from driver coordinates to pixels of the current mode
942    /// (the conversion a program has to do itself).
943    pub fn mouse_pixel(&self) -> (i32, i32) {
944        let (w, h) = self.video.resolution();
945        let [x0, x1, y0, y1] = self.mouse.range;
946        let sx = (self.mouse.x - x0) as i64 * w as i64 / (x1 - x0 + 1).max(1) as i64;
947        let sy = (self.mouse.y - y0) as i64 * h as i64 / (y1 - y0 + 1).max(1) as i64;
948        (sx as i32, sy as i32)
949    }
950
951    // ---------------------------------------------------------------------------------------
952    // Program control
953
954    /// Ask the host to end the program (`INT 21h, AH=4Ch`).
955    pub fn exit(&mut self) {
956        self.exit = true;
957    }
958
959    pub fn exit_requested(&self) -> bool {
960        self.exit
961    }
962}
963
964#[cfg(test)]
965mod tests {
966    use super::*;
967
968    fn vga() -> Dos {
969        let mut d = Dos::new(MachineConfig::default().with_bus(BusTiming::INSTANT));
970        assert!(d.set_mode(0x13));
971        d
972    }
973
974    #[test]
975    fn status_register_polling_terminates() {
976        // Even on the INSTANT bus a port read costs time, so this cannot spin forever.
977        let mut d = vga();
978        while d.inb(0x3DA) & 8 != 0 {}
979        while d.inb(0x3DA) & 8 == 0 {}
980        assert!(d.beam().v_retrace);
981    }
982
983    #[test]
984    fn polling_with_bus_cost_finds_retrace() {
985        let mut d = Dos::new(MachineConfig::default());
986        d.set_mode(0x13);
987        while d.inb(0x3DA) & 8 != 0 {}
988        while d.inb(0x3DA) & 8 == 0 {}
989        assert!(d.beam().v_retrace);
990        assert_eq!(d.beam().line, d.timing().v_retrace_start);
991    }
992
993    #[test]
994    fn wait_vretrace_lands_on_retrace_start() {
995        let mut d = vga();
996        d.wait_vretrace();
997        let b = d.beam();
998        assert!(b.v_retrace);
999        assert_eq!(b.line, d.timing().v_retrace_start);
1000        let f = b.frame;
1001        d.wait_vretrace();
1002        assert_eq!(d.beam().frame, f + 1);
1003    }
1004
1005    #[test]
1006    fn mode13h_pixel_reaches_the_screen() {
1007        let mut d = vga();
1008        d.poke(0xA0000 + 100 * 320 + 160, 4); // red
1009        d.wait_vretrace();
1010        d.wait_vretrace();
1011        let f = d.frame();
1012        let [ax, ay, ..] = f.active;
1013        let x = ax + 160 * 2;
1014        let y = ay + 100 * 2;
1015        let i = ((y * f.width + x) * 4) as usize;
1016        assert_eq!(&f.rgba[i..i + 3], &[0xAA, 0, 0]);
1017        let i2 = (((y + 1) * f.width + x + 1) * 4) as usize;
1018        assert_eq!(
1019            &f.rgba[i2..i2 + 3],
1020            &[0xAA, 0, 0],
1021            "double-scanned and doubled"
1022        );
1023    }
1024
1025    #[test]
1026    fn palette_change_mid_frame_splits_the_screen() {
1027        let mut d = vga();
1028        d.fill(0xA0000, 64000, 1);
1029        d.wait_frame_start();
1030        d.outb(0x3C8, 1);
1031        d.outb(0x3C9, 63);
1032        d.outb(0x3C9, 0);
1033        d.outb(0x3C9, 0);
1034        d.wait_line(200);
1035        d.outb(0x3C8, 1);
1036        d.outb(0x3C9, 0);
1037        d.outb(0x3C9, 63);
1038        d.outb(0x3C9, 0);
1039        d.wait_vretrace();
1040        let f = d.frame();
1041        let [ax, ay, ..] = f.active;
1042        let px = |y: u32| {
1043            let i = (((ay + y) * f.width + ax + 10) * 4) as usize;
1044            [f.rgba[i], f.rgba[i + 1], f.rgba[i + 2]]
1045        };
1046        assert_eq!(px(10), [255, 0, 0]);
1047        assert_eq!(px(390), [0, 255, 0]);
1048    }
1049
1050    #[test]
1051    fn slow_bus_copy_tears() {
1052        // Copy a full frame of color 2 over color 1 on an ISA bus, starting at the top of the
1053        // display: the beam overtakes the copy, so the finished frame shows both.
1054        let mut d = Dos::new(MachineConfig::default().with_bus(BusTiming::ISA16));
1055        d.set_mode(0x13);
1056        d.wait_vretrace();
1057        d.wait_frame_start();
1058        let data = vec![2u8; 64000];
1059        d.poke_slice(0xA0000, &data);
1060        assert!(
1061            d.now_ns() > 14e6,
1062            "64000 bytes on ISA16 take longer than a frame"
1063        );
1064    }
1065
1066    #[test]
1067    fn timer_ticks() {
1068        let mut d = vga();
1069        d.set_timer_hz(100.0);
1070        d.delay_us(105_000.0);
1071        assert_eq!(d.take_timer_ticks(), 10);
1072        assert_eq!(d.take_timer_ticks(), 0);
1073        assert!((d.timer_hz() - 100.0).abs() < 0.1);
1074    }
1075
1076    #[test]
1077    fn timer_programmed_through_the_ports() {
1078        let mut d = vga();
1079        // Power-on: 18.2 Hz.
1080        assert!((d.timer_hz() - 18.2).abs() < 0.01);
1081        // A game's own INT 8 setup: channel 0, lo/hi, mode 3, 1193 (~1 kHz).
1082        d.outb(0x43, 0x36);
1083        d.outb(0x40, (1193u16 & 0xFF) as u8);
1084        d.outb(0x40, (1193u16 >> 8) as u8);
1085        d.delay_us(10_500.0);
1086        assert_eq!(d.take_timer_ticks(), 10);
1087        // Latch and read the counter: it counts down from the divisor.
1088        d.outb(0x43, 0x00);
1089        let lo = d.inb(0x40);
1090        let hi = d.inb(0x40);
1091        let count = u16::from_le_bytes([lo, hi]);
1092        assert!((1..=1193).contains(&count), "{count}");
1093        // Interrupts that fired before reprogramming are still taken; the new rate counts
1094        // from the reprogram on.
1095        d.delay_us(5_200.0); // ~15.7 ms since programming: edges at 11..=15 ms
1096        d.set_timer_hz(100.0);
1097        assert_eq!(d.timer_ticks(), 0);
1098        d.delay_us(10_500.0);
1099        assert_eq!(d.take_timer_ticks(), 5 + 1);
1100        assert_eq!(d.take_timer_ticks(), 0);
1101    }
1102
1103    #[test]
1104    fn mode_support_per_adapter() {
1105        let mut d = Dos::new(MachineConfig::new(Adapter::Cga(CgaMonitor::Rgb)));
1106        assert!(d.set_mode(4));
1107        assert!(!d.set_mode(0x13));
1108        let mut d = Dos::new(MachineConfig::new(Adapter::Svga { memory_kib: 2048 }));
1109        assert_eq!(d.vbe_set_mode(0x101).map(|m| m.width), Some(640));
1110        assert_eq!(d.resolution(), (640, 480));
1111        assert!((d.refresh_hz() - 59.94).abs() < 0.05);
1112    }
1113}