diff --git a/docs/WIDESCREEN.md b/docs/WIDESCREEN.md index 710624cb..8106bf5c 100644 --- a/docs/WIDESCREEN.md +++ b/docs/WIDESCREEN.md @@ -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| ` (always-on per-prim draw ring → CSV), `ws_margin ` (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`. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 0eae55c4..68b1877c 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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 + $,-Wl$-dead_strip,-Wl$--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 diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 8a70965c..12e9533d 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -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 diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 1acf1716..19a27ada 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -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. diff --git a/runtime/include/savestate.h b/runtime/include/savestate.h index 665d9ab5..58eb5ac3 100644 --- a/runtime/include/savestate.h +++ b/runtime/include/savestate.h @@ -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 } diff --git a/runtime/include/ws_gte_sxy.h b/runtime/include/ws_gte_sxy.h new file mode 100644 index 00000000..a37f1a33 --- /dev/null +++ b/runtime/include/ws_gte_sxy.h @@ -0,0 +1,59 @@ +#ifndef PSXRECOMP_WS_GTE_SXY_H +#define PSXRECOMP_WS_GTE_SXY_H + +#include +#include + +#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 diff --git a/runtime/include/ws_hud_anchor.h b/runtime/include/ws_hud_anchor.h index 40480f23..ce5d2773 100644 --- a/runtime/include/ws_hud_anchor.h +++ b/runtime/include/ws_hud_anchor.h @@ -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, diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 31391384..1b448d9e 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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)); diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 66fc8b68..40641694 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -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) diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index e1edccfb..06a0b31c 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -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. */ diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 0ea08733..3b912b7f 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -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); diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 3ccb88fd..29ef2bdd 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -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); } diff --git a/runtime/src/savestate.c b/runtime/src/savestate.c index a939b4e6..f0548323 100644 --- a/runtime/src/savestate.c +++ b/runtime/src/savestate.c @@ -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 diff --git a/runtime/tests/check_debug_port_exclusivity.py b/runtime/tests/check_debug_port_exclusivity.py new file mode 100644 index 00000000..2632190f --- /dev/null +++ b/runtime/tests/check_debug_port_exclusivity.py @@ -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() +#include +#include +#include +#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; +} diff --git a/runtime/tests/test_ws_gte_sxy.c b/runtime/tests/test_ws_gte_sxy.c new file mode 100644 index 00000000..c2154b1b --- /dev/null +++ b/runtime/tests/test_ws_gte_sxy.c @@ -0,0 +1,44 @@ +#include "ws_gte_sxy.h" + +#include +#include +#include + +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; +} diff --git a/runtime/tests/test_ws_hud_anchor.c b/runtime/tests/test_ws_hud_anchor.c index 5f3a7089..630d7b3e 100644 --- a/runtime/tests/test_ws_hud_anchor.c +++ b/runtime/tests/test_ws_hud_anchor.c @@ -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)); diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index d2f24c12..82b2ed99 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -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)