From 6ed59996c64a729dbbef0466df72748febcdf8ec Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:27:10 -0800 Subject: [PATCH 1/2] REX3: BLENDALPHA only changes how alpha itself is blended MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DRAWMODE1 bit 27 was read as "the source factor BF_SA is 1.0" for all four channels, so with BLENDALPHA=0 a blend never attenuated the source colour. The spec says the bit selects how the *alpha component* is blended (§3.8, and the pin table's "Blend source alpha with alpha"); red, green and blue always use the real source alpha. IRIX's OpenGL on Newport draws GL_SRC_ALPHA/GL_ONE_MINUS_SRC_ALPHA with BLENDALPHA=0, so every blended primitive came out opaque. Fixed in both the interpreter and the REX JIT; tests updated to the corrected meaning, with a direct check of the alpha channel. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 ++ rules/rex3/blendalpha-and-alpha-blending.md | 16 ++++ src/dev/ng1/rex3_generic.rs | 19 ++--- src/dev/ng1/rex3_jit/compiler.rs | 26 +++--- src/dev/ng1/rex3_tests.rs | 87 ++++++++++++--------- 5 files changed, 97 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f3312f0..8eb4a9f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,13 @@ hashes are given where a change is easiest to understand by reading the commit. `nvram` ones, so both chips get a real MAC regardless of machine profile. ### Graphics and host OpenGL +- **2026-10-09 — Newport (REX3) blends again under IRIX OpenGL.** + DRAWMODE1's BLENDALPHA bit was applied to every channel, so with it clear + `BF_SA` was 1.0 for red, green and blue too and nothing was ever + attenuated. It only changes how the alpha component is blended (rex3.pdf + §3.8). IRIX's GL draws `GL_SRC_ALPHA`/`GL_ONE_MINUS_SRC_ALPHA` with it + clear, so every blend came out opaque: half-transparent quads, antialiased + text, and the dark halo around `blast`'s nebula billboard. - **2026-10-06 — IMPACT OpenGL and texture pipeline** (`862d0fe`): GE11 command HLE, shared matrix/clipping/lighting routines in `src/dev/gl/`, triangle rasterization, context storage and eviction through ERAM, and diff --git a/rules/rex3/blendalpha-and-alpha-blending.md b/rules/rex3/blendalpha-and-alpha-blending.md index 2f880af9..f8fa41c9 100644 --- a/rules/rex3/blendalpha-and-alpha-blending.md +++ b/rules/rex3/blendalpha-and-alpha-blending.md @@ -1,5 +1,21 @@ # REX3 BLENDALPHA: what it selects, and why the `blast` billboard exposes it +> **Corrected 2026-10-09.** BLENDALPHA changes the source multiplier of the +> **alpha component only**; red, green and blue always use the real source +> alpha. §3.8 says "*alpha component* can be blended in two different ways", +> and the pin table names the bit "Blend source alpha with alpha". Reading it as +> "BF_SA = 1.0 for every channel" (below) made every IRIX OpenGL blend opaque: +> IRIX's Newport GL sets `GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA` with +> BLENDALPHA=0, so a 50% quad, antialiased text and transparent texels all +> wrote at full strength. With the fix, a GL probe (textured and untextured +> quads, 12-bit double-buffered and 24-bit single-buffered) blends exactly as +> on the XZ. It is also what the `blast` billboard below was showing: run +> textured (`blast -T -p` from its `data` directory, as `blast_audio` does), +> the nebula's dark surround drew as a brown/pink haze across the whole quad +> before the fix and is gone after it. The sections below that call the haze +> "not a REX3 bug" predate this. The white triangle and smeared rows along the +> quad's upper-left edge are still there with the fix: a separate problem. + ## The bit `DRAWMODE1` bit 27, `BLENDALPHA`. Register table (spec Table 11): diff --git a/src/dev/ng1/rex3_generic.rs b/src/dev/ng1/rex3_generic.rs index a2f4ab5a..12216cc8 100644 --- a/src/dev/ng1/rex3_generic.rs +++ b/src/dev/ng1/rex3_generic.rs @@ -777,16 +777,17 @@ pub fn blend(m: &M, src: u32, dst: u32) -> u32 { let s_factor_sel = m.sfactor(); let d_factor_sel = m.dfactor(); - // BLENDALPHA (DRAWMODE1 bit 27) substitutes the SOURCE multiplier only. - // Spec §3.8: "When source multiplier is set to source alpha (SFACTOR=4) ... + // BLENDALPHA (DRAWMODE1 bit 27) changes how the ALPHA component is blended, + // nothing else. Spec §3.8: "When source multiplier is set to source alpha + // (SFACTOR=4), alpha component can be blended in two different ways ... // When BLENDALPHA is set to 0, the source multiplier for blending alpha is - // one instead of source alpha AND DESTINATION MULTIPLIER IS DEFINED BY - // DFACTOR." The trailing clause is load-bearing: DFACTOR keeps its own - // definition, so a DFACTOR of BF_MSA still evaluates 1 - source alpha - // against the real alpha. Substituting in both factors would zero BF_MSA - // and discard the destination entirely, which the spec does not say. + // one instead of source alpha and destination multiplier is defined by + // DFACTOR." The pin table calls the bit "Blend source alpha with alpha". + // So red, green and blue always use the real source alpha; only the alpha + // channel's source multiplier becomes 1.0. IRIX's OpenGL relies on this: + // it draws GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA with BLENDALPHA=0. let sa_real = (src >> 24) & 0xFF; - let sa_src = if m.blendalpha() != 0 { sa_real } else { 255 }; + let sa_alpha = if m.blendalpha() != 0 { sa_real } else { 255 }; // `c` is the *other* operand's channel — destination when computing the // source factor, source when computing the destination factor — which is @@ -811,7 +812,7 @@ pub fn blend(m: &M, src: u32, dst: u32) -> u32 { let s_c = (src >> shift) & 0xFF; let d_c = (dst >> shift) & 0xFF; - let sf = get_factor(s_factor_sel, d_c, sa_src); + let sf = get_factor(s_factor_sel, d_c, if i == 3 { sa_alpha } else { sa_real }); let df = get_factor(d_factor_sel, s_c, sa_real); let val = (s_c * sf + d_c * df) / 255; diff --git a/src/dev/ng1/rex3_jit/compiler.rs b/src/dev/ng1/rex3_jit/compiler.rs index f26f49f1..f53af4c0 100644 --- a/src/dev/ng1/rex3_jit/compiler.rs +++ b/src/dev/ng1/rex3_jit/compiler.rs @@ -2856,11 +2856,11 @@ fn emit_expand_ir(b: &mut FunctionBuilder, val: Value, drawdepth: u32) -> Value /// Mirrors Rex3::blend but specialized — constant factors let Cranelift fold all /// the factor-selection branches away. /// -/// BLENDALPHA (DRAWMODE1 bit 27) selects what BF_SA resolves to for the SOURCE -/// multiplier only: '1' = the real source alpha, '0' = 1.0 (spec Table 11). -/// §3.8 adds "...and destination multiplier is defined by DFACTOR", so DFACTOR -/// keeps its own definition and still evaluates against the real source alpha. -/// Substituting into both factors would zero BF_MSA and discard the destination. +/// BLENDALPHA (DRAWMODE1 bit 27) selects what BF_SA resolves to in the source +/// multiplier of the ALPHA channel only: '1' = the real source alpha, '0' = 1.0 +/// (spec Table 11, §3.8 "alpha component can be blended in two different +/// ways"). Red, green and blue always use the real source alpha, and DFACTOR +/// is unaffected. fn emit_blend_ir( b: &mut FunctionBuilder, src: Value, @@ -2871,8 +2871,8 @@ fn emit_blend_ir( ) -> Value { let sa = b.ins().ushr_imm_s(src, 24); // alpha from src bits[31:24] let c255 = b.ins().iconst(types::I32, 255); - // Source-side alpha: real alpha when BLENDALPHA=1, otherwise 1.0 (255). - let sa_src = if blendalpha { sa } else { c255 }; + // The alpha channel's source alpha: real when BLENDALPHA=1, otherwise 1.0. + let sa_alpha = if blendalpha { sa } else { c255 }; // Extract each 8-bit channel (no nesting) let sr = b.ins().band_imm_s(src, 0xFF); @@ -2904,8 +2904,8 @@ fn emit_blend_ir( // Blend one channel: (sc*sf + dc*df)/255, clamped to 255, shifted macro_rules! blend_ch { - ($sc:expr, $dc:expr, $shift:literal) => {{ - let sf = get_factor_ir(b, sfactor, $dc, sa_src, c255); + ($sc:expr, $dc:expr, $sa:expr, $shift:literal) => {{ + let sf = get_factor_ir(b, sfactor, $dc, $sa, c255); let df = get_factor_ir(b, dfactor, $sc, sa, c255); let sc_sf = b.ins().imul($sc, sf); let dc_df = b.ins().imul($dc, df); @@ -2916,10 +2916,10 @@ fn emit_blend_ir( }} } - let r_out = blend_ch!(sr, dr, 0); - let g_out = blend_ch!(sg, dg, 8); - let b_out = blend_ch!(sb, db, 16); - let a_out = blend_ch!(sa, da, 24); + let r_out = blend_ch!(sr, dr, sa, 0); + let g_out = blend_ch!(sg, dg, sa, 8); + let b_out = blend_ch!(sb, db, sa, 16); + let a_out = blend_ch!(sa, da, sa_alpha, 24); let t1 = b.ins().bor(r_out, g_out); let t2 = b.ins().bor(t1, b_out); diff --git a/src/dev/ng1/rex3_tests.rs b/src/dev/ng1/rex3_tests.rs index 3d9883a6..bd81b8f9 100644 --- a/src/dev/ng1/rex3_tests.rs +++ b/src/dev/ng1/rex3_tests.rs @@ -4692,10 +4692,10 @@ mod jit_tests { } /// BLENDALPHA (DRAWMODE1 bit 27) must be honoured identically by the JIT and - /// the interpreter: it selects what BF_SA resolves to for the SOURCE - /// multiplier only ('1' = real source alpha, '0' = 1.0), while DFACTOR keeps - /// its own definition against the real alpha. Runs both polarities over a - /// spread of alphas against a lit destination, where the two differ most. + /// the interpreter: it selects what BF_SA resolves to in the alpha + /// component's source multiplier ('1' = real source alpha, '0' = 1.0), and + /// leaves the colour channels alone. Runs both polarities over a spread of + /// alphas against a lit destination. #[test] fn jit_blend_blendalpha_matches_interp() { for blendalpha in [false, true] { @@ -5121,14 +5121,17 @@ fn test_blend_alpha_test_discards_zero_alpha() { } // ============================================================================ -// BLENDALPHA (DRAWMODE1 bit 27) selects the VALUE of BF_SA +// BLENDALPHA (DRAWMODE1 bit 27) selects the VALUE of BF_SA for the alpha +// component only // // Spec Table 11: "Selects SFACTOR BF_SA source alpha: '1' = source alpha, -// '0' = 1.0", and §3.8 adds the load-bearing qualifier: when BLENDALPHA=0 "the -// source multiplier ... is one instead of source alpha AND DESTINATION -// MULTIPLIER IS DEFINED BY DFACTOR". So the substitution applies to SFACTOR -// only — DFACTOR keeps its own definition and still evaluates against the real -// source alpha. These tests pin that asymmetry. +// '0' = 1.0", qualified by §3.8: "alpha component can be blended in two +// different ways depending on how BLENDALPHA ... is set. When BLENDALPHA is set +// to 0, the source multiplier for blending alpha is one instead of source alpha +// and destination multiplier is defined by DFACTOR." The pin table calls it +// "Blend source alpha with alpha". Red, green and blue always use the real +// source alpha; IRIX's OpenGL draws GL_SRC_ALPHA/GL_ONE_MINUS_SRC_ALPHA with +// BLENDALPHA=0 and expects a normal blend. // ============================================================================ /// Build a 24bpp RGB blend DRAWMODE1 with the given factors and BLENDALPHA. @@ -5165,43 +5168,56 @@ fn test_blendalpha1_sa_msa_attenuates() { "BLENDALPHA=1 + alpha 8 should attenuate src heavily, got {px:#08x}"); } -/// BF_SA/BF_ONE with BLENDALPHA=0 is ADDITIVE (`1*src + 1*dst`), not a no-op. -/// Treating BLENDALPHA=0 as "skip the blend" would wrongly discard dst here. +/// BF_SA/BF_ONE with BLENDALPHA=0 still attenuates the source colour by the +/// real alpha: `alpha*src + dst`. #[test] -fn test_blendalpha0_sa_one_is_additive() { +fn test_blendalpha0_sa_one_attenuates_rgb() { let rex = make_rex3(); rex3init(&rex); // Lay down a destination first, with blending off. let dm1_src = DRAWMODE1_PLANES_RGB | (3 << 3) | (1 << 15) | DRAWMODE1_COMPARE_DISABLE_SH | DRAWMODE1_LOGICOP_SRC_SH; blend_one(&rex, 32, 40, dm1_src, 255, 0x202020); - // Now blend additively over it. + // Now blend over it: (0x10*128 + 0x20*255)/255 = 0x28 per channel. let px = blend_one(&rex, 32, 40, - dm1_blend24(DRAWMODE1_BF_SA, DRAWMODE1_BF_ONE, false), 8, 0x101010); - assert_eq!(px, 0x303030, - "BLENDALPHA=0 + BF_SA/BF_ONE must add src and dst, got {px:#08x}"); + dm1_blend24(DRAWMODE1_BF_SA, DRAWMODE1_BF_ONE, false), 128, 0x101010); + assert_eq!(px, 0x282828, + "BLENDALPHA=0 + BF_SA/BF_ONE must give alpha*src + dst, got {px:#08x}"); } -/// BLENDALPHA=0 substitutes only the SOURCE multiplier: SFACTOR BF_SA becomes -/// 1.0, but DFACTOR BF_MSA still evaluates 1 - real source alpha. So a low-alpha -/// source over a lit destination keeps most of the destination, rather than -/// replacing it (which is what substituting in both factors would do). +/// The colour channels come out the same whichever way BLENDALPHA is set. #[test] -fn test_blendalpha0_substitutes_sfactor_only() { +fn test_blendalpha_leaves_rgb_alone() { let rex = make_rex3(); rex3init(&rex); let dm1_src = DRAWMODE1_PLANES_RGB | (3 << 3) | (1 << 15) | DRAWMODE1_COMPARE_DISABLE_SH | DRAWMODE1_LOGICOP_SRC_SH; - // Destination 0x404040, source 0x101010 at alpha 8. - blend_one(&rex, 50, 50, dm1_src, 255, 0x404040); - let px = blend_one(&rex, 50, 50, - dm1_blend24(DRAWMODE1_BF_SA, DRAWMODE1_BF_MSA, false), 8, 0x101010); - // out = 1.0*src + (1 - 8/255)*dst = 0x10 + ~0x3E = ~0x4E per channel. - let ch = px & 0xFF; - assert!((0x48..=0x52).contains(&ch), - "expected src + (1-alpha)*dst ≈ 0x4E per channel, got {px:#08x}"); - assert_ne!(px, 0x101010, - "destination must still contribute — BLENDALPHA=0 must not zero DFACTOR"); + let mut got = [0u32; 2]; + for (i, blendalpha) in [false, true].iter().enumerate() { + let x = 50 + i as i32; + blend_one(&rex, x, 52, dm1_src, 255, 0x404040); + got[i] = blend_one(&rex, x, 52, + dm1_blend24(DRAWMODE1_BF_SA, DRAWMODE1_BF_MSA, *blendalpha), 8, 0x101010); + } + assert_eq!(got[0], got[1], "BLENDALPHA must not change RGB: {got:08x?}"); +} + +/// The alpha component: with BLENDALPHA=0 its source multiplier is 1.0, with +/// BLENDALPHA=1 it is the source alpha; DFACTOR uses the real alpha in both. +#[test] +fn test_blendalpha_selects_the_alpha_channels_factor() { + use crate::dev::ng1::rex3_generic::{blend, DynMode}; + let src = 0x8010_1010; // alpha 0x80 + let dst = 0x4040_4040; // alpha 0x40 + for (blendalpha, want) in [(0, 0x9F), (1, 0x60)] { + let m = DynMode { sfactor: DRAWMODE1_BF_SA, dfactor: DRAWMODE1_BF_MSA, + blendalpha, ..Default::default() }; + let out = blend(&m, src, dst); + // 0x80*sf + 0x40*(255-0x80), over 255. + assert_eq!(out >> 24, want, "BLENDALPHA={blendalpha}: alpha {out:#010x}"); + // Red: (0x10*0x80 + 0x40*0x7F)/255 = 0x27, both ways. + assert_eq!(out & 0xFF, 0x27, "BLENDALPHA={blendalpha}: red {out:#010x}"); + } } /// With BLENDALPHA=1 both factors use the real alpha, giving a classic blend. @@ -5221,10 +5237,9 @@ fn test_blendalpha1_uses_alpha_in_both_factors() { } /// AFUNCTION compares the REAL source alpha (from DDA or host per ALPHAHOST) — -/// spec §3.3 — and is unaffected by BLENDALPHA, which only substitutes the blend's -/// source multiplier. With BLENDALPHA=0 the blender sees BF_SA=1.0, but the alpha -/// test must still see the true alpha: alpha 0 vs ALPHAREF 0 under COMPARE='!=' -/// must inhibit the write regardless of BLENDALPHA. +/// spec §3.3 — and is unaffected by BLENDALPHA, which only changes how the alpha +/// component is blended: alpha 0 vs ALPHAREF 0 under COMPARE='!=' must inhibit +/// the write regardless of BLENDALPHA. #[test] fn test_afunction_uses_real_alpha_not_blendalpha() { let rex = make_rex3(); From f6dc11c2239c55ba8b82403db7f1483250c1e364 Mon Sep 17 00:00:00 2001 From: atomchild411 <143453386+atomchild411@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:29:28 -0800 Subject: [PATCH 2/2] REX3: host alpha from 8-bit host fields; A_LINE alpha is coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs the BLENDALPHA misreading had hidden, both found by tracing what IRIX sends: - ALPHAHOST without COLORHOST: the host fields are alpha for the DDA colour (spec §3.9). At host depths 4 and 8 the fields are 8 bits wide (§3.10), so the whole leading byte is the alpha. It was unpacked as a 4- or 8-bit colour and lost, so IRIX's smooth points (per-pixel coverage sent as 0xNN000000 words) drew with alpha 0. 12-bit fields are left as they were. - A_LINE takes its alpha from pixel coverage, not COLORALPHA: IRIX's OpenGL draws GL_LINE_SMOOTH with blending on and COLORALPHA left over from the previous primitive. Coverage is not modelled, so A_LINE pixels count as fully covered. Interpreter and REX JIT (the block/span shader for host alpha, the line shader for A_LINE), with tests for both. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +- docs/rex3.md | 2 +- .../rex3/alphahost-fields-and-aline-alpha.md | 37 ++++++ src/dev/ng1/rex3_generic.rs | 18 ++- src/dev/ng1/rex3_jit/compiler.rs | 21 +++- src/dev/ng1/rex3_tests.rs | 113 +++++++++++++++++- 6 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 rules/rex3/alphahost-fields-and-aline-alpha.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb4a9f4..755ffb05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,7 +99,12 @@ hashes are given where a change is easiest to understand by reading the commit. attenuated. It only changes how the alpha component is blended (rex3.pdf §3.8). IRIX's GL draws `GL_SRC_ALPHA`/`GL_ONE_MINUS_SRC_ALPHA` with it clear, so every blend came out opaque: half-transparent quads, antialiased - text, and the dark halo around `blast`'s nebula billboard. + text, and the dark halo around `blast`'s nebula billboard. Two things the + old reading hid are fixed with it: with ALPHAHOST and 8-bit host fields the + field is now the alpha (it was unpacked as a 4-bit colour, so IRIX's smooth + points, `blast`'s stars among them, had alpha 0), and A_LINE pixels take + full coverage as their alpha instead of a stale COLORALPHA (IRIX's + GL_LINE_SMOOTH lines faded to nearly nothing; coverage is not modelled yet). - **2026-10-06 — IMPACT OpenGL and texture pipeline** (`862d0fe`): GE11 command HLE, shared matrix/clipping/lighting routines in `src/dev/gl/`, triangle rasterization, context storage and eviction through ERAM, and diff --git a/docs/rex3.md b/docs/rex3.md index 9d88eab1..52514877 100644 --- a/docs/rex3.md +++ b/docs/rex3.md @@ -304,5 +304,5 @@ The frame buffer memory (controlled by RB2 chips) contains distinct functional p outside that set; `src/dev/ng1/rex3_profile.rs` persists the modes seen to `~/.iris/rex-jit-profile.bin`. - `src/dev/ng1/rex3_tests.rs` — interpreter/JIT equivalence and drawing tests. -- `rules/rex3/` — findings: CIDMATCH is a mask, blend alpha, fastclear and CID, +- `rules/rex3/` — findings: CIDMATCH is a mask, blend alpha, host and line alpha, fastclear and CID, GFIFO batching, HOSTRW batching, LINE+HOST not being JIT-able. diff --git a/rules/rex3/alphahost-fields-and-aline-alpha.md b/rules/rex3/alphahost-fields-and-aline-alpha.md new file mode 100644 index 00000000..228fccb0 --- /dev/null +++ b/rules/rex3/alphahost-fields-and-aline-alpha.md @@ -0,0 +1,37 @@ +# REX3 source alpha: host fields and antialiased lines + +Two places where the source alpha does not come from COLORALPHA. Both were +invisible while BLENDALPHA was misread (see `blendalpha-and-alpha-blending.md`): +with BF_SA forced to 1.0 a wrong alpha only changed the destination term. + +## ALPHAHOST without COLORHOST: the host field is the alpha + +Spec §3.9: "ALPHAHOST=1 with COLORHOST=0 specifies the HOSTRW1,0 alpha fields +are to be used to blend the DDA R,G,B components". §3.10: each value sits in a +field of 8, 16 or 32 bits chosen by HOSTDEPTH, leftmost first. + +IRIX's OpenGL and IRIS GL draw antialiased points this way: one I_LINE GO per +pixel, DRAWMODE0 `0x8a` (ILINE ALPHAHOST), HOSTDEPTH 0 (4-bit, so 8-bit +fields), unpacked, and the CPU-computed coverage in the top byte of a +32-bit HOSTRW0 write (`0xdd000000`, `0x22000000`, ...). The colour comes from +COLORRED/GRN/BLUE. Unpacking that field as a 4-bit colour gives alpha 0. + +For 8-bit fields (host depths 4 and 8) the whole field is the alpha. 32-bit +fields are ABGR, alpha in the top byte, as before. Where an alpha sits in a +16-bit field (host depth 12) is not stated and has not been seen in a trace. + +## A_LINE: alpha is coverage + +IRIX's OpenGL draws GL_LINE_SMOOTH as A_LINE (`0x00440b32`: DOSETUP, SHADE, +SKIPLAST, ENDPTFILTER) with blending SA/MSA and never loads COLORALPHA for it: +the register still holds whatever the previous primitive left. The spec's line +algorithms write each pixel with "alpha represents pixel coverage" (§3.6), the +coverage coming from the AWEIGHT tables. IRIS does not model coverage, so +A_LINE pixels count as fully covered (alpha 255): solid, not antialiased. +`blast` draws its HUD lines as A_LINE with blending off, so it is unaffected. + +## How these were found + +`rex buslog on` in a `developer` build (GFIFO register log), around a small GL +program that draws one primitive of each kind, compared against the same +program on the emulated XZ. diff --git a/src/dev/ng1/rex3_generic.rs b/src/dev/ng1/rex3_generic.rs index 12216cc8..6f8789a5 100644 --- a/src/dev/ng1/rex3_generic.rs +++ b/src/dev/ng1/rex3_generic.rs @@ -1200,7 +1200,17 @@ pub fn fetch_host_pixel(ctx: &mut Rex3Context, m: &M) -> u32 { ctx.hostcnt = host_count(m); } - let pixel = host_unpack(m, ctx.host_shifter); + let mut pixel = host_unpack(m, ctx.host_shifter); + // ALPHAHOST without COLORHOST: the host fields are alpha, blending the DDA + // colour (spec §3.9). Fields are 8 bits wide for host depths 4 and 8 + // (§3.10: "a field of 8, 16, or 32 bits"), so the whole leading byte is + // the alpha; unpacking it as a 4- or 8-bit colour would lose it. IRIX's + // smooth points send their per-pixel coverage this way. + if m.alphahost() != 0 && m.colorhost() == 0 + && matches!(m.hostdepth(), DRAWMODE1_HOSTDEPTH_12 | DRAWMODE1_HOSTDEPTH_8) + { + pixel = (pixel & 0x00FF_FFFF) | ((ctx.host_shifter >> 32) as u32 & 0xFF00_0000); + } ctx.host_shifter <<= host_shift(m); ctx.hostcnt -= 1; pixel @@ -1261,6 +1271,12 @@ fn combine_host_dda(ctx: &Rex3Context, m: &M, host_pixel: u32) -> u32 { // mask down to the plane-depth index and never look at these bits. let a = if m.alphahost() != 0 { (host_pixel >> 24) & 0xFF + } else if m.adrmode() == DRAWMODE0_ADRMODE_A_LINE { + // Antialiased lines take their alpha from the line's pixel coverage + // (§3.6), not the DDA: IRIX's OpenGL draws GL_LINE_SMOOTH this way + // without loading COLORALPHA. Coverage is not modelled, so every pixel + // counts as fully covered. + 0xFF } else { Rex3Context::clamp_color_component(ctx.coloralpha) }; diff --git a/src/dev/ng1/rex3_jit/compiler.rs b/src/dev/ng1/rex3_jit/compiler.rs index f53af4c0..beb0f786 100644 --- a/src/dev/ng1/rex3_jit/compiler.rs +++ b/src/dev/ng1/rex3_jit/compiler.rs @@ -1305,7 +1305,13 @@ fn emit_shader( // independent of RGB/CI plane format — mirrors combine_host_dda in rex3.rs. // Always overlay bits 31:24 so px_afunc/afunc_cmp below see the right value; // CI write paths mask down to the plane-depth index and never see these bits. - let alpha_byte = if dm0.alphahost() { + let alpha_byte = if dm0.alphahost() && !dm0.colorhost() && dm1.hostdepth() <= 1 { + // 8-bit host fields carrying alpha alone: the field's whole + // leading byte (mirrors fetch_host_pixel). + let hi = b.ins().ushr_imm_s(host_shifter_v, 32); + let hi32 = b.ins().ireduce(types::I32, hi); + b.ins().band_imm_s(hi32, 0xFF00_0000u64 as i64) + } else if dm0.alphahost() { b.ins().band_imm_s(host_pixel_v, 0xFF00_0000u64 as i64) } else { // Hoisted to the entry block — constant for the whole draw. @@ -2068,10 +2074,15 @@ fn emit_draw_iline( // Afunction source alpha for lines is always DDA (lines never use host mode — // see compile_shader guard), independent of RGB/CI plane format. Mirrors the - // block/span shader's alpha_byte overlay. - let ca = ld32!(ctx_off!(coloralpha)); - let ca_c = clamp_color_component(&mut b, ca); - let alpha_byte = b.ins().ishl_imm_s(ca_c, 24); + // block/span shader's alpha_byte overlay. A_LINE alpha is the pixel + // coverage instead, which is not modelled: full (mirrors combine_host_dda). + let alpha_byte = if dm0.adrmode() == DRAWMODE0_ADRMODE_A_LINE { + b.ins().iconst(types::I32, 0xFF00_0000u32 as i64) + } else { + let ca = ld32!(ctx_off!(coloralpha)); + let ca_c = clamp_color_component(&mut b, ca); + b.ins().ishl_imm_s(ca_c, 24) + }; let color24 = b.ins().band_imm_s(raw_src, 0x00FF_FFFFi64); let raw_src = b.ins().bor(color24, alpha_byte); diff --git a/src/dev/ng1/rex3_tests.rs b/src/dev/ng1/rex3_tests.rs index bd81b8f9..3b9283a2 100644 --- a/src/dev/ng1/rex3_tests.rs +++ b/src/dev/ng1/rex3_tests.rs @@ -1755,8 +1755,8 @@ const DM0_DRAW_ILINE_STEP: u32 = DRAWMODE0_OPCODE_DRAW | DRAWMODE0_ADRMODE_I_LIN // DM0 for a full F_LINE draw — fractional-endpoint Bresenham correction. const DM0_DRAW_FLINE: u32 = DRAWMODE0_OPCODE_DRAW | DRAWMODE0_ADRMODE_F_LINE_SH | DM0_DOSETUP | DM0_STOPONXY; // DM0 for a full A_LINE draw — F_LINE plus AWEIGHT-LUT endpoint suppression (needs ENDPTFILTER, bit 22, set separately). -// A_LINE tests are out of scope for this pass (see rules/testing/rex3-fline-fractional-bresenham.md) — -// kept for a future session, not yet exercised by any test. +// Its Bresenham walk is not tested yet (see rules/testing/rex3-fline-fractional-bresenham.md); +// the blend tests use it for its alpha. #[allow(dead_code)] const DM0_DRAW_ALINE: u32 = DRAWMODE0_OPCODE_DRAW | DRAWMODE0_ADRMODE_A_LINE_SH | DM0_DOSETUP | DM0_STOPONXY; #[allow(dead_code)] @@ -4763,6 +4763,115 @@ mod jit_tests { } + /// Runs `setup` on an interpreter REX3 and on a JIT one (after the JIT has + /// compiled `dm0`/`dm1`), returning the pixels at `xs` on row `y` from each. + fn interp_and_jit(dm0: u32, dm1: u32, xs: &[i32], y: i32, setup: &dyn Fn(&Rex3)) -> (Vec, Vec) { + let rex_i = make_rex3(); + rex3init(rex_i); + setup(rex_i); + wait(rex_i); + let interp = xs.iter().map(|&x| read_pixel(rex_i, x, y) & 0xFFFFFF).collect(); + let rex_j = make_rex3_jit(); + rex3init(rex_j); + setup(rex_j); + wait(rex_j); + if let Some(ref jit) = rex_j.rex_jit { + assert!(jit.wait_compiled(dm0, dm1, 0xF << CLIPMODE_CIDMATCH_SHIFT), + "JIT compile failed dm0={dm0:#010x} dm1={dm1:#010x}"); + } + clear_region(rex_j, 0, y, 31, y); + rex3init(rex_j); + setup(rex_j); + wait(rex_j); + let jit = xs.iter().map(|&x| read_pixel(rex_j, x, y) & 0xFFFFFF).collect(); + (interp, jit) + } + + /// ALPHAHOST without COLORHOST at a 4-bit host depth: the 8-bit host field + /// is the alpha that blends the DDA colour (spec §3.9, §3.10). IRIX's smooth + /// points send their per-pixel coverage this way; unpacking the field as a + /// 4-bit colour lost it, so every such pixel had alpha 0. + #[test] + fn alphahost_field_is_the_alpha() { + let dm1 = DRAWMODE1_PLANES_RGB | (3 << 3) | (1 << 15) + | DRAWMODE1_COMPARE_DISABLE_SH | (1 << 18) + | (DRAWMODE1_BF_SA << 19) | (DRAWMODE1_BF_MSA << 22) + | DRAWMODE1_LOGICOP_SRC_SH; // host depth 0: 4 bits, 8-bit fields + let dm0 = DM0_DRAW_BLOCK | (1 << 7); // ALPHAHOST, colour from the DDA + let alphas: [u32; 3] = [0x00, 0x80, 0xFF]; + let setup = |rex: &Rex3| { + for (i, a) in alphas.iter().enumerate() { + let x = i as i32; + reg(rex, REX3_DRAWMODE1, dm1); + reg(rex, REX3_WRMASK, 0xFFFFFF); + reg(rex, REX3_COLORRED, 0xC0 << 11); + reg(rex, REX3_COLORGRN, 0xC0 << 11); + reg(rex, REX3_COLORBLUE, 0xC0 << 11); + reg(rex, REX3_XYENDI, xy(x, 0)); + reg(rex, REX3_XYSTARTI, xy(x, 0)); + reg(rex, REX3_DRAWMODE0, dm0); + write_hostrw32(rex, a << 24); + wait(rex); + } + }; + let (interp, jit) = interp_and_jit(dm0, dm1, &[0, 1, 2], 0, &setup); + // Over black: 0xC0 * alpha / 255. + assert_eq!(interp, vec![0x000000, 0x606060, 0xC0C0C0], "interp {interp:08x?}"); + assert_eq!(interp, jit, "JIT/interp mismatch: interp={interp:08x?} jit={jit:08x?}"); + } + + /// A_LINE takes its alpha from pixel coverage, not COLORALPHA: IRIX's + /// OpenGL draws GL_LINE_SMOOTH with blending on and COLORALPHA left at + /// whatever the last primitive set. Coverage is not modelled, so the line + /// must draw at full strength even with COLORALPHA 0. + #[test] + fn aline_alpha_is_coverage_not_coloralpha() { + let dm1 = DRAWMODE1_PLANES_RGB | (3 << 3) | (1 << 15) + | DRAWMODE1_COMPARE_DISABLE_SH | (1 << 18) + | (DRAWMODE1_BF_SA << 19) | (DRAWMODE1_BF_MSA << 22) + | DRAWMODE1_LOGICOP_SRC_SH; + let dm0 = DM0_DRAW_ALINE; + let setup = |rex: &Rex3| { + reg(rex, REX3_DRAWMODE1, dm1); + reg(rex, REX3_WRMASK, 0xFFFFFF); + reg(rex, REX3_COLORALPHA, 0); + reg(rex, REX3_COLORRED, 0xC0 << 11); + reg(rex, REX3_COLORGRN, 0xC0 << 11); + reg(rex, REX3_COLORBLUE, 0xC0 << 11); + reg(rex, REX3_XYSTARTI, xy(10, 4)); + reg(rex, REX3_XYENDI, xy(20, 4)); + reg_go(rex, REX3_DRAWMODE0, dm0); + }; + // Where the line lands is not what this checks (the interpreter and + // the JIT walk an integer-coordinate A_LINE one row apart): every pixel + // it lit must be at full strength, in both. + let lit = |rex: &Rex3| -> Vec { + (0..10).flat_map(|y| (8..23).map(move |x| (x, y))) + .map(|(x, y)| read_pixel(rex, x, y) & 0xFFFFFF) + .filter(|&p| p != 0).collect() + }; + let rex_i = make_rex3(); + rex3init(rex_i); + setup(rex_i); + wait(rex_i); + let rex_j = make_rex3_jit(); + rex3init(rex_j); + setup(rex_j); + wait(rex_j); + if let Some(ref jit) = rex_j.rex_jit { + assert!(jit.wait_compiled(dm0, dm1, 0xF << CLIPMODE_CIDMATCH_SHIFT), + "JIT compile failed dm0={dm0:#010x} dm1={dm1:#010x}"); + } + clear_region(rex_j, 0, 0, 31, 9); + rex3init(rex_j); + setup(rex_j); + wait(rex_j); + for (name, px) in [("interp", lit(rex_i)), ("jit", lit(rex_j))] { + assert!(px.len() >= 10, "{name}: line not drawn ({} pixels)", px.len()); + assert!(px.iter().all(|&p| p == 0xC0C0C0), "{name}: not full strength: {px:08x?}"); + } + } + /// FASTCLEAR with CID checking ENABLED must draw as an ordinary draw. /// /// rex3.pdf says so three times — DRAWMODE1 bit 17 ("when CID checking