mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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:
+110
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user