From e82d3063ff3a788d7b5b1df832ecbc92dfb8bfab Mon Sep 17 00:00:00 2001 From: technomancer Date: Wed, 30 Sep 2026 05:08:36 -0700 Subject: [PATCH] improved dma reads, added a fifo also implemented XGetImage/glReadPixels VRAM readbacks --- rules/gr2/cx-save-must-wait-for-hq.md | 14 +- src/dev/gr2/gl.rs | 78 ++++++- src/dev/gr2/hq2.rs | 159 +++++++++++-- src/dev/gr2/hq2_tests.rs | 189 +++++++++++++++ src/dev/gr2/mod.rs | 170 ++++++++------ test/gltest/glprim | Bin 47220 -> 67676 bytes test/gltest/glprim.c | 320 +++++++++++++++++++++++++- test/gltest/gltest | Bin 43300 -> 43300 bytes 8 files changed, 840 insertions(+), 90 deletions(-) diff --git a/rules/gr2/cx-save-must-wait-for-hq.md b/rules/gr2/cx-save-must-wait-for-hq.md index 5e41cda..0de87ee 100644 --- a/rules/gr2/cx-save-must-wait-for-hq.md +++ b/rules/gr2/cx-save-must-wait-for-hq.md @@ -9,13 +9,15 @@ the magic check and the incoming context kept the outgoing one's live GL state: another demo (ideas) drew into amesh's window with amesh's window, clip and matrices. Rare: took minutes of two GL demos running together. -Fix (mod.rs `cx_save_pending`, counted like `fin3_pending` on the FIFO -push, decremented after the HQ2 executes the token): an HQ2_GEDMA read -returns bus busy while a save is queued. The VDMA worker and the CPU both -retry busy reads. Also: a rejected restore now resets GlState to defaults -instead of keeping the previous context's state (draws nothing until the -next window / matrix tokens, rather than into someone else's window). +Fix: HQ2_GEDMA reads wait (bus busy) while the port is empty and the HQ2 +still has work; see gedma-read-port.md (the first fix counted queued 0x1E1 +tokens, `cx_save_pending`; replaced by the general read-port rule when pixel +DMA reads needed the same). Also: a rejected restore now resets GlState to +defaults instead of keeping the previous context's state (draws nothing +until the next window / matrix tokens, rather than into someone else's +window). Trace diagnostics: GE_HQMSAV lines show `(live was )` and flag `LEAK?` for state 1 over another context's state; restores report `REJECTED: `. Test: `gl_context_save_read_waits_for_hq`. + diff --git a/src/dev/gr2/gl.rs b/src/dev/gr2/gl.rs index cee4122..a10ecba 100644 --- a/src/dev/gr2/gl.rs +++ b/src/dev/gr2/gl.rs @@ -186,6 +186,16 @@ pub const T_GET_COLOR: u32 = 0x0e9; pub const T_GET_NORMAL: u32 = 0x0ea; pub const T_GET_RASTERPOS: u32 = 0x107; pub const T_READ_DONE: u32 = 0x0bd; +/// Pixel read setup (__glExpReadPixelsKDMA, Fetch, ReadColor): read setup +/// 0x0A7 / 0x0A8 = 0 and read mode 0x0BC (1 before a pixel DMA read, 0 +/// before READ_RECT). Meaning unknown; the HLE ignores them. +pub const T_READ_SETUP_A: u32 = 0x0a7; +pub const T_READ_SETUP_B: u32 = 0x0a8; +pub const T_READ_MODE: u32 = 0x0bc; +/// Read source (__glExpSetReadBuffer): two words to the token, kind and +/// buffer: 0, 0 = front; 0, 1 = back; 1, n = with aux buffers; 2, 0 = the +/// Z buffer (depth and stencil reads). +pub const T_READ_BUFFER: u32 = 0x10a; const READBACK_SHRAM: usize = 0x4022; // Vertex routines (LOADV token) and Begin/End tokens (gr2_prim.c). @@ -367,6 +377,8 @@ pub struct GlState { /// User clip planes (eye space) and their enable bits. uclip: [[f32; 4]; 6], uclip_on: u32, + /// Read source from 0x10A: kind, buffer (0, 0 = front). + read_src: [u32; 2], /// Vertices drawn / primitives emitted since the last trace note. pub stats_vertices: u32, } @@ -659,7 +671,7 @@ fn port_words(index: u32) -> Option { T_IRIS_CHAR16_TALL | T_IRIS_CHAR32 => Some(21), T_NORMAL_MATRIX => Some(9), T_NORMAL | T_IRIS_NORMAL => Some(3), - T_COLOR_WRITEMASK => Some(2), + T_COLOR_WRITEMASK | T_READ_BUFFER => Some(2), T_BLEND_FACTOR => Some(3), t => light::Lighting::port_words(t), } @@ -720,7 +732,53 @@ pub struct GlCx { impl Hq2Engine { /// 0x1E1 save main: the image the kernel now reads from HQ2_GEDMA. pub(super) fn gl_cx_save(&mut self, out: &mut dyn Re3Sink) { - out.cx_publish(&self.gl_ctx.save); + out.gedma_out(&self.gl_ctx.save); + } + + /// How 0x0AC reads the current read source (0x10A) in this window's + /// pixel format. Double-buffered 12-bit: after the swap to state s the + /// front is bank s and the back bank s ^ 1 (the inverse of GL_BACK's + /// write masks 0xFFF000 / 0x000FFF for states 0 / 1). + fn gl_read_decode(&self) -> super::ReadDecode { + use super::ReadDecode; + let g = &self.gl; + if g.read_src[0] == 2 { + return ReadDecode::Depth; + } + let bank = (g.swap ^ (g.read_src[1] & 1)) & 1; + match g.pixfmt { + re3::PIXFMT_RGB12 => ReadDecode::Rgb12 { bank }, + re3::PIXFMT_CI12 => ReadDecode::Ci12 { bank }, + _ => ReadDecode::Rgb24, + } + } + + pub(super) fn gl_read_desc(&self) -> String { + format!("src {} {} {:?} window ({}, {})", self.gl.read_src[0], self.gl.read_src[1], + self.gl_read_decode(), self.gl.win[0], self.gl.win[1]) + } + + /// 0x0AC pixel DMA read (lrectread, glReadPixels KDMA): x on the token; + /// y, width, height, words/row, flag, 0. Window-relative, y = the + /// bottom row; rows go out top first, as 0x0B5 takes them in. Pixels + /// per word from width / words per row, MSB first. + pub(super) fn gl_dma_read(&mut self, a: &[u32], out: &mut dyn Re3Sink) { + use super::{ReadDest, ReadImage}; + self.gl.ensure_init(); + let (w, rows, wpr) = (a[2], a[3], a[4].max(1)); + let mut s2d = self.s2d; + s2d.buf_select = match (w + wpr - 1) / wpr { 4 => 2, 2 => 1, _ => 0 }; + s2d.buf_offset = 0; + let req = ReadImage { + x: self.gl.win[0] + a[0] as i32, + top: self.gl.win[1] + a[1] as i32 + rows as i32 - 1, + w, + rows, + words_per_row: wpr, + s2d, + decode: self.gl_read_decode(), + }; + out.read_image(&req, ReadDest::Gedma); } /// The live GL state as a context image. @@ -841,7 +899,8 @@ impl Hq2Engine { T_SHADE_MODEL | T_FRONT_FACE | T_MAKECURRENT | T_CULL_FRONT | T_CULL_BACK | T_POLYGON_MODE | T_SWAP_BUFFERS | T_DITHER | T_STIPPLE_OFF | T_DEPTH_TEST | T_DEPTH_FUNC | T_DEPTH_MASK | T_STENCIL_WMASK | T_STENCIL_CONFIG - | T_BLEND_MODE | T_LOGIC_OP | T_GET_COLOR | T_GET_NORMAL | T_GET_RASTERPOS | T_READ_DONE => 1, + | T_BLEND_MODE | T_LOGIC_OP | T_GET_COLOR | T_GET_NORMAL | T_GET_RASTERPOS | T_READ_DONE + | T_READ_SETUP_A | T_READ_SETUP_B | T_READ_MODE => 1, T_FRAGMENT => 7, T_STENCIL_CLEAR | T_IRIS_BUFFER | T_IRIS_DB => 2, T_IRIS_WRITEMASK | T_IRIS_CLEAR | T_IRIS_ZCLEAR | T_IRIS_BGNPOLYGON | T_IRIS_ENDPOLYGON @@ -1040,6 +1099,12 @@ impl Hq2Engine { } }; } + T_READ_BUFFER => { + g.read_src = [b[0], b[1]]; + if let Some(d) = done.as_mut() { + d(format!("GL_READ_BUFFER {} {}", b[0], b[1])); + } + } T_COLOR_WRITEMASK => { // Word 0 = plane mask for swap state 0, word 1 = state 1. // (Swapping them makes ideas flicker from the start: tested.) @@ -1126,7 +1191,7 @@ impl Hq2Engine { g.lt.command(cmd, &a[..]); } T_LOGIC_OP => g.logic_op = a[0] & 0xf, - T_READ_DONE => {} + T_READ_DONE | T_READ_SETUP_A | T_READ_SETUP_B | T_READ_MODE => {} T_GET_COLOR => { let c = g.color; for (k, v) in c.iter().enumerate() { @@ -1280,6 +1345,8 @@ impl Hq2Engine { T_IRIS_GETCPOS => format!("IRISGL_GETCPOS -> ({}, {}){}", g.cpos[0], g.cpos[1], if g.cpos[2] != 0 { " invalid" } else { "" }), T_GET_COLOR | T_GET_NORMAL | T_GET_RASTERPOS => format!("{} -> shram[{:#x}]", super::index_label(cmd), READBACK_SHRAM), T_READ_DONE => "GL_READ_DONE".to_string(), + T_READ_SETUP_A | T_READ_SETUP_B => format!("GL_READ_SETUP {:#x} {:#x}", cmd, a[0]), + T_READ_MODE => format!("GL_READ_MODE {}", a[0]), T_LOGIC_OP => format!("GL_LOGIC_OP {}", g.logic_op), light::T_LIGHTING => format!("GL_LIGHTING {}", if g.lt.on != 0 { "on" } else { "off" }), light::T_TWO_SIDED => format!("GL_LIGHT_MODEL_TWO_SIDE {}", g.lt.two_sided), @@ -1412,8 +1479,9 @@ impl Hq2Engine { out.reg(re3::REG_ALIGNPAT, 0); let mask = 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); if self.gl.pixfmt != 0 { - out.op(re3::RE3_OP_PIXFMT, self.gl.pixfmt as u64); out.reg(re3::REG_ENABDITH, self.gl.dither); } if self.gl_zs_active() { diff --git a/src/dev/gr2/hq2.rs b/src/dev/gr2/hq2.rs index 4031bda..c1a923d 100644 --- a/src/dev/gr2/hq2.rs +++ b/src/dev/gr2/hq2.rs @@ -151,6 +151,13 @@ pub const HQ2_DMA_WRITE_PIXELS: u32 = 327; /// 0x147, drawn in the current GL context (window-relative, GL y up). pub const HQ2_GL_DMA_WRITE: u32 = 0x0b5; pub const HQ2_GL_DMA_WRITE_ZOOM: u32 = 0x0b8; +/// Pixel DMA reads (HQ2.h "PIXEL DMA DIRECTION"): the same kernel ioctl and +/// header as the writes, but the VDMA reads height * words/row words from +/// HQ2_GEDMA; FIN2 when done. 0x152 = Xsgi expReadImage* (screen, X-style +/// y = top row, format from BUF_SELECT); 0x0AC = GL lrectread / +/// glReadPixels (window-relative, y = bottom row, source from 0x10A). +pub const HQ2_2D_DMA_READ_PIXELS: u32 = 0x152; +pub const HQ2_GL_DMA_READ: u32 = 0x0ac; /// Pixel rectangles streamed by the kernel's pixel DMA (_Gr2DMAtrigger): /// x on the token; y, width, height, words/row, flag, 0 on GE_DATA; then @@ -226,16 +233,45 @@ pub struct State2d { pub buf_offset: u32, } -/// A READ_IMAGE request, as the sink needs it. +/// How a screen-to-host read turns a VRAM word into a pixel value. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ReadDecode { + /// Xsgi: the plane group selected by the 2D MODE (`read_value`). + Planes2d, + /// GL 24-bit RGB: 0xAABBGGRR, alpha 0xFF (no alpha planes). + Rgb24, + /// GL 12-bit RGB in `bank` (0 = bits 11:0, 1 = bits 23:12), nibbles + /// widened to 8 bits (x 17), as Rgb24. + Rgb12 { bank: u32 }, + /// GL colour index in 12-bit `bank`. + Ci12 { bank: u32 }, + /// GL depth: Z right-justified (libglcore shifts it up by 32 - bits). + Depth, +} + +/// Where a screen-to-host read goes. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ReadDest { + /// READ_IMAGE: shram from READ_IMAGE_SHRAM, then FIN3. + Shram, + /// Pixel DMA read: the HQ2_GEDMA read port, then FIN2. + Gedma, +} + +/// A screen-to-host read (READ_IMAGE, 0x152, 0x0AC), as the sink needs it. #[derive(Clone, Copy)] pub struct ReadImage { - /// Left x and GL row of the top line (y = 0 at the bottom). + /// Left x and GL row of the top line (y = 0 at the bottom); rows go + /// down from there. pub x: i32, pub top: i32, pub w: u32, pub rows: u32, pub words_per_row: u32, + /// Pixel format (buf_select: 2 = 8-bit, 1 = 16-bit, 0 = 32-bit) and + /// first-pixel slot (buf_offset); the planes for Planes2d. pub s2d: State2d, + pub decode: ReadDecode, } impl ReadImage { @@ -260,7 +296,7 @@ impl ReadImage { for p in 0..per_word { let i = (k as u32 * per_word + p) as i32 - off; if i >= 0 && (i as u32) < self.w { - let v = read_value(&self.s2d, read(self.x + i, gy)) & mask; + let v = self.value(read(self.x + i, gy)) & mask; word |= v << (32 - bits * (p + 1)); } } @@ -270,6 +306,23 @@ impl ReadImage { } } +impl ReadImage { + /// The pixel value of VRAM word `vram` (for Depth: the Z buffer word). + pub fn value(&self, vram: u32) -> u32 { + let wide = |n: u32| (n & 0xf) * 17; + match self.decode { + ReadDecode::Planes2d => read_value(&self.s2d, vram), + ReadDecode::Rgb24 => 0xff00_0000 | (vram & 0x00ff_ffff), + ReadDecode::Rgb12 { bank } => { + let v = (vram >> (12 * (bank & 1))) & 0xfff; + 0xff00_0000 | wide(v) | (wide(v >> 4) << 8) | (wide(v >> 8) << 16) + } + ReadDecode::Ci12 { bank } => (vram >> (12 * (bank & 1))) & 0xfff, + ReadDecode::Depth => vram & 0x00ff_ffff, + } + } +} + /// The value a VRAM word holds for the active plane group (the inverse of /// `value2d`): 4-bit overlay (MODE 8), 2-bit overlay or popup (MODE 0xB), /// or the colour planes (R in bits 7:0). @@ -278,6 +331,16 @@ pub fn read_value(s: &State2d, vram: u32) -> u32 { match s.mode & 0xff { 8 => aux, 0xb => if s.aux_mask & 0xc != 0 { (aux >> 2) & 3 } else { aux & 3 }, + // 12-bit RGB (expReadImage12TC: MODE 0x1002, ROP flag = the + // window's buffer * 8): the bank the flag selects, R 3:0, G 7:4, + // B 11:8, as 8:8:8 with each nibble replicated. The DDX takes the + // low nibble of each byte back ((w & 0xF) | (w & 0xF00) >> 4 | + // (w & 0xF0000) >> 8); expDrawImage12TC sends the high nibbles. + 2 => { + let v = (vram >> (12 * ((s.flag >> 3) & 1))) & 0xfff; + let wide = |n: u32| (n & 0xf) * 17; + wide(v) | (wide(v >> 4) << 8) | (wide(v >> 8) << 16) + } _ => vram & 0x00ff_ffff, } } @@ -303,6 +366,7 @@ pub fn token_name(index: u32) -> Option<&'static str> { 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", + HQ2_2D_DMA_READ_PIXELS => "2D_DMA_READ_PIXELS", HQ2_GL_DMA_READ => "GL_DMA_READ", 340 => "2D_COPY_RECT", 342 => "2D_DRAW_IMAGE", 343 => "2D_DRAW_IMAGE_SMALL", HQ2_2D_READ_IMAGE => "2D_READ_IMAGE", 346 | 347 => "2D_STIPPLED_SPAN", HQ2_2D_STIPPLE_RECT => "2D_STIPPLE_RECT", 349 => "2D_MONO_IMAGE_8", 350 => "2D_MONO_IMAGE_16", 351 => "2D_MONO_IMAGE_32", @@ -351,7 +415,7 @@ pub fn gl_token_name(tok: u32) -> Option<&'static str> { 0x0f3 => "GL_BEGIN_QUADS", 0x0f4 => "GL_END_QUADS", 0x0f5 => "GL_VTX_QUADS", 0x0f6 => "GL_EDGE_FLAG", 0x0f7 => "GL_BEGIN_POLYGON", 0x0f8 => "GL_VTX_POLYGON", 0x0fc | 0x0fd => "GL_END_POLYGON", 0x104 => "GL_CLEAR_COLOR", 0x108 => "GL_FRONT_FACE", - 0x10a => "GL_BUFFER_MODE", 0x10b => "GL_COLOR_WRITEMASK", 0x10d => "GL_NORMAL", + 0x10a => "GL_READ_BUFFER", 0x10b => "GL_COLOR_WRITEMASK", 0x10d => "GL_NORMAL", 0x10e => "GL_LIGHTPATH", 0x11d => "GL_BEGIN_POINTS", 0x123 => "GL_VTX_POINTS", 0x168 => "GL_END_POINTS", 0x17c => "GL_BEGIN_LINES", 0x182 => "COLOR", 0x1e5 => "CX_RESTORE_1E5", 0x1e7 => "GL_SWAP_BUFFERS", @@ -481,15 +545,18 @@ pub trait Re3Sink { /// Raise a finish flag (FIN1/FIN2/FIN3), as the microcode does when it /// completes a request the host is waiting on. fn finish(&mut self, flag: usize); - /// READ_IMAGE: once all drawing is done, pack the rectangle into shram - /// and raise FIN3. - fn read_image(&mut self, req: &ReadImage); + /// Screen-to-host read: once all drawing is done, pack the rectangle + /// into shram and raise FIN3 (READ_IMAGE), or feed it to the HQ2_GEDMA + /// read port and raise FIN2 (pixel DMA reads). + fn read_image(&mut self, req: &ReadImage, dest: ReadDest); /// Emulator-private RE3 operation (re3::RE3_OP_*). fn op(&mut self, op: u32, val: u64); /// Store a word into shared RAM, as the microcode does for readbacks. fn shram(&mut self, word: usize, val: u32); - /// Make a saved context image available on HQ2_GEDMA reads (0x1E1). - fn cx_publish(&mut self, words: &[u32]); + /// Start a new transfer on the HQ2_GEDMA read port and queue `words` + /// (a context image, 0x1E1). Words a previous transfer left unread are + /// dropped. Blocks while the port is full. + fn gedma_out(&mut self, words: &[u32]); } /// HLE command interpreter state. Owned by the HQ2 thread. Plain data. @@ -559,6 +626,8 @@ impl Hq2Engine { | HQ2_2D_CID_WRITE | HQ2_2D_END_PRIMITIVE => 1, HQ2_2D_BUF_SELECT => 2, HQ2_2D_READ_IMAGE => 5, + // Token x, then y, width, height, words/row, flag, 0 on GE_DATA. + HQ2_2D_DMA_READ_PIXELS | HQ2_GL_DMA_READ => 7, HQ2_2D_ROP => 4, HQ2_2D_TILE_SETUP => 5, HQ2_2D_MONO_IMAGE_8 => 8, @@ -897,8 +966,30 @@ impl Hq2Engine { } } - /// Set the RE3 colour for `pixel` (colour planes: R in bits 7:0). + /// Set the RE3 colour for a GC pixel (fg, COLOR_ON / OFF; colour planes: + /// R in bits 7:0). In 12-bit mode (MODE 2) the pixel is the X visual's + /// 12-bit value (R 3:0, G 7:4, B 11:8): the DDX passes GC pixels + /// unconverted (expDrawPoints), so widen it here (inferred). fn color2d(&self, pixel: u32, out: &mut dyn Re3Sink) { + if self.s2d.mode & 0xff == 2 { + let wide = |n: u32| (n & 0xf) * 17; + return self.rgb2d(wide(pixel) | (wide(pixel >> 4) << 8) | (wide(pixel >> 8) << 16), out); + } + self.rgb2d(pixel, out); + } + + /// Colour of an image / tile pixel word: in 12-bit mode the DDX has + /// already widened it to 8:8:8 (expDrawImage12TC, expTileRects: the + /// nibbles in the high halves), which the RE3 12-bit packer takes as is. + fn image2d(&self, pixel: u32, out: &mut dyn Re3Sink) { + if self.s2d.mode & 0xff == 2 && self.s2d.cid_write & 0xf000 == 0 { + return self.rgb2d(pixel, out); + } + self.value2d(pixel, out); + } + + /// RE3 colour registers from an 8:8:8 value (R in bits 7:0). + fn rgb2d(&self, pixel: u32, out: &mut dyn Re3Sink) { out.reg(re3::REG_R, (pixel & 0xff) << 11); out.reg(re3::REG_G, ((pixel >> 8) & 0xff) << 11); out.reg(re3::REG_B, ((pixel >> 16) & 0xff) << 11); @@ -1022,7 +1113,7 @@ impl Hq2Engine { } let sx = x + (n - start) as i32; if sx < re3::FB_W as i32 && sx + (e - n) as i32 > 0 { - self.value2d(c, out); + self.image2d(c, out); self.span(sx, gy, (e - n) as u32, None, out); } n = e; @@ -1177,7 +1268,7 @@ impl Hq2Engine { while e < x2 && tile_px((e - ox).rem_euclid(tw), ty) == c { e += 1; } - self.value2d(c, out); + self.image2d(c, out); self.span(x, gy, (e - x) as u32, None, out); x = e; } @@ -1207,11 +1298,33 @@ impl Hq2Engine { out.reg(re3::REG_PIXMASK, 0); self.value2d(fg, out); } + // 12-bit RGB (MODE 2, 12-bit TrueColor windows): the RE3 + // writes the value to both 12-bit buffers and the plane mask + // picks one (RE3.h). The buffers never move; ROP flag bit 3 + // (the window's dbc buffer * 8) names buffer 1. A mask the + // DDX already moved or doubled (dbc -3 / -1, expTileRects) + // is used as it is. + 2 => { + let pm = s.planemask & 0x00ff_ffff; + let mask = if s.flag & 8 == 0 { + pm & 0xfff + } else if pm & 0xfff000 != 0 { + pm + } else { + pm << 12 + }; + out.reg(re3::REG_UAUXDATA, 0); + out.reg(re3::REG_AUXMASK, 0); + out.reg(re3::REG_PIXMASK, mask); + out.op(re3::RE3_OP_PIXFMT, re3::PIXFMT_RGB12 as u64); + out.reg(re3::REG_ENABDITH, 0); + } // Colour planes. _ => { out.reg(re3::REG_UAUXDATA, 0); out.reg(re3::REG_AUXMASK, 0); out.reg(re3::REG_PIXMASK, s.planemask & 0x00ff_ffff); + out.op(re3::RE3_OP_PIXFMT, 0); } } } @@ -1263,6 +1376,10 @@ impl Hq2Engine { HQ2_2D_MODE | HQ2_2D_COLOR_AUX | HQ2_2D_COLOR_OFF | HQ2_2D_COLOR_ON | HQ2_2D_CID_WRITE | HQ2_2D_BEGIN | HQ2_2D_END_PRIMITIVE => format!("{name} {:#x}", a[0]), HQ2_2D_BUF_SELECT => format!("{name} format={} offset={}", i(0), i(1)), + HQ2_2D_DMA_READ_PIXELS => format!("{name} ({}, {}) {}x{} words/row={} flag={} format={} offset={} mode={:#x} -> GEDMA, FIN2", + i(0), i(1), i(2), i(3), i(4), i(5), self.s2d.buf_select, self.s2d.buf_offset, self.s2d.mode), + HQ2_GL_DMA_READ => format!("{name} ({}, {}) {}x{} words/row={} flag={} {} -> GEDMA, FIN2", + i(0), i(1), i(2), i(3), i(4), i(5), self.gl_read_desc()), HQ2_2D_READ_IMAGE => format!("{name} ({}, {}) {}x{} words/row={} format={} offset={} mode={:#x} -> shram[{:#x}], FIN3", i(1), i(2), i(3), i(4), i(0), self.s2d.buf_select, self.s2d.buf_offset, self.s2d.mode, READ_IMAGE_SHRAM), PUC_COLOR => format!("{name} ci={}", i(0)), @@ -1316,7 +1433,7 @@ impl Hq2Engine { out.finish(FIN2); } GE_CX_SAVE_EXT => { - out.cx_publish(&[]); + out.gedma_out(&[]); out.finish(FIN2); } HQ_PCX_1E0 | HQ_PCX_1E6 => out.finish(FIN2), @@ -1358,9 +1475,23 @@ impl Hq2Engine { rows: a[4], words_per_row: a[0], s2d: self.s2d, + decode: ReadDecode::Planes2d, + }; + out.read_image(&req, ReadDest::Shram); + } + HQ2_2D_DMA_READ_PIXELS => { + let req = ReadImage { + x: a[0] as i32, + top: SCREEN_H - 1 - a[1] as i32, + w: a[2], + rows: a[3], + words_per_row: a[4], + s2d: self.s2d, + decode: ReadDecode::Planes2d, }; - out.read_image(&req); + out.read_image(&req, ReadDest::Gedma); } + HQ2_GL_DMA_READ => self.gl_dma_read(&a, out), HQ2_2D_TILE_SETUP => { self.tile_w = a[1]; self.tile_h = a[2]; diff --git a/src/dev/gr2/hq2_tests.rs b/src/dev/gr2/hq2_tests.rs index 0c28f64..d1fad0f 100644 --- a/src/dev/gr2/hq2_tests.rs +++ b/src/dev/gr2/hq2_tests.rs @@ -2304,3 +2304,192 @@ fn gl_line_steep_downwards() { } assert_eq!(gl_px(g, 20, 10), 0, "last point not drawn"); } + +/// Kernel pixel-DMA read as Xsgi's expReadImage drives it for large +/// rectangles (XGetImage, readximage.log): BUF_SELECT; fin2 = 0; 0x152 x; +/// GE_DATA y, width, rows, words/row, flag, 0; the VDMA then reads the +/// packed rows (top first, X-style y) from HQ2_GEDMA and waits for FIN2. +/// Before IRIS knew 0x152 the reads got zeros, FIN2 never came and the +/// kernel reset the board. +#[test] +fn ddx_dma_read_pixels_streams_gedma_and_sets_fin2() { + use super::hq2::*; + let g = live_gr2(Gr2Variant::Xz); + cmd(g, HQ2_2D_BEGIN, 0); + cmd(g, HQ2_2D_MODE, 0x1009); + cmd(g, HQ2_2D_ROP, 0); + for v in [0xff, 3, 0] { data(g, v); } + cmd(g, HQ2_2D_DRAW_IMAGE, 100); + for v in [50, 6, 2, 2, 2, 0] { data(g, v); } + for v in [0x01020304u32, 0x05060000, 0x11121314, 0x15160000, 0xdeadbeef, 0xdeadbeef] { + data(g, v); + } + cmd(g, HQ2_2D_END_PRIMITIVE, 0); + cmd(g, HQ2_2D_BUF_SELECT, 2); + data(g, 0); + w32(g, 0x6a04c, 0); + cmd(g, HQ2_2D_DMA_READ_PIXELS, 100); + for v in [50, 6, 2, 2, 0, 0] { cmd(g, 0, v); } + // No wait: read the way the VDMA does, right after the header. + let words: Vec = (0..4).map(|_| r32(g, 0x6a068)).collect(); + assert_eq!(words, vec![0x0102_0304, 0x0506_0000, 0x1112_1314, 0x1516_0000]); + assert_eq!(r32(g, 0x6a040) & 2, 2, "FIN2 after the transfer"); +} + +/// A GEDMA read with nothing produced and the HQ2 idle is an overrun: 0, +/// not a hang. (With the HQ2 busy it waits instead; see the save test.) +#[test] +fn gedma_read_with_hq_idle_is_an_overrun() { + let g = live_gr2(Gr2Variant::Xz); + g.wait_idle(); + assert_eq!(r32(g, 0x6a068), 0); +} + +fn gl_dma_read(g: &Gr2, x: u32, y: u32, w: u32, rows: u32) -> Vec { + w32(g, 0x6a04c, 0); + cmd(g, super::hq2::HQ2_GL_DMA_READ, x); + for v in [y, w, rows, w, 0, 0] { cmd(g, 0, v); } + let words = (0..w * rows).map(|_| r32(g, 0x6a068)).collect(); + assert_eq!(r32(g, 0x6a040) & 2, 2, "FIN2 after the transfer"); + words +} + +/// glReadPixels RGBA through the kernel pixel DMA (libglcore +/// __glExpReadPixelsKDMARGBA, readback.log): 0x10A read source; 0x0AC x; +/// GE_DATA y (window-relative, bottom row), width, rows, words/row, 0, 0. +/// 24-bit RGB: one pixel per word as 0xAABBGGRR (the order 0x0B5 takes), +/// alpha 0xFF; rows go out top first. +#[test] +fn gl_dma_read_rgb24_top_row_first() { + let g = live_gr2(Gr2Variant::Xz); + gl_setup_window(g); + gl_quad3(g, [1., 0., 0.], [[0., 0., 0.], [400., 0., 0.], [400., 150., 0.], [0., 150., 0.]]); + gl_quad3(g, [0., 0., 1.], [[0., 150., 0.], [400., 150., 0.], [400., 300., 0.], [0., 300., 0.]]); + for v in [0, 0] { cmd(g, 0x10a, v); } + let (red, blue) = (0xff00_00ff, 0xffff_0000); + assert_eq!(gl_dma_read(g, 10, 148, 2, 4), vec![blue, blue, blue, blue, red, red, red, red]); +} + +/// Double-buffered 12-bit RGB (MAKECURRENT 2): GL_BACK draws with the write +/// masks 0xFFF000 / 0x000FFF, so in swap state 0 the back buffer is bank 1. +/// A back read (0x10A = 0, 1) returns it with the nibbles widened; a front +/// read returns bank 0. +#[test] +fn gl_dma_read_rgb12_back_and_front_banks() { + let g = live_gr2(Gr2Variant::Xz); + gl_setup_window(g); + cmd(g, 0x004, 2); + for v in [0x00ff_f000, 0x0000_0fff] { cmd(g, 0x10b, v); } + cmd(g, 0x011, 0); + gl_quad3(g, [1., 0.5, 0.], [[0., 0., 0.], [400., 0., 0.], [400., 300., 0.], [0., 300., 0.]]); + for v in [0, 1] { cmd(g, 0x10a, v); } + let back = gl_dma_read(g, 20, 20, 1, 1); + assert_eq!(back[0] & 0xff00_00ff, 0xff00_00ff, "back: red nibble 0xF widened to 0xFF, alpha 0xFF"); + assert_eq!(back[0] & 0x00ff_0000, 0, "back: no blue"); + for v in [0, 0] { cmd(g, 0x10a, v); } + assert_eq!(gl_dma_read(g, 20, 20, 1, 1), vec![0xff00_0000], "front bank untouched"); +} + +/// XGetImage of a 12-bit double-buffered window (cap.log, glprim --scene +/// quadrants --db): expReadImage12TC sets MODE 0x1002 and ROP flag = the +/// window's buffer * 8, reads 32-bit words by 0x152 and keeps the low +/// nibble of each byte. So the read returns the flagged bank as 8:8:8 with +/// the nibbles replicated, not the raw 24 bits of both banks. +#[test] +fn ddx_dma_read_rgb12_takes_the_flagged_bank() { + use super::hq2::*; + let g = live_gr2(Gr2Variant::Xz); + gl_setup_window(g); + cmd(g, 0x004, 2); + cmd(g, 0x011, 0); + // Bank 0 red, bank 1 blue (12-bit: R 3:0, G 7:4, B 11:8). + for v in [0x0000_0fff, 0x0000_0fff] { cmd(g, 0x10b, v); } + gl_quad3(g, [1., 0., 0.], [[0., 0., 0.], [400., 0., 0.], [400., 300., 0.], [0., 300., 0.]]); + for v in [0x00ff_f000, 0x00ff_f000] { cmd(g, 0x10b, v); } + gl_quad3(g, [0., 0., 1.], [[0., 0., 0.], [400., 0., 0.], [400., 300., 0.], [0., 300., 0.]]); + let read = |buffer: u32| { + cmd(g, HQ2_2D_BEGIN, 0); + cmd(g, HQ2_2D_MODE, 0x1002); + cmd(g, HQ2_2D_ROP, 0); + for v in [0xff_ffff, 3, buffer * 8] { data(g, v); } + cmd(g, HQ2_2D_BUF_SELECT, 0); + data(g, 0); + w32(g, 0x6a04c, 0); + // Screen x 100, X-style y 1023 - 700 (inside the window). + cmd(g, HQ2_2D_DMA_READ_PIXELS, 100); + for v in [1023 - 700, 1, 1, 1, 0, 0] { cmd(g, 0, v); } + let w = r32(g, 0x6a068); + (w & 0xf) | ((w & 0xf00) >> 4) | ((w & 0xf0000) >> 8) + }; + assert_eq!(read(0), 0x00f, "buffer 0: red as an X 12-bit pixel"); + assert_eq!(read(1), 0xf00, "buffer 1: blue"); +} + +fn ddx_12bit_setup(g: &Gr2, planemask: u32, buffer: u32) { + use super::hq2::*; + cmd(g, HQ2_2D_BEGIN, 0); + cmd(g, HQ2_2D_MODE, 0x1002); + cmd(g, HQ2_2D_ROP, 0); + for v in [planemask, 3, buffer.wrapping_mul(8)] { data(g, v); } +} + +/// 12-bit TrueColor windows (MODE 0x1002): the buffers stay put in VRAM +/// (bank 0 = bits 11:0, bank 1 = bits 23:12) and the ROP flag (dbc buffer +/// * 8) says which one X draws into. A GC pixel is the visual's 12-bit +/// value (R 3:0, G 7:4, B 11:8). +#[test] +fn ddx_12bit_fill_goes_to_the_flagged_bank() { + use super::hq2::*; + let g = live_gr2(Gr2Variant::Xz); + // Solid rect, X-style y: (10, 1023 - 20) .. (13, 1023 - 20). + let rect = |g: &Gr2, fg: u32, pm: u32, buffer: u32| { + ddx_12bit_setup(g, pm, buffer); + cmd(g, HQ2_2D_ROP, fg); + for v in [pm, 3, buffer.wrapping_mul(8)] { data(g, v); } + cmd(g, HQ2_2D_SOLID_RECT, 0); + for v in [10, 1023 - 20, 14, 1023 - 19] { data(g, v); } + cmd(g, HQ2_2D_END_PRIMITIVE, 0); + g.wait_idle(); + }; + rect(g, 0x00f, 0xff_ffff, 0); + assert_eq!(pixel(g, 11, 20), 0x00_000f, "buffer 0 only, even with an all-ones plane mask"); + rect(g, 0xf00, 0xfff, 1); + assert_eq!(pixel(g, 11, 20), 0xf0_000f, "buffer 1: the low mask moved up"); + // dbc -1: the DDX doubles the mask itself. + rect(g, 0x0f0, 0xff_ffff, u32::MAX); + assert_eq!(pixel(g, 11, 20), 0x0f_00f0, "both buffers"); +} + +/// Image words in 12-bit mode come widened by the DDX (expDrawImage12TC: +/// (x & 0xF) << 4 | (x & 0xF0) << 8 | (x & 0xF00) << 12) and land as the +/// 12-bit pixel. +#[test] +fn ddx_12bit_image_word_is_widened_pixel() { + use super::hq2::*; + let g = live_gr2(Gr2Variant::Xz); + ddx_12bit_setup(g, 0xfff, 0); + let widen = |x: u32| ((x & 0xf) << 4) | ((x & 0xf0) << 8) | ((x & 0xf00) << 12); + cmd(g, HQ2_2D_DRAW_IMAGE, 100); + for v in [50, 2, 1, 2, 0, 0] { data(g, v); } + for v in [widen(0x123), widen(0xabc)] { data(g, v); } + cmd(g, HQ2_2D_END_PRIMITIVE, 0); + g.wait_idle(); + assert_eq!(pixel(g, 100, 1023 - 50), 0x123); + assert_eq!(pixel(g, 101, 1023 - 50), 0xabc); +} + +/// A 2D 12-bit draw must not leave the RE3 in 12-bit mode for a 24-bit GL +/// window drawn next. +#[test] +fn gl_24bit_after_ddx_12bit_draw() { + use super::hq2::*; + let g = live_gr2(Gr2Variant::Xz); + gl_setup_window(g); + ddx_12bit_setup(g, 0xfff, 0); + cmd(g, HQ2_2D_SOLID_RECT, 0); + for v in [0, 0, 2, 2] { data(g, v); } + cmd(g, HQ2_2D_END_PRIMITIVE, 0); + gl_quad3(g, [1., 0.5, 0.25], [[0., 0., 0.], [400., 0., 0.], [400., 300., 0.], [0., 300., 0.]]); + g.wait_idle(); + assert_eq!(gl_px(g, 20, 20), 0x40_80ff); +} diff --git a/src/dev/gr2/mod.rs b/src/dev/gr2/mod.rs index 6b17e34..77132ed 100644 --- a/src/dev/gr2/mod.rs +++ b/src/dev/gr2/mod.rs @@ -92,6 +92,10 @@ pub const TP_PROBE_ID: usize = 0x7fff; pub const HQ_FIFO_DEPTH: usize = 65536; pub const RE3_FIFO_DEPTH: usize = 65536; +/// Words the HQ2_GEDMA read port holds ahead of its reader (power of two). +pub const GEDMA_OUT_WORDS: usize = 16384; +/// How long the HQ2 waits for a reader when the read port is full. +const GEDMA_OUT_STALL_LIMIT_NS: u64 = 2_000_000_000; /// Which board is installed. Same hardware family; only the probe answers differ. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -167,14 +171,6 @@ pub struct Gr2 { /// kernel sees FIN2 as soon as the HQ2 gets there. See /// rules/gr2/fin2-wait-must-stall.md. fin2_wait: AtomicU32, - /// Context saves (0x1E1) the CPU has queued that the HQ2 has not executed - /// yet. HQ2_GEDMA reads stall while any is pending: the kernel starts its - /// VDMA read right after queuing 0x1E1, and a read that beat the HQ2 - /// returned the end of the previous image, shifting the saved image one - /// word; its restore was then rejected and the previous context's state - /// stayed live (another demo drawn in amesh's window). See - /// rules/gr2/cx-save-must-wait-for-hq.md. - cx_save_pending: AtomicU32, /// Host time (fin2_clock ns) of the first stalled FIN2 poll since the /// wait was armed (0 = none yet). The stall gives up FIN2_STALL_LIMIT /// after that, so a pipeline that can never finish (RE3 held by a @@ -182,10 +178,15 @@ pub struct Gr2 { /// poll, not from the ack: the DMA between ack and poll may take long on /// a busy host (a 400-row pixel DMA on a GitHub CI runner did). fin2_armed_ns: AtomicU64, - /// Context image handed to the kernel through HQ2_GEDMA reads (0x1E1). - cx_out: [AtomicU32; hq2::CX_WORDS], - cx_out_len: AtomicU32, - cx_out_pos: AtomicU32, + /// HQ2_GEDMA read port: HQ2 -> host words (context saves 0x1E1, pixel + /// DMA reads 0x152 / 0x0AC). A ring the HQ2 thread fills and the reader + /// (the kernel's VDMA) drains; head / tail count words since reset. + /// A read with the ring empty waits (bus busy) while the HQ2 still has + /// work, since the kernel starts the VDMA right after queuing the + /// request. See rules/gr2/gedma-read-port.md. + gedma_out: [AtomicU32; GEDMA_OUT_WORDS], + gedma_out_head: AtomicU32, + gedma_out_tail: AtomicU32, hq_fifo: GFifo, re3_fifo: GFifo, /// Pending half of a two-entry COPY op (RE3 thread only). @@ -324,6 +325,58 @@ impl Gr2 { } } + /// HQ2 thread: a new HQ2_GEDMA read transfer starts. Words an earlier + /// one left unread would shift this one, so drop them. + fn gedma_begin(&self) { + let head = self.gedma_out_head.load(Ordering::Relaxed); + let tail = self.gedma_out_tail.load(Ordering::Acquire); + if head != tail { + crate::dlog_dev!(crate::devlog::LogModule::Gr2, + "GR2: GEDMA: {} words of the previous transfer never read, dropped", head.wrapping_sub(tail)); + let _ = self.gedma_out_tail.compare_exchange(tail, head, Ordering::AcqRel, Ordering::Relaxed); + } + } + + /// HQ2 thread: queue one word on the HQ2_GEDMA read port, waiting while + /// it is full. False if no reader drained it within the limit (or the + /// engine stops): the rest of the transfer is dropped. + fn gedma_push(&self, w: u32) -> bool { + let head = self.gedma_out_head.load(Ordering::Relaxed); + let mut since = 0u64; + while head.wrapping_sub(self.gedma_out_tail.load(Ordering::Acquire)) as usize >= GEDMA_OUT_WORDS { + if !self.running.load(Ordering::Relaxed) { + return false; + } + let now = fin2_clock(); + if since == 0 { + since = now; + } else if now - since > GEDMA_OUT_STALL_LIMIT_NS { + crate::dlog_dev!(crate::devlog::LogModule::Gr2, "GR2: GEDMA read port full, no reader: transfer dropped"); + return false; + } + thread::yield_now(); + } + self.gedma_out[head as usize & (GEDMA_OUT_WORDS - 1)].store(w, Ordering::Relaxed); + self.gedma_out_head.store(head.wrapping_add(1), Ordering::Release); + true + } + + /// Reader: the next HQ2_GEDMA word, if the HQ2 has produced one. + fn gedma_pop(&self) -> Option { + let mut tail = self.gedma_out_tail.load(Ordering::Acquire); + loop { + if tail == self.gedma_out_head.load(Ordering::Acquire) { + return None; + } + let v = self.gedma_out[tail as usize & (GEDMA_OUT_WORDS - 1)].load(Ordering::Relaxed); + // CAS: the CPU and the VDMA worker may both read the port. + match self.gedma_out_tail.compare_exchange(tail, tail.wrapping_add(1), Ordering::AcqRel, Ordering::Acquire) { + Ok(_) => return Some(v), + Err(t) => tail = t, + } + } + } + fn wake(slot: &Mutex>) { if let Some(t) = slot.lock().as_ref() { t.unpark(); @@ -360,20 +413,21 @@ impl Gr2 { FIFO..FIFO_END => 0, HQUCODE..HQUCODE_END => r.hq.ucode[((off - HQUCODE) >> 2) as usize], GEWIN..GEWIN_END => r.ge.read(((off - GEWIN) >> 10) as usize, ((off >> 2) & 0xff) as usize), - // HQ2_GEDMA read: the next word of a context image (0x1E1 save, - // read by the kernel's VDMA). Past the end: 0. + // HQ2_GEDMA read: the next word the HQ2 produced (context image, + // pixel DMA read), read by the kernel's VDMA. 0x6a068 => { - if self.cx_save_pending.load(Ordering::Acquire) != 0 { - // The save the kernel is reading has not run yet. - return BusRead32::busy(); - } - let len = self.cx_out_len.load(Ordering::Acquire); - let pos = self.cx_out_pos.fetch_add(1, Ordering::Relaxed); - if pos < len { - self.cx_out[pos as usize].load(Ordering::Relaxed) - } else { - crate::dlog_dev!(crate::devlog::LogModule::Gr2, "GR2: GEDMA read past the context image ({pos}/{len})"); - 0 + // Sample "HQ2 has work" before looking at the port: a word + // is pushed before the HQ2 goes idle, so an idle HQ2 seen + // here means every word it produced is visible below. + let working = !self.hq_fifo.is_empty() || self.hq_busy.load(Ordering::Acquire); + match self.gedma_pop() { + Some(v) => v, + // Not produced yet: the request is queued or running. + None if working => return BusRead32::busy(), + None => { + crate::dlog_dev!(crate::devlog::LogModule::Gr2, "GR2: GEDMA read overrun (HQ2 idle, nothing to read)"); + 0 + } } } 0x6b000 => if self.fin3_pending.load(Ordering::Acquire) != 0 { 0 } else { r.hq.fin[hq2::FIN3].load(Ordering::Acquire) }, @@ -452,22 +506,15 @@ impl Gr2 { FIFO..FIFO_END => { let idx = (off - FIFO) >> 2; let fin = is_fin3_token(idx); - let save = is_cx_save_token(idx); if fin { // Counted before the push so the HQ can never see the // token before the counter includes it. self.fin3_pending.fetch_add(1, Ordering::AcqRel); } - if save { - self.cx_save_pending.fetch_add(1, Ordering::AcqRel); - } if !self.hq_fifo.try_push(idx, val as u64) { if fin { self.fin3_pending.fetch_sub(1, Ordering::AcqRel); } - if save { - self.cx_save_pending.fetch_sub(1, Ordering::AcqRel); - } return BUS_BUSY; } Self::wake(&self.hq_thread); @@ -611,34 +658,46 @@ impl Gr2 { *w = val; } } - fn cx_publish(&mut self, words: &[u32]) { - let g = self.0; - for (slot, &w) in g.cx_out.iter().zip(words) { - slot.store(w, Ordering::Relaxed); + fn gedma_out(&mut self, words: &[u32]) { + self.0.gedma_begin(); + for &w in words { + if !self.0.gedma_push(w) { + break; + } } - g.cx_out_pos.store(0, Ordering::Relaxed); - g.cx_out_len.store(words.len().min(g.cx_out.len()) as u32, Ordering::Release); } fn op(&mut self, op: u32, val: u64) { self.0.re3_fifo.push(op, val); Gr2::wake(&self.0.re3_thread); } - fn read_image(&mut self, req: &hq2::ReadImage) { + fn read_image(&mut self, req: &hq2::ReadImage, dest: hq2::ReadDest) { while !self.0.re3_fifo.is_empty() || self.0.re3_busy.load(Ordering::Acquire) { std::hint::spin_loop(); } - // SAFETY: RE3 is drained and idle; VRAM is only read here, as - // the display thread does. - let vram = unsafe { &(*self.0.re3.get()).vram }; + // SAFETY: RE3 is drained and idle; VRAM and Z are only read + // here, as the display thread does. + let re3 = unsafe { &*self.0.re3.get() }; + let plane: &[u32] = if req.decode == hq2::ReadDecode::Depth { &re3.zbuf } else { &re3.vram }; let read = |x: i32, y: i32| { if x < 0 || y < 0 || x as usize >= re3::FB_W || y as usize >= re3::FB_H { 0 } else { - vram[y as usize * re3::FB_W + x as usize] + plane[y as usize * re3::FB_W + x as usize] } }; - req.pack(read, &mut self.0.regs().shram[hq2::READ_IMAGE_SHRAM..]); - self.0.regs().hq.fin[hq2::FIN3].store(1, Ordering::Release); + match dest { + hq2::ReadDest::Shram => { + req.pack(read, &mut self.0.regs().shram[hq2::READ_IMAGE_SHRAM..]); + self.0.regs().hq.fin[hq2::FIN3].store(1, Ordering::Release); + } + hq2::ReadDest::Gedma => { + let mut words = vec![0u32; req.rows as usize * req.words_per_row as usize]; + req.pack(read, &mut words); + self.gedma_out(&words); + // The kernel polls FIN2 once its VDMA has read them. + self.0.regs().hq.fin[hq2::FIN2].store(1, Ordering::Release); + } + } } } *self.hq_thread.lock() = Some(thread::current()); @@ -665,11 +724,6 @@ impl Gr2 { let _ = self.fin3_pending.fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| Some(n.saturating_sub(1))); } - if is_cx_save_token(index) { - // Executed: the image is published (cx_publish). - let _ = self.cx_save_pending.fetch_update(Ordering::AcqRel, Ordering::Acquire, - |n| Some(n.saturating_sub(1))); - } self.hq_fifo.consume(); backoff.reset(); } else { @@ -824,20 +878,13 @@ impl BusDevice for Gr2 { // Both words or neither: a retried store must not push the first twice. let idx = (off - FIFO) >> 2; let fins = is_fin3_token(idx) as u32 + is_fin3_token(idx + 1) as u32; - let saves = is_cx_save_token(idx) as u32 + is_cx_save_token(idx + 1) as u32; if fins != 0 { self.fin3_pending.fetch_add(fins, Ordering::AcqRel); } - if saves != 0 { - self.cx_save_pending.fetch_add(saves, Ordering::AcqRel); - } if !self.hq_fifo.try_push2(idx, hi as u64, idx + 1, lo as u64) { if fins != 0 { self.fin3_pending.fetch_sub(fins, Ordering::AcqRel); } - if saves != 0 { - self.cx_save_pending.fetch_sub(saves, Ordering::AcqRel); - } return BUS_BUSY; } Self::wake(&self.hq_thread); @@ -890,7 +937,8 @@ impl Device for Gr2 { self.hq_fifo.reset(); self.fin3_pending.store(0, Ordering::Release); self.fin2_wait.store(0, Ordering::Release); - self.cx_save_pending.store(0, Ordering::Release); + let head = self.gedma_out_head.load(Ordering::Acquire); + self.gedma_out_tail.store(head, Ordering::Release); self.re3_fifo.reset(); } @@ -984,12 +1032,6 @@ fn fin2_clock() -> u64 { START.get_or_init(std::time::Instant::now).elapsed().as_nanos() as u64 } -/// Context save main (0x1E1): publishes the image the kernel then reads -/// from HQ2_GEDMA. -fn is_cx_save_token(idx: u32) -> bool { - idx == hq2::GE_CX_SAVE_MAIN -} - fn is_fin3_token(idx: u32) -> bool { idx == hq2::GL_FINISH || idx == hq2::HQ_GL_FIN3 } diff --git a/test/gltest/glprim b/test/gltest/glprim index 50b85f2f1bae013158463e7615882e5331830ab5..cc5adc5336d28b9f5c777a202cddd44b3c48dfc2 100755 GIT binary patch literal 67676 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 LXRc>=e$oB{H*j*L delta 13144 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 I!o2JM004qzDgXcg diff --git a/test/gltest/glprim.c b/test/gltest/glprim.c index 18fd6b5..7fde077 100644 --- a/test/gltest/glprim.c +++ b/test/gltest/glprim.c @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -59,6 +61,33 @@ static char scene[16] = ""; /* --scene depth|stencil|alphatest|blend * static GLenum depth_func = GL_LESS; /* --depthfunc */ static int want_stencil = 0; +/* ---- readback (--read) --------------------------------------------------- + After the draw (and glFinish), read pixels back and print what came back: + ximage XGetImage of the window (Xsgi, front buffer) + front glReadBuffer(GL_FRONT) + glReadPixels colour + back glReadBuffer(GL_BACK) + glReadPixels colour (needs --db) + depth glReadPixels GL_DEPTH_COMPONENT (needs a depth buffer) + stencil glReadPixels GL_STENCIL_INDEX (needs a stencil buffer) + With --db the scene is swapped to the front, then the back buffer is + cleared to --backclear with a white marker (bottom left), so front and + back reads differ. libglcore (EXPRESS gr2_pixel.c) picks the path: + colour GL_RGBA/UNSIGNED_BYTE kernel pixel DMA (0x0AC), rows swapped + colour GL_ABGR_EXT/UNSIGNED_BYTE kernel pixel DMA (0x0AC), as is + depth UNSIGNED_INT kernel pixel DMA (0x0AC), buffer_mode 2 + unaligned buffer / odd stride PIO READ_RECT (0x0AD) via the mailbox + GL_FLOAT, stencil slow path, one READ_RECT per pixel + --readfmt/--readtype/--readunaligned choose among them. */ +#define MAX_READS 8 +static char reads[MAX_READS][8]; +static int nreads = 0; +static int rrect[4] = { -1, 0, 0, 0 }; /* --readrect x,y,w,h (GL window coords) */ +static char readfmt[8] = "rgba"; /* --readfmt rgba|abgr */ +static char readtype[8] = "ub"; /* --readtype ub|float (colour), uint|float (depth) */ +static int read_unaligned = 0; /* --readunaligned: force the PIO path */ +static int pack_row = 0; /* --packrow N: GL_PACK_ROW_LENGTH */ +static const char *read_out = 0; /* --readout PREFIX: write PREFIX_.ppm/.pgm */ +static float back_rgb[3] = { 0.0f, 0.75f, 0.75f }; /* --backclear */ + /* ---- multi-primitive scenes (window pixel coordinates, glOrtho) --------- Each is small and has a known expected image: depth: red triangle at z = 0, then a blue band whose z runs from -0.5 @@ -254,6 +283,47 @@ static void draw_scene(float w, float h) { glColor4f(0, 0, 1, 1.0f); glVertex2f(0.5f * w, 0.9f * h); glEnd(); glDisable(GL_BLEND); + } else if (!strcmp(scene, "quadrants")) { + /* Readback pattern: every orientation error shows. Border and gaps + keep the clear colour; depth and stencil differ per quadrant. + bottom left red, z 0.5 (depth 0.25), stencil 1 + bottom right green, z 0.0 (depth 0.50), stencil 2 + top left blue, z -0.5 (depth 0.75), stencil 3 + top right black -> yellow ramp left to right, z 0.9 -> -0.9 + (depth 0.05 -> 0.95), stencil 4 */ + float x0 = 8, x1 = w * 0.5f - 4, x2 = w * 0.5f + 4, x3 = w - 8; + float y0 = 8, y1 = h * 0.5f - 4, y2 = h * 0.5f + 4, y3 = h - 8; + if (want_stencil) { + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 0, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + } + glShadeModel(GL_FLAT); + if (want_stencil) glStencilFunc(GL_ALWAYS, 1, 0xff); + glBegin(GL_QUADS); + glColor3f(1, 0, 0); + glVertex3f(x0, y0, 0.5f); glVertex3f(x1, y0, 0.5f); glVertex3f(x1, y1, 0.5f); glVertex3f(x0, y1, 0.5f); + glEnd(); + if (want_stencil) glStencilFunc(GL_ALWAYS, 2, 0xff); + glBegin(GL_QUADS); + glColor3f(0, 1, 0); + glVertex3f(x2, y0, 0.0f); glVertex3f(x3, y0, 0.0f); glVertex3f(x3, y1, 0.0f); glVertex3f(x2, y1, 0.0f); + glEnd(); + if (want_stencil) glStencilFunc(GL_ALWAYS, 3, 0xff); + glBegin(GL_QUADS); + glColor3f(0, 0, 1); + glVertex3f(x0, y2, -0.5f); glVertex3f(x1, y2, -0.5f); glVertex3f(x1, y3, -0.5f); glVertex3f(x0, y3, -0.5f); + glEnd(); + if (want_stencil) glStencilFunc(GL_ALWAYS, 4, 0xff); + glShadeModel(GL_SMOOTH); + glBegin(GL_QUADS); + glColor3f(0, 0, 0); glVertex3f(x2, y2, 0.9f); + glColor3f(1, 1, 0); glVertex3f(x3, y2, -0.9f); + glColor3f(1, 1, 0); glVertex3f(x3, y3, -0.9f); + glColor3f(0, 0, 0); glVertex3f(x2, y3, 0.9f); + glEnd(); + glShadeModel(smooth ? GL_SMOOTH : GL_FLAT); + glDisable(GL_STENCIL_TEST); } else { printf("unknown scene %s\n", scene); } @@ -365,6 +435,15 @@ static void usage(void) { printf(" --scene NAME depth | stencil | alphatest | blend | blendsmooth |\n" " lit | litlocal | twoside | fog (see source)\n" " --depthfunc F never less equal lequal greater notequal gequal always\n"); + printf(" --read M[,M...] after drawing, read back: ximage front back depth stencil\n" + " --readrect X,Y,W,H area to read (GL window coords; default whole window)\n" + " --readfmt rgba|abgr colour format for glReadPixels (default rgba)\n" + " --readtype T ub|float for colour, uint|float for depth (default ub/uint)\n" + " --readunaligned read into a misaligned buffer (forces the PIO path)\n" + " --packrow N GL_PACK_ROW_LENGTH N (strided pixel DMA)\n" + " --readout PREFIX also write PREFIX_.ppm (colour) / .pgm (depth, stencil)\n" + " --backclear R,G,B with --db: back buffer colour for the back read (default 0,.75,.75)\n" + " (--scene quadrants is the readback pattern: depth + stencil, see source)\n"); } static int parse_ints(const char *s, int *out, int n) { @@ -449,7 +528,27 @@ static void parse_args(int argc, char **argv) { strncpy(scene, next, sizeof(scene) - 1); if (!strcmp(scene, "depth")) depth = 1; if (!strcmp(scene, "stencil")) want_stencil = 1; - } else if (!strcmp(a, "--depthfunc")) { + if (!strcmp(scene, "quadrants")) { depth = 1; want_stencil = 1; } + } else if (!strcmp(a, "--read")) { + char buf[64], *t; + NEED(); + strncpy(buf, next, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = 0; + for (t = strtok(buf, ","); t; t = strtok(0, ",")) { + if (strcmp(t, "ximage") && strcmp(t, "front") && strcmp(t, "back") + && strcmp(t, "depth") && strcmp(t, "stencil")) { + fprintf(stderr, "bad --read mode %s\n", t); exit(2); + } + if (nreads < MAX_READS) strncpy(reads[nreads++], t, 7); + } + } else if (!strcmp(a, "--readrect")) { NEED(); if (!parse_ints(next, rrect, 4)) { fprintf(stderr, "bad --readrect\n"); exit(2); } } + else if (!strcmp(a, "--readfmt")) { NEED(); strncpy(readfmt, next, sizeof(readfmt) - 1); } + else if (!strcmp(a, "--readtype")) { NEED(); strncpy(readtype, next, sizeof(readtype) - 1); } + else if (!strcmp(a, "--readunaligned")) read_unaligned = 1; + else if (!strcmp(a, "--packrow")) { NEED(); pack_row = atoi(next); } + else if (!strcmp(a, "--readout")) { NEED(); read_out = next; } + else if (!strcmp(a, "--backclear")) { NEED(); if (!parse_floats(next, back_rgb, 3)) { fprintf(stderr, "bad --backclear\n"); exit(2); } } + else if (!strcmp(a, "--depthfunc")) { static const char *names[] = { "never", "less", "equal", "lequal", "greater", "notequal", "gequal", "always" }; NEED(); for (k = 0; k < 8; k++) if (!strcmp(next, names[k])) break; @@ -462,6 +561,207 @@ static void parse_args(int argc, char **argv) { } } +/* ---- readback ------------------------------------------------------------ */ + +enum { RB_COLOR, RB_DEPTH, RB_STENCIL }; + +static double now_ms(void) { + struct timeval tv; + gettimeofday(&tv, 0); + return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0; +} + +/* Width in bits and shift of a visual channel mask. */ +static void mask_bits(unsigned long m, int *shift, int *bits) { + *shift = 0; *bits = 0; + if (!m) return; + while (!(m & 1)) { m >>= 1; (*shift)++; } + while (m & 1) { m >>= 1; (*bits)++; } +} + +/* Scale an n-bit channel value to 8 bits by bit replication. */ +static unsigned long to8(unsigned long v, int bits) { + unsigned long r = 0; + int have = 0; + if (bits <= 0) return 0; + if (bits >= 8) return v >> (bits - 8); + while (have < 8) { r = (r << bits) | v; have += bits; } + return r >> (have - 8); +} + +/* Read one buffer into vals[] (GL order: row 0 = bottom), as 0xRRGGBBAA + for colour, the raw 32-bit value for depth, the index for stencil. + Returns the kind, or -1 when the read is not possible. */ +static int read_pixels(Display *dpy, Window win, XVisualInfo *vi, const char *m, + int x, int y, int w, int h, unsigned long *vals) { + int row = pack_row > w ? pack_row : w; + int r, c; + if (!strcmp(m, "ximage")) { + XImage *im; + int rs, rb, gs, gb, bs, bb; + mask_bits(vi->red_mask, &rs, &rb); + mask_bits(vi->green_mask, &gs, &gb); + mask_bits(vi->blue_mask, &bs, &bb); + im = XGetImage(dpy, win, x, win_h - y - h, (unsigned)w, (unsigned)h, AllPlanes, ZPixmap); + if (!im) { printf(" XGetImage failed\n"); return -1; } + for (r = 0; r < h; r++) + for (c = 0; c < w; c++) { + unsigned long p = XGetPixel(im, c, h - 1 - r); + vals[r * w + c] = (to8((p & vi->red_mask) >> rs, rb) << 24) + | (to8((p & vi->green_mask) >> gs, gb) << 16) + | (to8((p & vi->blue_mask) >> bs, bb) << 8) | 0xff; + } + XDestroyImage(im); + return RB_COLOR; + } + glPixelStorei(GL_PACK_ALIGNMENT, read_unaligned ? 1 : 4); + glPixelStorei(GL_PACK_ROW_LENGTH, pack_row); + if (!strcmp(m, "front") || !strcmp(m, "back")) { + int isf = !strcmp(readtype, "float"); + int abgr = !strcmp(readfmt, "abgr"); + size_t bpp = isf ? 16 : 4; + char *mem = malloc((size_t)row * h * bpp + 8); + unsigned char *b = (unsigned char *)mem + (read_unaligned ? 1 : 0); + if (!mem) return -1; + glReadBuffer(!strcmp(m, "front") ? GL_FRONT : GL_BACK); +#ifdef GL_ABGR_EXT + glReadPixels(x, y, w, h, abgr ? GL_ABGR_EXT : GL_RGBA, isf ? GL_FLOAT : GL_UNSIGNED_BYTE, b); +#else + if (abgr) printf(" (no GL_ABGR_EXT, reading GL_RGBA)\n"); + abgr = 0; + glReadPixels(x, y, w, h, GL_RGBA, isf ? GL_FLOAT : GL_UNSIGNED_BYTE, b); +#endif + for (r = 0; r < h; r++) + for (c = 0; c < w; c++) { + unsigned long ch[4]; + int k; + for (k = 0; k < 4; k++) { + if (isf) { + float f; + memcpy(&f, b + ((size_t)r * row + c) * 16 + k * 4, 4); + ch[k] = (unsigned long)(f * 255.0f + 0.5f) & 0xff; + } else ch[k] = b[((size_t)r * row + c) * 4 + k]; + } + /* ABGR_EXT is A, B, G, R in memory. */ + vals[r * w + c] = abgr + ? (ch[3] << 24) | (ch[2] << 16) | (ch[1] << 8) | ch[0] + : (ch[0] << 24) | (ch[1] << 16) | (ch[2] << 8) | ch[3]; + } + free(mem); + return RB_COLOR; + } + if (!strcmp(m, "depth")) { + int isf = !strcmp(readtype, "float"); + char *mem = malloc((size_t)row * h * 4 + 8); + unsigned char *b = (unsigned char *)mem + (read_unaligned ? 1 : 0); + if (!mem) return -1; + glReadPixels(x, y, w, h, GL_DEPTH_COMPONENT, isf ? GL_FLOAT : GL_UNSIGNED_INT, b); + for (r = 0; r < h; r++) + for (c = 0; c < w; c++) { + if (isf) { + float f; + memcpy(&f, b + ((size_t)r * row + c) * 4, 4); + vals[r * w + c] = (unsigned long)((double)f * 4294967295.0); + } else { + unsigned int u; + memcpy(&u, b + ((size_t)r * row + c) * 4, 4); + vals[r * w + c] = u; + } + } + free(mem); + return RB_DEPTH; + } + if (!strcmp(m, "stencil")) { + unsigned char *b = malloc((size_t)row * h + 8); + if (!b) return -1; + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(x, y, w, h, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, b); + for (r = 0; r < h; r++) + for (c = 0; c < w; c++) vals[r * w + c] = b[(size_t)r * row + c]; + free(b); + return RB_STENCIL; + } + return -1; +} + +static void print_val(int kind, unsigned long v) { + if (kind == RB_COLOR) printf("%08lx", v); + else if (kind == RB_DEPTH) printf("%08lx (%.3f)", v, (double)v / 4294967295.0); + else printf("%lu", v); +} + +/* PPM/PGM, top row first (image file convention). */ +static void write_image(const char *m, int kind, int w, int h, const unsigned long *vals) { + char name[256]; + FILE *f; + int r, c; + sprintf(name, "%.200s_%s.%s", read_out, m, kind == RB_COLOR ? "ppm" : "pgm"); + f = fopen(name, "wb"); + if (!f) { printf(" cannot write %s\n", name); return; } + fprintf(f, "%s\n%d %d\n255\n", kind == RB_COLOR ? "P6" : "P5", w, h); + for (r = h - 1; r >= 0; r--) + for (c = 0; c < w; c++) { + unsigned long v = vals[r * w + c]; + if (kind == RB_COLOR) { + fputc((int)(v >> 24) & 0xff, f); + fputc((int)(v >> 16) & 0xff, f); + fputc((int)(v >> 8) & 0xff, f); + } else if (kind == RB_DEPTH) fputc((int)(v >> 24) & 0xff, f); + else fputc(v > 4 ? 255 : (int)v * 60, f); + } + fclose(f); + printf(" wrote %s\n", name); +} + +static void do_reads(Display *dpy, Window win, XVisualInfo *vi) { + int x = 0, y = 0, w = win_w, h = win_h, i; + unsigned long *vals; + if (rrect[0] >= 0) { x = rrect[0]; y = rrect[1]; w = rrect[2]; h = rrect[3]; } + vals = malloc(sizeof(unsigned long) * (size_t)w * h); + if (!vals) return; + for (i = 0; i < nreads; i++) { + /* Sample points, relative to the read rectangle: corners 2 px in, + the centre, the quadrant centres (the --scene quadrants colours). */ + static const char *lbl[9] = { "bl", "br", "tl", "tr", "centre", "q.bl", "q.br", "q.tl", "q.tr" }; + int sx[9], sy[9], kind, k; + unsigned long sum = 2166136261UL; + GLenum err; + double t0, t1; + sx[0] = 2; sy[0] = 2; sx[1] = w - 3; sy[1] = 2; sx[2] = 2; sy[2] = h - 3; + sx[3] = w - 3; sy[3] = h - 3; sx[4] = w / 2; sy[4] = h / 2; + sx[5] = w / 4; sy[5] = h / 4; sx[6] = 3 * w / 4; sy[6] = h / 4; + sx[7] = w / 4; sy[7] = 3 * h / 4; sx[8] = 3 * w / 4; sy[8] = 3 * h / 4; + memset(vals, 0, sizeof(unsigned long) * (size_t)w * h); + while (glGetError() != GL_NO_ERROR) ; + t0 = now_ms(); + kind = read_pixels(dpy, win, vi, reads[i], x, y, w, h, vals); + t1 = now_ms(); + err = glGetError(); + if (kind < 0) continue; + for (k = 0; k < w * h; k++) { + /* Colour: RGB only, so XGetImage and glReadPixels compare. */ + unsigned long v = kind == RB_COLOR ? vals[k] >> 8 : vals[k]; + sum = ((sum ^ (v & 0xff)) * 16777619UL) & 0xffffffffUL; + sum = ((sum ^ ((v >> 8) & 0xff)) * 16777619UL) & 0xffffffffUL; + sum = ((sum ^ ((v >> 16) & 0xff)) * 16777619UL) & 0xffffffffUL; + sum = ((sum ^ ((v >> 24) & 0xff)) * 16777619UL) & 0xffffffffUL; + } + printf("glprim: read %s %dx%d at %d,%d%s%s%s: %.1f ms, gl error 0x%x, sum %08lx\n", + reads[i], w, h, x, y, + kind == RB_COLOR && strcmp(reads[i], "ximage") ? (strcmp(readfmt, "abgr") ? " rgba" : " abgr") : "", + strcmp(reads[i], "ximage") && strcmp(reads[i], "stencil") ? (!strcmp(readtype, "float") ? " float" : "") : "", + read_unaligned ? " unaligned" : "", t1 - t0, (unsigned)err, sum); + for (k = 0; k < 9; k++) { + printf(" %-6s (%3d,%3d) ", lbl[k], sx[k], sy[k]); + print_val(kind, vals[sy[k] * w + sx[k]]); + printf("\n"); + } + if (read_out) write_image(reads[i], kind, w, h, vals); + fflush(stdout); + } + free(vals); +} + /* ---- main ---------------------------------------------------------------- */ int main(int argc, char **argv) { @@ -611,6 +911,24 @@ int main(int argc, char **argv) { else glFlush(); glFinish(); + if (nreads > 0) { + if (dbl) { + /* The scene is now in front; give the back buffer its own + content: --backclear with a white 40x40 marker at the bottom left + (the "bl" sample). + Depth and stencil keep the scene's values. */ + glDrawBuffer(GL_BACK); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glClearColor(back_rgb[0], back_rgb[1], back_rgb[2], 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glColor3f(1, 1, 1); + glRecti(0, 0, 40, 40); + glFinish(); + } + do_reads(dpy, win, vi); + } + if (hold_ms > 0) { int left = hold_ms; XSync(dpy, False); diff --git a/test/gltest/gltest b/test/gltest/gltest index 7a8f6a5255a5ab4699067076ac6b9f5102151d56..97cbc8d2e3247df032d3bcd3ca33986ce7667139 100755 GIT binary patch delta 27 lcmV+$0ObFq(gLK?0