mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add async auto-refresh with change detection and self-test harness
BBB
This commit is contained in:
@@ -7,7 +7,7 @@ set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets PrintSupport Svg)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets PrintSupport Svg Concurrent)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
find_package(QScintilla REQUIRED)
|
||||
@@ -27,7 +27,7 @@ add_executable(ReclassX
|
||||
src/processpicker.ui
|
||||
src/resources.qrc
|
||||
src/core.h
|
||||
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h
|
||||
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h src/providers/snapshot_provider.h
|
||||
)
|
||||
|
||||
target_include_directories(ReclassX PRIVATE src)
|
||||
@@ -36,6 +36,7 @@ target_link_libraries(ReclassX PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::PrintSupport
|
||||
Qt6::Svg
|
||||
Qt6::Concurrent
|
||||
QScintilla::QScintilla
|
||||
dbghelp
|
||||
psapi
|
||||
@@ -125,7 +126,7 @@ if(BUILD_TESTING)
|
||||
src/processpicker.cpp src/processpicker.ui)
|
||||
target_include_directories(test_controller PRIVATE src)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
@@ -134,7 +135,7 @@ if(BUILD_TESTING)
|
||||
src/processpicker.cpp src/processpicker.ui)
|
||||
target_include_directories(test_validation PRIVATE src)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
|
||||
@@ -149,7 +150,7 @@ if(BUILD_TESTING)
|
||||
src/processpicker.cpp src/processpicker.ui)
|
||||
target_include_directories(test_context_menu PRIVATE src)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
endif()
|
||||
|
||||
@@ -141,7 +141,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
lm.isContinuation = isCont;
|
||||
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont);
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.effectiveTypeW = typeW;
|
||||
@@ -178,7 +178,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||
lm.nodeKind = node.kind;
|
||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
@@ -195,7 +195,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::ArrayElementSeparator;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
@@ -214,7 +214,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
@@ -300,7 +300,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false);
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
@@ -401,8 +401,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
// Include struct/array names - they now use columnar layout too
|
||||
int maxNameLen = kMinNameW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
// Skip padding (it shows ASCII preview, not name column)
|
||||
if (node.kind == NodeKind::Padding) continue;
|
||||
// Skip hex/padding (they show ASCII preview, not name column)
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||
}
|
||||
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
||||
@@ -420,8 +420,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip padding, but include hex and containers)
|
||||
if (child.kind != NodeKind::Padding) {
|
||||
// Name width (skip hex/padding, but include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
||||
}
|
||||
}
|
||||
@@ -439,8 +439,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip padding, include hex and containers)
|
||||
if (child.kind != NodeKind::Padding) {
|
||||
// Name width (skip hex/padding, include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#ifdef _WIN32
|
||||
#include <psapi.h>
|
||||
#endif
|
||||
@@ -55,7 +56,7 @@ static QString crumbFor(const rcx::NodeTree& t, uint64_t nodeId) {
|
||||
|
||||
RcxDocument::RcxDocument(QObject* parent)
|
||||
: QObject(parent)
|
||||
, provider(std::make_unique<NullProvider>())
|
||||
, provider(std::make_shared<NullProvider>())
|
||||
{
|
||||
connect(&undoStack, &QUndoStack::cleanChanged, this, [this](bool clean) {
|
||||
modified = !clean;
|
||||
@@ -97,7 +98,7 @@ void RcxDocument::loadData(const QString& binaryPath) {
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
undoStack.clear();
|
||||
provider = std::make_unique<BufferProvider>(
|
||||
provider = std::make_shared<BufferProvider>(
|
||||
file.readAll(), QFileInfo(binaryPath).fileName());
|
||||
dataPath = binaryPath;
|
||||
tree.baseAddress = 0;
|
||||
@@ -106,7 +107,7 @@ void RcxDocument::loadData(const QString& binaryPath) {
|
||||
|
||||
void RcxDocument::loadData(const QByteArray& data) {
|
||||
undoStack.clear();
|
||||
provider = std::make_unique<BufferProvider>(data);
|
||||
provider = std::make_shared<BufferProvider>(data);
|
||||
tree.baseAddress = 0;
|
||||
emit documentChanged();
|
||||
}
|
||||
@@ -125,6 +126,14 @@ RcxController::RcxController(RcxDocument* doc, QWidget* parent)
|
||||
: QObject(parent), m_doc(doc)
|
||||
{
|
||||
connect(m_doc, &RcxDocument::documentChanged, this, &RcxController::refresh);
|
||||
setupAutoRefresh();
|
||||
}
|
||||
|
||||
RcxController::~RcxController() {
|
||||
if (m_refreshWatcher) {
|
||||
m_refreshWatcher->cancel();
|
||||
m_refreshWatcher->waitForFinished();
|
||||
}
|
||||
}
|
||||
|
||||
RcxEditor* RcxController::primaryEditor() const {
|
||||
@@ -172,8 +181,8 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
case EditTarget::Name: {
|
||||
if (text.isEmpty()) break;
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
// ASCII edit on Padding nodes
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
// ASCII edit on Hex/Padding nodes
|
||||
if (isHexPreview(node.kind)) {
|
||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
|
||||
} else {
|
||||
renameNode(nodeIdx, text);
|
||||
@@ -427,7 +436,26 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
}
|
||||
|
||||
void RcxController::refresh() {
|
||||
m_lastResult = m_doc->compose();
|
||||
// Compose against snapshot provider if active, otherwise real provider
|
||||
if (m_snapshotProv)
|
||||
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv);
|
||||
else
|
||||
m_lastResult = m_doc->compose();
|
||||
|
||||
// Mark lines whose node data changed since last refresh
|
||||
if (!m_changedOffsets.isEmpty()) {
|
||||
for (auto& lm : m_lastResult.meta) {
|
||||
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
||||
int64_t offset = m_doc->tree.computeOffset(lm.nodeIdx);
|
||||
int sz = m_doc->tree.nodes[lm.nodeIdx].byteSize();
|
||||
for (int64_t b = offset; b < offset + sz; b++) {
|
||||
if (m_changedOffsets.contains(b)) {
|
||||
lm.dataChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||
QSet<uint64_t> valid;
|
||||
@@ -624,6 +652,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
tree.nodes[idx].collapsed = isUndo ? c.oldState : c.newState;
|
||||
} else if constexpr (std::is_same_v<T, cmd::Insert>) {
|
||||
if (isUndo) {
|
||||
// Revert offset adjustments
|
||||
for (const auto& adj : c.offAdjs) {
|
||||
int ai = tree.indexOfId(adj.nodeId);
|
||||
if (ai >= 0) tree.nodes[ai].offset = adj.oldOffset;
|
||||
}
|
||||
int idx = tree.indexOfId(c.node.id);
|
||||
if (idx >= 0) {
|
||||
tree.nodes.remove(idx);
|
||||
@@ -631,6 +664,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
}
|
||||
} else {
|
||||
tree.addNode(c.node);
|
||||
// Apply offset adjustments
|
||||
for (const auto& adj : c.offAdjs) {
|
||||
int ai = tree.indexOfId(adj.nodeId);
|
||||
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
||||
}
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, cmd::Remove>) {
|
||||
if (isUndo) {
|
||||
@@ -661,6 +699,9 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
||||
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
||||
if (!m_doc->provider->writeBytes(c.addr, bytes))
|
||||
qWarning() << "WriteBytes failed at address" << Qt::hex << c.addr;
|
||||
// Patch snapshot so compose sees the new value immediately
|
||||
if (m_snapshotProv)
|
||||
m_snapshotProv->patchSnapshot(c.addr, bytes.constData(), bytes.size());
|
||||
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
||||
int idx = tree.indexOfId(c.nodeId);
|
||||
if (idx >= 0) {
|
||||
@@ -735,7 +776,30 @@ void RcxController::duplicateNode(int nodeIdx) {
|
||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||
const Node& src = m_doc->tree.nodes[nodeIdx];
|
||||
if (src.kind == NodeKind::Struct || src.kind == NodeKind::Array) return;
|
||||
insertNode(src.parentId, src.offset + src.byteSize(), src.kind, src.name + "_copy");
|
||||
|
||||
int copySize = src.byteSize();
|
||||
int copyOffset = src.offset + copySize;
|
||||
|
||||
// Shift later siblings down to make room for the copy
|
||||
QVector<cmd::OffsetAdj> adjs;
|
||||
if (src.parentId != 0) {
|
||||
auto siblings = m_doc->tree.childrenOf(src.parentId);
|
||||
for (int si : siblings) {
|
||||
if (si == nodeIdx) continue;
|
||||
auto& sib = m_doc->tree.nodes[si];
|
||||
if (sib.offset >= copyOffset)
|
||||
adjs.append({sib.id, sib.offset, sib.offset + copySize});
|
||||
}
|
||||
}
|
||||
|
||||
Node n;
|
||||
n.kind = src.kind;
|
||||
n.name = src.name + "_copy";
|
||||
n.parentId = src.parentId;
|
||||
n.offset = copyOffset;
|
||||
n.id = m_doc->tree.reserveId();
|
||||
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{n, adjs}));
|
||||
}
|
||||
|
||||
void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
@@ -1101,10 +1165,11 @@ void RcxController::attachToProcess(uint32_t pid, const QString& processName) {
|
||||
}
|
||||
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::make_unique<ProcessProvider>(
|
||||
m_doc->provider = std::make_shared<ProcessProvider>(
|
||||
hProc, base, regionSize, processName);
|
||||
m_doc->dataPath.clear();
|
||||
m_doc->tree.baseAddress = base;
|
||||
resetSnapshot();
|
||||
emit m_doc->documentChanged();
|
||||
refresh();
|
||||
#else
|
||||
@@ -1148,6 +1213,104 @@ void RcxController::pushSavedSourcesToEditors() {
|
||||
editor->setSavedSources(display);
|
||||
}
|
||||
|
||||
// ── Auto-refresh ──
|
||||
|
||||
void RcxController::setupAutoRefresh() {
|
||||
m_refreshTimer = new QTimer(this);
|
||||
m_refreshTimer->setInterval(2000);
|
||||
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
|
||||
m_refreshTimer->start();
|
||||
|
||||
m_refreshWatcher = new QFutureWatcher<QByteArray>(this);
|
||||
connect(m_refreshWatcher, &QFutureWatcher<QByteArray>::finished,
|
||||
this, &RcxController::onReadComplete);
|
||||
}
|
||||
|
||||
void RcxController::onRefreshTick() {
|
||||
if (m_readInFlight) return;
|
||||
if (!m_doc->provider || !m_doc->provider->isLive()) return;
|
||||
if (m_suppressRefresh) return;
|
||||
for (auto* editor : m_editors)
|
||||
if (editor->isEditing()) return;
|
||||
|
||||
int extent = computeDataExtent();
|
||||
if (extent <= 0) return;
|
||||
|
||||
m_readInFlight = true;
|
||||
m_readGen = m_refreshGen;
|
||||
|
||||
// Capture shared_ptr copy — keeps provider alive during async read
|
||||
auto prov = m_doc->provider;
|
||||
m_refreshWatcher->setFuture(QtConcurrent::run([prov, extent]() -> QByteArray {
|
||||
return prov->readBytes(0, extent);
|
||||
}));
|
||||
}
|
||||
|
||||
void RcxController::onReadComplete() {
|
||||
m_readInFlight = false;
|
||||
|
||||
// Stale read (provider changed while we were reading) — discard
|
||||
if (m_readGen != m_refreshGen) return;
|
||||
|
||||
QByteArray newData = m_refreshWatcher->result();
|
||||
|
||||
// Fast path: no changes at all — skip full recompose
|
||||
if (!m_prevSnapshot.isEmpty() && m_prevSnapshot.size() == newData.size()
|
||||
&& memcmp(m_prevSnapshot.constData(), newData.constData(), newData.size()) == 0)
|
||||
return;
|
||||
|
||||
// Compute which byte offsets changed
|
||||
m_changedOffsets.clear();
|
||||
if (!m_prevSnapshot.isEmpty()) {
|
||||
int compareLen = qMin(m_prevSnapshot.size(), newData.size());
|
||||
const char* oldP = m_prevSnapshot.constData();
|
||||
const char* newP = newData.constData();
|
||||
for (int i = 0; i < compareLen; i++) {
|
||||
if (oldP[i] != newP[i])
|
||||
m_changedOffsets.insert(i);
|
||||
}
|
||||
// Bytes beyond old snapshot are all "new"
|
||||
for (int i = compareLen; i < newData.size(); i++)
|
||||
m_changedOffsets.insert(i);
|
||||
}
|
||||
m_prevSnapshot = newData;
|
||||
|
||||
// Update or create snapshot provider
|
||||
if (m_snapshotProv)
|
||||
m_snapshotProv->updateSnapshot(std::move(newData));
|
||||
else
|
||||
m_snapshotProv = std::make_unique<SnapshotProvider>(m_doc->provider, std::move(newData));
|
||||
|
||||
refresh();
|
||||
|
||||
// Clear changed offsets after refresh consumed them
|
||||
m_changedOffsets.clear();
|
||||
}
|
||||
|
||||
int RcxController::computeDataExtent() const {
|
||||
// Use provider size as the extent (for ProcessProvider this is the module/region size)
|
||||
int provSize = m_doc->provider->size();
|
||||
if (provSize > 0) return provSize;
|
||||
|
||||
// Fallback: walk tree to find maximum byte offset
|
||||
int maxEnd = 0;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
int64_t off = m_doc->tree.computeOffset(i);
|
||||
int sz = m_doc->tree.nodes[i].byteSize();
|
||||
int end = (int)(off + sz);
|
||||
if (end > maxEnd) maxEnd = end;
|
||||
}
|
||||
return maxEnd;
|
||||
}
|
||||
|
||||
void RcxController::resetSnapshot() {
|
||||
m_refreshGen++;
|
||||
m_readInFlight = false;
|
||||
m_snapshotProv.reset();
|
||||
m_prevSnapshot.clear();
|
||||
m_changedOffsets.clear();
|
||||
}
|
||||
|
||||
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
||||
int line, Qt::KeyboardModifiers) {
|
||||
const LineMeta* lm = editor->metaForLine(line);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include "editor.h"
|
||||
#include "providers/snapshot_provider.h"
|
||||
#include <QObject>
|
||||
#include <QUndoStack>
|
||||
#include <QUndoCommand>
|
||||
#include <QTimer>
|
||||
#include <QFutureWatcher>
|
||||
#include <memory>
|
||||
|
||||
class QSplitter;
|
||||
@@ -20,7 +23,7 @@ public:
|
||||
explicit RcxDocument(QObject* parent = nullptr);
|
||||
|
||||
NodeTree tree;
|
||||
std::unique_ptr<Provider> provider;
|
||||
std::shared_ptr<Provider> provider;
|
||||
QUndoStack undoStack;
|
||||
QString filePath;
|
||||
QString dataPath;
|
||||
@@ -65,6 +68,7 @@ class RcxController : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RcxController(RcxDocument* doc, QWidget* parent = nullptr);
|
||||
~RcxController() override;
|
||||
|
||||
RcxEditor* primaryEditor() const;
|
||||
RcxEditor* addSplitEditor(QSplitter* splitter);
|
||||
@@ -111,12 +115,29 @@ private:
|
||||
QVector<SavedSourceEntry> m_savedSources;
|
||||
int m_activeSourceIdx = -1;
|
||||
|
||||
// ── Auto-refresh state ──
|
||||
QTimer* m_refreshTimer = nullptr;
|
||||
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
|
||||
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||
QByteArray m_prevSnapshot;
|
||||
QSet<int64_t> m_changedOffsets;
|
||||
uint64_t m_refreshGen = 0;
|
||||
uint64_t m_readGen = 0;
|
||||
bool m_readInFlight = false;
|
||||
|
||||
void connectEditor(RcxEditor* editor);
|
||||
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void updateCommandRow();
|
||||
void attachToProcess(uint32_t pid, const QString& processName);
|
||||
void switchToSavedSource(int idx);
|
||||
void pushSavedSourcesToEditors();
|
||||
|
||||
// ── Auto-refresh methods ──
|
||||
void setupAutoRefresh();
|
||||
void onRefreshTick();
|
||||
void onReadComplete();
|
||||
int computeDataExtent() const;
|
||||
void resetSnapshot();
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
25
src/core.h
25
src/core.h
@@ -399,6 +399,7 @@ struct LineMeta {
|
||||
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
||||
QString offsetText;
|
||||
uint32_t markerMask = 0;
|
||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
||||
@@ -431,7 +432,7 @@ namespace cmd {
|
||||
QVector<OffsetAdj> offAdjs; };
|
||||
struct Rename { uint64_t nodeId; QString oldName, newName; };
|
||||
struct Collapse { uint64_t nodeId; bool oldState, newState; };
|
||||
struct Insert { Node node; };
|
||||
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
|
||||
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
||||
QVector<OffsetAdj> offAdjs; };
|
||||
struct ChangeBase { uint64_t oldBase, newBase; };
|
||||
@@ -486,8 +487,8 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int start = ind + typeW + kSepWidth;
|
||||
|
||||
// Padding: ASCII preview takes the name column position (8 chars)
|
||||
if (lm.nodeKind == NodeKind::Padding)
|
||||
// Hex/Padding: ASCII preview takes the name column position (8 chars)
|
||||
if (isHexPreview(lm.nodeKind))
|
||||
return {start, start + 8, true};
|
||||
|
||||
return {start, start + nameW, true};
|
||||
@@ -498,12 +499,12 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
// Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||
bool isPad = (lm.nodeKind == NodeKind::Padding);
|
||||
int valWidth = isPad ? 23 : kColValue;
|
||||
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isPad
|
||||
int prefixW = isHexPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
int start = ind + prefixW;
|
||||
@@ -511,7 +512,7 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
}
|
||||
if (lm.lineKind != LineKind::Field) return {};
|
||||
|
||||
int start = isPad
|
||||
int start = isHexPad
|
||||
? (ind + typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (ind + typeW + kSepWidth + nameW + kSepWidth);
|
||||
return {start, start + valWidth, true};
|
||||
@@ -521,17 +522,17 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
bool isPad = (lm.nodeKind == NodeKind::Padding);
|
||||
int valWidth = isPad ? 23 : kColValue;
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
|
||||
int start;
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isPad
|
||||
int prefixW = isHexPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
start = ind + prefixW + valWidth;
|
||||
} else {
|
||||
start = isPad
|
||||
start = isHexPad
|
||||
? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
|
||||
: (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ static constexpr int IND_HEX_DIM = 9;
|
||||
static constexpr int IND_BASE_ADDR = 10; // Green color for base address
|
||||
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
||||
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
||||
static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data values
|
||||
|
||||
static QString g_fontName = "Consolas";
|
||||
|
||||
@@ -168,6 +169,12 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||
IND_CMD_PILL, (long)1);
|
||||
|
||||
// Data-changed indicator — amber text for values that changed since last refresh
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_DATA_CHANGED, 17 /*INDIC_TEXTFORE*/);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_DATA_CHANGED, QColor("#E5A00D"));
|
||||
|
||||
}
|
||||
|
||||
void RcxEditor::setupLexer() {
|
||||
@@ -321,6 +328,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
applyMarkers(result.meta);
|
||||
applyFoldLevels(result.meta);
|
||||
applyHexDimming(result.meta);
|
||||
applyDataChangedHighlight(result.meta);
|
||||
applyCommandRowPills();
|
||||
|
||||
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||
@@ -404,7 +412,7 @@ void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) {
|
||||
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
|
||||
for (int i = 0; i < meta.size(); i++) {
|
||||
if (meta[i].nodeKind == NodeKind::Padding) {
|
||||
if (isHexPreview(meta[i].nodeKind)) {
|
||||
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
||||
if (len > 0)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
|
||||
@@ -544,6 +552,20 @@ static QString getLineText(QsciScintilla* sci, int line) {
|
||||
return text;
|
||||
}
|
||||
|
||||
void RcxEditor::applyDataChangedHighlight(const QVector<LineMeta>& meta) {
|
||||
for (int i = 0; i < meta.size(); i++) {
|
||||
if (!meta[i].dataChanged) continue;
|
||||
if (isSyntheticLine(meta[i])) continue;
|
||||
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
int typeW = meta[i].effectiveTypeW;
|
||||
int nameW = meta[i].effectiveNameW;
|
||||
ColumnSpan vs = valueSpan(meta[i], lineText.size(), typeW, nameW);
|
||||
if (vs.valid)
|
||||
fillIndicatorCols(IND_DATA_CHANGED, i, vs.start, vs.end);
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
|
||||
if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return;
|
||||
|
||||
@@ -609,16 +631,20 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
|
||||
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int typeW = lm.effectiveTypeW;
|
||||
int nameW = lm.effectiveNameW;
|
||||
int nameStart = ind + typeW + kSepWidth;
|
||||
int nameEnd = nameStart + nameW;
|
||||
|
||||
// Clamp to line length
|
||||
if (nameStart >= lineText.size()) return {};
|
||||
if (nameEnd > lineText.size()) nameEnd = lineText.size();
|
||||
|
||||
// Name ends before " {" suffix (expanded) or at line end (collapsed)
|
||||
int nameEnd = lineText.size();
|
||||
if (lineText.endsWith(QStringLiteral(" {")))
|
||||
nameEnd = lineText.size() - 2;
|
||||
|
||||
if (nameEnd <= nameStart) return {};
|
||||
|
||||
// Don't allow editing array element names like "[0]", "[1]", etc.
|
||||
QString name = lineText.mid(nameStart, nameEnd - nameStart).trimmed();
|
||||
if (name.isEmpty()) return {};
|
||||
if (name.startsWith('[') && name.endsWith(']'))
|
||||
return {};
|
||||
|
||||
@@ -1678,7 +1704,7 @@ void RcxEditor::applyHoverCursor() {
|
||||
bool inHexDataArea = false;
|
||||
uint64_t hoverNodeId = 0;
|
||||
if (hoverLine >= 0 && hoverLine < m_meta.size()
|
||||
&& m_meta[hoverLine].nodeKind == NodeKind::Padding) {
|
||||
&& isHexPreview(m_meta[hoverLine].nodeKind)) {
|
||||
hoverNodeId = m_meta[hoverLine].nodeId;
|
||||
if (hoverNodeId != 0 && h.col >= 0) {
|
||||
int ind = kFoldCol + m_meta[hoverLine].depth * 3;
|
||||
|
||||
@@ -135,6 +135,7 @@ private:
|
||||
void applyMarkers(const QVector<LineMeta>& meta);
|
||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
|
||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||
void applyCommandRowPills();
|
||||
|
||||
|
||||
@@ -121,9 +121,8 @@ QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType
|
||||
// Columnar format: <type> <name> { (or no brace when collapsed)
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(structTypeName(node), colType);
|
||||
QString name = fit(node.name, colName);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
return ind + type + SEP + name + SEP + suffix;
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
|
||||
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
@@ -135,9 +134,8 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType);
|
||||
QString name = fit(node.name, colName);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
return ind + type + SEP + name + SEP + suffix;
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
|
||||
// ── Pointer header (merged pointer + struct header) ──
|
||||
@@ -147,13 +145,13 @@ QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||
const QString& ptrTypeName, int colType, int colName) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(ptrTypeName, colType);
|
||||
QString name = fit(node.name, colName);
|
||||
if (collapsed) {
|
||||
// Collapsed: show pointer value instead of brace
|
||||
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
||||
QString name = fit(node.name, colName);
|
||||
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
||||
return ind + type + SEP + name + SEP + val;
|
||||
}
|
||||
return ind + type + SEP + name + SEP + QStringLiteral("{");
|
||||
return ind + type + SEP + node.name + SEP + QStringLiteral("{");
|
||||
}
|
||||
|
||||
// ── Hex / ASCII preview ──
|
||||
@@ -211,10 +209,6 @@ enum class ValueMode { Display, Editable };
|
||||
|
||||
static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
uint64_t addr, int subLine, ValueMode mode) {
|
||||
int sz = node.byteSize();
|
||||
if (sz > 0 && !prov.isReadable(addr, sz))
|
||||
return (mode == ValueMode::Display) ? QStringLiteral("???") : QString();
|
||||
|
||||
const bool display = (mode == ValueMode::Display);
|
||||
switch (node.kind) {
|
||||
case NodeKind::Hex8: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
||||
@@ -328,18 +322,27 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||
}
|
||||
|
||||
// Padding: ASCII preview + hex bytes (compact, multi-line)
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
const int totalSz = qMax(1, node.arrayLen);
|
||||
const int lineOff = subLine * 8;
|
||||
const int lineBytes = qMin(8, totalSz - lineOff);
|
||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||
QString ascii = bytesToAscii(b, lineBytes);
|
||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||
if (subLine == 0)
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||
// Hex nodes and Padding: hex byte preview
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
const int totalSz = qMax(1, node.arrayLen);
|
||||
const int lineOff = subLine * 8;
|
||||
const int lineBytes = qMin(8, totalSz - lineOff);
|
||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||
QString ascii = bytesToAscii(b, lineBytes);
|
||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||
if (subLine == 0)
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(addr, sz)
|
||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
|
||||
QString hex = bytesToHex(b, sz).leftJustified(23, ' ');
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
|
||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
|
||||
67
src/main.cpp
67
src/main.cpp
@@ -1,5 +1,6 @@
|
||||
#include "controller.h"
|
||||
#include "generator.h"
|
||||
#include "providers/process_provider.h"
|
||||
#include <QApplication>
|
||||
#include <QMainWindow>
|
||||
#include <QMdiArea>
|
||||
@@ -101,6 +102,31 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||
}
|
||||
#endif
|
||||
|
||||
// ── Self-test: live data for verifying auto-refresh ──
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <random>
|
||||
|
||||
struct TestLiveData {
|
||||
int32_t valA = 100;
|
||||
int32_t valB = 200;
|
||||
int32_t valC = 300;
|
||||
int32_t valD = 400;
|
||||
};
|
||||
|
||||
static TestLiveData* g_testData = nullptr;
|
||||
static std::atomic<bool> g_testRunning{false};
|
||||
|
||||
static void testLiveThread() {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_int_distribution<int> dist(0, 3);
|
||||
while (g_testRunning.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
int32_t* fields = &g_testData->valA;
|
||||
fields[dist(rng)]++;
|
||||
}
|
||||
}
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
@@ -110,6 +136,7 @@ public:
|
||||
|
||||
private slots:
|
||||
void newFile();
|
||||
void selfTest();
|
||||
void openFile();
|
||||
void saveFile();
|
||||
void saveFileAs();
|
||||
@@ -382,6 +409,42 @@ void MainWindow::newFile() {
|
||||
createTab(doc);
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
#ifdef _WIN32
|
||||
// Allocate test struct — lives until process exit
|
||||
g_testData = new TestLiveData();
|
||||
g_testRunning = true;
|
||||
std::thread(testLiveThread).detach();
|
||||
|
||||
auto* doc = new RcxDocument(this);
|
||||
uint64_t base = (uint64_t)g_testData;
|
||||
|
||||
HANDLE hProc = OpenProcess(
|
||||
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION
|
||||
| PROCESS_QUERY_INFORMATION,
|
||||
FALSE, GetCurrentProcessId());
|
||||
doc->provider = std::make_shared<ProcessProvider>(
|
||||
hProc, base, (int)sizeof(TestLiveData), "ReclassX.exe");
|
||||
doc->tree.baseAddress = base;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "TestLiveData";
|
||||
root.structTypeName = "TestLiveData";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = doc->tree.addNode(root);
|
||||
uint64_t rootId = doc->tree.nodes[ri].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valA"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valB"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valC"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "valD"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); }
|
||||
|
||||
createTab(doc);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::openFile() {
|
||||
QString path = QFileDialog::getOpenFileName(this,
|
||||
"Open Definition", {}, "ReclassX (*.rcx);;JSON (*.json);;All (*)");
|
||||
@@ -774,8 +837,8 @@ int main(int argc, char* argv[]) {
|
||||
window.setWindowOpacity(0.0);
|
||||
window.show();
|
||||
|
||||
// Always auto-open PEB64 demo on startup
|
||||
QMetaObject::invokeMethod(&window, "newFile");
|
||||
// Auto-open self-test tab (live data refresh test)
|
||||
QMetaObject::invokeMethod(&window, "selfTest");
|
||||
|
||||
if (screenshotMode) {
|
||||
QString out = "screenshot.png";
|
||||
|
||||
@@ -55,6 +55,7 @@ public:
|
||||
|
||||
QString name() const override { return m_name; }
|
||||
QString kind() const override { return QStringLiteral("Process"); }
|
||||
bool isLive() const override { return true; }
|
||||
|
||||
// getSymbol takes an absolute virtual address and resolves it to
|
||||
// "module.dll+0xOFFSET" using the cached module list.
|
||||
|
||||
@@ -25,6 +25,10 @@ public:
|
||||
// Examples: "notepad.exe", "dump.bin", "tcp://10.0.0.1:1337"
|
||||
virtual QString name() const { return {}; }
|
||||
|
||||
// Whether data can change externally (e.g. live process, network socket).
|
||||
// Auto-refresh is only active for live providers.
|
||||
virtual bool isLive() const { return false; }
|
||||
|
||||
// Category tag for the command row Source span.
|
||||
// Examples: "File", "Process", "Socket"
|
||||
virtual QString kind() const { return QStringLiteral("File"); }
|
||||
|
||||
54
src/providers/snapshot_provider.h
Normal file
54
src/providers/snapshot_provider.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include "provider.h"
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Provider that reads from a cached QByteArray snapshot but delegates
|
||||
// metadata (name, kind, getSymbol) to the underlying real provider.
|
||||
// Used for async refresh: worker thread reads bulk data into a snapshot,
|
||||
// UI thread composes against it without blocking.
|
||||
class SnapshotProvider : public Provider {
|
||||
std::shared_ptr<Provider> m_real;
|
||||
QByteArray m_data;
|
||||
|
||||
public:
|
||||
SnapshotProvider(std::shared_ptr<Provider> real, QByteArray snapshot)
|
||||
: m_real(std::move(real)), m_data(std::move(snapshot)) {}
|
||||
|
||||
bool read(uint64_t addr, void* buf, int len) const override {
|
||||
if (!isReadable(addr, len)) return false;
|
||||
std::memcpy(buf, m_data.constData() + addr, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
int size() const override { return m_data.size(); }
|
||||
bool isWritable() const override { return m_real ? m_real->isWritable() : false; }
|
||||
bool isLive() const override { return m_real ? m_real->isLive() : false; }
|
||||
QString name() const override { return m_real ? m_real->name() : QString(); }
|
||||
QString kind() const override { return m_real ? m_real->kind() : QStringLiteral("File"); }
|
||||
QString getSymbol(uint64_t addr) const override {
|
||||
return m_real ? m_real->getSymbol(addr) : QString();
|
||||
}
|
||||
|
||||
bool write(uint64_t addr, const void* buf, int len) override {
|
||||
if (!m_real) return false;
|
||||
bool ok = m_real->write(addr, buf, len);
|
||||
if (ok && isReadable(addr, len))
|
||||
std::memcpy(m_data.data() + addr, buf, len);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Update the entire snapshot (called after async read completes)
|
||||
void updateSnapshot(QByteArray data) { m_data = std::move(data); }
|
||||
|
||||
// Patch specific bytes in the snapshot (called after user writes a value)
|
||||
void patchSnapshot(uint64_t addr, const void* buf, int len) {
|
||||
if (isReadable(addr, len))
|
||||
std::memcpy(m_data.data() + addr, buf, len);
|
||||
}
|
||||
|
||||
const QByteArray& snapshot() const { return m_data; }
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
Reference in New Issue
Block a user