mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge branch 'fix/spu-shadow-cd-activity-gate-20261024' into feat/launcher-quick-controls-artwork-20261024
This commit is contained in:
@@ -170,6 +170,12 @@ int duck_texture_pack_match_parts(DuckTexturePack* pack,
|
||||
* Release the lease even after pack destruction. */
|
||||
void duck_texture_pack_set_decode_budget(DuckTexturePack* pack, size_t bytes);
|
||||
int duck_texture_pack_request_decode(DuckTexturePack* pack, uint64_t entry_id);
|
||||
/* Elective full preload with the hd_texture_pack_preload contract: bounded
|
||||
* aggregate decoded bytes and concurrency, pinned images outside the decode
|
||||
* budget, everything not preloaded left to the asynchronous decode path.
|
||||
* Returns 1 complete, 0 partial (see out), -1 on error. */
|
||||
int duck_texture_pack_preload(DuckTexturePack* pack, size_t max_bytes,
|
||||
unsigned max_threads, HdTexturePreloadResult* out);
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* pack, uint64_t entry_id,
|
||||
DuckTexturePixels* out_pixels);
|
||||
void duck_texture_pixels_release(DuckTexturePixels* pixels);
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/* func_override.h — replace a guest function with hand-written C.
|
||||
*
|
||||
* WHY THIS EXISTS
|
||||
*
|
||||
* A recomp turns the game into C, but that C is a build artifact: regenerated
|
||||
* from the player's own disc, never hand-edited (CLAUDE.md rule 4). So there
|
||||
* is no supported way to say "this function should do something else" — which
|
||||
* is the single most important thing a port needs to grow beyond a faithful
|
||||
* replay of the original.
|
||||
*
|
||||
* This is that mechanism. Register hand-written C against a guest address and
|
||||
* the dispatcher calls yours instead of the recompiled original.
|
||||
*
|
||||
* NO SIZE LIMIT. This is not a ROM patch; there is no slot to fit inside, no
|
||||
* code cave, nothing to shift. Your replacement is native code in a native
|
||||
* binary — five instructions or five thousand lines. That freedom is the
|
||||
* entire point of recompiling rather than hacking bytes.
|
||||
*
|
||||
* WHERE IT HOOKS, AND WHY IT MATTERS
|
||||
*
|
||||
* The hook is consulted in psx_dispatch_impl after the BIOS tiers (so an
|
||||
* override can never shadow a kernel service vector) and BEFORE every game
|
||||
* code backend. That placement is deliberate and load-bearing: one
|
||||
* address-keyed hook covers the STATIC EXE, runtime-loaded OVERLAYS and
|
||||
* DIRTY RAM alike. The interpreter's JAL/JALR call-resolution tiers consult
|
||||
* it on the SAME terms — before enter-compiled, before overlay-native and
|
||||
* before the local pc-chain (the reserved CRES_OVERRIDE slot) — so no call
|
||||
* the interpreter resolves itself can bypass an override. Ordering there is
|
||||
* not cosmetic: interp_enter_compiled calls psx_dispatch_game_compiled ->
|
||||
* entry->fn(cpu) directly and never re-enters psx_dispatch_impl, so a hook
|
||||
* placed after it is unreachable for any override on a statically-compiled
|
||||
* function reached from interpreted code, while registration and the
|
||||
* func_override inventory both still look healthy.
|
||||
*
|
||||
* SCOPE: overrides are CALL-site keyed. A guest tail transfer (j / jr) into
|
||||
* an overridden address from the dirty-RAM block loop is a TRANSFER, not a
|
||||
* call, and does not consult the hook — see dirty_ram_interp.c's compiled
|
||||
* handoff sites. Registering on a function only ever entered by tail
|
||||
* transfer will show calls == 0.
|
||||
*
|
||||
* THE CONTRACT
|
||||
*
|
||||
* Your function receives the CPUState and must obey the guest ABI:
|
||||
*
|
||||
* int my_impl(CPUState *cpu) {
|
||||
* uint32_t a0 = cpu->gpr[4], a1 = cpu->gpr[5]; // arguments
|
||||
* ...
|
||||
* cpu->gpr[2] = result; // $v0 = return value
|
||||
* return 1;
|
||||
* }
|
||||
*
|
||||
* Return 1 and the guest resumes at $ra exactly as if the original had run
|
||||
* `jr $ra`. Return 0 and the original recompiled or interpreted code runs
|
||||
* untouched, so an override can decline case by case (a conditional
|
||||
* pre-hook: mutate state, return 0, original runs).
|
||||
*
|
||||
* POINTERS MUST BE GUEST ADDRESSES. The caller is recompiled too: when it
|
||||
* dereferences your return value it performs a guest load. A host pointer
|
||||
* will read garbage. If you need to hand back a buffer, write it into guest
|
||||
* RAM (psx_write_byte and friends, or psx_mod_alloc_guest_memory) and return
|
||||
* that address.
|
||||
*
|
||||
* DETERMINISM. Keep ALL mutable state in guest RAM. Rollback/rewind and
|
||||
* netplay snapshot guest state only; host-side statics in an override
|
||||
* desync replays. Reading host config that never changes mid-session is
|
||||
* fine.
|
||||
*
|
||||
* CYCLE ACCOUNTING — REQUIRED AT REGISTRATION.
|
||||
* On hardware the replaced function took guest time, and every device in
|
||||
* the machine schedules against that clock; a replacement that takes zero
|
||||
* cycles shifts IRQ phase for the rest of the session. So every
|
||||
* registration MUST state its timing intent via the `credit` argument —
|
||||
* there is no default to inherit. Two forms:
|
||||
*
|
||||
* credit >= 0 The tier charges exactly `credit` guest cycles
|
||||
* (psx_advance_cycles) on every HANDLED call, after
|
||||
* the body runs. Declines charge nothing — the
|
||||
* original runs and accrues its own cycles. 0 is
|
||||
* legal and means "deliberately free": right for a
|
||||
* player-toggled mod whose behavior has no hardware
|
||||
* analog, indefensible for an always-on
|
||||
* reimplementation claiming to be indistinguishable.
|
||||
*
|
||||
* FO_CREDIT_SELF The tier charges nothing; the BODY owns its timing
|
||||
* and calls psx_advance_cycles() itself. Use this
|
||||
* when the original's cost is data-dependent
|
||||
* (charge base + per_iteration * n, the bios_hle.c
|
||||
* TIMING NOTE pattern), and ALWAYS for wraps: the
|
||||
* original re-dispatched by func_override_call_original
|
||||
* accrues its exact cycles by executing, so a fixed
|
||||
* nonzero credit on a wrap double-counts. Guest code
|
||||
* invoked via func_override_guest_call likewise
|
||||
* self-charges — credit only the code you REPLACED.
|
||||
*
|
||||
* Credits are approximations of the original's dynamic instruction count
|
||||
* unless measured; LLE remains the timing oracle (run with the override
|
||||
* declined and sample the cycle delta across the call boundary — the
|
||||
* callret ring — to measure a real distribution). See FAITHFUL_TIMING_PLAN.
|
||||
*
|
||||
* WHAT IT DOES NOT DO
|
||||
*
|
||||
* It does not lift the game's own assumptions. If the caller copies your
|
||||
* result into a fixed-size field, that field is still fixed. The override
|
||||
* removes the code limit, not every limit.
|
||||
*/
|
||||
|
||||
#ifndef PSXRECOMP_FUNC_OVERRIDE_H
|
||||
#define PSXRECOMP_FUNC_OVERRIDE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct CPUState;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define FO_MAX_OVERRIDES 128
|
||||
#define FO_MAX_ID 64
|
||||
#define FO_MAX_GUARD_WORDS 4
|
||||
|
||||
/* `credit` value: the override body states its own timing by calling
|
||||
* psx_advance_cycles() itself (mandatory for wraps — see CYCLE ACCOUNTING
|
||||
* above). Any other negative value is FO_ERR_ARGS. */
|
||||
#define FO_CREDIT_SELF (-1)
|
||||
|
||||
/* func_override_add return codes. */
|
||||
#define FO_OK 0
|
||||
#define FO_ERR_FULL 1
|
||||
#define FO_ERR_ARGS 2
|
||||
#define FO_ERR_DUPLICATE 3
|
||||
|
||||
/* An override implementation. Reads arguments from cpu->gpr[4..7], writes
|
||||
* the return value to cpu->gpr[2]. Returning nonzero means "handled";
|
||||
* returning 0 declines this call and the original code runs instead. */
|
||||
typedef int (*FuncOverrideFn)(struct CPUState *cpu);
|
||||
|
||||
/* Register `fn` for guest address `addr` (KSEG or physical — normalised).
|
||||
* `id` is copied, used for diagnostics, and may be NULL. Registering the
|
||||
* same address twice is an error rather than a silent last-wins, because a
|
||||
* silent shadow is impossible to debug. `credit` is the guest-cycle policy
|
||||
* (CYCLE ACCOUNTING above): >= 0 charged per handled call, or
|
||||
* FO_CREDIT_SELF when the body charges its own. */
|
||||
int func_override_add(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
int32_t credit);
|
||||
|
||||
/* Like func_override_add, plus a residency guard: before each consult the
|
||||
* first `n_words` guest words at `addr` are compared against
|
||||
* `expected_words`; on mismatch the override silently declines (the guard
|
||||
* miss is counted, see func_override_get_ex). Use this when the address
|
||||
* belongs to overlay/dirty RAM that other code may occupy at other times —
|
||||
* the guard makes "wrong code resident" a decline instead of a corruption.
|
||||
* n_words must be 1..FO_MAX_GUARD_WORDS. */
|
||||
int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words,
|
||||
int32_t credit);
|
||||
|
||||
/* Call a guest function from inside an override (or any native code on the
|
||||
* dispatch thread): args already placed in cpu->gpr[4..7], returns after the
|
||||
* callee completes; result in cpu->gpr[2]. `site_ra` is the return address
|
||||
* the callee should observe in $ra — pass the original call site's return
|
||||
* address when re-creating a call the original code would have made, so
|
||||
* callees, fntrace and crash forensics see authentic values. $ra is
|
||||
* restored around the call. */
|
||||
void func_override_guest_call(struct CPUState *cpu, uint32_t target,
|
||||
uint32_t site_ra);
|
||||
|
||||
/* Run the ORIGINAL code of the function currently being overridden — the
|
||||
* wrap primitive. Callable only from inside an executing override; arms a
|
||||
* one-shot bypass for this address, re-dispatches it, and returns after the
|
||||
* original completes (result in cpu->gpr[2]). Typical post-hook:
|
||||
*
|
||||
* int wrap(CPUState *cpu) {
|
||||
* func_override_call_original(cpu); // original runs fully
|
||||
* ...inspect / adjust results...
|
||||
* return 1; // we handled the call
|
||||
* }
|
||||
*
|
||||
* The bypass is one-shot per invocation: if the original recursively calls
|
||||
* itself, the recursive calls consult the override again (matching what a
|
||||
* guest-level wrap would observe). */
|
||||
void func_override_call_original(struct CPUState *cpu);
|
||||
|
||||
/* Register on behalf of a mod package plan. Identical to the two calls above
|
||||
* (pass n_words 0 for unguarded) except the entry is tagged package-armed, so
|
||||
* func_override_reset_package_armed can drop it when the plan is cleared.
|
||||
* The mod runtime uses this; game code registering its own always-on
|
||||
* reimplementations should use func_override_add/_add_guarded. */
|
||||
int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words,
|
||||
int32_t credit);
|
||||
/* Scoped package variant. The owner supplies a stable plan context and
|
||||
* invokes fn inside that plugin's resource/completion context. The plan must
|
||||
* disarm package entries before replacing that context. */
|
||||
typedef int (*FuncOverrideInvoker)(struct CPUState *cpu, FuncOverrideFn fn,
|
||||
const void *context);
|
||||
int func_override_add_package_scoped(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words, int32_t credit,
|
||||
FuncOverrideInvoker invoke, const void *context);
|
||||
/* Nonzero while a native override stack is live, including nested guest
|
||||
* calls. Save/load and rewind must defer until this returns to zero. */
|
||||
int func_override_active(void);
|
||||
|
||||
/* Install the dispatcher hook. Call once at startup AFTER registering (the
|
||||
* mod runtime calls it again after arming package-gated overrides — safe).
|
||||
* Cheap with nothing registered — it leaves the hook NULL, so dispatch is
|
||||
* byte-identical to a build without the tier. */
|
||||
void func_override_install(void);
|
||||
|
||||
/* Drop every package-armed override, keep direct registrations, re-install
|
||||
* (so the hook goes back to NULL if the table empties). Returns how many were
|
||||
* dropped. THIS IS REQUIRED FOR CORRECTNESS, not a convenience: a cleared mod
|
||||
* plan stops activation/vblank callbacks simply by no longer being iterated,
|
||||
* but an armed override lives in this module's own table, so without an
|
||||
* explicit reset it keeps firing after the plan is gone — including after
|
||||
* netplay's clear-mods announces a vanilla session, which is a peer
|
||||
* divergence. Direct registrations are deliberately kept: they are the game's
|
||||
* own always-on faithful reimplementations, present identically in both
|
||||
* peers' builds and not player-selectable. */
|
||||
int func_override_reset_package_armed(void);
|
||||
|
||||
/* Introspection, surfaced by the `func_override` TCP command. An override
|
||||
* whose `calls` stays 0 was never reached: wrong address, or that code path
|
||||
* never ran. `guard_misses` counts consults declined by the residency
|
||||
* guard. `credit_out` reports the declared cycle policy (FO_CREDIT_SELF or
|
||||
* the fixed per-call charge), so every armed override's timing intent is
|
||||
* inspectable, not just documented. */
|
||||
int func_override_count(void);
|
||||
int func_override_get(int index, char *id_out, size_t id_cap,
|
||||
uint32_t *addr_out, uint64_t *calls_out);
|
||||
int func_override_get_ex(int index, char *id_out, size_t id_cap,
|
||||
uint32_t *addr_out, uint64_t *calls_out,
|
||||
uint64_t *guard_misses_out, int *guarded_out,
|
||||
int32_t *credit_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSXRECOMP_FUNC_OVERRIDE_H */
|
||||
@@ -348,6 +348,17 @@ void gpu_ws_set_clip_edge_x_load_sites(const uint32_t *sites, int nsites,
|
||||
void gpu_ws_set_cull_keep_sites(const uint32_t *addresses,
|
||||
const uint32_t *expected,
|
||||
const uint32_t *results, int nsites);
|
||||
/* [[widescreen.cull.sxy]]: packed-SXY all-vertices reject folds. Per site i,
|
||||
* vertices[i] (3 or 4) ANDs at fold_addresses[i*4 + k] with words
|
||||
* fold_expected[i*4 + k] and SXY operand vertex_regs[i*4 + k], then the
|
||||
* beq/bne consumer. The recompiler config loader has validated the shape. */
|
||||
#define PSX_WS_SXY_CULL_SITES_MAX 64
|
||||
void gpu_ws_set_sxy_cull_sites(const uint32_t *vertices,
|
||||
const uint32_t *fold_addresses,
|
||||
const uint32_t *fold_expected,
|
||||
const uint32_t *vertex_regs,
|
||||
const uint32_t *consumer_addresses,
|
||||
const uint32_t *consumer_expected, int nsites);
|
||||
void gpu_ws_set_angle_sites(const uint32_t *addresses,
|
||||
const uint32_t *expected, int nsites);
|
||||
/* [[widescreen.cull.scale]]: full-word-guarded SLT/SLTU sites whose rs (0) or
|
||||
@@ -401,6 +412,14 @@ uint32_t psx_ws_clip_edge_width(void);
|
||||
uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced);
|
||||
int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla,
|
||||
uint32_t *out);
|
||||
/* One AND of a [[widescreen.cull.sxy]] fold (ws_cull_edge.h); identity at 4:3. */
|
||||
uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy);
|
||||
/* Interpreter lookup: the SXY operand register (1..31) when `instr` at `pc`
|
||||
* is an AND of a configured fold and every fold and consumer word, read
|
||||
* through `read_word` (physical address), matches; else 0. */
|
||||
extern int g_psx_ws_sxy_cull_n;
|
||||
uint32_t psx_ws_sxy_cull_vertex_reg(uint32_t pc, uint32_t instr,
|
||||
uint32_t (*read_word)(uint32_t phys));
|
||||
uint32_t psx_ws_angle_widen(uint32_t vanilla);
|
||||
int psx_ws_angle_site(uint32_t pc, uint32_t instr, uint32_t *out);
|
||||
/* bound * (half_extent + margin) / half_extent; identity at 4:3. */
|
||||
|
||||
@@ -200,6 +200,8 @@ float gl_renderer_get_bloom(void);
|
||||
* compile is logged and skipped. */
|
||||
void gl_renderer_set_post_shader(const char *path);
|
||||
int gl_renderer_post_shader_loaded(void);
|
||||
/* Debug: path, last load outcome and live uniforms of the user chain (JSON members). */
|
||||
int gl_renderer_post_shader_debug(char *out, int cap);
|
||||
typedef struct {
|
||||
float strength, threshold;
|
||||
uint64_t passes, us_n;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef struct GtePostfxProjection { int32_t h, ofx, ofy; } GtePostfxProjection;
|
||||
/* Coherent latest CPU projection, not a frame/view identity. One CPU producer. */
|
||||
void gte_postfx_projection_enable(int enabled);
|
||||
int gte_postfx_projection_read(GtePostfxProjection *out);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "gte_postfx_projection.h"
|
||||
#include <atomic>
|
||||
/* CPU owns cached fields; renderer owns enable(). Atomic payloads plus an SC
|
||||
* version guard make concurrent reads defined and reject mixed tuples. */
|
||||
class GteProjectionPublication {
|
||||
std::atomic<uint32_t> control{0}, version{0}, published{0};
|
||||
std::atomic<int32_t> h{0}, x{0}, y{0};
|
||||
uint32_t seen=0; GtePostfxProjection cached{};
|
||||
public:
|
||||
void enable(bool on) {
|
||||
const auto c=control.load();
|
||||
if(bool(c&1)!=on) control.store(((c+2)&~1u)|uint32_t(on));
|
||||
}
|
||||
void publish(int32_t ph,int32_t px,int32_t py) {
|
||||
const auto c=control.load(std::memory_order_relaxed);
|
||||
if(!(c&1))return;
|
||||
if(c==seen && ph==cached.h && px==cached.ofx && py==cached.ofy)return;
|
||||
version.fetch_add(1); h.store(ph); x.store(px); y.store(py); published.store(c);
|
||||
version.fetch_add(1); seen=c; cached={ph,px,py};
|
||||
}
|
||||
bool read(GtePostfxProjection& out) const {
|
||||
const auto c=control.load(); if(!(c&1))return false;
|
||||
for(unsigned i=0;i<3;++i) {
|
||||
const auto v=version.load(); if(v&1)continue;
|
||||
const auto p=published.load(); GtePostfxProjection value{h.load(),x.load(),y.load()};
|
||||
if(v==version.load() && p==c && c==control.load()) {out=value;return true;}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,13 @@ enum HdTextureLookupStatus {
|
||||
HD_TEXTURE_LOOKUP_AMBIGUOUS = 2
|
||||
};
|
||||
|
||||
/* Palette identity of an indexed replacement, named TTTTTTTT-index.png: its
|
||||
* red channel holds CLUT indices (alpha 0 = transparent) and the renderer
|
||||
* resolves them through the live CLUT of each draw, so one image serves every
|
||||
* palette the game draws the texture with (highlights, fades). An exact
|
||||
* TTTTTTTT-PPPPPPPP image still wins for its own palette. */
|
||||
#define HD_TEXTURE_PALETTE_INDEXED 0xFFFFFFFFu
|
||||
|
||||
typedef struct HdTexturePackInfo {
|
||||
const char* asset_root; /* valid until hd_texture_pack_destroy */
|
||||
const char* replacement_root; /* valid until hd_texture_pack_destroy */
|
||||
@@ -109,6 +116,8 @@ uint32_t hd_texture_hash_clut(const uint16_t* vram,
|
||||
/* Upload bytes are row-major logical upload words, independent of destination
|
||||
* wrapping. Tracking invalidates/splits every intersected older residency.
|
||||
* Width <= 1024 and height <= 512; X/Y may wrap. */
|
||||
int hd_texture_pack_debug_uploads_at(const HdTexturePack* pack, unsigned x, unsigned y,
|
||||
char* out, size_t cap);
|
||||
int hd_texture_pack_track_upload(HdTexturePack* pack,
|
||||
uint16_t x,
|
||||
uint16_t y,
|
||||
@@ -180,6 +189,27 @@ typedef struct HdTexturePixels {
|
||||
|
||||
void hd_texture_pack_set_decode_budget(HdTexturePack* pack,
|
||||
size_t budget_bytes);
|
||||
/* Elective full preload: decode unambiguous replacements up front and pin
|
||||
* them, bounded by max_bytes of decoded RGBA in aggregate and by max_threads
|
||||
* concurrent decodes (the caller's thread included). Pinned images are kept
|
||||
* outside the on-demand decode budget, which stays as configured. Images not
|
||||
* preloaded (over the cap, unreadable, malformed, a worker failure) are left
|
||||
* to the normal asynchronous, budgeted decode path; nothing is marked failed
|
||||
* by the preload. Returns 1 when every image was preloaded, 0 for a partial
|
||||
* preload (see out), -1 on error. Repeated calls count already pinned images
|
||||
* without reading/decoding them again; max_bytes caps the pack's total pinned
|
||||
* residency. A cap smaller than existing residency is rejected with -1 and
|
||||
* the existing residency reported; it does not discard live pinned images. */
|
||||
typedef struct HdTexturePreloadResult {
|
||||
int requested; /* unambiguous replacements considered */
|
||||
int loaded; /* resident and pinned, including earlier calls */
|
||||
int over_budget; /* skipped: the aggregate max_bytes was reached */
|
||||
int failed; /* unreadable, malformed, too large, or worker error */
|
||||
int worker_errors; /* of failed: a decode that threw (caught) */
|
||||
size_t bytes; /* total decoded RGBA pinned by this pack */
|
||||
} HdTexturePreloadResult;
|
||||
int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes,
|
||||
unsigned max_threads, HdTexturePreloadResult* out);
|
||||
int hd_texture_pack_request_decode(HdTexturePack* pack,
|
||||
uint32_t texture_hash,
|
||||
uint32_t palette_hash);
|
||||
|
||||
@@ -56,6 +56,13 @@ void psx_host_overlay_draw(int width, int height); /* renderer side */
|
||||
void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx);
|
||||
void psx_host_hud_draw(int width, int height); /* renderer side */
|
||||
|
||||
/* ---- Post-effect live uniforms ------------------------------------------
|
||||
* Sets the value of a `uniform <name> ...` line of the loaded post-effect chain
|
||||
* ([video] post_shader, postfx_chain.h) from game state, e.g. a light source
|
||||
* projected to the screen. Any thread; effective at the next present; the
|
||||
* preset default applies until the first call. Up to 16 names. */
|
||||
void psx_host_set_post_uniform(const char *name, float x, float y, float z, float w);
|
||||
|
||||
/* ---- P3: input sink -----------------------------------------------------
|
||||
* The sink sees every host SDL event first (main thread), in the normal event
|
||||
* drain and in the host pause loop. Returning non-zero consumes the event.
|
||||
|
||||
@@ -637,6 +637,7 @@ bool mod_register_function_filter_plugin(const std::string& id, uint32_t address
|
||||
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
|
||||
PSXModFunctionEntryCallback callback);
|
||||
bool mod_plugin_registered(const std::string& id);
|
||||
bool mod_register_function_override_marker(const std::string& id);
|
||||
void mod_invoke_activation_plugin(const std::string& id);
|
||||
void mod_invoke_vblank_plugin(const std::string& id);
|
||||
struct ModFunctionEntryHook {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "func_override.h" /* FO_CREDIT_SELF for the credit argument below */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -135,6 +137,37 @@ int psx_mod_function_entry_active(void);
|
||||
void psx_mod_set_guest_cycle_scale_gate(int open);
|
||||
/* Live scale: the configured one while every gate is open, else 1. */
|
||||
uint32_t psx_mod_guest_cycle_scale(void);
|
||||
|
||||
/*
|
||||
* Package-gated function override (see runtime/include/func_override.h for
|
||||
* the full contract). Registration queues the override under the plugin id;
|
||||
* it is ARMED into the dispatcher tier only when a resolved package plan
|
||||
* selects that plugin — the same gating as vblank/activation callbacks. The
|
||||
* callback obeys the guest ABI (args cpu->gpr[4..7], result cpu->gpr[2]);
|
||||
* return 1 = handled (guest resumes at $ra), 0 = decline (original runs).
|
||||
* The owning plugin resource context is active throughout the callback and
|
||||
* restored across nested callbacks. Snapshots wait until its native stack
|
||||
* unwinds. Wrap semantics via func_override_call_original(); optional residency
|
||||
* guard via expected_words (NULL/0 = unguarded).
|
||||
*
|
||||
* `id` may carry an optional ":label" suffix ("pkg.feature:aim") that names
|
||||
* the override in the `func_override` TCP inventory; gating and manifest
|
||||
* matching use only the part before the ':', so several overrides can sit
|
||||
* under one [[plugin]] entry and still read apart in diagnostics.
|
||||
*
|
||||
* `credit` is the REQUIRED guest-cycle policy (func_override.h, CYCLE
|
||||
* ACCOUNTING): >= 0 cycles charged per handled call, or FO_CREDIT_SELF when
|
||||
* the body charges its own. For a mod whose behavior has no hardware analog
|
||||
* an explicit 0 is the honest value; a wrap that runs the original via
|
||||
* func_override_call_original must pass FO_CREDIT_SELF (the original
|
||||
* self-charges by executing — a fixed credit would double-count).
|
||||
*/
|
||||
typedef int (*PSXModFunctionOverrideFn)(struct CPUState* cpu);
|
||||
int psx_mod_register_function_override(const char* id, uint32_t address,
|
||||
PSXModFunctionOverrideFn fn,
|
||||
const uint32_t* expected_words,
|
||||
int n_words, int32_t credit);
|
||||
|
||||
/* Narrow guest services available to trusted plugin callbacks. */
|
||||
int psx_mod_game_started(void);
|
||||
/* Read an original mounted-disc file without changing guest CD state/timing.
|
||||
|
||||
@@ -113,6 +113,8 @@ void mod_runtime_set_progress_hook(void (*hook)(void));
|
||||
/* Invokes activation callbacks for the committed plan. Call after the final
|
||||
* launcher commit and before renderer/window initialization. */
|
||||
void mod_runtime_activate_plugins(void);
|
||||
/* NULL on success; a non-NULL arming failure requires refusing this launch. */
|
||||
const char* mod_runtime_activation_error(void);
|
||||
void mod_runtime_on_vblank(void);
|
||||
/* Host VBlank query for the active title's optional disc-request provider. */
|
||||
int mod_runtime_disc_request(int *ready);
|
||||
|
||||
@@ -293,6 +293,10 @@ int32_t psx_ws_plane_nx(int32_t nx) {
|
||||
uint32_t psx_ws_xclip_bound(uint32_t vanilla) {
|
||||
return g_cbs.ws_xclip_bound ? g_cbs.ws_xclip_bound(vanilla) : vanilla;
|
||||
}
|
||||
/* [[widescreen.cull.sxy]] fold AND (ws_cull_edge.h). MUST match gpu.c. */
|
||||
uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy) {
|
||||
return psx_ws_sxy_and_value(acc, sxy, psx_ws_x_margin());
|
||||
}
|
||||
uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced) {
|
||||
return g_cbs.ws_cull_keep_result
|
||||
? g_cbs.ws_cull_keep_result(vanilla, forced)
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
* pfx 1 # format version (required first)
|
||||
* pass <name> # starts a pass; the last pass writes the window
|
||||
* shader <file.glsl|builtin:name> # fragment body (see below)
|
||||
* input <sampler> <source|source_down|mask|previous|<pass name>>
|
||||
* input <sampler> <source|source_down|mask|depth|history|previous|<pass name>>
|
||||
* # depth: PGXP view depth sz (GTE screen z, R32F;
|
||||
* # 0 where nothing 3D drew); history: this pass's
|
||||
* # own target from the last real frame (not last pass)
|
||||
* # source_down: the image scaled to this pass's
|
||||
* # target (one bilinear blit, no full-size copy)
|
||||
* scale <f> # target size, fraction of the game rect (default 1)
|
||||
@@ -27,6 +30,7 @@
|
||||
* uniform vec2 u_rcp; // 1 / this pass's target size in px
|
||||
* uniform vec2 u_size; // this pass's target size in px
|
||||
* uniform ivec2 u_org; // window origin of the game rect (gl_FragCoord - u_org = px in it)
|
||||
* uniform float u_time; // seconds since the chain loaded (real and generated frames)
|
||||
* plus one `uniform sampler2D <sampler>;` per input. v_uv is 0..1 over the
|
||||
* game rect, bottom-up like the window. */
|
||||
#ifndef PSX_POSTFX_CHAIN_H
|
||||
@@ -38,7 +42,8 @@ extern "C" {
|
||||
|
||||
enum { PFX_MAX_PASSES = 16, PFX_MAX_INPUTS = 4, PFX_MAX_UNIFORMS = 8, PFX_NAME = 32 };
|
||||
/* Input sources; a value >= 0 is an earlier pass's index. */
|
||||
enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3 };
|
||||
enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3, PFX_IN_DEPTH = -4,
|
||||
PFX_IN_HISTORY = -5 };
|
||||
enum { PFX_BLEND_REPLACE = 0, PFX_BLEND_SCREEN = 1 };
|
||||
enum { PFX_FMT_RGBA8 = 0, PFX_FMT_RGBA16F = 1 };
|
||||
|
||||
@@ -69,6 +74,7 @@ typedef struct {
|
||||
char name[PFX_NAME];
|
||||
int npasses;
|
||||
int uses_mask; /* some pass reads `mask` */
|
||||
int uses_depth; /* some pass reads `depth` */
|
||||
PfxPass pass[PFX_MAX_PASSES];
|
||||
} PfxChain;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
typedef struct PfxDebugLive { char name[32]; float v[4]; } PfxDebugLive;
|
||||
typedef struct PfxJsonWriter { char *out; size_t cap,used; int ok; } PfxJsonWriter;
|
||||
static inline void pfx_json_raw(PfxJsonWriter *w,const char *s) {
|
||||
const size_t n=strlen(s);
|
||||
if(!w->ok || n>=w->cap-w->used){w->ok=0;return;}
|
||||
memcpy(w->out+w->used,s,n+1);w->used+=n;
|
||||
}
|
||||
static inline void pfx_json_quote(PfxJsonWriter *w,const char *s) {
|
||||
pfx_json_raw(w,"\"");
|
||||
for(const unsigned char *p=(const unsigned char*)s;*p;++p) {
|
||||
char b[7];
|
||||
if(*p=='"'||*p=='\\'){b[0]='\\';b[1]=(char)*p;b[2]=0;}
|
||||
else if(*p<32)snprintf(b,sizeof b,"\\u%04x",(unsigned)*p);
|
||||
else {b[0]=(char)*p;b[1]=0;}
|
||||
pfx_json_raw(w,b);
|
||||
}
|
||||
pfx_json_raw(w,"\"");
|
||||
}
|
||||
/* Returns 0 with an empty buffer on insufficient space, never partial JSON. */
|
||||
static inline int pfx_debug_json(char *out,int cap,const char *path,const char *status,
|
||||
const PfxDebugLive *live,int count) {
|
||||
if(!out||cap<=0)return 0;
|
||||
out[0]=0;PfxJsonWriter w={out,(size_t)cap,0,1};
|
||||
pfx_json_raw(&w,"\"path\":");pfx_json_quote(&w,path);
|
||||
pfx_json_raw(&w,",\"status\":");pfx_json_quote(&w,status);
|
||||
pfx_json_raw(&w,",\"uniforms\":{");
|
||||
for(int i=0;i<count;++i) {
|
||||
if(i)pfx_json_raw(&w,",");
|
||||
pfx_json_quote(&w,live[i].name);pfx_json_raw(&w,":[");
|
||||
for(int j=0;j<4;++j) {
|
||||
if(j)pfx_json_raw(&w,",");
|
||||
if(!isfinite(live[i].v[j])){w.ok=0;break;}
|
||||
char number[48];snprintf(number,sizeof number,"%.9g",(double)live[i].v[j]);pfx_json_raw(&w,number);
|
||||
}
|
||||
pfx_json_raw(&w,"]");
|
||||
}
|
||||
pfx_json_raw(&w,"}");
|
||||
if(!w.ok){out[0]=0;return 0;}return (int)w.used;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifndef PSX_TEXTURE_PACK_PRELOAD_HPP
|
||||
#define PSX_TEXTURE_PACK_PRELOAD_HPP
|
||||
|
||||
/* Shared machinery for the elective full texture-pack preload
|
||||
* (hd_texture_pack_preload, duck_texture_pack_preload): an aggregate
|
||||
* decoded-byte reservation and a bounded worker pool that can neither leak a
|
||||
* joinable std::thread nor let an exception escape a thread entry. */
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace psx_texpreload {
|
||||
|
||||
/* Test-only fault injection. 1: the first item's decode throws
|
||||
* std::bad_alloc. 2: creating the second pool thread throws std::system_error.
|
||||
* 3: publication fails after the first pinned image. */
|
||||
inline std::atomic<int>& fault_mode() {
|
||||
static std::atomic<int> mode{0};
|
||||
return mode;
|
||||
}
|
||||
inline void after_publish(size_t count) {
|
||||
if (count == 1 && fault_mode().load() == 3) throw std::bad_alloc();
|
||||
}
|
||||
|
||||
/* Aggregate decoded bytes, reserved before an image is decoded. */
|
||||
class Budget {
|
||||
public:
|
||||
explicit Budget(size_t cap) : cap_(cap) {}
|
||||
bool reserve(size_t bytes) {
|
||||
size_t cur = used_.load();
|
||||
do {
|
||||
if (bytes > cap_ || cur > cap_ - bytes) return false;
|
||||
} while (!used_.compare_exchange_weak(cur, cur + bytes));
|
||||
return true;
|
||||
}
|
||||
void release(size_t bytes) { used_.fetch_sub(bytes); }
|
||||
private:
|
||||
size_t cap_;
|
||||
std::atomic<size_t> used_{0};
|
||||
};
|
||||
|
||||
/* Per-item outcome, written by exactly one worker. */
|
||||
enum ItemStatus : unsigned char { kPending = 0, kLoaded, kFailed, kOverBudget };
|
||||
|
||||
/* Joins every started thread when the pool goes out of scope, including while
|
||||
* unwinding from a failed std::thread construction. */
|
||||
struct Joiner {
|
||||
std::vector<std::thread>& threads;
|
||||
~Joiner() {
|
||||
for (auto& t : threads)
|
||||
if (t.joinable()) t.join();
|
||||
}
|
||||
};
|
||||
|
||||
/* Runs item(k) for every k < n on at most max_threads threads, the caller
|
||||
* being one of them. An exception from item(k) is caught and counted, never
|
||||
* propagated out of a thread entry. A failure to start a thread only lowers
|
||||
* concurrency. Returns the number of items whose item(k) threw. */
|
||||
template <class F>
|
||||
size_t run(size_t n, unsigned max_threads, F&& item) {
|
||||
std::atomic<size_t> next{0}, errors{0};
|
||||
auto work = [&]() noexcept {
|
||||
for (size_t k; (k = next.fetch_add(1)) < n;) {
|
||||
try {
|
||||
if (k == 0 && fault_mode().load() == 1) throw std::bad_alloc();
|
||||
item(k);
|
||||
} catch (...) {
|
||||
errors.fetch_add(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
unsigned hw = std::thread::hardware_concurrency();
|
||||
if (hw == 0) hw = 1;
|
||||
const unsigned threads = std::max(1u, std::min(max_threads ? max_threads : 1u, hw));
|
||||
std::vector<std::thread> pool;
|
||||
{
|
||||
Joiner joiner{pool};
|
||||
for (unsigned t = 1; t < threads && t < n; ++t) {
|
||||
try {
|
||||
if (t == 1 && fault_mode().load() == 2)
|
||||
throw std::system_error(
|
||||
std::make_error_code(std::errc::resource_unavailable_try_again));
|
||||
pool.emplace_back(work);
|
||||
} catch (...) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
work();
|
||||
}
|
||||
return errors.load();
|
||||
}
|
||||
|
||||
} // namespace psx_texpreload
|
||||
|
||||
#endif
|
||||
@@ -30,6 +30,18 @@ void psx_video_telemetry_present(int result);
|
||||
* Completion/error counters remain available in that case. */
|
||||
void psx_video_telemetry_snapshot(PsxVideoTelemetry *out);
|
||||
|
||||
typedef struct PsxPresentCadence {
|
||||
int valid;
|
||||
uint32_t count, mean_us, p50_us, p95_us, p99_us, max_us, hitches;
|
||||
double span_ms, fps;
|
||||
} PsxPresentCadence;
|
||||
/* Host API completion intervals, shared by all backends; no monitor-display
|
||||
* guarantee. Bounded passive snapshot. reset is consumed by the presenter. */
|
||||
void psx_video_telemetry_cadence(int window_ms, int reset, PsxPresentCadence *out);
|
||||
int psx_video_telemetry_cadence_json(int window_ms, int reset, char *out, int capacity);
|
||||
/* Explicit monotonic host microseconds for adapters and deterministic tests. */
|
||||
void psx_video_telemetry_present_at(int result, uint64_t now_us);
|
||||
|
||||
/* Emulation-owner settings after runtime mod overrides, sampled every VBlank.
|
||||
* Actual allocated scale/dimensions come from the separate backend snapshot. */
|
||||
typedef struct PsxVideoConfigTelemetry {
|
||||
|
||||
@@ -47,6 +47,26 @@
|
||||
* The `lui` moves to (W + m) << 16 and the `bltz` is taken only while
|
||||
* SX < -m. bltz_sites does not fit this `bltz`: it compares the whole
|
||||
* register with -m, and every negative `SX << 16` is far below that.
|
||||
* `slti rt, x, 0` on the same `SX << 16` value is the same sign test as a
|
||||
* flag instead of a branch, and screen_x_sites widens it identically.
|
||||
*
|
||||
* [[widescreen.cull.sxy]] -- the all-vertices trivial reject over packed GTE
|
||||
* SXY words (Y in the high half, X in the low half), folded with AND:
|
||||
*
|
||||
* and r, v0, mask ; mask holds the sign bits 0x80008000
|
||||
* and r, v1, r
|
||||
* and r, v2, r ; (and r, v3, r for a quad)
|
||||
* bne r, zero, reject
|
||||
*
|
||||
* Bit 31 of r survives only if every Y is negative (all above the screen)
|
||||
* and bit 15 only if every X is negative (all left of it). Each AND of the
|
||||
* chain becomes psx_ws_sxy_and_value(acc, v, m): the vanilla AND, except that
|
||||
* bit 15 also needs this vertex's X < -m. The fold then rejects to the left
|
||||
* only when every vertex is left of the revealed edge. Every other bit,
|
||||
* including the vertical bit 31, is the vanilla AND, and at m == 0 the whole
|
||||
* word is the vanilla AND. Each step reads its operands when the guest's
|
||||
* own AND does, so the chain stays exact whatever the guest does between
|
||||
* steps.
|
||||
* ========================================================================== */
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -92,6 +112,17 @@ static inline int psx_ws_cull_bltz_hi_value(uint32_t v, int32_t m)
|
||||
return (int32_t)v < -((m > 0 ? m : 0) * 65536);
|
||||
}
|
||||
|
||||
/* One AND of a [[widescreen.cull.sxy]] fold: `acc & sxy`, keeping bit 15 (X
|
||||
* sign) only while this vertex's X (low half of `sxy`) is left of -m. */
|
||||
static inline uint32_t psx_ws_sxy_and_value(uint32_t acc, uint32_t sxy,
|
||||
int32_t m)
|
||||
{
|
||||
const uint32_t r = acc & sxy;
|
||||
if (m <= 0 || !(r & 0x8000u)) return r;
|
||||
if ((int32_t)(int16_t)(uint16_t)sxy < -m) return r;
|
||||
return r & ~0x8000u;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user