Merge pull request #373 from wheremyfoodat/scheduler

Add basic event scheduler functionality
This commit is contained in:
wheremyfoodat 2024-01-22 13:49:04 +00:00 committed by GitHub
commit ebf705d10d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 286 additions and 112 deletions

View file

@ -244,7 +244,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp
include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp
include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp
include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp
include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp
) )
cmrc_add_resource_library( cmrc_add_resource_library(

View file

@ -9,15 +9,18 @@
#include "helpers.hpp" #include "helpers.hpp"
#include "kernel.hpp" #include "kernel.hpp"
#include "memory.hpp" #include "memory.hpp"
#include "scheduler.hpp"
class Emulator;
class CPU; class CPU;
class MyEnvironment final : public Dynarmic::A32::UserCallbacks { class MyEnvironment final : public Dynarmic::A32::UserCallbacks {
public: public:
u64 ticksLeft = 0; u64 ticksLeft = 0;
u64 totalTicks = 0; u64 totalTicks = 0;
Memory& mem; Memory& mem;
Kernel& kernel; Kernel& kernel;
Scheduler& scheduler;
u64 getCyclesForInstruction(bool isThumb, u32 instruction); u64 getCyclesForInstruction(bool isThumb, u32 instruction);
@ -76,54 +79,56 @@ public:
std::terminate(); std::terminate();
} }
void CallSVC(u32 swi) override { void CallSVC(u32 swi) override {
kernel.serviceSVC(swi); kernel.serviceSVC(swi);
} }
void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override {
switch (exception) { switch (exception) {
case Dynarmic::A32::Exception::UnpredictableInstruction: case Dynarmic::A32::Exception::UnpredictableInstruction:
Helpers::panic("Unpredictable instruction at pc = %08X", pc); Helpers::panic("Unpredictable instruction at pc = %08X", pc);
break; break;
default: Helpers::panic("Fired exception oops"); default: Helpers::panic("Fired exception oops");
} }
} }
void AddTicks(u64 ticks) override { void AddTicks(u64 ticks) override {
totalTicks += ticks; scheduler.currentTimestamp += ticks;
if (ticks > ticksLeft) { if (ticks > ticksLeft) {
ticksLeft = 0; ticksLeft = 0;
return; return;
} }
ticksLeft -= ticks; ticksLeft -= ticks;
} }
u64 GetTicksRemaining() override { u64 GetTicksRemaining() override {
return ticksLeft; return ticksLeft;
} }
u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override { u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override {
return getCyclesForInstruction(isThumb, instruction); return getCyclesForInstruction(isThumb, instruction);
} }
MyEnvironment(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} MyEnvironment(Memory& mem, Kernel& kernel, Scheduler& scheduler) : mem(mem), kernel(kernel), scheduler(scheduler) {}
}; };
class CPU { class CPU {
std::unique_ptr<Dynarmic::A32::Jit> jit; std::unique_ptr<Dynarmic::A32::Jit> jit;
std::shared_ptr<CP15> cp15; std::shared_ptr<CP15> cp15;
// Make exclusive monitor with only 1 CPU core // Make exclusive monitor with only 1 CPU core
Dynarmic::ExclusiveMonitor exclusiveMonitor{1}; Dynarmic::ExclusiveMonitor exclusiveMonitor{1};
MyEnvironment env; MyEnvironment env;
Memory& mem; Memory& mem;
Scheduler& scheduler;
Emulator& emu;
public: public:
static constexpr u64 ticksPerSec = 268111856; static constexpr u64 ticksPerSec = Scheduler::arm11Clock;
CPU(Memory& mem, Kernel& kernel); CPU(Memory& mem, Kernel& kernel, Emulator& emu);
void reset(); void reset();
void setReg(int index, u32 value) { void setReg(int index, u32 value) {
@ -162,29 +167,18 @@ public:
} }
u64 getTicks() { u64 getTicks() {
return env.totalTicks; return scheduler.currentTimestamp;
} }
// Get reference to tick count. Memory needs access to this // Get reference to tick count. Memory needs access to this
u64& getTicksRef() { u64& getTicksRef() {
return env.totalTicks; return scheduler.currentTimestamp;
} }
void clearCache() { jit->ClearCache(); } Scheduler& getScheduler() {
return scheduler;
void runFrame() {
env.ticksLeft = ticksPerSec / 60;
execute:
const auto exitReason = jit->Run();
if (static_cast<u32>(exitReason) != 0) [[unlikely]] {
// Cache invalidation needs to exit the JIT so it returns a CacheInvalidation HaltReason. In our case, we just go back to executing
// The goto might be terrible but it does guarantee that this does not recursively call run and crash, instead getting optimized to a jump
if (Dynarmic::Has(exitReason, Dynarmic::HaltReason::CacheInvalidation)) {
goto execute;
} else {
Helpers::panic("Exit reason: %d\nPC: %08X", static_cast<u32>(exitReason), getReg(15));
}
}
} }
void clearCache() { jit->ClearCache(); }
void runFrame();
}; };

