diff --git a/CMakeLists.txt b/CMakeLists.txt index b4b1503a..e557c5c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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/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/PICA/dynapica/shader_rec_emitter_arm64.hpp + include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp ) cmrc_add_resource_library( diff --git a/include/cpu_dynarmic.hpp b/include/cpu_dynarmic.hpp index 8f1e277b..97acddd0 100644 --- a/include/cpu_dynarmic.hpp +++ b/include/cpu_dynarmic.hpp @@ -9,15 +9,18 @@ #include "helpers.hpp" #include "kernel.hpp" #include "memory.hpp" +#include "scheduler.hpp" +class Emulator; class CPU; class MyEnvironment final : public Dynarmic::A32::UserCallbacks { -public: - u64 ticksLeft = 0; - u64 totalTicks = 0; - Memory& mem; - Kernel& kernel; + public: + u64 ticksLeft = 0; + u64 totalTicks = 0; + Memory& mem; + Kernel& kernel; + Scheduler& scheduler; u64 getCyclesForInstruction(bool isThumb, u32 instruction); @@ -76,54 +79,56 @@ public: std::terminate(); } - void CallSVC(u32 swi) override { - kernel.serviceSVC(swi); - } + void CallSVC(u32 swi) override { + kernel.serviceSVC(swi); + } - void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { - switch (exception) { - case Dynarmic::A32::Exception::UnpredictableInstruction: - Helpers::panic("Unpredictable instruction at pc = %08X", pc); - break; + void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { + switch (exception) { + case Dynarmic::A32::Exception::UnpredictableInstruction: + Helpers::panic("Unpredictable instruction at pc = %08X", pc); + break; - default: Helpers::panic("Fired exception oops"); - } - } + default: Helpers::panic("Fired exception oops"); + } + } - void AddTicks(u64 ticks) override { - totalTicks += ticks; + void AddTicks(u64 ticks) override { + scheduler.currentTimestamp += ticks; - if (ticks > ticksLeft) { - ticksLeft = 0; - return; - } - ticksLeft -= ticks; - } + if (ticks > ticksLeft) { + ticksLeft = 0; + return; + } + ticksLeft -= ticks; + } - u64 GetTicksRemaining() override { - return ticksLeft; - } + u64 GetTicksRemaining() override { + return ticksLeft; + } - u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override { - return getCyclesForInstruction(isThumb, instruction); - } + u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override { + 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 { - std::unique_ptr jit; - std::shared_ptr cp15; + std::unique_ptr jit; + std::shared_ptr cp15; - // Make exclusive monitor with only 1 CPU core - Dynarmic::ExclusiveMonitor exclusiveMonitor{1}; - MyEnvironment env; - Memory& mem; + // Make exclusive monitor with only 1 CPU core + Dynarmic::ExclusiveMonitor exclusiveMonitor{1}; + MyEnvironment env; + Memory& mem; + Scheduler& scheduler; + Emulator& emu; -public: - static constexpr u64 ticksPerSec = 268111856; + public: + static constexpr u64 ticksPerSec = Scheduler::arm11Clock; - CPU(Memory& mem, Kernel& kernel); + CPU(Memory& mem, Kernel& kernel, Emulator& emu); void reset(); void setReg(int index, u32 value) { @@ -162,29 +167,18 @@ public: } u64 getTicks() { - return env.totalTicks; + return scheduler.currentTimestamp; } // Get reference to tick count. Memory needs access to this u64& getTicksRef() { - return env.totalTicks; + return scheduler.currentTimestamp; } - void clearCache() { jit->ClearCache(); } - - void runFrame() { - env.ticksLeft = ticksPerSec / 60; - execute: - const auto exitReason = jit->Run(); - - if (static_cast(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(exitReason), getReg(15)); - } - } + Scheduler& getScheduler() { + return scheduler; } + + void clearCache() { jit->ClearCache(); } + void runFrame(); }; \ No newline at end of file diff --git a/include/emulator.hpp b/include/emulator.hpp index da12c1bd..d3377f6c 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -15,6 +15,7 @@ #include "io_file.hpp" #include "lua_manager.hpp" #include "memory.hpp" +#include "scheduler.hpp" #ifdef PANDA3DS_ENABLE_HTTP_SERVER #include "http_server.hpp" @@ -42,6 +43,7 @@ class Emulator { Kernel kernel; Crypto::AESEngine aesEngine; Cheats cheats; + Scheduler scheduler; // 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 @@ -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 // ROM and just resets the emu enum class ReloadOption { NoReload, Reload }; + // Used in CPU::runFrame + bool frameDone = false; Emulator(); ~Emulator(); @@ -94,6 +98,8 @@ class Emulator { void reset(ReloadOption reload); void run(void* frontend = nullptr); void runFrame(); + // Poll the scheduler for events + void pollScheduler(); void resume(); // Resume the emulator void pause(); // Pause the emulator @@ -121,6 +127,7 @@ class Emulator { Cheats& getCheats() { return cheats; } ServiceManager& getServiceManager() { return kernel.getServiceManager(); } LuaManager& getLua() { return lua; } + Scheduler& getScheduler() { return scheduler; } RendererType getRendererType() const { return config.rendererType; } Renderer* getRenderer() { return gpu.getRenderer(); } diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp index 3f09bf12..217dd148 100644 --- a/include/kernel/kernel.hpp +++ b/include/kernel/kernel.hpp @@ -70,6 +70,7 @@ public: 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 makeTimer(ResetType resetType); + void pollTimers(); // Signals an event, returns true on success or false if the event does not exist bool signalEvent(Handle e); @@ -94,7 +95,6 @@ public: void releaseMutex(Mutex* moo); void cancelTimer(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 // Returns the index of the woken up thread diff --git a/include/kernel/kernel_types.hpp b/include/kernel/kernel_types.hpp index 53c60774..79684e17 100644 --- a/include/kernel/kernel_types.hpp +++ b/include/kernel/kernel_types.hpp @@ -177,13 +177,12 @@ struct Timer { u64 waitlist; // Refer to the getWaitlist function below for documentation ResetType resetType = ResetType::OneShot; - u64 startTick; // CPU tick the timer started - u64 currentDelay; // Number of ns until the timer fires next time + u64 fireTick; // CPU tick the timer will be fired u64 interval; // Number of ns until the timer fires for the second and future times bool fired; // Has this timer been signalled? 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 { diff --git a/include/scheduler.hpp b/include/scheduler.hpp new file mode 100644 index 00000000..92328878 --- /dev/null +++ b/include/scheduler.hpp @@ -0,0 +1,87 @@ +#pragma once +#include +#include +#include + +#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(EventType::TotalNumberOfEvents); + static constexpr u64 arm11Clock = 268111856; + + template + using EventMap = boost::container::flat_multimap, boost::container::static_vector, size>>; + + EventMap 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::max()); + } + + private: + static constexpr u64 MAX_VALUE_TO_MULTIPLY = std::numeric_limits::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(MAX_VALUE_TO_MULTIPLY)) { + return std::numeric_limits::max(); + } + + if (ns > static_cast(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::max(); + } + + if (ns > MAX_VALUE_TO_MULTIPLY) { + return arm11Clock * (s64(ns) / 1000000000); + } + + return (arm11Clock * s64(ns)) / 1000000000; + } +}; \ No newline at end of file diff --git a/src/core/CPU/cpu_dynarmic.cpp b/src/core/CPU/cpu_dynarmic.cpp index 29ca49d1..da5270b4 100644 --- a/src/core/CPU/cpu_dynarmic.cpp +++ b/src/core/CPU/cpu_dynarmic.cpp @@ -1,32 +1,59 @@ #ifdef CPU_DYNARMIC #include "cpu_dynarmic.hpp" + #include "arm_defs.hpp" +#include "emulator.hpp" -CPU::CPU(Memory& mem, Kernel& kernel) : mem(mem), env(mem, kernel) { - cp15 = std::make_shared(); +CPU::CPU(Memory& mem, Kernel& kernel, Emulator& emu) : mem(mem), emu(emu), scheduler(emu.getScheduler()), env(mem, kernel, emu.getScheduler()) { + cp15 = std::make_shared(); - Dynarmic::A32::UserConfig config; - config.arch_version = Dynarmic::A32::ArchVersion::v6K; - config.callbacks = &env; - config.coprocessors[15] = cp15; - config.define_unpredictable_behaviour = true; - config.global_monitor = &exclusiveMonitor; - config.processor_id = 0; - - jit = std::make_unique(config); + Dynarmic::A32::UserConfig config; + config.arch_version = Dynarmic::A32::ArchVersion::v6K; + config.callbacks = &env; + config.coprocessors[15] = cp15; + config.define_unpredictable_behaviour = true; + config.global_monitor = &exclusiveMonitor; + config.processor_id = 0; + + jit = std::make_unique(config); } void CPU::reset() { - setCPSR(CPSR::UserMode); - setFPSCR(FPSCR::MainThreadDefault); - env.totalTicks = 0; + setCPSR(CPSR::UserMode); + setFPSCR(FPSCR::MainThreadDefault); + env.totalTicks = 0; - cp15->reset(); - cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage - jit->Reset(); - jit->ClearCache(); - jit->Regs().fill(0); - jit->ExtRegs().fill(0); + cp15->reset(); + cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage + jit->Reset(); + jit->ClearCache(); + jit->Regs().fill(0); + jit->ExtRegs().fill(0); } -#endif // CPU_DYNARMIC \ No newline at end of file +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(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(exitReason), getReg(15)); + } + } + } +} + +#endif // CPU_DYNARMIC \ No newline at end of file diff --git a/src/core/kernel/timers.cpp b/src/core/kernel/timers.cpp index a9c95292..35fc57a4 100644 --- a/src/core/kernel/timers.cpp +++ b/src/core/kernel/timers.cpp @@ -1,5 +1,8 @@ -#include "kernel.hpp" +#include + #include "cpu.hpp" +#include "kernel.hpp" +#include "scheduler.hpp" Handle Kernel::makeTimer(ResetType type) { Handle ret = makeObject(KernelObjectType::Timer); @@ -13,27 +16,44 @@ Handle Kernel::makeTimer(ResetType type) { return ret; } -void Kernel::updateTimer(Handle handle, Timer* timer) { - if (timer->running) { - const u64 currentTicks = cpu.getTicks(); - u64 elapsedTicks = currentTicks - timer->startTick; +void Kernel::pollTimers() { + u64 currentTick = cpu.getTicks(); - constexpr double ticksPerSec = double(CPU::ticksPerSec); - constexpr double nsPerTick = ticksPerSec / 1000000000.0; - const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick); + // Find the next timestamp we'll poll KTimers on. To do this, we find the minimum tick one of our timers will fire + u64 nextTimestamp = std::numeric_limits::max(); + // 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 - if (elapsedNs >= timer->currentDelay) { - timer->startTick = currentTicks; - timer->currentDelay = timer->interval; - signalTimer(handle, timer); + for (auto handle : timerHandles) { + KernelObject* object = getObject(handle, KernelObjectType::Timer); + if (object != nullptr) { + Timer* timer = object->getData(); + + 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(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) { 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) { @@ -54,6 +74,8 @@ void Kernel::signalTimer(Handle timerHandle, Timer* timer) { if (timer->interval == 0) { cancelTimer(timer); + } else { + timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(timer->interval); } } @@ -87,18 +109,20 @@ void Kernel::svcSetTimer() { Timer* timer = object->getData(); cancelTimer(timer); - timer->currentDelay = initial; timer->interval = interval; 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 (initial == 0) { signalTimer(handle, timer); - } else { - // This should schedule an event in the scheduler when we have one } - + regs[0] = Result::Success; } diff --git a/src/emulator.cpp b/src/emulator.cpp index 673e0ebc..1ac8d5b2 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -17,10 +17,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; #endif Emulator::Emulator() - : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel), gpu(memory, config), - memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false) + : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), + cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false) #ifdef PANDA3DS_ENABLE_HTTP_SERVER - , httpServer(this) + , + httpServer(this) #endif { #ifdef PANDA3DS_ENABLE_DISCORD_RPC @@ -45,6 +46,10 @@ void Emulator::reset(ReloadOption reload) { cpu.reset(); gpu.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.reset(); @@ -99,12 +104,6 @@ void Emulator::runFrame() { if (running) { cpu.runFrame(); // Run 1 frame of instructions 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 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(eventType)); + break; + } + } + } +} + // 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 // %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart.