Add opt-in retained texture banks and enhancement memory snapshots

This commit is contained in:
Matthew Stanley
2026-09-13 16:50:26 -07:00
parent 7025bb5f88
commit 9037e7bd23
22 changed files with 563 additions and 18 deletions
+57
View File
@@ -0,0 +1,57 @@
# Retained-terrain services (experimental, opt-in)
Issue: `beads-eio.3.156`. These services do not replace the faithful renderer.
They support a trusted, statically linked game plugin retaining original assets
outside guest RAM/VRAM. The first consumer is Crash's N. Sanity Beach experiment.
- `psx_mod_read_disc_file`: emulation-thread-only, reads the original or derived
mounted ISO/CUE/BIN/CHD through an independent reader. Query with NULL/0, then
supply capacity for the whole file (64 MiB ceiling). Applies active raw/user
sector patches; never seeks the emulated CD controller. Current ISO path
lookup supports root files and one subdirectory.
- Immutable texture banks: stable nonzero 16-bit IDs, owned copies of PS1
texels/indices and palettes, lazy OpenGL upload. ID 0 means ordinary VRAM.
256 MiB aggregate host budget, maximum 1024x512 words per bank. A resolver can
rebuild a missing bank after a cold save-state restore. IDs must not be reused
for different assets during a process lifetime. Software/Vulkan and the GL
CPU-authoritative dual path report unsupported.
- Trusted GT3 packet arenas: bank ID in unused C1/C2 colour high bytes; optional
fractional XY/reciprocal-depth suffix. Only explicitly allocated packet
arenas interpret this extension; stock packets retain their meaning. See
`runtime/include/mod_plugins.h` for the wire layout. CPU-side immutable assets
are intentionally not serialized. No host/GL pointers are written to guest
memory or save states.
- GPU-DMA enhancement aperture: allocated-only physical 0x00800000..0x00FFFFFF
(8 MiB), outside all four PS1 RAM mirrors. Unallocated DMA addresses retain
retail 2 MiB folding. BIOS/expansion/MMIO aliases cannot map to this aperture.
- Save states with allocations use v8 and a required enhancement-memory section.
Both CPU and DMA allocations are captured; a layout cookie rejects older or
differently sized layouts before applying RAM. Vanilla saves still write v7.
Allocate during activation, not while rewind is recording. A matching layout
is necessary, not a substitute for the game's asset/version compatibility.
The texture bank store lives for the process lifetime. This prototype is not a
general eviction/reload API, and there is no global terrain depth buffer or bulk
native mesh submission API yet. The Crash prototype's textured whole-level path
is substantially slower than its stock path; do not treat it as production-ready.
## Validation
Source-owned CTests: `mod_texture_banks_test`, `mod_memory_snapshot_test`,
`mod_gpu_dma_aperture_test`, `mod_runtime_test`, `gl_readback_runner_test` pass.
The real-GL readback fixture passes 82 checks at both 1x and 4x on NVIDIA,
including retained 4-bit CLUT/16-bit textures, VRAM bank transitions and painter
order. `run_gl_readback_region.py --sdl-library` accepts an explicit system SDL3
import library in addition to its default static dependency.
Crash's expanded-memory save/load and cold asset reconstruction were exercised
live. Baseline rewind was previously validated; this new enhanced path still
needs a dedicated live rewind acceptance run and performance profiling.
## TCP-only view diagnostics
`display_aspect num=21 den=9` changes the rendered view without moving, resizing
or focusing the window. `display_aspect num=3 den=1 adaptive=1` resumes adaptive
view, capped at 3:1. Neither writes settings. Unlike `ws_aspect` (GTE-only), this
updates the presented view and native-wide surface. Use `screenshot` for the
rendered native-wide buffer or `present_shot` for the composed window surface.
+10
View File
@@ -23,6 +23,16 @@ psxrecomp_add_runtime_target(psx-runtime
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
if(BUILD_TESTING)
add_executable(mod_texture_banks_test tests/test_mod_texture_banks.c src/mod_texture_banks.c)
target_include_directories(mod_texture_banks_test PRIVATE include)
add_test(NAME mod_texture_banks_test COMMAND mod_texture_banks_test)
if(NOT MSVC)
add_executable(mod_memory_snapshot_test tests/test_mod_memory_snapshot.c)
target_include_directories(mod_memory_snapshot_test PRIVATE include src)
target_compile_options(mod_memory_snapshot_test PRIVATE -O2 -flto -ffunction-sections -fdata-sections)
target_link_options(mod_memory_snapshot_test PRIVATE -flto -Wl,--gc-sections)
add_test(NAME mod_memory_snapshot_test COMMAND mod_memory_snapshot_test)
endif()
# Optional real-hardware regression. Configure its dependencies explicitly;
# absence must remain visible, never counted as a passing GPU test.
set(PSX_GL_READBACK_SDL_ROOT "" CACHE PATH "SDL3 static build root for GL readback test")
+5 -2
View File
@@ -42,7 +42,9 @@ extern "C" {
* v5 = v4 + CD-ROM Sub-Q replacement state;
* v6 = v5 + per-word GPU DMA2 linked-list progress;
* v7 = v6 + pending XA DATA_END IRQ state. */
#define BOOT_STATE_VERSION 7u
#define BOOT_STATE_VERSION 8u
/* v8 adds allocated enhancement memory. Vanilla writers remain v7; older
* runtimes must not accept enhanced snapshots while ignoring their arenas. */
/* v7 intentionally breaks older savestates after the CD-ROM wire grew. Reject
* them at the header before any section changes the live machine. */
#define BOOT_STATE_VERSION_MIN_READ 7u
@@ -65,7 +67,7 @@ typedef struct {
uint32_t codegen_ver; /* PSX_OVERLAY_CODEGEN_VER */
/* ---- layout ---- */
uint32_t section_count; /* number of sections that follow */
uint32_t reserved; /* 0 */
uint32_t reserved; /* v8: enhancement-memory layout cookie; vanilla 0 */
} BootStateHeader;
#define BOOT_STATE_HEADER_WIRE_BYTES 36u
@@ -82,6 +84,7 @@ typedef struct {
* never allowed) -> normal boot + recapture.
*/
enum {
BS_SEC_MODMEM = 0x11, /* allocated opt-in CPU/GPU enhancement arenas */
BS_SEC_CPU = 0x01, /* CPUState: gpr/pc/hi/lo/cop0/gte_data/gte_ctrl */
BS_SEC_RAM = 0x02, /* 2 MB main RAM */
BS_SEC_SPAD = 0x03, /* 1 KB scratchpad */
+4
View File
@@ -18,6 +18,10 @@ extern "C" {
/* Create the GL context on a window made with SDL_WINDOW_OPENGL.
* Returns 1 on success, 0 to fall back to the SDL_Renderer present path. */
int gl_renderer_init_context(struct SDL_Window *win);
/* Select a retained immutable bank for the next textured submission; zero
* selects live VRAM. Emulation/GL owning thread only. Returns 0 if unavailable. */
int gl_renderer_select_texture_bank(uint16_t id);
int gl_renderer_texture_banks_supported(void);
/* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive).
* Safe before or after context creation; applies live when a context exists. */
+12 -5
View File
@@ -9,18 +9,18 @@ extern "C" {
/*
* GPU linked-list tags carry only 24 address bits. Trusted enhancements that
* need a larger primitive arena use this otherwise-unmapped 4 MiB aperture so
* need a larger primitive arena use this otherwise-unmapped 8 MiB aperture so
* their CPU pointers survive the tag truncation. Nothing is mapped until an
* enhancement explicitly allocates from it.
*/
#define PSX_MOD_GPU_DMA_APERTURE_BASE 0x00C00000u
#define PSX_MOD_GPU_DMA_APERTURE_SIZE (4u * 1024u * 1024u)
#define PSX_MOD_GPU_DMA_GUEST_BASE 0x80C00000u
#define PSX_MOD_GPU_DMA_APERTURE_BASE 0x00800000u
#define PSX_MOD_GPU_DMA_APERTURE_SIZE (8u * 1024u * 1024u)
#define PSX_MOD_GPU_DMA_GUEST_BASE 0x80800000u
static inline int psx_mod_gpu_dma_aperture_offset_for(
uint32_t address, uint32_t width, uint32_t used, uint32_t *offset) {
/* CPU addresses retain all physical bits after KSEG normalization. The
* BIOS at 0x1FC00000 must never alias the aperture at 0x00C00000.
* BIOS at 0x1FC00000 must never alias the enhancement aperture.
* DMA callers pass an already-truncated 24-bit tag separately below. */
uint32_t canonical = address & 0x1FFFFFFFu;
uint32_t off;
@@ -48,6 +48,13 @@ static inline uint32_t psx_mod_gpu_dma_resolve_address_for(
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t size, uint32_t alignment);
uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address);
uint32_t psx_mod_memory_alloc(uint32_t size, uint32_t alignment);
/* Allocations are fixed during activation. Snapshot allocated bytes, never
* host pointers; immutable host assets are reconstructed separately. */
uint32_t psx_mod_memory_snapshot_bytes(void);
void psx_mod_memory_snapshot_write(uint8_t* out);
int psx_mod_memory_snapshot_read(const uint8_t* data, uint32_t size);
uint32_t psx_mod_memory_layout_cookie(void);
#ifdef __cplusplus
}
+24
View File
@@ -28,6 +28,30 @@ void psx_mod_function_entry(struct CPUState* cpu, uint32_t address);
/* 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.
* Emulation-thread callbacks only. NULL buffer + zero capacity queries size;
* otherwise capacity must hold the entire file. Active sector mods apply. */
int psx_mod_read_disc_file(const char* path, void* buffer, uint32_t capacity,
uint32_t* size);
/* Experimental retained-texture service (currently OpenGL only). IDs are
* nonzero, stable game-owned identities, NOT GL names. Banks are immutable
* 16-bit PS1 texels/indices with a caller-selected row pitch (width).
* A missing bank may be reconstructed from original assets by the resolver,
* including when a restored DMA queue refers to a previously unseen level. */
typedef int (*PSXModTextureBankResolver)(uint16_t id);
int psx_mod_texture_banks_supported(void);
int psx_mod_define_texture_bank(uint16_t id, uint32_t width, uint32_t height,
const uint16_t* pixels);
void psx_mod_set_texture_bank_resolver(PSXModTextureBankResolver resolver);
/* A dedicated GPU-DMA packet arena. Only GT3 commands sourced from this
* allocation interpret C1/C2's otherwise-unused high bytes as a bank ID:
* id = (C1 >> 24) | ((C2 >> 24) << 8). ID zero uses ordinary VRAM. The rest
* of the packet is standard GP0, retaining OT order, palettes and STP blend.
* Optional 40-byte suffix after the 40-byte tagged GT3: u32 magic 0x48545031,
* three IEEE float 1/z weights, six IEEE float x/y coordinates. This enables
* precise perspective rendering without transient host-pointer side tables.
* Allocate during activation; do not mix stock game packets into this arena. */
uint32_t psx_mod_alloc_texture_packet_memory(uint32_t size, uint32_t alignment);
uint8_t psx_mod_read_byte(uint32_t address);
void psx_mod_write_byte(uint32_t address, uint8_t value);
uint16_t psx_mod_read_half(uint32_t address);
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
const uint16_t* mod_texture_bank_pixels(uint16_t id, uint32_t* width, uint32_t* height);
uint16_t mod_texture_packet_bank(uint32_t source, const uint32_t* words, uint32_t count);
int mod_texture_packet_precision(uint32_t source, float q[3], float xy[6]);
#ifdef __cplusplus
}
#endif
+1
View File
@@ -416,6 +416,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_bezel.c
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_texture_banks.c
${PSXRECOMP_ROOT}/runtime/src/psx_keybinds.c
${PSXRECOMP_ROOT}/runtime/src/psx_bios_backend.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay.c
+15 -3
View File
@@ -1,4 +1,5 @@
#include "boot_state.h"
#include "mod_memory.h"
#include "overlay_api.h" /* PSX_OVERLAY_CODEGEN_HASH / _ABI_TAG / _CODEGEN_VER */
#include "dirty_ram_interp.h"
#include "gpu.h" /* gpu_get_vram — CPU-auth mirror under dual-raster */
@@ -364,19 +365,23 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
int ok;
memset(&h, 0, sizeof h);
h.magic = BOOT_STATE_MAGIC;
h.version = BOOT_STATE_VERSION;
h.version = psx_mod_memory_snapshot_bytes() ? BOOT_STATE_VERSION : 7u;
h.reserved = psx_mod_memory_layout_cookie();
h.bios_checksum = bios_checksum;
h.entry_pc = entry_pc;
h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH;
h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG;
h.codegen_ver = (uint32_t)PSX_OVERLAY_CODEGEN_VER;
h.section_count = 16;
h.section_count = 16 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u);
ok = write_header_le(o, &h);
if (ok) ok = write_cpu_section(o, cpu);
if (ok) ok = write_section(o, BS_SEC_RAM, memory_get_ram_ptr(), RAM_SIZE);
if (ok) ok = write_section(o, BS_SEC_SPAD, memory_get_scratchpad_ptr(), SPAD_SIZE);
if (ok && psx_mod_memory_snapshot_bytes())
ok = write_module_section(o, BS_SEC_MODMEM, psx_mod_memory_snapshot_bytes,
psx_mod_memory_snapshot_write);
if (ok) {
/* 12B: i_stat, i_mask, cycles_since_vblank. Zeroing csv on warm load
* rebased every tip to phase 0 and forked MotK wait-loop resim
@@ -668,6 +673,8 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
free(words);
return 1;
}
case BS_SEC_MODMEM:
return psx_mod_memory_snapshot_read(p, len);
case BS_SEC_ICACHE: {
PstR r;
if (len != 1024u * 4u) return 0;
@@ -752,6 +759,10 @@ int boot_state_check_buffer(const uint8_t* file, size_t file_len,
(unsigned)h.magic, (unsigned)BOOT_STATE_MAGIC);
boot_state_append_reason(reason, reason_cap, part);
}
if (h.reserved != psx_mod_memory_layout_cookie()) {
boot_state_append_reason(reason, reason_cap, "enhancement_memory_layout");
return 0;
}
if (h.version < BOOT_STATE_VERSION_MIN_READ ||
h.version > BOOT_STATE_VERSION) {
snprintf(part, sizeof(part), "version=%u(want %u..%u)",
@@ -802,7 +813,8 @@ int boot_state_load_buffer(const uint8_t* file, size_t file_len,
(1u<<BS_SEC_CPU)|(1u<<BS_SEC_RAM)|(1u<<BS_SEC_SPAD)|(1u<<BS_SEC_IRQ)|
(1u<<BS_SEC_TIMER)|(1u<<BS_SEC_CLOCK)|(1u<<BS_SEC_GPU)|(1u<<BS_SEC_VRAM)|
(1u<<BS_SEC_SPU)|(1u<<BS_SEC_SPURAM)|(1u<<BS_SEC_CDROM)|(1u<<BS_SEC_DMA)|
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_MDEC)|(1u<<BS_SEC_DIRTY);
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_MDEC)|(1u<<BS_SEC_DIRTY)|
(psx_mod_memory_snapshot_bytes() ? (1u<<BS_SEC_MODMEM) : 0u);
uint32_t seen = 0;
int ok = 1;
const double t0 = boot_state_mono_ms();
+11
View File
@@ -7892,6 +7892,16 @@ static void handle_ws_aspect(int id, const char *json)
send_fmt("{\"id\":%d,\"ok\":true,\"num\":%d,\"den\":%d}", id, num, den);
}
extern int psx_debug_display_aspect(int num, int den, int adaptive);
static void handle_display_aspect(int id, const char *json) {
int num=json_get_int(json,"num",-1), den=json_get_int(json,"den",-1);
int adaptive=json_get_int(json,"adaptive",0);
if (!psx_debug_display_aspect(num,den,adaptive)) {
send_err(id,"invalid display aspect (4:3 through 32:9)");return;
}
send_fmt("{\"id\":%d,\"ok\":true,\"num\":%d,\"den\":%d,\"adaptive\":%d}",id,num,den,adaptive!=0);
}
/* Live native-wide vs squash toggle (A/B): ws_nw on=<0|1> re-engages the wide
* path in the chosen mode without a relaunch. 2 = native-wide, 1 = squash. */
extern void psx_ws_set_native_wide(int on);
@@ -13609,6 +13619,7 @@ static const CmdEntry s_commands[] = {
{ "ws_hud_mode", handle_ws_hud_mode },
{ "kernel_bless", handle_kernel_bless },
{ "ws_aspect", handle_ws_aspect },
{ "display_aspect", handle_display_aspect },
{ "ws_nw", handle_ws_nw },
{ "scanline", handle_scanline },
{ "ws_backdrop_ring", handle_ws_backdrop_ring },
+24
View File
@@ -11,6 +11,8 @@
*/
#include "gpu.h"
#include "gpu_gl_renderer.h"
#include "mod_texture_banks.h"
#include "display_scanout.h"
#include "pgxp.h"
#include "mod_memory.h"
@@ -4122,14 +4124,36 @@ static void gp0_exec_shaded_textured_tri(void) {
}
if (draw_area_out_bbox(vx, vy, 3)) return;
uint16_t host_bank = mod_texture_packet_bank(gp0_cmd_source_addr, gp0_cmd_buf, 9u);
if (host_bank && (gr_backend() != GR_BACKEND_OPENGL ||
!gl_renderer_select_texture_bank(host_bank))) return;
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
prepare_precise_triangle(1, 4, 7,
vx, vy);
prepare_texture_triangle(1, 4, 7);
if (host_bank) {
float q[3], xy[6];
if (mod_texture_packet_precision(gp0_cmd_source_addr, q, xy)) {
gr_set_precise_triangle(1,
(int32_t)((xy[0] + draw_offset_x) * 65536.0f),
(int32_t)((xy[1] + draw_offset_y) * 65536.0f),
(int32_t)((xy[2] + draw_offset_x) * 65536.0f),
(int32_t)((xy[3] + draw_offset_y) * 65536.0f),
(int32_t)((xy[4] + draw_offset_x) * 65536.0f),
(int32_t)((xy[5] + draw_offset_y) * 65536.0f));
gr_set_perspective_triangle(1, q[0], q[1], q[2]);
}
}
gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0],
vx[1], vy[1], u[1], v[1], c[1],
vx[2], vy[2], u[2], v[2], c[2],
clut_x, clut_y, tpage, raw_texture);
if (host_bank) (void)gl_renderer_select_texture_bank(0);
}
int psx_mod_texture_banks_supported(void) {
return gr_backend() == GR_BACKEND_OPENGL && gl_renderer_texture_banks_supported();
}
/* Execute shaded textured quad (GP0 0x3C-0x3F) */
+54 -4
View File
@@ -67,6 +67,7 @@
#include "gpu_render.h"
#include "gpu_sw_renderer.h"
#include "gpu_gl_renderer.h"
#include "mod_texture_banks.h"
#include "frame_interpolation.h"
#include "host_osd.h"
#include "psx_savestate_menu.h"
@@ -359,6 +360,8 @@ static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh);
static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh);
static int s_raster_ok = 0; /* full GPU pipeline available */
static GLuint s_bank_tex[65536];
static GLuint s_selected_bank_tex;
/* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */
static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0;
@@ -1107,7 +1110,9 @@ static const char *TEX_FS =
"uniform int u_filter; /* 1 = bilinear */\n"
"uniform float u_shift;\n"
"int vram_at(int x, int y){\n"
" return int(texelFetch(u_vram, ivec2(x & 1023, y & 511), 0).r);\n"
" ivec2 p = ivec2(x & 1023, y & 511);\n"
" if (any(greaterThanEqual(p, textureSize(u_vram, 0)))) return 0;\n"
" return int(texelFetch(u_vram, p, 0).r);\n"
"}\n"
"int fetch_texel(int u, int v){\n"
" u &= 255; v &= 255;\n"
@@ -1746,6 +1751,7 @@ static float s_tb[TEXBATCH_MAXV * TEXV];
static int s_tb_n = 0; /* verts queued */
static int s_tb_semi = -2;
static int s_tb_mask = 0, s_tb_filter = 0;
static GLuint s_tb_bank_tex;
static int s_tb_twin[4] = {0, 0, 0, 0};
static uint64_t s_batch_total = 0, s_batch_reason[7];
@@ -1857,7 +1863,7 @@ static void flush_tex_batch(void) {
hr_begin(1);
p_glUseProgram(s_tex_prog);
p_glActiveTexture(PSXGL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, s_raw_tex);
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex);
p_glUniform1i(s_uVram, 0);
p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]);
p_glUniform1i(s_uMaskset, s_tb_mask);
@@ -2053,7 +2059,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
int depth = (texpage >> 7) & 3; if (depth > 2) depth = 2;
flush_cpu_upload(); /* if a CPU->VRAM upload is pending it flushes the batch first */
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y); /* flushes batch iff it must pack */
if (!s_selected_bank_tex)
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y);
mark_prim_dirty(xs, ys, 3, 1 /* textured */);
/* Append to the textured batch. Flush first if this prim's blend/mask/twin/
@@ -2089,7 +2096,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
int isolate = (semi >= 0);
int reason = -1;
if (s_tb_n > 0) {
if (isolate) reason = 0;
if (s_tb_bank_tex != s_selected_bank_tex) reason = 0;
else if (isolate) reason = 0;
else if (batch_semi != s_tb_semi) reason = 1;
else if (s_mask_set != s_tb_mask) reason = 2;
else if (s_tex_filter != s_tb_filter) reason = 3;
@@ -2104,6 +2112,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
if (s_tb_n + 3 > TEXBATCH_MAXV) { s_batch_reason[6]++; flush_tex_batch(); }
if (s_tb_n == 0) { /* opening a batch: capture its keyed state */
s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate;
s_tb_bank_tex = s_selected_bank_tex;
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
}
float *vp = &s_tb[s_tb_n * TEXV];
@@ -2984,6 +2993,41 @@ static int init_gpu_raster(void) {
return 1;
}
int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual; }
int gl_renderer_select_texture_bank(uint16_t id) {
uint32_t width, height;
const uint16_t* pixels;
GLint alignment, row_length;
if (!id) { s_selected_bank_tex = 0; return 1; }
if (!gl_renderer_texture_banks_supported()) return 0;
if (!s_bank_tex[id]) {
pixels = mod_texture_bank_pixels(id, &width, &height);
if (!pixels) return 0;
glGenTextures(1, &s_bank_tex[id]);
p_glActiveTexture(PSXGL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, s_bank_tex[id]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment);
glGetIntegerv(PSXGL_UNPACK_ROW_LENGTH, &row_length);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
glTexImage2D(GL_TEXTURE_2D, 0, PSXGL_R16UI, (GLsizei)width,
(GLsizei)height, 0, PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, pixels);
glPixelStorei(GL_UNPACK_ALIGNMENT, alignment);
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, row_length);
if (glGetError() != GL_NO_ERROR) {
glDeleteTextures(1, &s_bank_tex[id]); s_bank_tex[id] = 0;
return 0;
}
}
s_selected_bank_tex = s_bank_tex[id];
return 1;
}
int gl_renderer_init_context(SDL_Window *win) {
s_win = win;
s_present_w = 0;
@@ -3084,6 +3128,12 @@ void gl_renderer_set_swap_interval(int interval) {
}
void gl_renderer_shutdown(void) {
if (s_ctx) {
for (unsigned i = 1; i < 65536u; ++i)
if (s_bank_tex[i]) glDeleteTextures(1, &s_bank_tex[i]);
}
memset(s_bank_tex, 0, sizeof s_bank_tex);
s_selected_bank_tex = s_tb_bank_tex = 0;
if (s_ctx) {
ensure_cpu();
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
+16
View File
@@ -1774,6 +1774,22 @@ extern "C" void psx_ws_set_native_wide(int on) {
}
extern "C" int psx_ws_get_native_wide(void) { return g_ws_native_wide; }
/* TCP diagnostics: change the rendered view without moving, resizing, raising
* or focusing the user's window. Transient; never writes settings.toml. */
extern "C" int psx_debug_display_aspect(int num, int den, int adaptive) {
if (adaptive) return psx_mod_set_adaptive_display_aspect(num, den);
if (!psx_mod_set_fixed_display_aspect(num, den)) return 0;
gl_renderer_set_display_aspect(num, den);
vk_renderer_set_display_aspect(num, den);
if (sdl_renderer) {
g_logical_w = 480 * num * g_video_scale / den;
SDL_RenderSetLogicalSize(sdl_renderer, g_logical_w, 480 * g_video_scale);
}
g_ws_projection_mode = -1;
refresh_widescreen_projection();
return 1;
}
static bool g_gl_active = false; /* GL context live -> GL present path */
static bool g_vk_active = false; /* Vulkan context live -> VK present path */
+40
View File
@@ -16,6 +16,7 @@
#include "gpu.h"
#include "mdec.h"
#include "mod_memory.h"
#include "pst_wire.h"
#include "sio.h"
#include "spu.h"
#include "timers.h"
@@ -43,6 +44,45 @@ static uint32_t mod_memory_used;
static uint8_t mod_gpu_dma_memory[PSX_MOD_GPU_DMA_APERTURE_SIZE];
static uint32_t mod_gpu_dma_memory_used;
uint32_t psx_mod_memory_snapshot_bytes(void) {
return mod_memory_used || mod_gpu_dma_memory_used
? 16u + mod_memory_used + mod_gpu_dma_memory_used : 0u;
}
uint32_t psx_mod_memory_layout_cookie(void) {
/* Compatibility guard, not a cryptographic identity. Include aperture
* revision; old enabled-mod saves must be rejected before applying RAM. */
if (!psx_mod_memory_snapshot_bytes()) return 0u;
return 0x4D4F4402u ^ (mod_memory_used * 16777619u) ^ mod_gpu_dma_memory_used;
}
void psx_mod_memory_snapshot_write(uint8_t* out) {
PstW w;
if (!out || !psx_mod_memory_snapshot_bytes()) return;
pst_w_init(&w, out, psx_mod_memory_snapshot_bytes());
pst_w_u32(&w, 1u);
pst_w_u32(&w, PSX_MOD_GPU_DMA_APERTURE_BASE);
pst_w_u32(&w, mod_memory_used);
pst_w_u32(&w, mod_gpu_dma_memory_used);
memcpy(out + 16u, mod_memory, mod_memory_used);
memcpy(out + 16u + mod_memory_used, mod_gpu_dma_memory, mod_gpu_dma_memory_used);
}
int psx_mod_memory_snapshot_read(const uint8_t* data, uint32_t size) {
PstR r;
uint32_t version, base, cpu_bytes, dma_bytes;
if (!data || size < 16u || size != psx_mod_memory_snapshot_bytes()) return 0;
pst_r_init(&r, data, size);
if (!pst_r_u32(&r, &version) || !pst_r_u32(&r, &base) ||
!pst_r_u32(&r, &cpu_bytes) || !pst_r_u32(&r, &dma_bytes) ||
version != 1u || base != PSX_MOD_GPU_DMA_APERTURE_BASE ||
cpu_bytes != mod_memory_used || dma_bytes != mod_gpu_dma_memory_used)
return 0;
memcpy(mod_memory, data + 16u, cpu_bytes);
memcpy(mod_gpu_dma_memory, data + 16u + cpu_bytes, dma_bytes);
return 1;
}
/*
* Trusted mods may opt into host-backed guest memory in Expansion 1. Before
* the first allocation this entire region retains hardware open-bus behavior.
+37
View File
@@ -1294,6 +1294,43 @@ extern "C" void mod_runtime_enable_disc_patches(void) {
PSXRecompV4::state().disc_enabled = true;
}
extern "C" int psx_mod_read_disc_file(const char* path, void* buffer,
uint32_t capacity, uint32_t* size) {
using namespace PSXRecompV4;
if (size) *size = 0;
if (!path || !*path || !size || (!buffer && capacity)) return 0;
try {
const auto& s = state();
const auto& mount = s.effective_disc_path.empty() ? s.disc_path : s.effective_disc_path;
if (mount.empty()) return 0;
PS1::ISOReader reader;
PS1::ISOFileEntry entry;
if (!reader.Open(mount.string()) || !reader.FindFile(path, entry) ||
entry.is_directory || !entry.size || entry.size > 64u * 1024u * 1024u)
return 0;
if (!buffer) { *size = entry.size; return 1; }
if (capacity < entry.size) return 0;
uint8_t sector[2048], raw[2352];
for (uint32_t offset = 0; offset < entry.size; offset += 2048u) {
const uint32_t lba = entry.lba + offset / 2048u;
if (reader.ReadRawSector(lba, raw)) {
if (raw[15] != 1 && (raw[15] != 2 || (raw[18] & 0x20u))) return 0;
mod_runtime_patch_disc_sector(lba, 1, raw, sizeof raw);
std::memcpy(sector, raw + (raw[15] == 1 ? 16 : 24), sizeof sector);
if (raw[15] == 1) mod_runtime_patch_disc_sector(lba, 0, sector, sizeof sector);
} else {
if (!reader.ReadSector(lba, sector)) return 0;
mod_runtime_patch_disc_sector(lba, 0, sector, sizeof sector);
}
if (state().disc_guard_failed) return 0;
const uint32_t count = std::min(2048u, entry.size - offset);
std::memcpy(static_cast<uint8_t*>(buffer) + offset, sector, count);
}
*size = entry.size;
return 1;
} catch (...) { return 0; }
}
extern "C" void mod_runtime_activate_plugins(void) {
using namespace PSXRecompV4;
RuntimeMods& s = state();
+94
View File
@@ -0,0 +1,94 @@
#include "mod_plugins.h"
#include "mod_memory.h"
#include "mod_texture_banks.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>
typedef struct { uint16_t* pixels; uint32_t width, height; } Bank;
typedef struct { uint32_t begin, size; } PacketArena;
static Bank banks[65536];
static PacketArena arenas[16];
static unsigned arena_count;
static uint32_t bank_bytes;
static PSXModTextureBankResolver resolve_bank;
static int resolving;
void psx_mod_set_texture_bank_resolver(PSXModTextureBankResolver resolver) {
resolve_bank = resolver;
}
int psx_mod_define_texture_bank(uint16_t id, uint32_t w, uint32_t h,
const uint16_t* pixels) {
uint32_t bytes;
Bank* bank = &banks[id];
if (!id || !pixels || !w || !h || w > 1024u || h > 512u) return 0;
bytes = w * h * 2u;
if (bank->pixels)
return bank->width == w && bank->height == h &&
memcmp(bank->pixels, pixels, bytes) == 0;
if (bytes > 256u * 1024u * 1024u - bank_bytes) return 0;
bank->pixels = (uint16_t*)malloc(bytes);
if (!bank->pixels) return 0;
memcpy(bank->pixels, pixels, bytes);
bank->width = w; bank->height = h;
bank_bytes += bytes;
return 1;
}
const uint16_t* mod_texture_bank_pixels(uint16_t id, uint32_t* w, uint32_t* h) {
if (!id || !w || !h) return NULL;
if (!banks[id].pixels && resolve_bank && !resolving) {
resolving = 1;
(void)resolve_bank(id);
resolving = 0;
}
*w = banks[id].width; *h = banks[id].height;
return banks[id].pixels;
}
uint32_t psx_mod_alloc_texture_packet_memory(uint32_t size, uint32_t alignment) {
uint32_t addr;
if (arena_count == 16u || size < 40u) return 0;
addr = psx_mod_gpu_dma_memory_alloc(size, alignment);
if (!addr) return 0;
arenas[arena_count].begin = addr & 0x1FFFFFFFu;
arenas[arena_count++].size = size;
return addr;
}
uint16_t mod_texture_packet_bank(uint32_t source, const uint32_t* words, uint32_t count) {
unsigned i;
uint32_t address = source & 0x1FFFFFFFu;
if (!words || count != 9u || (words[0] >> 26) != 0xDu) return 0;
for (i = 0; i < arena_count; ++i) {
if (address >= arenas[i].begin && address - arenas[i].begin <= arenas[i].size - 36u)
return (uint16_t)((words[3] >> 24) | ((words[6] >> 24) << 8));
}
return 0;
}
int mod_texture_packet_precision(uint32_t source, float q[3], float xy[6]) {
unsigned i, j;
uint32_t address = source & 0x1FFFFFFFu;
for (i = 0; i < arena_count; ++i) {
if (arenas[i].size < 80u || address < arenas[i].begin ||
address - arenas[i].begin > arenas[i].size - 76u) continue;
if (psx_mod_read_word(source + 36u) != 0x48545031u) return 0;
for (j = 0; j < 9u; ++j) {
uint32_t bits = psx_mod_read_word(source + 40u + j * 4u);
float f;
memcpy(&f, &bits, sizeof f);
if (!isfinite(f)) return 0;
if (j < 3u) {
if (f <= 0.0f || f > 1.0f) return 0;
q[j] = f;
} else {
if (f < -2048.0f || f > 2048.0f) return 0;
xy[j - 3u] = f;
}
}
return 1;
}
return 0;
}
+5 -3
View File
@@ -26,6 +26,7 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--compiler-bin", required=True)
parser.add_argument("--sdl-root", required=True)
parser.add_argument("--sdl-library", help="Explicit SDL3 library (e.g. a system DLL import library)")
parser.add_argument("--output", required=True)
parser.add_argument("--gl-source")
parser.add_argument("--fixture", type=pathlib.Path)
@@ -39,8 +40,9 @@ def main():
sdl = pathlib.Path(args.sdl_root).resolve()
if not (sdl / "sdl3-src/include").is_dir():
parser.error(f"--sdl-root must contain the sdl3-src/include directory: {sdl}")
if not (sdl / "sdl3-build/libSDL3.a").is_file():
parser.error(f"--sdl-root must contain sdl3-build/libSDL3.a: {sdl}")
sdl_library = pathlib.Path(args.sdl_library).resolve() if args.sdl_library else sdl / "sdl3-build/libSDL3.a"
if not sdl_library.is_file():
parser.error(f"SDL3 library does not exist: {sdl_library}")
output = pathlib.Path(args.output).resolve()
output.mkdir(parents=True, exist_ok=True)
dest = pathlib.Path(tempfile.mkdtemp(prefix="readback-", dir=output))
@@ -73,7 +75,7 @@ def main():
"oleaut32", "version", "uuid", "advapi32", "setupapi", "shell32",
"dinput8", "opengl32"]
if run([compiler / "g++.exe", "-O2", "-flto", dest / "probe.o", dest / "sw.o",
sdl / "sdl3-build/libSDL3.a", *["-l" + name for name in libraries],
sdl_library, *["-l" + name for name in libraries],
"-o", dest / "probe.exe"]).returncode:
return 2
for scale in (1, 4):
+21
View File
@@ -1,5 +1,8 @@
/* Original source-owned GL readback-coherence regression. No retail payload. */
#include "gpu_gl_renderer.c"
#include "mod_texture_banks.c"
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
static uint16_t image[1024*512], oracle[1024*512];
int g_psx_vram_dirty_tracking=0;
uint64_t s_frame_count=0;
@@ -73,6 +76,24 @@ int main(int argc,char **argv){
check(glb_vram_read(40,40)==0,"depth24 cleared band immediate CPU read");
check(glb_vram_read(33,33)==0x3210,"newer overlapping texture survives clear");
verify("depth24 leave coherence without subsequent primitive");
/* Retained banks use their own texels AND CLUT, and bank/VRAM transitions
* must split batches without changing painter order. No retail assets. */
static uint16_t bank[256*128];
bank[0]=0x001f; bank[1]=0x03e0; bank[2]=0x7c00;
bank[16]=0x1111; /* 4-bit indices, CLUT at (0,0) */
check(psx_mod_define_texture_bank(7,256,128,bank),"define retained bank");
check(gl_renderer_select_texture_bank(7),"select retained bank");
glb_draw_shaded_textured_triangle(100,250,64,0,0x808080,132,250,64,0,0x808080,100,282,64,0,0x808080,0,0,0,1);
check(gl_renderer_select_texture_bank(0),"select original VRAM");
glb_vram_write(512,0,0x7c00);
glb_draw_shaded_textured_triangle(116,250,0,0,0x808080,148,250,0,0,0x808080,116,282,0,0,0x808080,0,0,0x108,1);
check(glb_vram_read(102,252)==0x03e0,"retained 4-bit CLUT independent of guest VRAM");
check(glb_vram_read(118,252)==0x7c00,"following stock texture wins overlap");
check(gl_renderer_select_texture_bank(7),"reselect retained bank");
glb_draw_shaded_textured_triangle(300,250,0,0,0x808080,332,250,0,0,0x808080,300,282,0,0,0x808080,0,0,0x100,1);
check(gl_renderer_select_texture_bank(0),"reset bank after direct texture");
check(glb_vram_read(302,252)==0x001f,"retained 16-bit texel");
verify("retained banks and original VRAM ordered together");
printf("checks=%d failures=%d\n",checks,failures);
gl_renderer_shutdown();SDL_DestroyWindow(win);SDL_Quit();return failures?1:0;
}
+1 -1
View File
@@ -50,7 +50,7 @@ int main(void) {
check(psx_mod_gpu_dma_aperture_offset_for(0xA0000000u + base, 4u, size, &off)
&& off == 0u, "KSEG1 CPU aperture alias resolves correctly");
check(psx_mod_gpu_dma_aperture_offset_for(base + size - 4u, 4u, size, &off)
&& off == size - 4u, "last word of full 4 MiB aperture is accessible");
&& off == size - 4u, "last word of full enhancement aperture is accessible");
check(!psx_mod_gpu_dma_aperture_offset_for(base - 4u, 4u, size, &off),
"word before aperture is rejected");
check(!psx_mod_gpu_dma_aperture_offset_for(base + size, 4u, size, &off),
+29
View File
@@ -0,0 +1,29 @@
/* Include implementation to inspect owned backing storage without pulling in
* hardware routing. Section GC discards unrelated memory access functions. */
#include "memory.c"
static void check(int ok,const char* text){if(!ok){fprintf(stderr,"FAIL %s\n",text);exit(1);}}
int main(void){
check(psx_mod_memory_snapshot_bytes()==0 && psx_mod_memory_layout_cookie()==0,"vanilla unchanged");
psx_mod_memory_snapshot_write(NULL);
check(psx_mod_memory_alloc(13,16)==0x9f000000u,"CPU allocation");
check(psx_mod_gpu_dma_memory_alloc(5u*1024u*1024u,16)==PSX_MOD_GPU_DMA_GUEST_BASE,"beyond old four MiB");
check(psx_mod_gpu_dma_resolve_address(0xd00000u)==0x100000u,"unallocated tag retains retail folding");
uint32_t size=psx_mod_memory_snapshot_bytes(),cookie=psx_mod_memory_layout_cookie();
uint8_t* saved=(uint8_t*)malloc(size);check(saved!=NULL,"test buffer");
memset(mod_memory,0x12,mod_memory_used);memset(mod_gpu_dma_memory,0x34,mod_gpu_dma_memory_used);
psx_mod_memory_snapshot_write(saved);
memset(mod_memory,0x56,mod_memory_used);memset(mod_gpu_dma_memory,0x78,mod_gpu_dma_memory_used);
check(!psx_mod_memory_snapshot_read(saved,size-1),"truncation rejected");
saved[0]=99;
check(!psx_mod_memory_snapshot_read(saved,size) && mod_memory[0]==0x56,"bad version no mutation");
saved[0]=1;saved[8]++;
check(!psx_mod_memory_snapshot_read(saved,size) && mod_gpu_dma_memory[0]==0x78,"bad layout no mutation");
saved[8]--;
check(psx_mod_memory_snapshot_read(saved,size),"restore");
for(uint32_t i=0;i<mod_memory_used;++i)check(mod_memory[i]==0x12,"CPU bytes restored");
for(uint32_t i=0;i<mod_gpu_dma_memory_used;++i)check(mod_gpu_dma_memory[i]==0x34,"DMA bytes restored");
check(psx_mod_memory_layout_cookie()==cookie,"stable layout identity");
check(psx_mod_gpu_dma_memory_alloc(16,16)!=0 && psx_mod_memory_layout_cookie()!=cookie,"layout change detected");
check(!psx_mod_memory_snapshot_read(saved,size),"old layout rejected");
free(saved);puts("enhancement snapshot checks passed");return 0;
}
+39
View File
@@ -6,6 +6,7 @@
#include "gpu.h"
#include <array>
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <fstream>
@@ -491,6 +492,44 @@ int main() {
"unestablished display geometry must report zero so callers skip "
"drawing instead of guessing");
/* Source-owned ISO: nested file spanning two sectors, without a derived
* disc. Host reads must choose the original mount and leave sizes honest. */
std::vector<uint8_t> iso(24*2048);
auto le32 = [&](size_t at,uint32_t n) { for(unsigned i=0;i<4;++i) iso[at+i]=(uint8_t)(n>>(i*8)); };
auto record = [&](size_t at,uint32_t lba,uint32_t bytes,bool directory,const std::string& name) {
iso[at]=(uint8_t)((33+name.size()+1)&~size_t(1));
le32(at+2,lba); le32(at+10,bytes); iso[at+25]=directory?2:0;
iso[at+28]=1;iso[at+31]=1;iso[at+32]=(uint8_t)name.size();
std::copy(name.begin(),name.end(),iso.begin()+at+33);
};
iso[16*2048]=1;std::copy_n("CD001",5,iso.begin()+16*2048+1);iso[16*2048+6]=1;
record(16*2048+156,20,2048,true,std::string(1,'\0'));
record(20*2048,21,2048,true,"S0");
record(21*2048,22,3000,false,"LEVEL.NSF;1");
for(unsigned i=0;i<3000;++i)iso[22*2048+i]=(uint8_t)(i*7);
const auto disc_root=root/"host-reader";
const auto iso_path=disc_root/"original.iso";
write_bytes(iso_path,iso);
check(PSXRecompV4::mod_runtime_initialize(disc_root,"READER",0,{},&error),"reader initialize");
check(PSXRecompV4::mod_runtime_commit(iso_path,&error),"reader mount original ISO");
uint32_t bytes=0;
check(psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&bytes) && bytes==3000,"query original nested file size");
std::vector<uint8_t> result(3000);
check(!psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),2999,&bytes) && bytes==0,"undersized destination rejected");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
bytes==3000 && std::equal(result.begin(),result.end(),iso.begin()+22*2048),"complete original file bytes");
check(!psx_mod_read_disc_file("S0/MISSING.NSF",nullptr,0,&bytes) && bytes==0,"missing file explicit");
check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected");
std::vector<uint8_t> raw_iso(24*2352);
for(unsigned i=0;i<24;++i) {
raw_iso[i*2352+15]=2;
std::copy_n(iso.begin()+i*2048,2048,raw_iso.begin()+i*2352+24);
}
const auto raw_path=disc_root/"original.bin";
write_bytes(raw_path,raw_iso);
check(PSXRecompV4::mod_runtime_commit(raw_path,&error),"reader mount raw disc");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
std::equal(result.begin(),result.end(),iso.begin()+22*2048),"raw and ISO reads identical");
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";
+53
View File
@@ -0,0 +1,53 @@
#include "mod_plugins.h"
#include "mod_memory.h"
#include "mod_texture_banks.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
static uint32_t memory[256], used, reads, resolves;
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a) {
(void)a; if(n>sizeof memory-used)return 0;
uint32_t address=PSX_MOD_GPU_DMA_GUEST_BASE+used;used+=n;return address;
}
uint32_t psx_mod_read_word(uint32_t address) {
uint32_t offset=(address&0x1fffffffu)-PSX_MOD_GPU_DMA_APERTURE_BASE;
if(offset>sizeof memory-4)abort(); ++reads;return memory[offset/4];
}
static void check(int ok,const char* text){if(!ok){fprintf(stderr,"FAIL %s\n",text);exit(1);}}
static int resolve(uint16_t id){uint16_t data=0x4567;++resolves;return psx_mod_define_texture_bank(id,1,1,&data);}
static uint32_t bits(float f){uint32_t n;memcpy(&n,&f,4);return n;}
int main(void){
uint16_t data[4]={1,2,3,4};uint32_t w,h;
check(!psx_mod_define_texture_bank(0,2,2,data),"bank zero reserved");
check(!psx_mod_define_texture_bank(1,1025,2,data),"dimensions bounded");
check(psx_mod_define_texture_bank(1,2,2,data),"define");
check(psx_mod_define_texture_bank(1,2,2,data),"identical definition idempotent");
data[0]=99;
check(!psx_mod_define_texture_bank(1,2,2,data),"immutable id cannot change");
check(mod_texture_bank_pixels(1,&w,&h)[0]==1 && w==2 && h==2,"owned deep copy");
check(!mod_texture_bank_pixels(2,&w,&h),"missing bank explicit");
psx_mod_set_texture_bank_resolver(resolve);
check(mod_texture_bank_pixels(2,&w,&h)[0]==0x4567 && resolves==1,"lazy restored-bank resolver");
check(mod_texture_bank_pixels(2,&w,&h)!=NULL && resolves==1,"resident bank no reload");
const uint32_t arena=psx_mod_alloc_texture_packet_memory(80,16);
uint32_t words[9]={0x34000000,0,0,0x23000000,0,0,0x01000000,0,0};
check(!mod_texture_packet_bank(0x80001004,words,9),"stock packet cannot opt in");
check(mod_texture_packet_bank(arena+4,words,9)==0x123,"bank id only in trusted arena");
check(!mod_texture_packet_bank(arena+48,words,9),"whole command must fit");
check(!mod_texture_packet_bank(arena+4,words,8),"command size checked");
words[0]=0x30000000;
check(!mod_texture_packet_bank(arena+4,words,9),"GT3 opcode checked");
float q[3],xy[6];
check(!mod_texture_packet_precision(0x80001004,q,xy) && reads==0,"stock precision does not read suffix");
memory[10]=0x48545031;
for(unsigned i=0;i<3;++i)memory[11+i]=bits(.01f);
for(unsigned i=0;i<6;++i)memory[14+i]=bits(15.25f+i);
check(mod_texture_packet_precision(arena+4,q,xy) && q[1]==.01f && xy[5]==20.25f,"fractional projection and reciprocal depth");
check(!mod_texture_packet_precision(arena+8,q,xy),"suffix bounds");
memory[11]=bits(NAN);
check(!mod_texture_packet_precision(arena+4,q,xy),"NaN rejected");
memory[11]=bits(0);
check(!mod_texture_packet_precision(arena+4,q,xy),"zero reciprocal depth rejected");
puts("texture bank checks passed");return 0;
}