Skip to content
74 changes: 64 additions & 10 deletions src/dev/mgras/gl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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],
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 20 additions & 1 deletion src/dev/mgras/hq3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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<u8> = 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);
Expand Down Expand Up @@ -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 };
Expand Down
86 changes: 84 additions & 2 deletions src/dev/mgras/mgras_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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]);
Expand All @@ -1492,8 +1527,17 @@ fn gl_read_pixels_to_host_memory() {
m.state_hash();
let b = mem.bytes.lock();
let got: Vec<u8> = (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();
}

Expand Down Expand Up @@ -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<u32> = [(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<u32> = [(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
Expand Down
Loading