From 0206d9b2d052ab73c8494eded0d58b688dd393b1 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 6 Sep 2023 01:25:41 +1000 Subject: [PATCH] wip --- src/core/bios.cpp | 57 +++++++- src/core/bios.h | 21 ++- src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 2 + src/core/cpu_stack_walk.cpp | 194 ++++++++++++++++++++++++++ src/core/cpu_stack_walk.h | 26 ++++ src/duckstation-qt/debuggermodels.cpp | 12 ++ src/duckstation-qt/debuggermodels.h | 42 +++--- src/duckstation-qt/debuggerwindow.cpp | 78 ++++++++++- src/duckstation-qt/debuggerwindow.h | 2 + src/duckstation-qt/debuggerwindow.ui | 42 ++++-- 11 files changed, 442 insertions(+), 36 deletions(-) create mode 100644 src/core/cpu_stack_walk.cpp create mode 100644 src/core/cpu_stack_walk.h diff --git a/src/core/bios.cpp b/src/core/bios.cpp index b742e6ce9..e547765b6 100644 --- a/src/core/bios.cpp +++ b/src/core/bios.cpp @@ -1,16 +1,21 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin and contributors. +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin and contributors. // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "bios.h" + +#include "bus.h" +#include "cpu_disasm.h" +#include "host.h" +#include "settings.h" + #include "common/assert.h" #include "common/file_system.h" #include "common/log.h" #include "common/md5_digest.h" #include "common/path.h" -#include "cpu_disasm.h" -#include "host.h" -#include "settings.h" + #include + Log_SetChannel(BIOS); static constexpr BIOS::Hash MakeHashFromString(const char str[]) @@ -426,3 +431,47 @@ bool BIOS::HasAnyBIOSImages() { return FindBIOSImageInDirectory(ConsoleRegion::Auto, EmuFolders::Bios.c_str()).has_value(); } + +std::span BIOS::GetTCBs() +{ + if (!Bus::g_ram) + return {}; + + u32 base_address, num_threads; + std::memcpy(&base_address, &Bus::g_ram[0x110], sizeof(base_address)); + std::memcpy(&num_threads, &Bus::g_ram[0x114], sizeof(num_threads)); + + base_address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK; + num_threads /= sizeof(ThreadControlBlock); + + if (base_address == 0 || num_threads == 0 || + (base_address + num_threads * sizeof(ThreadControlBlock)) > Bus::RAM_2MB_SIZE) + { + return {}; + } + + return std::span(reinterpret_cast(&Bus::g_ram[base_address]), + num_threads); +} + +const BIOS::ThreadControlBlock* BIOS::GetCurrentThreadTCB() +{ + if (!Bus::g_ram) + return nullptr; + + u32 pcb_base_address; + std::memcpy(&pcb_base_address, &Bus::g_ram[0x108], sizeof(pcb_base_address)); + pcb_base_address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK; + + if (pcb_base_address == 0 || (pcb_base_address + sizeof(u32)) > Bus::RAM_2MB_SIZE) + return nullptr; + + u32 tcb_address; + std::memcpy(&tcb_address, &Bus::g_ram[pcb_base_address], sizeof(tcb_address)); + tcb_address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK; + + if (tcb_address == 0 || (tcb_address + sizeof(ThreadControlBlock)) > Bus::RAM_2MB_SIZE) + return nullptr; + + return reinterpret_cast(&Bus::g_ram[tcb_address]); +} diff --git a/src/core/bios.h b/src/core/bios.h index d5f114abb..2b8d226a2 100644 --- a/src/core/bios.h +++ b/src/core/bios.h @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin . +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin . // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once #include "types.h" #include +#include #include #include #include @@ -88,4 +89,22 @@ std::vector> FindBIOSImagesInDire /// Returns true if any BIOS images are found in the configured BIOS directory. bool HasAnyBIOSImages(); + +#pragma pack(push, 1) +struct ThreadControlBlock +{ + u32 status; + u32 unused; + u32 regs[32]; + u32 epc; + u32 hi; + u32 lo; + u32 sr; + u32 cause; + u32 unused2[9]; +}; +#pragma pack(pop) + +std::span GetTCBs(); +const ThreadControlBlock* GetCurrentThreadTCB(); } // namespace BIOS \ No newline at end of file diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 1b9275f2f..15736d27c 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -31,6 +31,7 @@ true + @@ -101,6 +102,7 @@ true + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index fb7699b44..5f6923a20 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -60,6 +60,7 @@ + @@ -124,5 +125,6 @@ + \ No newline at end of file diff --git a/src/core/cpu_stack_walk.cpp b/src/core/cpu_stack_walk.cpp new file mode 100644 index 000000000..fbbedbae3 --- /dev/null +++ b/src/core/cpu_stack_walk.cpp @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: 2012 PPSSPP Project, 2014-2014 PCSX2 Dev Team, 2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-2.0+) + +#include "cpu_stack_walk.h" +#include "bus.h" +#include "cpu_core_private.h" + +#define _RS ((rawOp >> 21) & 0x1F) +#define _RT ((rawOp >> 16) & 0x1F) +#define _RD ((rawOp >> 11) & 0x1F) +#define _IMM16 ((signed short)(rawOp & 0xFFFF)) +#define MIPS_REG_SP 29 +#define MIPS_REG_FP 30 +#define MIPS_REG_RA 31 + +#define INVALIDTARGET 0xFFFFFFFF + +#define MIPSTABLE_IMM_MASK 0xFC000000 +#define MIPSTABLE_SPECIAL_MASK 0xFC00003F + +namespace MipsStackWalk { +// In the worst case, we scan this far above the pc for an entry. +//constexpr int MAX_FUNC_SIZE = 32768 * 4; +constexpr int MAX_FUNC_SIZE = 1024 * 4; +// After this we assume we're stuck. +constexpr size_t MAX_DEPTH = 1024; + +static bool IsSWInstr(u32 rawOp) +{ + return (rawOp & MIPSTABLE_IMM_MASK) == 0xAC000000; +} + +static bool IsAddImmInstr(u32 rawOp) +{ + return (rawOp & MIPSTABLE_IMM_MASK) == 0x20000000 || (rawOp & MIPSTABLE_IMM_MASK) == 0x24000000; +} + +static bool IsMovRegsInstr(u32 rawOp) +{ + if ((rawOp & MIPSTABLE_SPECIAL_MASK) == 0x00000021) + { + return _RS == 0 || _RT == 0; + } + return false; +} + +static bool IsValidAddress(u32 addr) +{ + return (addr & CPU::PHYSICAL_MEMORY_ADDRESS_MASK) < Bus::RAM_2MB_SIZE; +} + +static bool ScanForAllocaSignature(u32 pc) +{ + // In God Eater Burst, for example, after 0880E750, there's what looks like an alloca(). + // It's surrounded by "mov fp, sp" and "mov sp, fp", which is unlikely to be used for other reasons. + + // It ought to be pretty close. + u32 stop = pc - 32 * 4; + for (; IsValidAddress(pc) && pc >= stop; pc -= 4) + { + u32 rawOp; + if (!CPU::SafeReadMemoryWord(pc, &rawOp)) + return false; + + // We're looking for a "mov fp, sp" close by a "addiu sp, sp, -N". + if (IsMovRegsInstr(rawOp) && _RD == MIPS_REG_FP && (_RS == MIPS_REG_SP || _RT == MIPS_REG_SP)) + { + return true; + } + } + return false; +} + +static bool ScanForEntry(StackFrame& frame, u32 entry, u32& ra) +{ + // Let's hope there are no > 1MB functions on the PSP, for the sake of humanity... + const u32 LONGEST_FUNCTION = 1024 * 1024; + // TODO: Check if found entry is in the same symbol? Might be wrong sometimes... + + int ra_offset = -1; + const u32 start = frame.pc; + u32 stop = entry; + if (entry == INVALIDTARGET) + { + stop = 0x80000; + } + if (stop < start - LONGEST_FUNCTION) + { + stop = (LONGEST_FUNCTION > start) ? 0 : (start - LONGEST_FUNCTION); + } + for (u32 pc = start; IsValidAddress(pc) && pc >= stop; pc -= 4) + { + u32 rawOp; + if (!CPU::SafeReadMemoryWord(pc, &rawOp)) + return false; + + // Here's where they store the ra address. + if (IsSWInstr(rawOp) && _RT == MIPS_REG_RA && _RS == MIPS_REG_SP) + { + ra_offset = _IMM16; + } + + if (IsAddImmInstr(rawOp) && _RT == MIPS_REG_SP && _RS == MIPS_REG_SP) + { + // A positive imm either means alloca() or we went too far. + if (_IMM16 > 0) + { + // TODO: Maybe check for any alloca() signature and bail? + continue; + } + if (ScanForAllocaSignature(pc)) + { + continue; + } + + frame.entry = pc; + frame.stackSize = -_IMM16; + if (ra_offset != -1 && IsValidAddress(frame.sp + ra_offset)) + { + CPU::SafeReadMemoryWord(frame.sp + ra_offset, &ra); + } + return true; + } + } + return false; +} + +static bool DetermineFrameInfo(StackFrame& frame, u32 possibleEntry, u32 threadEntry, u32& ra) +{ + if (ScanForEntry(frame, possibleEntry, ra)) + { + // Awesome, found one that looks right. + return true; + } + else if (ra != INVALIDTARGET && possibleEntry != INVALIDTARGET) + { + // Let's just assume it's a leaf. + frame.entry = possibleEntry; + frame.stackSize = 0; + return true; + } + + // Okay, we failed to get one. Our possibleEntry could be wrong, it often is. + // Let's just scan upward. + u32 newPossibleEntry = frame.pc > threadEntry ? threadEntry : frame.pc - MAX_FUNC_SIZE; + return ScanForEntry(frame, newPossibleEntry, ra); +} + +std::vector Walk(u32 pc, u32 ra, u32 sp, u32 threadEntry, u32 threadStackTop) +{ + std::vector frames; + StackFrame current; + current.pc = pc; + current.sp = sp; + current.entry = INVALIDTARGET; + current.stackSize = -1; + + u32 prevEntry = INVALIDTARGET; + while (pc != threadEntry) + { + u32 possibleEntry = INVALIDTARGET; // GuessEntry(cpu, current.pc); + if (DetermineFrameInfo(current, possibleEntry, threadEntry, ra)) + { + frames.push_back(current); + if (current.entry == threadEntry /*|| GuessEntry(cpu, current.entry) == threadEntry*/) + { + break; + } + if (current.entry == prevEntry || frames.size() >= MAX_DEPTH) + { + // Recursion, means we're screwed. Let's just give up. + break; + } + prevEntry = current.entry; + + current.pc = ra; + current.sp += current.stackSize; + ra = INVALIDTARGET; + current.entry = INVALIDTARGET; + current.stackSize = -1; + } + else + { + // Well, we got as far as we could. + current.entry = possibleEntry; + current.stackSize = 0; + frames.push_back(current); + break; + } + } + + return frames; +} +}; // namespace MipsStackWalk diff --git a/src/core/cpu_stack_walk.h b/src/core/cpu_stack_walk.h new file mode 100644 index 000000000..68c6a5266 --- /dev/null +++ b/src/core/cpu_stack_walk.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2012 PPSSPP Project, 2014-2014 PCSX2 Dev Team, 2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-2.0+) + +#pragma once + +#include "cpu_types.h" + +#include + +namespace MipsStackWalk { + +struct StackFrame +{ + // Beginning of function symbol (may be estimated.) + u32 entry; + // Next position within function. + u32 pc; + // Value of SP inside this function (assuming no alloca()...) + u32 sp; + // Size of stack frame in bytes. + int stackSize; +}; + +std::vector Walk(u32 pc, u32 ra, u32 sp, u32 threadEntry, u32 threadStackTop); + +}; // namespace MipsStackWalk \ No newline at end of file diff --git a/src/duckstation-qt/debuggermodels.cpp b/src/duckstation-qt/debuggermodels.cpp index 56038b179..fd47e4d76 100644 --- a/src/duckstation-qt/debuggermodels.cpp +++ b/src/duckstation-qt/debuggermodels.cpp @@ -140,6 +140,8 @@ QVariant DebuggerCodeModel::data(const QModelIndex& index, int role /*= Qt::Disp // return QApplication::palette().toolTipBase(); if (address == m_last_pc) return QColor(100, 100, 0); + else if (address == m_last_highlight_address) + return QColor(80, 80, 80); else return QVariant(); } @@ -244,6 +246,16 @@ void DebuggerCodeModel::setPC(VirtualMemoryAddress pc) } } +void DebuggerCodeModel::setHighlightAddress(VirtualMemoryAddress address) +{ + if (m_last_highlight_address == address) + return; + + emitDataChangedForAddress(m_last_highlight_address); + m_last_highlight_address = address; + emitDataChangedForAddress(address); +} + void DebuggerCodeModel::ensureAddressVisible(VirtualMemoryAddress address) { updateRegion(address); diff --git a/src/duckstation-qt/debuggermodels.h b/src/duckstation-qt/debuggermodels.h index 2836b5497..b12f10879 100644 --- a/src/duckstation-qt/debuggermodels.h +++ b/src/duckstation-qt/debuggermodels.h @@ -1,27 +1,29 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once +#include "core/bios.h" #include "core/bus.h" #include "core/cpu_core.h" #include "core/cpu_types.h" + #include #include #include #include -class DebuggerCodeModel : public QAbstractTableModel +class DebuggerCodeModel final : public QAbstractTableModel { Q_OBJECT public: DebuggerCodeModel(QObject* parent = nullptr); - virtual ~DebuggerCodeModel(); + ~DebuggerCodeModel() override; - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; - virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; - virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; // Returns the row for this instruction pointer void resetCodeView(VirtualMemoryAddress start_address); @@ -30,6 +32,7 @@ public: VirtualMemoryAddress getAddressForRow(int row) const; VirtualMemoryAddress getAddressForIndex(QModelIndex index) const; void setPC(VirtualMemoryAddress pc); + void setHighlightAddress(VirtualMemoryAddress address); void ensureAddressVisible(VirtualMemoryAddress address); void setBreakpointList(std::vector bps); void setBreakpointState(VirtualMemoryAddress address, bool enabled); @@ -45,24 +48,25 @@ private: VirtualMemoryAddress m_code_region_start = 0; VirtualMemoryAddress m_code_region_end = 0; VirtualMemoryAddress m_last_pc = 0; + VirtualMemoryAddress m_last_highlight_address = 0xFFFFFFFFu; std::vector m_breakpoints; QPixmap m_pc_pixmap; QPixmap m_breakpoint_pixmap; }; -class DebuggerRegistersModel : public QAbstractListModel +class DebuggerRegistersModel final : public QAbstractListModel { Q_OBJECT public: DebuggerRegistersModel(QObject* parent = nullptr); - virtual ~DebuggerRegistersModel(); + ~DebuggerRegistersModel() override; - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; - virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; - virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void invalidateView(); void saveCurrentValues(); @@ -71,18 +75,18 @@ private: u32 m_old_reg_values[CPU::NUM_DEBUGGER_REGISTER_LIST_ENTRIES] = {}; }; -class DebuggerStackModel : public QAbstractListModel +class DebuggerStackModel final : public QAbstractListModel { Q_OBJECT public: DebuggerStackModel(QObject* parent = nullptr); - virtual ~DebuggerStackModel(); + ~DebuggerStackModel() override; - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; - virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; - virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void invalidateView(); }; diff --git a/src/duckstation-qt/debuggerwindow.cpp b/src/duckstation-qt/debuggerwindow.cpp index 786146f62..b9f0af8cf 100644 --- a/src/duckstation-qt/debuggerwindow.cpp +++ b/src/duckstation-qt/debuggerwindow.cpp @@ -1,12 +1,18 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "debuggerwindow.h" -#include "common/assert.h" -#include "core/cpu_core_private.h" #include "debuggermodels.h" #include "qthost.h" #include "qtutils.h" + +#include "core/bios.h" +#include "core/bus.h" +#include "core/cpu_core_private.h" +#include "core/cpu_stack_walk.h" + +#include "common/assert.h" + #include #include #include @@ -57,6 +63,7 @@ void DebuggerWindow::refreshAll() m_registers_model->invalidateView(); m_stack_model->invalidateView(); m_ui.memoryView->repaint(); + updateThreadsList(); m_code_model->setPC(CPU::g_state.pc); scrollToPC(); @@ -70,6 +77,7 @@ void DebuggerWindow::scrollToPC() void DebuggerWindow::scrollToCodeAddress(VirtualMemoryAddress address) { m_code_model->ensureAddressVisible(address); + m_code_model->setHighlightAddress(address); int row = m_code_model->getRowForAddress(address); if (row >= 0) @@ -388,6 +396,23 @@ void DebuggerWindow::onMemorySearchStringChanged(const QString&) m_next_memory_search_address = 0; } +void DebuggerWindow::onThreadsItemDoubleClicked(const QTreeWidgetItem* item, int column) +{ + if (column != 0) + return; + + const QVariant vdata = item->data(0, Qt::UserRole); + if (!vdata.isValid()) + return; + + bool okay; + uint addr = vdata.toUInt(&okay); + if (!okay) + return; + + scrollToCodeAddress(addr); +} + void DebuggerWindow::closeEvent(QCloseEvent* event) { g_emu_thread->disconnect(this); @@ -450,6 +475,8 @@ void DebuggerWindow::connectSignals() connect(m_ui.memorySearch, &QPushButton::clicked, this, &DebuggerWindow::onMemorySearchTriggered); connect(m_ui.memorySearchString, &QLineEdit::textChanged, this, &DebuggerWindow::onMemorySearchStringChanged); + + connect(m_ui.threadsView, &QTreeWidget::itemDoubleClicked, this, &DebuggerWindow::onThreadsItemDoubleClicked); } void DebuggerWindow::disconnectSignals() @@ -614,3 +641,48 @@ void DebuggerWindow::refreshBreakpointList() m_ui.breakpointsWidget->addTopLevelItem(item); } } + +void DebuggerWindow::updateThreadsList() +{ + const std::span tcbs = BIOS::GetTCBs(); + const BIOS::ThreadControlBlock* current_tcb = BIOS::GetCurrentThreadTCB(); + + while (m_ui.threadsView->topLevelItemCount() > 0) + delete m_ui.threadsView->takeTopLevelItem(0); + + for (u32 i = 0; i < tcbs.size(); i++) + { + const BIOS::ThreadControlBlock& tcb = tcbs[i]; + const bool is_current = (&tcb == current_tcb); + const bool is_unused = (tcb.status & 0x4000) != 0x4000; + + // This is awful. + // TODO: Only walk stack when it's the current thread... + QTreeWidgetItem* item = new QTreeWidgetItem(m_ui.threadsView); + item->setText(0, + QString::asprintf("Thread %08X%s", static_cast(reinterpret_cast(&tcb) - Bus::g_ram), + is_current ? " [current]" : (is_unused ? "[unused]" : ""))); + if (is_unused) + continue; + + const u32 pc = is_current ? CPU::g_state.pc : tcb.regs[static_cast(CPU::Reg::ra)]; + const u32 ra = is_current ? CPU::g_state.regs.ra : tcb.regs[static_cast(CPU::Reg::ra)]; + const u32 sp = is_current ? CPU::g_state.regs.sp : tcb.regs[static_cast(CPU::Reg::sp)]; + + if (is_current) + { + const std::vector stack = MipsStackWalk::Walk(pc, ra, sp, 0xFFFFFFFFu, 0xFFFFFFFFu); + for (const MipsStackWalk::StackFrame& frame : stack) + { + QTreeWidgetItem* framei = new QTreeWidgetItem(item); + framei->setData(0, Qt::UserRole, QVariant(frame.pc)); + framei->setText(0, QString::asprintf("0x%08X", frame.pc)); + } + } + else + { + QTreeWidgetItem* frame = new QTreeWidgetItem(item); + frame->setText(0, QString::asprintf("0x%08X", pc)); + } + } +} diff --git a/src/duckstation-qt/debuggerwindow.h b/src/duckstation-qt/debuggerwindow.h index c4cbc54f1..cc0b14077 100644 --- a/src/duckstation-qt/debuggerwindow.h +++ b/src/duckstation-qt/debuggerwindow.h @@ -59,6 +59,8 @@ private Q_SLOTS: void onMemorySearchTriggered(); void onMemorySearchStringChanged(const QString&); + void onThreadsItemDoubleClicked(const QTreeWidgetItem* item, int column); + void updateThreadsList(); private: void setupAdditionalUi(); diff --git a/src/duckstation-qt/debuggerwindow.ui b/src/duckstation-qt/debuggerwindow.ui index ac04ec0e4..fce9e1121 100644 --- a/src/duckstation-qt/debuggerwindow.ui +++ b/src/duckstation-qt/debuggerwindow.ui @@ -6,7 +6,7 @@ 0 0 - 1210 + 1299 800 @@ -22,8 +22,8 @@ 0 0 - 1210 - 21 + 1299 + 22 @@ -36,15 +36,14 @@ - - + + - @@ -82,7 +81,7 @@ - + @@ -225,6 +224,33 @@ + + + + 200 + 524287 + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Threads + + + 8 + + + + true + + + + 1 + + + + @@ -481,8 +507,6 @@ Ctrl+T - -