From 1ed251bfec3dd385b8b11bfc606ec64d000ae50c Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Mon, 2 Nov 2026 17:26:35 +0100 Subject: [PATCH] SPG: keep the beam from one read of SPG_STATUS to the next A game waiting on the beam reads SPG_STATUS hundreds of times a line, and each read found the next line due and divided twice to place the beam. spg_beam() now keeps what it found - the line, the pending event's end, the cycles that bound the line - and answers a read on the same line, aimed by the same event, with a subtraction. Aiming the event, a timing change or a state loaded starts it afresh. The headless disc runs 3000 frames in 11.5 s, from 17.8 s. tools/pvr/spg_beam_test.sh makes every read twice, kept and afresh, straight and with every frame run ahead, and fails where they differ. --- .github/workflows/headless.yml | 6 +++- core/hw/pvr/spg.cpp | 62 +++++++++++++++++++++++++++++++--- core/hw/pvr/spg.h | 2 ++ core/serialize.cpp | 2 ++ tools/pvr/spg_beam_test.cpp | 57 +++++++++++++++++++++++++++++++ tools/pvr/spg_beam_test.sh | 47 ++++++++++++++++++++++++++ 6 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 tools/pvr/spg_beam_test.cpp create mode 100644 tools/pvr/spg_beam_test.sh diff --git a/.github/workflows/headless.yml b/.github/workflows/headless.yml index 3c3dc5b9..04686212 100644 --- a/.github/workflows/headless.yml +++ b/.github/workflows/headless.yml @@ -9,7 +9,9 @@ name: Headless # And a strip's indices, the parse's C89 kernel, against the loop it # replaced (tools/pvr/ta_index_test.sh); and the parse timed on the heavy # disc, the strips its vertex loop found drawable whole making the indices -# a look at their vertices does (tools/pvr/ta_parse_bench.sh). +# a look at their vertices does (tools/pvr/ta_parse_bench.sh). And the beam +# each read of SPG_STATUS sees, kept from the read before on its line, +# against the beam found afresh (tools/pvr/spg_beam_test.sh). on: push: @@ -53,3 +55,5 @@ jobs: run: NEON=required sh tools/pvr/ta_index_test.sh - name: The parse timed on the heavy disc, the strips known good making the indices a look at them does run: sh tools/pvr/ta_parse_bench.sh + - name: The beam kept from read to read of SPG_STATUS against the beam found afresh, straight and run ahead + run: sh tools/pvr/spg_beam_test.sh diff --git a/core/hw/pvr/spg.cpp b/core/hw/pvr/spg.cpp index 8e32c18e..3a44a494 100644 --- a/core/hw/pvr/spg.cpp +++ b/core/hw/pvr/spg.cpp @@ -40,6 +40,33 @@ u32 sh4_sched_remaining(int id); int render_end_schid; int vblank_schid; +/* What spg_beam() last found, for the reads of SPG_STATUS a game makes + * while the beam is still on the same line - hundreds a line, as it waits + * for one: the line, the pending event's end it was aimed by, and + * Line_Num's generation; the cycle the event's line starts at, the line's + * own, and the cycles the event still has while the beam is before the + * next line (more than beam_stay) or still at the line's start (beam_span + * or more). Anything else that moves the answer - a register written, a + * gun, the run's window - aims the event again, and its end is another. */ +extern std::vector sch_list; +static u32 beam_line = ~0u; +static int beam_end; +static u32 beam_gen, line_gen = 1; +static u64 beam_target_at, beam_line_at; +static u32 beam_span, beam_stay; + +void spg_beam_forget() +{ + beam_line = ~0u; +} + +/* The pending event aimed again: whatever spg_beam() kept is from before. */ +static void spg_aim(int cycles) +{ + line_gen++; + sh4_sched_request(vblank_schid, cycles); +} + static u32 lightgun_line = 0xffff; static u32 lightgun_hpos; static bool maple_int_pending; @@ -142,7 +169,7 @@ static void frame_window_close() } const u64 beam = spg_beam(); frame_window = false; - sh4_sched_request(vblank_schid, (int)(line_start(spg_next_line()) - beam)); + spg_aim((int)(line_start(spg_next_line()) - beam)); } static void frame_end() @@ -222,6 +249,8 @@ void CalculateSync(bool reschedule) Line_Num = (u64)SH4_MAIN_CLOCK * (u64)(SPG_LOAD.hcount + 1); Line_Den = pixel_clock; + // (spg_beam()'s lines are another's now) + line_gen++; float scale_x = 1.f, scale_y = 1.f; @@ -253,7 +282,7 @@ void CalculateSync(bool reschedule) if (reschedule) { pvr_cur_scanline = 0; - sh4_sched_request(vblank_schid, (int)line_start(spg_next_line())); + spg_aim((int)line_start(spg_next_line())); } } @@ -435,14 +464,27 @@ static void spg_run_lines(u32 lines) * event would have been aimed at one of them. */ static u64 spg_beam() { + const u32 remaining = sh4_sched_remaining(vblank_schid); + // on the line it was on, by the same event: no division, no line looked for + if (beam_line == pvr_cur_scanline && beam_gen == line_gen && remaining != (u32)-1 + && beam_end == sch_list[vblank_schid].end) + { + if (remaining >= beam_span) + return beam_line_at; + if (remaining > beam_stay) + return beam_target_at - remaining; + } + const u32 target = spg_next_line(); const u64 target_at = line_start(target); const u64 line_at = line_start(pvr_cur_scanline); - const u32 remaining = sh4_sched_remaining(vblank_schid); // nothing pending, or the event is due: the beam is where it was put last if (remaining == (u32)-1 || (u64)remaining >= target_at - line_at) + { + beam_line = ~0u; return line_at; + } const u64 beam = target_at - remaining; u32 line = (u32)(beam * Line_Den / Line_Num); @@ -454,6 +496,14 @@ static u64 spg_beam() line = target - 1; if (line > pvr_cur_scanline) spg_run_lines(line - pvr_cur_scanline); + // (the line it is on now, and the next line's start, for the reads to come) + beam_line = pvr_cur_scanline; + beam_end = sch_list[vblank_schid].end; + beam_gen = line_gen; + beam_target_at = target_at; + beam_line_at = line_start(pvr_cur_scanline); + beam_span = (u32)(target_at - beam_line_at); + beam_stay = pvr_cur_scanline + 1 < target ? (u32)(target_at - line_start(pvr_cur_scanline + 1)) : 0; return beam; } @@ -478,7 +528,7 @@ void spg_write_timing(u32 addr, u32 data) const u64 beam = spg_beam(); PvrReg(addr, u32) = data; - sh4_sched_request(vblank_schid, (int)(line_start(spg_next_line()) - beam)); + spg_aim((int)(line_start(spg_next_line()) - beam)); } //called from sh4 context , should update pvr/ta state and everything else @@ -527,7 +577,7 @@ void read_lightgun_position(int x, int y) lightgun_flip ^= 1; } if (pending) - sh4_sched_request(vblank_schid, (int)(line_start(spg_next_line()) - beam)); + spg_aim((int)(line_start(spg_next_line()) - beam)); } // The game cancelled the gun's occupation of the bus: its transfer ends in its own time @@ -567,6 +617,7 @@ void spg_gun_unserialize(void **data, unsigned int *total_size) /* A line is at most 479 down from the line the picture starts on, which * is a 10-bit register; the place goes into ten bits of a register. */ + spg_beam_forget(); lightgun_line = line > 479 + 1023 ? 0xffff : line; lightgun_hpos = hpos & 0x3FF; maple_int_pending = (flags & 1) != 0; @@ -582,6 +633,7 @@ void spg_gun_before_v21(void) { extern int maple_sched; + spg_beam_forget(); lightgun_line = 0xffff; lightgun_hpos = 0; lightgun_flip = 0; diff --git a/core/hw/pvr/spg.h b/core/hw/pvr/spg.h index ef4ed669..fe6e4639 100644 --- a/core/hw/pvr/spg.h +++ b/core/hw/pvr/spg.h @@ -12,6 +12,8 @@ void read_lightgun_position(int x, int y); void spg_gun_occupy_cancel(); // Before a read of SPG_STATUS: brings its scanline up to where the beam is void spg_sync(); +// The beam's state has been put in place from elsewhere (a save state) +void spg_beam_forget(); // A write to SPG_HBLANK_INT, SPG_VBLANK_INT or SPG_VBLANK void spg_write_timing(u32 addr, u32 data); // Where a run (a retro_run's frame) ends, spg.cpp: a run starts (dc_run())... diff --git a/core/serialize.cpp b/core/serialize.cpp index 650c91a5..fa058394 100644 --- a/core/serialize.cpp +++ b/core/serialize.cpp @@ -12,6 +12,7 @@ #include "hw/gdrom/gdromv3.h" #include "hw/maple/maple_devs.h" #include "hw/maple/maple_cfg.h" +#include "hw/pvr/spg.h" #include "hw/pvr/Renderer_if.h" #include "hw/pvr/ta_structs.h" #include "hw/sh4/sh4_interrupts.h" @@ -1440,6 +1441,7 @@ bool dc_unserialize(void **data, unsigned int *total_size, size_t actual_data_si LIBRETRO_US(dummy_int) ; // the tag: what the device registered with, not state LIBRETRO_US(sch_list[vblank_schid].start) ; LIBRETRO_US(sch_list[vblank_schid].end) ; + spg_beam_forget(); if (version < V9) { diff --git a/tools/pvr/spg_beam_test.cpp b/tools/pvr/spg_beam_test.cpp new file mode 100644 index 00000000..a70d0a3e --- /dev/null +++ b/tools/pvr/spg_beam_test.cpp @@ -0,0 +1,57 @@ +/* The beam a read of SPG_STATUS sees, as spg_beam() keeps it from one read + * to the next on the same line, against the beam found afresh. Linked into + * one program with the core's objects and tools/threads/headless.c, with + * spg_sync() wrapped (spg_beam_test.sh): each read is made as the core + * makes it, and made again once spg_beam_forget() has thrown away what it + * kept. The second finds the beam from nothing; were the first's line + * short of it, the second would move it on, and SPG_STATUS would read + * otherwise. At the end: "spg_beam_test: N reads, M lines, K different". */ +#include "types.h" +#include "hw/pvr/pvr_regs.h" +#include "hw/pvr/spg.h" +#include +#include + +extern "C" void __real__Z8spg_syncv(void); +extern "C" void *__real_dlopen(const char *file, int mode); +extern u32 pvr_cur_scanline; + +/* headless.c opens the core; "self" is the program, the core linked in */ +extern "C" void *__wrap_dlopen(const char *file, int mode) +{ + return __real_dlopen(file != NULL && strcmp(file, "self") == 0 ? NULL : file, mode); +} + +static unsigned long long reads, lines, different; +static u32 last_line = ~0u; + +extern "C" void __wrap__Z8spg_syncv(void) +{ + u32 status, line; + + __real__Z8spg_syncv(); + status = SPG_STATUS.full; + line = pvr_cur_scanline; + reads++; + if (line != last_line) + lines++; + last_line = line; + + spg_beam_forget(); + __real__Z8spg_syncv(); + if (SPG_STATUS.full != status || pvr_cur_scanline != line) + { + if (different++ < 8) + fprintf(stderr, "spg_beam_test: read %llu: line %u status %08x kept, %u %08x afresh\n", + reads, line, status, pvr_cur_scanline, SPG_STATUS.full); + } +} + +/* (headless.c leaves through _exit(), past the destructors) */ +extern "C" void __real__exit(int status); +extern "C" void __wrap__exit(int status) +{ + printf("spg_beam_test: %llu reads, %llu lines, %llu different\n", reads, lines, different); + fflush(stdout); + __real__exit(status); +} diff --git a/tools/pvr/spg_beam_test.sh b/tools/pvr/spg_beam_test.sh new file mode 100644 index 00000000..0c60ea32 --- /dev/null +++ b/tools/pvr/spg_beam_test.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# The beam SPG_STATUS reads give, kept from read to read on a line by +# spg_beam(), against the beam found afresh at each (spg_beam_test.cpp): +# the test disc run on tools/threads/headless.c, straight and then with +# every frame run ahead (a state saved and loaded each frame). The core's +# own objects are linked in, so the core has to be built for this machine +# first (make platform=unix). +# +# sh tools/pvr/spg_beam_test.sh [FRAMES] +# +# KEEP=1: the program is kept, in $TMPDIR/spg_beam_test.. +# Fails where a read's beam differs, or too few reads were made to tell. +set -e +cd "$(dirname "$0")/../.." +FRAMES=${1:-600} +WORK=${TMPDIR:-/tmp}/spg_beam_test.$$ +mkdir -p "$WORK" +[ -n "${KEEP:-}" ] || trap 'rm -rf "$WORK"' EXIT +[ -f core/hw/pvr/spg.o ] || { echo "build the core first: core/hw/pvr/spg.o is not there" >&2; exit 1; } + +CORE_FLAGS=$(make -n platform=unix -W core/hw/pvr/spg.cpp core/hw/pvr/spg.o 2>/dev/null | grep -m1 'core/hw/pvr/spg.cpp' \ + | sed -e 's/^g++ //' -e 's/ -c / /' -e 's/ -MMD -MP .*$//' -e 's/-DGIT_VERSION=[^ ]* [^ ]*//') +OBJS=$(make -n platform=unix -W core/hw/pvr/spg.cpp 2>/dev/null | tail -1 | tr ' ' '\n' | grep '\.o$' | grep -v 'glslang.js.o') +${CC:-cc} -O1 -c -o "$WORK/headless.o" tools/threads/headless.c +${CXX:-g++} $CORE_FLAGS -w -c -o "$WORK/test.o" tools/pvr/spg_beam_test.cpp +${CXX:-g++} -fopenmp -rdynamic -o "$WORK/spg_beam_test" "$WORK/headless.o" "$WORK/test.o" $OBJS \ + -Wl,--wrap=dlopen -Wl,--wrap=_exit -Wl,--wrap=_Z8spg_syncv -lGL -lrt -lpthread -lm -ldl +# an OpenGL that does nothing, for the core's renderer (headless.sh's) +nm -D --undefined-only "$WORK/spg_beam_test" | awk '{print $NF}' | sed 's/@.*//' | grep '^gl' \ + | python3 tools/threads/headless_gl.py > "$WORK/nogl.c" || true +${CC:-cc} -O1 -shared -fPIC -Wl,-soname,libGLESv2.so.2 -o "$WORK/libGLESv2.so.2" "$WORK/nogl.c" +python3 tools/threads/live_disc.py "$WORK/test.gdi" + +fail=0 +for ahead in "" 10; do + rm -rf "$WORK/dir"; mkdir -p "$WORK/dir" + HEADLESS_AHEAD=$ahead LD_PRELOAD="$WORK/libGLESv2.so.2" HEADLESS_DIR="$WORK/dir" \ + "$WORK/spg_beam_test" self "$WORK/test.gdi" "$FRAMES" reicast_threaded_rendering=disabled \ + > "$WORK/out" 2> "$WORK/err" || true + r=$(grep '^spg_beam_test:' "$WORK/out") || { echo "spg_beam_test: no result"; tail -5 "$WORK/err"; exit 1; } + echo "${ahead:+run ahead from frame $ahead: }$r" + grep 'spg_beam_test: read' "$WORK/err" || true + # many reads to a line (a wait on the beam), and every one the same + echo "$r" | awk '{ if ($6 != 0 || $2 < 100000 || $2 < 4 * $4) exit 1 }' || fail=1 +done +[ $fail = 0 ] || { echo "spg_beam_test: FAIL: a read's beam kept from the last differs from the beam found afresh, or too few reads"; exit 1; } +echo "spg_beam_test: pass"