View file

@ -15,6 +15,7 @@
#include "io_file.hpp" #include "io_file.hpp"
#include "lua_manager.hpp" #include "lua_manager.hpp"
#include "memory.hpp" #include "memory.hpp"
#include "scheduler.hpp"
#ifdef PANDA3DS_ENABLE_HTTP_SERVER #ifdef PANDA3DS_ENABLE_HTTP_SERVER
#include "http_server.hpp" #include "http_server.hpp"
@ -42,6 +43,7 @@ class Emulator {
Kernel kernel; Kernel kernel;
Crypto::AESEngine aesEngine; Crypto::AESEngine aesEngine;
Cheats cheats; Cheats cheats;
Scheduler scheduler;
// Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard
// This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state
@ -85,6 +87,8 @@ class Emulator {
// change ROMs. If Reload is selected, the emulator will reload its selected ROM. This is useful for eg a "reset" button that keeps the current // change ROMs. If Reload is selected, the emulator will reload its selected ROM. This is useful for eg a "reset" button that keeps the current
// ROM and just resets the emu // ROM and just resets the emu
enum class ReloadOption { NoReload, Reload }; enum class ReloadOption { NoReload, Reload };
// Used in CPU::runFrame
bool frameDone = false;
Emulator(); Emulator();
~Emulator(); ~Emulator();
@ -94,6 +98,8 @@ class Emulator {
void reset(ReloadOption reload); void reset(ReloadOption reload);
void run(void* frontend = nullptr); void run(void* frontend = nullptr);
void runFrame(); void runFrame();
// Poll the scheduler for events
void pollScheduler();
void resume(); // Resume the emulator void resume(); // Resume the emulator
void pause(); // Pause the emulator void pause(); // Pause the emulator
@ -121,6 +127,7 @@ class Emulator {
Cheats& getCheats() { return cheats; } Cheats& getCheats() { return cheats; }
ServiceManager& getServiceManager() { return kernel.getServiceManager(); } ServiceManager& getServiceManager() { return kernel.getServiceManager(); }
LuaManager& getLua() { return lua; } LuaManager& getLua() { return lua; }
Scheduler& getScheduler() { return scheduler; }
RendererType getRendererType() const { return config.rendererType; } RendererType getRendererType() const { return config.rendererType; }
Renderer* getRenderer() { return gpu.getRenderer(); } Renderer* getRenderer() { return gpu.getRenderer(); }

View file

@ -70,6 +70,7 @@ public:
Handle makeMutex(bool locked = false); // Needs to be public to be accessible to the APT/DSP services Handle makeMutex(bool locked = false); // Needs to be public to be accessible to the APT/DSP services
Handle makeSemaphore(u32 initialCount, u32 maximumCount); // Needs to be public to be accessible to the service manager port Handle makeSemaphore(u32 initialCount, u32 maximumCount); // Needs to be public to be accessible to the service manager port
Handle makeTimer(ResetType resetType); Handle makeTimer(ResetType resetType);
void pollTimers();
// Signals an event, returns true on success or false if the event does not exist // Signals an event, returns true on success or false if the event does not exist
bool signalEvent(Handle e); bool signalEvent(Handle e);
@ -94,7 +95,6 @@ public:
void releaseMutex(Mutex* moo); void releaseMutex(Mutex* moo);
void cancelTimer(Timer* timer); void cancelTimer(Timer* timer);
void signalTimer(Handle timerHandle, Timer* timer); void signalTimer(Handle timerHandle, Timer* timer);
void updateTimer(Handle timerHandle, Timer* timer);
// Wake up the thread with the highest priority out of all threads in the waitlist // Wake up the thread with the highest priority out of all threads in the waitlist
// Returns the index of the woken up thread // Returns the index of the woken up thread

View file

@ -177,13 +177,12 @@ struct Timer {
u64 waitlist; // Refer to the getWaitlist function below for documentation u64 waitlist; // Refer to the getWaitlist function below for documentation
ResetType resetType = ResetType::OneShot; ResetType resetType = ResetType::OneShot;
u64 startTick; // CPU tick the timer started u64 fireTick; // CPU tick the timer will be fired
u64 currentDelay; // Number of ns until the timer fires next time
u64 interval; // Number of ns until the timer fires for the second and future times u64 interval; // Number of ns until the timer fires for the second and future times
bool fired; // Has this timer been signalled? bool fired; // Has this timer been signalled?
bool running; // Is this timer running or stopped? bool running; // Is this timer running or stopped?
Timer(ResetType type) : resetType(type), startTick(0), currentDelay(0), interval(0), waitlist(0), fired(false), running(false) {} Timer(ResetType type) : resetType(type), fireTick(0), interval(0), waitlist(0), fired(false), running(false) {}
}; };
struct MemoryBlock { struct MemoryBlock {

87
include/scheduler.hpp Normal file
View file

@ -0,0 +1,87 @@
#pragma once
#include <boost/container/flat_map.hpp>
#include <boost/container/static_vector.hpp>
#include <limits>
#include "helpers.hpp"
#include "logger.hpp"
struct Scheduler {
enum class EventType {
VBlank = 0, // End of frame event
UpdateTimers = 1, // Update kernel timer objects
Panic = 2, // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX)
TotalNumberOfEvents // How many event types do we have in total?
};
static constexpr usize totalNumberOfEvents = static_cast<usize>(EventType::TotalNumberOfEvents);
static constexpr u64 arm11Clock = 268111856;
template <typename Key, typename Val, usize size>
using EventMap = boost::container::flat_multimap<Key, Val, std::less<Key>, boost::container::static_vector<std::pair<Key, Val>, size>>;
EventMap<u64, EventType, totalNumberOfEvents> events;
u64 currentTimestamp = 0;
u64 nextTimestamp = 0;
// Set nextTimestamp to the timestamp of the next event
void updateNextTimestamp() { nextTimestamp = events.cbegin()->first; }
void addEvent(EventType type, u64 timestamp) {
events.emplace(timestamp, type);
updateNextTimestamp();
}
void removeEvent(EventType type) {
for (auto it = events.begin(); it != events.end(); it++) {
// Find first event of type "type" and remove it.
// Our scheduler shouldn't have duplicate events, so it's safe to exit when an event is found
if (it->second == type) {
events.erase(it);
updateNextTimestamp();
break;
}
}
};
void reset() {
currentTimestamp = 0;
// Clear any pending events
events.clear();
// Add a dummy event to always keep the scheduler non-empty
addEvent(EventType::Panic, std::numeric_limits<u64>::max());
}
private:
static constexpr u64 MAX_VALUE_TO_MULTIPLY = std::numeric_limits<s64>::max() / arm11Clock;
public:
// Function for converting time units to cycles for various kernel functions
// Thank you Citra
static constexpr s64 nsToCycles(float ns) { return s64(arm11Clock * (0.000000001f) * ns); }
static constexpr s64 nsToCycles(int ns) { return arm11Clock * s64(ns) / 1000000000; }
static constexpr s64 nsToCycles(s64 ns) {
if (ns / 1000000000 > static_cast<s64>(MAX_VALUE_TO_MULTIPLY)) {
return std::numeric_limits<s64>::max();
}
if (ns > static_cast<s64>(MAX_VALUE_TO_MULTIPLY)) {
return arm11Clock * (ns / 1000000000);
}
return (arm11Clock * ns) / 1000000000;
}
static constexpr s64 nsToCycles(u64 ns) {
if (ns / 1000000000 > MAX_VALUE_TO_MULTIPLY) {
return std::numeric_limits<s64>::max();
}
if (ns > MAX_VALUE_TO_MULTIPLY) {
return arm11Clock * (s64(ns) / 1000000000);
}
return (arm11Clock * s64(ns)) / 1000000000;
}
};

View file

@ -1,32 +1,59 @@
#ifdef CPU_DYNARMIC #ifdef CPU_DYNARMIC
#include "cpu_dynarmic.hpp" #include "cpu_dynarmic.hpp"
#include "arm_defs.hpp" #include "arm_defs.hpp"
#include "emulator.hpp"
CPU::CPU(Memory& mem, Kernel& kernel) : mem(mem), env(mem, kernel) { CPU::CPU(Memory& mem, Kernel& kernel, Emulator& emu) : mem(mem), emu(emu), scheduler(emu.getScheduler()), env(mem, kernel, emu.getScheduler()) {
cp15 = std::make_shared<CP15>(); cp15 = std::make_shared<CP15>();
Dynarmic::A32::UserConfig config; Dynarmic::A32::UserConfig config;
config.arch_version = Dynarmic::A32::ArchVersion::v6K; config.arch_version = Dynarmic::A32::ArchVersion::v6K;
config.callbacks = &env; config.callbacks = &env;
config.coprocessors[15] = cp15; config.coprocessors[15] = cp15;
config.define_unpredictable_behaviour = true; config.define_unpredictable_behaviour = true;
config.global_monitor = &exclusiveMonitor; config.global_monitor = &exclusiveMonitor;
config.processor_id = 0; config.processor_id = 0;
jit = std::make_unique<Dynarmic::A32::Jit>(config); jit = std::make_unique<Dynarmic::A32::Jit>(config);
} }
void CPU::reset() { void CPU::reset() {
setCPSR(CPSR::UserMode); setCPSR(CPSR::UserMode);
setFPSCR(FPSCR::MainThreadDefault); setFPSCR(FPSCR::MainThreadDefault);
env.totalTicks = 0; env.totalTicks = 0;
cp15->reset(); cp15->reset();
cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage
jit->Reset(); jit->Reset();
jit->ClearCache(); jit->ClearCache();
jit->Regs().fill(0); jit->Regs().fill(0);
jit->ExtRegs().fill(0); jit->ExtRegs().fill(0);
} }
#endif // CPU_DYNARMIC void CPU::runFrame() {
emu.frameDone = false;
while (!emu.frameDone) {
// Run CPU until the next scheduler event
env.ticksLeft = scheduler.nextTimestamp - scheduler.currentTimestamp;
execute:
const auto exitReason = jit->Run();
// Handle any scheduler events that need handling.
emu.pollScheduler();
if (static_cast<u32>(exitReason) != 0) [[unlikely]] {
// Cache invalidation needs to exit the JIT so it returns a CacheInvalidation HaltReason. In our case, we just go back to executing
// The goto might be terrible but it does guarantee that this does not recursively call run and crash, instead getting optimized to a jump
if (Dynarmic::Has(exitReason, Dynarmic::HaltReason::CacheInvalidation)) {
goto execute;
} else {
Helpers::panic("Exit reason: %d\nPC: %08X", static_cast<u32>(exitReason), getReg(15));
}
}
}
}
#endif // CPU_DYNARMIC

View file

@ -1,5 +1,8 @@
#include "kernel.hpp" #include <limits>
#include "cpu.hpp" #include "cpu.hpp"
#include "kernel.hpp"
#include "scheduler.hpp"
Handle Kernel::makeTimer(ResetType type) { Handle Kernel::makeTimer(ResetType type) {
Handle ret = makeObject(KernelObjectType::Timer); Handle ret = makeObject(KernelObjectType::Timer);
@ -13,27 +16,44 @@ Handle Kernel::makeTimer(ResetType type) {
return ret; return ret;
} }
void Kernel::updateTimer(Handle handle, Timer* timer) { void Kernel::pollTimers() {
if (timer->running) { u64 currentTick = cpu.getTicks();
const u64 currentTicks = cpu.getTicks();
u64 elapsedTicks = currentTicks - timer->startTick;
constexpr double ticksPerSec = double(CPU::ticksPerSec); // Find the next timestamp we'll poll KTimers on. To do this, we find the minimum tick one of our timers will fire
constexpr double nsPerTick = ticksPerSec / 1000000000.0; u64 nextTimestamp = std::numeric_limits<u64>::max();
const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick); // Do we have any active timers anymore? If not, then we won't need to schedule a new timer poll event
bool haveActiveTimers = false;
// Timer has fired for (auto handle : timerHandles) {
if (elapsedNs >= timer->currentDelay) { KernelObject* object = getObject(handle, KernelObjectType::Timer);
timer->startTick = currentTicks; if (object != nullptr) {
timer->currentDelay = timer->interval; Timer* timer = object->getData<Timer>();
signalTimer(handle, timer);
if (timer->running) {
// If timer has fired, signal it and set the tick it will next time
if (currentTick >= timer->fireTick) {
signalTimer(handle, timer);
}
// Update our next timer fire timestamp and mark that we should schedule a new event to poll timers
// We recheck timer->running because signalling a timer stops it if interval == 0
if (timer->running) {
nextTimestamp = std::min<u64>(nextTimestamp, timer->fireTick);
haveActiveTimers = true;
}
}
} }
} }
// If we still have active timers, schedule next poll event
if (haveActiveTimers) {
Scheduler& scheduler = cpu.getScheduler();
scheduler.addEvent(Scheduler::EventType::UpdateTimers, nextTimestamp);
}
} }
void Kernel::cancelTimer(Timer* timer) { void Kernel::cancelTimer(Timer* timer) {
timer->running = false; timer->running = false;
// TODO: When we have a scheduler this should properly cancel timer events in the scheduler
} }
void Kernel::signalTimer(Handle timerHandle, Timer* timer) { void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
@ -54,6 +74,8 @@ void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
if (timer->interval == 0) { if (timer->interval == 0) {
cancelTimer(timer); cancelTimer(timer);
} else {
timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(timer->interval);
} }
} }
@ -87,18 +109,20 @@ void Kernel::svcSetTimer() {
Timer* timer = object->getData<Timer>(); Timer* timer = object->getData<Timer>();
cancelTimer(timer); cancelTimer(timer);
timer->currentDelay = initial;
timer->interval = interval; timer->interval = interval;
timer->running = true; timer->running = true;
timer->startTick = cpu.getTicks(); timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(initial);
Scheduler& scheduler = cpu.getScheduler();
// Signal an event to poll timers as soon as possible
scheduler.removeEvent(Scheduler::EventType::UpdateTimers);
scheduler.addEvent(Scheduler::EventType::UpdateTimers, cpu.getTicks() + 1);
// If the initial delay is 0 then instantly signal the timer // If the initial delay is 0 then instantly signal the timer
if (initial == 0) { if (initial == 0) {
signalTimer(handle, timer); signalTimer(handle, timer);
} else {
// This should schedule an event in the scheduler when we have one
} }
regs[0] = Result::Success; regs[0] = Result::Success;
} }

View file

@ -17,10 +17,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
#endif #endif
Emulator::Emulator() Emulator::Emulator()
: config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel), gpu(memory, config), : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config),
memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false) cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false)
#ifdef PANDA3DS_ENABLE_HTTP_SERVER #ifdef PANDA3DS_ENABLE_HTTP_SERVER
, httpServer(this) ,
httpServer(this)
#endif #endif
{ {
#ifdef PANDA3DS_ENABLE_DISCORD_RPC #ifdef PANDA3DS_ENABLE_DISCORD_RPC
@ -45,6 +46,10 @@ void Emulator::reset(ReloadOption reload) {
cpu.reset(); cpu.reset();
gpu.reset(); gpu.reset();
memory.reset(); memory.reset();
// Reset scheduler and add a VBlank event
scheduler.reset();
scheduler.addEvent(Scheduler::EventType::VBlank, CPU::ticksPerSec / 60);
// Kernel must be reset last because it depends on CPU/Memory state // Kernel must be reset last because it depends on CPU/Memory state
kernel.reset(); kernel.reset();
@ -99,12 +104,6 @@ void Emulator::runFrame() {
if (running) { if (running) {
cpu.runFrame(); // Run 1 frame of instructions cpu.runFrame(); // Run 1 frame of instructions
gpu.display(); // Display graphics gpu.display(); // Display graphics
lua.signalEvent(LuaEvent::Frame);
// Send VBlank interrupts
ServiceManager& srv = kernel.getServiceManager();
srv.sendGPUInterrupt(GPUInterrupt::VBlank0);
srv.sendGPUInterrupt(GPUInterrupt::VBlank1);
// Run cheats if any are loaded // Run cheats if any are loaded
if (cheats.haveCheats()) [[unlikely]] { if (cheats.haveCheats()) [[unlikely]] {
@ -117,6 +116,43 @@ void Emulator::runFrame() {
} }
} }
void Emulator::pollScheduler() {
auto& events = scheduler.events;
// Pop events until there's none pending anymore
while (scheduler.currentTimestamp >= scheduler.nextTimestamp) {
// Read event timestamp and type, pop it from the scheduler and handle it
auto [time, eventType] = std::move(*events.begin());
events.erase(events.begin());
scheduler.updateNextTimestamp();
switch (eventType) {
case Scheduler::EventType::VBlank: [[likely]] {
// Signal that we've reached the end of a frame
frameDone = true;
lua.signalEvent(LuaEvent::Frame);
// Send VBlank interrupts
ServiceManager& srv = kernel.getServiceManager();
srv.sendGPUInterrupt(GPUInterrupt::VBlank0);
srv.sendGPUInterrupt(GPUInterrupt::VBlank1);
// Queue next VBlank event
scheduler.addEvent(Scheduler::EventType::VBlank, time + CPU::ticksPerSec / 60);
break;
}
case Scheduler::EventType::UpdateTimers: kernel.pollTimers(); break;
default: {
Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast<int>(eventType));
break;
}
}
}
}
// Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc) // Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc)
// Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in // Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in
// %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart. // %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart.