Integrate validated SH1 widescreen and save-request fixes

This commit is contained in:
Matthew Stanley
2026-10-25 12:50:36 -07:00
25 changed files with 649 additions and 56 deletions
+14 -1
View File
@@ -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`.
+21
View File
@@ -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
+16
View File
@@ -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
+15
View File
@@ -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.
+3
View File
@@ -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
}
+59
View File
@@ -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
+51 -11
View File
@@ -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,
+16
View File
@@ -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
View File
@@ -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)
+3
View File
@@ -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. */
+4 -3
View File
@@ -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);
+9
View File
@@ -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);
}
+7 -7
View File
@@ -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))
+9 -1
View File
@@ -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; }
+18
View File
@@ -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; }
+3
View File
@@ -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; }
+3
View File
@@ -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; }
+3
View File
@@ -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;
+54
View File
@@ -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;
}
+44
View File
@@ -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;
}
+34
View File
@@ -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));
+12 -2
View File
@@ -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)