mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge master and enable PS1 blend correction by default
This commit is contained in:
@@ -58,6 +58,36 @@ into `licenses/`. Developers can use the verified archive or an explicit
|
||||
`FETCHCONTENT_SOURCE_DIR_PSX_LIBWEBP` / `PSX_LIBWEBP_SOURCE_DIR` source override.
|
||||
PNG and JPEG continue to use the existing shared stb_image implementation.
|
||||
|
||||
## xBR — edge-directed texture filter (Hyllian)
|
||||
|
||||
The `texture_filtering = "xbr"` mode in `runtime/src/gpu_gl_renderer.c`
|
||||
(`xbr_d`, `xbr_sample`) follows Hyllian's xBR algorithm: its 5x5 neighbourhood,
|
||||
YUV-weighted colour distance and the `4*d(H,F)` / `4*d(E,I)` edge sums. It is
|
||||
an adaptation (CLUT texel fetches, primitive UV clamping, an anti-aliased corner
|
||||
cut), not a copy of a shader file, but it derives from that work, licensed MIT:
|
||||
|
||||
```
|
||||
Copyright (C) 2011-2016 Hyllian - sergiogdb@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
```
|
||||
|
||||
## Vendored libraries
|
||||
|
||||
These are checked in under `recompiler/lib/` and `runtime/third_party/` with
|
||||
|
||||
@@ -757,6 +757,13 @@ some selections of one feature is a `[[requirement]]`, not a dependency.
|
||||
|
||||
## Implicit requirements across packages
|
||||
|
||||
Trusted graphics plugins can call `psx_mod_set_internal_resolution(lines)` to
|
||||
request a rendering height for their session. Values from 120 through 8192 use
|
||||
the existing integer-scale resolver and backend allocation limits; zero clears
|
||||
the request. `PSX_INTERNAL_RESOLUTION` takes precedence. The player's saved
|
||||
resolution stays unchanged, and every new session clears the request before
|
||||
its mod plan activates, including rematches and sessions without that mod.
|
||||
|
||||
The shared PGXP plugin uses `psx_mod_set_pgxp_precision(enabled, cpu_mode)`.
|
||||
This stores a session selection as well as setting live correction flags, so
|
||||
later renderer initialization cannot erase mod activation with the base video
|
||||
|
||||
@@ -486,9 +486,9 @@ between Play and TCP availability.
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**368 commands registered** — 355 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**369 commands registered** — 356 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
82 of 368 have prose above; **286 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
82 of 369 have prose above; **287 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -800,6 +800,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `stereo_dump` | ✓ | | |
|
||||
| `stereo_stats` | ✓ | | |
|
||||
| `synth_recurse` | ✓ | | |
|
||||
| `texture_filter` | ✓ | | |
|
||||
| `texture_lod` | ✓ | | ✓ |
|
||||
| `thread_ctx_ring` | ✓ | | |
|
||||
| `thread_trace` | ✓ | | |
|
||||
|
||||
+34
-1
@@ -702,9 +702,12 @@ switches them live.
|
||||
|
||||
```toml
|
||||
[video]
|
||||
accurate_blending = false # default
|
||||
accurate_blending = true # default: PS1 colour/blend correctness
|
||||
```
|
||||
|
||||
Accurate blending is enabled by default because it corrects PS1 rendering.
|
||||
`false` retains the historical path for diagnosis.
|
||||
|
||||
The OpenGL surface holds each 5-bit VRAM channel as an 8-bit value. Uploads,
|
||||
copies and untextured primitives store `k*8`, but fills and texels have
|
||||
always stored `k*255/31` (full-range white). Semi-transparent blending of two
|
||||
@@ -723,6 +726,29 @@ first layer's full 8-bit result where the console would have truncated it to
|
||||
`PSX_ACCURATE_BLENDING=0|1` overrides it for one run and the
|
||||
`accurate_blending` TCP command switches it live.
|
||||
|
||||
### Texture filtering (`texture_filtering`)
|
||||
|
||||
```toml
|
||||
[video]
|
||||
texture_filtering = "nearest" # nearest (default) | bilinear | xbr
|
||||
```
|
||||
|
||||
`nearest` is the PS1's point sampling. `bilinear` blends the four nearest
|
||||
texels (opacity-gated, so cutout edges keep their colour). `xbr` (OpenGL
|
||||
only) is edge-directed magnification after Hyllian's xBR (MIT; see
|
||||
`THIRD_PARTY_ATTRIBUTION.md`): for the texel
|
||||
corner nearest each sample it compares weighted colour distances over a 5x5
|
||||
neighbourhood, and where an edge runs along that corner's diagonal the corner
|
||||
is cut and takes the closer side neighbour, with an anti-aliased boundary one
|
||||
output pixel wide. Flat areas, straight edges and gradients stay crisp
|
||||
texels; diagonals and curves lose their staircase. It acts only where a
|
||||
texel spans two or more output pixels (high internal scale or a magnified
|
||||
texture), so a 1:1 draw is exactly nearest; cutouts and STP stay the centre
|
||||
texel's. It costs up to 13 texel fetches per pixel. JINC2 is not offered.
|
||||
`PSX_TEXTURE_FILTER=0..3` overrides it for one run (2 is the mod-only "stable
|
||||
world" minification filter) and the `texture_filter` TCP command switches it
|
||||
live. The launcher's toggle still offers nearest and bilinear.
|
||||
|
||||
The runtime clamps N per backend:
|
||||
|
||||
- Software and Vulkan stop at 4.
|
||||
@@ -834,6 +860,13 @@ has), and a title-allowed direct Rewind button (`psx_mod_allow_direct_shortcut`)
|
||||
is not claimed, so it reaches the game as if Rewind were off. The block clears
|
||||
at every mod/session reset; nothing changes for a title that never calls it.
|
||||
|
||||
A title that computes vibration but cannot send it over SIO to the presented
|
||||
pad (a NeGcon has no motors) can call `psx_mod_set_host_rumble(player, small,
|
||||
large)` every frame with DualShock motor values. The runtime drives the host
|
||||
pad with the louder of that and the guest's SIO motors; a value lapses 8
|
||||
VBlanks after the last call and all are cleared at every mod/session reset.
|
||||
`PSX_RUMBLE_TRACE=1` logs every change, headless included.
|
||||
|
||||
Bezel artwork is intentionally not a `[video]` key. It is exposed as the
|
||||
disabled-by-default `psx.presentation.bezel` mod package, which draws a
|
||||
user-selected image resource behind the game image in OpenGL letterbox or
|
||||
|
||||
@@ -1 +1 @@
|
||||
83f82f8b2daed07e75765c7ae4adb78cdb1a9fd7396fb879089c2352c8dc78a6
|
||||
e7c8c626e955b89839c5dda1d9990857d87a0042a1cdc1071bbdc42b754a4146
|
||||
|
||||
+1
-1
Submodule lib/recomp-net updated: b1eab87bf3...a48d355b15
@@ -776,8 +776,9 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
const auto mode = toml::find<std::string>(video, "texture_filtering");
|
||||
if (mode == "nearest") rt.video_texture_filter = 0;
|
||||
else if (mode == "bilinear") rt.video_texture_filter = 1;
|
||||
else if (mode == "xbr") rt.video_texture_filter = 3;
|
||||
else throw std::runtime_error(fmt::format(
|
||||
"[video] texture_filtering must be \"nearest\" or \"bilinear\": {}", mode));
|
||||
"[video] texture_filtering must be \"nearest\", \"bilinear\" or \"xbr\": {}", mode));
|
||||
}
|
||||
if (video.contains("fmv_filter")) {
|
||||
const auto mode = toml::find<std::string>(video, "fmv_filter");
|
||||
@@ -2869,6 +2870,7 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
const auto m = toml::find<std::string>(v, "texture_filtering");
|
||||
if (m == "nearest") { s.texture_filter = 0; s.has_texture_filter = true; }
|
||||
else if (m == "bilinear") { s.texture_filter = 1; s.has_texture_filter = true; }
|
||||
else if (m == "xbr") { s.texture_filter = 3; s.has_texture_filter = true; }
|
||||
});
|
||||
if (v.contains("fmv_filter")) try_get([&]{
|
||||
const auto m = toml::find<std::string>(v, "fmv_filter");
|
||||
@@ -3270,7 +3272,7 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
if (s.has_antialiasing)
|
||||
f << "antialiasing = " << (s.antialiasing ? "true" : "false") << "\n";
|
||||
if (s.has_texture_filter)
|
||||
f << "texture_filtering = \"" << (s.texture_filter ? "bilinear" : "nearest") << "\"\n";
|
||||
f << "texture_filtering = \"" << (s.texture_filter == 3 ? "xbr" : s.texture_filter ? "bilinear" : "nearest") << "\"\n";
|
||||
if (s.has_fmv_filter)
|
||||
f << "fmv_filter = \"" << video_fmv_filter_name(s.fmv_filter) << "\"\n";
|
||||
if (s.has_geometry_correction)
|
||||
|
||||
@@ -460,6 +460,7 @@ struct RuntimeConfig {
|
||||
int video_supersample_milli = 1000;
|
||||
|
||||
// texture_filtering: "nearest" (default, native PSX look) | "bilinear"
|
||||
// | "xbr" (3: edge-directed magnification, OpenGL only)
|
||||
// (smooths textures and 2D backgrounds). Stored as 0/1.
|
||||
int video_texture_filter = 0;
|
||||
|
||||
@@ -575,8 +576,8 @@ struct RuntimeConfig {
|
||||
int video_anisotropic_filtering = 1;
|
||||
// accurate_blending: one 8-bit basis (k*8) for every VRAM write so GL
|
||||
// semi-transparency truncates exactly like the PS1's 5-bit blend.
|
||||
// Default false (historical: fills/texels full-range). OpenGL only.
|
||||
bool video_accurate_blending = false;
|
||||
// Default true: PS1 colour/blend correctness. OpenGL only.
|
||||
bool video_accurate_blending = true;
|
||||
bool video_pgxp_color_correction = false;
|
||||
int video_pgxp_seam = 0;
|
||||
double video_pgxp_depth_threshold = 4096.0;
|
||||
|
||||
@@ -53,6 +53,9 @@ if(BUILD_TESTING)
|
||||
target_include_directories(netplay_sim_pad_cache_test PRIVATE include)
|
||||
target_compile_definitions(netplay_sim_pad_cache_test PRIVATE PSX_MAX_PLAYERS=4)
|
||||
add_test(NAME netplay_sim_pad_cache_test COMMAND netplay_sim_pad_cache_test)
|
||||
add_executable(netplay_fmv_confirm_test tests/test_netplay_fmv_confirm.c)
|
||||
target_include_directories(netplay_fmv_confirm_test PRIVATE include)
|
||||
add_test(NAME netplay_fmv_confirm_test COMMAND netplay_fmv_confirm_test)
|
||||
add_executable(netplay_local_view_test tests/test_netplay_local_view.c)
|
||||
target_include_directories(netplay_local_view_test PRIVATE include)
|
||||
add_test(NAME netplay_local_view_test COMMAND netplay_local_view_test)
|
||||
@@ -288,6 +291,18 @@ if(BUILD_TESTING)
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-blend-accuracy-results")
|
||||
set_tests_properties(gl_blend_accuracy_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
|
||||
# texture_filtering = "xbr": diagonals lose the staircase, straight
|
||||
# edges, flat areas, cutouts and 1:1 draws stay nearest; RT agrees.
|
||||
add_test(NAME gl_xbr_filter_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" --single
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_xbr.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-xbr-results")
|
||||
set_tests_properties(gl_xbr_filter_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
|
||||
unset(_gl_scale_sdl_inc)
|
||||
else()
|
||||
message(STATUS "gl_scale_invariance_test: not registered (needs macOS, Linux or MinGW, Python 3 and SDL3)")
|
||||
@@ -622,6 +637,11 @@ if(BUILD_TESTING)
|
||||
target_include_directories(psx_pad_apply_test PRIVATE include)
|
||||
add_test(NAME psx_pad_apply_test COMMAND psx_pad_apply_test)
|
||||
|
||||
# Title-supplied host rumble (psx_mod_set_host_rumble) merge and expiry.
|
||||
add_executable(host_rumble_test tests/test_host_rumble.c)
|
||||
target_include_directories(host_rumble_test PRIVATE include)
|
||||
add_test(NAME host_rumble_test COMMAND host_rumble_test)
|
||||
|
||||
add_executable(controller_type_test tests/test_controller_type.cpp)
|
||||
target_include_directories(controller_type_test PRIVATE include)
|
||||
add_test(NAME controller_type_test COMMAND controller_type_test)
|
||||
@@ -1059,6 +1079,10 @@ if(BUILD_TESTING)
|
||||
../recompiler/lib/toml11)
|
||||
add_test(NAME text_xlate_fix_only_test COMMAND text_xlate_fix_only_test)
|
||||
|
||||
add_executable(netplay_peer_frontier_test tests/test_netplay_peer_frontier.c)
|
||||
target_include_directories(netplay_peer_frontier_test PRIVATE include)
|
||||
add_test(NAME netplay_peer_frontier_test COMMAND netplay_peer_frontier_test)
|
||||
|
||||
add_executable(netplay_content_gate_test tests/test_netplay_content_gate.c)
|
||||
target_include_directories(netplay_content_gate_test PRIVATE include)
|
||||
add_test(NAME netplay_content_gate_test COMMAND netplay_content_gate_test)
|
||||
@@ -1286,6 +1310,12 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME render_pass_plan_test COMMAND render_pass_plan_test)
|
||||
|
||||
add_executable(mod_internal_resolution_test
|
||||
tests/test_mod_internal_resolution.c
|
||||
src/mod_internal_resolution.c)
|
||||
target_include_directories(mod_internal_resolution_test PRIVATE include)
|
||||
add_test(NAME mod_internal_resolution_test COMMAND mod_internal_resolution_test)
|
||||
|
||||
# Motion kit and per-frame driver for render-pass plugins: blending,
|
||||
# identity matching, limits and the crossfade fallback, on mock RAM.
|
||||
add_executable(render_pass_motion_test
|
||||
|
||||
@@ -107,6 +107,12 @@ enum {
|
||||
BS_SEC_SIO = 0x0D, /* SIO regs + pad-config FSM + memcard FSM */
|
||||
BS_SEC_DIRTY = 0x0E, /* dirty-RAM page bitmap (guest-written code pages) */
|
||||
BS_SEC_MDEC = 0x0F, /* MDEC command/FIFOs/quant/scale (FMV decode resume) */
|
||||
BS_SEC_SPUCD = 0x15, /* SPU CD-input frames queued (u32 n + n*(l,r) s16) */
|
||||
BS_SEC_CPUTIM = 0x14, /* [timing] in-flight CPU stall state, relative to
|
||||
the clock: GTE / mul-div cycles left, the load-
|
||||
delay give-back (read_absorb[33], which, fudge,
|
||||
ld_which_t, ld_absorb). Absent => anchored at the
|
||||
restored clock (pre-0x14 behaviour). */
|
||||
BS_SEC_GCS = 0x13, /* [timing] guest_cycle_scale: carried fraction + gate
|
||||
state (3 u32). Written only when the title's
|
||||
scale is not 1, so faithful states are unchanged;
|
||||
|
||||
@@ -300,7 +300,7 @@ int gl_renderer_anisotropy(void);
|
||||
/* [video] accurate_blending: store every VRAM write in the k*8/255 basis so
|
||||
* 8-bit GL blending truncated to 5 bits is exactly the PS1's semi-transparency
|
||||
* arithmetic (whites of fills and textures become 248 instead of 255, as
|
||||
* uploads and untextured primitives already are). Off by default. */
|
||||
* uploads and untextured primitives already are). On by default. */
|
||||
void gl_renderer_set_accurate_blending(int on);
|
||||
int gl_renderer_accurate_blending(void);
|
||||
int gl_renderer_get_pgxp_depth(void);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef PSX_MOD_INTERNAL_RESOLUTION_H
|
||||
#define PSX_MOD_INTERNAL_RESOLUTION_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Runtime session plumbing; plugins use mod_plugins.h. Zero means no request. */
|
||||
uint32_t psx_mod_internal_resolution_request(void);
|
||||
void psx_mod_internal_resolution_reset(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -439,6 +439,13 @@ int psx_mod_set_adaptive_display_aspect(uint32_t max_numerator,
|
||||
* title's setting before activation.
|
||||
*/
|
||||
int psx_mod_set_widescreen_hud_size(int proportional);
|
||||
/* Request an internal rendering height for this mod session (120..8192).
|
||||
* The existing resolution resolver selects an integer raster scale and the
|
||||
* backend applies its normal allocation limits. Zero clears the request.
|
||||
* Player settings are preserved, explicit PSX_INTERNAL_RESOLUTION overrides
|
||||
* win, and each session clears this request before activating its committed
|
||||
* mod plan. */
|
||||
int psx_mod_set_internal_resolution(uint32_t target_lines);
|
||||
/*
|
||||
* Set the wall-clock cadence of simulated guest VBlanks. A value of zero
|
||||
* removes frontend pacing; 60 and higher request that many native guest
|
||||
@@ -1014,6 +1021,17 @@ int psx_mod_allow_direct_shortcut(uint32_t shortcut);
|
||||
* title's state is fine). Cleared at every mod/session reset. */
|
||||
void psx_mod_set_rewind_blocked(int blocked);
|
||||
|
||||
/* Title-supplied host rumble for one guest port (0-based): DualShock motor
|
||||
* values (small: 0 off / nonzero on, large: 0..255) for a title that
|
||||
* computes vibration but cannot send it over SIO to the presented pad (e.g. a
|
||||
* NeGcon has no motors). The runtime drives the host pad with the louder of
|
||||
* this and the guest's own SIO motors. Online only the local seat's value
|
||||
* is used (this peer's local port, on its pad) and calls during a rollback resim are
|
||||
* ignored, so a netplay = "host_output" plugin can call it for every seat.
|
||||
* Opt-in: call every frame while it applies; a value lapses 8 VBlanks after the last call, and all are cleared
|
||||
* at every mod/session reset (psx_host_rumble.h). Returns 0 for a bad seat. */
|
||||
int psx_mod_set_host_rumble(uint32_t player, uint32_t small, uint32_t large);
|
||||
|
||||
/* Local P1 mouse policy. The runtime delivers ordered events on the SDL owner
|
||||
* (main) thread, owns relative capture and folds the resulting right-stick
|
||||
* bytes after native input/presentation (and after any controller source),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/* netplay_fmv_confirm.h - post-movie agreement counting for rollback netplay.
|
||||
*
|
||||
* After a movie, both peers must agree for RB_FMV_LOCKSTEP_CONFIRM ticks
|
||||
* before prediction is unlocked. Agreement is measured in hash-confirmed
|
||||
* ticks: each newly confirmed tick past the movie's end counts once, however
|
||||
* late its confirmation arrives. A confirmation is always about one RTT
|
||||
* behind the tick it confirms, so testing "is sim-1 confirmed yet" every tick
|
||||
* never succeeds on the peer that runs ahead. */
|
||||
#ifndef PSX_NETPLAY_FMV_CONFIRM_H
|
||||
#define PSX_NETPLAY_FMV_CONFIRM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Confirmed ticks gained when the confirmed frontier moves from `from` (the
|
||||
* highest tick already counted, or the movie's end) to `resolved`. */
|
||||
static inline uint32_t rb_fmv_confirm_gain(uint32_t from, uint32_t resolved)
|
||||
{
|
||||
return resolved > from ? resolved - from : 0u;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,111 @@
|
||||
/* Per-seat peer RESOLVED frontiers for rollback episodes.
|
||||
*
|
||||
* A follower can only replay from a snapshot at or below the frontier it
|
||||
* advertised. With two seats there is one follower and one number. With more,
|
||||
* the initiator must open at or below the SLOWEST follower's frontier: taking
|
||||
* the highest advert (the old single watermark) let the fastest peer stand in
|
||||
* for the others, and on a 3-seat WAN race the host opened load=2400 against
|
||||
* followers at 2344 and 2319 -- every follower refused, the episodes aborted
|
||||
* and the session diverged before the race started.
|
||||
*
|
||||
* Header-only and free of runtime state so the bookkeeping is unit-tested
|
||||
* (tests/test_netplay_peer_frontier.c). */
|
||||
#ifndef NETPLAY_PEER_FRONTIER_H
|
||||
#define NETPLAY_PEER_FRONTIER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#define NETPLAY_PEER_FRONTIER_SEATS 16u
|
||||
|
||||
typedef struct NetplayPeerFrontier {
|
||||
uint32_t through[NETPLAY_PEER_FRONTIER_SEATS];
|
||||
} NetplayPeerFrontier;
|
||||
|
||||
static inline void netplay_peer_frontier_reset(NetplayPeerFrontier *f)
|
||||
{
|
||||
if (f)
|
||||
memset(f, 0, sizeof(*f));
|
||||
}
|
||||
|
||||
/* A RESOLVED advert from `seat`. Watermark: never lowers. */
|
||||
static inline void netplay_peer_frontier_note(NetplayPeerFrontier *f, int seat,
|
||||
uint32_t through)
|
||||
{
|
||||
if (!f || seat < 0 || (uint32_t)seat >= NETPLAY_PEER_FRONTIER_SEATS)
|
||||
return;
|
||||
if (through > f->through[seat])
|
||||
f->through[seat] = through;
|
||||
}
|
||||
|
||||
/* Evidence above `tick` was invalidated (realign) or a follower proved it
|
||||
* cannot follow above it (NACK): no seat may stay above it. */
|
||||
static inline void netplay_peer_frontier_clamp(NetplayPeerFrontier *f, uint32_t tick)
|
||||
{
|
||||
uint32_t i;
|
||||
if (!f)
|
||||
return;
|
||||
for (i = 0; i < NETPLAY_PEER_FRONTIER_SEATS; ++i)
|
||||
if (f->through[i] > tick)
|
||||
f->through[i] = tick;
|
||||
}
|
||||
|
||||
/* Frontier every seat in `seats` has reached: the lowest, and 0 while any of
|
||||
* them has not advertised yet. */
|
||||
static inline uint32_t netplay_peer_frontier_min(const NetplayPeerFrontier *f,
|
||||
uint32_t seats)
|
||||
{
|
||||
uint32_t i, lo = 0xffffffffu;
|
||||
int any = 0;
|
||||
if (!f)
|
||||
return 0u;
|
||||
for (i = 0; i < NETPLAY_PEER_FRONTIER_SEATS; ++i) {
|
||||
if (!(seats & (1u << i)))
|
||||
continue;
|
||||
any = 1;
|
||||
if (f->through[i] < lo)
|
||||
lo = f->through[i];
|
||||
}
|
||||
return any ? lo : 0u;
|
||||
}
|
||||
|
||||
/* §55 bisect cap: after a baseline mismatch at load L the next load must be
|
||||
* below L. Once every peer has hash-confirmed a tick at or past L (agreed
|
||||
* ADVANCE from a live confirm) the fork is healed and the cap is stale; left
|
||||
* armed it refused every later episode, because the only loads it allowed had
|
||||
* long left the snapshot ring (4-seat race: cap 1928, agreed 3136, ring
|
||||
* 2656..3166 -> "no confirmed snap" and a permanent fork from 3168). */
|
||||
static inline int netplay_fork_cap_retired(uint32_t fork_cap, uint32_t agreed_through)
|
||||
{
|
||||
return fork_cap > 0u && agreed_through >= fork_cap;
|
||||
}
|
||||
|
||||
/* Concurrent BEGIN while an episode is active. Wire epoch ids carry their
|
||||
* initiator in the low bits (`slot_mask`); the lower initiator slot wins.
|
||||
* Returns +1 when the incoming episode wins (yield ours if still in the
|
||||
* handshake, then follow it), -1 when ours wins (drop the incoming BEGIN),
|
||||
* 0 when the rule does not apply (same epoch, or a follower seeing another
|
||||
* epoch from the initiator it already follows -- an abort-and-reopen).
|
||||
*
|
||||
* A follower applies it too: with three or more seats two guests can both
|
||||
* BEGIN on the same mispredict, and a follower that followed whichever
|
||||
* arrived first split the seats across epochs (4-seat WAN: host following
|
||||
* slot 3's epoch while slot 1's won; every seat then waited on baselines
|
||||
* that never came). */
|
||||
static inline int netplay_rb_concurrent_begin(uint32_t ours, uint32_t theirs,
|
||||
int following, uint32_t slot_mask)
|
||||
{
|
||||
uint32_t our_slot = ours & slot_mask;
|
||||
uint32_t their_slot = theirs & slot_mask;
|
||||
if (ours == theirs)
|
||||
return 0;
|
||||
if (following && our_slot == their_slot)
|
||||
return 0;
|
||||
if (their_slot < our_slot)
|
||||
return 1;
|
||||
if (their_slot > our_slot)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* NETPLAY_PEER_FRONTIER_H */
|
||||
@@ -0,0 +1,44 @@
|
||||
/* psx_host_rumble.h - title-supplied host rumble (psx_mod_set_host_rumble).
|
||||
*
|
||||
* Some titles compute vibration for every pad but only send it over SIO to a
|
||||
* DualShock (R4 with a presented NeGcon keeps running its vibration patterns
|
||||
* and writes them nowhere useful). A trusted mod can hand the runtime the
|
||||
* DualShock-equivalent motor values instead; the runtime forwards the louder
|
||||
* of that and the guest's own SIO motors to the host pad. A value lapses
|
||||
* PSX_HOST_RUMBLE_TTL VBlanks after the last call, so a mod that stops
|
||||
* calling (menus, unloaded) can never leave a pad vibrating. Pure, so tests
|
||||
* link it directly. Nothing changes for a title that never calls it. */
|
||||
#ifndef PSX_HOST_RUMBLE_H
|
||||
#define PSX_HOST_RUMBLE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define PSX_HOST_RUMBLE_TTL 8u
|
||||
|
||||
typedef struct PsxHostRumbleSlot {
|
||||
uint8_t small; /* DualShock small motor: 0 off, nonzero on */
|
||||
uint8_t large; /* DualShock large motor strength 0..255 */
|
||||
uint8_t valid;
|
||||
uint32_t stamp; /* VBlank of the last call */
|
||||
} PsxHostRumbleSlot;
|
||||
|
||||
static inline void psx_host_rumble_set(PsxHostRumbleSlot *slot, uint32_t now,
|
||||
uint32_t small, uint32_t large) {
|
||||
if (!slot) return;
|
||||
slot->small = small ? 1u : 0u;
|
||||
slot->large = (uint8_t)(large > 255u ? 255u : large);
|
||||
slot->valid = 1u;
|
||||
slot->stamp = now;
|
||||
}
|
||||
|
||||
/* Merge into the guest's SIO motor values (in/out). */
|
||||
static inline void psx_host_rumble_merge(const PsxHostRumbleSlot *slot,
|
||||
uint32_t now, uint8_t *small,
|
||||
uint8_t *large) {
|
||||
if (!slot || !slot->valid || !small || !large) return;
|
||||
if ((uint32_t)(now - slot->stamp) >= PSX_HOST_RUMBLE_TTL) return;
|
||||
if (slot->small && !*small) *small = 0xFFu;
|
||||
if (slot->large > *large) *large = slot->large;
|
||||
}
|
||||
|
||||
#endif /* PSX_HOST_RUMBLE_H */
|
||||
@@ -47,11 +47,15 @@ void psx_netplay_rb_cold_reset(void);
|
||||
* without longjmp. Call psx_netplay_rb_flush_resume() afterward from a C BB-edge
|
||||
* (or after all C++ RAII in the vblank path has been destroyed). */
|
||||
void psx_netplay_rb_poll(struct CPUState *cpu, uint32_t resume_pc);
|
||||
/* psx_netplay.c: the one resume-PC rule for Live and Replay tick snaps. */
|
||||
uint32_t psx_netplay_snap_resume_hint(const struct CPUState *cpu);
|
||||
|
||||
/* If a baseline snap was applied without resume, longjmp to the scheduler
|
||||
* (same as savestate_poll). No-op when nothing is pending. Never returns on
|
||||
* success. */
|
||||
void psx_netplay_rb_flush_resume(void);
|
||||
/* psx_netplay.c: admit the Live tick a realign resumes into (1 = admitted). */
|
||||
int psx_netplay_live_realign_admit(void);
|
||||
|
||||
/* 1 after flush_resume longjmp until finish_frame / abort — top-level
|
||||
* dispatch has no native call chain under the resume PC. */
|
||||
|
||||
@@ -476,6 +476,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/game_options.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_speed.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_pgxp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_internal_resolution.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_bezel.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_ram.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_hd_textures.c
|
||||
|
||||
@@ -110,6 +110,10 @@ static uint32_t boot_state_layout_cookie(void) {
|
||||
#define CPU_REGS_WIRE_BYTES (524u)
|
||||
/* Timer wire: 3*u16 + 3*u32 + 3*u16 + 3*i32 + 3*u32 = 48 bytes (no pad holes). */
|
||||
#define TIMER_REGS_WIRE_BYTES (48u)
|
||||
/* CPU stall wire: 2*u32 + 33 + 3*u8 + u32 = 48 bytes. */
|
||||
#define BS_CPUTIM_BYTES (48u)
|
||||
/* Set by a BS_SEC_CPUTIM load; psx_cycles_resync_after_restore consumes it. */
|
||||
int g_boot_state_cpu_timing_restored;
|
||||
|
||||
/* ---- deferred capture state (armed before first boot, fired at handoff) ---- */
|
||||
static char s_capture_path[512];
|
||||
@@ -322,8 +326,21 @@ static int write_cpu_section(BsOut* o, const CPUState* cpu) {
|
||||
return 0;
|
||||
for (int i = 0; i < 32; i++)
|
||||
if (!pst_w_u32(&w, cpu->cop0[i])) return 0;
|
||||
for (int i = 0; i < 32; i++)
|
||||
if (!pst_w_u32(&w, cpu->gte_data[i])) return 0;
|
||||
for (int i = 0; i < 32; i++) {
|
||||
uint32_t v = cpu->gte_data[i];
|
||||
/* LZCR is derived from LZCS (what MFC2 31 returns, and what a load
|
||||
* recomputes). Serialize the derived value: the raw backing stays 0
|
||||
* until the first LZCS write, so a live save and a replayed save of
|
||||
* the same tick differed here although no guest could tell. */
|
||||
if (i == 31) {
|
||||
uint32_t bits = (cpu->gte_data[30] & 0x80000000u) ? ~cpu->gte_data[30]
|
||||
: cpu->gte_data[30];
|
||||
v = 0;
|
||||
if (bits == 0) v = 32;
|
||||
else while (!(bits & 0x80000000u)) { bits <<= 1; ++v; }
|
||||
}
|
||||
if (!pst_w_u32(&w, v)) return 0;
|
||||
}
|
||||
for (int i = 0; i < 32; i++)
|
||||
if (!pst_w_u32(&w, cpu->gte_ctrl[i])) return 0;
|
||||
if (w.written != CPU_REGS_WIRE_BYTES) return 0;
|
||||
@@ -399,9 +416,27 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
|
||||
h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH;
|
||||
h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG;
|
||||
h.codegen_ver = BOOT_STATE_IMPLEMENTATION_KEY;
|
||||
h.section_count = 17 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u) +
|
||||
h.section_count = 18 + (cpu ? 1u : 0u) + (psx_mod_memory_snapshot_bytes() ? 1u : 0u) +
|
||||
(psx_guest_cycle_scale_config() != 1u ? 1u : 0u);
|
||||
|
||||
/* Publish deferred cycle charges before ANY section: IRQ (cycles since
|
||||
* VBlank) and the root counters were written before the old flush point
|
||||
* (just ahead of CLOCK), so they lagged the saved clock by whatever batch
|
||||
* was pending -- a host-path quantity (live vs rollback replay). */
|
||||
psx_cyc_batch_flush();
|
||||
/* Devices (root counters, VBlank phase, CD/SPU/DMA) are serviced lazily
|
||||
* up to the next event deadline, so between deadlines they lag the clock
|
||||
* by a host-path amount. A load re-anchors them at the restored clock,
|
||||
* dropping that lag: live and replay saves of one tick then differed in
|
||||
* cycles_since_vblank and the counters at the same clock. Bring every
|
||||
* device to the clock first so the snap is the canonical machine at it
|
||||
* (service_to_now replays the exact event-bounded timeline). */
|
||||
psx_devices_service_to_now();
|
||||
/* The SPU renders in whole samples when the host audio pump or a guest
|
||||
* SPU access asks; between those it lags the clock by a path-dependent
|
||||
* amount (live pumps at VBlank, rollback replay suppresses output). */
|
||||
{ extern void spu_catch_up(void); spu_catch_up(); }
|
||||
|
||||
ok = write_header_le(o, &h);
|
||||
|
||||
if (ok) ok = write_cpu_section(o, cpu);
|
||||
@@ -498,6 +533,31 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
|
||||
ok = pst_w_u32(&w, fntrace_is_game_started() ? 1u : 0u) &&
|
||||
write_section(o, BS_SEC_GAME_START, handoff, sizeof handoff);
|
||||
}
|
||||
if (ok) {
|
||||
extern uint32_t spu_cd_snapshot_bytes(void);
|
||||
extern void spu_cd_snapshot_write(uint8_t *p);
|
||||
ok = write_module_section(o, BS_SEC_SPUCD, spu_cd_snapshot_bytes, spu_cd_snapshot_write);
|
||||
}
|
||||
if (ok && cpu) {
|
||||
/* In-flight CPU stalls, relative to the clock. They used to be
|
||||
* re-anchored at the restored clock on every load, so a GTE / mul-div
|
||||
* op or load give-back in flight at the snap point cost the live run
|
||||
* its remaining stall but the replay nothing: replaying a snap with
|
||||
* the same rows ran a few cycles short of the live tick
|
||||
* (PSX_RB_DET_SELFTEST: IRQ/TIMER drift at equal clock). */
|
||||
uint8_t tb[BS_CPUTIM_BYTES];
|
||||
PstW w;
|
||||
uint64_t now = psx_cycle_count;
|
||||
uint32_t gte_rem = cpu->gte_ts_done > now ? (uint32_t)(cpu->gte_ts_done - now) : 0u;
|
||||
uint32_t md_rem = cpu->muldiv_ts_done > now ? (uint32_t)(cpu->muldiv_ts_done - now) : 0u;
|
||||
pst_w_init(&w, tb, sizeof tb);
|
||||
ok = pst_w_u32(&w, gte_rem) && pst_w_u32(&w, md_rem) &&
|
||||
pst_w_bytes(&w, cpu->read_absorb, sizeof cpu->read_absorb) &&
|
||||
pst_w_u8(&w, cpu->read_absorb_which) && pst_w_u8(&w, cpu->read_fudge) &&
|
||||
pst_w_u8(&w, cpu->ld_which_t) && pst_w_u32(&w, cpu->ld_absorb) &&
|
||||
w.written == BS_CPUTIM_BYTES &&
|
||||
write_section(o, BS_SEC_CPUTIM, tb, sizeof tb);
|
||||
}
|
||||
if (ok && psx_guest_cycle_scale_config() != 1u) {
|
||||
/* Rollback re-simulation must resume with the same carried fraction
|
||||
* and gate verdicts, or scaled charges land a cycle apart. */
|
||||
@@ -725,6 +785,26 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
|
||||
case BS_SEC_GAME_START:
|
||||
fntrace_restore_game_started(p[0]);
|
||||
return 1;
|
||||
case BS_SEC_SPUCD: {
|
||||
extern int spu_cd_snapshot_read(const uint8_t *p, uint32_t len);
|
||||
return spu_cd_snapshot_read(p, len);
|
||||
}
|
||||
case BS_SEC_CPUTIM: {
|
||||
PstR r;
|
||||
uint32_t gte_rem, md_rem;
|
||||
if (len != BS_CPUTIM_BYTES || !cpu) return 0;
|
||||
pst_r_init(&r, p, len);
|
||||
if (!pst_r_u32(&r, >e_rem) || !pst_r_u32(&r, &md_rem) ||
|
||||
!pst_r_bytes(&r, cpu->read_absorb, sizeof cpu->read_absorb) ||
|
||||
!pst_r_u8(&r, &cpu->read_absorb_which) || !pst_r_u8(&r, &cpu->read_fudge) ||
|
||||
!pst_r_u8(&r, &cpu->ld_which_t) || !pst_r_u32(&r, &cpu->ld_absorb))
|
||||
return 0;
|
||||
/* CLOCK precedes this section, so psx_cycle_count is restored. */
|
||||
cpu->gte_ts_done = psx_cycle_count + gte_rem;
|
||||
cpu->muldiv_ts_done = psx_cycle_count + md_rem;
|
||||
g_boot_state_cpu_timing_restored = 1;
|
||||
return 1;
|
||||
}
|
||||
case BS_SEC_GCS: {
|
||||
PstR r;
|
||||
uint32_t g[3];
|
||||
@@ -795,6 +875,8 @@ static int validate_section(uint32_t tag, const uint8_t* p, uint32_t len) {
|
||||
return len == 4u && p[0] <= 1u && !p[1] && !p[2] && !p[3];
|
||||
case BS_SEC_ICACHE: return len == 1024u * 4u;
|
||||
case BS_SEC_GCS: return len == PSX_GCS_SNAPSHOT_BYTES;
|
||||
case BS_SEC_CPUTIM: return len == BS_CPUTIM_BYTES;
|
||||
case BS_SEC_SPUCD: return len >= 4u && (len - 4u) % 4u == 0u;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8638,6 +8638,15 @@ static void handle_accurate_blending(int id, const char *json)
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d}", id, gl_renderer_accurate_blending());
|
||||
}
|
||||
|
||||
/* texture_filter: {"cmd":"texture_filter"} reads, {"cmd":"texture_filter",
|
||||
* "mode":0..3} sets it live (nearest, bilinear, stable world, xbr). */
|
||||
static void handle_texture_filter(int id, const char *json)
|
||||
{
|
||||
int mode = json_get_int(json, "mode", -1);
|
||||
if (mode >= 0) gr_set_texture_filter(mode);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d}", id, gr_texture_filter());
|
||||
}
|
||||
|
||||
static void handle_frame_perf(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -15864,6 +15873,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "fmv_chroma", handle_fmv_chroma },
|
||||
{ "texture_lod", handle_texture_lod },
|
||||
{ "accurate_blending", handle_accurate_blending },
|
||||
|
||||
{ "texture_filter", handle_texture_filter },
|
||||
{ "gl_ws_ablate", handle_gl_ws_ablate },
|
||||
{ "gl_interp", handle_gl_interp },
|
||||
{ "render_pass_stats", handle_render_pass_stats },
|
||||
|
||||
@@ -908,8 +908,8 @@ static int s_tex_lod = 0, s_tex_aniso = 1;
|
||||
* different bases and truncating to 5 bits at readback is off by one in
|
||||
* places (e.g. B+F/4, B/2+F/2 with odd sums). On, every VRAM write uses
|
||||
* k*8, so GL's 8-bit blend followed by >>3 is exactly the PS1's 5-bit
|
||||
* arithmetic. Off (default) keeps the historical bases. */
|
||||
static int s_accurate_blend = 0;
|
||||
* arithmetic. On by default; off is the historical diagnostic path. */
|
||||
static int s_accurate_blend = 1;
|
||||
static void c5_to_float(uint16_t c, float *r, float *g, float *b) {
|
||||
if (s_accurate_blend) {
|
||||
*r = (float)((c & 0x1F) << 3) / 255.0f;
|
||||
@@ -1938,7 +1938,7 @@ static const char *TEX_FS =
|
||||
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
||||
"uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n"
|
||||
"uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
|
||||
"uniform int u_filter; /* bits 0-3: 0 nearest, 1 bilinear, 2 stable; bit 4: LOD */\n"
|
||||
"uniform int u_filter; /* bits 0-3: 0 nearest, 1 bilinear, 2 stable, 3 xbr; bit 4: LOD */\n"
|
||||
"uniform int u_aniso; /* [video] anisotropic_filtering, 1..16 */\n"
|
||||
"uniform float u_shift;\n"
|
||||
"int vram_at(int x, int y){\n"
|
||||
@@ -2034,6 +2034,47 @@ static const char *TEX_FS =
|
||||
" }\n"
|
||||
" return sum.a > 0.0 ? sum.rgb / sum.a : base;\n"
|
||||
"}\n"
|
||||
/* [video] texture_filtering = "xbr": edge-directed magnification after
|
||||
* Hyllian's xBR (MIT; notice in THIRD_PARTY_ATTRIBUTION.md): the same
|
||||
* E/F/H/I neighbourhood and 4x-weighted edge sums, adapted to CLUT
|
||||
* texels and an anti-aliased corner cut. For the
|
||||
* texel corner nearest the sample, two weighted colour-distance sums
|
||||
* across a 5x5 neighbourhood decide whether an edge runs along the
|
||||
* corner's anti-diagonal; if so, the corner is cut by that diagonal and
|
||||
* takes the closer of the two side neighbours, with an anti-aliased
|
||||
* boundary one output pixel wide. Smooth gradients and straight edges are
|
||||
* left as nearest texels, so pixel art keeps its crisp blocks while
|
||||
* diagonals and curves lose their staircase. Transparent (0) texels never
|
||||
* supply a colour; cutout and STP stay the centre texel's. */
|
||||
"vec3 xbr_c(int raw){ return col5(raw); }\n"
|
||||
"float xbr_d(int a, int b){\n"
|
||||
" vec3 x = xbr_c(a), y = xbr_c(b);\n"
|
||||
" vec3 dv = x - y;\n"
|
||||
" float yy = dot(dv, vec3(0.299, 0.587, 0.114));\n"
|
||||
" float u = dot(dv, vec3(-0.169, -0.331, 0.5)), v = dot(dv, vec3(0.5, -0.419, -0.081));\n"
|
||||
" return 48.0*abs(yy) + 7.0*abs(u) + 6.0*abs(v) + (((a==0)!=(b==0)) ? 64.0 : 0.0);\n"
|
||||
"}\n"
|
||||
"int xbr_at(ivec2 e, ivec2 s, int x, int y){ return stable_texel(e + ivec2(x*s.x, y*s.y)); }\n"
|
||||
"vec3 xbr_sample(vec2 uv, int e_raw, vec2 fw){\n"
|
||||
" ivec2 e = ivec2(floor(uv)); vec2 f = fract(uv);\n"
|
||||
" ivec2 s = ivec2(f.x < 0.5 ? -1 : 1, f.y < 0.5 ? -1 : 1);\n"
|
||||
" vec2 l = vec2(s.x > 0 ? f.x : 1.0 - f.x, s.y > 0 ? f.y : 1.0 - f.y); /* 0.5..1 toward the corner */\n"
|
||||
" int E=e_raw;\n"
|
||||
" if (max(fw.x, fw.y) >= 0.5) return xbr_c(E); /* under 2x: stay nearest, no fetches */\n"
|
||||
" int F=xbr_at(e,s,1,0), H=xbr_at(e,s,0,1);\n"
|
||||
" if (E == F || E == H) return xbr_c(E);\n"
|
||||
" int I=xbr_at(e,s,1,1);\n"
|
||||
" int B=xbr_at(e,s,0,-1), C=xbr_at(e,s,1,-1), D=xbr_at(e,s,-1,0), G=xbr_at(e,s,-1,1);\n"
|
||||
" int F4=xbr_at(e,s,2,0), I4=xbr_at(e,s,2,1), H5=xbr_at(e,s,0,2), I5=xbr_at(e,s,1,2);\n"
|
||||
" float we = xbr_d(E,C)+xbr_d(E,G)+xbr_d(I,H5)+xbr_d(I,F4)+4.0*xbr_d(H,F);\n"
|
||||
" float wi = xbr_d(H,D)+xbr_d(H,I5)+xbr_d(F,I4)+xbr_d(F,B)+4.0*xbr_d(E,I);\n"
|
||||
" if (!(we < wi)) return xbr_c(E);\n"
|
||||
" int px = xbr_d(E,F) <= xbr_d(E,H) ? F : H;\n"
|
||||
" if (px == 0) return xbr_c(E);\n"
|
||||
" float w = max(fw.x + fw.y, 1e-3);\n"
|
||||
" float a = clamp((l.x + l.y - 1.5) / w + 0.5, 0.0, 1.0);\n"
|
||||
" return mix(xbr_c(E), xbr_c(px), a);\n"
|
||||
"}\n"
|
||||
"void main(){\n"
|
||||
" int stp; vec3 rgb;\n"
|
||||
" int fmode = u_filter & 15;\n"
|
||||
@@ -2063,6 +2104,12 @@ static const char *TEX_FS =
|
||||
" if (raw == 0) discard;\n"
|
||||
" rgb = col5(raw);\n"
|
||||
" stp = (raw >> 15) & 1;\n"
|
||||
" } else if (fmode == 3) {\n"
|
||||
" /* The nearest texel stays the PS1 one (floor of the unshifted uv). */\n"
|
||||
" int raw = stable_texel(ivec2(floor(uv)));\n"
|
||||
" if (raw == 0) discard;\n"
|
||||
" stp = (raw >> 15) & 1;\n"
|
||||
" rgb = xbr_sample(uv, raw, abs(dx) + abs(dy));\n"
|
||||
" } else if (fmode == 2) {\n"
|
||||
" uv+=vec2(u_shift); int raw=stable_texel(ivec2(floor(uv)));\n"
|
||||
" if(raw==0) discard; stp=(raw>>15)&1;\n"
|
||||
@@ -4985,7 +5032,7 @@ void gl_renderer_set_accurate_blending(int on) {
|
||||
if (s_ctx) accurate_apply_uniforms();
|
||||
}
|
||||
int gl_renderer_accurate_blending(void) { return s_accurate_blend; }
|
||||
static void glb_set_texture_filter(int b) { s_tex_filter = b >= 0 && b <= 2 ? b : 0; sw_set_texture_filter(b != 0); }
|
||||
static void glb_set_texture_filter(int b) { s_tex_filter = b >= 0 && b <= 3 ? b : 0; sw_set_texture_filter(b != 0); }
|
||||
static int glb_texture_filter(void) { return s_tex_filter; }
|
||||
|
||||
static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); }
|
||||
|
||||
+75
-28
@@ -11,6 +11,7 @@
|
||||
#include "mod_controller_source.h"
|
||||
#include "window_size.h" /* default game-window size */
|
||||
#include "internal_resolution.h" /* Settings -> Display -> Internal resolution */
|
||||
#include "mod_internal_resolution.h"
|
||||
#include "dynamic_resolution.h" /* [video] dynamic_resolution: the step controller */
|
||||
#include "render_thread.h" /* rt_get_stats: queue backpressure (dynres) */
|
||||
#include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */
|
||||
@@ -64,6 +65,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "frame_pacing.h"
|
||||
#include "latency_ring.h"
|
||||
#include "sio.h"
|
||||
#include "psx_host_rumble.h"
|
||||
#ifndef PSX_MAX_PLAYERS
|
||||
#define PSX_MAX_PLAYERS 2
|
||||
#endif
|
||||
@@ -81,6 +83,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "host_time.h"
|
||||
#if defined(PSX_HAS_RECOMP_NET)
|
||||
#include "recomp_net/auth.h"
|
||||
#include "recomp_netplay_account.h" /* shared recomp-ui account sync */
|
||||
#include "recomp_net/host_relay.h" /* RNetHostRelayStatus for the launcher relay line */
|
||||
#include "recomp_net/chat_filter.h" /* chat profanity mask, LAN rooms too */
|
||||
#endif
|
||||
@@ -1283,7 +1286,7 @@ static int g_video_pgxp_depth_buffer = 0;
|
||||
static int g_video_fmv_chroma = 0; /* [video] fmv_chroma_smoothing / PSX_FMV_CHROMA */
|
||||
/* [video] texture_lod / anisotropic_filtering; PSX_TEXTURE_LOD / PSX_ANISO. */
|
||||
static int g_video_texture_lod = 0, g_video_aniso = 1;
|
||||
static int g_video_accurate_blending = 0; /* [video] accurate_blending / PSX_ACCURATE_BLENDING */
|
||||
static int g_video_accurate_blending = 1; /* [video] accurate_blending / PSX_ACCURATE_BLENDING */
|
||||
static int g_video_pgxp_color_correction = 0;
|
||||
static int g_video_pgxp_seam = 0;
|
||||
static float g_video_pgxp_depth_threshold = 4096.0f;
|
||||
@@ -1366,11 +1369,11 @@ static int video_scale_ceiling(void) {
|
||||
return g_video_renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE;
|
||||
}
|
||||
|
||||
/* The preset in effect this run: the environment override, else the
|
||||
* configured one. */
|
||||
/* Session priority: environment override, active mod request, player setting. */
|
||||
static int effective_internal_resolution(void) {
|
||||
const uint32_t mod_lines = psx_mod_internal_resolution_request();
|
||||
return g_video_internal_res_env != PSX_IR_UNSET ? g_video_internal_res_env
|
||||
: g_video_internal_res;
|
||||
: mod_lines ? (int)mod_lines : g_video_internal_res;
|
||||
}
|
||||
|
||||
/* Resolve the Internal resolution preset into g_video_scale. display_px_h is
|
||||
@@ -1428,6 +1431,27 @@ extern "C" int psx_mod_allow_direct_shortcut(uint32_t shortcut) {
|
||||
extern "C" void psx_mod_set_rewind_blocked(int blocked) {
|
||||
psx_rewind_set_title_blocked(blocked);
|
||||
}
|
||||
|
||||
/* psx_mod_set_host_rumble: per-seat title rumble, stamped with the rumble
|
||||
* VBlank so a value the mod stops refreshing lapses (psx_host_rumble.h). */
|
||||
static PsxHostRumbleSlot g_mod_rumble[PSX_MAX_PLAYERS];
|
||||
static uint32_t g_rumble_vblank = 0;
|
||||
|
||||
extern "C" int psx_mod_set_host_rumble(uint32_t player, uint32_t small,
|
||||
uint32_t large) {
|
||||
if (player >= PSX_MAX_PLAYERS) return 0;
|
||||
if (psx_netplay_active()) {
|
||||
/* Online: only this peer's own guest port drives its own pad, once per
|
||||
* real tick (a rollback resim replays ticks the pad already felt). */
|
||||
if (psx_netplay_is_resimulating() ||
|
||||
(int)player != psx_netplay_local_port())
|
||||
return 1;
|
||||
const int card = psx_netplay_input_player();
|
||||
player = card >= 0 && card < PSX_MAX_PLAYERS ? (uint32_t)card : 0u;
|
||||
}
|
||||
psx_host_rumble_set(&g_mod_rumble[player], g_rumble_vblank, small, large);
|
||||
return 1;
|
||||
}
|
||||
static uint32_t g_savestate_input_guard_min_until = 0;
|
||||
static uint32_t g_savestate_input_guard_max_until = 0;
|
||||
static int g_headless = 0; /* debug/CI frontend: no SDL window/audio */
|
||||
@@ -1707,6 +1731,7 @@ extern "C" int psx_mod_set_adaptive_display_aspect(
|
||||
static PSXModSessionBaseline g_mod_owned_baseline;
|
||||
|
||||
static void reset_mod_owned_presentation(void) {
|
||||
psx_mod_internal_resolution_reset();
|
||||
psx_local_mouse_clear();
|
||||
PSXModSessionScalars live;
|
||||
live.video_vsync = g_video_vsync;
|
||||
@@ -5212,11 +5237,18 @@ static void update_controller_rumble(void) {
|
||||
const char* e = std::getenv("PSX_RUMBLE_TRACE");
|
||||
return e && e[0] && e[0] != '0';
|
||||
}();
|
||||
++g_rumble_vblank;
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
PlayerInput& p = g_players[s];
|
||||
uint8_t small = 0, large = 0;
|
||||
sio_get_pad_rumble(s, &small, &large);
|
||||
psx_host_rumble_merge(&g_mod_rumble[s], g_rumble_vblank, &small, &large);
|
||||
if (!p.handle) {
|
||||
if (trace && (!p.rumble_known || p.rumble_small != small ||
|
||||
p.rumble_large != large))
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp rumble: slot=%d small=%u large=%u host=none\n",
|
||||
s + 1, (unsigned)small, (unsigned)large);
|
||||
p.rumble_small = small;
|
||||
p.rumble_large = large;
|
||||
p.rumble_known = true;
|
||||
@@ -8434,7 +8466,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
psx_selfcheck_finish_frame(
|
||||
(g_offline_pad_count >= 3 && !sio_get_multitap()) ? 1 : 0);
|
||||
}
|
||||
if (!g_headless) update_controller_rumble();
|
||||
/* Headless has no pads; PSX_RUMBLE_TRACE still logs what would be sent. */
|
||||
if (!g_headless || std::getenv("PSX_RUMBLE_TRACE")) update_controller_rumble();
|
||||
|
||||
/* Latency ring: open this present cycle's slot, stamping when input was
|
||||
* sampled into SIO. Always-on; queried via the debug server "latency". */
|
||||
@@ -9490,7 +9523,7 @@ static int debug_toggles_on(void) {
|
||||
}
|
||||
static int s_dbg_scale_target = 0;
|
||||
static const char *dbg_texfilter_name(int f) {
|
||||
return f == 1 ? "bilinear" : f == 2 ? "stable world" : "nearest";
|
||||
return f == 1 ? "bilinear" : f == 2 ? "stable world" : f == 3 ? "xbr" : "nearest";
|
||||
}
|
||||
static void debug_toggle_summary(char *buf, size_t cap) {
|
||||
GlDynresStats st; gl_renderer_dynres_stats(&st);
|
||||
@@ -9546,7 +9579,7 @@ static int debug_toggle_key(int key, char *out, int cap) {
|
||||
on && !g_render_thread ? " (inert: render thread off)" : "");
|
||||
break; }
|
||||
case SDLK_5: {
|
||||
const int f = (gr_texture_filter() + 1) % 3;
|
||||
const int f = (gr_texture_filter() + 1) % 4;
|
||||
gr_set_texture_filter(f);
|
||||
std::snprintf(msg, sizeof msg, "Texture filter: %s", dbg_texfilter_name(f));
|
||||
break; }
|
||||
@@ -12802,28 +12835,22 @@ namespace {
|
||||
int ae_np_account_sign_out(void*) { return rnet_account_sign_out(); }
|
||||
int ae_np_account_set_handle(void*, const char* h) { return rnet_account_set_handle(h); }
|
||||
|
||||
/* Point the account client at the lobby host -- once per URL, not once
|
||||
* per pump: the login worker thread reads the host while a sign-in is in
|
||||
* flight, and re-initialising it 60 times a second under that read is a
|
||||
* data race for no gain. The secret is anchored to the EXECUTABLE
|
||||
* directory before the first init: its default is the bare relative name
|
||||
* "netplay_secret", resolved against the working directory, so the same
|
||||
* install signed itself out depending on where it was launched from.
|
||||
* rnet_auth migrates an old CWD-relative file into this path on first
|
||||
* load, so nobody is signed out by the move. Same shape as the SNES
|
||||
* host (snes_host_lobby.c cb_pump). */
|
||||
/* Point the account client at the lobby host. The rule (resolved URL,
|
||||
* once per URL, secret anchored to the exe dir) lives in recomp-ui, shared
|
||||
* with recomp_netplay_host.c. The URL is ae_np_default_url, NOT the raw
|
||||
* g_lnch_lobby_url: that is empty until a player saves one, which left the
|
||||
* auth host blank and every sign-in "could not reach the lobby server". */
|
||||
static int ae_np_exe_dir_path(void*, const char* leaf, char* out, size_t cap) {
|
||||
const std::string p =
|
||||
(exe_dir_from_argv(g_lnch_argv0 ? g_lnch_argv0 : "") / leaf).string();
|
||||
if (p.size() + 1 > cap) return 0;
|
||||
std::memcpy(out, p.c_str(), p.size() + 1);
|
||||
return 1;
|
||||
}
|
||||
const char* ae_np_default_url(void*); /* defined below */
|
||||
void ae_np_account_sync(void) {
|
||||
static std::string s_auth_url;
|
||||
const std::string& url = g_lnch_lobby_url;
|
||||
if (url.empty() || url == s_auth_url) return;
|
||||
if (s_auth_url.empty()) {
|
||||
const std::string secret =
|
||||
(exe_dir_from_argv(g_lnch_argv0 ? g_lnch_argv0 : "") /
|
||||
"netplay_secret").string();
|
||||
rnet_account_set_secret_path(secret.c_str());
|
||||
}
|
||||
s_auth_url = url;
|
||||
rnet_account_init(url.c_str());
|
||||
recomp_netplay_account_sync(ae_np_default_url(nullptr),
|
||||
ae_np_exe_dir_path, nullptr);
|
||||
}
|
||||
|
||||
#endif /* PSX_HAS_RECOMP_NET: account client is not linked in offline builds */
|
||||
@@ -17366,6 +17393,14 @@ int main(int argc, char** argv) {
|
||||
ls.scanlines = seed.scanlines ? 1 : 0;
|
||||
ls.scanline_strength_pct = seed.has_scanline_strength
|
||||
? (int)(seed.scanline_strength * 100.0 + 0.5) : 50;
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_VSYNC_ADAPTIVE)
|
||||
/* [video] vsync (1 on, 0 off/immediate, -1 adaptive, 2 VRR) <->
|
||||
* the launcher's V-Sync row (1 On, 2 Off, 3 Adaptive). VRR has no
|
||||
* row; it shows as On and is kept unless the player changes it. */
|
||||
ls.vsync = g_video_vsync == 0 ? RECOMP_LAUNCHER_VSYNC_OFF
|
||||
: g_video_vsync == -1 ? RECOMP_LAUNCHER_VSYNC_ADAPTIVE
|
||||
: RECOMP_LAUNCHER_VSYNC_ON;
|
||||
#endif
|
||||
ls.frame_interp = seed.frame_interpolation ? 1 : 0;
|
||||
ls.frame_interp_fps = seed.frame_interpolation_fps;
|
||||
@@ -17721,6 +17756,16 @@ int main(int argc, char** argv) {
|
||||
seed.scanline_strength = ls.scanline_strength_pct / 100.0;
|
||||
seed.has_scanline_strength = true;
|
||||
}
|
||||
#endif
|
||||
#if defined(RECOMP_LAUNCHER_VSYNC_ADAPTIVE)
|
||||
if (ls.vsync >= RECOMP_LAUNCHER_VSYNC_ON && ls.vsync <= RECOMP_LAUNCHER_VSYNC_ADAPTIVE) {
|
||||
const int was = g_video_vsync;
|
||||
g_video_vsync = ls.vsync == RECOMP_LAUNCHER_VSYNC_OFF ? 0
|
||||
: ls.vsync == RECOMP_LAUNCHER_VSYNC_ADAPTIVE ? -1
|
||||
: (was == 2 ? 2 : 1);
|
||||
seed.vsync = g_video_vsync;
|
||||
seed.has_vsync = true;
|
||||
}
|
||||
#endif
|
||||
seed.frame_interpolation = ls.frame_interp != 0; seed.has_frame_interpolation = true;
|
||||
seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = true;
|
||||
@@ -18133,6 +18178,7 @@ int main(int argc, char** argv) {
|
||||
mod_controller_source_reset();
|
||||
mod_pad_transform_reset();
|
||||
g_direct_shortcut_allowed = 0;
|
||||
std::memset(g_mod_rumble, 0, sizeof g_mod_rumble);
|
||||
g_p1_claimed_buttons = 0;
|
||||
psx_rewind_set_title_blocked(0);
|
||||
for (auto& policy : g_mod_controller_policy)
|
||||
@@ -18419,6 +18465,7 @@ session_reboot:
|
||||
(net_cfg.enabled && s_netplay_gl_present &&
|
||||
gl_renderer_cpu_auth_dual());
|
||||
g_video_scale = gr_scale(); /* reflect any clamp / alloc fallback */
|
||||
if (const char* e = std::getenv("PSX_TEXTURE_FILTER")) g_video_texfilter = std::atoi(e);
|
||||
gr_set_texture_filter(g_mod_texfilter < 0 ? g_video_texfilter : g_mod_texfilter);
|
||||
/* Sub-pixel vertex precision + perspective-correct UVs. Both default off;
|
||||
* with both off every setter below leaves the tracking caches disabled and
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_internal_resolution.h"
|
||||
|
||||
static uint32_t requested_lines;
|
||||
|
||||
int psx_mod_set_internal_resolution(uint32_t target_lines) {
|
||||
if (target_lines && (target_lines < 120u || target_lines > 8192u))
|
||||
return 0;
|
||||
requested_lines = target_lines;
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t psx_mod_internal_resolution_request(void) {
|
||||
return requested_lines;
|
||||
}
|
||||
|
||||
void psx_mod_internal_resolution_reset(void) {
|
||||
requested_lines = 0;
|
||||
}
|
||||
@@ -12,12 +12,74 @@
|
||||
#include "boot_state.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zlib.h>
|
||||
#if defined(_WIN32)
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <time.h>
|
||||
#endif
|
||||
|
||||
/* PSX_RB_SNAP_AUDIT=<dir>: one line per ring save -- mode (L live, R replay),
|
||||
* tick, and per boot_state section the CRC32 (+ hex for sections <= 4 KiB).
|
||||
* tools/rb_snap_audit_diff.py compares seats tick by tick and a seat's replay
|
||||
* against its own live save, naming the first section/byte that differs. It
|
||||
* is the replay-determinism check: same snap + same rows must give the same
|
||||
* machine, field by field. Off (zero cost) unless the variable is set. */
|
||||
static void netplay_snap_audit_note(uint32_t tick, const uint8_t *d, size_t n)
|
||||
{
|
||||
extern int psx_netplay_local_slot(void);
|
||||
extern int psx_netplay_is_resimulating(void);
|
||||
static FILE *f;
|
||||
static int init;
|
||||
size_t start;
|
||||
if (!init) {
|
||||
const char *dir = getenv("PSX_RB_SNAP_AUDIT");
|
||||
char nm[600];
|
||||
init = 1;
|
||||
if (dir && *dir) {
|
||||
snprintf(nm, sizeof(nm), "%s/snap-audit-p%d.txt", dir, psx_netplay_local_slot());
|
||||
f = fopen(nm, "w");
|
||||
}
|
||||
}
|
||||
if (!f || !d)
|
||||
return;
|
||||
for (start = 0; start < 256; start += 4) {
|
||||
size_t o = start;
|
||||
int cnt = 0, ok = 1;
|
||||
while (o + 16 <= n) {
|
||||
uint32_t tag;
|
||||
uint64_t ln;
|
||||
memcpy(&tag, d + o, 4);
|
||||
memcpy(&ln, d + o + 8, 8);
|
||||
if (ln > n || tag == 0 || tag > 0x7fffffffu) { ok = 0; break; }
|
||||
o += 16 + (size_t)ln;
|
||||
cnt++;
|
||||
}
|
||||
if (!ok || o != n || cnt < 6)
|
||||
continue;
|
||||
fprintf(f, "%c %u", psx_netplay_is_resimulating() ? 'R' : 'L', (unsigned)tick);
|
||||
o = start;
|
||||
while (o + 16 <= n) {
|
||||
uint32_t tag;
|
||||
uint64_t ln, i;
|
||||
memcpy(&tag, d + o, 4);
|
||||
memcpy(&ln, d + o + 8, 8);
|
||||
fprintf(f, " %x:%08lx", (unsigned)tag,
|
||||
(unsigned long)crc32(0L, d + o + 16, (uInt)ln));
|
||||
if (ln <= 4096u && tag != 0x10u) {
|
||||
fputc('=', f);
|
||||
for (i = 0; i < ln; i++)
|
||||
fprintf(f, "%02x", d[o + 16 + i]);
|
||||
}
|
||||
o += 16 + (size_t)ln;
|
||||
}
|
||||
fputc('\n', f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static double snap_mono_ms(void)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
@@ -69,6 +131,7 @@ int netplay_snap_ring_save(NetplaySnapRing *r, uint32_t tick,
|
||||
100.0 * (double)s_tele_incr_n / (double)s_tele_n);
|
||||
fflush(stderr);
|
||||
}
|
||||
netplay_snap_audit_note(tick, data, len);
|
||||
if (!netplay_snap_ring_store(r, tick, data, len))
|
||||
return 0;
|
||||
return 1;
|
||||
|
||||
@@ -93,6 +93,12 @@ uint32_t netplay_av_digest(void)
|
||||
gcap = gn;
|
||||
}
|
||||
gpu_snapshot_write(gbuf);
|
||||
/* The last 12 bytes are depth-24 present helpers (upload span,
|
||||
* present hold, previous display height): host presentation state
|
||||
* that a suppressed-present replay leaves different from Live. Not
|
||||
* guest-visible; keep them out of the av comparison. */
|
||||
if (gn >= 12u)
|
||||
memset(gbuf + gn - 12u, 0, 12u);
|
||||
crc = crc32_update(crc, gbuf, gn);
|
||||
}
|
||||
if (vram)
|
||||
|
||||
@@ -766,14 +766,20 @@ void psx_cycles_resync_after_restore(CPUState *cpu) {
|
||||
* would then advance (live_ts - restored_cycle) in one shot — tens of
|
||||
* millions of cycles / N nested presents with zero IRQ checks (MotK
|
||||
* transform CTC2 path). Anchor them at the restored clock. */
|
||||
if (cpu) {
|
||||
cpu->gte_ts_done = psx_cycle_count;
|
||||
cpu->muldiv_ts_done = psx_cycle_count;
|
||||
memset(cpu->read_absorb, 0, sizeof(cpu->read_absorb));
|
||||
cpu->read_absorb_which = 0;
|
||||
cpu->read_fudge = 0x20u; /* no committed predecessor load */
|
||||
cpu->ld_which_t = 0x20u; /* no pending load dest */
|
||||
cpu->ld_absorb = 0;
|
||||
{
|
||||
/* A snap with BS_SEC_CPUTIM carried them relative to its clock --
|
||||
* keep those; older snaps anchor at the restored clock as before. */
|
||||
extern int g_boot_state_cpu_timing_restored;
|
||||
if (cpu && !g_boot_state_cpu_timing_restored) {
|
||||
cpu->gte_ts_done = psx_cycle_count;
|
||||
cpu->muldiv_ts_done = psx_cycle_count;
|
||||
memset(cpu->read_absorb, 0, sizeof(cpu->read_absorb));
|
||||
cpu->read_absorb_which = 0;
|
||||
cpu->read_fudge = 0x20u; /* no committed predecessor load */
|
||||
cpu->ld_which_t = 0x20u; /* no pending load dest */
|
||||
cpu->ld_absorb = 0;
|
||||
}
|
||||
g_boot_state_cpu_timing_restored = 0;
|
||||
}
|
||||
/* Dirty-RAM interpreter load-delay writebacks live in host statics, not
|
||||
* BS_SEC_CPU. Discard (do not flush): snap GPRs are already architectural.
|
||||
|
||||
@@ -779,6 +779,37 @@ static void np_log_live_digest(uint32_t tick, const NetplayCoreParts *parts,
|
||||
* episode reloads from the newest provably-safe snap (≤ fork tick — HC is
|
||||
* confirmed through fork-1 by definition) and reseals both seats from
|
||||
* wire-authoritative history, converging the states. */
|
||||
/* PSX_RB_DET_SELFTEST=<K>: the replay-determinism test. Every K ticks the
|
||||
* slot-0 host opens a rollback episode 16 ticks back although nothing was
|
||||
* mispredicted, so every seat reloads a snap and replays confirmed rows over
|
||||
* ticks it already ran live. Same snap + same rows must give the same
|
||||
* machine: with PSX_RB_SNAP_AUDIT set, tools/rb_snap_audit_diff.py compares
|
||||
* each replayed save with the live save of that tick section by section
|
||||
* (CPU, RAM, timers, DMA, SIO/multitap, SPU, ...) and names the first field
|
||||
* that differs. Off unless set. */
|
||||
static void np_det_selftest_tick(uint32_t done)
|
||||
{
|
||||
static int k = -1;
|
||||
static uint32_t last;
|
||||
if (k < 0) {
|
||||
const char *e = getenv("PSX_RB_DET_SELFTEST");
|
||||
k = (e && *e) ? atoi(e) : 0;
|
||||
if (k < 0) k = 0;
|
||||
}
|
||||
if (k <= 0 || g_np.local_slot != 0 || !g_np.rollback || done < 64u)
|
||||
return;
|
||||
if (done - last < (uint32_t)k)
|
||||
return;
|
||||
if (psx_netplay_rb_active() || psx_netplay_rb_tip_holding() ||
|
||||
psx_netplay_rb_load_pending())
|
||||
return;
|
||||
last = done;
|
||||
fprintf(stderr, "psxrecomp: rb det-selftest rewind mismatch=%u\n",
|
||||
(unsigned)(done - 16u));
|
||||
fflush(stderr);
|
||||
(void)psx_netplay_rb_begin_rewind(done - 16u, g_np.slot_count > 1 ? 1 : 0);
|
||||
}
|
||||
|
||||
static void np_try_hc_fork_recovery(uint32_t fork_tick)
|
||||
{
|
||||
uint32_t sim;
|
||||
@@ -5016,6 +5047,21 @@ static int np_try_admit_load_barrier_unconfirmed(void)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* A Live realign restores the snap at T and resumes the guest at the frame
|
||||
* boundary after it, with the session already at T+1. Admit T+1 before the
|
||||
* resume: resuming first ran frame T+1 unadmitted (no inputs latched, no
|
||||
* advance), so the next frame was counted as T+1 and this peer ran one frame
|
||||
* ahead of its tick label (4-seat race: L-saves after a NACK realign carried
|
||||
* the next tick's clock; the realigned seat forked from every other). */
|
||||
int psx_netplay_live_realign_admit(void)
|
||||
{
|
||||
if (!psx_netplay_active() || !g_np.rollback || g_np.xfer != NP_XFER_NONE)
|
||||
return 1;
|
||||
if (g_np.needs_advance)
|
||||
return 1;
|
||||
return np_try_admit_rollback();
|
||||
}
|
||||
|
||||
int psx_netplay_poll_admit(void)
|
||||
{
|
||||
rnet_u32 sim;
|
||||
@@ -5222,6 +5268,22 @@ int psx_netplay_poll_admit(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The resume PC a tick snapshot is saved with. Live finish_frame and the
|
||||
* Replay finish_frame must use the same rule: Replay passed no hint, so its
|
||||
* snap often fell back to a different PC or was deferred to a later BB edge,
|
||||
* and the replayed save of tick T sat at another guest point than the live
|
||||
* save of T (pc, cycles_since_vblank, root counters all differed --
|
||||
* PSX_RB_DET_SELFTEST: 106 of 262 replayed saves). */
|
||||
uint32_t psx_netplay_snap_resume_hint(const CPUState *cpu)
|
||||
{
|
||||
uint32_t hint = psx_compiled_irq_resume_pc();
|
||||
if (!psx_is_dispatchable(hint))
|
||||
hint = psx_last_irq_check_pc();
|
||||
if (!psx_is_dispatchable(hint) && cpu)
|
||||
hint = cpu->pc;
|
||||
return hint;
|
||||
}
|
||||
|
||||
void psx_netplay_finish_frame(void)
|
||||
{
|
||||
rnet_u32 done;
|
||||
@@ -5261,15 +5323,12 @@ void psx_netplay_finish_frame(void)
|
||||
np_rollback_reconcile_wire();
|
||||
psx_netplay_rb_pump();
|
||||
psx_netplay_rb_request_snap(done);
|
||||
np_det_selftest_tick(done);
|
||||
/* Flush at vblank: MotK IRQ fast/mid paths used to skip poll_snap, so
|
||||
* deferred BB-edge saves never ran and the ring stayed empty. Prefer
|
||||
* IRQ BB-edge PCs — cpu->pc is often 0 during present/finish_frame. */
|
||||
if (g_np.cpu) {
|
||||
resume_hint = psx_compiled_irq_resume_pc();
|
||||
if (!psx_is_dispatchable(resume_hint))
|
||||
resume_hint = psx_last_irq_check_pc();
|
||||
if (!psx_is_dispatchable(resume_hint))
|
||||
resume_hint = g_np.cpu->pc;
|
||||
resume_hint = psx_netplay_snap_resume_hint(g_np.cpu);
|
||||
psx_netplay_rb_poll(g_np.cpu, resume_hint);
|
||||
}
|
||||
}
|
||||
|
||||
+154
-28
@@ -116,6 +116,8 @@ uint32_t psx_netplay_rb_rtt_estimate_ms(void) { return 0; }
|
||||
#else
|
||||
|
||||
#include "netplay_hash_confirm.h"
|
||||
#include "netplay_peer_frontier.h"
|
||||
#include "netplay_fmv_confirm.h"
|
||||
#include "netplay_input_hist.h"
|
||||
#include "netplay_rb_post.h"
|
||||
#include "netplay_snap_ring.h"
|
||||
@@ -435,6 +437,9 @@ static uint32_t g_agreed_span_lo;
|
||||
* otherwise ADVANCE→begin races the follower's agreed and NACK-storms
|
||||
* (2026-08-02 post-§39 soak). */
|
||||
static uint32_t g_peer_resolved_through;
|
||||
/* Per-seat RESOLVED adverts; g_peer_resolved_through is the lowest of them
|
||||
* across every peer seat (netplay_peer_frontier.h). */
|
||||
static NetplayPeerFrontier g_peer_frontier;
|
||||
/* §41: wall-clock when choose_load/begin first blocked on peer RESOLVED
|
||||
* (0 = not waiting). After RB_MOTK_PEER_RESOLVED_GATE_MS, force-open —
|
||||
* unbounded defer silently desynced Live (2026-08-02 post-§40 soak). */
|
||||
@@ -715,6 +720,8 @@ static uint32_t g_fmv_settle_until;
|
||||
static uint32_t g_fmv_lockstep_until;
|
||||
static uint32_t g_fmv_media_end_sim;
|
||||
static uint32_t g_fmv_core_match_streak;
|
||||
/* Highest hash-confirmed tick already counted into the streak. */
|
||||
static uint32_t g_fmv_confirm_seen;
|
||||
static int g_fmv_lockstep_released; /* sticky: never re-lock after invent on */
|
||||
/* Inclusive last tick both peers dense-snapped (media/lockstep/+tip). */
|
||||
static uint32_t g_fmv_dense_through;
|
||||
@@ -1037,21 +1044,31 @@ static int rb_fmv_media_active(void)
|
||||
/* After media: grow/shrink lockstep_until from hash_confirm agreement. */
|
||||
static void rb_fmv_update_lockstep_gate(uint32_t sim)
|
||||
{
|
||||
int matched = 0;
|
||||
uint32_t cap;
|
||||
uint32_t prev_until;
|
||||
if (!g_fmv_media_end_sim || rb_fmv_media_active())
|
||||
return;
|
||||
|
||||
if (g_b.hc && !netplay_hc_peek_mismatch(g_b.hc, NULL, NULL, NULL)) {
|
||||
uint32_t need = (sim > 0u) ? (sim - 1u) : 0u;
|
||||
if (netplay_hc_confirm_through(g_b.hc, need))
|
||||
matched = 1;
|
||||
/* Agreement is counted in CONFIRMED ticks, not in "sim-1 is already
|
||||
* confirmed" ticks. The peer's digest for a tick arrives one RTT after
|
||||
* the tick; the peer that runs ahead (usually the host, cross-machine)
|
||||
* never has sim-1 confirmed when it asks, so the old per-tick test reset
|
||||
* the streak every tick and fired a false "MAX unmatched" DESYNC after
|
||||
* every movie even though every digest matched. Only a real mismatch
|
||||
* resets the streak now; a confirm that is merely late leaves it. */
|
||||
{
|
||||
uint32_t gained = 0u;
|
||||
if (g_b.hc && netplay_hc_peek_mismatch(g_b.hc, NULL, NULL, NULL)) {
|
||||
g_fmv_core_match_streak = 0;
|
||||
} else if (g_b.hc && netplay_hc_confirm_through(g_b.hc, g_fmv_media_end_sim)) {
|
||||
const uint32_t rt = netplay_hc_resolved_through(g_b.hc);
|
||||
const uint32_t from = (g_fmv_confirm_seen > g_fmv_media_end_sim)
|
||||
? g_fmv_confirm_seen : g_fmv_media_end_sim;
|
||||
gained = rb_fmv_confirm_gain(from, rt);
|
||||
if (rt > g_fmv_confirm_seen) g_fmv_confirm_seen = rt;
|
||||
}
|
||||
g_fmv_core_match_streak += gained;
|
||||
}
|
||||
if (matched)
|
||||
g_fmv_core_match_streak++;
|
||||
else
|
||||
g_fmv_core_match_streak = 0;
|
||||
|
||||
/* §93: after MAX-unmatched DESYNC, keep counting so a later rematch can
|
||||
* clear invent/begin hold without requiring a new media bout. */
|
||||
@@ -1237,6 +1254,7 @@ static void rb_fmv_tick_settle(void)
|
||||
if (sim >= g_fmv_media_hi)
|
||||
g_fmv_media_hi = sim;
|
||||
g_fmv_core_match_streak = 0;
|
||||
g_fmv_confirm_seen = 0;
|
||||
g_fmv_lockstep_released = 0;
|
||||
g_fmv_settle_until = sim + RB_FMV_SETTLE_TICKS;
|
||||
g_fmv_lockstep_until = sim + RB_FMV_LOCKSTEP_MIN;
|
||||
@@ -2245,6 +2263,7 @@ static void realign_invalidate_evidence(uint32_t tick)
|
||||
}
|
||||
if (g_peer_resolved_through > tick)
|
||||
g_peer_resolved_through = tick;
|
||||
netplay_peer_frontier_clamp(&g_peer_frontier, tick);
|
||||
if (g_local_advertised_through > tick)
|
||||
g_local_advertised_through = tick;
|
||||
if (g_snaps)
|
||||
@@ -3295,10 +3314,35 @@ static int maybe_abandon_tip_extend(void)
|
||||
|
||||
/* Apply inbound peer RESOLVED: raise g_peer_resolved_through + library
|
||||
* convergence, and complete Verify if POST was lost. */
|
||||
static void apply_peer_resolved(uint32_t resolved)
|
||||
static void apply_peer_resolved(int from, uint32_t advert)
|
||||
{
|
||||
if (!g_rb || resolved == 0u)
|
||||
uint32_t seats = psx_netplay_peer_seats();
|
||||
uint32_t resolved;
|
||||
if (!g_rb || advert == 0u)
|
||||
return;
|
||||
if (seats == 0u) {
|
||||
/* No seat roster (bare two-seat session): the one peer's advert. */
|
||||
resolved = advert;
|
||||
} else {
|
||||
int seat = -1;
|
||||
if (from >= 0 && (uint32_t)from < NETPLAY_PEER_FRONTIER_SEATS &&
|
||||
(seats & (1u << from)))
|
||||
seat = from;
|
||||
else if ((seats & (seats - 1u)) == 0u) {
|
||||
/* Sole peer: the sender is unambiguous. */
|
||||
seat = 0;
|
||||
while (!(seats & (1u << seat)))
|
||||
seat++;
|
||||
}
|
||||
if (seat < 0)
|
||||
return;
|
||||
netplay_peer_frontier_note(&g_peer_frontier, seat, advert);
|
||||
/* With more than two seats an advert speaks only for its sender:
|
||||
* the frontier every follower has reached is the lowest. */
|
||||
resolved = netplay_peer_frontier_min(&g_peer_frontier, seats);
|
||||
if (resolved == 0u)
|
||||
return;
|
||||
}
|
||||
if (resolved > g_peer_resolved_through) {
|
||||
g_peer_resolved_through = resolved;
|
||||
g_peer_resolved_gate_expired = 0;
|
||||
@@ -3522,6 +3566,15 @@ static void advance_agreed_watermark_from_hc(void)
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb agreed ADVANCE %u→%u (hc=%u live confirm)\n",
|
||||
(unsigned)old, (unsigned)t, (unsigned)rt);
|
||||
/* §55: every peer matched cores at t >= the bisect cap, so the
|
||||
* fork it bounded is healed. A stale cap refuses every later
|
||||
* episode once its loads leave the ring. */
|
||||
if (netplay_fork_cap_retired(g_bl_fork_cap, t)) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb fork cap %u retired (agreed %u live confirm)\n",
|
||||
(unsigned)g_bl_fork_cap, (unsigned)t);
|
||||
g_bl_fork_cap = 0u;
|
||||
}
|
||||
fflush(stderr);
|
||||
/* §40: peer follow frontier lags local agreed until RESOLVED lands. */
|
||||
advertise_agreed_resolved(t);
|
||||
@@ -3889,6 +3942,31 @@ static int choose_load_tick_inner(uint32_t mismatch, uint32_t *out_load)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The bisect window (below the fork cap, above the peer NACK floor) is
|
||||
* empty or holds no loadable snap. Bisecting cannot heal the fork, but a
|
||||
* MEDIA_KF heal can: it transfers the initiator's pin to every follower, so
|
||||
* any recent mutual snap works. Without this an hc-fork recovery on a
|
||||
* 4-seat WAN race refused for the rest of the session (cap 3296, floor 3280:
|
||||
* "no mutually-loadable snap") and two pairs of seats finished the race on
|
||||
* different states. */
|
||||
static int choose_load_fork_cap_heal_bypass(uint32_t mismatch, uint32_t *out_load)
|
||||
{
|
||||
static uint32_t s_log_cap;
|
||||
if (!rb_media_kf_enabled() || !(g_request_post_fmv_heal || g_post_fmv_heal_kf))
|
||||
return 0;
|
||||
if (!choose_load_tick_inner(mismatch, out_load))
|
||||
return 0;
|
||||
if (s_log_cap != g_bl_fork_cap) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb choose_load fork cap %u bypass load=%u "
|
||||
"(bisect window empty; MEDIA_KF heal transfers the pin)\n",
|
||||
(unsigned)g_bl_fork_cap, (unsigned)*out_load);
|
||||
fflush(stderr);
|
||||
s_log_cap = g_bl_fork_cap;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
{
|
||||
/* §55: after a baseline core mismatch at load L the fork provably predates
|
||||
@@ -3899,7 +3977,7 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
if (m > g_bl_fork_cap)
|
||||
m = g_bl_fork_cap; /* inner's shared walk returns load < m */
|
||||
if (!choose_load_tick_inner(m, out_load))
|
||||
return 0;
|
||||
return choose_load_fork_cap_heal_bypass(mismatch, out_load);
|
||||
/* §62: peer proved it cannot follow ≤ floor (snap evicted). If the
|
||||
* bisect window (must be < fork cap AND > peer floor) is empty, no
|
||||
* mutually-loadable snap covers the fork — refuse to open rather
|
||||
@@ -3915,7 +3993,7 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
fflush(stderr);
|
||||
s_floor_log = *out_load;
|
||||
}
|
||||
return 0;
|
||||
return choose_load_fork_cap_heal_bypass(mismatch, out_load);
|
||||
}
|
||||
if (*out_load >= g_bl_fork_cap) {
|
||||
static uint32_t s_refuse_log;
|
||||
@@ -3927,7 +4005,7 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
fflush(stderr);
|
||||
s_refuse_log = g_bl_fork_cap;
|
||||
}
|
||||
return 0;
|
||||
return choose_load_fork_cap_heal_bypass(mismatch, out_load);
|
||||
}
|
||||
{
|
||||
static uint32_t s_bisect_log;
|
||||
@@ -4728,8 +4806,13 @@ static void log_resim_tick_audit(uint32_t sim, const char *tag)
|
||||
for (slot = 0; slot < n; ++slot) {
|
||||
RNetRbFrame row;
|
||||
if (rnet_rb_get_sealed_frame(g_rb, slot, sim, &row) && row.is_valid)
|
||||
fprintf(stderr, " s%d=%04x%s", slot, (unsigned)row.buttons,
|
||||
row.analog ? "A" : "");
|
||||
/* Device type and axes too: equal buttons hid a sealed-row
|
||||
* difference (a digital idle fill vs the seat's analog row). */
|
||||
fprintf(stderr, " s%d=%04x%s:t%u:%02x%02x%02x%02x", slot,
|
||||
(unsigned)row.buttons, row.analog ? "A" : "",
|
||||
(unsigned)row.analog, (unsigned)(uint8_t)row.stick_x,
|
||||
(unsigned)(uint8_t)row.stick_y, (unsigned)row.rx,
|
||||
(unsigned)row.ry);
|
||||
else
|
||||
fprintf(stderr, " s%d=----", slot);
|
||||
}
|
||||
@@ -6415,6 +6498,7 @@ void psx_netplay_rb_start(void)
|
||||
g_agreed_through = 0;
|
||||
g_agreed_span_lo = 0;
|
||||
g_peer_resolved_through = 0;
|
||||
netplay_peer_frontier_reset(&g_peer_frontier);
|
||||
g_peer_resolved_wait_t0_ms = 0ull;
|
||||
g_peer_resolved_gate_expired = 0;
|
||||
g_past_frontier_sticky = 0;
|
||||
@@ -6498,6 +6582,7 @@ void psx_netplay_rb_cold_reset(void)
|
||||
g_fmv_settle_until = 0;
|
||||
g_fmv_lockstep_until = 0;
|
||||
g_fmv_media_end_sim = 0;
|
||||
g_fmv_confirm_seen = 0;
|
||||
g_fmv_core_match_streak = 0;
|
||||
g_fmv_lockstep_released = 0;
|
||||
g_fmv_dense_through = 0;
|
||||
@@ -6551,6 +6636,7 @@ void psx_netplay_rb_shutdown(void)
|
||||
g_fmv_settle_until = 0;
|
||||
g_fmv_lockstep_until = 0;
|
||||
g_fmv_media_end_sim = 0;
|
||||
g_fmv_confirm_seen = 0;
|
||||
g_fmv_core_match_streak = 0;
|
||||
g_fmv_lockstep_released = 0;
|
||||
g_fmv_dense_through = 0;
|
||||
@@ -6595,6 +6681,7 @@ void psx_netplay_rb_shutdown(void)
|
||||
g_agreed_through = 0;
|
||||
g_agreed_span_lo = 0;
|
||||
g_peer_resolved_through = 0;
|
||||
netplay_peer_frontier_reset(&g_peer_frontier);
|
||||
g_peer_resolved_wait_t0_ms = 0ull;
|
||||
g_peer_resolved_gate_expired = 0;
|
||||
g_past_frontier_sticky = 0;
|
||||
@@ -6630,6 +6717,15 @@ static int rb_fmv_dense_snap_window(void)
|
||||
void psx_netplay_rb_request_snap(uint32_t tick)
|
||||
{
|
||||
uint32_t iv;
|
||||
/* Every tick boundary, Live and Replay alike: re-anchor the dirty-RAM
|
||||
* interpreter's host-only IRQ poll stride. A load resets it to phase 0;
|
||||
* Live otherwise carried an arbitrary phase, so a replay took the VBlank
|
||||
* in RAM code at another instruction (EPC / clock split at R4's race
|
||||
* start under PSX_RB_DET_SELFTEST). */
|
||||
{
|
||||
extern void dirty_ram_irq_ambient_resync_after_restore(void);
|
||||
dirty_ram_irq_ambient_resync_after_restore();
|
||||
}
|
||||
if (!g_snaps)
|
||||
return;
|
||||
/* Never overwrite the frozen episode baseline (or its ring slot). */
|
||||
@@ -6747,8 +6843,14 @@ static int try_apply_pending_load(CPUState *cpu_in)
|
||||
psx_cycles_resync_after_restore(cpu_in);
|
||||
interrupts_resync_after_restore();
|
||||
cdrom_resync_deadlines_after_restore();
|
||||
if (!cdrom_xa_stream_active() && !cdrom_fmv_stream_pending())
|
||||
spu_cd_audio_reset();
|
||||
{
|
||||
/* The snap carried the CD input FIFO (BS_SEC_SPUCD): keep it. */
|
||||
extern int g_spu_cd_restored;
|
||||
if (!g_spu_cd_restored && !cdrom_xa_stream_active() &&
|
||||
!cdrom_fmv_stream_pending())
|
||||
spu_cd_audio_reset();
|
||||
g_spu_cd_restored = 0;
|
||||
}
|
||||
{
|
||||
extern void overlay_loader_clear_lazy_miss(void);
|
||||
overlay_loader_clear_lazy_miss();
|
||||
@@ -6882,8 +6984,14 @@ static int try_apply_pending_load(CPUState *cpu_in)
|
||||
cdrom_resync_deadlines_after_restore();
|
||||
/* Wipe SPU CD FIFO only when XA is idle — clearing mid-stream
|
||||
* left MotK FMV silent/black after tip loads into media. */
|
||||
if (!cdrom_xa_stream_active() && !cdrom_fmv_stream_pending())
|
||||
spu_cd_audio_reset();
|
||||
{
|
||||
/* The snap carried the CD input FIFO (BS_SEC_SPUCD): keep it. */
|
||||
extern int g_spu_cd_restored;
|
||||
if (!g_spu_cd_restored && !cdrom_xa_stream_active() &&
|
||||
!cdrom_fmv_stream_pending())
|
||||
spu_cd_audio_reset();
|
||||
g_spu_cd_restored = 0;
|
||||
}
|
||||
{
|
||||
extern void overlay_loader_clear_lazy_miss(void);
|
||||
overlay_loader_clear_lazy_miss();
|
||||
@@ -7045,6 +7153,11 @@ void psx_netplay_rb_flush_resume(void)
|
||||
live_realign_resume = !g_rb || !rnet_rb_is_active(g_rb);
|
||||
if (!live_realign_resume && rnet_rb_get_phase(g_rb) != nRNetRbPhaseReplay)
|
||||
return;
|
||||
/* Live realign: the resumed frame must be an admitted tick, or it runs
|
||||
* uncounted and this seat ends one frame ahead of its label. Stay
|
||||
* spinning until the tick's inputs admit. */
|
||||
if (live_realign_resume && !psx_netplay_live_realign_admit())
|
||||
return;
|
||||
pc = g_pending_resume_pc;
|
||||
if (!rb_resume_pc_ok(pc)) {
|
||||
uint32_t alt = pick_snap_resume_pc(cpu(), pc);
|
||||
@@ -7796,7 +7909,7 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
|
||||
{
|
||||
rnet_u32 resolved = 0;
|
||||
while (rnet_session_take_rb_resolved(s, &resolved))
|
||||
apply_peer_resolved(resolved);
|
||||
apply_peer_resolved(rnet_session_rb_last_take_from(s), resolved);
|
||||
}
|
||||
|
||||
/* Tick settle tracker. Media/lockstep episodes resume into MotK wait/VLC
|
||||
@@ -8537,9 +8650,20 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
|
||||
* after local snap apply. Gating on !snap_applied left the loser
|
||||
* stashing the winner's BEGIN forever while both sides waited on
|
||||
* peer baseline (2026-08-02 soak: epoch 16 vs 17, 404× WIN drop).
|
||||
* Replay/Verify still refuse — abort/NACK owns those. */
|
||||
if (g_rb && rnet_rb_is_active(g_rb) && !rnet_rb_is_from_peer_notify(g_rb) &&
|
||||
epoch != rnet_rb_get_epoch_id(g_rb)) {
|
||||
* Replay/Verify still refuse — abort/NACK owns those.
|
||||
* With three or more seats a FOLLOWER sees the race too: two guests
|
||||
* mispredict the same tick and both BEGIN. The follower used to follow
|
||||
* whichever BEGIN landed first and stash the other, so with three
|
||||
* initiators the seats split across epochs and each waited forever for
|
||||
* baselines that never came (4-seat WAN: host on epoch 43 from slot 3,
|
||||
* slot 1 winning with 41). The epoch id names its initiator, so a
|
||||
* follower applies the same rule to the episode it follows
|
||||
* (netplay_rb_concurrent_begin). Two seats never reach this for a
|
||||
* follower: the only other initiator is the one it already follows. */
|
||||
if (g_rb && rnet_rb_is_active(g_rb) &&
|
||||
netplay_rb_concurrent_begin(rnet_rb_get_epoch_id(g_rb), epoch,
|
||||
rnet_rb_is_from_peer_notify(g_rb),
|
||||
RB_EPOCH_SLOT_MASK) != 0) {
|
||||
uint32_t our_epoch = rnet_rb_get_epoch_id(g_rb);
|
||||
uint32_t our_slot = our_epoch & RB_EPOCH_SLOT_MASK;
|
||||
uint32_t their_slot = epoch & RB_EPOCH_SLOT_MASK;
|
||||
@@ -9003,7 +9127,7 @@ void psx_netplay_rb_pump(void)
|
||||
{
|
||||
rnet_u32 resolved = 0;
|
||||
while (rnet_session_take_rb_resolved(s, &resolved))
|
||||
apply_peer_resolved(resolved);
|
||||
apply_peer_resolved(rnet_session_rb_last_take_from(s), resolved);
|
||||
}
|
||||
/* §53b: Live cadence — only HC-confirmed ADVANCE. Aged HEAL / HEAL-FORCE
|
||||
* stay at begin/follow: running them every pump under a live HC stall
|
||||
@@ -9027,7 +9151,7 @@ void psx_netplay_rb_pump(void)
|
||||
(g_peer_commit_epoch != epoch || load > g_peer_commit_tick)) {
|
||||
g_peer_commit_epoch = epoch;
|
||||
g_peer_commit_tick = load;
|
||||
apply_peer_resolved(load);
|
||||
apply_peer_resolved(rnet_session_rb_last_take_from(s), load);
|
||||
(void)maybe_abandon_tip_extend();
|
||||
}
|
||||
continue;
|
||||
@@ -9144,7 +9268,7 @@ void psx_netplay_rb_pump(void)
|
||||
(unsigned)peer_frontier);
|
||||
fflush(stderr);
|
||||
}
|
||||
apply_peer_resolved(peer_frontier);
|
||||
apply_peer_resolved(rnet_session_rb_last_take_from(s), peer_frontier);
|
||||
if (!g_agreed_valid || g_agreed_through < peer_frontier) {
|
||||
g_agreed_through = peer_frontier;
|
||||
g_agreed_span_lo = peer_frontier;
|
||||
@@ -9284,6 +9408,8 @@ void psx_netplay_rb_pump(void)
|
||||
if (g_peer_resolved_through == 0u ||
|
||||
g_peer_resolved_through > peer_frontier)
|
||||
g_peer_resolved_through = peer_frontier;
|
||||
netplay_peer_frontier_clamp(&g_peer_frontier,
|
||||
peer_frontier);
|
||||
g_peer_resolved_gate_expired = 0;
|
||||
g_peer_resolved_wait_t0_ms = 0ull;
|
||||
g_past_frontier_sticky = 1;
|
||||
@@ -9957,7 +10083,7 @@ void psx_netplay_rb_finish_frame(void)
|
||||
log_resim_tick_audit(done, "fin");
|
||||
psx_netplay_rb_request_snap(done);
|
||||
if (c)
|
||||
psx_netplay_rb_poll(c, pick_snap_resume_pc(c, 0u));
|
||||
psx_netplay_rb_poll(c, psx_netplay_snap_resume_hint(c));
|
||||
rnet_session_advance(s);
|
||||
g_needs_advance = 0;
|
||||
|
||||
|
||||
@@ -466,6 +466,41 @@ void spu_cd_audio_push(const int16_t* stereo, int frames) {
|
||||
cd_frame_count);
|
||||
}
|
||||
|
||||
/* CD input frames queued for the SPU are guest state: they come from the
|
||||
* CD-ROM decoder at guest time and the SPU consumes them sample by sample
|
||||
* (output and the CD capture area of SPU RAM). They used to be outside the
|
||||
* snapshot and were wiped on rollback loads, so a replay mixed different CD
|
||||
* audio than the live run (SPU voices / SPU RAM split at R4's race start). */
|
||||
int g_spu_cd_restored;
|
||||
uint32_t spu_cd_snapshot_bytes(void) { return 4u + cd_frame_count * 4u; }
|
||||
void spu_cd_snapshot_write(uint8_t *p) {
|
||||
uint32_t n = cd_frame_count, pos = cd_read_pos;
|
||||
p[0] = (uint8_t)n; p[1] = (uint8_t)(n >> 8); p[2] = (uint8_t)(n >> 16); p[3] = (uint8_t)(n >> 24);
|
||||
p += 4;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
uint16_t l = (uint16_t)cd_ring[pos * 2u], r = (uint16_t)cd_ring[pos * 2u + 1u];
|
||||
p[0] = (uint8_t)l; p[1] = (uint8_t)(l >> 8); p[2] = (uint8_t)r; p[3] = (uint8_t)(r >> 8);
|
||||
p += 4;
|
||||
pos = (pos + 1u) % SPU_CD_RING_FRAMES;
|
||||
}
|
||||
}
|
||||
int spu_cd_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
uint32_t n;
|
||||
if (!p || len < 4u) return 0;
|
||||
n = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
if (n > SPU_CD_RING_FRAMES || len != 4u + n * 4u) return 0;
|
||||
p += 4;
|
||||
for (uint32_t i = 0; i < n; i++, p += 4) {
|
||||
cd_ring[i * 2u] = (int16_t)(uint16_t)(p[0] | (p[1] << 8));
|
||||
cd_ring[i * 2u + 1u] = (int16_t)(uint16_t)(p[2] | (p[3] << 8));
|
||||
}
|
||||
cd_read_pos = 0;
|
||||
cd_write_pos = n % SPU_CD_RING_FRAMES;
|
||||
cd_frame_count = n;
|
||||
g_spu_cd_restored = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int cd_audio_pop(int16_t* left, int16_t* right) {
|
||||
if (cd_frame_count == 0) return 0;
|
||||
*left = cd_ring[cd_read_pos * 2u + 0u];
|
||||
|
||||
@@ -197,7 +197,14 @@ int main(void) {
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
|
||||
page();
|
||||
/* Default bases (accurate_blending off): measured, not required. */
|
||||
/* No caller opt-in: default rendering must match the console. */
|
||||
check(gl_renderer_accurate_blending() == 1, "correct blending is the default");
|
||||
scene(0);
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, out);
|
||||
check(oracle_errors(out) == 0, "default GL rendering matches the PS1 formulas");
|
||||
check(diff(out, ref) == 0, "default GL VRAM matches the software reference");
|
||||
/* Historical bases: measured for diagnosis, not required. */
|
||||
gl_renderer_set_accurate_blending(0);
|
||||
scene(0);
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, out);
|
||||
printf("gl 4x accurate_blending=0: oracle mismatches=%ld (historical bases)\n", oracle_errors(out));
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/* [video] texture_filtering = "xbr" on a real, hidden OpenGL context, driven
|
||||
* through the renderer facade (gr_*).
|
||||
*
|
||||
* - a 45-degree red/blue edge magnified 8x loses its 8-pixel staircase: the
|
||||
* boundary column in each row follows the diagonal closely;
|
||||
* - straight edges and flat areas stay exactly the nearest-texel image, and
|
||||
* a 1:1 (unmagnified) draw is byte-identical to nearest;
|
||||
* - cutouts (texel 0) stay cutouts and never lend their colour;
|
||||
* - the render thread gives the same pixels as the direct path;
|
||||
* - the other filter modes are unchanged by the new one (nearest after xbr
|
||||
* restores the exact image).
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#define main rt_fixture_main
|
||||
#include "test_gl_render_thread.c"
|
||||
#undef main
|
||||
|
||||
static uint16_t nat[1024 * 512];
|
||||
#define TP 0x0108 /* 15-bit page at (512,0) */
|
||||
|
||||
static void pages(void) {
|
||||
static uint16_t pg[32 * 16];
|
||||
for (int y = 0; y < 16; y++)
|
||||
for (int x = 0; x < 32; x++) {
|
||||
uint16_t c;
|
||||
if (x < 16) c = x > y ? 0x001f : 0x7c00; /* diagonal edge */
|
||||
else if (x < 24) c = x < 20 ? 0x001f : 0x7c00; /* vertical edge */
|
||||
else c = (x == 28 && y == 8) ? 0x0000 : 0x03e0; /* green with a hole */
|
||||
pg[y * 32 + x] = c;
|
||||
}
|
||||
cpu_upload(512, 0, 32, 16, pg);
|
||||
}
|
||||
/* Each triangle carries GTE provenance (precise vertices), as world geometry
|
||||
* does, so [video] texture_lod sets its LOD bit on these draws. */
|
||||
static void quad(int x, int y, int w, int h, int u0, int v0, int uw, int vh) {
|
||||
gr_set_precise_triangle(1, x << 16, y << 16, (x + w) << 16, y << 16, x << 16, (y + h) << 16);
|
||||
gr_draw_textured_triangle(x, y, u0, v0, x + w, y, u0 + uw, v0, x, y + h, u0, v0 + vh, 0, 0, TP);
|
||||
gr_set_precise_triangle(1, (x + w) << 16, y << 16, (x + w) << 16, (y + h) << 16, x << 16, (y + h) << 16);
|
||||
gr_draw_textured_triangle(x + w, y, u0 + uw, v0, x + w, y + h, u0 + uw, v0 + vh, x, y + h, u0, v0 + vh, 0, 0, TP);
|
||||
}
|
||||
static void scene(void) {
|
||||
gr_set_draw_area(0, 0, 319, 239);
|
||||
gr_set_draw_offset(0, 0);
|
||||
mask_bits(0, 0);
|
||||
gr_set_semi_transparency(0, 0);
|
||||
gr_set_color_modulation(128, 128, 128, 1);
|
||||
gr_fill_rect(0, 0, 320, 240, 0x1084);
|
||||
quad(0, 0, 128, 128, 0, 0, 16, 16); /* diagonal, 8x */
|
||||
quad(140, 0, 64, 128, 16, 0, 8, 16); /* vertical edge, 8x */
|
||||
quad(210, 0, 64, 128, 24, 0, 8, 16); /* flat green with a hole, 8x */
|
||||
quad(0, 150, 32, 16, 0, 0, 32, 16); /* 1:1 */
|
||||
gl_renderer_render_thread_frame_boundary();
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, nat);
|
||||
}
|
||||
/* Mean distance between each row's first red-dominant column and the ideal
|
||||
* diagonal (x = y + 0.5 texel, in pixels). */
|
||||
static double stair_error(void) {
|
||||
double e = 0; int n = 0;
|
||||
for (int y = 4; y < 124; y++) {
|
||||
int edge = 128;
|
||||
for (int x = 0; x < 128; x++) {
|
||||
uint16_t p = nat[y * 1024 + x];
|
||||
if ((p & 31) > ((p >> 10) & 31)) { edge = x; break; }
|
||||
}
|
||||
e += fabs((double)edge - ((double)y + 4.0)); n++;
|
||||
}
|
||||
return e / n;
|
||||
}
|
||||
static int region_equal(const uint16_t *a, const uint16_t *b, int x, int y, int w, int h) {
|
||||
for (int r = y; r < y + h; r++)
|
||||
if (memcmp(a + r * 1024 + x, b + r * 1024 + x, (size_t)w * 2)) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SKIP no video\n"); return 77; }
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
#if defined(__APPLE__)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
||||
#endif
|
||||
SDL_Window *win = SDL_CreateWindow("xBR hidden test", 0, 0, 320, 240,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
|
||||
if (!win) { fprintf(stderr, "SKIP no window\n"); return 77; }
|
||||
gr_set_backend(GR_BACKEND_OPENGL);
|
||||
gr_init(vram);
|
||||
gr_set_scale(1);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
|
||||
pages();
|
||||
static uint16_t ref[1024 * 512], x[1024 * 512];
|
||||
gr_set_texture_filter(0);
|
||||
scene(); memcpy(ref, nat, sizeof ref);
|
||||
const double e_near = stair_error();
|
||||
gr_set_texture_filter(3);
|
||||
check(gr_texture_filter() == 3, "xbr selectable");
|
||||
scene(); memcpy(x, nat, sizeof x);
|
||||
const double e_xbr = stair_error();
|
||||
printf("stair error nearest=%.2f xbr=%.2f\n", e_near, e_xbr);
|
||||
check(e_xbr < e_near * 0.7, "diagonal staircase smoothed");
|
||||
check(region_equal(ref, x, 140, 0, 64, 128), "straight edge unchanged");
|
||||
check(region_equal(ref, x, 210, 0, 64, 128), "flat area and cutout unchanged");
|
||||
check(region_equal(ref, x, 0, 150, 32, 16), "1:1 draw identical to nearest");
|
||||
/* texture_lod adds bit 4 to the filter key; xbr must still run (the
|
||||
* magnified quads keep their texels under LOD). */
|
||||
gl_renderer_set_texture_lod(1, 16);
|
||||
scene();
|
||||
check(memcmp(nat, x, sizeof x) == 0, "xbr unchanged with texture_lod on");
|
||||
gl_renderer_set_texture_lod(0, 1);
|
||||
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
|
||||
scene();
|
||||
gl_renderer_render_thread_sync("xbr");
|
||||
check(memcmp(nat, x, sizeof x) == 0, "render thread gives the same pixels");
|
||||
gl_renderer_render_thread_stop();
|
||||
gr_set_texture_filter(0);
|
||||
scene();
|
||||
check(memcmp(nat, ref, sizeof ref) == 0, "nearest after xbr restores the exact image");
|
||||
check(glGetError() == GL_NO_ERROR, "GL errors");
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/* psx_host_rumble.h: title-supplied rumble merge and expiry. */
|
||||
#include <stdio.h>
|
||||
#include "psx_host_rumble.h"
|
||||
|
||||
static int s_fail;
|
||||
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL %s\n", m); ++s_fail; } } while (0)
|
||||
|
||||
int main(void) {
|
||||
PsxHostRumbleSlot slot = {0, 0, 0, 0};
|
||||
uint8_t small = 0, large = 0;
|
||||
psx_host_rumble_merge(&slot, 5, &small, &large);
|
||||
CHECK(small == 0 && large == 0, "never set: guest motors unchanged");
|
||||
|
||||
psx_host_rumble_set(&slot, 10, 1, 150);
|
||||
psx_host_rumble_merge(&slot, 10, &small, &large);
|
||||
CHECK(small == 0xFF && large == 150, "set: small on, large 150");
|
||||
|
||||
small = 0; large = 200;
|
||||
psx_host_rumble_merge(&slot, 12, &small, &large);
|
||||
CHECK(large == 200 && small == 0xFF, "louder guest large motor wins");
|
||||
|
||||
small = 0; large = 0;
|
||||
psx_host_rumble_merge(&slot, 10 + PSX_HOST_RUMBLE_TTL - 1, &small, &large);
|
||||
CHECK(large == 150, "still live just before the TTL");
|
||||
small = 0; large = 0;
|
||||
psx_host_rumble_merge(&slot, 10 + PSX_HOST_RUMBLE_TTL, &small, &large);
|
||||
CHECK(small == 0 && large == 0, "lapses after the TTL without a refresh");
|
||||
|
||||
psx_host_rumble_set(&slot, 0xFFFFFFFEu, 0, 999);
|
||||
small = 0; large = 0;
|
||||
psx_host_rumble_merge(&slot, 1u, &small, &large);
|
||||
CHECK(large == 255 && small == 0, "clamped to 255; VBlank wrap handled");
|
||||
|
||||
if (s_fail) return 1;
|
||||
puts("host rumble tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_internal_resolution.h"
|
||||
#include <stdio.h>
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { fprintf(stderr, "Failed: %s\n", #x); return 1; } } while (0)
|
||||
|
||||
int main(void) {
|
||||
/* Disabled sessions retain the configured baseline; a later launch must
|
||||
* not inherit an earlier active graphics mod's requested height. */
|
||||
CHECK(psx_mod_internal_resolution_request() == 0u);
|
||||
CHECK(psx_mod_set_internal_resolution(1080u));
|
||||
CHECK(psx_mod_internal_resolution_request() == 1080u);
|
||||
CHECK(!psx_mod_set_internal_resolution(119u));
|
||||
CHECK(!psx_mod_set_internal_resolution(8193u));
|
||||
CHECK(!psx_mod_set_internal_resolution(UINT32_MAX));
|
||||
CHECK(psx_mod_internal_resolution_request() == 1080u);
|
||||
psx_mod_internal_resolution_reset();
|
||||
CHECK(psx_mod_internal_resolution_request() == 0u);
|
||||
CHECK(psx_mod_set_internal_resolution(120u));
|
||||
CHECK(psx_mod_set_internal_resolution(8192u));
|
||||
CHECK(psx_mod_set_internal_resolution(0u));
|
||||
CHECK(psx_mod_internal_resolution_request() == 0u);
|
||||
puts("session-scoped mod resolution validation passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "netplay_fmv_confirm.h"
|
||||
#undef NDEBUG
|
||||
#include <assert.h>
|
||||
|
||||
/* Model the gate's counting: the peer's digests trail by `lag` ticks. */
|
||||
static uint32_t streak_after(uint32_t media_end, uint32_t ticks, uint32_t lag)
|
||||
{
|
||||
uint32_t seen = 0, streak = 0, sim;
|
||||
for (sim = media_end; sim < media_end + ticks; ++sim) {
|
||||
const uint32_t resolved = sim > lag ? sim - lag : 0;
|
||||
if (resolved < media_end) continue; /* nothing past the movie yet */
|
||||
streak += rb_fmv_confirm_gain(seen > media_end ? seen : media_end, resolved);
|
||||
if (resolved > seen) seen = resolved;
|
||||
}
|
||||
return streak;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
assert(rb_fmv_confirm_gain(10, 10) == 0);
|
||||
assert(rb_fmv_confirm_gain(10, 9) == 0);
|
||||
assert(rb_fmv_confirm_gain(10, 13) == 3);
|
||||
/* Same machine (no lag) and cross-machine (2-6 ticks of lag) both reach
|
||||
* the 16-tick confirmation well inside the 300-tick cap. */
|
||||
assert(streak_after(1000, 300, 0) >= 16);
|
||||
assert(streak_after(1000, 300, 2) >= 16);
|
||||
assert(streak_after(1000, 300, 6) >= 16);
|
||||
/* Bursty confirmations (a frontier jump) still count every tick once. */
|
||||
assert(streak_after(1000, 40, 30) == 9);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "netplay_peer_frontier.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, msg) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL: %s\n", msg); \
|
||||
failures++; \
|
||||
} else { \
|
||||
printf("ok: %s\n", msg); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
NetplayPeerFrontier f;
|
||||
const uint32_t two = 1u << 1; /* host + seat 1 */
|
||||
const uint32_t three = (1u << 1) | (1u << 2); /* host + seats 1, 2 */
|
||||
const uint32_t four = three | (1u << 3);
|
||||
|
||||
netplay_peer_frontier_reset(&f);
|
||||
CHECK(netplay_peer_frontier_min(&f, three) == 0u, "nothing advertised -> 0");
|
||||
|
||||
netplay_peer_frontier_note(&f, 1, 752u);
|
||||
CHECK(netplay_peer_frontier_min(&f, two) == 752u, "two seats: the single peer's frontier");
|
||||
CHECK(netplay_peer_frontier_min(&f, three) == 0u,
|
||||
"three seats: one advert does not speak for the silent seat");
|
||||
|
||||
/* The 3-seat WAN failure: host had 2400 from the fast peer only. */
|
||||
netplay_peer_frontier_reset(&f);
|
||||
netplay_peer_frontier_note(&f, 1, 2344u);
|
||||
netplay_peer_frontier_note(&f, 2, 2319u);
|
||||
netplay_peer_frontier_note(&f, 1, 2400u);
|
||||
CHECK(netplay_peer_frontier_min(&f, three) == 2319u, "slowest follower bounds the load");
|
||||
|
||||
netplay_peer_frontier_note(&f, 2, 2300u);
|
||||
CHECK(netplay_peer_frontier_min(&f, three) == 2319u, "reordered older advert never lowers");
|
||||
|
||||
netplay_peer_frontier_note(&f, 3, 5000u);
|
||||
CHECK(netplay_peer_frontier_min(&f, four) == 2319u, "four seats: still the slowest");
|
||||
|
||||
netplay_peer_frontier_clamp(&f, 2320u);
|
||||
CHECK(f.through[1] == 2320u && f.through[2] == 2319u && f.through[3] == 2320u,
|
||||
"clamp lowers every seat above the tick, leaves the rest");
|
||||
|
||||
netplay_peer_frontier_note(&f, -1, 9u);
|
||||
netplay_peer_frontier_note(&f, 99, 9u);
|
||||
CHECK(netplay_peer_frontier_min(&f, four) == 2319u, "out-of-range seats ignored");
|
||||
CHECK(netplay_peer_frontier_min(&f, 0u) == 0u, "no peer seats -> 0");
|
||||
|
||||
CHECK(!netplay_fork_cap_retired(0u, 3136u), "no cap: nothing to retire");
|
||||
CHECK(!netplay_fork_cap_retired(1928u, 1920u), "agreed below the cap keeps it");
|
||||
CHECK(netplay_fork_cap_retired(1928u, 1928u), "agreed at the cap retires it");
|
||||
CHECK(netplay_fork_cap_retired(1928u, 3136u), "agreed past the cap retires it (4-seat race)");
|
||||
|
||||
/* Epoch ids: (counter << 3) | initiator slot. */
|
||||
#define EP(n, slot) (((uint32_t)(n) << 3) | (uint32_t)(slot))
|
||||
CHECK(netplay_rb_concurrent_begin(EP(5, 1), EP(5, 1), 0, 7u) == 0, "same epoch: no contest");
|
||||
CHECK(netplay_rb_concurrent_begin(EP(5, 2), EP(5, 1), 0, 7u) == 1,
|
||||
"initiator: lower peer slot wins, we yield");
|
||||
CHECK(netplay_rb_concurrent_begin(EP(5, 1), EP(5, 2), 0, 7u) == -1,
|
||||
"initiator: higher peer slot loses, drop it");
|
||||
CHECK(netplay_rb_concurrent_begin(EP(5, 3), EP(5, 1), 1, 7u) == 1,
|
||||
"follower of slot 3 switches to slot 1 (4-seat WAN split)");
|
||||
CHECK(netplay_rb_concurrent_begin(EP(5, 1), EP(5, 3), 1, 7u) == -1,
|
||||
"follower of slot 1 drops slot 3's BEGIN");
|
||||
CHECK(netplay_rb_concurrent_begin(EP(5, 0), EP(6, 0), 1, 7u) == 0,
|
||||
"follower: initiator reopened -> not a contest (two-seat path)");
|
||||
|
||||
if (failures) {
|
||||
printf("%d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Replay-determinism diff for PSX_RB_SNAP_AUDIT logs.
|
||||
|
||||
usage: rb_snap_audit_diff.py <dir> (dir holds snap-audit-p<N>.txt per seat)
|
||||
|
||||
1. Same seat, replay vs live: every R save at tick T must equal the seat's
|
||||
live L save at T section by section (same snap + same rows => same machine).
|
||||
2. Across seats: the last save of each tick (after any rollback) must agree.
|
||||
Prints the first differing section/byte offsets; exit 1 on any difference.
|
||||
"""
|
||||
import collections, glob, re, sys
|
||||
|
||||
NAMES = {1: 'CPU', 2: 'RAM', 3: 'SPAD', 4: 'IRQ', 5: 'TIMER', 6: 'CLOCK', 7: 'GPU',
|
||||
8: 'VRAM', 9: 'SPU', 0xa: 'SPURAM', 0xb: 'CDROM', 0xc: 'DMA', 0xd: 'SIO',
|
||||
0xe: 'DIRTY', 0xf: 'MDEC', 0x10: 'ICACHE', 0x11: 'MODMEM', 0x13: 'GCS'}
|
||||
|
||||
|
||||
def parse(path):
|
||||
live, replay = {}, collections.defaultdict(list)
|
||||
for line in open(path):
|
||||
p = line.split()
|
||||
if len(p) < 3:
|
||||
continue
|
||||
secs = {}
|
||||
for x in p[2:]:
|
||||
tag, rest = x.split(':', 1)
|
||||
crc, _, hexd = rest.partition('=')
|
||||
secs[int(tag, 16)] = (crc, hexd)
|
||||
(live.__setitem__(int(p[1]), secs) if p[0] == 'L'
|
||||
else replay[int(p[1])].append(secs))
|
||||
return live, replay
|
||||
|
||||
|
||||
def bytes_diff(a, b):
|
||||
if not a or not b:
|
||||
return []
|
||||
x, y = bytes.fromhex(a), bytes.fromhex(b)
|
||||
return [(i, x[i], y[i]) for i in range(min(len(x), len(y))) if x[i] != y[i]][:8]
|
||||
|
||||
|
||||
def main(d):
|
||||
seats = {int(re.search(r'p(-?\d+)', f)[1]): parse(f)
|
||||
for f in glob.glob(f'{d}/snap-audit-p*.txt')}
|
||||
bad = 0
|
||||
for s, (live, rep) in sorted(seats.items()):
|
||||
for t, rows in sorted(rep.items()):
|
||||
for r in rows:
|
||||
for tag, (crc, h) in r.items():
|
||||
if t in live and tag in live[t] and live[t][tag][0] != crc:
|
||||
bad += 1
|
||||
if bad <= 10:
|
||||
print(f'seat {s} tick {t} replay!=live {NAMES.get(tag, hex(tag))}',
|
||||
bytes_diff(live[t][tag][1], h))
|
||||
if len(seats) > 1:
|
||||
common = set.intersection(*(set(v[0]) for v in seats.values()))
|
||||
for t in sorted(common):
|
||||
ref = min(seats)
|
||||
for tag in seats[ref][0][t]:
|
||||
vals = {s: v[0][t].get(tag, ('', ''))[0] for s, v in seats.items()}
|
||||
if len(set(vals.values())) > 1:
|
||||
bad += 1
|
||||
if bad <= 20:
|
||||
print(f'tick {t} seats differ {NAMES.get(tag, hex(tag))} {vals}')
|
||||
print('differences:', bad)
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1]))
|
||||
Reference in New Issue
Block a user