Single-click type chooser, popup warmup fix, rename ProcessMemory plugin

- Type chooser popup now opens on single click (no need to pre-select node)
- Fix ~170ms first-open delay by pre-initializing Qt popup subsystem at startup
- Rename ProcessMemoryWindows -> ProcessMemory (already supports Linux)
This commit is contained in:
IChooseYOu
2026-02-14 16:08:44 -07:00
parent c856ba2697
commit 0a8244dad4
8 changed files with 207 additions and 44 deletions

View File

@@ -288,5 +288,5 @@ if(BUILD_TESTING)
) )
endif() endif()
endif() endif()
add_subdirectory(plugins/ProcessMemoryWindows) add_subdirectory(plugins/ProcessMemory)
add_subdirectory(plugins/WinDbgMemory) add_subdirectory(plugins/WinDbgMemory)

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(ProcessMemoryWindowsPlugin LANGUAGES CXX) project(ProcessMemoryPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON)
# Plugin sources # Plugin sources
set(PLUGIN_SOURCES set(PLUGIN_SOURCES
ProcessMemoryWindowsPlugin.h ProcessMemoryPlugin.h
ProcessMemoryWindowsPlugin.cpp ProcessMemoryPlugin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
) )
# Create shared library (DLL) # Create shared library (DLL)
add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES}) add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
# Link Qt # Link Qt
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS}) target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
# Platform-specific linking # Platform-specific linking
if(WIN32) if(WIN32)
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32) target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
endif() endif()
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported # On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden) target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
endif() endif()
# Include directories # Include directories
target_include_directories(ProcessMemoryWindowsPlugin PRIVATE target_include_directories(ProcessMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src ${CMAKE_CURRENT_SOURCE_DIR}/../../src
) )
# Output to Plugins folder # Output to Plugins folder
set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES set_target_properties(ProcessMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
) )

View File

