diff --git a/src/dev/mgras/gl.rs b/src/dev/mgras/gl.rs index 59b25b22..def62864 100644 --- a/src/dev/mgras/gl.rs +++ b/src/dev/mgras/gl.rs @@ -103,6 +103,10 @@ mod tok { /// glBitmap: format, row length in bits, rows, x / y origin and x / y /// move (floats), data words; the rows follow as FIFO pixel data. pub const BITMAP: u32 = 0x094; + /// Large glBitmap images: header, row batches, then raster advance. + pub const GENERAL_BITMAP: u32 = 0x091; + pub const BITMAP_ROWS: u32 = 0x090; + pub const BITMAP_MOVE: u32 = 0x092; /// glDrawPixels through the GE's pixel path (libGLcore /// __glMgrSendPixels), 8 words: data words per row, two skips, the /// pixel-state counts, rows (provisional), the GE routine (0x49D0 for @@ -455,6 +459,9 @@ pub struct Gl { /// A glBitmap header waiting for its rows (pixel data). bitmap: [u32; 8], bitmap_pending: u32, + /// GENERAL_BITMAP rows are padded to 64 bits and arrive in batches. + bitmap_general: u32, + bitmap_row: u32, /// A SEND_PIXELS header waiting for its rows, and the raster engine's /// transfer mode as the driver last set it (host pixel format). send_pixels: [u32; 8], @@ -467,7 +474,8 @@ pub struct Gl { pub pixel_block: [u32; 2], read_back: u32, /// Between SAVE_RSS and RESTORE_RSS: a GL pixel operation, whose - /// register lists belong inside the raster bracket. + /// register lists belong inside the raster bracket. 2 denotes a + /// framebuffer read, whose DMA bytes need GE component ordering. pub pixel_op: u32, /// Primitive assembly: the open primitive (`prim`), vertices since its /// BEGIN, and the buffered vertices (strips and fans keep the first @@ -1115,8 +1123,28 @@ impl Gl { } tok::BITMAP if d.len() >= 8 => { self.bitmap.copy_from_slice(&d[..8]); + self.bitmap_general = 0; + self.bitmap_row = 0; self.bitmap_pending = 1; } + tok::GENERAL_BITMAP if d.len() >= 5 => { + self.bitmap = [d[0], d[1], d[2], d[3], d[4], 0, 0, 0]; + self.bitmap_general = 1; + self.bitmap_row = 0; + self.bitmap_pending = 0; + } + tok::BITMAP_ROWS if d.len() >= 2 && self.bitmap_general != 0 => { + self.bitmap[7] = d[1]; + self.bitmap_pending = 1; + } + tok::BITMAP_MOVE if d.len() >= 2 => { + if self.raster_valid != 0 { + self.raster[0] += f32::from_bits(d[0]); + self.raster[1] += f32::from_bits(d[1]); + } + self.bitmap_general = 0; + self.bitmap_pending = 0; + } _ => return false, } true @@ -1521,12 +1549,16 @@ impl Gl { self.bitmap_pending != 0 || self.send_pixels_pending != 0 } + pub fn wants_pixel_rows(&self) -> bool { + self.send_pixels_pending != 0 + } + /// Words a pending SEND_PIXELS image needs before it can be drawn or /// loaded (words per row times rows, when the header says; 0 for /// glBitmap, drawn as it comes). pub fn pixel_words_needed(&self) -> usize { if self.send_pixels_pending == 0 { - return 0; + return if self.bitmap_pending != 0 { self.bitmap[7] as usize } else { 0 }; } let rows = (self.send_pixels[3] + 3 * self.send_pixels[4]) as usize; (self.send_pixels[0] as usize * rows).min(1 << 22) @@ -1538,6 +1570,7 @@ impl Gl { /// (raster interface register 5, see hq3.rs). The raster state stays /// loaded until RESTORE_RSS. fn get_pixels(&mut self, sink: &mut dyn Hq3Sink) { + self.pixel_op = 2; self.begin_raster(sink); let depth = (self.xfrmode >> 4) & 0xF == 2 && self.xfrmode & 0xF == 3; let drb = if depth { @@ -1564,20 +1597,31 @@ impl Gl { /// zoom) are taken as identity. fn draw_pixels(&mut self, words: &[u32], sink: &mut dyn Hq3Sink) { self.send_pixels_pending = 0; - let wpr = self.send_pixels[0] as usize; + let mut wpr = self.send_pixels[0] as usize; if self.raster_valid == 0 || wpr == 0 || words.len() < wpr { return; } // Rows: header words 3 + 3 * 4 (mandel 1 + 0, snoop 2 + 3 * 18 = // 56), else what came. let hdr_rows = (self.send_pixels[3] + 3 * self.send_pixels[4]) as usize; - let rows = if hdr_rows > 0 { hdr_rows.min(words.len() / wpr) } else { words.len() / wpr }; + let mut rows = if hdr_rows > 0 { hdr_rows.min(words.len() / wpr) } else { words.len() / wpr }; let bpp = super::rss::bytes_per_pixel(self.xfrmode) as usize; + // SEND_PIXELS counts transport rows. Narrow StudioPaint tile edges + // arrive flattened (e.g. 6x11 pixels as one 66-word row); 0xDA8 + // still gives the image shape and its offset from the raster position. + let xs = self.tl_rect[4]; + let (image_w, image_h) = ((xs & 0xFFFF) as usize, (xs >> 16) as usize); + if image_w > 0 && image_h > 0 && image_h != rows + && image_w * bpp % 4 == 0 + && image_w * image_h * bpp == wpr * rows * 4 + { + wpr = image_w * bpp / 4; + rows = image_h; + } // The host's pixels may carry fewer components than the raster // engine's format: glCopyPixels draws RGB16 back in RGBA16 (the GE's // pixel pipeline adds alpha). The image's size (pixel state 0xDA8's // transfer size, when its rows match) gives the host's pixel size. - let xs = self.tl_rect[4]; let host_bpp = match (xs & 0xFFFF) as usize { w if w > 0 && (xs >> 16) as usize == rows && wpr * 4 % w == 0 => wpr * 4 / w, _ => bpp, @@ -1690,15 +1734,21 @@ impl Gl { } self.bitmap_pending = 0; let h = self.bitmap; - let (bits, rows) = (h[1] as usize, h[2] as usize); + let bits = h[1] as usize; + let stride = if self.bitmap_general != 0 { bits.div_ceil(64) * 8 } else { bits.div_ceil(8) }; + if stride == 0 { return; } + let rows = if self.bitmap_general != 0 { + (words.len() * 4 / stride).min(h[2].saturating_sub(self.bitmap_row) as usize) + } else { + h[2] as usize + }; let f = |i: usize| f32::from_bits(h[i]); let (xorig, yorig, xmove, ymove) = (f(3), f(4), f(5), f(6)); if self.raster_valid == 0 { return; } let x0 = (self.raster[0] - xorig).floor(); - let y0 = (self.raster[1] - yorig).floor(); - let stride = bits.div_ceil(8); + let y0 = (self.raster[1] - yorig).floor() + self.bitmap_row as f32; let byte = |i: usize| words.get(i / 4).map_or(0, |w| (w >> (24 - 8 * (i % 4))) as u8); let mut v = Wv { z: self.raster[2], c: self.raster_color, cb: self.raster_color, ok: 1, ..Default::default() }; for r in 0..rows { @@ -1731,8 +1781,12 @@ impl Gl { } } } - self.raster[0] += xmove; - self.raster[1] += ymove; + if self.bitmap_general != 0 { + self.bitmap_row += rows as u32; + } else { + self.raster[0] += xmove; + self.raster[1] += ymove; + } } fn point_window(&mut self, v: Wv, sink: &mut dyn Hq3Sink) { diff --git a/src/dev/mgras/hq3.rs b/src/dev/mgras/hq3.rs index b9385072..08202fff 100644 --- a/src/dev/mgras/hq3.rs +++ b/src/dev/mgras/hq3.rs @@ -664,7 +664,7 @@ impl Hq3Engine { return; } f.need -= 1; - if f.pixel { + if f.pixel && !(f.need == 0 && (f.cmd & 0xF_FFFF).div_ceil(4) & 1 != 0) { if let Some(buf) = self.eram_in.as_mut() { buf.push(w); } else if self.gl.wants_bitmap_rows() { @@ -692,6 +692,16 @@ impl Hq3Engine { if self.eram_in.is_none() && self.gl.wants_bitmap_rows() { // An image bigger than one pixel command (255 bytes) comes // in several: wait for all of it. + if self.gl.wants_pixel_rows() { + let n = (cmd & 0xF_FFFF).div_ceil(4) as usize; + let start = self.bitmap_in.len().saturating_sub(n); + let tail = &mut self.bitmap_in[start..]; + let mut bytes: Vec = tail.iter().flat_map(|w| w.to_be_bytes()).collect(); + format_pixels(self.formatter, &mut bytes[..(cmd & 0xF_FFFF) as usize]); + for (w, b) in tail.iter_mut().zip(bytes.chunks_exact(4)) { + *w = u32::from_be_bytes(b.try_into().unwrap()); + } + } if self.bitmap_in.len() >= self.gl.pixel_words_needed() { let rows = std::mem::take(&mut self.bitmap_in); self.gl.bitmap_rows(&rows, sink); @@ -1124,6 +1134,15 @@ impl Hq3Engine { stream.drain(..n).collect() } }; + // Byte RGBA transfers request component reversal with XFRMODE + // bit 22 (libGLcore: GL_RGBA uses 0xC00080, GL_ABGR_EXT + // 0x800080). The RSS exposes packed ABGR. Preserve that + // order for ABGR reads, including Dynamation menu saves. + if matches!(peer, DmaPeer::Raster) && self.gl.pixel_op == 2 && self.gl.xfrmode & 0xFF == 0x80 && self.gl.xfrmode & (1 << 22) != 0 { + for px in bytes.chunks_exact_mut(4) { + px.reverse(); + } + } format_pixels(self.formatter, &mut bytes); for (k, b) in bytes.iter().take(len as usize).enumerate() { let Some(pa) = phys(a + k as u32) else { return }; diff --git a/src/dev/mgras/mgras_tests.rs b/src/dev/mgras/mgras_tests.rs index d5858f07..dc2f63a9 100644 --- a/src/dev/mgras/mgras_tests.rs +++ b/src/dev/mgras/mgras_tests.rs @@ -1248,6 +1248,32 @@ fn gl_bitmap_draws_set_bits_at_the_raster_position() { m.stop_engines(); } +/// StudioTools uses GENERAL_BITMAP for text: rows padded to 64 bits, +/// split across pixel commands, with a separate raster advance at the end. +#[test] +fn gl_general_bitmap_batches_and_raster_advance() { + let m = gl_board([0.0, 0.0, 0.0]); + gl_color4(&m, [1.0, 1.0, 1.0, 1.0]); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_token(&m, 0x91, &[0x1800F, 65, 3, f(2.0), f(1.0)]); + fifo_token(&m, 0x90, &[1, 8]); + fifo_pixel_data(&m, &[0x8000_0000, 0, 0x8000_0000, 0]); + assert_eq!(gl_px(&m, 8, 19), 0, "wait for the complete batch"); + fifo_pixel_data(&m, &[0x4000_0000, 0, 0, 0]); + assert_eq!(gl_px(&m, 8, 19), 0xFF_FFFF); + assert_eq!(gl_px(&m, 72, 19), 0xFF_FFFF, "last bit of the wide row"); + assert_eq!(gl_px(&m, 9, 20), 0xFF_FFFF, "second padded row"); + assert_eq!(gl_px(&m, 8, 21), 0, "third row still pending"); + fifo_token(&m, 0x90, &[1, 4]); + fifo_pixel_data(&m, &[0x2000_0000, 0, 0, 0]); + assert_eq!(gl_px(&m, 10, 21), 0xFF_FFFF, "next batch continues above"); + fifo_token(&m, 0x92, &[f(100.0), f(4.0)]); + fifo_token(&m, 0x94, &[0x1800F, 8, 1, f(0.0), f(0.0), f(0.0), f(0.0), 1]); + fifo_pixel_data(&m, &[0x8000_0000, 0]); + assert_eq!(gl_px(&m, 110, 24), 0xFF_FFFF, "separate raster advance"); + m.stop_engines(); +} + /// A colour-index GL context (INIT_CI, as gr_osview's IRIS GL window /// sends): colours are indices and land in the pixels as 12-bit indices, /// for the colour-index visual's colormap; the clear uses the clear index. @@ -1463,6 +1489,15 @@ fn gl_draw_pixels_zoomed() { /// interface register 5 (0x4009). #[test] fn gl_read_pixels_to_host_memory() { + gl_read_pixels_component_order(true); +} + +#[test] +fn gl_read_pixels_abgr_to_host_memory() { + gl_read_pixels_component_order(false); +} + +fn gl_read_pixels_component_order(rgba: bool) { let m = gl_board([0.0, 0.0, 0.0]); gl_color4(&m, [1.0, 0.5, 0.25, 1.0]); gl_full_quad(&m); @@ -1481,7 +1516,7 @@ fn gl_read_pixels_to_host_memory() { } fifo_token(&m, 0x7F, &[2, 4]); write(&m, 32, CFIFO, 0x8000_0010); - for w in [0x158, 0x0001_0001, 0x159, 0x0088_0080] { + for w in [0x158, 0x0001_0001, 0x159, 0x0088_0080 | if rgba { 1 << 22 } else { 0 }] { write(&m, 32, CFIFO, w); } fifo_token(&m, 0xDB, &[0x4009]); @@ -1492,8 +1527,17 @@ fn gl_read_pixels_to_host_memory() { m.state_hash(); let b = mem.bytes.lock(); let got: Vec = (0..4).map(|i| b.get(&(0x2000 + i)).copied().unwrap_or(0)).collect(); - assert_ne!(got, [0, 0, 0, 0], "the pixel reached host memory (window pixel {px:#x})"); + let mut expected = [px as u8, (px >> 8) as u8, (px >> 16) as u8, 255]; + if !rgba { expected.reverse(); } + assert_eq!(got, expected, "GE requested component order"); drop(b); + // Feed the readback to glDrawPixels: repeated UI background copies + // must preserve their colour rather than rotate alpha into red. + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_token(&m, 0x8D, &[1, 0, 0, 1, 0, 1, 0x49D0, 0x99]); + fifo_token(&m, 0xC00, &[if rgba { 0xE0D } else { 0xE8D }]); + fifo_pixel_data(&m, &[u32::from_be_bytes(got.try_into().unwrap())]); + assert_eq!(gl_px(&m, 10, 20), px, "read/draw round trip"); m.stop_engines(); } @@ -2358,6 +2402,44 @@ fn gl_draw_pixels_rgba8_component_order() { m.stop_engines(); } +#[test] +fn gl_draw_pixels_flat_transport_keeps_image_shape_and_offset() { + for (words_per_row, transport_rows) in [(6, 1), (2, 3)] { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x7F, &[1, 2]); + fifo_pixel_data(&m, &[0x159, 0x00C1_0080]); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + // StudioPaint sends narrow tile edges as one transport row, with the + // two-dimensional image rectangle still in GE pixel state. + fifo_token(&m, 0xCD, &[1, 2, 0x18, 0xDA8, 0, 2, 6, 5, 7, 7, 8, 0x2_0003, 1]); + fifo_token(&m, 0x8D, &[words_per_row, 0, 0, transport_rows, 0, 1, 0x49D0, 0x18]); + fifo_pixel_data(&m, &[0xFF00_00FF, 0x00FF_00FF, 0x0000_FFFF, + 0xFFFF_00FF, 0x00FF_FFFF, 0xFF00_FFFF]); + let got: Vec = [(15, 27), (16, 27), (17, 27), (15, 28), (16, 28), (17, 28)] + .iter().map(|&(x, y)| gl_px(&m, x, y)).collect(); + assert_eq!(got, [0xFF, 0xFF00, 0xFF0000, 0xFFFF, 0xFFFF00, 0xFF00FF]); + assert_eq!(gl_px(&m, 10, 20), 0, "transport shape must not overwrite the raster origin"); + assert_eq!(gl_px(&m, 18, 27), 0, "image ends at its rectangle edge"); + m.stop_engines(); + } +} + +#[test] +fn gl_fifo_abgr_rows_ignore_doubleword_padding() { + let m = gl_board([0.0, 0.0, 0.0]); + fifo_token(&m, 0x7F, &[1, 2]); + fifo_pixel_data(&m, &[0x159, 0x00C1_0080]); + fifo_token(&m, 0x38, &[f(10.0), f(20.0), f(0.0)]); + fifo_dma(&m, 0xC00, 0xE8D); + fifo_token(&m, 0x8D, &[3, 0, 0, 2, 0, 1, 0x49D0, 0x99]); + fifo_pixel_data(&m, &[0xFF00_0000, 0xFF56_3412, 0xFF00_00FF]); + fifo_pixel_data(&m, &[0xFF00_FF00, 0xFFFF_0000, 0xFFFF_FFFF]); + let got: Vec = [(10, 20), (11, 20), (12, 20), (10, 21), (11, 21), (12, 21)] + .iter().map(|&(x, y)| gl_px(&m, x, y)).collect(); + assert_eq!(got, [0, 0x56_3412, 0xFF, 0xFF00, 0xFF0000, 0xFFFFFF]); + m.stop_engines(); +} + /// glCopyPixels' write half by host DMA (Maya copies the front buffer to /// the back after a full redraw, then redraws only what changes): the /// image size from pixel state 0xDA8, the transfer mode from the RSS diff --git a/src/dev/mgras/rss.rs b/src/dev/mgras/rss.rs index eec39f5e..c9f2d1b3 100644 --- a/src/dev/mgras/rss.rs +++ b/src/dev/mgras/rss.rs @@ -415,7 +415,7 @@ mod block { /// Whether the pixel processors' pixel type (fill mode bits 10:8) is an RGB /// one; the others are colour index. pub(super) fn rgb_pixtype(pp1fillmode: u32) -> bool { - matches!((pp1fillmode >> 8) & 7, 0 | 1 | 2 | 4) + matches!((pp1fillmode >> 8) & 7, 0 | 1 | 2) } /// Pixel processor logic op (fill mode bit 2 enables it; bits 29:26 hold @@ -1042,30 +1042,16 @@ impl Rss { } else { v }; - // Plane write masks where the pixel format matches our storage - // (pixel type 2, RGBA8888: R G B in the low 24 planes, alpha in the - // top 8): ColorMaskLSBsA the low 24 planes (all 32 when it is all - // ones), ColorMaskMSBs the top 8. Overlay writes: the X server - // sends ColorMaskLSBsA 0 and ColorMaskMSBs 0xF0 (overlay visuals, - // values 0-15) or 0x70 (popup menus, values 0-3, colormap entries - // 0-3); read as the high nibble masking overlay planes 3:0 (our - // reading: the stored values stay as the X server wrote them). - // Other formats keep their decoded values: their masks describe - // a packed storage layout this model does not keep. + // Native GL masks name overlay index bits. X uses packed PP1 plane + // masks; its decoded CI8 indices include values above the low nibble. let v = if b.kind == Kind::Overlay { - // Native GL CI8 overlays select the upper overlay planes - // (DRAW_BUFFER 0x48). X's 4-bit overlay/popup selector 0x4f - // presents those planes as indices 0..15. - let mask = if draw_buffer(pp1) == 0x48 { - self.reg(reg::COLORMASKMSBS) & 0xFF - } else { - (self.reg(reg::COLORMASKMSBS) >> 4) & 0xF - }; + let msbs = self.reg(reg::COLORMASKMSBS); + let mask = if draw_buffer(pp1) == 0x48 { msbs & 0xFF } else if msbs != 0 { 0xFF } else { 0 }; (old & !mask) | (v & mask) } else if b.kind == Kind::Wide && (pp1 >> 8) & 7 == 2 { let mask = if lsb == u32::MAX { lsb } else { lsb & 0xFF_FFFF | (self.reg(reg::COLORMASKMSBS) & 0xFF) << 24 }; (old & !mask) | (v & mask) - } else if b.kind == Kind::Wide && (pp1 >> 8) & 7 == 6 { + } else if b.kind == Kind::Wide && matches!((pp1 >> 8) & 7, 4 | 6) { let mask = lsb & 0xFFF; (old & !mask) | (v & mask) } else { @@ -1956,6 +1942,47 @@ mod tests { r.write(reg::BLOCKXYENDI, x1 << 16 | y1, true); } + #[test] + fn marquee_ci12_fill_and_stipple_preserve_indices_and_masks() { + let mut r = x_server(); + r.write(reg::PP1FILLMODE, 0x0C00_4404, false); + r.write(reg::COLORMASKLSBSA, 0xFFF, false); + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 3, false); + block(&mut r, 15, 671, 16, 671); + assert_eq!(px(&r, 15, 671), 3); + r.write(reg::FILLMODE, 1 << 22 | FILL_CHAR_STIPPLE, false); + r.write(reg::RED, 0xABC << 12, false); + block(&mut r, 15, 671, 16, 671); + r.write(reg::CHAR_H, 0x8000_0000, true); + assert_eq!(px(&r, 15, 671), 0xABC); + assert_eq!(px(&r, 16, 671), 3); + r.write(reg::COLORMASKLSBSA, 0xF, false); + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 5, false); + block(&mut r, 15, 671, 15, 671); + assert_eq!(px(&r, 15, 671), 0xAB5); + } + + #[test] + fn maya_popup_hover_preserves_upper_overlay_color_bits() { + let mut r = x_server(); + r.write(reg::PP1FILLMODE, 0x0C13_F504, false); + r.write(reg::DRBPOINTERS, 0x101C0, false); + r.write(reg::COLORMASKLSBSA, 0, false); + r.write(reg::COLORMASKMSBS, 0x70, false); + let b = r.target(); + let y = (SCREEN_H - 1 - 139) as u32; + r.mem.put(&b, 18, y, 4); + r.write(reg::FILLMODE, FILL_FAST, false); + r.write(reg::FILL_COLOR_R, 0x10, false); + block(&mut r, 18, 139, 18, 139); + assert_eq!(r.mem.get(&b, 18, y), 0x10); + r.write(reg::FILL_COLOR_R, 0, false); + block(&mut r, 18, 139, 18, 139); + assert_eq!(r.mem.get(&b, 18, y), 0); + } + #[test] fn fast_fill_lands_top_down_with_yflip() { let mut r = x_server(); diff --git a/src/dev/mgras/rss_jit/mod.rs b/src/dev/mgras/rss_jit/mod.rs index 35b18178..cfbce254 100644 --- a/src/dev/mgras/rss_jit/mod.rs +++ b/src/dev/mgras/rss_jit/mod.rs @@ -265,10 +265,10 @@ fn target(rss: &Rss, b: Buffer, back: bool) -> Target { let wide = rss::rgb_pixtype(pp1) || lsb == 0xFF_FFFF || lsb == u32::MAX; let ptype = (pp1 >> 8) & 7; let mask = if b.kind == Kind::Overlay { - if rss::draw_buffer(pp1) == 0x48 { msbs & 0xFF } else { (msbs >> 4) & 0xF } + if rss::draw_buffer(pp1) == 0x48 { msbs & 0xFF } else if msbs != 0 { 0xFF } else { 0 } } else if ptype == 2 { if lsb == u32::MAX { lsb } else { lsb & 0xFF_FFFF | (msbs & 0xFF) << 24 } - } else if ptype == 6 { + } else if matches!(ptype, 4 | 6) { lsb & 0xFFF } else { u32::MAX @@ -313,7 +313,7 @@ fn common(rss: &mut Rss) -> PipeKey { let pix = match ptype { 0 if pp1 & (1 << 13) == 0 => Pix::Rgb12, 2 => Pix::Rgba8, - 6 => Pix::Ci12, + 4 | 6 => Pix::Ci12, _ => Pix::Plain, }; let winmode = rss.reg(reg::PP1WINMODE); diff --git a/src/dev/ps2.rs b/src/dev/ps2.rs index 059dfbe2..f7debf3b 100644 --- a/src/dev/ps2.rs +++ b/src/dev/ps2.rs @@ -30,6 +30,7 @@ enum CommandState { SetTypematic, MouseData, // consuming one data byte for a mouse command (e.g. F3 sample rate, E8 resolution) AuxLoop, // 0xD3: next data-port byte is echoed back as an AUX (mouse-source) byte + SetKeyMode(u8), // set-3 per-key typematic/break mode } struct Ps2State { @@ -47,6 +48,9 @@ struct Ps2State { mouse_id: u8, // Last two sample rates written, to detect knock sequences sample_rate_history: [u8; 2], + /// Set-3 key modes: bit 0 permits typematic makes, bit 1 permits breaks. + key_modes: [u8; 256], + held_keys: [bool; 256], } /// Combined PS/2 Keyboard and Mouse Controller @@ -76,6 +80,8 @@ impl Ps2Controller { last_read: 0xAA, // pretend we finished BAT at startup mouse_id: 0, sample_rate_history: [0, 0], + key_modes: [3; 256], + held_keys: [false; 256], }), callback, running: AtomicBool::new(false), @@ -204,6 +210,18 @@ impl Ps2Controller { } } } else { + // Set-3 per-key mode commands accept scan codes until the next + // command. IRIX exempts Caps Lock and Num Lock from typematic. + if let CommandState::SetKeyMode(mode) = state.command_state { + if val < 0xED { + state.key_modes[val as usize] = mode; + state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); + drop(state); + self.update_interrupt(); + return; + } + state.command_state = CommandState::Idle; + } match state.command_state { CommandState::Idle => match val { 0xFF => { @@ -211,6 +229,8 @@ impl Ps2Controller { state.rx_queue.clear(); state.mouse_queue_bytes = 0; state.led_state = 0; state.scancode_set = 2; + state.key_modes = [3; 256]; + state.held_keys = [false; 256]; state.scanning_enabled = false; // reset leaves scanning disabled; PROM enables via F4 state.command_state = CommandState::Idle; state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); // ACK @@ -236,7 +256,7 @@ impl Ps2Controller { state.scanning_enabled = false; state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); } - 0x14 => { + 0xF3 | 0x14 => { if dbg { dlog!(LogModule::Ps2, "PS2: Keyboard Set Typematic Rate/Delay <- {:02x}", val); } state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); state.command_state = CommandState::SetTypematic; @@ -253,20 +273,24 @@ impl Ps2Controller { } 0xF6 => { if dbg { dlog!(LogModule::Ps2, "PS2: Keyboard Reset to Defaults <- {:02x}", val); } + state.key_modes = [3; 256]; + state.held_keys = [false; 256]; state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); } - 0xFC => { - if dbg { dlog!(LogModule::Ps2, "PS2: Keyboard Reset and Disable <- {:02x}", val); } - state.scanning_enabled = false; + 0xF7..=0xFA => { + let mode = match val { 0xF7 => 1, 0xF8 => 2, 0xF9 => 0, _ => 3 }; + state.key_modes.fill(mode); + state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); + } + 0xFB..=0xFD => { + let mode = match val { 0xFB => 1, 0xFC => 2, _ => 0 }; + state.command_state = CommandState::SetKeyMode(mode); state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); } 0x76 => { if dbg { dlog!(LogModule::Ps2, "PS2: Keyboard Reset to Defaults <- {:02x}", val); } state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); } - 0xFA => { - // ACK echoed back by PROM — ignore silently - } _ => { if dbg { dlog!(LogModule::Ps2, "PS2: Keyboard unsupported <- {:02x}", val); } state.rx_queue.push_back((0xFA, Ps2Source::Keyboard)); // Default ACK @@ -307,6 +331,7 @@ impl Ps2Controller { state.command_state = CommandState::Idle; } CommandState::AuxLoop => unreachable!("handled earlier in write_data"), + CommandState::SetKeyMode(_) => unreachable!("handled before keyboard commands"), } } drop(state); @@ -471,6 +496,15 @@ impl Ps2Controller { 3 => { if let Some(scancode) = self.map_keycode_set3(key) { let val = scancode as u8; + let mode = state.key_modes[val as usize]; + let held = state.held_keys[val as usize]; + state.held_keys[val as usize] = pressed; + // The host supplies typematic makes. X disables them + // with F8 and manages repeat itself, including keys + // whose repeat applications disable during a drag. + if (pressed && held && mode & 1 == 0) || (!pressed && mode & 2 == 0) { + return; + } if !pressed { state.rx_queue.push_back((0xF0, Ps2Source::Keyboard)); if crate::devlog::devlog_is_active(LogModule::Ps2) { @@ -1059,6 +1093,8 @@ impl Resettable for Ps2Controller { state.last_read = 0xAA; state.mouse_id = 0; state.sample_rate_history = [0, 0]; + state.key_modes = [3; 256]; + state.held_keys = [false; 256]; } } @@ -1089,8 +1125,14 @@ impl Saveable for Ps2Controller { CommandState::SetTypematic => 4, CommandState::MouseData => 5, CommandState::AuxLoop => 6, + CommandState::SetKeyMode(_) => 7, }; tbl.insert("command_state".into(), toml::Value::Integer(cmd_state)); + if let CommandState::SetKeyMode(mode) = state.command_state { + tbl.insert("key_mode".into(), hex_u8(mode)); + } + tbl.insert("key_modes".into(), toml::Value::Array(state.key_modes.iter().map(|v| hex_u8(*v)).collect())); + tbl.insert("held_keys".into(), toml::Value::Array(state.held_keys.iter().map(|v| toml::Value::Boolean(*v)).collect())); tbl.insert("scanning_enabled".into(), toml::Value::Boolean(state.scanning_enabled)); tbl.insert("mouse_enabled".into(), toml::Value::Boolean(state.mouse_enabled)); @@ -1138,10 +1180,20 @@ impl Saveable for Ps2Controller { 4 => CommandState::SetTypematic, 5 => CommandState::MouseData, 6 => CommandState::AuxLoop, + 7 => CommandState::SetKeyMode(get_field(v, "key_mode").and_then(toml_u8).unwrap_or(3) & 3), _ => CommandState::Idle, }; } + state.key_modes = [3; 256]; + state.held_keys = [false; 256]; + if let Some(toml::Value::Array(arr)) = get_field(v, "key_modes") { + for (dst, src) in state.key_modes.iter_mut().zip(arr) { *dst = toml_u8(src).unwrap_or(3) & 3; } + } + if let Some(toml::Value::Array(arr)) = get_field(v, "held_keys") { + for (dst, src) in state.held_keys.iter_mut().zip(arr) { *dst = toml_bool(src).unwrap_or(false); } + } + if let Some(x) = get_field(v, "scanning_enabled") { state.scanning_enabled = toml_bool(x).unwrap_or(false); } if let Some(x) = get_field(v, "mouse_enabled") { state.mouse_enabled = toml_bool(x).unwrap_or(false); } if let Some(x) = get_field(v, "last_read") { state.last_read = toml_u8(x).unwrap_or(0xAA); } @@ -1161,6 +1213,51 @@ impl Saveable for Ps2Controller { mod tests { use super::*; + #[test] + fn set3_make_break_suppresses_host_repeat_and_preserves_release() { + let ps2 = Ps2Controller::new(None); + ps2.start(); + for b in [0xF0, 3, 0xF4, 0xF8] { + ps2.write_data(b); + assert_eq!(ps2.read_data(), 0xFA); + } + let scan = ps2.map_keycode_set3(KeyCode::Space).unwrap() as u8; + ps2.push_kb(KeyCode::Space, true); + for _ in 0..10 { ps2.push_kb(KeyCode::Space, true); } + ps2.push_kb(KeyCode::Space, false); + assert_eq!(ps2.state.lock().rx_queue.len(), 3); + assert_eq!([ps2.read_data(), ps2.read_data(), ps2.read_data()], [scan, 0xF0, scan]); + ps2.push_kb(KeyCode::Space, true); + assert_eq!(ps2.read_data(), scan, "press again after release"); + } + + #[test] + fn set3_per_key_modes_and_snapshot_preserve_console_repeat() { + let ps2 = Ps2Controller::new(None); + ps2.start(); + let space = ps2.map_keycode_set3(KeyCode::Space).unwrap() as u8; + for b in [0xF0, 3, 0xFA, 0xFC, space, 0xF4] { + ps2.write_data(b); + assert_eq!(ps2.read_data(), 0xFA); + } + ps2.push_kb(KeyCode::Space, true); + let saved = ps2.save_state(); + let dst = Ps2Controller::new(None); + dst.start(); + dst.load_state(&saved).unwrap(); + assert_eq!(saved, dst.save_state()); + dst.push_kb(KeyCode::Space, true); + dst.push_kb(KeyCode::Space, false); + assert_eq!(dst.state.lock().rx_queue.len(), 3, "a key in make/break mode does not repeat"); + assert!(dst.state.lock().scanning_enabled, "FC sets a key mode; it does not disable scanning"); + while dst.read_status() & 1 != 0 { dst.read_data(); } + let a = dst.map_keycode_set3(KeyCode::KeyA).unwrap() as u8; + dst.push_kb(KeyCode::KeyA, true); + dst.push_kb(KeyCode::KeyA, true); + dst.push_kb(KeyCode::KeyA, false); + assert_eq!([dst.read_data(), dst.read_data(), dst.read_data(), dst.read_data()], [a, a, 0xF0, a], "console typematic remains enabled"); + } + /// Phase 1.7 round-trip: a fresh PS/2 controller loaded from a captured /// save_state must re-serialize byte-identically. Mutates rx_queue, /// command_state, and assorted flag/byte fields so the test exercises every