Pica/DebugUtils: Add breakpoint functionality.
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user