From adaea3d8bf1bd019d995315f8ba0e667b493ea1d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 12 Apr 2026 20:57:41 -0700 Subject: [PATCH] Salvage clean hardware sim from v2/v3: GTE, SW renderer, ISO reader, CRC32 Audited psxrecomp-v3 and psxrecomp-v2 hardware simulation files. Carried forward only pure hardware simulation with zero HLE/BIOS contamination. Each file was read in full and checked for: - event_deliver / bios_ / interpreter_ references (none found) - printf/fprintf (removed from GTE, none in others) - stubs (return 0, TODO, FIXME) (none found) - v3-specific state dependencies (removed psx_runtime.h, automation.h) Added files: - gte.cpp/h: COP2 coprocessor (876 lines, all GTE commands) - gpu_sw_renderer.c/h: Software rasterizer with texture support (723 lines) - crc32.c/h: CRC32 utility (27 lines) - iso_reader.cpp/h + iso_reader_c.cpp: ISO 9660 reader (490 lines) Modified: - cpu_state.h: Added gte_data[32] and gte_ctrl[32] arrays - CMakeLists.txt: Added new source files Explicitly rejected (per CLAUDE.md rule 7): - bios.c, events.c, threads.c, interpreter.c (HLE/kernel state) - bios_trace.c, func_logger.c, main_runner.cpp (interpreter era) - cdrom.c (has event_deliver + hard-coded RAM writes, needs surgery) - spu.cpp from v2 (WinMM-only, needs SDL port) Build verified: clean compile and link. Co-Authored-By: Claude Opus 4.6 (1M context) --- runtime/CMakeLists.txt | 5 + runtime/include/cpu_state.h | 9 + runtime/include/crc32.h | 6 + runtime/include/gpu_sw_renderer.h | 59 +++ runtime/include/gte.h | 176 ++++++ runtime/include/iso_reader.h | 162 ++++++ runtime/src/crc32.c | 27 + runtime/src/gpu_sw_renderer.c | 722 +++++++++++++++++++++++++ runtime/src/gte.cpp | 854 ++++++++++++++++++++++++++++++ runtime/src/iso_reader.cpp | 459 ++++++++++++++++ runtime/src/iso_reader_c.cpp | 35 ++ 11 files changed, 2514 insertions(+) create mode 100644 runtime/include/crc32.h create mode 100644 runtime/include/gpu_sw_renderer.h create mode 100644 runtime/include/gte.h create mode 100644 runtime/include/iso_reader.h create mode 100644 runtime/src/crc32.c create mode 100644 runtime/src/gpu_sw_renderer.c create mode 100644 runtime/src/gte.cpp create mode 100644 runtime/src/iso_reader.cpp create mode 100644 runtime/src/iso_reader_c.cpp diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index a90e6647..3918d4fd 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(psx-runtime src/main.cpp src/memory.c src/gpu.c + src/gpu_sw_renderer.c src/dma.c src/timers.c src/interrupts.c @@ -15,6 +16,10 @@ add_executable(psx-runtime src/memcard.c src/debug_server.c src/traps.c + src/gte.cpp + src/crc32.c + src/iso_reader.cpp + src/iso_reader_c.cpp ../generated/SCPH1001_full.c ../generated/SCPH1001_dispatch.c ) diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 27855d64..fa5fa1d9 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -23,6 +23,8 @@ typedef struct CPUState { uint32_t pc; /* program counter */ uint32_t hi, lo; /* mult/div result registers */ uint32_t cop0[32]; /* COP0 system control registers (SR, Cause, EPC, ...) */ + uint32_t gte_data[32]; /* COP2 (GTE) data registers */ + uint32_t gte_ctrl[32]; /* COP2 (GTE) control registers */ /* Memory access function pointers -- wired at init to psx_read/psx_write. */ uint32_t (*read_word)(uint32_t addr); @@ -45,6 +47,13 @@ extern void psx_dispatch(CPUState* cpu, uint32_t target_addr); /* Unknown dispatch — defined in runtime/src/traps.c */ extern void psx_unknown_dispatch(CPUState* cpu, uint32_t addr, uint32_t phys); +/* GTE (COP2) — defined in runtime/src/gte.cpp */ +extern void gte_execute(CPUState* cpu, uint32_t cmd); +extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg); +extern uint32_t gte_read_ctrl(CPUState* cpu, uint8_t reg); +extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val); +extern void gte_write_ctrl(CPUState* cpu, uint8_t reg, uint32_t val); + #ifdef __cplusplus } #endif diff --git a/runtime/include/crc32.h b/runtime/include/crc32.h new file mode 100644 index 00000000..5d604095 --- /dev/null +++ b/runtime/include/crc32.h @@ -0,0 +1,6 @@ +#pragma once +#include +#include + +/* Compute CRC32 (IEEE 802.3) over a byte buffer. */ +uint32_t crc32_compute(const uint8_t *data, size_t len); diff --git a/runtime/include/gpu_sw_renderer.h b/runtime/include/gpu_sw_renderer.h new file mode 100644 index 00000000..f64fefc5 --- /dev/null +++ b/runtime/include/gpu_sw_renderer.h @@ -0,0 +1,59 @@ +#ifndef PSX_GPU_SW_RENDERER_H +#define PSX_GPU_SW_RENDERER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initialize software renderer */ +void sw_renderer_init(uint16_t* vram); + +/* Draw state — must be set by gpu.c before each primitive */ +void sw_set_semi_transparency(int enabled, int mode); +void sw_set_mask_bits(int set_bit, int check_bit); +void sw_set_texture_window(uint32_t raw); +void sw_set_color_modulation(int r, int g, int b, int raw_texture); + +/* Drawing primitives — called from gpu.c command processing */ +void sw_fill_rect(int x, int y, int w, int h, uint16_t color); +void sw_copy_rect(int src_x, int src_y, int dst_x, int dst_y, int w, int h); +void sw_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, + uint16_t color); +void sw_draw_gouraud_triangle(int x0, int y0, uint16_t c0, + int x1, int y1, uint16_t c1, + int x2, int y2, uint16_t c2); +void sw_draw_textured_triangle(int x0, int y0, int u0, int v0, + int x1, int y1, int u1, int v1, + int x2, int y2, int u2, int v2, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage); +void sw_draw_flat_rect(int x, int y, int w, int h, uint16_t color); +void sw_draw_textured_rect(int x, int y, int w, int h, + int u, int v, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage); +void sw_draw_line(int x0, int y0, int x1, int y1, uint16_t color); + +/* Copy display region to a 32-bit RGBA buffer for SDL presentation. + Returns number of pixels written. */ +int sw_render_display(uint32_t* out_pixels, int out_pitch, + int disp_x, int disp_y, int disp_w, int disp_h); + +/* VRAM transfer (CPU->VRAM, VRAM->CPU) */ +void sw_vram_write(int x, int y, uint16_t pixel); +uint16_t sw_vram_read(int x, int y); +void sw_vram_transfer_in(int x, int y, int w, int h, const uint16_t* data); +void sw_vram_transfer_out(int x, int y, int w, int h, uint16_t* data); + +/* Set draw area and draw offset */ +void sw_set_draw_area(int x1, int y1, int x2, int y2); +void sw_get_draw_area(int *x1, int *y1, int *x2, int *y2); +void sw_set_draw_offset(int x, int y); + +#ifdef __cplusplus +} +#endif + +#endif /* PSX_GPU_SW_RENDERER_H */ diff --git a/runtime/include/gte.h b/runtime/include/gte.h new file mode 100644 index 00000000..02179e01 --- /dev/null +++ b/runtime/include/gte.h @@ -0,0 +1,176 @@ +#pragma once + +#include +#include + +namespace PSXRecomp { +namespace GTE { + +// PSX GTE FLAG register bit definitions +// Bit 31: Error summary (set if any of bits 30-23 or 18-13 are set) +// Bits 30-25: MAC1/2/3 positive/negative overflow (>43 bits) +// Bits 24-22: IR1/2/3 saturated +// Bits 21-19: Color R/G/B saturated +// Bit 18: SZ3/OTZ saturated +// Bit 17: Divide overflow +// Bit 16: MAC0 positive overflow +// Bit 15: MAC0 negative overflow +// Bits 14-13: SX2/SY2 saturated +// Bit 12: IR0 saturated +enum GTEFlags : uint32_t { + FLAG_MAC1_POS = 1u << 30, FLAG_MAC2_POS = 1u << 29, FLAG_MAC3_POS = 1u << 28, + FLAG_MAC1_NEG = 1u << 27, FLAG_MAC2_NEG = 1u << 26, FLAG_MAC3_NEG = 1u << 25, + FLAG_IR1_SAT = 1u << 24, FLAG_IR2_SAT = 1u << 23, FLAG_IR3_SAT = 1u << 22, + FLAG_COLOR_R = 1u << 21, FLAG_COLOR_G = 1u << 20, FLAG_COLOR_B = 1u << 19, + FLAG_SZ3_OTZ = 1u << 18, FLAG_DIV_OVF = 1u << 17, + FLAG_MAC0_POS = 1u << 16, FLAG_MAC0_NEG = 1u << 15, + FLAG_SX2_SAT = 1u << 14, FLAG_SY2_SAT = 1u << 13, + FLAG_IR0_SAT = 1u << 12, + FLAG_ERROR_MASK = 0x7F87E000u, + FLAG_ERROR_BIT = 0x80000000u, +}; + +struct GTEState { + // === Data Registers === + int16_t V0[3], V1[3], V2[3]; // Vertex vectors (s3.12) + int32_t SXY[4]; // Screen XY FIFO (packed: X low16, Y high16) + uint16_t SZ[4]; // Screen Z FIFO (unsigned 16-bit) + uint32_t RGB[4]; // RGB color FIFO (packed R,G,B,CD bytes) + + // === Control Registers (matrices, vectors, config) === + int16_t RT[3][3]; // Rotation matrix (s3.12) + int32_t TR[3]; // Translation vector (32-bit) + int16_t L[3][3]; // Light source direction matrix (s3.12) + int32_t BK[3]; // Background color (32-bit) + int16_t LC[3][3]; // Light color matrix (s3.12) — replaces LR/LG/LB + int32_t FC[3]; // Far color (32-bit) + int32_t OFX, OFY; // Screen offset (s15.16) + uint16_t H; // Projection plane distance (unsigned 16-bit) + int16_t DQA; // Depth queing coefficient A (signed 16-bit) + int32_t DQB; // Depth queing coefficient B (signed 32-bit!) + int16_t ZSF3, ZSF4; // Z-scale factors (signed 16-bit) + + // === Intermediate/Result Registers === + int16_t IR0, IR1, IR2, IR3; // Intermediate results (signed 16-bit) + int32_t MAC0, MAC1, MAC2, MAC3; // Multiply-accumulate results (32-bit) + uint16_t OTZ; // Ordering table Z (unsigned 16-bit) + int32_t LZCS; // Leading zero count source (32-bit) + int32_t LZCR; // Leading zero count result (32-bit) + + // === Flag Register === + uint32_t FLAG; + + GTEState() { std::memset(this, 0, sizeof(GTEState)); } + + // --- Saturation helpers --- + + int16_t saturate_ir(int32_t value, int ir_num, bool lm = false) { + int32_t min_val = lm ? 0 : -0x8000; + if (value < min_val) { + FLAG |= (1u << (25 - ir_num)); // IR1=24, IR2=23, IR3=22 + return static_cast(min_val); + } + if (value > 0x7FFF) { + FLAG |= (1u << (25 - ir_num)); + return 0x7FFF; + } + return static_cast(value); + } + + int16_t saturate_ir0(int32_t value) { + if (value < 0) { FLAG |= FLAG_IR0_SAT; return 0; } + if (value > 0x1000) { FLAG |= FLAG_IR0_SAT; return 0x1000; } + return static_cast(value); + } + + uint8_t saturate_color(int32_t value, int component) { + if (value < 0) { FLAG |= (1u << (21 - component)); return 0; } + if (value > 255) { FLAG |= (1u << (21 - component)); return 255; } + return static_cast(value); + } + + uint16_t saturate_sz(int32_t value) { + if (value < 0) { FLAG |= FLAG_SZ3_OTZ; return 0; } + if (value > 0xFFFF) { FLAG |= FLAG_SZ3_OTZ; return 0xFFFF; } + return static_cast(value); + } + + void check_mac_overflow(int64_t value, int mac_num) { + // mac_num: 1,2,3 + if (value > 0x7FFFFFFFFLL) FLAG |= (1u << (31 - mac_num)); // MAC1=30, MAC2=29, MAC3=28 + if (value < -0x800000000LL) FLAG |= (1u << (28 - mac_num)); // MAC1=27, MAC2=26, MAC3=25 + } + + void check_mac0_overflow(int64_t value) { + if (value > 0x7FFFFFFFLL) FLAG |= FLAG_MAC0_POS; + if (value < -0x80000000LL) FLAG |= FLAG_MAC0_NEG; + } + + void push_sxy(int64_t sx, int64_t sy) { + // Saturate to screen coordinates and set flags + int16_t sx_sat, sy_sat; + if (sx < -0x400) { sx_sat = -0x400; FLAG |= FLAG_SX2_SAT; } + else if (sx > 0x3FF) { sx_sat = 0x3FF; FLAG |= FLAG_SX2_SAT; } + else { sx_sat = static_cast(sx); } + if (sy < -0x400) { sy_sat = -0x400; FLAG |= FLAG_SY2_SAT; } + else if (sy > 0x3FF) { sy_sat = 0x3FF; FLAG |= FLAG_SY2_SAT; } + else { sy_sat = static_cast(sy); } + // Push FIFO + SXY[0] = SXY[1]; SXY[1] = SXY[2]; + SXY[2] = (static_cast(sy_sat & 0xFFFF) << 16) | + (static_cast(sx_sat & 0xFFFF)); + SXY[3] = SXY[2]; + } + + void push_sz(int32_t value) { + SZ[0] = SZ[1]; SZ[1] = SZ[2]; SZ[2] = SZ[3]; + SZ[3] = saturate_sz(value); + } + + void push_rgb(uint8_t r, uint8_t g, uint8_t b) { + uint8_t cd = (RGB[0] >> 24) & 0xFF; // Code byte from RGBC + RGB[0] = RGB[1]; RGB[1] = RGB[2]; + RGB[2] = (cd << 24) | (b << 16) | (g << 8) | r; + } + + void set_error_flag() { + if (FLAG & FLAG_ERROR_MASK) FLAG |= FLAG_ERROR_BIT; + } +}; + +// === GTE Operations === +void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0); +void gte_rtps(GTEState* gte, uint32_t instr); +void gte_rtpt(GTEState* gte, uint32_t instr); +void gte_nclip(GTEState* gte, uint32_t instr); +void gte_avsz3(GTEState* gte, uint32_t instr); +void gte_avsz4(GTEState* gte, uint32_t instr); +void gte_nccs_internal(GTEState* gte, int16_t* V); +void gte_nccs(GTEState* gte, uint32_t instr); +void gte_ncct(GTEState* gte, uint32_t instr); +void gte_ncds_internal(GTEState* gte, int16_t* V); +void gte_ncds(GTEState* gte, uint32_t instr); +void gte_ncdt(GTEState* gte, uint32_t instr); +void gte_ncs_internal(GTEState* gte, int16_t* V); +void gte_ncs(GTEState* gte, uint32_t instr); +void gte_nct(GTEState* gte, uint32_t instr); +void gte_dpcs(GTEState* gte, uint32_t instr); +void gte_dpct(GTEState* gte, uint32_t instr); +void gte_dpcl(GTEState* gte, uint32_t instr); +void gte_intpl(GTEState* gte, uint32_t instr); +void gte_cdp(GTEState* gte, uint32_t instr); +void gte_cc(GTEState* gte, uint32_t instr); +void gte_mvmva(GTEState* gte, uint32_t instr); +void gte_op(GTEState* gte, uint32_t instr); +void gte_sqr(GTEState* gte, uint32_t instr); +void gte_gpf(GTEState* gte, uint32_t instr); +void gte_gpl(GTEState* gte, uint32_t instr); + +// Register transfer +void gte_mtc2(GTEState* gte, uint8_t reg, uint32_t value); +uint32_t gte_mfc2(GTEState* gte, uint8_t reg); +void gte_ctc2(GTEState* gte, uint8_t reg, uint32_t value); +uint32_t gte_cfc2(GTEState* gte, uint8_t reg); + +} // namespace GTE +} // namespace PSXRecomp diff --git a/runtime/include/iso_reader.h b/runtime/include/iso_reader.h new file mode 100644 index 00000000..aa0de4b1 --- /dev/null +++ b/runtime/include/iso_reader.h @@ -0,0 +1,162 @@ +#pragma once + +#include +#include +#include +#include + +/** + * ISO/BIN/CUE Reader for PS1 CD-ROM images + * Session 24-25 - Validation-first approach + * + * Features: + * - Open ISO/BIN file + * - Read sectors by LBA + * - Parse ISO9660 filesystem + * - List files in directory + * - Read file contents by path + */ + +namespace PS1 { + +/** + * Information about a file entry in the ISO filesystem + */ +struct ISOFileEntry { + std::string name; // Filename (without version suffix) + uint32_t lba; // Logical Block Address (sector number) + uint32_t size; // File size in bytes + bool is_directory; // True if this is a directory +}; + +/** + * Root directory information from Primary Volume Descriptor + */ +struct RootDirectoryInfo { + uint32_t lba; // LBA of root directory data + uint32_t size; // Size of root directory in bytes +}; + +class ISOReader { +public: + /** + * Constructor + */ + ISOReader(); + + /** + * Destructor - ensures file is closed + */ + ~ISOReader(); + + /** + * Open an ISO/BIN file for reading + * @param filename Path to .iso, .bin, or .cue file + * @return true if opened successfully, false otherwise + */ + bool Open(const std::string& filename); + + /** + * Close the currently open file + */ + void Close(); + + /** + * Read a single sector from the ISO + * @param lba Logical Block Address (sector number) + * @param buffer Buffer to read into (must be at least 2048 bytes) + * @return true if read successfully, false otherwise + */ + bool ReadSector(uint32_t lba, uint8_t* buffer); + + /** + * Check if a file is currently open + * @return true if file is open + */ + bool IsOpen() const; + + /** + * Get volume ID from Primary Volume Descriptor + * @return Volume ID string (disc name) + */ + std::string GetVolumeID() const; + + /** + * Get the resolved path to the BIN file (set after Open()) + */ + std::string GetBinPath() const; + + /** + * Get root directory information + * @return RootDirectoryInfo containing LBA and size + */ + RootDirectoryInfo GetRootDirectory() const; + + /** + * List all files in a directory + * @param path Directory path (empty string = root directory) + * @return Vector of ISOFileEntry for all files/subdirectories + */ + std::vector ListFiles(const std::string& path = ""); + + /** + * Find a file by name in the root directory + * @param path Filename to search for (e.g., "SYSTEM.CNF" or "SCUS_942.36") + * @param entry Output ISOFileEntry to populate with file info + * @return true if found, false if not found + */ + bool FindFile(const std::string& path, ISOFileEntry& entry); + + /** + * Read entire file contents into buffer + * @param path Filename to read + * @param buffer Buffer to read into (must be large enough) + * @param max_size Maximum bytes to read (buffer size limit) + * @return Number of bytes read (0 if error or file not found) + */ + size_t ReadFile(const std::string& path, uint8_t* buffer, size_t max_size); + + /** + * Get file size without reading data + * @param path Filename to query + * @return File size in bytes (0 if file not found) + */ + size_t GetFileSize(const std::string& path); + +private: + /** + * Parse the Primary Volume Descriptor (sector 16) + * Extracts volume ID and root directory information + * @return true if parsed successfully, false otherwise + */ + bool ParseVolumeDescriptor(); + + /** + * Helper: Read a 32-bit value in both-endian format (733 format) + * @param data Pointer to 8-byte both-endian value + * @return 32-bit value (reads little-endian half) + */ + uint32_t Read733(const uint8_t* data) const; + + /** + * Helper: Parse a directory record from raw sector data + * @param data Pointer to directory record data + * @param entry Output ISOFileEntry to populate + * @return true if parsed successfully, false if invalid record + */ + bool ParseDirectoryRecord(const uint8_t* data, ISOFileEntry& entry) const; + + /** + * Helper: List files in a directory given its LBA and size + * Used for subdirectory navigation + */ + std::vector ListFilesByLBA(uint32_t lba, uint32_t dir_size); + + std::ifstream file_; + bool is_open_; + std::string volume_id_; + std::string bin_path_; + RootDirectoryInfo root_dir_; +}; + +} // namespace PS1 diff --git a/runtime/src/crc32.c b/runtime/src/crc32.c new file mode 100644 index 00000000..9d211d68 --- /dev/null +++ b/runtime/src/crc32.c @@ -0,0 +1,27 @@ +/* + * crc32.c — Simple CRC32 implementation (no external dependencies). + * Uses the standard IEEE 802.3 polynomial 0xEDB88320. + */ +#include "crc32.h" +#include + +static uint32_t s_table[256]; +static int s_table_ready = 0; + +static void crc32_init_table(void) { + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (int j = 0; j < 8; j++) + c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1); + s_table[i] = c; + } + s_table_ready = 1; +} + +uint32_t crc32_compute(const uint8_t *data, size_t len) { + if (!s_table_ready) crc32_init_table(); + uint32_t crc = 0xFFFFFFFFu; + for (size_t i = 0; i < len; i++) + crc = s_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8); + return crc ^ 0xFFFFFFFFu; +} diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c new file mode 100644 index 00000000..addd383e --- /dev/null +++ b/runtime/src/gpu_sw_renderer.c @@ -0,0 +1,722 @@ +/* + * gpu_sw_renderer.c — PS1 software rasterizer + * + * Operates on a 1024x512 uint16_t VRAM array (15-bit color + mask bit). + * All drawing is clipped to the current draw area. + * + * Texture formats: 4-bit CLUT, 8-bit CLUT, 15-bit direct. + * Supports: semi-transparency (4 modes), mask bit, texture window, + * color modulation for textured primitives. + */ + +#include "gpu_sw_renderer.h" +#include +#include + +/* ------------------------------------------------------------------ */ +/* Constants */ +/* ------------------------------------------------------------------ */ + +#define VRAM_WIDTH 1024 +#define VRAM_HEIGHT 512 + +/* ------------------------------------------------------------------ */ +/* State */ +/* ------------------------------------------------------------------ */ + +static uint16_t *g_vram; + +/* Draw area clipping rectangle */ +static int g_clip_x1, g_clip_y1, g_clip_x2, g_clip_y2; + +/* Draw offset */ +static int g_offset_x, g_offset_y; + +/* Semi-transparency state */ +static int g_semi_trans_enabled; +static int g_semi_trans_mode; /* 0=B/2+F/2, 1=B+F, 2=B-F, 3=B+F/4 */ + +/* Mask bit state */ +static int g_mask_set_bit; /* force bit 15 on written pixels */ +static int g_mask_check_bit; /* skip write if dest bit 15 is set */ + +/* Texture window (from GP0(E2h)) */ +static uint8_t g_tw_mask_x, g_tw_mask_y; +static uint8_t g_tw_off_x, g_tw_off_y; + +/* Color modulation for textured primitives */ +static uint8_t g_mod_r, g_mod_g, g_mod_b; +static int g_raw_texture; /* 1 = skip modulation */ + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +static inline int clamp_i(int v, int lo, int hi) { + if (v < lo) return lo; + if (v > hi) return hi; + return v; +} + +static inline int min_i(int a, int b) { return a < b ? a : b; } +static inline int max_i(int a, int b) { return a > b ? a : b; } + +static inline uint16_t vram_get(int x, int y) { + x &= (VRAM_WIDTH - 1); + y &= (VRAM_HEIGHT - 1); + return g_vram[y * VRAM_WIDTH + x]; +} + +static inline int inside_draw_area(int x, int y) { + return (x >= g_clip_x1 && x <= g_clip_x2 && + y >= g_clip_y1 && y <= g_clip_y2); +} + +/* ------------------------------------------------------------------ */ +/* Semi-transparency blending */ +/* ------------------------------------------------------------------ */ + +static inline uint16_t blend_pixels(uint16_t back, uint16_t front, int mode) { + int br = (back >> 0) & 0x1F; + int bg = (back >> 5) & 0x1F; + int bb = (back >> 10) & 0x1F; + int fr = (front >> 0) & 0x1F; + int fg = (front >> 5) & 0x1F; + int fb = (front >> 10) & 0x1F; + int r, g, b; + + switch (mode) { + case 0: /* B/2+F/2 */ + r = (br + fr) >> 1; + g = (bg + fg) >> 1; + b = (bb + fb) >> 1; + break; + case 1: /* B+F */ + r = br + fr; if (r > 31) r = 31; + g = bg + fg; if (g > 31) g = 31; + b = bb + fb; if (b > 31) b = 31; + break; + case 2: /* B-F */ + r = br - fr; if (r < 0) r = 0; + g = bg - fg; if (g < 0) g = 0; + b = bb - fb; if (b < 0) b = 0; + break; + case 3: /* B+F/4 */ + r = br + (fr >> 2); if (r > 31) r = 31; + g = bg + (fg >> 2); if (g > 31) g = 31; + b = bb + (fb >> 2); if (b > 31) b = 31; + break; + default: + r = fr; g = fg; b = fb; + break; + } + return (uint16_t)(r | (g << 5) | (b << 10)); +} + +/* ------------------------------------------------------------------ */ +/* Pixel write — central function with mask + semi-trans */ +/* ------------------------------------------------------------------ */ + +/* Write an opaque (untextured) pixel with semi-transparency if enabled */ +static inline void draw_pixel_opaque(int x, int y, uint16_t color) { + if (x < 0 || x >= VRAM_WIDTH || y < 0 || y >= VRAM_HEIGHT) return; + if (!inside_draw_area(x, y)) return; + + int idx = y * VRAM_WIDTH + x; + + /* Mask bit check: don't overwrite if dest has bit 15 set */ + if (g_mask_check_bit && (g_vram[idx] & 0x8000)) return; + + /* Semi-transparency: for untextured primitives, always blend when flag set */ + if (g_semi_trans_enabled) { + color = blend_pixels(g_vram[idx], color, g_semi_trans_mode); + } + + /* Mask bit set: force bit 15 on written pixel */ + if (g_mask_set_bit) color |= 0x8000; + + g_vram[idx] = color; +} + +/* Write a textured pixel — semi-trans only if texel bit 15 is set */ +static inline void draw_pixel_textured(int x, int y, uint16_t texel) { + if (x < 0 || x >= VRAM_WIDTH || y < 0 || y >= VRAM_HEIGHT) return; + if (!inside_draw_area(x, y)) return; + + /* Transparent texel (0x0000) is always skipped */ + if (texel == 0x0000) return; + + int idx = y * VRAM_WIDTH + x; + + /* Mask bit check */ + if (g_mask_check_bit && (g_vram[idx] & 0x8000)) return; + + /* Color modulation: multiply texel by vertex color unless raw texture */ + uint16_t color; + if (!g_raw_texture) { + int tr = (texel >> 0) & 0x1F; + int tg = (texel >> 5) & 0x1F; + int tb = (texel >> 10) & 0x1F; + /* PS1 formula: (texel * color * 2) / 256, with 5-bit texel scaled to 8-bit. + * Simplified: (tr * mod_r) >> 4, clamped to 31 */ + int r = (tr * g_mod_r) >> 4; if (r > 31) r = 31; + int g = (tg * g_mod_g) >> 4; if (g > 31) g = 31; + int b = (tb * g_mod_b) >> 4; if (b > 31) b = 31; + color = (uint16_t)(r | (g << 5) | (b << 10)); + } else { + color = texel & 0x7FFF; + } + + /* Semi-transparency: for textured primitives, only blend if texel has bit 15 */ + if (g_semi_trans_enabled && (texel & 0x8000)) { + color = blend_pixels(g_vram[idx], color, g_semi_trans_mode); + } + + /* Mask bit set */ + if (g_mask_set_bit) color |= 0x8000; + + g_vram[idx] = color; +} + +/* ------------------------------------------------------------------ */ +/* Texture lookup with texture window */ +/* ------------------------------------------------------------------ */ + +static uint16_t texel_fetch(int u, int v, uint16_t texpage, + uint16_t clut_x, uint16_t clut_y) { + /* Apply texture window: + * texcoord = (texcoord AND NOT(Mask*8)) OR ((Offset AND Mask)*8) + * Mask and Offset are in 8-pixel steps. */ + if (g_tw_mask_x | g_tw_mask_y) { + u = (u & ~(g_tw_mask_x * 8)) | ((g_tw_off_x & g_tw_mask_x) * 8); + v = (v & ~(g_tw_mask_y * 8)) | ((g_tw_off_y & g_tw_mask_y) * 8); + } + + int tpx = (texpage & 0xF) * 64; + int tpy = ((texpage >> 4) & 1) * 256; + int depth = (texpage >> 7) & 3; + + switch (depth) { + case 0: { /* 4-bit CLUT */ + int vram_x = tpx + (u / 4); + int vram_y = tpy + v; + uint16_t texel_word = vram_get(vram_x, vram_y); + int shift = (u & 3) * 4; + int index = (texel_word >> shift) & 0xF; + return vram_get(clut_x + index, clut_y); + } + case 1: { /* 8-bit CLUT */ + int vram_x = tpx + (u / 2); + int vram_y = tpy + v; + uint16_t texel_word = vram_get(vram_x, vram_y); + int shift = (u & 1) * 8; + int index = (texel_word >> shift) & 0xFF; + return vram_get(clut_x + index, clut_y); + } + case 2: /* 15-bit direct */ + case 3: { + return vram_get(tpx + u, tpy + v); + } + default: + return 0; + } +} + +/* ------------------------------------------------------------------ */ +/* Public API: init */ +/* ------------------------------------------------------------------ */ + +void sw_renderer_init(uint16_t *vram) { + g_vram = vram; + g_clip_x1 = 0; + g_clip_y1 = 0; + g_clip_x2 = VRAM_WIDTH - 1; + g_clip_y2 = VRAM_HEIGHT - 1; + g_offset_x = 0; + g_offset_y = 0; + g_semi_trans_enabled = 0; + g_semi_trans_mode = 0; + g_mask_set_bit = 0; + g_mask_check_bit = 0; + g_tw_mask_x = g_tw_mask_y = 0; + g_tw_off_x = g_tw_off_y = 0; + g_mod_r = g_mod_g = g_mod_b = 16; /* neutral = 128/8 = 16 (no modulation) */ + g_raw_texture = 0; +} + +/* ------------------------------------------------------------------ */ +/* Draw state setters */ +/* ------------------------------------------------------------------ */ + +void sw_set_semi_transparency(int enabled, int mode) { + g_semi_trans_enabled = enabled; + g_semi_trans_mode = mode & 3; +} + +void sw_set_mask_bits(int set_bit, int check_bit) { + g_mask_set_bit = set_bit; + g_mask_check_bit = check_bit; +} + +void sw_set_texture_window(uint32_t raw) { + g_tw_mask_x = (uint8_t)(raw & 0x1F); + g_tw_mask_y = (uint8_t)((raw >> 5) & 0x1F); + g_tw_off_x = (uint8_t)((raw >> 10) & 0x1F); + g_tw_off_y = (uint8_t)((raw >> 15) & 0x1F); +} + +void sw_set_color_modulation(int r, int g, int b, int raw_texture) { + /* Convert 8-bit vertex color to modulation factor. + * PS1 formula: result = (texel_5bit * color_8bit) >> 4 + * When color_8bit=128 (0x80, neutral gray), result = texel * 8 = texel << 3 ≈ original. + * We store color_8bit >> 3 so the multiply in draw_pixel_textured is >> 4. + * Actually: store raw 8-bit value / 8 = >> 3 to get 5-bit-range factor. + * + * Simpler approach: store the 8-bit color, shift later. + * result_5bit = (texel_5bit * color_8bit + 127) >> 7, clamped to 31 + * But that's slower. Let's use: (texel * (color >> 3)) >> 4 + * At color=128: factor=16, (31*16)>>4 = 31 — perfect. + * At color=255: factor=31, (31*31)>>4 = 60 → clamp 31 — saturated. + * At color=0: factor=0, result=0 — black. + */ + g_mod_r = (uint8_t)(r >> 3); + g_mod_g = (uint8_t)(g >> 3); + g_mod_b = (uint8_t)(b >> 3); + g_raw_texture = raw_texture; +} + +/* ------------------------------------------------------------------ */ +/* Draw area / offset */ +/* ------------------------------------------------------------------ */ + +void sw_set_draw_area(int x1, int y1, int x2, int y2) { + g_clip_x1 = clamp_i(x1, 0, VRAM_WIDTH - 1); + g_clip_y1 = clamp_i(y1, 0, VRAM_HEIGHT - 1); + g_clip_x2 = clamp_i(x2, 0, VRAM_WIDTH - 1); + g_clip_y2 = clamp_i(y2, 0, VRAM_HEIGHT - 1); +} + +void sw_get_draw_area(int *x1, int *y1, int *x2, int *y2) { + *x1 = g_clip_x1; *y1 = g_clip_y1; + *x2 = g_clip_x2; *y2 = g_clip_y2; +} + +void sw_set_draw_offset(int x, int y) { + g_offset_x = x; + g_offset_y = y; +} + +/* ------------------------------------------------------------------ */ +/* Fill rectangle (GP0(02h) — directly in VRAM, no draw offset) */ +/* ------------------------------------------------------------------ */ + +void sw_fill_rect(int x, int y, int w, int h, uint16_t color) { + /* Fill rect ignores draw area, mask bits, and semi-transparency. + * It writes directly to VRAM with coordinates wrapping. */ + int x0 = x & (VRAM_WIDTH - 1); + int y0 = y & (VRAM_HEIGHT - 1); + + for (int row = 0; row < h; row++) { + int py = (y0 + row) & (VRAM_HEIGHT - 1); + for (int col = 0; col < w; col++) { + int px = (x0 + col) & (VRAM_WIDTH - 1); + g_vram[py * VRAM_WIDTH + px] = color; + } + } +} + +/* ------------------------------------------------------------------ */ +/* Copy rectangle (VRAM -> VRAM) */ +/* ------------------------------------------------------------------ */ + +void sw_copy_rect(int src_x, int src_y, int dst_x, int dst_y, int w, int h) { + uint16_t row_buf[VRAM_WIDTH]; + + for (int row = 0; row < h; row++) { + int sy = (src_y + row) & (VRAM_HEIGHT - 1); + int dy = (dst_y + row) & (VRAM_HEIGHT - 1); + + for (int col = 0; col < w; col++) { + int sx = (src_x + col) & (VRAM_WIDTH - 1); + row_buf[col] = g_vram[sy * VRAM_WIDTH + sx]; + } + for (int col = 0; col < w; col++) { + int dx = (dst_x + col) & (VRAM_WIDTH - 1); + uint16_t pix = row_buf[col]; + /* Copy applies mask bit settings */ + if (g_mask_check_bit && (g_vram[dy * VRAM_WIDTH + dx] & 0x8000)) + continue; + if (g_mask_set_bit) pix |= 0x8000; + g_vram[dy * VRAM_WIDTH + dx] = pix; + } + } +} + +/* ------------------------------------------------------------------ */ +/* Flat-shaded triangle (scanline rasterization) */ +/* ------------------------------------------------------------------ */ + +void sw_draw_flat_triangle(int x0, int y0, int x1, int y1, + int x2, int y2, uint16_t color) { + /* Sort vertices by Y coordinate */ + if (y0 > y1) { int t; t=x0; x0=x1; x1=t; t=y0; y0=y1; y1=t; } + if (y0 > y2) { int t; t=x0; x0=x2; x2=t; t=y0; y0=y2; y2=t; } + if (y1 > y2) { int t; t=x1; x1=x2; x2=t; t=y1; y1=y2; y2=t; } + + int dy_total = y2 - y0; + if (dy_total == 0) return; + + for (int y = y0; y <= y2; y++) { + if (y < g_clip_y1 || y > g_clip_y2) continue; + + int second_half = (y >= y1); + int seg_height = second_half ? (y2 - y1) : (y1 - y0); + if (seg_height == 0) seg_height = 1; + + float alpha = (float)(y - y0) / (float)dy_total; + float beta; + if (second_half) + beta = (float)(y - y1) / (float)seg_height; + else + beta = (float)(y - y0) / (float)seg_height; + + int xa = x0 + (int)((float)(x2 - x0) * alpha); + int xb; + if (second_half) + xb = x1 + (int)((float)(x2 - x1) * beta); + else + xb = x0 + (int)((float)(x1 - x0) * beta); + + if (xa > xb) { int t = xa; xa = xb; xb = t; } + + int sx = max_i(xa, g_clip_x1); + int ex = min_i(xb, g_clip_x2); + + for (int x = sx; x <= ex; x++) { + draw_pixel_opaque(x, y, color); + } + } +} + +/* ------------------------------------------------------------------ */ +/* Gouraud-shaded triangle (scanline rasterization with color interp) */ +/* ------------------------------------------------------------------ */ + +void sw_draw_gouraud_triangle(int x0, int y0, uint16_t c0, + int x1, int y1, uint16_t c1, + int x2, int y2, uint16_t c2) { + /* Extract 5-bit color components for each vertex */ + int r0 = (c0 >> 0) & 0x1F, g0 = (c0 >> 5) & 0x1F, b0 = (c0 >> 10) & 0x1F; + int r1 = (c1 >> 0) & 0x1F, g1 = (c1 >> 5) & 0x1F, b1 = (c1 >> 10) & 0x1F; + int r2 = (c2 >> 0) & 0x1F, g2 = (c2 >> 5) & 0x1F, b2 = (c2 >> 10) & 0x1F; + + /* Sort vertices by Y coordinate, keeping colors in sync */ + if (y0 > y1) { + int t; t=x0; x0=x1; x1=t; t=y0; y0=y1; y1=t; + t=r0; r0=r1; r1=t; t=g0; g0=g1; g1=t; t=b0; b0=b1; b1=t; + } + if (y0 > y2) { + int t; t=x0; x0=x2; x2=t; t=y0; y0=y2; y2=t; + t=r0; r0=r2; r2=t; t=g0; g0=g2; g2=t; t=b0; b0=b2; b2=t; + } + if (y1 > y2) { + int t; t=x1; x1=x2; x2=t; t=y1; y1=y2; y2=t; + t=r1; r1=r2; r2=t; t=g1; g1=g2; g2=t; t=b1; b1=b2; b2=t; + } + + int dy_total = y2 - y0; + if (dy_total == 0) return; + + for (int y = y0; y <= y2; y++) { + if (y < g_clip_y1 || y > g_clip_y2) continue; + + int second_half = (y >= y1); + int seg_height = second_half ? (y2 - y1) : (y1 - y0); + if (seg_height == 0) seg_height = 1; + + float alpha = (float)(y - y0) / (float)dy_total; + float beta; + if (second_half) + beta = (float)(y - y1) / (float)seg_height; + else + beta = (float)(y - y0) / (float)seg_height; + + /* Interpolate X along edges */ + int xa = x0 + (int)((float)(x2 - x0) * alpha); + int xb; + if (second_half) + xb = x1 + (int)((float)(x2 - x1) * beta); + else + xb = x0 + (int)((float)(x1 - x0) * beta); + + /* Interpolate colors along edges (in 5-bit space) */ + int ra = r0 + (int)((float)(r2 - r0) * alpha); + int ga = g0 + (int)((float)(g2 - g0) * alpha); + int ba = b0 + (int)((float)(b2 - b0) * alpha); + int rb, gb, bb; + if (second_half) { + rb = r1 + (int)((float)(r2 - r1) * beta); + gb = g1 + (int)((float)(g2 - g1) * beta); + bb = b1 + (int)((float)(b2 - b1) * beta); + } else { + rb = r0 + (int)((float)(r1 - r0) * beta); + gb = g0 + (int)((float)(g1 - g0) * beta); + bb = b0 + (int)((float)(b1 - b0) * beta); + } + + /* Ensure xa < xb, swap colors too */ + if (xa > xb) { + int t; + t = xa; xa = xb; xb = t; + t = ra; ra = rb; rb = t; + t = ga; ga = gb; gb = t; + t = ba; ba = bb; bb = t; + } + + int sx = max_i(xa, g_clip_x1); + int ex = min_i(xb, g_clip_x2); + int span = xb - xa; + + for (int x = sx; x <= ex; x++) { + /* Interpolate color across the scanline */ + uint16_t color; + if (span > 0) { + float t = (float)(x - xa) / (float)span; + int r = ra + (int)((float)(rb - ra) * t); + int g = ga + (int)((float)(gb - ga) * t); + int b = ba + (int)((float)(bb - ba) * t); + if (r < 0) r = 0; if (r > 31) r = 31; + if (g < 0) g = 0; if (g > 31) g = 31; + if (b < 0) b = 0; if (b > 31) b = 31; + color = (uint16_t)(r | (g << 5) | (b << 10)); + } else { + color = (uint16_t)(ra | (ga << 5) | (ba << 10)); + } + draw_pixel_opaque(x, y, color); + } + } +} + +/* ------------------------------------------------------------------ */ +/* Textured triangle */ +/* ------------------------------------------------------------------ */ + +void sw_draw_textured_triangle(int x0, int y0, int u0, int v0, + int x1, int y1, int u1, int v1, + int x2, int y2, int u2, int v2, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage) { + /* Sort by Y, keeping UV in sync */ + if (y0 > y1) { + int t; + t=x0; x0=x1; x1=t; t=y0; y0=y1; y1=t; + t=u0; u0=u1; u1=t; t=v0; v0=v1; v1=t; + } + if (y0 > y2) { + int t; + t=x0; x0=x2; x2=t; t=y0; y0=y2; y2=t; + t=u0; u0=u2; u2=t; t=v0; v0=v2; v2=t; + } + if (y1 > y2) { + int t; + t=x1; x1=x2; x2=t; t=y1; y1=y2; y2=t; + t=u1; u1=u2; u2=t; t=v1; v1=v2; v2=t; + } + + int dy_total = y2 - y0; + if (dy_total == 0) return; + + for (int y = y0; y <= y2; y++) { + if (y < g_clip_y1 || y > g_clip_y2) continue; + + int second_half = (y >= y1); + int seg_height = second_half ? (y2 - y1) : (y1 - y0); + if (seg_height == 0) seg_height = 1; + + float alpha = (float)(y - y0) / (float)dy_total; + float beta; + if (second_half) + beta = (float)(y - y1) / (float)seg_height; + else + beta = (float)(y - y0) / (float)seg_height; + + int xa = x0 + (int)((float)(x2 - x0) * alpha); + int xb; + if (second_half) + xb = x1 + (int)((float)(x2 - x1) * beta); + else + xb = x0 + (int)((float)(x1 - x0) * beta); + + float ua = u0 + (float)(u2 - u0) * alpha; + float va = v0 + (float)(v2 - v0) * alpha; + float ub, vb; + if (second_half) { + ub = u1 + (float)(u2 - u1) * beta; + vb = v1 + (float)(v2 - v1) * beta; + } else { + ub = u0 + (float)(u1 - u0) * beta; + vb = v0 + (float)(v1 - v0) * beta; + } + + if (xa > xb) { + int t = xa; xa = xb; xb = t; + float tf; + tf = ua; ua = ub; ub = tf; + tf = va; va = vb; vb = tf; + } + + int span = xb - xa; + if (span == 0) span = 1; + + int sx = max_i(xa, g_clip_x1); + int ex = min_i(xb, g_clip_x2); + + for (int x = sx; x <= ex; x++) { + float t_val = (float)(x - xa) / (float)span; + int u = (int)(ua + (ub - ua) * t_val) & 0xFF; + int v_coord = (int)(va + (vb - va) * t_val) & 0xFF; + + uint16_t texel = texel_fetch(u, v_coord, texpage, clut_x, clut_y); + draw_pixel_textured(x, y, texel); + } + } +} + +/* ------------------------------------------------------------------ */ +/* Flat rectangle */ +/* ------------------------------------------------------------------ */ + +void sw_draw_flat_rect(int x, int y, int w, int h, uint16_t color) { + for (int row = 0; row < h; row++) { + int py = y + row; + if (py < g_clip_y1 || py > g_clip_y2) continue; + + int sx = max_i(x, g_clip_x1); + int ex = min_i(x + w - 1, g_clip_x2); + + for (int px = sx; px <= ex; px++) { + draw_pixel_opaque(px, py, color); + } + } +} + +/* ------------------------------------------------------------------ */ +/* Textured rectangle */ +/* ------------------------------------------------------------------ */ + +void sw_draw_textured_rect(int x, int y, int w, int h, + int u, int v, + uint16_t clut_x, uint16_t clut_y, + uint16_t texpage) { + for (int row = 0; row < h; row++) { + int py = y + row; + if (py < g_clip_y1 || py > g_clip_y2) continue; + + for (int col = 0; col < w; col++) { + int px = x + col; + if (px < g_clip_x1 || px > g_clip_x2) continue; + + int tu = (u + col) & 0xFF; + int tv = (v + row) & 0xFF; + uint16_t texel = texel_fetch(tu, tv, texpage, clut_x, clut_y); + draw_pixel_textured(px, py, texel); + } + } +} + +/* ------------------------------------------------------------------ */ +/* Line (Bresenham) */ +/* ------------------------------------------------------------------ */ + +void sw_draw_line(int x0, int y0, int x1, int y1, uint16_t color) { + int dx = abs(x1 - x0); + int dy = -abs(y1 - y0); + int sx = (x0 < x1) ? 1 : -1; + int sy = (y0 < y1) ? 1 : -1; + int err = dx + dy; + + for (;;) { + draw_pixel_opaque(x0, y0, color); + + if (x0 == x1 && y0 == y1) break; + + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } + } +} + +/* ------------------------------------------------------------------ */ +/* VRAM pixel access */ +/* ------------------------------------------------------------------ */ + +void sw_vram_write(int x, int y, uint16_t pixel) { + x &= (VRAM_WIDTH - 1); + y &= (VRAM_HEIGHT - 1); + g_vram[y * VRAM_WIDTH + x] = pixel; +} + +uint16_t sw_vram_read(int x, int y) { + return vram_get(x, y); +} + +/* ------------------------------------------------------------------ */ +/* Bulk VRAM transfers */ +/* ------------------------------------------------------------------ */ + +void sw_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) { + int idx = 0; + for (int row = 0; row < h; row++) { + int py = (y + row) & (VRAM_HEIGHT - 1); + for (int col = 0; col < w; col++) { + int px = (x + col) & (VRAM_WIDTH - 1); + g_vram[py * VRAM_WIDTH + px] = data[idx++]; + } + } +} + +void sw_vram_transfer_out(int x, int y, int w, int h, uint16_t *data) { + int idx = 0; + for (int row = 0; row < h; row++) { + int py = (y + row) & (VRAM_HEIGHT - 1); + for (int col = 0; col < w; col++) { + int px = (x + col) & (VRAM_WIDTH - 1); + data[idx++] = g_vram[py * VRAM_WIDTH + px]; + } + } +} + +/* ------------------------------------------------------------------ */ +/* Display output — convert 15-bit VRAM to 32-bit RGBA */ +/* ------------------------------------------------------------------ */ + +int sw_render_display(uint32_t *out_pixels, int out_pitch, + int disp_x, int disp_y, int disp_w, int disp_h) { + int count = 0; + + for (int row = 0; row < disp_h; row++) { + int vy = (disp_y + row) & (VRAM_HEIGHT - 1); + uint32_t *dst = (uint32_t *)((uint8_t *)out_pixels + row * out_pitch); + + for (int col = 0; col < disp_w; col++) { + int vx = (disp_x + col) & (VRAM_WIDTH - 1); + uint16_t pix = g_vram[vy * VRAM_WIDTH + vx]; + + int r5 = (pix >> 0) & 0x1F; + int g5 = (pix >> 5) & 0x1F; + int b5 = (pix >> 10) & 0x1F; + + uint8_t r = (uint8_t)((r5 << 3) | (r5 >> 2)); + uint8_t g = (uint8_t)((g5 << 3) | (g5 >> 2)); + uint8_t b = (uint8_t)((b5 << 3) | (b5 >> 2)); + + dst[col] = (uint32_t)r | + ((uint32_t)g << 8) | + ((uint32_t)b << 16) | + 0xFF000000u; + count++; + } + } + + return count; +} diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp new file mode 100644 index 00000000..f71cd7f8 --- /dev/null +++ b/runtime/src/gte.cpp @@ -0,0 +1,854 @@ +#include "gte.h" +#include "cpu_state.h" +#include +#include + +namespace PSXRecomp { +namespace GTE { + +// --------------------------------------------------------------------------- +// Common helpers +// --------------------------------------------------------------------------- + +// Perspective division: H * 0x20000 / SZ3, result capped at 0x1FFFF +static int32_t gte_divide(uint16_t H, uint16_t SZ3, uint32_t& FLAG) { + if (SZ3 == 0) { + FLAG |= FLAG_DIV_OVF; + return 0x1FFFF; + } + // (H * 0x20000 / SZ3 + 1) / 2 — correct PS1 formula with rounding + int64_t result = (((int64_t)H * 0x20000) / SZ3 + 1) / 2; + if (result > 0x1FFFF) { + FLAG |= FLAG_DIV_OVF; + return 0x1FFFF; + } + return static_cast(result); +} + +// Transform a vertex by Light matrix → IR1/IR2/IR3 (step 1 of lighting) +static void light_transform(GTEState* gte, int16_t* V) { + int64_t mac1 = ((int64_t)gte->L[0][0] * V[0] + + (int64_t)gte->L[0][1] * V[1] + + (int64_t)gte->L[0][2] * V[2]) >> 12; + int64_t mac2 = ((int64_t)gte->L[1][0] * V[0] + + (int64_t)gte->L[1][1] * V[1] + + (int64_t)gte->L[1][2] * V[2]) >> 12; + int64_t mac3 = ((int64_t)gte->L[2][0] * V[0] + + (int64_t)gte->L[2][1] * V[1] + + (int64_t)gte->L[2][2] * V[2]) >> 12; + gte->MAC1 = static_cast(mac1); + gte->MAC2 = static_cast(mac2); + gte->MAC3 = static_cast(mac3); + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true); +} + +// Apply light color matrix + background color to IR → IR (step 2 of lighting) +static void light_color(GTEState* gte) { + int64_t mac1 = ((int64_t)gte->BK[0] << 12) + + (int64_t)gte->LC[0][0] * gte->IR1 + + (int64_t)gte->LC[0][1] * gte->IR2 + + (int64_t)gte->LC[0][2] * gte->IR3; + int64_t mac2 = ((int64_t)gte->BK[1] << 12) + + (int64_t)gte->LC[1][0] * gte->IR1 + + (int64_t)gte->LC[1][1] * gte->IR2 + + (int64_t)gte->LC[1][2] * gte->IR3; + int64_t mac3 = ((int64_t)gte->BK[2] << 12) + + (int64_t)gte->LC[2][0] * gte->IR1 + + (int64_t)gte->LC[2][1] * gte->IR2 + + (int64_t)gte->LC[2][2] * gte->IR3; + gte->MAC1 = static_cast(mac1 >> 12); + gte->MAC2 = static_cast(mac2 >> 12); + gte->MAC3 = static_cast(mac3 >> 12); + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true); +} + +// Multiply IR by RGBC color and push to RGB FIFO (step 3 of color output) +static void color_output(GTEState* gte) { + uint8_t r0 = (gte->RGB[0] >> 0) & 0xFF; + uint8_t g0 = (gte->RGB[0] >> 8) & 0xFF; + uint8_t b0 = (gte->RGB[0] >> 16) & 0xFF; + int64_t mac1 = ((int64_t)r0 * gte->IR1) << 4; + int64_t mac2 = ((int64_t)g0 * gte->IR2) << 4; + int64_t mac3 = ((int64_t)b0 * gte->IR3) << 4; + gte->MAC1 = static_cast(mac1 >> 12); + gte->MAC2 = static_cast(mac2 >> 12); + gte->MAC3 = static_cast(mac3 >> 12); + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true); + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); +} + +// Depth cue from current IR values using IR0 interpolation toward far color +// MAC = FC * 0x1000 - (BK + LC*IR) * 0x1000 ; then IR = (BK+LC*IR) + IR0 * MAC / 0x1000 +static void depth_cue_from_ir(GTEState* gte) { + // Interpolate: result = IR + IR0 * (FC - IR) / 0x1000 + int64_t mac1 = ((int64_t)gte->FC[0] << 12) - ((int64_t)gte->IR1 << 12); + int64_t mac2 = ((int64_t)gte->FC[1] << 12) - ((int64_t)gte->IR2 << 12); + int64_t mac3 = ((int64_t)gte->FC[2] << 12) - ((int64_t)gte->IR3 << 12); + // Scale by IR0 and add back + mac1 = (int64_t)gte->IR1 + ((gte->IR0 * (mac1 >> 12)) >> 12); + mac2 = (int64_t)gte->IR2 + ((gte->IR0 * (mac2 >> 12)) >> 12); + mac3 = (int64_t)gte->IR3 + ((gte->IR0 * (mac3 >> 12)) >> 12); + gte->MAC1 = static_cast(mac1); + gte->MAC2 = static_cast(mac2); + gte->MAC3 = static_cast(mac3); + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true); +} + +// --------------------------------------------------------------------------- +// RTPS — Perspective Transformation (internal, operates on given vertex V) +// --------------------------------------------------------------------------- +void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0) { + // Step 1: Matrix multiplication + translation + int64_t mac1 = ((int64_t)gte->TR[0] << 12) + + (int64_t)gte->RT[0][0] * V[0] + + (int64_t)gte->RT[0][1] * V[1] + + (int64_t)gte->RT[0][2] * V[2]; + int64_t mac2 = ((int64_t)gte->TR[1] << 12) + + (int64_t)gte->RT[1][0] * V[0] + + (int64_t)gte->RT[1][1] * V[1] + + (int64_t)gte->RT[1][2] * V[2]; + int64_t mac3 = ((int64_t)gte->TR[2] << 12) + + (int64_t)gte->RT[2][0] * V[0] + + (int64_t)gte->RT[2][1] * V[1] + + (int64_t)gte->RT[2][2] * V[2]; + + gte->check_mac_overflow(mac1 >> 12, 1); + gte->check_mac_overflow(mac2 >> 12, 2); + gte->check_mac_overflow(mac3 >> 12, 3); + gte->MAC1 = static_cast(mac1 >> 12); + gte->MAC2 = static_cast(mac2 >> 12); + gte->MAC3 = static_cast(mac3 >> 12); + + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, false); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, false); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, false); + + // Step 2: Push SZ FIFO + gte->push_sz(gte->MAC3); + + // Step 3: Perspective division + int32_t h_div_sz = gte_divide(gte->H, gte->SZ[3], gte->FLAG); + + // Step 4: Project to screen coordinates + int64_t sx = (gte->OFX + (int64_t)gte->IR1 * h_div_sz) >> 16; + int64_t sy = (gte->OFY + (int64_t)gte->IR2 * h_div_sz) >> 16; + gte->push_sxy(sx, sy); + + // Step 5: Depth cueing (MAC0/IR0) — only for last vertex of RTPT or RTPS + if (setMac0) { + int64_t mac0 = (int64_t)gte->DQA * h_div_sz + gte->DQB; + gte->check_mac0_overflow(mac0); + gte->MAC0 = static_cast(mac0); + gte->IR0 = gte->saturate_ir0(static_cast(mac0 >> 12)); + } +} + +// RTPS (0x01) — single vertex, always sets MAC0 +void gte_rtps(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_rtps_internal(gte, gte->V0, true); + gte->set_error_flag(); +} + +// RTPT (0x30) — triple vertex, only last sets MAC0 +void gte_rtpt(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_rtps_internal(gte, gte->V0, false); + gte_rtps_internal(gte, gte->V1, false); + gte_rtps_internal(gte, gte->V2, true); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// NCLIP (0x06) — Normal Clipping (2D cross product for backface culling) +// --------------------------------------------------------------------------- +void gte_nclip(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + int16_t sx0 = static_cast(gte->SXY[0] & 0xFFFF); + int16_t sy0 = static_cast(gte->SXY[0] >> 16); + int16_t sx1 = static_cast(gte->SXY[1] & 0xFFFF); + int16_t sy1 = static_cast(gte->SXY[1] >> 16); + int16_t sx2 = static_cast(gte->SXY[2] & 0xFFFF); + int16_t sy2 = static_cast(gte->SXY[2] >> 16); + int64_t mac0 = (int64_t)sx0 * (sy1 - sy2) + + (int64_t)sx1 * (sy2 - sy0) + + (int64_t)sx2 * (sy0 - sy1); + gte->check_mac0_overflow(mac0); + gte->MAC0 = static_cast(mac0); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// AVSZ3 (0x2D) — Average Z (3 points) +// --------------------------------------------------------------------------- +void gte_avsz3(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + int64_t mac0 = (int64_t)gte->ZSF3 * (gte->SZ[1] + gte->SZ[2] + gte->SZ[3]); + gte->check_mac0_overflow(mac0); + gte->MAC0 = static_cast(mac0 >> 12); + gte->OTZ = gte->saturate_sz(gte->MAC0); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// AVSZ4 (0x2E) — Average Z (4 points) +// --------------------------------------------------------------------------- +void gte_avsz4(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + int64_t mac0 = (int64_t)gte->ZSF4 * (gte->SZ[0] + gte->SZ[1] + gte->SZ[2] + gte->SZ[3]); + gte->check_mac0_overflow(mac0); + gte->MAC0 = static_cast(mac0 >> 12); + gte->OTZ = gte->saturate_sz(gte->MAC0); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// NCCS (0x1B) — Normal Color Color Single +// --------------------------------------------------------------------------- +void gte_nccs_internal(GTEState* gte, int16_t* V) { + light_transform(gte, V); + light_color(gte); + color_output(gte); +} + +void gte_nccs(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_nccs_internal(gte, gte->V0); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// NCCT (0x3F) — Normal Color Color Triple +// --------------------------------------------------------------------------- +void gte_ncct(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_nccs_internal(gte, gte->V0); + gte_nccs_internal(gte, gte->V1); + gte_nccs_internal(gte, gte->V2); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// NCDS (0x13) — Normal Color Depth Cue Single +// --------------------------------------------------------------------------- +void gte_ncds_internal(GTEState* gte, int16_t* V) { + light_transform(gte, V); + light_color(gte); + depth_cue_from_ir(gte); + color_output(gte); +} + +void gte_ncds(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_ncds_internal(gte, gte->V0); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// NCDT (0x16) — Normal Color Depth Cue Triple +// --------------------------------------------------------------------------- +void gte_ncdt(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_ncds_internal(gte, gte->V0); + gte_ncds_internal(gte, gte->V1); + gte_ncds_internal(gte, gte->V2); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// NCS (0x1E) — Normal Color Single (no vertex color multiply) +// --------------------------------------------------------------------------- +void gte_ncs_internal(GTEState* gte, int16_t* V) { + light_transform(gte, V); + light_color(gte); + // Output directly (no RGBC multiply) + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); +} + +void gte_ncs(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_ncs_internal(gte, gte->V0); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// NCT (0x20) — Normal Color Triple +// --------------------------------------------------------------------------- +void gte_nct(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + gte_ncs_internal(gte, gte->V0); + gte_ncs_internal(gte, gte->V1); + gte_ncs_internal(gte, gte->V2); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// DPCS (0x10) — Depth Cueing Single (from RGBC color) +// --------------------------------------------------------------------------- +void gte_dpcs(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + uint8_t r = (gte->RGB[0] >> 0) & 0xFF; + uint8_t g = (gte->RGB[0] >> 8) & 0xFF; + uint8_t b = (gte->RGB[0] >> 16) & 0xFF; + // MAC = RGBC << 4 + gte->IR1 = gte->saturate_ir(r << 4, 1, false); + gte->IR2 = gte->saturate_ir(g << 4, 2, false); + gte->IR3 = gte->saturate_ir(b << 4, 3, false); + // Interpolate toward far color using IR0 + depth_cue_from_ir(gte); + // Output + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// DPCT (0x2A) — Depth Cueing Triple (from RGB FIFO entries) +// --------------------------------------------------------------------------- +void gte_dpct(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + for (int i = 0; i < 3; i++) { + uint8_t r = (gte->RGB[0] >> 0) & 0xFF; + uint8_t g = (gte->RGB[0] >> 8) & 0xFF; + uint8_t b = (gte->RGB[0] >> 16) & 0xFF; + gte->IR1 = gte->saturate_ir(r << 4, 1, false); + gte->IR2 = gte->saturate_ir(g << 4, 2, false); + gte->IR3 = gte->saturate_ir(b << 4, 3, false); + depth_cue_from_ir(gte); + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); + } + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// DPCL (0x29) — Depth Cueing Light (from IR, uses existing lighting result) +// --------------------------------------------------------------------------- +void gte_dpcl(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + // IR already contains lighting result, multiply by RGBC + uint8_t r = (gte->RGB[0] >> 0) & 0xFF; + uint8_t g = (gte->RGB[0] >> 8) & 0xFF; + uint8_t b = (gte->RGB[0] >> 16) & 0xFF; + gte->MAC1 = (r * gte->IR1) >> 8; + gte->MAC2 = (g * gte->IR2) >> 8; + gte->MAC3 = (b * gte->IR3) >> 8; + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, true); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, true); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, true); + // Depth cue toward far color + depth_cue_from_ir(gte); + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// INTPL (0x11) — Interpolation (IR toward far color using IR0) +// --------------------------------------------------------------------------- +void gte_intpl(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + depth_cue_from_ir(gte); + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// CDP (0x14) — Color Depth Cue (light color + depth cue, no normal transform) +// --------------------------------------------------------------------------- +void gte_cdp(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + // IR1/IR2/IR3 already set (from previous NCS or similar) + light_color(gte); + depth_cue_from_ir(gte); + color_output(gte); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// CC (0x1C) — Color Color (light color + vertex color, no depth cue) +// --------------------------------------------------------------------------- +void gte_cc(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + light_color(gte); + color_output(gte); + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// MVMVA (0x12) — Matrix Vector Multiply Add +// Highly configurable: matrix, vector, and translation selected by bits +// --------------------------------------------------------------------------- +void gte_mvmva(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + uint32_t mx = (instr >> 17) & 3; // Matrix: 0=RT, 1=Light, 2=LightColor, 3=reserved + uint32_t vv = (instr >> 15) & 3; // Vector: 0=V0, 1=V1, 2=V2, 3=IR + uint32_t tv = (instr >> 13) & 3; // Translation: 0=TR, 1=BK, 2=FC/bugged, 3=none + int sf = (instr >> 19) & 1; // Shift: 0=no shift, 1=shift right 12 + + // Select matrix + int16_t M[3][3]; + switch (mx) { + case 0: std::memcpy(M, gte->RT, sizeof(M)); break; + case 1: std::memcpy(M, gte->L, sizeof(M)); break; + case 2: std::memcpy(M, gte->LC, sizeof(M)); break; + default: std::memset(M, 0, sizeof(M)); break; // Garbage on real HW + } + + // Select vector + int16_t V[3]; + switch (vv) { + case 0: V[0] = gte->V0[0]; V[1] = gte->V0[1]; V[2] = gte->V0[2]; break; + case 1: V[0] = gte->V1[0]; V[1] = gte->V1[1]; V[2] = gte->V1[2]; break; + case 2: V[0] = gte->V2[0]; V[1] = gte->V2[1]; V[2] = gte->V2[2]; break; + case 3: V[0] = gte->IR1; V[1] = gte->IR2; V[2] = gte->IR3; break; + } + + // Select translation vector + int64_t T[3]; + switch (tv) { + case 0: T[0] = (int64_t)gte->TR[0] << 12; T[1] = (int64_t)gte->TR[1] << 12; T[2] = (int64_t)gte->TR[2] << 12; break; + case 1: T[0] = (int64_t)gte->BK[0] << 12; T[1] = (int64_t)gte->BK[1] << 12; T[2] = (int64_t)gte->BK[2] << 12; break; + case 2: T[0] = (int64_t)gte->FC[0] << 12; T[1] = (int64_t)gte->FC[1] << 12; T[2] = (int64_t)gte->FC[2] << 12; break; + case 3: T[0] = 0; T[1] = 0; T[2] = 0; break; + } + + // Multiply + int64_t mac1 = T[0] + (int64_t)M[0][0] * V[0] + (int64_t)M[0][1] * V[1] + (int64_t)M[0][2] * V[2]; + int64_t mac2 = T[1] + (int64_t)M[1][0] * V[0] + (int64_t)M[1][1] * V[1] + (int64_t)M[1][2] * V[2]; + int64_t mac3 = T[2] + (int64_t)M[2][0] * V[0] + (int64_t)M[2][1] * V[1] + (int64_t)M[2][2] * V[2]; + + if (sf) { + mac1 >>= 12; mac2 >>= 12; mac3 >>= 12; + } + + gte->check_mac_overflow(mac1, 1); + gte->check_mac_overflow(mac2, 2); + gte->check_mac_overflow(mac3, 3); + + gte->MAC1 = static_cast(mac1); + gte->MAC2 = static_cast(mac2); + gte->MAC3 = static_cast(mac3); + + bool lm = (instr >> 10) & 1; + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm); + + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// OP (0x0C) — Outer Product of two vectors (cross product) +// --------------------------------------------------------------------------- +void gte_op(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + int sf = (instr >> 19) & 1; + // D1=RT[0][0], D2=RT[1][1], D3=RT[2][2] (rotation matrix diagonal) + int16_t d1 = gte->RT[0][0]; + int16_t d2 = gte->RT[1][1]; + int16_t d3 = gte->RT[2][2]; + + int64_t mac1 = (int64_t)d2 * gte->IR3 - (int64_t)d3 * gte->IR2; + int64_t mac2 = (int64_t)d3 * gte->IR1 - (int64_t)d1 * gte->IR3; + int64_t mac3 = (int64_t)d1 * gte->IR2 - (int64_t)d2 * gte->IR1; + + if (sf) { mac1 >>= 12; mac2 >>= 12; mac3 >>= 12; } + + gte->MAC1 = static_cast(mac1); + gte->MAC2 = static_cast(mac2); + gte->MAC3 = static_cast(mac3); + + bool lm = (instr >> 10) & 1; + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm); + + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// SQR (0x28) — Square of IR vector +// --------------------------------------------------------------------------- +void gte_sqr(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + int sf = (instr >> 19) & 1; + + int64_t mac1 = (int64_t)gte->IR1 * gte->IR1; + int64_t mac2 = (int64_t)gte->IR2 * gte->IR2; + int64_t mac3 = (int64_t)gte->IR3 * gte->IR3; + + if (sf) { mac1 >>= 12; mac2 >>= 12; mac3 >>= 12; } + + gte->MAC1 = static_cast(mac1); + gte->MAC2 = static_cast(mac2); + gte->MAC3 = static_cast(mac3); + + bool lm = (instr >> 10) & 1; + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm); + + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// GPF (0x3D) — General Purpose Interpolation (IR0 * IR) +// --------------------------------------------------------------------------- +void gte_gpf(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + int sf = (instr >> 19) & 1; + + int64_t mac1 = (int64_t)gte->IR0 * gte->IR1; + int64_t mac2 = (int64_t)gte->IR0 * gte->IR2; + int64_t mac3 = (int64_t)gte->IR0 * gte->IR3; + + if (sf) { mac1 >>= 12; mac2 >>= 12; mac3 >>= 12; } + + gte->MAC1 = static_cast(mac1); + gte->MAC2 = static_cast(mac2); + gte->MAC3 = static_cast(mac3); + + bool lm = (instr >> 10) & 1; + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm); + + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); + + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// GPL (0x3E) — General Purpose Interpolation (MAC + IR0 * IR) +// --------------------------------------------------------------------------- +void gte_gpl(GTEState* gte, uint32_t instr) { + gte->FLAG = 0; + int sf = (instr >> 19) & 1; + + int64_t mac1 = ((int64_t)gte->MAC1 << (sf ? 12 : 0)) + (int64_t)gte->IR0 * gte->IR1; + int64_t mac2 = ((int64_t)gte->MAC2 << (sf ? 12 : 0)) + (int64_t)gte->IR0 * gte->IR2; + int64_t mac3 = ((int64_t)gte->MAC3 << (sf ? 12 : 0)) + (int64_t)gte->IR0 * gte->IR3; + + if (sf) { mac1 >>= 12; mac2 >>= 12; mac3 >>= 12; } + + gte->MAC1 = static_cast(mac1); + gte->MAC2 = static_cast(mac2); + gte->MAC3 = static_cast(mac3); + + bool lm = (instr >> 10) & 1; + gte->IR1 = gte->saturate_ir(gte->MAC1, 1, lm); + gte->IR2 = gte->saturate_ir(gte->MAC2, 2, lm); + gte->IR3 = gte->saturate_ir(gte->MAC3, 3, lm); + + gte->push_rgb( + gte->saturate_color(gte->MAC1 >> 4, 0), + gte->saturate_color(gte->MAC2 >> 4, 1), + gte->saturate_color(gte->MAC3 >> 4, 2)); + + gte->set_error_flag(); +} + +// --------------------------------------------------------------------------- +// MTC2 — Move To Coprocessor 2 (write GTE data register) +// --------------------------------------------------------------------------- +void gte_mtc2(GTEState* gte, uint8_t reg, uint32_t value) { + switch (reg) { + case 0: gte->V0[0] = value & 0xFFFF; gte->V0[1] = value >> 16; break; + case 1: gte->V0[2] = value & 0xFFFF; break; + case 2: gte->V1[0] = value & 0xFFFF; gte->V1[1] = value >> 16; break; + case 3: gte->V1[2] = value & 0xFFFF; break; + case 4: gte->V2[0] = value & 0xFFFF; gte->V2[1] = value >> 16; break; + case 5: gte->V2[2] = value & 0xFFFF; break; + case 6: gte->RGB[0] = value; break; + case 7: gte->OTZ = value & 0xFFFF; break; + case 8: gte->IR0 = static_cast(value & 0xFFFF); break; + case 9: gte->IR1 = static_cast(value & 0xFFFF); break; + case 10: gte->IR2 = static_cast(value & 0xFFFF); break; + case 11: gte->IR3 = static_cast(value & 0xFFFF); break; + case 12: gte->SXY[0] = value; break; + case 13: gte->SXY[1] = value; break; + case 14: gte->SXY[2] = value; gte->SXY[3] = value; break; + case 15: // SXYP — push to FIFO + gte->SXY[0] = gte->SXY[1]; gte->SXY[1] = gte->SXY[2]; + gte->SXY[2] = value; gte->SXY[3] = value; break; + case 16: gte->SZ[0] = value & 0xFFFF; break; + case 17: gte->SZ[1] = value & 0xFFFF; break; + case 18: gte->SZ[2] = value & 0xFFFF; break; + case 19: gte->SZ[3] = value & 0xFFFF; break; + case 20: gte->RGB[0] = value; break; + case 21: gte->RGB[1] = value; break; + case 22: gte->RGB[2] = value; break; + case 23: break; // RES1 reserved + case 24: gte->MAC0 = value; break; + case 25: gte->MAC1 = value; break; + case 26: gte->MAC2 = value; break; + case 27: gte->MAC3 = value; break; + case 28: // IRGB — packed 5-bit RGB → IR1/IR2/IR3 + gte->IR1 = (value & 0x1F) << 7; + gte->IR2 = ((value >> 5) & 0x1F) << 7; + gte->IR3 = ((value >> 10) & 0x1F) << 7; + break; + case 29: break; // ORGB read-only + case 30: // LZCS — set source and compute LZCR + gte->LZCS = static_cast(value); + { + uint32_t v = value; + if ((int32_t)value < 0) v = ~v; + int cnt = 0; + if (v == 0) { cnt = 32; } + else { while (!(v & 0x80000000u)) { v <<= 1; cnt++; } } + gte->LZCR = cnt; + } + break; + case 31: break; // LZCR read-only + default: break; + } +} + +// --------------------------------------------------------------------------- +// MFC2 — Move From Coprocessor 2 (read GTE data register) +// --------------------------------------------------------------------------- +uint32_t gte_mfc2(GTEState* gte, uint8_t reg) { + switch (reg) { + case 0: return (static_cast(gte->V0[1]) << 16) | static_cast(gte->V0[0]); + case 1: return static_cast(gte->V0[2]); + case 2: return (static_cast(gte->V1[1]) << 16) | static_cast(gte->V1[0]); + case 3: return static_cast(gte->V1[2]); + case 4: return (static_cast(gte->V2[1]) << 16) | static_cast(gte->V2[0]); + case 5: return static_cast(gte->V2[2]); + case 6: return gte->RGB[0]; + case 7: return gte->OTZ; + case 8: return static_cast(gte->IR0); // sign-extend + case 9: return static_cast(gte->IR1); + case 10: return static_cast(gte->IR2); + case 11: return static_cast(gte->IR3); + case 12: return gte->SXY[0]; + case 13: return gte->SXY[1]; + case 14: return gte->SXY[2]; + case 15: return gte->SXY[3]; + case 16: return gte->SZ[0]; + case 17: return gte->SZ[1]; + case 18: return gte->SZ[2]; + case 19: return gte->SZ[3]; + case 20: return gte->RGB[0]; + case 21: return gte->RGB[1]; + case 22: return gte->RGB[2]; + case 23: return 0; // RES1 + case 24: return gte->MAC0; + case 25: return gte->MAC1; + case 26: return gte->MAC2; + case 27: return gte->MAC3; + case 28: case 29: { // IRGB/ORGB + uint32_t r = std::clamp((int)(gte->IR1 >> 7), 0, 0x1F); + uint32_t g = std::clamp((int)(gte->IR2 >> 7), 0, 0x1F); + uint32_t b = std::clamp((int)(gte->IR3 >> 7), 0, 0x1F); + return (b << 10) | (g << 5) | r; + } + case 30: return gte->LZCS; + case 31: return gte->LZCR; + default: return 0; + } +} + +// --------------------------------------------------------------------------- +// CTC2 — Move To Coprocessor 2 Control (write GTE control register) +// --------------------------------------------------------------------------- +void gte_ctc2(GTEState* gte, uint8_t reg, uint32_t value) { + switch (reg) { + case 0: gte->RT[0][0] = value & 0xFFFF; gte->RT[0][1] = value >> 16; break; + case 1: gte->RT[0][2] = value & 0xFFFF; gte->RT[1][0] = value >> 16; break; + case 2: gte->RT[1][1] = value & 0xFFFF; gte->RT[1][2] = value >> 16; break; + case 3: gte->RT[2][0] = value & 0xFFFF; gte->RT[2][1] = value >> 16; break; + case 4: gte->RT[2][2] = value & 0xFFFF; break; + case 5: gte->TR[0] = value; break; + case 6: gte->TR[1] = value; break; + case 7: gte->TR[2] = value; break; + case 8: gte->L[0][0] = value & 0xFFFF; gte->L[0][1] = value >> 16; break; + case 9: gte->L[0][2] = value & 0xFFFF; gte->L[1][0] = value >> 16; break; + case 10: gte->L[1][1] = value & 0xFFFF; gte->L[1][2] = value >> 16; break; + case 11: gte->L[2][0] = value & 0xFFFF; gte->L[2][1] = value >> 16; break; + case 12: gte->L[2][2] = value & 0xFFFF; break; + case 13: gte->BK[0] = value; break; + case 14: gte->BK[1] = value; break; + case 15: gte->BK[2] = value; break; + case 16: gte->LC[0][0] = value & 0xFFFF; gte->LC[0][1] = value >> 16; break; + case 17: gte->LC[0][2] = value & 0xFFFF; gte->LC[1][0] = value >> 16; break; + case 18: gte->LC[1][1] = value & 0xFFFF; gte->LC[1][2] = value >> 16; break; + case 19: gte->LC[2][0] = value & 0xFFFF; gte->LC[2][1] = value >> 16; break; + case 20: gte->LC[2][2] = value & 0xFFFF; break; + case 21: gte->FC[0] = value; break; + case 22: gte->FC[1] = value; break; + case 23: gte->FC[2] = value; break; + case 24: gte->OFX = value; break; + case 25: gte->OFY = value; break; + case 26: gte->H = value & 0xFFFF; break; + case 27: gte->DQA = static_cast(value & 0xFFFF); break; + case 28: gte->DQB = value; break; + case 29: gte->ZSF3 = static_cast(value & 0xFFFF); break; + case 30: gte->ZSF4 = static_cast(value & 0xFFFF); break; + case 31: gte->FLAG = value & 0x7FFFF000u; break; + default: break; + } +} + +// --------------------------------------------------------------------------- +// CFC2 — Move From Coprocessor 2 Control (read GTE control register) +// --------------------------------------------------------------------------- +uint32_t gte_cfc2(GTEState* gte, uint8_t reg) { + switch (reg) { + case 0: return (static_cast(gte->RT[0][1]) << 16) | static_cast(gte->RT[0][0]); + case 1: return (static_cast(gte->RT[1][0]) << 16) | static_cast(gte->RT[0][2]); + case 2: return (static_cast(gte->RT[1][2]) << 16) | static_cast(gte->RT[1][1]); + case 3: return (static_cast(gte->RT[2][1]) << 16) | static_cast(gte->RT[2][0]); + case 4: return static_cast(gte->RT[2][2]); + case 5: return gte->TR[0]; + case 6: return gte->TR[1]; + case 7: return gte->TR[2]; + case 8: return (static_cast(gte->L[0][1]) << 16) | static_cast(gte->L[0][0]); + case 9: return (static_cast(gte->L[1][0]) << 16) | static_cast(gte->L[0][2]); + case 10: return (static_cast(gte->L[1][2]) << 16) | static_cast(gte->L[1][1]); + case 11: return (static_cast(gte->L[2][1]) << 16) | static_cast(gte->L[2][0]); + case 12: return static_cast(gte->L[2][2]); + case 13: return gte->BK[0]; + case 14: return gte->BK[1]; + case 15: return gte->BK[2]; + case 16: return (static_cast(gte->LC[0][1]) << 16) | static_cast(gte->LC[0][0]); + case 17: return (static_cast(gte->LC[1][0]) << 16) | static_cast(gte->LC[0][2]); + case 18: return (static_cast(gte->LC[1][2]) << 16) | static_cast(gte->LC[1][1]); + case 19: return (static_cast(gte->LC[2][1]) << 16) | static_cast(gte->LC[2][0]); + case 20: return static_cast(gte->LC[2][2]); + case 21: return gte->FC[0]; + case 22: return gte->FC[1]; + case 23: return gte->FC[2]; + case 24: return gte->OFX; + case 25: return gte->OFY; + case 26: return gte->H; + case 27: return static_cast(gte->DQA); // sign-extend + case 28: return gte->DQB; + case 29: return static_cast(gte->ZSF3); + case 30: return static_cast(gte->ZSF4); + case 31: return gte->FLAG; + default: return 0; + } +} + +} // namespace GTE +} // namespace PSXRecomp + +// --------------------------------------------------------------------------- +// gte_execute — C-linkage bridge called from generated code +// --------------------------------------------------------------------------- +extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) { + using namespace PSXRecomp::GTE; + + GTEState gte; + // Skip reg 15 (SXYP: push-write, would corrupt SXY FIFO) and + // reg 28 (IRGB: overwrites IR1/2/3 with lossy 5-bit values; use regs 9-11 instead) + for (int i = 0; i < 32; i++) { + if (i == 15 || i == 28) continue; + gte_mtc2(>e, i, cpu->gte_data[i]); + } + for (int i = 0; i < 32; i++) gte_ctc2(>e, i, cpu->gte_ctrl[i]); + + uint8_t func = cmd & 0x3F; + switch (func) { + case 0x01: gte_rtps(>e, cmd); break; + case 0x06: gte_nclip(>e, cmd); break; + case 0x0C: gte_op(>e, cmd); break; + case 0x10: gte_dpcs(>e, cmd); break; + case 0x11: gte_intpl(>e, cmd); break; + case 0x12: gte_mvmva(>e, cmd); break; + case 0x13: gte_ncds(>e, cmd); break; + case 0x14: gte_cdp(>e, cmd); break; + case 0x16: gte_ncdt(>e, cmd); break; + case 0x1B: gte_nccs(>e, cmd); break; + case 0x1C: gte_cc(>e, cmd); break; + case 0x1E: gte_ncs(>e, cmd); break; + case 0x20: gte_nct(>e, cmd); break; + case 0x28: gte_sqr(>e, cmd); break; + case 0x29: gte_dpcl(>e, cmd); break; + case 0x2A: gte_dpct(>e, cmd); break; + case 0x2D: gte_avsz3(>e, cmd); break; + case 0x2E: gte_avsz4(>e, cmd); break; + case 0x30: gte_rtpt(>e, cmd); break; + case 0x3D: gte_gpf(>e, cmd); break; + case 0x3E: gte_gpl(>e, cmd); break; + case 0x3F: gte_ncct(>e, cmd); break; + default: + exit(1); + break; + } + + for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(>e, i); + for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(>e, i); +} + +/* C-callable wrappers for GTE register transfers */ +// Helper: load GTEState from CPUState, skipping push-write and lossy registers +static void gte_load_data(PSXRecomp::GTE::GTEState& gte, CPUState* cpu) { + using namespace PSXRecomp::GTE; + for (int i = 0; i < 32; i++) { + if (i == 15 || i == 28) continue; // SXYP (push) and IRGB (lossy overwrite) + gte_mtc2(>e, i, cpu->gte_data[i]); + } + for (int i = 0; i < 32; i++) gte_ctc2(>e, i, cpu->gte_ctrl[i]); +} + +extern "C" uint32_t gte_read_data(CPUState* cpu, uint8_t reg) { + using namespace PSXRecomp::GTE; + GTEState gte; + gte_load_data(gte, cpu); + return gte_mfc2(>e, reg); +} + +extern "C" uint32_t gte_read_ctrl(CPUState* cpu, uint8_t reg) { + using namespace PSXRecomp::GTE; + GTEState gte; + gte_load_data(gte, cpu); + return gte_cfc2(>e, reg); +} + +extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { + using namespace PSXRecomp::GTE; + GTEState gte; + gte_load_data(gte, cpu); + gte_mtc2(>e, reg, val); + for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(>e, i); + for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(>e, i); +} + +extern "C" void gte_write_ctrl(CPUState* cpu, uint8_t reg, uint32_t val) { + using namespace PSXRecomp::GTE; + GTEState gte; + gte_load_data(gte, cpu); + gte_ctc2(>e, reg, val); + for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(>e, i); + for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(>e, i); +} diff --git a/runtime/src/iso_reader.cpp b/runtime/src/iso_reader.cpp new file mode 100644 index 00000000..9d1447a9 --- /dev/null +++ b/runtime/src/iso_reader.cpp @@ -0,0 +1,459 @@ +#include "iso_reader.h" +#include +#include +#include + +namespace PS1 { + +// PS1 CD-ROM sector size (Mode 2, Form 1 user data) +constexpr size_t SECTOR_SIZE = 2048; + +// Full sector size including headers/subchannel (2352 bytes for raw BIN files) +constexpr size_t RAW_SECTOR_SIZE = 2352; + +// Offset to user data in raw sector (Mode 2, Form 1) +constexpr size_t RAW_DATA_OFFSET = 24; + +// Primary Volume Descriptor location +constexpr uint32_t PVD_SECTOR = 16; + +ISOReader::ISOReader() + : is_open_(false) { + root_dir_.lba = 0; + root_dir_.size = 0; +} + +ISOReader::~ISOReader() { + Close(); +} + +bool ISOReader::Open(const std::string& filename) { + // Close any previously opened file + Close(); + + // Check if file exists + if (!std::filesystem::exists(filename)) { + return false; + } + + // Handle .cue files - parse to find .bin file + std::string bin_filename = filename; + auto ends_with = [](const std::string& s, const std::string& suffix) { + return s.size() >= suffix.size() && + s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; + }; + if (ends_with(filename, ".cue") || ends_with(filename, ".CUE")) { + // Simple .cue parsing - look for FILE "..." BINARY line + std::ifstream cue_file(filename); + if (!cue_file.is_open()) { + return false; + } + + std::string line; + while (std::getline(cue_file, line)) { + // Look for: FILE "filename.bin" BINARY + size_t file_pos = line.find("FILE"); + size_t binary_pos = line.find("BINARY"); + + if (file_pos != std::string::npos && binary_pos != std::string::npos) { + // Extract filename between quotes + size_t quote1 = line.find('"', file_pos); + size_t quote2 = line.find('"', quote1 + 1); + + if (quote1 != std::string::npos && quote2 != std::string::npos) { + std::string bin_name = line.substr(quote1 + 1, quote2 - quote1 - 1); + + // If it's a relative path, combine with .cue directory + std::filesystem::path cue_path(filename); + std::filesystem::path bin_path(bin_name); + + if (bin_path.is_relative()) { + bin_filename = (cue_path.parent_path() / bin_name).string(); + } else { + bin_filename = bin_name; + } + break; + } + } + } + cue_file.close(); + } + + // Store the resolved bin path for callers (e.g. xa_audio) + bin_path_ = bin_filename; + + // Open the ISO/BIN file in binary mode + file_.open(bin_filename, std::ios::binary); + if (!file_.is_open()) { + return false; + } + + is_open_ = true; + + // Parse the volume descriptor to extract filesystem metadata + if (!ParseVolumeDescriptor()) { + Close(); + return false; + } + + return true; +} + +void ISOReader::Close() { + if (file_.is_open()) { + file_.close(); + } + is_open_ = false; +} + +bool ISOReader::ReadSector(uint32_t lba, uint8_t* buffer) { + if (!is_open_ || !buffer) { + return false; + } + + // Clear any error flags + file_.clear(); + + // Determine sector format by checking file size + file_.seekg(0, std::ios::end); + std::streampos file_size = file_.tellg(); + + // Clear error flags after seeking + file_.clear(); + + // Check if file uses raw sectors (2352 bytes) or cooked sectors (2048 bytes) + bool is_raw_format = (file_size % RAW_SECTOR_SIZE == 0); + + if (is_raw_format) { + // Raw BIN format - read full sector, extract user data + std::streampos offset = static_cast(lba) * RAW_SECTOR_SIZE + RAW_DATA_OFFSET; + file_.seekg(offset, std::ios::beg); + + if (!file_.good()) { + file_.clear(); + return false; + } + + file_.read(reinterpret_cast(buffer), SECTOR_SIZE); + std::streamsize bytes_read = file_.gcount(); + bool success = (bytes_read == SECTOR_SIZE); + file_.clear(); + return success; + } else { + // ISO format - sectors are already 2048 bytes + std::streampos offset = static_cast(lba) * SECTOR_SIZE; + file_.seekg(offset, std::ios::beg); + + if (!file_.good()) { + file_.clear(); + return false; + } + + file_.read(reinterpret_cast(buffer), SECTOR_SIZE); + std::streamsize bytes_read = file_.gcount(); + bool success = (bytes_read == SECTOR_SIZE); + file_.clear(); + return success; + } +} + +bool ISOReader::IsOpen() const { + return is_open_; +} + +std::string ISOReader::GetVolumeID() const { + return volume_id_; +} + +std::string ISOReader::GetBinPath() const { + return bin_path_; +} + +RootDirectoryInfo ISOReader::GetRootDirectory() const { + return root_dir_; +} + +uint32_t ISOReader::Read733(const uint8_t* data) const { + // Read little-endian half of both-endian 32-bit value + return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); +} + +bool ISOReader::ParseVolumeDescriptor() { + // Read Primary Volume Descriptor from sector 16 + uint8_t pvd[SECTOR_SIZE]; + if (!ReadSector(PVD_SECTOR, pvd)) { + return false; + } + + // Verify ISO9660 signature: offset 1 should contain "CD001" + if (pvd[0] != 0x01 || std::memcmp(&pvd[1], "CD001", 5) != 0) { + return false; // Not a valid ISO9660 disc + } + + // Extract volume ID (offset 40, 32 bytes, space-padded ASCII) + volume_id_.clear(); + for (int i = 0; i < 32; i++) { + char c = pvd[40 + i]; + if (c != ' ' && c != '\0') { + volume_id_ += c; + } + } + + // Extract root directory record (offset 156, 34 bytes) + const uint8_t* root_record = &pvd[156]; + + // Root directory LBA is at offset 2 within the directory record (both-endian 32-bit) + root_dir_.lba = Read733(&root_record[2]); + + // Root directory size is at offset 10 within the directory record (both-endian 32-bit) + root_dir_.size = Read733(&root_record[10]); + + return true; +} + +bool ISOReader::ParseDirectoryRecord(const uint8_t* data, ISOFileEntry& entry) const { + // Check record length (offset 0) + uint8_t record_len = data[0]; + if (record_len == 0 || record_len < 33) { + return false; // Invalid or padding record + } + + // Extract LBA (offset 2, both-endian 32-bit) + entry.lba = Read733(&data[2]); + + // Extract file size (offset 10, both-endian 32-bit) + entry.size = Read733(&data[10]); + + // Extract file flags (offset 25) + uint8_t flags = data[25]; + entry.is_directory = (flags & 0x02) != 0; + + // Extract filename length (offset 32) + uint8_t name_len = data[32]; + if (name_len == 0) { + return false; // Invalid record + } + + // Extract filename (offset 33) + const char* name_ptr = reinterpret_cast(&data[33]); + + // Handle special directory entries + if (name_len == 1 && name_ptr[0] == '\x00') { + entry.name = "."; // Current directory + return true; + } + if (name_len == 1 && name_ptr[0] == '\x01') { + entry.name = ".."; // Parent directory + return true; + } + + // Parse regular filename, strip version suffix (";1") + entry.name.clear(); + for (uint8_t i = 0; i < name_len; i++) { + char c = name_ptr[i]; + if (c == ';') { + break; // Stop at version separator + } + entry.name += c; + } + + return true; +} + +std::vector ISOReader::ListFilesByLBA(uint32_t lba, uint32_t dir_size) { + std::vector results; + + if (!is_open_ || lba == 0 || dir_size == 0) { + return results; + } + + // Calculate number of sectors needed for directory data + uint32_t num_sectors = (dir_size + 2047) / 2048; + + // Allocate buffer for directory data + std::vector dir_data(num_sectors * 2048, 0); + + // Read all directory sectors + for (uint32_t i = 0; i < num_sectors; i++) { + if (!ReadSector(lba + i, &dir_data[i * 2048])) { + return results; // Error reading sector + } + } + + // Parse directory records + uint32_t offset = 0; + while (offset < dir_size) { + uint8_t record_len = dir_data[offset]; + + if (record_len == 0) { + // Skip to next sector boundary + uint32_t sector_offset = offset % 2048; + if (sector_offset != 0) { + offset += (2048 - sector_offset); + continue; + } + break; + } + + if (offset + record_len > dir_data.size()) break; + + ISOFileEntry entry; + if (ParseDirectoryRecord(&dir_data[offset], entry)) { + if (entry.name != "." && entry.name != "..") { + results.push_back(entry); + } + } + + offset += record_len; + } + + return results; +} + +std::vector ISOReader::ListFiles(const std::string& path) { + std::vector results; + + // Check if file is open + if (!is_open_) { + return results; + } + + if (path.empty()) { + // List root directory + RootDirectoryInfo root = GetRootDirectory(); + return ListFilesByLBA(root.lba, root.size); + } + + // Non-empty path: navigate to that subdirectory within the root + // Find the matching directory entry in root + RootDirectoryInfo root = GetRootDirectory(); + std::vector root_entries = ListFilesByLBA(root.lba, root.size); + + std::string path_upper = path; + std::transform(path_upper.begin(), path_upper.end(), path_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + + for (const auto& e : root_entries) { + if (!e.is_directory) continue; + std::string name_upper = e.name; + std::transform(name_upper.begin(), name_upper.end(), name_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + if (name_upper == path_upper) { + // Found the subdirectory — list its contents + return ListFilesByLBA(e.lba, e.size > 0 ? e.size : 2048); + } + } + + return results; // Directory not found +} + +bool ISOReader::FindFile(const std::string& path, ISOFileEntry& entry) { + // Check if file is open + if (!is_open_) { + return false; + } + + // Check if path contains a directory separator + size_t sep = path.find('/'); + if (sep == std::string::npos) { + sep = path.find('\\'); + } + + if (sep != std::string::npos) { + // Subdirectory path: "DIR/FILE" or "DIR\FILE" + std::string dir_name = path.substr(0, sep); + std::string file_name = path.substr(sep + 1); + + // List the subdirectory + std::vector sub_files = ListFiles(dir_name); + + std::string file_upper = file_name; + std::transform(file_upper.begin(), file_upper.end(), file_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + + for (const auto& f : sub_files) { + std::string name_upper = f.name; + std::transform(name_upper.begin(), name_upper.end(), name_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + if (name_upper == file_upper) { + entry = f; + return true; + } + } + return false; + } + + // Root-level file: search root directory + std::vector files = ListFiles(""); + + // Search for matching filename (case-insensitive comparison) + for (const auto& file : files) { + std::string file_upper = file.name; + std::string path_upper = path; + + std::transform(file_upper.begin(), file_upper.end(), file_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + std::transform(path_upper.begin(), path_upper.end(), path_upper.begin(), + [](unsigned char c) { return std::toupper(c); }); + + if (file_upper == path_upper) { + entry = file; + return true; + } + } + + // File not found + return false; +} + +size_t ISOReader::ReadFile(const std::string& path, uint8_t* buffer, size_t max_size) { + // Validate buffer pointer + if (!buffer) { + return 0; + } + + // Find the file + ISOFileEntry entry; + if (!FindFile(path, entry)) { + return 0; // File not found + } + + // Calculate how many bytes to read (min of file size and max_size) + size_t bytes_to_read = std::min(static_cast(entry.size), max_size); + + // Calculate number of sectors to read + uint32_t sectors_to_read = (bytes_to_read + SECTOR_SIZE - 1) / SECTOR_SIZE; + + // Read sectors sequentially + size_t bytes_read = 0; + for (uint32_t i = 0; i < sectors_to_read; i++) { + // Calculate how many bytes to read from this sector + size_t bytes_remaining = bytes_to_read - bytes_read; + size_t sector_bytes = std::min(bytes_remaining, SECTOR_SIZE); + + // Read sector into temporary buffer + uint8_t sector_buffer[SECTOR_SIZE]; + if (!ReadSector(entry.lba + i, sector_buffer)) { + return bytes_read; // Error - return what we've read so far + } + + // Copy data to output buffer + std::memcpy(buffer + bytes_read, sector_buffer, sector_bytes); + bytes_read += sector_bytes; + } + + return bytes_read; +} + +size_t ISOReader::GetFileSize(const std::string& path) { + // Find the file + ISOFileEntry entry; + if (!FindFile(path, entry)) { + return 0; // File not found + } + + // Return file size + return entry.size; +} + +} // namespace PS1 diff --git a/runtime/src/iso_reader_c.cpp b/runtime/src/iso_reader_c.cpp new file mode 100644 index 00000000..eec386a6 --- /dev/null +++ b/runtime/src/iso_reader_c.cpp @@ -0,0 +1,35 @@ +/* + * iso_reader_c.cpp — C wrapper for the C++ ISOReader class + * + * Provides iso_open() / iso_read_sector() / iso_close() for use by cdrom.c + */ + +#include "iso_reader.h" +#include + +extern "C" { + +void* iso_open(const char* path) { + auto* reader = new PS1::ISOReader(); + if (!reader->Open(path)) { + delete reader; + return nullptr; + } + return reader; +} + +int iso_read_sector(void* handle, uint32_t lba, uint8_t* buffer, int size) { + if (!handle) return 0; + auto* reader = static_cast(handle); + (void)size; /* ReadSector always reads 2048 bytes */ + return reader->ReadSector(lba, buffer) ? 1 : 0; +} + +void iso_close(void* handle) { + if (!handle) return; + auto* reader = static_cast(handle); + reader->Close(); + delete reader; +} + +} /* extern "C" */