diff --git a/README.md b/README.md index 5e368bf6..d5df5071 100644 --- a/README.md +++ b/README.md @@ -543,13 +543,14 @@ See [`docs/STRING_TRANSLATION.md`](docs/STRING_TRANSLATION.md); *Tsumu Light* ### 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`: +so the faithful look stays the baseline. A title sets them in `game.toml` +as its baseline; a player switches them with the built-in PGXP Precision mod, +and picks the internal resolution with the built-in Internal Resolution mod: ```toml [video] geometry_correction = true # sub-pixel vertex precision — removes polygon - # jitter/wobble. Needs supersampling >= 2. + # jitter/wobble. Needs a 2x+ internal scale. perspective_texturing = true # perspective-correct UVs — removes the texture # warp on large floor/wall polygons. supersampling = 2 diff --git a/docs/ENHANCEMENTS.md b/docs/ENHANCEMENTS.md index 46b82c10..2ae348af 100644 --- a/docs/ENHANCEMENTS.md +++ b/docs/ENHANCEMENTS.md @@ -438,9 +438,11 @@ want stable geometry without changing texture mapping. Settable per-game in launcher row; since then the player's switch is the PGXP Precision mod, see S1 below.) -`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. +`geometry_correction` needs an internal scale of 2x or more (720p and up at +240 lines; the title's `internal_resolution`/`supersampling`, or the Internal +Resolution mod): 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 @@ -1324,8 +1326,8 @@ image there. ## S1 — Enhancement settings become built-in mods (2026-10-24, beads-eio.3.398) The launcher's Settings page keeps PC/system controls only (window size, -fullscreen, renderer, VSync, internal resolution, audio rate and volume, -hotkeys, BIOS, multitap). The game's look moved to four framework built-in +fullscreen, renderer, VSync, audio volume, hotkeys, BIOS, multitap; recomp-ui +docs/SETTINGS_STANDARD.md). The game's look moved to five framework built-in mods, all default-off, all `game_id = "*"`, all host-only (`[[plugin]] netplay = "host_only"`: never in the plan fingerprint, the player's own in netplay): @@ -1336,26 +1338,34 @@ player's own in netplay): | Image Filtering (`psx.enhancement.image-filtering`) | Antialiasing (linear present), Texture filtering, FMV filtering rows | Smoothing, Texture filtering, FMV filtering | | Enhanced Audio (`psx.enhancement.enhanced-audio`) | High-quality SPU row | (on = high-quality SPU) | | Rewind (`psx.feature.rewind`) | Rewind / buffer / interval rows | Rewind buffer, Rewind interval | +| Internal Resolution (`psx.enhancement.internal-resolution`) | Internal resolution row (launcher and in-game), Dynamic resolution / Lowest resolution rows | Internal resolution: Game default, Native … 8K, Match display, Auto | The title's `[video]` / `[audio]` keys stay its baseline: mod off is exactly what the title ships. Activation only records the request (`host_enhancements.h`); the renderer setup resolves it over the baseline after activation, the G1.11 lesson. Perspective textures left Settings for the PGXP Precision mod; the Dynamic resolution checkbox and Lowest resolution -row became Internal resolution = Auto (Match display with dynamic resolution, -the title's floor). Graphics presets (`[quality.*]`, hardware detection, -Re-detect) were removed outright. A mod that presents with vsync off declares -`[[feature]] vsync = "off"` and the launcher shows its VSync row overridden. +row became the Internal Resolution mod's Auto (Match display with dynamic +resolution, the title's floor). Its option changes apply live through the same +`gl_renderer_set_internal_scale_live` path the debug server's `video_set` uses, +and never during a netplay match. The player's `settings.toml` no longer has +`internal_resolution`, `supersampling` or `supersample`. The audio Sample rate +row left too: the device opens at the runtime's built-in 44.1 kHz (old +`[audio] frequency` keys are ignored); Volume stays. Graphics presets +(`[quality.*]`, hardware detection, Re-detect) were removed outright. A mod +that presents with vsync off declares `[[feature]] vsync = "off"` and the +launcher shows its VSync row overridden. ## 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 +**What the player gets.** Internal resolution presets: Native, 720p, 1080p, +1440p, 4K, 5K, 8K, Match display (and, since S1, Auto). 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`. +area-resolved to 1080), 6x, 9x, 12x, 18x. It was a Settings row until S1 +(2026-10-24); it is now the built-in Internal Resolution mod over the title's +`internal_resolution`. 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 diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 3f56b108..6f10fae5 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -617,7 +617,7 @@ defensive, for the state listed here. | Frame-interpolation blend mode | `psx_mod_set_frame_interpolation_blend` | default | every session | | Native VBlank pacing and its rate | `psx_mod_set_native_vblank_rate` | off, 0 | every session | | PGXP request (`psx.enhancement.pgxp`) | the builtin `psx.pgxp` activation (`pgxp_mod_request`) | none | every session | -| Host enhancement requests (CRT Filter, Image Filtering, Enhanced Audio, Rewind) | the builtin activations (`psx_host_enh_request_*`) | none | every session | +| Host enhancement requests (CRT Filter, Image Filtering, Enhanced Audio, Rewind, Internal Resolution) | the builtin activations (`psx_host_enh_request_*`) | none | every session | | Frame period, if native VBlank pacing was on | `psx_mod_set_native_vblank_rate` | first-session value | later sessions | | Frame interpolation and its rate | `psx_mod_set_frame_interpolation` | first-session value | later sessions | | Vsync forced off | `psx_mod_set_frame_interpolation`, `psx_mod_set_native_vblank_rate` | first-session value | later sessions | @@ -863,9 +863,10 @@ an opaque tail. Trusted graphics plugins can call `psx_mod_set_internal_resolution(lines)` to request a rendering height for their session. Values from 120 through 8192 use the existing integer-scale resolver and backend allocation limits; zero clears -the request. `PSX_INTERNAL_RESOLUTION` takes precedence. The player's saved -resolution stays unchanged, and every new session clears the request before -its mod plan activates, including rematches and sessions without that mod. +the request. `PSX_INTERNAL_RESOLUTION` takes precedence. It wins over the +built-in Internal Resolution mod's choice and the title's baseline, which stay +unchanged, and every new session clears the request before its mod plan +activates, including rematches and sessions without that mod. The shared PGXP plugin uses `psx_mod_set_pgxp_precision(enabled, cpu_mode)`. This stores a session selection as well as setting live correction flags, so @@ -1126,8 +1127,9 @@ immediates". ### Built-in host enhancements -Four framework packages replace what used to be Settings rows (owner decision, -beads-eio.3.398). Each targets `game_id = "*"`, is default-off, keeps save +Five framework packages replace what used to be Settings rows (owner decision, +beads-eio.3.398; the rule is recomp-ui's docs/SETTINGS_STANDARD.md: anything +that changes the game's picture, sound or play is a mod). Each targets `game_id = "*"`, is default-off, keeps save states shared, and declares its plugin `netplay = "host_only"` (below): it changes only what this machine shows, plays or keeps, so it is the player's own in every netplay mode and never part of the plan fingerprint. @@ -1138,32 +1140,48 @@ in every netplay mode and never part of the plan fingerprint. | `psx.enhancement.image-filtering` (Image Filtering) | `psx.image-filtering` | `smoothing`: default / smooth / sharp (default); `texture_filtering`: default / nearest / bilinear / xbr (default); `fmv_filtering`: default / nearest / bilinear / sharp / bicubic (default) | | `psx.enhancement.enhanced-audio` (Enhanced Audio) | `psx.enhanced-audio` | none: enabled = the high-quality SPU mix | | `psx.feature.rewind` (Rewind) | `psx.rewind` | `buffer`: 50 / 100 / 150 / 200 (50); `interval`: 1 / 4 / 8 / 12 / 15 (15) | +| `psx.enhancement.internal-resolution` (Internal Resolution) | `psx.internal-resolution` | `resolution`: default / native / 720p / 1080p / 1440p / 4k / 5k / 8k / display / auto (default) | **The title's keys stay its baseline.** `[video] crt_filter`, `scanlines`, `scanline_strength`, `antialiasing` (the linear present scale; Smoothing is not the `antialiasing_mode` FXAA pass), `texture_filtering`, `fmv_filter` and -`[audio] spu_hq` are what the title ships. With a mod off they apply unchanged; -with it on, each option replaces its part, and an option at `default` ("Game -default" in the launcher) keeps the baseline value. Scanline strength applies -to the player's own scanlines (Scanlines = On); Game default keeps the -title's scanlines and their strength. Enhanced Audio turns the high-quality -mix on; with it off the title's `spu_hq` stands. +`[audio] spu_hq`, and `[video] internal_resolution` (or the legacy +`supersampling` factor) with `dynamic_resolution`, are what the title ships. +With a mod off they apply unchanged; with it on, each option replaces its +part, and an option at `default` ("Game default" in the launcher) keeps the +baseline value. Scanline strength applies to the player's own scanlines +(Scanlines = On); Game default keeps the title's scanlines and their strength. +Enhanced Audio turns the high-quality mix on; with it off the title's `spu_hq` +stands. Internal Resolution's Auto is Match display with dynamic resolution +(floor: the title's `dynamic_resolution_min`); a fixed choice turns dynamic +resolution off. PGXP Precision needs 2x or more (720p and up at 240 lines) to +be visible. A title mod's `psx_mod_set_internal_resolution` request still wins +over the Internal Resolution mod, and `PSX_INTERNAL_RESOLUTION` over both +(docs/config_schema.md "Internal resolution"). **Activation only records.** Each plugin's activation records a request (`psx_host_enh_request_*`, `runtime/include/host_enhancements.h`); the renderer setup at the session start resolves the requests over the baseline -after activation (`session_host_look` in main.cpp) and arms the rewind ring -when the machine is configured. Applying at activation would be undone by the +after activation (`session_host_look` and `session_internal_resolution` in +main.cpp) and arms the rewind ring when the machine is configured. Applying at activation would be undone by the renderer setup, which is why the PGXP request works the same way. The env overrides (`PSX_TEXTURE_FILTER`, `PSX_SCANLINES`, `PSX_AUDIO_SHADOW`, -`PSX_REWIND`, ...) still win for a validation run, and the debug TCP commands -(`scanline`, `texture_filter`, `post_aa`, `dynres`) change the session's -effective values. +`PSX_REWIND`, `PSX_INTERNAL_RESOLUTION`, `PSX_SUPERSAMPLE`, ...) still win for +a validation run, and the debug TCP commands (`scanline`, `texture_filter`, +`post_aa`, `dynres`, `video_set`) change the session's effective values. -**Live changes.** CRT Filter and Image Filtering register option-changed -callbacks: an in-game menu option change (`psx_mod_set_option_live`) records -the new request and re-applies the look at once. Enhanced Audio and Rewind, -and switching any feature on or off, apply at the next start. +**Live changes.** CRT Filter, Image Filtering and Internal Resolution register +option-changed callbacks: an in-game menu option change +(`psx_mod_set_option_live`) records the new request and re-applies it at once. +Internal Resolution changes the scale through the same live path as the debug +server's `video_set` (`gl_renderer_set_internal_scale_live`); when the backend +cannot change its scale on a running game, or during a netplay match (the +scale is held), the callback refuses and the saved choice applies at the next +start. Enhanced Audio and Rewind, and switching any feature on or off, apply at +the next start. Because these features are host-only, the in-game menu accepts +their option changes during a netplay match too (`mh_mod_set` asks +`psx_mod_option_live_host_only`); every other feature stays fixed for the +match. **Rewind** keeps `psx_mod_set_rewind_blocked` and the netplay suppression (rewind never runs in a match). Its hotkey stays a player setting diff --git a/docs/NETPLAY.md b/docs/NETPLAY.md index 8d6601d7..833c5669 100644 --- a/docs/NETPLAY.md +++ b/docs/NETPLAY.md @@ -196,10 +196,12 @@ 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× +**Internal resolution is a per-peer presentation choice.** It is the built-in +Internal Resolution mod (`psx.enhancement.internal-resolution`), a host-only +mod (below), so it stays each player's own in 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. Its option does not change during a match (the scale is +held); a change made then applies at the next session. Only the GL present surface changes; the 1× software authority, the digests and the rollback snapshots are the same on every peer. Mods are cleared for netplay, except a player's own-view mods (below), which stay per player and never touch the shared game. @@ -459,8 +461,8 @@ repeat on a resim (e.g. `psx_mod_set_host_rumble`) check A fourth class, `netplay = "host_only"` (format 11), is for plugins that never touch guest execution at all -- the framework's CRT Filter, Image Filtering, -Enhanced Audio and Rewind mods: they only set host presentation, audio output -or host-tool state. Their features are no part of the plan fingerprint and stay +Enhanced Audio, Rewind and Internal Resolution mods: they only set host +presentation, audio output or host-tool state. Their features are no part of the plan fingerprint and stay each player's own in every mode, content negotiation included: a host never publishes them and a host's plan never switches them (docs/MOD_PACKAGES.md "Host-only plugins"). diff --git a/docs/RENDER_THREAD.md b/docs/RENDER_THREAD.md index eedbf637..234b1cea 100644 --- a/docs/RENDER_THREAD.md +++ b/docs/RENDER_THREAD.md @@ -252,8 +252,8 @@ on the present thread), queue high water. ## Dynamic resolution (`[video] dynamic_resolution`) -Opt-in: a player chooses Internal resolution = Auto (`settings.toml` -`internal_resolution = "auto"`), and a title ships `dynamic_resolution = +Opt-in: a player chooses Auto in the built-in Internal Resolution mod +(`psx.enhancement.internal-resolution`), and a title ships `dynamic_resolution = true` (or `internal_resolution = "auto"`) with its `dynamic_resolution_min` floor in `game.toml`; `PSX_DYNRES=0/1`, `PSX_DYNRES_MIN` for one run. The level steps between the configured internal resolution (the ceiling, e.g. Match diff --git a/docs/config_schema.md b/docs/config_schema.md index 7a0b4f4e..5e4e1790 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -763,8 +763,15 @@ Local rewind has no `game.toml` key; it is the Rewind mod A `settings.toml` written before this change may still carry `antialiasing`, `texture_filtering`, `fmv_filter`, `crt_filter`, `scanlines`, `scanline_strength`, `geometry_correction`, `perspective_texturing`, `rewind`, -`rewind_depth`, `rewind_interval`, `[audio] spu_hq` or the `quality_*` keys. -They are ignored, never an error, and the next save drops them. +`rewind_depth`, `rewind_interval`, `supersampling`, `internal_resolution`, +`supersample`, `dynamic_resolution`, `dynamic_resolution_min`, `[audio] +spu_hq`, `[audio] frequency` or the `quality_*` keys. They are ignored, never +an error, and the next save drops them (an empty `[audio]` table included). + +**Audio sample rate.** The host audio device opens at the runtime's built-in +rate (44.1 kHz, the SPU's own). It is not a player setting: the launcher has no +Sample rate row for PSX and `settings.toml` has no `[audio] frequency`. Volume +stays a launcher setting. **Graphics presets were removed.** A `game.toml` that still has a `[quality]` table (`[quality.low]` .. `[quality.ultra]`) fails to load with an error naming @@ -772,29 +779,31 @@ the table; delete it. No title shipped one. ### Internal resolution (`internal_resolution`, `supersampling`) -The player picks a preset in Settings → Display → **Internal resolution**: -Native, 720p, 1080p, 1440p, 4K, 5K, 8K, Match display or **Auto**. It is -stored in the player's `settings.toml`, and a game may ship a default the same -way: +The internal resolution is the title's `game.toml` baseline, and the player +changes it with the built-in **Internal Resolution** mod +(`psx.enhancement.internal-resolution`, docs/MOD_PACKAGES.md "Built-in host +enhancements"): Game default, Native, 720p, 1080p, 1440p, 4K, 5K, 8K, Match +display or **Auto**. It is not a Settings row and not a `settings.toml` key. +With the mod off, or at Game default, the title's keys apply unchanged: ```toml [video] internal_resolution = "4k" # native | 720p | 1080p | 1440p | 4k | 5k | 8k | display | auto, or a number of lines -resolution_reference_lines = 240 # game.toml only: the title's usual display height (120..1024) -supersample_disable_at_lines = 0 # game.toml only: supersample 1.0 at/above this display height (0 = off, 240..8640) -match_display_max_lines = 0 # game.toml only: cap on the display height Match display uses (0 = none, 240..8640) +resolution_reference_lines = 240 # the title's usual display height (120..1024) +supersample_disable_at_lines = 0 # supersample 1.0 at/above this display height (0 = off, 240..8640) +match_display_max_lines = 0 # cap on the display height Match display uses (0 = none, 240..8640) ``` `match_display_max_lines` caps the monitor height that Match display (and Auto) and its dynamic-resolution floor (`display`) resolve against. It also caps the final Match display target after supersampling, -so a larger saved factor cannot exceed it. Fixed presets keep their own target. +so a larger factor cannot exceed it. Fixed presets keep their own target. With no cap, an 8K panel asks 18x (27x with `supersample = 1.5`), which is past the full-VRAM surface. The high-resolution window then engages, and dynamic resolution cannot step there. A title whose GPU budget is set for 4K lines sets 2160. `supersample_disable_at_lines` uses the physical monitor height before the cap. -At or above its threshold the effective factor is 1.0; the saved factor is +At or above its threshold the effective factor is 1.0; the configured factor is retained for smaller monitors. Zero preserves the default behaviour. A preset is a target height. The runtime renders at the integer scale @@ -804,48 +813,41 @@ A preset is a target height. The runtime renders at the integer scale 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. +PGXP Precision's sub-pixel geometry needs S >= 2 (720p and up at 240 lines) to +be visible: at native resolution the corrected positions round back to the +same pixels. **Auto** is Match display with dynamic resolution on: the scale starts at the monitor's resolution and steps down to the title's `dynamic_resolution_min` -floor only while the game would miss frames, then back up. Choosing any fixed -preset turns dynamic resolution off; there is no separate Dynamic resolution -switch or Lowest resolution row. In `settings.toml` it is -`internal_resolution = "auto"`; the retired `dynamic_resolution` key is read -once (`true` becomes `"auto"`, `false` keeps the saved preset) and dropped on -the next save, and the retired `dynamic_resolution_min` is ignored (the floor -is the title's). A title may ship `internal_resolution = "auto"`, or its own -fixed ceiling with `dynamic_resolution = true`; the launcher then shows Auto, -and the title's ceiling stays until the player picks a different row. -`PSX_DYNRES=0|1` still overrides dynamic resolution for one run. +floor only while the game would miss frames, then back up. The mod's fixed +choices turn dynamic resolution off; there is no separate Dynamic resolution +switch or Lowest resolution row. A title may ship `internal_resolution = +"auto"`, or its own fixed ceiling with `dynamic_resolution = true`; Game default +keeps either. `PSX_DYNRES=0|1` still overrides dynamic resolution for one run. -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. +Precedence for a session: `PSX_INTERNAL_RESOLUTION` (one run), then a title +mod's `psx_mod_set_internal_resolution` request, then the Internal Resolution +mod's choice, then the title's `internal_resolution` (or its legacy +`supersampling` factor). Every session start restores the title's values before +the mod's choice applies, so a rematch never inherits the last session's scale. +The mod's option changes apply live from the in-game menu when the OpenGL +backend can change its scale on a running game (otherwise at the next start), +and never during a netplay match. The debug server's `video_set` changes the +session's effective internal resolution and supersample live; it saves +nothing. -A host built against a recomp-ui without the Internal resolution row keeps -the legacy Supersampling row (1x to 4x) and that row stays in charge: with no -preset configured the launcher round trip is exactly the historical one, and -no `internal_resolution` key is written. A preset from `game.toml` or a -hand-edited `settings.toml` starts the row on the nearest factor it can show -and survives if the row is left alone; picking a different factor there -drops the preset. - -`PSX_INTERNAL_RESOLUTION=` overrides every layer for one -run (validation, and two local netplay peers that share one `settings.toml`). -It is never written to `settings.toml`: the launcher shows and saves the -configured preset. +`PSX_INTERNAL_RESOLUTION=` overrides every layer for one run +(validation, and two local netplay peers that share one install). 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 -player's `settings.toml`; 1 (the default) is native and unchanged. +`supersampling = N` (game.toml) renders at N times the native resolution per +axis and downsamples to the window, 1 to 32; 1 (the default) is native and +unchanged. It is the title's legacy form of the baseline: an +`internal_resolution`, or the mod's choice, supersedes it. `settings.toml` +`window_width` accepts 640 to 7680. ### Bloom (`bloom`) @@ -890,8 +892,9 @@ it becomes a scale: Match display on a 1080 px monitor with 2.0 asks for 2160 lines (S = 9) and the present's area resolve brings it back to 1080, which is ordered-grid SSAA of everything, 2D included. Native and the legacy `supersampling` factor are left alone. Dynamic resolution takes the result as -its ceiling and steps under it as before. `PSX_SUPERSAMPLE=` overrides -it for one run. +its ceiling and steps under it as before. It is the title's key only (the +Internal Resolution mod applies it to the player's choice too); +`PSX_SUPERSAMPLE=` overrides it for one run. To make supersampling the elastic part only, set the dynamic resolution floor to the output: `dynamic_resolution_min = "display"` (game.toml, also diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index fcb658ee..526279ee 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -5d97f855b28e4dd86fa43e30f86ad0f189c55f2c08f54bf593e8fc5148315eda +0cc780c1659092c6d7777245b76d51d61ff513bf7ca9fbecd5981c10533a2841 diff --git a/mods/builtin/packages/psx.enhancement.internal-resolution/1.0.0/manifest.toml b/mods/builtin/packages/psx.enhancement.internal-resolution/1.0.0/manifest.toml new file mode 100644 index 00000000..4ef8a6a8 --- /dev/null +++ b/mods/builtin/packages/psx.enhancement.internal-resolution/1.0.0/manifest.toml @@ -0,0 +1,77 @@ +format_version = 11 +id = "psx.enhancement.internal-resolution" +version = "1.0.0" +name = "Internal Resolution" +author = "psxrecomp" +description = "The height the 3D picture is rendered at before it is scaled to the window: Native is the console's own, higher settings render sharper geometry. Auto renders at your display's resolution and steps down only while the game would otherwise miss frames. Presentation only: the game runs exactly the same, and in netplay each player keeps their own choice." +resolver = "declarative" +save_compatibility = "shared" + +# The render height is a property of the host renderer, not of any particular +# disc, so this ships with the framework and applies to every game. The +# title's [video] internal_resolution (or its legacy supersampling factor) and +# dynamic_resolution stay its baseline: with the feature off they apply +# unchanged, and "Game default" keeps them. +[[target]] +game_id = "*" + +[[feature]] +id = "internal-resolution" +name = "Internal Resolution" +description = "Disabled by default: the game keeps its own resolution. Turn it on to choose one. PGXP Precision needs 2x or more (720p and up) to be visible." +group = "Visual" +default_enabled = false + +[[option]] +feature = "internal-resolution" +id = "resolution" +label = "Internal resolution" +description = "Native is the console's own resolution. 720p through 8K render that many lines (rounded up to a whole multiple of the game's own lines). Match display renders at your monitor's height. Auto is Match display that steps down to the game's minimum only while frames would be missed." +group = "Visual" +type = "choice" +default = "default" + +[[option.choice]] +value = "default" +label = "Game default" + +[[option.choice]] +value = "native" +label = "Native" + +[[option.choice]] +value = "720p" +label = "720p" + +[[option.choice]] +value = "1080p" +label = "1080p" + +[[option.choice]] +value = "1440p" +label = "1440p" + +[[option.choice]] +value = "4k" +label = "4K" + +[[option.choice]] +value = "5k" +label = "5K" + +[[option.choice]] +value = "8k" +label = "8K" + +[[option.choice]] +value = "display" +label = "Match display" + +[[option.choice]] +value = "auto" +label = "Auto" + +[[plugin]] +feature = "internal-resolution" +id = "psx.internal-resolution" +netplay = "host_only" diff --git a/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml b/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml index 77bc0fab..e0fce90f 100644 --- a/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml +++ b/mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml @@ -15,7 +15,7 @@ game_id = "*" [[feature]] id = "pgxp" name = "PGXP Precision" -description = "Disabled by default (the faithful floor). Corrections are validated per vertex against the exact packet words the game drew with and clamped to half a pixel, so 2D sprites and UI are never touched and a vertex the tracker cannot prove simply draws the faithful way. Most effective on a build compiled with the PGXP hook variant; on other builds coverage is reduced but never harmful. Needs supersampling >= 2 to be visible." +description = "Disabled by default (the faithful floor). Corrections are validated per vertex against the exact packet words the game drew with and clamped to half a pixel, so 2D sprites and UI are never touched and a vertex the tracker cannot prove simply draws the faithful way. Most effective on a build compiled with the PGXP hook variant; on other builds coverage is reduced but never harmful. Needs an internal resolution of 2x or more (720p and up) to be visible: set one with the Internal Resolution mod if the game does not." group = "Visual" default_enabled = false diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 9f5e9050..d0153ccc 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -3149,10 +3149,6 @@ GameOptions load_game_options(const fs::path& path) { // ---- UserSettings (settings.toml) — launcher-written override layer ---- -static bool valid_user_audio_freq(int n) { - return n == 32040 || n == 32000 || n == 44100 || n == 48000; -} - UserSettings load_user_settings(const fs::path& path) { UserSettings s; std::error_code ec; @@ -3180,46 +3176,12 @@ UserSettings load_user_settings(const fs::path& path) { else if (m == "opengl") { s.renderer = 1; s.has_renderer = true; } else if (m == "vulkan") { s.renderer = 2; s.has_renderer = true; } }); - if (v.contains("supersampling")) try_get([&]{ - const auto n = toml::find(v, "supersampling"); - if (n >= 1 && n <= 32) { s.supersampling = (int)n; s.has_supersampling = true; } - }); - if (v.contains("supersample")) try_get([&]{ - const toml::value& ss = toml::find(v, "supersample"); - const double f = ss.is_integer() ? static_cast(ss.as_integer()) - : toml::get(ss); - if (f >= 1.0 && f <= 4.0) { - s.supersample_milli = static_cast(f * 1000.0 + 0.5); - s.has_supersample = 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; - } - } - }); - // Retired: the Dynamic resolution checkbox became Internal resolution - // = Auto. A file written before that is read once: a player's - // `dynamic_resolution = true` was dynamic resolution under some - // ceiling, which Auto (Match display, the title's floor) now means, - // so it becomes internal_resolution = "auto" and the next save drops - // the old key. `false`, and the old dynamic_resolution_min (the floor - // is the title's now), are ignored. - if (v.contains("dynamic_resolution")) try_get([&]{ - if (toml::find(v, "dynamic_resolution")) { - s.internal_resolution = PSX_IR_AUTO; - s.has_internal_resolution = true; - } - }); + // supersampling, internal_resolution, supersample, dynamic_resolution + // and dynamic_resolution_min are no longer player settings: the + // built-in Internal Resolution mod (psx.enhancement.internal-resolution) + // owns the internal resolution over the title's [video] baseline, Auto + // (dynamic resolution) included. Old keys are ignored, not errors, and + // the next save drops them (docs/config_schema.md). if (v.contains("window_width")) try_get([&]{ const auto n = toml::find(v, "window_width"); if (n >= 640 && n <= 7680) { s.window_width = (int)n; s.has_window_width = true; } @@ -3310,17 +3272,9 @@ UserSettings load_user_settings(const fs::path& path) { // rewind / rewind_depth / rewind_interval moved to the built-in // Rewind mod (psx.feature.rewind); the old keys are ignored. } - if (doc.contains("audio")) { - const toml::value& a = toml::find(doc, "audio"); - if (a.contains("frequency")) try_get([&]{ - const auto n = toml::find(a, "frequency"); - if (valid_user_audio_freq((int)n)) { - s.audio_freq = (int)n; - s.has_audio_freq = true; - } - }); - // spu_hq moved to the built-in Enhanced Audio mod; ignored here. - } + // [audio] holds no player setting: frequency (the output sample rate) is + // the runtime's built-in rate and spu_hq is the built-in Enhanced Audio + // mod. Both old keys are ignored and the next save drops them. if (doc.contains("hotkeys")) { const toml::value& h = toml::find(doc, "hotkeys"); if (h.contains("rewind_pad")) try_get([&]{ @@ -3569,17 +3523,6 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { f << "renderer = \"" << (s.renderer == 2 ? "vulkan" : s.renderer == 1 ? "opengl" : "software") << "\"\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_supersample) - f << "supersample = " << (s.supersample_milli / 1000.0) << "\n"; if (s.has_window_width) f << "window_width = " << s.window_width << "\n"; if (s.has_fov_scale) @@ -3615,9 +3558,6 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { f << "aspect_ratio = \"" << s.aspect_num << ":" << s.aspect_den << "\"\n"; if (s.has_adaptive_view) f << "adaptive_view = " << (s.adaptive_view ? "true" : "false") << "\n"; - f << "\n[audio]\n"; - if (s.has_audio_freq) - f << "frequency = " << s.audio_freq << "\n"; if (s.has_hotkey_pad_rewind || s.has_hotkey_pad_save_state_menu || s.has_hotkey_pad_fast_forward || s.has_hotkey_pad_fast_forward_toggle || s.has_fast_forward_speed || s.has_fast_forward_mode) { diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index eca6feef..4ae0afc6 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -1562,20 +1562,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; - // Supersampling on top of the internal resolution, in thousandths - // (1000 off .. 4000), the player's pick of [video] supersample. - bool has_supersample = false; int supersample_milli = 1000; - // There is no dynamic-resolution key: internal_resolution = "auto" - // (PSX_IR_AUTO) is dynamic resolution, and a fixed value is not. A file - // from before that change is read once: dynamic_resolution = true becomes - // "auto" (load_user_settings). The floor is the title's - // [video] dynamic_resolution_min. + // No supersampling / internal_resolution / supersample / + // dynamic_resolution keys: the built-in Internal Resolution mod + // (psx.enhancement.internal-resolution) owns the internal resolution, + // Auto (dynamic resolution) included, over the title's [video] + // baseline (runtime/include/host_enhancements.h). Old keys are ignored. // 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 @@ -1647,8 +1638,9 @@ struct UserSettings { bool has_fast_forward_speed = false; double fast_forward_speed = 4.0; bool has_fast_forward_mode = false; bool fast_forward_toggle_mode = false; bool has_hotkey_pad_fast_forward_toggle = false; int hotkey_pad_fast_forward_toggle = 0; /* unbound */ - // [audio] (the high-quality SPU is the built-in Enhanced Audio mod) - bool has_audio_freq = false; int audio_freq = 44100; + // [audio]: no player keys. The output sample rate is the runtime's + // built-in rate (the old `frequency` key is ignored) and the + // high-quality SPU is the built-in Enhanced Audio mod. // [bios] / [disc] / [memcard] bool has_bios_path = false; std::filesystem::path bios_path; bool has_disc_path = false; std::filesystem::path disc_path; diff --git a/recompiler/tests/rewind_settings_test.cpp b/recompiler/tests/rewind_settings_test.cpp index 298fdfcb..383d1589 100644 --- a/recompiler/tests/rewind_settings_test.cpp +++ b/recompiler/tests/rewind_settings_test.cpp @@ -50,7 +50,7 @@ static std::string read_text(const fs::path& p) { static void test_old_keys_ignored_and_dropped() { fs::path p = write_temp("psxrecomp_rewind_retired.toml", "[video]\n" - "supersampling = 2\n" + "window_width = 1600\n" "rewind = true\n" "rewind_depth = 150\n" "rewind_interval = 8\n" @@ -58,7 +58,7 @@ static void test_old_keys_ignored_and_dropped() { "rewind_pad = 1272\n"); auto us = PSXRecompV4::load_user_settings(p); check(!us.parse_error, "a settings.toml with the old rewind keys still parses"); - check(us.has_supersampling && us.supersampling == 2, + check(us.has_window_width && us.window_width == 1600, "the player's other settings survive the retired keys"); check(us.has_hotkey_pad_rewind && us.hotkey_pad_rewind == 1272, "the Rewind controller hotkey is still a setting"); diff --git a/recompiler/tests/video_enhancement_settings_test.cpp b/recompiler/tests/video_enhancement_settings_test.cpp index b4f6c67a..90d43a1d 100644 --- a/recompiler/tests/video_enhancement_settings_test.cpp +++ b/recompiler/tests/video_enhancement_settings_test.cpp @@ -9,10 +9,11 @@ * 1. both default OFF (the faithful floor) when game.toml says nothing; * 2. game.toml [video] turns them on -- the title's baseline; * 3. settings.toml no longer carries them, nor any other look the built-in - * mods own (CRT Filter, Image Filtering, PGXP Precision): old keys are - * ignored, not errors, and a save drops them; - * 4. Internal resolution "auto" (dynamic resolution) and the one-time - * migration of the retired dynamic_resolution key; + * mods own (CRT Filter, Image Filtering, PGXP Precision, Internal + * Resolution), nor the audio sample rate: old keys are ignored, not + * errors, and a save drops them; + * 4. game.toml internal_resolution "auto" (dynamic resolution), the title's + * baseline the Internal Resolution mod resolves over; * 5. a game.toml with the retired [quality.*] presets fails to load. */ #include "config_loader.h" @@ -170,16 +171,19 @@ static void test_retired_player_look_keys() { "rewind = true\n" "rewind_depth = 100\n" "rewind_interval = 4\n" + "dynamic_resolution = true\n" "dynamic_resolution_min = \"1080p\"\n" + "supersampling = 2\n" + "internal_resolution = \"4k\"\n" + "supersample = 1.5\n" + "window_width = 1600\n" "[audio]\n" "frequency = 48000\n" "spu_hq = true\n"); auto us = PSXRecompV4::load_user_settings(p); check(!us.parse_error, "a settings.toml with retired keys still parses"); check(us.has_renderer && us.renderer == 1, "the player's other video settings survive"); - check(us.has_audio_freq && us.audio_freq == 48000, "the player's audio frequency survives"); - check(!us.has_internal_resolution, - "dynamic_resolution_min alone does not invent an internal resolution"); + check(us.has_window_width && us.window_width == 1600, "the player's window size survives"); fs::remove(p); fs::path out = fs::temp_directory_path() / "psxrecomp_retired_look_saved.toml"; @@ -188,16 +192,19 @@ static void test_retired_player_look_keys() { for (const char* key : {"antialiasing", "texture_filtering", "fmv_filter", "crt_filter", "scanlines", "scanline_strength", "geometry_correction", "perspective_texturing", "quality_", "rewind =", "rewind_depth", - "rewind_interval", "spu_hq", "dynamic_resolution"}) + "rewind_interval", "spu_hq", "dynamic_resolution", + "supersampling", "internal_resolution", "supersample", + "frequency", "[audio]"}) check(text.find(key) == std::string::npos, (std::string("a save drops the retired key ") + key).c_str()); - check(text.find("frequency = 48000") != std::string::npos, "a save keeps the rest"); + check(text.find("renderer") != std::string::npos && + text.find("window_width = 1600") != std::string::npos, "a save keeps the rest"); fs::remove(out); } -/* Internal resolution "auto" is Match display with dynamic resolution on: a - * title can ship it, a player can choose it, and it round-trips by its id. A - * player's retired dynamic_resolution = true is read once as "auto". */ +/* Internal resolution "auto" is Match display with dynamic resolution on. A + * title can ship it as its baseline (the player chooses it through the + * Internal Resolution mod); a dynamic-resolution floor never is. */ static void test_internal_resolution_auto() { fs::path p = write_game_toml("psxrecomp_ir_auto.toml", "[video]\n" @@ -214,43 +221,6 @@ static void test_internal_resolution_auto() { try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; } check(rejected, "dynamic_resolution_min is a fixed floor, never \"auto\""); fs::remove(p); - - p = write_temp("psxrecomp_ir_settings_auto.toml", - "[video]\n" - "internal_resolution = \"auto\"\n"); - auto us = PSXRecompV4::load_user_settings(p); - check(us.has_internal_resolution && us.internal_resolution == -2, - "settings.toml internal_resolution = \"auto\" reads PSX_IR_AUTO"); - fs::remove(p); - - struct { const char* body; bool has; int value; const char* what; } migrate[] = { - { "[video]\ndynamic_resolution = true\n", true, -2, - "dynamic_resolution = true alone becomes auto" }, - { "[video]\ninternal_resolution = \"1080p\"\ndynamic_resolution = true\n", true, -2, - "dynamic resolution under a fixed ceiling becomes auto" }, - { "[video]\ninternal_resolution = \"1080p\"\ndynamic_resolution = false\n", true, 1080, - "dynamic_resolution = false keeps the fixed resolution" }, - { "[video]\ndynamic_resolution = false\n", false, 0, - "dynamic_resolution = false alone chooses nothing" }, - }; - for (const auto& m : migrate) { - p = write_temp("psxrecomp_dynres_migrate.toml", m.body); - auto mu = PSXRecompV4::load_user_settings(p); - check(mu.has_internal_resolution == m.has && - (!m.has || mu.internal_resolution == m.value), m.what); - fs::remove(p); - } - - PSXRecompV4::UserSettings out; - out.has_internal_resolution = true; out.internal_resolution = -2; - p = fs::temp_directory_path() / "psxrecomp_ir_auto_roundtrip.toml"; - check(PSXRecompV4::save_user_settings(p, out), "save_user_settings writes auto"); - check(read_text(p).find("internal_resolution = \"auto\"") != std::string::npos, - "auto is persisted by its stable id"); - auto back = PSXRecompV4::load_user_settings(p); - check(back.has_internal_resolution && back.internal_resolution == -2, - "auto survives a save/load round trip"); - fs::remove(p); } /* Graphics presets were removed. A title that still carries [quality.*] @@ -350,54 +320,15 @@ static void test_internal_resolution_game_toml() { } } -static void test_internal_resolution_settings() { - fs::path p = write_temp("psxrecomp_ir_settings.toml", +/* window_width is still a player setting, up to an 8K-wide window. */ +static void test_window_width_settings() { + fs::path p = write_temp("psxrecomp_window_width.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::string text; - { // closed before fs::remove: Windows cannot delete an open file - std::ifstream f(p); - text.assign(std::istreambuf_iterator(f), std::istreambuf_iterator()); - } - 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); - } } /* [video] texture_window_batching: a game.toml-only OpenGL batching opt-in @@ -717,7 +648,7 @@ int main() { } test_fractional_fast_forward(); test_internal_resolution_game_toml(); - test_internal_resolution_settings(); + test_window_width_settings(); test_defaults_off(); test_game_toml_opt_in(); test_game_window_width_validation(); diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 72f957ff..654432f4 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1397,7 +1397,8 @@ if(BUILD_TESTING) src/mod_packages.cpp src/mod_media.cpp src/iso_reader.cpp src/cue_sheet.cpp src/crc32.c - src/psx_sha256.c) + src/psx_sha256.c + src/host_enhancements.c) target_include_directories(builtin_mod_manifests_test PRIVATE include ../recompiler/include @@ -1741,15 +1742,16 @@ if(BUILD_TESTING) add_test(NAME mod_internal_resolution_test COMMAND mod_internal_resolution_test) # The built-in host enhancement mods (CRT Filter, Image Filtering, - # Enhanced Audio, Rewind): the real plugins and their session requests - # against a fake mod runtime (host_enhancements.h). + # Enhanced Audio, Rewind, Internal Resolution): the real plugins and their + # session requests against a fake mod runtime (host_enhancements.h). add_executable(host_enhancements_test tests/test_host_enhancements.c src/host_enhancements.c src/mod_builtin_crt_filter.c src/mod_builtin_image_filtering.c src/mod_builtin_enhanced_audio.c - src/mod_builtin_rewind.c) + src/mod_builtin_rewind.c + src/mod_builtin_internal_resolution.c) target_include_directories(host_enhancements_test PRIVATE include) add_test(NAME host_enhancements_test COMMAND host_enhancements_test) diff --git a/runtime/include/host_enhancements.h b/runtime/include/host_enhancements.h index 6bd26549..7e6058ca 100644 --- a/runtime/include/host_enhancements.h +++ b/runtime/include/host_enhancements.h @@ -14,6 +14,10 @@ * ([audio] spu_hq) * psx.feature.rewind Rewind: the local rewind ring, its buffer * depth and capture interval + * psx.enhancement.internal-resolution + * Internal Resolution: the render height + * ([video] internal_resolution), Auto + * (dynamic resolution) included * * These used to be player settings (settings.toml, the launcher's Display and * Audio cards, the in-game menu). The title's game.toml [video] / [audio] keys @@ -32,7 +36,7 @@ * Activation only records: it runs before the renderer setup, which applies * the baseline and would otherwise undo whatever activation had applied. * - * All four are host-only (their [[plugin]] declares netplay = "host_only"): + * All five are host-only (their [[plugin]] declares netplay = "host_only"): * they change what this machine shows, plays or keeps, never guest execution, * so they stay each player's own in netplay and never enter the plan * fingerprint (docs/MOD_PACKAGES.md "Host-only plugins"). @@ -40,9 +44,14 @@ * Live option changes (the in-game menu's psx_mod_set_option_live): the CRT * Filter and Image Filtering plugins update their request and call the * applier main.cpp registered, which resolves again and pushes the result to - * the renderer. Main thread only, like the rest of the mod runtime. + * the renderer. The Internal Resolution plugin calls its own applier, which + * can refuse (the backend cannot change scale live here, or a netplay match + * holds the scale): the change then applies at the next start. Main thread + * only, like the rest of the mod runtime. */ +#include "internal_resolution.h" + #ifdef __cplusplus extern "C" { #endif @@ -75,8 +84,21 @@ typedef struct PsxHostEnhRequests { int rewind; /* Rewind enabled */ int rewind_depth; /* 50 / 100 / 150 / 200 */ int rewind_interval; /* 1 / 4 / 8 / 12 / 15 */ + int resolution; /* Internal Resolution enabled */ + int resolution_value; /* PSX_IR_UNSET (Game default) or a PSX_IR_* + * value (internal_resolution.h), Auto included; + * not PSX_HOST_ENH_DEFAULT, which is + * PSX_IR_DISPLAY's encoding */ } PsxHostEnhRequests; +/* The session's internal resolution in main.cpp's terms: the value + * (PSX_IR_UNSET: the legacy [video] supersampling factor stands) and whether + * dynamic resolution steps under it. */ +typedef struct PsxHostResolution { + int internal_resolution; + int dynamic_resolution; +} PsxHostResolution; + #define PSX_HOST_ENH_STRENGTH_DEFAULT 50 #define PSX_HOST_ENH_REWIND_DEPTH_DEFAULT 50 #define PSX_HOST_ENH_REWIND_INTERVAL_DEFAULT 15 @@ -108,6 +130,20 @@ static inline PsxHostLook psx_host_enh_resolve(const PsxHostLook* baseline, return out; } +/* The effective internal resolution: the title's (its internal_resolution and + * dynamic_resolution) unless the Internal Resolution mod names one. Auto is + * Match display with dynamic resolution on; any fixed choice turns it off. + * Pure. */ +static inline PsxHostResolution psx_host_enh_resolve_resolution( + const PsxHostResolution* baseline, const PsxHostEnhRequests* req) { + PsxHostResolution out = *baseline; + if (req->resolution && req->resolution_value != PSX_IR_UNSET) { + out.internal_resolution = req->resolution_value; + out.dynamic_resolution = psx_ir_is_auto(req->resolution_value) ? 1 : 0; + } + return out; +} + /* Session start, before plugin activation: forget every request. */ void psx_host_enh_reset(void); @@ -118,6 +154,7 @@ void psx_host_enh_request_crt(int screen_kind, int scanlines, int strength_pct); void psx_host_enh_request_image(int smoothing, int texture_filter, int fmv_filter); void psx_host_enh_request_audio(void); void psx_host_enh_request_rewind(int depth, int interval); +void psx_host_enh_request_resolution(int value); /* A copy of this session's requests. */ void psx_host_enh_requests(PsxHostEnhRequests* out); @@ -130,6 +167,13 @@ typedef void (*PsxHostEnhApplier)(void); void psx_host_enh_set_live_applier(PsxHostEnhApplier fn); int psx_host_enh_apply_live(void); +/* The Internal Resolution plugin's live re-application. The applier returns 1 + * when the new scale took effect now, 0 when it waits for the next start + * (no live-capable backend, or a netplay match). No applier registered: 0. */ +typedef int (*PsxHostEnhResolutionApplier)(void); +void psx_host_enh_set_resolution_applier(PsxHostEnhResolutionApplier fn); +int psx_host_enh_apply_resolution_live(void); + /* Manifest option text -> encoding. Unknown text gives the option's default * (Game default, or the documented number), never an error: a value the * catalog accepted is always one of these. */ @@ -141,6 +185,9 @@ int psx_host_enh_parse_texture_filter(const char* text);/* default nearest bilin int psx_host_enh_parse_fmv_filter(const char* text); /* default nearest bilinear sharp bicubic */ int psx_host_enh_parse_rewind_depth(const char* text); /* "50" "100" "150" "200" */ int psx_host_enh_parse_rewind_interval(const char* text);/* "1" "4" "8" "12" "15" */ +/* "default" (and anything unknown) -> PSX_IR_UNSET; otherwise a preset id + * of internal_resolution.h ("native", "720p", ..., "display", "auto"). */ +int psx_host_enh_parse_internal_resolution(const char* text); #ifdef __cplusplus } diff --git a/runtime/include/host_overlay.h b/runtime/include/host_overlay.h index 2fd76833..a695a5c3 100644 --- a/runtime/include/host_overlay.h +++ b/runtime/include/host_overlay.h @@ -76,25 +76,29 @@ void psx_host_set_ui_capture(int on); int psx_host_ui_capture_active(void); /* ---- P4: live video settings --------------------------------------------- - * The [video] keys an in-game menu may change, in settings.toml terms: the - * player's PC/system display settings. Screen model, scanlines, smoothing, - * texture and FMV filtering are the built-in CRT Filter and Image Filtering - * mods (host_enhancements.h), changed live through their mod options - * (psx_mod_set_option_live), not here; dynamic resolution is - * internal_resolution = PSX_IR_AUTO. psx_video_apply_live() applies what can - * change on a running game, saves every field to settings.toml, and returns - * PSX_VIDEO_RESTART_* bits for the fields that only take effect on the next - * start (compared with boot). Main thread only. */ + * The session's video state an in-game menu (or the debug server's video_set) + * may change. Screen model, scanlines, smoothing, texture and FMV filtering + * and the internal resolution are the built-in CRT Filter, Image Filtering + * and Internal Resolution mods (host_enhancements.h), changed live through + * their mod options (psx_mod_set_option_live); the internal_resolution and + * supersample_milli fields here are the session's effective values, for the + * debug server and the restart bits, and are not player settings. + * psx_video_apply_live() applies what can change on a running game, saves the + * player's PC/system fields (fullscreen, renderer, threads, frame generation) + * to settings.toml, and returns PSX_VIDEO_RESTART_* bits for the fields that + * only take effect on the next start (compared with boot). Main thread + * only. */ typedef struct PsxHostVideoSettings { int fullscreen; /* 0 windowed, 1 borderless, 2 exclusive */ int frame_generation; /* 0/1 */ int renderer; /* restart */ - int internal_resolution; /* live (psx_video_apply_live); PSX_IR_AUTO = - * Match display with dynamic resolution */ - int supersampling; /* legacy factor; restart */ + int internal_resolution; /* session, live (psx_video_apply_live); + * PSX_IR_AUTO = Match display with dynamic + * resolution; not saved */ + int supersampling; /* the title's legacy factor; restart */ int render_thread; /* restart */ int present_thread; /* restart */ - int supersample_milli; /* live: 1000 off, 1250, 1500, 2000 */ + int supersample_milli; /* session, live: 1000 off .. 4000; not saved */ } PsxHostVideoSettings; /* PsxHostVideoSettings no longer carries texture_filter, fmv_filter, * scanlines, scanline_strength_pct, present_linear or dynamic_resolution: a diff --git a/runtime/include/internal_resolution.h b/runtime/include/internal_resolution.h index 70816465..c9fa76e1 100644 --- a/runtime/include/internal_resolution.h +++ b/runtime/include/internal_resolution.h @@ -1,9 +1,12 @@ -/* internal_resolution.h — the "Internal resolution" setting: presets, their - * persisted ids, and the integer scale each one asks the renderer for. +/* internal_resolution.h — the internal resolution: presets, their ids (the + * game.toml [video] internal_resolution spellings and the built-in Internal + * Resolution mod's option values), 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. + * Pure (no SDL, no GL) so the config loader, the mod plugin and a test can + * use it. * - * VALUE ENCODING (shared with recomp-ui's Settings.internal_resolution): + * VALUE ENCODING: * 0 unset: the legacy [video] supersampling factor stands * 1 native (1x) * -1 match display: the target is the monitor's pixel height @@ -37,8 +40,8 @@ #define PSX_IR_MAX_LINES 8640 typedef struct PsxIrPreset { - const char *id; /* stable id persisted in settings.toml */ - const char *label; /* launcher label */ + const char *id; /* stable id: game.toml and the mod option value */ + const char *label; /* display label (the mod option's) */ int value; /* encoding above */ } PsxIrPreset; @@ -199,88 +202,6 @@ static inline int psx_resolve_internal_scale_ss_cap(int value, int ref_lines, return 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; -} - -/* ---- The launcher round trip ------------------------------------------------- - * The host seeds the launcher with its video state, the player may change it, - * and the host adopts what comes back. A recomp-ui built with the Internal - * resolution row (RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) owns the preset. - * An older one only has the legacy Supersampling row, a 1x..4x cycle that - * clamps whatever it is seeded with. There a pick in that row must win over - * any preset, and a preset the player cannot see (game.toml, a hand-edited - * settings.toml) must survive an untouched row. With no preset the round trip - * is exactly the historical one: seed the factor, take the factor back. */ -#define PSX_IR_LEGACY_SS_MAX 4 /* the legacy row's range, and settings.toml's - * supersampling for an older runtime */ - -/* The value seeded into the legacy Supersampling row. has_row: the launcher - * shows the Internal resolution row instead (the factor passes through). */ -static inline int psx_ir_launcher_seed_supersampling(int has_row, int preset, - int supersampling, - int ref_lines, - int display_px_h) { - if (has_row || preset == PSX_IR_UNSET) return supersampling; - /* The preset's scale, as near as the row can show it. */ - return psx_resolve_internal_scale(preset, ref_lines, display_px_h, - PSX_IR_LEGACY_SS_MAX); -} - -typedef struct PsxIrAdopted { - int preset; /* the preset kept (PSX_IR_UNSET: the factor stands) */ - int scale; /* the scale to request */ - int save_ss; /* settings.toml supersampling */ - int save_ir; /* settings.toml internal_resolution; PSX_IR_UNSET omits it */ -} PsxIrAdopted; - -/* What the host adopts when the launcher returns. - * has_row the launcher showed the Internal resolution row - * preset the host's preset when it seeded the launcher - * ss_seed what psx_ir_launcher_seed_supersampling returned - * ss_result the Supersampling row's value on return - * ir_result the Internal resolution row's value (has_row only) - * s_max the chosen renderer's ceiling */ -static inline PsxIrAdopted psx_ir_adopt_launcher(int has_row, int preset, - int ss_seed, int ss_result, - int ir_result, int ref_lines, - int display_px_h, int s_max) { - PsxIrAdopted a; - if (has_row) { - if (psx_ir_value_valid(ir_result)) a.preset = ir_result; - else if (preset != PSX_IR_UNSET) a.preset = preset; - else a.preset = psx_ir_from_supersampling(ss_seed < 1 ? 1 : ss_seed, ref_lines); - } else { - int shown = ss_seed < 1 ? 1 : ss_seed > PSX_IR_LEGACY_SS_MAX - ? PSX_IR_LEGACY_SS_MAX : ss_seed; - a.preset = (preset != PSX_IR_UNSET && ss_result == shown) ? preset - : PSX_IR_UNSET; - } - if (a.preset == PSX_IR_UNSET) { - a.scale = ss_result; - a.save_ss = ss_result; - a.save_ir = PSX_IR_UNSET; - } else { - a.scale = psx_resolve_internal_scale(a.preset, ref_lines, display_px_h, s_max); - /* An older runtime reading settings.toml only knows supersampling - * (1..4): leave it the nearest it can do. */ - a.save_ss = a.scale < PSX_IR_LEGACY_SS_MAX ? a.scale : PSX_IR_LEGACY_SS_MAX; - a.save_ir = a.preset; - } - return a; -} - /* ---- Dynamic resolution floor ([video] dynamic_resolution_min) ------------- * A preset or line count resolves as usual. "display" (PSX_IR_DISPLAY) is the * output's own lines: the selected internal resolution WITHOUT the supersample diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index d99c26b3..7e174645 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -471,8 +471,8 @@ int psx_mod_current_option_value(const char* option_id, * option for the rest of the session and notifies its plugin. Returns 1 when * a plugin applied it live; 0 when the feature has no live-capable plugin (or * is not in the plan) -- then nothing changes until the next start; -1 when - * refused: during netplay the plan is - * negotiated between peers and must not change. Persisting the choice stays + * refused: during netplay the plan is negotiated between peers and must not + * change, except a host-only feature's (psx_mod_option_live_host_only). Persisting the choice stays * the mod provider's job (mods/state.toml). */ typedef int (*PSXModOptionChangedCallback)(const char* option_id, const char* value); @@ -483,6 +483,12 @@ int psx_mod_set_option_live(const char* package_id, const char* feature_id, /* 1 when the committed feature's plugin registered an option-changed callback * (so a live set can take effect now). Read-only; any thread. */ int psx_mod_option_live_capable(const char* package_id, const char* feature_id); +/* 1 when the committed feature is host-only (every plugin it activates + * declares netplay = "host_only"): it is the player's own in a match, so its + * options may change live during netplay (psx_mod_set_option_live), unlike + * every other feature's. Read-only; any thread, like + * psx_mod_option_live_capable. */ +int psx_mod_option_live_host_only(const char* package_id, const char* feature_id); /* Write the player's current selection (feature switches and option values, * as edited through the launcher mod provider) to mods/state.toml now, * without re-resolving the running plan. Main thread. 1 on success. */ @@ -575,9 +581,10 @@ int psx_mod_set_widescreen_hud_size(int proportional); /* Request an internal rendering height for this mod session (120..8192). * The existing resolution resolver selects an integer raster scale and the * backend applies its normal allocation limits. Zero clears the request. - * Player settings are preserved, explicit PSX_INTERNAL_RESOLUTION overrides - * win, and each session clears this request before activating its committed - * mod plan. */ + * It wins over the built-in Internal Resolution mod's choice and the title's + * baseline (both left unchanged); explicit PSX_INTERNAL_RESOLUTION overrides + * win over it, and each session clears this request before activating its + * committed mod plan. */ int psx_mod_set_internal_resolution(uint32_t target_lines); /* * Set the wall-clock cadence of simulated guest VBlanks. A value of zero diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index c7efc717..4821a2b7 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -512,6 +512,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/mod_builtin_image_filtering.c ${PSXRECOMP_ROOT}/runtime/src/mod_builtin_enhanced_audio.c ${PSXRECOMP_ROOT}/runtime/src/mod_builtin_rewind.c + ${PSXRECOMP_ROOT}/runtime/src/mod_builtin_internal_resolution.c ${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp ${PSXRECOMP_ROOT}/runtime/src/mod_media.cpp ${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 1e959dce..31391384 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -10739,10 +10739,12 @@ static void handle_dynres_check(int id, const char *json) } /* {"cmd":"video_set","internal_resolution":N,"supersample":M} - change the - * Internal resolution preset (lines, 1 native, -1 match display, -2 auto: - * match display with dynamic resolution) and/or the - * supersample (thousandths, 1000..4000) through the in-game menu's live path. - * Invalid values are rejected without applying or saving anything. Returns + * session's effective internal resolution (lines, 1 native, -1 match display, + * -2 auto: match display with dynamic resolution) and/or the supersample + * (thousandths, 1000..4000) through the live path the Internal Resolution + * mod's option changes use. Neither is a player setting, so nothing is saved: + * the next session starts from the mod's choice or the title's baseline. + * Invalid values are rejected without applying anything. Returns * the restart bits the change still needs (0 = applied live), the requested * values, and the values the backend actually applied. */ static void handle_video_set(int id, const char *json) diff --git a/runtime/src/host_enhancements.c b/runtime/src/host_enhancements.c index 4e6b5ef0..5d8453bd 100644 --- a/runtime/src/host_enhancements.c +++ b/runtime/src/host_enhancements.c @@ -10,6 +10,7 @@ static PsxHostEnhRequests s_req; static PsxHostEnhApplier s_applier; +static PsxHostEnhResolutionApplier s_resolution_applier; void psx_host_enh_reset(void) { memset(&s_req, 0, sizeof s_req); @@ -39,6 +40,11 @@ void psx_host_enh_request_rewind(int depth, int interval) { s_req.rewind_interval = interval; } +void psx_host_enh_request_resolution(int value) { + s_req.resolution = 1; + s_req.resolution_value = value; +} + void psx_host_enh_requests(PsxHostEnhRequests* out) { if (out) *out = s_req; } @@ -53,6 +59,14 @@ int psx_host_enh_apply_live(void) { return 1; } +void psx_host_enh_set_resolution_applier(PsxHostEnhResolutionApplier fn) { + s_resolution_applier = fn; +} + +int psx_host_enh_apply_resolution_live(void) { + return s_resolution_applier ? (s_resolution_applier() ? 1 : 0) : 0; +} + static int is(const char* a, const char* b) { return a && strcmp(a, b) == 0; } @@ -112,6 +126,14 @@ int psx_host_enh_parse_rewind_depth(const char* text) { ? v : PSX_HOST_ENH_REWIND_DEPTH_DEFAULT; } +/* Only the preset ids: a manifest choice is never a raw line count. */ +int psx_host_enh_parse_internal_resolution(const char* text) { + if (!text || is(text, "default")) return PSX_IR_UNSET; + for (int i = 0; i < PSX_IR_PRESET_COUNT; i++) + if (is(text, k_psx_ir_presets[i].id)) return k_psx_ir_presets[i].value; + return PSX_IR_UNSET; +} + int psx_host_enh_parse_rewind_interval(const char* text) { const int v = parse_int(text, PSX_HOST_ENH_REWIND_INTERVAL_DEFAULT); return v == 1 || v == 4 || v == 8 || v == 12 || v == 15 diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index bafb7678..858d038a 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -13,7 +13,7 @@ #include "projection_scale_config.hpp" #include "mod_controller_source.h" #include "window_size.h" /* default game-window size */ -#include "internal_resolution.h" /* Settings -> Display -> Internal resolution */ +#include "internal_resolution.h" /* the Internal Resolution mod and [video] internal_resolution */ #include "mod_internal_resolution.h" #include "dynamic_resolution.h" /* [video] dynamic_resolution: the step controller */ #include "render_thread.h" /* rt_get_stats: queue backpressure (dynres) */ @@ -159,13 +159,6 @@ extern "C" uint32_t g_guest_last_store_addr, g_guest_last_store_val; #if defined(RECOMP_LAUNCHER) #include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher */ -/* A recomp-ui that shows the Internal resolution row; an older one only has - * the legacy Supersampling row (internal_resolution.h, launcher round trip). */ -#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) -static constexpr int kLauncherHasInternalResolution = 1; -#else -static constexpr int kLauncherHasInternalResolution = 0; -#endif #include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */ #include "launcher_boot_timing.h" /* PSX_LAUNCHER_BOOT_TIMING stamps */ #if defined(PSX_HAS_CODEGEN_SETUP_HOST) @@ -1316,13 +1309,17 @@ extern "C" void psx_mod_set_texture_filter(int mode) { } #if defined(RECOMP_LAUNCHER) /* Rows that are not PSX Settings any more: what the built-in CRT Filter, - * Image Filtering, Enhanced Audio and Rewind mods replaced - * (host_enhancements.h), Perspective textures (psx.enhancement.pgxp) and the - * Dynamic resolution checkbox (Internal resolution = Auto). recomp-ui's psx - * profile no longer offers them; clearing them here too keeps an older + * Image Filtering, Enhanced Audio, Rewind and Internal Resolution mods + * replaced (host_enhancements.h), Perspective textures (psx.enhancement.pgxp) + * and the Dynamic resolution checkbox (Internal Resolution = Auto). recomp-ui's + * psx profile no longer offers them; clearing them here too keeps an older * recomp-ui, whose profile still does, from drawing controls that nothing - * applies. */ + * applies. (The Sample rate row is the profile's hide_audio_freq, which a + * GameInfo cannot clear: an older recomp-ui still draws it, and the runtime + * ignores what it returns.) */ static void psx_launcher_hide_mod_owned_rows(RecompLauncherCGameInfo* gi) { + gi->has_supersampling = 0; /* Internal Resolution (and the legacy + * Supersampling cycle) */ gi->has_antialiasing = 0; /* Image Filtering: Smoothing */ gi->has_texture_filter = 0; /* Image Filtering: Texture filtering */ gi->has_screen_kind = 0; /* CRT Filter: Screen model */ @@ -1375,28 +1372,30 @@ static float g_video_pgxp_depth_threshold = 4096.0f; static int g_video_pgxp_mod_only = 0; 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. This is the - * configured preset (game.toml < settings.toml < launcher) and the only one - * the launcher shows or settings.toml saves; Auto (PSX_IR_AUTO) is Match - * display with dynamic resolution on. PSX_INTERNAL_RESOLUTION lives apart in - * g_video_internal_res_env: it wins for the run but is never persisted, so - * two local peers sharing one settings.toml stay independent. */ +/* The session's internal resolution (internal_resolution.h): the title's + * [video] internal_resolution, or the built-in Internal Resolution mod's + * choice (session_internal_resolution). The value supersedes the legacy + * supersampling factor (g_video_scale) once set; PSX_IR_UNSET leaves the + * factor as game.toml wrote it. Auto (PSX_IR_AUTO) is Match display with + * dynamic resolution on. PSX_INTERNAL_RESOLUTION lives apart in + * g_video_internal_res_env: it wins for the run, so two local peers sharing + * one install stay independent. */ static int g_video_internal_res = PSX_IR_UNSET; static int g_video_internal_res_env = PSX_IR_UNSET; -/* settings.toml holds the player's own Internal resolution choice. Until it - * does, a launcher round trip that leaves the row alone keeps the title's - * internal resolution and dynamic resolution and writes neither. */ -static bool g_video_internal_res_chosen = false; +/* The title's baseline, captured when the config loads: its internal + * resolution and dynamic resolution, its legacy supersampling factor and its + * [video] supersample. Every session start restores it before the mod's + * request applies, so a rematch never inherits the last session's scale. */ +static PsxHostResolution g_title_resolution = {PSX_IR_UNSET, 0}; +static int g_title_scale = 1; +static int g_title_ss_milli = 1000; /* Dynamic resolution (dynamic_resolution.h): the internal resolution above is * the ceiling, and the GL scale steps down to the floor only while the game - * would miss frames. A player turns it on by choosing Internal resolution = - * Auto and off by choosing a fixed resolution (set_internal_resolution_choice); - * until they choose, the title's [video] dynamic_resolution stands, over its - * own internal resolution. The floor is the title's [video] - * dynamic_resolution_min. PSX_DYNRES=0/1 and PSX_DYNRES_MIN win for one run, - * never persisted. */ + * would miss frames. The Internal Resolution mod turns it on with Auto and off + * with a fixed resolution; at Game default (or with the mod off) the title's + * [video] dynamic_resolution stands, over its own internal resolution. The + * floor is the title's [video] dynamic_resolution_min. PSX_DYNRES=0/1 and + * PSX_DYNRES_MIN win for one run. */ static int g_video_dynres = 0; static int g_video_dynres_min = 720; static int g_video_dynres_env = -1; @@ -1448,14 +1447,13 @@ static int g_video_requested_scale = 1; static bool g_video_scale_applies = true; static bool g_video_hidpi_window = false; -static_assert(PSX_IR_LEGACY_SS_MAX == SW_MAX_INTERNAL_SCALE, - "settings.toml supersampling for an older runtime is the software ceiling"); - static int video_scale_ceiling(void) { return g_video_renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE; } -/* Session priority: environment override, active mod request, player setting. +/* Session priority: environment override, a title mod's request + * (psx_mod_set_internal_resolution), then the session's choice: the Internal + * Resolution mod's, or the title's baseline. * Auto renders at its ceiling, Match display; dynres_requested() says whether * the scale may step down under it. */ static int effective_internal_resolution(void) { @@ -1464,50 +1462,6 @@ static int effective_internal_resolution(void) { : mod_lines ? (int)mod_lines : g_video_internal_res); } -/* The player's Internal resolution choice: Auto turns dynamic resolution on, - * any fixed resolution turns it off. */ -static void set_internal_resolution_choice(int ir) { - g_video_internal_res = ir; - g_video_dynres = psx_ir_is_auto(ir) ? 1 : 0; -} - -#if defined(RECOMP_LAUNCHER) -/* Adopt a launcher's Internal resolution round trip (internal_resolution.h). - * row_seeded / row_result: what the row was seeded with and returned - * (PSX_IR_UNSET without the row). A row the player left alone keeps the - * selection as it was -- the title's internal resolution and dynamic - * resolution, or the player's saved choice -- so a title that ships, say, - * 1080p with dynamic resolution is not turned into Auto by a Play. A row they - * changed is their choice: Auto or a fixed resolution, saved. Returns the - * adopted values; *save_ir / *has_save_ir say what settings.toml keeps. */ -[[maybe_unused]] static PsxIrAdopted adopt_launcher_internal_resolution( - int preset_seeded, int ss_seeded, int ss_result, int row_seeded, - int row_result, int s_max, int* save_ir, bool* has_save_ir) { - const bool has_row = kLauncherHasInternalResolution != 0; - const bool untouched = !has_row || row_result == row_seeded; - const PsxIrAdopted ir = psx_ir_adopt_launcher( - has_row, preset_seeded, ss_seeded, ss_result, - untouched ? PSX_IR_UNSET : row_result, g_video_ref_lines, - match_display_px_h(nullptr), s_max); - if (untouched && has_row) { - g_video_internal_res = ir.preset; /* a legacy factor becomes its preset */ - *has_save_ir = g_video_internal_res_chosen; - *save_ir = g_video_dynres ? PSX_IR_AUTO : ir.preset; - } else if (has_row) { - set_internal_resolution_choice(ir.preset); - g_video_internal_res_chosen = ir.save_ir != PSX_IR_UNSET; - *has_save_ir = g_video_internal_res_chosen; - *save_ir = ir.save_ir; - } else { - /* An older recomp-ui: its legacy Supersampling row decides alone. */ - g_video_internal_res = ir.preset; - *has_save_ir = ir.save_ir != PSX_IR_UNSET; - *save_ir = ir.save_ir; - } - return ir; -} -#endif - /* 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. */ @@ -1518,21 +1472,15 @@ static void apply_internal_resolution(int display_px_h) { display_px_h, video_scale_ceiling(), display_supersample(nullptr), g_video_match_max_lines); } -/* The value the launcher row starts on: Auto while dynamic resolution is on, - * else the preset, or the legacy factor shown as the preset it matches - * (3 -> 720p) or as its own "Nx" entry. */ -[[maybe_unused]] static int internal_resolution_for_launcher(void) { - if (g_video_dynres) return PSX_IR_AUTO; - 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 */ static int g_video_screen = 0; /* 0=raw,1=crt,2=composite,3=trinitron (session) */ static int g_video_win_w = 0; /* 0 = fit the display; see clamp_window_aspect */ static bool g_audio_spu_hq = false; /* SPU float-shadow, session (env overrides) */ -static int g_audio_freq = 44100; /* host device request */ +/* The host audio device's sample rate: the runtime's built-in output rate + * (not a player setting; the SPU mixes at 44.1 kHz). */ +static constexpr int g_audio_freq = 44100; static int g_auto_skip_fmv = 0; /* skip FMVs the instant they're detected */ /* Local rewind is opt-in: the snap ring is whole-machine state on a frame * cadence, too much to charge every host for a feature many never open. The @@ -10457,6 +10405,27 @@ static void host_look_apply_live(void) { if (g_gl_active) gl_renderer_set_fmv_filter(g_video_fmv_filter); } +/* ---- The session's internal resolution (host_enhancements.h) ------------ + * The title's baseline (g_title_resolution, its legacy factor and supersample) + * restored at every session start, then the Internal Resolution mod's choice + * from this session's activation. Called at the renderer setup after + * start_mod_session, before the PSX_INTERNAL_RESOLUTION / PSX_SUPERSAMPLE + * env overrides and the scale resolution. */ +static void session_internal_resolution(void) { + PsxHostEnhRequests req; + psx_host_enh_requests(&req); + const PsxHostResolution r = psx_host_enh_resolve_resolution(&g_title_resolution, &req); + g_video_internal_res = r.internal_resolution; + g_video_dynres = r.dynamic_resolution; + g_video_scale = g_title_scale; + g_video_ss_milli = g_title_ss_milli; + if (req.resolution && req.resolution_value != PSX_IR_UNSET) { + const char* lbl = psx_ir_label_for(req.resolution_value); + std::fprintf(stdout, "psxrecomp: host enhancements: Internal Resolution %s\n", + lbl ? lbl : "?"); + } +} + /* ---- Live video apply (host_overlay.h P4) ------------------------------- */ static std::filesystem::path g_overlay_settings_path; /* settings.toml */ static PsxHostVideoSettings g_video_boot; /* as the session booted */ @@ -10531,6 +10500,52 @@ extern "C" void psx_video_current(PsxHostVideoSettings *v) { } } +/* Move the session to an internal resolution while the game runs: the value + * (PSX_IR_UNSET: the title's legacy supersampling factor), dynamic resolution + * under it, and the supersample factor (out of range: unchanged). The GL + * owner confirms allocation/application before this returns. The request is + * kept either way; the session's scale only moves when the backend takes it, + * otherwise the change waits for a relaunch and the restart bits say so. + * Returns 1 when the new scale applied now. Main thread. */ +static int video_resolution_apply_live(int ir, int dynres, int ss_milli) { + const int was_dynres = g_video_dynres; + g_video_internal_res = ir; + g_video_dynres = dynres ? 1 : 0; + if (ss_milli >= PSX_SS_MILLI_MIN && ss_milli <= PSX_SS_MILLI_MAX) + g_video_ss_milli = ss_milli; + const int shown = g_video_dynres ? PSX_IR_AUTO : g_video_internal_res; + const int prev_scale = g_video_scale; + video_res_refresh_capable(); + int applied = 0; + if (g_video_res_live_capable.load()) { + /* No value anywhere: the title's legacy factor is the scale. */ + if (effective_internal_resolution() == PSX_IR_UNSET) g_video_scale = g_title_scale; + apply_internal_resolution(match_display_px_h(sdl_window)); + applied = gl_renderer_set_internal_scale_live(g_video_scale); + } + if (applied) { + std::fprintf(stdout, "psxrecomp: internal scale %dx (live)\n", applied); + g_video_scale = applied; + video_res_set_applied(shown, g_video_ss_milli); + g_video_res_live_failed.store(false); + dynres_setup(); /* the new scale is the controller's ceiling */ + } else { + g_video_scale = prev_scale; + g_video_res_live_failed.store(true); + std::fprintf(stdout, "psxrecomp: internal resolution change needs a " + "relaunch (live change unavailable)\n"); + } + /* Into or out of Auto: the controller follows the choice. */ + if (!dynres_requested()) { + g_dynres.active = false; + } else if (!was_dynres || !g_dynres.active) { + dynres_setup(); + if (!g_dynres.active && gl_renderer_dynamic_resolution_ceiling() >= 2) + g_dynres.active = true; + } + return applied ? 1 : 0; +} + extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) { if (!next) return 0; PsxHostVideoSettings cur; @@ -10553,64 +10568,32 @@ extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) { log_present_cadence(); } if (m & PSX_VIDEO_LIVE_RESOLUTION) { - /* The GL owner confirms allocation/application before this returns. - * Auto is Match display with dynamic resolution on; a fixed choice - * turns it off. */ - const int was_dynres = g_video_dynres; + /* Auto is Match display with dynamic resolution on; a fixed choice + * turns it off. Unchanged, the session's own choice stands. */ + int ir = g_video_internal_res, dynres = g_video_dynres; if (next->internal_resolution != PSX_IR_UNSET && - next->internal_resolution != cur.internal_resolution) - set_internal_resolution_choice(next->internal_resolution); - if (next->supersample_milli >= PSX_SS_MILLI_MIN && - next->supersample_milli <= PSX_SS_MILLI_MAX) - g_video_ss_milli = next->supersample_milli; - /* The request is kept (and saved) either way; the session's scale - * only moves when the backend takes it. Otherwise the change waits - * for a relaunch and the restart bits say so. */ - const int prev_scale = g_video_scale; - video_res_refresh_capable(); - int applied = 0; - if (g_video_res_live_capable.load()) { - apply_internal_resolution(match_display_px_h(sdl_window)); - applied = gl_renderer_set_internal_scale_live(g_video_scale); - } - if (applied) { - std::fprintf(stdout, "psxrecomp: internal scale %dx (live)\n", applied); - g_video_scale = applied; - video_res_set_applied(next->internal_resolution, g_video_ss_milli); - g_video_res_live_failed.store(false); - dynres_setup(); /* the new scale is the controller's ceiling */ - } else { - g_video_scale = prev_scale; - g_video_res_live_failed.store(true); - std::fprintf(stdout, "psxrecomp: internal resolution change needs a " - "relaunch (live change unavailable)\n"); - } - /* Into or out of Auto: the controller follows the choice. */ - if (!dynres_requested()) { - g_dynres.active = false; - } else if (!was_dynres || !g_dynres.active) { - dynres_setup(); - if (!g_dynres.active && gl_renderer_dynamic_resolution_ceiling() >= 2) - g_dynres.active = true; + next->internal_resolution != cur.internal_resolution) { + ir = next->internal_resolution; + dynres = psx_ir_is_auto(ir) ? 1 : 0; } + (void)video_resolution_apply_live(ir, dynres, next->supersample_milli); } if ((m & PSX_VIDEO_LIVE_FRAME_GEN) && g_gl_active) { g_frame_generation = next->frame_generation ? 1 : 0; gl_renderer_set_frame_generation(g_frame_generation); } - /* Persist everything the menu shows, live or not, like the launcher. */ + /* Persist the player settings the menu shows, live or not, like the + * launcher. The internal resolution and supersample are not player + * settings (the Internal Resolution mod and the title own them): a + * debug-server change lasts for this session only. */ if (!g_overlay_settings_path.empty()) { try { PSXRecompV4::UserSettings us = PSXRecompV4::load_user_settings(g_overlay_settings_path); us.fullscreen = next->fullscreen; us.has_fullscreen = true; us.frame_generation = next->frame_generation != 0; us.has_frame_generation = true; us.renderer = next->renderer; us.has_renderer = true; - us.supersampling = next->supersampling; us.has_supersampling = true; us.render_thread = next->render_thread != 0; us.has_render_thread = true; us.present_thread = next->present_thread != 0; us.has_present_thread = true; - us.internal_resolution = next->internal_resolution; us.has_internal_resolution = true; - us.supersample_milli = next->supersample_milli; - us.has_supersample = next->supersample_milli >= 1000; (void)PSXRecompV4::save_user_settings(g_overlay_settings_path, us); } catch (...) { } @@ -10618,6 +10601,21 @@ extern "C" unsigned psx_video_apply_live(const PsxHostVideoSettings *next) { return video_restart_bits(&g_video_boot, next) | deferred; } +/* The Internal Resolution plugin's live option change (in-game menu): resolve + * again and move the scale now when the backend can. A netplay match holds + * the scale (psx_video_apply_live defers it the same way), and a backend + * without a live scale change waits for the next start: both return 0 and + * change nothing, so the plugin's choice applies at the next session. */ +static int host_resolution_apply_live(void) { + if (psx_netplay_active()) return 0; + video_res_refresh_capable(); + if (!g_video_res_live_capable.load()) return 0; + PsxHostEnhRequests req; + psx_host_enh_requests(&req); + const PsxHostResolution r = psx_host_enh_resolve_resolution(&g_title_resolution, &req); + return video_resolution_apply_live(r.internal_resolution, r.dynamic_resolution, 0); +} + #ifndef PSX_NO_DEBUG_TOOLS /* ---- Live rendering A/B keys (debug-tools builds) -------------------------- * PSX_DEBUG_TOGGLES=0 turns them off. Each key flips one rendering feature @@ -16651,35 +16649,6 @@ 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: OpenGL only. Past 16x on Apple GPUs only the displayed " - "frame is kept at full resolution."; - 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(); @@ -16773,14 +16742,6 @@ namespace { #endif gi->has_skip_fmv = skip_fmv_offered_b ? 1 : 0; gi->has_turbo_loads = turbo_loads_offered_b ? 1 : 0; -#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 defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE) gi->has_render_pipeline = g_offer_render_pipeline ? 1 : 0; #endif @@ -17036,10 +16997,14 @@ std::vector g_ovl_mod_posts; /* The provider never touches the mod catalogue itself (it may run on the GL * thread): every change is posted here, recorded in the catalogue and saved * to mods/state.toml on the main thread, and handed to a live-capable plugin. - * option == NULL is the feature switch (value "true"/"false"): restart. */ + * option == NULL is the feature switch (value "true"/"false"): restart. + * During netplay the plan is negotiated with the peers, so nothing changes -- + * except an option of a host-only feature (CRT Filter, Image Filtering, + * Internal Resolution, ...), which is only this player's presentation or + * audio and is never part of the plan (psx_mod_option_live_host_only). */ int mh_mod_set(const char* p, const char* f, const char* o, const char* v) { if (!p || !f || !v) return 0; - if (psx_netplay_active()) return 0; /* the plan is negotiated with peers */ + if (psx_netplay_active() && !(o && psx_mod_option_live_host_only(p, f))) return 0; std::lock_guard lk(g_ovl_mu); g_ovl_mod_posts.push_back({p, f, o ? o : "", v}); return o ? psx_mod_option_live_capable(p, f) : 0; @@ -17082,9 +17047,9 @@ int mh_disc_insert(int n) { } #if defined(RECOMP_LAUNCHER) -/* The launcher-Settings view of the in-game menu: the same rows, and the - * same Internal resolution vocabulary (Auto included), as the pre-game - * launcher's Display card. */ +/* The launcher-Settings view of the in-game menu: the same rows as the + * pre-game launcher's Settings page. The internal resolution is not one of + * them: the Internal Resolution mod's option changes it live (Mods page). */ const char* const kOvlRenderers[] = {"Software", "OpenGL", "Vulkan"}; PsxHostVideoSettings ovl_video_from(const RecompLauncherCSettings& ls) { PsxHostVideoSettings v; @@ -17094,10 +17059,6 @@ PsxHostVideoSettings ovl_video_from(const RecompLauncherCSettings& ls) { v.renderer = ls.renderer; v.render_thread = ls.render_thread ? 1 : 0; v.present_thread = ls.present_thread ? 1 : 0; - if (ls.internal_resolution != 0) v.internal_resolution = ls.internal_resolution; -#if defined(RECOMP_LAUNCHER_HAS_SUPERSAMPLE) - if (ls.supersample_milli >= 1000) v.supersample_milli = ls.supersample_milli; -#endif return v; } void mh_launcher_settings_get(RecompLauncherCSettings* out) { @@ -17110,15 +17071,6 @@ void mh_launcher_settings_get(RecompLauncherCSettings* out) { ls.renderer = v.renderer; ls.render_thread = v.render_thread; ls.present_thread = v.present_thread; - /* The main-thread snapshot's choice (Auto while dynamic resolution is - * on), never the live globals: this runs on a provider's draw thread. */ - ls.internal_resolution = v.internal_resolution != PSX_IR_UNSET - ? v.internal_resolution - : psx_ir_from_supersampling(v.supersampling < 1 ? 1 : v.supersampling, - g_video_ref_lines); -#if defined(RECOMP_LAUNCHER_HAS_SUPERSAMPLE) - ls.supersample_milli = v.supersample_milli; -#endif #if defined(RECOMP_LAUNCHER_HAS_JOGCON_FORCE_FEEDBACK) && \ defined(PSX_HAS_JOGCON_FFB_PLAYER_SETTING) ls.jogcon_force_feedback = psx_jogcon_ffb_player_enabled(); @@ -17149,21 +17101,6 @@ void mh_launcher_game_info(RecompLauncherCGameInfo* out) { gi.num_players = PSX_MAX_PLAYERS < 4 ? PSX_MAX_PLAYERS : 4; gi.renderer_labels = kOvlRenderers; gi.num_renderers = 2; -#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) - /* Internal resolution applies live (psx_video_apply_live), Auto included: - * the in-game Settings view offers the launcher's own row, from the - * vocabulary recomp_overlay_setup built on the main thread. */ - gi.has_supersampling = 1; - 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; -#else - gi.has_supersampling = 0; /* the legacy factor is restart-only */ -#endif -#if defined(RECOMP_LAUNCHER_HAS_SUPERSAMPLE) - gi.has_supersample = 1; /* live with Internal resolution */ -#endif gi.has_render_pipeline = 1; gi.has_vsync = 0; gi.mods = PSXRecompV4::mod_runtime_launcher_provider(); @@ -17203,12 +17140,6 @@ void ovl_draw(int w, int h, void*) { if (g_ovl && g_ovl->draw) g_ovl->draw(g_ovl #endif static void recomp_overlay_setup(const char* argv0, const std::string& title) { -#if defined(RECOMP_LAUNCHER) && defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) - /* Main thread: the Internal resolution vocabulary the menu's Settings - * view publishes (mh_launcher_game_info). Fixed before any provider - * attaches, never written while one can read it. */ - if (!g_ovl_on) build_internal_resolution_vocab(internal_resolution_for_launcher()); -#endif const char* e = std::getenv("PSX_OVERLAY"); const PsxMenuProvider* p = psx_menu_provider(); g_menu_host.default_selection = 0; @@ -17949,6 +17880,10 @@ int main(int argc, char** argv) { /* The title's dynamic resolution: its own switch, or Auto. */ g_video_dynres = (gc.runtime.video_dynamic_resolution || psx_ir_is_auto(gc.runtime.video_internal_resolution)) ? 1 : 0; + /* The baseline the Internal Resolution mod resolves over. */ + g_title_resolution.internal_resolution = g_video_internal_res; + g_title_resolution.dynamic_resolution = g_video_dynres; + g_title_scale = g_video_scale; g_video_dynres_min = gc.runtime.video_dynamic_resolution_min; if (gc.runtime.video_window_width > 0) { g_video_win_w = gc.runtime.video_window_width; @@ -17961,6 +17896,7 @@ int main(int argc, char** argv) { g_video_post_shader.assign(u8.begin(), u8.end()); } g_video_ss_milli = gc.runtime.video_supersample_milli; + g_title_ss_milli = g_video_ss_milli; g_video_texfilter = gc.runtime.video_texture_filter; g_video_fmv_filter = gc.runtime.video_fmv_filter; g_video_geometry_correction = @@ -18525,24 +18461,12 @@ 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 - /* 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; - } - /* Auto or a fixed resolution: the player's choice also decides - * dynamic resolution. Unchosen, the title's own stands. */ - if (us.has_internal_resolution) { - set_internal_resolution_choice(us.internal_resolution); - g_video_internal_res_chosen = true; - } - if (us.has_supersample) g_video_ss_milli = us.supersample_milli; if (us.has_window_width) g_video_win_w = us.window_width; - /* Screen model, scanlines, filtering, the high-quality SPU, rewind and - * the PGXP corrections are not settings.toml keys any more: the - * built-in mods own them over the title's baseline - * (host_enhancements.h, pgxp_session.h). */ + /* Internal resolution (and supersampling, dynamic resolution), screen + * model, scanlines, filtering, the high-quality SPU, rewind and the + * PGXP corrections are not settings.toml keys any more: the built-in + * mods own them over the title's baseline (host_enhancements.h, + * pgxp_session.h). Neither is the audio sample rate (g_audio_freq). */ /* fov_scale precedence: game.toml < settings.toml < PSX_GTE_FOV_SCALE. * Bad values warn and leave the lower layer's value in place. */ if (us.has_fov_scale) { @@ -18578,7 +18502,6 @@ int main(int argc, char** argv) { g_video_aspect_num = us.aspect_num; g_video_aspect_den = us.aspect_den; } - if (us.has_audio_freq) g_audio_freq = us.audio_freq; if (us.has_hotkey_pad_rewind) g_hotkey_pad_rewind = normalize_hotkey_pad_binding( us.hotkey_pad_rewind, @@ -19160,21 +19083,9 @@ int main(int argc, char** argv) { bool match_session_bios_set = false; seed.vsync = g_video_vsync; seed.has_vsync = true; seed.renderer = g_video_renderer; seed.has_renderer = true; - /* Internal resolution round trip (internal_resolution.h): only a - * launcher with the row seeds and returns the preset. An older one - * gets the legacy Supersampling row, and a pick there wins. */ - const int ir_preset_seeded = g_video_internal_res; - const int ir_ss_seeded = psx_ir_launcher_seed_supersampling( - kLauncherHasInternalResolution, g_video_internal_res, g_video_scale, - g_video_ref_lines, match_display_px_h(nullptr)); - seed.supersampling = ir_ss_seeded; seed.has_supersampling = true; - int ir_row_result = PSX_IR_UNSET; /* the row's pick, when it has one */ - /* Auto while dynamic resolution is on (internal_resolution.h). */ - const int ir_row_seeded = internal_resolution_for_launcher(); -#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) - seed.internal_resolution = ir_row_seeded; - seed.has_internal_resolution = true; -#endif + /* No Internal resolution / Supersampling round trip: the built-in + * Internal Resolution mod owns it (psx_launcher_hide_mod_owned_rows + * clears the row). */ #if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE) seed.render_thread = g_render_thread != 0; seed.present_thread = g_present_thread != 0; @@ -19193,7 +19104,6 @@ int main(int argc, char** argv) { seed.has_frame_interpolation_fps = true; seed.aspect_num = g_video_aspect_num; seed.aspect_den = g_video_aspect_den; seed.has_aspect_ratio = true; - seed.audio_freq = g_audio_freq; seed.has_audio_freq = true; seed.hotkey_pad_rewind = g_hotkey_pad_rewind; seed.has_hotkey_pad_rewind = true; seed.hotkey_pad_save_state_menu = g_hotkey_pad_save_state_menu; @@ -19316,7 +19226,7 @@ int main(int argc, char** argv) { ls.widescreen = (seed.aspect_num == 16 && seed.aspect_den == 9) ? 1 : 0; ls.widescreen_hud = ls.widescreen; ls.enable_audio = 1; - ls.audio_freq = seed.audio_freq; + ls.audio_freq = g_audio_freq; /* fixed; the Sample rate row is hidden */ ls.volume = host_volume_get(); { const int n = std::min(PSX_MAX_PLAYERS, RECOMP_LAUNCHER_MAX_PLAYERS); @@ -19365,10 +19275,6 @@ int main(int argc, char** argv) { if (ls.renderer == 0 && !user_settings_has_renderer && 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 #if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE) ls.render_thread = seed.render_thread ? 1 : 0; ls.present_thread = seed.present_thread ? 1 : 0; @@ -19708,25 +19614,6 @@ int main(int argc, char** argv) { * fields above), all gated on by the "psx" launcher_profile caps. */ 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) - ir_row_result = ls.internal_resolution; -#endif - /* Play and Quit both keep the row's choice (or, left alone, - * the selection as it was: adopt_launcher_internal_resolution). */ - { - int save_ir = PSX_IR_UNSET; - bool has_save_ir = false; - const PsxIrAdopted ir = adopt_launcher_internal_resolution( - ir_preset_seeded, ir_ss_seeded, seed.supersampling, - ir_row_seeded, ir_row_result, - seed.renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE, - &save_ir, &has_save_ir); - g_video_scale = ir.scale; - seed.supersampling = ir.save_ss; - seed.has_internal_resolution = has_save_ir; - seed.internal_resolution = save_ir; - } #if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE) seed.render_thread = ls.render_thread != 0; seed.has_render_thread = true; seed.present_thread = ls.present_thread != 0; seed.has_present_thread = true; @@ -19741,7 +19628,6 @@ int main(int argc, char** argv) { #endif seed.frame_interpolation = ls.frame_interp != 0; seed.has_frame_interpolation = true; seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = true; - seed.audio_freq = ls.audio_freq; seed.has_audio_freq = true; seed.hotkey_pad_rewind = normalize_hotkey_pad_binding( ls.assist_pad_bind[PSX_ASSIST_BIND_REWIND], PSX_HOTKEY_PAD_SELECT_R3); @@ -19997,7 +19883,6 @@ int main(int argc, char** argv) { g_frame_interpolation_fps = seed.frame_interpolation_fps; g_video_aspect_num = seed.aspect_num; g_video_aspect_den = seed.aspect_den; - g_audio_freq = seed.audio_freq; g_hotkey_pad_rewind = seed.has_hotkey_pad_rewind ? seed.hotkey_pad_rewind : PSX_HOTKEY_PAD_SELECT_R3; @@ -20393,6 +20278,10 @@ session_reboot: g_headless && g_video_renderer != 0 ? " (headless)" : ""); } gpu_init(); + /* This session's internal resolution: the title's baseline and the + * Internal Resolution mod's request from start_mod_session's activation + * (first boot and rematch alike); the env overrides below still win. */ + session_internal_resolution(); /* 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. */ @@ -20484,6 +20373,7 @@ session_reboot: * for a validation run. */ session_host_look(); psx_host_enh_set_live_applier(host_look_apply_live); + psx_host_enh_set_resolution_applier(host_resolution_apply_live); if (const char* e = std::getenv("PSX_TEXTURE_FILTER")) g_video_texfilter = std::atoi(e); gr_set_texture_filter(g_mod_texfilter < 0 ? g_video_texfilter : g_mod_texfilter); /* Sub-pixel vertex precision + perspective-correct UVs. Both default off; @@ -20583,7 +20473,7 @@ session_reboot: pgxp_arm.culling ? ", precise culling" : "", pgxp_in.mod_enabled ? " (PGXP mod)" : "", (pgxp_arm.geometry && requested_scale < 2) - ? " (needs [video] supersampling >= 2 to be visible)" : ""); + ? " (needs an internal resolution of 2x or more 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 @@ -21948,22 +21838,12 @@ soft_return_lobby: (g_video_aspect_num == 16 && g_video_aspect_den == 9) ? 1 : 0; ls.widescreen_hud = ls.widescreen; ls.enable_audio = 1; - ls.audio_freq = g_audio_freq; + ls.audio_freq = g_audio_freq; /* fixed; the Sample rate row is hidden */ ls.volume = host_volume_get(); ls.window_width = g_video_win_w; ls.renderer = g_video_renderer; if (ls.renderer < 0 || ls.renderer > (vulkan_offered ? 2 : 1)) ls.renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER; - /* Internal resolution round trip, as at first boot. */ - const int ir_preset_seeded = g_video_internal_res; - const int ir_ss_seeded = psx_ir_launcher_seed_supersampling( - kLauncherHasInternalResolution, g_video_internal_res, g_video_scale, - g_video_ref_lines, match_display_px_h(nullptr)); - const int ir_row_seeded = internal_resolution_for_launcher(); - ls.supersampling = ir_ss_seeded; -#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) - ls.internal_resolution = ir_row_seeded; -#endif #if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE) /* The saved choice, not this run's (env A/B or already running). */ ls.render_thread = g_render_thread_pref; @@ -22264,17 +22144,6 @@ soft_return_lobby: player_deadzone[i] = ls.deadzone[i] * 32767 / 100; } } - int ir_row_result = PSX_IR_UNSET; -#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION) - ir_row_result = ls.internal_resolution; -#endif - int ir_save = PSX_IR_UNSET; - bool ir_has_save = false; - const PsxIrAdopted ir = adopt_launcher_internal_resolution( - ir_preset_seeded, ir_ss_seeded, ls.supersampling, ir_row_seeded, - ir_row_result, - ls.renderer == 1 ? GL_MAX_INTERNAL_SCALE : SW_MAX_INTERNAL_SCALE, - &ir_save, &ir_has_save); /* Persist controller (and rematch video) choices without wiping * the rest of settings.toml — merge into the on-disk file. */ { @@ -22346,10 +22215,6 @@ soft_return_lobby: } us.renderer = ls.renderer; us.has_renderer = true; - us.supersampling = ir.save_ss; - us.has_supersampling = true; - us.internal_resolution = ir_save; - us.has_internal_resolution = ir_has_save; #if defined(RECOMP_LAUNCHER_HAS_RENDER_PIPELINE) /* Persisted only: the running pipeline keeps its threads; * the new choice applies at next launch. */ @@ -22365,8 +22230,6 @@ soft_return_lobby: us.has_frame_interpolation = true; us.frame_interpolation_fps = ls.frame_interp_fps; us.has_frame_interpolation_fps = true; - us.audio_freq = ls.audio_freq; - us.has_audio_freq = true; us.hotkey_pad_rewind = normalize_hotkey_pad_binding( ls.assist_pad_bind[PSX_ASSIST_BIND_REWIND], PSX_HOTKEY_PAD_SELECT_R3); @@ -22412,10 +22275,10 @@ soft_return_lobby: (void)PSXRecompV4::save_user_settings(settings_path, us); } g_video_renderer = ls.renderer; - g_video_scale = ir.scale; - /* The screen model, scanlines, filtering, high-quality SPU and - * rewind are the host enhancement mods': the rematch's session - * start resolves them again (session_host_look). */ + /* The internal resolution, screen model, scanlines, filtering, + * high-quality SPU and rewind are the host enhancement mods': the + * rematch's session start resolves them again + * (session_internal_resolution, session_host_look). */ /* Load acceleration and FMV skipping are mod-owned on PSX, and the * launcher struct these come from was snapshotted BEFORE * mod_runtime_activate_plugins() ran. Applying them here would @@ -22433,7 +22296,6 @@ soft_return_lobby: g_video_vsync = psx_vsync_from_launcher(ls.vsync, launcher_vsync_preference()); g_mod_owned_baseline.values.video_vsync = g_video_vsync; apply_present_cadence(); - g_audio_freq = ls.audio_freq; g_fast_forward_preference = psx_fast_forward_preference(ls.assist_fast_forward_multiplier / 10.0); g_fast_forward_toggle_mode = ls.fast_forward_toggle_mode != 0; g_hotkey_pad_rewind = normalize_hotkey_pad_binding( diff --git a/runtime/src/mod_builtin_internal_resolution.c b/runtime/src/mod_builtin_internal_resolution.c new file mode 100644 index 00000000..c25fe52c --- /dev/null +++ b/runtime/src/mod_builtin_internal_resolution.c @@ -0,0 +1,49 @@ +/* + * Framework-owned Internal Resolution mod, available to every game. + * + * The height the host renders the 3D picture at -- Native, a line count + * preset, Match display, or Auto (Match display with dynamic resolution) -- + * is a property of the host renderer, not of any particular disc, so it ships + * here and mods/builtin/packages/psx.enhancement.internal-resolution targets + * game_id "*". Default off: the title's [video] internal_resolution (or its + * legacy supersampling factor) and dynamic_resolution stay its baseline, and + * "Game default" keeps them (host_enhancements.h). + * + * Activation only RECORDS the request (psx_host_enh_request_resolution); the + * renderer setup in main.cpp resolves it over the baseline after activation + * (the psx.enhancement.pgxp lesson, docs/ENHANCEMENTS.md G1.11). The in-game + * menu's option changes apply live through main.cpp's resolution applier when + * the backend can change its scale on a running game; otherwise they apply at + * the next start. + */ +#include "mod_plugins.h" +#include "host_enhancements.h" + +#define PKG_RESOLUTION "psx.enhancement.internal-resolution" + +static void resolution_request(void) { + char value[16]; + value[0] = '\0'; + (void)psx_mod_option_value(PKG_RESOLUTION, "internal-resolution", "resolution", + value, sizeof value); + psx_host_enh_request_resolution(psx_host_enh_parse_internal_resolution(value)); +} + +static void builtin_internal_resolution_activate(void) { + resolution_request(); +} + +static int builtin_internal_resolution_option_changed(const char* option_id, + const char* value) { + (void)option_id; + (void)value; + resolution_request(); + return psx_host_enh_apply_resolution_live(); +} + +PSX_MOD_CONSTRUCTOR(psx_register_builtin_internal_resolution_plugin) { + (void)psx_mod_register_activation_plugin("psx.internal-resolution", + builtin_internal_resolution_activate); + (void)psx_mod_register_option_changed_plugin( + "psx.internal-resolution", builtin_internal_resolution_option_changed); +} diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 38bc7513..6dc15a73 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -3291,6 +3291,20 @@ extern "C" int psx_mod_option_live_capable(const char* package_id, const char* f return 0; } +extern "C" int psx_mod_option_live_host_only(const char* package_id, const char* feature_id) { + using namespace PSXRecompV4; + if (!package_id || !feature_id) return 0; + RuntimeMods& s = state(); + if (!s.initialized || !s.plan.ok) return 0; + bool any = false; + for (const ModResolution::Plugin& plugin : s.plan.plugins) { + if (plugin.package_id != package_id || plugin.feature_id != feature_id) continue; + if (!plugin.netplay_host_only) return 0; + any = true; + } + return any ? 1 : 0; +} + extern "C" int psx_mod_save_selection(void) { using namespace PSXRecompV4; RuntimeMods& s = state(); diff --git a/runtime/tests/test_builtin_mod_manifests.cpp b/runtime/tests/test_builtin_mod_manifests.cpp index 09929881..3a7bba05 100644 --- a/runtime/tests/test_builtin_mod_manifests.cpp +++ b/runtime/tests/test_builtin_mod_manifests.cpp @@ -6,6 +6,8 @@ * * Usage: builtin_mod_manifests_test */ #include "mod_packages.h" +#include "host_enhancements.h" +#include "internal_resolution.h" #include #include @@ -51,6 +53,8 @@ int main(int argc, char** argv) { {"psx.enhancement.enhanced-audio", {"enhanced-audio", "psx.enhanced-audio", {}}}, {"psx.feature.rewind", {"rewind", "psx.rewind", {{"buffer", "50"}, {"interval", "15"}}}}, + {"psx.enhancement.internal-resolution", {"internal-resolution", + "psx.internal-resolution", {{"resolution", "default"}}}}, }; for (const auto& entry : fs::recursive_directory_iterator(root)) { if (!entry.is_regular_file() || entry.path().filename() != "manifest.toml") @@ -106,6 +110,25 @@ int main(int argc, char** argv) { who + option.id + " defaults to its documented value"); } } + /* Internal Resolution offers Game default and exactly the runtime's + * preset vocabulary (internal_resolution.h), Auto included, and every + * choice parses to the value the runtime resolves. */ + if (package.id == "psx.enhancement.internal-resolution" && package.options.size() == 1) { + const ModOption& option = package.options[0]; + check(option.choices.size() == (size_t)PSX_IR_PRESET_COUNT + 1, + "Internal Resolution offers Game default plus every preset"); + check(!option.choices.empty() && option.choices[0].value == "default" && + option.choices[0].label == "Game default", + "Internal Resolution's first choice is Game default"); + for (int i = 0; i < PSX_IR_PRESET_COUNT && i + 1 < (int)option.choices.size(); ++i) { + const ModChoice& c = option.choices[i + 1]; + check(c.value == k_psx_ir_presets[i].id && c.label == k_psx_ir_presets[i].label, + "Internal Resolution choice " + c.value + " matches the preset table"); + check(psx_host_enh_parse_internal_resolution(c.value.c_str()) == + k_psx_ir_presets[i].value, + "Internal Resolution choice " + c.value + " parses to its value"); + } + } } check(parsed > 0, "no builtin manifests found under " + root.string()); check(saw_8mb, "psx.enhancement.8mb-ram builtin package missing"); diff --git a/runtime/tests/test_gl_internal_scale_guards.py b/runtime/tests/test_gl_internal_scale_guards.py index 44e6ba9f..79b22fcc 100644 --- a/runtime/tests/test_gl_internal_scale_guards.py +++ b/runtime/tests/test_gl_internal_scale_guards.py @@ -318,42 +318,38 @@ class InternalResolutionGuards(unittest.TestCase): r"effective_internal_resolution\(\) == 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", - "= internal_resolution_for_launcher();"): - 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_launcher_never_carries_the_internal_resolution(self): + # The internal resolution is the built-in Internal Resolution mod's + # (psx.enhancement.internal-resolution), not a launcher row: no + # launcher seed, adoption or settings.toml write may carry it, and the + # row is cleared for an older recomp-ui whose profile still offers it. + for field in ("internal_resolution_labels", "ls.internal_resolution", + "seed.internal_resolution", "us.internal_resolution", + "ls.supersampling", "seed.supersampling", "us.supersampling", + "us.supersample_milli"): + self.assertNotIn(field, MAIN, field) + self.assertIn("gi->has_supersampling = 0;", + body(MAIN, "static void psx_launcher_hide_mod_owned_rows(")) + + def test_session_resolves_the_mod_over_the_title(self): + # Every session start restores the title's baseline (its value, dynamic + # resolution, legacy factor and supersample) and lays the mod's choice + # over it, before the env overrides and the scale resolution. + fn = body(MAIN, "static void session_internal_resolution(void)") + self.assertIn("psx_host_enh_resolve_resolution(&g_title_resolution, &req)", fn) + self.assertIn("g_video_scale = g_title_scale;", fn) + self.assertIn("g_video_ss_milli = g_title_ss_milli;", fn) + call = MAIN.index(" session_internal_resolution();") + self.assertLess(call, MAIN.index('std::getenv("PSX_INTERNAL_RESOLUTION")')) + self.assertLess(call, MAIN.index(" apply_internal_resolution(match_display_px_h(nullptr));")) def test_unset_preset_leaves_supersampling(self): self.assertIn("if (preset == PSX_IR_UNSET) return;", body(MAIN, "static void apply_internal_resolution(int display_px_h)")) - def test_launcher_trips_use_the_round_trip_helpers(self): - # Both launcher exits (first boot, netplay soft-return) seed and adopt - # through internal_resolution.h, whose behaviour with and without the - # Internal resolution row is unit-tested (internal_resolution_test). - # A bare factor copy next to a sticky preset let the preset override - # a pick in an older launcher's Supersampling row. Adoption goes - # through one host helper, which also decides Auto (dynamic - # resolution) and leaves an untouched row's selection alone. - self.assertEqual(MAIN.count("psx_ir_launcher_seed_supersampling("), 2) - self.assertEqual(MAIN.count("psx_ir_adopt_launcher("), 1) - self.assertIn("psx_ir_adopt_launcher(", - body(MAIN, "[[maybe_unused]] static PsxIrAdopted adopt_launcher_internal_resolution(")) - self.assertEqual(MAIN.count("adopt_launcher_internal_resolution("), 3) - self.assertNotRegex(MAIN, r"g_video_scale\s*=\s*(seed|ls)\.supersampling;") - self.assertIn("g_video_internal_res = ir.preset;", MAIN) - def test_env_override_is_never_persisted(self): - # PSX_INTERNAL_RESOLUTION wins for the run only: the launcher shows and - # settings.toml saves the configured preset. + # PSX_INTERNAL_RESOLUTION wins for the run only, over the session's + # choice (the Internal Resolution mod's or the title's). self.assertIn("if (psx_ir_parse(e, &v)) g_video_internal_res_env = v;", MAIN) self.assertNotRegex(MAIN, r"g_video_internal_res\s*=\s*v;") self.assertIn("g_video_internal_res_env != PSX_IR_UNSET ? g_video_internal_res_env", diff --git a/runtime/tests/test_host_enhancements.c b/runtime/tests/test_host_enhancements.c index bc3902f9..c054d957 100644 --- a/runtime/tests/test_host_enhancements.c +++ b/runtime/tests/test_host_enhancements.c @@ -1,5 +1,6 @@ /* test_host_enhancements.c -- the built-in host enhancement mods (CRT Filter, - * Image Filtering, Enhanced Audio, Rewind) and their session requests + * Image Filtering, Enhanced Audio, Rewind, Internal Resolution) and their + * session requests * (host_enhancements.h, beads-eio.3.398). * * Links the real plugins and host_enhancements.c against a fake mod runtime @@ -13,7 +14,11 @@ * - reset forgets every request (a session with the mod off, or a netplay * clear, cannot inherit one); * - the CRT Filter / Image Filtering option-changed callbacks re-record and - * report live only once main.cpp registered its applier. */ + * report live only once main.cpp registered its applier; + * - the Internal Resolution mod: Game default keeps the title's value and + * dynamic resolution, Auto turns dynamic resolution on, a fixed choice + * turns it off; its option change reports what main.cpp's resolution + * applier answers (it can refuse: no live-capable backend, netplay). */ #include "host_enhancements.h" #include "mod_plugins.h" #include "video_texture_filter.h" @@ -68,10 +73,85 @@ static int option_changed(const char* id, const char* option, const char* value) } static int g_applied; static void applier(void) { ++g_applied; } +static int g_res_applied, g_res_answer; +static int res_applier(void) { ++g_res_applied; return g_res_answer; } #define CRT "psx.enhancement.crt-filter" #define IMG "psx.enhancement.image-filtering" #define RWD "psx.feature.rewind" +#define IR "psx.enhancement.internal-resolution" + +static void test_resolution(void) { + PsxHostEnhRequests req; + memset(&req, 0, sizeof req); + /* A title shipping 1080p with dynamic resolution on under it. */ + const PsxHostResolution base = {1080, 1}; + PsxHostResolution r = psx_host_enh_resolve_resolution(&base, &req); + CHECK(r.internal_resolution == 1080 && r.dynamic_resolution == 1); /* mod off */ + req.resolution = 1; + req.resolution_value = PSX_IR_UNSET; /* Game default */ + r = psx_host_enh_resolve_resolution(&base, &req); + CHECK(r.internal_resolution == 1080 && r.dynamic_resolution == 1); + req.resolution_value = 2160; /* 4K: fixed */ + r = psx_host_enh_resolve_resolution(&base, &req); + CHECK(r.internal_resolution == 2160 && r.dynamic_resolution == 0); + req.resolution_value = PSX_IR_AUTO; + r = psx_host_enh_resolve_resolution(&base, &req); + CHECK(r.internal_resolution == PSX_IR_AUTO && r.dynamic_resolution == 1); + req.resolution_value = PSX_IR_DISPLAY; /* not "default" */ + r = psx_host_enh_resolve_resolution(&base, &req); + CHECK(r.internal_resolution == PSX_IR_DISPLAY && r.dynamic_resolution == 0); + /* A title with only the legacy factor: Game default keeps it unset. */ + const PsxHostResolution legacy = {PSX_IR_UNSET, 0}; + req.resolution_value = PSX_IR_UNSET; + r = psx_host_enh_resolve_resolution(&legacy, &req); + CHECK(r.internal_resolution == PSX_IR_UNSET && r.dynamic_resolution == 0); + req.resolution = 0; + req.resolution_value = PSX_IR_NATIVE; /* mod off again */ + r = psx_host_enh_resolve_resolution(&base, &req); + CHECK(r.internal_resolution == 1080 && r.dynamic_resolution == 1); + + /* Every manifest choice parses; Game default and junk are unset. */ + static const struct { const char* text; int value; } choices[] = { + {"default", PSX_IR_UNSET}, {"native", PSX_IR_NATIVE}, {"720p", 720}, + {"1080p", 1080}, {"1440p", 1440}, {"4k", 2160}, {"5k", 2880}, {"8k", 4320}, + {"display", PSX_IR_DISPLAY}, {"auto", PSX_IR_AUTO}, + {"", PSX_IR_UNSET}, {"1600", PSX_IR_UNSET}, {"4K", PSX_IR_UNSET}, + }; + for (unsigned i = 0; i < sizeof choices / sizeof choices[0]; ++i) + CHECK(psx_host_enh_parse_internal_resolution(choices[i].text) == choices[i].value); + CHECK(psx_host_enh_parse_internal_resolution(NULL) == PSX_IR_UNSET); + + /* The plugin: activation records, an option change asks the applier. */ + psx_host_enh_reset(); + psx_host_enh_requests(&req); + CHECK(!req.resolution && req.resolution_value == PSX_IR_UNSET); + activate("psx.internal-resolution"); /* no value: Game default */ + psx_host_enh_requests(&req); + CHECK(req.resolution && req.resolution_value == PSX_IR_UNSET); + set_opt(IR, "internal-resolution", "resolution", "auto"); + psx_host_enh_reset(); + activate("psx.internal-resolution"); + psx_host_enh_requests(&req); + CHECK(req.resolution && req.resolution_value == PSX_IR_AUTO); + /* No applier yet (renderer setup has not run): not live. */ + set_opt(IR, "internal-resolution", "resolution", "1440p"); + CHECK(option_changed("psx.internal-resolution", "resolution", "1440p") == 0); + psx_host_enh_set_resolution_applier(res_applier); + g_res_answer = 0; /* backend cannot, or netplay */ + CHECK(option_changed("psx.internal-resolution", "resolution", "1440p") == 0 && + g_res_applied == 1); + g_res_answer = 1; + CHECK(option_changed("psx.internal-resolution", "resolution", "1440p") == 1 && + g_res_applied == 2); + psx_host_enh_requests(&req); + CHECK(req.resolution_value == 1440); + psx_host_enh_set_resolution_applier(NULL); + CHECK(psx_host_enh_apply_resolution_live() == 0); + psx_host_enh_reset(); + psx_host_enh_requests(&req); + CHECK(!req.resolution && req.resolution_value == PSX_IR_UNSET); +} static void test_resolve(void) { /* A Tomba-like title: nearest, no smoothing, composite, no scanlines. */ @@ -216,6 +296,7 @@ int main(void) { test_resolve(); test_parsers(); test_plugins(); + test_resolution(); if (g_fail) { fprintf(stderr, "host_enhancements_test: %d failure(s)\n", g_fail); return 1; diff --git a/runtime/tests/test_internal_resolution.cpp b/runtime/tests/test_internal_resolution.cpp index 9d002d06..008cab88 100644 --- a/runtime/tests/test_internal_resolution.cpp +++ b/runtime/tests/test_internal_resolution.cpp @@ -1,5 +1,5 @@ -// internal_resolution.h: preset ids, the value encoding shared with the -// launcher, and the integer scale each preset asks for. Presets x reference +// internal_resolution.h: preset ids, the value encoding, 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). @@ -17,129 +17,6 @@ static void expect(const char* what, long long got, long long want) { } } -// ---- Launcher round trip: seed -> launcher -> adopt ------------------------- -// A model of recomp-ui's legacy Supersampling row (before the Internal -// resolution row): launcher_model clamps the seeded value to 1..4 on load and -// each click cycles 1 -> 2 -> 3 -> 4 -> 1. -static int legacy_row(int seeded, int clicks) { - int v = seeded < 1 ? 1 : seeded > 4 ? 4 : seeded; - for (int i = 0; i < clicks; i++) v = (v % 4) + 1; - return v; -} - -struct Host { // the host's video state between launcher trips - int preset; // g_video_internal_res - int scale; // g_video_scale -}; - -// One trip, the way main.cpp runs it at first boot and on the netplay -// soft-return. has_row = RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION. -// Legacy: ss_clicks cycles the Supersampling row. Row: row_pick > 0 picks a -// preset in the Internal resolution row (0 leaves it). -static PsxIrAdopted trip(Host& h, int has_row, int ss_clicks, int row_pick, - int s_max = 32, int display_px_h = 2160) { - const int seeded_preset = h.preset; - const int ss_seed = psx_ir_launcher_seed_supersampling(has_row, h.preset, h.scale, - 240, display_px_h); - const int ss_result = has_row ? ss_seed : legacy_row(ss_seed, ss_clicks); - int ir_result = PSX_IR_UNSET; - if (has_row) { - const int shown = h.preset != PSX_IR_UNSET - ? h.preset : psx_ir_from_supersampling(h.scale, 240); - ir_result = row_pick ? row_pick : shown; - } - PsxIrAdopted a = psx_ir_adopt_launcher(has_row, seeded_preset, ss_seed, ss_result, - ir_result, 240, display_px_h, s_max); - h.preset = a.preset; - h.scale = a.scale; - return a; -} - -static void launcher_round_trips() { - // Older recomp-ui (no Internal resolution row), nothing configured: the - // historical round trip. The player's Supersampling pick is what runs and - // what is saved, and no internal_resolution key appears. - { - Host h{PSX_IR_UNSET, 1}; - PsxIrAdopted a = trip(h, 0, 2, 0); // 1x -> 3x - expect("legacy pick scale", a.scale, 3); - expect("legacy pick preset", a.preset, PSX_IR_UNSET); - expect("legacy pick save ss", a.save_ss, 3); - expect("legacy pick no ir key", a.save_ir, PSX_IR_UNSET); - // Netplay soft-return: seeded with the running scale, picked again. - a = trip(h, 0, 1, 0); // 3x -> 4x - expect("legacy soft-return pick", a.scale, 4); - expect("legacy soft-return no ir key", a.save_ir, PSX_IR_UNSET); - a = trip(h, 0, 0, 0); // untouched - expect("legacy untouched", a.scale, 4); - } - { - Host h{PSX_IR_UNSET, 8}; // hand-edited 8 - PsxIrAdopted a = trip(h, 0, 0, 0); - expect("legacy clamps like before", a.scale, 4); // the row returns 4 - expect("legacy clamps save", a.save_ss, 4); - } - // A settings.toml "native" written by a build with the row, then an older - // launcher: the preset must not reset the player's pick (the reported bug). - { - Host h{PSX_IR_NATIVE, 1}; - PsxIrAdopted a = trip(h, 0, 2, 0); // 1x -> 3x - expect("native preset yields to pick", a.scale, 3); - expect("native preset dropped", a.preset, PSX_IR_UNSET); - expect("native preset key dropped", a.save_ir, PSX_IR_UNSET); - expect("native preset save ss", a.save_ss, 3); - } - // A preset the older launcher cannot show (game.toml or hand-edited 4K): - // the row starts on the nearest it can show (4x) and an untouched row - // keeps the preset, first boot and soft-return alike. - { - Host h{2160, 1}; - PsxIrAdopted a = trip(h, 0, 0, 0); - expect("hidden preset kept", a.preset, 2160); - expect("hidden preset scale", a.scale, 9); - expect("hidden preset save ss", a.save_ss, 4); - expect("hidden preset save ir", a.save_ir, 2160); - a = trip(h, 0, 0, 0); // soft-return, running 9x - expect("hidden preset soft-return kept", a.scale, 9); - a = trip(h, 0, 1, 0); // 4x -> 1x - expect("hidden preset replaced", a.scale, 1); - expect("hidden preset replaced preset", a.preset, PSX_IR_UNSET); - expect("hidden preset replaced key", a.save_ir, PSX_IR_UNSET); - } - { - Host h{PSX_IR_DISPLAY, 1}; // 2160-px display - PsxIrAdopted a = trip(h, 0, 0, 0); - expect("display kept", a.scale, 9); - a = trip(h, 0, 2, 0); // 4x -> 1x -> 2x - expect("display replaced", a.scale, 2); - } - // Launcher with the row: the row owns the preset, as before. - { - Host h{PSX_IR_UNSET, 3}; - PsxIrAdopted a = trip(h, 1, 0, 0); // shows 720p, untouched - expect("row untouched preset", a.preset, 720); - expect("row untouched scale", a.scale, 3); - expect("row untouched save", a.save_ir, 720); - a = trip(h, 1, 0, 4320); // 8K - expect("row 8k scale", a.scale, 18); - expect("row 8k save ss", a.save_ss, 4); - expect("row 8k save ir", a.save_ir, 4320); - a = trip(h, 1, 0, 4320, 4); // software ceiling - expect("row 8k software", a.scale, 4); - a = trip(h, 1, 0, PSX_IR_NATIVE); - expect("row native", a.scale, 1); - expect("row native save", a.save_ir, PSX_IR_NATIVE); - } - // Invalid row values never reach the host: the seeded preset stands. - { - PsxIrAdopted a = psx_ir_adopt_launcher(1, 1080, 1, 1, 0, 240, 0, 32); - expect("row invalid keeps preset", a.preset, 1080); - a = psx_ir_adopt_launcher(1, PSX_IR_UNSET, 2, 2, -7, 240, 0, 32); - expect("row invalid legacy factor", a.preset, 480); - expect("row invalid legacy scale", a.scale, 2); - } -} - int main() { // Presets at the NTSC 240-line reference (R4 races are 320x240). struct { const char* id; int s240; int s480; } table[] = { @@ -192,13 +69,6 @@ int main() { va, 240, 8640, 32, 4000, 4320), 18); expect("auto floor display", psx_dynres_floor_scale(PSX_IR_DISPLAY, va, 240, 1080, 9), 5); expect("auto floor 720p", psx_dynres_floor_scale(720, va, 240, 1080, 9), 3); - { - // A launcher row returning Auto keeps Auto (persisted as "auto"). - PsxIrAdopted a = psx_ir_adopt_launcher(1, 1080, 5, 5, PSX_IR_AUTO, 240, 1080, 32); - expect("adopt auto preset", a.preset, PSX_IR_AUTO); - expect("adopt auto scale", a.scale, 5); - expect("adopt auto save", a.save_ir, PSX_IR_AUTO); - } // Integer line counts and rejects. int v = 0; @@ -215,19 +85,6 @@ int main() { 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); - // Supersample: a factor on the target, not on native or the legacy factor. expect("ss parse 1.5", psx_ss_parse_milli("1.5"), 1500); expect("ss parse 2x", psx_ss_parse_milli("2x"), 2000); @@ -265,7 +122,6 @@ int main() { 4320, 240, 8640, 32, 4000, 4320), 32); expect("cap opt-out", psx_resolve_internal_scale_ss_cap( PSX_IR_DISPLAY, 240, 4320, 32, 1500, 0), 27); - launcher_round_trips(); if (g_failures) return 1; std::printf("internal_resolution: all checks passed\n"); diff --git a/runtime/tests/test_menu_launcher_callbacks.py b/runtime/tests/test_menu_launcher_callbacks.py index 88e6501d..6b909e84 100644 --- a/runtime/tests/test_menu_launcher_callbacks.py +++ b/runtime/tests/test_menu_launcher_callbacks.py @@ -3,9 +3,9 @@ thread while the game runs: they must not write runtime state (psxrecomp#674 review P1). settings_get reads only the main-thread snapshot (mh_video_get), and the -menu's game info publishes vocabulary built on the main thread -(recomp_overlay_setup -> build_internal_resolution_vocab) instead of building -it, or reading the live video globals, itself.""" +menu's game info reads no live video globals. Neither offers the internal +resolution: the built-in Internal Resolution mod owns it (its option changes +live through the Mods page), so the Settings view never draws that row.""" import re from pathlib import Path from test_netplay_load_apply_flow import function_body, require @@ -13,8 +13,7 @@ from test_netplay_load_apply_flow import function_body, require source = (Path(__file__).resolve().parents[1] / "src/main.cpp").read_text(encoding="utf-8") WRITE = re.compile(r"\bg_\w+\s*(?:\.\w+\s*)?(?:=(?!=)|\+\+|--|\+=|-=)") STATEFUL = ("psx_video_current", "psx_sdl_display_pixel_height", - "build_internal_resolution_vocab", "internal_resolution_for_launcher", - "session_host_look", "psx_host_enh_") + "session_host_look", "session_internal_resolution", "psx_host_enh_") for sig in ("void mh_launcher_settings_get(RecompLauncherCSettings* out)", "void mh_launcher_game_info(RecompLauncherCGameInfo* out)"): @@ -25,9 +24,11 @@ for sig in ("void mh_launcher_settings_get(RecompLauncherCSettings* out)", require(name not in body, f"{sig}: calls {name}") gi = function_body(source, "void mh_launcher_game_info(RecompLauncherCGameInfo* out)") -require("gi.internal_resolution_labels = g_ir_labels" in gi, - "menu game info publishes the main-thread Internal resolution vocabulary") -setup = function_body(source, "static void recomp_overlay_setup(const char* argv0, const std::string& title)") -require("build_internal_resolution_vocab(internal_resolution_for_launcher())" in setup, - "the vocabulary is built on the main thread before a provider attaches") +require("internal_resolution" not in gi, + "menu game info offers no Internal resolution row (the mod owns it)") +require("psx_launcher_hide_mod_owned_rows(&gi);" in gi, + "menu game info clears the mod-owned rows, Internal resolution included") +ls_get = function_body(source, "void mh_launcher_settings_get(RecompLauncherCSettings* out)") +require("internal_resolution" not in ls_get and "supersample" not in ls_get, + "settings_get hands the Settings view no internal resolution") print("menu launcher callbacks: draw-thread safe PASS")