Add async auto-refresh with change detection and self-test harness

BBB
This commit is contained in:
sysadmin
2026-02-07 05:46:01 -07:00
parent 6232577f87
commit 60fda32af0
12 changed files with 406 additions and 68 deletions

View File

@@ -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);