Pica/DebugUtils: Add breakpoint functionality.
This commit is contained in:
parent
706f9c5574
commit
2c71ec7052
5 changed files with 204 additions and 2 deletions
|
@ -14,6 +14,8 @@
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
|
|
||||||
|
#include "video_core/debug_utils/debug_utils.h"
|
||||||
|
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
#include "citra_qt/version.h"
|
#include "citra_qt/version.h"
|
||||||
|
@ -65,14 +67,21 @@ void EmuThread::Stop()
|
||||||
}
|
}
|
||||||
stop_run = true;
|
stop_run = true;
|
||||||
|
|
||||||
|
// Release emu threads from any breakpoints, so that this doesn't hang forever.
|
||||||
|
Pica::g_debug_context->ClearBreakpoints();
|
||||||
|
|
||||||
//core::g_state = core::SYS_DIE;
|
//core::g_state = core::SYS_DIE;
|
||||||
|
|
||||||
wait(500);
|
// TODO: Waiting here is just a bad workaround for retarded shutdown logic.
|
||||||
|
wait(1000);
|
||||||
if (isRunning())
|
if (isRunning())
|
||||||
{
|
{
|
||||||
WARN_LOG(MASTER_LOG, "EmuThread still running, terminating...");
|
WARN_LOG(MASTER_LOG, "EmuThread still running, terminating...");
|
||||||
quit();
|
quit();
|
||||||
wait(1000);
|
|
||||||
|
// TODO: Waiting 50 seconds can be necessary if the logging subsystem has a lot of spam
|
||||||
|
// queued... This should be fixed.
|
||||||
|
wait(50000);
|
||||||
if (isRunning())
|
if (isRunning())
|
||||||
{
|
{
|
||||||
WARN_LOG(MASTER_LOG, "EmuThread STILL running, something is wrong here...");
|
WARN_LOG(MASTER_LOG, "EmuThread STILL running, something is wrong here...");
|
||||||
|
|
|
@ -36,6 +36,8 @@ GMainWindow::GMainWindow()
|
||||||
{
|
{
|
||||||
LogManager::Init();
|
LogManager::Init();
|
||||||
|
|
||||||
|
Pica::g_debug_context = Pica::DebugContext::Construct();
|
||||||
|
|
||||||
Config config;
|
Config config;
|
||||||
|
|
||||||
if (!Settings::values.enable_log)
|
if (!Settings::values.enable_log)
|
||||||
|
@ -133,6 +135,8 @@ GMainWindow::~GMainWindow()
|
||||||
// will get automatically deleted otherwise
|
// will get automatically deleted otherwise
|
||||||
if (render_window->parent() == nullptr)
|
if (render_window->parent() == nullptr)
|
||||||
delete render_window;
|
delete render_window;
|
||||||
|
|
||||||
|
Pica::g_debug_context.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::BootGame(std::string filename)
|
void GMainWindow::BootGame(std::string filename)
|
||||||
|
|
|
@ -34,6 +34,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
|
||||||
u32 old_value = registers[id];
|
u32 old_value = registers[id];
|
||||||
registers[id] = (old_value & ~mask) | (value & mask);
|
registers[id] = (old_value & ~mask) | (value & mask);
|
||||||
|
|
||||||
|
if (g_debug_context)
|
||||||
|
g_debug_context->OnEvent(DebugContext::Event::CommandLoaded, reinterpret_cast<void*>(&id));
|
||||||
|
|
||||||
DebugUtils::OnPicaRegWrite(id, registers[id]);
|
DebugUtils::OnPicaRegWrite(id, registers[id]);
|
||||||
|
|
||||||
switch(id) {
|
switch(id) {
|
||||||
|
@ -43,6 +46,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
|
||||||
{
|
{
|
||||||
DebugUtils::DumpTevStageConfig(registers.GetTevStages());
|
DebugUtils::DumpTevStageConfig(registers.GetTevStages());
|
||||||
|
|
||||||
|
if (g_debug_context)
|
||||||
|
g_debug_context->OnEvent(DebugContext::Event::IncomingPrimitiveBatch, nullptr);
|
||||||
|
|
||||||
const auto& attribute_config = registers.vertex_attributes;
|
const auto& attribute_config = registers.vertex_attributes;
|
||||||
const u8* const base_address = Memory::GetPointer(attribute_config.GetBaseAddress());
|
const u8* const base_address = Memory::GetPointer(attribute_config.GetBaseAddress());
|
||||||
|
|
||||||
|
@ -132,6 +138,10 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
|
||||||
clipper_primitive_assembler.SubmitVertex(output, Clipper::ProcessTriangle);
|
clipper_primitive_assembler.SubmitVertex(output, Clipper::ProcessTriangle);
|
||||||
}
|
}
|
||||||
geometry_dumper.Dump();
|
geometry_dumper.Dump();
|
||||||
|
|
||||||
|
if (g_debug_context)
|
||||||
|
g_debug_context->OnEvent(DebugContext::Event::FinishedPrimitiveBatch, nullptr);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +239,9 @@ static inline void WritePicaReg(u32 id, u32 value, u32 mask) {
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (g_debug_context)
|
||||||
|
g_debug_context->OnEvent(DebugContext::Event::CommandProcessed, reinterpret_cast<void*>(&id));
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::ptrdiff_t ExecuteCommandBlock(const u32* first_command_word) {
|
static std::ptrdiff_t ExecuteCommandBlock(const u32* first_command_word) {
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <list>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
@ -12,6 +14,7 @@
|
||||||
#include <png.h>
|
#include <png.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "common/log.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
|
|
||||||
#include "video_core/pica.h"
|
#include "video_core/pica.h"
|
||||||
|
@ -20,6 +23,46 @@
|
||||||
|
|
||||||
namespace Pica {
|
namespace Pica {
|
||||||
|
|
||||||
|
void DebugContext::OnEvent(Event event, void* data) {
|
||||||
|
if (!breakpoints[event].enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(breakpoint_mutex);
|
||||||
|
|
||||||
|
// TODO: Should stop the CPU thread here once we multithread emulation.
|
||||||
|
|
||||||
|
active_breakpoint = event;
|
||||||
|
at_breakpoint = true;
|
||||||
|
|
||||||
|
// Tell all observers that we hit a breakpoint
|
||||||
|
for (auto& breakpoint_observer : breakpoint_observers) {
|
||||||
|
breakpoint_observer->OnPicaBreakPointHit(event, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until another thread tells us to Resume()
|
||||||
|
resume_from_breakpoint.wait(lock, [&]{ return !at_breakpoint; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugContext::Resume() {
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(breakpoint_mutex);
|
||||||
|
|
||||||
|
// Tell all observers that we are about to resume
|
||||||
|
for (auto& breakpoint_observer : breakpoint_observers) {
|
||||||
|
breakpoint_observer->OnPicaResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume the waiting thread (i.e. OnEvent())
|
||||||
|
at_breakpoint = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resume_from_breakpoint.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<DebugContext> g_debug_context; // TODO: Get rid of this global
|
||||||
|
|
||||||
namespace DebugUtils {
|
namespace DebugUtils {
|
||||||
|
|
||||||
void GeometryDumper::AddTriangle(Vertex& v0, Vertex& v1, Vertex& v2) {
|
void GeometryDumper::AddTriangle(Vertex& v0, Vertex& v1, Vertex& v2) {
|
||||||
|
|
|
@ -5,13 +5,146 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <list>
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "video_core/pica.h"
|
#include "video_core/pica.h"
|
||||||
|
|
||||||
namespace Pica {
|
namespace Pica {
|
||||||
|
|
||||||
|
class DebugContext {
|
||||||
|
public:
|
||||||
|
enum class Event {
|
||||||
|
FirstEvent = 0,
|
||||||
|
|
||||||
|
CommandLoaded = FirstEvent,
|
||||||
|
CommandProcessed,
|
||||||
|
IncomingPrimitiveBatch,
|
||||||
|
FinishedPrimitiveBatch,
|
||||||
|
|
||||||
|
NumEvents
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inherit from this class to be notified of events registered to some debug context.
|
||||||
|
* Most importantly this is used for our debugger GUI.
|
||||||
|
*
|
||||||
|
* To implement event handling, override the OnPicaBreakPointHit and OnPicaResume methods.
|
||||||
|
* @warning All BreakPointObservers need to be on the same thread to guarantee thread-safe state access
|
||||||
|
* @todo Evaluate an alternative interface, in which there is only one managing observer and multiple child observers running (by design) on the same thread.
|
||||||
|
*/
|
||||||
|
class BreakPointObserver {
|
||||||
|
public:
|
||||||
|
/// Constructs the object such that it observes events of the given DebugContext.
|
||||||
|
BreakPointObserver(std::shared_ptr<DebugContext> debug_context) : context_weak(debug_context) {
|
||||||
|
std::unique_lock<std::mutex> lock(debug_context->breakpoint_mutex);
|
||||||
|
debug_context->breakpoint_observers.push_back(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~BreakPointObserver() {
|
||||||
|
auto context = context_weak.lock();
|
||||||
|
if (context) {
|
||||||
|
std::unique_lock<std::mutex> lock(context->breakpoint_mutex);
|
||||||
|
context->breakpoint_observers.remove(this);
|
||||||
|
|
||||||
|
// If we are the last observer to be destroyed, tell the debugger context that
|
||||||
|
// it is free to continue. In particular, this is required for a proper Citra
|
||||||
|
// shutdown, when the emulation thread is waiting at a breakpoint.
|
||||||
|
if (context->breakpoint_observers.empty())
|
||||||
|
context->Resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to perform when a breakpoint was reached.
|
||||||
|
* @param event Type of event which triggered the breakpoint
|
||||||
|
* @param data Optional data pointer (if unused, this is a nullptr)
|
||||||
|
* @note This function will perform nothing unless it is overridden in the child class.
|
||||||
|
*/
|
||||||
|
virtual void OnPicaBreakPointHit(Event, void*) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to perform when emulation is resumed from a breakpoint.
|
||||||
|
* @note This function will perform nothing unless it is overridden in the child class.
|
||||||
|
*/
|
||||||
|
virtual void OnPicaResume() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/**
|
||||||
|
* Weak context pointer. This need not be valid, so when requesting a shared_ptr via
|
||||||
|
* context_weak.lock(), always compare the result against nullptr.
|
||||||
|
*/
|
||||||
|
std::weak_ptr<DebugContext> context_weak;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple structure defining a breakpoint state
|
||||||
|
*/
|
||||||
|
struct BreakPoint {
|
||||||
|
bool enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static constructor used to create a shared_ptr of a DebugContext.
|
||||||
|
*/
|
||||||
|
static std::shared_ptr<DebugContext> Construct() {
|
||||||
|
return std::shared_ptr<DebugContext>(new DebugContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by the emulation core when a given event has happened. If a breakpoint has been set
|
||||||
|
* for this event, OnEvent calls the event handlers of the registered breakpoint observers.
|
||||||
|
* The current thread then is halted until Resume() is called from another thread (or until
|
||||||
|
* emulation is stopped).
|
||||||
|
* @param event Event which has happened
|
||||||
|
* @param data Optional data pointer (pass nullptr if unused). Needs to remain valid until Resume() is called.
|
||||||
|
*/
|
||||||
|
void OnEvent(Event event, void* data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume from the current breakpoint.
|
||||||
|
* @warning Calling this from the same thread that OnEvent was called in will cause a deadlock. Calling from any other thread is safe.
|
||||||
|
*/
|
||||||
|
void Resume();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all set breakpoints and resume emulation.
|
||||||
|
*/
|
||||||
|
void ClearBreakpoints() {
|
||||||
|
breakpoints.clear();
|
||||||
|
Resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Evaluate if access to these members should be hidden behind a public interface.
|
||||||
|
std::map<Event, BreakPoint> breakpoints;
|
||||||
|
Event active_breakpoint;
|
||||||
|
bool at_breakpoint = false;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Private default constructor to make sure people always construct this through Construct()
|
||||||
|
* instead.
|
||||||
|
*/
|
||||||
|
DebugContext() = default;
|
||||||
|
|
||||||
|
/// Mutex protecting current breakpoint state and the observer list.
|
||||||
|
std::mutex breakpoint_mutex;
|
||||||
|
|
||||||
|
/// Used by OnEvent to wait for resumption.
|
||||||
|
std::condition_variable resume_from_breakpoint;
|
||||||
|
|
||||||
|
/// List of registered observers
|
||||||
|
std::list<BreakPointObserver*> breakpoint_observers;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern std::shared_ptr<DebugContext> g_debug_context; // TODO: Get rid of this global
|
||||||
|
|
||||||
namespace DebugUtils {
|
namespace DebugUtils {
|
||||||
|
|
||||||
// Simple utility class for dumping geometry data to an OBJ file
|
// Simple utility class for dumping geometry data to an OBJ file
|
||||||
|
|
Loading…
Reference in a new issue