mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Honor HOLD when presenting native replay generations
This commit is contained in:
@@ -108,3 +108,17 @@ hash and its execution manifest. Zero replay, image blending, disabled replay,
|
||||
counter resets or verification failures cannot produce a passing sample. This
|
||||
is one evidence window; visual coverage, load speed and owner acceptance remain
|
||||
separate checks. Use `tools/load_probe.py` for passive load-window measurements.
|
||||
|
||||
Run correctness and performance windows separately. `PSX_RENDER_PASS_VERIFY=1`
|
||||
adds synchronous GPU readbacks plus state hashing. After recording correctness,
|
||||
relaunch without it and use `--purpose performance`. The tool rejects a
|
||||
performance sample with active verification. Spider-Man 2's initial verified
|
||||
replay took 28-35 ms; verification off measured 19.531 ms, of which 19.176 ms
|
||||
was guest draw. That evidence points to title geometry processing rather than
|
||||
shared GPU checkpoint/capture as the remaining throughput bottleneck.
|
||||
|
||||
The shared presenter now honors HOLD for native phase generations instead of
|
||||
crossfading neighboring phase images. Phase selection tests and the real OpenGL
|
||||
probe (329 checks) pass, including red/blue native images that must stay distinct
|
||||
under HOLD while explicit blend mode still mixes them. Title requalification
|
||||
on this correction remains required.
|
||||
|
||||
@@ -64,6 +64,9 @@ int render_pass_select(const uint32_t *phases, uint32_t n, double p,
|
||||
* game that stops flipping for several frame lengths gets that back. */
|
||||
int render_pass_gen_select(const uint32_t *phases, uint32_t n, double p,
|
||||
uint32_t *lo, uint32_t *hi, float *t);
|
||||
/* HOLD presents one native phase image without a temporal crossfade. */
|
||||
int render_pass_gen_select_mode(const uint32_t *phases, uint32_t n, double p,
|
||||
int hold, uint32_t *lo, uint32_t *hi, float *t);
|
||||
|
||||
/* Is the flip the FLIP source just saw the one a pending generation waits
|
||||
* for? A generation built for the next flip (`shown` 0, the canonical PsyQ
|
||||
|
||||
@@ -7052,13 +7052,13 @@ static int pass_gen_present(uint64_t deadline) {
|
||||
/* Past phase 1 the next flip is late (a lagging tick): the newest image
|
||||
* holds until it comes. Only a game that stops flipping for several
|
||||
* frame lengths expires the generation. */
|
||||
if (!render_pass_gen_select(g->phase, g->n, p, &lo, &hi, &t)) {
|
||||
if (!render_pass_gen_select_mode(g->phase, g->n, p, s_interp_hold, &lo, &hi, &t)) {
|
||||
g->valid = 0;
|
||||
s_pgen_expired++;
|
||||
return 0;
|
||||
}
|
||||
if (!interp_present_pair(s_pgen_tex[s_pgen_cur][lo],
|
||||
s_pgen_tex[s_pgen_cur][hi], t, 0))
|
||||
s_pgen_tex[s_pgen_cur][hi], t, s_interp_blend_mode))
|
||||
return 0;
|
||||
s_pgen_presents++;
|
||||
if (lo != hi) s_pgen_blends++;
|
||||
|
||||
@@ -127,6 +127,13 @@ int render_pass_gen_select(const uint32_t *phases, uint32_t n, double p,
|
||||
return render_pass_select(phases, n, p, lo, hi, t);
|
||||
}
|
||||
|
||||
int render_pass_gen_select_mode(const uint32_t *phases, uint32_t n, double p,
|
||||
int hold, uint32_t *lo, uint32_t *hi, float *t) {
|
||||
if (!render_pass_gen_select(phases, n, p, lo, hi, t)) return 0;
|
||||
if (hold) { *hi = *lo; *t = 0.0f; }
|
||||
return 1;
|
||||
}
|
||||
|
||||
double render_pass_ema(double current, double sample) {
|
||||
if (!(sample >= 0.0) || !isfinite(sample)) return current;
|
||||
if (!(current > 0.0)) return sample;
|
||||
|
||||
@@ -73,6 +73,29 @@ static void atlas_edge(int depth) {
|
||||
check(gl_renderer_fbo_peek(0,0,64,64,peek),"atlas-edge readback");
|
||||
check((peek[21*64+21]&0x7fff)==0x7c00,"footprint clamps before wrapping into far atlas row");
|
||||
}
|
||||
static void native_hold_images(void) {
|
||||
GLuint textures[2];
|
||||
const uint8_t colors[2][4] = {{255,0,0,255},{0,0,255,255}};
|
||||
const uint32_t phases[2] = {0,32768};
|
||||
glGenTextures(2,textures);
|
||||
for(int i=0;i<2;++i) {
|
||||
glBindTexture(GL_TEXTURE_2D,textures[i]);
|
||||
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,1,1,0,GL_RGBA,GL_UNSIGNED_BYTE,colors[i]);
|
||||
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
for(int hold=0;hold<2;++hold) {
|
||||
uint32_t lo,hi; float blend; uint8_t pixel[4];
|
||||
check(render_pass_gen_select_mode(phases,2,0.25,hold,&lo,&hi,&blend),"native phase selection");
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
interp_draw_textures(textures[lo],textures[hi],blend,0,0,0,8,8);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,0);
|
||||
glReadPixels(4,4,1,1,GL_RGBA,GL_UNSIGNED_BYTE,pixel);
|
||||
if(hold) check(pixel[0]>250 && pixel[2]<5,"HOLD presents original native red image without ghosting");
|
||||
else check(pixel[0]>60 && pixel[2]>60,"explicit blend still mixes red and blue images");
|
||||
}
|
||||
glDeleteTextures(2,textures);
|
||||
}
|
||||
int main(void) {
|
||||
if(SDL_Init(SDL_INIT_VIDEO)!=0) return 2;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3);
|
||||
@@ -82,6 +105,7 @@ int main(void) {
|
||||
if(!win) return 2;
|
||||
glb_init(vram); glb_set_scale(1); gl_renderer_set_swap_interval(0);
|
||||
if(!gl_renderer_init_context(win)) return 2;
|
||||
native_hold_images();
|
||||
for(int depth=0;depth<3;++depth) {
|
||||
upload_checker(depth,0);
|
||||
triangle(0,depth,0,1); int nearest=chroma();
|
||||
|
||||
@@ -147,8 +147,8 @@ assert re.search(r"if \(want && s_pass_cost_w == s_interp_w && "
|
||||
# A frame on screen longer than planned (a lagging tick) must hold its newest
|
||||
# pass image, never fall back to the older capture (render_pass_plan_test).
|
||||
pgp = definition(gl, "pass_gen_present")
|
||||
assert "render_pass_gen_select(g->phase, g->n, p, &lo, &hi, &t)" in pgp, (
|
||||
"pass images must be selected through the tested late-flip rule")
|
||||
assert "render_pass_gen_select_mode(g->phase, g->n, p, s_interp_hold, &lo, &hi, &t)" in pgp, (
|
||||
"pass images must honor HOLD through the tested late-flip selection rule")
|
||||
assert "render_pass_select(" not in pgp and "0.5 / (double)g->period" not in pgp, (
|
||||
"no early expiry of a late frame's images")
|
||||
cls = body(plan, "int render_pass_mmio_class(")
|
||||
|
||||
@@ -327,6 +327,20 @@ static void test_stereo_pair_fresh(void) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
{
|
||||
const uint32_t phases[] = {0, 16384, 32768, 65536};
|
||||
uint32_t lo, hi; float blend;
|
||||
CHECK(render_pass_gen_select_mode(phases, 4, 0.375, 1, &lo, &hi, &blend) &&
|
||||
lo == 1 && hi == 1 && blend == 0.0f, "HOLD keeps a native sample between phases");
|
||||
CHECK(render_pass_gen_select_mode(phases, 4, 0.5, 1, &lo, &hi, &blend) &&
|
||||
lo == 2 && hi == 2 && blend == 0.0f, "HOLD advances at the next native phase");
|
||||
CHECK(render_pass_gen_select_mode(phases, 4, 0.375, 0, &lo, &hi, &blend) &&
|
||||
lo == 1 && hi == 2 && blend == 0.5f, "explicit blending remains supported");
|
||||
CHECK(render_pass_gen_select_mode(phases, 4, 2.0, 1, &lo, &hi, &blend) &&
|
||||
lo == 3 && hi == 3, "late HOLD uses newest native image");
|
||||
CHECK(!render_pass_gen_select_mode(phases, 4, 5.0, 1, &lo, &hi, &blend),
|
||||
"HOLD cannot revive an expired generation");
|
||||
}
|
||||
test_stereo_pair_fresh();
|
||||
test_store_policy();
|
||||
test_counts_per_rate();
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
This observes an already running debug build. It never boots, drives, pauses or
|
||||
changes settings. A passing sample is one gameplay window, not title acceptance.
|
||||
Run with PSX_RENDER_PASS_VERIFY=1 to check that replay restores guest state.
|
||||
Use correctness with PSX_RENDER_PASS_VERIFY=1 to check guest-state restoration.
|
||||
For performance, relaunch without verification and select --purpose performance;
|
||||
synchronous verification readbacks and hashing are not normal-play costs.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
@@ -19,7 +21,7 @@ COUNTERS = ("passes", "pass_presents", "verify_checks", "verify_mismatch",
|
||||
"aborted", "blended_presents")
|
||||
|
||||
|
||||
def assess(before, after):
|
||||
def assess(before, after, purpose="correctness"):
|
||||
reasons, delta = [], {}
|
||||
for key in COUNTERS:
|
||||
a, b = before.get(key), after.get(key)
|
||||
@@ -27,7 +29,8 @@ def assess(before, after):
|
||||
reasons.append("missing, invalid or reset counter: " + key)
|
||||
else:
|
||||
delta[key] = b - a
|
||||
for key in ("passes", "pass_presents", "verify_checks"):
|
||||
required = ("passes", "pass_presents", "verify_checks") if purpose == "correctness" else ("passes", "pass_presents")
|
||||
for key in required:
|
||||
if key in delta and not delta[key]:
|
||||
reasons.append("no evidence during this window: " + key)
|
||||
for key in ("verify_mismatch", "vram_leaks", "watchdog", "nesting_repairs",
|
||||
@@ -44,6 +47,7 @@ def main():
|
||||
parser.add_argument("--port", type=int, required=True)
|
||||
parser.add_argument("--case", required=True, help="actual gameplay path being observed")
|
||||
parser.add_argument("--seconds", type=float, default=10)
|
||||
parser.add_argument("--purpose", choices=("correctness", "performance"), default="correctness")
|
||||
parser.add_argument("--executable", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
@@ -57,7 +61,7 @@ def main():
|
||||
receipt = dict(schema=1, case=args.case, executable=str(executable),
|
||||
executable_sha256=binary_sha, execution=manifest,
|
||||
started_utc=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
port=args.port, sample_passed=False)
|
||||
port=args.port, purpose=args.purpose, sample_passed=False)
|
||||
|
||||
def query():
|
||||
with connect(port=args.port) as sock:
|
||||
@@ -72,7 +76,9 @@ def main():
|
||||
time.sleep(args.seconds)
|
||||
receipt["after"] = query()
|
||||
receipt["elapsed_seconds"] = time.monotonic() - start
|
||||
receipt["delta"], receipt["reasons"] = assess(receipt["before"], receipt["after"])
|
||||
receipt["delta"], receipt["reasons"] = assess(receipt["before"], receipt["after"], args.purpose)
|
||||
if args.purpose == "performance" and receipt["delta"].get("verify_checks", 0):
|
||||
receipt["reasons"].append("performance sample includes verification readbacks and hashing")
|
||||
receipt["sample_passed"] = not receipt["reasons"]
|
||||
except (OSError, ValueError, RuntimeError) as error:
|
||||
receipt["reasons"] = [str(error)]
|
||||
|
||||
Reference in New Issue
Block a user