mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge validated renderer fixes into generic post-effect pipeline
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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` | ✓ | | ✓ |
|
||||
|
||||
@@ -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 @@
|
||||
be595cf0213678b41119568563c77352c0e69862cec44def5f75e13605c02519
|
||||
77a1ea51cf0498290de3f78b4fb42ed51640dcee62b379ff44cde8b47cc5714e
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user