mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Settings: Internal resolution presets (Native ... 8K, Match display)
Add a player-facing internal resolution on top of the raised GL ceiling. A preset is a target height; the runtime renders at the integer scale S = ceil(target / reference_lines), reference 240 by default: 720p 3x, 1080p 5x (area-resolved to 1080), 1440p 6x, 4K 9x, 5K 12x, 8K 18x. The backend then clamps S to what it can allocate. - runtime/include/internal_resolution.h: the preset table, stable ids, the value encoding shared with recomp-ui (0 unset, 1 native, -1 match display, N target lines) and the pure resolver. - config: [video] internal_resolution (a preset id or a number of lines) and resolution_reference_lines in game.toml; internal_resolution in settings.toml, written as a stable id beside supersampling = min(S, 4) for older runtimes. Precedence: game default < player's legacy factor < player's preset. settings.toml window_width now accepts up to 7680. - main.cpp: resolve the preset before gpu_init; Match display re-resolves from the game window's monitor (SDL3 mode height x pixel density) before the GL context allocates. The launcher gets the vocabulary (with a synthesized "Nx (L lines)" entry for a legacy factor no preset matches) behind RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION, on first boot and on the netplay soft-return. - The OpenGL game window gets SDL_WINDOW_HIGH_PIXEL_DENSITY (SDL2: ALLOW_HIGHDPI) only when the player chose more than native, so the drawable is in pixels on Retina; native keeps the window as it was. - debug server: video_info (preset, requested and effective scale, GL limit and clamp, hr surface, window points vs pixels), screenshot_hires reads the GL hr FBO at full size, screenshot_wide_hires captures the native-wide band at internal resolution. - docs: config_schema (keys, precedence), NETPLAY (a per-peer presentation setting), TCP_COMMANDS (index regenerated), ENHANCEMENTS IR1. Default-off: with no internal_resolution anywhere the scale, the window flags and the present path are unchanged. Tests: internal_resolution_test (presets x reference 240/480 x backend maxima, legacy mapping round trip), video_enhancement_settings_test (game and settings keys, stable-id round trip, legacy key still written), gl_internal_scale_guards (HiDPI only when opted in, launcher fields behind the capability macro). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -868,3 +868,49 @@ tooling defect found on the way: `screenshot_hires` produces a tiled/black
|
||||
PNG at 768x480 scale-2 windowed (row-pitch bug in the hires readback) —
|
||||
the census + the player's own captures carried the session; fix it before
|
||||
the formal Crash/Tomba2 A/B.
|
||||
|
||||
## IR1 — Internal resolution presets (Native … 8K) and the GL scale ceiling (2026-09-26)
|
||||
|
||||
**What the player gets.** Settings → Display → **Internal resolution**: Native,
|
||||
720p, 1080p, 1440p, 4K, 5K, 8K, Match display. A preset is a target height and
|
||||
resolves to an integer scale over the title's reference height
|
||||
(`[video] resolution_reference_lines`, 240 by default): 3x, 5x (1200 lines,
|
||||
area-resolved to 1080), 6x, 9x, 12x, 18x. It is a Settings row, not a mod,
|
||||
because it needs no game hooks; it is presentation-only, so it also applies in
|
||||
netplay (each peer its own). Keys and precedence: `docs/config_schema.md`.
|
||||
|
||||
**Why the old ceiling was 4.** The software renderer keeps a CPU mirror of
|
||||
1 MiB·S², so it caps at 4, and every backend inherited that cap. OpenGL keeps
|
||||
the hr surface on the GPU; its real limits are the driver's texture,
|
||||
renderbuffer and viewport sizes (the surface is `1024·S × 512·S`) and memory.
|
||||
The GL backend now clamps at context init to those limits and a 2 GiB budget,
|
||||
logs the clamp, and steps down instead of failing (a failure used to drop the
|
||||
whole backend to software). Measured on an Apple M4 (`4.1 Metal - 91.7`): all
|
||||
three limits are 16384, so the full-VRAM surface stops at 16x.
|
||||
|
||||
**Fixes that only bite above 1x** (all gated on S > 1, so native is
|
||||
byte-identical):
|
||||
|
||||
- Lines: `glLineWidth(S)` is capped at 1 on core profiles (macOS reports
|
||||
`GL_ALIASED_LINE_WIDTH_RANGE` 1..1), so every line was one hr pixel thick.
|
||||
Lines are now quads one native pixel thick that include both endpoints, like
|
||||
the software rasterizer.
|
||||
- Present: a supersampled source more than 1.25x the output is area-resolved
|
||||
(bilinear taps over the pixel's footprint) instead of one tap, and the UV
|
||||
inset is half a texel, not half a native pixel (which cropped (S-1)/2 texels
|
||||
per edge).
|
||||
- The copy scratch is sized to the largest copy and the mask stencil is
|
||||
rebuilt in tiles over the primitive bbox union, so a large S does not cost
|
||||
another full-size surface.
|
||||
- The CPU present path under GL presents at 1x (its readout is native) and its
|
||||
staging buffer no longer grows with S².
|
||||
- The macOS game window gets a high-pixel-density drawable when the player
|
||||
picks anything above native; without it the drawable is in points and the
|
||||
compositor stretches it, throwing half the resolution away.
|
||||
|
||||
**Verification.** `gl_scale_invariance_test` renders one GP0 scene on a hidden
|
||||
real GL context at 1, 2, 3, 5 and 9x and requires identical guest-visible VRAM
|
||||
(the pack of the hr surface) at every scale, one-native-pixel lines at
|
||||
internal resolution, and a 32x request that stays on GL at the driver clamp.
|
||||
In a running game, `video_info` reports the requested and effective scale and
|
||||
the drawable; `screenshot_hires` reads the hr FBO at full size.
|
||||
|
||||
+8
-1
@@ -128,12 +128,19 @@ When **OpenGL** is selected and netplay is active, psxrecomp can run a
|
||||
| Layer | Scale | Role |
|
||||
|-------|-------|------|
|
||||
| Software rasterizer | **1×** (headless authority) | Deterministic VRAM / digests / rollback snaps |
|
||||
| OpenGL | Player supersampling (e.g. 2×–4×) | Window present quality only |
|
||||
| OpenGL | Player internal resolution (Native … 8K, or legacy 2×–4×) | Window present quality only |
|
||||
|
||||
Cost: extra CPU for the 1× SW pass. Benefit: peers can use different GL
|
||||
settings without desyncing the sim. SW-only netplay forces scale 1 for the
|
||||
whole path. Offline play keeps full supersampling with no dual-raster tax.
|
||||
|
||||
**Internal resolution is a per-peer presentation setting.** Settings → Display →
|
||||
Internal resolution is not a mod, so it is not cleared for netplay: each peer
|
||||
keeps its own preset, clamped to its own GPU, and one peer at 4K next to another
|
||||
at Native is a supported match. Only the GL present surface changes; the 1×
|
||||
software authority, the digests and the rollback snapshots are the same on every
|
||||
peer. Widescreen and frame-rate mods, by contrast, are cleared for netplay.
|
||||
|
||||
---
|
||||
|
||||
## Connectivity: ICE, TURN, SFU, LAN
|
||||
|
||||
@@ -50,7 +50,9 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) |
|
||||
| `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) |
|
||||
| `gpu_state` | ✓ | ✓ | — | Display area, display depth, `screen_offset_y`, draw offset, GPUSTAT, clip rect, xfer state. A positive screen offset moves 24-bit scanout down from the PAL or NTSC broadcast centre |
|
||||
| `screenshot_hires` | ✓ | ✓ | `path` | PNG of the **supersampled** surface (the present path the window uses), at `display × gr_scale()`. ⚠ `screenshot`/`screenshot_file` capture native 15-bit VRAM and are **blind to anything that only exists in the hi-res mirror** — geometry correction, SSAA edges, perspective UVs — so they show a clean frame while the player sees a broken one. Use this one to verify those. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists |
|
||||
| `screenshot_hires` | ✓ | ✓ | `path` | PNG of the **supersampled** surface (the present path the window uses), at `display × gr_scale()`. ⚠ `screenshot`/`screenshot_file` capture native 15-bit VRAM and are **blind to anything that only exists in the hi-res mirror** — geometry correction, SSAA edges, perspective UVs — so they show a clean frame while the player sees a broken one. Use this one to verify those. Under OpenGL it reads the hr FBO directly (the CPU-side mirror is native there), so it is the capture that proves an internal-resolution preset: 4K is 2880×2160 for a 320×240 display. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists |
|
||||
| `video_info` | ✓ | | — | Internal resolution end to end: `preset` (0 unset, 1 native, -1 match display, else target lines), `reference_lines`, `requested_scale` (what the preset asked for), `effective_scale` (what the backend allocated), `internal_lines`, the GL limit (`gl_max_dim`, `gl_max_scale`, `gl_clamp_reason`: 1 texture, 2 budget, 4 ceiling; `gl_alloc_retries`, `gl_budget_mib`), the hr surface (`fbo_w`/`fbo_h`), and the window in points (`window_w`/`window_h`) and pixels (`drawable_w`/`drawable_h`, `hidpi_window`) |
|
||||
| `screenshot_wide_hires` | ✓ | | `path`, `base_x` | The displayed band of the native-wide surface at internal resolution (`wide_w×S` by `height×S`). `present_shot` is capped at the window; this checks a widescreen + internal-resolution combination at full size |
|
||||
| `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) |
|
||||
| `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates |
|
||||
| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate |
|
||||
@@ -336,9 +338,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**316 commands registered** — 303 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**318 commands registered** — 305 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
54 of 316 have prose above; **262 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
56 of 318 have prose above; **262 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -572,6 +574,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `screenshot` | ✓ | ✓ | ✓ |
|
||||
| `screenshot_file` | ✓ | ✓ | ✓ |
|
||||
| `screenshot_hires` | ✓ | | ✓ |
|
||||
| `screenshot_wide_hires` | ✓ | | ✓ |
|
||||
| `set_input` | ✓ | ✓ | ✓ |
|
||||
| `set_snapshot` | ✓ | ✓ | ✓ |
|
||||
| `sio_arm_audit` | ✓ | | |
|
||||
@@ -613,6 +616,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `unknown_dispatch_log` | ✓ | | |
|
||||
| `unwatch` | ✓ | | ✓ |
|
||||
| `vblank_rate` | ✓ | | |
|
||||
| `video_info` | ✓ | | ✓ |
|
||||
| `vk_perf` | ✓ | | |
|
||||
| `vram_peek` | ✓ | ✓ | ✓ |
|
||||
| `vsync_query_hle` | ✓ | | |
|
||||
|
||||
+31
-1
@@ -450,7 +450,37 @@ a build compiled with Vulkan support. `offer_vulkan` controls launcher
|
||||
visibility only; it defaults to false so game projects must explicitly expose
|
||||
Vulkan after validating their visuals and stability.
|
||||
|
||||
### Internal resolution (`supersampling`)
|
||||
### Internal resolution (`internal_resolution`, `supersampling`)
|
||||
|
||||
The player picks a preset in Settings → Display → **Internal resolution**:
|
||||
Native, 720p, 1080p, 1440p, 4K, 5K, 8K or Match display. It is stored in the
|
||||
player's `settings.toml`, and a game may ship a default the same way:
|
||||
|
||||
```toml
|
||||
[video]
|
||||
internal_resolution = "4k" # native | 720p | 1080p | 1440p | 4k | 5k | 8k | display, or a number of lines
|
||||
resolution_reference_lines = 240 # game.toml only: the title's usual display height (120..1024)
|
||||
```
|
||||
|
||||
A preset is a target height. The runtime renders at the integer scale
|
||||
`S = ceil(target / resolution_reference_lines)`: at the default 240 lines,
|
||||
720p is 3x, 1080p is 5x (1200 lines, area-resolved to 1080), 1440p 6x, 4K 9x,
|
||||
5K 12x and 8K 18x. A 480-line interlaced screen renders at twice the target
|
||||
and is resolved down. **Match display** takes the monitor's pixel height,
|
||||
measured from the game window's display when it opens. The backend clamps S
|
||||
to what it can allocate (below); `video_info` over TCP reports both numbers.
|
||||
|
||||
Precedence: the game's `internal_resolution` is the default; a player's legacy
|
||||
`supersampling` in `settings.toml` outranks it; the player's own
|
||||
`internal_resolution` outranks both. The launcher writes the preset as a stable
|
||||
id (`"4k"`) beside `supersampling = min(S, 4)`, so an older runtime reading the
|
||||
same file still gets the nearest factor it supports. A legacy factor with no
|
||||
matching preset appears in the launcher as its own entry, for example
|
||||
"2x (480 lines)". `settings.toml` `window_width` accepts 640 to 7680.
|
||||
|
||||
On OpenGL, any choice above native opens the game window with a
|
||||
high-pixel-density drawable (macOS Retina, Wayland scaling), so the window has
|
||||
the pixels to show it. Native keeps the window exactly as before.
|
||||
|
||||
`supersampling = N` renders at N times the native resolution per axis and
|
||||
downsamples to the window. It accepts 1 to 32 in both `game.toml` and the
|
||||
|
||||
@@ -416,6 +416,12 @@ if(BUILD_TESTING)
|
||||
target_include_directories(gl_scale_limits_test PRIVATE ../runtime/include)
|
||||
add_test(NAME gl_scale_limits_test COMMAND gl_scale_limits_test)
|
||||
|
||||
# Internal resolution presets -> integer scale (pure header).
|
||||
add_executable(internal_resolution_test
|
||||
../runtime/tests/test_internal_resolution.cpp)
|
||||
target_include_directories(internal_resolution_test PRIVATE ../runtime/include)
|
||||
add_test(NAME internal_resolution_test COMMAND internal_resolution_test)
|
||||
|
||||
add_executable(cli_boot_path_test
|
||||
tests/cli_boot_path_test.cpp
|
||||
src/cli_boot_path.cpp
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "bios_rom_alias.h"
|
||||
#include "../../runtime/include/internal_resolution.h"
|
||||
#include "host_path.h"
|
||||
#include "fmt/format.h"
|
||||
#include "ps1_exe_parser.h"
|
||||
@@ -582,6 +583,32 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
}
|
||||
rt.video_supersampling = static_cast<int>(n);
|
||||
}
|
||||
if (video.contains("internal_resolution")) {
|
||||
const toml::value& ir = toml::find(video, "internal_resolution");
|
||||
int value = 0;
|
||||
bool ok = false;
|
||||
if (ir.is_string()) {
|
||||
ok = psx_ir_parse(ir.as_string().str.c_str(), &value) != 0;
|
||||
} else if (ir.is_integer()) {
|
||||
const auto n = ir.as_integer();
|
||||
ok = n >= PSX_IR_MIN_LINES && n <= PSX_IR_MAX_LINES;
|
||||
value = static_cast<int>(n);
|
||||
}
|
||||
if (!ok) {
|
||||
throw std::runtime_error(
|
||||
"[video] internal_resolution must be native, 720p, 1080p, "
|
||||
"1440p, 4k, 5k, 8k, display, or a number of lines");
|
||||
}
|
||||
rt.video_internal_resolution = value;
|
||||
}
|
||||
if (video.contains("resolution_reference_lines")) {
|
||||
const auto n = toml::find<int64_t>(video, "resolution_reference_lines");
|
||||
if (n < 120 || n > 1024) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"[video] resolution_reference_lines out of range (120..1024): {}", n));
|
||||
}
|
||||
rt.video_resolution_reference_lines = static_cast<int>(n);
|
||||
}
|
||||
if (video.contains("window_width")) {
|
||||
const auto n = toml::find<int64_t>(video, "window_width");
|
||||
if (n < 640 || n > 7680) {
|
||||
@@ -2317,9 +2344,23 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
const auto n = toml::find<int64_t>(v, "supersampling");
|
||||
if (n >= 1 && n <= 32) { s.supersampling = (int)n; s.has_supersampling = true; }
|
||||
});
|
||||
if (v.contains("internal_resolution")) try_get([&]{
|
||||
const toml::value& ir = toml::find(v, "internal_resolution");
|
||||
int value = 0;
|
||||
if (ir.is_string()) {
|
||||
if (psx_ir_parse(ir.as_string().str.c_str(), &value)) {
|
||||
s.internal_resolution = value; s.has_internal_resolution = true;
|
||||
}
|
||||
} else if (ir.is_integer()) {
|
||||
const auto n = ir.as_integer();
|
||||
if (n >= PSX_IR_MIN_LINES && n <= PSX_IR_MAX_LINES) {
|
||||
s.internal_resolution = (int)n; s.has_internal_resolution = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (v.contains("window_width")) try_get([&]{
|
||||
const auto n = toml::find<int64_t>(v, "window_width");
|
||||
if (n >= 640 && n <= 3840) { s.window_width = (int)n; s.has_window_width = true; }
|
||||
if (n >= 640 && n <= 7680) { s.window_width = (int)n; s.has_window_width = true; }
|
||||
});
|
||||
if (v.contains("antialiasing")) try_get([&]{
|
||||
s.antialiasing = toml::find<bool>(v, "antialiasing"); s.has_antialiasing = true;
|
||||
@@ -2670,6 +2711,13 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
<< "\"\n";
|
||||
if (s.has_supersampling)
|
||||
f << "supersampling = " << s.supersampling << "\n";
|
||||
if (s.has_internal_resolution && psx_ir_value_valid(s.internal_resolution)) {
|
||||
const char* id = psx_ir_id_for(s.internal_resolution);
|
||||
if (id)
|
||||
f << "internal_resolution = \"" << id << "\"\n";
|
||||
else
|
||||
f << "internal_resolution = " << s.internal_resolution << "\n";
|
||||
}
|
||||
if (s.has_window_width)
|
||||
f << "window_width = " << s.window_width << "\n";
|
||||
if (s.has_antialiasing)
|
||||
|
||||
@@ -385,6 +385,18 @@ struct RuntimeConfig {
|
||||
// driver's texture limits and a memory budget (gl_scale_limits.h).
|
||||
int video_supersampling = 1;
|
||||
|
||||
// internal_resolution: the "Internal resolution" preset (Settings ->
|
||||
// Display), which supersedes supersampling when set. Encoding as in
|
||||
// runtime/include/internal_resolution.h: 0 = unset (supersampling stands),
|
||||
// 1 = native, -1 = match the display, N >= 2 = target output lines. In
|
||||
// game.toml it is the shipped default ("native", "720p", "1080p",
|
||||
// "1440p", "4k", "5k", "8k", "display", or a number of lines).
|
||||
int video_internal_resolution = 0;
|
||||
// resolution_reference_lines: the title's usual display height, which a
|
||||
// preset divides into (S = ceil(target / reference)). 240 for NTSC
|
||||
// 320x240 games; 120..1024.
|
||||
int video_resolution_reference_lines = 240;
|
||||
|
||||
// Optional initial window width declared by the title profile. Zero keeps
|
||||
// the historical fit-to-display behavior; player settings may override it.
|
||||
int video_window_width = 0;
|
||||
@@ -1202,6 +1214,11 @@ struct UserSettings {
|
||||
// [video]
|
||||
bool has_renderer = false; int renderer = DEFAULT_VIDEO_RENDERER; // 0=software,1=opengl,2=vulkan
|
||||
bool has_supersampling = false; int supersampling = 1; // 1..32 (runtime clamps per backend)
|
||||
// Internal resolution preset (RuntimeConfig::video_internal_resolution
|
||||
// encoding). Written as a stable id ("4k") or a line count; when present
|
||||
// it wins over supersampling, which is still written (capped at 4) so an
|
||||
// older runtime reading the same file degrades gracefully.
|
||||
bool has_internal_resolution = false; int internal_resolution = 0;
|
||||
// Window size: width in px; height is always width*3/4 (PSX 4:3). Applies to
|
||||
// both the launcher and the emulator window so they boot at the same size.
|
||||
bool has_window_width = false; int window_width = 1280; // -> 1280x960
|
||||
|
||||
@@ -187,7 +187,97 @@ static void test_user_settings_round_trip() {
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
/* Internal resolution (Settings -> Display): game.toml shipped default,
|
||||
* settings.toml precedence over the legacy factor, stable-id round trip,
|
||||
* and the legacy supersampling range widened to the runtime's 1..32. */
|
||||
static void test_internal_resolution_game_toml() {
|
||||
fs::path p = write_game_toml("psxrecomp_ir_default.toml", "");
|
||||
auto gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.video_internal_resolution == 0,
|
||||
"internal_resolution defaults unset (supersampling stands)");
|
||||
check(gc.runtime.video_resolution_reference_lines == 240,
|
||||
"resolution_reference_lines defaults to 240");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("psxrecomp_ir_4k.toml",
|
||||
"[video]\n"
|
||||
"internal_resolution = \"4K\"\n"
|
||||
"resolution_reference_lines = 240\n"
|
||||
"supersampling = 12\n");
|
||||
gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.video_internal_resolution == 2160, "game.toml \"4K\" parses (case-insensitive)");
|
||||
check(gc.runtime.video_supersampling == 12, "supersampling accepts 12 (1..32)");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("psxrecomp_ir_lines.toml",
|
||||
"[video]\n"
|
||||
"internal_resolution = 1600\n");
|
||||
gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.video_internal_resolution == 1600, "game.toml integer lines parse");
|
||||
fs::remove(p);
|
||||
|
||||
for (const char* bad : { "internal_resolution = \"9k\"\n",
|
||||
"internal_resolution = 1\n",
|
||||
"resolution_reference_lines = 50\n",
|
||||
"supersampling = 33\n" }) {
|
||||
p = write_game_toml("psxrecomp_ir_bad.toml", std::string("[video]\n") + bad);
|
||||
bool rejected = false;
|
||||
try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; }
|
||||
check(rejected, bad);
|
||||
fs::remove(p);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_internal_resolution_settings() {
|
||||
fs::path p = write_temp("psxrecomp_ir_settings.toml",
|
||||
"[video]\n"
|
||||
"supersampling = 2\n"
|
||||
"internal_resolution = \"display\"\n"
|
||||
"window_width = 7680\n");
|
||||
auto us = PSXRecompV4::load_user_settings(p);
|
||||
check(us.has_internal_resolution && us.internal_resolution == -1,
|
||||
"settings.toml internal_resolution = \"display\" reads -1");
|
||||
check(us.has_supersampling && us.supersampling == 2, "legacy supersampling still read");
|
||||
check(us.has_window_width && us.window_width == 7680,
|
||||
"settings.toml window_width accepts 7680 (was capped at 3840)");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_temp("psxrecomp_ir_settings_bad.toml",
|
||||
"[video]\n"
|
||||
"internal_resolution = \"huge\"\n");
|
||||
us = PSXRecompV4::load_user_settings(p);
|
||||
check(!us.parse_error && !us.has_internal_resolution,
|
||||
"an unknown preset is ignored, not an error");
|
||||
fs::remove(p);
|
||||
|
||||
const int values[] = { 1, 720, 1080, 1440, 2160, 2880, 4320, -1, 480 };
|
||||
const char* ids[] = { "native", "720p", "1080p", "1440p", "4k", "5k", "8k", "display", nullptr };
|
||||
for (int i = 0; i < 9; i++) {
|
||||
PSXRecompV4::UserSettings out;
|
||||
out.has_internal_resolution = true; out.internal_resolution = values[i];
|
||||
out.has_supersampling = true; out.supersampling = 4;
|
||||
p = fs::temp_directory_path() / "psxrecomp_ir_roundtrip.toml";
|
||||
check(PSXRecompV4::save_user_settings(p, out), "save_user_settings writes");
|
||||
std::ifstream f(p);
|
||||
std::string text((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
if (ids[i])
|
||||
check(text.find(std::string("internal_resolution = \"") + ids[i] + "\"") != std::string::npos,
|
||||
"a preset is persisted by its stable id");
|
||||
else
|
||||
check(text.find("internal_resolution = 480") != std::string::npos,
|
||||
"a custom line count is persisted as an integer");
|
||||
auto back = PSXRecompV4::load_user_settings(p);
|
||||
check(back.has_internal_resolution && back.internal_resolution == values[i],
|
||||
"internal_resolution survives a save/load round trip");
|
||||
check(back.has_supersampling && back.supersampling == 4,
|
||||
"the legacy supersampling key is still written beside it");
|
||||
fs::remove(p);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_internal_resolution_game_toml();
|
||||
test_internal_resolution_settings();
|
||||
test_defaults_off();
|
||||
test_game_toml_opt_in();
|
||||
test_game_window_width_validation();
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/* internal_resolution.h — the "Internal resolution" setting: presets, their
|
||||
* persisted ids, and the integer scale each one asks the renderer for.
|
||||
*
|
||||
* Pure (no SDL, no GL) so both the config loader and a test can use it.
|
||||
*
|
||||
* VALUE ENCODING (shared with recomp-ui's Settings.internal_resolution):
|
||||
* 0 unset: the legacy [video] supersampling factor stands
|
||||
* 1 native (1x)
|
||||
* -1 match display: the target is the monitor's pixel height
|
||||
* N (N >= 2) a target output height in lines (720, 1080, ... or a
|
||||
* legacy "Nx" entry expressed as reference_lines * N)
|
||||
*
|
||||
* SCALE. The renderer only does integer scales (the pack of the hr surface
|
||||
* back to native VRAM is exact only then), so a target height becomes
|
||||
* S = ceil(target_lines / reference_lines)
|
||||
* clamped to [1, s_max]. reference_lines is the title's usual display height
|
||||
* ([video] resolution_reference_lines, default 240): R4's races are 320x240,
|
||||
* so 1080p is S = 5 (1200 lines, area-resolved to 1080) and 4K is S = 9.
|
||||
* A 480-line mode (interlaced menus) renders at twice the target and is
|
||||
* resolved down. The backend then clamps S again to what it can allocate. */
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#define PSX_IR_UNSET 0
|
||||
#define PSX_IR_NATIVE 1
|
||||
#define PSX_IR_DISPLAY (-1)
|
||||
#define PSX_IR_DEFAULT_REF_LINES 240
|
||||
/* Custom line counts accepted from config (a 16K panel is 8640 lines). */
|
||||
#define PSX_IR_MIN_LINES 2
|
||||
#define PSX_IR_MAX_LINES 8640
|
||||
|
||||
typedef struct PsxIrPreset {
|
||||
const char *id; /* stable id persisted in settings.toml */
|
||||
const char *label; /* launcher label */
|
||||
int value; /* encoding above */
|
||||
} PsxIrPreset;
|
||||
|
||||
static const PsxIrPreset k_psx_ir_presets[] = {
|
||||
{ "native", "Native", PSX_IR_NATIVE },
|
||||
{ "720p", "720p", 720 },
|
||||
{ "1080p", "1080p", 1080 },
|
||||
{ "1440p", "1440p", 1440 },
|
||||
{ "4k", "4K", 2160 },
|
||||
{ "5k", "5K", 2880 },
|
||||
{ "8k", "8K", 4320 },
|
||||
{ "display", "Match display", PSX_IR_DISPLAY },
|
||||
};
|
||||
#define PSX_IR_PRESET_COUNT ((int)(sizeof(k_psx_ir_presets) / sizeof(k_psx_ir_presets[0])))
|
||||
|
||||
static inline int psx_ir_lower_eq(const char *a, const char *b) {
|
||||
for (; *a && *b; a++, b++) {
|
||||
char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a - 'A' + 'a') : *a;
|
||||
if (ca != *b) return 0;
|
||||
}
|
||||
return *a == 0 && *b == 0;
|
||||
}
|
||||
|
||||
/* 1 if value is a legal encoding (unset excluded). */
|
||||
static inline int psx_ir_value_valid(int value) {
|
||||
return value == PSX_IR_NATIVE || value == PSX_IR_DISPLAY ||
|
||||
(value >= PSX_IR_MIN_LINES && value <= PSX_IR_MAX_LINES);
|
||||
}
|
||||
|
||||
/* Parse a persisted value: a preset id (case-insensitive) or a decimal line
|
||||
* count in [PSX_IR_MIN_LINES, PSX_IR_MAX_LINES]. Returns 1 and stores the
|
||||
* value on success; 0 for anything else (the caller keeps its default). */
|
||||
static inline int psx_ir_parse(const char *s, int *out) {
|
||||
if (!s || !*s) return 0;
|
||||
for (int i = 0; i < PSX_IR_PRESET_COUNT; i++)
|
||||
if (psx_ir_lower_eq(s, k_psx_ir_presets[i].id)) { *out = k_psx_ir_presets[i].value; return 1; }
|
||||
long v = 0;
|
||||
const char *p = s;
|
||||
while (*p >= '0' && *p <= '9') { v = v * 10 + (*p - '0'); p++; if (v > PSX_IR_MAX_LINES) return 0; }
|
||||
if (p == s || *p) return 0;
|
||||
if (v < PSX_IR_MIN_LINES) return 0; /* a number is always lines, never "1 = native" */
|
||||
*out = (int)v;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The preset id for a value, or NULL for a custom line count (persist those
|
||||
* as an integer). */
|
||||
static inline const char *psx_ir_id_for(int value) {
|
||||
for (int i = 0; i < PSX_IR_PRESET_COUNT; i++)
|
||||
if (k_psx_ir_presets[i].value == value) return k_psx_ir_presets[i].id;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline const char *psx_ir_label_for(int value) {
|
||||
for (int i = 0; i < PSX_IR_PRESET_COUNT; i++)
|
||||
if (k_psx_ir_presets[i].value == value) return k_psx_ir_presets[i].label;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Unclamped scale a value asks for. display_px_h <= 0 (unknown) makes Match
|
||||
* display 1 until the caller can measure the monitor. */
|
||||
static inline int psx_ir_scale_for(int value, int ref_lines, int display_px_h) {
|
||||
if (ref_lines < 1) ref_lines = PSX_IR_DEFAULT_REF_LINES;
|
||||
int target;
|
||||
if (value == PSX_IR_DISPLAY) target = display_px_h;
|
||||
else if (value >= PSX_IR_MIN_LINES) target = value;
|
||||
else return 1; /* native / unset */
|
||||
if (target <= ref_lines) return 1;
|
||||
return (target + ref_lines - 1) / ref_lines;
|
||||
}
|
||||
|
||||
/* The scale to request: psx_ir_scale_for clamped to [1, s_max]. */
|
||||
static inline int psx_resolve_internal_scale(int value, int ref_lines,
|
||||
int display_px_h, int s_max) {
|
||||
int s = psx_ir_scale_for(value, ref_lines, display_px_h);
|
||||
if (s_max >= 1 && s > s_max) s = s_max;
|
||||
return s < 1 ? 1 : s;
|
||||
}
|
||||
|
||||
/* A legacy [video] supersampling factor as a value the launcher can show:
|
||||
* the preset that resolves to the same scale (1 -> Native, 3 -> 720p,
|
||||
* 5 -> 1080p, 6 -> 1440p, 9 -> 4K, 12 -> 5K, 18 -> 8K at 240 lines),
|
||||
* otherwise ref_lines * n as a custom entry ("2x (480 lines)"). */
|
||||
static inline int psx_ir_from_supersampling(int n, int ref_lines) {
|
||||
if (ref_lines < 1) ref_lines = PSX_IR_DEFAULT_REF_LINES;
|
||||
if (n <= 1) return PSX_IR_NATIVE;
|
||||
for (int i = 0; i < PSX_IR_PRESET_COUNT; i++) {
|
||||
int v = k_psx_ir_presets[i].value;
|
||||
if (v >= PSX_IR_MIN_LINES && psx_ir_scale_for(v, ref_lines, 0) == n) return v;
|
||||
}
|
||||
long lines = (long)ref_lines * n;
|
||||
return lines > PSX_IR_MAX_LINES ? PSX_IR_MAX_LINES : (int)lines;
|
||||
}
|
||||
@@ -137,6 +137,25 @@ static inline int psx_sdl_get_current_display_mode(
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Opt-in high-pixel-density game window. Without it, SDL3 on a Retina Mac
|
||||
* gives the GL context a drawable in POINTS (half the panel resolution) and
|
||||
* the compositor stretches it, so any internal resolution above that is
|
||||
* averaged away. Only set when the player chose an internal resolution. */
|
||||
#define PSX_SDL_WINDOW_HIGH_DENSITY SDL_WINDOW_HIGH_PIXEL_DENSITY
|
||||
|
||||
/* Pixel height of the display a window is on (the primary display when the
|
||||
* window is NULL), for "Match display". 0 when unknown. */
|
||||
static inline int psx_sdl_display_pixel_height(SDL_Window *window)
|
||||
{
|
||||
SDL_DisplayID display = window ? SDL_GetDisplayForWindow(window) : 0;
|
||||
if (!display) display = SDL_GetPrimaryDisplay();
|
||||
if (!display) return 0;
|
||||
const SDL_DisplayMode *mode = SDL_GetCurrentDisplayMode(display);
|
||||
if (!mode || mode->h <= 0) return 0;
|
||||
float density = mode->pixel_density > 0.0f ? mode->pixel_density : 1.0f;
|
||||
return (int)((float)mode->h * density + 0.5f);
|
||||
}
|
||||
|
||||
static inline const Uint8 *psx_sdl_get_keyboard_state(int *count)
|
||||
{
|
||||
return (const Uint8 *)(const void *)SDL_GetKeyboardState(count);
|
||||
@@ -239,4 +258,15 @@ static inline int psx_sdl_cond_wait_timeout(
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#define PSX_SDL_WINDOW_HIGH_DENSITY SDL_WINDOW_ALLOW_HIGHDPI
|
||||
|
||||
static inline int psx_sdl_display_pixel_height(SDL_Window *window)
|
||||
{
|
||||
int index = window ? SDL_GetWindowDisplayIndex(window) : 0;
|
||||
SDL_DisplayMode mode;
|
||||
if (index < 0) index = 0;
|
||||
if (SDL_GetCurrentDisplayMode(index, &mode) != 0 || mode.h <= 0) return 0;
|
||||
return mode.h;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8910,6 +8910,16 @@ static void handle_screenshot_hires(int id, const char *json)
|
||||
* deterministic black, never whatever the allocator handed back. */
|
||||
uint32_t *argb = (uint32_t *)calloc((size_t)ow * oh, sizeof(uint32_t));
|
||||
if (!argb) { send_err(id, "alloc failed"); return; }
|
||||
/* OpenGL keeps the internal resolution in its FBO only (the CPU-side
|
||||
* mirror is native), so read it from there. This is the capture that
|
||||
* proves what an internal-resolution preset really renders. */
|
||||
if (gr_backend() == GR_BACKEND_OPENGL && scale > 1) {
|
||||
int gw = 0, gh = 0;
|
||||
int n = gl_renderer_read_display_hires((int)di.display_x, (int)di.display_y,
|
||||
(int)w, (int)h, argb,
|
||||
(int)((size_t)ow * oh), &gw, &gh);
|
||||
if (n > 0 && (uint32_t)gw == ow && (uint32_t)gh == oh) goto encode;
|
||||
}
|
||||
/* Renderer pitches are byte strides (the live SDL presentation path uses
|
||||
* the same contract). Passing `ow` here advanced each row by only one
|
||||
* quarter of its ARGB width, overlapping four rows and producing a PNG
|
||||
@@ -8939,6 +8949,7 @@ static void handle_screenshot_hires(int id, const char *json)
|
||||
}
|
||||
}
|
||||
|
||||
encode:;
|
||||
uint8_t *rgb = (uint8_t *)malloc((size_t)ow * oh * 3);
|
||||
if (!rgb) { free(argb); send_err(id, "alloc failed"); return; }
|
||||
for (size_t i = 0; i < (size_t)ow * oh; i++) {
|
||||
@@ -8960,6 +8971,85 @@ static void handle_screenshot_hires(int id, const char *json)
|
||||
"\"height\":%u,\"scale\":%d}", id, path, ow, oh, scale);
|
||||
}
|
||||
|
||||
/* video_info — the Internal resolution state end to end: the preset and its
|
||||
* reference height, the scale requested of the backend, the scale the GL hr
|
||||
* surface was actually allocated at (after the driver/memory clamp), the
|
||||
* driver limit, the hr surface size, and the window's point and pixel sizes
|
||||
* (the HiDPI drawable). Everything a preset needs to be verified against. */
|
||||
static void handle_video_info(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
extern void psx_video_resolution_info(int *preset, int *ref_lines, int *requested,
|
||||
int *hidpi, int *win_w, int *win_h,
|
||||
int *px_w, int *px_h);
|
||||
int preset = 0, ref = 0, req = 0, hidpi = 0, ww = 0, wh = 0, pw = 0, ph = 0;
|
||||
psx_video_resolution_info(&preset, &ref, &req, &hidpi, &ww, &wh, &pw, &ph);
|
||||
GlScaleInfo si;
|
||||
int gl = gl_renderer_scale_info(&si);
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
int eff = gr_scale();
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"backend\":\"%s\",\"preset\":%d,"
|
||||
"\"reference_lines\":%d,\"requested_scale\":%d,\"effective_scale\":%d,"
|
||||
"\"internal_lines\":%u,\"gl\":%d,\"gl_max_dim\":%d,\"gl_max_scale\":%d,"
|
||||
"\"gl_clamp_reason\":%d,\"gl_alloc_retries\":%d,\"gl_budget_mib\":%d,"
|
||||
"\"fbo_w\":%d,\"fbo_h\":%d,\"hidpi_window\":%d,\"window_w\":%d,"
|
||||
"\"window_h\":%d,\"drawable_w\":%d,\"drawable_h\":%d,"
|
||||
"\"display_w\":%u,\"display_h\":%u}",
|
||||
id, gr_backend() == GR_BACKEND_OPENGL ? "opengl"
|
||||
: gr_backend() == GR_BACKEND_VULKAN ? "vulkan" : "software",
|
||||
preset, ref, req, eff, di.height * (unsigned)(eff > 0 ? eff : 1), gl,
|
||||
si.max_dim, si.max_scale, si.clamp_reason, si.alloc_retries, si.budget_mib,
|
||||
si.fbo_w, si.fbo_h, hidpi, ww, wh, pw, ph, di.width, di.height);
|
||||
}
|
||||
|
||||
/* screenshot_wide_hires — the displayed band of the native-wide surface at
|
||||
* internal resolution (GL): wide_w*S x h*S. present_shot is capped at the
|
||||
* window, so this is how a widescreen + internal-resolution combination is
|
||||
* checked without an 8K monitor. */
|
||||
static void handle_screenshot_wide_hires(int id, const char *json)
|
||||
{
|
||||
extern int gr_wide_dump_full(uint32_t *out, int cap_pixels, int *ow, int *oh, int base_x);
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.height == 0) { send_err(id, "display disabled"); return; }
|
||||
int S = gr_scale();
|
||||
if (S < 1) S = 1;
|
||||
int base_x = json_get_int(json, "base_x", (int)di.display_x);
|
||||
char path[512];
|
||||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||||
strncpy(path, "psx_screenshot_wide_hires.png", sizeof(path) - 1);
|
||||
path[sizeof(path) - 1] = '\0';
|
||||
int cap = 8192 * 8192;
|
||||
uint32_t *buf = (uint32_t *)malloc((size_t)cap * sizeof(uint32_t));
|
||||
if (!buf) { send_err(id, "alloc failed"); return; }
|
||||
int W = 0, H = 0;
|
||||
int n = gr_wide_dump_full(buf, cap, &W, &H, base_x);
|
||||
if (n <= 0) { free(buf); send_err(id, "no wide surface (native-wide not engaged?)"); return; }
|
||||
int y0 = (int)di.display_y * S, oh = (int)di.height * S;
|
||||
if (y0 < 0) y0 = 0;
|
||||
if (y0 + oh > H) oh = H - y0;
|
||||
if (oh <= 0) { free(buf); send_err(id, "display band outside the surface"); return; }
|
||||
uint8_t *rgb = (uint8_t *)malloc((size_t)W * oh * 3);
|
||||
if (!rgb) { free(buf); send_err(id, "alloc failed"); return; }
|
||||
for (int y = 0; y < oh; y++)
|
||||
for (int x = 0; x < W; x++) {
|
||||
uint32_t px = buf[(size_t)(y0 + y) * W + x];
|
||||
uint8_t *p = rgb + ((size_t)y * W + x) * 3;
|
||||
p[0] = (uint8_t)((px >> 16) & 0xFF);
|
||||
p[1] = (uint8_t)((px >> 8) & 0xFF);
|
||||
p[2] = (uint8_t)(px & 0xFF);
|
||||
}
|
||||
free(buf);
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) { free(rgb); send_err(id, "cannot open file"); return; }
|
||||
int ok = png_write_rgb(f, rgb, (uint32_t)W, (uint32_t)oh);
|
||||
free(rgb); fclose(f);
|
||||
if (!ok) { send_err(id, "png encode failed"); return; }
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"width\":%d,\"height\":%d,"
|
||||
"\"scale\":%d}", id, path, W, oh, S);
|
||||
}
|
||||
|
||||
/* present_shot — PNG of the COMPOSED renderer output: the frame after SDL fits
|
||||
* the display buffer into the logical surface, i.e. at the aspect the player is
|
||||
* actually looking at.
|
||||
@@ -14034,6 +14124,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "display_ring_stats", handle_display_ring_stats },
|
||||
{ "dump_buffer", handle_dump_buffer },
|
||||
{ "wide_full", handle_wide_full },
|
||||
{ "video_info", handle_video_info },
|
||||
{ "screenshot_wide_hires", handle_screenshot_wide_hires },
|
||||
{ "wide_shot", handle_wide_shot },
|
||||
{ "gpu_opcodes", handle_gpu_opcodes },
|
||||
{ "gpu_ring_stats", handle_gpu_ring_stats },
|
||||
|
||||
+180
-3
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "window_size.h" /* default game-window size */
|
||||
#include "internal_resolution.h" /* Settings -> Display -> Internal resolution */
|
||||
#include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */
|
||||
#include "parity_trace.h" /* general two-process control-flow parity ring */
|
||||
#include "device_trace.h" /* general two-process device-event cycle ring */
|
||||
@@ -1209,6 +1210,38 @@ static int g_video_perspective_texturing = 0;
|
||||
static int g_video_pgxp_cpu_mode = 0;
|
||||
static float g_video_pgxp_tolerance = 0.5f;
|
||||
static int g_video_renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
|
||||
|
||||
/* Settings -> Display -> Internal resolution (internal_resolution.h). The
|
||||
* preset supersedes the legacy supersampling factor once set; PSX_IR_UNSET
|
||||
* leaves the factor as the player (or game.toml) wrote it. */
|
||||
static int g_video_internal_res = PSX_IR_UNSET;
|
||||
static int g_video_ref_lines = PSX_IR_DEFAULT_REF_LINES;
|
||||
/* The scale asked of the backend before its own clamp (GL reports its real
|
||||
* scale only after context init), and whether that request applies (netplay
|
||||
* software-only forces 1x). Used for the opt-in HiDPI window and Match display. */
|
||||
static int g_video_requested_scale = 1;
|
||||
static bool g_video_scale_applies = true;
|
||||
static bool g_video_hidpi_window = false;
|
||||
|
||||
static int video_scale_ceiling(void) {
|
||||
return g_video_renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE;
|
||||
}
|
||||
|
||||
/* Resolve the Internal resolution preset into g_video_scale. display_px_h is
|
||||
* the monitor's pixel height for Match display (0 = not known yet: 1x until
|
||||
* the window exists). No-op while unset. */
|
||||
static void apply_internal_resolution(int display_px_h) {
|
||||
if (g_video_internal_res == PSX_IR_UNSET) return;
|
||||
g_video_scale = psx_resolve_internal_scale(g_video_internal_res, g_video_ref_lines,
|
||||
display_px_h, video_scale_ceiling());
|
||||
}
|
||||
|
||||
/* The value the launcher row starts on: the preset, or the legacy factor shown
|
||||
* as the preset it matches (3 -> 720p) or as its own "Nx" entry. */
|
||||
static int internal_resolution_for_launcher(void) {
|
||||
if (g_video_internal_res != PSX_IR_UNSET) return g_video_internal_res;
|
||||
return psx_ir_from_supersampling(g_video_scale < 1 ? 1 : g_video_scale, g_video_ref_lines);
|
||||
}
|
||||
static std::string g_bezel_path; /* mod-owned OpenGL margin artwork */
|
||||
static int g_fullscreen = 0; /* tri-state: 0 windowed, 1 borderless (desktop)
|
||||
* fullscreen, 2 exclusive fullscreen */
|
||||
@@ -1830,6 +1863,28 @@ static bool s_present_shot_pending = false;
|
||||
static std::atomic<int> s_present_shot_seq{0}; /* bumps on every completion */
|
||||
static std::atomic<int> s_present_shot_ok{0}; /* 1 = last completion wrote a PNG */
|
||||
|
||||
/* video_info (debug server): the Internal resolution state as main.cpp sees
|
||||
* it, plus the game window's size in points and in pixels. */
|
||||
extern "C" void psx_video_resolution_info(int *preset, int *ref_lines, int *requested,
|
||||
int *hidpi, int *win_w, int *win_h,
|
||||
int *px_w, int *px_h) {
|
||||
if (preset) *preset = g_video_internal_res;
|
||||
if (ref_lines) *ref_lines = g_video_ref_lines;
|
||||
if (requested) *requested = g_video_requested_scale;
|
||||
if (hidpi) *hidpi = g_video_hidpi_window ? 1 : 0;
|
||||
int ww = 0, wh = 0, pw = 0, ph = 0;
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
if (sdl_window) {
|
||||
SDL_GetWindowSize(sdl_window, &ww, &wh);
|
||||
SDL_GL_GetDrawableSize(sdl_window, &pw, &ph);
|
||||
}
|
||||
#endif
|
||||
if (win_w) *win_w = ww;
|
||||
if (win_h) *win_h = wh;
|
||||
if (px_w) *px_w = pw;
|
||||
if (px_h) *px_h = ph;
|
||||
}
|
||||
|
||||
extern "C" int present_shot_request(const char *path)
|
||||
{
|
||||
if (!path || !*path) return 0;
|
||||
@@ -12295,6 +12350,35 @@ namespace {
|
||||
"Fast-forward toggle",
|
||||
};
|
||||
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
/* Internal resolution vocabulary for the launcher's Display card: the
|
||||
* presets, plus the player's current legacy factor as its own "Nx (L
|
||||
* lines)" entry when no preset matches it, so nothing changes silently. */
|
||||
static const char* g_ir_labels[PSX_IR_PRESET_COUNT + 1];
|
||||
static int g_ir_values[PSX_IR_PRESET_COUNT + 1];
|
||||
static int g_ir_count = 0;
|
||||
static char g_ir_custom_label[48];
|
||||
static const char kIrNote[] =
|
||||
"Above 4x needs OpenGL. The GPU's texture limit can lower the scale "
|
||||
"(16x on Apple GPUs).";
|
||||
static void build_internal_resolution_vocab(int current) {
|
||||
g_ir_count = 0;
|
||||
for (int i = 0; i < PSX_IR_PRESET_COUNT; i++) {
|
||||
g_ir_labels[g_ir_count] = k_psx_ir_presets[i].label;
|
||||
g_ir_values[g_ir_count] = k_psx_ir_presets[i].value;
|
||||
g_ir_count++;
|
||||
}
|
||||
if (current >= PSX_IR_MIN_LINES && !psx_ir_id_for(current)) {
|
||||
int s = psx_ir_scale_for(current, g_video_ref_lines, 0);
|
||||
std::snprintf(g_ir_custom_label, sizeof g_ir_custom_label,
|
||||
"%dx (%d lines)", s, current);
|
||||
g_ir_labels[g_ir_count] = g_ir_custom_label;
|
||||
g_ir_values[g_ir_count] = current;
|
||||
g_ir_count++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ae_rui_set_sidecar_paths(const char* argv0) {
|
||||
const auto exe = exe_dir_from_argv(argv0 ? argv0 : "");
|
||||
g_rui_keybinds_path = (exe / "keybinds.ini").string();
|
||||
@@ -12350,6 +12434,14 @@ namespace {
|
||||
gi->has_turbo_loads = turbo_loads_offered_b ? 1 : 0;
|
||||
gi->has_geometry_precision = 1;
|
||||
gi->has_rewind_depth = 1;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
/* The Supersampling row becomes an Internal resolution list. */
|
||||
build_internal_resolution_vocab(internal_resolution_for_launcher());
|
||||
gi->internal_resolution_labels = g_ir_labels;
|
||||
gi->internal_resolution_values = g_ir_values;
|
||||
gi->num_internal_resolutions = g_ir_count;
|
||||
gi->internal_resolution_note = kIrNote;
|
||||
#endif
|
||||
if (language_labels && num_languages > 0) {
|
||||
gi->language_labels = language_labels;
|
||||
gi->num_languages = num_languages;
|
||||
@@ -12842,6 +12934,8 @@ int main(int argc, char** argv) {
|
||||
for (uint32_t site : gc.vsync_event_horizon_extra_sites)
|
||||
psx_vsync_query_hle_add_extra_event_horizon_site(site);
|
||||
g_video_scale = gc.runtime.video_supersampling;
|
||||
g_video_internal_res = gc.runtime.video_internal_resolution;
|
||||
g_video_ref_lines = gc.runtime.video_resolution_reference_lines;
|
||||
if (gc.runtime.video_window_width > 0) {
|
||||
g_video_win_w = gc.runtime.video_window_width;
|
||||
}
|
||||
@@ -13241,7 +13335,13 @@ int main(int argc, char** argv) {
|
||||
if (us.has_netplay_lobby_url && !us.netplay_lobby_url.empty())
|
||||
g_lnch_lobby_url = us.netplay_lobby_url;
|
||||
#endif
|
||||
if (us.has_supersampling) g_video_scale = us.supersampling;
|
||||
/* A player's legacy factor outranks the game's shipped preset; their
|
||||
* own preset (written by this build) outranks both. */
|
||||
if (us.has_supersampling) {
|
||||
g_video_scale = us.supersampling;
|
||||
g_video_internal_res = PSX_IR_UNSET;
|
||||
}
|
||||
if (us.has_internal_resolution) g_video_internal_res = us.internal_resolution;
|
||||
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;
|
||||
@@ -13792,6 +13892,8 @@ int main(int argc, char** argv) {
|
||||
bool match_session_bios_set = false;
|
||||
seed.renderer = g_video_renderer; seed.has_renderer = true;
|
||||
seed.supersampling = g_video_scale; seed.has_supersampling = true;
|
||||
seed.internal_resolution = internal_resolution_for_launcher();
|
||||
seed.has_internal_resolution = true;
|
||||
seed.antialiasing = g_video_aa; seed.has_antialiasing = true;
|
||||
seed.texture_filter = g_video_texfilter; seed.has_texture_filter = true;
|
||||
seed.fmv_filter = g_video_fmv_filter; seed.has_fmv_filter = true;
|
||||
@@ -13992,6 +14094,9 @@ int main(int argc, char** argv) {
|
||||
PSXRecompV4::DEFAULT_VIDEO_RENDERER != 0)
|
||||
ls.renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
|
||||
ls.supersampling = seed.supersampling;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
ls.internal_resolution = seed.internal_resolution;
|
||||
#endif
|
||||
ls.antialiasing = seed.antialiasing ? 1 : 0;
|
||||
ls.texture_filter = seed.texture_filter;
|
||||
ls.fmv_filter = cfg_fmv_filter_to_launcher(seed.fmv_filter);
|
||||
@@ -14288,6 +14393,12 @@ int main(int argc, char** argv) {
|
||||
seed.window_width = ls.window_width; seed.has_window_width = true;
|
||||
seed.renderer = ls.renderer; seed.has_renderer = true;
|
||||
seed.supersampling = ls.supersampling; seed.has_supersampling = true;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
if (psx_ir_value_valid(ls.internal_resolution)) {
|
||||
seed.internal_resolution = ls.internal_resolution;
|
||||
seed.has_internal_resolution = true;
|
||||
}
|
||||
#endif
|
||||
seed.antialiasing = ls.antialiasing != 0; seed.has_antialiasing = true;
|
||||
seed.geometry_correction = ls.geometry_correction != 0;
|
||||
seed.has_geometry_correction = true;
|
||||
@@ -14522,6 +14633,15 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
g_video_renderer = seed.renderer;
|
||||
g_video_scale = seed.supersampling;
|
||||
if (seed.has_internal_resolution &&
|
||||
psx_ir_value_valid(seed.internal_resolution)) {
|
||||
g_video_internal_res = seed.internal_resolution;
|
||||
apply_internal_resolution(psx_sdl_display_pixel_height(nullptr));
|
||||
/* An older runtime reading this settings.toml only knows
|
||||
* supersampling (1..4): leave it the nearest it can do. */
|
||||
seed.supersampling = g_video_scale < SW_MAX_INTERNAL_SCALE
|
||||
? g_video_scale : SW_MAX_INTERNAL_SCALE;
|
||||
}
|
||||
g_video_aa = seed.antialiasing;
|
||||
g_video_texfilter = seed.texture_filter;
|
||||
g_video_fmv_filter = seed.fmv_filter;
|
||||
@@ -14860,6 +14980,10 @@ session_reboot:
|
||||
/* Internal-resolution supersampling (SSAA). Must follow gpu_init.
|
||||
* Dual-raster: gr_set_scale(N) arms GL hr FBO @ N× while glb_set_scale
|
||||
* keeps SW at 1×. SW-only netplay: force scale 1. Offline: full SSAA. */
|
||||
/* Internal resolution preset -> scale (no-op while unset). Match display
|
||||
* knows the monitor only once SDL video is up (launcher path); otherwise it
|
||||
* is re-resolved from the game window's display before GL context init. */
|
||||
apply_internal_resolution(psx_sdl_display_pixel_height(nullptr));
|
||||
if (g_video_scale < 1) g_video_scale = 1;
|
||||
{
|
||||
/* Per-backend ceiling. OpenGL allocates its hr surface at context init
|
||||
@@ -14899,6 +15023,10 @@ session_reboot:
|
||||
* 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_requested_scale = requested_scale;
|
||||
g_video_scale_applies = !(net_cfg.enabled || s_netplay_sim_native_scale) ||
|
||||
(net_cfg.enabled && s_netplay_gl_present &&
|
||||
gl_renderer_cpu_auth_dual());
|
||||
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;
|
||||
@@ -14958,10 +15086,21 @@ session_reboot:
|
||||
/* Present-time screen-colour model (verified-enhancement LUT). Default raw
|
||||
* is byte-identical; PSX_SCREEN env overrides this at scanout. */
|
||||
gpu_set_screen_kind(g_video_screen);
|
||||
if (g_video_scale > 1 || g_video_texfilter)
|
||||
if (g_video_internal_res != PSX_IR_UNSET) {
|
||||
const char* lbl = psx_ir_label_for(g_video_internal_res);
|
||||
char custom[32];
|
||||
if (!lbl) {
|
||||
std::snprintf(custom, sizeof custom, "%d lines", g_video_internal_res);
|
||||
lbl = custom;
|
||||
}
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: internal resolution %s (reference %d lines): "
|
||||
"%dx requested\n", lbl, g_video_ref_lines, requested_scale);
|
||||
}
|
||||
if (requested_scale > 1 || g_video_texfilter)
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: supersampling %dx (antialiasing %s, texture filter %s)\n",
|
||||
g_video_scale, g_video_aa ? "on" : "off",
|
||||
requested_scale, g_video_aa ? "on" : "off",
|
||||
g_video_texfilter ? "bilinear" : "nearest");
|
||||
if (g_video_screen != 0)
|
||||
std::fprintf(stdout, "psxrecomp: screen-colour model %s\n",
|
||||
@@ -15279,6 +15418,13 @@ session_reboot:
|
||||
if (g_video_renderer == 1) {
|
||||
configure_core_gl_context_attributes();
|
||||
win_flags |= SDL_WINDOW_OPENGL;
|
||||
/* Opt-in high-pixel-density drawable: only when the player asked for
|
||||
* more than native resolution, so the default window, OSD and bezel
|
||||
* maths stay exactly as they were. */
|
||||
g_video_hidpi_window = g_video_scale_applies &&
|
||||
(g_video_requested_scale > 1 || g_video_internal_res == PSX_IR_DISPLAY);
|
||||
if (g_video_hidpi_window)
|
||||
win_flags |= PSX_SDL_WINDOW_HIGH_DENSITY;
|
||||
}
|
||||
if (g_video_renderer == 2) win_flags |= SDL_WINDOW_VULKAN;
|
||||
/* Fullscreen on launch (launcher's tri-state Fullscreen control): 1 =
|
||||
@@ -15318,6 +15464,18 @@ session_reboot:
|
||||
* facade back to software (rasterization already runs through software in
|
||||
* this phase) and fall through to the SDL_Renderer present path below. */
|
||||
if (g_video_renderer == 1) {
|
||||
/* Match display: the monitor's pixel height, now that the window says
|
||||
* which monitor. glb_set_scale only records the request; the hr
|
||||
* surface is allocated at context init below. */
|
||||
if (g_video_internal_res == PSX_IR_DISPLAY && g_video_scale_applies) {
|
||||
const int dh = psx_sdl_display_pixel_height(sdl_window);
|
||||
const int s = psx_resolve_internal_scale(PSX_IR_DISPLAY, g_video_ref_lines,
|
||||
dh, GL_MAX_INTERNAL_SCALE);
|
||||
gr_set_scale(s);
|
||||
g_video_requested_scale = s;
|
||||
std::fprintf(stdout, "psxrecomp: internal resolution Match display: "
|
||||
"%d px -> %dx requested\n", dh, s);
|
||||
}
|
||||
gl_renderer_set_swap_interval(present_effective_swap_interval()); /* applied at context init */
|
||||
g_gl_active = (gl_renderer_init_context(sdl_window) != 0);
|
||||
|
||||
@@ -16065,6 +16223,9 @@ soft_return_lobby:
|
||||
if (ls.renderer < 0 || ls.renderer > (vulkan_offered ? 2 : 1))
|
||||
ls.renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
|
||||
ls.supersampling = g_video_scale;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
ls.internal_resolution = internal_resolution_for_launcher();
|
||||
#endif
|
||||
ls.antialiasing = g_video_aa ? 1 : 0;
|
||||
ls.texture_filter = g_video_texfilter;
|
||||
ls.fmv_filter = cfg_fmv_filter_to_launcher(g_video_fmv_filter);
|
||||
@@ -16410,6 +16571,17 @@ soft_return_lobby:
|
||||
us.has_renderer = true;
|
||||
us.supersampling = ls.supersampling;
|
||||
us.has_supersampling = true;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
if (psx_ir_value_valid(ls.internal_resolution)) {
|
||||
us.internal_resolution = ls.internal_resolution;
|
||||
us.has_internal_resolution = true;
|
||||
int s = psx_resolve_internal_scale(ls.internal_resolution, g_video_ref_lines,
|
||||
psx_sdl_display_pixel_height(nullptr),
|
||||
ls.renderer == 1 ? GL_MAX_INTERNAL_SCALE
|
||||
: SW_MAX_INTERNAL_SCALE);
|
||||
us.supersampling = s < SW_MAX_INTERNAL_SCALE ? s : SW_MAX_INTERNAL_SCALE;
|
||||
}
|
||||
#endif
|
||||
us.antialiasing = ls.antialiasing != 0;
|
||||
us.has_antialiasing = true;
|
||||
us.texture_filter = ls.texture_filter;
|
||||
@@ -16486,6 +16658,11 @@ soft_return_lobby:
|
||||
}
|
||||
g_video_renderer = ls.renderer;
|
||||
g_video_scale = ls.supersampling;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
if (psx_ir_value_valid(ls.internal_resolution))
|
||||
g_video_internal_res = ls.internal_resolution;
|
||||
apply_internal_resolution(psx_sdl_display_pixel_height(nullptr));
|
||||
#endif
|
||||
g_video_aa = ls.antialiasing;
|
||||
g_video_texfilter = ls.texture_filter;
|
||||
g_video_fmv_filter = launcher_fmv_filter_to_cfg(ls.fmv_filter);
|
||||
|
||||
@@ -70,6 +70,29 @@ class GlScaleGuards(unittest.TestCase):
|
||||
self.assertIn("(g_video_renderer == 1) ? GL_MAX_INTERNAL_SCALE", MAIN)
|
||||
|
||||
|
||||
class InternalResolutionGuards(unittest.TestCase):
|
||||
def test_hidpi_window_only_when_opted_in(self):
|
||||
self.assertIn("win_flags |= PSX_SDL_WINDOW_HIGH_DENSITY;", MAIN)
|
||||
self.assertRegex(MAIN, r"g_video_hidpi_window = g_video_scale_applies &&\s*"
|
||||
r"\(g_video_requested_scale > 1 \|\| g_video_internal_res == PSX_IR_DISPLAY\);")
|
||||
self.assertRegex(MAIN, r"if \(g_video_hidpi_window\)\s*win_flags \|= PSX_SDL_WINDOW_HIGH_DENSITY;")
|
||||
|
||||
def test_vocabulary_is_optional_abi(self):
|
||||
# Builds against an older recomp-ui must still compile: every use of
|
||||
# the new launcher fields sits behind the capability macro.
|
||||
for field in ("gi->internal_resolution_labels", "ls.internal_resolution"):
|
||||
for m in re.finditer(re.escape(field), MAIN):
|
||||
before = MAIN[:m.start()]
|
||||
opened = before.count("#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)")
|
||||
closed = len(re.findall(r"#endif", before[before.rfind(
|
||||
"#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)"):]))
|
||||
self.assertGreater(opened, 0, field)
|
||||
self.assertEqual(closed, 0, field + " outside its #if block")
|
||||
|
||||
def test_unset_preset_leaves_supersampling(self):
|
||||
self.assertIn("if (g_video_internal_res == PSX_IR_UNSET) return;", MAIN)
|
||||
|
||||
|
||||
class RunnerParsing(unittest.TestCase):
|
||||
def test_parse(self):
|
||||
self.assertEqual(parse_run("driver=x\ndigest=0123456789abcdef\nchecks=7 failures=0\n"),
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// internal_resolution.h: preset ids, the value encoding shared with the
|
||||
// launcher, and the integer scale each preset asks for. Presets x reference
|
||||
// 240/480 x backend maxima (the GL clamp at 16384 is 16x; 18x needs the
|
||||
// windowed high-resolution surface or a 32768-limit GPU).
|
||||
|
||||
#include "internal_resolution.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
static int g_failures = 0;
|
||||
|
||||
static void expect(const char* what, long long got, long long want) {
|
||||
if (got != want) {
|
||||
std::printf("FAIL %s: got %lld want %lld\n", what, got, want);
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Presets at the NTSC 240-line reference (R4 races are 320x240).
|
||||
struct { const char* id; int s240; int s480; } table[] = {
|
||||
{ "native", 1, 1 }, { "720p", 3, 2 }, { "1080p", 5, 3 }, { "1440p", 6, 3 },
|
||||
{ "4k", 9, 5 }, { "5k", 12, 6 }, { "8k", 18, 9 },
|
||||
};
|
||||
for (const auto& t : table) {
|
||||
int v = 0;
|
||||
expect(t.id, psx_ir_parse(t.id, &v), 1);
|
||||
expect(t.id, psx_resolve_internal_scale(v, 240, 0, 32), t.s240);
|
||||
expect(t.id, psx_resolve_internal_scale(v, 480, 0, 32), t.s480);
|
||||
expect("id round trip", std::strcmp(psx_ir_id_for(v), t.id), 0);
|
||||
}
|
||||
// Backend maxima: software/Vulkan 4, full-VRAM GL at a 16384 limit 16.
|
||||
int v8k = 0;
|
||||
psx_ir_parse("8K", &v8k);
|
||||
expect("8k sw", psx_resolve_internal_scale(v8k, 240, 0, 4), 4);
|
||||
expect("8k gl16", psx_resolve_internal_scale(v8k, 240, 0, 16), 16);
|
||||
expect("8k lines", v8k, 4320);
|
||||
|
||||
// Match display: the monitor's pixel height, 1 until it is known.
|
||||
int vd = 0;
|
||||
expect("display parse", psx_ir_parse("display", &vd), 1);
|
||||
expect("display value", vd, PSX_IR_DISPLAY);
|
||||
expect("display unknown", psx_resolve_internal_scale(vd, 240, 0, 32), 1);
|
||||
expect("display 1080", psx_resolve_internal_scale(vd, 240, 1080, 32), 5);
|
||||
expect("display 1964 (14in MBP)", psx_resolve_internal_scale(vd, 240, 1964, 32), 9);
|
||||
expect("display 2160", psx_resolve_internal_scale(vd, 240, 2160, 32), 9);
|
||||
expect("display 2880 (5K)", psx_resolve_internal_scale(vd, 240, 2880, 32), 12);
|
||||
expect("display 4320 (8K)", psx_resolve_internal_scale(vd, 240, 4320, 32), 18);
|
||||
expect("display id", std::strcmp(psx_ir_id_for(vd), "display"), 0);
|
||||
|
||||
// Integer line counts and rejects.
|
||||
int v = 0;
|
||||
expect("lines parse", psx_ir_parse("1600", &v), 1);
|
||||
expect("lines value", v, 1600);
|
||||
expect("lines scale", psx_resolve_internal_scale(v, 240, 0, 32), 7);
|
||||
expect("custom has no id", psx_ir_id_for(1600) == nullptr, 1);
|
||||
v = 77;
|
||||
expect("reject junk", psx_ir_parse("4kk", &v), 0);
|
||||
expect("reject empty", psx_ir_parse("", &v), 0);
|
||||
expect("reject 1", psx_ir_parse("1", &v), 0);
|
||||
expect("reject huge", psx_ir_parse("99999", &v), 0);
|
||||
expect("untouched on reject", v, 77);
|
||||
expect("unset scale", psx_resolve_internal_scale(PSX_IR_UNSET, 240, 0, 32), 1);
|
||||
expect("bad reference defaults to 240", psx_resolve_internal_scale(2160, 0, 0, 32), 9);
|
||||
|
||||
// Legacy supersampling shown as a preset when one resolves to it.
|
||||
expect("legacy 1", psx_ir_from_supersampling(1, 240), PSX_IR_NATIVE);
|
||||
expect("legacy 2", psx_ir_from_supersampling(2, 240), 480);
|
||||
expect("legacy 3", psx_ir_from_supersampling(3, 240), 720);
|
||||
expect("legacy 4", psx_ir_from_supersampling(4, 240), 960);
|
||||
expect("legacy 5", psx_ir_from_supersampling(5, 240), 1080);
|
||||
expect("legacy 9", psx_ir_from_supersampling(9, 240), 2160);
|
||||
expect("legacy 18", psx_ir_from_supersampling(18, 240), 4320);
|
||||
// And the legacy value resolves back to the same scale.
|
||||
for (int n = 1; n <= 32; n++)
|
||||
expect("legacy round trip",
|
||||
psx_resolve_internal_scale(psx_ir_from_supersampling(n, 240), 240, 0, 32), n);
|
||||
|
||||
if (g_failures) return 1;
|
||||
std::printf("internal_resolution: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user