diff --git a/CMakeLists.txt b/CMakeLists.txt index 75dc4fe..68752bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,5 +288,5 @@ if(BUILD_TESTING) ) endif() endif() -add_subdirectory(plugins/ProcessMemoryWindows) +add_subdirectory(plugins/ProcessMemory) add_subdirectory(plugins/WinDbgMemory) diff --git a/plugins/ProcessMemoryWindows/CMakeLists.txt b/plugins/ProcessMemory/CMakeLists.txt similarity index 61% rename from plugins/ProcessMemoryWindows/CMakeLists.txt rename to plugins/ProcessMemory/CMakeLists.txt index c28ea3b..4bd2e9b 100644 --- a/plugins/ProcessMemoryWindows/CMakeLists.txt +++ b/plugins/ProcessMemory/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(ProcessMemoryWindowsPlugin LANGUAGES CXX) +project(ProcessMemoryPlugin LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON) # Plugin sources set(PLUGIN_SOURCES - ProcessMemoryWindowsPlugin.h - ProcessMemoryWindowsPlugin.cpp + ProcessMemoryPlugin.h + ProcessMemoryPlugin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui ) # Create shared library (DLL) -add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES}) +add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES}) # Link Qt -target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS}) +target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS}) # Platform-specific linking if(WIN32) - target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32) + target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32) endif() # On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported if(UNIX AND NOT APPLE) - target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden) + target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden) endif() # Include directories -target_include_directories(ProcessMemoryWindowsPlugin PRIVATE +target_include_directories(ProcessMemoryPlugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../src ) # Output to Plugins folder -set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES +set_target_properties(ProcessMemoryPlugin PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" ) diff --git a/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.cpp b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp similarity index 90% rename from plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.cpp rename to plugins/ProcessMemory/ProcessMemoryPlugin.cpp index 4ff2c4f..81dd366 100644 --- a/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.cpp +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp @@ -1,4 +1,4 @@ -#include "ProcessMemoryWindowsPlugin.h" +#include "ProcessMemoryPlugin.h" #include "../../src/processpicker.h" @@ -32,12 +32,12 @@ #endif // ────────────────────────────────────────────────────────────────────────── -// ProcessMemoryWindowsProvider implementation +// ProcessMemoryProvider implementation // ────────────────────────────────────────────────────────────────────────── #ifdef _WIN32 -ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName) +ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName) : m_handle(nullptr) , m_pid(pid) , m_processName(processName) @@ -60,7 +60,7 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q cacheModules(); } -bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const +bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const { if (!m_handle || len <= 0) return false; @@ -71,7 +71,7 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const return bytesRead > 0; } -bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len) +bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) { if (!m_handle || !m_writable || len <= 0) return false; @@ -81,7 +81,7 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len return false; } -QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const +QString ProcessMemoryProvider::getSymbol(uint64_t addr) const { for (const auto& mod : m_modules) { @@ -96,7 +96,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const return {}; } -void ProcessMemoryWindowsProvider::cacheModules() +void ProcessMemoryProvider::cacheModules() { HMODULE mods[1024]; DWORD needed = 0; @@ -126,7 +126,7 @@ void ProcessMemoryWindowsProvider::cacheModules() #elif defined(__linux__) -ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName) +ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName) : m_fd(-1) , m_pid(pid) , m_processName(processName) @@ -152,7 +152,7 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q } -bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const +bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const { if (m_fd < 0 || len <= 0) return false; @@ -176,7 +176,7 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const return nread == static_cast(len); } -bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len) +bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) { if (m_fd < 0 || !m_writable || len <= 0) return false; @@ -200,7 +200,7 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len return nwritten == static_cast(len); } -QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const +QString ProcessMemoryProvider::getSymbol(uint64_t addr) const { for (const auto& mod : m_modules) { @@ -215,7 +215,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const return {}; } -void ProcessMemoryWindowsProvider::cacheModules() +void ProcessMemoryProvider::cacheModules() { // Parse /proc//maps to discover loaded modules QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid); @@ -288,7 +288,7 @@ void ProcessMemoryWindowsProvider::cacheModules() #endif // platform -ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider() +ProcessMemoryProvider::~ProcessMemoryProvider() { #ifdef _WIN32 if (m_handle) @@ -299,7 +299,7 @@ ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider() #endif } -int ProcessMemoryWindowsProvider::size() const +int ProcessMemoryProvider::size() const { #ifdef _WIN32 return m_handle ? 0x10000 : 0; @@ -309,22 +309,22 @@ int ProcessMemoryWindowsProvider::size() const } // ────────────────────────────────────────────────────────────────────────── -// ProcessMemoryWindowsPlugin implementation +// ProcessMemoryPlugin implementation // ────────────────────────────────────────────────────────────────────────── -QIcon ProcessMemoryWindowsPlugin::Icon() const +QIcon ProcessMemoryPlugin::Icon() const { return qApp->style()->standardIcon(QStyle::SP_ComputerIcon); } -bool ProcessMemoryWindowsPlugin::canHandle(const QString& target) const +bool ProcessMemoryPlugin::canHandle(const QString& target) const { // Target format: "pid:name" or just "pid" QRegularExpression re("^\\d+"); return re.match(target).hasMatch(); } -std::unique_ptr ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg) +std::unique_ptr ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg) { // Parse target: "pid:name" or just "pid" QStringList parts = target.split(':'); @@ -339,7 +339,7 @@ std::unique_ptr ProcessMemoryWindowsPlugin::createProvider(const QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid); - auto provider = std::make_unique(pid, name); + auto provider = std::make_unique(pid, name); if (!provider->isValid()) { if (errorMsg) @@ -352,7 +352,7 @@ std::unique_ptr ProcessMemoryWindowsPlugin::createProvider(const return provider; } -uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const +uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const { #ifdef _WIN32 // Parse PID from target @@ -409,7 +409,7 @@ uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target #endif } -bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target) +bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target) { // Use custom process enumeration from plugin QVector pluginProcesses = enumerateProcesses(); @@ -440,7 +440,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target) return false; } -QVector ProcessMemoryWindowsPlugin::enumerateProcesses() +QVector ProcessMemoryPlugin::enumerateProcesses() { QVector processes; @@ -543,5 +543,5 @@ QVector ProcessMemoryWindowsPlugin::enumerateProcesses() extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin() { - return new ProcessMemoryWindowsPlugin(); + return new ProcessMemoryPlugin(); } diff --git a/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h similarity index 80% rename from plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.h rename to plugins/ProcessMemory/ProcessMemoryPlugin.h index 0394afd..5a5fa74 100644 --- a/plugins/ProcessMemoryWindows/ProcessMemoryWindowsPlugin.h +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -5,14 +5,14 @@ #include /** - * Process memory provider (Windows) - * Reads/writes memory from a live process using Windows platform APIs + * Process memory provider + * Reads/writes memory from a live process using platform APIs */ -class ProcessMemoryWindowsProvider : public rcx::Provider +class ProcessMemoryProvider : public rcx::Provider { public: - ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName); - ~ProcessMemoryWindowsProvider() override; + ProcessMemoryProvider(uint32_t pid, const QString& processName); + ~ProcessMemoryProvider() override; // Required overrides bool read(uint64_t addr, void* buf, int len) const override; @@ -57,15 +57,15 @@ private: }; /** - * Plugin that provides ProcessMemoryWindowsProvider + * Plugin that provides ProcessMemoryProvider */ -class ProcessMemoryWindowsPlugin : public IProviderPlugin +class ProcessMemoryPlugin : public IProviderPlugin { public: - std::string Name() const override { return "Process Memory Windows"; } + std::string Name() const override { return "Process Memory"; } std::string Version() const override { return "1.0.0"; } std::string Author() const override { return "Reclass"; } - std::string Description() const override { return "Read and write memory from local running processes (Windows)"; } + std::string Description() const override { return "Read and write memory from local running processes"; } k_ELoadType LoadType() const override { return k_ELoadTypeAuto; } QIcon Icon() const override; diff --git a/src/controller.cpp b/src/controller.cpp index a95da92..313ca8f 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -178,6 +178,14 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) { editor->applyDocument(m_lastResult); } updateCommandRow(); + + // Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start). + if (!m_cachedPopup) { + QTimer::singleShot(0, this, [this, editor]() { + if (!m_cachedPopup && !m_editors.isEmpty()) + ensurePopup(editor); + }); + } return editor; } diff --git a/src/editor.cpp b/src/editor.cpp index 3c3098d..b3593e1 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1330,7 +1330,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { // Single-click on editable token of already-selected node → edit int tLine, tCol; EditTarget t; if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) { - if (alreadySelected && plain) { + // Type/ArrayElementType/PointerTarget open a dismissible popup + // (not inline text edit), so allow on first click without + // requiring the node to be pre-selected. + bool isPopupTarget = (t == EditTarget::Type + || t == EditTarget::ArrayElementType + || t == EditTarget::PointerTarget); + if ((alreadySelected || isPopupTarget) && plain) { + if (!alreadySelected) + emit nodeClicked(h.line, h.nodeId, me->modifiers()); m_pendingClickNodeId = 0; return beginInlineEdit(t, tLine, tCol); } diff --git a/src/typeselectorpopup.cpp b/src/typeselectorpopup.cpp index b2de817..d1e25dd 100644 --- a/src/typeselectorpopup.cpp +++ b/src/typeselectorpopup.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "themes/thememanager.h" namespace rcx { @@ -384,10 +385,33 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent) } void TypeSelectorPopup::warmUp() { + // One-time per-process cost (~170ms): Qt lazily initializes the style/font/DLL + // subsystem the first time a popup with complex children is shown. Pre-pay it + // by briefly showing a throwaway dummy popup with a QListView, then show+hide + // ourselves. + { + auto* primer = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint); + primer->resize(300, 400); + auto* lay = new QVBoxLayout(primer); + lay->addWidget(new QLabel(QStringLiteral("x"))); + lay->addWidget(new QLineEdit); + auto* model = new QStringListModel(primer); + QStringList items; for (int i = 0; i < 10; i++) items << QStringLiteral("x"); + model->setStringList(items); + auto* lv = new QListView; + lv->setModel(model); + lay->addWidget(lv); + primer->show(); + QApplication::processEvents(); + primer->hide(); + QApplication::processEvents(); + delete primer; + } + TypeEntry dummy; dummy.entryKind = TypeEntry::Primitive; dummy.primitiveKind = NodeKind::Hex8; - dummy.displayName = "warmup"; + dummy.displayName = QStringLiteral("warmup"); setTypes({dummy}); popup(QPoint(-9999, -9999)); hide(); diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp index 954b89a..a0f7c8d 100644 --- a/tests/test_type_selector.cpp +++ b/tests/test_type_selector.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include "controller.h" #include "typeselectorpopup.h" @@ -198,6 +200,127 @@ private slots: } } + // ── Isolate first-show cost with different window flags ── + + void benchmarkFirstShow() { + auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); }; + + struct FlagTest { + const char* name; + Qt::WindowFlags flags; + }; + FlagTest tests[] = { + {"Qt::Popup|Frameless", Qt::Popup | Qt::FramelessWindowHint}, + {"Qt::Tool|Frameless", Qt::Tool | Qt::FramelessWindowHint}, + {"Qt::ToolTip", Qt::ToolTip}, + {"Qt::Window|Frameless", Qt::Window | Qt::FramelessWindowHint}, + {"Qt::Popup|Frameless (2nd)", Qt::Popup | Qt::FramelessWindowHint}, + }; + + for (const auto& test : tests) { + auto* f = new QFrame(nullptr, test.flags); + f->resize(300, 400); + + QElapsedTimer t; t.start(); + f->show(); + qint64 t1 = t.nsecsElapsed(); t.restart(); + QApplication::processEvents(); + qint64 t2 = t.nsecsElapsed(); + f->hide(); + QApplication::processEvents(); + + t.restart(); + f->show(); + qint64 t3 = t.nsecsElapsed(); t.restart(); + QApplication::processEvents(); + qint64 t4 = t.nsecsElapsed(); + f->hide(); + QApplication::processEvents(); + + qDebug() << ""; + qDebug().noquote() << QString("=== %1 ===").arg(test.name); + qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms") + .arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4)); + delete f; + } + + // TypeSelectorPopup: cold vs after warmUp + { + auto* popup = new TypeSelectorPopup(); + TypeEntry dummy; + dummy.entryKind = TypeEntry::Primitive; + dummy.primitiveKind = NodeKind::Hex8; + dummy.displayName = "test"; + popup->setTypes({dummy}); + + QElapsedTimer t; t.start(); + popup->show(); + qint64 t1 = t.nsecsElapsed(); t.restart(); + QApplication::processEvents(); + qint64 t2 = t.nsecsElapsed(); + popup->hide(); + QApplication::processEvents(); + + t.restart(); + popup->show(); + qint64 t3 = t.nsecsElapsed(); t.restart(); + QApplication::processEvents(); + qint64 t4 = t.nsecsElapsed(); + popup->hide(); + QApplication::processEvents(); + + qDebug() << ""; + qDebug().noquote() << QString("=== TypeSelectorPopup (cold, Qt::Popup) ==="); + qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms") + .arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4)); + delete popup; + } + + // Clean order test: dummy popup with children FIRST, then TypeSelectorPopup + qDebug() << ""; + qDebug() << "=== CLEAN: dummy popup first, then TypeSelectorPopup ==="; + { + auto* dummy = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint); + dummy->resize(300, 400); + auto* dLay = new QVBoxLayout(dummy); + dLay->addWidget(new QLabel("dummy")); + dLay->addWidget(new QLineEdit); + auto* dModel = new QStringListModel(dummy); + QStringList dItems; for (int i = 0; i < 10; i++) dItems << "x"; + dModel->setStringList(dItems); + auto* dLv = new QListView; dLv->setModel(dModel); + dLay->addWidget(dLv); + + QElapsedTimer t; t.start(); + dummy->show(); + qint64 t1 = t.nsecsElapsed(); t.restart(); + QApplication::processEvents(); + qint64 t2 = t.nsecsElapsed(); + dummy->hide(); + QApplication::processEvents(); + qDebug().noquote() << QString(" Dummy popup: show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2)); + delete dummy; + } + { + auto* popup = new TypeSelectorPopup(); + TypeEntry e; + e.entryKind = TypeEntry::Primitive; + e.primitiveKind = NodeKind::Hex8; + e.displayName = "test"; + popup->setTypes({e}); + popup->resize(300, 400); + QElapsedTimer t; t.start(); + popup->show(); + qint64 t1 = t.nsecsElapsed(); t.restart(); + QApplication::processEvents(); + qint64 t2 = t.nsecsElapsed(); + popup->hide(); + QApplication::processEvents(); + qDebug().noquote() << QString(" TypeSelectorPopup (after dummy): show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2)); + delete popup; + } + } + // ── Popup data model ── void testPopupListsRootStructs() {