diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index f47792fd6..551617ae7 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -215,6 +215,8 @@ void Config::ReadValues() { } catch (...) { } } + ReadSetting("System", Settings::values.init_ticks_type); + ReadSetting("System", Settings::values.init_ticks_override); ReadSetting("System", Settings::values.plugin_loader_enabled); ReadSetting("System", Settings::values.allow_plugin_loader); diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 8718b264b..a2cd02c85 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -288,6 +288,14 @@ init_clock = # Note: 3DS can only handle times later then Jan 1 2000 init_time = +# The system ticks count to use when citra starts +# 0: Random (default), 1: Fixed +init_ticks_type = + +# Tick count to use when init_ticks_type is set to Fixed. +# Defaults to 0. +init_ticks_override = + # Plugin loader state, if enabled plugins will be loaded from the SD card. # You can also set if homebrew apps are allowed to enable the plugin loader plugin_loader = diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 5f1f9c9c8..5c0ffcbdf 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -225,6 +225,8 @@ void Config::ReadValues() { std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch()) .count(); } + ReadSetting("System", Settings::values.init_ticks_type); + ReadSetting("System", Settings::values.init_ticks_override); ReadSetting("System", Settings::values.plugin_loader_enabled); ReadSetting("System", Settings::values.allow_plugin_loader); diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index 6dd52b372..273e9a423 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -307,6 +307,14 @@ init_clock = # Note: 3DS can only handle times later then Jan 1 2000 init_time = +# The system ticks count to use when citra starts +# 0: Random (default), 1: Fixed +init_ticks_type = + +# Tick count to use when init_ticks_type is set to Fixed. +# Defaults to 0. +init_ticks_override = + [Camera] # Which camera engine to use for the right outer camera # blank (default): a dummy camera that always returns black image diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index b00f17664..5c0abbc2a 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -687,6 +687,8 @@ void Config::ReadSystemValues() { ReadBasicSetting(Settings::values.init_clock); ReadBasicSetting(Settings::values.init_time); ReadBasicSetting(Settings::values.init_time_offset); + ReadBasicSetting(Settings::values.init_ticks_type); + ReadBasicSetting(Settings::values.init_ticks_override); ReadBasicSetting(Settings::values.plugin_loader_enabled); ReadBasicSetting(Settings::values.allow_plugin_loader); } @@ -1173,6 +1175,8 @@ void Config::SaveSystemValues() { WriteBasicSetting(Settings::values.init_clock); WriteBasicSetting(Settings::values.init_time); WriteBasicSetting(Settings::values.init_time_offset); + WriteBasicSetting(Settings::values.init_ticks_type); + WriteBasicSetting(Settings::values.init_ticks_override); WriteBasicSetting(Settings::values.plugin_loader_enabled); WriteBasicSetting(Settings::values.allow_plugin_loader); } diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp index ebf2eb6dd..7936df256 100644 --- a/src/citra_qt/configuration/configure_system.cpp +++ b/src/citra_qt/configuration/configure_system.cpp @@ -230,6 +230,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent) &ConfigureSystem::UpdateBirthdayComboBox); connect(ui->combo_init_clock, qOverload(&QComboBox::currentIndexChanged), this, &ConfigureSystem::UpdateInitTime); + connect(ui->combo_init_ticks_type, qOverload(&QComboBox::currentIndexChanged), this, + &ConfigureSystem::UpdateInitTicks); connect(ui->button_regenerate_console_id, &QPushButton::clicked, this, &ConfigureSystem::RefreshConsoleID); connect(ui->button_start_download, &QPushButton::clicked, this, @@ -293,6 +295,11 @@ void ConfigureSystem::SetConfiguration() { QTime time = QTime::fromMSecsSinceStartOfDay(static_cast(time_offset * 1000)); ui->edit_init_time_offset_time->setTime(time); + ui->combo_init_ticks_type->setCurrentIndex( + static_cast(Settings::values.init_ticks_type.GetValue())); + ui->edit_init_ticks_value->setText( + QString::number(Settings::values.init_ticks_override.GetValue())); + cfg = Service::CFG::GetModule(system); ReadSystemSettings(); @@ -413,6 +420,11 @@ void ConfigureSystem::ApplyConfiguration() { static_cast(ui->combo_init_clock->currentIndex()); Settings::values.init_time = ui->edit_init_time->dateTime().toSecsSinceEpoch(); + Settings::values.init_ticks_type = + static_cast(ui->combo_init_ticks_type->currentIndex()); + Settings::values.init_ticks_override = + static_cast(ui->edit_init_ticks_value->text().toLongLong()); + s64 time_offset_time = ui->edit_init_time_offset_time->time().msecsSinceStartOfDay() / 1000; s64 time_offset_days = ui->edit_init_time_offset_days->value() * 86400; @@ -462,6 +474,7 @@ void ConfigureSystem::ConfigureTime() { SetConfiguration(); UpdateInitTime(ui->combo_init_clock->currentIndex()); + UpdateInitTicks(ui->combo_init_ticks_type->currentIndex()); } void ConfigureSystem::UpdateInitTime(int init_clock) { @@ -477,6 +490,15 @@ void ConfigureSystem::UpdateInitTime(int init_clock) { ui->edit_init_time_offset_time->setVisible(!is_fixed_time && is_global); } +void ConfigureSystem::UpdateInitTicks(int init_ticks_type) { + const bool is_global = Settings::IsConfiguringGlobal(); + const bool is_fixed = + static_cast(init_ticks_type) == Settings::InitTicks::Fixed; + + ui->label_init_ticks_value->setVisible(is_fixed && is_global); + ui->edit_init_ticks_value->setVisible(is_fixed && is_global); +} + void ConfigureSystem::RefreshConsoleID() { QMessageBox::StandardButton reply; QString warning_text = tr("This will replace your current virtual 3DS with a new one. " @@ -512,6 +534,8 @@ void ConfigureSystem::SetupPerGameUI() { ui->label_birthday->setVisible(false); ui->label_init_clock->setVisible(false); ui->label_init_time->setVisible(false); + ui->label_init_ticks_type->setVisible(false); + ui->label_init_ticks_value->setVisible(false); ui->label_console_id->setVisible(false); ui->label_sound->setVisible(false); ui->label_language->setVisible(false); @@ -522,12 +546,14 @@ void ConfigureSystem::SetupPerGameUI() { ui->combo_birthday->setVisible(false); ui->combo_birthmonth->setVisible(false); ui->combo_init_clock->setVisible(false); + ui->combo_init_ticks_type->setVisible(false); ui->combo_sound->setVisible(false); ui->combo_language->setVisible(false); ui->combo_country->setVisible(false); ui->label_init_time_offset->setVisible(false); ui->edit_init_time_offset_days->setVisible(false); ui->edit_init_time_offset_time->setVisible(false); + ui->edit_init_ticks_value->setVisible(false); ui->toggle_system_setup->setVisible(false); ui->button_regenerate_console_id->setVisible(false); // Apps can change the state of the plugin loader, so plugins load diff --git a/src/citra_qt/configuration/configure_system.h b/src/citra_qt/configuration/configure_system.h index 86b6b7016..2c1d30649 100644 --- a/src/citra_qt/configuration/configure_system.h +++ b/src/citra_qt/configuration/configure_system.h @@ -43,6 +43,7 @@ private: void UpdateBirthdayComboBox(int birthmonth_index); void UpdateInitTime(int init_clock); + void UpdateInitTicks(int init_ticks_type); void RefreshConsoleID(); void SetupPerGameUI(); diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui index 728db6c76..bd3332bfd 100644 --- a/src/citra_qt/configuration/configure_system.ui +++ b/src/citra_qt/configuration/configure_system.ui @@ -304,34 +304,75 @@ + + + Initial System Ticks + + + + + + + + Random + + + + + Fixed + + + + + + + + Initial System Ticks Override + + + + + + + + 0 + 0 + + + + 20 + + + + Play Coins: - + 300 - + Run System Setup when Home Menu is launched - + Console ID: - + @@ -347,35 +388,35 @@ - + 3GX Plugin Loader: - + Enable 3GX plugin loader - + Allow games to change plugin loader state - + Download System Files from Nitendo servers - + diff --git a/src/common/settings.h b/src/common/settings.h index 4f308a7c3..3b51e5dd4 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -28,6 +28,11 @@ enum class InitClock : u32 { FixedTime = 1, }; +enum class InitTicks : u32 { + Random = 0, + Fixed = 1, +}; + enum class LayoutOption : u32 { Default, SingleScreen, @@ -437,6 +442,8 @@ struct Values { Setting init_clock{InitClock::SystemTime, "init_clock"}; Setting init_time{946681277ULL, "init_time"}; Setting init_time_offset{0, "init_time_offset"}; + Setting init_ticks_type{InitTicks::Random, "init_ticks_type"}; + Setting init_ticks_override{0, "init_ticks_override"}; Setting plugin_loader_enabled{false, "plugin_loader"}; Setting allow_plugin_loader{true, "allow_plugin_loader"}; diff --git a/src/core/core.cpp b/src/core/core.cpp index e8c0c4178..db5096a1b 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -379,7 +379,8 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, memory = std::make_unique(*this); - timing = std::make_unique(num_cores, Settings::values.cpu_clock_percentage.GetValue()); + timing = std::make_unique(num_cores, Settings::values.cpu_clock_percentage.GetValue(), + movie.GetOverrideBaseTicks()); kernel = std::make_unique( *memory, *timing, [this] { PrepareReschedule(); }, memory_mode, num_cores, n3ds_hw_caps, diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp index 991f85ea1..0d2506918 100644 --- a/src/core/core_timing.cpp +++ b/src/core/core_timing.cpp @@ -3,9 +3,11 @@ // Refer to the license.txt file included. #include +#include #include #include "common/assert.h" #include "common/logging/log.h" +#include "common/settings.h" #include "core/core_timing.h" namespace Core { @@ -19,15 +21,28 @@ bool Timing::Event::operator<(const Timing::Event& right) const { return std::tie(time, fifo_order) < std::tie(right.time, right.fifo_order); } -Timing::Timing(std::size_t num_cores, u32 cpu_clock_percentage) { +Timing::Timing(std::size_t num_cores, u32 cpu_clock_percentage, s64 override_base_ticks) { + // Generate non-zero base tick count to simulate time the system ran before launching the game. + // This accounts for games that rely on the system tick to seed randomness. + const auto base_ticks = override_base_ticks >= 0 ? override_base_ticks : GenerateBaseTicks(); + timers.resize(num_cores); for (std::size_t i = 0; i < num_cores; ++i) { - timers[i] = std::make_shared(); + timers[i] = std::make_shared(base_ticks); } UpdateClockSpeed(cpu_clock_percentage); current_timer = timers[0].get(); } +s64 Timing::GenerateBaseTicks() { + if (Settings::values.init_ticks_type.GetValue() == Settings::InitTicks::Fixed) { + return Settings::values.init_ticks_override.GetValue(); + } + // Bounded to 32 bits to make sure we don't generate too high of a counter and risk overflowing. + std::mt19937 random_gen(std::random_device{}()); + return random_gen(); +} + void Timing::UpdateClockSpeed(u32 cpu_clock_percentage) { for (auto& timer : timers) { timer->cpu_clock_scale = 100.0 / cpu_clock_percentage; @@ -146,7 +161,7 @@ std::shared_ptr Timing::GetTimer(std::size_t cpu_id) { return timers[cpu_id]; } -Timing::Timer::Timer() = default; +Timing::Timer::Timer(s64 base_ticks) : executed_ticks(base_ticks) {} Timing::Timer::~Timer() { MoveEvents(); diff --git a/src/core/core_timing.h b/src/core/core_timing.h index 9f9c40027..e8bded04f 100644 --- a/src/core/core_timing.h +++ b/src/core/core_timing.h @@ -185,7 +185,7 @@ public: class Timer { public: - Timer(); + Timer(s64 base_ticks = 0); ~Timer(); s64 GetMaxSliceLength() const; @@ -249,7 +249,7 @@ public: friend class boost::serialization::access; }; - explicit Timing(std::size_t num_cores, u32 cpu_clock_percentage); + explicit Timing(std::size_t num_cores, u32 cpu_clock_percentage, s64 override_base_ticks = -1); ~Timing(){}; @@ -290,6 +290,9 @@ public: event_queue_locked = false; } + /// Generates a random tick count to seed the system tick timer with. + static s64 GenerateBaseTicks(); + private: // unordered_map stores each element separately as a linked list node so pointers to // elements remain stable regardless of rehashes/resizing. diff --git a/src/core/movie.cpp b/src/core/movie.cpp index 1dba19ef9..c7dd23c90 100644 --- a/src/core/movie.cpp +++ b/src/core/movie.cpp @@ -120,8 +120,9 @@ struct CTMHeader { std::array author; /// Author of the movie u32_le rerecord_count; /// Number of rerecords when making the movie u64_le input_count; /// Number of inputs (button and pad states) when making the movie + s64_le timing_base_ticks; /// The base system tick count to initialize core timing with. - std::array reserved; /// Make heading 256 bytes so it has consistent size + std::array reserved; /// Make heading 256 bytes so it has consistent size }; static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes"); #pragma pack(pop) @@ -158,6 +159,7 @@ void Movie::serialize(Archive& ar, const unsigned int file_version) { ar& recorded_input_; ar& init_time; + ar& base_ticks; if (Archive::is_loading::value) { u64 savestate_movie_id; @@ -453,6 +455,10 @@ u64 Movie::GetOverrideInitTime() const { return init_time; } +s64 Movie::GetOverrideBaseTicks() const { + return base_ticks; +} + Movie::ValidationResult Movie::ValidateHeader(const CTMHeader& header) const { if (header_magic_bytes != header.filetype) { LOG_ERROR(Movie, "Playback file does not have valid header"); @@ -487,6 +493,7 @@ void Movie::SaveMovie() { header.filetype = header_magic_bytes; header.program_id = program_id; header.clock_init_time = init_time; + header.timing_base_ticks = base_ticks; header.id = id; std::memcpy(header.author.data(), record_movie_author.data(), @@ -591,6 +598,7 @@ void Movie::PrepareForPlayback(const std::string& movie_file) { return; init_time = header.value().clock_init_time; + base_ticks = header.value().timing_base_ticks; } void Movie::PrepareForRecording() { @@ -605,6 +613,8 @@ void Movie::PrepareForRecording() { } else { init_time = Settings::values.init_time.GetValue(); } + + base_ticks = Timing::GenerateBaseTicks(); } Movie::ValidationResult Movie::ValidateMovie(const std::string& movie_file) const { @@ -661,6 +671,7 @@ void Movie::Shutdown() { current_byte = 0; current_input = 0; init_time = 0; + base_ticks = -1; id = 0; } diff --git a/src/core/movie.h b/src/core/movie.h index 3e4ff908f..729bc459b 100644 --- a/src/core/movie.h +++ b/src/core/movie.h @@ -74,6 +74,9 @@ public: /// Get the init time that would override the one in the settings u64 GetOverrideInitTime() const; + /// Get the base system ticks value that would override the one generated by core timing + s64 GetOverrideBaseTicks() const; + struct MovieMetadata { u64 program_id; std::string author; @@ -168,7 +171,8 @@ private: std::string record_movie_file; std::string record_movie_author; - u64 init_time; // Clock init time override for RNG consistency + u64 init_time; // Clock init time override for RNG consistency + s64 base_ticks = -1; // Core timing base system ticks override for RNG consistency std::vector recorded_input; std::size_t current_byte = 0;