Skip to main content

game_toolkit_dosvideo/
draw.rs

1//! Drawing the way DOS programs drew: palette programming, register helpers, pixel plotting for
2//! every adapter's memory layout, Mode X with page flipping, and an off-screen [`Bitmap`] for
3//! double buffering in system memory.
4//!
5//! Everything here goes through [`Dos::outb`] / [`Dos::poke`], so it costs bus time and shows
6//! up on screen where the beam is when it happens.
7
8use crate::font::Font;
9use crate::machine::{Dos, Video};
10
11/// An 8-bit-per-pixel bitmap in system memory: a back buffer, a sprite, a tile sheet.
12///
13/// Drawing into it is free (it is not video memory). Copy it to the screen with
14/// [`Dos::blit13`], [`ModeX::blit`] or the per-adapter helpers.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Bitmap {
17    pub width: u32,
18    pub height: u32,
19    pub data: Vec<u8>,
20}
21
22/// How [`Bitmap::from_rgba`] spreads the error when a true-color pixel has no exact match in
23/// the palette.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
25pub enum Dither {
26    /// Nearest color: flat areas, visible banding.
27    #[default]
28    None,
29    /// Ordered dither with a 2x2 Bayer matrix: coarse checkerboards.
30    Bayer2,
31    /// Ordered dither with a 4x4 Bayer matrix: the classic crosshatch of 16-color games.
32    Bayer4,
33    /// Ordered dither with an 8x8 Bayer matrix: finer gradients.
34    Bayer8,
35    /// Floyd-Steinberg error diffusion: organic noise, best for photos, worst for animation.
36    FloydSteinberg,
37}
38
39const BAYER8: [[u8; 8]; 8] = [
40    [0, 32, 8, 40, 2, 34, 10, 42],
41    [48, 16, 56, 24, 50, 18, 58, 26],
42    [12, 44, 4, 36, 14, 46, 6, 38],
43    [60, 28, 52, 20, 62, 30, 54, 22],
44    [3, 35, 11, 43, 1, 33, 9, 41],
45    [51, 19, 59, 27, 49, 17, 57, 25],
46    [15, 47, 7, 39, 13, 45, 5, 37],
47    [63, 31, 55, 23, 61, 29, 53, 21],
48];
49
50/// Index of the palette color closest to `c` (weighted RGB distance).
51pub fn nearest_color(palette: &[[u8; 3]], c: [f32; 3]) -> u8 {
52    let mut best = 0;
53    let mut best_d = f32::MAX;
54    for (i, p) in palette.iter().enumerate() {
55        let dr = c[0] - p[0] as f32;
56        let dg = c[1] - p[1] as f32;
57        let db = c[2] - p[2] as f32;
58        let d = 2.0 * dr * dr + 4.0 * dg * dg + 3.0 * db * db;
59        if d < best_d {
60            best_d = d;
61            best = i;
62        }
63    }
64    best as u8
65}
66
67impl Bitmap {
68    /// Convert true-color RGBA8 pixels to palette indices, the way artwork was prepared for
69    /// 16- and 256-color modes. `palette` holds 8-bit RGB (see
70    /// [`crate::palette::CGA`] or [`Dos::dac`] scaled with [`crate::palette::dac6_to_8`]);
71    /// index `i` of the result is entry `i` of `palette`.
72    pub fn from_rgba(
73        width: u32,
74        height: u32,
75        rgba: &[u8],
76        palette: &[[u8; 3]],
77        dither: Dither,
78    ) -> Self {
79        let mut b = Self::new(width, height);
80        if palette.is_empty() {
81            return b;
82        }
83        let px = |x: u32, y: u32| {
84            let i = ((y * width + x) * 4) as usize;
85            [rgba[i] as f32, rgba[i + 1] as f32, rgba[i + 2] as f32]
86        };
87        // How far an ordered dither may push a pixel: about one palette step.
88        let spread = 0.8 * 255.0 / (palette.len() as f32).cbrt();
89        match dither {
90            Dither::None | Dither::Bayer2 | Dither::Bayer4 | Dither::Bayer8 => {
91                let n = match dither {
92                    Dither::Bayer2 => 2,
93                    Dither::Bayer4 => 4,
94                    Dither::Bayer8 => 8,
95                    _ => 0,
96                };
97                for y in 0..height {
98                    for x in 0..width {
99                        let mut c = px(x, y);
100                        if let Some(s) = 8usize.checked_div(n) {
101                            // The 8x8 matrix contains the smaller ones at a stride, with values 0..n².
102                            let m = BAYER8[(y as usize % n) * s][(x as usize % n) * s] as f32;
103                            let t = ((m + 0.5) / (n * n) as f32 - 0.5) * spread;
104                            for ch in &mut c {
105                                *ch += t;
106                            }
107                        }
108                        b.data[(y * width + x) as usize] = nearest_color(palette, c);
109                    }
110                }
111            }
112            Dither::FloydSteinberg => {
113                let w = width as usize;
114                let mut err = vec![[0.0f32; 3]; w * 2];
115                for y in 0..height {
116                    let (cur, next) = err.split_at_mut(w);
117                    for x in 0..width as usize {
118                        let o = px(x as u32, y);
119                        let c = [o[0] + cur[x][0], o[1] + cur[x][1], o[2] + cur[x][2]];
120                        let i = nearest_color(palette, c);
121                        b.data[y as usize * w + x] = i;
122                        let p = palette[i as usize];
123                        for ch in 0..3 {
124                            let e = c[ch] - p[ch] as f32;
125                            if x + 1 < w {
126                                cur[x + 1][ch] += e * 7.0 / 16.0;
127                                next[x + 1][ch] += e * 1.0 / 16.0;
128                            }
129                            if x > 0 {
130                                next[x - 1][ch] += e * 3.0 / 16.0;
131                            }
132                            next[x][ch] += e * 5.0 / 16.0;
133                        }
134                    }
135                    let (cur, next) = err.split_at_mut(w);
136                    cur.copy_from_slice(next);
137                    next.fill([0.0; 3]);
138                }
139            }
140        }
141        b
142    }
143
144    pub fn new(width: u32, height: u32) -> Self {
145        Self {
146            width,
147            height,
148            data: vec![0; (width * height) as usize],
149        }
150    }
151
152    pub fn from_data(width: u32, height: u32, data: Vec<u8>) -> Option<Self> {
153        (data.len() == (width * height) as usize).then_some(Self {
154            width,
155            height,
156            data,
157        })
158    }
159
160    /// Build a bitmap from rows of characters, mapping each character through `palette`
161    /// (characters not in it become 0, the transparent color). Handy for small sprites.
162    pub fn from_art(rows: &[&str], palette: &[(char, u8)]) -> Self {
163        let h = rows.len() as u32;
164        let w = rows.iter().map(|r| r.chars().count()).max().unwrap_or(0) as u32;
165        let mut b = Self::new(w, h);
166        for (y, r) in rows.iter().enumerate() {
167            for (x, ch) in r.chars().enumerate() {
168                let c = palette.iter().find(|p| p.0 == ch).map_or(0, |p| p.1);
169                b.data[y * w as usize + x] = c;
170            }
171        }
172        b
173    }
174
175    #[inline]
176    pub fn get(&self, x: i32, y: i32) -> u8 {
177        if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
178            return 0;
179        }
180        self.data[(y as u32 * self.width + x as u32) as usize]
181    }
182
183    #[inline]
184    pub fn pset(&mut self, x: i32, y: i32, c: u8) {
185        if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
186            return;
187        }
188        self.data[(y as u32 * self.width + x as u32) as usize] = c;
189    }
190
191    pub fn clear(&mut self, c: u8) {
192        self.data.fill(c);
193    }
194
195    pub fn row(&self, y: u32) -> &[u8] {
196        let w = self.width as usize;
197        &self.data[y as usize * w..(y as usize + 1) * w]
198    }
199
200    pub fn row_mut(&mut self, y: u32) -> &mut [u8] {
201        let w = self.width as usize;
202        &mut self.data[y as usize * w..(y as usize + 1) * w]
203    }
204
205    pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, c: u8) {
206        let x0 = x.max(0);
207        let y0 = y.max(0);
208        let x1 = (x + w).min(self.width as i32);
209        let y1 = (y + h).min(self.height as i32);
210        if x0 >= x1 || y0 >= y1 {
211            return;
212        }
213        for yy in y0..y1 {
214            let o = (yy as u32 * self.width) as usize;
215            self.data[o + x0 as usize..o + x1 as usize].fill(c);
216        }
217    }
218
219    pub fn rect(&mut self, x: i32, y: i32, w: i32, h: i32, c: u8) {
220        self.hline(x, x + w - 1, y, c);
221        self.hline(x, x + w - 1, y + h - 1, c);
222        self.vline(x, y, y + h - 1, c);
223        self.vline(x + w - 1, y, y + h - 1, c);
224    }
225
226    pub fn hline(&mut self, x0: i32, x1: i32, y: i32, c: u8) {
227        let (a, b) = (x0.min(x1), x0.max(x1));
228        self.fill_rect(a, y, b - a + 1, 1, c);
229    }
230
231    pub fn vline(&mut self, x: i32, y0: i32, y1: i32, c: u8) {
232        let (a, b) = (y0.min(y1), y0.max(y1));
233        self.fill_rect(x, a, 1, b - a + 1, c);
234    }
235
236    /// Bresenham line.
237    pub fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, c: u8) {
238        let (mut x, mut y) = (x0, y0);
239        let dx = (x1 - x0).abs();
240        let dy = -(y1 - y0).abs();
241        let sx = if x0 < x1 { 1 } else { -1 };
242        let sy = if y0 < y1 { 1 } else { -1 };
243        let mut err = dx + dy;
244        loop {
245            self.pset(x, y, c);
246            if x == x1 && y == y1 {
247                break;
248            }
249            let e2 = 2 * err;
250            if e2 >= dy {
251                err += dy;
252                x += sx;
253            }
254            if e2 <= dx {
255                err += dx;
256                y += sy;
257            }
258        }
259    }
260
261    /// Midpoint circle; `fill` draws a disc.
262    pub fn circle(&mut self, cx: i32, cy: i32, r: i32, c: u8, fill: bool) {
263        let (mut x, mut y, mut d) = (r, 0, 1 - r);
264        while x >= y {
265            if fill {
266                self.hline(cx - x, cx + x, cy + y, c);
267                self.hline(cx - x, cx + x, cy - y, c);
268                self.hline(cx - y, cx + y, cy + x, c);
269                self.hline(cx - y, cx + y, cy - x, c);
270            } else {
271                for (px, py) in [
272                    (x, y),
273                    (y, x),
274                    (-y, x),
275                    (-x, y),
276                    (-x, -y),
277                    (-y, -x),
278                    (y, -x),
279                    (x, -y),
280                ] {
281                    self.pset(cx + px, cy + py, c);
282                }
283            }
284            y += 1;
285            if d < 0 {
286                d += 2 * y + 1;
287            } else {
288                x -= 1;
289                d += 2 * (y - x) + 1;
290            }
291        }
292    }
293
294    /// Copy `src` with its top-left at `(x, y)`; `transparent` pixels are skipped.
295    pub fn blit(&mut self, src: &Bitmap, x: i32, y: i32, transparent: Option<u8>) {
296        for sy in 0..src.height as i32 {
297            let dy = y + sy;
298            if dy < 0 || dy >= self.height as i32 {
299                continue;
300            }
301            for sx in 0..src.width as i32 {
302                let dx = x + sx;
303                if dx < 0 || dx >= self.width as i32 {
304                    continue;
305                }
306                let c = src.data[(sy as u32 * src.width + sx as u32) as usize];
307                if Some(c) != transparent {
308                    self.data[(dy as u32 * self.width + dx as u32) as usize] = c;
309                }
310            }
311        }
312    }
313
314    /// Copy a `w` x `h` region of `src` starting at `(sx, sy)`.
315    #[allow(clippy::too_many_arguments)]
316    pub fn blit_region(
317        &mut self,
318        src: &Bitmap,
319        sx: i32,
320        sy: i32,
321        w: i32,
322        h: i32,
323        x: i32,
324        y: i32,
325        transparent: Option<u8>,
326    ) {
327        for yy in 0..h {
328            for xx in 0..w {
329                let c = src.get(sx + xx, sy + yy);
330                if Some(c) != transparent {
331                    self.pset(x + xx, y + yy, c);
332                }
333            }
334        }
335    }
336
337    /// Draw text with an 8-pixel-wide bitmap font; `bg` fills the cell background.
338    pub fn text(&mut self, x: i32, y: i32, s: &str, fg: u8, bg: Option<u8>, font: &Font) {
339        let h = font.height() as i32;
340        for (i, ch) in crate::font::cp437(s).into_iter().enumerate() {
341            let g = font.glyph(ch);
342            let cx = x + i as i32 * 8;
343            for r in 0..h {
344                for b in 0..8 {
345                    if (g[r as usize] >> (7 - b)) & 1 != 0 {
346                        self.pset(cx + b, y + r, fg);
347                    } else if let Some(bg) = bg {
348                        self.pset(cx + b, y + r, bg);
349                    }
350                }
351            }
352        }
353    }
354}
355
356/// Which port the CRTC answers on in the current configuration.
357fn crtc_port(dos: &Dos) -> u16 {
358    match dos.video() {
359        Video::Hercules(_) => 0x3B4,
360        Video::Vga(v) if v.misc & 1 == 0 => 0x3B4,
361        Video::Svga(s) if s.vga.misc & 1 == 0 => 0x3B4,
362        _ => 0x3D4,
363    }
364}
365
366fn status_port(dos: &Dos) -> u16 {
367    crtc_port(dos) + 6
368}
369
370fn vga_ref(dos: &Dos) -> Option<&crate::vga::Vga> {
371    dos.video().as_vga()
372}
373
374impl Dos {
375    // ---------------------------------------------------------------------------------------
376    // Palette
377
378    /// Set DAC entry `i` to a 6-bit `[r, g, b]` (each `0..=63`), via ports 0x3C8/0x3C9.
379    pub fn set_palette(&mut self, i: u8, rgb: [u8; 3]) {
380        self.outb(0x3C8, i);
381        self.outb(0x3C9, rgb[0]);
382        self.outb(0x3C9, rgb[1]);
383        self.outb(0x3C9, rgb[2]);
384    }
385
386    /// Set DAC entries from `start` on (the auto-increment makes this one index write).
387    pub fn set_palette_range(&mut self, start: u8, colors: &[[u8; 3]]) {
388        self.outb(0x3C8, start);
389        for c in colors {
390            self.outb(0x3C9, c[0]);
391            self.outb(0x3C9, c[1]);
392            self.outb(0x3C9, c[2]);
393        }
394    }
395
396    /// Set a DAC entry from 8-bit RGB (converted to 6 bits, unless the DAC is in 8-bit mode).
397    pub fn set_palette8(&mut self, i: u8, rgb: [u8; 3]) {
398        let eight = vga_ref(self).is_some_and(|v| v.dac_8bit);
399        let c = if eight {
400            rgb
401        } else {
402            [rgb[0] >> 2, rgb[1] >> 2, rgb[2] >> 2]
403        };
404        self.set_palette(i, c);
405    }
406
407    /// Read DAC entry `i` via 0x3C7/0x3C9.
408    pub fn palette(&mut self, i: u8) -> [u8; 3] {
409        self.outb(0x3C7, i);
410        [self.inb(0x3C9), self.inb(0x3C9), self.inb(0x3C9)]
411    }
412
413    /// Read the whole DAC without bus traffic (what a program keeps as its own copy).
414    pub fn dac(&self) -> Option<[[u8; 3]; 256]> {
415        vga_ref(self).map(|v| v.dac)
416    }
417
418    /// Rotate DAC entries `first..=last` by one: classic palette cycling (water, fire, plasma)
419    /// for the price of a few port writes.
420    pub fn cycle_palette(&mut self, first: u8, last: u8) {
421        let Some(dac) = self.dac() else { return };
422        let (a, b) = (first as usize, last as usize);
423        if b <= a {
424            return;
425        }
426        let mut seg: Vec<[u8; 3]> = dac[a..=b].to_vec();
427        seg.rotate_right(1);
428        self.set_palette_range(first, &seg);
429    }
430
431    /// Fade the whole palette towards black: `level` 0 (black) ..= 64 (original `base`).
432    pub fn fade_palette(&mut self, base: &[[u8; 3]; 256], level: u8) {
433        let l = level.min(64) as u16;
434        let faded: Vec<[u8; 3]> = base
435            .iter()
436            .map(|c| {
437                [
438                    (c[0] as u16 * l / 64) as u8,
439                    (c[1] as u16 * l / 64) as u8,
440                    (c[2] as u16 * l / 64) as u8,
441                ]
442            })
443            .collect();
444        self.set_palette_range(0, &faded);
445    }
446
447    // ---------------------------------------------------------------------------------------
448    // VGA / EGA registers
449
450    /// Write sequencer register `index`.
451    pub fn seq(&mut self, index: u8, v: u8) {
452        self.outb(0x3C4, index);
453        self.outb(0x3C5, v);
454    }
455
456    /// Write graphics controller register `index`.
457    pub fn gc(&mut self, index: u8, v: u8) {
458        self.outb(0x3CE, index);
459        self.outb(0x3CF, v);
460    }
461
462    /// Write CRTC register `index` (on the color or mono port, whichever is active).
463    pub fn crtc(&mut self, index: u8, v: u8) {
464        let p = crtc_port(self);
465        self.outb(p, index);
466        self.outb(p + 1, v);
467    }
468
469    /// Current value of CRTC register `index` (from the card; EGA registers cannot be read
470    /// back through the ports, so DOS programs kept a copy - this is that copy).
471    pub fn crtc_value(&self, index: u8) -> u8 {
472        match self.video() {
473            Video::Vga(v) => v.crtc.get(index as usize).copied().unwrap_or(0),
474            Video::Svga(s) => s.vga.crtc.get(index as usize).copied().unwrap_or(0),
475            Video::Cga(c) => c.crtc.r.get(index as usize).copied().unwrap_or(0),
476            Video::Hercules(h) => h.crtc.r.get(index as usize).copied().unwrap_or(0),
477        }
478    }
479
480    /// Write attribute controller register `index`: reset the flip-flop by reading the status
481    /// port, write index and value, then set the palette address source bit again (forgetting
482    /// that blanks the screen).
483    pub fn attr(&mut self, index: u8, v: u8) {
484        let s = status_port(self);
485        self.inb(s);
486        self.outb(0x3C0, index & 0x1F);
487        self.outb(0x3C0, v);
488        self.outb(0x3C0, 0x20);
489    }
490
491    /// Sequencer map mask: which planes CPU writes go to.
492    pub fn map_mask(&mut self, planes: u8) {
493        self.seq(2, planes & 0x0F);
494    }
495
496    /// Graphics controller read map select.
497    pub fn read_map(&mut self, plane: u8) {
498        self.gc(4, plane & 3);
499    }
500
501    /// Graphics controller write mode (0-3), keeping the read mode.
502    pub fn write_mode(&mut self, mode: u8) {
503        let cur = vga_ref(self).map_or(0, |v| v.gc[5]);
504        self.gc(5, (cur & !3) | (mode & 3));
505    }
506
507    /// Graphics controller bit mask.
508    pub fn bit_mask(&mut self, mask: u8) {
509        self.gc(8, mask);
510    }
511
512    /// Set the CRTC start address (in CRTC address units: words in text mode, 4-pixel groups in
513    /// mode 13h and Mode X, 8-pixel groups in 16-color modes). Latched at the next vertical
514    /// retrace, so set it during the display and *then* wait for retrace.
515    pub fn set_start_address(&mut self, addr: u16) {
516        self.crtc(0x0C, (addr >> 8) as u8);
517        self.crtc(0x0D, addr as u8);
518    }
519
520    /// Current CRTC start address.
521    pub fn start_address(&self) -> u16 {
522        ((self.crtc_value(0x0C) as u16) << 8) | self.crtc_value(0x0D) as u16
523    }
524
525    /// Set the CRTC offset register: the distance between rows, in words (pairs of CRTC
526    /// address units). Make it bigger than the screen for a virtual screen to scroll around.
527    pub fn set_offset(&mut self, offset: u8) {
528        self.crtc(0x13, offset);
529    }
530
531    /// Split screen: from scanline `line + 1` on, the display restarts at address 0 (and,
532    /// with attribute mode bit 5, ignores pel panning). Spreads the 10-bit value over CRTC
533    /// 0x18, bit 4 of the overflow register and bit 6 of max scan line. `0x3FF` turns it off.
534    pub fn set_line_compare(&mut self, line: u16) {
535        let ov = self.crtc_value(7);
536        let msl = self.crtc_value(9);
537        self.crtc(0x18, line as u8);
538        self.crtc(7, (ov & !0x10) | (((line >> 8) & 1) as u8) << 4);
539        self.crtc(9, (msl & !0x40) | (((line >> 9) & 1) as u8) << 6);
540    }
541
542    /// Horizontal pel panning (0-7 pixels in 16-color modes; 0, 2, 4, 6 for 0-3 pixels in
543    /// 256-color modes; 8 means 0 in 9-dot text).
544    pub fn set_pel_pan(&mut self, pan: u8) {
545        self.attr(0x13 | 0x20, pan & 0x0F);
546    }
547
548    /// Overscan (border) color: a DAC index on VGA, a 6-bit color on EGA. On CGA it is the
549    /// low four bits of the color select register.
550    pub fn set_border(&mut self, c: u8) {
551        match self.video() {
552            Video::Cga(cga) => {
553                let col = cga.color;
554                self.outb(0x3D9, (col & 0xF0) | (c & 0x0F));
555            }
556            Video::Hercules(_) => {}
557            _ => self.attr(0x11 | 0x20, c),
558        }
559    }
560
561    /// Set one of the 16 attribute palette registers (EGA: the 6-bit color shown for index `i`).
562    pub fn set_attr_palette(&mut self, i: u8, v: u8) {
563        self.attr((i & 0x0F) | 0x20, v & 0x3F);
564    }
565
566    /// Turn the display off (sequencer clocking mode bit 5) or on.
567    pub fn screen_on(&mut self, on: bool) {
568        let cur = vga_ref(self).map_or(0, |v| v.seq[1]);
569        self.seq(1, if on { cur & !0x20 } else { cur | 0x20 });
570    }
571
572    // ---------------------------------------------------------------------------------------
573    // Mode 13h
574
575    /// Plot a pixel in mode 13h.
576    pub fn pset13(&mut self, x: u32, y: u32, c: u8) {
577        if x < 320 && y < 200 {
578            self.poke(0xA0000 + y * 320 + x, c);
579        }
580    }
581
582    /// Copy a 320-pixel-wide bitmap to `A000:0000` (the double-buffer flip of every mode 13h
583    /// game). How long it takes depends on the bus; if the beam overtakes it, it tears.
584    pub fn blit13(&mut self, bmp: &Bitmap) {
585        let n = (bmp.width * bmp.height).min(64000) as usize;
586        self.poke_slice(0xA0000, &bmp.data[..n]);
587    }
588
589    // ---------------------------------------------------------------------------------------
590    // Any graphics mode
591
592    /// Plot a pixel in whatever graphics mode is active, using that mode's memory layout:
593    /// mode 13h (and Mode X page 0), the 16-color planar modes, CGA, Tandy, Hercules graphics
594    /// page 0, VBE 8-bit modes. `c` is the color index (or `0`/`1` on Hercules).
595    pub fn pset(&mut self, x: u32, y: u32, c: u8) {
596        let (w, h) = self.resolution();
597        if x >= w || y >= h {
598            return;
599        }
600        let mode = self.mode();
601        let chained = self.video().as_vga().is_some_and(|v| v.seq[4] & 0x08 != 0);
602        if self.vbe_mode().is_some() {
603            let addr = y * self.vbe_pitch() + x;
604            self.poke(crate::svga::LFB_BASE + addr, c);
605            return;
606        }
607        match self.video() {
608            Video::Vga(_) | Video::Svga(_) if mode == 0x13 => {
609                if chained {
610                    self.pset13(x, y, c);
611                } else {
612                    self.map_mask(1 << (x & 3));
613                    self.poke(0xA0000 + y * (w / 4) + x / 4, c);
614                }
615            }
616            Video::Vga(_) | Video::Svga(_) => self.planar_pset(x, y, c),
617            Video::Cga(cga) => {
618                let tandy = cga.kind == crate::cga::CgaKind::Tandy;
619                let (addr, shift, bits) = match mode {
620                    4 | 5 => (0x2000 * (y & 1) + (y >> 1) * 80 + x / 4, 6 - 2 * (x & 3), 2),
621                    6 => (0x2000 * (y & 1) + (y >> 1) * 80 + x / 8, 7 - (x & 7), 1),
622                    8 if tandy => (0x2000 * (y & 3) + (y >> 2) * 80 + x / 2, 4 - 4 * (x & 1), 4),
623                    9 if tandy => (
624                        0x2000 * (y & 3) + (y >> 2) * 160 + x / 2,
625                        4 - 4 * (x & 1),
626                        4,
627                    ),
628                    0x0A if tandy => {
629                        let a = 0xB8000 + 0x2000 * (y & 3) + (y >> 2) * 160 + (x / 8) * 2;
630                        let bit = 7 - (x & 7);
631                        for plane in 0..2 {
632                            let old = self.peek(a + plane);
633                            let v = (old & !(1 << bit)) | (((c >> plane) & 1) << bit);
634                            self.poke(a + plane, v);
635                        }
636                        return;
637                    }
638                    _ => return,
639                };
640                let a = 0xB8000 + addr;
641                let mask = (((1u16 << bits) - 1) as u8) << shift;
642                let old = self.peek(a);
643                self.poke(a, (old & !mask) | ((c << shift) & mask));
644            }
645            Video::Hercules(_) => self.herc_pset(x, y, c != 0, 0),
646        }
647    }
648
649    /// Plot a pixel in a 16-color planar mode (0Dh, 0Eh, 10h, 12h), the textbook way:
650    /// set/reset supplies the color, the bit mask selects the pixel, and a dummy read loads
651    /// the latches so the other seven pixels of the byte survive.
652    pub fn planar_pset(&mut self, x: u32, y: u32, c: u8) {
653        let (w, _) = self.resolution();
654        let addr = 0xA0000 + y * (w / 8) + x / 8;
655        self.gc(0, c & 0x0F);
656        self.gc(1, 0x0F);
657        self.gc(8, 0x80 >> (x & 7));
658        self.peek(addr);
659        self.poke(addr, 0xFF);
660        self.gc(1, 0x00);
661        self.gc(8, 0xFF);
662    }
663
664    /// Fill a rectangle in a 16-color planar mode with set/reset, eight pixels per byte write.
665    pub fn planar_fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, c: u8) {
666        let (sw, sh) = self.resolution();
667        let x1 = (x + w).min(sw);
668        let y1 = (y + h).min(sh);
669        if x >= x1 || y >= y1 {
670            return;
671        }
672        let pitch = sw / 8;
673        self.gc(0, c & 0x0F);
674        self.gc(1, 0x0F);
675        let (bx0, bx1) = (x / 8, (x1 - 1) / 8);
676        for yy in y..y1 {
677            for bx in bx0..=bx1 {
678                let mut m = 0xFFu8;
679                if bx == bx0 {
680                    m &= 0xFF >> (x & 7);
681                }
682                if bx == bx1 {
683                    m &= 0xFF << (7 - ((x1 - 1) & 7));
684                }
685                let addr = 0xA0000 + yy * pitch + bx;
686                if m != 0xFF {
687                    self.gc(8, m);
688                    self.peek(addr);
689                } else {
690                    self.gc(8, 0xFF);
691                }
692                self.poke(addr, 0xFF);
693            }
694        }
695        self.gc(1, 0x00);
696        self.gc(8, 0xFF);
697    }
698
699    /// Copy an 8-bit bitmap (colors 0-15) into a 16-color planar mode at a byte-aligned `x`,
700    /// one plane at a time through the map mask. Assumes the BIOS row pitch; see
701    /// [`Dos::planar_blit_at`] for virtual screens and off-screen memory.
702    pub fn planar_blit(&mut self, bmp: &Bitmap, x: u32, y: u32) {
703        let pitch = self.resolution().0 / 8;
704        self.planar_blit_at(bmp, 0xA0000 + y * pitch + x / 8, pitch);
705    }
706
707    /// Copy an 8-bit bitmap (colors 0-15) to planar video memory at physical address `addr`,
708    /// rows `pitch` bytes apart: for a widened offset register, or for tiles stored off-screen
709    /// to be copied with latches later.
710    pub fn planar_blit_at(&mut self, bmp: &Bitmap, addr: u32, pitch: u32) {
711        for plane in 0..4u8 {
712            self.map_mask(1 << plane);
713            for row in 0..bmp.height {
714                let bytes: Vec<u8> = bmp
715                    .row(row)
716                    .chunks(8)
717                    .map(|c| {
718                        c.iter()
719                            .enumerate()
720                            .fold(0u8, |acc, (i, &p)| acc | (((p >> plane) & 1) << (7 - i)))
721                    })
722                    .collect();
723                self.poke_slice(addr + row * pitch, &bytes);
724            }
725        }
726        self.map_mask(0x0F);
727    }
728
729    /// Plot a Hercules graphics pixel on `page`.
730    pub fn herc_pset(&mut self, x: u32, y: u32, on: bool, page: u32) {
731        if x >= 720 || y >= 348 {
732            return;
733        }
734        let a = 0xB0000 + (page & 1) * 0x8000 + (y & 3) * 0x2000 + (y >> 2) * 90 + x / 8;
735        let bit = 0x80 >> (x & 7);
736        let old = self.peek(a);
737        self.poke(a, if on { old | bit } else { old & !bit });
738    }
739
740    /// Copy a 1-bit-per-pixel image (bytes, MSB left, 90 bytes per row, 348 rows) to a
741    /// Hercules page, de-interleaving the rows.
742    pub fn herc_blit(&mut self, packed: &[u8], page: u32) {
743        for y in 0..348u32 {
744            let row = &packed[(y * 90) as usize..((y + 1) * 90) as usize];
745            let a = 0xB0000 + (page & 1) * 0x8000 + (y & 3) * 0x2000 + (y >> 2) * 90;
746            self.poke_slice(a, row);
747        }
748    }
749
750    /// Copy a 320x200 bitmap of colors 0-3 to the CGA 4-color mode, interleaving even and odd
751    /// lines into the two banks.
752    pub fn cga_blit(&mut self, bmp: &Bitmap) {
753        for y in 0..200u32.min(bmp.height) {
754            let row = bmp.row(y);
755            let bytes: Vec<u8> = row
756                .chunks(4)
757                .take(80)
758                .map(|c| {
759                    c.iter()
760                        .enumerate()
761                        .fold(0, |a, (i, &p)| a | ((p & 3) << (6 - 2 * i)))
762                })
763                .collect();
764            self.poke_slice(0xB8000 + 0x2000 * (y & 1) + (y >> 1) * 80, &bytes);
765        }
766    }
767
768    /// Copy a 320x200 bitmap of colors 0-15 to Tandy mode 9.
769    pub fn tandy_blit(&mut self, bmp: &Bitmap) {
770        for y in 0..200u32.min(bmp.height) {
771            let row = bmp.row(y);
772            let bytes: Vec<u8> = row
773                .chunks(2)
774                .take(160)
775                .map(|c| (c[0] << 4) | (c.get(1).copied().unwrap_or(0) & 0x0F))
776                .collect();
777            self.poke_slice(0xB8000 + 0x2000 * (y & 3) + (y >> 2) * 160, &bytes);
778        }
779    }
780
781    // ---------------------------------------------------------------------------------------
782    // VBE
783
784    /// Write one pixel in a VBE mode through the bank window: switch banks if the pixel lies
785    /// in another one (the expensive part), then poke into `A000`. `color` is a palette index
786    /// or a packed 15/16/32-bit value.
787    pub fn vbe_pset_banked(&mut self, x: u32, y: u32, color: u32) {
788        let Some(m) = self.vbe_mode() else { return };
789        if x >= m.width || y >= m.height {
790            return;
791        }
792        let bpp = m.bytes_per_pixel();
793        let off = y * self.vbe_pitch() + x * bpp;
794        let bank = off / crate::svga::BANK_SIZE;
795        if bank != self.vbe_bank() {
796            self.vbe_set_bank(bank);
797        }
798        let a = 0xA0000 + off % crate::svga::BANK_SIZE;
799        match bpp {
800            1 => self.poke(a, color as u8),
801            2 => self.pokew(a, color as u16),
802            _ => self.poke32(a, color),
803        }
804    }
805
806    /// Write one pixel in a VBE mode through the linear framebuffer.
807    pub fn vbe_pset_lfb(&mut self, x: u32, y: u32, color: u32) {
808        let Some(m) = self.vbe_mode() else { return };
809        if x >= m.width || y >= m.height {
810            return;
811        }
812        let bpp = m.bytes_per_pixel();
813        let a = crate::svga::LFB_BASE + y * self.vbe_pitch() + x * bpp;
814        match bpp {
815            1 => self.poke(a, color as u8),
816            2 => self.pokew(a, color as u16),
817            _ => self.poke32(a, color),
818        }
819    }
820
821    /// Copy raw bytes to VBE memory at byte offset `off`, through the bank window: split into
822    /// chunks at bank boundaries, switching banks in between.
823    pub fn vbe_write_banked(&mut self, off: u32, data: &[u8]) {
824        let mut done = 0usize;
825        while done < data.len() {
826            let o = off + done as u32;
827            let bank = o / crate::svga::BANK_SIZE;
828            if bank != self.vbe_bank() {
829                self.vbe_set_bank(bank);
830            }
831            let in_bank = o % crate::svga::BANK_SIZE;
832            let n = ((crate::svga::BANK_SIZE - in_bank) as usize).min(data.len() - done);
833            self.poke_slice(0xA0000 + in_bank, &data[done..done + n]);
834            done += n;
835        }
836    }
837
838    /// Copy raw bytes to VBE memory at byte offset `off`, through the linear framebuffer.
839    pub fn vbe_write_lfb(&mut self, off: u32, data: &[u8]) {
840        self.poke_slice(crate::svga::LFB_BASE + off, data);
841    }
842}
843
844/// Mode X: VGA 256-color graphics with chain-4 off, so all 256 KiB are usable as four pages of
845/// planar memory, with page flipping, hardware scrolling, split screens and four-pixels-at-once
846/// fills.
847///
848/// Pixel `(x, y)` lives in plane `x & 3` at offset `y * width/4 + x/4` of a page. To write it,
849/// select the plane with the sequencer map mask, then poke the byte.
850#[derive(Clone, Copy, Debug)]
851pub struct ModeX {
852    pub width: u32,
853    pub height: u32,
854    /// Bytes (per plane) of one page.
855    pub page_size: u32,
856    /// Page shown.
857    pub visible: u32,
858    /// Page drawn to.
859    pub active: u32,
860}
861
862impl ModeX {
863    /// Set 320x240 ("Mode X", square pixels at 60 Hz) when `height` is 240, else 320x200
864    /// unchained ("Mode Y", 70 Hz). Uses the register sequence from Michael Abrash's articles;
865    /// note the CRTC protect bit that has to be cleared before the timing registers take.
866    pub fn set(dos: &mut Dos, height: u32) -> Option<Self> {
867        if !dos.set_mode(0x13) {
868            return None;
869        }
870        dos.seq(4, 0x06); // chain-4 off, odd/even off
871        if height == 240 {
872            dos.seq(0, 0x01); // synchronous reset
873            dos.outb(0x3C2, 0xE3); // 25 MHz, 480-line sync polarity
874            dos.seq(0, 0x03);
875            dos.outb(0x3D4, 0x11);
876            let v = dos.inb(0x3D5);
877            dos.outb(0x3D5, v & 0x7F); // unprotect CRTC 0-7
878            for (i, v) in [
879                (0x06u8, 0x0Du8),
880                (0x07, 0x3E),
881                (0x09, 0x41),
882                (0x10, 0xEA),
883                (0x11, 0xAC),
884                (0x12, 0xDF),
885                (0x15, 0xE7),
886                (0x16, 0x06),
887            ] {
888                dos.crtc(i, v);
889            }
890        }
891        dos.crtc(0x14, 0x00); // dword mode off
892        dos.crtc(0x17, 0xE3); // byte mode
893        dos.map_mask(0x0F);
894        dos.fill(0xA0000, 0x10000, 0);
895        if height == 240 {
896            dos.mouse_mut().set_range(0, 639, 0, 239);
897        }
898        let h = if height == 240 { 240 } else { 200 };
899        let page_size = 320 / 4 * h;
900        Some(Self {
901            width: 320,
902            height: h,
903            page_size,
904            visible: 0,
905            active: 1,
906        })
907    }
908
909    /// Number of pages that fit into 64 KiB per plane.
910    pub fn pages(&self) -> u32 {
911        0x10000 / self.page_size
912    }
913
914    /// Byte offset of `page`.
915    pub fn page_offset(&self, page: u32) -> u32 {
916        page * self.page_size
917    }
918
919    fn pitch(&self) -> u32 {
920        self.width / 4
921    }
922
923    /// Plot a pixel on the active page.
924    pub fn pset(&self, dos: &mut Dos, x: u32, y: u32, c: u8) {
925        if x >= self.width || y >= self.height {
926            return;
927        }
928        dos.map_mask(1 << (x & 3));
929        dos.poke(
930            0xA0000 + self.page_offset(self.active) + y * self.pitch() + x / 4,
931            c,
932        );
933    }
934
935    /// Fill a rectangle on the active page; the middle part writes four pixels per byte.
936    pub fn fill_rect(&self, dos: &mut Dos, x: i32, y: i32, w: i32, h: i32, c: u8) {
937        let x0 = x.max(0) as u32;
938        let y0 = y.max(0) as u32;
939        let x1 = ((x + w).max(0) as u32).min(self.width);
940        let y1 = ((y + h).max(0) as u32).min(self.height);
941        if x0 >= x1 || y0 >= y1 {
942            return;
943        }
944        let base = 0xA0000 + self.page_offset(self.active);
945        let (b0, b1) = (x0 / 4, (x1 - 1) / 4);
946        let lm = 0x0Fu8 << (x0 & 3) & 0x0F;
947        let rm = 0x0Fu8 >> (3 - ((x1 - 1) & 3));
948        for yy in y0..y1 {
949            let row = base + yy * self.pitch();
950            if b0 == b1 {
951                dos.map_mask(lm & rm);
952                dos.poke(row + b0, c);
953                continue;
954            }
955            dos.map_mask(lm);
956            dos.poke(row + b0, c);
957            if b1 > b0 + 1 {
958                dos.map_mask(0x0F);
959                dos.fill(row + b0 + 1, (b1 - b0 - 1) as usize, c);
960            }
961            dos.map_mask(rm);
962            dos.poke(row + b1, c);
963        }
964    }
965
966    /// Clear the active page.
967    pub fn clear(&self, dos: &mut Dos, c: u8) {
968        dos.map_mask(0x0F);
969        dos.fill(
970            0xA0000 + self.page_offset(self.active),
971            self.page_size as usize,
972            c,
973        );
974    }
975
976    /// Copy a bitmap to the active page, plane by plane (four passes, one map mask write each).
977    /// `transparent` pixels are skipped (slower: pixel by pixel).
978    pub fn blit(&self, dos: &mut Dos, bmp: &Bitmap, x: i32, y: i32, transparent: Option<u8>) {
979        let base = 0xA0000 + self.page_offset(self.active);
980        for plane in 0..4i32 {
981            dos.map_mask(1 << plane);
982            for row in 0..bmp.height as i32 {
983                let dy = y + row;
984                if dy < 0 || dy >= self.height as i32 {
985                    continue;
986                }
987                // Source columns that land on this plane.
988                let first = (plane - x).rem_euclid(4);
989                let mut run: Vec<u8> = Vec::with_capacity(bmp.width as usize / 4 + 1);
990                let mut run_start = None;
991                let mut sx = first;
992                while sx < bmp.width as i32 {
993                    let dx = x + sx;
994                    let c = bmp.get(sx, row);
995                    if dx >= 0 && dx < self.width as i32 && Some(c) != transparent {
996                        if run_start.is_none() {
997                            run_start = Some(dx as u32 / 4);
998                        }
999                        run.push(c);
1000                    } else if let Some(s) = run_start.take() {
1001                        dos.poke_slice(base + dy as u32 * self.pitch() + s, &run);
1002                        run.clear();
1003                    }
1004                    sx += 4;
1005                }
1006                if let Some(s) = run_start {
1007                    dos.poke_slice(base + dy as u32 * self.pitch() + s, &run);
1008                }
1009            }
1010        }
1011        dos.map_mask(0x0F);
1012    }
1013
1014    /// Copy `len` bytes (x4 planes) from one place in video memory to another with write mode 1:
1015    /// each read fills the latches with four pixels, each write stores them. Four times the
1016    /// bandwidth of a CPU copy; how Mode X games moved backgrounds around.
1017    pub fn copy_vram(&self, dos: &mut Dos, src: u32, dst: u32, len: u32) {
1018        dos.map_mask(0x0F);
1019        dos.write_mode(1);
1020        dos.copy_vram(0xA0000 + src, 0xA0000 + dst, len as usize);
1021        dos.write_mode(0);
1022    }
1023
1024    /// Show the active page and draw on the next one. The start address is written now and
1025    /// latched by the CRTC at the next vertical retrace; waiting for that retrace makes sure
1026    /// we never draw on the page that is still being displayed.
1027    pub fn flip(&mut self, dos: &mut Dos) {
1028        self.visible = self.active;
1029        dos.set_start_address(self.page_offset(self.visible) as u16);
1030        dos.wait_vretrace();
1031        self.active = (self.active + 1) % 2;
1032        if self.active == self.visible {
1033            self.active = (self.active + 1) % 2;
1034        }
1035    }
1036
1037    /// Like [`ModeX::flip`] but cycling through `n` pages (triple buffering).
1038    pub fn flip_n(&mut self, dos: &mut Dos, n: u32) {
1039        self.visible = self.active;
1040        dos.set_start_address(self.page_offset(self.visible) as u16);
1041        dos.wait_vretrace();
1042        self.active = (self.active + 1) % n.clamp(2, self.pages());
1043    }
1044}