// Copyright 2017 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include #include #include #include #include "common/file_util.h" #include "core/hw/gpu.h" #include "core/perf_stats.h" #include "core/settings.h" using namespace std::chrono_literals; using DoubleSecs = std::chrono::duration; using std::chrono::duration_cast; using std::chrono::microseconds; // Purposefully ignore the first five frames, as there's a significant amount of overhead in // booting that we shouldn't account for constexpr std::size_t IgnoreFrames = 5; namespace Core { PerfStats::PerfStats(u64 title_id) : title_id(title_id) {} PerfStats::~PerfStats() { if (!Settings::values.record_frame_times || title_id == 0) { return; } const std::time_t t = std::time(nullptr); std::ostringstream stream; std::copy(perf_history.begin() + IgnoreFrames, perf_history.begin() + current_index, std::ostream_iterator(stream, "\n")); const std::string& path = FileUtil::GetUserPath(FileUtil::UserPath::LogDir); // %F Date format expanded is "%Y-%m-%d" const std::string filename = fmt::format("{}/{:%F-%H-%M}_{:016X}.csv", path, *std::localtime(&t), title_id); FileUtil::IOFile file(filename, "w"); file.WriteString(stream.str()); } void PerfStats::BeginSystemFrame() { std::lock_guard lock{object_mutex}; frame_begin = Clock::now(); } void PerfStats::EndSystemFrame() { std::lock_guard lock{object_mutex}; auto frame_end = Clock::now(); const auto frame_time = frame_end - frame_begin; if (current_index < perf_history.size()) { perf_history[current_index++] = std::chrono::duration(frame_time).count(); } accumulated_frametime += frame_time; system_frames += 1; previous_frame_length = frame_end - previous_frame_end; previous_frame_end = frame_end; } void PerfStats::EndGameFrame() { std::lock_guard lock{object_mutex}; game_frames += 1; } double PerfStats::GetMeanFrametime() { std::lock_guard lock{object_mutex}; if (current_index <= IgnoreFrames) { return 0; } const double sum = std::accumulate(perf_history.begin() + IgnoreFrames, perf_history.begin() + current_index, 0); return sum / (current_index - IgnoreFrames); } PerfStats::Results PerfStats::GetAndResetStats(microseconds current_system_time_us) { std::lock_guard lock(object_mutex); const auto now = Clock::now(); // Walltime elapsed since stats were reset const auto interval = duration_cast(now - reset_point).count(); const auto system_us_per_second = (current_system_time_us - reset_point_system_us) / interval; Results results{}; results.system_fps = static_cast(system_frames) / interval; results.game_fps = static_cast(game_frames) / interval; results.frametime = duration_cast(accumulated_frametime).count() / static_cast(system_frames); results.emulation_speed = system_us_per_second.count() / 1'000'000.0; // Reset counters reset_point = now; reset_point_system_us = current_system_time_us; accumulated_frametime = Clock::duration::zero(); system_frames = 0; game_frames = 0; return results; } double PerfStats::GetLastFrameTimeScale() { std::lock_guard lock{object_mutex}; constexpr double FRAME_LENGTH = 1.0 / GPU::SCREEN_REFRESH_RATE; return duration_cast(previous_frame_length).count() / FRAME_LENGTH; } void FrameLimiter::WaitOnce() { if (frame_advancing_enabled) { // Frame advancing is enabled: wait on event instead of doing framelimiting frame_advance_event.Wait(); frame_advance_event.Reset(); } } void FrameLimiter::DoFrameLimiting(microseconds current_system_time_us) { if (frame_advancing_enabled) { // Frame advancing is enabled: wait on event instead of doing framelimiting frame_advance_event.Wait(); frame_advance_event.Reset(); return; } if (!Settings::values.use_frame_limit) { return; } auto now = Clock::now(); double sleep_scale = Settings::values.frame_limit / 100.0; // Max lag caused by slow frames. Shouldn't be more than the length of a frame at the current // speed percent or it will clamp too much and prevent this from properly limiting to that // percent. High values means it'll take longer after a slow frame to recover and start limiting const microseconds max_lag_time_us = duration_cast( std::chrono::duration(25ms / sleep_scale)); frame_limiting_delta_err += duration_cast( std::chrono::duration( (current_system_time_us - previous_system_time_us) / sleep_scale)); frame_limiting_delta_err -= duration_cast(now - previous_walltime); frame_limiting_delta_err = std::clamp(frame_limiting_delta_err, -max_lag_time_us, max_lag_time_us); if (frame_limiting_delta_err > microseconds::zero()) { std::this_thread::sleep_for(frame_limiting_delta_err); auto now_after_sleep = Clock::now(); frame_limiting_delta_err -= duration_cast(now_after_sleep - now); now = now_after_sleep; } previous_system_time_us = current_system_time_us; previous_walltime = now; } void FrameLimiter::SetFrameAdvancing(bool value) { const bool was_enabled = frame_advancing_enabled.exchange(value); if (was_enabled && !value) { // Set the event to let emulation continue frame_advance_event.Set(); } } void FrameLimiter::AdvanceFrame() { frame_advance_event.Set(); } } // namespace Core