mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-08-18 08:19:59 +00:00
Merge branch 'AdmiralCurtiss-netplay-events'
This commit is contained in:
commit
fa950acffc
11 changed files with 318 additions and 1 deletions
|
@ -563,6 +563,9 @@ static void EmuThread(Core::System& system, std::unique_ptr<BootParameters> boot
|
|||
HW::Init(system,
|
||||
NetPlay::IsNetPlayRunning() ? &(boot_session_data.GetNetplaySettings()->sram) : nullptr);
|
||||
|
||||
if (NetPlay::IsNetPlayRunning())
|
||||
NetPlay::NetPlay_RegisterEvents();
|
||||
|
||||
Common::ScopeGuard hw_guard{[&system] {
|
||||
// We must set up this flag before executing HW::Shutdown()
|
||||
s_hardware_initialized = false;
|
||||
|
|
|
@ -212,6 +212,9 @@ void CoreTimingManager::DoState(PointerWrap& p)
|
|||
// The stave state has changed the time, so our previous Throttle targets are invalid.
|
||||
// Especially when global_time goes down; So we create a fake throttle update.
|
||||
ResetThrottle(m_globals.global_timer);
|
||||
|
||||
// Throw away pending external events when loading state, they no longer apply.
|
||||
m_external_event_queue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,6 +285,24 @@ void CoreTimingManager::ScheduleEvent(s64 cycles_into_future, EventType* event_t
|
|||
}
|
||||
}
|
||||
|
||||
void CoreTimingManager::ScheduleExternalEvent(u64 timepoint, EventType* event_type, u64 userdata,
|
||||
u64 unique_id)
|
||||
{
|
||||
if (Core::IsCPUThread())
|
||||
{
|
||||
m_external_event_queue.emplace_back(
|
||||
Event{static_cast<s64>(timepoint), unique_id, userdata, event_type});
|
||||
std::push_heap(m_external_event_queue.begin(), m_external_event_queue.end(),
|
||||
std::greater<Event>());
|
||||
}
|
||||
else
|
||||
{
|
||||
std::lock_guard lk(m_ts_write_lock);
|
||||
m_external_pending_queue.Push(
|
||||
Event{static_cast<s64>(timepoint), unique_id, userdata, event_type});
|
||||
}
|
||||
}
|
||||
|
||||
void CoreTimingManager::RemoveEvent(EventType* event_type)
|
||||
{
|
||||
auto itr = std::remove_if(m_event_queue.begin(), m_event_queue.end(),
|
||||
|
@ -322,6 +343,13 @@ void CoreTimingManager::MoveEvents()
|
|||
m_event_queue.emplace_back(std::move(ev));
|
||||
std::push_heap(m_event_queue.begin(), m_event_queue.end(), std::greater<Event>());
|
||||
}
|
||||
|
||||
for (Event ev; m_external_pending_queue.Pop(ev);)
|
||||
{
|
||||
m_external_event_queue.emplace_back(std::move(ev));
|
||||
std::push_heap(m_external_event_queue.begin(), m_external_event_queue.end(),
|
||||
std::greater<Event>());
|
||||
}
|
||||
}
|
||||
|
||||
void CoreTimingManager::Advance()
|
||||
|
@ -351,6 +379,16 @@ void CoreTimingManager::Advance()
|
|||
evt.type->callback(m_system, evt.userdata, m_globals.global_timer - evt.time);
|
||||
}
|
||||
|
||||
while (!m_external_event_queue.empty() &&
|
||||
m_external_event_queue.front().time <= m_globals.global_timer)
|
||||
{
|
||||
Event evt = std::move(m_external_event_queue.front());
|
||||
std::pop_heap(m_external_event_queue.begin(), m_external_event_queue.end(),
|
||||
std::greater<Event>());
|
||||
m_external_event_queue.pop_back();
|
||||
evt.type->callback(m_system, evt.userdata, m_globals.global_timer - evt.time);
|
||||
}
|
||||
|
||||
m_is_global_timer_sane = false;
|
||||
|
||||
// Still events left (scheduled in the future)
|
||||
|
|
|
@ -104,6 +104,13 @@ public:
|
|||
void ScheduleEvent(s64 cycles_into_future, EventType* event_type, u64 userdata = 0,
|
||||
FromThread from = FromThread::CPU);
|
||||
|
||||
// Similar to ScheduleEvent, but enqueues an event in the secondary event queue that does not
|
||||
// affect timing logic and isn't savestated. Used primarily for handling events in a deterministic
|
||||
// manner during netplay. Note that 'timepoint' is absolute (instead of ScheduleEvent's relative)
|
||||
// and that the user should try to provide a 'unique_id' for consistent event ordering if they
|
||||
// happen to be at the same timepoint.
|
||||
void ScheduleExternalEvent(u64 timepoint, EventType* event_type, u64 userdata, u64 unique_id);
|
||||
|
||||
// We only permit one event of each type in the queue at a time.
|
||||
void RemoveEvent(EventType* event_type);
|
||||
void RemoveAllEvents(EventType* event_type);
|
||||
|
@ -172,6 +179,15 @@ private:
|
|||
std::mutex m_ts_write_lock;
|
||||
Common::SPSCQueue<Event, false> m_ts_queue;
|
||||
|
||||
// A second event queue that is used for timing 'external' events that are sent by the emulator
|
||||
// rather than by the emulated game. Netplay uses these for syncing non-controller-button events
|
||||
// sent by a single client, such as a press of the physical Reset button on the console, or an
|
||||
// unplugging of a controller. These don't affect timing logic (and thus will not run at a precise
|
||||
// time, but instead at the first opportunity given by the regular events) and do not get written
|
||||
// to savestates.
|
||||
std::vector<Event> m_external_event_queue;
|
||||
Common::SPSCQueue<Event, false> m_external_pending_queue;
|
||||
|
||||
float m_last_oc_factor = 0.0f;
|
||||
|
||||
s64 m_idled_cycles = 0;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#include "Core/HW/SystemTimers.h"
|
||||
#include "Core/IOS/IOS.h"
|
||||
#include "Core/IOS/STM/STM.h"
|
||||
#include "Core/NetPlayClient.h"
|
||||
#include "Core/PowerPC/PowerPC.h"
|
||||
#include "Core/System.h"
|
||||
#include "VideoCommon/AsyncRequests.h"
|
||||
|
@ -266,6 +267,14 @@ void ProcessorInterfaceManager::ResetButton_Tap()
|
|||
m_event_type_toggle_reset_button, false, CoreTiming::FromThread::ANY);
|
||||
}
|
||||
|
||||
void ProcessorInterfaceManager::ResetButton_Tap_FromUser()
|
||||
{
|
||||
if (NetPlay::IsNetPlayRunning())
|
||||
NetPlay::ScheduleResetButtonTap();
|
||||
else
|
||||
ResetButton_Tap();
|
||||
}
|
||||
|
||||
void ProcessorInterfaceManager::PowerButton_Tap()
|
||||
{
|
||||
if (!Core::IsRunning())
|
||||
|
|
|
@ -82,6 +82,7 @@ public:
|
|||
|
||||
// Thread-safe func which sets and clears reset button state automagically
|
||||
void ResetButton_Tap();
|
||||
void ResetButton_Tap_FromUser();
|
||||
void PowerButton_Tap();
|
||||
|
||||
u32 m_interrupt_cause = 0;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#include "Core/NetPlayClient.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
@ -49,6 +51,7 @@
|
|||
#include "Core/HW/GBAPad.h"
|
||||
#include "Core/HW/GCMemcard/GCMemcard.h"
|
||||
#include "Core/HW/GCPad.h"
|
||||
#include "Core/HW/ProcessorInterface.h"
|
||||
#include "Core/HW/SI/SI.h"
|
||||
#include "Core/HW/SI/SI_Device.h"
|
||||
#include "Core/HW/SI/SI_DeviceGCController.h"
|
||||
|
@ -84,6 +87,19 @@ static std::mutex crit_netplay_client;
|
|||
static NetPlayClient* netplay_client = nullptr;
|
||||
static bool s_si_poll_batching = false;
|
||||
|
||||
struct ExternalEventSyncData
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::map<PlayerId, u64> map;
|
||||
ExternalEventID event_id = ExternalEventID::None;
|
||||
};
|
||||
|
||||
static std::mutex s_event_sync_mutex;
|
||||
static std::map<u64, ExternalEventSyncData> s_event_sync_map;
|
||||
static CoreTiming::EventType* event_type_sync_time_for_ext_event = nullptr;
|
||||
static CoreTiming::EventType* event_type_ext_event = nullptr;
|
||||
|
||||
// called from ---GUI--- thread
|
||||
NetPlayClient::~NetPlayClient()
|
||||
{
|
||||
|
@ -436,6 +452,14 @@ void NetPlayClient::OnData(sf::Packet& packet)
|
|||
OnPowerButton();
|
||||
break;
|
||||
|
||||
case MessageID::ScheduleExternalEvent:
|
||||
OnScheduleExternalEvent(packet);
|
||||
break;
|
||||
|
||||
case MessageID::SyncTimepointForExternalEvent:
|
||||
OnSyncTimepointForExternalEvent(packet);
|
||||
break;
|
||||
|
||||
case MessageID::Ping:
|
||||
OnPing(packet);
|
||||
break;
|
||||
|
@ -952,6 +976,61 @@ void NetPlayClient::OnPowerButton()
|
|||
m_dialog->OnMsgPowerButton();
|
||||
}
|
||||
|
||||
void NetPlayClient::OnScheduleExternalEvent(sf::Packet& packet)
|
||||
{
|
||||
// A user (could be ourselves, too) has requested the scheduling of an external event.
|
||||
|
||||
ExternalEventID eeid;
|
||||
packet >> eeid;
|
||||
sf::Uint64 uid;
|
||||
packet >> uid;
|
||||
sf::Uint64 timepoint;
|
||||
packet >> timepoint;
|
||||
|
||||
DEBUG_LOG_FMT(NETPLAY, "OnScheduleExternalEvent(): eeid {}, timepoint {}, uid {}", u8(eeid),
|
||||
timepoint, uid);
|
||||
|
||||
// The 'uid' is unique per netplay session and is both used to provide an absolute ordering in
|
||||
// case two events get assigned to the same timepoint, and also to keep track (in
|
||||
// s_event_sync_map) of what event-related packet belongs to what event if multiple are in flight
|
||||
// at the same time.
|
||||
|
||||
{
|
||||
std::lock_guard lk(s_event_sync_mutex);
|
||||
s_event_sync_map[static_cast<u64>(uid)].event_id = eeid;
|
||||
}
|
||||
|
||||
auto& core_timing = Core::System::GetInstance().GetCoreTiming();
|
||||
core_timing.ScheduleExternalEvent(static_cast<u64>(timepoint), event_type_sync_time_for_ext_event,
|
||||
static_cast<u64>(uid), static_cast<u64>(uid));
|
||||
}
|
||||
|
||||
void NetPlayClient::OnSyncTimepointForExternalEvent(sf::Packet& packet)
|
||||
{
|
||||
// A user (not ourselves) is currently waiting to execute an external event and has sent us their
|
||||
// current timepoint to determine what the earliest timepoint the event can execute is.
|
||||
|
||||
PlayerId pid;
|
||||
packet >> pid;
|
||||
sf::Uint64 uid;
|
||||
packet >> uid;
|
||||
sf::Uint64 timepoint;
|
||||
packet >> timepoint;
|
||||
|
||||
DEBUG_LOG_FMT(NETPLAY, "OnSyncTimepointForExternalEvent(): player id {}, timepoint {}, uid {}",
|
||||
u8(pid), timepoint, uid);
|
||||
|
||||
{
|
||||
std::lock_guard lk(s_event_sync_mutex);
|
||||
auto& event_data = s_event_sync_map[static_cast<u64>(uid)];
|
||||
{
|
||||
std::lock_guard lk2(event_data.mutex);
|
||||
event_data.map[pid] = static_cast<u64>(timepoint);
|
||||
}
|
||||
event_data.cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void NetPlayClient::OnPing(sf::Packet& packet)
|
||||
{
|
||||
u32 ping_key = 0;
|
||||
|
@ -2361,6 +2440,14 @@ void NetPlayClient::RequestStopGame()
|
|||
SendStopGamePacket();
|
||||
}
|
||||
|
||||
void NetPlayClient::ScheduleExternalEvent(ExternalEventID id)
|
||||
{
|
||||
sf::Packet packet;
|
||||
packet << MessageID::ScheduleExternalEvent;
|
||||
packet << id;
|
||||
SendAsync(std::move(packet));
|
||||
}
|
||||
|
||||
void NetPlayClient::SendPowerButtonEvent()
|
||||
{
|
||||
sf::Packet packet;
|
||||
|
@ -2687,6 +2774,12 @@ void SetSIPollBatching(bool state)
|
|||
s_si_poll_batching = state;
|
||||
}
|
||||
|
||||
void ScheduleResetButtonTap()
|
||||
{
|
||||
ASSERT(IsNetPlayRunning());
|
||||
netplay_client->ScheduleExternalEvent(ExternalEventID::ResetButton);
|
||||
}
|
||||
|
||||
void SendPowerButtonEvent()
|
||||
{
|
||||
ASSERT(IsNetPlayRunning());
|
||||
|
@ -2767,6 +2860,103 @@ void NetPlay_Disable()
|
|||
std::lock_guard lk(crit_netplay_client);
|
||||
netplay_client = nullptr;
|
||||
}
|
||||
|
||||
static void NetPlayExtEvent(Core::System& system, u64 userdata, s64 cyclesLate)
|
||||
{
|
||||
ExternalEventID eeid = static_cast<ExternalEventID>(userdata);
|
||||
switch (eeid)
|
||||
{
|
||||
case ExternalEventID::ResetButton:
|
||||
system.GetProcessorInterface().ResetButton_Tap();
|
||||
break;
|
||||
default:
|
||||
WARN_LOG_FMT(NETPLAY, "NetPlayExtEvent: Invalid event type. ({})", userdata);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void NetPlayClient::SendTimepointForNetPlayEvent(u64 timepoint, u64 uid)
|
||||
{
|
||||
sf::Packet packet;
|
||||
packet << MessageID::SyncTimepointForExternalEvent;
|
||||
packet << sf::Uint64(uid);
|
||||
packet << sf::Uint64(timepoint);
|
||||
SendAsync(std::move(packet));
|
||||
}
|
||||
|
||||
static void NetPlaySyncEvent(Core::System& system, u64 userdata, s64 cyclesLate)
|
||||
{
|
||||
// An external event should be executed, but we're not sure of a valid timepoint for it yet. To
|
||||
// figure this out, we now send our current timepoint to all other players (as they do the same),
|
||||
// then once we know all timepoints we execute the event after the last timepoint of any player;
|
||||
// this ensures that every player can run it at the same timepoint and thus stay deterministic
|
||||
// with eachother.
|
||||
|
||||
auto& core_timing = system.GetCoreTiming();
|
||||
const u64 timepoint = core_timing.GetTicks();
|
||||
ExternalEventSyncData* ptr_event_data;
|
||||
{
|
||||
std::lock_guard lk(s_event_sync_mutex);
|
||||
ptr_event_data = &s_event_sync_map[userdata];
|
||||
}
|
||||
auto& event_data = *ptr_event_data;
|
||||
|
||||
{
|
||||
std::lock_guard lk(event_data.mutex);
|
||||
event_data.map[netplay_client->GetLocalPlayerId()] = timepoint;
|
||||
}
|
||||
|
||||
netplay_client->SendTimepointForNetPlayEvent(timepoint, userdata);
|
||||
const size_t num_players = netplay_client->GetPlayers().size();
|
||||
|
||||
bool do_schedule = false;
|
||||
u64 latest_timepoint = 0;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(event_data.mutex);
|
||||
if (event_data.map.size() != num_players)
|
||||
{
|
||||
DEBUG_LOG_FMT(NETPLAY, "NetPlaySyncEvent({}): Waiting for timepoints.", userdata);
|
||||
event_data.cv.wait_for(lock, std::chrono::seconds(5), [&event_data, num_players] {
|
||||
return event_data.map.size() == num_players;
|
||||
});
|
||||
}
|
||||
if (event_data.map.size() != num_players)
|
||||
{
|
||||
WARN_LOG_FMT(NETPLAY, "NetPlaySyncEvent({}): Timed out waiting for timepoints.", userdata);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& it : event_data.map)
|
||||
{
|
||||
DEBUG_LOG_FMT(NETPLAY, "NetPlaySyncEvent({}): Timepoint {} from player {}.", userdata,
|
||||
it.second, u8(it.first));
|
||||
latest_timepoint = std::max(it.second, latest_timepoint);
|
||||
}
|
||||
do_schedule = true;
|
||||
}
|
||||
}
|
||||
|
||||
const ExternalEventID eeid = event_data.event_id;
|
||||
|
||||
{
|
||||
std::lock_guard lk(s_event_sync_mutex);
|
||||
s_event_sync_map.erase(userdata);
|
||||
}
|
||||
|
||||
if (do_schedule)
|
||||
{
|
||||
core_timing.ScheduleExternalEvent(latest_timepoint + 1, event_type_ext_event,
|
||||
static_cast<u64>(eeid), userdata);
|
||||
}
|
||||
}
|
||||
|
||||
void NetPlay_RegisterEvents()
|
||||
{
|
||||
auto& core_timing = Core::System::GetInstance().GetCoreTiming();
|
||||
event_type_ext_event = core_timing.RegisterEvent("NetPlayExtEvent", NetPlayExtEvent);
|
||||
event_type_sync_time_for_ext_event =
|
||||
core_timing.RegisterEvent("NetPlaySyncEvent", NetPlaySyncEvent);
|
||||
}
|
||||
} // namespace NetPlay
|
||||
|
||||
// stuff hacked into dolphin
|
||||
|
|
|
@ -130,6 +130,8 @@ public:
|
|||
bool ChangeGame(const std::string& game);
|
||||
void SendChatMessage(const std::string& msg);
|
||||
void RequestStopGame();
|
||||
void SendTimepointForNetPlayEvent(u64 timepoint, u64 uid);
|
||||
void ScheduleExternalEvent(ExternalEventID id);
|
||||
void SendPowerButtonEvent();
|
||||
void SendActiveGeckoCodes();
|
||||
void GetActiveGeckoCodes();
|
||||
|
@ -300,6 +302,8 @@ private:
|
|||
void OnStartGame(sf::Packet& packet);
|
||||
void OnStopGame(sf::Packet& packet);
|
||||
void OnPowerButton();
|
||||
void OnScheduleExternalEvent(sf::Packet& packet);
|
||||
void OnSyncTimepointForExternalEvent(sf::Packet& packet);
|
||||
void OnPing(sf::Packet& packet);
|
||||
void OnPlayerPingData(sf::Packet& packet);
|
||||
void OnDesyncDetected(sf::Packet& packet);
|
||||
|
@ -359,4 +363,6 @@ void NetPlay_Enable(NetPlayClient* const np);
|
|||
void NetPlay_Disable();
|
||||
bool NetPlay_GetWiimoteData(const std::span<NetPlayClient::WiimoteDataBatchEntry>& entries);
|
||||
unsigned int NetPlay_GetLocalWiimoteForSlot(unsigned int slot);
|
||||
|
||||
void NetPlay_RegisterEvents();
|
||||
} // namespace NetPlay
|
||||
|
|
|
@ -169,6 +169,9 @@ enum class MessageID : u8
|
|||
HostInputAuthority = 0xA6,
|
||||
PowerButton = 0xA7,
|
||||
|
||||
ScheduleExternalEvent = 0xA8,
|
||||
SyncTimepointForExternalEvent = 0xA9,
|
||||
|
||||
TimeBase = 0xB0,
|
||||
DesyncDetected = 0xB1,
|
||||
|
||||
|
@ -220,6 +223,12 @@ enum class SyncCodeID : u8
|
|||
Failure = 6,
|
||||
};
|
||||
|
||||
enum class ExternalEventID : u8
|
||||
{
|
||||
None = 0,
|
||||
ResetButton = 1,
|
||||
};
|
||||
|
||||
constexpr u32 MAX_NAME_LENGTH = 30;
|
||||
constexpr size_t CHUNKED_DATA_UNIT_SIZE = 16384;
|
||||
constexpr u32 MAX_ENET_MTU = 1392; // see https://github.com/lsalzman/enet/issues/132
|
||||
|
@ -257,6 +266,7 @@ std::string GetPlayerMappingString(PlayerId pid, const PadMappingArray& pad_map,
|
|||
const PadMappingArray& wiimote_map);
|
||||
bool IsNetPlayRunning();
|
||||
void SetSIPollBatching(bool state);
|
||||
void ScheduleResetButtonTap();
|
||||
void SendPowerButtonEvent();
|
||||
std::string GetGBASavePath(int pad_num);
|
||||
PadDetails GetPadDetails(int pad_num);
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
#include "Core/NetPlayClient.h" //for NetPlayUI
|
||||
#include "Core/NetPlayCommon.h"
|
||||
#include "Core/SyncIdentifier.h"
|
||||
#include "Core/System.h"
|
||||
|
||||
#include "DiscIO/Enums.h"
|
||||
#include "DiscIO/RiivolutionPatcher.h"
|
||||
|
@ -1243,6 +1244,47 @@ unsigned int NetPlayServer::OnData(sf::Packet& packet, Client& player)
|
|||
}
|
||||
break;
|
||||
|
||||
case MessageID::ScheduleExternalEvent:
|
||||
{
|
||||
ExternalEventID eeid;
|
||||
packet >> eeid;
|
||||
|
||||
sf::Packet spac;
|
||||
spac << MessageID::ScheduleExternalEvent;
|
||||
spac << eeid;
|
||||
const u64 uid = m_external_event_uid_counter++;
|
||||
spac << sf::Uint64(uid);
|
||||
auto& core_timing = Core::System::GetInstance().GetCoreTiming();
|
||||
|
||||
// We schedule the event in the future so that it's likely it will reach all users in time.
|
||||
// There's a syncing logic in place to ensure the event executes at the same timepoint
|
||||
// everywhere even if this packet is a bit too late, but the sync can get tripped up (and
|
||||
// subsequently time out) if there's too large of a time distance between multiple players
|
||||
// executing the sync function, because one of them may be waiting for controller input while
|
||||
// another is waiting for the event timepoint sync.
|
||||
const u64 target_timepoint =
|
||||
static_cast<u64>(core_timing.GetGlobals().global_timer) + SystemTimers::GetTicksPerSecond();
|
||||
spac << sf::Uint64(target_timepoint);
|
||||
SendToClients(spac);
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageID::SyncTimepointForExternalEvent:
|
||||
{
|
||||
sf::Uint64 uid;
|
||||
packet >> uid;
|
||||
sf::Uint64 timepoint;
|
||||
packet >> timepoint;
|
||||
|
||||
sf::Packet spac;
|
||||
spac << MessageID::SyncTimepointForExternalEvent;
|
||||
spac << player.pid;
|
||||
spac << uid;
|
||||
spac << timepoint;
|
||||
SendToClients(spac, player.pid);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
PanicAlertFmtT("Unknown message with id:{0} received from player:{1} Kicking player!",
|
||||
static_cast<u8>(mid), player.pid);
|
||||
|
|
|
@ -214,5 +214,7 @@ private:
|
|||
Common::TraversalClient* m_traversal_client = nullptr;
|
||||
NetPlayUI* m_dialog = nullptr;
|
||||
NetPlayIndex m_index;
|
||||
|
||||
u64 m_external_event_uid_counter = 0;
|
||||
};
|
||||
} // namespace NetPlay
|
||||
|
|
|
@ -1122,7 +1122,7 @@ void MainWindow::Reset()
|
|||
auto& movie = system.GetMovie();
|
||||
if (movie.IsRecordingInput())
|
||||
movie.SetReset(true);
|
||||
system.GetProcessorInterface().ResetButton_Tap();
|
||||
system.GetProcessorInterface().ResetButton_Tap_FromUser();
|
||||
}
|
||||
|
||||
void MainWindow::FrameAdvance()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue