diff --git a/src/rex3.rs b/src/rex3.rs index 488b2fcb..45531e29 100644 --- a/src/rex3.rs +++ b/src/rex3.rs @@ -3,7 +3,7 @@ use parking_lot::Mutex; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use std::thread; use crossbeam_utils::CachePadded; -use crate::traits::{BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, BUS_ERR, BusDevice, Device, Resettable, Saveable}; +use crate::traits::{BusRead8, BusRead16, BusRead32, BusRead64, BUS_OK, BUS_ERR, BUS_BUSY, BusDevice, Device, Resettable, Saveable}; use crate::devlog::{LogModule, devlog_is_active, devlog}; use crate::snapshot::{get_field, u32_slice_to_toml, u16_slice_to_toml, u8_slice_to_toml, load_u32_slice, load_u16_slice, load_u8_slice, toml_u32, toml_u64, toml_u8, hex_u32, hex_u64, hex_u8}; use std::cell::{Cell, UnsafeCell}; @@ -1015,26 +1015,40 @@ impl GFifo { tail.wrapping_sub(head) & GFIFO_MASK } - /// Push an entry. Spins if full. Safe to call from multiple producers concurrently. + /// Try to push an entry without blocking. Returns `false` if another + /// producer holds the lock or the queue is full — the caller should report + /// back-pressure and retry rather than spin here. + /// + /// Spinning inside the CPU's store path is what this exists to avoid. That + /// spin runs with no interrupt servicing, so a sustained full queue starves + /// IP7 delivery — and because the guest's own clock is driven by IP7, it + /// also *dilates guest time*: wall-clock advances while guest-visible time + /// does not. Any guest-side benchmark then reports inflated throughput, and + /// inflated most for whatever configuration spins most. Returning `false` + /// lets the bus write report `BUS_BUSY` (== `EXEC_RETRY`), so the CPU leaves + /// the store, re-enters `step()` — sampling interrupts in `step_preamble!` + /// — and re-dispatches the same instruction. Nothing is lost by not making + /// progress here: if the queue is full the CPU cannot retire this store + /// anyway. #[inline] - pub fn push(&self, addr: u32, val: u64) { - // Acquire the spinlock — uncontested in the common case (one active producer). - while self.lock.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() { - while self.lock.load(Ordering::Relaxed) { - std::hint::spin_loop(); - } + pub fn try_push(&self, addr: u32, val: u64) -> bool { + // Acquire the spinlock — uncontested in the common case (one active + // producer: IRIX only drives DMA for pixmap blits, never while the CPU + // is writing REX3 registers), so a failure here is rare. + if self.lock.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() { + return false; } - // Spin if full — consumer will drain it. let tail = self.tail.load(Ordering::Relaxed); let next_tail = tail.wrapping_add(1) & GFIFO_MASK; let mut cached_head = self.shadow_head.get(); if next_tail == cached_head { cached_head = self.head.load(Ordering::Acquire); self.shadow_head.set(cached_head); - while next_tail == cached_head { - std::hint::spin_loop(); - cached_head = self.head.load(Ordering::Acquire); - self.shadow_head.set(cached_head); + if next_tail == cached_head { + // Full — release the lock and let the caller retry once the + // consumer has drained something. + self.lock.store(false, Ordering::Release); + return false; } } // SAFETY: we hold the lock; no other producer touches this slot. @@ -1046,6 +1060,20 @@ impl GFifo { // Release: consumer's Acquire on tail sees the slot write above. self.tail.store(next_tail, Ordering::Release); self.lock.store(false, Ordering::Release); + true + } + + /// Push an entry, spinning until it fits. Safe to call from multiple + /// producers concurrently. + /// + /// For callers with no way to report back-pressure: shutdown sentinels, and + /// MC's VDMA worker thread, which has no EXEC_RETRY mechanism of its own. + /// The CPU store path uses `try_push` instead — see its doc comment. + #[inline] + pub fn push(&self, addr: u32, val: u64) { + while !self.try_push(addr, val) { + std::hint::spin_loop(); + } } /// Peek at the next entry without advancing head. Returns `None` if empty. @@ -3409,6 +3437,34 @@ impl Rex3 { } } + /// Non-blocking `gfifo_push`: returns `false` when the queue is full or a + /// producer holds the lock, so a bus write can report `BUS_BUSY` + /// (== `EXEC_RETRY`) instead of spinning with interrupts unserviced. + /// + /// Only safe for callers that commit no other state first: the CPU + /// re-executes the entire store on retry, so anything done beforehand would + /// be applied twice. + #[must_use] + fn gfifo_try_push(&self, addr: u32, val: u64) -> bool { + #[cfg(feature = "developer")] + { + let len = self.gfifo.len() + 1; + let _ = self.gfifo_hwm.try_update(Ordering::Relaxed, Ordering::Relaxed, |hwm| { + if len > hwm { Some(len) } else { None } + }); + } + if !self.gfifo.try_push(addr, val) { + return false; + } + #[cfg(feature = "idle-pause")] + if self.processor_parked.load(Ordering::Acquire) { + if let Some(t) = self.processor_unparker.get() { + t.unpark(); + } + } + true + } + fn wait_idle(&self) { loop { // Acquire load: when gfxbusy goes false, all execute_go() writes become visible. @@ -5184,6 +5240,11 @@ impl BusDevice for Rex3 { } } + // Blocking push, deliberately: `result` is already computed above and a + // HOSTRW read has already called note_hostrw_read(), advancing the + // read-then-advance pipeline. Returning BUS_BUSY here would re-run that + // on retry. Unlike write32's default arm, this path cannot be made + // retryable without moving the push ahead of the read side effects. if is_go { self.gfifo_push(GFIFO_PURE_GO, 0); } result } @@ -5281,7 +5342,15 @@ impl BusDevice for Rex3 { } REX3_DCBRESET => { *self.dcb.lock() = Rex3DcbState::default(); } _ => { - self.gfifo_push(offset, val as u64); + // The push is this write's ONLY effect, so a full queue can + // safely report BUS_BUSY (== EXEC_RETRY): the CPU re-executes + // the store from scratch, having sampled interrupts in + // step_preamble!, and nothing was half-applied. Spinning here + // would starve IP7 — and with it the guest clock — for as long + // as the queue stays full. + if !self.gfifo_try_push(offset, val as u64) { + return BUS_BUSY; + } return BUS_OK; } } @@ -5363,7 +5432,11 @@ impl BusDevice for Rex3 { if reg_offset64 == REX3_HOSTRW0 { // Encode as REX3_HOSTRW64 (0x0231) + GO bit if present. // addr bit 0 = is_64bit, bit 11 = GO. - self.gfifo_push(REX3_HOSTRW64 | (offset & 0x0800), val); + // Sole effect of this write, so a full queue reports BUS_BUSY and + // the CPU retries the store (see write32's default arm). + if !self.gfifo_try_push(REX3_HOSTRW64 | (offset & 0x0800), val) { + return BUS_BUSY; + } return BUS_OK; } diff --git a/src/rex3_tests.rs b/src/rex3_tests.rs index 020ff49b..f5889f78 100644 --- a/src/rex3_tests.rs +++ b/src/rex3_tests.rs @@ -3160,6 +3160,232 @@ mod jit_tests { ); } + /// GFIFO pressure sweep: how much can we draw per second, as primitives shrink? + /// + /// `jit_timing_shade_scanlines_fullscreen` already goes through the GFIFO + /// (`reg()` calls `rex.write32()`, the real bus entry point), but it draws + /// 1280-pixel scanlines from 5 register writes -- about 0.004 queue entries + /// per pixel. Guest GL drawing small triangles is nothing like that: tens of + /// entries per primitive covering ~32 pixels, call it 1 entry/pixel, some + /// 250x denser. So the fullscreen number says the queue is fast at *low* + /// entry density and nothing about high density. + /// + /// This runs each configuration for a fixed wall-clock budget and reports + /// how much it managed, rather than timing a fixed amount of work: at + /// ~2000 Mpx/s a fixed-work run finishes in milliseconds and measures + /// mostly noise. Short spans mean more GOs and more register writes for the + /// same fill, so the Mpx/s curve across span lengths isolates what queue + /// traffic costs. Flat means the queue is free at any density; collapsing + /// means per-entry cost dominates once primitives get small -- the regime + /// real GL content lives in. + /// + /// Both plain Gouraud and ZPATTERN-masked spans are measured. ZPATTERN is + /// Indy's depth path (the GL driver compares in software and hands REX3 a + /// 32-bit coverage mask per 32-pixel span), so it costs an extra register + /// write per span *and* a per-pixel mask test -- exactly what depth-tested + /// content pays, and the guest-side numbers show depth is expensive. + /// + /// Not an assertion test: it prints a table. Run with + /// `cargo test --release --features rex-jit gfifo_pressure_sweep -- --nocapture` + /// (add `--ignored`; it is ignored by default since it burns real seconds). + #[test] + #[ignore = "benchmark: runs for several seconds of wall clock"] + fn gfifo_pressure_sweep() { + /// Minimum wall-clock per sample. The loop runs whole batches and stops + /// once this has elapsed, so a sample is always *at least* this long and + /// usually a little over — which is why every rate below divides the + /// pixels actually drawn by the nanoseconds actually measured, never by + /// an assumed budget. + const BUDGET: std::time::Duration = std::time::Duration::from_millis(1000); + /// Samples per cell; the median is reported, with min/max as spread. + /// Three at >=1s each, rather than one: the short-span rows varied 2.6x + /// run to run on single samples (span 32 read 163 / 429 / 229 Mpx/s), + /// and one number gives the reader no way to see that. + const SAMPLES: usize = 3; + const SPAN_LENS: [i32; 6] = [1280, 256, 64, 32, 16, 8]; + /// Spans per timing check — checking the clock every span would itself + /// cost more than the draw at short lengths. + const BATCH: u64 = 256; + + let dm1 = DM1_RGB24_SRC; + let dm0_plain = DM0_DRAW_SPAN | (1 << 18); // shade + stoponx + // Depth mode as the GL driver actually drives it: ENZPATTERN (bit 12) + // for the coverage mask AND LENGTH32 (bit 15), which hard-caps the draw + // at 32 pixels (see execute_go's `length32 && pixel_count > 32`). That + // cap is why ZPATTERN is a *fixed* 32-pixel row below rather than part + // of the span sweep: a longer span would not draw longer, it would just + // recycle the same 32-bit mask over pixels it never reaches. + let dm0_zpat = DM0_DRAW_SPAN | (1 << 18) | (1 << 12) | (1 << 15); + + // Draw spans of `len` pixels for BUDGET, through the GFIFO. + // Returns (pixels drawn, queue entries pushed, elapsed nanos). + let run = |rex: &Rex3, len: i32, zpat: bool| -> (u64, u64, u64) { + reg(rex, REX3_DRAWMODE0, if zpat { dm0_zpat } else { dm0_plain }); + reg(rex, REX3_DRAWMODE1, dm1); + reg(rex, REX3_WRMASK, 0xFFFFFF); + reg(rex, REX3_SLOPERED, 2u32 << 11); + reg(rex, REX3_SLOPEGRN, 1u32 << 11); + reg(rex, REX3_SLOPEBLUE, 0); + + // 5 writes + 1 GO per span, plus the ZPATTERN mask when enabled -- + // the extra queue entry per span that depth content actually pays. + let per_span = if zpat { 7 } else { 6 }; + let mut spans = 0u64; + let start = std::time::Instant::now(); + loop { + for _ in 0..BATCH { + let i = spans; + // Walk across the framebuffer so successive draws touch + // different lines rather than rewriting one hot row. + let y = (i % 1024) as i32; + let x0 = ((i / 1024) as i32 * len) % (1280 - len).max(1); + if zpat { + // A fresh mask per span, as the GL driver emits after + // each 32-pixel software depth compare. Varying it (not + // a constant) keeps the per-pixel test honest and stops + // the value being hoisted; the alternating-ish patterns + // reject roughly half the pixels. + reg(rex, REX3_ZPATTERN, (0xAAAA_AAAAu32 ^ (i as u32).wrapping_mul(2654435761))); + } + reg(rex, REX3_COLORRED, ((i * 7 % 200) as u32) << 11); + reg(rex, REX3_COLORGRN, ((i * 3 % 180) as u32) << 11); + reg(rex, REX3_COLORBLUE, ((i * 5 % 160) as u32) << 11); + reg(rex, REX3_XYENDI, xy(x0 + len - 1, y)); + rex.write32(go_addr(REX3_XYSTARTI), xy(x0, y)); + spans += 1; + } + if start.elapsed() >= BUDGET { break; } + } + rex.wait_idle(); + let ns = start.elapsed().as_nanos() as u64; + // LENGTH32 caps the draw at 32 pixels however long the span is, so + // count what was actually rasterized, not what was requested. + let drawn = if zpat { len.min(32) } else { len } as u64; + (spans * drawn, spans * per_span, ns) + }; + + let rex_interp = make_rex3(); + rex3init(rex_interp); + let rex_jit = make_rex3_jit(); + rex3init(rex_jit); + + // Force both shader variants compiled before timing. + for dm0 in [dm0_plain, dm0_zpat] { + reg(rex_jit, REX3_DRAWMODE0, dm0); + reg(rex_jit, REX3_DRAWMODE1, dm1); + reg(rex_jit, REX3_XYENDI, xy(63, 0)); + reg_go(rex_jit, REX3_XYSTARTI, xy(0, 0)); + if let Some(ref jit) = rex_jit.rex_jit { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + while !jit.compiled_pairs().contains(&(dm0, dm1, 0)) { + assert!(std::time::Instant::now() < deadline, + "JIT compile timed out for dm0={dm0:#010x}"); + jit.request_compile(dm0, dm1, 0); + std::thread::sleep(std::time::Duration::from_millis(5)); + } + } + } + + // Median of `SAMPLES` runs, plus the observed min/max, in Mpx/s. + let sample = |rex: &Rex3, len: i32, zpat: bool| -> (u64, u64, u64, u64, u64, u64) { + let mut rates = Vec::with_capacity(SAMPLES); + let mut entries = 0u64; + let mut last_ms = 0u64; + let mut last_spans = 0u64; + for _ in 0..SAMPLES { + let (px, e, ns) = run(rex, len, zpat); + entries = e; + last_ms = ns / 1_000_000; + last_spans = px / if zpat { len.min(32) } else { len } as u64; + // Measured pixels over measured nanos — never an assumed budget. + rates.push(px * 1000 / ns.max(1)); + } + rates.sort_unstable(); + (rates[SAMPLES / 2], rates[0], rates[SAMPLES - 1], entries, last_ms, last_spans) + }; + + // --- self-validation ------------------------------------------------- + // A throughput number from draws that never touched a pixel is worse + // than no number: it looks like a fast configuration. Likewise a "JIT" + // column that is really the interpreter. Check both before reporting, + // on both engines and both modes, rather than trusting the setup. + for (rex, engine) in [(rex_interp, "interp"), (rex_jit, "jit")] { + for (zpat, mode) in [(false, "plain"), (true, "zpat")] { + // Clear a known region, draw one span into it, confirm it moved. + let probe_y = 700; + { + let fb = unsafe { &mut *rex.fb_rgb.get() }; + for x in 0..64usize { fb[probe_y as usize * 2048 + x] = 0; } + } + let go_before = rex.jit_go_count.load(Ordering::Relaxed); + let int_before = rex.interp_go_count.load(Ordering::Relaxed); + + reg(rex, REX3_DRAWMODE0, if zpat { dm0_zpat } else { dm0_plain }); + reg(rex, REX3_DRAWMODE1, dm1); + reg(rex, REX3_WRMASK, 0xFFFFFF); + reg(rex, REX3_SLOPERED, 2u32 << 11); + reg(rex, REX3_SLOPEGRN, 1u32 << 11); + reg(rex, REX3_SLOPEBLUE, 0); + if zpat { reg(rex, REX3_ZPATTERN, 0xFFFF_FFFF); } + reg(rex, REX3_COLORRED, 200u32 << 11); + reg(rex, REX3_COLORGRN, 180u32 << 11); + reg(rex, REX3_COLORBLUE, 160u32 << 11); + reg(rex, REX3_XYENDI, xy(31, probe_y)); + reg_go(rex, REX3_XYSTARTI, xy(0, probe_y)); + + let changed = { + let fb = unsafe { &*rex.fb_rgb.get() }; + (0..32usize).filter(|&x| fb[probe_y as usize * 2048 + x] != 0).count() + }; + assert!(changed > 0, + "{engine}/{mode}: draw mutated no pixels — the benchmark would be timing nothing"); + + let jit_gos = rex.jit_go_count.load(Ordering::Relaxed) - go_before; + let int_gos = rex.interp_go_count.load(Ordering::Relaxed) - int_before; + println!(" validate {engine:>6}/{mode:<5}: {changed:>2}/32 px written, \ + GOs jit={jit_gos} interp={int_gos}"); + if engine == "jit" { + assert!(jit_gos > 0, + "{engine}/{mode}: no GO dispatched through the JIT (jit={jit_gos} \ + interp={int_gos}) — the 'jit' column would just be the interpreter"); + } + } + } + + for (label, zpat) in [("plain Gouraud", false), ("ZPATTERN-masked (LENGTH32: 32px draws)", true)] { + println!("\n=== GFIFO pressure sweep: {label} ({} ms per cell) ===", + BUDGET.as_millis()); + println!(" {:>6} {:>10} {:>21} {:>21} {:>8} {:>11} {:>17}", + "span", "entries/px", "interp Mpx/s [min-max]", "jit Mpx/s [min-max]", + "jit x", "ms i/j", "spans i/j"); + // ZPATTERN draws are capped at 32 pixels, so sweeping span length + // past that measures nothing new -- one row is the whole story. + let lens: &[i32] = if zpat { &[32, 16, 8] } else { &SPAN_LENS }; + for &len in lens { + let (i_mpx, i_lo, i_hi, entries, i_ms, i_spans) = sample(rex_interp, len, zpat); + let (j_mpx, j_lo, j_hi, _, j_ms, j_spans) = sample(rex_jit, len, zpat); + let drawn = if zpat { len.min(32) } else { len } as u64; + // entries/px uses pixels actually rasterized (LENGTH32 caps + // ZPATTERN draws at 32), not the span length requested. + let per_px = entries as f64 + / (entries / if zpat { 7 } else { 6 }).max(1) as f64 + / drawn as f64; + // Ratio must come from WORK DONE, not elapsed time: every + // sample runs for the same wall-clock budget, so i_ns/j_ns is + // ~1.00 by construction and says nothing. (It printed a + // reassuring "1.00x" next to cells where the JIT was doing + // half the interpreter's work.) + println!(" {:>6} {:>10.3} {:>6}[{:>5}-{:>6}] {:>6}[{:>5}-{:>6}] {:>7.2}x {:>5}/{:<5} {:>8}/{:<8}", + len, per_px, + i_mpx, i_lo, i_hi, + j_mpx, j_lo, j_hi, + j_mpx as f64 / i_mpx.max(1) as f64, + i_ms, j_ms, i_spans, j_spans); + } + } + println!("\n For comparison: guest-side gltest --bench ~44 Mpx/s,\n \x20 --bench --depth ~20 Mpx/s.\n"); + } + /// Verify Gouraud interpolation pixel-by-pixel: R ramps from 255 down to 0 across 256 pixels. /// /// slope = (0 - 255) / 255 = -1 per pixel = -1 << 11 in o12.11 fixed-point. diff --git a/test/gltest/gltest b/test/gltest/gltest index 1446dc54..74cf052a 100755 Binary files a/test/gltest/gltest and b/test/gltest/gltest differ diff --git a/test/gltest/main.c b/test/gltest/main.c index 06904638..60b9671e 100644 --- a/test/gltest/main.c +++ b/test/gltest/main.c @@ -100,23 +100,68 @@ static double now_sec(void) { return ts.tv_sec + ts.tv_nsec * 1e-9; } -/* Draw one full-screen Gouraud-shaded quad. - Projection must already be set to ortho 0..w, 0..h. */ -static void bench_quad(int w, int h) { - glBegin(GL_QUADS); - glColor3f(1.0f, 0.0f, 0.0f); glVertex2i(0, 0); - glColor3f(0.0f, 1.0f, 0.0f); glVertex2i(w, 0); - glColor3f(0.0f, 0.0f, 1.0f); glVertex2i(w, h); - glColor3f(1.0f, 1.0f, 0.0f); glVertex2i(0, h); - glEnd(); +/* --- repeat-run statistics ------------------------------------------------- + + A single timing is not a measurement. Guest-side results here vary a lot run + to run -- JIT warmup, host scheduling, and (for queue-bound work) how the CPU + and painter threads happen to interleave. Reporting min/median/max across N + repeats makes that visible instead of letting one lucky peak stand in for the + truth, and the per-iteration list shows whether the first run is the slow one + (JIT cold) or the spread is genuinely random. */ + +static int cmp_double(const void *a, const void *b) { + double x = *(const double *)a, y = *(const double *)b; + return (x > y) - (x < y); } -static void run_bench(Display *dpy, Window win, GLXContext glc, int w, int h, int n) { - double t0, t1, elapsed; - long long total_px; +/* Print min/median/max of `vals` (n entries), labelled with `unit`, at + `prec` decimal places. Higher is better for every rate we report. */ +static void report_stats(const char *label, const char *unit, double *vals, int n, int prec) { + double *sorted; + double med; int i; - /* Set up orthographic projection matching window pixels */ + if (n <= 0) return; + + printf("%s (%d runs):", label, n); + for (i = 0; i < n; i++) printf(" %.*f", prec, vals[i]); + printf("\n"); + + if (n == 1) { + printf(" %s: %.*f\n", unit, prec, vals[0]); + return; + } + + sorted = (double *)malloc((size_t)n * sizeof(double)); + if (!sorted) return; + memcpy(sorted, vals, (size_t)n * sizeof(double)); + qsort(sorted, (size_t)n, sizeof(double), cmp_double); + med = (n % 2) ? sorted[n / 2] + : (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0; + + printf(" %s: min %.*f median %.*f max %.*f (spread %.1f%%)\n", + unit, prec, sorted[0], prec, med, prec, sorted[n - 1], + sorted[0] > 0.0 ? (sorted[n - 1] / sorted[0] - 1.0) * 100.0 : 0.0); + free(sorted); +} + +/* Shared projection/state setup for both benchmarks. + + `depth` enables the depth test. On Indy that is a split between CPU and REX3: + the GL driver performs the depth comparison in software on the MIPS side, + then hands REX3 the 32-bit result as a ZPATTERN coverage mask for a 32-pixel + span. REX3 draws only the pixels whose bit is set (see process_pixel_zpattern + in src/rex3.rs). All rendering goes out in 32-pixel spans, which is why + ZPATTERN is a 32-bit rotate reset per row and why LENGTH32 exists. + + That makes --depth interesting for GFIFO work specifically: each span now + costs an extra register write (the ZPATTERN mask) on top of the draw command, + so depth testing *raises* GFIFO traffic per rasterized pixel rather than + diluting it with rasterizer work. Expect it to widen queue differences, not + compress them. It also costs guest CPU time for the software compare, so a + slower queue and a busier CPU are both in play -- read the two benchmarks + together rather than either alone. */ +static void bench_setup(int w, int h, int depth) { glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); @@ -124,36 +169,168 @@ static void run_bench(Display *dpy, Window win, GLXContext glc, int w, int h, in glMatrixMode(GL_MODELVIEW); glLoadIdentity(); - glDisable(GL_DEPTH_TEST); + if (depth) { + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + } else { + glDisable(GL_DEPTH_TEST); + } glShadeModel(GL_SMOOTH); - /* Clear once so we start clean */ glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); + glClear(GL_COLOR_BUFFER_BIT | (depth ? GL_DEPTH_BUFFER_BIT : 0)); glFinish(); +} - printf("Benchmark: %d x %d, %d quads\n", w, h, n); +/* Draw one full-screen Gouraud-shaded quad. + `z` varies per iteration so a depth-tested run actually exercises the depth + comparison instead of rejecting/accepting every fragment identically. */ +static void bench_quad_z(int w, int h, float z) { + glBegin(GL_QUADS); + glColor3f(1.0f, 0.0f, 0.0f); glVertex3f(0.0f, 0.0f, z); + glColor3f(0.0f, 1.0f, 0.0f); glVertex3f((float)w, 0.0f, z); + glColor3f(0.0f, 0.0f, 1.0f); glVertex3f((float)w, (float)h, z); + glColor3f(1.0f, 1.0f, 0.0f); glVertex3f(0.0f, (float)h, z); + glEnd(); +} + +/* Fill-rate benchmark: few GFIFO commands, then ~w*h rasterized pixels per + quad. Rasterizer-bound, so it is nearly blind to the GFIFO implementation -- + use --tribench for that. With --depth it measures Z-buffered fill rate, where + each pixel also costs a depth read and conditional write. */ +static void run_bench(int w, int h, int n, int repeat, int depth, int warmup) { + double *rates; + long long total_px; + int r, i; + + rates = (double *)malloc((size_t)repeat * sizeof(double)); + if (!rates) { printf("out of memory\n"); return; } + + printf("Fill benchmark: %d x %d, %d quads, depth %s\n", + w, h, n, depth ? "ON" : "off"); fflush(stdout); - t0 = now_sec(); - for (i = 0; i < n; i++) { - bench_quad(w, h); + total_px = (long long)w * h * n; + + /* Untimed warmup. The first timed run otherwise measures the emulator's + JIT compiling the draw paths, not the draws themselves -- observed as a + ~25%% low outlier in the min column that recovers on later runs. */ + for (r = 0; r < warmup; r++) { + bench_setup(w, h, depth); + for (i = 0; i < n; i++) bench_quad_z(w, h, 0.0f); + glFinish(); } - glFinish(); - t1 = now_sec(); + if (warmup > 0) { printf(" (%d warmup run%s, untimed)\n", warmup, warmup == 1 ? "" : "s"); fflush(stdout); } - elapsed = t1 - t0; - total_px = (long long)w * h * n; - printf("Time : %.3f s\n", elapsed); - printf("Pixels : %lld\n", total_px); - printf("Fill rate: %.1f Mpx/s\n", total_px / elapsed / 1e6); + for (r = 0; r < repeat; r++) { + double t0, t1, elapsed; + + bench_setup(w, h, depth); + + t0 = now_sec(); + for (i = 0; i < n; i++) { + /* Sweep z back to front across the run so the depth test has real + work to do; harmless when depth is off. */ + float z = depth ? (float)i / (float)(n > 1 ? n - 1 : 1) * 2.0f - 1.0f + : 0.0f; + bench_quad_z(w, h, z); + } + glFinish(); + t1 = now_sec(); + + elapsed = t1 - t0; + rates[r] = elapsed > 0.0 ? (double)total_px / elapsed / 1e6 : 0.0; + printf(" run %d: %.3f s %.1f Mpx/s\n", r + 1, elapsed, rates[r]); + fflush(stdout); + } + + printf("Pixels/run: %lld\n", total_px); + report_stats("Fill rate", "Mpx/s", rates, repeat, 1); fflush(stdout); + free(rates); +} + +/* Triangle-throughput benchmark. + + run_bench() above measures fill rate: a handful of GFIFO commands per + full-screen quad, then hundreds of thousands of rasterized pixels. That is + rasterizer-bound, so it barely moves when the GFIFO implementation changes. + + This instead draws many *small* triangles: the per-primitive GL work (and so + the REX3 register writes queued through the GFIFO) dominates, and the pixel + count per primitive is small. That is the workload whose cost actually lands + on the queue, so this is the one to use when comparing GFIFO backends. */ +static void run_tribench(int w, int h, int n, int tri_px, int repeat, int depth, int warmup) { + double *rates; + int r, i; + + rates = (double *)malloc((size_t)repeat * sizeof(double)); + if (!rates) { printf("out of memory\n"); return; } + + printf("Triangle benchmark: %d tris, %d px each, %d x %d, depth %s\n", + n, tri_px, w, h, depth ? "ON" : "off"); + fflush(stdout); + + /* Untimed warmup -- see run_bench. */ + for (r = 0; r < warmup; r++) { + int wx = 0, wy = 0; + bench_setup(w, h, depth); + for (i = 0; i < n; i++) { + wx += tri_px; + if (wx + tri_px >= w) { wx = 0; wy += tri_px; if (wy + tri_px >= h) wy = 0; } + glBegin(GL_TRIANGLES); + glColor3f(1.0f, 0.0f, 0.0f); glVertex3f((float)wx, (float)wy, 0.0f); + glColor3f(0.0f, 1.0f, 0.0f); glVertex3f((float)(wx + tri_px), (float)wy, 0.0f); + glColor3f(0.0f, 0.0f, 1.0f); glVertex3f((float)wx, (float)(wy + tri_px), 0.0f); + glEnd(); + } + glFinish(); + } + if (warmup > 0) { printf(" (%d warmup run%s, untimed)\n", warmup, warmup == 1 ? "" : "s"); fflush(stdout); } + + for (r = 0; r < repeat; r++) { + double t0, t1, elapsed; + int x = 0, y = 0; + + bench_setup(w, h, depth); + + t0 = now_sec(); + for (i = 0; i < n; i++) { + /* Walk across the window so successive triangles touch different + pixels (no degenerate all-same-address case), wrapping at the + edge. With depth on, z cycles so fragments are a mix of passes + and fails rather than a uniform accept. */ + float z = depth ? (float)(i & 255) / 255.0f * 2.0f - 1.0f : 0.0f; + x += tri_px; + if (x + tri_px >= w) { x = 0; y += tri_px; if (y + tri_px >= h) y = 0; } + glBegin(GL_TRIANGLES); + glColor3f(1.0f, 0.0f, 0.0f); glVertex3f((float)x, (float)y, z); + glColor3f(0.0f, 1.0f, 0.0f); glVertex3f((float)(x + tri_px), (float)y, z); + glColor3f(0.0f, 0.0f, 1.0f); glVertex3f((float)x, (float)(y + tri_px), z); + glEnd(); + } + glFinish(); + t1 = now_sec(); + + elapsed = t1 - t0; + rates[r] = elapsed > 0.0 ? (double)n / elapsed : 0.0; + printf(" run %d: %.3f s %.0f tris/s %.3f us/tri\n", + r + 1, elapsed, rates[r], elapsed * 1e6 / (double)n); + fflush(stdout); + } - (void)dpy; (void)win; (void)glc; + report_stats("Triangles", "tris/s", rates, repeat, 0); + fflush(stdout); + free(rates); } int main(int argc, char *argv[]) { - int bench_n = 0; /* 0 = interactive mode */ + int bench_n = 0; /* 0 = interactive mode */ + int tribench_n = 0; /* >0 = triangle-throughput mode */ + int tri_px = 8; /* triangle leg length, --trisize */ + int repeat = 1; /* --repeat: timed runs, reported as min/median/max */ + int depth = 0; /* --depth: enable depth testing (Z fill rate) */ + int warmup = 0; /* --warmup: untimed runs before timing starts */ int i; Display *dpy; Window root; @@ -171,6 +348,31 @@ int main(int argc, char *argv[]) { for (i = 1; i < argc; i++) { if (strcmp(argv[i], "--bench") == 0 && i + 1 < argc) { bench_n = atoi(argv[++i]); + } else if (strcmp(argv[i], "--tribench") == 0 && i + 1 < argc) { + tribench_n = atoi(argv[++i]); + } else if (strcmp(argv[i], "--trisize") == 0 && i + 1 < argc) { + tri_px = atoi(argv[++i]); + if (tri_px < 1) tri_px = 1; + } else if (strcmp(argv[i], "--repeat") == 0 && i + 1 < argc) { + repeat = atoi(argv[++i]); + if (repeat < 1) repeat = 1; + } else if (strcmp(argv[i], "--depth") == 0) { + depth = 1; + } else if (strcmp(argv[i], "--warmup") == 0 && i + 1 < argc) { + warmup = atoi(argv[++i]); + if (warmup < 0) warmup = 0; + } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + printf("usage: gltest [options]\n" + " --bench N fill-rate benchmark: N full-screen quads\n" + " --tribench N triangle throughput: N small triangles\n" + " --trisize PX triangle leg length for --tribench (default 8)\n" + " --repeat N run the benchmark N times, report min/median/max\n" + " --warmup N N untimed runs first (lets the JIT compile)\n" + " --depth enable depth testing (Z-buffered fill rate)\n" + " (no option) interactive spinning-cube mode\n" + "\n" + "--tribench is the GFIFO-sensitive one; --bench is rasterizer-bound.\n"); + exit(0); } } @@ -211,13 +413,43 @@ int main(int argc, char *argv[]) { glc = glXCreateContext(dpy, vi, NULL, GL_TRUE); glXMakeCurrent(dpy, win, glc); + /* Report the depth buffer we actually got, not the one we asked for. + The visual fallback chain above ends at att_fb3, which requests NO depth + buffer -- and GLX_DEPTH_SIZE is a minimum, so the other three can return + more bits than requested. If that last fallback is what matched, then + glEnable(GL_DEPTH_TEST) is a silent no-op: the state turns on, there is + no depth buffer behind it, and every fragment passes. A --depth run would + then report a perfectly ordinary number that measures nothing at all, so + check the granted config rather than trusting the request. */ + { + int granted = 0; + glXGetConfig(dpy, vi, GLX_DEPTH_SIZE, &granted); + printf("Visual: depth %d bits\n", granted); + if (depth && granted == 0) { + printf("ERROR: --depth requested but this visual has no depth buffer;\n" + " the depth test would silently pass every fragment and the\n" + " result would be indistinguishable from a no-depth run.\n" + " Refusing to report a meaningless number.\n"); + return 1; + } + } + // Init OpenGL state glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); glClearColor(0.4f, 0.1f, 0.6f, 1.0f); // Purple background + if (tribench_n > 0) { + run_tribench(800, 600, tribench_n, tri_px, repeat, depth, warmup); + glXMakeCurrent(dpy, None, NULL); + glXDestroyContext(dpy, glc); + XDestroyWindow(dpy, win); + XCloseDisplay(dpy); + return 0; + } + if (bench_n > 0) { - run_bench(dpy, win, glc, 800, 600, bench_n); + run_bench(800, 600, bench_n, repeat, depth, warmup); glXMakeCurrent(dpy, None, NULL); glXDestroyContext(dpy, glc); XDestroyWindow(dpy, win);