Merge validated renderer fixes into generic post-effect pipeline

This commit is contained in:
Matthew Stanley
2026-10-13 10:18:03 -07:00
40 changed files with 1712 additions and 684 deletions
+14
View File
@@ -443,6 +443,20 @@ id = "r4.widescreen"
netplay = "local_view"
```
Two more plugin classes stay on per player. `netplay = "input"` transforms
only this player's pad before it is staged (`psx_mod_set_pad_transform`).
`netplay = "host_output"` only reads guest state to drive this player's host
output (rumble, haptics): its function-entry hooks run in the shared
simulation (including rollback resimulation) on a private copy of the CPU
state, whose register changes are discarded; a completion request from them
is ignored; and every guest-mutating mod service (`psx_mod_write_*`,
`psx_mod_host_write_ram`, `psx_mod_call_guest*`, DMA / SPU / VRAM uploads,
guest allocations, the cycle-scale gate) refuses while one runs
(`psx_mod_host_output_scope()`), so the shared simulation is unchanged by
them. This is a determinism contract for trusted plugins, not a sandbox. Host-side calls that must not
repeat on a resim (e.g. `psx_mod_set_host_rumble`) check
`psx_netplay_is_resimulating()` themselves.
At a netplay session start the runtime resolves the player's own selection
and keeps only features whose every contribution is such a plugin (no EXE or
disc write, overlay or derived disc; `mod_runtime_commit_netplay_view`). They
+2
View File
@@ -63,6 +63,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `fmv_chroma` | ✓ | | `on` (optional: 0/1) | Read or set `[video] fmv_chroma_smoothing` live. Replies `on` and `frames` (24-bit presents smoothed so far) |
| `texture_lod` | ✓ | | `mode` (optional: 0 off, 1 mipmap), `aniso` (optional: 1..16) | Read or set `[video] texture_lod` / `anisotropic_filtering` live. Replies `mode`, `aniso` |
| `bloom` | ✓ | | `strength` (0..2), `threshold`, `time` (0/1), all optional | Read or set bloom live (`[video] bloom`). Replies the settings, `passes` and, while `time` is on, `us_mean`: the mean `glFinish`-fenced pass time over `timed` passes since the last query |
| `dithering` | ✓ | | `mode` (optional: 0 off, 1 on (default, native grid), 2 fine (internal pixels)) | Read or set `[video] dithering` live. Replies `mode` |
| `post_aa` | ✓ | | `mode` (optional: 0 off, 1 fxaa, 2 fxaa_hq) | Read or set post-process anti-aliasing live (`[video] antialiasing_mode`). Replies `mode`, `passes` (filtered presents so far) and `gpu_us`: the mean pass time since the last query when the run has `PSX_POST_AA_TIME=N` (N passes per present between `glFinish` fences, a measurement mode), else 0 |
| `hd_textures` | ✓ | | optional `replacements`, `dump` (0 or 1), `reload` (1) | HD pack root, switches, backend support, replacement count, matched/ready/applied draw counts, queued/pending dumps, composition hits/builds/pixels, GL uploads/evictions, decoded images/evictions/bytes. Optional controls update the configured host pack on the emulation thread; no configured root is an error. Reload retains unchanged upload identities. OpenGL displays replacements; software/Vulkan retain original artwork. Pair with `present_shot` for visual evidence and native VRAM probes for architectural data |
| `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) |
@@ -581,6 +582,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `display_ring_aux` | ✓ | | |
| `display_ring_get` | ✓ | | |
| `display_ring_stats` | ✓ | | |
| `dithering` | ✓ | | ✓ |
| `dma_cdrom_history` | ✓ | | |
| `dma_hold_ring` | ✓ | | ✓ |
| `dma_hold_stats` | ✓ | | ✓ |
+30
View File
@@ -754,6 +754,36 @@ first layer's full 8-bit result where the console would have truncated it to
5 bits first (at most one 5-bit step). OpenGL only.
`PSX_ACCURATE_BLENDING=0|1` overrides it for one run and the
`accurate_blending` TCP command switches it live.
### Dithering (`dithering`)
```toml
[video]
dithering = "on" # on (default) | fine | off
```
The PS1 GPU dithers while the game sets GP0(E1h) bit 9: it adds a 4x4
offset pattern to every pixel of a Gouraud-shaded polygon or line and of a
texture-modulated polygon, then truncates to 5 bits per channel. Flat
untextured primitives, raw-texture polygons and all rectangles (sprites) are
never dithered. Eligibility is decided per primitive from its command and
carried to the shader, never guessed from colours.
`on` (the default; `scaled` is accepted as an alias) is that hardware
behaviour with the pattern on the native VRAM grid, so it looks like the
console at any internal scale and the VRAM a game reads back is identical at
every scale. `fine` is elective: the pattern per internal-resolution pixel, a
grain that all but vanishes at high scales (guest VRAM then depends on the
scale). `off` is the old true-colour output (8 bits per channel, no pattern).
Semi-transparent primitives are an approximation: the pattern is applied to
the source colour before blending, while the console dithers the blended
result at its final 8-to-5-bit write. Fixed-function GL blending cannot run
a shader after the blend (GL 3.3 has no framebuffer fetch, and macOS offers
none), so a dithered semi-transparent pixel can differ from the console by
one 5-bit step; opaque pixels match exactly. The dither output follows the
surface colour basis, so it composes with `accurate_blending`. OpenGL only;
the software and Vulkan renderers do not dither. `PSX_DITHERING=0|1|2`
overrides it for one run and the `dithering` TCP command switches it live.
### Texture filtering (`texture_filtering`)
+1 -1
View File
@@ -1 +1 @@
be595cf0213678b41119568563c77352c0e69862cec44def5f75e13605c02519
77a1ea51cf0498290de3f78b4fb42ed51640dcee62b379ff44cde8b47cc5714e
+8
View File
@@ -855,6 +855,14 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
}
if (video.contains("accurate_blending"))
rt.video_accurate_blending = toml::find<bool>(video, "accurate_blending");
if (video.contains("dithering")) {
const auto mode = toml::find<std::string>(video, "dithering");
if (mode == "off") rt.video_dithering = 0;
else if (mode == "on" || mode == "scaled") rt.video_dithering = 1;
else if (mode == "fine") rt.video_dithering = 2;
else throw std::runtime_error(fmt::format(
"[video] dithering must be \"on\", \"fine\" or \"off\": {}", mode));
}
if (video.contains("pgxp_depth_buffer"))
rt.video_pgxp_depth_buffer = toml::find<bool>(video, "pgxp_depth_buffer");
if (video.contains("pgxp_color_correction"))
+6
View File
@@ -586,6 +586,12 @@ struct RuntimeConfig {
// semi-transparency truncates exactly like the PS1's 5-bit blend.
// Default true: PS1 colour/blend correctness. OpenGL only.
bool video_accurate_blending = true;
// dithering: PS1 dithering of Gouraud-shaded and texture-modulated
// polygons while the guest sets GP0(E1h) bit 9 (hardware behaviour).
// 1 "on" (default; "scaled" is an alias) = pattern on the native VRAM
// grid, identical guest VRAM at every scale; 2 "fine" = elective pattern
// per internal-resolution pixel; 0 "off" = true colour. OpenGL only.
int video_dithering = 1;
bool video_pgxp_color_correction = false;
int video_pgxp_seam = 0;
double video_pgxp_depth_threshold = 4096.0;
+25 -1
View File
@@ -364,6 +364,19 @@ if(BUILD_TESTING)
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-xbr-results")
set_tests_properties(gl_xbr_filter_test PROPERTIES
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
# [video] dithering: off ignores the guest's dither bit (byte-identical
# true colour); on/scaled add the PS1 pattern to shaded/modulated draws
# only, through the facade, the render thread, PGXP depth and wide.
add_test(NAME gl_dithering_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" --single
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_dithering.c"
--cc "${CMAKE_C_COMPILER}"
--sdl-include "${_gl_scale_sdl_inc}"
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-dithering-results")
set_tests_properties(gl_dithering_test PROPERTIES
LABELS "gpu;opengl;hardware" TIMEOUT 180 SKIP_RETURN_CODE 77)
unset(_gl_scale_sdl_inc)
else()
message(STATUS "gl_scale_invariance_test: not registered (needs macOS, Linux or MinGW, Python 3 and SDL3)")
@@ -1146,6 +1159,16 @@ if(BUILD_TESTING)
target_link_libraries(iso_reader_pregap_test PRIVATE chdr-static)
add_test(NAME iso_reader_pregap_test COMMAND iso_reader_pregap_test)
# Raw vs cooked geometry comes from the cue TRACK mode / sector-0 sync,
# never from file length.
add_executable(iso_reader_geometry_test
tests/test_iso_reader_geometry.cpp
src/iso_reader.cpp
src/cue_sheet.cpp)
target_include_directories(iso_reader_geometry_test PRIVATE include ${PSXRECOMP_ROOT}/recompiler/include)
target_link_libraries(iso_reader_geometry_test PRIVATE chdr-static)
add_test(NAME iso_reader_geometry_test COMMAND iso_reader_geometry_test)
add_executable(iso_reader_sbi_test
tests/test_iso_reader_sbi.cpp
src/iso_reader.cpp
@@ -1263,7 +1286,8 @@ if(BUILD_TESTING)
src/cue_sheet.cpp
src/iso_reader.cpp
src/crc32.c
src/psx_sha256.c)
src/psx_sha256.c
src/sio.c)
target_include_directories(netplay_view_mods_test PRIVATE
include
../recompiler/include
+5
View File
@@ -45,6 +45,11 @@ struct CueTrackRef {
// shifting every later disc LBA (and the TOC) accordingly.
uint32_t pregap = 0;
uint32_t postgap = 0;
// Bytes per stored sector declared by the TRACK mode: 2352 for AUDIO and
// every "/2352" mode, 2048 or 2336 for the cooked modes. 0 = the mode named
// no size (unknown). The cue is the authority for a file's geometry; the
// file length is not (dumps can carry a partial trailing sector).
uint32_t sector_size = 0;
};
struct CueSheet {
+5
View File
@@ -64,6 +64,11 @@ void gpu_texture_correction_stats(uint64_t *attempts, uint64_t *armed,
int gpu_display_is_depth24(void);
void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y,
uint8_t* r, uint8_t* g, uint8_t* b);
/* Same decode against any 1024x512 VRAM image (e.g. a display-ring snapshot)
* instead of live VRAM: 15-bit or depth24 per `di`. */
void gpu_display_pixel_rgb_in(const uint16_t* image, const GpuDisplayInfo* di,
uint32_t x, uint32_t y,
uint8_t* r, uint8_t* g, uint8_t* b);
uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y);
/* Batch equivalent of calling gpu_display_pixel_argb(di, x, y) for x in
* [0, count) on a depth24 (FMV) scanline — same output, far less per-pixel
+6
View File
@@ -328,6 +328,12 @@ int gl_renderer_anisotropy(void);
* uploads and untextured primitives already are). On by default. */
void gl_renderer_set_accurate_blending(int on);
int gl_renderer_accurate_blending(void);
/* [video] dithering: 1 (default) PS1 dither pattern on the native VRAM grid
* (guest VRAM identical at every internal scale), 2 elective pattern per
* internal-resolution pixel, 0 off (true colour). Applies to eligible
* primitives (Gouraud, texture-modulated polygons) while GP0(E1h) bit 9 is set. */
void gl_renderer_set_dithering(int mode);
int gl_renderer_dithering(void);
int gl_renderer_get_pgxp_depth(void);
void gl_renderer_set_pgxp_color_perspective(int on);
int gl_renderer_get_pgxp_color_perspective(void);
+4
View File
@@ -61,6 +61,9 @@ void gr_set_precise_triangle(int enabled,
* perspective_texturing). q[i] is the normalized 1/z homogeneous weight at
* vertex i. enabled == 0 restores the PS1's affine UV interpolation. */
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2);
/* GP0(E1h) bit 9 (dither enable). Only a backend with [video] dithering
* implements it; the default renderers stay true colour. */
void gr_set_dither(int enabled);
/* PGXP depth for the NEXT triangle (docs/ENHANCEMENTS.md G1.14): the GTE SZ
* of each vertex (1..65535), taken from validated dataflow shadows. Only a
* backend with a PGXP depth buffer / perspective-correct colour uses it;
@@ -167,6 +170,7 @@ typedef struct GpuRenderBackend {
int32_t x1, int32_t y1,
int32_t x2, int32_t y2);
void (*set_perspective_triangle)(int enabled, float q0, float q1, float q2);
void (*set_dither)(int enabled); /* optional: GP0(E1h) bit 9 */
void (*fill_rect)(int x, int y, int w, int h, uint16_t color);
void (*copy_rect)(int src_x, int src_y, int dst_x, int dst_y, int w, int h);
void (*draw_flat_triangle)(int x0, int y0, int x1, int y1, int x2, int y2,
+72 -60
View File
@@ -1,12 +1,12 @@
#pragma once
#include <cstdint>
#include <array>
#include <string>
#include <fstream>
#include <memory>
#include <unordered_map>
#include <vector>
#include <cstdint>
#include <array>
#include <string>
#include <fstream>
#include <memory>
#include <unordered_map>
#include <vector>
/**
* ISO/BIN/CUE Reader for PS1 CD-ROM images
@@ -20,10 +20,10 @@
* - Read file contents by path
*/
namespace PS1 {
struct CHDState;
namespace PS1 {
struct CHDState;
/**
* Information about a file entry in the ISO filesystem
*/
@@ -44,21 +44,21 @@ struct RootDirectoryInfo {
/**
* A CD track parsed from the .cue sheet (or the synthesized single data
* track for a bare .bin/.iso). Multi-track discs can carry a
* track for a bare .bin/.iso). Multi-track discs can carry a
* Red Book CD-DA audio track after the data track; the CD model's TOC
* commands (GetTN/GetTD) must report them.
*/
struct CDTrack {
struct CDTrack {
int number; // 1-based track number
bool is_audio; // true = CD-DA audio (Red Book); false = data
uint32_t start_lba; // disc-relative start LBA (cue INDEX 01; track 1 = 0).
uint32_t start_lba; // disc-relative start LBA (cue INDEX 01; track 1 = 0).
// Single-FILE cues: equals the .bin-relative INDEX time.
// Multi-FILE cues (redump "(Track N).bin" dumps): the
// owning file's first disc sector plus its INDEX time.
// Either way, plus every cue PREGAP/POSTGAP before it
// (gaps the dump does not store).
uint32_t pregap_lba; // disc-relative start of the track's pregap: its cue
// PREGAP if any, else INDEX 00, else INDEX 01
// owning file's first disc sector plus its INDEX time.
// Either way, plus every cue PREGAP/POSTGAP before it
// (gaps the dump does not store).
uint32_t pregap_lba; // disc-relative start of the track's pregap: its cue
// PREGAP if any, else INDEX 00, else INDEX 01
};
/**
@@ -70,23 +70,23 @@ struct CDTrack {
struct BinSegment {
std::string path; // resolved on-disk path
std::ifstream file; // opened for the reader's lifetime
uint32_t start_lba; // disc sector of this file's first stored sector
uint32_t start_lba; // disc sector of this file's first stored sector
uint32_t sector_count; // sectors stored in this file
bool raw; // 2352-byte raw sectors (BIN) vs 2048 (ISO)
};
/**
* A contiguous run of disc sectors. A stored span maps onto sectors of one
* BinSegment; a virtual span is a cue PREGAP/POSTGAP that the dump does not
* store and that reads back as zero (silence), as the CHD path already does.
* Without PREGAP/POSTGAP lines each file is exactly one stored span, so the
* disc layout is the plain concatenation of the files.
*/
struct BinSpan {
uint32_t disc_start; // first disc-relative sector
uint32_t sector_count;
int segment; // index into segments_, or -1 for a virtual gap
uint32_t file_sector; // first sector within the segment's file
};
/**
* A contiguous run of disc sectors. A stored span maps onto sectors of one
* BinSegment; a virtual span is a cue PREGAP/POSTGAP that the dump does not
* store and that reads back as zero (silence), as the CHD path already does.
* Without PREGAP/POSTGAP lines each file is exactly one stored span, so the
* disc layout is the plain concatenation of the files.
*/
struct BinSpan {
uint32_t disc_start; // first disc-relative sector
uint32_t sector_count;
int segment; // index into segments_, or -1 for a virtual gap
uint32_t file_sector; // first sector within the segment's file
};
class ISOReader {
@@ -103,11 +103,23 @@ public:
/**
* Open an ISO/BIN file for reading
* @param filename Path to .iso, .bin, .img, .car, .cue, or .chd file
* @param filename Path to .iso, .bin, .img, .car, .cue, or .chd file
* @return true if opened successfully, false otherwise
*/
bool Open(const std::string& filename);
/**
* Sector geometry of the first stored file behind an image, decided the
* same way Open() decides it: a .cue's TRACK mode is authoritative; a bare
* file is raw when sector 0 starts with the CD sync pattern. File length
* is never the evidence (raw dumps can be a multiple of 2048).
* @param path .cue or bare .bin/.iso/.img
* @param raw set to true for 2352-byte raw sectors, false for 2048
* @return false when the image cannot be opened or its geometry is not
* mountable (conflicting or MODE2/2336 track modes)
*/
static bool FirstFileIsRaw(const std::string& path, bool* raw);
/**
* Close the currently open file
*/
@@ -127,15 +139,15 @@ public:
* @param buffer Buffer to read into (must be at least 2352 bytes)
* @return true if the image stores raw sectors and read succeeded
*/
bool ReadRawSector(uint32_t lba, uint8_t* buffer);
/**
* Read generated or SBI-replacement subchannel Q for a disc LBA.
* SBI records intentionally carry invalid CRC data; valid is false for
* those sectors so the CD controller can retain its last valid Q packet.
*/
bool ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const;
bool HasSubChannelReplacements() const { return !subq_replacements_.empty(); }
bool ReadRawSector(uint32_t lba, uint8_t* buffer);
/**
* Read generated or SBI-replacement subchannel Q for a disc LBA.
* SBI records intentionally carry invalid CRC data; valid is false for
* those sectors so the CD controller can retain its last valid Q packet.
*/
bool ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const;
bool HasSubChannelReplacements() const { return !subq_replacements_.empty(); }
/**
* Check if a file is currently open
@@ -163,11 +175,11 @@ public:
/**
* CD-track TOC accessors (multi-track .cue support).
* TrackCount() is >= 1 (a bare image synthesizes one data track).
* TrackStartLBA(n)/TrackPregapLBA(n)/TrackIsAudio(n) take a 1-based track number.
* TrackStartLBA(n)/TrackPregapLBA(n)/TrackIsAudio(n) take a 1-based track number.
*/
int TrackCount() const;
uint32_t TrackStartLBA(int track) const;
uint32_t TrackPregapLBA(int track) const;
int TrackCount() const;
uint32_t TrackStartLBA(int track) const;
uint32_t TrackPregapLBA(int track) const;
bool TrackIsAudio(int track) const;
/**
@@ -237,23 +249,23 @@ private:
std::vector<ISOFileEntry> ListFilesByLBA(uint32_t lba, uint32_t dir_size);
/**
* Helper: find the span containing a disc-relative LBA.
* @return span pointer, or nullptr when lba is past the disc end
* Helper: find the span containing a disc-relative LBA.
* @return span pointer, or nullptr when lba is past the disc end
*/
const BinSpan* SpanForLBA(uint32_t lba) const;
bool LoadSBICompanion(const std::string& image_path);
const BinSpan* SpanForLBA(uint32_t lba) const;
bool LoadSBICompanion(const std::string& image_path);
bool is_open_;
std::string volume_id_;
std::string bin_path_; // first (data) segment; kept for callers
RootDirectoryInfo root_dir_;
std::vector<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
std::vector<BinSegment> segments_; // cue FILE entries in disc order; >=1 after Open()
std::vector<BinSpan> spans_; // disc layout over segments_ + cue gaps
uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span
std::unique_ptr<CHDState> chd_; // present only for a directly mounted CHD
std::unordered_map<uint32_t, std::array<uint8_t, 12>> subq_replacements_;
};
std::vector<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
std::vector<BinSegment> segments_; // cue FILE entries in disc order; >=1 after Open()
std::vector<BinSpan> spans_; // disc layout over segments_ + cue gaps
uint32_t disc_sector_count_ = 0; // BIN/CUE: end of the last span
std::unique_ptr<CHDState> chd_; // present only for a directly mounted CHD
std::unordered_map<uint32_t, std::array<uint8_t, 12>> subq_replacements_;
};
} // namespace PS1
+6
View File
@@ -223,6 +223,11 @@ struct ModPlugin {
* before it is staged (psx_mod_set_pad_transform). Online it stays on per
* player: the post-transform pad is what every peer simulates. */
bool netplay_input = false;
/* netplay = "host_output": the plugin only reads guest state and drives
* host output (rumble, haptics). Online it stays on per player and its
* hooks run in the shared simulation, but every guest write it attempts
* is refused there, so it can never diverge the peers. */
bool netplay_host_output = false;
};
struct ModResource {
@@ -388,6 +393,7 @@ struct ModResolution {
std::string feature_id;
bool netplay_local_view = false;
bool netplay_input = false;
bool netplay_host_output = false;
};
std::vector<Plugin> plugins;
struct Resource {
+2
View File
@@ -215,6 +215,8 @@ uint16_t sio_get_pad_buttons_slot(int slot);
uint16_t sio_peek_stat(void);
uint16_t sio_peek_ctrl(void);
uint8_t sio_peek_rx_data(void);
/* SIO_STAT reads left before a visible ACK clears (side-effect free). */
int sio_peek_ack_visible_reads(void);
/* Debug accessors: is a logical pad connected, and is it in analog mode. */
int sio_get_pad_connected(int slot);
+15 -1
View File
@@ -5,6 +5,7 @@
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
@@ -125,6 +126,7 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
std::string line;
int cur_track_num = -1;
bool cur_track_audio = false;
uint32_t cur_sector_size = 0;
uint32_t cur_index00 = 0;
bool cur_has_index00 = false;
uint32_t cur_pregap = 0;
@@ -162,8 +164,19 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
int tn = 0;
char type_buf[32] = {0};
if (std::sscanf(upper.c_str(), " TRACK %d %31s", &tn, type_buf) == 2) {
const std::string type(type_buf);
cur_track_num = tn;
cur_track_audio = (std::string(type_buf).find("AUDIO") != std::string::npos);
cur_track_audio = (type.find("AUDIO") != std::string::npos);
// "MODE2/2352" etc. name the stored sector size; AUDIO is always
// 2352. Anything else (CDG, malformed) leaves it unknown.
cur_sector_size = 0;
const size_t slash = type.find('/');
if (cur_track_audio) {
cur_sector_size = 2352;
} else if (slash != std::string::npos) {
const unsigned long n = std::strtoul(type.c_str() + slash + 1, nullptr, 10);
if (n == 2048 || n == 2336 || n == 2352) cur_sector_size = (uint32_t)n;
}
cur_index00 = 0;
cur_has_index00 = false;
cur_pregap = 0;
@@ -200,6 +213,7 @@ CueSheet parse_cue_sheet(const fs::path& cue_path) {
t.index00 = cur_index00;
t.has_index00 = cur_has_index00;
t.pregap = cur_pregap;
t.sector_size = cur_sector_size;
sheet.tracks.push_back(t);
last_pushed_track = cur_track_num;
cur_track_num = -1;
+47 -55
View File
@@ -8669,6 +8669,15 @@ static void handle_texture_filter(int id, const char *json)
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d}", id, gr_texture_filter());
}
/* dithering: {"cmd":"dithering"} reads, {"cmd":"dithering","mode":0|1|2}
* sets [video] dithering live (0 off, 1 on = native grid, 2 fine). */
static void handle_dithering(int id, const char *json)
{
int mode = json_get_int(json, "mode", -1);
if (mode >= 0) gl_renderer_set_dithering(mode);
send_fmt("{\"id\":%d,\"ok\":true,\"mode\":%d}", id, gl_renderer_dithering());
}
static void handle_frame_perf(int id, const char *json)
{
(void)json;
@@ -9952,23 +9961,22 @@ static void handle_get_snapshots(int id, const char *json)
* deterministic runs (GL vs software) can each serve the display for the
* SAME frame number, giving a pixel-exact renderer-divergence census.
* GL reads GPU-side truth via the fbo peek; software reads CPU VRAM (its
* authoritative surface). depth24 scanout frames are tagged and refused
* (15-bit only). */
#define DISP_RING_CAP 64 /* full-VRAM entries are 1MB each; 32 frames of
* lookback (~0.3-0.5s) for exact-frame forensics */
#define DISP_RING_MAX_W 640
#define DISP_RING_MAX_H 256
/* Full-VRAM aux capture alongside the display: the entire 1024x512 raw VRAM
* as the renderer sees it at this frame (GL: FBO truth; SW: CPU truth), so an
* offline client can micro-cosim ANY prim of the frame — rasterize it from
* the GP0 ring against the exact same-frame texels/CLUTs — and attribute a
* bad pixel to sampling vs content. Streamed texture pages and palette
* cycling make later peeks worthless; only a same-frame capture is evidence. */
* authoritative surface). Each entry is the full 1024x512 VRAM plus the
* display geometry of that frame, and the PNG is decoded from it with the
* same scanout decode as screenshot_file — so 480-line interlaced displays
* and depth24 (FMV) frames are served whole, not cropped or refused. */
#define DISP_RING_CAP 64 /* full-VRAM entries are 1MB each; ~1s of
* lookback for exact-frame forensics */
/* The entire 1024x512 raw VRAM as the renderer sees it at this frame (GL: FBO
* truth; SW and depth24: CPU truth), so an offline client can micro-cosim ANY
* prim of the frame — rasterize it from the GP0 ring against the exact
* same-frame texels/CLUTs — and attribute a bad pixel to sampling vs content.
* Streamed texture pages and palette cycling make later peeks worthless; only
* a same-frame capture is evidence. */
typedef struct {
uint32_t frame;
uint16_t w, h;
uint8_t depth24, valid;
uint16_t *px; /* DISP_RING_MAX_W*DISP_RING_MAX_H halfwords */
uint8_t valid;
GpuDisplayInfo di; /* display geometry/depth at capture */
uint16_t *vram; /* full 1024x512 */
} DispRingEntry;
static PSX_BSS DispRingEntry s_disp_ring[DISP_RING_CAP];
@@ -9993,15 +10001,11 @@ static void disp_ring_capture(void)
}
if (!enabled || s_capture_frozen) return;
if (!s_disp_ring_px) {
size_t per = (size_t)DISP_RING_MAX_W * DISP_RING_MAX_H;
size_t vram = (size_t)1024 * 512;
s_disp_ring_px = (uint16_t *)malloc(DISP_RING_CAP * (per + vram) * sizeof(uint16_t));
s_disp_ring_px = (uint16_t *)malloc(DISP_RING_CAP * vram * sizeof(uint16_t));
if (!s_disp_ring_px) return;
for (int i = 0; i < DISP_RING_CAP; i++) {
uint16_t *base = s_disp_ring_px + (size_t)i * (per + vram);
s_disp_ring[i].px = base;
s_disp_ring[i].vram = base + per;
}
for (int i = 0; i < DISP_RING_CAP; i++)
s_disp_ring[i].vram = s_disp_ring_px + (size_t)i * vram;
}
DispRingEntry *e = &s_disp_ring[(uint32_t)(s_frame_count % DISP_RING_CAP)];
e->frame = (uint32_t)s_frame_count;
@@ -10010,30 +10014,15 @@ static void disp_ring_capture(void)
gpu_get_display_info(&di);
if (di.disabled || di.width == 0 || di.height == 0) return;
gpu_timeline_note(GTL_DISP_CAPTURE, 0, di.display_x | (di.display_y << 16));
uint32_t w = di.width, h = di.height;
if (w > DISP_RING_MAX_W) w = DISP_RING_MAX_W;
if (h > DISP_RING_MAX_H) h = DISP_RING_MAX_H;
e->w = (uint16_t)w; e->h = (uint16_t)h;
e->depth24 = (uint8_t)(di.depth24 ? 1 : 0);
/* GPU-side truth when the GL raster pipeline is live (scissored pack of
* just the display rect + one small readback); CPU VRAM otherwise (the
* software backend's authoritative surface). Runs on the present thread,
* where the GL context is current. */
/* Deferred: with the render thread on, the readback is recorded at this
* point of the command stream and filled in by the render thread (ring
* readers sync first), instead of a per-frame sync point. */
int got = 0;
if (di.display_x + w <= 1024 && di.display_y + h <= 512)
got = gl_renderer_fbo_peek_deferred((int)di.display_x, (int)di.display_y,
(int)w, (int)h, e->px);
if (!got) {
for (uint32_t y = 0; y < h; y++)
for (uint32_t x = 0; x < w; x++)
e->px[y * w + x] =
gpu_vram_peek((int)(di.display_x + x), (int)(di.display_y + y));
}
/* Full-VRAM aux capture (same GL-truth/CPU-truth split as the display). */
if (!gl_renderer_fbo_peek_deferred(0, 0, 1024, 512, e->vram)) {
e->di = di;
/* GPU-side truth when the GL raster pipeline is live; CPU VRAM otherwise
* (the software backend's authoritative surface). depth24 always reads
* the CPU mirror: packed RGB888 MDEC bytes live there, not in the FBO
* (see screenshot_file). Runs on the present thread, where the GL context
* is current. Deferred: with the render thread on, the readback is
* recorded at this point of the command stream and filled in by the
* render thread (ring readers sync first). */
if (di.depth24 || !gl_renderer_fbo_peek_deferred(0, 0, 1024, 512, e->vram)) {
const uint16_t *v = gpu_get_vram();
memcpy(e->vram, v, (size_t)1024 * 512 * sizeof(uint16_t));
}
@@ -10096,16 +10085,17 @@ static void handle_display_ring_get(int id, const char *json)
if (!e->valid || e->frame != (uint32_t)f) {
send_err(id, "frame not in display ring"); return;
}
if (e->depth24) { send_err(id, "frame is 24bpp scanout (unsupported)"); return; }
uint32_t w = e->w, h = e->h;
/* Same clamp and scanout decode as screenshot_file, against this frame's
* VRAM snapshot. */
uint32_t w = e->di.width; if (w > 640) w = 640;
uint32_t h = e->di.height; if (h > 512) h = 512;
uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * 3);
if (!rgb) { send_err(id, "alloc failed"); return; }
for (uint32_t i = 0; i < w * h; i++) {
uint16_t p = e->px[i];
rgb[i * 3 + 0] = (uint8_t)((p & 0x1F) << 3);
rgb[i * 3 + 1] = (uint8_t)(((p >> 5) & 0x1F) << 3);
rgb[i * 3 + 2] = (uint8_t)(((p >> 10) & 0x1F) << 3);
}
for (uint32_t y = 0; y < h; y++)
for (uint32_t x = 0; x < w; x++) {
uint8_t *p = rgb + ((size_t)y * w + x) * 3;
gpu_display_pixel_rgb_in(e->vram, &e->di, x, y, &p[0], &p[1], &p[2]);
}
FILE *fp = fopen(path, "wb");
if (!fp) { free(rgb); send_err(id, "cannot open file"); return; }
int ok = png_write_rgb(fp, rgb, w, h);
@@ -10113,7 +10103,8 @@ static void handle_display_ring_get(int id, const char *json)
fclose(fp);
if (!ok) { send_err(id, "png encode failed"); return; }
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%d,\"path\":\"%s\","
"\"width\":%u,\"height\":%u}", id, f, path, w, h);
"\"width\":%u,\"height\":%u,\"depth24\":%d}", id, f, path, w, h,
e->di.depth24 ? 1 : 0);
}
static void handle_screenshot_file(int id, const char *json)
@@ -15943,6 +15934,7 @@ static const CmdEntry s_commands[] = {
{ "texture_filter", handle_texture_filter },
{ "bloom", handle_bloom },
{ "dithering", handle_dithering },
{ "gl_ws_ablate", handle_gl_ws_ablate },
{ "gl_interp", handle_gl_interp },
{ "render_pass_stats", handle_render_pass_stats },
+25 -13
View File
@@ -3831,6 +3831,7 @@ static void gpu_reset_state(int clear_vram) {
semi_transparency = 0;
texpage_colors = 0;
dither_enabled = 0;
gr_set_dither(0);
draw_to_display = 0;
set_mask_bit = 0;
check_mask_bit = 0;
@@ -4198,11 +4199,6 @@ const uint16_t* gpu_get_vram(void) {
return vram;
}
static uint8_t gpu_vram_byte(uint32_t byte_x, uint32_t y) {
uint16_t hw = vram[((y & 511u) * 1024u) + ((byte_x >> 1) & 1023u)];
return (byte_x & 1u) ? (uint8_t)(hw >> 8) : (uint8_t)hw;
}
/* Depth24: note/query/reset the CPU→VRAM upload span tracked above. Used to
* hide trailing RGB columns when a movie blit doesn't fill the full CRTC
* width — MotK's Star Wars crawl leaves ~8px of stale VRAM on the right.
@@ -4308,8 +4304,9 @@ static void gpu_rgb555_to_rgb888(uint16_t c, uint8_t* r, uint8_t* g, uint8_t* b)
*b = (uint8_t)(((c >> 10) & 0x1Fu) << 3);
}
void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y,
uint8_t* r, uint8_t* g, uint8_t* b) {
void gpu_display_pixel_rgb_in(const uint16_t* image, const GpuDisplayInfo* di,
uint32_t x, uint32_t y,
uint8_t* r, uint8_t* g, uint8_t* b) {
if (di->depth24) {
uint32_t byte_x = ((di->display_x & 1023u) * 2u) + x * 3u;
uint32_t vy = (di->display_y + y) & 511u;
@@ -4320,15 +4317,24 @@ void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y,
*r = *g = *b = 0;
return;
}
*r = gpu_vram_byte(byte_x + 0u, vy);
*g = gpu_vram_byte(byte_x + 1u, vy);
*b = gpu_vram_byte(byte_x + 2u, vy);
const uint16_t* row = image + (size_t)vy * 1024u;
uint8_t px[3];
for (uint32_t i = 0; i < 3u; i++) {
uint16_t hw = row[((byte_x + i) >> 1) & 1023u];
px[i] = ((byte_x + i) & 1u) ? (uint8_t)(hw >> 8) : (uint8_t)hw;
}
*r = px[0]; *g = px[1]; *b = px[2];
return;
}
uint32_t vx = (di->display_x + x) & 1023u;
uint32_t vy = (di->display_y + y) & 511u;
gpu_rgb555_to_rgb888(vram[vy * 1024u + vx], r, g, b);
gpu_rgb555_to_rgb888(image[vy * 1024u + vx], r, g, b);
}
void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y,
uint8_t* r, uint8_t* g, uint8_t* b) {
gpu_display_pixel_rgb_in(vram, di, x, y, r, g, b);
}
uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y) {
@@ -4342,11 +4348,11 @@ uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y
* output, including the black-fill past the 2048-byte VRAM row), but hoists
* the per-row invariants (vy, base_byte_x, the "how many pixels are past the
* row edge" test) out of the per-pixel path instead of recomputing them
* `count` times and paying three gpu_vram_byte() calls per pixel. The
* `count` times and paying three byte extractions per pixel. The
* per-pixel path was the dominant present-side cost of FMV frames (3x the
* VRAM touches of the 16-bit path, done as a function-call chain instead of
* a straight-line loop). Still scalar C — no host-endianness assumptions,
* same byte-order shifts as gpu_vram_byte. */
* same byte-order shifts as gpu_display_pixel_rgb_in. */
void gpu_depth24_present_row(const GpuDisplayInfo* di, uint32_t y, uint32_t* out,
uint32_t count) {
uint32_t vy = (di->display_y + y) & 511u;
@@ -5941,6 +5947,7 @@ static void gp0_exec_draw_mode(void) {
semi_transparency = (param >> 5) & 3;
texpage_colors = (param >> 7) & 3;
dither_enabled = (param >> 9) & 1;
gr_set_dither((int)dither_enabled);
draw_to_display = (param >> 10) & 1;
texture_disable = (param >> 11) & 1;
/* Sync semi-transparency to SW renderer — actual blending is per-primitive */
@@ -7915,6 +7922,10 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) {
* on a stale draw area after savestate load. */
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
(int)draw_area_right, (int)draw_area_bottom);
/* GP0(E1h) bit 9 is restored with the registers; the renderer only
* learns it from E1, so push it now (else dithering keeps the pre-load
* setting until the game's next E1). */
gr_set_dither((int)dither_enabled);
/* A state saved mid-A0 resumes streaming its payload into the array. */
gr_vram_upload_set_open(gp0_state == GP0_VRAM_WRITE);
ws_nw_sync_target();
@@ -8069,6 +8080,7 @@ void gpu_pass_checkpoint_restore(void) {
(int)draw_area_right, (int)draw_area_bottom);
gr_set_draw_offset(draw_offset_x, draw_offset_y);
gr_set_mask_bits((int)set_mask_bit, (int)check_mask_bit);
gr_set_dither((int)dither_enabled);
ws_nw_sync_target();
/* Target sync may classify the scene before render_pass.c restores RAM.
* Do not retain history learned from the synthetic draw or that query. */
+201 -21
View File
@@ -464,7 +464,7 @@ enum {
RTH_WIDE_DISABLE, RTH_WIDE_CLEAR, RTH_WIDE_CLEAR_MARGINS, RTH_PROJ_TRI,
RTH_WIDE_RECOVERY, RTH_INTERP_SUSPENDED, RTH_PRESENT_VRAM,
RTH_PRESENT_WIDE, RTH_STATE, RTH_PEEK, RTH_RING_CAPTURE, RTH_DYN_STEP,
RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE
RTH_FRAME, RTH_FG_SRC, RTH_DEPTH, RTH_HD_NOTE, RTH_DITHER
};
/* GP0(A0) whose payload gpu.c is still streaming into its array (the
* facade's vram_upload_open, emulation thread), tracked whatever the HD
@@ -672,6 +672,8 @@ static void pass_note_new_frame(int origin_x, int origin_y, int source_path,
static void pass_apply_promotion(void);
static int pass_gen_present(uint64_t deadline);
static int stereo_present(int w, int h);
static int xr_mirror_mode(void);
static int xr_mirror_active(void);
static void stereo_resources_release(void);
static void stereo_invalidate(void);
static uint64_t s_idle_ticks_accum_fwd(uint64_t add);
@@ -922,6 +924,21 @@ static void c5_to_float(uint16_t c, float *r, float *g, float *b) {
*r = (c & 0x1F) / 31.0f; *g = ((c >> 5) & 0x1F) / 31.0f; *b = ((c >> 10) & 0x1F) / 31.0f;
}
}
/* [video] dithering (see psx_dither in the shaders): 1 (default) the PS1 4x4
* pattern on the native VRAM grid, 2 elective pattern per internal-resolution
* pixel, 0 off = true colour. s_dither_bit is the last GP0(E1h) bit 9. Each primitive's
* eligibility (bit set AND Gouraud-shaded, or a texture-modulated polygon;
* never flat, raw-textured or a rectangle) is fixed when it is queued and
* rides in its vertices, so bit changes need no flush. s_prim_shaded /
* s_prim_rect are set by the glb_draw_* entry points for gpu_geometry and
* gpu_textured_triangle. */
static int s_dither_mode = 1, s_dither_bit = 0;
static int s_prim_shaded = 0, s_prim_rect = 0;
/* Added to a geometry vertex's a_col.a (as 2*131072) for an eligible prim. */
#define GPU_GEO_DITHER_CODE 262144.0f
static float geo_dither_code(void) {
return (s_dither_mode && s_dither_bit && s_prim_shaded) ? GPU_GEO_DITHER_CODE : 0.0f;
}
/* Opaque textured draws carry the exact mask bit in FBO alpha. Keeping the
* duplicate stencil copy current is deferred until mask checking is requested. */
static int s_stencil_valid = 1;
@@ -1743,6 +1760,28 @@ static const char *INTERP_FS =
* rect coverage exactly [x*S, (x+w)*S) at every scale AND makes the
* top-left subpixel of each S*S block sample the exact PS1 value (which is
* what the PACK pass reads back). */
/* PS1 dithering ([video] dithering, on by default). While GP0(E1h) bit 9 is
* set the GPU adds the 4x4 offset matrix below to every pixel of a
* Gouraud-shaded polygon or line and of a texture-modulated polygon, then
* truncates to 5 bits. Flat untextured primitives, raw-texture polygons and
* all rectangles (sprites) are never dithered. Eligibility is decided per
* primitive on the CPU and carried in the vertex (v_dith), never inferred
* from the colour. u_dither 1 (default) = the PS1 pattern on the native
* VRAM grid (v_npos is the fragment's native position), so guest-visible
* VRAM is the same at every internal scale; 2 = elective finer pattern per
* internal-resolution pixel; 0 = off (true colour). The result is written
* in the surface's colour basis (u_accurate). */
#define PSX_DITHER_GLSL \
"uniform int u_dither;\n" \
"uniform int u_accurate; /* [video] accurate_blending: VRAM in the k*8/255 basis */\n" \
"vec3 psx_dither(vec3 c, vec2 npos, int eligible){\n" \
" if (u_dither == 0 || eligible == 0) return c;\n" \
" ivec2 p = (u_dither == 2 ? ivec2(gl_FragCoord.xy) : ivec2(floor(npos))) & 3;\n" \
" const float m[16] = float[16](-4.0,0.0,-3.0,1.0, 2.0,-2.0,3.0,-1.0,\n" \
" -3.0,1.0,-4.0,0.0, 3.0,-1.0,2.0,-2.0);\n" \
" vec3 k = clamp(floor((c * 255.0 + m[p.y * 4 + p.x]) / 8.0), 0.0, 31.0);\n" \
" return u_accurate != 0 ? k * (8.0 / 255.0) : k / 31.0; /* the surface's basis */\n" \
"}\n"
static const char *GEO_VS =
"#version 330\n"
"layout(location=0) in vec2 a_pos;\n"
@@ -1757,12 +1796,17 @@ static const char *GEO_VS =
"smooth out vec3 v_col_p; /* perspective-correct colour (PGXP, G1.14) */\n"
"flat out int v_cp;\n"
"noperspective out float v_b3d; /* bloom source: 1 on a PGXP 3D vertex */\n"
"noperspective out vec2 v_npos; /* native VRAM position (dithering) */\n"
"flat out int v_dith; /* this primitive is dither-eligible */\n"
"void main(){\n"
" v_npos = a_pos;\n"
" /* a_col.a carries the mask bit (0/1) and, for a PGXP 3D vertex, its\n"
" * GTE SZ: a = mask + 2*(sz + 65536*cp), cp = perspective colour. A\n"
" * negative a is a depth-clear vertex (beyond every SZ, inside the far plane). */\n"
" float a = a_col.a, code = floor(a * 0.5), zn = 0.0, w = 1.0;\n"
" float m = a - 2.0 * code;\n"
" /* + 2*131072: the primitive is dither-eligible (GPU_GEO_DITHER_CODE). */\n"
" float dth = (a >= 0.0 && code >= 131072.0) ? 1.0 : 0.0; code -= 131072.0 * dth; v_dith = int(dth);\n"
" float cp = code >= 65536.0 ? 1.0 : 0.0, sz = code - 65536.0 * cp;\n"
" if (a < 0.0) { m = 0.0; zn = 0.9999; sz = 0.0; cp = 0.0; } /* inside the far plane: never clipped */\n"
" else if (sz > 0.5) { zn = 1.0 - 512.0 / (max(sz * (1.0 - u_zbias) - (u_zbias > 0.0 ? 48.0 : 0.0), 1.0) + 256.0); if (cp > 0.5) w = sz / 1024.0; }\n"
@@ -1781,9 +1825,12 @@ static const char *GEO_FS =
"noperspective in float v_b3d;\n"
"uniform int u_bcls; /* bloom source pass (gl_postfx.inc): 0 off, 2 clear, 1 mark */\n"
"uniform int u_badd; /* this batch blends additively */\n"
"noperspective in vec2 v_npos; flat in int v_dith;\n"
PSX_DITHER_GLSL
"void main(){\n"
" if (u_bcls != 0) { if (u_bcls == 1 && !(v_b3d > 0.999 || u_badd != 0)) discard; frag = vec4(0.0); return; }\n"
" frag = v_cp != 0 ? vec4(v_col_p, v_col.a) : v_col; }\n";
" frag = v_cp != 0 ? vec4(v_col_p, v_col.a) : v_col;\n"
" frag.rgb = psx_dither(frag.rgb, v_npos, v_dith); }\n";
/* Textured prims: sample raw 1555 VRAM (integer), CLUT decode per depth,
* texture window, optional bilinear, texel-0 discard, STP-split discard,
@@ -1825,16 +1872,20 @@ static const char *TEX_VS =
"flat out int v_persp;\n"
"flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n"
"flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n"
"flat out int v_dith;\n"
"flat out int v_twin;\n"
"flat out vec4 v_hd_source; flat out int v_hd_mode;\n"
"flat out int v_filter;\n"
"smooth out vec3 v_col_p; flat out int v_cp;\n"
"noperspective out float v_b3d; /* bloom source: 1 on a PGXP 3D vertex */\n"
"noperspective out vec2 v_npos; /* native VRAM position (dithering) */\n"
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col; v_col_p = a_col.rgb;\n"
" v_npos = a_pos;\n"
" v_persp = (a_q > 0.0) ? 1 : 0;\n"
" v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n"
" v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n"
" v_semi = int(a_semi + 0.5);\n"
" v_dith = v_semi >> 3; v_semi &= 7; /* +8: dither-eligible primitive */\n"
" v_twin = int(a_twin + 0.5);\n"
" v_filter = int(a_filter + 0.5);\n"
" v_hd_source=a_hd_source; v_hd_mode=int(a_hd_mode+0.5);\n"
@@ -1866,6 +1917,8 @@ static const char *TEX_FS =
"noperspective in vec2 v_uv; noperspective in vec4 v_col;\n"
"smooth in vec2 v_uv_p; flat in int v_persp;\n"
"smooth in vec3 v_col_p; flat in int v_cp;\n"
"noperspective in vec2 v_npos;\n"
PSX_DITHER_GLSL
"out vec4 frag; out vec4 blend_factor;\n"
"noperspective in float v_b3d;\n"
"uniform int u_bcls; /* bloom source pass (gl_postfx.inc): 0 off, 2 clear, 1 mark */\n"
@@ -1875,6 +1928,7 @@ static const char *TEX_FS =
"flat in int v_raw; /* 1 = no color modulation */\n"
"flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n"
"flat in int v_semi; /* GP0 command has semi-transparency enabled */\n"
"flat in int v_dith; /* dither-eligible (modulated polygon, E1 bit 9) */\n"
"flat in int v_twin; /* texture window, GP0(E2h) bits 0..19 */\n"
"flat in int v_filter; /* sampling mode belongs to this primitive */\n"
"flat in vec4 v_hd_source; flat in int v_hd_mode;\n"
@@ -1917,7 +1971,6 @@ static const char *TEX_FS =
" }\n"
" return vram_at(v_tpage.x + u, v_tpage.y + v);\n"
"}\n"
"uniform int u_accurate; /* [video] accurate_blending: VRAM in the k*8/255 basis */\n"
"vec3 col5(int raw){\n"
" vec3 k = vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31));\n"
" return u_accurate != 0 ? k * (8.0 / 255.0) : k / 31.0;\n"
@@ -2105,6 +2158,7 @@ static const char *TEX_FS =
" if (u_semipass == 1 && stp == 1) discard;\n"
" if (u_semipass == 2 && stp == 0) discard;\n"
" if (v_raw == 0) rgb = clamp(rgb * (v_cp != 0 ? v_col_p : v_col.rgb) * 2.0, 0.0, 1.0);\n"
" rgb = psx_dither(rgb, v_npos, v_dith);\n"
" float dst_factor = 0.0;\n"
" if (u_semimode == 4 && v_semi != 0 && stp != 0) {\n"
" dst_factor = v_semi == 1 ? 0.5 : 1.0;\n"
@@ -2995,6 +3049,29 @@ static const HiwTile *hiw_ensure(int x0, int x1) {
}
/* ---- coherency: hr FBO -> raw mirror (pack) ------------------------------ */
#ifndef PSXGL_PIXEL_UNPACK_BUFFER
#define PSXGL_PIXEL_UNPACK_BUFFER 0x88EC
#endif
static GLuint s_raw_pbo = 0;
/* On by default only on macOS, where the stall was measured (Apple's GL on
* Metal); elsewhere opt-in until there is AMD/NVIDIA/Intel data (an AMD iGPU
* on Windows showed no stall and no change). PSX_GL_PBO_UPLOAD=0/1 overrides
* it on any platform. */
#if defined(__APPLE__)
#define PSXGL_PBO_UPLOAD_DEFAULT 1
#else
#define PSXGL_PBO_UPLOAD_DEFAULT 0
#endif
static int raw_upload_pbo_on(void) {
static int env = -1;
if (env < 0) {
const char *e = getenv("PSX_GL_PBO_UPLOAD");
env = (e && e[0]) ? (e[0] != '0') : PSXGL_PBO_UPLOAD_DEFAULT;
}
if (!env) return 0;
if (!s_raw_pbo) p_glGenBuffers(1, &s_raw_pbo);
return s_raw_pbo != 0;
}
static void pack_flush(void) {
if (!s_raster_ok || !s_pack_dirty.set) return;
/* A generated frame draws into its own surfaces: the raw mirror keeps
@@ -3013,8 +3090,26 @@ static void pack_flush(void) {
p_glActiveTexture(PSXGL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, s_raw_tex);
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, VRAM_W);
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, PSXGL_RED_INTEGER,
GL_UNSIGNED_SHORT, s_vram + (size_t)y * VRAM_W + x);
if (raw_upload_pbo_on()) {
/* Through an orphaned pixel-unpack buffer: the copy into a
* texture the GPU is still sampling is queued on the GPU, where
* a client-memory upload makes the driver wait for those draws
* (Apple's GL: GLDShareGroupRec::waitUsage, about 40 % of the
* render thread's work in an R4 race with the HD HUD). Same
* bytes, same order. PSX_GL_PBO_UPLOAD=0 for the direct path. */
const size_t rows = (size_t)(y + h - 1) * VRAM_W + (size_t)x + (size_t)w;
const size_t first = (size_t)y * VRAM_W + (size_t)x;
const size_t bytes = (rows - first) * sizeof(uint16_t);
p_glBindBuffer(PSXGL_PIXEL_UNPACK_BUFFER, s_raw_pbo);
p_glBufferData(PSXGL_PIXEL_UNPACK_BUFFER, (GLsizeiptr)bytes,
s_vram + first, PSXGL_STREAM_DRAW);
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, PSXGL_RED_INTEGER,
GL_UNSIGNED_SHORT, (const void *)0);
p_glBindBuffer(PSXGL_PIXEL_UNPACK_BUFFER, 0);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, PSXGL_RED_INTEGER,
GL_UNSIGNED_SHORT, s_vram + (size_t)y * VRAM_W + x);
}
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
return;
}
@@ -4463,7 +4558,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
s_fb_depth = 0; /* lines are 2D: never depth-tested (G1.14) */
s_fb_semi = semi;
s_fb_mask = (int)s_mask_set;
float mask_a = s_mask_set ? 1.0f : 0.0f;
float mask_a = (s_mask_set ? 1.0f : 0.0f) + geo_dither_code();
for (int i = 0; i < 2; i++) {
float *v = &s_fb[s_fb_n * 6];
v[0] = (float)xs[i];
@@ -4490,7 +4585,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
* batch above. */
if (mode == GL_LINES && n == 2 && s_out_scale > 1 && !bd_prim_gate(xs, n, 0)) {
float lv[2 * 6], quad[6 * 6];
float mask_a = s_mask_set ? 1.0f : 0.0f;
float mask_a = (s_mask_set ? 1.0f : 0.0f) + geo_dither_code();
for (int i = 0; i < 2; i++) {
lv[i*6+0] = (float)xs[i];
lv[i*6+1] = (float)ys[i];
@@ -4523,7 +4618,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
if (mode != GL_TRIANGLES || n < 3) {
flush_flat_batch();
float verts[3 * 6];
float mask_a = s_mask_set ? 1.0f : 0.0f;
float mask_a = (s_mask_set ? 1.0f : 0.0f) + geo_dither_code();
for (int i = 0; i < n; i++) {
verts[i*6+0] = precise ? s_pc_x[i] : (float)xs[i];
verts[i*6+1] = precise ? s_pc_y[i] : (float)ys[i];
@@ -4608,7 +4703,7 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
s_fb_depth = dmode;
s_fb_b3d = b3d;
float mask_a = s_mask_set ? 1.0f : 0.0f;
float mask_a = (s_mask_set ? 1.0f : 0.0f) + geo_dither_code();
float *v0 = &s_fb[s_fb_n * 6];
for (int i = 0; i < n; i++) {
float *v = &s_fb[s_fb_n * 6];
@@ -4677,6 +4772,9 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
const float *col, uint16_t texpage,
uint16_t clut_x, uint16_t clut_y, int rawtex,
int semi, const int *lim) {
/* PS1 dithering: texture-modulated polygons only (never raw texels or
* rectangles), while E1 bit 9 is set. */
const int dither_tex = s_dither_mode && s_dither_bit && !rawtex && !s_prim_rect;
// Mode 2 needs proven world geometry. Sprites, HUD and untracked packets
// retain point sampling; their cutout pixels must stay sharp.
const int untracked=(lim && !s_projected_uv_valid) || (!s_pc_valid && !s_pq_valid);
@@ -4786,7 +4884,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
vp[12] = (float)depth; vp[13] = (float)rawtex; /* a_depth, a_raw */
vp[14] = (float)lim[0]; vp[15] = (float)lim[1]; /* a_limits */
vp[16] = (float)lim[2]; vp[17] = (float)lim[3];
vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */
vp[18] = (semi >= 0 ? (float)(semi + 1) : 0.0f) /* a_semi code */
+ (dither_tex ? 8.0f : 0.0f); /* + dither-eligible */
vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
vp[20] = twin; /* a_twin */
vp[21] = (float)hd.origin_u; vp[22] = (float)hd.origin_v;
@@ -5087,12 +5186,15 @@ void gl_renderer_set_texture_lod(int mode, int aniso) {
}
int gl_renderer_texture_lod(void) { return s_tex_lod; }
int gl_renderer_anisotropy(void) { return s_tex_aniso; }
static GLint s_uAccurate = -1;
static GLint s_uAccurate = -1, s_geo_uAccurate = -1;
static void accurate_apply_uniforms(void) {
if (!s_tex_prog) return;
GLint prev = 0; glGetIntegerv(GL_CURRENT_PROGRAM, &prev);
if (s_uAccurate < 0) s_uAccurate = p_glGetUniformLocation(s_tex_prog, "u_accurate");
if (s_geo_uAccurate < 0 && s_geo_prog) s_geo_uAccurate = p_glGetUniformLocation(s_geo_prog, "u_accurate");
p_glUseProgram(s_tex_prog); p_glUniform1i(s_uAccurate, s_accurate_blend);
/* The dither snippet in the geometry program follows the same basis. */
if (s_geo_prog) { p_glUseProgram(s_geo_prog); p_glUniform1i(s_geo_uAccurate, s_accurate_blend); }
p_glUseProgram((GLuint)prev);
}
void gl_renderer_set_accurate_blending(int on) {
@@ -5102,6 +5204,29 @@ void gl_renderer_set_accurate_blending(int on) {
if (s_ctx) accurate_apply_uniforms();
}
int gl_renderer_accurate_blending(void) { return s_accurate_blend; }
/* Dither mode uniform in the geometry and textured programs; queued draws
* are drained before it changes. */
static GLint s_geo_uDither = -1, s_tex_uDither = -1;
static void dither_apply_uniforms(void) {
if (!s_geo_prog || !s_tex_prog) return;
GLint prev = 0; glGetIntegerv(GL_CURRENT_PROGRAM, &prev);
if (s_geo_uDither < 0) s_geo_uDither = p_glGetUniformLocation(s_geo_prog, "u_dither");
if (s_tex_uDither < 0) s_tex_uDither = p_glGetUniformLocation(s_tex_prog, "u_dither");
p_glUseProgram(s_geo_prog); p_glUniform1i(s_geo_uDither, s_dither_mode);
p_glUseProgram(s_tex_prog); p_glUniform1i(s_tex_uDither, s_dither_mode);
p_glUseProgram((GLuint)prev);
}
/* GP0(E1h) bit 9: recorded; it is read when each primitive is queued. */
static void glb_set_dither(int on) { s_dither_bit = on ? 1 : 0; }
void gl_renderer_set_dithering(int mode) {
GL_RT_SYNC("dithering");
mode = (mode == 1 || mode == 2) ? mode : 0;
if (mode == s_dither_mode) return;
if (s_ctx) { flush_flat_batch(); flush_tex_batch(); hiw_flush_queue(); }
s_dither_mode = mode;
if (s_ctx) dither_apply_uniforms();
}
int gl_renderer_dithering(void) { return s_dither_mode; }
static void glb_set_texture_filter(int b) { s_tex_filter = b >= 0 && b <= 3 ? b : 0; sw_set_texture_filter(b != 0); }
static int glb_texture_filter(void) { return s_tex_filter; }
@@ -5262,7 +5387,9 @@ static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,ui
native_draw_end(faithful);
}
if (!s_raster_ok) return;
s_prim_shaded = 1; /* Gouraud: dither-eligible */
gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1);
s_prim_shaded = 0;
precise_consumed();
}
static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){
@@ -5334,7 +5461,7 @@ static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t
if (native_required && !s_hd_native_authority)
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
if (s_raster_ok)
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
{ s_prim_rect = 1; gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1); s_prim_rect = 0; }
if (native_required && s_hd_native_authority) {
int faithful = native_draw_begin();
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
@@ -5346,7 +5473,7 @@ static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,
if (native_required && !s_hd_native_authority)
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
if (s_raster_ok)
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
{ s_prim_rect = 1; gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1); s_prim_rect = 0; }
if (native_required && s_hd_native_authority) {
int faithful = native_draw_begin();
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
@@ -5369,7 +5496,9 @@ static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_
native_draw_end(faithful);
}
if (!s_raster_ok) return;
s_prim_shaded = 1; /* shaded line: dither-eligible */
gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1);
s_prim_shaded = 0;
}
static int gl_read_display_argb(int x, int y, int w, int h, uint32_t *out,
int pitch_bytes, int cap_px, int *ow, int *oh);
@@ -5966,7 +6095,7 @@ static int init_gpu_raster(void) {
s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset");
s_uAniso = -1;
lod_apply_uniforms();
s_uAccurate = -1;
s_uAccurate = s_geo_uAccurate = -1;
accurate_apply_uniforms();
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
s_uHdTexture = p_glGetUniformLocation(s_tex_prog, "u_hd_texture");
@@ -6019,6 +6148,8 @@ static int init_gpu_raster(void) {
s_shift_hr = shift;
s_shift_hi = 0.5f / (float)s_out_scale - 1.0f / 64.0f;
s_geo_uShift = p_glGetUniformLocation(s_geo_prog, "u_shift");
s_geo_uDither = s_tex_uDither = -1;
dither_apply_uniforms();
s_tex_uShift = p_glGetUniformLocation(s_tex_prog, "u_shift");
s_geo_uZbias = p_glGetUniformLocation(s_geo_prog, "u_zbias");
s_tex_uZbias = p_glGetUniformLocation(s_tex_prog, "u_zbias");
@@ -6426,8 +6557,12 @@ void gl_renderer_shutdown(void) {
s_selected_bank_live_clut = s_tb_bank_live_clut = 0;
if (s_ctx) {
ensure_cpu();
/* The upload buffer belongs to this context: a lobby rematch
* creates a new one, which must create its own. */
if (s_raw_pbo) p_glDeleteBuffers(1, &s_raw_pbo);
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
}
s_raw_pbo = 0;
free(s_conv); s_conv = NULL;
s_hq_n = 0; s_hq_vn = 0; /* queued window draws die with the context */
s_raster_ok = 0;
@@ -8848,15 +8983,29 @@ static int stereo_present(int w, int h) {
gl_renderer_stereo_unavailable() != PSX_MOD_RENDER_PASS_READY) return 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
gl_perf_present_enter();
letterbox_rect_aspect(ww, wh, s_aspect_num * 2, s_aspect_den, &lx, &ly, &lw, &lh);
/* Desktop spectator layout while XR runs: default single left eye filled to
* the window, PSX_VR_DESKTOP_MIRROR selects 0 legacy / 1 left / 2 SBS / 3 right. */
const int mirror = xr_mirror_active() ? xr_mirror_mode() : 0;
const int solo = (mirror == 1 || mirror == 3);
const int solo_eye = (mirror == 3) ? 1 : 0;
if (solo)
letterbox_rect_aspect(ww, wh, p->tw[solo_eye], p->th[solo_eye], &lx, &ly, &lw, &lh);
else
letterbox_rect_aspect(ww, wh, s_aspect_num * 2, s_aspect_den, &lx, &ly, &lw, &lh);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST); glViewport(0, 0, ww, wh);
glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT);
for (int eye = 0; eye < 2; eye++) {
int start = eye ? lw / 2 : 0, width = eye ? lw - lw / 2 : lw / 2;
present_target_quad(p->tex[eye], p->tw[eye], p->th[eye],
0, 0, p->tw[eye], p->th[eye], 0,
lx + start, ly, width, lh, 1, 1, 1);
if (solo) {
present_target_quad(p->tex[solo_eye], p->tw[solo_eye], p->th[solo_eye],
0, 0, p->tw[solo_eye], p->th[solo_eye], 0,
lx, ly, lw, lh, 1, 1, 1);
} else {
for (int eye = 0; eye < 2; eye++) {
int start = eye ? lw / 2 : 0, width = eye ? lw - lw / 2 : lw / 2;
present_target_quad(p->tex[eye], p->tw[eye], p->th[eye],
0, 0, p->tw[eye], p->th[eye], 0,
lx + start, ly, width, lh, 1, 1, 1);
}
}
pres_record(GL_PRES_STEREO, p->x, p->y, w, h, lx, ly, lw, lh);
hold_capture_drawable();
@@ -8871,6 +9020,31 @@ static int stereo_present(int w, int h) {
return 1;
}
/* ---- Desktop spectator mirror -------------------------------------------
* stereo_present() draws the live XR eye textures, so this shows the headset
* image on the monitor. Default is a single left eye filled to the window;
* PSX_VR_DESKTOP_MIRROR selects 0 (restore the legacy desktop presentation, the
* side-by-side pair -- the window is not blanked or stopped), 1 left (default),
* 2 side-by-side, 3 right. Applies only while an XR session is running. */
static int s_xr_mirror = -2;
static int xr_mirror_mode(void) {
if (s_xr_mirror == -2) {
const char* e = getenv("PSX_VR_DESKTOP_MIRROR");
if (!e || !*e) s_xr_mirror = 1; /* on by default */
else if (*e == '0') s_xr_mirror = 0;
else if (!strcmp(e, "sbs") || !strcmp(e, "both") || !strcmp(e, "2")) s_xr_mirror = 2;
else if (!strcmp(e, "right") || !strcmp(e, "r") || !strcmp(e, "3")) s_xr_mirror = 3;
else s_xr_mirror = 1;
}
return s_xr_mirror;
}
static int xr_mirror_active(void) {
if (xr_mirror_mode() == 0) return 0;
PSXOpenXRStats s;
psx_openxr_stats(&s);
return s.running ? 1 : 0;
}
/* Draw two presented images crossfaded (t = weight of b) into the window. */
static int interp_present_pair(GLuint a, GLuint b, float t, int blend_mode);
@@ -10695,6 +10869,7 @@ static const GpuRenderBackend GL_BACKEND = {
.set_texture_window = glb_set_texture_window, .set_color_modulation = glb_set_color_modulation,
.set_precise_triangle = glb_set_precise_triangle,
.set_perspective_triangle = glb_set_perspective_triangle,
.set_dither = glb_set_dither,
.fill_rect = glb_fill_rect, .copy_rect = glb_copy_rect,
.draw_flat_triangle = glb_draw_flat_triangle, .draw_gouraud_triangle = glb_draw_gouraud_triangle,
.draw_textured_triangle = glb_draw_textured_triangle,
@@ -11352,7 +11527,7 @@ static int fg_raw_add(const FgRaw *r) {
static int fg_op_kept(uint16_t op) {
switch (op) {
case RTH_SEMI: case RTH_MASK: case RTH_TWIN: case RTH_MOD: case RTH_PRECISE:
case RTH_PERSP: case RTH_DEPTH: case RTH_FILL: case RTH_FLAT_TRI: case RTH_GOURAUD_TRI:
case RTH_PERSP: case RTH_DEPTH: case RTH_DITHER: case RTH_FILL: case RTH_FLAT_TRI: case RTH_GOURAUD_TRI:
case RTH_TEX_TRI: case RTH_SHADED_TEX_TRI: case RTH_FLAT_RECT: case RTH_TEX_RECT:
case RTH_TEX_RECT_SCALED: case RTH_LINE: case RTH_SHADED_LINE: case RTH_AREA:
case RTH_OFFSET: case RTH_WIDE_VIEW: case RTH_WIDE_TARGET: case RTH_WIDE_DISABLE:
@@ -11823,6 +11998,7 @@ static void fg_replay(const FgList *a, const FgList *b, double t, GLuint gen_wid
const int in = !s_fg_replay_objects_only && fg_area_meets(area, disp);
switch (h.op) {
case RTH_SEMI: glb_set_semi_transparency(v[0], v[1]); break;
case RTH_DITHER: glb_set_dither(v[0]); break;
case RTH_MASK: glb_set_mask_bits(v[0], v[1]); break;
case RTH_TWIN: glb_set_texture_window((uint32_t)v[0]); break;
case RTH_MOD: glb_set_color_modulation(v[0], v[1], v[2], v[3]); break;
@@ -13309,6 +13485,8 @@ static int rtb_scale(void) {
}
static void rtb_set_texture_filter(int b) { GL_RT_SYNC("texture_filter"); glb_set_texture_filter(b); }
static int rtb_texture_filter(void) { return s_tex_filter; }
static void rtb_set_dither(int on) {
RTH_DIRECT_OR(RTH_REC(RTH_DITHER, 0, on ? 1 : 0)); glb_set_dither(on); }
static void rtb_set_semi_transparency(int e, int m) {
RTH_DIRECT_OR(RTH_REC(RTH_SEMI, 0, e, m)); glb_set_semi_transparency(e, m); }
static void rtb_set_mask_bits(int sb, int cb) {
@@ -13517,6 +13695,7 @@ static const GpuRenderBackend GL_RT_BACKEND = {
.set_texture_window = rtb_set_texture_window, .set_color_modulation = rtb_set_color_modulation,
.set_precise_triangle = rtb_set_precise_triangle,
.set_perspective_triangle = rtb_set_perspective_triangle,
.set_dither = rtb_set_dither,
.fill_rect = rtb_fill_rect, .copy_rect = rtb_copy_rect,
.draw_flat_triangle = rtb_draw_flat_triangle, .draw_gouraud_triangle = rtb_draw_gouraud_triangle,
.draw_textured_triangle = rtb_draw_textured_triangle,
@@ -13555,6 +13734,7 @@ static void gl_rth_exec(void *user, const RtCmd *c, const void *payload) {
if (s_fg_on) fg_capture(c, payload);
switch (c->op) {
case RTH_SEMI: glb_set_semi_transparency(v[0], v[1]); break;
case RTH_DITHER: glb_set_dither(v[0]); break;
case RTH_MASK: glb_set_mask_bits(v[0], v[1]); break;
case RTH_TWIN: glb_set_texture_window((uint32_t)v[0]); break;
case RTH_MOD: glb_set_color_modulation(v[0], v[1], v[2], v[3]); break;
+3
View File
@@ -177,6 +177,9 @@ void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
if (g_b->set_perspective_triangle)
g_b->set_perspective_triangle(enabled, q0, q1, q2);
}
void gr_set_dither(int enabled) {
if (g_b->set_dither) g_b->set_dither(enabled);
}
void gr_set_depth_triangle(int enabled, float z0, float z1, float z2) {
if (g_b->set_depth_triangle)
g_b->set_depth_triangle(enabled, z0, z1, z2);
+588 -520
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -1296,6 +1296,7 @@ static int g_video_fmv_chroma = 0; /* [video] fmv_chroma_smoothing /
/* [video] texture_lod / anisotropic_filtering; PSX_TEXTURE_LOD / PSX_ANISO. */
static int g_video_texture_lod = 0, g_video_aniso = 1;
static int g_video_accurate_blending = 1; /* [video] accurate_blending / PSX_ACCURATE_BLENDING */
static int g_video_dithering = 1; /* [video] dithering / PSX_DITHERING */
static int g_video_pgxp_color_correction = 0;
static int g_video_pgxp_seam = 0;
static float g_video_pgxp_depth_threshold = 4096.0f;
@@ -16361,6 +16362,7 @@ int main(int argc, char** argv) {
g_video_texture_lod = gc.runtime.video_texture_lod;
g_video_aniso = gc.runtime.video_anisotropic_filtering;
g_video_accurate_blending = gc.runtime.video_accurate_blending ? 1 : 0;
g_video_dithering = gc.runtime.video_dithering;
g_video_pgxp_color_correction = gc.runtime.video_pgxp_color_correction ? 1 : 0;
g_video_pgxp_seam = gc.runtime.video_pgxp_seam;
g_video_pgxp_depth_threshold = (float)gc.runtime.video_pgxp_depth_threshold;
@@ -18793,6 +18795,8 @@ session_reboot:
gl_renderer_set_texture_lod(g_video_texture_lod, g_video_aniso);
if (const char* e = std::getenv("PSX_ACCURATE_BLENDING")) g_video_accurate_blending = (*e && *e != '0');
gl_renderer_set_accurate_blending(g_video_accurate_blending);
if (const char* e = std::getenv("PSX_DITHERING")) g_video_dithering = std::atoi(e);
gl_renderer_set_dithering(g_video_dithering);
gl_renderer_set_pgxp_color_perspective(g_video_pgxp_color_correction);
gl_renderer_set_pgxp_seam(g_video_pgxp_seam);
if (const char* e = std::getenv("PSX_PGXP_DEPTH_THRESHOLD")) g_video_pgxp_depth_threshold = (float)std::atof(e);
+23 -1
View File
@@ -11,9 +11,14 @@
#include "spu.h"
#include <string.h>
/* mod_runtime.cpp: 1 while a netplay host-output plugin's hook runs; every
* guest-mutating service refuses then (docs/NETPLAY.md). */
int psx_mod_host_output_scope(void);
uint32_t psx_mod_call_guest(struct CPUState* cpu, uint32_t function,
uint32_t return_address, uint32_t a0, uint32_t a1,
uint32_t a2, uint32_t a3) {
if (psx_mod_host_output_scope()) return 0;
uint32_t regs[32];
const uint32_t pc = cpu->pc, hi = cpu->hi, lo = cpu->lo;
memcpy(regs, cpu->gpr, sizeof regs);
@@ -67,6 +72,11 @@ uint32_t psx_mod_call_guest_uncharged_counted(struct CPUState* cpu,
uint32_t function, uint32_t return_address, uint32_t a0, uint32_t a1,
uint32_t a2, uint32_t a3, uint32_t budget_cycles, int* charged,
uint64_t* counted_cycles) {
if (psx_mod_host_output_scope()) {
if (charged) *charged = 0;
if (counted_cycles) *counted_cycles = 0;
return 0;
}
PsxCycleFreeze save;
CallerTiming timing;
timing_save(&timing, cpu);
@@ -85,12 +95,14 @@ uint32_t psx_mod_call_guest_uncharged(struct CPUState* cpu, uint32_t function,
uint32_t return_address, uint32_t a0,
uint32_t a1, uint32_t a2, uint32_t a3,
uint32_t budget_cycles, int* charged) {
if (psx_mod_host_output_scope()) { if (charged) *charged = 0; return 0; }
return psx_mod_call_guest_uncharged_counted(cpu, function, return_address,
a0, a1, a2, a3, budget_cycles, charged, NULL);
}
int psx_mod_dma_write_ram(uint32_t address, const void* data, uint32_t bytes,
int lba) {
if (psx_mod_host_output_scope()) return 0;
return dma_host_cdrom_write(address, (const uint8_t*)data, bytes, lba);
}
@@ -101,6 +113,7 @@ static int ram_span(uint32_t address, uint32_t bytes) {
}
int psx_mod_host_write_ram(uint32_t address, const void* data, uint32_t bytes) {
if (psx_mod_host_output_scope()) return 0;
const uint8_t* p = (const uint8_t*)data;
if ((!p && bytes) || !ram_span(address, bytes)) return 0;
while (bytes && (address & 3u)) { psx_host_write_byte(address++, *p++); --bytes; }
@@ -118,22 +131,30 @@ int psx_mod_host_write_ram(uint32_t address, const void* data, uint32_t bytes) {
#define SPU_CONTROL 0x1F801DAAu
int psx_mod_spu_sample_bank(uint32_t bank,const void *adpcm,uint32_t bytes) {
if (psx_mod_host_output_scope()) return 0;
return spu_register_sample_bank(bank,adpcm,bytes);
}
int psx_mod_spu_bind_voice_bank(unsigned voice,uint32_t bank) {
if (psx_mod_host_output_scope()) return 0;
return spu_bind_next_voice_bank(voice,bank);
}
int psx_mod_spu_private_voice_play(unsigned slot,uint32_t bank,const uint16_t regs[8],unsigned mode) {
if (psx_mod_host_output_scope()) return 0;
return spu_private_voice_play(slot,bank,regs,mode);
}
void psx_mod_spu_private_voice_stop(uint32_t mask) {spu_private_voice_stop(mask);}
void psx_mod_spu_private_voice_stop(uint32_t mask) {
if (psx_mod_host_output_scope()) return;
spu_private_voice_stop(mask);
}
int psx_mod_spu_private_voice_volume(unsigned slot,uint16_t left,uint16_t right) {
if (psx_mod_host_output_scope()) return 0;
return spu_private_voice_volume(slot,left,right);
}
uint32_t psx_mod_spu_private_voice_active(void) {return spu_private_voice_active();}
int psx_mod_spu_upload(uint32_t spu_address, uint32_t guest_source,
uint32_t bytes, int stop_after) {
if (psx_mod_host_output_scope()) return 0;
if ((spu_address & 7u) || (guest_source & 3u) || (bytes & 3u) ||
spu_address >= SPU_RAM_BYTES || bytes > SPU_RAM_BYTES ||
!ram_span(guest_source, bytes))
@@ -148,6 +169,7 @@ int psx_mod_spu_upload(uint32_t spu_address, uint32_t guest_source,
int psx_mod_psyq_load_image(uint32_t x, uint32_t y, uint32_t w, uint32_t h,
uint32_t guest_source) {
if (psx_mod_host_output_scope()) return 0;
if (!w || w > 1024u || !h || h > 512u || x >= 1024u || y >= 512u ||
(guest_source & 3u))
return 0;
+5 -2
View File
@@ -2599,11 +2599,13 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
const std::string netplay =
toml::find_or<std::string>(v, "netplay", "");
if (!netplay.empty() && netplay != "local_view" &&
netplay != "input")
netplay != "input" && netplay != "host_output")
throw std::runtime_error(
"plugin netplay must be \"local_view\" or \"input\"");
"plugin netplay must be \"local_view\", \"input\" "
"or \"host_output\"");
plugin.netplay_local_view = netplay == "local_view";
plugin.netplay_input = netplay == "input";
plugin.netplay_host_output = netplay == "host_output";
}
read_conditions(v, out.options, plugin.feature_id,
plugin.when, "plugin");
@@ -3945,6 +3947,7 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
resolved.feature_id = plugin->feature_id;
resolved.netplay_local_view = plugin->netplay_local_view;
resolved.netplay_input = plugin->netplay_input;
resolved.netplay_host_output = plugin->netplay_host_output;
result.plugins.push_back(std::move(resolved));
}
for (const ModResource& resource : package->resources) {
+95 -4
View File
@@ -714,8 +714,14 @@ bool sha256_disc_range(const std::filesystem::path& image,
std::array<uint8_t, 2048> bytes{};
size_t remaining = size;
uint64_t at = location;
const bool raw_source = file_size > 0 &&
((uint64_t)file_size % 2352u) == 0;
// Same geometry rule as the mount (cue TRACK mode, else sector-0 sync):
// a raw dump whose length happens to be a multiple of 2048 is still raw.
bool raw_source = false;
if (target == ModPatchTarget::DiscUser &&
!PS1::ISOReader::FirstFileIsRaw(image.string(), &raw_source)) {
if (error) *error = "cannot determine stock image sector geometry: " + image.string();
return false;
}
while (remaining != 0) {
uint64_t physical = at;
size_t chunk = remaining;
@@ -2285,7 +2291,8 @@ std::vector<std::string> mod_runtime_netplay_view_features(const ModResolution&
for (const ModResolution::Plugin& p : plan.plugins) {
const std::string key = p.package_id + "/" + p.feature_id;
auto it = ok.find(key);
const bool good = p.netplay_local_view || p.netplay_input;
const bool good = p.netplay_local_view || p.netplay_input ||
p.netplay_host_output;
ok[key] = it == ok.end() ? good : (it->second && good);
}
for (const ModResolution::Write& w : plan.writes)
@@ -2905,7 +2912,27 @@ extern "C" int psx_mod_reload_hd_texture_pack(void) {
return ok;
}
/* Host-output observation reads: side-effect-free peeks of RAM, scratchpad
* and BIOS ROM (psx_peek_word); MMIO and anything else reads 0, because a bus
* read of a device register can change the device (SIO data pops the RX FIFO,
* SIO_STAT ages the ACK). */
extern "C" int psx_mod_host_output_scope(void);
static uint32_t ho_peek_word(uint32_t address) {
uint32_t v = 0;
return psx_peek_word(address & ~3u, &v) ? v : 0u;
}
static uint8_t ho_peek_byte(uint32_t address) {
return (uint8_t)(ho_peek_word(address) >> ((address & 3u) * 8u));
}
static uint16_t ho_peek_half(uint32_t address) {
return (uint16_t)(ho_peek_word(address) >> ((address & 2u) * 8u));
}
static void ho_drop_word(uint32_t, uint32_t) {}
static void ho_drop_half(uint32_t, uint16_t) {}
static void ho_drop_byte(uint32_t, uint8_t) {}
extern "C" uint8_t psx_mod_read_byte(uint32_t address) {
if (psx_mod_host_output_scope()) return ho_peek_byte(address);
return psx_read_byte(address);
}
@@ -2930,38 +2957,50 @@ extern "C" int psx_mod_current_resource_bytes(const char* resource_id,
return 0;
}
static bool mod_host_output_write_blocked(void);
extern "C" void psx_mod_write_byte(uint32_t address, uint8_t value) {
if (mod_host_output_write_blocked()) return;
psx_host_write_byte(address, value);
}
extern "C" uint16_t psx_mod_read_half(uint32_t address) {
if (psx_mod_host_output_scope()) return ho_peek_half(address);
return psx_read_half(address);
}
static bool mod_host_output_write_blocked(void);
extern "C" void psx_mod_write_half(uint32_t address, uint16_t value) {
if (mod_host_output_write_blocked()) return;
psx_host_write_half(address, value);
}
extern "C" uint32_t psx_mod_read_word(uint32_t address) {
if (psx_mod_host_output_scope()) return ho_peek_word(address);
return psx_read_word(address);
}
static bool mod_host_output_write_blocked(void);
extern "C" void psx_mod_write_word(uint32_t address, uint32_t value) {
if (mod_host_output_write_blocked()) return;
psx_host_write_word(address, value);
}
extern "C" void psx_mod_write_code_word(uint32_t address, uint32_t value) {
if (mod_host_output_write_blocked()) return;
psx_host_write_word(address, value);
dirty_ram_mark_executable_range(address & 0x1FFFFFFFu, 4u);
}
extern "C" int psx_mod_host_output_scope(void);
extern "C" uint32_t psx_mod_alloc_guest_memory(uint32_t size,
uint32_t alignment) {
if (psx_mod_host_output_scope()) return 0;
return psx_mod_memory_alloc(size, alignment);
}
extern "C" uint32_t psx_mod_alloc_gpu_dma_memory(uint32_t size,
uint32_t alignment) {
if (psx_mod_host_output_scope()) return 0;
return psx_mod_gpu_dma_memory_alloc(size, alignment);
}
@@ -3078,6 +3117,7 @@ extern "C" int psx_mod_register_function_filter_plugin(
extern "C" int psx_mod_finish_function(CPUState* cpu) {
using namespace PSXRecompV4;
RuntimeMods& s = state();
if (psx_mod_host_output_scope()) return 0;
if (!cpu || s.current_function_cpu != cpu || !s.current_plugin) return 0;
s.current_function_finished = true;
return 1;
@@ -3135,11 +3175,62 @@ extern "C" void psx_mod_instruction(CPUState* cpu, uint32_t address, uint32_t in
static int game_netplay_function_entry(CPUState* cpu, uint32_t address);
/* Netplay host-output plugins ([[plugin]] netplay = "host_output") run their
* entry hooks in the shared simulation while the rest of the own-view plan is
* parked. They observe only: each callback gets a private copy of the CPU
* state (register changes to it are discarded; its bus pointers peek RAM /
* ROM and drop writes), psx_mod_read_* peek too (no MMIO side effects), a
* completion request is
* ignored, and every guest-mutating mod service refuses while one runs
* (psx_mod_host_output_scope), so the shared simulation is bit-identical
* with or without them. */
static int s_host_output_depth;
extern "C" int psx_mod_host_output_scope(void) {
return s_host_output_depth > 0 &&
PSXRecompV4::state().netplay_view_plan ? 1 : 0;
}
static void run_host_output_hooks(CPUState* cpu, uint32_t address) {
using namespace PSXRecompV4;
RuntimeMods& s = state();
const auto& table = active_function_entry_hooks();
const uint32_t key = function_entry_key(address);
auto it = std::lower_bound(
table.begin(), table.end(), key,
[](const ActiveFunctionEntryHook& hook, uint32_t k) { return hook.key < k; });
for (; it != table.end() && it->key == key; ++it) {
if (!it->plugin || !it->plugin->netplay_host_output || !it->callback)
continue;
CPUState view = *cpu; /* observation copy: peek reads, dropped writes */
view.read_word = ho_peek_word; view.write_word = ho_drop_word;
view.read_half = ho_peek_half; view.write_half = ho_drop_half;
view.read_byte = ho_peek_byte; view.write_byte = ho_drop_byte;
PluginCallbackScope scope(s, it->plugin, &view);
++s_host_output_depth;
it->callback(&view, address);
--s_host_output_depth;
s.current_function_finished = false;
}
}
/* Memory writes from a host-output plugin while a netplay plan is live would
* exist on one peer only. */
static bool mod_host_output_write_blocked(void) {
const auto& s = PSXRecompV4::state();
return psx_mod_host_output_scope() ||
(s.netplay_view_plan && s.current_plugin &&
s.current_plugin->netplay_host_output);
}
extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) {
using namespace PSXRecompV4;
if (!g_psx_mod_function_entry_hooks || !cpu) return 0;
RuntimeMods& s = state();
if (mod_plan_hooks_parked()) return game_netplay_function_entry(cpu, address);
if (mod_plan_hooks_parked()) {
run_host_output_hooks(cpu, address);
return game_netplay_function_entry(cpu, address);
}
const auto& table = active_function_entry_hooks();
const uint32_t key = function_entry_key(address);
auto it = std::lower_bound(
+2
View File
@@ -54,8 +54,10 @@ const uint16_t* mod_texture_bank_pixels(uint16_t id, uint32_t* w, uint32_t* h) {
return banks[id].pixels;
}
int psx_mod_host_output_scope(void);
uint32_t psx_mod_alloc_texture_packet_memory(uint32_t size, uint32_t alignment) {
uint32_t addr;
if (psx_mod_host_output_scope()) return 0;
if (arena_count == 16u || size < 40u) return 0;
addr = psx_mod_gpu_dma_memory_alloc(size, alignment);
if (!addr) return 0;
+5 -1
View File
@@ -104,7 +104,11 @@ void psx_guest_cycle_scale_vblank(void) {
}
/* Public mod API (mod_plugins.h): the mod gate. */
void psx_mod_set_guest_cycle_scale_gate(int open) { psx_guest_cycle_scale_gate_open(open); }
int psx_mod_host_output_scope(void);
void psx_mod_set_guest_cycle_scale_gate(int open) {
if (psx_mod_host_output_scope()) return;
psx_guest_cycle_scale_gate_open(open);
}
uint32_t psx_mod_guest_cycle_scale(void) { return s_psx_gcs_live; }
void psx_guest_cycle_scale_snapshot(uint32_t out[3]) {
+1
View File
@@ -2172,6 +2172,7 @@ uint32_t sio_read(uint32_t addr) {
uint16_t sio_peek_stat(void) { return sio_stat; }
uint16_t sio_peek_ctrl(void) { return sio_ctrl; }
uint8_t sio_peek_rx_data(void) { return sio_rx_data; }
int sio_peek_ack_visible_reads(void) { return (int)sio_ack_visible_reads; }
void sio_write(uint32_t addr, uint32_t value) {
sio_debug_poll_maybe();
+14 -3
View File
@@ -2028,9 +2028,20 @@ int spu_snapshot_read(const uint8_t *p, uint32_t len) {
}
uint32_t phase = 0;
if (len == spu_snapshot_bytes() && !pst_r_u32(&r, &phase)) return 0;
/* Applied at the next catch-up, once the guest clock is restored too. */
s_clock_carry = phase % SPU_CYCLES_PER_SAMPLE;
s_clock_valid = 0;
/* Anchor the SPU's sample phase to the restored clock NOW. boot_state
* writes CLOCK before SPU, so the clock is already restored here. It used
* to be applied lazily at the next catch-up -- after the guest had run on,
* so the phase was taken against a later clock and lost: a rollback
* replay rendered SPU samples at other guest cycles than the live run
* (voice state / SPU RAM split with identical CPU paths,
* PSX_RB_DET_SELFTEST). */
{
const uint64_t now = psx_get_cycle_count();
const uint32_t carry = phase % SPU_CYCLES_PER_SAMPLE;
s_clock = now >= carry ? now - carry : now;
s_clock_carry = 0;
s_clock_valid = 1;
}
spu_output_flush();
return 1;
}
+1
View File
@@ -272,6 +272,7 @@ int gr_scale(void) { return 1; }
void gr_set_texture_filter(int bilinear) { (void)bilinear; }
int gr_texture_filter(void) { return 0; }
void gr_set_semi_transparency(int enabled, int mode) { (void)enabled; (void)mode; }
void gr_set_dither(int enabled) { (void)enabled; }
void gr_set_mask_bits(int set_bit, int check_bit) { (void)set_bit; (void)check_bit; }
void gr_set_texture_window(uint32_t raw) { (void)raw; }
void gr_set_color_modulation(int r, int g, int b, int raw_texture) {
+213
View File
@@ -0,0 +1,213 @@
/* PS1 dithering ([video] dithering, on by default) on a real, hidden OpenGL
* context, driven through the renderer facade (gr_*) the way gpu.c drives it.
*
* Eligibility, with GP0(E1h) bit 9 set (hardware rule):
* - Gouraud polygon .................. dithered (also when its three
* vertex colours are equal and 5-bit exact: eligibility is the command,
* not the colour)
* - shaded line ...................... dithered
* - texture-modulated polygon ........ dithered (also at the neutral
* 0x80 modulation, whose output is 5-bit exact)
* - flat polygon, flat line, flat rect, raw-texture polygon,
* textured rectangle (modulated or not) ...... never dithered
* With the bit clear nothing is dithered, and "off" is the true-colour
* output whatever the bit. Each eligible case is compared against "off"
* region by region in the native VRAM.
* The default "on" puts the pattern on the native VRAM grid: the native VRAM
* at 4x, stepped by dynamic resolution, equals 1x (no guest-visible change
* across scales). "fine" (elective) dithers per internal pixel. Also: the render thread, PGXP depth
* and native-wide margins give the same pixels; with accurate_blending the
* dithered channels stay on the k*8 grid.
* Original source-owned scene; no retail payload. */
#define main rt_fixture_main
#include "test_gl_render_thread.c"
#undef main
#define DW 320
#define DH 240
static uint16_t nat[1024 * 512];
static uint32_t hi[DW * 4 * DH * 4];
static int k_test_scale = 4;
/* Regions (native px): x, y, w, h */
enum { R_GOURAUD, R_GOURAUD_FLATCOL, R_SHADED_LINE, R_TEX_MOD, R_TEX_NEUTRAL,
R_FLAT_TRI, R_FLAT_RECT, R_FLAT_LINE, R_TEX_RAW, R_RECT_MOD, R_RECT_RAW, R_N };
static const int REG[R_N][4] = {
{0, 0, 160, 40}, /* Gouraud gradient */
{170, 0, 60, 40}, /* Gouraud, equal 5-bit-exact vertex colours */
{240, 0, 70, 40}, /* shaded lines */
{0, 50, 70, 60}, /* modulated textured polygon */
{80, 50, 70, 60}, /* textured polygon at neutral 0x80 modulation */
{160, 50, 60, 40}, /* flat triangle */
{230, 50, 60, 40}, /* flat rect */
{0, 120, 70, 30}, /* flat lines */
{80, 120, 70, 60}, /* raw textured polygon */
{160, 120, 64, 64}, /* modulated textured rect */
{240, 120, 64, 64}, /* raw textured rect */
};
static const char *RNAME[R_N] = {"gouraud", "gouraud equal colours", "shaded line",
"modulated textured poly", "neutral-modulated textured poly", "flat triangle",
"flat rect", "flat line", "raw textured poly", "modulated textured rect", "raw textured rect"};
static const int ELIGIBLE[R_N] = {1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0};
static void tquad(int x, int y, int w, int h, uint32_t c, int raw) {
gr_draw_shaded_textured_triangle(x, y, 0, 0, c, x + w, y, 63, 0, c, x, y + h, 0, 63, c, 512, 256, 0x0008, raw);
gr_draw_shaded_textured_triangle(x + w, y, 63, 0, c, x + w, y + h, 63, 63, c, x, y + h, 0, 63, c, 512, 256, 0x0008, raw);
}
static void scene(int bit, int depth) {
gr_set_draw_area(0, 0, DW - 1, DH - 1);
gr_set_draw_offset(0, 0);
gr_wide_set_target(0);
mask_bits(0, 0);
gr_set_semi_transparency(0, 0);
gr_set_dither(0);
gr_fill_rect(0, 0, DW, DH, 0x0000);
gr_wide_clear(0, 0, DH, 0x0000);
gr_set_dither(bit);
/* Gouraud gradient, black to white, reaching into the left wide margin. */
if (depth) gr_set_depth_triangle(1, 900.0f, 900.0f, 900.0f);
gr_draw_gouraud_triangle(-40, 0, 0x0000, 160, 0, 0x7fff, -40, 40, 0x0000);
if (depth) gr_set_depth_triangle(1, 900.0f, 900.0f, 900.0f);
gr_draw_gouraud_triangle(160, 0, 0x7fff, 160, 40, 0x7fff, -40, 40, 0x0000);
/* Gouraud with three equal 5-bit-exact colours. */
gr_draw_gouraud_triangle(170, 0, 0x4210, 230, 0, 0x4210, 170, 40, 0x4210);
gr_draw_gouraud_triangle(230, 0, 0x4210, 230, 40, 0x4210, 170, 40, 0x4210);
for (int i = 0; i < 40; i += 2) gr_draw_shaded_line(240, i, 0x0421, 309, i, 0x5ef7);
gr_set_color_modulation(128, 128, 128, 0);
tquad(0, 50, 70, 60, 0x6080a0, 0);
tquad(80, 50, 70, 60, 0x808080, 0);
gr_draw_flat_triangle(160, 50, 220, 50, 160, 90, 0x2d6b);
gr_draw_flat_rect(230, 50, 60, 40, 0x4210);
for (int i = 0; i < 30; i += 2) gr_draw_line(0, 120 + i, 69, 120 + i, 0x5294);
tquad(80, 120, 70, 60, 0x6080a0, 1);
gr_set_color_modulation(0x60, 0x80, 0xa0, 0);
gr_draw_textured_rect(160, 120, 64, 64, 0, 0, 512, 256, 0x0008);
gr_set_color_modulation(128, 128, 128, 1);
gr_draw_textured_rect(240, 120, 64, 64, 0, 0, 512, 256, 0x0008);
gr_set_color_modulation(128, 128, 128, 0);
gr_set_dither(0);
gl_renderer_render_thread_frame_boundary();
}
typedef struct { uint64_t native, hires, wide; } Shot;
static Shot shot(void) {
Shot s;
gr_vram_transfer_out(0, 0, 1024, 512, nat);
s.native = fnv(nat, sizeof nat, 0xcbf29ce484222325ull);
int ow = 0, oh = 0, sc = gr_scale();
int n = gl_renderer_read_display_hires(0, 0, DW, DH, hi, DW * sc * DH * sc, &ow, &oh);
check(n == DW * sc * DH * sc, "hires readback");
s.hires = fnv(hi, (size_t)n * 4, 0xcbf29ce484222325ull);
static uint32_t wb[426 * 4 * 512 * 4];
int gw = 0, gh = 0, got = gr_wide_dump_full(wb, 426 * sc * 512 * sc, &gw, &gh, 0);
s.wide = got > 0 ? fnv(wb, (size_t)got * 4, 0xcbf29ce484222325ull) : 0;
return s;
}
static int same(Shot a, Shot b) { return a.native == b.native && a.hires == b.hires && a.wide == b.wide; }
static int region_equal(const uint16_t *a, const uint16_t *b, int r) {
const int x = REG[r][0], y = REG[r][1], w = REG[r][2], h = REG[r][3];
for (int yy = y; yy < y + h; yy++)
if (memcmp(a + yy * 1024 + x, b + yy * 1024 + x, (size_t)w * 2)) return 0;
return 1;
}
static double region_mean(const uint16_t *v, int r) {
long s = 0; int n = 0;
for (int y = REG[r][1]; y < REG[r][1] + REG[r][3]; y++)
for (int x = REG[r][0]; x < REG[r][0] + REG[r][2]; x++) {
uint16_t p = v[y * 1024 + x]; s += (p & 31) + ((p >> 5) & 31) + ((p >> 10) & 31); n += 3;
}
return (double)s / n;
}
int main(void) {
if (SDL_Init(SDL_INIT_VIDEO) != 0) { fprintf(stderr, "SKIP no video\n"); return 77; }
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#if defined(__APPLE__)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif
SDL_Window *win = SDL_CreateWindow("Dithering hidden test", 0, 0, 320, 240,
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
if (!win) { fprintf(stderr, "SKIP no window\n"); return 77; }
gr_set_backend(GR_BACKEND_OPENGL);
gr_init(vram);
gr_set_scale(k_test_scale);
gl_renderer_set_dynamic_resolution(1);
gl_renderer_set_swap_interval(0);
if (!gl_renderer_init_context(win)) { fprintf(stderr, "SKIP no GL context\n"); return 77; }
textures();
gr_wide_configure(426, 53);
static uint16_t ref[1024 * 512], dith[1024 * 512];
check(gl_renderer_dithering() == 1, "on by default");
/* Off: true colour whatever the bit. */
gl_renderer_set_dithering(0);
scene(0, 0); Shot off0 = shot(); memcpy(ref, nat, sizeof ref);
scene(1, 0); Shot off1 = shot();
check(same(off0, off1), "off: dither bit changes nothing");
for (int mode = 1; mode <= 2; mode++) {
gl_renderer_set_dithering(mode);
scene(0, 0); Shot clear = shot();
check(same(off0, clear), "bit clear: nothing is dithered");
scene(1, 0); Shot on = shot();
memcpy(dith, nat, sizeof dith);
check(on.wide != off0.wide, "native-wide margin dithers too");
for (int r = 0; r < R_N; r++) {
const int eq = region_equal(ref, dith, r);
const double m0 = region_mean(ref, r), m1 = region_mean(dith, r);
printf("mode=%d %-32s %s mean %.2f -> %.2f\n", mode, RNAME[r], eq ? "unchanged" : "dithered", m0, m1);
char msg[128];
snprintf(msg, sizeof msg, "%s: %s", RNAME[r], ELIGIBLE[r] ? "dithered" : "never dithered");
/* "fine" at 4x: the native pack sees the top-left sample of each
* 4x4 block, i.e. one pattern cell (-4), still a change. */
check(ELIGIBLE[r] ? !eq : eq, msg);
/* Mean over the native pack: the default reads the whole pattern;
* "fine" at 4x reads one pattern cell per block. */
if (ELIGIBLE[r] && mode == 1) check(fabs(m1 - m0) < 1.0, "dither keeps the mean brightness");
}
/* PGXP depth: the same scene, depth-tested. */
gl_renderer_set_pgxp_depth(1);
scene(1, 1); Shot dep = shot();
check(dep.native == on.native, "PGXP depth path dithers the same");
gl_renderer_set_pgxp_depth(0);
/* Render thread: bit, mode and per-primitive eligibility recorded. */
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
scene(1, 0);
gl_renderer_render_thread_sync("dither");
Shot rt = shot();
gl_renderer_render_thread_stop();
check(same(rt, on), "render thread gives the same pixels");
/* With accurate_blending every dithered gradient channel is on the
* k*8 grid (the surface basis). */
const int acc0 = gl_renderer_accurate_blending();
gl_renderer_set_accurate_blending(1);
scene(1, 0); (void)shot();
int off_basis = 0;
for (int y = 0; y < 40 * gr_scale(); y++)
for (int x = 0; x < 150 * gr_scale(); x++)
off_basis += ((hi[y * DW * gr_scale() + x] & 0xff) % 8) != 0;
printf("mode=%d accurate: gradient channels off the k*8 grid=%d\n", mode, off_basis);
check(off_basis == 0, "dither output follows the accurate (k*8) basis");
gl_renderer_set_accurate_blending(acc0);
if (mode == 1) { /* default: guest VRAM independent of the scale */
check(gl_renderer_step_internal_scale_now(1) == 1, "step to 1x");
scene(1, 0); Shot one = shot();
(void)one;
/* Lines are drawn as one-native-pixel quads above 1x (see
* test_gl_scale_invariance.c), so they are not compared. */
int inv = 1;
for (int r = 0; r < R_N; r++)
if (r != R_SHADED_LINE && r != R_FLAT_LINE) inv &= region_equal(dith, nat, r);
check(inv, "default dithering: native VRAM at 4x equals 1x");
check(gl_renderer_step_internal_scale_now(k_test_scale) == 1, "back to 4x");
}
}
gl_renderer_set_dithering(0);
scene(1, 0); Shot back = shot();
check(same(back, off0), "off again restores the exact true-colour image");
check(glGetError() == GL_NO_ERROR, "GL errors");
printf("checks=%d failures=%d\n", checks, failures);
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
return failures ? 1 : 0;
}
+2
View File
@@ -53,6 +53,8 @@ void gpu_hd_textures_begin_copy(int sx,int sy,int dx,int dy,int w,int h){
(void)sx;(void)sy;(void)dx;(void)dy;(void)w;(void)h;}
void gpu_hd_textures_end_copy(void){}
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
/* mod_texture_banks.c refuses inside a netplay host-output hook (mod_runtime.cpp); none here. */
int psx_mod_host_output_scope(void){return 0;}
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
int g_psx_vram_dirty_tracking=0;
uint64_t s_frame_count=0;
+2
View File
@@ -14,6 +14,8 @@ uint64_t psx_cycle_count=0;
static int test_full_composite;
int gpu_ws_background_requires_full_composite(void){return test_full_composite;}
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
/* mod_texture_banks.c refuses inside a netplay host-output hook (mod_runtime.cpp); none here. */
int psx_mod_host_output_scope(void){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;
+2
View File
@@ -49,6 +49,8 @@ void gpu_hd_textures_begin_copy(int sx,int sy,int dx,int dy,int w,int h){
(void)sx;(void)sy;(void)dx;(void)dy;(void)w;(void)h;}
void gpu_hd_textures_end_copy(void){}
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
/* mod_texture_banks.c refuses inside a netplay host-output hook (mod_runtime.cpp); none here. */
int psx_mod_host_output_scope(void){return 0;}
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
int g_psx_vram_dirty_tracking=0;
uint64_t s_frame_count=0;
+2
View File
@@ -84,6 +84,8 @@
#include "gl_batch_policy_hle.c"
#include "gl_batch_policy_select.c"
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
/* mod_texture_banks.c refuses inside a netplay host-output hook (mod_runtime.cpp); none here. */
int psx_mod_host_output_scope(void){return 0;}
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
int g_psx_vram_dirty_tracking=0;
uint64_t s_frame_count=0;
@@ -404,6 +404,7 @@ void gr_set_draw_offset(int x, int y) { (void)x; (void)y; }
void gr_set_mask_bits(int set, int check) { (void)set; (void)check; }
void gr_set_texture_window(uint32_t value) { (void)value; }
void gr_set_semi_transparency(int enabled, int mode) { (void)enabled; (void)mode; }
void gr_set_dither(int enabled) { (void)enabled; }
void gr_set_color_modulation(int r, int g, int b, int raw)
{ (void)r; (void)g; (void)b; (void)raw; }
void gr_set_perspective_triangle(int enabled, float a, float b, float c)
+136
View File
@@ -0,0 +1,136 @@
// Raw (2352) vs cooked (2048) sector geometry. The cue's TRACK mode decides;
// a bare file is raw when sector 0 starts with the CD sync pattern. File
// length is never the evidence: the Legend of Dragoon disc 1 dump is a raw
// MODE2/2352 image with 384 trailing bytes whose length is a multiple of 2048,
// and a length test mounted it as a cooked ISO (PVD read back as zeros, BIOS
// SystemError B/906).
#include "iso_reader.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static const uint8_t kSync[12] = {0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
// Raw MODE2 sectors: sync + header + 8-byte subheader, user data = marker i+1.
static void write_raw(const fs::path& path, uint32_t sectors, size_t trailing) {
std::ofstream out(path, std::ios::binary);
for (uint32_t i = 0; i < sectors; ++i) {
std::vector<uint8_t> s(2352, 0);
std::memcpy(s.data(), kSync, sizeof(kSync));
s[15] = 2;
std::memset(s.data() + 24, (int)(i + 1), 2048);
out.write(reinterpret_cast<const char*>(s.data()), (std::streamsize)s.size());
}
std::vector<uint8_t> tail(trailing, 0xEE);
out.write(reinterpret_cast<const char*>(tail.data()), (std::streamsize)tail.size());
}
// Cooked sectors; sector 0 is an all-zero system-area sector, others = i+1.
static void write_cooked(const fs::path& path, uint32_t sectors) {
std::ofstream out(path, std::ios::binary);
for (uint32_t i = 0; i < sectors; ++i) {
std::vector<uint8_t> s(2048, i == 0 ? 0 : (uint8_t)(i + 1));
out.write(reinterpret_cast<const char*>(s.data()), (std::streamsize)s.size());
}
}
static void write_text(const fs::path& path, const std::string& text) {
std::ofstream out(path);
out << text;
}
static void check_markers(const std::string& image, uint32_t sectors, bool first_is_zero) {
PS1::ISOReader r;
CHECK(r.Open(image));
CHECK(r.GetSectorCount() == sectors);
uint8_t buf[2048];
for (uint32_t i = 0; i < sectors; ++i) {
CHECK(r.ReadSector(i, buf));
const uint8_t want = (i == 0 && first_is_zero) ? 0 : (uint8_t)(i + 1);
if (buf[0] != want || buf[2047] != want) {
std::printf(" %s sector %u: got %02x want %02x\n", image.c_str(), i, buf[0], want);
++g_failures;
}
}
CHECK(!r.ReadSector(sectors, buf));
}
int main() {
const fs::path dir = fs::temp_directory_path() / "psxrecomp-iso-geometry-test";
fs::remove_all(dir);
fs::create_directories(dir);
// 20 raw sectors + 64 trailing bytes = 47104 = 23 * 2048: a length test
// calls this a cooked ISO.
const fs::path odd = dir / "odd.bin";
write_raw(odd, 20, 64);
CHECK(fs::file_size(odd) % 2048 == 0);
CHECK(fs::file_size(odd) % 2352 != 0);
// Cue declares MODE2/2352: raw.
write_text(dir / "odd.cue", "FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n");
check_markers((dir / "odd.cue").string(), 20, false);
bool raw = false;
CHECK(PS1::ISOReader::FirstFileIsRaw((dir / "odd.cue").string(), &raw) && raw);
// Bare: sector-0 sync says raw.
check_markers(odd.string(), 20, false);
raw = false;
CHECK(PS1::ISOReader::FirstFileIsRaw(odd.string(), &raw) && raw);
// A cooked ISO whose length is a multiple of BOTH 2048 and 2352
// (147 * 2048 = 128 * 2352): no sync at sector 0, so cooked.
const fs::path both = dir / "both.iso";
write_cooked(both, 147);
CHECK(fs::file_size(both) % 2352 == 0);
check_markers(both.string(), 147, true);
raw = true;
CHECK(PS1::ISOReader::FirstFileIsRaw(both.string(), &raw) && !raw);
// Cue MODE1/2048 over a cooked file: cooked.
write_text(dir / "both.cue", "FILE \"both.iso\" BINARY\n TRACK 01 MODE1/2048\n INDEX 01 00:00:00\n");
check_markers((dir / "both.cue").string(), 147, true);
// Tracks of one FILE disagreeing on sector size: refuse.
write_text(dir / "conflict.cue",
"FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n"
" TRACK 02 MODE1/2048\n INDEX 01 00:00:10\n");
{
PS1::ISOReader r;
CHECK(!r.Open((dir / "conflict.cue").string()));
CHECK(!PS1::ISOReader::FirstFileIsRaw((dir / "conflict.cue").string(), &raw));
}
// MODE2/2336 has no sync/header; not mountable here: refuse, never misread.
write_text(dir / "m2336.cue", "FILE \"odd.bin\" BINARY\n TRACK 01 MODE2/2336\n INDEX 01 00:00:00\n");
{
PS1::ISOReader r;
CHECK(!r.Open((dir / "m2336.cue").string()));
}
fs::remove_all(dir);
if (g_failures) {
std::printf("iso_reader_geometry_test: %d failure(s)\n", g_failures);
return 1;
}
std::printf("iso_reader_geometry_test: PASS\n");
return 0;
}
+2
View File
@@ -33,6 +33,8 @@ extern "C" uint16_t psx_read_half(uint32_t a) {
extern "C" void psx_write_half(uint32_t a, uint16_t v) {
psx_write_byte(a, (uint8_t)v); psx_write_byte(a + 1, (uint8_t)(v >> 8));
}
/* Side-effect-free peek (memory.c); host-output observation reads. */
extern "C" int psx_peek_word(uint32_t, uint32_t* out) { *out = 0; return 0; }
extern "C" uint32_t psx_read_word(uint32_t a) {
return (uint32_t)psx_read_half(a) | (uint32_t)psx_read_half(a + 2) << 16;
}
+5
View File
@@ -69,6 +69,8 @@ extern "C" void psx_write_half(uint32_t address, uint16_t value) {
ram[offset + 1] = (uint8_t)(value >> 8);
}
/* Side-effect-free peek (memory.c); host-output observation reads. */
extern "C" int psx_peek_word(uint32_t, uint32_t* out) { *out = 0; return 0; }
extern "C" uint32_t psx_read_word(uint32_t address) {
const uint32_t offset = address & 0x1fffffu;
return (uint32_t)ram[offset] |
@@ -1165,6 +1167,9 @@ int main(int argc, char** argv) {
check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected");
std::vector<uint8_t> raw_iso(24*2352);
for(unsigned i=0;i<24;++i) {
/* A raw sector starts with the CD sync pattern; geometry is read from
* it, never from the file length. */
raw_iso[i*2352]=0; std::fill_n(raw_iso.begin()+i*2352+1,10,(uint8_t)0xFF); raw_iso[i*2352+11]=0;
raw_iso[i*2352+15]=2;
std::copy_n(iso.begin()+i*2048,2048,raw_iso.begin()+i*2352+24);
}
+130 -1
View File
@@ -1,6 +1,8 @@
#include "mod_runtime.h"
#include "mod_packages.h"
#include "mod_plugins.h"
#include "sio.h"
#include <cstring>
#include "psx_sha256.h"
#include "gpu.h"
@@ -38,26 +40,71 @@ extern "C" int gpu_hd_textures_configure(const char*, int, int, char*, size_t) {
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) { *out = {}; }
extern "C" void gpu_hd_textures_set_dump_enabled(int) {}
extern "C" int gpu_hd_textures_reload(char*, size_t) { return 0; }
/* sio.c's environment (as in test_sio_jogcon_protocol.c). */
extern "C" {
uint32_t i_stat = 0, i_mask = 0;
uint32_t g_debug_current_func_addr = 0, g_debug_last_store_pc = 0;
int psx_get_in_exception(void) { return 0; }
uint64_t psx_get_cycle_count(void) { static uint64_t t; return t += 64; }
uint32_t memory_get_sr(void) { return 0; }
void debug_server_poll(void) {}
void debug_server_log_sio_write(uint32_t, uint32_t, uint8_t) {}
void starvation_ring_record(uint8_t, uint8_t, uint8_t, uint16_t, uint16_t,
uint8_t, uint8_t, uint16_t, uint8_t, uint16_t,
uint8_t, uint32_t) {}
void card_read_summary_record(uint8_t, uint8_t, uint16_t, uint8_t, uint8_t,
uint8_t, uint8_t, uint32_t) {}
void card_data_writes_arm(uint8_t, uint16_t, uint8_t, uint8_t) {}
void event_ring_record_aux(uint16_t, uint8_t, uint32_t) {}
void psx_irq_raise(uint32_t bit, uint32_t) { i_stat |= 1u << bit; }
int memcard_is_present(int) { return 0; }
int memcard_read_sector(int, int, uint8_t* buf) { std::memset(buf, 0, 128); return -1; }
int memcard_write_sector(int, int, const uint8_t*) { return -1; }
void memcard_flush(int) {}
}
/* The guest bus: main RAM, plus the real SIO (sio.c) at 0x1F801040..5F, so
* a bus read of SIO_DATA / SIO_STAT has its hardware side effects here too.
* Every MMIO bus access is counted. */
static int mmio_bus_accesses;
static bool is_sio(uint32_t a) {
a &= 0x1fffffffu;
return a >= 0x1F801040u && a < 0x1F801060u;
}
extern "C" uint8_t psx_read_byte(uint32_t address) {
if (is_sio(address)) { ++mmio_bus_accesses; return (uint8_t)sio_read(address); }
return ram[address & 0x1fffffu];
}
extern "C" void psx_write_byte(uint32_t address, uint8_t value) {
if (is_sio(address)) { ++mmio_bus_accesses; sio_write(address, value); return; }
ram[address & 0x1fffffu] = value;
}
/* Side-effect-free peek (memory.c): RAM only in this fixture. */
extern "C" int psx_peek_word(uint32_t addr, uint32_t* out) {
if (is_sio(addr) || (addr & 0x1fffffffu) >= 0x200000u) return 0;
const uint32_t o = addr & 0x1ffffcu;
*out = (uint32_t)ram[o] | ((uint32_t)ram[o + 1] << 8) |
((uint32_t)ram[o + 2] << 16) | ((uint32_t)ram[o + 3] << 24);
return 1;
}
extern "C" uint16_t psx_read_half(uint32_t address) {
if (is_sio(address)) { ++mmio_bus_accesses; return (uint16_t)sio_read(address); }
const uint32_t offset = address & 0x1fffffu;
return (uint16_t)(ram[offset] | ((uint16_t)ram[offset + 1] << 8));
}
extern "C" void psx_write_half(uint32_t address, uint16_t value) {
if (is_sio(address)) { ++mmio_bus_accesses; sio_write(address, value); return; }
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
}
extern "C" uint32_t psx_read_word(uint32_t address) {
if (is_sio(address)) { ++mmio_bus_accesses; return sio_read(address); }
const uint32_t offset = address & 0x1fffffu;
return (uint32_t)ram[offset] |
((uint32_t)ram[offset + 1] << 8) |
@@ -66,6 +113,7 @@ extern "C" uint32_t psx_read_word(uint32_t address) {
}
extern "C" void psx_write_word(uint32_t address, uint32_t value) {
if (is_sio(address)) { ++mmio_bus_accesses; sio_write(address, value); return; }
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
@@ -139,6 +187,28 @@ static void sim_entry(CPUState*, uint32_t) { sim_hits++; }
static void view_activate(void) { view_activations++; }
static void sim_activate(void) { sim_activations++; }
static void view_vblank(void) { vblanks++; }
static int out_hits, out_activations, out_scope;
static uint32_t out_peeks[5];
extern "C" int psx_mod_host_output_scope(void);
static void out_entry(CPUState* cpu, uint32_t) {
out_hits++;
out_scope = psx_mod_host_output_scope();
cpu->gpr[2] = 0x12345678u; /* review repro: must not stick */
/* review 2: MMIO reads through the mod API and the CPU's bus pointers,
* and stores through cpu->write_* (RAM and SIO_CTRL). */
out_peeks[0] = psx_mod_read_word(0x1F801040u); /* SIO_DATA: pops RX */
out_peeks[1] = psx_mod_read_half(0x1F801044u); /* SIO_STAT: ages ACK */
out_peeks[2] = psx_mod_read_byte(0x80004000u);
if (cpu->read_word) out_peeks[3] = cpu->read_word(0x1F801040u);
if (cpu->read_half) out_peeks[4] = cpu->read_half(0x1F801044u);
if (cpu->write_word) cpu->write_word(0x80004004u, 0xDEADBEEFu);
if (cpu->write_half) cpu->write_half(0x1F80104Au, 0x0040u); /* SIO reset */
if (cpu->write_byte) cpu->write_byte(0x80004008u, 0x77u);
cpu->pc = 0x80001234u;
psx_mod_write_byte(0x80004000u, 0x5Au); /* must be refused online */
psx_mod_finish_function(cpu); /* must be ignored online */
}
static void out_activate(void) { out_activations++; }
static std::string package(const std::string& id, const std::string& feature,
const std::string& plugin, const std::string& extra) {
@@ -157,12 +227,15 @@ int main() {
package("view.pkg", "wide", "view.wide", "netplay = \"local_view\"\n"));
write_text(root / "bundled/sim.pkg/1.0.0/manifest.toml",
package("sim.pkg", "cheat", "sim.cheat", ""));
write_text(root / "bundled/out.pkg/1.0.0/manifest.toml",
package("out.pkg", "rumble", "out.rumble", "netplay = \"host_output\"\n"));
write_text(root / "bundled/bad.pkg/1.0.0/manifest.toml",
package("bad.pkg", "x", "bad.x", "netplay = \"everywhere\"\n"));
const std::string state =
"format_version = 2\n"
"[[feature]]\npackage_id = \"view.pkg\"\nid = \"wide\"\nenabled = true\n"
"[[feature]]\npackage_id = \"sim.pkg\"\nid = \"cheat\"\nenabled = true\n";
"[[feature]]\npackage_id = \"sim.pkg\"\nid = \"cheat\"\nenabled = true\n"
"[[feature]]\npackage_id = \"out.pkg\"\nid = \"rumble\"\nenabled = true\n";
write_text(root / "state.toml", state);
check(psx_mod_register_function_entry_plugin("view.wide", 0x80003000u, view_entry),
@@ -173,6 +246,9 @@ int main() {
psx_mod_register_activation_plugin("sim.cheat", sim_activate),
"register activations");
check(psx_mod_register_vblank_plugin("view.wide", view_vblank), "register vblank");
check(psx_mod_register_function_entry_plugin("out.rumble", 0x80003200u, out_entry) &&
psx_mod_register_activation_plugin("out.rumble", out_activate),
"register host-output hook");
std::string error;
check(PSXRecompV4::mod_runtime_initialize(root, "SLUS-VIEW", 0x80002000u, {}, &error),
@@ -186,6 +262,11 @@ int main() {
psx_mod_function_entry(&cpu, 0x80003000u);
psx_mod_function_entry(&cpu, 0x80003100u);
check(view_hits == 1 && sim_hits == 1, "offline hooks run in the game");
check(psx_mod_function_entry(&cpu, 0x80003200u) == 1 && out_hits == 1 &&
psx_read_byte(0x80004000u) == 0x5Au && !out_scope,
"offline a host-output hook is an ordinary hook");
cpu = CPUState{};
psx_write_byte(0x80004000u, 0);
/* Netplay: only the local-view feature remains, parked outside the scope. */
view_hits = sim_hits = view_activations = sim_activations = vblanks = 0;
@@ -195,6 +276,54 @@ int main() {
mod_runtime_activate_plugins();
check(view_activations == 1 && sim_activations == 0,
"only the local-view plugin activates in a match");
check(out_activations == 2, "the host-output plugin activates in a match too");
out_hits = 0;
cpu = CPUState{};
cpu.gpr[31] = 0x80005550u;
cpu.read_word = psx_read_word; cpu.write_word = psx_write_word;
cpu.read_half = psx_read_half; cpu.write_half = psx_write_half;
cpu.read_byte = psx_read_byte; cpu.write_byte = psx_write_byte;
/* A live pad exchange: select port 1, send 0x01, so SIO holds RX data
* and a pending ACK that a bus read would consume or age. */
sio_init();
sio_set_pad_connected(0, 1);
sio_write(0x1F80104Au, 0x1003u); /* TX_EN | SELECT | ACK_IRQ_EN, port 1 */
sio_write(0x1F801040u, 0x01u); /* pad address byte */
sio_tick(2000); /* exchange done: RX byte + ACK pending */
check((sio_peek_stat() & 0x0002u) != 0, "fixture: RX_RDY set (pending RX byte)");
check(sio_peek_ack_visible_reads() > 0, "fixture: ACK counter > 0");
const int ack0 = sio_peek_ack_visible_reads();
const uint16_t stat0 = sio_peek_stat(), ctrl0 = sio_peek_ctrl();
const uint8_t rx0 = sio_peek_rx_data();
psx_write_word(0x80004004u, 0x11111111u);
psx_write_byte(0x80004008u, 0x22u);
mmio_bus_accesses = 0;
const CPUState before = cpu;
check(psx_mod_function_entry(&cpu, 0x80003200u) == 0 && out_hits == 1,
"a host-output hook runs in the shared simulation, cannot finish it");
check(out_scope == 1 && !psx_mod_host_output_scope(),
"host-output scope is set only while its hook runs");
check(cpu.gpr[2] == 0 && std::memcmp(&cpu, &before, sizeof cpu) == 0,
"a host-output hook's CPU register changes do not reach the shared CPU");
check(mmio_bus_accesses == 0,
"a host-output hook makes no MMIO bus access (reads or cpu->write_*)");
check((sio_peek_stat() & 0x0002u) != 0,
"SIO RX_RDY still set: the hook did not pop the RX byte");
check(sio_peek_ack_visible_reads() == ack0,
"SIO ACK counter unchanged: the hook's SIO_STAT reads did not age it");
check(sio_peek_stat() == stat0 && sio_peek_ctrl() == ctrl0 &&
sio_peek_rx_data() == rx0,
"SIO stat, ctrl and RX data unchanged by the hook");
check(psx_read_word(0x80004004u) == 0x11111111u &&
psx_read_byte(0x80004008u) == 0x22u,
"cpu->write_* from a host-output hook do not reach guest RAM");
check(out_peeks[0] == 0 && out_peeks[1] == 0 && out_peeks[3] == 0 &&
out_peeks[4] == 0,
"MMIO observation reads return 0 (no device access)");
check(out_peeks[2] == psx_read_byte(0x80004000u),
"RAM observation reads still see guest RAM");
check(psx_read_byte(0x80004000u) == 0,
"a host-output plugin's guest write is refused online");
psx_mod_function_entry(&cpu, 0x80003000u);
psx_mod_function_entry(&cpu, 0x80003100u);
mod_runtime_on_vblank();
+2
View File
@@ -114,6 +114,8 @@ extern "C" void psx_write_half(uint32_t a, uint16_t v) {
ram[o] = (uint8_t)v;
ram[o + 1] = (uint8_t)(v >> 8);
}
/* Side-effect-free peek (memory.c); host-output observation reads. */
extern "C" int psx_peek_word(uint32_t, uint32_t* out) { *out = 0; return 0; }
extern "C" uint32_t psx_read_word(uint32_t a) {
const uint32_t o = a & 0x1fffffu;
return (uint32_t)ram[o] | ((uint32_t)ram[o + 1] << 8) |