From 81ddf82e4cfaf5f96672f3b3304c38f2fa4d572d Mon Sep 17 00:00:00 2001 From: technomancer Date: Sat, 17 Oct 2026 12:21:45 -0700 Subject: [PATCH 1/4] gr2: IRIS GL overlay / popup drawing and sbox; ps2 key hold Blender 1.0's Shift+A toolbox (IRIX 5.3) never showed: IRIS GL drawmode(OVERDRAW / PUPDRAW) switches with MakeCurrent 11 (8 for the 4-bit overlay), colour planes masked off and 0x003 naming the aux bits to write, then draws in colour indices. The HLE kept 12-bit RGB with a zero write mask, so the whole menu went nowhere. GL drawing now targets the aux planes in those modes (index into the aux bits as the 2D path's value2d does) and resets them afterwards. sbox / sboxi (0x052 / 0x4052, a screen-aligned box outline) was not implemented; Blender draws its grids and button borders as degenerate boxes. Each outline pixel is drawn once. Monitor: `ps2 key ` holds a modifier across other input; IRIS GL programs read the modifier's state when they handle the key, so `ps2 type A` reached Blender as a plain a. Co-Authored-By: Claude Opus 5.5 --- src/dev/gr2/gl.rs | 121 ++++++++++++++++++++++++++++++++++----- src/dev/gr2/hq2_tests.rs | 36 ++++++++++++ src/dev/ps2.rs | 23 +++++++- 3 files changed, 165 insertions(+), 15 deletions(-) diff --git a/src/dev/gr2/gl.rs b/src/dev/gr2/gl.rs index 7520084da..1a9fe6c47 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; @@ -454,6 +462,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 +476,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 +1003,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, @@ -1311,6 +1339,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 +1471,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]) }; @@ -1550,6 +1589,12 @@ impl Hq2Engine { 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_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 +1706,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 +1728,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 +1756,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 +1832,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 +2029,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_tests.rs b/src/dev/gr2/hq2_tests.rs index 82883458c..e2163e295 100644 --- a/src/dev/gr2/hq2_tests.rs +++ b/src/dev/gr2/hq2_tests.rs @@ -3031,3 +3031,39 @@ 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"); +} diff --git a/src/dev/ps2.rs b/src/dev/ps2.rs index 2d625006d..b591e2743 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,6 +1025,25 @@ 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 { @@ -1043,7 +1062,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()) } From dd2a7fda09bc9a37889f62d39171d24f35ed9e08 Mon Sep 17 00:00:00 2001 From: technomancer Date: Sat, 17 Oct 2026 13:06:47 -0700 Subject: [PATCH 2/4] ps2: split monitor mouse moves into paced packets ps2 mouse sent one packet per command, so moves past the 9-bit delta range were mangled and large ones hit X's acceleration erratically. Send the move as packets of at most 50 mickeys an axis, one a millisecond, the last carrying the remainder so the path stays straight. Co-Authored-By: Claude Opus 5.5 --- src/dev/ps2.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/dev/ps2.rs b/src/dev/ps2.rs index b591e2743..a8e6648bb 100644 --- a/src/dev/ps2.rs +++ b/src/dev/ps2.rs @@ -1050,8 +1050,23 @@ impl Device for Ps2Controller { 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" { From 81954f772ea104ed7839d79b1a7e002c3c108613 Mon Sep 17 00:00:00 2001 From: technomancer Date: Sat, 17 Oct 2026 13:06:47 -0700 Subject: [PATCH 3/4] gr2: IRIS GL readsource selects the rectcopy read bank Token 0x008 is gl_setreadsource (plane, absolute bank), not a draw-buffer no-op. 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 left views black on first open and made headers vanish when the pointer moved between views. libgl sends no swap tokens, so the absolute bank goes straight into read_src. Co-Authored-By: Claude Opus 5.5 --- src/dev/gr2/gl.rs | 13 +++++++++---- src/dev/gr2/hq2_tests.rs | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/dev/gr2/gl.rs b/src/dev/gr2/gl.rs index 1a9fe6c47..085255a00 100644 --- a/src/dev/gr2/gl.rs +++ b/src/dev/gr2/gl.rs @@ -230,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). @@ -1296,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; @@ -1588,7 +1593,7 @@ 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) }; diff --git a/src/dev/gr2/hq2_tests.rs b/src/dev/gr2/hq2_tests.rs index e2163e295..51360945a 100644 --- a/src/dev/gr2/hq2_tests.rs +++ b/src/dev/gr2/hq2_tests.rs @@ -3067,3 +3067,25 @@ fn iris_overdraw_boxes_into_the_overlay_planes() { 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"); +} From fed6cbb87580749795c8bbe80905fcf35a96cec7 Mon Sep 17 00:00:00 2001 From: technomancer Date: Sat, 17 Oct 2026 15:46:06 -0700 Subject: [PATCH 4/4] gr2: 2D POLYPOINT (0x161) for XDrawPoints expPolyPoint sends 0x161 after LINE_MODE / LINE_CLIP: a stream of (x, y) points, padded with (1280, 1024) pairs. It was unhandled, so xlock's pop and hop modes (the Pop and Space Plants screen savers) drew only their black window. Each point is drawn as a one-pixel line through the line clip. Co-Authored-By: Claude Opus 5.5 --- src/dev/gr2/hq2.rs | 16 +++++++++++++--- src/dev/gr2/hq2_tests.rs | 9 +++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) 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 51360945a..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");