diff --git a/docs/RETAINED-TERRAIN-EXPERIMENT.md b/docs/RETAINED-TERRAIN-EXPERIMENT.md new file mode 100644 index 00000000..02df57f3 --- /dev/null +++ b/docs/RETAINED-TERRAIN-EXPERIMENT.md @@ -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. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index cead44ad..0c41349c 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -23,6 +23,16 @@ psxrecomp_add_runtime_target(psx-runtime # (docs/ENHANCEMENTS.md G1.10). Titles get their _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") diff --git a/runtime/include/boot_state.h b/runtime/include/boot_state.h index 5ecf29b9..3ee203e1 100644 --- a/runtime/include/boot_state.h +++ b/runtime/include/boot_state.h @@ -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 */ diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index a0007bd1..e9ad1263 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -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. */ diff --git a/runtime/include/mod_memory.h b/runtime/include/mod_memory.h index 5f3e0fd0..3eb54b66 100644 --- a/runtime/include/mod_memory.h +++ b/runtime/include/mod_memory.h @@ -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 } diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index a1e4645e..bacae0c2 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -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); diff --git a/runtime/include/mod_texture_banks.h b/runtime/include/mod_texture_banks.h new file mode 100644 index 00000000..2e79ac25 --- /dev/null +++ b/runtime/include/mod_texture_banks.h @@ -0,0 +1,11 @@ +#pragma once +#include +#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 diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index db6d8c48..5b92e4e4 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -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 diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index c68e14ad..0a644f4d 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -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< 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 }, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index ea3c79d6..1d671cd0 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -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) */ diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index e2befd6f..d78b44f4 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -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; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index e19f8d7d..4dd39e78 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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 */ diff --git a/runtime/src/memory.c b/runtime/src/memory.c index f4b3cb78..fc199c93 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -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. diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 3f8b5765..af31e895 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -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(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(); diff --git a/runtime/src/mod_texture_banks.c b/runtime/src/mod_texture_banks.c new file mode 100644 index 00000000..5eba304d --- /dev/null +++ b/runtime/src/mod_texture_banks.c @@ -0,0 +1,94 @@ +#include "mod_plugins.h" +#include "mod_memory.h" +#include "mod_texture_banks.h" +#include +#include +#include + +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; +} diff --git a/runtime/tests/run_gl_readback_region.py b/runtime/tests/run_gl_readback_region.py index 5dccb731..5f4e2a4b 100644 --- a/runtime/tests/run_gl_readback_region.py +++ b/runtime/tests/run_gl_readback_region.py @@ -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): diff --git a/runtime/tests/test_gl_readback_region.c b/runtime/tests/test_gl_readback_region.c index 355eebab..bdc71b5d 100644 --- a/runtime/tests/test_gl_readback_region.c +++ b/runtime/tests/test_gl_readback_region.c @@ -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; } diff --git a/runtime/tests/test_mod_gpu_dma_aperture.c b/runtime/tests/test_mod_gpu_dma_aperture.c index 2033c64e..7829075c 100644 --- a/runtime/tests/test_mod_gpu_dma_aperture.c +++ b/runtime/tests/test_mod_gpu_dma_aperture.c @@ -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), diff --git a/runtime/tests/test_mod_memory_snapshot.c b/runtime/tests/test_mod_memory_snapshot.c new file mode 100644 index 00000000..71344d5b --- /dev/null +++ b/runtime/tests/test_mod_memory_snapshot.c @@ -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 +#include #include #include #include @@ -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 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 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 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"; diff --git a/runtime/tests/test_mod_texture_banks.c b/runtime/tests/test_mod_texture_banks.c new file mode 100644 index 00000000..9b39ed7c --- /dev/null +++ b/runtime/tests/test_mod_texture_banks.c @@ -0,0 +1,53 @@ +#include "mod_plugins.h" +#include "mod_memory.h" +#include "mod_texture_banks.h" +#include +#include +#include +#include +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; +}