feat(video): opt-in sub-pixel vertex precision + perspective-correct textures

Makes the parked projection/perspective work reachable and gets it into the
renderers people actually use. Addresses issue #92 (PGXP-style geometry and
texture correction).

The GTE side cache, SWC2 RAM provenance, GP0 triangle preparation and the
software-renderer consumption path already landed in 2ceaf5a ("park projection
and perspective correction", from kem0x PR #14), but were unreachable: the
generic setters gte_geometry_correction_set() / gpu_texture_correction_set()
had no production caller, and the corrected vertices went straight to
sw_set_precise_triangle(), bypassing the renderer vtable — while the default
renderer is OpenGL.

This adds:

- [video] geometry_correction and [video] perspective_texturing, in game.toml
  and settings.toml, both defaulting false. Independent knobs; the launcher
  seed marks them present so a save round-trips a hand-edited key instead of
  dropping it. Startup names the active pair, and notes when geometry
  correction is on below supersampling 2 (where it cannot be seen).
- gr_set_precise_triangle() / gr_set_perspective_triangle() on the
  GpuRenderBackend vtable, so gpu.c hands the correction to whichever backend
  is live rather than to the software rasterizer directly. NULL entries no-op.
- OpenGL and Vulkan consumption. Positions are already float end-to-end, so
  sub-pixel vertices drop in; the readback dirty rect widens by one pixel to
  cover them. Perspective UVs use a per-vertex a_q weight and clip coordinates
  pre-multiplied by w = 1/q, letting the hardware's own perspective divide
  interpolate a smooth UV varying beside the affine noperspective one. a_q == 0
  — the default, and what every prim gets with the feature off — makes w
  exactly 1.0 and selects the affine varying, so the faithful path is the
  pre-feature pipeline unchanged.

The PS1-visible GTE SXY FIFO stays integer either way: guest-side screen-bounds
culling and SXY readback are untouched, and this remains visual-only.

Also fixes a pre-existing concurrent-build race found while validating: every
runtime target runs embed_spirv.py over the same shaders, and each wrote
"<shader>.spv" next to the source, so under -j N one process deleted the file
another had just written (FileNotFoundError on a shader that compiled fine).
The intermediates now go to a per-invocation temp dir.

Parked-work provenance retained; see ENHANCEMENTS.md G1.

Co-authored-by: Kareem Olim <kareemolim@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-08-05 14:02:58 -07:00
parent 7f7fbc8e1a
commit c8b692eda2
18 changed files with 642 additions and 34 deletions
+110
View File
@@ -400,3 +400,113 @@ stays, inert + A/B-able). What was learned, so the next attempt starts ahead:
- **Groundwork landed on `feat/ws-2d-scene-pillarbox`:** census `tagged`
column, `ws_hud_mode` live A/B, and the untagged-rect scoping that makes
`nw_hud_corners` safe to experiment with on tag titles.
---
## G1 — Sub-pixel vertex precision + perspective-correct textures (issue #92)
PS1 polygon jitter ("wobble", "bouncing lines") and warped floor/wall textures
have one root cause each, both in the fixed-point geometry pipeline:
- **Jitter.** The GTE computes the projected screen position in 16.16, then
saturates it to an integer pixel when it pushes SXY. The fraction is thrown
away. A slowly moving mesh therefore snaps its vertices between whole pixels
and the model shimmers.
- **Texture warp.** The GPU interpolates UV *affinely* across a triangle, with
no 1/z term, so a large floor or wall polygon's texture swims as the camera
moves.
Both are addressed as **opt-in, visual-only** enhancements. The PS1-visible GTE
SXY FIFO stays integer and fully faithful — a game's own post-projection
screen-bounds culls and any SXY readback see exactly what hardware produces.
Nothing about guest state changes; the correction lives entirely on the host
render path.
### Configuration
```toml
[video]
geometry_correction = true # sub-pixel vertex precision (kills the wobble)
perspective_texturing = true # perspective-correct UVs on world polygons
supersampling = 2 # REQUIRED for geometry_correction to be visible
```
Both default **false** (the faithful floor). They are independent — a title may
want stable geometry without changing texture mapping. Settable per-game in
`game.toml` and per-player in `settings.toml` (the player's file wins, and a
launcher save round-trips both keys rather than dropping them).
`geometry_correction` needs `supersampling >= 2`: at native resolution the
corrected position rounds back to the pixel it started on, so there is nothing
to see. The runtime prints a note at startup when it is on at scale 1.
### How it works
The recompiler emits GTE commands as calls to a single runtime entry point
(`gte_execute`), which both the compiled backend and the dirty-RAM interpreter
share — so unlike an interpreter/dynarec emulator there is no dispatch hook to
add, just one funnel to instrument.
1. **`runtime/src/gte.cpp`** — RTPS/RTPT keep the discarded 16.16 fraction in a
side cache keyed by the packed SXY word it rounded to (`geom_note`).
Saturated (off-screen) projections are rejected: they carry no usable
sub-pixel information.
2. **SWC2 provenance** — the recompiler, strict translator, dirty-RAM interp,
overlay ABI (v14) and fallback interp all call
`gte_precision_store_word(addr, reg)` when a projection register is stored to
guest RAM, recording *which RAM address* a projection landed at. Perspective
texturing only fires when all three of a triangle's position words came from
such a store at that exact DMA packet address — which preserves the
association through ordering-table reordering and rejects CPU-built UI and
2D sprites outright. A plain `sw` to a tracked address invalidates it.
3. **`runtime/src/gpu.c`** — `prepare_precise_triangle()` /
`prepare_texture_triangle()` look the packet up per triangle and hand the
result to the renderer facade as sideband state for the next draw
(`gr_set_precise_triangle` / `gr_set_perspective_triangle`).
4. **All three renderers consume it.** Software uses the fractional positions
in its supersampled mirror; OpenGL and Vulkan take them as float vertex
positions directly. For perspective UVs both GPU backends carry a per-vertex
`a_q` weight and emit clip coordinates pre-multiplied by `w = 1/q`, so the
hardware's own perspective divide interpolates a `smooth` UV varying while
the affine `noperspective` one stays available. **`a_q == 0` (the default)
makes `w` exactly 1.0 and selects the affine varying — the pre-feature
pipeline, unchanged.**
Save states and speculative native-validation passes drop host-only provenance
(`gte_precision_timeline_invalidate`, `gte_precision_speculative_begin/end`) so
a rewind can never resurrect a stale projection.
### Validation story
By construction this feature *diverges* from stock hardware output, so the
Beetle oracle cannot be the judge of the corrected frame. What the oracle still
pins is the part that must not move: **with both flags off the output is
byte-identical to the pre-feature build**, and guest-visible GTE state is
identical either way (the SXY FIFO is untouched in both). That reduces
validation to (a) an off/off pixel-identity check against the oracle, and
(b) human A/B of the on/off frames on a 3D title.
`gte_geometry_correction_hits()` and `gpu_texture_correction_hits()` report how
many vertices/triangles were actually corrected — the "is this doing anything
on this title" counter, and the thing to check first when a title shows no
visible change.
### Provenance
The GTE side cache, SWC2 provenance tracking, GP0 triangle preparation and the
software-renderer consumption path were contributed by **Kareem Olim (kem0x)**
in [PR #14](https://github.com/mstan/psxrecomp/pull/14) and parked in commit
`2ceaf5a` (see `docs/internal/upstream/kem0x-pr14-projection-perspective.md`),
disabled pending generic setters. This work adds the opt-in configuration, the
renderer-facade seam, and OpenGL + Vulkan support.
### Status / next
- **Done:** config plumbing (game.toml + settings.toml + launcher seed
round-trip), renderer-facade sideband, software / OpenGL / Vulkan consumption,
`[video]` plumbing unit test.
- **Open:** per-title A/B validation. Ape Escape is the obvious first 3D
subject (Tomba 1/2 and MMX5/6 are largely 2D, where neither knob does much).
- **Open:** launcher (recomp-ui) toggles. The keys round-trip through
`settings.toml` today, but there is no UI row yet — that lives in the
recomp-ui repo.
+19
View File
@@ -375,6 +375,25 @@ See [`docs/STRING_TRANSLATION.md`](docs/STRING_TRANSLATION.md); *Tsumu Light*
| **Vulkan** | **Experimental.** Built when the SDK is present, opt-in at runtime; falls back to OpenGL if unavailable. |
| **Software** | CPU rasterizer — the reference look, and the most portable fallback. |
### Geometry enhancements (opt-in)
Two optional fixes for the PS1's fixed-point geometry pipeline, off by default
so the faithful look stays the baseline. Set them in `game.toml` or the
player's `settings.toml`:
```toml
[video]
geometry_correction = true # sub-pixel vertex precision — removes polygon
# jitter/wobble. Needs supersampling >= 2.
perspective_texturing = true # perspective-correct UVs — removes the texture
# warp on large floor/wall polygons.
supersampling = 2
```
Both are visual-only: the GTE's guest-visible screen coordinates stay integer
and fully faithful, so game logic and culling are unaffected. Supported on all
three renderers. See [`ENHANCEMENTS.md`](ENHANCEMENTS.md) §G1.
## How to use PSXRecomp
PSXRecomp takes a PlayStation disc image and creates a recompilation project
@@ -20,3 +20,19 @@ Excluded: all Web host exports and every title-specific state, action, address,
or presentation hook from the source commit. The separate 60 Hz presentation
experiment is parked on its own branch. Authorship is retained in the commit
trailer.
## UNPARKED
Both are now reachable enhancements, wired up in response to
[issue #92](https://github.com/mstan/psxrecomp/issues/92):
- `[video] geometry_correction` and `[video] perspective_texturing` call the
generic setters this branch left dangling (`gte_geometry_correction_set`,
`gpu_texture_correction_set`), from `game.toml` or `settings.toml`;
- the software-renderer-only consumption path became a `GpuRenderBackend`
sideband (`gr_set_precise_triangle` / `gr_set_perspective_triangle`), and
OpenGL and Vulkan now consume it too — the parked code only reached the
software rasterizer, while the default renderer is OpenGL.
See `ENHANCEMENTS.md` §G1 for the shipped design. This file stays as the record
of what was imported and what was deliberately left out.
+16
View File
@@ -316,6 +316,22 @@ if(BUILD_TESTING)
add_test(NAME recompiler_patch_cxx17_header_test
COMMAND recompiler_patch_cxx17_header_test)
# [video] geometry_correction / perspective_texturing opt-in plumbing.
add_executable(video_enhancement_settings_test
tests/video_enhancement_settings_test.cpp
src/config_loader.cpp
src/ps1_exe_parser.cpp
)
target_include_directories(video_enhancement_settings_test PRIVATE
include
src
lib/fmt/include
lib/toml11
)
target_link_libraries(video_enhancement_settings_test PRIVATE fmt)
add_test(NAME video_enhancement_settings_test
COMMAND video_enhancement_settings_test)
# Needs bios/SCPH1001.toml — run from the framework root.
add_test(NAME bios_address_model_test
COMMAND bios_address_model_test
+22
View File
@@ -523,6 +523,14 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (video.contains("offer_vulkan")) {
rt.video_offer_vulkan = toml::find<bool>(video, "offer_vulkan");
}
if (video.contains("geometry_correction")) {
rt.video_geometry_correction =
toml::find<bool>(video, "geometry_correction");
}
if (video.contains("perspective_texturing")) {
rt.video_perspective_texturing =
toml::find<bool>(video, "perspective_texturing");
}
if (video.contains("crt_filter")) {
const auto mode = toml::find<std::string>(video, "crt_filter");
if (mode == "raw") rt.video_screen_kind = 0;
@@ -2040,6 +2048,14 @@ UserSettings load_user_settings(const fs::path& path) {
if (m == "nearest") { s.texture_filter = 0; s.has_texture_filter = true; }
else if (m == "bilinear") { s.texture_filter = 1; s.has_texture_filter = true; }
});
if (v.contains("geometry_correction")) try_get([&]{
s.geometry_correction = toml::find<bool>(v, "geometry_correction");
s.has_geometry_correction = true;
});
if (v.contains("perspective_texturing")) try_get([&]{
s.perspective_texturing = toml::find<bool>(v, "perspective_texturing");
s.has_perspective_texturing = true;
});
if (v.contains("crt_filter")) try_get([&]{
const auto m = toml::find<std::string>(v, "crt_filter");
if (m == "raw") { s.screen_kind = 0; s.has_screen_kind = true; }
@@ -2263,6 +2279,12 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
f << "antialiasing = " << (s.antialiasing ? "true" : "false") << "\n";
if (s.has_texture_filter)
f << "texture_filtering = \"" << (s.texture_filter ? "bilinear" : "nearest") << "\"\n";
if (s.has_geometry_correction)
f << "geometry_correction = "
<< (s.geometry_correction ? "true" : "false") << "\n";
if (s.has_perspective_texturing)
f << "perspective_texturing = "
<< (s.perspective_texturing ? "true" : "false") << "\n";
if (s.has_screen_kind) {
const char* k = s.screen_kind == 1 ? "crt"
: s.screen_kind == 2 ? "composite"
+27
View File
@@ -329,6 +329,29 @@ struct RuntimeConfig {
// fallback. Stored as VIDEO_RENDERER_*.
int video_renderer = DEFAULT_VIDEO_RENDERER;
// geometry_correction: sub-pixel vertex precision (the PGXP-style fix for
// PS1 polygon jitter/wobble). The GTE projects in 16.16 and then throws the
// fraction away when it saturates SXY to integer screen pixels; vertices of
// a moving mesh therefore snap between whole pixels and the model appears to
// shimmer. With this on, the GTE keeps the discarded fraction in a side
// cache and the rasterizer places the vertex between native pixels.
//
// The PS1-visible SXY FIFO stays integer and fully faithful — the game's own
// screen-bounds culls and any SXY readback see exactly what hardware would
// produce. This is visual-only. Default off (faithful floor).
//
// Only observable at [video] supersampling >= 2: at native resolution the
// corrected position rounds back to the same pixel it started on.
bool video_geometry_correction = false;
// perspective_texturing: perspective-correct UV interpolation for textured
// world polygons (the PS1 GPU interpolates UV affinely, which warps textures
// on large floor/wall polygons as the camera moves). Uses the exact SWC2
// projection provenance — a polygon only qualifies when every one of its
// position words was written to that DMA packet address by a projection
// store — so CPU-built UI and 2D sprites are never touched. Default off.
bool video_perspective_texturing = false;
// offer_vulkan: expose the experimental Vulkan renderer in the launcher.
// Defaults false even for Vulkan-enabled builds; developers must opt in per
// game once visuals are validated.
@@ -969,6 +992,10 @@ struct UserSettings {
bool has_window_width = false; int window_width = 1280; // -> 1280x960
bool has_antialiasing = false; bool antialiasing = true;
bool has_texture_filter = false; int texture_filter = 0; // 0=nearest,1=bilinear
// Sub-pixel vertex precision / perspective-correct UVs (see RuntimeConfig).
// Both default off — the faithful floor — and are player-selectable.
bool has_geometry_correction = false; bool geometry_correction = false;
bool has_perspective_texturing = false; bool perspective_texturing = false;
bool has_screen_kind = false; int screen_kind = 0; // 0..3 (ScreenKind)
bool has_auto_skip_fmv = false; bool auto_skip_fmv = false; // skip FMVs
// Turbo through in-game load screens: while the CD data stream is active, run
@@ -0,0 +1,159 @@
/* video_enhancement_settings_test — [video] geometry_correction /
* perspective_texturing plumbing.
*
* These two knobs are the opt-in for the sub-pixel vertex precision and
* perspective-correct UV enhancements (psxrecomp issue #92). The underlying
* GTE/GPU machinery has its own unit coverage in the runtime suite; what this
* test pins is the part that decides whether it is ever switched on:
*
* 1. both default OFF (the faithful floor) when game.toml says nothing;
* 2. game.toml [video] turns them on;
* 3. settings.toml (the player's file) parses them;
* 4. save_user_settings round-trips them — a launcher save must not silently
* drop a hand-edited key, which would look like "the setting does nothing".
*/
#include "config_loader.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
namespace fs = std::filesystem;
static int failures = 0;
static void check(bool cond, const char* what) {
if (!cond) {
std::fprintf(stderr, "FAIL: %s\n", what);
++failures;
}
}
static fs::path write_temp(const std::string& name, const std::string& body) {
fs::path p = fs::temp_directory_path() / name;
std::ofstream f(p, std::ios::binary | std::ios::trunc);
f << body;
return p;
}
/* game.toml needs the fields load_game_config() requires; keep this to the
* documented minimum plus whichever [video] body the case under test needs.
*
* [runtime] is required even when empty: parse_runtime_block() early-returns
* on a config with no [runtime] table, so a game.toml carrying [video] alone
* would silently ignore every video key. Every shipped game.toml has both. */
static fs::path write_game_toml(const std::string& name,
const std::string& video_block) {
return write_temp(name,
"[game]\n"
"name = \"probe\"\n"
"exe = \"probe.exe\"\n"
"load_address = \"0x80010000\"\n"
"entry_pc = \"0x80010000\"\n"
"text_size = \"0x1000\"\n"
"[recompiler]\n"
"seeds = \"seeds.json\"\n"
"[runtime]\n"
+ video_block);
}
static void test_defaults_off() {
fs::path p = write_game_toml("psxrecomp_pgxp_default.toml", "");
auto gc = PSXRecompV4::load_game_config(p);
check(!gc.runtime.video_geometry_correction,
"geometry_correction defaults off (faithful floor)");
check(!gc.runtime.video_perspective_texturing,
"perspective_texturing defaults off (faithful floor)");
fs::remove(p);
}
static void test_game_toml_opt_in() {
fs::path p = write_game_toml("psxrecomp_pgxp_on.toml",
"[video]\n"
"geometry_correction = true\n"
"perspective_texturing = true\n");
auto gc = PSXRecompV4::load_game_config(p);
check(gc.runtime.video_geometry_correction,
"[video] geometry_correction = true is honoured");
check(gc.runtime.video_perspective_texturing,
"[video] perspective_texturing = true is honoured");
fs::remove(p);
}
/* The two knobs are independent: a title may want stable geometry without
* changing texture mapping (or the reverse) — so a single flag would be wrong. */
static void test_knobs_independent() {
fs::path p = write_game_toml("psxrecomp_pgxp_geom_only.toml",
"[video]\n"
"geometry_correction = true\n");
auto gc = PSXRecompV4::load_game_config(p);
check(gc.runtime.video_geometry_correction,
"geometry_correction alone turns on");
check(!gc.runtime.video_perspective_texturing,
"geometry_correction alone leaves perspective_texturing off");
fs::remove(p);
}
static void test_user_settings_read() {
fs::path p = write_temp("psxrecomp_pgxp_settings.toml",
"[video]\n"
"geometry_correction = true\n"
"perspective_texturing = false\n");
auto us = PSXRecompV4::load_user_settings(p);
check(!us.parse_error, "settings.toml parses");
check(us.has_geometry_correction && us.geometry_correction,
"settings.toml geometry_correction = true read");
check(us.has_perspective_texturing && !us.perspective_texturing,
"settings.toml perspective_texturing = false read (explicit off)");
fs::remove(p);
}
/* An absent key must stay absent, so layering leaves the game.toml value alone
* instead of forcing it off. */
static void test_user_settings_absent_key() {
fs::path p = write_temp("psxrecomp_pgxp_settings_empty.toml",
"[video]\n"
"supersampling = 2\n");
auto us = PSXRecompV4::load_user_settings(p);
check(!us.has_geometry_correction,
"absent geometry_correction leaves has_* false");
check(!us.has_perspective_texturing,
"absent perspective_texturing leaves has_* false");
fs::remove(p);
}
static void test_user_settings_round_trip() {
PSXRecompV4::UserSettings out;
out.geometry_correction = true; out.has_geometry_correction = true;
out.perspective_texturing = true; out.has_perspective_texturing = true;
fs::path p = fs::temp_directory_path() / "psxrecomp_pgxp_roundtrip.toml";
check(PSXRecompV4::save_user_settings(p, out), "save_user_settings writes");
auto back = PSXRecompV4::load_user_settings(p);
check(!back.parse_error, "written settings.toml re-parses");
check(back.has_geometry_correction && back.geometry_correction,
"geometry_correction survives a save/load round trip");
check(back.has_perspective_texturing && back.perspective_texturing,
"perspective_texturing survives a save/load round trip");
fs::remove(p);
}
int main() {
test_defaults_off();
test_game_toml_opt_in();
test_knobs_independent();
test_user_settings_read();
test_user_settings_absent_key();
test_user_settings_round_trip();
if (failures) {
std::fprintf(stderr, "video_enhancement_settings_test: %d failure(s)\n",
failures);
return 1;
}
std::printf("PASS: [video] geometry_correction / perspective_texturing "
"plumbing\n");
return 0;
}
+9
View File
@@ -205,6 +205,15 @@ extern void gte_precision_timeline_invalidate(void);
extern void gte_precision_speculative_begin(void);
extern void gte_precision_speculative_end(void);
extern void gte_precision_store_word(uint32_t addr, uint8_t reg);
/* Sub-pixel vertex precision ([video] geometry_correction). Enables the side
* cache that retains the 16.16 projection fraction the GTE discards when it
* saturates SXY to integer screen pixels. Guest-visible GTE state is
* unchanged — this is a visual-only enhancement, default off. */
extern void gte_geometry_correction_set(int enabled);
extern int gte_geometry_correction_enabled(void);
/* Corrected vertices consumed since the last gte_geometry_correction_set —
* the "is this actually doing anything on this title" counter. */
extern uint32_t gte_geometry_correction_hits(void);
/* ============================================================================
* Dispatch call contract (Bug D / wild-return family fix)
+7
View File
@@ -283,6 +283,13 @@ int psx_ws_is_cull_w_imm(uint32_t imm);
* never opted in must never have its live code pattern-scanned and rewritten. */
void gpu_ws_set_auto_hooks(int cull_on, int backdrop_on);
int psx_ws_auto_cull_on(void);
/* Perspective-correct UV interpolation for textured world polygons
* ([video] perspective_texturing). Also turns on the SWC2 RAM-provenance
* tracking it needs, so a polygon only qualifies when every position word in
* its DMA packet was written by a GTE projection store. Default off. */
void gpu_texture_correction_set(int enabled);
/* Triangles drawn with perspective-correct UVs since startup. */
uint32_t gpu_texture_correction_hits(void);
/* GTE-activity gameplay detector ([widescreen] gte_game_mode) for 3D titles
* with no sprite-tag helper: gte.cpp notes every RTPS/RTPT projection; a frame
* that projects enough vertices is stamped as gameplay. */
+23
View File
@@ -39,6 +39,22 @@ void gr_set_mask_bits(int set_bit, int check_bit);
void gr_set_texture_window(uint32_t raw);
void gr_set_color_modulation(int r, int g, int b, int raw_texture);
/* Sub-pixel vertex override for the NEXT triangle ([video] geometry_correction).
* x/y are signed 16.16 screen coordinates carrying the projection fraction the
* GTE discarded; the integer positions still passed to gr_draw_*_triangle
* remain authoritative for anything that must stay faithful (dirty-rect
* tracking, the software path's native VRAM write). enabled == 0 clears the
* override. Backends that do not implement it ignore the call, so the integer
* path is what they draw. The override is consumed by the next triangle. */
void gr_set_precise_triangle(int enabled,
int32_t x0, int32_t y0,
int32_t x1, int32_t y1,
int32_t x2, int32_t y2);
/* Perspective-correct UV weights for the NEXT triangle ([video]
* 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);
/* Primitives */
void gr_fill_rect(int x, int y, int w, int h, uint16_t color);
void gr_copy_rect(int src_x, int src_y, int dst_x, int dst_y, int w, int h);
@@ -119,6 +135,13 @@ typedef struct GpuRenderBackend {
void (*set_mask_bits)(int set_bit, int check_bit);
void (*set_texture_window)(uint32_t raw);
void (*set_color_modulation)(int r, int g, int b, int raw_texture);
/* Optional (NULL = unsupported, facade no-ops): sub-pixel vertex override
* and perspective UV weights for the next triangle. See gr_* above. */
void (*set_precise_triangle)(int enabled,
int32_t x0, int32_t y0,
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 (*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,
+9 -3
View File
@@ -11,6 +11,8 @@ layout(location = 3) flat in ivec2 v_clut; /* CLUT base, VRAM px */
layout(location = 4) flat in int v_depth; /* 0=4bit 1=8bit 2=15bit */
layout(location = 5) flat in int v_raw; /* 1 = no colour modulation */
layout(location = 6) flat in ivec4 v_limits; /* prim uv bounds (inclusive) */
layout(location = 7) smooth in vec2 v_uv_p; /* perspective-correct UV */
layout(location = 8) flat in int v_persp; /* 1 = use v_uv_p, else affine */
layout(set = 0, binding = 0) uniform usampler2D u_vram;
@@ -52,8 +54,12 @@ vec3 col5(int raw) {
}
void main() {
int stp; vec3 rgb;
/* v_persp is 0 for every prim unless [video] perspective_texturing is on
* AND this prim's packet carried full GTE projection provenance, so the
* default is the PS1's affine (noperspective) mapping. */
vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;
if (pc.u_filter == 0) {
int raw = fetch_texel(int(floor(v_uv.x)), int(floor(v_uv.y)));
int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));
if (raw == 0) discard;
rgb = col5(raw);
stp = (raw >> 15) & 1;
@@ -62,8 +68,8 @@ void main() {
* (cutout + STP authority), the neighbours lie toward the sub-texel
* offset and clamp to v_limits, and each texel's weight is gated by its
* opacity with the colour renormalised. */
int iu = int(floor(v_uv.x)), iv = int(floor(v_uv.y));
float fx = v_uv.x - float(iu) - 0.5, fy = v_uv.y - float(iv) - 0.5;
int iu = int(floor(uv.x)), iv = int(floor(uv.y));
float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;
int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;
fx = abs(fx); fy = abs(fy);
int c00 = fetch_texel(iu, iv);
+15 -3
View File
@@ -13,6 +13,10 @@ layout(location = 4) in vec2 a_clut;
layout(location = 5) in float a_depth;
layout(location = 6) in float a_raw;
layout(location = 7) in vec4 a_limits;
/* Perspective weight ([video] perspective_texturing). 0 = affine, the
* PS1-faithful default: w below is then exactly 1.0 and the fragment shader
* reads the noperspective varying, i.e. the pre-feature pipeline unchanged. */
layout(location = 8) in float a_q;
layout(push_constant) uniform PC {
float u_shift;
@@ -31,14 +35,22 @@ layout(location = 3) flat out ivec2 v_clut;
layout(location = 4) flat out int v_depth;
layout(location = 5) flat out int v_raw;
layout(location = 6) flat out ivec4 v_limits;
layout(location = 7) smooth out vec2 v_uv_p; /* perspective-correct UV */
layout(location = 8) flat out int v_persp;
void main() {
v_uv = a_uv; v_col = a_col;
v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;
v_persp = (a_q > 0.0) ? 1 : 0;
v_tpage = ivec2(a_tpage + 0.5);
v_clut = ivec2(a_clut + 0.5);
v_depth = int(a_depth + 0.5);
v_raw = int(a_raw + 0.5);
v_limits = ivec4(floor(a_limits + 0.5));
gl_Position = vec4((a_pos.x + pc.u_shift + pc.u_xoff) / pc.u_xhalf - 1.0,
(a_pos.y + pc.u_shift) / 256.0 - 1.0, 0.0, 1.0);
/* Perspective-correct UV rides the standard w divide: emit clip coords
* pre-multiplied by w = 1/q so post-divide NDC is unchanged while the
* rasterizer interpolates v_uv_p hyperbolically. a_q == 0 => w == 1.0. */
float w = (a_q > 0.0) ? (1.0 / a_q) : 1.0;
vec2 ndc = vec2((a_pos.x + pc.u_shift + pc.u_xoff) / pc.u_xhalf - 1.0,
(a_pos.y + pc.u_shift) / 256.0 - 1.0);
gl_Position = vec4(ndc * w, 0.0, w);
}
+6 -6
View File
@@ -2795,16 +2795,16 @@ static void prepare_precise_triangle(uint32_t w0, uint32_t w1, uint32_t w2,
uint32_t words[3] = { w0, w1, w2 };
int32_t fx[3], fy[3];
const int geometry_enabled = gte_geometry_correction_enabled();
sw_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
if (!geometry_enabled) {
sw_set_precise_triangle(0, 0,0, 0,0, 0,0);
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
return;
}
for (int i = 0; i < 3; i++) {
int32_t raw_x, raw_y;
parse_vertex(words[i], &raw_x, &raw_y);
if (!gte_geometry_correction_lookup(words[i], &fx[i], &fy[i])) {
sw_set_precise_triangle(0, 0,0, 0,0, 0,0);
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
return;
}
fx[i] = (int32_t)((int64_t)fx[i] +
@@ -2812,14 +2812,14 @@ static void prepare_precise_triangle(uint32_t w0, uint32_t w1, uint32_t w2,
fy[i] = (int32_t)((int64_t)fy[i] +
(int64_t)(vy[i] - raw_y) * 65536);
}
sw_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], fx[2],fy[2]);
gr_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], fx[2],fy[2]);
}
/* Enable perspective UVs only when every position word came from an exact
* SWC2 projection store at that same DMA packet address. This preserves the
* association through ordering-table reordering and rejects CPU-built UI. */
static void prepare_texture_triangle(int i0, int i1, int i2) {
sw_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
if (!s_texture_correction_enabled || gp0_cmd_source_addr == 0xFFFFFFFFu)
return;
int indices[3] = { i0, i1, i2 };
@@ -2835,7 +2835,7 @@ static void prepare_texture_triangle(int i0, int i1, int i2) {
if (q[1] > qmax) qmax = q[1];
if (q[2] > qmax) qmax = q[2];
if (qmax <= 0.0f) return;
sw_set_perspective_triangle(1, q[0] / qmax, q[1] / qmax, q[2] / qmax);
gr_set_perspective_triangle(1, q[0] / qmax, q[1] / qmax, q[2] / qmax);
}
/* Write a single pixel to VRAM with draw area clipping and mask bit handling */
+82 -12
View File
@@ -355,11 +355,25 @@ static GLuint s_scratch_tex = 0, s_scratch_fbo = 0;
static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0;
static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0;
/* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4)
* — per-prim texture state in flat attributes so prims batch (see flush_tex_batch). */
#define TEXV 19
* semi(1) q(1)
* — per-prim texture state in flat attributes so prims batch (see flush_tex_batch).
* q is the perspective weight ([video] perspective_texturing): 0 = affine, the
* PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the
* fragment shader read the noperspective varying — i.e. bit-identical to the
* pre-feature pipeline. */
#define TEXV 20
static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0;
static GLuint s_pack_prog = 0, s_stencil_prog = 0, s_empty_vao = 0;
/* Sub-pixel vertex override + perspective UV weights for the next triangle
* ([video] geometry_correction / perspective_texturing; see gpu_render.h).
* gpu.c sets these immediately before the matching gr_draw_*_triangle and they
* are consumed by it. All-zero == the faithful integer/affine path. */
static int s_pc_valid = 0; /* sub-pixel positions present */
static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */
static int s_pq_valid = 0; /* perspective weights present */
static float s_pq[3];
/* TEX program uniforms. */
static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1;
static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1;
@@ -816,15 +830,19 @@ static const char *TEX_VS =
"layout(location=6) in float a_raw;\n"
"layout(location=7) in vec4 a_limits;\n"
"layout(location=8) in float a_semi;\n"
"layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n"
"uniform float u_shift;\n"
"uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n"
"uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n"
"uniform float u_xscale; /* native-wide 2D-backdrop x-stretch; 1 canonical */\n"
"uniform float u_xcenter;/* stretch centre in VRAM px; 0 canonical */\n"
"noperspective out vec2 v_uv; noperspective out vec4 v_col;\n"
"smooth out vec2 v_uv_p; /* perspective-correct UV (used when v_persp!=0) */\n"
"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"
"void main(){ v_uv = a_uv; v_col = a_col;\n"
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\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"
@@ -837,10 +855,17 @@ static const char *TEX_VS =
" float l = u_xcenter - h, r = u_xcenter + h;\n"
" if (xb < l) xb = l + (xb-l)*s; else if (xb > r) xb = r + (xb-r)*s;\n"
" } else xb = (xb - u_xcenter)*u_xscale + u_xcenter;\n"
" gl_Position = vec4((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0, 0.0, 1.0); }\n";
" /* Perspective-correct UV rides the standard w divide: emit clip coords\n"
" * pre-multiplied by w = 1/q so the post-divide NDC is unchanged while\n"
" * the rasterizer interpolates the smooth varying hyperbolically. With\n"
" * a_q == 0 (feature off) w is exactly 1.0 and this is the old expression. */\n"
" float w = (a_q > 0.0) ? (1.0 / a_q) : 1.0;\n"
" vec2 ndc = vec2((xb+u_shift+u_xoff)/u_xhalf - 1.0, (a_pos.y+u_shift)/256.0 - 1.0);\n"
" gl_Position = vec4(ndc * w, 0.0, w); }\n";
static const char *TEX_FS =
"#version 330\n"
"noperspective in vec2 v_uv; noperspective in vec4 v_col;\n"
"smooth in vec2 v_uv_p; flat in int v_persp;\n"
"out vec4 frag; out vec4 blend_factor;\n"
"flat in ivec2 v_tpage; /* texture page base, VRAM px */\n"
"flat in ivec2 v_clut; /* CLUT base, VRAM px */\n"
@@ -880,8 +905,12 @@ static const char *TEX_FS =
"}\n"
"void main(){\n"
" int stp; vec3 rgb;\n"
" /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n"
" * AND this prim's packet carried full GTE projection provenance, so the\n"
" * default is the PS1's affine (noperspective) mapping. */\n"
" vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n"
" if (u_filter == 0) {\n"
" int raw = fetch_texel(int(floor(v_uv.x)), int(floor(v_uv.y)));\n"
" int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n"
" if (raw == 0) discard;\n"
" rgb = col5(raw);\n"
" stp = (raw >> 15) & 1;\n"
@@ -892,8 +921,8 @@ static const char *TEX_FS =
" * its opacity with the colour renormalised — so prim edges and\n"
" * cutout borders keep their colour instead of dissolving into the\n"
" * transparent (black) neighbour and discarding whole edge columns. */\n"
" int iu = int(floor(v_uv.x)), iv = int(floor(v_uv.y));\n"
" float fx = v_uv.x - float(iu) - 0.5, fy = v_uv.y - float(iv) - 0.5;\n"
" int iu = int(floor(uv.x)), iv = int(floor(uv.y));\n"
" float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;\n"
" int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n"
" fx = abs(fx); fy = abs(fy);\n"
" int c00 = fetch_texel(iu, iv);\n"
@@ -1291,6 +1320,11 @@ static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) {
if (xs[i] < x0) x0 = xs[i]; if (xs[i] > x1) x1 = xs[i];
if (ys[i] < y0) y0 = ys[i]; if (ys[i] > y1) y1 = ys[i];
}
/* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position
* it was rounded from, so the corrected prim can touch one more pixel on
* each max edge than the integer bbox covers. Widen before clipping so the
* pack/readback dirty rect never trails the drawn area. */
if (s_pc_valid) { x1 += 1; y1 += 1; }
s_bdg_prims++; /* dbg: prims seen this frame (gate is now per-prim, see bd_prim_gate) */
if (s_ptrace_n < PTRACE_CAP) {
PrimRec *p = &s_ptrace[s_ptrace_n++];
@@ -1647,6 +1681,8 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
flush_tex_batch(); /* flat prim: drain textured draws first (order + program) */
flush_cpu_upload(); /* also drains flat batch if an upload was pending */
mark_prim_dirty(xs, ys, n, 0 /* flat */);
/* Sub-pixel positions only describe a 3-vertex projected triangle. */
const int precise = s_pc_valid && mode == GL_TRIANGLES && n == 3;
/* Lines stay immediate (rare); tris batch for MotK 0x68 starfields. */
if (mode != GL_TRIANGLES || n < 3) {
@@ -1654,8 +1690,8 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
float verts[3 * 6];
float mask_a = s_mask_set ? 1.0f : 0.0f;
for (int i = 0; i < n; i++) {
verts[i*6+0] = (float)xs[i];
verts[i*6+1] = (float)ys[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];
verts[i*6+2] = ((cs[i] & 0x1F) << 3) / 255.0f;
verts[i*6+3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f;
verts[i*6+4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
@@ -1697,8 +1733,8 @@ static void gpu_geometry(GLenum mode, const int *xs, const int *ys,
float mask_a = s_mask_set ? 1.0f : 0.0f;
for (int i = 0; i < n; i++) {
float *v = &s_fb[s_fb_n * 6];
v[0] = (float)xs[i];
v[1] = (float)ys[i];
v[0] = precise ? s_pc_x[i] : (float)xs[i];
v[1] = precise ? s_pc_y[i] : (float)ys[i];
v[2] = ((cs[i] & 0x1F) << 3) / 255.0f;
v[3] = (((cs[i] >> 5) & 0x1F) << 3) / 255.0f;
v[4] = (((cs[i] >> 10) & 0x1F) << 3) / 255.0f;
@@ -1804,7 +1840,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
}
float *vp = &s_tb[s_tb_n * TEXV];
for (int i = 0; i < 3; i++, vp += TEXV) {
vp[0] = (float)xs[i]; vp[1] = (float)ys[i];
vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i];
vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i];
vp[2] = (float)us[i]; vp[3] = (float)vs[i];
vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f;
vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */
@@ -1813,6 +1850,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
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[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
}
s_tb_n += 3;
if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */
@@ -2050,6 +2088,27 @@ static void glb_set_texture_window(uint32_t r) {
sw_set_texture_window(r);
}
static void glb_set_color_modulation(int r,int g,int b,int raw) { s_mod_r=r; s_mod_g=g; s_mod_b=b; s_mod_raw=raw; sw_set_color_modulation(r,g,b,raw); }
/* Sub-pixel / perspective overrides for the next triangle. Forwarded to the
* software rasterizer too: pre-context draws (s_raster_ok == 0) still go there,
* and the GL backend delegates whole prim classes to it. */
static void glb_set_precise_triangle(int enabled,
int32_t x0,int32_t y0, int32_t x1,int32_t y1,
int32_t x2,int32_t y2) {
s_pc_valid = enabled ? 1 : 0;
if (s_pc_valid) {
/* 16.16 native VRAM px -> float. The GL pipeline is float end-to-end,
* so the fraction survives to the rasterizer at any internal scale. */
s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f;
s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f;
s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f;
}
sw_set_precise_triangle(enabled, x0,y0, x1,y1, x2,y2);
}
static void glb_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0;
s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2;
sw_set_perspective_triangle(enabled, q0, q1, q2);
}
static void glb_set_draw_area(int x1,int y1,int x2,int y2) { flush_flat_batch(); flush_tex_batch(); s_area_x1=x1; s_area_y1=y1; s_area_x2=x2; s_area_y2=y2; sw_set_draw_area(x1,y1,x2,y2); }
static void glb_get_draw_area(int *x1,int *y1,int *x2,int *y2) { sw_get_draw_area(x1,y1,x2,y2); }
static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_batch(); s_off_x=x; s_off_y=y; sw_set_draw_offset(x,y); }
@@ -2057,13 +2116,19 @@ static void glb_set_draw_offset(int x,int y) { flush_flat_batch(); flush_tex_bat
/* Pre-context draws (s_raster_ok == 0) fall back to the software rasterizer
* over CPU VRAM; the initial full-VRAM upload at context init folds them in.
* Post-init, the GPU pipeline is all-or-nothing — no per-prim fallback. */
/* The sub-pixel / perspective override describes exactly one triangle; drop it
* once that triangle has been submitted so a later prim can never inherit it. */
static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; }
static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) {
if (!s_raster_ok) { sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col); return; }
gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1);
precise_consumed();
}
static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) {
if (!s_raster_ok) { sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2); return; }
gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1);
precise_consumed();
}
static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){
if (!s_raster_ok) { sw_fill_rect(x,y,w,h,c); return; }
@@ -2079,6 +2144,7 @@ static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
precise_consumed();
}
static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){
if (!s_raster_ok) { sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw); return; }
@@ -2086,6 +2152,7 @@ static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32
uint32_t cc[3]={c0,c1,c2}; float col[9];
for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; }
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
precise_consumed();
}
static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){
if (!s_raster_ok) { sw_draw_flat_rect(x,y,w,h,c); return; }
@@ -2392,6 +2459,7 @@ static int init_gpu_raster(void) {
p_glVertexAttribPointer(6, 1, GL_FLOAT, GL_FALSE, st, (void*)(13*sizeof(float))); p_glEnableVertexAttribArray(6); /* raw */
p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */
p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */
p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */
}
p_glGenVertexArrays(1, &s_blit_vao);
@@ -3702,6 +3770,8 @@ static const GpuRenderBackend GL_BACKEND = {
.set_texture_filter = glb_set_texture_filter, .texture_filter = glb_texture_filter,
.set_semi_transparency = glb_set_semi_transparency, .set_mask_bits = glb_set_mask_bits,
.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,
.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,
+11
View File
@@ -25,6 +25,8 @@ static const GpuRenderBackend SW_BACKEND = {
.set_mask_bits = sw_set_mask_bits,
.set_texture_window = sw_set_texture_window,
.set_color_modulation = sw_set_color_modulation,
.set_precise_triangle = sw_set_precise_triangle,
.set_perspective_triangle = sw_set_perspective_triangle,
.fill_rect = sw_fill_rect,
.copy_rect = sw_copy_rect,
.draw_flat_triangle = sw_draw_flat_triangle,
@@ -101,6 +103,15 @@ void gr_set_semi_transparency(int e, int m) { g_b->set_semi_transparenc
void gr_set_mask_bits(int s, int c) { g_b->set_mask_bits(s, c); }
void gr_set_texture_window(uint32_t raw) { g_b->set_texture_window(raw); }
void gr_set_color_modulation(int r, int g, int b, int raw) { g_b->set_color_modulation(r, g, b, raw); }
void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0, int32_t x1, int32_t y1,
int32_t x2, int32_t y2) {
if (g_b->set_precise_triangle)
g_b->set_precise_triangle(enabled, x0, y0, x1, y1, x2, y2);
}
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_fill_rect(int x, int y, int w, int h, uint16_t c) { g_b->fill_rect(x, y, w, h, c); }
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { g_b->copy_rect(sx, sy, dx, dy, w, h); }
void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t c) {
+59 -7
View File
@@ -401,8 +401,19 @@ static int s_geo_mirror_suppress = 0; /* open batch: skip the wide
* (full-screen overlay rect draws its
* own full-width wide pass instead) */
/* Textured batch: 18 floats/vert (pos,uv,col,tpage,clut,depth,raw,limits). */
#define TEXV 18
/* Sub-pixel vertex override + perspective UV weights for the next triangle
* ([video] geometry_correction / perspective_texturing; see gpu_render.h).
* gpu.c sets these immediately before the matching gr_draw_*_triangle and they
* are consumed by it. All-zero == the faithful integer/affine path. */
static int s_pc_valid = 0; /* sub-pixel positions present */
static float s_pc_x[3], s_pc_y[3]; /* native VRAM px, fractional */
static int s_pq_valid = 0; /* perspective weights present */
static float s_pq[3];
/* Textured batch: 19 floats/vert (pos,uv,col,tpage,clut,depth,raw,limits,q).
* q is the perspective weight ([video] perspective_texturing); 0 = affine,
* which the vertex shader turns into w == 1.0 — the faithful default. */
#define TEXV 19
typedef struct { float v[TEXV]; } TexVert;
#define VK_TBUF_VERTS 24576 /* multiple of 3 */
static VkBuffer s_tbuf;
@@ -1224,7 +1235,7 @@ static VkPipeline get_pipeline(int prog, int topo, int blend, int stencil, int c
VkShaderModule vs, fs; VkPipelineLayout layout;
VkVertexInputBindingDescription bind = {0};
VkVertexInputAttributeDescription attrs[8]; uint32_t nattr;
VkVertexInputAttributeDescription attrs[9]; uint32_t nattr;
if (prog == 0) { /* GEO */
vs = s_mod_geo_v; fs = s_mod_geo_f; layout = s_pl_geo;
bind = (VkVertexInputBindingDescription){ 0, sizeof(Vert), VK_VERTEX_INPUT_RATE_VERTEX };
@@ -1242,7 +1253,8 @@ static VkPipeline get_pipeline(int prog, int topo, int blend, int stencil, int c
attrs[5] = (VkVertexInputAttributeDescription){ 5, 0, VK_FORMAT_R32_SFLOAT, 48 };
attrs[6] = (VkVertexInputAttributeDescription){ 6, 0, VK_FORMAT_R32_SFLOAT, 52 };
attrs[7] = (VkVertexInputAttributeDescription){ 7, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 56 };
nattr = 8;
attrs[8] = (VkVertexInputAttributeDescription){ 8, 0, VK_FORMAT_R32_SFLOAT, 72 }; /* a_q */
nattr = 9;
} else { /* BLIT (vertex-less: rect from push constant) */
vs = s_mod_blit_v; fs = s_mod_blit_f; layout = s_pl_blit;
nattr = 0;
@@ -2563,15 +2575,46 @@ static void vkb_set_color_modulation(int r, int g, int b, int raw) {
s_mod_r = r; s_mod_g = g; s_mod_b = b; s_mod_raw = raw ? 1 : 0;
}
/* Sub-pixel / perspective overrides for the next triangle. */
static void vkb_set_precise_triangle(int enabled,
int32_t x0,int32_t y0, int32_t x1,int32_t y1,
int32_t x2,int32_t y2) {
s_pc_valid = enabled ? 1 : 0;
if (s_pc_valid) {
/* 16.16 native VRAM px -> float; the VK pipeline is float end-to-end. */
s_pc_x[0] = (float)x0 / 65536.0f; s_pc_y[0] = (float)y0 / 65536.0f;
s_pc_x[1] = (float)x1 / 65536.0f; s_pc_y[1] = (float)y1 / 65536.0f;
s_pc_x[2] = (float)x2 / 65536.0f; s_pc_y[2] = (float)y2 / 65536.0f;
}
}
static void vkb_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
s_pq_valid = (enabled && q0 > 0.0f && q1 > 0.0f && q2 > 0.0f) ? 1 : 0;
s_pq[0] = q0; s_pq[1] = q1; s_pq[2] = q2;
/* The corrected-triangle tally lives in the software rasterizer and backs
* gpu_texture_correction_hits() — the "is this doing anything on this
* title" counter. Forward so it reads the same on every backend. */
sw_set_perspective_triangle(enabled, q0, q1, q2);
}
/* The override describes exactly one triangle; drop it once submitted so a
* later prim can never inherit it. */
static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; }
/* Position a vertex: the sub-pixel value when geometry_correction supplied one
* for this triangle, else the integer GP0 position. */
#define PCX(i, v) (s_pc_valid ? s_pc_x[i] : (float)(v))
#define PCY(i, v) (s_pc_valid ? s_pc_y[i] : (float)(v))
static void vkb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t c){
geo_prim_begin();
float col[3]; col555(c, col);
tri3((float)x0,(float)y0,col, (float)x1,(float)y1,col, (float)x2,(float)y2,col);
tri3(PCX(0,x0),PCY(0,y0),col, PCX(1,x1),PCY(1,y1),col, PCX(2,x2),PCY(2,y2),col);
precise_consumed();
}
static void vkb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2){
geo_prim_begin();
float a[3],b[3],cc[3]; col555(c0,a); col555(c1,b); col555(c2,cc);
tri3((float)x0,(float)y0,a, (float)x1,(float)y1,b, (float)x2,(float)y2,cc);
tri3(PCX(0,x0),PCY(0,y0),a, PCX(1,x1),PCY(1,y1),b, PCX(2,x2),PCY(2,y2),cc);
precise_consumed();
}
/* Full-screen-overlay wide pass (pause gray-filter / load fade): draw a flat
* rect covering the FULL wide width [0, wide_w) x [y, y+h) directly into the
@@ -2788,7 +2831,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys, const int *us, c
int bx0 = xs[0], bx1 = xs[0], by0 = ys[0], by1 = ys[0];
for (int i = 0; i < 3; i++) {
float *vp = s_tmap[s_tbase + s_tcount + i].v;
vp[0] = (float)xs[i]; vp[1] = (float)ys[i];
vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i];
vp[1] = s_pc_valid ? s_pc_y[i] : (float)ys[i];
vp[2] = (float)us[i]; vp[3] = (float)vs[i];
vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f;
vp[8] = (float)base_x; vp[9] = (float)base_y;
@@ -2796,10 +2840,14 @@ static void gpu_textured_triangle(const int *xs, const int *ys, const int *us, c
vp[12] = (float)depth; vp[13] = (float)rawtex;
vp[14] = (float)lim[0]; vp[15] = (float)lim[1];
vp[16] = (float)lim[2]; vp[17] = (float)lim[3];
vp[18] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
if (xs[i] < bx0) bx0 = xs[i]; if (xs[i] > bx1) bx1 = xs[i];
if (ys[i] < by0) by0 = ys[i]; if (ys[i] > by1) by1 = ys[i];
}
s_tcount += 3;
/* A sub-pixel-corrected vertex lies in [int, int+1) of the integer position
* it was rounded from, so widen the readback rect by one pixel. */
if (s_pc_valid) { bx1 += 1; by1 += 1; }
/* mark dirty for later readback/pack (clamped to draw area) */
if (bx0 < s_da_x1) bx0 = s_da_x1; if (by0 < s_da_y1) by0 = s_da_y1;
if (bx1 > s_da_x2) bx1 = s_da_x2; if (by1 > s_da_y2) by1 = s_da_y2;
@@ -2832,6 +2880,7 @@ static void vkb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
precise_consumed();
}
static void vkb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,
int x1,int y1,int u1,int v1,uint32_t c1,
@@ -2842,6 +2891,7 @@ static void vkb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32
float col[9];
col888(c0, &col[0]); col888(c1, &col[3]); col888(c2, &col[6]);
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
precise_consumed();
}
static void vkb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
@@ -3058,6 +3108,8 @@ static const GpuRenderBackend VK_BACKEND = {
.set_mask_bits = vkb_set_mask_bits,
.set_texture_window = vkb_set_texture_window,
.set_color_modulation = vkb_set_color_modulation,
.set_precise_triangle = vkb_set_precise_triangle,
.set_perspective_triangle = vkb_set_perspective_triangle,
.fill_rect = vkb_fill_rect,
.copy_rect = vkb_copy_rect,
.draw_flat_triangle = vkb_draw_flat_triangle,
+39
View File
@@ -514,6 +514,11 @@ extern "C" EMSCRIPTEN_KEEPALIVE void psx_web_set_smooth_60fps(int enabled) {
static int g_video_scale = 1; /* internal-resolution SSAA factor */
static bool g_video_aa = true; /* linear present filtering */
static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */
/* Sub-pixel vertex precision + perspective-correct UVs (PGXP-style). Visual
* only: the PS1-visible GTE SXY FIFO stays integer, so guest-side culling and
* SXY readback are untouched. Default off = the faithful floor. */
static int g_video_geometry_correction = 0;
static int g_video_perspective_texturing = 0;
static int g_video_renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
static int g_fullscreen = 0; /* tri-state: 0 windowed, 1 borderless (desktop)
* fullscreen, 2 exclusive fullscreen */
@@ -5147,6 +5152,10 @@ int main(int argc, char** argv) {
g_video_scale = gc.runtime.video_supersampling;
g_video_aa = gc.runtime.video_antialiasing;
g_video_texfilter = gc.runtime.video_texture_filter;
g_video_geometry_correction =
gc.runtime.video_geometry_correction ? 1 : 0;
g_video_perspective_texturing =
gc.runtime.video_perspective_texturing ? 1 : 0;
g_video_renderer = gc.runtime.video_renderer;
g_video_screen = gc.runtime.video_screen_kind;
g_video_aspect_num = gc.runtime.video_aspect_num;
@@ -5639,6 +5648,10 @@ int main(int argc, char** argv) {
if (us.has_window_width) g_video_win_w = us.window_width;
if (us.has_antialiasing) g_video_aa = us.antialiasing;
if (us.has_texture_filter) g_video_texfilter = us.texture_filter;
if (us.has_geometry_correction)
g_video_geometry_correction = us.geometry_correction ? 1 : 0;
if (us.has_perspective_texturing)
g_video_perspective_texturing = us.perspective_texturing ? 1 : 0;
if (us.has_screen_kind) g_video_screen = us.screen_kind;
if (us.has_auto_skip_fmv) g_auto_skip_fmv = us.auto_skip_fmv ? 1 : 0;
if (us.has_turbo_loads) g_turbo_loads_enabled = us.turbo_loads ? 1 : 0;
@@ -5931,6 +5944,12 @@ int main(int argc, char** argv) {
seed.supersampling = g_video_scale; seed.has_supersampling = true;
seed.antialiasing = g_video_aa; seed.has_antialiasing = true;
seed.texture_filter = g_video_texfilter; seed.has_texture_filter = true;
/* Seeded (and marked present) so a launcher save round-trips the
* player's hand-edited value instead of dropping the key. */
seed.geometry_correction = (g_video_geometry_correction != 0);
seed.has_geometry_correction = true;
seed.perspective_texturing = (g_video_perspective_texturing != 0);
seed.has_perspective_texturing = true;
seed.screen_kind = g_video_screen; seed.has_screen_kind = true;
seed.auto_skip_fmv = (g_auto_skip_fmv != 0);
seed.has_auto_skip_fmv = skip_fmv_offered;
@@ -6356,6 +6375,8 @@ int main(int argc, char** argv) {
g_video_scale = seed.supersampling;
g_video_aa = seed.antialiasing;
g_video_texfilter = seed.texture_filter;
g_video_geometry_correction = seed.geometry_correction ? 1 : 0;
g_video_perspective_texturing = seed.perspective_texturing ? 1 : 0;
g_video_screen = seed.screen_kind;
g_auto_skip_fmv = skip_fmv_offered && seed.auto_skip_fmv ? 1 : 0;
g_turbo_loads_enabled =
@@ -6541,8 +6562,26 @@ session_reboot:
if (g_video_scale < 1) g_video_scale = 1;
if (g_video_scale > SW_MAX_INTERNAL_SCALE) g_video_scale = SW_MAX_INTERNAL_SCALE;
gr_set_scale(g_video_scale);
/* The scale we asked the backend for. Read it before the reflection below:
* the GL backend's gr_scale() reports its REAL internal scale, which is
* still 0/1 until the GL context comes up later, so g_video_scale does not
* hold the effective factor at this point. */
const int requested_scale = g_video_scale;
g_video_scale = gr_scale(); /* reflect any clamp / alloc fallback */
gr_set_texture_filter(g_video_texfilter);
/* Sub-pixel vertex precision + perspective-correct UVs. Both default off;
* with both off every setter below leaves the tracking caches disabled and
* the draw path is the faithful integer one, unchanged. */
gte_geometry_correction_set(g_video_geometry_correction);
gpu_texture_correction_set(g_video_perspective_texturing);
if (g_video_geometry_correction || g_video_perspective_texturing) {
std::fprintf(stdout,
"psxrecomp: geometry correction %s, perspective texturing %s%s\n",
g_video_geometry_correction ? "on" : "off",
g_video_perspective_texturing ? "on" : "off",
(g_video_geometry_correction && requested_scale < 2)
? " (needs [video] supersampling >= 2 to be visible)" : "");
}
/* Display aspect. Identity at the default 4:3. The present letterbox uses
* this aspect; native-wide fills it with a genuinely wider frame (no
* stretch), squash mode stretches the 4:3 frame into it. */
+13 -3
View File
@@ -10,7 +10,7 @@ file extension.
Usage:
embed_spirv.py --glslc <glslc> --out <header.h> shader1.vert shader2.frag ...
"""
import argparse, os, subprocess, sys, struct
import argparse, os, subprocess, sys, struct, tempfile
STAGE = {".vert": "vert", ".frag": "frag", ".comp": "comp"}
@@ -32,9 +32,19 @@ def main():
"#define PSX_VK_SHADERS_SPV_H",
"#include <stdint.h>", ""]
for sh in a.shaders:
# Scratch dir for the intermediate .spv files. It MUST NOT be the shader
# source dir: every runtime target (psx-runtime, psx-oracle, each game exe)
# runs this script over the SAME shaders, and under `ninja -j N` two of them
# overlap. Writing "<shader>.spv" next to the source made them race — one
# process removed the file the other had just written, and the loser died
# with FileNotFoundError on a shader it had compiled successfully. Keeping
# the scratch per-invocation makes concurrent runs independent, and the
# TemporaryDirectory also stops a crash from leaving .spv litter in the
# source tree.
with tempfile.TemporaryDirectory(prefix="psx_spirv_") as scratch:
for sh in a.shaders:
name, stage = ident(sh)
spv = sh + ".spv"
spv = os.path.join(scratch, os.path.basename(sh) + ".spv")
cmd = [a.glslc, "--target-env=vulkan1.1", "-O", sh, "-o", spv]
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0: