Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ 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. 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
Expand Down
2 changes: 1 addition & 1 deletion docs/rex3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
37 changes: 37 additions & 0 deletions rules/rex3/alphahost-fields-and-aline-alpha.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions rules/rex3/blendalpha-and-alpha-blending.md
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
37 changes: 27 additions & 10 deletions src/dev/ng1/rex3_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -777,16 +777,17 @@ pub fn blend<M: Mode>(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
Expand All @@ -811,7 +812,7 @@ pub fn blend<M: Mode>(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;
Expand Down Expand Up @@ -1199,7 +1200,17 @@ pub fn fetch_host_pixel<M: Mode>(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
Expand Down Expand Up @@ -1260,6 +1271,12 @@ fn combine_host_dda<M: Mode>(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)
};
Expand Down
47 changes: 29 additions & 18 deletions src/dev/ng1/rex3_jit/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -2856,11 +2867,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,
Expand All @@ -2871,8 +2882,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);
Expand Down Expand Up @@ -2904,8 +2915,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);
Expand All @@ -2916,10 +2927,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);
Expand Down
Loading