From d4e336435d2e683d8df0c0e75d7648a32ff130cf Mon Sep 17 00:00:00 2001 From: technomancer Date: Thu, 8 Oct 2026 20:33:09 -0700 Subject: [PATCH 1/2] refactor gr2 to use common t&l code and add 0xCF current color readback --- src/dev/gl/light.rs | 8 +- src/dev/gl/mod.rs | 6 +- src/dev/gl/vertex.rs | 50 +++-- src/dev/gr2/gl.rs | 205 +++++++------------- src/dev/gr2/gl_light.rs | 407 ++++++++++----------------------------- src/dev/gr2/hq2_tests.rs | 16 ++ src/dev/mgras/gl.rs | 14 +- 7 files changed, 240 insertions(+), 466 deletions(-) diff --git a/src/dev/gl/light.rs b/src/dev/gl/light.rs index 56122850..a2dd977e 100644 --- a/src/dev/gl/light.rs +++ b/src/dev/gl/light.rs @@ -1,6 +1,5 @@ -//! The OpenGL 1.1 lighting equation and fog, per vertex. Copied from GR2's -//! GE7 HLE (`src/dev/gr2/gl_light.rs`), without its FIFO token loader: a -//! board fills `Lighting` from its own protocol. +//! The OpenGL 1.1 lighting equation and fog, per vertex. A board fills +//! `Lighting` from its own protocol (GR2: `src/dev/gr2/gl_light.rs`). //! //! Two things vary between boards' libglcore and are parameters here: //! - specular and spot exponents arrive either as numbers (`shininess`, @@ -86,7 +85,8 @@ fn dot(a: [f32; 3], b: [f32; 3]) -> f32 { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] } -fn norm(a: [f32; 3]) -> [f32; 3] { +/// `a` scaled to unit length (unchanged if ~0). +pub fn norm(a: [f32; 3]) -> [f32; 3] { let l = dot(a, a).sqrt(); if l > 1e-20 { [a[0] / l, a[1] / l, a[2] / l] } else { a } } diff --git a/src/dev/gl/mod.rs b/src/dev/gl/mod.rs index 08a54ca2..7852e64b 100644 --- a/src/dev/gl/mod.rs +++ b/src/dev/gl/mod.rs @@ -1,7 +1,7 @@ //! Board-independent OpenGL core for the high-level geometry engines: what -//! the GE microcode of every SGI board does the same way. GR4 (IMPACT's -//! GE11) builds on it; GR2 (GE7, `src/dev/gr2/gl.rs`) is where it was copied -//! from and moves onto it next. +//! the GE microcode of every SGI board does the same way. GR2 (GE7, +//! `src/dev/gr2/gl.rs`) and GR4 (IMPACT's GE11, `src/dev/mgras/gl.rs`) +//! build on it. //! //! - `math`: 4x4 matrices (column-major, glLoadMatrix order), the GL matrix //! builders and matrix stacks. diff --git a/src/dev/gl/vertex.rs b/src/dev/gl/vertex.rs index 64c3f7da..1b65bf8d 100644 --- a/src/dev/gl/vertex.rs +++ b/src/dev/gl/vertex.rs @@ -1,9 +1,12 @@ //! The transformed vertex, clipping and the viewport mapping. /// A vertex after transform: window coordinates (GL: y up), colour 0..1. -#[derive(Clone, Copy, Default, Debug)] +/// `T` texture coordinates and `F` fog factors ride along and are +/// interpolated by clipping; a board without them uses 0 (GR2: `Wv<0, 0>`, +/// IMPACT: `Wv<4, 1>`). +#[derive(Clone, Copy, Debug)] #[repr(C)] -pub struct Wv { +pub struct Wv { pub x: f32, pub y: f32, pub z: f32, @@ -22,9 +25,28 @@ pub struct Wv { pub h: [f32; 4], pub e: [f32; 4], /// Texture coordinates (s, t, r, q), after the texture matrix. - pub t: [f32; 4], + pub t: [f32; T], /// Fog factor (1 = no fog), when fog is applied per fragment. - pub f: f32, + pub f: [f32; F], +} + +// Not derived: `[f32; N]: Default` exists only for literal sizes. +impl Default for Wv { + fn default() -> Self { + Wv { + x: 0.0, + y: 0.0, + z: 0.0, + c: [0.0; 4], + cb: [0.0; 4], + ok: 0, + oc: 0, + h: [0.0; 4], + e: [0.0; 4], + t: [0.0; T], + f: [0.0; F], + } + } } /// Clip planes: 0..5 the view volume -w <= x, y, z <= w, 6..11 the user @@ -38,7 +60,7 @@ pub const MAX_POLY: usize = 32 + CLIP_PLANES; /// Point on a-b at the crossing of a plane where the signed distances are /// da and db: positions and colours interpolated, window position still to /// be computed (`Viewport::project`). -pub fn clip_cross(a: &Wv, b: &Wv, da: f32, db: f32) -> Wv { +pub fn clip_cross(a: &Wv, b: &Wv, da: f32, db: f32) -> Wv { let t = da / (da - db); let l = |x: f32, y: f32| x + (y - x) * t; let mut v = *a; @@ -48,9 +70,13 @@ pub fn clip_cross(a: &Wv, b: &Wv, da: f32, db: f32) -> Wv { v.e[k] = l(a.e[k], b.e[k]); v.c[k] = l(a.c[k], b.c[k]); v.cb[k] = l(a.cb[k], b.cb[k]); + } + for k in 0..T { v.t[k] = l(a.t[k], b.t[k]); } - v.f = l(a.f, b.f); + for k in 0..F { + v.f[k] = l(a.f[k], b.f[k]); + } v } @@ -65,7 +91,7 @@ pub struct Clip { impl Clip { /// Signed distance of `v` to clip plane `i` (>= 0 inside). - pub fn dist(&self, v: &Wv, i: usize) -> f32 { + pub fn dist(&self, v: &Wv, i: usize) -> f32 { let h = &v.h; match i { 0 => h[3] + h[0], @@ -87,7 +113,7 @@ impl Clip { } /// Bit i set when `v` is outside plane i. - pub fn outcode(&self, v: &Wv) -> u32 { + pub fn outcode(&self, v: &Wv) -> u32 { let mask = self.mask(); (0..CLIP_PLANES).filter(|&i| mask & (1 << i) != 0 && self.dist(v, i) < 0.0) .fold(0, |o, i| o | (1 << i)) @@ -96,7 +122,7 @@ impl Clip { /// Clip the polygon `v[..n]` against every plane its vertices are /// outside of, in place (Sutherland-Hodgman). Returns the new vertex /// count (0: nothing left). New vertices still need projecting. - pub fn clip_polygon(&self, v: &mut [Wv; MAX_POLY], n: usize) -> usize { + pub fn clip_polygon(&self, v: &mut [Wv; MAX_POLY], n: usize) -> usize { let any = v[..n].iter().fold(0, |o, w| o | w.oc); if any == 0 { return n; @@ -127,7 +153,7 @@ impl Clip { } /// Clip the segment a-b; None when it is entirely outside. - pub fn clip_line(&self, mut a: Wv, mut b: Wv) -> Option<(Wv, Wv)> { + pub fn clip_line(&self, mut a: Wv, mut b: Wv) -> Option<(Wv, Wv)> { let any = a.oc | b.oc; for plane in 0..CLIP_PLANES { if any & (1 << plane) == 0 { @@ -171,7 +197,7 @@ impl Viewport { /// offset by `origin` and snapped to 1/`subpixel` of a pixel (the /// raster engine's fixed-point precision; removes float noise such as /// 319.99998 on edges). - pub fn project(&self, mut out: Wv, origin: [f32; 2], subpixel: f32) -> Wv { + pub fn project(&self, mut out: Wv, origin: [f32; 2], subpixel: f32) -> Wv { let [cx, cy, cz, cw] = out.h; out.ok = 0; if cw <= 1e-6 { @@ -191,6 +217,8 @@ impl Viewport { mod tests { use super::*; + type Wv = super::Wv<4, 1>; + fn at(x: f32, y: f32) -> Wv { let mut v = Wv { h: [x, y, 0.0, 1.0], ..Default::default() }; v.oc = Clip::default().outcode(&v); diff --git a/src/dev/gr2/gl.rs b/src/dev/gr2/gl.rs index a4aa715e..047bc012 100644 --- a/src/dev/gr2/gl.rs +++ b/src/dev/gr2/gl.rs @@ -15,12 +15,20 @@ //! libglcore rasterizes alpha-tested primitives on the CPU and sends the //! surviving fragments (T_FRAGMENT). See GR2.h "BLENDING AND ALPHA TEST". //! -//! Current scope: RGB windows, no lighting (colours from glColor), no -//! clipping beyond the window/scissor rectangle (vertices behind the eye -//! reject the whole primitive), lines and points without Z. +//! Transform, view-volume and user clipping, the viewport mapping, lighting +//! and fog use the board-independent GL core (`crate::dev::gl`); this file +//! and gl_light.rs decode the GR2 token protocol into it and keep what is +//! GR2's own: primitive assembly, polygon clipping on vertex-buffer indices +//! (with GL_LINE edge flags), window clipping (0x1E5) and RE3 rasterization. use super::re3; use super::{Hq2Engine, Re3Sink, SCREEN_H}; +use crate::dev::gl::math::{self, Mat4}; +use crate::dev::gl::light::Lighting; +use crate::dev::gl::vertex::{self, clip_cross, Clip, Viewport, CLIP_PLANES}; + +/// GR2 vertices: no texture, fog applied per vertex. +pub type Wv = vertex::Wv<0, 0>; #[path = "gl_light.rs"] mod light; @@ -114,6 +122,11 @@ pub const T_IRIS_SETCPOS: u32 = 0x0d0; /// setcpos use the same screen space: writepixels places its row at /// getcpos - getorigin. pub const T_GET_ORIGIN: u32 = 0x0cd; +/// Current colour readback (IRIS GL gRGBcolor, getcolor; blast per frame): +/// token 0; Finish; the mailbox holds R, G, B as the RE3's 8.11 iterator +/// values (libgl takes (w >> 11) & 0xff of each; getcolor masks R with the +/// write mask, the colour index in CI mode). +pub const T_IRIS_GET_COLOR: u32 = 0x0cf; /// FIFO pixel writes (IRIS GL writepixels / writeRGB via rectwrite; OpenGL /// glDrawPixels, __glExpDrawPixelsUnpack*), per chunk of <= 48 pixels: /// 0x0B1 = 0 start (the chunk's data follows) @@ -281,33 +294,10 @@ const USEV: u32 = 0x200; const LOADV: u32 = 0x400; const ITOF: u32 = 0x4000; -/// A vertex after transform: window coordinates (GL: y up), colour 0..1. -#[derive(Clone, Copy, Default)] -#[repr(C)] -pub struct Wv { - pub x: f32, - pub y: f32, - pub z: f32, - pub c: [f32; 4], - /// Back-face colour (two-sided lighting; = c otherwise). - pub cb: [f32; 4], - /// 0 when the vertex is behind the eye (w <= 0): it has no window - /// position and its primitive must be clipped (gl_poly / gl_line). - pub ok: u32, - /// Clip planes the vertex is outside of (bit i = plane i), computed once - /// at transform; 0 for vertices made by clipping or in window space. - pub oc: u32, - /// Clip-space position (frustum clipping) and eye-space position (user - /// clip planes). Both are linear in the object position, so clipping - /// interpolates them along an edge exactly. - pub h: [f32; 4], - pub e: [f32; 4], -} - -/// Clip planes: 0..5 the view volume -w <= x, y, z <= w (GL and IRIS GL -/// alike; their different Z ranges come after clipping, in the viewport -/// transform), 6..11 the user planes (0x02E / 0x02F, eye space). -const CLIP_PLANES: usize = 12; +// Clip planes (vertex::CLIP_PLANES): 0..5 the view volume -w <= x, y, z +// <= w (GL and IRIS GL alike; their different Z ranges come after +// clipping, in the viewport transform), 6..11 the user planes (0x02E / +// 0x02F, eye space). /// Most visible rectangles a 0x1E5 clip decomposes into (visible_rects). const MAX_VIS: usize = 32; @@ -325,23 +315,6 @@ const MAXP: usize = VB + CLIP_PLANES; const SCRATCH: usize = 2 * CLIP_PLANES; const ALL_EDGES: u64 = u64::MAX; -/// Point on a-b at the crossing of a plane where the signed distances are -/// da and db: positions and colours interpolated, window position still to -/// be computed (GlState::project). -fn clip_cross(a: &Wv, b: &Wv, da: f32, db: f32) -> Wv { - let t = da / (da - db); - let l = |x: f32, y: f32| x + (y - x) * t; - let mut v = *a; - v.oc = 0; - for k in 0..4 { - v.h[k] = l(a.h[k], b.h[k]); - v.e[k] = l(a.e[k], b.e[k]); - v.c[k] = l(a.c[k], b.c[k]); - v.cb[k] = l(a.cb[k], b.cb[k]); - } - v -} - /// GL state held by the HQ2/GE7. Plain data, valid when zeroed. #[derive(Clone, Copy)] #[repr(C)] @@ -363,11 +336,11 @@ pub struct GlState { wid_sent: u32, /// Window: x0, y0 (screen, GL y up), w, h. win: [i32; 4], - /// Viewport (window relative): x0, x1, y0, y1, zscale, zcenter. - vp: [f32; 6], - mv: [f32; 16], - proj: [f32; 16], - mvp: [f32; 16], + /// Viewport (window relative); 0x03B carries its fields in order. + vp: Viewport, + mv: Mat4, + proj: Mat4, + mvp: Mat4, mvp_dirty: u32, smooth: u32, color: [f32; 4], @@ -404,8 +377,9 @@ pub struct GlState { blend_fast: u32, logic_op: u32, normal: [f32; 3], - /// Lighting and fog (gl_light.rs). - lt: light::Lighting, + /// Lighting and fog (shared core), and where gl_light.rs's loads go. + lt: Lighting, + ltl: light::Loader, front_ccw: u32, polymode: u32, /// Self-streaming port being assembled. @@ -440,20 +414,21 @@ pub struct GlState { /// Pixel zoom (0x0BB): x, y; 0 = 1. pzoom: [f32; 2], /// User clip planes (eye space) and their enable bits. - uclip: [[f32; 4]; 6], - uclip_on: u32, + clip: Clip, /// Read source from 0x10A: kind, buffer (0, 0 = front). read_src: [u32; 2], /// IRIS GL lmcolor(LMC_COLOR) rule: an IRIS GL colour command after the /// last IRIS GL normal leaves what follows unlit (see T_IRIS_COLOR). /// [0] = set, [1] spare (keeps GlState free of padding). iris_unlit: [u32; 2], + /// Spare word: GlState is 8-byte aligned (`span`), so its 4-byte + /// fields must add up to an even count or the struct gets tail padding + /// (asserted below). Drop or add one when a field changes size. + pad: u32, /// Vertices drawn / primitives emitted since the last trace note. pub stats_vertices: u32, } -const IDENT: [f32; 16] = [1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]; - impl GlState { /// Range of the R channel in GL colour units (see to_fixed). fn cmax(&self) -> f32 { @@ -463,22 +438,23 @@ impl GlState { fn ensure_init(&mut self) { if self.inited == 0 { self.inited = 1; - self.mv = IDENT; - self.proj = IDENT; - self.mvp = IDENT; + self.mv = math::IDENT; + self.proj = math::IDENT; + self.mvp = math::IDENT; self.color = [1.0; 4]; self.smooth = 1; self.colormask = 0x00ff_ffff; self.masks = [0x00ff_ffff; 2]; self.logic_op = 3; self.lt.init(); + self.ltl.init(&mut self.lt); self.blend_src = 1; self.normal = [0.0, 0.0, 1.0]; self.front_ccw = 1; self.polymode = 1; self.scissor = [0, 0, 0x7ff, 0x7ff]; self.win = [0, 0, re3::FB_W as i32, SCREEN_H]; - self.vp = [0.0, re3::FB_W as f32 - 1.0, 0.0, SCREEN_H as f32 - 1.0, 1.0, 0.0]; + self.vp = Viewport { x0: 0.0, x1: re3::FB_W as f32 - 1.0, y0: 0.0, y1: SCREEN_H as f32 - 1.0, zscale: 1.0, zcenter: 0.0 }; } } @@ -497,28 +473,17 @@ impl GlState { if self.mvp_dirty != 0 { self.mvp_dirty = 0; // Column-major (glLoadMatrix order): clip = P * MV * v. - let (a, b) = (&self.proj, &self.mv); - let mut m = [0.0f32; 16]; - for c in 0..4 { - for r in 0..4 { - m[c * 4 + r] = (0..4).map(|k| a[k * 4 + r] * b[c * 4 + k]).sum(); - } - } - self.mvp = m; + self.mvp = math::mul(&self.proj, &self.mv); } } /// Object coordinates -> window vertex (screen pixels, GL y up). fn transform(&mut self, v: [f32; 4]) -> Wv { self.update_mvp(); - let m = &self.mvp; - let clip = |r: usize| m[r] * v[0] + m[4 + r] * v[1] + m[8 + r] * v[2] + m[12 + r] * v[3]; - let h = [clip(0), clip(1), clip(2), clip(3)]; - let m = &self.mv; - let eye = |r: usize| m[r] * v[0] + m[4 + r] * v[1] + m[8 + r] * v[2] + m[12 + r] * v[3]; - let e = [eye(0), eye(1), eye(2), eye(3)]; + let h = math::xform(&self.mvp, v); + let e = math::xform(&self.mv, v); let mut out = Wv { c: self.color, cb: self.color, h, e, ..Default::default() }; - out.oc = self.outcode(&out); + out.oc = self.clip.outcode(&out); self.project(out) } @@ -535,52 +500,11 @@ impl GlState { 0 } - /// Signed distance of `v` to clip plane `i` (>= 0 inside). - fn clip_dist(&self, v: &Wv, i: usize) -> f32 { - let h = &v.h; - match i { - 0 => h[3] + h[0], - 1 => h[3] - h[0], - 2 => h[3] + h[1], - 3 => h[3] - h[1], - 4 => h[3] + h[2], - 5 => h[3] - h[2], - _ => { - let p = &self.uclip[i - 6]; - p[0] * v.e[0] + p[1] * v.e[1] + p[2] * v.e[2] + p[3] * v.e[3] - } - } - } - - /// Planes in use: the view volume plus the enabled user planes. - fn clip_mask(&self) -> u32 { - 0x3f | ((self.uclip_on & 0x3f) << 6) - } - - /// Bit i set when `v` is outside plane i. - fn outcode(&self, v: &Wv) -> u32 { - let mask = self.clip_mask(); - (0..CLIP_PLANES).filter(|&i| mask & (1 << i) != 0 && self.clip_dist(v, i) < 0.0) - .fold(0, |o, i| o | (1 << i)) - } - - /// Window position of `v` from its clip position (`ok` = 0 if w <= 0). - fn project(&self, mut out: Wv) -> Wv { - let [cx, cy, cz, cw] = out.h; - out.ok = 0; - if cw <= 1e-6 { - return out; - } - let (nx, ny, nz) = (cx / cw, cy / cw, cz / cw); - let [vx0, vx1, vy0, vy1, zs, zc] = self.vp; - // Snap to 1/16 pixel, like the hardware's fixed-point subpixel - // coordinates (removes float noise such as 319.99998 on edges). - let snap = |v: f32| (v * 16.0).round() / 16.0; - out.x = snap(self.win[0] as f32 + vx0 + (nx + 1.0) * 0.5 * (vx1 - vx0 + 1.0)); - out.y = snap(self.win[1] as f32 + vy0 + (ny + 1.0) * 0.5 * (vy1 - vy0 + 1.0)); - out.z = zc + nz * zs; - out.ok = 1; - out + /// Window position of `v` from its clip position (`ok` = 0 if w <= 0), + /// snapped to 1/16 pixel like the hardware's fixed-point subpixel + /// coordinates. + fn project(&self, v: Wv) -> Wv { + self.vp.project(v, [self.win[0] as f32, self.win[1] as f32], 16.0) } /// Window clipping by the RE3 WID test: more visible pieces than the 4 @@ -741,7 +665,7 @@ fn port_words(index: u32) -> Option { T_NORMAL | T_IRIS_NORMAL => Some(3), T_COLOR_WRITEMASK | T_READ_BUFFER => Some(2), T_BLEND_FACTOR => Some(3), - t => light::Lighting::port_words(t), + t => light::port_words(t), } } @@ -1000,7 +924,7 @@ impl Hq2Engine { T_DEPTH_CLEAR => 3, T_IRIS_SETCPOS | T_PIXEL_ZOOM => 3, T_READ_RECT => 7, - T_GET_ORIGIN | T_PIX_START | T_PIX_END => 1, + T_GET_ORIGIN | T_IRIS_GET_COLOR | T_PIX_START | T_PIX_END => 1, T_PIX_RECT => 5, T_RASTER_POS => 4, T_BITMAP => (BITMAP_HDR + 9) as u32, @@ -1159,9 +1083,7 @@ impl Hq2Engine { let lit = g.lt.on != 0 && g.iris_unlit[0] == 0; if lit || g.lt.fog_on != 0 { // Eye-space position for lighting and fog. - let m = &g.mv; - let e = |r: usize| m[r] * v[0] + m[4 + r] * v[1] + m[8 + r] * v[2] + m[12 + r] * v[3]; - let eye = [e(0), e(1), e(2), e(3)]; + let eye = math::xform(&g.mv, v); if lit { let (fc, bc) = g.lt.light_vertex(eye, g.normal, g.color); wv.c = fc; @@ -1236,7 +1158,7 @@ impl Hq2Engine { } T_TEXTURE_MATRIX => {} t => { - g.lt.port(t, &b[..n as usize]); + g.ltl.port(&mut g.lt, t, &b[..n as usize]); // Turning colour material on takes the current colour. if t == light::T_COLOR_MATERIAL { let c = g.color; @@ -1277,7 +1199,9 @@ impl Hq2Engine { ((w0 >> 11) & 0x7ff) as i32 + 1, ((w1 >> 10) & 0x3ff) as i32 + 1]; } } - T_VIEWPORT => g.vp = [f(a[1]), f(a[2]), f(a[3]), f(a[4]), f(a[5]), f(a[6])], + T_VIEWPORT => { + g.vp = Viewport { x0: f(a[1]), x1: f(a[2]), y0: f(a[3]), y1: f(a[4]), zscale: f(a[5]), zcenter: f(a[6]) }; + } T_SCISSOR => g.scissor = [a[0] as i32, a[1] as i32, a[2] as i32, a[3] as i32], T_SHADE_MODEL => g.smooth = a[0] & 1, T_FRONT_FACE => g.front_ccw = a[0] & 1, @@ -1312,7 +1236,7 @@ impl Hq2Engine { T_IRIS_ENDPOLYGON | T_IRIS_PCLOS => {} light::T_LIGHTING | light::T_TWO_SIDED | light::T_NORMALIZE | light::T_NORMALIZE_B | light::T_FOG_ON | light::T_MATERIAL_COMMIT => { - g.lt.command(cmd, &a[..]); + light::command(&mut g.lt, cmd, &a[..]); } T_LOGIC_OP => g.logic_op = a[0] & 0xf, T_READ_DONE | T_READ_SETUP_A | T_READ_SETUP_B | T_READ_MODE => {} @@ -1331,10 +1255,10 @@ impl Hq2Engine { T_IRIS_MOVE => g.pen = 0, T_CLIP_PLANE_ENABLE => if a[1] < 6 { let bit = 1 << a[1]; - g.uclip_on = if a[0] & 1 != 0 { g.uclip_on | bit } else { g.uclip_on & !bit }; + g.clip.user_on = if a[0] & 1 != 0 { g.clip.user_on | bit } else { g.clip.user_on & !bit }; }, T_CLIP_PLANE => if a[0] < 6 { - g.uclip[a[0] as usize] = [f(a[1]), f(a[2]), f(a[3]), f(a[4])]; + g.clip.user[a[0] as usize] = [f(a[1]), f(a[2]), f(a[3]), f(a[4])]; }, T_IRIS_SWAPTMESH => { g.pv.swap(0, 1); @@ -1372,6 +1296,11 @@ impl Hq2Engine { out.shram(READBACK_SHRAM, g.win[0] as u32); out.shram(READBACK_SHRAM + 1, g.win[1] as u32); } + T_IRIS_GET_COLOR => { + for (k, w) in to_fixed(g.color, g.cmax()).into_iter().enumerate() { + out.shram(READBACK_SHRAM + k, w); + } + } T_PIX_START | T_PIX_END => self.pix_n = 0, T_PIX_DATA => { let n = (self.last_nargs as usize).min(a.len()); @@ -1503,7 +1432,7 @@ impl Hq2Engine { let g = &self.gl; Some(match cmd { T_WINDOW => format!("GL_WINDOW origin ({}, {}) {}x{}", g.win[0], g.win[1], g.win[2], g.win[3]), - T_VIEWPORT => format!("GL_VIEWPORT x {}..{} y {}..{} zscale {} zcenter {}", g.vp[0], g.vp[1], g.vp[2], g.vp[3], g.vp[4], g.vp[5]), + T_VIEWPORT => format!("GL_VIEWPORT x {}..{} y {}..{} zscale {} zcenter {}", g.vp.x0, g.vp.x1, g.vp.y0, g.vp.y1, g.vp.zscale, g.vp.zcenter), T_SCISSOR => format!("GL_SCISSOR ({}, {})-({}, {})", g.scissor[0], g.scissor[1], g.scissor[2], g.scissor[3]), T_SHADE_MODEL => format!("GL_SHADE_MODEL {}", if g.smooth != 0 { "smooth" } else { "flat" }), T_CLEAR_CI => format!("GL_CLEAR index {} rect {:?}", f(a[0]), g.clip_rect()), @@ -1521,6 +1450,10 @@ impl Hq2Engine { a[0] as i32 - FRAGMENT_BIAS, a[1] as i32 - FRAGMENT_BIAS, a[2], f(a[3]), f(a[4]), f(a[5]), f(a[6])), T_IRIS_MOVE => "IRISGL_MOVE".to_string(), T_GET_ORIGIN => format!("GL_GET_ORIGIN -> ({}, {})", g.win[0], g.win[1]), + T_IRIS_GET_COLOR => { + let c = to_fixed(g.color, g.cmax()); + format!("IRISGL_GET_COLOR -> ({}, {}, {})", c[0] >> 11, c[1] >> 11, c[2] >> 11) + } T_PIX_START => "GL_PIXELS_START".to_string(), T_PIX_END => "GL_PIXELS_END".to_string(), T_PIX_DATA => format!("GL_PIXEL_DATA {} words (chunk {})", self.last_nargs, self.pix_n), @@ -1950,7 +1883,7 @@ impl Hq2Engine { if (oa | ob) & (1 << plane) == 0 { continue; } - let (da, db) = (self.gl.clip_dist(&a, plane), self.gl.clip_dist(&b, plane)); + let (da, db) = (self.gl.clip.dist(&a, plane), self.gl.clip.dist(&b, plane)); let t = da / (da - db); if da < 0.0 { t0 = t0.max(t) } else { t1 = t1.min(t) } } @@ -2152,7 +2085,7 @@ impl Hq2Engine { for i in 0..cn { let (ia, ib) = (cur[i], cur[(i + 1) % cn]); let (a, b) = (vtx(&scratch, ia), vtx(&scratch, ib)); - let (da, db) = (g.clip_dist(&a, plane), g.clip_dist(&b, plane)); + let (da, db) = (g.clip.dist(&a, plane), g.clip.dist(&b, plane)); let e = redges >> i & 1; if da >= 0.0 && m < MAXP { // The edge from an inside vertex is (part of) edge i. diff --git a/src/dev/gr2/gl_light.rs b/src/dev/gr2/gl_light.rs index c0bd69e2..dab4080f 100644 --- a/src/dev/gr2/gl_light.rs +++ b/src/dev/gr2/gl_light.rs @@ -28,11 +28,15 @@ //! specular (LCOLOR) instead of 0x101/0x102; //! - fog: 0x027 enable, 0x028 = mode (0 linear, 1 EXP, 2 EXP2), a, b, RGB. //! -//! The per-vertex maths is the OpenGL 1.1 lighting equation evaluated with -//! those parameters; specular and spot factors use the uploaded tables the -//! way the GE does (the exponents are only known through them). +//! This file is the loader: it decodes those ports into the shared GL +//! core's `Lighting` (`crate::dev::gl::light`), which evaluates the OpenGL +//! 1.1 lighting equation per vertex. On GR2 the specular and spot factors +//! always come from the uploaded tables (the exponents are only known +//! through them), and the scene ambient is folded the way +//! __glExpLoadAmbientSum decides (`refold`). use super::f; +use crate::dev::gl::light::{norm, Light, Lighting, MAX_LIGHTS, SPEC_N, SPOT_N}; pub const T_LIGHT_PARAM: u32 = 0x080; pub const T_MODEL_PARAM: u32 = 0x085; @@ -60,140 +64,94 @@ pub const T_FOG: u32 = 0x028; /// Scale libglcore applies to the EXP fog density (__glExpPassFog). const FOG_EXP_SCALE: f32 = 0.180_464_26; -pub const MAX_LIGHTS: usize = 8; -const SPEC_N: usize = 128; -const SPOT_N: usize = 32; +/// Where the GE's lighting loads are going: the selected light (0x080 5), +/// the table being uploaded and how far (0x080 7), and the light owning the +/// spot table parameters (0x080 31). Plain data, valid zeroed after `init`. #[derive(Clone, Copy)] #[repr(C)] -pub struct Material { - pub emission: [f32; 3], - pub ambient: [f32; 3], - pub diffuse: [f32; 4], - pub specular: [f32; 3], - pub spec_scale: f32, - pub spec_start: f32, - pub spec: [f32; SPEC_N], -} - -#[derive(Clone, Copy)] -#[repr(C)] -pub struct Light { - pub ambient: [f32; 3], - pub diffuse: [f32; 3], - pub specular: [f32; 3], - pub pos: [f32; 4], - pub spot_dir: [f32; 3], - /// linear, quadratic, constant - pub atten: [f32; 3], - pub spot_on: u32, - pub spot_scale: f32, - pub spot_start: f32, - pub spot: [f32; SPOT_N], - pub next: i32, -} - -/// Lighting and fog state. Plain data, valid when zeroed (see `init`). -#[derive(Clone, Copy)] -#[repr(C)] -pub struct Lighting { - pub on: u32, - pub two_sided: u32, - pub local_viewer: u32, - pub amb_sum: [f32; 3], - pub mat: [Material; 2], - pub lights: [Light; MAX_LIGHTS], - pub head: i32, - pub count: u32, +pub struct Loader { cur: i32, table_slot: i32, table_pos: u32, - /// Light that owns the spot table parameters being loaded. spot_light: i32, - pub cmat_param: u32, - pub cmat_face: u32, - pub cmat_on: u32, - pub normal_matrix: [f32; 9], - pub normalize: u32, - pub fog_on: u32, - pub fog: [f32; 6], } -fn dot(a: [f32; 3], b: [f32; 3]) -> f32 { - a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +fn light_mut(lt: &mut Lighting, i: i32) -> Option<&mut Light> { + if (0..MAX_LIGHTS as i32).contains(&i) { Some(&mut lt.lights[i as usize]) } else { None } } -fn norm(a: [f32; 3]) -> [f32; 3] { - let l = dot(a, a).sqrt(); - if l > 1e-20 { [a[0] / l, a[1] / l, a[2] / l] } else { a } +/// libglcore folds the lights' ambient into 0x075 unless there are local +/// lights, a local viewer or two-sided lighting (__glExpLoadAmbientSum). +/// Kept current after every load, since any of those can change. +fn refold(lt: &mut Lighting) { + let per_light = lt.local_viewer != 0 || lt.two_sided != 0 || lt.enabled().any(|l| l.pos[3] != 0.0); + lt.ambient_folded = (!per_light) as u32; } -/// Table lookup as the GE does it: x mapped to (x - start) * scale, -/// linearly interpolated, 0 below the table. -fn table(t: &[f32], start: f32, scale: f32, x: f32) -> f32 { - let p = (x - start) * scale; - if !p.is_finite() || p < 0.0 { - return 0.0; - } - let last = (t.len() - 1) as f32; - if p >= last { - return t[t.len() - 1]; +/// Words a lighting/fog port takes (None: not a lighting port). +pub fn port_words(tok: u32) -> Option { + Some(match tok { + T_LIGHT_PARAM | T_MODEL_PARAM => 2, + T_AMBIENT_SUM | T_LIGHT_AMBIENT | T_LIGHT_DIFFUSE | T_LIGHT_SPECULAR | T_SPOT_DIRECTION + | T_ATTENUATION | T_COLOR_MATERIAL | T_IRIS_LCOLOR => 3, + 0x07a | 0x07b | T_LIGHT_POSITION => 4, + T_MAT_FIRST..=T_MAT_LAST => 3, + T_FOG => 6, + T_SPEC_TABLE | T_SPOT_TABLE => 1, + _ => return None, + }) +} + +/// Single-word lighting commands. Returns false if not one. +pub fn command(lt: &mut Lighting, tok: u32, a: &[u32]) -> bool { + match tok { + T_LIGHTING => lt.on = a[1] & 1, + T_TWO_SIDED => lt.two_sided = a[1] & 1, + T_NORMALIZE | T_NORMALIZE_B => lt.normalize = a[0] & 1, + T_FOG_ON => lt.fog_on = a[0] & 1, + T_MATERIAL_COMMIT => {} + _ => return false, } - let i = p.floor() as usize; - let fr = p - i as f32; - t[i] + (t[i + 1] - t[i]) * fr + refold(lt); + true } -impl Lighting { - pub fn init(&mut self) { - self.head = -1; +impl Loader { + /// `lt` freshly `Lighting::init`ed: the GE's starting state. Unlike + /// OpenGL's initial state the scene ambient and light 0's colours start + /// at 0 (libglcore loads them; kept as the HLE has always had them). + pub fn init(&mut self, lt: &mut Lighting) { self.cur = -1; + self.table_slot = 0; + self.table_pos = 0; self.spot_light = -1; - for l in self.lights.iter_mut() { - l.next = -1; - l.atten = [0.0, 0.0, 1.0]; - l.pos = [0.0, 0.0, 1.0, 0.0]; + lt.amb_sum = [0.0; 3]; + lt.lights[0].diffuse = [0.0; 3]; + lt.lights[0].specular = [0.0; 3]; + for m in lt.mat.iter_mut() { + m.spec_table = 1; } - self.normal_matrix = [1., 0., 0., 0., 1., 0., 0., 0., 1.]; - for m in self.mat.iter_mut() { - m.ambient = [0.2; 3]; - m.diffuse = [0.8, 0.8, 0.8, 1.0]; - m.spec_scale = 1.0; + for l in lt.lights.iter_mut() { + l.spot_table = 1; } - } - - fn light_mut(&mut self, i: i32) -> Option<&mut Light> { - if (0..MAX_LIGHTS as i32).contains(&i) { Some(&mut self.lights[i as usize]) } else { None } - } - - /// Words a lighting/fog port takes (None: not a lighting port). - pub fn port_words(tok: u32) -> Option { - Some(match tok { - T_LIGHT_PARAM | T_MODEL_PARAM => 2, - T_AMBIENT_SUM | T_LIGHT_AMBIENT | T_LIGHT_DIFFUSE | T_LIGHT_SPECULAR | T_SPOT_DIRECTION - | T_ATTENUATION | T_COLOR_MATERIAL | T_IRIS_LCOLOR => 3, - 0x07a | 0x07b | T_LIGHT_POSITION => 4, - T_MAT_FIRST..=T_MAT_LAST => 3, - T_FOG => 6, - T_SPEC_TABLE | T_SPOT_TABLE => 1, - _ => return None, - }) + refold(lt); } /// A complete port write (`b` holds `port_words(tok)` words). - pub fn port(&mut self, tok: u32, b: &[u32]) { + pub fn port(&mut self, lt: &mut Lighting, tok: u32, b: &[u32]) { let v3 = |b: &[u32]| [f(b[0]), f(b[1]), f(b[2])]; match tok { - T_LIGHT_PARAM => self.param(f(b[0]), f(b[1])), + T_LIGHT_PARAM => self.param(lt, f(b[0]), f(b[1])), T_MODEL_PARAM => { if f(b[0]) as i32 == 6 { - self.local_viewer = (f(b[1]) != 0.0) as u32; + lt.local_viewer = (f(b[1]) != 0.0) as u32; } } - T_AMBIENT_SUM => self.amb_sum = v3(b), + T_AMBIENT_SUM => lt.amb_sum = v3(b), T_MAT_FIRST..=T_MAT_LAST => { let face = ((tok - T_MAT_FIRST) & 1) as usize; - let m = &mut self.mat[face]; + let m = &mut lt.mat[face]; match (tok - T_MAT_FIRST) / 2 { 0 => m.emission = v3(b), 1 => m.ambient = v3(b), @@ -203,8 +161,7 @@ impl Lighting { } T_LIGHT_AMBIENT | T_LIGHT_DIFFUSE | T_LIGHT_SPECULAR | T_LIGHT_POSITION | T_SPOT_DIRECTION | T_ATTENUATION | T_IRIS_LCOLOR => { - let cur = self.cur; - if let Some(l) = self.light_mut(cur) { + if let Some(l) = light_mut(lt, self.cur) { match tok { T_LIGHT_AMBIENT => l.ambient = v3(b), T_LIGHT_DIFFUSE => l.diffuse = v3(b), @@ -220,44 +177,49 @@ impl Lighting { } } T_COLOR_MATERIAL => { - self.cmat_param = b[0]; - self.cmat_face = b[1]; - self.cmat_on = b[2] & 1; + lt.cmat_param = b[0]; + lt.cmat_face = b[1]; + lt.cmat_on = b[2] & 1; } + // Mode (an integer), then linear: end, 1 / (end - start); + // EXP/EXP2: the density as libglcore scales it. T_FOG => { - for (k, w) in b.iter().enumerate().take(6) { - self.fog[k] = f(*w); - } + lt.fog_mode = b[0]; + let a = f(b[1]); + // EXP2: density scale constant not yet confirmed (unverified). + lt.fog_a = if b[0] == 0 { a } else { a / FOG_EXP_SCALE }; + lt.fog_b = f(b[2]); + lt.fog_color = v3(&b[3..]); } T_SPEC_TABLE | T_SPOT_TABLE => { let (slot, pos) = (self.table_slot, self.table_pos as usize); self.table_pos += 1; let v = f(b[0]); if tok == T_SPEC_TABLE && (0..2).contains(&slot) && pos < SPEC_N { - self.mat[slot as usize].spec[pos] = v; + lt.mat[slot as usize].spec[pos] = v; } else if tok == T_SPOT_TABLE && pos < SPOT_N { - if let Some(l) = self.light_mut(slot - 2) { + if let Some(l) = light_mut(lt, slot - 2) { l.spot[pos] = v; } } } _ => {} } + refold(lt); } /// GE parameter write through 0x080. - fn param(&mut self, addr: f32, val: f32) { + fn param(&mut self, lt: &mut Lighting, addr: f32, val: f32) { let (a, vi) = (addr as i32, val as i32); match a { - 2 => self.mat[0].spec_scale = val, - 3 => self.mat[0].spec_start = val, - 24 => self.mat[1].spec_scale = val, - 25 => self.mat[1].spec_start = val, - 4 => self.head = vi, + 2 => lt.mat[0].spec_scale = val, + 3 => lt.mat[0].spec_start = val, + 24 => lt.mat[1].spec_scale = val, + 25 => lt.mat[1].spec_start = val, + 4 => lt.head = vi, 5 => self.cur = vi, 6 => { - let cur = self.cur; - if let Some(l) = self.light_mut(cur) { + if let Some(l) = light_mut(lt, self.cur) { l.next = vi; } } @@ -265,196 +227,25 @@ impl Lighting { self.table_slot = vi; self.table_pos = 0; } - 10 => self.count = vi.max(0) as u32, + 10 => lt.count = vi.max(0) as u32, 31 => { self.spot_light = vi; if vi < 0 { - let cur = self.cur; - if let Some(l) = self.light_mut(cur) { + if let Some(l) = light_mut(lt, self.cur) { l.spot_on = 0; } - } else if let Some(l) = self.light_mut(vi) { + } else if let Some(l) = light_mut(lt, vi) { l.spot_on = 1; } } 29 | 30 => { - let s = self.spot_light; - if let Some(l) = self.light_mut(s) { + if let Some(l) = light_mut(lt, self.spot_light) { if a == 29 { l.spot_scale = val } else { l.spot_start = val } } } _ => {} } } - - /// Single-word lighting commands. Returns false if not one. - pub fn command(&mut self, tok: u32, a: &[u32]) -> bool { - match tok { - T_LIGHTING => self.on = a[1] & 1, - T_TWO_SIDED => self.two_sided = a[1] & 1, - T_NORMALIZE | T_NORMALIZE_B => self.normalize = a[0] & 1, - T_FOG_ON => self.fog_on = a[0] & 1, - T_MATERIAL_COMMIT => {} - _ => return false, - } - true - } - - /// Enabled lights in the GE's list order. - fn enabled(&self) -> impl Iterator { - let mut i = self.head; - let mut left = self.count.min(MAX_LIGHTS as u32); - std::iter::from_fn(move || { - if left == 0 || !(0..MAX_LIGHTS as i32).contains(&i) { - return None; - } - left -= 1; - let l = &self.lights[i as usize]; - i = l.next; - Some(l) - }) - } - - /// libglcore folds the lights' ambient into 0x075 unless there are local - /// lights, a local viewer or two-sided lighting (__glExpLoadAmbientSum). - fn per_light_ambient(&self) -> bool { - self.local_viewer != 0 || self.two_sided != 0 || self.enabled().any(|l| l.pos[3] != 0.0) - } - - /// Material of `face`. Colour material has already been written into - /// it by `track_color`. - fn material(&self, face: usize, _color: [f32; 4]) -> Material { - self.mat[face] - } - - /// Colour material (0x081; IRIS GL lmcolor, OpenGL glColorMaterial): - /// while tracking is on, a colour command WRITES the colour into the - /// tracked properties of the active material, and the change lasts - /// until the material is loaded again (IRIS GL Programming Guide 9.6.4: - /// lmcolor changes "are lost when you use lmbind() to bind another - /// material"; OpenGL likewise updates the material). Substituting the - /// current colour at lighting time instead drew the X-logo background's - /// first frame in the colour sent before its grey material was bound - /// (gold instead of grey). - pub fn track_color(&mut self, color: [f32; 4]) { - if self.cmat_on == 0 { - return; - } - let c3 = [color[0], color[1], color[2]]; - for face in 0..2 { - if self.cmat_face != 2 && self.cmat_face as usize != face { - continue; - } - let m = &mut self.mat[face]; - match self.cmat_param { - 1 => m.emission = c3, - 2 => m.ambient = c3, - 3 => m.diffuse = color, - 4 => m.specular = c3, - 5 => { - m.ambient = c3; - m.diffuse = color; - } - _ => {} - } - } - } - - /// Lit colours (front, back) of a vertex: `eye` = eye-space position, - /// `n_obj` = current normal, `color` = current colour. - pub fn light_vertex(&self, eye: [f32; 4], n_obj: [f32; 3], color: [f32; 4]) -> ([f32; 4], [f32; 4]) { - let nm = &self.normal_matrix; - let mut n = [ - nm[0] * n_obj[0] + nm[3] * n_obj[1] + nm[6] * n_obj[2], - nm[1] * n_obj[0] + nm[4] * n_obj[1] + nm[7] * n_obj[2], - nm[2] * n_obj[0] + nm[5] * n_obj[1] + nm[8] * n_obj[2], - ]; - if self.normalize != 0 { - n = norm(n); - } - let p = if eye[3] != 0.0 { [eye[0] / eye[3], eye[1] / eye[3], eye[2] / eye[3]] } else { [eye[0], eye[1], eye[2]] }; - let front = self.shade(0, n, p, color); - let back = if self.two_sided != 0 { self.shade(1, [-n[0], -n[1], -n[2]], p, color) } else { front }; - (front, back) - } - - fn shade(&self, face: usize, n: [f32; 3], p: [f32; 3], color: [f32; 4]) -> [f32; 4] { - let m = self.material(face, color); - let per_light_amb = self.per_light_ambient(); - let mut c = [0.0f32; 3]; - for k in 0..3 { - c[k] = m.emission[k] + m.ambient[k] * self.amb_sum[k]; - } - let v = if self.local_viewer != 0 { norm([-p[0], -p[1], -p[2]]) } else { [0.0, 0.0, 1.0] }; - for l in self.enabled() { - let (lv, mut att) = if l.pos[3] == 0.0 { - (norm([l.pos[0], l.pos[1], l.pos[2]]), 1.0) - } else { - let d = [l.pos[0] - p[0], l.pos[1] - p[1], l.pos[2] - p[2]]; - let dist = dot(d, d).sqrt(); - let k = l.atten[2] + l.atten[0] * dist + l.atten[1] * dist * dist; - (norm(d), if k > 0.0 { 1.0 / k } else { 1.0 }) - }; - if l.spot_on != 0 { - let cos = -dot(lv, l.spot_dir); - att *= table(&l.spot, l.spot_start, l.spot_scale, cos); - } - if att == 0.0 { - continue; - } - let ndl = dot(n, lv); - for k in 0..3 { - let mut t = 0.0; - if per_light_amb { - t += l.ambient[k] * m.ambient[k]; - } - if ndl > 0.0 { - t += ndl * l.diffuse[k] * m.diffuse[k]; - } - c[k] += att * t; - } - if ndl > 0.0 { - let h = norm([lv[0] + v[0], lv[1] + v[1], lv[2] + v[2]]); - let s = table(&m.spec, m.spec_start, m.spec_scale, dot(n, h)); - for k in 0..3 { - c[k] += att * s * l.specular[k] * m.specular[k]; - } - } - } - [c[0].clamp(0.0, 1.0), c[1].clamp(0.0, 1.0), c[2].clamp(0.0, 1.0), m.diffuse[3].clamp(0.0, 1.0)] - } - - /// Fog factor for eye-space distance `z` (>= 0): 1 = no fog. - fn fog_factor(&self, z: f32) -> f32 { - let fg = &self.fog; - let mode = fg[0].to_bits(); - let f = match mode { - 0 => (fg[1] - z) * fg[2], - 1 => (-(fg[1] / FOG_EXP_SCALE) * z).exp(), - // EXP2: density scale constant not yet confirmed (unverified). - _ => { - let d = fg[1] / FOG_EXP_SCALE; - (-(d * z) * (d * z)).exp() - } - }; - f.clamp(0.0, 1.0) - } - - /// Apply fog to a colour for eye-space position `eye`. - pub fn fog_color(&self, eye: [f32; 4], c: [f32; 4]) -> [f32; 4] { - if self.fog_on == 0 { - return c; - } - let z = if eye[3] != 0.0 { (eye[2] / eye[3]).abs() } else { eye[2].abs() }; - let fct = self.fog_factor(z); - let fc = [self.fog[3], self.fog[4], self.fog[5]]; - [ - fct * c[0] + (1.0 - fct) * fc[0], - fct * c[1] + (1.0 - fct) * fc[1], - fct * c[2] + (1.0 - fct) * fc[2], - c[3], - ] - } } #[cfg(test)] @@ -472,15 +263,17 @@ mod tests { #[test] fn iris_lcolor_sets_diffuse_and_specular() { let mut lt: Lighting = unsafe { std::mem::zeroed() }; + let mut ld: Loader = unsafe { std::mem::zeroed() }; lt.init(); - lt.port(T_LIGHT_PARAM, &[fw(4.0), fw(0.0)]); - lt.port(T_LIGHT_PARAM, &[fw(10.0), fw(1.0)]); - lt.port(T_LIGHT_PARAM, &[fw(5.0), fw(0.0)]); - lt.port(T_IRIS_LCOLOR, &[fw(1.0), fw(1.0), fw(1.0)]); - lt.port(T_LIGHT_POSITION, &[fw(0.0), fw(1.0), fw(0.0), fw(0.0)]); - lt.port(0x07a, &[fw(0.46), fw(0.66), fw(0.795), fw(1.0)]); - lt.port(0x078, &[fw(0.0), fw(0.1), fw(0.2)]); - lt.port(T_AMBIENT_SUM, &[fw(0.4), fw(0.4), fw(0.4)]); + ld.init(&mut lt); + ld.port(&mut lt, T_LIGHT_PARAM, &[fw(4.0), fw(0.0)]); + ld.port(&mut lt, T_LIGHT_PARAM, &[fw(10.0), fw(1.0)]); + ld.port(&mut lt, T_LIGHT_PARAM, &[fw(5.0), fw(0.0)]); + ld.port(&mut lt, T_IRIS_LCOLOR, &[fw(1.0), fw(1.0), fw(1.0)]); + ld.port(&mut lt, T_LIGHT_POSITION, &[fw(0.0), fw(1.0), fw(0.0), fw(0.0)]); + ld.port(&mut lt, 0x07a, &[fw(0.46), fw(0.66), fw(0.795), fw(1.0)]); + ld.port(&mut lt, 0x078, &[fw(0.0), fw(0.1), fw(0.2)]); + ld.port(&mut lt, T_AMBIENT_SUM, &[fw(0.4), fw(0.4), fw(0.4)]); assert_eq!(lt.lights[0].specular, [1.0; 3]); let (c, _) = lt.light_vertex([0.0, 0.0, -5.0, 1.0], [0.0, 1.0, 0.0], [1.0; 4]); let want = [0.46, 0.66 + 0.04, 0.795 + 0.08]; diff --git a/src/dev/gr2/hq2_tests.rs b/src/dev/gr2/hq2_tests.rs index 7ad51482..76e44ddd 100644 --- a/src/dev/gr2/hq2_tests.rs +++ b/src/dev/gr2/hq2_tests.rs @@ -2664,6 +2664,22 @@ fn iris_getcpos_status_and_setcpos() { assert_eq!((sh(0), sh(1)), (164, 662)); } +/// IRIS GL gRGBcolor / getcolor (solid.log; blast per frame): 0x0CF, Finish, +/// then libgl reads shram 0x4022..0x4024 and takes (w >> 11) & 0xff of each. +/// Answering nothing returned black. +#[test] +fn iris_get_color_reads_back_the_current_colour() { + let g = live_gr2(Gr2Variant::Xz); + gl_setup_window(g); + let fl = |v: f32| v.to_bits(); + // IRIS GL colour, three floats (conversion 3), all on the port's index. + for v in [1.0f32, 0.5, 0.0] { cmd(g, 0x1913, fl(v)); } + cmd(g, 0x0cf, 0); + g.wait_idle(); + let sh = |i: u32| (r32(g, (0x4022 + i) * 4) >> 11) & 0xff; + assert_eq!((sh(0), sh(1), sh(2)), (255, 128, 0)); +} + /// IRIS GL writepixels row (mandel2.log): 0x0B1; 0x071 blocks of 16 words, /// one colour index per word; 0x0B2 x; DATA y, width, width, flag; 0x0B3. /// The chunk lands at window (x, y); flag set = right to left. diff --git a/src/dev/mgras/gl.rs b/src/dev/mgras/gl.rs index def62864..ecd0d21e 100644 --- a/src/dev/mgras/gl.rs +++ b/src/dev/mgras/gl.rs @@ -45,7 +45,11 @@ use super::hq3::Hq3Sink; use super::te1::{self, reg as te_reg}; use crate::dev::gl::light::{Lighting, MAX_LIGHTS}; use crate::dev::gl::math::{self, Mat4, Stack}; -use crate::dev::gl::vertex::{Clip, Viewport, Wv, MAX_POLY}; +use crate::dev::gl::vertex::{self, Clip, Viewport, MAX_POLY}; + +/// IMPACT vertices: texture coordinates (s, t, r, q) and a fog factor, for +/// the TE and per-fragment fog. +type Wv = vertex::Wv<4, 1>; /// GL tokens the GE handles (numbers as in `hq3::token_name`). mod tok { @@ -1176,7 +1180,7 @@ impl Gl { cb = self.lt.fog_color(e, cb); } } - let mut w = Wv { c, cb, h, e, f, ..Default::default() }; + let mut w = Wv { c, cb, h, e, f: [f], ..Default::default() }; if self.tex_on != 0 { let tc = self.texgen(v, e); w.t = math::xform(self.tex.get(), tc); @@ -1532,7 +1536,7 @@ impl Gl { put64(sink, dr, (qb[k] - qa[k]) / len as f64 * te1::ITER_ONE); } } - self.fog_plane(a.f as f64, (b.f as f64 - a.f as f64) / len as f64, 0.0, sink); + self.fog_plane(a.f[0] as f64, (b.f[0] as f64 - a.f[0] as f64) / len as f64, 0.0, sink); sink.rss_write(re::IR, IR_GL_LINE, true); } @@ -2142,7 +2146,7 @@ impl Gl { } } if self.fog_after_texture() { - let (a, b, c) = (aw.f as f64, bw.f as f64, cw.f as f64); + let (a, b, c) = (aw.f[0] as f64, bw.f[0] as f64, cw.f[0] as f64); let mid = c + perc as f64 * (a - c); let dx = if span_x != 0.0 { (b - mid) / span_x as f64 } else { 0.0 }; let ddown = (a - c - dx * major_x as f64) / major_y as f64; @@ -2438,7 +2442,7 @@ fn lerp_window(a: &Wv, b: &Wv, t: f32) -> Wv { v.cb[k] = l(a.cb[k], b.cb[k]); v.t[k] = l(a.t[k], b.t[k]); } - v.f = l(a.f, b.f); + v.f[0] = l(a.f[0], b.f[0]); v } From b4c0cafb285c0a1dd1eb474e7b8999473d0d7327 Mon Sep 17 00:00:00 2001 From: technomancer Date: Thu, 8 Oct 2026 21:17:17 -0700 Subject: [PATCH 2/2] fixed warnings --- src/dev/gr2/re3_tests.rs | 14 +++++++------- src/dev/mc_vdma.rs | 2 +- src/dev/mgras/mgras_tests.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/dev/gr2/re3_tests.rs b/src/dev/gr2/re3_tests.rs index ee0aaf5b..06512b88 100644 --- a/src/dev/gr2/re3_tests.rs +++ b/src/dev/gr2/re3_tests.rs @@ -25,7 +25,7 @@ fn setup(r: &mut Re3) { fn flat(r: &mut Re3, x: u32, y: u32, n: u32, ci: u32) { r.write_reg(REG_R, ci << 11); - r.write_reg(REG_X, (x)); + r.write_reg(REG_X, x); r.write_reg(REG_Y, y); r.write_reg(REG_NUMPIX, n); r.write_reg(REG_IR, IR_FLAT); @@ -51,8 +51,8 @@ fn flat_span_draws_numpix_pixels() { fn scissor_clips_span() { let mut r = bare_re3(); setup(&mut r); - r.write_reg(REG_XMIN, (10)); - r.write_reg(REG_XMAX, (12)); + r.write_reg(REG_XMIN, 10); + r.write_reg(REG_XMAX, 12); flat(&mut r, 5, 0, 20, 1); let drawn: Vec = (0..30).filter(|&x| px(&r, x, 0) != 0).collect(); assert_eq!(drawn, vec![10, 11, 12]); @@ -117,7 +117,7 @@ fn wid_test_rejects_other_windows() { fn shaded_span_steps_colour() { let mut r = bare_re3(); setup(&mut r); - r.write_reg(REG_X, (0)); + r.write_reg(REG_X, 0); r.write_reg(REG_Y, 0); r.write_reg(REG_DX, 1 << 14); r.write_reg(REG_R, 10 << 11); @@ -149,7 +149,7 @@ fn copy_rect_handles_overlap_both_ways() { fn writebuf_then_readbuf_stream() { let mut r = bare_re3(); setup(&mut r); - r.write_reg(REG_X, (4)); + r.write_reg(REG_X, 4); r.write_reg(REG_Y, 3); r.write_reg(REG_DX, 1 << 14); r.write_reg(REG_NUMPIX, 3); @@ -160,7 +160,7 @@ fn writebuf_then_readbuf_stream() { assert_eq!((px(&r, 4, 3), px(&r, 5, 3), px(&r, 6, 3)), (0x11, 0x22, 0x33)); assert_eq!(r.ctx.stream, STREAM_IDLE); - r.write_reg(REG_X, (4)); + r.write_reg(REG_X, 4); r.write_reg(REG_NUMPIX, 3); r.write_reg(REG_IR, IR_READBUF); let mut got = vec![]; @@ -209,7 +209,7 @@ fn cpu_register_window_draws_and_reads_back() { let g = live_gr2(Gr2Variant::Xz); w32(g, re3_reg(REG_FUNC), ROP_COPY); w32(g, re3_reg(REG_PIXMASK), 0xffffff); - w32(g, re3_reg(REG_XMAX), (1279)); + w32(g, re3_reg(REG_XMAX), 1279); w32(g, re3_reg(REG_YMAX), 1023); w32(g, re3_reg(REG_R), 0x5 << 11); w32(g, re3_reg(REG_YX), (20 << 12) | (30)); diff --git a/src/dev/mc_vdma.rs b/src/dev/mc_vdma.rs index 06b95c70..5f876dd6 100644 --- a/src/dev/mc_vdma.rs +++ b/src/dev/mc_vdma.rs @@ -1910,7 +1910,7 @@ mod line_bulk_tests_support { } } - pub(super) fn mc_with(sink: Arc) -> MemoryController { + fn mc_with(sink: Arc) -> MemoryController { let phys: Arc = sink; let mc = MemoryController::new( Arc::new(PlMutex::new(Eeprom93c56::new())), true, [1, 0, 0, 0]); diff --git a/src/dev/mgras/mgras_tests.rs b/src/dev/mgras/mgras_tests.rs index dc2f63a9..d9a645e7 100644 --- a/src/dev/mgras/mgras_tests.rs +++ b/src/dev/mgras/mgras_tests.rs @@ -136,7 +136,7 @@ fn pio_read_through_the_bus() { direct_rss(&m, XFRMODE, 2 << 8, false); direct_rss(&m, XFRSIZE, 2 << 16 | 3, false); block(&m, 100, 50, 102, 51); - let mut dw = || (read(&m, 32, 0x7D1C0) << 32) | read(&m, 32, 0x7C1C4); + let dw = || (read(&m, 32, 0x7D1C0) << 32) | read(&m, 32, 0x7C1C4); assert_eq!(dw(), 0x0000_0102_0300_0000); assert_eq!(dw(), 0x0000_0000_0004_0506); assert_eq!(dw(), 0);