// Copyright (c) 2012- PPSSPP Project. // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 2.0 or later versions. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License 2.0 for more details. // A copy of the GPL 2.0 should have been included with the program. // If not, see http://www.gnu.org/licenses/ // Official git repository and contact information can be found at // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. #include #include #include #include "native/base/mutex.h" #include "Windows/GEDebugger/GEDebugger.h" #include "Windows/GEDebugger/SimpleGLWindow.h" #include "Windows/GEDebugger/CtrlDisplayListView.h" #include "Windows/GEDebugger/TabDisplayLists.h" #include "Windows/GEDebugger/TabState.h" #include "Windows/InputBox.h" #include "Windows/WindowsHost.h" #include "Windows/WndMainWindow.h" #include "Windows/main.h" #include "GPU/GPUInterface.h" #include "GPU/Common/GPUDebugInterface.h" #include "GPU/GPUState.h" #include "Core/Config.h" #include #include enum PauseAction { PAUSE_CONTINUE, PAUSE_BREAK, PAUSE_GETFRAMEBUF, PAUSE_GETDEPTHBUF, PAUSE_GETSTENCILBUF, PAUSE_GETTEX, PAUSE_SETCMDVALUE, }; static bool attached = false; // TODO static bool textureCaching = true; static recursive_mutex pauseLock; static condition_variable pauseWait; static PauseAction pauseAction = PAUSE_CONTINUE; static recursive_mutex actionLock; static condition_variable actionWait; static bool actionComplete; static recursive_mutex breaksLock; static std::vector breakCmds; static std::set breakPCs; static u32 tempBreakpoint = -1; static std::set breakTextures; static BreakNextType breakNext = BREAK_NONE; static u32 lastTexture = -1; static bool bufferResult; static GPUDebugBuffer bufferFrame; static GPUDebugBuffer bufferDepth; static GPUDebugBuffer bufferStencil; static GPUDebugBuffer bufferTex; static u32 pauseSetCmdValue; enum PrimaryDisplayType { PRIMARY_FRAMEBUF, PRIMARY_DEPTHBUF, PRIMARY_STENCILBUF, }; // TODO: Simplify and move out of windows stuff, just block in a common way for everyone. void CGEDebugger::Init() { SimpleGLWindow::RegisterClass(); CtrlDisplayListView::registerClass(); } bool CGEDebugger::IsAddressBreakPoint(u32 pc) { lock_guard guard(breaksLock); return breakPCs.find(pc) != breakPCs.end(); } bool CGEDebugger::IsOpBreakPoint(u32 op) { u8 cmd = op >> 24; return breakCmds[cmd]; } // Hmm, this is probably kinda slow now... bool CGEDebugger::IsTextureBreakPoint(u32 op) { u8 cmd = op >> 24; bool interesting = (cmd >= GE_CMD_TEXADDR0 && cmd <= GE_CMD_TEXADDR7); interesting = interesting || (cmd >= GE_CMD_TEXBUFWIDTH0 && cmd <= GE_CMD_TEXBUFWIDTH7); if (breakNext == BREAK_NEXT_NONTEX) { // Okay, so we hit an interesting texture, but let's not break if this is a clut/etc. // Otherwise we get garbage widths and colors. It's annoying. bool textureCmd = interesting || cmd == GE_CMD_CLUTADDR || cmd == GE_CMD_CLUTADDRUPPER || cmd == GE_CMD_LOADCLUT || cmd == GE_CMD_CLUTFORMAT; textureCmd = textureCmd || (cmd >= GE_CMD_TEXSIZE0 && cmd <= GE_CMD_TEXSIZE7); textureCmd = textureCmd || cmd == GE_CMD_TEXFORMAT || cmd == GE_CMD_TEXMODE || cmd == GE_CMD_TEXTUREMAPENABLE; if (!textureCmd) { return true; } } if (!interesting || !gpuDebug) { return false; } // Okay, so we just set a texture of some sort, check if it was one we were waiting for. auto state = gpuDebug->GetGState(); int level = cmd <= GE_CMD_TEXADDR7 ? cmd - GE_CMD_TEXADDR0 : cmd - GE_CMD_TEXBUFWIDTH0; // Are we breaking on any texture? As long as it's level 0. if (level == 0 && breakNext == BREAK_NEXT_TEX && lastTexture != state.getTextureAddress(level)) { // Don't break right away, we'll get a garbage texture... breakNext = BREAK_NEXT_NONTEX; } lock_guard guard(breaksLock); if (breakTextures.find(state.getTextureAddress(level)) != breakTextures.end() && breakNext == BREAK_NONE) { breakNext = BREAK_NEXT_NONTEX; } return false; } bool CGEDebugger::IsOpOrTextureBreakPoint(u32 op) { return IsOpBreakPoint(op) || IsTextureBreakPoint(op); } static void SetPauseAction(PauseAction act, bool waitComplete = true) { { lock_guard guard(pauseLock); actionLock.lock(); pauseAction = act; } actionComplete = false; pauseWait.notify_one(); while (waitComplete && !actionComplete) { actionWait.wait(actionLock); } actionLock.unlock(); } static void RunPauseAction() { lock_guard guard(actionLock); switch (pauseAction) { case PAUSE_CONTINUE: // Don't notify, just go back, woke up by accident. return; case PAUSE_BREAK: break; case PAUSE_GETFRAMEBUF: bufferResult = gpuDebug->GetCurrentFramebuffer(bufferFrame); break; case PAUSE_GETDEPTHBUF: bufferResult = gpuDebug->GetCurrentDepthbuffer(bufferDepth); break; case PAUSE_GETSTENCILBUF: bufferResult = gpuDebug->GetCurrentStencilbuffer(bufferStencil); break; case PAUSE_GETTEX: bufferResult = gpuDebug->GetCurrentTexture(bufferTex); break; case PAUSE_SETCMDVALUE: gpuDebug->SetCmdValue(pauseSetCmdValue); break; default: ERROR_LOG(HLE, "Unsupported pause action, forgot to add it to the switch."); } actionComplete = true; actionWait.notify_one(); pauseAction = PAUSE_BREAK; } static void ForceUnpause() { SetPauseAction(PAUSE_CONTINUE, false); actionComplete = true; actionWait.notify_one(); } CGEDebugger::CGEDebugger(HINSTANCE _hInstance, HWND _hParent) : Dialog((LPCSTR)IDD_GEDEBUGGER, _hInstance, _hParent), frameWindow(NULL), texWindow(NULL) { breakCmds.resize(256, false); Core_ListenShutdown(ForceUnpause); // minimum size = a little more than the default RECT windowRect; GetWindowRect(m_hDlg,&windowRect); minWidth = windowRect.right-windowRect.left+10; minHeight = windowRect.bottom-windowRect.top+10; // it's ugly, but .rc coordinates don't match actual pixels and it screws // up both the size and the aspect ratio // TODO: Could be scrollable in case the framebuf is larger? Also should be better positioned. RECT frameRect; HWND frameWnd = GetDlgItem(m_hDlg,IDC_GEDBG_FRAME); GetWindowRect(frameWnd,&frameRect); MapWindowPoints(HWND_DESKTOP,m_hDlg,(LPPOINT)&frameRect,2); MoveWindow(frameWnd,frameRect.left,frameRect.top,512,272,TRUE); tabs = new TabControl(GetDlgItem(m_hDlg,IDC_GEDBG_MAINTAB)); HWND wnd = tabs->AddTabWindow(L"CtrlDisplayListView",L"Display List"); displayList = CtrlDisplayListView::getFrom(wnd); fbTabs = new TabControl(GetDlgItem(m_hDlg, IDC_GEDBG_FBTABS)); fbTabs->SetMinTabWidth(50); // Must be in the same order as PrimaryDisplayType. fbTabs->AddTab(NULL, L"Color"); fbTabs->AddTab(NULL, L"Depth"); fbTabs->AddTab(NULL, L"Stencil"); fbTabs->ShowTab(0, true); flags = new TabStateFlags(_hInstance, m_hDlg); tabs->AddTabDialog(flags, L"Flags"); lighting = new TabStateLighting(_hInstance, m_hDlg); tabs->AddTabDialog(lighting, L"Lighting"); textureState = new TabStateTexture(_hInstance, m_hDlg); tabs->AddTabDialog(textureState, L"Texture"); settings = new TabStateSettings(_hInstance, m_hDlg); tabs->AddTabDialog(settings, L"Settings"); lists = new TabDisplayLists(_hInstance, m_hDlg); tabs->AddTabDialog(lists, L"Lists"); tabs->ShowTab(0, true); // set window position int x = g_Config.iGEWindowX == -1 ? windowRect.left : g_Config.iGEWindowX; int y = g_Config.iGEWindowY == -1 ? windowRect.top : g_Config.iGEWindowY; int w = g_Config.iGEWindowW == -1 ? minWidth : g_Config.iGEWindowW; int h = g_Config.iGEWindowH == -1 ? minHeight : g_Config.iGEWindowH; MoveWindow(m_hDlg,x,y,w,h,FALSE); } CGEDebugger::~CGEDebugger() { delete flags; delete lighting; delete textureState; delete settings; delete lists; delete tabs; delete fbTabs; } void CGEDebugger::SetupPreviews() { if (frameWindow == NULL) { frameWindow = SimpleGLWindow::GetFrom(GetDlgItem(m_hDlg, IDC_GEDBG_FRAME)); frameWindow->Initialize(SimpleGLWindow::ALPHA_IGNORE | SimpleGLWindow::RESIZE_SHRINK_CENTER); frameWindow->Clear(); } if (texWindow == NULL) { texWindow = SimpleGLWindow::GetFrom(GetDlgItem(m_hDlg, IDC_GEDBG_TEX)); texWindow->Initialize(SimpleGLWindow::ALPHA_BLEND | SimpleGLWindow::RESIZE_SHRINK_CENTER); texWindow->Clear(); } } void CGEDebugger::UpdatePreviews() { // TODO: Do something different if not paused? wchar_t desc[256]; GPUDebugBuffer *primaryBuffer = NULL; GPUgstate state; bufferResult = false; if (gpuDebug != NULL) { state = gpuDebug->GetGState(); } switch (PrimaryDisplayType(fbTabs->CurrentTabIndex())) { case PRIMARY_FRAMEBUF: SetPauseAction(PAUSE_GETFRAMEBUF); primaryBuffer = &bufferFrame; _snwprintf(desc, ARRAY_SIZE(desc), L"Color: 0x%08x (%dx%d)", state.getFrameBufRawAddress(), primaryBuffer->GetStride(), primaryBuffer->GetHeight()); break; case PRIMARY_DEPTHBUF: SetPauseAction(PAUSE_GETDEPTHBUF); primaryBuffer = &bufferDepth; _snwprintf(desc, ARRAY_SIZE(desc), L"Depth: 0x%08x (%dx%d)", state.getDepthBufRawAddress(), primaryBuffer->GetStride(), primaryBuffer->GetHeight()); break; case PRIMARY_STENCILBUF: SetPauseAction(PAUSE_GETSTENCILBUF); primaryBuffer = &bufferStencil; _snwprintf(desc, ARRAY_SIZE(desc), L"Stencil: 0x%08x (%dx%d)", state.getFrameBufRawAddress(), primaryBuffer->GetStride(), primaryBuffer->GetHeight()); break; } if (bufferResult && primaryBuffer != NULL) { auto fmt = SimpleGLWindow::Format(primaryBuffer->GetFormat()); frameWindow->Draw(primaryBuffer->GetData(), primaryBuffer->GetStride(), primaryBuffer->GetHeight(), primaryBuffer->GetFlipped(), fmt); SetDlgItemText(m_hDlg, IDC_GEDBG_FRAMEBUFADDR, desc); } else { frameWindow->Clear(); SetDlgItemText(m_hDlg, IDC_GEDBG_FRAMEBUFADDR, L"Failed"); } bufferResult = false; SetPauseAction(PAUSE_GETTEX); if (bufferResult) { auto fmt = SimpleGLWindow::Format(bufferTex.GetFormat()); texWindow->Draw(bufferTex.GetData(), bufferTex.GetStride(), bufferTex.GetHeight(), bufferTex.GetFlipped(), fmt); if (gpuDebug != NULL) { if (state.isTextureAlphaUsed()) { texWindow->SetFlags(SimpleGLWindow::ALPHA_BLEND | SimpleGLWindow::RESIZE_SHRINK_CENTER); } else { texWindow->SetFlags(SimpleGLWindow::RESIZE_SHRINK_CENTER); } _snwprintf(desc, ARRAY_SIZE(desc), L"Texture: 0x%08x (%dx%d)", state.getTextureAddress(0), state.getTextureWidth(0), state.getTextureHeight(0)); SetDlgItemText(m_hDlg, IDC_GEDBG_TEXADDR, desc); lastTexture = state.getTextureAddress(0); } else { lastTexture = -1; } } else { texWindow->Clear(); if (gpuDebug == NULL || state.isTextureMapEnabled()) { SetDlgItemText(m_hDlg, IDC_GEDBG_TEXADDR, L"Texture: failed"); } else { SetDlgItemText(m_hDlg, IDC_GEDBG_TEXADDR, L"Texture: disabled"); } lastTexture = -1; } DisplayList list; if (gpuDebug != NULL && gpuDebug->GetCurrentDisplayList(list)) { displayList->setDisplayList(list); } flags->Update(); lighting->Update(); textureState->Update(); settings->Update(); lists->Update(); } void CGEDebugger::UpdateSize(WORD width, WORD height) { // only resize the tab for now HWND tabControl = GetDlgItem(m_hDlg, IDC_GEDBG_MAINTAB); RECT tabRect; GetWindowRect(tabControl,&tabRect); MapWindowPoints(HWND_DESKTOP,m_hDlg,(LPPOINT)&tabRect,2); tabRect.right = tabRect.left + (width-tabRect.left*2); // assume same gap on both sides tabRect.bottom = tabRect.top + (height-tabRect.top-tabRect.left); // assume same gap on bottom too MoveWindow(tabControl,tabRect.left,tabRect.top,tabRect.right-tabRect.left,tabRect.bottom-tabRect.top,TRUE); } void CGEDebugger::SavePosition() { RECT rc; if (GetWindowRect(m_hDlg, &rc)) { g_Config.iGEWindowX = rc.left; g_Config.iGEWindowY = rc.top; g_Config.iGEWindowW = rc.right - rc.left; g_Config.iGEWindowH = rc.bottom - rc.top; } } void CGEDebugger::SetBreakNext(BreakNextType type) { attached = true; SetupPreviews(); SetPauseAction(PAUSE_CONTINUE, false); breakNext = type; } BOOL CGEDebugger::DlgProc(UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: return TRUE; case WM_GETMINMAXINFO: { MINMAXINFO* minmax = (MINMAXINFO*) lParam; minmax->ptMinTrackSize.x = minWidth; minmax->ptMinTrackSize.y = minHeight; } return TRUE; case WM_SIZE: UpdateSize(LOWORD(lParam), HIWORD(lParam)); SavePosition(); return TRUE; case WM_MOVE: SavePosition(); return TRUE; case WM_CLOSE: Show(false); return TRUE; case WM_SHOWWINDOW: SetupPreviews(); break; case WM_ACTIVATE: if (wParam == WA_ACTIVE || wParam == WA_CLICKACTIVE) { g_activeWindow = WINDOW_GEDEBUGGER; } break; case WM_NOTIFY: switch (wParam) { case IDC_GEDBG_MAINTAB: tabs->HandleNotify(lParam); break; case IDC_GEDBG_FBTABS: fbTabs->HandleNotify(lParam); // TODO: Move this somewhere... if (attached && gpuDebug != NULL) { UpdatePreviews(); } break; } break; case WM_COMMAND: switch (LOWORD(wParam)) { case IDC_GEDBG_STEPDRAW: SetBreakNext(BREAK_NEXT_DRAW); break; case IDC_GEDBG_STEP: SetBreakNext(BREAK_NEXT_OP); break; case IDC_GEDBG_STEPTEX: SetBreakNext(BREAK_NEXT_TEX); break; case IDC_GEDBG_STEPFRAME: SetBreakNext(BREAK_NEXT_FRAME); break; case IDC_GEDBG_BREAKTEX: { u32 texAddr; // TODO: Better interface that allows add/remove or something. if (InputBox_GetHex(GetModuleHandle(NULL), m_hDlg, L"Texture Address", lastTexture, texAddr)) { if (breakTextures.find(texAddr) != breakTextures.end()) { breakTextures.erase(texAddr); } else { breakTextures.insert(texAddr); } } } break; case IDC_GEDBG_RESUME: frameWindow->Clear(); texWindow->Clear(); SetDlgItemText(m_hDlg, IDC_GEDBG_FRAMEBUFADDR, L""); SetDlgItemText(m_hDlg, IDC_GEDBG_TEXADDR, L""); // TODO: detach? Should probably have separate UI, or just on activate? SetPauseAction(PAUSE_CONTINUE, false); breakNext = BREAK_NONE; break; } break; case WM_GEDBG_BREAK_CMD: { u32 pc = (u32)wParam; tempBreakpoint = -1; auto info = gpuDebug->DissassembleOp(pc); NOTICE_LOG(COMMON, "Waiting at %08x, %s", pc, info.desc.c_str()); UpdatePreviews(); } break; case WM_GEDBG_BREAK_DRAW: { NOTICE_LOG(COMMON, "Waiting at a draw"); UpdatePreviews(); } break; case WM_GEDBG_STEPDISPLAYLIST: SetBreakNext(BREAK_NEXT_OP); break; case WM_GEDBG_TOGGLEPCBREAKPOINT: { lock_guard guard(breaksLock); u32 pc = (u32)wParam; auto iter = breakPCs.find(pc); if (iter != breakPCs.end()) breakPCs.erase(iter); else breakPCs.insert(pc); } break; case WM_GEDBG_RUNTOWPARAM: { lock_guard guard(breaksLock); u32 pc = (u32)wParam; tempBreakpoint = pc; SendMessage(m_hDlg,WM_COMMAND,IDC_GEDBG_RESUME,0); } break; case WM_GEDBG_SETCMDWPARAM: { pauseSetCmdValue = (u32)wParam; SetPauseAction(PAUSE_SETCMDVALUE); } break; } return FALSE; } // The below WindowsHost methods are called on the GPU thread. bool WindowsHost::GPUDebuggingActive() { return attached; } static void PauseWithMessage(UINT msg, WPARAM wParam = NULL, LPARAM lParam = NULL) { lock_guard guard(pauseLock); if (coreState != CORE_RUNNING && coreState != CORE_NEXTFRAME) { return; } PostMessage(geDebuggerWindow->GetDlgHandle(), msg, wParam, lParam); // Just to be sure. if (pauseAction == PAUSE_CONTINUE) { pauseAction = PAUSE_BREAK; } do { RunPauseAction(); pauseWait.wait(pauseLock); } while (pauseAction != PAUSE_CONTINUE); } void WindowsHost::GPUNotifyCommand(u32 pc) { u32 op = Memory::ReadUnchecked_U32(pc); u8 cmd = op >> 24; if (breakNext == BREAK_NEXT_OP || CGEDebugger::IsOpOrTextureBreakPoint(op) || CGEDebugger::IsAddressBreakPoint(pc) || pc == tempBreakpoint) { PauseWithMessage(WM_GEDBG_BREAK_CMD, (WPARAM) pc); } } void WindowsHost::GPUNotifyDisplay(u32 framebuf, u32 stride, int format) { if (breakNext == BREAK_NEXT_FRAME) { // This should work fine, start stepping at the first op of the new frame. breakNext = BREAK_NEXT_OP; } } void WindowsHost::GPUNotifyDraw() { if (breakNext == BREAK_NEXT_DRAW) { PauseWithMessage(WM_GEDBG_BREAK_DRAW); } } void WindowsHost::GPUNotifyTextureAttachment(u32 addr) { } bool WindowsHost::GPUAllowTextureCache(u32 addr) { return textureCaching; }