mirror of
https://github.com/libretro/parallel-gx2.git
synced 2026-11-07 04:25:58 -10:00
Pace the core's audio by what the frontend asks for, and show the game's frame rate
The audio-stall branch, tested at both ends - sco8487 on a Mali phone running Deus Ex at 12 fps, NNshi on a desktop in Xenoblade, MK8 and BotW with FPS++ - and squashed here once both were satisfied. - AX frames are at least 1.7 ms apart in the core too, as on the wall-clock path, so a large grant no longer runs thirty-odd audio frames back to back and starves the title's streaming voices (Deus Ex repeating a line). - A retro_run never grants less than one frame's audio (800 samples), which a device that spends most of each frame waiting on audio sync needs, and never more than has played, so unevenly spaced or faster-than-60 retro_runs do not make RetroArch block on audio it has too much of. - The ring holds a whole grant: at 128 ms a slow device's 110-140 ms grants lost 4000-14000 samples a second in heavy scenes; at 341 ms it lost none in 24 minutes of sco8487's testing. - Video > Show Game FPS puts the frames the title actually finished on the OSD. RetroArch's own counter counts retro_run calls, which stay at 60 while a game drops frames. - log.txt names the build it came from (also cherry-picked earlier).
This commit is contained in:
1 parent
7bf6ce49e3
commit
627802831c
4 files changed
+140
-6
No files matched your search
@@ -573,6 +573,28 @@ namespace snd_core
|
||||
#ifdef RETRO_CORE
|
||||
if (s_libretro_ax_budget.load(std::memory_order_relaxed) < AX_SAMPLES_PER_3MS_48KHZ)
|
||||
return;
|
||||
|
||||
// The budget says how much audio to make, not how fast. A slow device's
|
||||
// retro_run grants 100 ms or more at once, and spent as soon as it
|
||||
// arrived that is thirty-odd 3 ms frames back to back - with no time
|
||||
// between them for the title's own threads, which is when a streaming
|
||||
// voice gets its next buffer. Starved of that, the voice loops what it
|
||||
// already has: sco8487's Deus Ex repeating a line ("stand stand
|
||||
// stand") while the NPCs wait for it to end. The wall-clock path below
|
||||
// keeps frames at least 1.7 ms apart for the same reason, so this one
|
||||
// does too - fast enough to catch up on a backlog at nearly twice real
|
||||
// time, slow enough to leave the title room between frames.
|
||||
constexpr static auto kLibretroMinSpacing = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::microseconds(1700));
|
||||
#if BOOST_OS_WINDOWS
|
||||
const auto libretroNow = tick_cached();
|
||||
static auto s_libretro_last_frame = libretroNow - kLibretroMinSpacing;
|
||||
#else
|
||||
const auto libretroNow = now_cached();
|
||||
static auto s_libretro_last_frame = libretroNow - kLibretroMinSpacing;
|
||||
#endif
|
||||
if ((libretroNow - s_libretro_last_frame) < kLibretroMinSpacing)
|
||||
return;
|
||||
|
||||
g_ax_update_passed.fetch_add(1, std::memory_order_relaxed);
|
||||
if (snd_core::isInitialized() && numQueuedFramesSndGeneric == snd_core::getNumProcessedFrames())
|
||||
{
|
||||
@@ -580,6 +602,7 @@ namespace snd_core
|
||||
snd_core::AXIst_QueueFrame();
|
||||
numQueuedFramesSndGeneric++;
|
||||
s_libretro_ax_budget.fetch_sub(AX_SAMPLES_PER_3MS_48KHZ, std::memory_order_relaxed);
|
||||
s_libretro_last_frame = libretroNow;
|
||||
}
|
||||
return;
|
||||
#endif
|
||||
|
||||
@@ -642,9 +642,15 @@ std::atomic_bool s_frame_ready{false};
|
||||
// title. The Vulkan renderer used to store the flag on its own and did exactly
|
||||
// that, while the OpenGL path signalled properly, which is why one was half the
|
||||
// speed of the other.
|
||||
// Frames the title finished, for the Show Game FPS option. Not
|
||||
// s_prof_frames_ready, which the profiling log resets on its own schedule.
|
||||
static std::atomic<uint32_t> s_game_frames{0};
|
||||
static bool s_show_game_fps = false;
|
||||
|
||||
void libretro_signal_frame_ready()
|
||||
{
|
||||
s_prof_frames_ready.fetch_add(1, std::memory_order_relaxed);
|
||||
s_game_frames.fetch_add(1, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard lock(s_frame_mutex);
|
||||
s_frame_ready.store(true, std::memory_order_release);
|
||||
@@ -2189,6 +2195,13 @@ static void libretro_apply_core_options()
|
||||
// LatteOverlay_pushNotification to SET_MESSAGE_EXT.
|
||||
cfg.notification.position = ScreenPosition::kDisabled;
|
||||
|
||||
if (const char* v = libretro_get_option_value("cemu_show_game_fps"))
|
||||
{
|
||||
bool b;
|
||||
if (libretro_parse_enabled_disabled(v, b))
|
||||
s_show_game_fps = b;
|
||||
}
|
||||
|
||||
// Async shader compilation
|
||||
if (const char* v = libretro_get_option_value("cemu_async_shader_compile"))
|
||||
{
|
||||
@@ -4659,11 +4672,71 @@ extern GLuint libretro_getBackbufferRBO();
|
||||
// frontend's audio callback (see the audio grant in retro_run).
|
||||
static int64_t s_last_audio_wait_us = 0;
|
||||
|
||||
// The game's own frame rate, once a second, as a status line on the
|
||||
// frontend's OSD. RetroArch's FPS counter cannot show it: retro_run keeps
|
||||
// coming at 60 Hz whatever the title renders at, presenting the last image
|
||||
// again when no new one is ready, so a title dropping to 40 still counts 60
|
||||
// there and only the frame time shows it (NNshi).
|
||||
static void libretro_report_game_fps(std::chrono::steady_clock::time_point now)
|
||||
{
|
||||
static std::chrono::steady_clock::time_point s_since{};
|
||||
static bool s_have_ext = false, s_checked_ext = false;
|
||||
static std::string s_text;
|
||||
|
||||
if (!s_show_game_fps || !environ_cb)
|
||||
{
|
||||
s_since = {};
|
||||
s_game_frames.store(0, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
if (!s_checked_ext)
|
||||
{
|
||||
unsigned version = 0;
|
||||
s_have_ext = environ_cb(RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION, &version) && version >= 1;
|
||||
s_checked_ext = true;
|
||||
}
|
||||
if (s_since == std::chrono::steady_clock::time_point{})
|
||||
{
|
||||
s_since = now;
|
||||
s_game_frames.store(0, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
const double seconds = std::chrono::duration<double>(now - s_since).count();
|
||||
if (seconds < 1.0)
|
||||
return;
|
||||
|
||||
const uint32_t frames = s_game_frames.exchange(0, std::memory_order_relaxed);
|
||||
s_since = now;
|
||||
s_text = fmt::format("Game: {:.1f} FPS", frames / seconds);
|
||||
|
||||
if (s_have_ext)
|
||||
{
|
||||
// A status message is the frontend's line for figures like this one,
|
||||
// replaced in place rather than stacked up as notifications.
|
||||
struct retro_message_ext message = {};
|
||||
message.msg = s_text.c_str();
|
||||
message.duration = 1500;
|
||||
message.priority = 1;
|
||||
message.level = RETRO_LOG_INFO;
|
||||
message.target = RETRO_MESSAGE_TARGET_OSD;
|
||||
message.type = RETRO_MESSAGE_TYPE_STATUS;
|
||||
message.progress = -1;
|
||||
environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE_EXT, &message);
|
||||
}
|
||||
else
|
||||
{
|
||||
struct retro_message message{s_text.c_str(), 90};
|
||||
environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &message);
|
||||
}
|
||||
}
|
||||
|
||||
static void libretro_finish_run(std::chrono::steady_clock::time_point start,
|
||||
std::chrono::steady_clock::time_point waited, bool timedOut)
|
||||
{
|
||||
using prof_clock = std::chrono::steady_clock;
|
||||
const auto presented = prof_clock::now();
|
||||
libretro_report_game_fps(presented);
|
||||
|
||||
LibretroAudioAPI::FlushAudio();
|
||||
s_last_audio_wait_us = std::chrono::duration_cast<std::chrono::microseconds>(prof_clock::now() - presented).count();
|
||||
@@ -4895,15 +4968,35 @@ RETRO_API void retro_run()
|
||||
// itself instead: a long wait means a smaller grant next time, the
|
||||
// frontend's buffer has room again, and the wait goes away. Capped at
|
||||
// 250 ms against a stall.
|
||||
//
|
||||
// But never less than one frame's worth. With audio sync on, waiting in
|
||||
// the audio callback is how RetroArch holds retro_run to 60 a second, so
|
||||
// a device that keeps up spends much of every frame there - and leaving
|
||||
// all of it out granted a fraction of the 800 samples a 60 Hz frame
|
||||
// needs. sco8487's phone sat in its menu at 60 fps with the frontend's
|
||||
// buffer underrunning 70-90% of the time. 800 a frame is what any core
|
||||
// hands over; only the time beyond it is what a slow frame adds.
|
||||
//
|
||||
// And never more than has played. The floor on its own gave 800 to every
|
||||
// retro_run however soon it came after the last, so a frontend that runs
|
||||
// them unevenly - 10 ms, then 23 ms - was granted more audio than the time
|
||||
// that passed, and one running faster than 60 Hz more still. NNshi saw it
|
||||
// as RetroArch blocking 20-50 % of the time on audio it had too much of.
|
||||
// Wall time fills a bucket, capped at the same 250 ms, and a grant takes
|
||||
// no more than the bucket holds: at 60 Hz that is 800 either way, and over
|
||||
// any stretch of time the grants add up to at most the time itself.
|
||||
{
|
||||
static std::chrono::steady_clock::time_point s_last_audio_grant{};
|
||||
static int64_t s_audio_bucket = 0; // in thousandths of a sample, so no fraction is lost
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
int32_t samples = 800;
|
||||
if (s_last_audio_grant.time_since_epoch().count() != 0)
|
||||
{
|
||||
const int64_t us = std::chrono::duration_cast<std::chrono::microseconds>(now - s_last_audio_grant).count()
|
||||
- s_last_audio_wait_us;
|
||||
samples = (int32_t)std::clamp<int64_t>(us * 48 / 1000, 0, 12000);
|
||||
const int64_t elapsed_us = std::chrono::duration_cast<std::chrono::microseconds>(now - s_last_audio_grant).count();
|
||||
s_audio_bucket = std::min<int64_t>(s_audio_bucket + elapsed_us * 48, 12000 * 1000);
|
||||
const int64_t us = elapsed_us - s_last_audio_wait_us;
|
||||
samples = (int32_t)std::min<int64_t>(std::clamp<int64_t>(us * 48 / 1000, 800, 12000), s_audio_bucket / 1000);
|
||||
s_audio_bucket -= int64_t(samples) * 1000;
|
||||
}
|
||||
s_last_audio_grant = now;
|
||||
snd_core::AXOut_LibretroGrantSamples(samples);
|
||||
|
||||
@@ -12,9 +12,13 @@
|
||||
class LibretroAudioRingBuffer
|
||||
{
|
||||
public:
|
||||
// ~128 ms at 48 kHz stereo. Large enough to absorb host scheduling jitter
|
||||
// but small enough that draining everything per retro_run stays cheap.
|
||||
static constexpr size_t kBufferFrames = 6144;
|
||||
// ~341 ms at 48 kHz stereo. It has to hold everything AX may make between
|
||||
// two retro_runs, and a retro_run grants up to 250 ms (see
|
||||
// AXOut_LibretroGrantSamples). At 128 ms a slow device's frames - 8 or 9
|
||||
// a second in sco8487's heavier Deus Ex scenes - got grants the ring could
|
||||
// not take, and the rest was dropped. Drained every retro_run, so the
|
||||
// size adds no latency.
|
||||
static constexpr size_t kBufferFrames = 16384;
|
||||
static constexpr size_t kChannels = 2;
|
||||
static constexpr size_t kBufferSamples = kBufferFrames * kChannels;
|
||||
|
||||
|
||||
@@ -194,6 +194,20 @@ struct retro_core_option_v2_definition option_defs_us[] = {
|
||||
},
|
||||
"linear"
|
||||
},
|
||||
{
|
||||
"cemu_show_game_fps",
|
||||
"Show Game FPS",
|
||||
NULL,
|
||||
"Show the frame rate the game renders at. RetroArch's own FPS counter counts how often the core runs, and that stays at 60 when the game drops frames, because every run carries a frame's worth of audio.",
|
||||
NULL,
|
||||
"video",
|
||||
{
|
||||
{ "disabled", NULL },
|
||||
{ "enabled", NULL },
|
||||
{ NULL, NULL },
|
||||
},
|
||||
"disabled"
|
||||
},
|
||||
{
|
||||
"cemu_downscale_filter",
|
||||
"Downscale Filter",
|
||||
|
||||
Reference in new issue
Block a user