@@ -1,4 +1,4 @@
#include "ProcessMemoryWindowsPlugin.h" #include "ProcessMemoryPlugin.h"
#include "../../src/processpicker.h" #include "../../src/processpicker.h"
@@ -32,12 +32,12 @@
#endif #endif
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryWindowsProvider implementation // ProcessMemoryProvider implementation
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
#ifdef _WIN32 #ifdef _WIN32
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName) ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_handle(nullptr) : m_handle(nullptr)
, m_pid(pid) , m_pid(pid)
, m_processName(processName) , m_processName(processName)
@@ -60,7 +60,7 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
cacheModules(); 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; 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; 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; 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; return false;
} }
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{ {
for (const auto& mod : m_modules) for (const auto& mod : m_modules)
{ {
@@ -96,7 +96,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
return {}; return {};
} }
void ProcessMemoryWindowsProvider::cacheModules() void ProcessMemoryProvider::cacheModules()
{ {
HMODULE mods[1024]; HMODULE mods[1024];
DWORD needed = 0; DWORD needed = 0;
@@ -126,7 +126,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
#elif defined(__linux__) #elif defined(__linux__)
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName) ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_fd(-1) : m_fd(-1)
, m_pid(pid) , m_pid(pid)
, m_processName(processName) , 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; 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<ssize_t>(len); return nread == static_cast<ssize_t>(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; 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<ssize_t>(len); return nwritten == static_cast<ssize_t>(len);
} }
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{ {
for (const auto& mod : m_modules) for (const auto& mod : m_modules)
{ {
@@ -215,7 +215,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
return {}; return {};
} }
void ProcessMemoryWindowsProvider::cacheModules() void ProcessMemoryProvider::cacheModules()
{ {
// Parse /proc/<pid>/maps to discover loaded modules // Parse /proc/<pid>/maps to discover loaded modules
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid); QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
@@ -288,7 +288,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
#endif // platform #endif // platform
ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider() ProcessMemoryProvider::~ProcessMemoryProvider()
{ {
#ifdef _WIN32 #ifdef _WIN32
if (m_handle) if (m_handle)
@@ -299,7 +299,7 @@ ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
#endif #endif
} }
int ProcessMemoryWindowsProvider::size() const int ProcessMemoryProvider::size() const
{ {
#ifdef _WIN32 #ifdef _WIN32
return m_handle ? 0x10000 : 0; 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); 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" // Target format: "pid:name" or just "pid"
QRegularExpression re("^\\d+"); QRegularExpression re("^\\d+");
return re.match(target).hasMatch(); return re.match(target).hasMatch();
} }
std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg) std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{ {
// Parse target: "pid:name" or just "pid" // Parse target: "pid:name" or just "pid"
QStringList parts = target.split(':'); QStringList parts = target.split(':');
@@ -339,7 +339,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid); QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
auto provider = std::make_unique<ProcessMemoryWindowsProvider>(pid, name); auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
if (!provider->isValid()) if (!provider->isValid())
{ {
if (errorMsg) if (errorMsg)
@@ -352,7 +352,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
return provider; return provider;
} }
uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
{ {
#ifdef _WIN32 #ifdef _WIN32
// Parse PID from target // Parse PID from target
@@ -409,7 +409,7 @@ uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target
#endif #endif
} }
bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target) bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{ {
// Use custom process enumeration from plugin // Use custom process enumeration from plugin
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses(); QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
@@ -440,7 +440,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
return false; return false;
} }
QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses() QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
{ {
QVector<PluginProcessInfo> processes; QVector<PluginProcessInfo> processes;
@@ -543,5 +543,5 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin() extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{ {
return new ProcessMemoryWindowsPlugin(); return new ProcessMemoryPlugin();
} }

View File

@@ -5,14 +5,14 @@
#include <cstdint> #include <cstdint>
/** /**
* Process memory provider (Windows) * Process memory provider
* Reads/writes memory from a live process using Windows platform APIs * Reads/writes memory from a live process using platform APIs
*/ */
class ProcessMemoryWindowsProvider : public rcx::Provider class ProcessMemoryProvider : public rcx::Provider
{ {
public: public:
ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName); ProcessMemoryProvider(uint32_t pid, const QString& processName);
~ProcessMemoryWindowsProvider() override; ~ProcessMemoryProvider() override;
// Required overrides // Required overrides
bool read(uint64_t addr, void* buf, int len) const override; 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: 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 Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; } 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; } k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override; QIcon Icon() const override;

View File

@@ -178,6 +178,14 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
editor->applyDocument(m_lastResult); editor->applyDocument(m_lastResult);
} }
updateCommandRow(); 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; return editor;
} }

View File

@@ -1330,7 +1330,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
// Single-click on editable token of already-selected node → edit // Single-click on editable token of already-selected node → edit
int tLine, tCol; EditTarget t; int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, 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; m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine, tCol); return beginInlineEdit(t, tLine, tCol);
} }

View File

@@ -16,6 +16,7 @@
#include <QApplication> #include <QApplication>
#include <QScreen> #include <QScreen>
#include <QIntValidator> #include <QIntValidator>
#include <QElapsedTimer>
#include "themes/thememanager.h" #include "themes/thememanager.h"
namespace rcx { namespace rcx {
@@ -384,10 +385,33 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
} }
void TypeSelectorPopup::warmUp() { 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; TypeEntry dummy;
dummy.entryKind = TypeEntry::Primitive; dummy.entryKind = TypeEntry::Primitive;
dummy.primitiveKind = NodeKind::Hex8; dummy.primitiveKind = NodeKind::Hex8;
dummy.displayName = "warmup"; dummy.displayName = QStringLiteral("warmup");
setTypes({dummy}); setTypes({dummy});
popup(QPoint(-9999, -9999)); popup(QPoint(-9999, -9999));
hide(); hide();

View File

@@ -8,6 +8,8 @@
#include <QLineEdit> #include <QLineEdit>
#include <QListView> #include <QListView>
#include <QStringListModel> #include <QStringListModel>
#include <QLabel>
#include <QFrame>
#include <Qsci/qsciscintilla.h> #include <Qsci/qsciscintilla.h>
#include "controller.h" #include "controller.h"
#include "typeselectorpopup.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 ── // ── Popup data model ──
void testPopupListsRootStructs() { void testPopupListsRootStructs() {