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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added docs/SCSI Commands Reference Manual.pdf
Binary file not shown.
Binary file added docs/o2/AIC-7850_Data_Book.pdf
Binary file not shown.
Binary file added docs/o2/AIC-7870_Data_Book.pdf
Binary file not shown.
Binary file added docs/o2/crime1_5.pdf
Binary file not shown.
Binary file added docs/o2/gbe.pdf
Binary file not shown.
Binary file added docs/o2/mace.pdf
Binary file not shown.
Binary file added docs/o2/vice.pdf
Binary file not shown.
96 changes: 96 additions & 0 deletions rules/rex3/soft-z-lines-zpattern-and-bresround.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
IRIS GL software-Z lines: ZPATTERN reload and BRESROUND

Symptom: fsn (IRIS GL) label text drawn in fragments, strokes missing or a
row off; the same shape of damage in other GL apps' small line text.

How IRIS GL draws a Z-buffered line on Newport (fns.log, `rex trace`): the
CPU does the depth test and each stroke becomes
XSTARTF YSTARTF XENDF YENDF fractional endpoints
DRAWMODE0 = 0x0000930e F_LINE STOPONX STOPONY ENZPAT LEN32, no DOSETUP
SETUP = 0 setup without iterating
ZPATTERN GO = 0xe0000000 one mask bit per pixel, MSB = first pixel
with BRESRNDINC2 = 0x001f.... (BRESROUND byte 0). Glyph strokes are 1-4
pixels, so every stroke is far shorter than the 32-bit pattern.

Two bugs, both fixed:

1. ZPATTERN is a recirculating register, "msb = first pixel": writing it
reloads it. We kept the value plus a bit index (`zpat_bit`) that only
DOSETUP and the end of a span row reset, so a stroke's mask was tested
from wherever the previous stroke stopped (bit 28 after a 3-pixel stroke;
bit 0 on a fresh board). Spans (polygons) never showed it: a full 32-pixel
chunk wraps the index back to 31 and row ends reset it. A ZPATTERN write
now sets zpat_bit = 31.

2. BRESROUND (BRESRNDINC2[31:24], one bit per octant, msb = spec octant 1;
hardware octant {XMAJOR,XDEC,YDEC} 5,1,3,7,6,2,0,4 = spec 1..8) picks the
step when d == 0: set = diagonal (d < 0 goes straight), clear = straight
(d <= 0 goes straight). We ignored it and always behaved as "set". IRIS GL
loads 0, the X server 0xff. It decides lines that pass exactly through a
pixel-row boundary at a column centre (a 2x1 I_LINE starts at d == 0).
It first looked like the cause of horizontal F_LINEs dropping a row; that
was fault 3's y mirroring, and those lines no longer tie. Both engines now
honour it (`bres_straight_on_zero`; JIT: `round_set_v` in
emit_draw_iline). The generated shaders call the generic code, so they
follow automatically.

3. F_LINE/A_LINE setup put pixels a row or column off (strays sticking
out of glyphs, and the spotlight showing floor through the label where
IRIS GL's Z buffer said text was). Four separate faults in setup():
- slope from the integer endpoint deltas; the spec uses the fixed-point
ones (a 2.5 x 1.44 stroke became 2 x 2);
- the spec's octant table has y pointing UP (its octant 1 = hardware
octant 5, YDEC set): its y fraction is 16 - the register's. Applying
the table to the register fraction mirrored y the wrong way round in
every octant;
- the fractional term was rounded (>> 4); the errata's BRESD is s18.8,
so d, incr1, incr2 are now kept in 1/256 pixel (fits the 20/21/27-bit
fields for any on-screen line);
- no first-pixel adjustment at all: the first pixel is the start
point's own pixel. The pseudocode's E test and the errata's "E and G
tests" both move it, and both are wrong on this REX3 (see below).
And the pixel count must be along the octant's major axis, not
max(|dx|, |dy|) of the integer deltas (they disagree near 45 degrees).
Result: the start point's pixel, then in each column of the major axis
the row holding the true line at the column's centre (pixel n covers
[n, n+1)), moving at most one row per column. The test oracle
`fline_geom` is that rule, written from geometry, not from the code; a
Python model of the setup matched it on 29547/29547 random lines (exact
row-boundary ties excluded: BRESROUND's call).
Ground truth: fsn draws its labels before the spotlight disc, and the
disc spans are masked by IRIS GL's software Z at the pixels GL computed
for each stroke. Where our stroke pixel differed, the floor colour
showed through inside the disc. Replaying fns.log (trace -> recording)
and counting those holes found and confirmed each fix.
The first-pixel rule needed more: fsn.rec (a `rex rec` of fsn redrawing
two labelled files), replayed on the interpreter with every stroke's
endpoints and pixels logged, plus every later soft-Z span pixel with its
mask bit. A 0 bit = IRIS GL thinks something nearer (the text) is there.
On the strokes where candidate rules put the first pixel in different
places and the next span's bits disagree: start point's own pixel 113/119,
pseudocode E 87, errata-style E/G (centre rule for the first pixel too)
6; on later pixels 35/36. The losers are 1-pixel strokes next to others.
Errata-style E/G gave the "position dependent" stubs: a 45-degree stroke
evaluated half a pixel behind its start lands in the neighbouring row.

Tests: test_zpattern_write_restarts_pattern_for_soft_z_lines (fsn's stroke
sequence), test_bresround_decides_the_d_zero_step (both settings, both
engines), test_fline_fractional_both_ends_matches_geometry (2000 random
lines, both engines), test_fline_fsn_label_stroke.

MAME is no reference here: its line code ignores ZPATTERN and steps x/y in
1/16-pixel units, which hides any wrong first step.

Also found with fsn.rec (REX3_REPLAY_ENGINE=interp vs jit, bisected with
REX3_REPLAY_LIMIT, diffed with REX3_REPLAY_STATE): the Cranelift shader ended
a SPAN like a BLOCK row (x back to XSAVE, y + 1); a span leaves XSTART one
past XEND and YSTART alone (spec §3.5.3, draw_span, MAME). And at a row end
it left a disabled pattern's bit position stale where the interpreter
restarts both at 31. Only shapes not in the precompiled table run through
Cranelift, so this showed up in one span of 4M records (IRIS GL reads
XSTART/YSTART back). Both engines now replay fsn.rec to the same state.

Not covered: A_LINE is still one pixel per step at full coverage (the spec
draws two, T and S, weighted from AWEIGHT), and its ENDPTFILTER drops end
pixels by a guessed rule instead of scaling coverage. fsn uses it rarely
(16 GOs in fns.log); check blast's HUD before assuming it is the same bug.
8 changes: 8 additions & 0 deletions rules/testing/rex3-fline-fractional-bresenham.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# REX3 F_LINE fractional Bresenham — fixes and open A_LINE follow-up

> **Superseded in part (2026-10-17):** the setup described below still had
> the spec's y orientation backwards, rounded the fractional term, lacked the
> errata's E/G first-pixel tests and sized the walk from integer deltas. The
> current setup is `fline_setup` (rex3_generic.rs) and the test oracle is the
> geometric `fline_geom`; see rules/rex3/soft-z-lines-zpattern-and-bresround.md.
> The "where the fix lives" (setup(), shared by both engines) and the
> test-coordinate notes below still hold.

## Fixed: base `d` formula was I_LINE's, not F_LINE's — and the fix lives in `setup()`, not `draw_line_bresenham`

`fline_apply_fract` (rex3.rs) received `*d` already computed by `setup()`
Expand Down
66 changes: 38 additions & 28 deletions src/dev/ng1/rex3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,21 @@ pub const OCTANT_XMAJOR: u32 = 1 << 2;
// per-pixel walk) — both interpreter code paths, but setup() also gates what the
// JIT sees (see setup()'s doc comment on why the fractional correction lives there).
#[rustfmt::skip]
/// Bit of BRESRNDINC2's BRESROUND byte (bits 31:24) for each hardware
/// octant. The spec numbers octants 1-8 with octant 1 in the msb; hardware
/// octant {XMAJOR, XDEC, YDEC} 5,1,3,7,6,2,0,4 is spec octant 1..8.
/// A set bit steps straight only when d < 0, a clear one when d <= 0
/// (rex3.pdf §3.6, "BRESROUND"); it only matters when d is exactly 0.
pub(crate) const BRESROUND_BIT: [u32; 8] = [1, 6, 2, 5, 0, 7, 3, 4];

/// Whether a Bresenham step with d == 0 goes straight (BRESROUND clear for
/// the line's octant). IRIS GL's lines run with BRESROUND = 0; the X server
/// loads 0xff.
#[inline(always)]
pub(crate) fn bres_straight_on_zero(ctx: &Rex3Context, octant: u32) -> bool {
(ctx.bresrndinc2.rnd() >> BRESROUND_BIT[(octant & 7) as usize]) & 1 == 0
}

pub(crate) const REX3_BRES_OCTANTS: [(i32, i32, i32, i32, bool); 8] = [
( 0, 1, -1, -1, true ), // octant 0
( 0, 1, 1, 1, true ), // octant 1
Expand Down Expand Up @@ -1736,6 +1751,15 @@ impl Rex3 {
}

fn setup(&self, ctx: &mut Rex3Context) {
// F_LINE/A_LINE set up from the fixed-point endpoints. Done here, not
// at draw time: the JIT never calls back into interpreter draw code,
// it walks whatever octant/incr/d/start setup() leaves in ctx.
let adrmode = ctx.drawmode0.adrmode();
if adrmode == DRAWMODE0_ADRMODE_F_LINE || adrmode == DRAWMODE0_ADRMODE_A_LINE {
crate::dev::ng1::rex3_generic::fline_setup(ctx);
return;
}

let dx = ctx.xend - ctx.xstart;
let dy = ctx.yend - ctx.ystart;

Expand All @@ -1755,33 +1779,12 @@ impl Rex3 {
// d = incr1 - major (initial decision variable, I_LINE formula)
let incr1: i32 = 2 * minor;
let incr2: i32 = 2 * (minor - major);
let mut d: i32 = incr1 - major;
let d: i32 = incr1 - major;

// Store as plain two's-complement integers masked to register field widths.
ctx.bresoctinc1.set_octant(octant);
ctx.bresoctinc1.set_incr1((incr1 as u32) & 0xFFFFF); // 20-bit, always positive
ctx.bresrndinc2.set_incr2((incr2 as u32) & 0x1FFFFF); // 21-bit signed

// F_LINE/A_LINE: apply the fractional-endpoint correction HERE, not at draw
// time. This must happen in setup() (not draw_line_bresenham/fline_apply_fract)
// because setup() is the only Bresenham-state derivation shared by both the
// interpreter and the JIT — the JIT-compiled entry point never calls back into
// interpreter draw functions, it just replays whatever bresd/bresoctinc1/
// bresrndinc2/xstart/ystart setup() already wrote into ctx. Applying the
// correction only in draw_line_bresenham (the old approach) left the JIT
// silently drawing plain-I_LINE trajectories for F_LINE/A_LINE, since the
// JIT never reaches that interpreter-only code path.
let adrmode = ctx.drawmode0.adrmode();
let is_fractional = adrmode == DRAWMODE0_ADRMODE_F_LINE || adrmode == DRAWMODE0_ADRMODE_A_LINE;
if is_fractional {
let (_incrx1, incrx2, _incry1, incry2, y_major) = REX3_BRES_OCTANTS[(octant & 7) as usize];
let mut x = ctx.xstart >> 11;
let mut y = ctx.ystart >> 11;
crate::dev::ng1::rex3_generic::fline_apply_fract(ctx, &mut d, &mut x, &mut y, incrx2, incry2, y_major);
ctx.xstart = x << 11;
ctx.ystart = y << 11;
}

ctx.bresd = (d as u32) & 0x7FF_FFFF; // 27-bit signed
}

Expand Down Expand Up @@ -2698,11 +2701,10 @@ impl Rex3 {
} else {
// Continuation GO: re-derive Bresenham when the segment axis disagrees with
// the persisted octant (e.g. degenerate setup point, then horizontal cont).
let adrmode = ctx.drawmode0.adrmode();
let is_line = adrmode == DRAWMODE0_ADRMODE_I_LINE
|| adrmode == DRAWMODE0_ADRMODE_F_LINE
|| adrmode == DRAWMODE0_ADRMODE_A_LINE;
if is_line {
// I_LINE only: F_LINE/A_LINE take their octant from the
// fixed-point deltas, which integer deltas can disagree with, and
// their setup moves the start point, so it must not run twice.
if ctx.drawmode0.adrmode() == DRAWMODE0_ADRMODE_I_LINE {
let xs = ctx.xstart >> 11;
let ys = ctx.ystart >> 11;
let xe = ctx.xend >> 11;
Expand Down Expand Up @@ -3302,7 +3304,15 @@ impl Rex3 {
REX3_LSMODE => ctx.lsmode = LsMode(val),
REX3_LSPATTERN => ctx.lspattern = val,
REX3_LSPATSAVE => ctx.lspatsave = val,
REX3_ZPATTERN => ctx.zpattern = val,
// ZPATTERN is a recirculating register, "msb = first pixel"
// (rex3.pdf register table): writing it reloads the pattern, so
// the next pixel tests bit 31 whatever the last primitive left.
// IRIS GL's software-Z lines rely on it: each stroke is an F_LINE
// without DOSETUP drawn by a ZPATTERN GO with an MSB-aligned mask.
REX3_ZPATTERN => {
ctx.zpattern = val;
ctx.zpat_bit = 31;
}
REX3_LSSAVE => {
ctx.lssave = val;
ctx.lspatsave = ctx.lspattern;
Expand Down
139 changes: 67 additions & 72 deletions src/dev/ng1/rex3_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1625,83 +1625,72 @@ pub fn draw_span_g<M: Mode>(fb: &Framebuffers, ctx: &mut Rex3Context, m: &M) {
}
}

/// Fractional d correction for F_LINE/A_LINE from 21.11 endpoint sub-pixel position.
/// Fractional nibble is bits [10:7] of the 21.11 coordinate (16.4(7) layout).
pub fn fline_apply_fract(
ctx: &Rex3Context,
d: &mut i32,
x: &mut i32,
y: &mut i32,
incrx2: i32,
incry2: i32,
y_major: bool,
) {
let octant = ctx.bresoctinc1.octant() & 7;
let x1p = ctx.xstart >> 11;
let y1p = ctx.ystart >> 11;
let x2p = ctx.xend >> 11;
let y2p = ctx.yend >> 11;
let mut dx = (x1p - x2p).abs();
let mut dy = (y1p - y2p).abs();
let mut xf = ((ctx.xstart >> 7) & 0xF) as i32;
let mut yf = ((ctx.ystart >> 7) & 0xF) as i32;

/// Bresenham setup for F_LINE/A_LINE (rex3.pdf §3.6.1.2, "Code for aliased
/// line with fractional endpoints"). The pixels: the start point's own pixel,
/// then in each column of the major axis the row holding the true line at the
/// column's centre (pixel n covers [n, n+1)), moving at most one row per
/// column.
///
/// - Octant, `incr1`, `incr2` and `d` come from the fixed-point (16.4)
/// endpoint deltas. Integer deltas get short strokes' slopes badly wrong
/// (fsn: 2.5 x 1.44 became 2 x 2).
/// - All terms are in 1/256 pixel, so the fractional term is exact (the
/// errata's s18.8 BRESD). For any on-screen line `incr1`/`incr2` still fit
/// their 20/21-bit fields.
/// - The spec's octant table has y pointing up (its octant 1 is hardware
/// octant 5, YDEC set) and the registers have y growing down, so its y
/// fraction is 1 - the register's.
/// - No first-pixel adjustment: neither the pseudocode's E test nor the
/// errata's E/G tests. IRIS GL's software Z buffer, which computes the
/// same pixels, confirms the start point's own pixel in 113 of 119 strokes
/// where the rules disagree (fsn.rec; the rest are 1-pixel strokes next to
/// others).
///
/// See rules/rex3/soft-z-lines-zpattern-and-bresround.md.
pub fn fline_setup(ctx: &mut Rex3Context) {
use crate::dev::ng1::rex3::{OCTANT_XDEC, OCTANT_XMAJOR, OCTANT_YDEC, REX3_BRES_OCTANTS};
// 21.11 -> 16.4
let (x1, y1) = (ctx.xstart >> 7, ctx.ystart >> 7);
let (x2, y2) = (ctx.xend >> 7, ctx.yend >> 7);
let mut dx = (x1 - x2).abs();
let mut dy = (y1 - y2).abs();

let mut octant = 0u32;
if y1 > y2 { octant |= OCTANT_YDEC; }
if x1 > x2 { octant |= OCTANT_XDEC; }
if dx > dy { octant |= OCTANT_XMAJOR; }

// Register row n + f is spec row -(n+1) + (1 - f); f = 0 is spec
// fraction 1 in row -(n+1), which keeps the start pixel on row n.
let mut xf = x1 & 0xF;
let mut yf = 0x10 - (y1 & 0xF);
// The spec's table, verbatim: map the start fractions into its octant 1,
// swapping for y-major octants, 1 - f on an axis that runs backwards.
match octant {
1 => {
std::mem::swap(&mut xf, &mut yf);
std::mem::swap(&mut dx, &mut dy);
}
3 => {
xf = 0x10 - xf;
std::mem::swap(&mut xf, &mut yf);
std::mem::swap(&mut dx, &mut dy);
}
1 => { std::mem::swap(&mut xf, &mut yf); std::mem::swap(&mut dx, &mut dy); }
3 => { xf = 0x10 - xf; std::mem::swap(&mut xf, &mut yf); std::mem::swap(&mut dx, &mut dy); }
7 => { xf = 0x10 - xf; }
6 => {
xf = 0x10 - xf;
yf = 0x10 - yf;
}
2 => {
let t = 0x10 - xf;
xf = 0x10 - yf;
yf = t;
std::mem::swap(&mut dx, &mut dy);
}
0 => {
let t = 0x10 - yf;
yf = xf;
xf = t;
std::mem::swap(&mut dx, &mut dy);
}
6 => { xf = 0x10 - xf; yf = 0x10 - yf; }
2 => { let t = 0x10 - xf; xf = 0x10 - yf; yf = t; std::mem::swap(&mut dx, &mut dy); }
0 => { let t = 0x10 - yf; yf = xf; xf = t; std::mem::swap(&mut dx, &mut dy); }
4 => { yf = 0x10 - yf; }
_ => {}
}
// Now dx = major, dy = minor, in 1/16 pixel; d and the increments in 1/256.
let incr1 = 32 * dy;
let incr2 = 32 * (dy - dx);
let d = 16 * (3 * dy - 2 * dx) + 2 * (dx * yf - dy * xf);

// `*d` arrives holding the I_LINE decision variable (2*minor - major,
// computed by setup() and shared by all line adrmodes). The REX3 spec
// (rex3_pdf.md 3.6.1.2/3.6.2.1) defines F_LINE/A_LINE's base d as
// 3*minor - 2*major instead — confirmed against MAME's do_fline,
// which independently derives the same 3dy-2dx formula. The two
// formulas differ by exactly (minor - major); apply that correction
// before adding the fractional term below. Without it, the fractional
// term is added to the wrong baseline and can flip d's sign on the
// very first step for near-degenerate (small minor-axis) lines,
// producing a spurious extra step in the minor-axis direction that
// the line never recovers from (confirmed via a real R4400/REX3
// fractional-line test that undershot its endpoint by one row).
*d += dy - dx;
*d += 2 * (((dx * yf) >> 4) - ((dy * xf) >> 4));
let major_delta = if y_major { dy } else { dx };
let e = *d - 2 * major_delta;
if e > 0 {
*d = e;
let x_major = !y_major;
if x_major {
*y -= incry2;
} else {
*x += incrx2;
}
}
// The first pixel is the start point's own pixel: no E/G adjustment.
let (x, y) = (x1 >> 4, y1 >> 4);

ctx.bresoctinc1.set_octant(octant);
ctx.bresoctinc1.set_incr1((incr1 as u32) & 0xFFFFF);
ctx.bresrndinc2.set_incr2((incr2 as u32) & 0x1FFFFF);
ctx.bresd = (d as u32) & 0x7FF_FFFF;
ctx.xstart = x << 11;
ctx.ystart = y << 11;
}

pub fn draw_iline_g<M: Mode>(fb: &Framebuffers, ctx: &mut Rex3Context, m: &M) {
Expand Down Expand Up @@ -1778,7 +1767,10 @@ fn draw_line_bresenham<M: Mode>(
// degenerate setup GO followed by a horizontal stipple continuation).
let adx = (x2 - x).abs();
let ady = (y2 - y).abs();
let major = adx.max(ady);
// F_LINE/A_LINE: the length along the axis the octant calls major. Their
// octant comes from the fixed-point deltas, and near 45 degrees the
// integer deltas can say the other axis is longer.
let major = if fract { if y_major { ady } else { adx } } else { adx.max(ady) };
let mut pixel_count = major + 1;
if m.length32() != 0 && pixel_count > 32 {
pixel_count = 32;
Expand All @@ -1796,9 +1788,12 @@ fn draw_line_bresenham<M: Mode>(

let lsadvlast = m.lsadvlast() != 0;

// Which way a step with d == 0 goes is programmable per octant.
let straight_on_zero = crate::dev::ng1::rex3::bres_straight_on_zero(ctx, octant as u32);

macro_rules! bres_step {
() => {
if d < 0 {
if d < 0 || (d == 0 && straight_on_zero) {
x += incrx1; y -= incry1; d += incr1;
} else {
x += incrx2; y -= incry2; d += incr2;
Expand Down
Loading
Loading