diff --git a/docs/config_schema.md b/docs/config_schema.md index 0d41756e..56b2d6f1 100644 --- a/docs/config_schema.md +++ b/docs/config_schema.md @@ -321,13 +321,16 @@ audio production cadence: ```toml [audio] buffer_ms = 60 +offer_buffer_ms = true ``` -`buffer_ms` accepts 30–500 milliseconds and defaults to 180. Lower values -reduce audible input-to-sound delay, but leave less reserve for frames where a -game temporarily produces no audio and can therefore crackle on affected -titles. This is deliberately a per-game developer choice; it is not read from -the player's `settings.toml`. +`buffer_ms` defaults to 180. Lower values reduce audible input-to-sound delay, +but leave less reserve for frames where a game temporarily produces no audio +and can therefore crackle on affected titles. `offer_buffer_ms` defaults to +false. When a game explicitly enables it, recomp-ui shows a raw, unconstrained +Audio latency debug field and persists the player's choice as +`[audio] buffer_ms` in `settings.toml`. Games that do not opt in keep their +validated developer value and show no control. ## Video Block diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 06f889f5..23ca495a 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -551,12 +551,12 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& const toml::value& audio = toml::find(cfg, "audio"); if (audio.contains("buffer_ms")) { const auto n = toml::find(audio, "buffer_ms"); - if (n < 30 || n > 500) { - throw std::runtime_error(fmt::format( - "[audio] buffer_ms out of range (30..500): {}", n)); - } rt.audio_buffer_ms = static_cast(n); } + if (audio.contains("offer_buffer_ms")) { + rt.audio_offer_buffer_ms = + toml::find(audio, "offer_buffer_ms"); + } if (audio.contains("spu_hq")) { rt.audio_spu_hq = toml::find(audio, "spu_hq"); } @@ -2012,6 +2012,10 @@ UserSettings load_user_settings(const fs::path& path) { } if (doc.contains("audio")) { const toml::value& a = toml::find(doc, "audio"); + if (a.contains("buffer_ms")) try_get([&]{ + const int n = toml::find(a, "buffer_ms"); + s.audio_buffer_ms = n; s.has_audio_buffer_ms = true; + }); if (a.contains("spu_hq")) try_get([&]{ s.spu_hq = toml::find(a, "spu_hq"); s.has_spu_hq = true; }); @@ -2174,6 +2178,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { if (s.has_adaptive_view) f << "adaptive_view = " << (s.adaptive_view ? "true" : "false") << "\n"; f << "\n[audio]\n"; + if (s.has_audio_buffer_ms) + f << "buffer_ms = " << s.audio_buffer_ms << "\n"; if (s.has_spu_hq) f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n"; if (s.has_skip_launcher) diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 2ac9b96b..bd6ee7b9 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -401,9 +401,14 @@ struct RuntimeConfig { // buffer_ms: steady-state host playback cushion. The ecosystem default // remains 180 ms because it survives long streamed-stage production gaps; // games with smoother production cadence may opt into a lower value to - // reduce controller-to-sound latency. This is a game-developer setting, - // not a player preference. + // reduce controller-to-sound latency. + // When exposed through offer_buffer_ms this is intentionally a raw debug + // value: recomp-ui does not clamp or quantize it. int audio_buffer_ms = 180; + // offer_buffer_ms: expose the buffer target in recomp-ui for this game. + // Hidden by default because many games need their validated developer + // value to survive irregular audio-production stalls. + bool audio_offer_buffer_ms = false; // spu_hq: enable the SPU float-shadow re-render (Catmull-Rom resample, float // headroom). Verified-enhancement, default OFF — spu_render output is @@ -980,6 +985,7 @@ struct UserSettings { bool has_adaptive_view = false; bool adaptive_view = false; // [audio] bool has_spu_hq = false; bool spu_hq = false; + bool has_audio_buffer_ms = false; int audio_buffer_ms = 180; // [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/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index 434c5d7b..fbfbb167 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -137,21 +137,44 @@ note = "Test-only fixture" [audio] buffer_ms = 60 +offer_buffer_ms = true )toml"); const auto audio_config = PSXRecompV4::load_game_config(audio_buffer); - check(audio_config.runtime.audio_buffer_ms == 60, - "parser preserves per-game audio buffer target"); + check(audio_config.runtime.audio_buffer_ms == 60 && + audio_config.runtime.audio_offer_buffer_ms, + "parser preserves per-game audio buffer target and UI opt-in"); - const auto bad_audio_buffer = write_config(root, "bad-audio-buffer", R"toml( + const auto audio_settings = write_config(root, "audio-settings", R"toml( +[audio] +buffer_ms = 30 +)toml"); + const auto user_audio = + PSXRecompV4::load_user_settings(audio_settings); + check(user_audio.has_audio_buffer_ms && user_audio.audio_buffer_ms == 30, + "user settings preserve an opted-in audio buffer choice"); + + PSXRecompV4::UserSettings raw_user_audio; + raw_user_audio.has_audio_buffer_ms = true; + raw_user_audio.audio_buffer_ms = -7; + const auto raw_audio_settings = root / "raw-audio-settings.toml"; + check(PSXRecompV4::save_user_settings(raw_audio_settings, raw_user_audio), + "user settings write a raw audio buffer choice"); + const auto raw_audio_roundtrip = + PSXRecompV4::load_user_settings(raw_audio_settings); + check(raw_audio_roundtrip.has_audio_buffer_ms && + raw_audio_roundtrip.audio_buffer_ms == -7, + "user settings round-trip raw audio buffer choices without clamping"); + + const auto raw_audio_buffer = write_config(root, "raw-audio-buffer", R"toml( [runtime] [audio] -buffer_ms = 20 +buffer_ms = 1 )toml"); - check_throws( - [&] { (void)PSXRecompV4::load_game_config(bad_audio_buffer); }, - "[audio] buffer_ms out of range (30..500)", - "parser rejects unsafe audio buffer target"); + const auto raw_audio_config = + PSXRecompV4::load_game_config(raw_audio_buffer); + check(raw_audio_config.runtime.audio_buffer_ms == 1, + "parser leaves debug audio buffer targets unconstrained"); const auto duplicate_address = write_config(root, "duplicate-address", R"toml( [[recompiler.patch]] diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index d61414e9..cf350e53 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -4931,6 +4931,7 @@ int main(int argc, char** argv) { bool ws_adaptive_view_supported = false; bool frame_interpolation_offered = true; bool skip_fmv_offered = true; + bool audio_buffer_offered = false; bool vulkan_offered = false; /* game.toml [video] offer_vulkan; developer opt-in for launcher visibility */ int resolved_deadzone = -1; /* <0 => keep input.ini/runtime default (12000) */ /* Localization: the effective language (game.toml default -> settings.toml -> @@ -5195,6 +5196,7 @@ int main(int argc, char** argv) { (int)gc.ws_backdrop_x_sites.size()); g_audio_spu_hq = gc.runtime.audio_spu_hq; g_audio_buffer_ms = (double)gc.runtime.audio_buffer_ms; + audio_buffer_offered = gc.runtime.audio_offer_buffer_ms; g_auto_skip_fmv = gc.runtime.video_auto_skip_fmv ? 1 : 0; /* [controller] game-declared input defaults (settings.toml/launcher * still override below). */ @@ -5514,6 +5516,8 @@ int main(int argc, char** argv) { } if (us.has_adaptive_view) g_ws_adaptive_view = us.adaptive_view; if (us.has_spu_hq) g_audio_spu_hq = us.spu_hq; + if (audio_buffer_offered && us.has_audio_buffer_ms) + g_audio_buffer_ms = (double)us.audio_buffer_ms; /* Bundled BIOS: ignore any persisted bios_path (same clamp-as-well- * as-hide treatment as lock_mode / ws_offered below) so a stale * settings.toml or launcher-less build can never point a bundled @@ -5775,6 +5779,8 @@ int main(int argc, char** argv) { seed.adaptive_view = g_ws_adaptive_view; seed.has_adaptive_view = ws_offered; seed.spu_hq = g_audio_spu_hq; seed.has_spu_hq = true; + seed.audio_buffer_ms = (int)g_audio_buffer_ms; + seed.has_audio_buffer_ms = audio_buffer_offered; seed.skip_launcher = skip_launcher_setting; seed.has_skip_launcher = true; if (has_netplay_player_name) { seed.netplay_player_name = netplay_player_name; @@ -5865,6 +5871,7 @@ int main(int argc, char** argv) { ls.frame_interp = seed.frame_interpolation ? 1 : 0; ls.frame_interp_fps = seed.frame_interpolation_fps; ls.spu_hq = seed.spu_hq ? 1 : 0; + ls.audio_buffer_ms = seed.audio_buffer_ms; ls.auto_skip_fmv = seed.auto_skip_fmv ? 1 : 0; ls.turbo_loads = seed.turbo_loads ? 1 : 0; /* Localization: index of resolved_language within lang_menu_options @@ -5976,6 +5983,7 @@ int main(int argc, char** argv) { gi.renderer_labels = kPsxRendererLabels; gi.num_renderers = vulkan_offered ? 3 : 2; gi.has_skip_fmv = skip_fmv_offered ? 1 : 0; + gi.has_audio_buffer_ms = audio_buffer_offered ? 1 : 0; /* Localization menu: shown only when the game declares languages. */ if (!rui_lang_labels.empty()) { gi.language_labels = rui_lang_labels.data(); @@ -6077,6 +6085,10 @@ int main(int argc, char** argv) { seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = frame_interpolation_offered; seed.spu_hq = ls.spu_hq != 0; seed.has_spu_hq = true; + if (audio_buffer_offered) { + seed.audio_buffer_ms = ls.audio_buffer_ms; + seed.has_audio_buffer_ms = true; + } seed.auto_skip_fmv = ls.auto_skip_fmv != 0; seed.has_auto_skip_fmv = skip_fmv_offered; seed.turbo_loads = ls.turbo_loads != 0; seed.has_turbo_loads = true; @@ -6167,6 +6179,8 @@ int main(int argc, char** argv) { g_video_aspect_den = ws_offered ? seed.aspect_den : 3; g_ws_adaptive_view = ws_offered && seed.adaptive_view; g_audio_spu_hq = seed.spu_hq; + if (audio_buffer_offered) + g_audio_buffer_ms = (double)seed.audio_buffer_ms; skip_launcher_setting = seed.skip_launcher; if (seed.has_bios_path) { settings_bios_storage = seed.bios_path.string(); diff --git a/runtime/src/recomp_audio_drc.h b/runtime/src/recomp_audio_drc.h index c96f141a..c33d6d1f 100644 --- a/runtime/src/recomp_audio_drc.h +++ b/runtime/src/recomp_audio_drc.h @@ -278,7 +278,12 @@ static void rab__update_controller(rab_bridge *b, int pull_frames) { double dt_ms = (double)pull_frames * 1000.0 / c->host_rate; if (dt_ms < 0.1) dt_ms = 0.1; if (dt_ms > 100.0) dt_ms = 100.0; - double err = (fill_ms - c->target_ms) / c->target_ms; + /* `target_ms` may be exposed as an unconstrained debug value. Keep zero + * mathematically defined without silently changing the requested target. */ + double norm_ms = fabs(c->target_ms); + if (norm_ms < 1000.0 / c->source_rate) + norm_ms = 1000.0 / c->source_rate; + double err = (fill_ms - c->target_ms) / norm_ms; if (fill_ms > c->target_ms - c->deadband_ms && fill_ms < c->target_ms + c->deadband_ms) { err = 0.0; /* deadband */