mirror of
https://github.com/mikage-emu/mikage-dev.git
synced 2025-01-08 14:30:59 +01:00
443 lines
13 KiB
C++
443 lines
13 KiB
C++
#pragma once
|
|
|
|
#include "arm.h"
|
|
#include "memory.h"
|
|
|
|
#include <spdlog/sinks/sink.h>
|
|
|
|
#include <atomic>
|
|
#include <condition_variable>
|
|
#include <cstring>
|
|
#include <list>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <fstream>
|
|
|
|
// TODO: Shouldn't need to include this header...
|
|
#include "loader/gamecard.hpp"
|
|
|
|
namespace HLE {
|
|
namespace OS {
|
|
class OS;
|
|
}
|
|
}
|
|
|
|
namespace Profiler {
|
|
class Profiler;
|
|
}
|
|
|
|
namespace Debugger {
|
|
class DebugServer;
|
|
}
|
|
|
|
struct KeyDatabase;
|
|
|
|
class LogManager;
|
|
|
|
namespace Interpreter {
|
|
|
|
struct Breakpoint {
|
|
uint32_t address;
|
|
|
|
bool operator==(const Breakpoint& bp) const {
|
|
return address == bp.address;
|
|
}
|
|
};
|
|
|
|
struct AttachInfo {
|
|
uint32_t pid;
|
|
};
|
|
|
|
struct WrappedAttachInfo {
|
|
// Waits until some other thread has successfully signalled request_continue via TryAccess
|
|
void AwaitProcessing() {
|
|
std::unique_lock<std::mutex> lock(condvar_mutex);
|
|
request_continue.wait(lock);
|
|
}
|
|
|
|
std::mutex condvar_mutex;
|
|
std::unique_ptr<AttachInfo> data;
|
|
|
|
private:
|
|
std::mutex access_mutex;
|
|
std::condition_variable request_continue;
|
|
|
|
friend bool TryAccess(std::weak_ptr<WrappedAttachInfo> attach_info_weak, std::function<bool(const AttachInfo&)> eval);
|
|
};
|
|
|
|
/**
|
|
* Tries to unwrap the AttachInfo data stored in the given weak_ptr and passes
|
|
* it to the given callback on success.
|
|
* @param eval Callback that should return true if the AttachInfo should be
|
|
* permanently consumed (allowing the main thread to continue)
|
|
* @return true if and only if the AttachInfo was consumed
|
|
* @todo Define this elsewhere
|
|
*/
|
|
inline bool TryAccess(std::weak_ptr<WrappedAttachInfo> attach_info_weak, std::function<bool(const AttachInfo&)> eval) {
|
|
auto attach_info_shared = attach_info_weak.lock();
|
|
if (!attach_info_shared)
|
|
return false;
|
|
|
|
std::lock_guard<std::mutex> guard(attach_info_shared->access_mutex);
|
|
|
|
auto* attach_info = attach_info_shared->data.get();
|
|
if (!attach_info)
|
|
return false;
|
|
|
|
if (!eval(*attach_info))
|
|
return false;
|
|
|
|
// Drop the original data and signal the main thread to continue
|
|
// Note that we lock the mutex here purely to make sure we don't signal the condition variable before it is being waited for
|
|
attach_info_shared->data = nullptr;
|
|
std::lock_guard<std::mutex> lock(attach_info_shared->condvar_mutex);
|
|
attach_info_shared->request_continue.notify_all();
|
|
return true;
|
|
}
|
|
|
|
// Controls thread-safe stepping/running/pausing/quitting of a Processor
|
|
class ProcessorController {
|
|
// Handlers called from ProcessEventQueue when a segfault has been reported
|
|
virtual void OnSegfault(uint32_t process_id, uint32_t thread_id) = 0;
|
|
virtual void OnBreakpoint(uint32_t process_id, uint32_t thread_id) = 0;
|
|
virtual void OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) = 0;
|
|
virtual void OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) = 0;
|
|
virtual void OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) = 0;
|
|
|
|
protected:
|
|
public: // TODO: Un-public-ize!
|
|
std::atomic<bool> request_pause{false};
|
|
// std::atomic<bool> paused{true};
|
|
std::atomic<bool> paused{false};
|
|
std::atomic<bool> request_continue{false};
|
|
|
|
protected:
|
|
/**
|
|
* If valid, this denotes the process-thread-id of the thread that should
|
|
* be paused. If boost::any is used for the thread-id (second element), any
|
|
* thread of the given process is paused.
|
|
*/
|
|
std::optional<std::pair<uint32_t, std::optional<uint32_t>>> thread_to_pause;
|
|
|
|
friend class Processor;
|
|
|
|
struct Event {
|
|
enum class Type {
|
|
Segfault,
|
|
Breakpoint,
|
|
ProcessLaunched,
|
|
ReadWatchpoint,
|
|
WriteWatchpoint,
|
|
} type;
|
|
|
|
uint32_t process_id;
|
|
uint32_t thread_id;
|
|
|
|
union {
|
|
uint32_t launched_process_id;
|
|
uint32_t watchpoint_data_address;
|
|
};
|
|
};
|
|
|
|
std::list<Event> events;
|
|
|
|
void ProcessEventQueue() {
|
|
while (!events.empty()) {
|
|
auto& event = events.front();
|
|
switch (event.type) {
|
|
case Event::Type::Segfault:
|
|
OnSegfault(event.process_id, event.thread_id);
|
|
break;
|
|
|
|
case Event::Type::Breakpoint:
|
|
OnBreakpoint(event.process_id, event.thread_id);
|
|
break;
|
|
|
|
case Event::Type::ProcessLaunched:
|
|
OnProcessLaunched(event.process_id, event.thread_id, event.launched_process_id);
|
|
break;
|
|
|
|
case Event::Type::ReadWatchpoint:
|
|
OnReadWatchpoint(event.process_id, event.thread_id, event.watchpoint_data_address);
|
|
break;
|
|
|
|
case Event::Type::WriteWatchpoint:
|
|
OnWriteWatchpoint(event.process_id, event.thread_id, event.watchpoint_data_address);
|
|
break;
|
|
|
|
default:
|
|
// Do nothing
|
|
;
|
|
}
|
|
|
|
events.pop_front();
|
|
}
|
|
}
|
|
|
|
public:
|
|
virtual ~ProcessorController() = default;
|
|
|
|
// TODO: This is probably a mess and full of race conditions... :(
|
|
|
|
// TODO: Need functions to signal quit requests
|
|
|
|
/**
|
|
* Request the emulation core to be paused and busy-waits until the request has been fulfilled.
|
|
* @note The amount of CPU instructions processed between the request of the pause and the actual pausing is undefined.
|
|
*/
|
|
void RequestPause() {
|
|
// TODO: Get rid of this! Currently, we need it in case automated stepping encounters a breakpoint..
|
|
if (paused)
|
|
return;
|
|
assert(!paused);
|
|
|
|
thread_to_pause = std::nullopt;
|
|
request_pause = true;
|
|
|
|
while (!paused) {
|
|
}
|
|
|
|
request_pause = false;
|
|
assert(paused);
|
|
}
|
|
|
|
// Request emulation to be continued without any planned pause in the future. Does not return before emulation has continued.
|
|
void RequestContinue() {
|
|
assert(paused);
|
|
|
|
request_continue = true;
|
|
while (paused) {};
|
|
request_continue = false;
|
|
|
|
assert(!paused);
|
|
}
|
|
|
|
// Request emulation to be continued and pause after the next instruction. Does not return before emulation has continued and stopped again.
|
|
void RequestStep(std::optional<std::pair<uint32_t, std::optional<uint32_t>>> process_thread_id = std::nullopt) {
|
|
assert(paused);
|
|
|
|
thread_to_pause = process_thread_id;
|
|
request_pause = true;
|
|
request_continue = true;
|
|
while (paused) {};
|
|
request_continue = false;
|
|
while (!paused) {};
|
|
request_pause = false;
|
|
|
|
assert(paused);
|
|
}
|
|
|
|
bool ShouldPause(uint32_t process_id, uint32_t thread_id) const {
|
|
if (!request_pause)
|
|
return false;
|
|
|
|
if (thread_to_pause == std::nullopt)
|
|
return true;
|
|
|
|
if (thread_to_pause->first != process_id)
|
|
return false;
|
|
|
|
return thread_to_pause->second.value_or(thread_id) == thread_id;
|
|
}
|
|
|
|
// Use this in the Processor when an internal (unrecoverable) error happens
|
|
void NotifySegfault(uint32_t process_id, uint32_t thread_id) {
|
|
events.push_back({Event::Type::Segfault, process_id, thread_id});
|
|
}
|
|
|
|
void NotifyBreakpoint(uint32_t process_id, uint32_t thread_id) {
|
|
events.push_back({Event::Type::Breakpoint, process_id, thread_id});
|
|
}
|
|
|
|
void NotifyReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) {
|
|
events.push_back({Event::Type::ReadWatchpoint, process_id, thread_id, address});
|
|
}
|
|
|
|
void NotifyWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) {
|
|
events.push_back({Event::Type::WriteWatchpoint, process_id, thread_id, address});
|
|
}
|
|
|
|
void NotifyProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) {
|
|
events.push_back({Event::Type::ProcessLaunched, process_id, thread_id, launched_process_id});
|
|
}
|
|
|
|
virtual void OfferAttach(std::weak_ptr<WrappedAttachInfo> attach_info) = 0;
|
|
};
|
|
|
|
/// Trivial ProcessorController with debugging features disabled
|
|
class DummyController final : public ProcessorController {
|
|
void OnSegfault(uint32_t process_id, uint32_t thread_id) override {
|
|
}
|
|
|
|
void OnBreakpoint(uint32_t process_id, uint32_t thread_id) override {
|
|
}
|
|
|
|
void OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override {
|
|
}
|
|
|
|
void OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override {
|
|
}
|
|
|
|
void OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) override {
|
|
}
|
|
|
|
void OfferAttach(std::weak_ptr<WrappedAttachInfo> attach_info) override {
|
|
}
|
|
};
|
|
|
|
} // namespace Interpreter
|
|
|
|
namespace Interpreter {
|
|
|
|
struct Callsite {
|
|
uint32_t source;
|
|
uint32_t target;
|
|
|
|
ARM::State state;
|
|
};
|
|
|
|
struct Setup;
|
|
|
|
struct CPUContext;
|
|
|
|
// #define CONTROL_FLOW_LOGGING
|
|
struct ControlFlowLogger {
|
|
uint32_t indent = 0;
|
|
|
|
void Branch(CPUContext& ctx, const char* kind, uint32_t addr);
|
|
|
|
void Return(CPUContext& ctx, const char* kind);
|
|
|
|
void SVC(CPUContext& ctx, uint32_t id);
|
|
|
|
void Log(CPUContext& ctx, const std::string& str);
|
|
|
|
private:
|
|
std::string GetIndent();
|
|
void MakeSureFileIsOpen(CPUContext& ctx);
|
|
|
|
#ifdef CONTROL_FLOW_LOGGING
|
|
std::ofstream os;
|
|
#endif
|
|
};
|
|
|
|
// TODO: Move to private implementation
|
|
struct CPUContext {
|
|
CPUContext(HLE::OS::OS* os = nullptr, Setup* setup = nullptr);
|
|
~CPUContext();
|
|
|
|
ARM::State cpu{};
|
|
|
|
std::list<Breakpoint> breakpoints;
|
|
std::list<Breakpoint> read_watchpoints;
|
|
std::list<Breakpoint> write_watchpoints;
|
|
|
|
std::vector<Callsite> backtrace;
|
|
|
|
// This must be true for any of the debugging functionality to be enabled
|
|
bool debugger_attached = false;
|
|
|
|
bool trap_on_resume = false;
|
|
|
|
ControlFlowLogger cfl{};
|
|
|
|
void RecordCall(uint32_t source, uint32_t target, ARM::State state);
|
|
|
|
HLE::OS::OS* os{};
|
|
|
|
Setup* setup{};
|
|
|
|
ProcessorController* controller{};
|
|
};
|
|
|
|
// TODO: This should be somewhere stored in its own header! (need to provide external default destructor in Processor to enable that
|
|
// TODO: Rename to Emulator (and strip the CPUContext; they belong in the Processors' ExecutionContext instead, and those are created by OS)
|
|
struct Setup {
|
|
Setup(LogManager&, const KeyDatabase&,
|
|
std::unique_ptr<Loader::GameCard>, Profiler::Profiler&,
|
|
Debugger::DebugServer&);
|
|
~Setup();
|
|
|
|
CPUContext cpus[2];
|
|
|
|
Memory::PhysicalMemory mem;
|
|
|
|
std::unique_ptr<HLE::OS::OS> os;
|
|
|
|
const KeyDatabase& keydb;
|
|
|
|
std::unique_ptr<Loader::GameCard> gamecard = nullptr;
|
|
|
|
Profiler::Profiler& profiler;
|
|
|
|
Debugger::DebugServer& debug_server;
|
|
};
|
|
|
|
// TODO: Don't leak this implementation detail
|
|
void Step(struct ExecutionContext&); // NOTE: This is only for the Interpreter
|
|
|
|
class Processor;
|
|
|
|
struct ExecutionContext {
|
|
Processor& parent;
|
|
|
|
ExecutionContext(Processor& parent);
|
|
|
|
ExecutionContext(const ExecutionContext&) = delete;
|
|
ExecutionContext& operator=(const ExecutionContext&) = delete;
|
|
ExecutionContext(const ExecutionContext&&) = delete;
|
|
ExecutionContext& operator=(ExecutionContext&&) = delete;
|
|
|
|
// Automatically unregisters this context from its parent Processor
|
|
virtual ~ExecutionContext();
|
|
|
|
// TODO: Provide generic accessors instead
|
|
virtual ARM::State ToGenericContext() = 0;
|
|
virtual void FromGenericContext(const ARM::State&) = 0;
|
|
|
|
/**
|
|
* Enables ProcessorController to take control over the thread corresponding to this context.
|
|
*/
|
|
virtual void SetDebuggingEnabled(bool enabled = true) { (void)enabled; }
|
|
|
|
virtual bool IsDebuggingEnabled() const { return false; }
|
|
};
|
|
|
|
class Processor {
|
|
friend struct ExecutionContext;
|
|
|
|
virtual ExecutionContext* CreateExecutionContextImpl() = 0;
|
|
|
|
void UnregisterContext(ExecutionContext&);
|
|
|
|
protected:
|
|
std::vector<ExecutionContext*> contexts;
|
|
|
|
public:
|
|
virtual ~Processor() = default;
|
|
|
|
virtual void WriteVirtualMemory8(uint32_t virt_address, const uint8_t value) = 0;
|
|
virtual void WriteVirtualMemory16(uint32_t virt_address, const uint16_t value) = 0;
|
|
virtual void WriteVirtualMemory32(uint32_t virt_address, const uint32_t value) = 0;
|
|
|
|
virtual uint8_t ReadVirtualMemory8(uint32_t virt_address) = 0;
|
|
virtual uint16_t ReadVirtualMemory16(uint32_t virt_address) = 0;
|
|
virtual uint32_t ReadVirtualMemory32(uint32_t virt_address) = 0;
|
|
|
|
// Runs until shut down by controller
|
|
virtual void Run(ExecutionContext&, ProcessorController& controller, uint32_t process_id, uint32_t thread_id) = 0;
|
|
|
|
virtual void OnVirtualMemoryMapped(uint32_t phys_addr, uint32_t size, uint32_t vaddr) = 0;
|
|
|
|
virtual void OnVirtualMemoryUnmapped(uint32_t vaddr, uint32_t size) = 0;
|
|
|
|
|
|
std::unique_ptr<ExecutionContext> CreateExecutionContext() {
|
|
return std::unique_ptr<ExecutionContext> { CreateExecutionContextImpl() };
|
|
}
|
|
};
|
|
|
|
std::unique_ptr<Processor> CreateInterpreter(Setup& setup);
|
|
|
|
} // namespace Interpreter
|