diff --git a/docs/DQ8003_SeeqTechnology.pdf b/docs/Indy/DQ8003_SeeqTechnology.pdf similarity index 100% rename from docs/DQ8003_SeeqTechnology.pdf rename to docs/Indy/DQ8003_SeeqTechnology.pdf diff --git a/docs/Dallas 1386 Indy.pdf b/docs/Indy/Dallas 1386 Indy.pdf similarity index 100% rename from docs/Dallas 1386 Indy.pdf rename to docs/Indy/Dallas 1386 Indy.pdf diff --git a/docs/FM93CS56.pdf b/docs/Indy/FM93CS56.pdf similarity index 100% rename from docs/FM93CS56.pdf rename to docs/Indy/FM93CS56.pdf diff --git a/docs/SAA7191B.pdf b/docs/Indy/SAA7191B.pdf similarity index 100% rename from docs/SAA7191B.pdf rename to docs/Indy/SAA7191B.pdf diff --git a/docs/WD33C93A_Data_Sheet_and_Application_Notes_Nov1990.pdf b/docs/Indy/WD33C93A_Data_Sheet_and_Application_Notes_Nov1990.pdf similarity index 100% rename from docs/WD33C93A_Data_Sheet_and_Application_Notes_Nov1990.pdf rename to docs/Indy/WD33C93A_Data_Sheet_and_Application_Notes_Nov1990.pdf diff --git a/docs/am33c93a.pdf b/docs/Indy/am33c93a.pdf similarity index 100% rename from docs/am33c93a.pdf rename to docs/Indy/am33c93a.pdf diff --git a/docs/bt445_ramdac.pdf b/docs/Indy/bt445_ramdac.pdf similarity index 100% rename from docs/bt445_ramdac.pdf rename to docs/Indy/bt445_ramdac.pdf diff --git a/docs/cmap_MS622424.pdf b/docs/Indy/cmap_MS622424.pdf similarity index 100% rename from docs/cmap_MS622424.pdf rename to docs/Indy/cmap_MS622424.pdf diff --git a/docs/dmux1.pdf b/docs/Indy/dmux1.pdf similarity index 100% rename from docs/dmux1.pdf rename to docs/Indy/dmux1.pdf diff --git a/docs/gio64.pdf b/docs/Indy/gio64.pdf similarity index 100% rename from docs/gio64.pdf rename to docs/Indy/gio64.pdf diff --git a/docs/hal2.md b/docs/Indy/hal2.md similarity index 100% rename from docs/hal2.md rename to docs/Indy/hal2.md diff --git a/docs/hpc3.pdf b/docs/Indy/hpc3.pdf similarity index 100% rename from docs/hpc3.pdf rename to docs/Indy/hpc3.pdf diff --git a/docs/ioc.pdf b/docs/Indy/ioc.pdf similarity index 100% rename from docs/ioc.pdf rename to docs/Indy/ioc.pdf diff --git a/docs/mc.pdf b/docs/Indy/mc.pdf similarity index 100% rename from docs/mc.pdf rename to docs/Indy/mc.pdf diff --git a/docs/n64_gio_specs.md b/docs/Indy/n64_gio_specs.md similarity index 100% rename from docs/n64_gio_specs.md rename to docs/Indy/n64_gio_specs.md diff --git a/docs/rb2.pdf b/docs/Indy/rb2.pdf similarity index 100% rename from docs/rb2.pdf rename to docs/Indy/rb2.pdf diff --git a/docs/rex3.md b/docs/Indy/rex3.md similarity index 100% rename from docs/rex3.md rename to docs/Indy/rex3.md diff --git a/docs/rex3.pdf b/docs/Indy/rex3.pdf similarity index 100% rename from docs/rex3.pdf rename to docs/Indy/rex3.pdf diff --git a/docs/ro1.pdf b/docs/Indy/ro1.pdf similarity index 100% rename from docs/ro1.pdf rename to docs/Indy/ro1.pdf diff --git a/docs/ultra64.md b/docs/Indy/ultra64.md similarity index 100% rename from docs/ultra64.md rename to docs/Indy/ultra64.md diff --git a/docs/vc2.pdf b/docs/Indy/vc2.pdf similarity index 100% rename from docs/vc2.pdf rename to docs/Indy/vc2.pdf diff --git a/docs/vdma.pdf b/docs/Indy/vdma.pdf similarity index 100% rename from docs/vdma.pdf rename to docs/Indy/vdma.pdf diff --git a/docs/vino/vino.01.pdf b/docs/Indy/vino/vino.01.pdf similarity index 100% rename from docs/vino/vino.01.pdf rename to docs/Indy/vino/vino.01.pdf diff --git a/docs/vino/vino.02.pdf b/docs/Indy/vino/vino.02.pdf similarity index 100% rename from docs/vino/vino.02.pdf rename to docs/Indy/vino/vino.02.pdf diff --git a/docs/vino/vino.03.pdf b/docs/Indy/vino/vino.03.pdf similarity index 100% rename from docs/vino/vino.03.pdf rename to docs/Indy/vino/vino.03.pdf diff --git a/docs/vino/vino.04.pdf b/docs/Indy/vino/vino.04.pdf similarity index 100% rename from docs/vino/vino.04.pdf rename to docs/Indy/vino/vino.04.pdf diff --git a/docs/vino/vino.05.pdf b/docs/Indy/vino/vino.05.pdf similarity index 100% rename from docs/vino/vino.05.pdf rename to docs/Indy/vino/vino.05.pdf diff --git a/docs/vino/vino.06.pdf b/docs/Indy/vino/vino.06.pdf similarity index 100% rename from docs/vino/vino.06.pdf rename to docs/Indy/vino/vino.06.pdf diff --git a/docs/vino/vino.pdf b/docs/Indy/vino/vino.pdf similarity index 100% rename from docs/vino/vino.pdf rename to docs/Indy/vino/vino.pdf diff --git a/docs/wd33c93a.md b/docs/Indy/wd33c93a.md similarity index 100% rename from docs/wd33c93a.md rename to docs/Indy/wd33c93a.md diff --git a/docs/xmap9.pdf b/docs/Indy/xmap9.pdf similarity index 100% rename from docs/xmap9.pdf rename to docs/Indy/xmap9.pdf diff --git a/docs/z85c30.pdf b/docs/Indy/z85c30.pdf similarity index 100% rename from docs/z85c30.pdf rename to docs/Indy/z85c30.pdf diff --git a/docs/Octane/SGI-Bridge_ASIC_Specification-Revision_4.0_for_Rev_D_Si-19980426.pdf b/docs/Octane/SGI-Bridge_ASIC_Specification-Revision_4.0_for_Rev_D_Si-19980426.pdf new file mode 100644 index 00000000..f945cecb Binary files /dev/null and b/docs/Octane/SGI-Bridge_ASIC_Specification-Revision_4.0_for_Rev_D_Si-19980426.pdf differ diff --git a/docs/Octane/SGI-Xbridge_ASIC_Specification-Revision_0.5_for_Rev_A_Si-19980611.pdf b/docs/Octane/SGI-Xbridge_ASIC_Specification-Revision_0.5_for_Rev_A_Si-19980611.pdf new file mode 100644 index 00000000..0793e30a Binary files /dev/null and b/docs/Octane/SGI-Xbridge_ASIC_Specification-Revision_0.5_for_Rev_A_Si-19980611.pdf differ diff --git a/docs/Octane/SGI_Crossbow_ASIC_Specification-Revision_12.0-19961018.pdf b/docs/Octane/SGI_Crossbow_ASIC_Specification-Revision_12.0-19961018.pdf new file mode 100644 index 00000000..b57e5ea0 Binary files /dev/null and b/docs/Octane/SGI_Crossbow_ASIC_Specification-Revision_12.0-19961018.pdf differ diff --git a/docs/SCSI Commands Reference Manual.pdf b/docs/SCSI Commands Reference Manual.pdf new file mode 100644 index 00000000..5b52732b Binary files /dev/null and b/docs/SCSI Commands Reference Manual.pdf differ diff --git a/docs/o2/AIC-7850_Data_Book.pdf b/docs/o2/AIC-7850_Data_Book.pdf new file mode 100644 index 00000000..260998f9 Binary files /dev/null and b/docs/o2/AIC-7850_Data_Book.pdf differ diff --git a/docs/o2/AIC-7870_Data_Book.pdf b/docs/o2/AIC-7870_Data_Book.pdf new file mode 100644 index 00000000..c69fb3ef Binary files /dev/null and b/docs/o2/AIC-7870_Data_Book.pdf differ diff --git a/docs/o2/crime1_5.pdf b/docs/o2/crime1_5.pdf new file mode 100644 index 00000000..cd8e853c Binary files /dev/null and b/docs/o2/crime1_5.pdf differ diff --git a/docs/o2/gbe.pdf b/docs/o2/gbe.pdf new file mode 100644 index 00000000..4afadfa0 Binary files /dev/null and b/docs/o2/gbe.pdf differ diff --git a/docs/o2/mace.pdf b/docs/o2/mace.pdf new file mode 100644 index 00000000..97fc3449 Binary files /dev/null and b/docs/o2/mace.pdf differ diff --git a/docs/o2/vice.pdf b/docs/o2/vice.pdf new file mode 100644 index 00000000..acc91ddc Binary files /dev/null and b/docs/o2/vice.pdf differ diff --git a/rules/rex3/soft-z-lines-zpattern-and-bresround.md b/rules/rex3/soft-z-lines-zpattern-and-bresround.md new file mode 100644 index 00000000..f7bff410 --- /dev/null +++ b/rules/rex3/soft-z-lines-zpattern-and-bresround.md @@ -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. diff --git a/rules/testing/rex3-fline-fractional-bresenham.md b/rules/testing/rex3-fline-fractional-bresenham.md index 9dc64a0b..48627927 100644 --- a/rules/testing/rex3-fline-fractional-bresenham.md +++ b/rules/testing/rex3-fline-fractional-bresenham.md @@ -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()` diff --git a/src/dev/ng1/rex3.rs b/src/dev/ng1/rex3.rs index 3b7ae430..26e8bba9 100644 --- a/src/dev/ng1/rex3.rs +++ b/src/dev/ng1/rex3.rs @@ -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 @@ -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; @@ -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 } @@ -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; @@ -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; diff --git a/src/dev/ng1/rex3_generic.rs b/src/dev/ng1/rex3_generic.rs index ec7ad25b..dec09abd 100644 --- a/src/dev/ng1/rex3_generic.rs +++ b/src/dev/ng1/rex3_generic.rs @@ -1625,83 +1625,72 @@ pub fn draw_span_g(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(fb: &Framebuffers, ctx: &mut Rex3Context, m: &M) { @@ -1778,7 +1767,10 @@ fn draw_line_bresenham( // 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; @@ -1796,9 +1788,12 @@ fn draw_line_bresenham( 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; diff --git a/src/dev/ng1/rex3_jit/compiler.rs b/src/dev/ng1/rex3_jit/compiler.rs index 93de6acb..faf78d44 100644 --- a/src/dev/ng1/rex3_jit/compiler.rs +++ b/src/dev/ng1/rex3_jit/compiler.rs @@ -1601,8 +1601,12 @@ fn emit_shader( b.ins().ishl(shifter, pad_bits) }; - if !stopony { - // Span / block without stopony: primitive done after one row + // A SPAN never steps y: at its end XSTART is left one past XEND and + // YSTART is untouched (rex3.pdf §3.5.3; draw_span; MAME writes back only + // x). Only a BLOCK row end wraps x to XSAVE and steps y. + let is_span = dm0.adrmode() == DRAWMODE0_ADRMODE_SPAN; + if !stopony || is_span { + // Span, or block without stopony: primitive done after one row let zero8 = b.ins().iconst(types::I8, 0); st_bool!(ctx_off!(mid_primitive), zero8); // For HOSTR: flush accumulated shifter to ctx.hostrw before returning. @@ -1613,8 +1617,10 @@ fn emit_shader( if is_hostw || is_hostr { b.ins().store(mem, hostcnt_row_reset, ctx_ptr, ir::immediates::Offset32::new(ctx_off!(hostcnt) as i32)); } - emit_writeback(&mut b, ctx_ptr, &mem, xsave_v, ystart_next, + let (end_x, end_y) = if is_span { (xstart_next, ystart_v) } else { (xsave_v, ystart_next) }; + emit_writeback(&mut b, ctx_ptr, &mem, end_x, end_y, dm0, new_cr, new_cg, new_cb, new_ca, zpat_bit_reset, pat_bit_reset, new_lsmode); + emit_row_end_pattern_reset(&mut b, ctx_ptr, &mem, dm0); b.ins().jump(loop_end, &[]); } else { // Block with stopony: advance y and check y_end @@ -1638,6 +1644,7 @@ fn emit_shader( } emit_writeback(&mut b, ctx_ptr, &mem, xsave_v, ystart_next, dm0, new_cr, new_cg, new_cb, new_ca, zpat_bit_reset, pat_bit_reset, new_lsmode); + emit_row_end_pattern_reset(&mut b, ctx_ptr, &mem, dm0); b.ins().jump(loop_end, &[]); b.switch_to_block(y_cont_block); b.seal_block(y_cont_block); @@ -1659,6 +1666,7 @@ fn emit_shader( b.ins().store(mem, hostcnt_row_reset, ctx_ptr, ir::immediates::Offset32::new(ctx_off!(hostcnt) as i32)); emit_writeback(&mut b, ctx_ptr, &mem, xsave_v, ystart_next, dm0, new_cr, new_cg, new_cb, new_ca, zpat_bit_reset, pat_bit_reset, new_lsmode); + emit_row_end_pattern_reset(&mut b, ctx_ptr, &mem, dm0); b.ins().jump(loop_end, &[]); } else { // Continue loop: reset x to xsave, new row @@ -1866,6 +1874,29 @@ fn emit_draw_iline( b.ins().band_imm_s(shifted, 7) }; + // BRESROUND bit for this octant (BRESRNDINC2[31:24], see BRESROUND_BIT): + // set = a step with d == 0 goes diagonal, clear = straight. The bit + // position per octant is packed one nibble per octant into a constant. + let round_set_v = { + const PACKED: u32 = { + let mut k = 0u32; + let mut o = 0; + while o < 8 { + k |= crate::dev::ng1::rex3::BRESROUND_BIT[o] << (o * 4); + o += 1; + } + k + }; + let packed = b.ins().iconst(types::I32, PACKED as i64); + let sh = b.ins().ishl_imm_s(octant_v, 2); + let bitpos = b.ins().ushr(packed, sh); + let bitpos = b.ins().band_imm_s(bitpos, 0xF); + let rnd = b.ins().ushr_imm_s(bres2_v, 24); + let bit = b.ins().ushr(rnd, bitpos); + let bit = b.ins().band_imm_s(bit, 1); + b.ins().icmp_imm_s(IntCC::NotEqual, bit, 0) + }; + // incr1: 20-bit, always positive let incr1_v = b.ins().band_imm_s(bres_v, 0xFFFFF); @@ -1918,8 +1949,15 @@ fn emit_draw_iline( let is_neg = b.ins().icmp_imm_s(IntCC::SignedLessThan, d, 0); b.ins().select(is_neg, neg, d) }; - let dx_gt_dy = b.ins().icmp(IntCC::SignedGreaterThan, dx_abs, dy_abs); - let major_v = b.ins().select(dx_gt_dy, dx_abs, dy_abs); + // F_LINE/A_LINE: the length along the octant's major axis (their octant + // comes from the fixed-point deltas); I_LINE: the longer integer delta. + let fract = matches!(dm0.adrmode(), DRAWMODE0_ADRMODE_F_LINE | DRAWMODE0_ADRMODE_A_LINE); + let major_v = if fract { + b.ins().select(y_major_v, dy_abs, dx_abs) + } else { + let dx_gt_dy = b.ins().icmp(IntCC::SignedGreaterThan, dx_abs, dy_abs); + b.ins().select(dx_gt_dy, dx_abs, dy_abs) + }; let pixel_count_v = b.ins().iadd_imm_s(major_v, 1); // iterate_one = !stoponx && !stopony (step-mode: always draw exactly 1 pixel) @@ -2393,8 +2431,10 @@ fn emit_draw_iline( // incry2: ±1 based on YDEC (inverted: YDEC=0→-1, YDEC=1→+1) let incry2 = b.ins().select(ydec_v, cp1i, cm1i); - // Choose step 1 or step 2 based on d < 0 - let d_neg = b.ins().icmp_imm_s(IntCC::SignedLessThan, d_v, 0); + // Step 1 (straight) when d < 0, or d <= 0 with BRESROUND clear. + let d_lt = b.ins().icmp_imm_s(IntCC::SignedLessThan, d_v, 0); + let d_le = b.ins().icmp_imm_s(IntCC::SignedLessThanOrEqual, d_v, 0); + let d_neg = b.ins().select(round_set_v, d_lt, d_le); let dx_step = b.ins().select(d_neg, incrx1, incrx2); let dy_step = b.ins().select(d_neg, incry1, incry2); let d_incr = b.ins().select(d_neg, incr1_v, incr2_v); @@ -2586,6 +2626,21 @@ fn emit_logic_op(b: &mut FunctionBuilder, logicop: u32, src: Value, dst: Value) } /// Write back all mutable per-draw state from IR values to ctx. +/// At a row or primitive end the interpreter restarts both pattern bit +/// positions at 31 whether or not this draw uses the patterns (draw_block, +/// draw_span); `emit_writeback` stores only the enabled ones, so store the +/// others here. A later draw that enables a pattern without DOSETUP starts +/// from them. +fn emit_row_end_pattern_reset(b: &mut FunctionBuilder, ctx_ptr: Value, mem: &MemFlagsData, dm0: &Dm0) { + let c31 = b.ins().iconst(types::I8, 31); + if !dm0.enzpattern() { + b.ins().store(*mem, c31, ctx_ptr, ir::immediates::Offset32::new(ctx_off!(zpat_bit) as i32)); + } + if !dm0.enlspattern() { + b.ins().store(*mem, c31, ctx_ptr, ir::immediates::Offset32::new(ctx_off!(pat_bit) as i32)); + } +} + fn emit_writeback( b: &mut FunctionBuilder, ctx_ptr: Value, diff --git a/src/dev/ng1/rex3_rec.rs b/src/dev/ng1/rex3_rec.rs index b4702f6a..f4dfe697 100644 --- a/src/dev/ng1/rex3_rec.rs +++ b/src/dev/ng1/rex3_rec.rs @@ -13,7 +13,9 @@ //! (`REX3_REPLAY_OUT=` saves the final framebuffers there, //! `REX3_REPLAY_TRACE=` writes the annotated trace of the replay, //! `REX3_REPLAY_ENGINE=interp|jit` picks the draw path in `rex-jit` builds, -//! `REX3_REPLAY_NOCHECK=1` skips the checkpoints). +//! `REX3_REPLAY_NOCHECK=1` skips the checkpoints, `REX3_REPLAY_LIMIT=` +//! replays only the first n records, `REX3_REPLAY_STATE=` writes the +//! final register state as TOML). //! //! File: `REX3REC\0` + u32 version, then records, little-endian. Offsets are //! within the REX3's 8 KB window, GO bit (0x800) included: @@ -660,7 +662,11 @@ pub(crate) mod tests { /// Replay a recording file on a fresh board and check every checkpoint. pub fn replay(path: &str, png: Option<&str>) -> Result<(), String> { - let recs = load(path).map_err(|e| format!("{path}: {e}"))?; + let mut recs = load(path).map_err(|e| format!("{path}: {e}"))?; + // REX3_REPLAY_LIMIT=: only the first n records (bisecting). + if let Some(n) = std::env::var("REX3_REPLAY_LIMIT").ok().and_then(|v| v.parse::().ok()) { + recs.truncate(n); + } let jit = match std::env::var("REX3_REPLAY_ENGINE").as_deref() { Ok("interp") => false, Ok("jit") | Err(_) => true, @@ -678,6 +684,15 @@ pub(crate) mod tests { if let Some(p) = png { rex.save_framebuffers_named(std::path::Path::new(p), "replay").map_err(|e| format!("{p}: {e}"))?; } + // REX3_REPLAY_STATE=: the final register state as TOML, to diff + // two replays (engines, or before and after a change). + if let Ok(f) = std::env::var("REX3_REPLAY_STATE") { + let mut st = rex.save_state(); + if let Some(t) = st.as_table_mut() { + t.insert("rec_extra".into(), rex.rec_extra()); + } + std::fs::write(&f, toml::to_string(&st).unwrap_or_default()).map_err(|e| format!("{f}: {e}"))?; + } let h = rex.rec_state_hash(); rex.stop(); eprintln!( diff --git a/src/dev/ng1/rex3_tests.rs b/src/dev/ng1/rex3_tests.rs index c1f2fbe9..eef44c72 100644 --- a/src/dev/ng1/rex3_tests.rs +++ b/src/dev/ng1/rex3_tests.rs @@ -1839,147 +1839,39 @@ fn draw_iline_step(rex: &Rex3, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) -> pts } -/// Reference F_LINE Bresenham in software — a mechanical transcription of -/// `setup()` + `fline_apply_fract()` + the `bres_step!` macro from -/// `draw_line_bresenham` (rex3.rs), NOT the simplified octant-agnostic form -/// `bres_pixels()` uses. This is deliberately duplicated rather than calling -/// the real implementation: a hand-transcribed oracle must fail if -/// `fline_apply_fract`/`setup()` regress, which a shared-code oracle cannot -/// detect (see `rules/` note on this — a call-through oracle would mask -/// exactly the kind of regression this test exists to catch). -/// -/// `x0_frac4`/`y0_frac4` are the *start* endpoint's fractional nibble -/// (0-15, 1/16-pixel units) — the only fractional input `fline_apply_fract` -/// consumes; the end endpoint is integer-only here, matching what the -/// F_LINE hardware path actually uses (draw_aline's AWEIGHT lookup is the -/// only consumer of the *end* endpoint's fraction, handled separately by -/// `aline_endpoint_skip` below). -fn fline_pixels(x0: i32, x0_frac4: i32, y0: i32, y0_frac4: i32, x1: i32, y1: i32) -> Vec<(i32, i32)> { - // BRES table copied verbatim from draw_line_bresenham (rex3.rs): - // (incrx1, incrx2, incry1, incry2, y_major), indexed by octant. - #[rustfmt::skip] - const BRES: [(i32, i32, i32, i32, bool); 8] = [ - ( 0, 1, -1, -1, true ), // octant 0 - ( 0, 1, 1, 1, true ), // octant 1 - ( 0, -1, -1, -1, true ), // octant 2 - ( 0, -1, 1, 1, true ), // octant 3 - ( 1, 1, 0, -1, false), // octant 4 - ( 1, 1, 0, 1, false), // octant 5 - (-1, -1, 0, -1, false), // octant 6 - (-1, -1, 0, 1, false), // octant 7 - ]; - - // 21.11 fixed-point endpoint values, matching how the register writes - // populate ctx.xstart/ystart/xend/yend. - let xstart = (x0 << 11) | (x0_frac4 << 7); - let ystart = (y0 << 11) | (y0_frac4 << 7); - let xend = x1 << 11; - let yend = y1 << 11; - - // --- setup(): derive octant + initial incr1/incr2/d (rex3.rs:1390-1419) --- - let dx = xend - xstart; - let dy = yend - ystart; - let adx = dx.abs() >> 11; - let ady = dy.abs() >> 11; - - let mut octant = 0u32; - if dy < 0 { octant |= 1 << 0; } // OCTANT_YDEC - if dx < 0 { octant |= 1 << 1; } // OCTANT_XDEC - if adx > ady { octant |= 1 << 2; } // OCTANT_XMAJOR - - let (major, minor) = if adx > ady { (adx, ady) } else { (ady, adx) }; - let incr1 = 2 * minor; - let incr2 = 2 * (minor - major); - let mut d = incr1 - major; - - let (incrx1, incrx2, incry1, incry2, y_major) = BRES[(octant & 7) as usize]; - - let mut x = xstart >> 11; - let mut y = ystart >> 11; - let x2 = xend >> 11; - let y2 = yend >> 11; - - // --- fline_apply_fract() (rex3.rs:1754-1816), transcribed verbatim --- - { - let x1p = xstart >> 11; - let y1p = ystart >> 11; - let x2p = xend >> 11; - let y2p = yend >> 11; - let mut fdx = (x1p - x2p).abs(); - let mut fdy = (y1p - y2p).abs(); - let mut xf = (xstart >> 7) & 0xF; - let mut yf = (ystart >> 7) & 0xF; - - match octant & 7 { - 1 => { - std::mem::swap(&mut xf, &mut yf); - std::mem::swap(&mut fdx, &mut fdy); - } - 3 => { - xf = 0x10 - xf; - std::mem::swap(&mut xf, &mut yf); - std::mem::swap(&mut fdx, &mut fdy); - } - 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 fdx, &mut fdy); - } - 0 => { - let t = 0x10 - yf; - yf = xf; - xf = t; - std::mem::swap(&mut fdx, &mut fdy); - } - 4 => { yf = 0x10 - yf; } - _ => {} - } - - // Spec-correct base d for F_LINE/A_LINE is 3*minor - 2*major, not - // I_LINE's 2*minor - major (see rex3.rs fline_apply_fract for the - // full derivation from rex3_pdf.md 3.6.1.2 and MAME's do_fline). - // `d` here still holds the I_LINE-formula value; apply the same - // (minor - major) correction the real fix applies before adding - // the fractional term. - d += fdy - fdx; - d += 2 * (((fdx * yf) >> 4) - ((fdy * xf) >> 4)); - let major_delta = if y_major { fdy } else { fdx }; - let e = d - 2 * major_delta; - if e > 0 { - d = e; - let x_major = !y_major; - if x_major { - y -= incry2; - } else { - x += incrx2; - } - } - } - - // --- step exactly like bres_step! / draw_line_bresenham's main loop --- - let adx2 = (x2 - x).abs(); - let ady2 = (y2 - y).abs(); - let pixel_count = adx2.max(ady2) + 1; - - let mut pts = Vec::new(); - for i in 0..pixel_count { - pts.push((x, y)); - let is_last = i == pixel_count - 1; - if !is_last { - if d < 0 { - x += incrx1; y -= incry1; d += incr1; - } else { - x += incrx2; y -= incry2; d += incr2; - } - } - } - pts +/// Reference F_LINE, from geometry rather than from the implementation: the +/// start point's own pixel, then in each column of the major axis, up to the +/// end point's, the row holding the true line at the column's centre (pixel n +/// covers [n, n+1)), moving at most one row per column. That is the pixel set +/// IRIS GL computes for its software Z buffer (fsn.rec: see +/// rules/rex3/soft-z-lines-zpattern-and-bresround.md). Endpoints in 1/16 +/// pixels. None when the line passes exactly through a row boundary at some +/// column centre: which side wins there is BRESROUND's choice. +fn fline_geom(x1: i32, y1: i32, x2: i32, y2: i32) -> Option> { + let x_major = (x2 - x1).abs() > (y2 - y1).abs(); + // (major, minor) start and end + let (a1, b1, a2, b2) = if x_major { (x1, y1, x2, y2) } else { (y1, x1, y2, x2) }; + let (p1, p2) = (a1 >> 4, a2 >> 4); + let step = if p2 >= p1 { 1 } else { -1 }; + let mut out = vec![(p1, b1 >> 4)]; + let mut p = p1; + while p != p2 { + p += step; + // minor at the column centre, as num / den in 1/16 units + let c = p * 16 + 8; + let (mut num, mut den) = (b1 as i64 * (a2 - a1) as i64 + (c - a1) as i64 * (b2 - b1) as i64, (a2 - a1) as i64); + if den < 0 { num = -num; den = -den; } + if num.rem_euclid(den * 16) == 0 { return None; } + let want = num.div_euclid(den * 16) as i32; + let prev = out.last().unwrap().1; + out.push((p, prev + (want - prev).clamp(-1, 1))); + } + Some(out.into_iter().map(|(a, b)| if x_major { (a, b) } else { (b, a) }).collect()) +} + +/// `fline_geom` for a start point with a fractional nibble and an integer end. +fn fline_pixels(x0: i32, x0_frac4: i32, y0: i32, y0_frac4: i32, x1: i32, y1: i32) -> Option> { + fline_geom(x0 * 16 + x0_frac4, y0 * 16 + y0_frac4, x1 * 16, y1 * 16) } /// Reference A_LINE endpoint-suppression decision — mirrors `draw_aline`'s @@ -2354,8 +2246,10 @@ fn test_fline_all_octants_half_pixel() { } } } - let expected: HashSet<(i32,i32)> = - fline_pixels(cx, cx_frac, cy, cy_frac, x1, y1).into_iter().collect(); + // A line through a pixel-row boundary is BRESROUND's call, not + // geometry's. + let Some(expected) = fline_pixels(cx, cx_frac, cy, cy_frac, x1, y1) else { continue }; + let expected: HashSet<(i32,i32)> = expected.into_iter().collect(); assert_eq!(pts, expected, "fline half-pixel octant test deg={deg}: ({cx}.5,{cy}.5)->({x1},{y1}): got {:?} expected {:?}", @@ -2408,8 +2302,11 @@ fn test_fline_quarter_pixel_offsets() { } } } - let expected: HashSet<(i32,i32)> = - fline_pixels(cx, frac4, cy, frac4, x1, y1).into_iter().collect(); + let Some(expected) = fline_pixels(cx, frac4, cy, frac4, x1, y1) else { + color = color.wrapping_add(1).max(1); + continue; + }; + let expected: HashSet<(i32,i32)> = expected.into_iter().collect(); assert_eq!(pts, expected, "fline frac4={frac4} deg={deg}: ({cx}+{frac4}/16,{cy}+{frac4}/16)->({x1},{y1}): \ @@ -7619,3 +7516,242 @@ fn test_hostw_full_block_replicates_single_word() { assert_eq!(read_pixel(&rex, x1 + 1, y0) & 0xFFFFFF, 0, "spilled past XEND"); assert_eq!(read_pixel(&rex, x0, y1 + 1) & 0xFFFFFF, 0, "spilled past YEND"); } + +/// IRIS GL's software-Z lines (fsn's label text, blast's HUD): each short +/// stroke is an F_LINE without DOSETUP, set up by a SETUP write, drawn by a +/// ZPATTERN GO whose mask is MSB-aligned (0xe0000000 = three pixels). The +/// pattern is a recirculating register, "msb = first pixel": a ZPATTERN +/// write reloads it, so every stroke starts at bit 31. When the bit position +/// carried over from the previous stroke instead, the second stroke tested +/// bits 28.. of its mask and came out partly or wholly missing. +#[test] +fn test_zpattern_write_restarts_pattern_for_soft_z_lines() { + const DM0_FLINE_SOFTZ: u32 = 0x0000_930e; // DRAW FLINE STOPONX STOPONY ENZPAT LEN32 + + let rex = &make_rex3(); + rex3init(rex); + reg(rex, REX3_DRAWMODE1, DM1_CI8_SRC); + reg(rex, REX3_WRMASK, 0xFF); + + // (x0, y0, x1, y1, mask), as fsn sends them. + let strokes = [ + (10, 10, 12, 10, 0xe000_0000u32), + (10, 20, 13, 20, 0xf000_0000), + (10, 30, 11, 30, 0xc000_0000), + (10, 40, 10, 43, 0xf000_0000), + ]; + for (i, &(x0, y0, x1, y1, mask)) in strokes.iter().enumerate() { + reg(rex, REX3_COLORI, 1 + i as u32); + write_xstartf(rex, x0, 0); + write_ystartf(rex, y0, 0); + write_xendf(rex, x1, 0); + write_yendf(rex, y1, 0); + reg(rex, REX3_DRAWMODE0, DM0_FLINE_SOFTZ); + reg(rex, REX3_SETUP, 0); + reg_go(rex, REX3_ZPATTERN, mask); + } + for (i, &(x0, y0, x1, y1, _)) in strokes.iter().enumerate() { + for x in x0..=x1 { + for y in y0..=y1 { + assert_eq!(read_pixel(rex, x, y) & 0xFF, 1 + i as u32, + "stroke {i} pixel ({x}, {y}) missing: its ZPATTERN mask was tested from the wrong bit"); + } + } + } +} + +/// BRESROUND (BRESRNDINC2[31:24], one bit per octant) picks which way a +/// Bresenham step with d == 0 goes: set = diagonal, clear = straight. A 2x1 +/// I_LINE starts at d = 2*1 - 2 = 0, so its middle pixel shows the choice. +/// IRIS GL draws with BRESROUND = 0, the X server with 0xff. Both engines. +#[test] +fn test_bresround_decides_the_d_zero_step() { + const DM0_DRAW_ILINE_XY: u32 = DRAWMODE0_OPCODE_DRAW | DRAWMODE0_ADRMODE_I_LINE_SH | DM0_DOSETUP | DM0_STOPONXY; + let mut cases = vec![(false, 0x00u32, 10), (false, 0xff, 11)]; + if cfg!(feature = "rex-jit") { + cases.push((true, 0x00, 10)); + cases.push((true, 0xff, 11)); + } + for (jit, rnd, middle_row) in cases { + let rex = &make_rex3(); + #[cfg(feature = "rex-jit")] + rex.jit_enabled.store(jit, std::sync::atomic::Ordering::Relaxed); + rex3init(rex); + reg(rex, REX3_DRAWMODE1, DM1_CI8_SRC); + reg(rex, REX3_WRMASK, 0xFF); + reg(rex, REX3_COLORI, 5); + reg(rex, REX3_BRESRNDINC2, rnd << 24); + #[cfg(feature = "rex-jit")] + let before = rex.jit_go_count.load(std::sync::atomic::Ordering::Relaxed); + #[cfg(feature = "rex-jit")] + if jit { + let j = rex.rex_jit.as_ref().unwrap(); + let dm1 = crate::dev::ng1::rex3_shape::normalize_dm1(DM1_CI8_SRC, DRAWMODE0_OPCODE_DRAW); + let cm = unsafe { (*rex.context.get()).clipmode } & CLIPMODE_JIT_KEY_MASK; + j.request_compile(DM0_DRAW_ILINE_XY, dm1, cm); + assert!(j.wait_compiled(DM0_DRAW_ILINE_XY, dm1, cm)); + } + reg(rex, REX3_XYENDI, xy(12, 11)); + reg(rex, REX3_XYSTARTI, xy(10, 10)); + reg_go(rex, REX3_DRAWMODE0, DM0_DRAW_ILINE_XY); + #[cfg(feature = "rex-jit")] + assert_eq!(rex.jit_go_count.load(std::sync::atomic::Ordering::Relaxed) > before, jit, + "jit={jit}: the line ran on the wrong engine"); + for (x, y) in [(10, 10), (11, middle_row), (12, 11)] { + assert_eq!(read_pixel(rex, x, y) & 0xFF, 5, + "jit={jit} BRESROUND={rnd:#x}: pixel ({x}, {y}) not drawn"); + } + } +} + +/// F_LINE with both endpoints fractional, every direction, lengths like +/// stroke text (0-12 pixels): the pixels must be `fline_geom`'s. Deterministic +/// pseudo-random endpoints; lines through a row boundary are skipped. +#[test] +fn test_fline_fractional_both_ends_matches_geometry() { + fline_geometry_sweep(false); + #[cfg(feature = "rex-jit")] + fline_geometry_sweep(true); +} + +fn fline_geometry_sweep(jit: bool) { + let rex = &make_rex3(); + #[cfg(feature = "rex-jit")] + rex.jit_enabled.store(jit, std::sync::atomic::Ordering::Relaxed); + rex3init(rex); + reg(rex, REX3_DRAWMODE1, DM1_CI8_SRC); + reg(rex, REX3_WRMASK, 0xFF); + #[cfg(feature = "rex-jit")] + if jit { + let j = rex.rex_jit.as_ref().unwrap(); + let dm1 = crate::dev::ng1::rex3_shape::normalize_dm1(DM1_CI8_SRC, DRAWMODE0_OPCODE_DRAW); + let cm = unsafe { (*rex.context.get()).clipmode } & CLIPMODE_JIT_KEY_MASK; + j.request_compile(DM0_DRAW_FLINE, dm1, cm); + assert!(j.wait_compiled(DM0_DRAW_FLINE, dm1, cm)); + } + #[cfg(feature = "rex-jit")] + let before = rex.jit_go_count.load(std::sync::atomic::Ordering::Relaxed); + let mut seed = 0x1234_5678u32; + let mut rnd = |n: u32| { seed ^= seed << 13; seed ^= seed >> 17; seed ^= seed << 5; seed % n }; + let (c, pad) = (100i32, 16i32); + let (mut checked, mut ties) = (0, 0); + for _ in 0..2000 { + let x1 = c * 16 + rnd(16) as i32; + let y1 = c * 16 + rnd(16) as i32; + let x2 = x1 + rnd(25 * 16) as i32 - 12 * 16; + let y2 = y1 + rnd(25 * 16) as i32 - 12 * 16; + let Some(want) = fline_geom(x1, y1, x2, y2) else { ties += 1; continue }; + reg(rex, REX3_COLORI, 0); + reg(rex, REX3_XYENDI, xy(c + pad, c + pad)); + reg(rex, REX3_XYSTARTI, xy(c - pad, c - pad)); + // DOSETUP: without it the block steps in the last line's octant. + reg_go(rex, REX3_DRAWMODE0, DM0_DRAW_BLOCK | DM0_DOSETUP); + reg(rex, REX3_COLORI, 7); + write_xstartf(rex, x1 >> 4, x1 & 15); + write_ystartf(rex, y1 >> 4, y1 & 15); + write_xendf(rex, x2 >> 4, x2 & 15); + write_yendf(rex, y2 >> 4, y2 & 15); + reg_go(rex, REX3_DRAWMODE0, DM0_DRAW_FLINE); + let mut got = HashSet::new(); + for y in c - pad..=c + pad { + for x in c - pad..=c + pad { + if read_pixel(rex, x, y) & 0xFF == 7 { got.insert((x, y)); } + } + } + let want: HashSet<(i32, i32)> = want.into_iter().collect(); + assert_eq!(got, want, "jit={jit}: F_LINE ({x1}/16, {y1}/16) -> ({x2}/16, {y2}/16)"); + checked += 1; + } + assert!(checked > 1500, "only {checked} lines checked ({ties} ties)"); + #[cfg(feature = "rex-jit")] + assert_eq!(rex.jit_go_count.load(std::sync::atomic::Ordering::Relaxed) > before, jit, + "jit={jit}: the lines ran on the wrong engine"); + let _ = jit; +} + +/// One of fsn's label strokes (the left of an 'a', from fns.log): y-major, +/// x and y increasing, (26052, 29923) -> (26094, 29976) in 1/16 pixels. +/// IRIS GL's software Z buffer expects x = 1628, 1629, 1630, 1630; with the +/// y fraction mirrored the wrong way round it came out 1628, 1628, 1629, 1630 +/// and the spotlight drawn afterwards left the expected pixels unpainted. +/// Two more from fsn.rec, where a first-pixel adjustment (the spec's E test, +/// or E/G) moved the start off the start point's own pixel. +#[test] +fn test_fline_fsn_label_stroke() { + let rex = &make_rex3(); + rex3init(rex); + reg(rex, REX3_DRAWMODE1, DM1_CI8_SRC); + reg(rex, REX3_WRMASK, 0xFF); + reg(rex, REX3_COLORI, 9); + // Same place, shifted into the visible framebuffer. + let (x1, y1, x2, y2) = (26052 - 1500 * 16, 29923 - 1800 * 16, 26094 - 1500 * 16, 29976 - 1800 * 16); + write_xstartf(rex, x1 >> 4, x1 & 15); + write_ystartf(rex, y1 >> 4, y1 & 15); + write_xendf(rex, x2 >> 4, x2 & 15); + write_yendf(rex, y2 >> 4, y2 & 15); + reg_go(rex, REX3_DRAWMODE0, DM0_DRAW_FLINE); + for (x, y) in [(128, 70), (129, 71), (130, 72), (130, 73)] { + assert_eq!(read_pixel(rex, x, y) & 0xFF, 9, "pixel ({x}, {y}) not drawn"); + } + assert_eq!(read_pixel(rex, 128, 71) & 0xFF, 0, "(128, 71) drawn: y fraction mirrored the wrong way"); + + // fsn.rec: (27278, 22016) -> (30550, 31190), shifted by (-1600, -1300) + // pixels. Starts at x = 1704.875: its first pixel is column 104, where + // the start point is; an E/G adjustment moved it to 105. + reg(rex, REX3_COLORI, 11); + let (x1, y1, x2, y2) = (27278 - 1600 * 16, 22016 - 1300 * 16, 30550 - 1600 * 16, 31190 - 1300 * 16); + write_xstartf(rex, x1 >> 4, x1 & 15); + write_ystartf(rex, y1 >> 4, y1 & 15); + write_xendf(rex, x2 >> 4, x2 & 15); + write_yendf(rex, y2 >> 4, y2 & 15); + reg_go(rex, REX3_DRAWMODE0, DM0_DRAW_FLINE); + assert_eq!(read_pixel(rex, 104, 76) & 0xFF, 11, "first pixel is not the start point's own"); + assert_eq!(read_pixel(rex, 105, 76) & 0xFF, 0, "first pixel moved off the start point's pixel"); +} + +/// A SPAN never steps y: once it reaches XEND, XSTART is left one past it and +/// YSTART is unchanged (rex3.pdf §3.5.3), and both pattern bit positions +/// restart at 31. The JIT used to end a span like a block row (x back to +/// XSAVE, y + 1), and left a disabled pattern's bit position stale; IRIS GL +/// reads XSTART/YSTART back (fsn.rec). fsn's soft-Z span: two LEN32 GOs. +#[test] +fn test_span_end_leaves_y_and_x_past_end() { + const DM0_SPAN_SOFTZ: u32 = 0x0008_9102; // DRAW SPAN STOPONX ENZPAT LEN32 LRONLY + let engines: &[bool] = if cfg!(feature = "rex-jit") { &[false, true] } else { &[false] }; + for &jit in engines { + let rex = &make_rex3(); + #[cfg(feature = "rex-jit")] + rex.jit_enabled.store(jit, std::sync::atomic::Ordering::Relaxed); + rex3init(rex); + reg(rex, REX3_DRAWMODE1, DM1_CI8_SRC); + reg(rex, REX3_WRMASK, 0xFF); + reg(rex, REX3_COLORI, 3); + #[cfg(feature = "rex-jit")] + if jit { + let j = rex.rex_jit.as_ref().unwrap(); + let dm1 = crate::dev::ng1::rex3_shape::normalize_dm1(DM1_CI8_SRC, DRAWMODE0_OPCODE_DRAW); + let cm = unsafe { (*rex.context.get()).clipmode } & CLIPMODE_JIT_KEY_MASK; + j.request_compile(DM0_SPAN_SOFTZ, dm1, cm); + assert!(j.wait_compiled(DM0_SPAN_SOFTZ, dm1, cm)); + } + #[cfg(feature = "rex-jit")] + let before = rex.jit_go_count.load(std::sync::atomic::Ordering::Relaxed); + // 46 pixels from x = 10 on row 20: a 32-pixel GO, then 14. + reg(rex, REX3_XYENDI, xy(55, 20)); + reg(rex, REX3_XYSTARTI, xy(10, 20)); + reg(rex, REX3_DRAWMODE0, DM0_SPAN_SOFTZ); + reg_go(rex, REX3_ZPATTERN, 0xffff_ffff); + reg_go(rex, REX3_ZPATTERN, 0); + #[cfg(feature = "rex-jit")] + assert_eq!(rex.jit_go_count.load(std::sync::atomic::Ordering::Relaxed) > before, jit, + "jit={jit}: the span ran on the wrong engine"); + let ctx = unsafe { &*rex.context.get() }; + let (x, y) = ((ctx.xstart >> 11) - REX3_COORD_BIAS, (ctx.ystart >> 11) - REX3_COORD_BIAS); + assert_eq!((x, y), (56, 20), "jit={jit}: span end left XSTART/YSTART at ({x}, {y})"); + assert_eq!((ctx.zpat_bit, ctx.pat_bit), (31, 31), "jit={jit}: pattern bits not restarted"); + for px in 10..42 { + assert_eq!(read_pixel(rex, px, 20) & 0xFF, 3, "jit={jit}: ({px}, 20) not drawn"); + } + assert_eq!(read_pixel(rex, 42, 20) & 0xFF, 0, "jit={jit}: masked chunk drew"); + } +} diff --git a/tools/rex3-rec.py b/tools/rex3-rec.py new file mode 100755 index 00000000..9b7e6c18 --- /dev/null +++ b/tools/rex3-rec.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Inspect and convert REX3 recordings (src/dev/ng1/rex3_rec.rs). + + rex3-rec.py dump IN.rec [--from N] [--to M] + records [N, M) with register names, numbered from 1 as replay counts + them (REX3_REPLAY_LIMIT=M replays exactly the first M) + rex3-rec.py text IN.rec OUT.txt [--from N] [--to M] [--no-hash] [--no-state] + write records [N, M) in the text form, for testdata/. A slice that + does not start at record 1 has no start state, so its checkpoints + cannot match a power-on board: use --no-hash (and replay it with + REX3_REPLAY_NOCHECK=1 or assert on pixels instead) + rex3-rec.py summary IN.rec + record counts by kind and the busiest registers + rex3-rec.py from-trace TRACE.log OUT.txt + a `rex trace` turned into a text recording: its GF lines (GFIFO + register writes, in order) as bus writes. It has no start state and no + checkpoints, DMA batch payloads are not in a trace (those entries are + skipped), and CPU-side DCB writes are left out (their access width is + not traced). Enough to replay a trace's drawing offline: + REX3_REPLAY=OUT.txt REX3_REPLAY_NOCHECK=1 REX3_REPLAY_OUT=dir \\ + cargo test --release --features gputrace --lib -- rex3_replay_file +""" +import argparse +import collections +import os +import re +import struct +import sys + +MAGIC = b'REX3REC\0' +REX3_RS = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'src', 'dev', 'ng1', 'rex3.rs') + + +def reg_names(): + """offset -> name, from rex3.rs's constants and rex3_reg_name().""" + src = open(REX3_RS).read() + consts = {c: int(v, 16) for c, v in re.findall(r'pub const (REX3_\w+): u32 = (0x[0-9A-Fa-f]+);', src)} + names = {} + for c, n in re.findall(r'(REX3_\w+) => "(\w+)",', src): + if c in consts: + names[consts[c]] = n + return names + + +def records(path): + """Yield (kind, fields) per record; fields as in the text form.""" + data = open(path, 'rb').read() + if not data.startswith(MAGIC): + for line in data.decode().splitlines(): + line = line.split('#')[0].strip() + if line: + f = line.split() + yield f[0], f[1:] + return + i = 12 + while i < len(data): + t = chr(data[i]) + try: + if t in 'WR': + bits = data[i + 1] + off, = struct.unpack_from(' 32: + val, = struct.unpack_from(' len(data): + return + yield t, fields + i += n + + +def describe(kind, f, names): + if kind in 'WR': + bits, off, val = int(f[0]), int(f[1], 16), int(f[2], 16) + go = ' GO' if off & 0x800 else '' + name = names.get(off & ~0x800, f'[{off & ~0x800:#06x}]') + return f'{kind} {bits:2} {name}{go} = {val:#x}' + if kind == 'D': + return f'D {names.get(int(f[0], 16) & ~0x800, f[0])} -> {int(f[1], 16):#018x}' + if kind in 'Bb': + n = int(f[1], 16) + sample = ' '.join(f[2:6]) + (' ...' if n > 4 else '') + return f'{kind} {n} qwords {"to" if kind == "B" else "from"} {names.get(int(f[0], 16) & ~0x800, f[0])}: {sample}' + if kind == 'H': + return f'H checkpoint {f[0][:16]}' + if kind == 'S': + return f'S start state ({len(f[0]) // 2} bytes)' + return kind + ' ' + ' '.join(f) + + +def in_range(k, a): + return k >= a.from_ and (a.to is None or k < a.to) + + +def cmd_dump(a): + names = reg_names() + for k, (kind, f) in enumerate(records(a.input), 1): + if a.to is not None and k >= a.to: + break + if in_range(k, a): + print(k, describe(kind, f, names)) + + +def cmd_text(a): + with open(a.output, 'w') as out: + out.write(f'# REX3 recording: {os.path.basename(a.input)} records [{a.from_}, {a.to or "end"})\n') + for k, (kind, f) in enumerate(records(a.input), 1): + if a.to is not None and k >= a.to: + break + if not in_range(k, a) or (a.no_hash and kind == 'H') or (a.no_state and kind == 'S'): + continue + out.write(' '.join([kind] + f) + '\n') + + +def cmd_summary(a): + names = reg_names() + kinds = collections.Counter() + regs = collections.Counter() + for kind, f in records(a.input): + kinds[kind] += 1 + if kind in 'WR': + off = int(f[1], 16) + regs[f"{kind} {names.get(off & ~0x800, hex(off & ~0x800))}{' GO' if off & 0x800 else ''}"] += 1 + print('records:', sum(kinds.values()), dict(kinds)) + for r, n in regs.most_common(20): + print(f' {n:9} {r}') + + +def cmd_from_trace(a): + names = {v: k for k, v in reg_names().items()} + line_re = re.compile(r'GF #\d+ (\w+)( GO)? = (0x[0-9a-f]+)') + n = skipped = 0 + with open(a.output, 'w') as out: + out.write(f'# REX3 recording from the GF lines of {os.path.basename(a.trace)} (rex3-rec.py from-trace)\n') + for line in open(a.trace): + m = line_re.search(line) + if not m: + continue + name, go, val = m[1], m[2], int(m[3], 16) + off = names.get(name) + if off is None or name.startswith('DMA_'): + skipped += 1 + continue + off |= 0x800 if go else 0 + if name == 'HOSTRW64': + out.write(f'W 64 {off & ~1:x} {val:x}\n') + else: + out.write(f'W 32 {off:x} {val:x}\n') + n += 1 + print(f'{n} writes, {skipped} skipped', file=sys.stderr) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = ap.add_subparsers(dest='cmd', required=True) + for name in ('dump', 'text'): + p = sub.add_parser(name) + p.add_argument('input') + if name == 'text': + p.add_argument('output') + p.add_argument('--no-hash', action='store_true') + p.add_argument('--no-state', action='store_true') + p.add_argument('--from', dest='from_', type=int, default=1) + p.add_argument('--to', type=int) + p = sub.add_parser('summary') + p.add_argument('input') + p = sub.add_parser('from-trace') + p.add_argument('trace') + p.add_argument('output') + a = ap.parse_args() + {'dump': cmd_dump, 'text': cmd_text, 'summary': cmd_summary, 'from-trace': cmd_from_trace}[a.cmd](a) + + +if __name__ == '__main__': + main()