mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Integrate validated SH1 widescreen and save-request fixes
This commit is contained in:
+14
-1
@@ -130,7 +130,8 @@ User has visually confirmed: world geometry, characters, HUD, menus, FMVs, and
|
||||
proportions on the 16:9 stretch.
|
||||
|
||||
**Live diagnostics (TCP debug server, committed `1e28a94`/earlier):**
|
||||
`ws_aspect num den` (toggle the GTE squash on/off in place — `1 1` = off),
|
||||
`ws_aspect num den` (set the GTE squash for a target aspect in place: `4 3` = off/identity, `16 9` = 0.75x;
|
||||
note `1 1` is NOT off, it expands X by 4/3),
|
||||
`ws_far_threshold [t]` (set the SZ depth split + read the backdrop driver's SZ
|
||||
stats so the split is data-driven), `ws_census on|off|<start> <end>` (always-on
|
||||
per-prim draw ring → CSV), `ws_margin <v|-1>` (force the cull margin), and the
|
||||
@@ -583,3 +584,15 @@ To enable 21:9 later: widen the backdrop generation, then promote the launcher's
|
||||
2-state toggle to a 3-way Off / 16:9 / 21:9 (the `cycle_aspect` callback already
|
||||
cycles `aspect_index` 0/1/2 and is the scaffold). Full feasibility analysis in
|
||||
the session memory `native_wide_fov_autocull.md`.
|
||||
|
||||
## Cull margin follows the display width (2026-10-24)
|
||||
|
||||
`psx_ws_x_margin()` on the squash path was `160 * (1/s - 1)`, correct only for
|
||||
320-pixel modes. The reveal is a fraction of the game's own half-width, so it
|
||||
is now `(W/2) * (1/s - 1)` with `W` the live horizontal display width (512 in
|
||||
Army Men's gameplay, 368 in Ape Escape): pixel-domain screen-X culls
|
||||
(`[widescreen.cull]`) get that margin. Ratio-style consumers (terrain angle,
|
||||
aspect cone, `cull_scale`, the gameplay-field scaler) read
|
||||
`psx_ws_x_margin_320()`, the same reveal on the 320 basis, and keep their
|
||||
behaviour in every mode. 320-wide titles are unchanged bit for bit.
|
||||
Test: `ws_cull_margin_width_test`.
|
||||
|
||||
@@ -1034,6 +1034,11 @@ if(BUILD_TESTING)
|
||||
target_include_directories(ws_hud_anchor_test PRIVATE include)
|
||||
add_test(NAME ws_hud_anchor_test COMMAND ws_hud_anchor_test)
|
||||
|
||||
add_executable(ws_gte_sxy_test
|
||||
tests/test_ws_gte_sxy.c)
|
||||
target_include_directories(ws_gte_sxy_test PRIVATE include)
|
||||
add_test(NAME ws_gte_sxy_test COMMAND ws_gte_sxy_test)
|
||||
|
||||
add_executable(gpu_textured_dot_nw_shift_exec_test
|
||||
tests/test_gpu_textured_dot_nw_shift_exec.c
|
||||
src/psx_video_timing.c
|
||||
@@ -1097,6 +1102,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME ws_local_view_margin_test COMMAND ws_local_view_margin_test)
|
||||
|
||||
add_executable(ws_cull_margin_width_test
|
||||
tests/test_ws_cull_margin_width.c
|
||||
src/psx_video_timing.c
|
||||
src/gpu_timeline.c)
|
||||
target_include_directories(ws_cull_margin_width_test PRIVATE include src)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(ws_cull_margin_width_test PRIVATE
|
||||
-ffunction-sections -fdata-sections)
|
||||
target_link_options(ws_cull_margin_width_test PRIVATE
|
||||
$<IF:$<PLATFORM_ID:Darwin>,-Wl$<COMMA>-dead_strip,-Wl$<COMMA>--gc-sections>)
|
||||
endif()
|
||||
add_test(NAME ws_cull_margin_width_test COMMAND ws_cull_margin_width_test)
|
||||
|
||||
# Depth24 GP1(07h) present hold (#636): hold count, upload span and the
|
||||
# staged frame agree across windowed/headless/deferred presents, rollback
|
||||
# replay and disk loads; stale trailing FMV pixels never reach a frame.
|
||||
@@ -1533,6 +1551,9 @@ if(BUILD_TESTING)
|
||||
|
||||
add_executable(mod_resident_test
|
||||
tests/test_mod_resident.cpp
|
||||
src/mod_immediate.c
|
||||
src/host_overlay.c
|
||||
src/func_override.c
|
||||
src/mod_resident.cpp
|
||||
src/mod_runtime.cpp
|
||||
src/mod_netplay.c
|
||||
|
||||
@@ -318,6 +318,8 @@ int gpu_ws_present_native_43(void);
|
||||
/* Per-side X cull-margin (screen/world units) emitted into the game's draw-
|
||||
* cull immediates by the recompiler ([widescreen.cull]); 0 unless stretching. */
|
||||
int psx_ws_x_margin(void);
|
||||
/* The same reveal on the 320-pixel basis (ratio-style sites: angles, cones). */
|
||||
int psx_ws_x_margin_320(void);
|
||||
/* Netplay own-view mods: while on, psx_ws_x_margin() is 0 (the stock cull)
|
||||
* except inside a sandboxed local-view render (render_pass.c sets the scope). */
|
||||
void gpu_ws_set_local_view_only(int on);
|
||||
@@ -525,6 +527,20 @@ void gpu_ws_set_adaptive_backdrop_preload(int enabled);
|
||||
* `prim` is the address of the PsyQ P_TAG word; the drawn command starts at
|
||||
* prim+4. anchor: -1 = left, 0 = center, +1 = right. */
|
||||
void gpu_ws_tag_hud_prim(uint32_t prim, int anchor);
|
||||
/* Same, for a primitive whose X the game took from GTE-projected screen
|
||||
* coordinates, which already carry the widescreen X squash about the display
|
||||
* centre. Projection-and-stretch translates it to the edge instead of scaling
|
||||
* it again; native-wide behaves as for gpu_ws_tag_hud_prim. */
|
||||
void gpu_ws_tag_hud_prim_projected(uint32_t prim, int anchor);
|
||||
/* GTE-projection provenance under the widescreen squash (ws_gte_sxy.h): the
|
||||
* GTE reports each SXY it produces; a title plugin asks whether a packet
|
||||
* vertex is one of them, i.e. whether the game built that primitive from
|
||||
* projected geometry (its X is already squashed) rather than authored
|
||||
* screen coordinates. Opt-in (gpu_ws_set_gte_sxy_tracking) and recorded
|
||||
* only while the squash is engaged. */
|
||||
void gpu_ws_set_gte_sxy_tracking(int on);
|
||||
void gpu_ws_note_gte_sxy(int32_t x, int32_t y);
|
||||
int gpu_ws_gte_sxy_recent(int32_t x, int32_t y);
|
||||
/* Stretch a trusted authored background primitive about the display center in
|
||||
* the native-wide output only (GL/SW). prim+4 is the command address, including
|
||||
* for a command embedded inside a longer DMA packet. Only opaque polygons and
|
||||
|
||||
@@ -372,6 +372,21 @@ void psx_mod_tag_hud_primitive(uint32_t primitive, int edge);
|
||||
* immediately before the GP0 colour command; for a compound E1+SPRT packet
|
||||
* this is its E1 word (P_TAG+4). */
|
||||
void psx_mod_anchor_hud_primitive(uint32_t primitive, int edge);
|
||||
/* As psx_mod_anchor_hud_primitive, for a packet whose X coordinates came out
|
||||
* of the GTE (so they already carry the widescreen squash about the display
|
||||
* centre, and the guest wrote them verbatim). Projection-and-stretch moves the
|
||||
* piece to the selected edge by translation, keeping its proportions; scaling
|
||||
* it about the edge again would squash it twice. */
|
||||
void psx_mod_anchor_hud_primitive_projected(uint32_t primitive, int edge);
|
||||
/* Did the GTE project a vertex at screen (x, y) in this or the previous
|
||||
* vblank? Under the widescreen squash a primitive whose every vertex answers
|
||||
* yes was built from projected geometry (already squashed); one that does not
|
||||
* carries authored screen coordinates. Always 0 while the squash is off. */
|
||||
int psx_mod_gte_sxy_recent(int32_t x, int32_t y);
|
||||
/* Opt in to (1) or out of (0) the recent-projection record behind
|
||||
* psx_mod_gte_sxy_recent. Off by default: until a plugin enables it nothing is
|
||||
* recorded and the query answers 0. Call from an activation callback. */
|
||||
void psx_mod_set_gte_sxy_tracking(int enabled);
|
||||
/* Screen-space masks identified by their title's producer, P_TAG addresses.
|
||||
* Flat panels extend only their exterior boundaries. Radial flat/Gouraud
|
||||
* quads scale around the display centre for an aspect-aware iris transition.
|
||||
|
||||
@@ -169,6 +169,9 @@ void psx_frontend_on_savestate_refused(int is_load, int slot, const char *reason
|
||||
* pending, serialize with cpu->pc = resume_pc; if a load is pending, restore and
|
||||
* longjmp to the scheduler (never returns in that case). Near-free when idle. */
|
||||
void savestate_poll(CPUState* cpu, uint32_t resume_pc);
|
||||
/* Host request latch for the IRQ maintenance fast paths. Read-only outside
|
||||
* savestate.c: a staged request must not wait thousands of quiet block edges. */
|
||||
extern int g_psx_savestate_pending;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef PSXRECOMP_WS_GTE_SXY_H
|
||||
#define PSXRECOMP_WS_GTE_SXY_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Which screen positions did the GTE project recently?
|
||||
*
|
||||
* Under projection-and-stretch widescreen the GTE squashes every projected X
|
||||
* about the display centre. A title that builds HUD from GTE-projected
|
||||
* geometry (vector digits and lettering run through its model renderer) then
|
||||
* writes already-squashed X into its packets, whereas screen-space HUD packets
|
||||
* carry authored X. The two need different corrections, and the packet alone
|
||||
* does not say which it is. The GTE stamps every SXY it produces here with the
|
||||
* vblank it happened in; a packet vertex that matches a recent stamp was
|
||||
* projected.
|
||||
*
|
||||
* "Recent" is WS_GTE_SXY_WINDOW vblanks: games commonly rebuild their packets
|
||||
* every few vblanks and resubmit the same ordering table in between (Army Men
|
||||
* rebuilds every fourth), so a retained packet is still younger than its
|
||||
* projection plus one rebuild period. The cell grid is 1024 x 512, covering
|
||||
* the signed 11-bit GP0 vertex range. A hit is evidence, not proof: with ~1000
|
||||
* projections a frame the chance that a whole polygon matches by accident is
|
||||
* negligible, so require every vertex of a primitive to hit. */
|
||||
#define WS_GTE_SXY_W 1024u
|
||||
#define WS_GTE_SXY_H 512u
|
||||
#define WS_GTE_SXY_CELLS (WS_GTE_SXY_W * WS_GTE_SXY_H)
|
||||
#define WS_GTE_SXY_WINDOW 8u
|
||||
|
||||
typedef struct {
|
||||
uint16_t stamp[WS_GTE_SXY_CELLS]; /* 0x8000 | vblank (15 bits); 0 = never */
|
||||
} WsGteSxy;
|
||||
|
||||
static inline uint32_t ws_gte_sxy_cell(int32_t x, int32_t y) {
|
||||
return (((uint32_t)(y + 256) & (WS_GTE_SXY_H - 1u)) * WS_GTE_SXY_W) +
|
||||
((uint32_t)(x + 512) & (WS_GTE_SXY_W - 1u));
|
||||
}
|
||||
|
||||
static inline void ws_gte_sxy_note(WsGteSxy *s, uint32_t frame,
|
||||
int32_t x, int32_t y) {
|
||||
s->stamp[ws_gte_sxy_cell(x, y)] = (uint16_t)(0x8000u | (frame & 0x7FFFu));
|
||||
}
|
||||
|
||||
static inline int ws_gte_sxy_recent(const WsGteSxy *s, uint32_t frame,
|
||||
int32_t x, int32_t y) {
|
||||
const uint16_t st = s->stamp[ws_gte_sxy_cell(x, y)];
|
||||
if (!st) return 0;
|
||||
return ((frame - st) & 0x7FFFu) <= WS_GTE_SXY_WINDOW;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -20,6 +20,11 @@ typedef struct {
|
||||
uint32_t frame;
|
||||
int8_t anchor;
|
||||
uint8_t used;
|
||||
/* The primitive's X came out of the GTE and already carries the
|
||||
* widescreen squash about the display centre (see ws_gte_sxy.h). The
|
||||
* projection path then translates it to the anchor instead of scaling it
|
||||
* about the anchor, and never lets automatic UI layout squash it again. */
|
||||
uint8_t projected;
|
||||
WsPrepassPacketGuard guard;
|
||||
} WsHudAnchorTag;
|
||||
|
||||
@@ -54,10 +59,12 @@ static inline void ws_hud_anchor_clear(WsHudAnchorTag *tags,
|
||||
tags[i].used = 0;
|
||||
}
|
||||
|
||||
static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count,
|
||||
uint32_t command_addr, int anchor,
|
||||
const WsPrepassPacketGuard *guard,
|
||||
uint32_t frame) {
|
||||
static inline void ws_hud_anchor_insert_stored(WsHudAnchorTag *tags,
|
||||
uint32_t count,
|
||||
uint32_t command_addr,
|
||||
int anchor, int projected,
|
||||
const WsPrepassPacketGuard *guard,
|
||||
uint32_t frame) {
|
||||
if (!tags || !count || !guard) return;
|
||||
uint32_t idx = ws_hud_anchor_slot(command_addr) & (count - 1u);
|
||||
uint32_t victim = idx;
|
||||
@@ -75,9 +82,29 @@ static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count,
|
||||
tags[victim].command_addr = command_addr;
|
||||
tags[victim].frame = frame;
|
||||
tags[victim].anchor = (int8_t)ws_hud_anchor_clamp(anchor);
|
||||
tags[victim].projected = projected ? 1u : 0u;
|
||||
tags[victim].guard = *guard;
|
||||
}
|
||||
|
||||
static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count,
|
||||
uint32_t command_addr, int anchor,
|
||||
const WsPrepassPacketGuard *guard,
|
||||
uint32_t frame) {
|
||||
ws_hud_anchor_insert_stored(tags, count, command_addr, anchor, 0, guard,
|
||||
frame);
|
||||
}
|
||||
|
||||
/* Tag a GTE-projected piece (see WsHudAnchorTag.projected). */
|
||||
static inline void ws_hud_anchor_insert_projected(WsHudAnchorTag *tags,
|
||||
uint32_t count,
|
||||
uint32_t command_addr,
|
||||
int edge,
|
||||
const WsPrepassPacketGuard *guard,
|
||||
uint32_t frame) {
|
||||
ws_hud_anchor_insert_stored(tags, count, command_addr, edge, 1, guard,
|
||||
frame);
|
||||
}
|
||||
|
||||
/* Lookup outcome, so callers can keep always-on counters that say WHY a tagged
|
||||
* primitive was or was not honoured (absent, expired, or packet rewritten). */
|
||||
enum {
|
||||
@@ -87,13 +114,14 @@ enum {
|
||||
WS_HUD_ANCHOR_GUARD_MISMATCH = -2
|
||||
};
|
||||
|
||||
static inline int ws_hud_anchor_lookup_result(const WsHudAnchorTag *tags,
|
||||
uint32_t count,
|
||||
uint32_t command_addr,
|
||||
const uint32_t *words,
|
||||
uint32_t word_count,
|
||||
uint32_t frame,
|
||||
int *out_anchor) {
|
||||
static inline int ws_hud_anchor_lookup_result_ex(const WsHudAnchorTag *tags,
|
||||
uint32_t count,
|
||||
uint32_t command_addr,
|
||||
const uint32_t *words,
|
||||
uint32_t word_count,
|
||||
uint32_t frame,
|
||||
int *out_anchor,
|
||||
int *out_projected) {
|
||||
if (!tags || !count || !words) return WS_HUD_ANCHOR_ABSENT;
|
||||
uint32_t idx = ws_hud_anchor_slot(command_addr) & (count - 1u);
|
||||
for (uint32_t i = 0; i < WS_HUD_ANCHOR_PROBES && i < count; i++) {
|
||||
@@ -106,11 +134,23 @@ static inline int ws_hud_anchor_lookup_result(const WsHudAnchorTag *tags,
|
||||
if (!ws_prepass_packet_matches(&tag->guard, words, word_count))
|
||||
return WS_HUD_ANCHOR_GUARD_MISMATCH;
|
||||
if (out_anchor) *out_anchor = tag->anchor;
|
||||
if (out_projected) *out_projected = tag->projected;
|
||||
return WS_HUD_ANCHOR_HIT;
|
||||
}
|
||||
return WS_HUD_ANCHOR_ABSENT;
|
||||
}
|
||||
|
||||
static inline int ws_hud_anchor_lookup_result(const WsHudAnchorTag *tags,
|
||||
uint32_t count,
|
||||
uint32_t command_addr,
|
||||
const uint32_t *words,
|
||||
uint32_t word_count,
|
||||
uint32_t frame,
|
||||
int *out_anchor) {
|
||||
return ws_hud_anchor_lookup_result_ex(tags, count, command_addr, words,
|
||||
word_count, frame, out_anchor, 0);
|
||||
}
|
||||
|
||||
static inline int ws_hud_anchor_lookup(const WsHudAnchorTag *tags,
|
||||
uint32_t count,
|
||||
uint32_t command_addr,
|
||||
|
||||
@@ -16988,7 +16988,23 @@ void debug_server_init(int port)
|
||||
#endif
|
||||
|
||||
int yes = 1;
|
||||
#ifdef _WIN32
|
||||
/* Winsock SO_REUSEADDR permits two listeners on one address/port, with
|
||||
* indeterminate routing. Parallel QA commands must reach exactly one
|
||||
* runtime. Exclusive ownership also refuses an already occupied port.
|
||||
* https://learn.microsoft.com/windows/win32/winsock/so-exclusiveaddruse */
|
||||
if (setsockopt(s_listen, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
|
||||
(const char *)&yes, sizeof(yes)) != 0) {
|
||||
s_listen_err = sock_error();
|
||||
fprintf(stdout, "psxrecomp: debug server exclusive socket option FAILED (%d)\n",
|
||||
s_listen_err);
|
||||
sock_close(s_listen);
|
||||
s_listen = SOCK_INVALID;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
setsockopt(s_listen, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
|
||||
#endif
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
|
||||
+128
-31
@@ -44,6 +44,7 @@
|
||||
#include "ws_backdrop_extent.h"
|
||||
#include "ws_prepass_guard.h"
|
||||
#include "ws_hud_anchor.h"
|
||||
#include "ws_gte_sxy.h"
|
||||
#include "ws_repeat_rect.h"
|
||||
#include "ws_feedback_rect.h"
|
||||
#include "ws_screen_mask.h"
|
||||
@@ -739,6 +740,8 @@ int gpu_ws_netplay_local_viewport_width(void) {
|
||||
* unconditionally so a probe can sweep the cull margin live (0 = force 4:3
|
||||
* cull while still stretching; large = over-draw) at a fixed camera position.
|
||||
* -1 = normal computed margin. */
|
||||
static uint32_t gpu_h_display_width(void);
|
||||
static int ws_px_to_320(int px);
|
||||
static int ws_margin_override = -1;
|
||||
static int ws_cull_guard_pixels = 0;
|
||||
static int ws_activation_guard_pixels = 0;
|
||||
@@ -1242,7 +1245,7 @@ void gpu_ws_set_cull_scale_sites(const uint32_t *addresses,
|
||||
* (x < half*z/H) widens to the live view by scaling the bound by
|
||||
* (half + margin) / half. 64-bit intermediate; saturates to int32. */
|
||||
int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent) {
|
||||
const int32_t margin = psx_ws_x_margin();
|
||||
const int32_t margin = psx_ws_x_margin_320();
|
||||
if (margin <= 0 || half_extent <= 0) return bound;
|
||||
int64_t v = (int64_t)bound * (int64_t)(half_extent + margin) / half_extent;
|
||||
if (v > INT32_MAX) v = INT32_MAX;
|
||||
@@ -1298,7 +1301,7 @@ void gpu_ws_set_angle_sites(const uint32_t *addresses,
|
||||
|
||||
uint32_t psx_ws_angle_widen(uint32_t vanilla) {
|
||||
ws_angle_calls++;
|
||||
const int margin = psx_ws_x_margin();
|
||||
const int margin = psx_ws_x_margin_320();
|
||||
if (margin <= 0) {
|
||||
ws_angle_43_identity++;
|
||||
return vanilla;
|
||||
@@ -1526,7 +1529,7 @@ uint32_t psx_ws_aspect_cone_result(uint32_t site_address, uint32_t vanilla,
|
||||
|
||||
ws_aspect_cone_calls++;
|
||||
site->calls++;
|
||||
const int total_margin = psx_ws_x_margin();
|
||||
const int total_margin = psx_ws_x_margin_320();
|
||||
if (total_margin <= 0) {
|
||||
ws_aspect_cone_43_identity++;
|
||||
site->identity_43++;
|
||||
@@ -1551,8 +1554,8 @@ uint32_t psx_ws_aspect_cone_result(uint32_t site_address, uint32_t vanilla,
|
||||
}
|
||||
|
||||
const int visible_margin =
|
||||
(total_margin > ws_cull_guard_pixels)
|
||||
? total_margin - ws_cull_guard_pixels : 0;
|
||||
(total_margin > ws_px_to_320(ws_cull_guard_pixels))
|
||||
? total_margin - ws_px_to_320(ws_cull_guard_pixels) : 0;
|
||||
const int32_t fx =
|
||||
(int16_t)psx_read_half(ws_aspect_cone_forward_addr +
|
||||
ws_aspect_cone_forward_offsets[0]);
|
||||
@@ -1574,7 +1577,7 @@ uint32_t psx_ws_aspect_cone_result(uint32_t site_address, uint32_t vanilla,
|
||||
!in_visible && !in_guard && state->active &&
|
||||
psx_ws_aspect_cone_contains(
|
||||
x, z, y, fx, fz, fy, threshold,
|
||||
total_margin + (int)ws_aspect_cone_hysteresis_pixels);
|
||||
total_margin + ws_px_to_320((int)ws_aspect_cone_hysteresis_pixels));
|
||||
|
||||
if (!in_visible && !in_guard && !in_hysteresis) {
|
||||
state->active = 0;
|
||||
@@ -1627,6 +1630,18 @@ void gpu_ws_set_local_view_only(int on) { ws_local_view_only = on ? 1 : 0; }
|
||||
void gpu_ws_set_local_view_scope(int on) { ws_local_view_scope = on ? 1 : 0; }
|
||||
int gpu_ws_local_view_only(void) { return ws_local_view_only; }
|
||||
|
||||
/* Half of the game's current horizontal display width (160 for 320 modes). */
|
||||
static int ws_cull_half_width(void) {
|
||||
int src_w = 0;
|
||||
uint32_t w;
|
||||
if (ws_local_viewport_layout(NULL, &src_w, NULL, NULL))
|
||||
w = (uint32_t)src_w;
|
||||
else
|
||||
w = gpu_h_display_width();
|
||||
if (w < 64u) w = 320u;
|
||||
return (int)(w / 2u);
|
||||
}
|
||||
|
||||
int psx_ws_x_margin(void) {
|
||||
if (ws_local_view_only && !ws_local_view_scope) return 0;
|
||||
if (ws_margin_override >= 0) return ws_margin_override;
|
||||
@@ -1645,10 +1660,28 @@ int psx_ws_x_margin(void) {
|
||||
return ws_nw_configured_offset() * ((ws_view_camera_addr || ws_view_manual) ? 2 : 1)
|
||||
+ ws_cull_guard_pixels;
|
||||
if (!ws_active()) return 0;
|
||||
return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum
|
||||
/* The reveal is a fraction of the game's own half-width: a 512-wide mode
|
||||
* (Army Men in-game) hides 256*(1/s-1) px per side, not the 160*(1/s-1)
|
||||
* a 320 mode does. The screen-X compares live in that mode's pixels. */
|
||||
return (ws_cull_half_width() * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum
|
||||
+ ws_cull_guard_pixels;
|
||||
}
|
||||
|
||||
/* A pixel quantity of the current display mode on the 320-pixel basis. */
|
||||
static int ws_px_to_320(int px) {
|
||||
const int half = ws_cull_half_width();
|
||||
return half == 160 ? px : (int)(((int64_t)px * 160 + half / 2) / half);
|
||||
}
|
||||
|
||||
/* The same margin on the 320-pixel basis. Camera-space frustum angles, aspect
|
||||
* cones, scaled bounds and gameplay-field scalers are ratios authored against
|
||||
* a 160-pixel half-screen; they must not grow with the display mode. */
|
||||
int psx_ws_x_margin_320(void) {
|
||||
const int m = psx_ws_x_margin();
|
||||
const int half = ws_cull_half_width();
|
||||
return half == 160 ? m : (int)(((int64_t)m * 160 + half / 2) / half);
|
||||
}
|
||||
|
||||
int psx_ws_activation_margin(void) {
|
||||
const int margin = psx_ws_x_margin();
|
||||
return margin > 0 ? margin + ws_activation_guard_pixels : 0;
|
||||
@@ -1656,7 +1689,7 @@ int psx_ws_activation_margin(void) {
|
||||
|
||||
int32_t psx_ws_player_x_bound(int32_t vanilla)
|
||||
{
|
||||
const int margin = psx_ws_x_margin();
|
||||
const int margin = psx_ws_x_margin_320();
|
||||
if (margin <= 0) return vanilla;
|
||||
return (int32_t)(((int64_t)vanilla * (320 + 2 * margin)) / 320);
|
||||
}
|
||||
@@ -2662,7 +2695,23 @@ static void ws_tag_stats_note_lookup(GpuWsTagStats *stats, int result) {
|
||||
}
|
||||
}
|
||||
|
||||
void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) {
|
||||
/* Positions the GTE projected this and the previous vblank (ws_gte_sxy.h).
|
||||
* Opt-in: nothing is recorded (and every query answers no) until a title
|
||||
* plugin enables it, and only while the squash is engaged, so titles that do
|
||||
* not use the query pay nothing and see no change. */
|
||||
static WsGteSxy ws_gte_sxy;
|
||||
static int ws_gte_sxy_tracking; /* opt-in: a title plugin turns it on */
|
||||
void gpu_ws_set_gte_sxy_tracking(int on) { ws_gte_sxy_tracking = on ? 1 : 0; }
|
||||
void gpu_ws_note_gte_sxy(int32_t x, int32_t y) {
|
||||
if (!ws_gte_sxy_tracking || !ws_active()) return;
|
||||
ws_gte_sxy_note(&ws_gte_sxy, (uint32_t)s_frame_count, x, y);
|
||||
}
|
||||
int gpu_ws_gte_sxy_recent(int32_t x, int32_t y) {
|
||||
if (!ws_gte_sxy_tracking) return 0;
|
||||
return ws_gte_sxy_recent(&ws_gte_sxy, (uint32_t)s_frame_count, x, y);
|
||||
}
|
||||
|
||||
static void ws_tag_hud_prim(uint32_t prim, int anchor, int projected) {
|
||||
ws_hud_tag_stats.tag_calls++;
|
||||
uint32_t command_addr = prim + 4u;
|
||||
uint32_t words[12];
|
||||
@@ -2676,11 +2725,23 @@ void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) {
|
||||
|
||||
WsPrepassPacketGuard guard =
|
||||
ws_prepass_packet_guard(words, word_count);
|
||||
ws_hud_anchor_insert(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
GPU_RAM_KEY(command_addr), anchor, &guard,
|
||||
(uint32_t)s_frame_count);
|
||||
if (projected)
|
||||
ws_hud_anchor_insert_projected(ws_hud_anchor_tags,
|
||||
WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
GPU_RAM_KEY(command_addr), anchor,
|
||||
&guard, (uint32_t)s_frame_count);
|
||||
else
|
||||
ws_hud_anchor_insert(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
GPU_RAM_KEY(command_addr), anchor, &guard,
|
||||
(uint32_t)s_frame_count);
|
||||
ws_hud_tag_stats.last_tag_frame = (uint32_t)s_frame_count;
|
||||
}
|
||||
void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) {
|
||||
ws_tag_hud_prim(prim, anchor, 0);
|
||||
}
|
||||
void gpu_ws_tag_hud_prim_projected(uint32_t prim, int anchor) {
|
||||
ws_tag_hud_prim(prim, anchor, 1);
|
||||
}
|
||||
|
||||
void gpu_ws_tag_background_prim(uint32_t prim) {
|
||||
ws_background_tag_stats.tag_calls++;
|
||||
@@ -2816,7 +2877,7 @@ static int ws_nw_radial_mask_transform(int32_t x[4], int32_t y[4]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int ws_explicit_hud_anchor(int *out_anchor) {
|
||||
static int ws_explicit_hud_anchor_ex(int *out_anchor, int *out_projected) {
|
||||
if (gp0_cmd_source_addr == 0xFFFFFFFFu)
|
||||
return 0;
|
||||
if (gp0_words_needed <= 0 || gp0_words_needed > 12)
|
||||
@@ -2825,17 +2886,21 @@ static int ws_explicit_hud_anchor(int *out_anchor) {
|
||||
if ((command_addr & 3u) ||
|
||||
(command_addr >= psx_ram_live_bytes() &&
|
||||
!psx_mod_gpu_dma_memory_contains(command_addr, 4u))) return 0;
|
||||
int anchor = 0;
|
||||
int result = ws_hud_anchor_lookup_result(
|
||||
int anchor = 0, projected = 0;
|
||||
int result = ws_hud_anchor_lookup_result_ex(
|
||||
ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE, GPU_RAM_KEY(command_addr),
|
||||
gp0_cmd_buf, (uint32_t)gp0_words_needed, (uint32_t)s_frame_count,
|
||||
&anchor);
|
||||
&anchor, &projected);
|
||||
ws_tag_stats_note_lookup(&ws_hud_tag_stats, result);
|
||||
if (result != WS_HUD_ANCHOR_HIT)
|
||||
return 0;
|
||||
if (out_anchor) *out_anchor = anchor;
|
||||
if (out_projected) *out_projected = projected;
|
||||
return 1;
|
||||
}
|
||||
static int ws_explicit_hud_anchor(int *out_anchor) {
|
||||
return ws_explicit_hud_anchor_ex(out_anchor, NULL);
|
||||
}
|
||||
|
||||
static int ws_nw_explicit_hud_delta(int32_t *out_delta) {
|
||||
if (out_delta) *out_delta = 0;
|
||||
@@ -2850,19 +2915,36 @@ static int ws_nw_explicit_hud_delta(int32_t *out_delta) {
|
||||
/* A title can identify one widget whose pieces arrive in separate DMA lists.
|
||||
* Per-list auto grouping cannot see that ownership. Use the same guarded
|
||||
* edge/centre role as native-wide, expressed as a squash pivot here. */
|
||||
static int ws_projection_hud_anchor(int32_t *out_anchor) {
|
||||
static int ws_projection_hud_anchor_ex(int32_t *out_anchor, int *out_projected) {
|
||||
if (!ws_active()) return 0;
|
||||
int edge;
|
||||
if (!ws_explicit_hud_anchor(&edge)) return 0;
|
||||
int edge, projected = 0;
|
||||
if (!ws_explicit_hud_anchor_ex(&edge, &projected)) return 0;
|
||||
if (out_projected) *out_projected = projected;
|
||||
if (out_anchor)
|
||||
*out_anchor = ws_disp_x() + (edge < 0 ? 0 : edge > 0 ? ws_disp_w() : ws_disp_w() / 2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int ws_projection_hud_anchor(int32_t *out_anchor) {
|
||||
return ws_projection_hud_anchor_ex(out_anchor, NULL);
|
||||
}
|
||||
|
||||
/* Shift that takes an X already squashed about the display centre to where
|
||||
* scaling it about the anchor would have put it: (anchor - centre) * (1 - s). */
|
||||
static int32_t ws_projected_hud_shift(int32_t anchor) {
|
||||
return ws_scale_about(0, anchor) - ws_scale_about(0, ws_disp_x() + ws_disp_w() / 2);
|
||||
}
|
||||
|
||||
static int ws_projection_hud_transform(int32_t *x, int count) {
|
||||
int32_t anchor;
|
||||
if (!ws_projection_hud_anchor(&anchor)) return 0;
|
||||
for (int i = 0; i < count; i++) x[i] = ws_scale_about(x[i], anchor);
|
||||
int projected = 0;
|
||||
if (!ws_projection_hud_anchor_ex(&anchor, &projected)) return 0;
|
||||
if (projected) {
|
||||
const int32_t shift = ws_projected_hud_shift(anchor);
|
||||
for (int i = 0; i < count; i++) x[i] += shift;
|
||||
} else {
|
||||
for (int i = 0; i < count; i++) x[i] = ws_scale_about(x[i], anchor);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -3364,9 +3446,14 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], int32_t vy[4]) {
|
||||
static int ws_auto_ui_transform_rect(int32_t *x, int32_t *y, int *w, int *h) {
|
||||
if (!x || !y || !w || !h || *w <= 0) return 0;
|
||||
int32_t explicit_anchor;
|
||||
if (ws_projection_hud_anchor(&explicit_anchor)) {
|
||||
*x = ws_scale_about(*x, explicit_anchor);
|
||||
*w = (int)ws_scale_len(*w);
|
||||
int explicit_projected = 0;
|
||||
if (ws_projection_hud_anchor_ex(&explicit_anchor, &explicit_projected)) {
|
||||
if (explicit_projected) {
|
||||
*x += ws_projected_hud_shift(explicit_anchor);
|
||||
} else {
|
||||
*x = ws_scale_about(*x, explicit_anchor);
|
||||
*w = (int)ws_scale_len(*w);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged())
|
||||
@@ -4653,12 +4740,10 @@ int gpu_display_is_depth24(void) {
|
||||
return (int)(display_depth & 1u);
|
||||
}
|
||||
|
||||
void gpu_get_display_info(GpuDisplayInfo* out) {
|
||||
out->display_x = display_area_x;
|
||||
out->display_y = display_area_y;
|
||||
out->depth24 = (int)(display_depth & 1u);
|
||||
out->disabled = (int)display_disabled;
|
||||
|
||||
/* Visible horizontal pixel width (GP1(08h) mode + GP1(06h) range). Shared by
|
||||
* gpu_get_display_info and the widescreen cull margin, which is called per
|
||||
* vertex and must not pay for the vertical layout. */
|
||||
static uint32_t gpu_h_display_width(void) {
|
||||
/* Dot-clock divider from GP1(08h) hres (PSX-SPX graphicsprocessingunitgpu,
|
||||
* "Horizontal Timings", Dotclocks: /10, /8, /7, /5, /4). */
|
||||
uint32_t cycles;
|
||||
@@ -4682,6 +4767,18 @@ void gpu_get_display_info(GpuDisplayInfo* out) {
|
||||
if (w == 0u) w = 4u;
|
||||
}
|
||||
|
||||
if (w > 640) w = 640;
|
||||
return w;
|
||||
}
|
||||
|
||||
void gpu_get_display_info(GpuDisplayInfo* out) {
|
||||
out->display_x = display_area_x;
|
||||
out->display_y = display_area_y;
|
||||
out->depth24 = (int)(display_depth & 1u);
|
||||
out->disabled = (int)display_disabled;
|
||||
|
||||
uint32_t w = gpu_h_display_width();
|
||||
|
||||
/* DuckStation GetFullDisplayResolution: clamp Y1/Y2 to the broadcast
|
||||
* active region before taking the difference. Unclamped Y2 past the
|
||||
* active end (common overscan programming) includes a flickering junk
|
||||
@@ -4707,7 +4804,7 @@ void gpu_get_display_info(GpuDisplayInfo* out) {
|
||||
* of the video — do not reintroduce it. Right-edge junk is a separate
|
||||
* present/filter issue, not a reason to shrink CRTC width. */
|
||||
|
||||
/* Clamp to sane maximums */
|
||||
/* Clamp to sane maximums (gpu_h_display_width clamps too) */
|
||||
if (w > 640) w = 640;
|
||||
if (out->depth24) {
|
||||
if (screen_h > PSX_DISPLAY_PRESENT_MAX_HEIGHT)
|
||||
|
||||
@@ -17,6 +17,7 @@ extern "C" int psx_netplay_active(void);
|
||||
|
||||
extern "C" uint32_t psx_read_word(uint32_t addr);
|
||||
extern "C" int gpu_ws_precise_nclip_enabled(void);
|
||||
extern "C" void gpu_ws_note_gte_sxy(int32_t x, int32_t y);
|
||||
/* overlay_loader.c: nonzero while an overlay shadow diff (interpreter record
|
||||
* plus native replay of one candidate) is running. */
|
||||
extern "C" int psx_overlay_shadow_diff_active(void);
|
||||
@@ -1259,6 +1260,8 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
int64_t sx = sx16 >> 16;
|
||||
int64_t sy = sy16 >> 16;
|
||||
gte->push_sxy(sx, sy);
|
||||
gpu_ws_note_gte_sxy((int16_t)(gte->SXY[2] & 0xFFFF),
|
||||
(int16_t)((uint32_t)gte->SXY[2] >> 16));
|
||||
if (!s_gte_replay_sandbox) {
|
||||
/* Bound the 16.16 transport so an extreme projection cannot wrap an
|
||||
* int32 and masquerade as a valid on-screen shadow. */
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
*/
|
||||
|
||||
#include "interrupts.h"
|
||||
#include "savestate.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "savestate.h"
|
||||
#include "psx_memory.h"
|
||||
@@ -1224,7 +1225,7 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
uint32_t sr = cpu->cop0[COP0_SR];
|
||||
if (!(sr & 0x01u) || !(sr & (1u << 10))) {
|
||||
s_irq_path_fast_sr++;
|
||||
if ((++s_fast_maintenance & 0x3FFFu) == 0) {
|
||||
if ((++s_fast_maintenance & 0x3FFFu) == 0 || g_psx_savestate_pending) {
|
||||
extern void savestate_poll(CPUState* cpu, uint32_t resume_pc);
|
||||
extern void psx_netplay_poll_snap(CPUState* cpu, uint32_t resume_pc);
|
||||
extern void psx_selfcheck_poll(CPUState* cpu, uint32_t resume_pc);
|
||||
@@ -1278,7 +1279,7 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
}
|
||||
if ((i_stat & i_mask) == 0 && sw_pending == 0) {
|
||||
s_irq_path_fast_none++;
|
||||
if ((++s_fast_maintenance & 0x3FFFu) == 0) {
|
||||
if ((++s_fast_maintenance & 0x3FFFu) == 0 || g_psx_savestate_pending) {
|
||||
extern void savestate_poll(CPUState* cpu, uint32_t resume_pc);
|
||||
extern void psx_netplay_poll_snap(CPUState* cpu, uint32_t resume_pc);
|
||||
extern void psx_selfcheck_poll(CPUState* cpu, uint32_t resume_pc);
|
||||
@@ -1307,7 +1308,7 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
s_last_interrupt_check_cycle = psx_get_cycle_count();
|
||||
g_ls_suppress_record++;
|
||||
total_checks++;
|
||||
if ((total_checks & 0x3FFFu) == 0) {
|
||||
if ((total_checks & 0x3FFFu) == 0 || g_psx_savestate_pending) {
|
||||
extern void savestate_poll(CPUState* cpu, uint32_t resume_pc);
|
||||
extern void psx_netplay_poll_snap(CPUState* cpu, uint32_t resume_pc);
|
||||
extern void psx_selfcheck_poll(CPUState* cpu, uint32_t resume_pc);
|
||||
|
||||
@@ -3658,6 +3658,15 @@ extern "C" void psx_mod_tag_hud_primitive(uint32_t primitive, int edge) {
|
||||
extern "C" void psx_mod_anchor_hud_primitive(uint32_t primitive, int edge) {
|
||||
gpu_ws_tag_hud_prim(primitive, edge);
|
||||
}
|
||||
extern "C" void psx_mod_set_gte_sxy_tracking(int enabled) {
|
||||
gpu_ws_set_gte_sxy_tracking(enabled);
|
||||
}
|
||||
extern "C" int psx_mod_gte_sxy_recent(int32_t x, int32_t y) {
|
||||
return gpu_ws_gte_sxy_recent(x, y);
|
||||
}
|
||||
extern "C" void psx_mod_anchor_hud_primitive_projected(uint32_t primitive, int edge) {
|
||||
gpu_ws_tag_hud_prim_projected(primitive, edge);
|
||||
}
|
||||
extern "C" void psx_mod_tag_screen_mask_quad(uint32_t primitive) {
|
||||
gpu_ws_tag_screen_mask_quad(primitive);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ static void cancel_pending_states(void) {
|
||||
if (s_load_pending >= 0) s_load_failed = 1;
|
||||
s_save_pending = s_load_pending = s_save_defer_slot = -1;
|
||||
s_last_save_pc = 0;
|
||||
s_status_pending = 0;
|
||||
g_psx_savestate_pending = 0;
|
||||
clear_load_blob();
|
||||
}
|
||||
|
||||
@@ -886,7 +886,7 @@ int savestate_pending(void) {
|
||||
void savestate_get_status(PSXSavestateStatus* out) {
|
||||
if (!out) return;
|
||||
out->generation = s_status_generation;
|
||||
out->pending = s_status_pending;
|
||||
out->pending = g_psx_savestate_pending;
|
||||
out->last_ok = s_status_last_ok;
|
||||
out->last_load = s_status_last_load;
|
||||
out->last_slot = s_status_last_slot;
|
||||
@@ -967,7 +967,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
|
||||
s_save_defer_slot = -1;
|
||||
s_last_save_pc = 0;
|
||||
s_save_failed = 1;
|
||||
s_status_pending = 0;
|
||||
g_psx_savestate_pending = 0;
|
||||
s_status_last_ok = 0;
|
||||
s_status_generation++;
|
||||
fprintf(stderr,
|
||||
@@ -1007,7 +1007,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
|
||||
s_last_save_pc = 0;
|
||||
s_save_failed = 1;
|
||||
}
|
||||
s_status_pending = 0;
|
||||
g_psx_savestate_pending = 0;
|
||||
s_status_last_ok = ok ? 1 : 0;
|
||||
s_status_generation++;
|
||||
fprintf(stderr, "savestate: %s slot %d @ pc=0x%08X -> %s\n",
|
||||
@@ -1016,7 +1016,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
|
||||
} else {
|
||||
s_last_save_pc = 0;
|
||||
s_save_failed = 1;
|
||||
s_status_pending = 0;
|
||||
g_psx_savestate_pending = 0;
|
||||
s_status_last_ok = 0;
|
||||
s_status_generation++;
|
||||
psx_frontend_on_savestate_notify(0, slot, 0);
|
||||
@@ -1099,7 +1099,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
|
||||
#endif
|
||||
free(rollback);
|
||||
if (!loaded) {
|
||||
s_status_pending = 0;
|
||||
g_psx_savestate_pending = 0;
|
||||
s_status_last_ok = 0;
|
||||
s_status_generation++;
|
||||
}
|
||||
@@ -1116,7 +1116,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
|
||||
cdrom_accelerate_after_savestate();
|
||||
/* Netplay post-load barrier observes this before the longjmp. */
|
||||
s_load_completed = 1;
|
||||
s_status_pending = 0;
|
||||
g_psx_savestate_pending = 0;
|
||||
s_status_last_ok = 1;
|
||||
s_status_generation++;
|
||||
/* Restage FBO/present latch so the restored frame is visible
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Windows integration gate using real hidden runtime processes.
|
||||
|
||||
Provide a disposable working directory/config and explicit BIOS/disc. Neither
|
||||
the title's release tree nor real memory cards are used. Logs are retained in
|
||||
--output; child processes are always stopped by their owned process handles.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
p=argparse.ArgumentParser()
|
||||
for name in ('exe','bios','disc','workdir','output'):
|
||||
p.add_argument('--'+name,type=Path,required=True)
|
||||
a=p.parse_args()
|
||||
for name in ('exe','bios','disc','workdir','output'):
|
||||
setattr(a,name,getattr(a,name).resolve())
|
||||
if os.name != 'nt':
|
||||
raise SystemExit('This integration gate targets Winsock on Windows.')
|
||||
a.output.mkdir(parents=True,exist_ok=True)
|
||||
children=[]
|
||||
streams=[]
|
||||
result={}
|
||||
def launch(name,port):
|
||||
out=a.output/(name+'.stdout.log'); err=a.output/(name+'.stderr.log')
|
||||
stdout=out.open('wb'); stderr=err.open('wb'); streams.extend((stdout,stderr))
|
||||
cards=a.output/(name+'-cards'); cards.mkdir(exist_ok=True)
|
||||
env=dict(os.environ,SDL_AUDIODRIVER='dummy',PSX_OVERLAY_AUTOCOMPILE_OFF='1')
|
||||
child=subprocess.Popen([str(a.exe),'--headless','--no-launcher','--bios',str(a.bios),
|
||||
'--disc',str(a.disc),'--memcard-dir',str(cards),
|
||||
'--debug-port',str(port)],cwd=a.workdir,env=env,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,stdout=stdout,stderr=stderr)
|
||||
children.append(child)
|
||||
return child,out
|
||||
def wait_log(child,path,needle):
|
||||
end=time.monotonic()+20
|
||||
while time.monotonic()<end:
|
||||
text=path.read_text(errors='replace')
|
||||
if needle in text: return text
|
||||
if child.poll() is not None: raise AssertionError((child.returncode,text))
|
||||
time.sleep(.1)
|
||||
raise TimeoutError((needle,path.read_text(errors='replace')))
|
||||
def stop(child):
|
||||
if child.poll() is None:
|
||||
child.terminate(); child.wait(timeout=10)
|
||||
def unused_port():
|
||||
with socket.socket() as s:
|
||||
s.bind(('127.0.0.1',0)); return s.getsockname()[1]
|
||||
def owners(port):
|
||||
code=f'@(Get-NetTCPConnection -LocalPort {port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess) | ConvertTo-Json -Compress'
|
||||
output=subprocess.check_output(['powershell.exe','-NoProfile','-Command',code],text=True).strip()
|
||||
value=json.loads(output) if output else []
|
||||
return value if isinstance(value,list) else [value]
|
||||
|
||||
try:
|
||||
# An existing legacy reusable listener must not be hijacked by this runtime.
|
||||
with socket.socket() as legacy:
|
||||
legacy.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
|
||||
legacy.bind(('127.0.0.1',0)); legacy.listen()
|
||||
port=legacy.getsockname()[1]
|
||||
child,log=launch('occupied-legacy',port)
|
||||
wait_log(child,log,f'debug server bind({port}) FAILED')
|
||||
assert owners(port)==[os.getpid()],owners(port)
|
||||
result['legacy_listener_preserved']=True
|
||||
stop(child)
|
||||
|
||||
port=unused_port()
|
||||
first,log=launch('first',port)
|
||||
wait_log(first,log,f'LISTENING on 127.0.0.1:{port}')
|
||||
assert owners(port)==[first.pid],owners(port)
|
||||
with socket.socket() as competitor:
|
||||
competitor.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
|
||||
try:
|
||||
competitor.bind(('127.0.0.1',port))
|
||||
except OSError as error:
|
||||
assert error.winerror in (10013,10048),error
|
||||
result['reusable_competitor_error']=error.winerror
|
||||
else:
|
||||
raise AssertionError('A competing SO_REUSEADDR bind unexpectedly succeeded')
|
||||
second,log=launch('second',port)
|
||||
wait_log(second,log,f'debug server bind({port}) FAILED')
|
||||
assert owners(port)==[first.pid],owners(port)
|
||||
result['second_runtime_refused']=True
|
||||
stop(second); stop(first)
|
||||
restarted,log=launch('restarted',port)
|
||||
wait_log(restarted,log,f'LISTENING on 127.0.0.1:{port}')
|
||||
assert owners(port)==[restarted.pid],owners(port)
|
||||
result['restart_after_owned_shutdown']=True
|
||||
finally:
|
||||
for child in children: stop(child)
|
||||
for stream in streams: stream.close()
|
||||
result['owned_processes_stopped']=all(c.poll() is not None for c in children)
|
||||
(a.output/'result.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
print(json.dumps(result))
|
||||
@@ -92,7 +92,15 @@ void psx_idle_note_check(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; }
|
||||
int psx_netplay_active(void) { return 0; }
|
||||
int psx_selfcheck_enabled(void) { return 0; }
|
||||
int psx_mod_function_entry_active(void) { return 0; }
|
||||
void savestate_poll(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; }
|
||||
int g_psx_savestate_pending;
|
||||
unsigned g_test_savestate_polls;
|
||||
uint32_t g_test_savestate_pc;
|
||||
void savestate_poll(struct CPUState *c, uint32_t pc) {
|
||||
(void)c;
|
||||
++g_test_savestate_polls;
|
||||
g_test_savestate_pc = pc;
|
||||
g_psx_savestate_pending = 0;
|
||||
}
|
||||
void psx_netplay_poll_snap(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; }
|
||||
void psx_selfcheck_poll(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; }
|
||||
void psx_rewind_poll(struct CPUState *c, uint32_t pc) { (void)c; (void)pc; }
|
||||
|
||||
@@ -61,6 +61,7 @@ extern "C" void gte_test_execute_reference(CPUState *cpu, uint32_t cmd);
|
||||
|
||||
/* gte.cpp runtime dependencies that are irrelevant to register-transfer tests. */
|
||||
extern "C" int gpu_ws_present_native_43(void) { return 0; }
|
||||
extern "C" void gpu_ws_note_gte_sxy(int32_t, int32_t) {}
|
||||
static int g_test_precise_nclip_enabled;
|
||||
static int g_test_render_projective;
|
||||
extern "C" int gpu_render_projective_enabled(void) { return g_test_render_projective; }
|
||||
|
||||
@@ -61,6 +61,9 @@ extern "C" void gpu_get_display_info(GpuDisplayInfo* out) { *out = GpuDisplayInf
|
||||
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
|
||||
extern "C" int fntrace_is_game_started(void) { return 1; }
|
||||
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_tag_hud_prim_projected(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_gte_sxy_tracking(int) {}
|
||||
extern "C" int gpu_ws_gte_sxy_recent(int32_t, int32_t) { return 0; }
|
||||
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
|
||||
extern "C" int gpu_ws_configured_x_reveal(void) { return 0; }
|
||||
@@ -70,6 +73,21 @@ extern "C" void gpu_ws_tag_screen_mask_quad(uint32_t) {}
|
||||
extern "C" { uint64_t s_frame_count = 0; }
|
||||
static uint64_t cycles;
|
||||
extern "C" uint64_t psx_get_cycle_count(void) { return cycles; }
|
||||
/* Link the real package override/immediate services, as mod_runtime_test does.
|
||||
* Resident-resource tests do not execute guest functions or service devices. */
|
||||
extern "C" uint32_t psx_peek_word_untraced(uint32_t a) { return psx_read_word(a); }
|
||||
extern "C" void psx_dispatch_call(struct CPUState*, uint32_t, uint32_t) {}
|
||||
extern "C" {
|
||||
uint64_t psx_cycle_count = 0;
|
||||
uint64_t psx_next_service_cycle = 0;
|
||||
int psx_in_device_service = 0;
|
||||
int g_event_step_conservative = 0;
|
||||
int g_ls_replay_active = 0;
|
||||
uint32_t g_psx_cyc_batch = 0;
|
||||
uint32_t g_psx_cyc_batch_limit = 0;
|
||||
void psx_devices_service_to_now(void) {}
|
||||
void psx_advance_cycles_slow(uint32_t amount) { psx_cycle_count += amount; }
|
||||
}
|
||||
extern "C" void gpu_ws_tag_radial_screen_mask_quad(uint32_t, float) {}
|
||||
extern "C" void gpu_ws_tag_feedback_rect(uint32_t) {}
|
||||
extern "C" int psx_netplay_active(void) { return 0; }
|
||||
|
||||
@@ -197,6 +197,9 @@ static int test_disc_request(int *ready) { ++disc_request_calls; *ready = 1; ret
|
||||
static void test_disc_request_activate(void) { psx_mod_set_disc_request_provider(test_disc_request); }
|
||||
/* Widescreen tag passthroughs mod_runtime forwards to the GPU; unused here. */
|
||||
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_tag_hud_prim_projected(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_gte_sxy_tracking(int) {}
|
||||
extern "C" int gpu_ws_gte_sxy_recent(int32_t, int32_t) { return 0; }
|
||||
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
|
||||
extern "C" int gpu_ws_configured_x_reveal(void) { return 0; }
|
||||
|
||||
@@ -156,6 +156,9 @@ extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
|
||||
extern "C" int fntrace_is_game_started(void) { return 1; }
|
||||
/* Widescreen tag passthroughs mod_runtime forwards to the GPU; unused here. */
|
||||
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_tag_hud_prim_projected(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_gte_sxy_tracking(int) {}
|
||||
extern "C" int gpu_ws_gte_sxy_recent(int32_t, int32_t) { return 0; }
|
||||
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
|
||||
extern "C" int gpu_ws_configured_x_reveal(void) { return 0; }
|
||||
|
||||
@@ -135,6 +135,9 @@ extern "C" void gpu_get_display_info(GpuDisplayInfo* out) { *out = GpuDisplayInf
|
||||
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
|
||||
extern "C" int fntrace_is_game_started(void) { return 1; }
|
||||
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_tag_hud_prim_projected(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_gte_sxy_tracking(int) {}
|
||||
extern "C" int gpu_ws_gte_sxy_recent(int32_t, int32_t) { return 0; }
|
||||
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
|
||||
extern "C" void psx_projection_reset_session(void) {}
|
||||
|
||||
@@ -510,6 +510,31 @@ int main(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* A quiet native menu may never accumulate 16K block checks before its
|
||||
* attract timer expires. Staged host requests must poll at the next edge,
|
||||
* including the no-IRQ and masked-IRQ fast paths. */
|
||||
{
|
||||
extern int g_psx_savestate_pending;
|
||||
extern unsigned g_test_savestate_polls;
|
||||
extern uint32_t g_test_savestate_pc;
|
||||
static CPUState cpu;
|
||||
for (int masked = 0; masked < 2; ++masked) {
|
||||
Program p = game;
|
||||
reset_machine(&cpu, &p);
|
||||
i_mask = 1;
|
||||
i_stat = masked ? 1 : 0;
|
||||
cpu.cop0[C0_SR] = masked ? 0 : (SR_IEC | SR_IM2);
|
||||
unsigned before = g_test_savestate_polls;
|
||||
g_psx_savestate_pending = 1;
|
||||
psx_check_interrupts_at(&cpu, p.base);
|
||||
if (g_test_savestate_polls != before + 1 ||
|
||||
g_test_savestate_pc != p.base || g_psx_savestate_pending) {
|
||||
fprintf(stderr, "pending snapshot missed %s fast edge\n",
|
||||
masked ? "masked IRQ" : "no IRQ");
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fail) { puts("restore_edge_irq_replay_test: FAIL"); return 1; }
|
||||
puts("restore_edge_irq_replay_test: PASS");
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
|
||||
/* The squash path reveals a fraction of the game's own half-width, so the
|
||||
* per-side cull margin follows the display mode: 320 px keeps the historical
|
||||
* 160*(1/s-1), a 512 px mode (Army Men in-game) hides 256*(1/s-1) px per side.
|
||||
* Ratio-style sites (angles, cones, scaled bounds) read the 320-basis margin and
|
||||
* never change with the mode. */
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "../src/gpu.c"
|
||||
#include "gpu_exec_stubs.inc"
|
||||
|
||||
static void set_mode(int hres, uint32_t x2) {
|
||||
hres1 = (uint32_t)hres; hres2 = 0; video_mode = 0;
|
||||
h_display_x1 = 0x260; h_display_x2 = x2;
|
||||
v_display_y1 = 0x10; v_display_y2 = 0x100;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
ws_mode = 1;
|
||||
ws_gte_game_mode_cfg = 1; /* game frame: squash is active */
|
||||
ws_last_gte_stamp = (uint32_t)s_frame_count;
|
||||
ws_cfg_num = 16; ws_cfg_den = 9;
|
||||
ws_xnum = 3; ws_xden = 4; /* 16:9 squash */
|
||||
|
||||
set_mode(1, 0xC60); /* 320 wide */
|
||||
assert(gpu_h_display_width() == 320);
|
||||
assert(psx_ws_x_margin() == 53);
|
||||
assert(psx_ws_x_margin_320() == 53);
|
||||
|
||||
set_mode(2, 0xC60); /* 512 wide */
|
||||
assert(gpu_h_display_width() == 512);
|
||||
assert(psx_ws_x_margin() == 85); /* 256 * (4/3 - 1) */
|
||||
assert(psx_ws_x_margin_320() == 53); /* ratio sites unchanged */
|
||||
assert(psx_ws_player_x_bound(160) == 160 * (320 + 2 * 53) / 320);
|
||||
|
||||
ws_xnum = 8; ws_xden = 15; /* 2.5:1 */
|
||||
assert(psx_ws_x_margin() == 224); /* 256 * (15/8 - 1) */
|
||||
assert(psx_ws_x_margin_320() == 140);
|
||||
|
||||
gpu_ws_set_cull_guard_pixels(32);
|
||||
assert(psx_ws_x_margin() == 256);
|
||||
gpu_ws_set_cull_guard_pixels(0);
|
||||
|
||||
ws_xnum = ws_xden = 1; /* true 4:3 stays identity */
|
||||
assert(psx_ws_x_margin() == 0);
|
||||
assert(psx_ws_x_margin_320() == 0);
|
||||
puts("ws_cull_margin_width_test: ok");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "ws_gte_sxy.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(void) {
|
||||
WsGteSxy *s = (WsGteSxy *)calloc(1, sizeof *s);
|
||||
assert(s);
|
||||
|
||||
/* Nothing recorded yet, including at the zero cell. */
|
||||
assert(!ws_gte_sxy_recent(s, 10u, 100, 50));
|
||||
assert(!ws_gte_sxy_recent(s, 0u, -512, -256));
|
||||
|
||||
ws_gte_sxy_note(s, 10u, 397, 58);
|
||||
ws_gte_sxy_note(s, 10u, -20, -8);
|
||||
assert(ws_gte_sxy_recent(s, 10u, 397, 58));
|
||||
assert(ws_gte_sxy_recent(s, 10u, -20, -8));
|
||||
assert(!ws_gte_sxy_recent(s, 10u, 398, 58));
|
||||
assert(!ws_gte_sxy_recent(s, 10u, 397, 59));
|
||||
|
||||
/* A packet built in one vblank and resubmitted for the next several
|
||||
* (the ordering table is rebuilt every fourth vblank) still matches. */
|
||||
for (uint32_t f = 10u; f <= 10u + WS_GTE_SXY_WINDOW; ++f)
|
||||
assert(ws_gte_sxy_recent(s, f, 397, 58));
|
||||
assert(!ws_gte_sxy_recent(s, 10u + WS_GTE_SXY_WINDOW + 1u, 397, 58));
|
||||
|
||||
/* A fresh projection of the same position refreshes it. */
|
||||
ws_gte_sxy_note(s, 16u, 397, 58);
|
||||
assert(ws_gte_sxy_recent(s, 20u, 397, 58));
|
||||
|
||||
/* A reader behind the stamp (render thread lag) does not match. */
|
||||
assert(!ws_gte_sxy_recent(s, 3u, 397, 58));
|
||||
|
||||
/* The 15-bit stamp wraps without turning ancient entries into hits. */
|
||||
ws_gte_sxy_note(s, 32767u, 1, 1);
|
||||
assert(ws_gte_sxy_recent(s, 32767u, 1, 1));
|
||||
assert(ws_gte_sxy_recent(s, 32768u + 2u, 1, 1));
|
||||
assert(!ws_gte_sxy_recent(s, 32768u + 100u, 1, 1));
|
||||
|
||||
free(s);
|
||||
puts("ws_gte_sxy_test: PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -65,6 +65,40 @@ int main(void) {
|
||||
assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
0x80010008u, packet, 4u, 20u, &anchor));
|
||||
|
||||
/* GTE-projected tags keep their edge (native-wide still translates by it)
|
||||
* and carry the projected flag; plain tags do not. */
|
||||
{
|
||||
int projected = 7;
|
||||
ws_hud_anchor_insert_projected(tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
0x80010004u, 5, &guard, 20u);
|
||||
anchor = 0;
|
||||
assert(ws_hud_anchor_lookup_result_ex(
|
||||
tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u,
|
||||
20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT);
|
||||
assert(anchor == 1 && projected == 1);
|
||||
ws_hud_anchor_insert_projected(tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
0x80010004u, -3, &guard, 20u);
|
||||
assert(ws_hud_anchor_lookup_result_ex(
|
||||
tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u,
|
||||
20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT);
|
||||
assert(anchor == -1 && projected == 1);
|
||||
assert(ws_hud_anchor_native_delta(1, 53, anchor) == -53);
|
||||
/* A projected centre tag is not the same as no tag: it must stop
|
||||
* automatic layout from squashing the piece a second time. */
|
||||
ws_hud_anchor_insert_projected(tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
0x80010004u, 0, &guard, 20u);
|
||||
assert(ws_hud_anchor_lookup_result_ex(
|
||||
tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u,
|
||||
20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT);
|
||||
assert(anchor == 0 && projected == 1);
|
||||
ws_hud_anchor_insert(tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
0x80010004u, -7, &guard, 20u);
|
||||
assert(ws_hud_anchor_lookup_result_ex(
|
||||
tags, WS_HUD_ANCHOR_TABLE_SIZE, 0x80010004u, packet, 4u,
|
||||
20u, &anchor, &projected) == WS_HUD_ANCHOR_HIT);
|
||||
assert(anchor == -1 && projected == 0);
|
||||
}
|
||||
|
||||
ws_hud_anchor_clear(tags, WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
assert(!ws_hud_anchor_lookup(tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
0x80010004u, packet, 4u, 20u, &anchor));
|
||||
|
||||
@@ -3533,10 +3533,20 @@ def patch_generated_c(src: str, load_addr: int, size: int) -> str:
|
||||
else:
|
||||
src = src[:last_inc] + '\n' + DISPATCH_PREAMBLE + src[last_inc:]
|
||||
|
||||
# 2. Remove out-of-range forward declarations
|
||||
# 2. Remove out-of-range forward declarations. In-range ones carry the
|
||||
# export attribute too: clang rejects a definition that adds dllexport
|
||||
# to a function already declared and called without it ("redeclaration
|
||||
# of 'func_X' cannot add 'dllexport' attribute"), which is what any
|
||||
# fall-through or intra-shard call ahead of the definition produces.
|
||||
def drop_extern(m):
|
||||
addr = int(m.group(1), 16)
|
||||
return '' if not in_overlay(addr) else m.group(0)
|
||||
if not in_overlay(addr):
|
||||
return ''
|
||||
return (
|
||||
'#ifdef _WIN32\n__declspec(dllexport)\n#else\n'
|
||||
'__attribute__((visibility("default")))\n#endif\n'
|
||||
+ m.group(0)
|
||||
)
|
||||
src = re.sub(r'^void func_([0-9A-Fa-f]{8})\(CPUState\* cpu\);\n',
|
||||
drop_extern, src, flags=re.MULTILINE)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user