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.
This commit is contained in:
libretroadmin committed 2026-11-02 18:20:12 +01:00
1 parent b60d97627a
commit 1ed251bfec
6 files changed
+170 -6

No files matched your search

+5 -1
View File
@@ -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
+57 -5
View File
@@ -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<sched_list> 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;
+2
View File
@@ -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())...
+2
View File
@@ -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)
{
+57
View File
@@ -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 <stdio.h>
#include <string.h>
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);
}
+47
View File
@@ -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.<pid>.
# 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"