diff --git a/src/dev/gr2/gl.rs b/src/dev/gr2/gl.rs index 7520084da..085255a00 100644 --- a/src/dev/gr2/gl.rs +++ b/src/dev/gr2/gl.rs @@ -215,6 +215,14 @@ pub enum PixZoom { /// READ_DONE. Unimplemented, snoop spun on FIN3 for its whole time slice. pub const T_READ_RECT: u32 = 0x0ad; pub const T_IRIS_SBOXF: u32 = 0x053; +/// sbox / sboxi / sboxs (gl_i_sbox*): a screen-aligned box outline, the +/// corners as for 0x053 (f32; integers on ITOF|0x052). Blender draws its +/// interface lines as degenerate ones. +pub const T_IRIS_SBOX: u32 = 0x052; +pub const T_IRIS_SBOXI: u32 = 0x4052; +/// Aux (overlay / popup) plane write mask (__glExpSetPixWritemask; IRIS GL +/// drawmode): the aux bits drawing may change, 0 in normal drawing. +pub const T_AUX_WRITEMASK: u32 = 0x003; /// swaptmesh (gl_i_swaptmesh): swap the two vertices a tmesh (bgntmesh = /// 0x046, LOADV|0x047) keeps; the next vertex makes a triangle with them. pub const T_IRIS_SWAPTMESH: u32 = 0x04b; @@ -222,8 +230,10 @@ pub const T_IRIS_SBOXFI: u32 = 0x4053; pub const T_IRIS_CHAR16: u32 = 0x069; pub const T_IRIS_CHAR16_TALL: u32 = 0x06a; pub const T_IRIS_CHAR32: u32 = 0x06d; -/// Frame setup: 0x008 = 0, back buffer (0/1); 0x005 = colour write mask -/// for it (0x000FFF / 0xFFF000 for 12-bit double buffering). +/// readsource (gl_setreadsource): 0x008 = plane (0 colour, 1 overlay / +/// popup, 2 and up other planes), then the absolute bank (0/1). 0x005 is +/// the colour write mask, also absolute (0x000FFF / 0xFFF000 for 12-bit +/// double buffering). pub const T_IRIS_BUFFER: u32 = 0x008; pub const T_IRIS_WRITEMASK: u32 = 0x005; /// clear() with the current colour; zclear(value). @@ -454,6 +464,12 @@ pub struct GlState { st_bits: u32, /// Pixel word packing (0x0A7): 0 = 32-bit, 1 = 2 x 16, 2 = 4 x 8. pix_pack: u32, + /// Drawing into the overlay / popup planes (IRIS GL drawmode OVERDRAW / + /// PUPDRAW): the MakeCurrent mode (8 = 4-bit overlay, 11 = 2-bit + /// overlay or popup, as the X server's 2D MODE numbers them), else 0; + /// and the aux plane write mask (0x003). + aux_draw: u32, + aux_wmask: u32, /// Spare (keeps the count of 4-byte fields even, see st_bits). pad: u32, /// Vertices drawn / primitives emitted since the last trace note. @@ -462,6 +478,19 @@ pub struct GlState { impl GlState { /// Range of the R channel in GL colour units (see to_fixed). + /// UAUXDATA for colour index `idx` in overlay / popup drawing: 4-bit + /// overlay takes it as is; the 2-bit planes take it shifted up when the + /// mask names aux bits 3:2 (popup), as the 2D path's value2d does. + fn aux_value(&self, idx: u32) -> u32 { + if self.aux_draw == 8 { + idx & 0xf + } else if self.aux_wmask & 0xc != 0 { + (idx << 2) & 0xc + } else { + idx & 3 + } + } + fn cmax(&self) -> f32 { if self.pixfmt == re3::PIXFMT_CI12 { 4095.0 / 255.0 } else { 1.0 } } @@ -976,7 +1005,8 @@ impl Hq2Engine { T_BITMAP => (BITMAP_HDR + 9) as u32, T_BITMAP_TALL => (BITMAP_HDR + 17) as u32, T_BITMAP_HUGE => (BITMAP_HDR + 33) as u32, - T_IRIS_SBOXF | T_IRIS_SBOXFI => 4, + T_IRIS_SBOXF | T_IRIS_SBOXFI | T_IRIS_SBOX | T_IRIS_SBOXI => 4, + T_AUX_WRITEMASK => 1, T_IRIS_SWAPTMESH => 1, T_CLIP_PLANE_ENABLE => 2, T_CLIP_PLANE => 5, @@ -1268,7 +1298,10 @@ impl Hq2Engine { T_STIPPLE_OFF => g.stipple_on = 0, T_BLEND_MODE => g.blend_fast = a[0] & 1, T_IRIS_DB => g.lt.on = a[1] & 1, - T_IRIS_BUFFER | T_IRIS_BGNPOLYGON | T_IRIS_PMV => {} + // readsource (gl_setreadsource): plane, then the absolute bank + // (libgl tracks swaps itself and sends no 0x1E7, so swap stays 0). + T_IRIS_BUFFER => g.read_src = [a[0], a[1]], + T_IRIS_BGNPOLYGON | T_IRIS_PMV => {} T_IRIS_WRITEMASK => g.masks = [a[0] & 0x00ff_ffff; 2], T_IRIS_CLEAR => { let c = g.color; @@ -1311,6 +1344,8 @@ impl Hq2Engine { g.pv.swap(0, 1); g.tflip ^= 1; } + T_AUX_WRITEMASK => g.aux_wmask = a[0] & 0xf, + T_IRIS_SBOX | T_IRIS_SBOXI => self.gl_sbox(cmd == T_IRIS_SBOXI, &a, out), T_IRIS_SBOXF | T_IRIS_SBOXFI => { let num = |w: u32| if cmd == T_IRIS_SBOXFI { w as i32 as f32 } else { f(w) }; let p = g.transform([num(a[0]), num(a[1]), 0.0, 1.0]); @@ -1441,15 +1476,24 @@ impl Hq2Engine { // Visual code from the kernel (inferred from 0, 1, 2, 4, 9, 10 in // traces and the DDX's matching 2D MODE numbers): bit 3 = colour // index, low bits = depth (0 = 4, 1 = 8, 2 = 12, 4 = 24 bits). - T_MAKECURRENT => match a[0] { - 0 => g.pixfmt = re3::PIXFMT_RGB4, - 4 => g.pixfmt = 0, - 2 => g.pixfmt = re3::PIXFMT_RGB12, - 1 => g.pixfmt = re3::PIXFMT_RGB8, - 10 => g.pixfmt = re3::PIXFMT_CI12, - 9 => g.pixfmt = re3::PIXFMT_CI8, - _ => {} - }, + T_MAKECURRENT => { + g.aux_draw = 0; + match a[0] { + // Overlay / popup planes: the colour index goes to the aux + // bits (gl_setup), the colour planes are left alone. + 8 | 11 => { + g.aux_draw = a[0]; + g.pixfmt = re3::PIXFMT_CI8; + } + 0 => g.pixfmt = re3::PIXFMT_RGB4, + 4 => g.pixfmt = 0, + 2 => g.pixfmt = re3::PIXFMT_RGB12, + 1 => g.pixfmt = re3::PIXFMT_RGB8, + 10 => g.pixfmt = re3::PIXFMT_CI12, + 9 => g.pixfmt = re3::PIXFMT_CI8, + _ => {} + } + } T_CLEAR_CI => self.gl_clear([f(a[0]) / 255.0, 0.0, 0.0, 1.0], out), T_CLEAR_CI_DEPTH | T_CLEAR_CI_DEPTH_I => { let i = if cmd == T_CLEAR_CI_DEPTH_I { a[0] as i32 as f32 } else { f(a[0]) }; @@ -1549,7 +1593,13 @@ impl Hq2Engine { light::T_FOG_ON => format!("GL_FOG {}", if g.lt.fog_on != 0 { "on" } else { "off" }), light::T_MATERIAL_COMMIT => "GL_MATERIAL_COMMIT".to_string(), T_BLEND_MODE => format!("GL_BLEND_MODE {}", a[0]), - T_IRIS_BUFFER => format!("IRISGL_BUFFER {} {}", a[0], a[1]), + T_IRIS_BUFFER => format!("IRISGL_READSOURCE plane {} bank {}", a[0], a[1]), + T_AUX_WRITEMASK => format!("GL_AUX_WRITEMASK {:#x}", a[0]), + T_IRIS_SBOX | T_IRIS_SBOXI | T_IRIS_SBOXF | T_IRIS_SBOXFI => { + let num = |w: u32| if cmd & 0x4000 != 0 { w as i32 as f32 } else { f(w) }; + format!("IRISGL_{} ({}, {})-({}, {})", if cmd & 0xff == T_IRIS_SBOX { "SBOX" } else { "SBOXF" }, + num(a[0]), num(a[1]), num(a[2]), num(a[3])) + } T_IRIS_WRITEMASK => format!("IRISGL_WRITEMASK {:#08x}", a[0]), T_IRIS_CLEAR => format!("IRISGL_CLEAR rgb ({:.3}, {:.3}, {:.3}) rect {:?}", g.color[0], g.color[1], g.color[2], g.clip_rect()), T_IRIS_ZCLEAR => format!("IRISGL_ZCLEAR {:#x}", a[0]), @@ -1661,8 +1711,16 @@ impl Hq2Engine { self.gl.blend_sent = 1; } out.reg(re3::REG_NOPUP, 1); - out.reg(re3::REG_UAUXDATA, 0); - out.reg(re3::REG_AUXMASK, 0); + if self.gl.aux_draw != 0 { + // Overlay / popup drawing: only the aux planes 0x003 names, the + // index as the X server's 2D path writes it (value2d). + out.reg(re3::REG_AUXMASK, self.gl.aux_wmask); + let idx = (self.gl.color[0] * 255.0).round() as u32; + out.reg(re3::REG_UAUXDATA, self.gl.aux_value(idx)); + } else { + out.reg(re3::REG_UAUXDATA, 0); + out.reg(re3::REG_AUXMASK, 0); + } if self.gl.wid_clip() { // More visible pieces than 0x1E5 carries: draw only where the // CID planes (painted by Xsgi, 2D_CID_WRITE) hold this window's @@ -1675,7 +1733,7 @@ impl Hq2Engine { } // Stipple patterns are span-relative (see gl_stipple_pattern). out.reg(re3::REG_ALIGNPAT, 0); - let mask = self.gl.masks[self.gl.swap as usize & 1] & self.gl.colormask; + let mask = if self.gl.aux_draw != 0 { 0 } else { self.gl.masks[self.gl.swap as usize & 1] & self.gl.colormask }; out.reg(re3::REG_PIXMASK, mask & 0x00ff_ffff); // Always: a 2D 12-bit draw (MODE 2) may have left RGB12 behind. out.op(re3::RE3_OP_PIXFMT, self.gl.pixfmt as u64); @@ -1703,6 +1761,11 @@ impl Hq2Engine { /// Undo GL-only RE3 state so 2D drawing sees what it expects. fn gl_done(&mut self, out: &mut dyn Re3Sink) { + if self.gl.aux_draw != 0 { + // 2D drawing keeps AUXMASK only for its own aux modes. + out.reg(re3::REG_AUXMASK, 0); + out.reg(re3::REG_UAUXDATA, 0); + } if self.gl.pixfmt != 0 { out.op(re3::RE3_OP_PIXFMT, 0); out.reg(re3::REG_ENABDITH, 0); @@ -1774,6 +1837,9 @@ impl Hq2Engine { } fn gl_color_regs(&mut self, c: [u32; 3], out: &mut dyn Re3Sink) { + if self.gl.aux_draw != 0 { + out.reg(re3::REG_UAUXDATA, self.gl.aux_value(c[0] >> 11)); + } out.reg(re3::REG_R, c[0]); out.reg(re3::REG_G, c[1]); out.reg(re3::REG_B, c[2]); @@ -1968,6 +2034,40 @@ impl Hq2Engine { self.gl_done(out); } + /// 0x052 sbox: the outline of the screen-aligned box between two + /// transformed corners, in the current colour, each pixel once (top + /// and bottom rows, then the sides between them). + fn gl_sbox(&mut self, ints: bool, a: &[u32], out: &mut dyn Re3Sink) { + let num = |w: u32| if ints { w as i32 as f32 } else { f(w) }; + let p = self.gl.transform([num(a[0]), num(a[1]), 0.0, 1.0]); + let q = self.gl.transform([num(a[2]), num(a[3]), 0.0, 1.0]); + if p.ok == 0 || q.ok == 0 { + return; + } + let (x0, x1) = (p.x.min(q.x).floor() as i32, p.x.max(q.x).floor() as i32); + let (y0, y1) = (p.y.min(q.y).floor() as i32, p.y.max(q.y).floor() as i32); + self.gl_setup(out); + let c = self.gl.color; + self.gl_color_regs(to_fixed(c, self.gl.cmax()), out); + let mut run = |e: &mut Self, xa: i32, xb: i32, y: i32| { + let (pieces, np) = e.gl.span_pieces(y, xa, xb + 1); + for &(s, t) in &pieces[..np] { + e.span(s, y, (t - s) as u32, None, out); + } + }; + run(self, x0, x1, y0); + if y1 > y0 { + run(self, x0, x1, y1); + } + for y in y0 + 1..y1 { + run(self, x0, x0, y); + if x1 > x0 { + run(self, x1, x1, y); + } + } + self.gl_done(out); + } + fn gl_point(&mut self, v: Wv, color: [f32; 4], out: &mut dyn Re3Sink) { if v.ok == 0 || v.oc != 0 { return; diff --git a/src/dev/gr2/hq2.rs b/src/dev/gr2/hq2.rs index 3e7f0bd0d..d4c47bc0c 100644 --- a/src/dev/gr2/hq2.rs +++ b/src/dev/gr2/hq2.rs @@ -186,6 +186,9 @@ pub const HQ2_2D_POLYLINE: u32 = 302; pub const HQ2_2D_POLYLINE_ALT: u32 = 357; /// Segments: stream of (x1, y1, x2, y2), padded with (1280, 1024) pairs. pub const HQ2_2D_SEGMENTS: u32 = 345; +/// Points (expPolyPoint, XDrawPoints): 0; stream of (x, y), padded with +/// (1280, 1024) pairs, clipped to LINE_CLIP. +pub const HQ2_2D_POLYPOINT: u32 = 353; /// Stipple opaque flag (1 = zero bits get bg). pub const HQ2_2D_STIPPLE_AUX: u32 = 313; /// Stippled box: 0; DATA origin x, x1, origin y, y1, x2, y2. @@ -392,7 +395,7 @@ pub fn token_name(index: u32) -> Option<&'static str> { HQ2_2D_COLOR_AUX => "2D_COLOR_AUX", HQ2_2D_COLOR_OFF => "2D_COLOR_OFF", HQ2_2D_COLOR_ON => "2D_COLOR_ON", 313 => "2D_STIPPLE_AUX", HQ2_2D_TILE_SETUP => "2D_TILE_SETUP", 315..=318 => "2D_TILE_DATA", HQ2_2D_STIPPLE_BOX_POW2 | HQ2_2D_STIPPLE_BOX => "2D_STIPPLE_BOX", HQ2_2D_TILE_RECT | HQ2_2D_TILE_RECT_ODD => "2D_TILE_RECT", - HQ2_2D_POLYLINE => "2D_POLYLINE", HQ2_2D_POLYLINE_ALT => "2D_POLYLINE", HQ2_2D_SEGMENTS => "2D_SEGMENTS", + HQ2_2D_POLYLINE => "2D_POLYLINE", HQ2_2D_POLYLINE_ALT => "2D_POLYLINE", HQ2_2D_SEGMENTS => "2D_SEGMENTS", HQ2_2D_POLYPOINT => "2D_POLYPOINT", 323 => "2D_GLYPH_8", 324 => "2D_GLYPH_16", 325 => "2D_GLYPH_32", 326 => "2D_GLYPH_64", HQ2_2D_LINE_MODE => "2D_LINE_MODE", HQ2_2D_LINE_CLIP => "2D_LINE_CLIP", HQ2_2D_MODE => "2D_MODE", HQ2_2D_ROP => "2D_ROP", HQ2_2D_CID_WRITE => "2D_CID_WRITE", HQ2_2D_BUF_SELECT => "2D_BUF_SELECT", @@ -679,7 +682,8 @@ impl Hq2Engine { HQ2_2D_LINE_CLIP => 4, HQ2_2D_STIPPLE_RECT | HQ2_2D_STIPPLE_BOX_POW2 | HQ2_2D_STIPPLE_BOX => 7, HQ2_2D_STIPPLED_SPAN_A | HQ2_2D_STIPPLED_SPAN_B => STREAM, - HQ2_2D_POLY_SPAN | HQ2_2D_POLYLINE | HQ2_2D_POLYLINE_ALT | HQ2_2D_SEGMENTS | HQ2_2D_LINE_SEG => STREAM, + HQ2_2D_POLY_SPAN | HQ2_2D_POLYLINE | HQ2_2D_POLYLINE_ALT | HQ2_2D_SEGMENTS | HQ2_2D_LINE_SEG + | HQ2_2D_POLYPOINT => STREAM, HQ2_2D_SOLID_RECT => STREAM, HQ2_2D_TILE_RECT | HQ2_2D_TILE_RECT_ODD => 7, HQ2_2D_DRAW_IMAGE | HQ2_2D_DRAW_IMAGE_SMALL | HQ2_DMA_WRITE_PIXELS @@ -868,7 +872,7 @@ impl Hq2Engine { } let group = match self.cmd { HQ2_2D_POLY_SPAN => 3, - HQ2_2D_POLYLINE | HQ2_2D_POLYLINE_ALT => 2, + HQ2_2D_POLYLINE | HQ2_2D_POLYLINE_ALT | HQ2_2D_POLYPOINT => 2, HQ2_2D_SEGMENTS | HQ2_2D_LINE_SEG | HQ2_2D_STIPPLED_SPAN_A | HQ2_2D_STIPPLED_SPAN_B => 4, _ => 0, }; @@ -902,6 +906,12 @@ impl Hq2Engine { a[1] as i32, a[2] as i32, a[3] as i32, a[4], self.stipple_opaque, self.s2d.color_on, self.s2d.color_off)); } } + HQ2_2D_POLYPOINT => { + if !(a[1] == PAD_X && a[2] == PAD_Y) { + let p = [a[1] as i32, a[2] as i32]; + self.line2d(p, p, true, out); + } + } HQ2_2D_SEGMENTS | HQ2_2D_LINE_SEG => { // Padding: (1280, 1024) pairs (SEGMENTS) or x1 = x2 // = 1280 with y 0 (LINE_SEG, 4Dwm frames). diff --git a/src/dev/gr2/hq2_tests.rs b/src/dev/gr2/hq2_tests.rs index 82883458c..545b53a94 100644 --- a/src/dev/gr2/hq2_tests.rs +++ b/src/dev/gr2/hq2_tests.rs @@ -589,8 +589,17 @@ fn ddx_glyphs_lines_spans_stipples() { // Segments with sentinel padding. cmd(g, HQ2_2D_SEGMENTS, 0); for v in [0x100, 0x80, 0x110, 0x80, 1280, 1024, 1280, 1024] { data(g, v); } + // Points (xlock pop's 2x2 sparks), clipped to x <= 0x1ff, padded. + cmd(g, HQ2_2D_LINE_CLIP, 0); + for v in [0x1ff, 0, 0x3ff] { data(g, v); } + cmd(g, HQ2_2D_POLYPOINT, 0); + for v in [0x30, 0x40, 0x31, 0x40, 0x20c, 0x1a5, 1280, 1024] { data(g, v); } + cmd(g, HQ2_2D_LINE_CLIP, 0); + for v in [0x7ff, 0, 0x3ff] { data(g, v); } cmd(g, HQ2_2D_END_PRIMITIVE, 0); g.wait_idle(); + assert_eq!((px(0x30, 0x40), px(0x31, 0x40), px(0x32, 0x40)), (7, 7, 0), "points"); + assert_eq!(px(0x20c, 0x1a5), 0, "point outside the clip"); assert_eq!((px(0x15, 0x300), px(0x1d, 0x300), px(0x1e, 0x300)), (7, 7, 0)); assert_eq!(px(0x16, 0x301), 7); assert!((0x3e0..=0x3ee).all(|y| px(0x11, y) == 7), "vertical run"); @@ -3031,3 +3040,61 @@ fn bench_gl_smooth_triangles() { println!("{size:5} px legs: {:8.0} tris/s {:6.1} Mpx/s", n as f64 / s, n as f64 * size as f64 * size as f64 / 2.0 / s / 1e6); } } + +/// IRIS GL drawmode(OVERDRAW) (Blender's toolbox, IRIX 5.3): MakeCurrent +/// 11, colour planes masked off (0x005 = 0), aux write mask 0x003 = 3 (the +/// 2-bit overlay, aux bits 1:0), then sboxfi / sboxi (0x4053 / 0x4052, +/// ITOF) in colour indices set by 0x7030. Only the overlay bits change; +/// sbox draws the outline alone. +#[test] +fn iris_overdraw_boxes_into_the_overlay_planes() { + let g = &live_gr2(Gr2Variant::Xz); + gl_setup_window(g); + let ints = |g: &Gr2, tok: u32, v: [u32; 4]| { + cmd(g, tok, v[0]); + for &w in &v[1..] { cmd(g, 0x41df, w); } + }; + cmd(g, 0x004, 11); + cmd(g, 0x005, 0); + cmd(g, 0x003, 3); + cmd(g, 0x008, 1); + cmd(g, 0x008, 0); + cmd(g, 0x7030, 2); + ints(g, 0x4053, [10, 10, 20, 20]); + cmd(g, 0x7030, 1); + ints(g, 0x4052, [30, 10, 40, 20]); + // Back to normal drawing, as Blender does. + cmd(g, 0x004, 2); + cmd(g, 0x003, 0); + g.wait_idle(); + let aux = |x: i32, y: i32| (g.vram()[((660 + y) as usize) * FB_W + (64 + x) as usize] >> 24) & 0xf; + assert_eq!(aux(15, 15), 2, "filled box in the overlay"); + assert_eq!(gl_px(g, 15, 15), 0, "colour planes untouched"); + assert_eq!(aux(30, 15), 1, "outline left side"); + assert_eq!(aux(35, 20), 1, "outline top"); + assert_eq!(aux(35, 15), 0, "outline only, not filled"); + assert_eq!(aux(25, 15), 0, "between the boxes"); + assert_eq!(g.re3_reg(super::re3::REG_AUXMASK), 0, "aux mask reset for 2D"); +} + +/// IRIS GL readsource (0x008 = plane, absolute bank) picks the bank a +/// rectcopy reads. Blender keeps its views in the back buffer and copies +/// them to the front with readsource bank 1 and write mask 0x000FFF; reading +/// bank 0 instead left those views black. +#[test] +fn iris_rectcopy_reads_the_readsource_bank() { + let g = &live_gr2(Gr2Variant::Xz); + gl_setup_window(g); + cmd(g, 0x004, 2); // MakeCurrent: 12-bit RGB + cmd(g, 0x005, 0xfff000); // draw into bank 1 only + cmd(g, 0x6913, 0x00ff_ffff); // cpack white + cmd(g, 0x09e, 0); // clear + cmd(g, 0x005, 0x000fff); // now bank 0 ... + cmd(g, 0x008, 0); + cmd(g, 0x008, 1); // ... reading bank 1 + cmd(g, 0x0ba, 0); + for v in [20, 40, 1, 10, 50, 3] { data(g, v); } + g.wait_idle(); + assert_eq!(gl_px(g, 50, 40), 0xfff_fff, "bank 1 copied into bank 0"); + assert_eq!(gl_px(g, 53, 40), 0xfff_000, "outside the copy bank 0 is untouched"); +} diff --git a/src/dev/ps2.rs b/src/dev/ps2.rs index 2d625006d..a8e6648bb 100644 --- a/src/dev/ps2.rs +++ b/src/dev/ps2.rs @@ -982,7 +982,7 @@ impl Device for Ps2Controller { fn get_clock(&self) -> u64 { 0 } fn register_commands(&self) -> Vec<(String, String)> { - vec![("ps2".to_string(), "PS/2 commands: ps2 debug | ps2 type | ps2 enter | ps2 esc | ps2 mouse [buttons] | ps2 status".to_string())] + vec![("ps2".to_string(), "PS/2 commands: ps2 debug | ps2 type | ps2 enter | ps2 esc | ps2 key | ps2 mouse [buttons] | ps2 status".to_string())] } fn execute_command(&self, cmd: &str, args: &[&str], mut writer: Box) -> Result<(), String> { @@ -1025,14 +1025,48 @@ impl Device for Ps2Controller { writeln!(writer, "PS/2: pressed Esc").unwrap(); return Ok(()); } + // Hold a modifier across other input: programs that read the + // modifier's state when they handle a key (IRIS GL getbutton) + // miss the quick press / release `type` sends around a capital. + if !args.is_empty() && args[0] == "key" { + let key = match args.get(1).copied() { + Some("shift") => KeyCode::ShiftLeft, + Some("ctrl") => KeyCode::ControlLeft, + Some("alt") => KeyCode::AltLeft, + _ => return Err("Usage: ps2 key ".to_string()), + }; + let down = match args.get(2).copied() { + Some("down") => true, + Some("up") => false, + _ => return Err("Usage: ps2 key ".to_string()), + }; + self.push_kb(key, down); + writeln!(writer, "PS/2: {} {}", args[1], if down { "down" } else { "up" }).unwrap(); + return Ok(()); + } if !args.is_empty() && args[0] == "mouse" { let n = |i: usize| args.get(i).and_then(|v| v.parse::().ok()); let (Some(dx), Some(dy)) = (n(1), n(2)) else { return Err("Usage: ps2 mouse [buttons: 1 left, 2 right, 4 middle]".to_string()); }; let buttons = n(3).unwrap_or(0) as u8; - self.push_mouse_input(buttons, dx, dy, 0); - writeln!(writer, "PS/2: mouse {} {} buttons {}", dx, dy, buttons).unwrap(); + // A PS/2 packet carries at most 9-bit deltas, and the guest + // accelerates fast motion: send the move as packets of at + // most MOUSE_STEP mickeys an axis, one a millisecond (a hand + // moving fast, well under 100 mickeys / ms), the last one + // with what is left so the path stays straight. + const MOUSE_STEP: i32 = 50; + let steps = ((dx.abs().max(dy.abs()) + MOUSE_STEP - 1) / MOUSE_STEP).max(1); + let (mut sent_x, mut sent_y) = (0, 0); + for i in 1..=steps { + let (tx, ty) = (dx * i / steps, dy * i / steps); + self.push_mouse_input(buttons, tx - sent_x, ty - sent_y, 0); + (sent_x, sent_y) = (tx, ty); + if i < steps { + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } + writeln!(writer, "PS/2: mouse {} {} buttons {} ({} packets)", dx, dy, buttons, steps).unwrap(); return Ok(()); } if !args.is_empty() && args[0] == "status" { @@ -1043,7 +1077,7 @@ impl Device for Ps2Controller { s.mouse_queue_bytes, s.scancode_set, s.config, s.last_read).unwrap(); return Ok(()); } - return Err("Usage: ps2 debug | ps2 type | ps2 enter | ps2 esc | ps2 mouse [buttons] | ps2 status".to_string()); + return Err("Usage: ps2 debug | ps2 type | ps2 enter | ps2 esc | ps2 key | ps2 mouse [buttons] | ps2 status".to_string()); } Err("Command not found".to_string()) }