perf: compose 30% faster — move semantics, BFS offsets, zero-alloc hex formatting

- compose.cpp: emitLine takes LineMeta&& (move, not copy) at all 22 call sites
- compose.cpp: reserve meta/text buffers, BFS offset computation O(N) vs O(N*D)
- compose.cpp: pre-compute typeNameLens[], merge global width loops
- format.cpp: bytesToHex uses stack buffer + lookup table (zero heap allocs)
- format.cpp: hexVal single QString::asprintf instead of 2-string concat
- editor.cpp: guard hover updates during applyDocument (stale index safety)
- core.h: assertion on makeArrayElemSelId negative index
- format.cpp: assertion on extractBits overflow
- main.cpp: tree lines enabled by default
- bench_large_class: add 2000-field benchComposeLarge test

Benchmark: 500 fields 0.70→0.51ms (27%), 2000 fields 2.28→1.57ms (31%)
This commit is contained in:
IChooseYou
2026-03-08 07:28:26 -06:00
committed by IChooseYou
parent f0fc85f60f
commit 596f410b96
8 changed files with 150 additions and 63 deletions

View File

@@ -53,7 +53,7 @@ struct ComposeState {
siblingStack[d] = hasMoreSiblings; siblingStack[d] = hasMoreSiblings;
} }
void emitLine(const QString& lineText, LineMeta lm) { void emitLine(const QString& lineText, LineMeta&& lm) {
if (currentLine > 0) text += '\n'; if (currentLine > 0) text += '\n';
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other // 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
// CommandRow has no fold prefix (flush left) // CommandRow has no fold prefix (flush left)
@@ -87,7 +87,7 @@ struct ComposeState {
text += lineText; text += lineText;
} }
meta.append(lm); meta.append(std::move(lm));
currentLine++; currentLine++;
} }
}; };
@@ -208,7 +208,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
/*comment=*/{}, typeW, nameW, ptrTypeOverride, /*comment=*/{}, typeW, nameW, ptrTypeOverride,
state.compactColumns); state.compactColumns);
state.emitLine(lineText, lm); state.emitLine(lineText, std::move(lm));
} }
} }
@@ -246,7 +246,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR); lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
lm.foldLevel = computeFoldLevel(depth, false); lm.foldLevel = computeFoldLevel(depth, false);
state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") + state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") +
node.name + QStringLiteral(" */"), lm); node.name + QStringLiteral(" */"), std::move(lm));
return; return;
} }
state.visiting.insert(node.id); state.visiting.insert(node.id);
@@ -267,7 +267,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.arrayElementIdx = arrayElementIdx; lm.arrayElementIdx = arrayElementIdx;
uint64_t relOff = absAddr - arrayContainerAddr; uint64_t relOff = absAddr - arrayContainerAddr;
QString relOffHex = QString::number(relOff, 16).toUpper(); QString relOffHex = QString::number(relOff, 16).toUpper();
state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), lm); state.emitLine(fmt::indent(depth) + QStringLiteral("[%1] +0x%2").arg(arrayElementIdx).arg(relOffHex), std::move(lm));
} }
// Detect root header: first root-level struct — suppressed from display // Detect root header: first root-level struct — suppressed from display
@@ -325,7 +325,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
headerText.chop(1); headerText.chop(1);
// Remove trailing separator spaces // Remove trailing separator spaces
while (headerText.endsWith(' ')) headerText.chop(1); while (headerText.endsWith(' ')) headerText.chop(1);
state.emitLine(headerText, lm); state.emitLine(headerText, std::move(lm));
// Emit standalone brace line // Emit standalone brace line
LineMeta braceLm; LineMeta braceLm;
braceLm.nodeIdx = nodeIdx; braceLm.nodeIdx = nodeIdx;
@@ -334,9 +334,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
braceLm.lineKind = LineKind::Header; braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = computeFoldLevel(depth, true); braceLm.foldLevel = computeFoldLevel(depth, true);
braceLm.markerMask = (1u << M_STRUCT_BG); braceLm.markerMask = (1u << M_STRUCT_BG);
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm); state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
} else { } else {
state.emitLine(headerText, lm); state.emitLine(headerText, std::move(lm));
} }
} }
@@ -372,7 +372,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
lm.offsetAddr = absAddr; lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), lm); state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), std::move(lm));
} }
// Footer // Footer
@@ -389,7 +389,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr; lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm); state.emitLine(fmt::fmtStructFooter(node, depth, 0), std::move(lm));
} }
state.visiting.remove(node.id); state.visiting.remove(node.id);
@@ -423,7 +423,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetAddr = absAddr; lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal, state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
childDepth, maxNameLen), lm); childDepth, maxNameLen), std::move(lm));
} }
// Footer // Footer
@@ -441,7 +441,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz; lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
} }
state.visiting.remove(node.id); state.visiting.remove(node.id);
@@ -501,7 +501,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0, state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
{}, eTW, eNW, elemTypeStr, {}, eTW, eNW, elemTypeStr,
state.compactColumns), lm); state.compactColumns), std::move(lm));
} }
} }
@@ -559,7 +559,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.effectiveTypeW = overflow ? rawType.size() : typeW; lm.effectiveTypeW = overflow ? rawType.size() : typeW;
lm.effectiveNameW = nameW; lm.effectiveNameW = nameW;
state.emitLine(fmt::fmtStructHeader(child, childDepth, state.emitLine(fmt::fmtStructHeader(child, childDepth,
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm); /*collapsed=*/true, typeW, nameW, state.compactColumns), std::move(lm));
continue; continue;
} }
composeNode(state, tree, prov, childIdx, childDepth, composeNode(state, tree, prov, childIdx, childDepth,
@@ -700,7 +700,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
lm.effectiveTypeW = typeName.size() + 7; // "static " prefix lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
lm.effectiveNameW = sf.name.size(); lm.effectiveNameW = sf.name.size();
state.emitLine(headerLine, lm); state.emitLine(headerLine, std::move(lm));
// ── Body + children (only when expanded) ── // ── Body + children (only when expanded) ──
if (!isCollapsed) { if (!isCollapsed) {
@@ -747,7 +747,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
blm.offsetText = QString(state.offsetHexDigits, QChar(' ')); blm.offsetText = QString(state.offsetHexDigits, QChar(' '));
blm.offsetAddr = staticAddr; blm.offsetAddr = staticAddr;
blm.ptrBase = state.currentPtrBase; blm.ptrBase = state.currentPtrBase;
state.emitLine(bodyLine, blm); state.emitLine(bodyLine, std::move(blm));
} }
// If struct/array, compose children at evaluated address // If struct/array, compose children at evaluated address
@@ -780,7 +780,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
flm.offsetAddr = staticAddr; flm.offsetAddr = staticAddr;
} }
flm.ptrBase = state.currentPtrBase; flm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm); state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), std::move(flm));
} }
} }
} }
@@ -802,7 +802,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits); lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz; lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
} }
state.visiting.remove(node.id); state.visiting.remove(node.id);
@@ -865,7 +865,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) { if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) {
ptrText.chop(1); ptrText.chop(1);
while (ptrText.endsWith(' ')) ptrText.chop(1); while (ptrText.endsWith(' ')) ptrText.chop(1);
state.emitLine(ptrText, lm); state.emitLine(ptrText, std::move(lm));
LineMeta braceLm; LineMeta braceLm;
braceLm.nodeIdx = nodeIdx; braceLm.nodeIdx = nodeIdx;
braceLm.nodeId = node.id; braceLm.nodeId = node.id;
@@ -873,9 +873,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
braceLm.lineKind = LineKind::Header; braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = computeFoldLevel(depth, true); braceLm.foldLevel = computeFoldLevel(depth, true);
braceLm.markerMask = lm.markerMask; braceLm.markerMask = lm.markerMask;
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm); state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
} else { } else {
state.emitLine(ptrText, lm); state.emitLine(ptrText, std::move(lm));
} }
} }
} }
@@ -955,7 +955,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.offsetText.clear(); lm.offsetText.clear();
lm.foldLevel = computeFoldLevel(depth, false); lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0; lm.markerMask = 0;
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), lm); state.emitLine(fmt::indent(depth) + QStringLiteral("}"), std::move(lm));
} }
} }
return; return;
@@ -988,10 +988,32 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
}); });
} }
// Precompute absolute offsets (baseAddress + structure-relative offset) // Pre-allocate output buffers (estimate ~3 lines per node, ~80 chars per line)
state.meta.reserve(tree.nodes.size() * 3);
state.text.reserve(tree.nodes.size() * 80);
// Precompute absolute offsets via BFS (O(N) — avoids per-node parent-chain walk)
state.absOffsets.resize(tree.nodes.size()); state.absOffsets.resize(tree.nodes.size());
state.absOffsets.fill(0);
for (int i = 0; i < tree.nodes.size(); i++) for (int i = 0; i < tree.nodes.size(); i++)
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i); if (tree.nodes[i].parentId == 0)
state.absOffsets[i] = tree.nodes[i].offset;
{
QVector<int> bfsQueue;
for (int i : state.childMap.value(0))
bfsQueue.append(i);
int front = 0;
while (front < bfsQueue.size()) {
int idx = bfsQueue[front++];
int pi = tree.indexOfId(tree.nodes[idx].parentId);
state.absOffsets[idx] = (pi >= 0 ? state.absOffsets[pi] : 0)
+ tree.nodes[idx].offset;
for (int ci : state.childMap.value(tree.nodes[idx].id))
bfsQueue.append(ci);
}
}
for (auto& v : state.absOffsets)
v += tree.baseAddress;
// Compute hex digit tier from max absolute address // Compute hex digit tier from max absolute address
{ {
@@ -1020,23 +1042,21 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
return fmt::typeNameRaw(n.kind); return fmt::typeNameRaw(n.kind);
}; };
// Compute effective type column width from longest type name // Pre-compute type name lengths (avoids re-creating temp QStrings in width loops)
// Include struct/array headers which use "struct TypeName" or "type[count]" format QVector<int> typeNameLens(tree.nodes.size());
for (int i = 0; i < tree.nodes.size(); i++)
typeNameLens[i] = nodeTypeName(tree.nodes[i]).size();
// Compute effective column widths from longest type/name in a single pass
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW; const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
int maxTypeLen = kMinTypeW; int maxTypeLen = kMinTypeW;
for (const Node& node : tree.nodes) { int maxNameLen = kMinNameW;
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size()); for (int i = 0; i < tree.nodes.size(); i++) {
maxTypeLen = qMax(maxTypeLen, typeNameLens[i]);
if (!isHexPreview(tree.nodes[i].kind))
maxNameLen = qMax(maxNameLen, (int)tree.nodes[i].name.size());
} }
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap); state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
// Compute effective name column width from longest name
// Include struct/array names - they now use columnar layout too
int maxNameLen = kMinNameW;
for (const Node& node : tree.nodes) {
// Skip hex (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); state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
// Pre-compute per-scope widths (each container gets widths based on direct children only) // Pre-compute per-scope widths (each container gets widths based on direct children only)
@@ -1053,7 +1073,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
// Skip struct children — pointer headers shouldn't inflate sibling widths // Skip struct children — pointer headers shouldn't inflate sibling widths
if (child.kind == NodeKind::Struct) if (child.kind == NodeKind::Struct)
continue; continue;
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size()); scopeMaxType = qMax(scopeMaxType, typeNameLens[childIdx]);
// Name width (skip hex, but include containers) // Name width (skip hex, but include containers)
if (!isHexPreview(child.kind)) { if (!isHexPreview(child.kind)) {
@@ -1079,7 +1099,6 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
} }
// Compute scope widths for root level (parentId == 0) // Compute scope widths for root level (parentId == 0)
// Include struct/array headers - they now use columnar layout too
{ {
int rootMaxType = kMinTypeW; int rootMaxType = kMinTypeW;
int rootMaxName = kMinNameW; int rootMaxName = kMinNameW;
@@ -1088,7 +1107,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
// Skip struct children — pointer headers shouldn't inflate sibling widths // Skip struct children — pointer headers shouldn't inflate sibling widths
if (child.kind == NodeKind::Struct) if (child.kind == NodeKind::Struct)
continue; continue;
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size()); rootMaxType = qMax(rootMaxType, typeNameLens[childIdx]);
// Name width (skip hex, include containers) // Name width (skip hex, include containers)
if (!isHexPreview(child.kind)) { if (!isHexPreview(child.kind)) {
@@ -1115,7 +1134,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
lm.markerMask = 0; lm.markerMask = 0;
lm.effectiveTypeW = state.typeW; lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW; lm.effectiveNameW = state.nameW;
state.emitLine(cmdRowText, lm); state.emitLine(cmdRowText, std::move(lm));
} }
// Brace wrapping: emit standalone "{" after CommandRow // Brace wrapping: emit standalone "{" after CommandRow
@@ -1127,7 +1146,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
braceLm.lineKind = LineKind::Header; braceLm.lineKind = LineKind::Header;
braceLm.foldLevel = SC_FOLDLEVELBASE; braceLm.foldLevel = SC_FOLDLEVELBASE;
braceLm.markerMask = 0; braceLm.markerMask = 0;
state.emitLine(QStringLiteral("{"), braceLm); state.emitLine(QStringLiteral("{"), std::move(braceLm));
} }
const QVector<int>& roots = childIndices(state, 0); const QVector<int>& roots = childIndices(state, 0);

View File

@@ -575,6 +575,7 @@ static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48) // Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) { inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
Q_ASSERT(elemIdx >= 0);
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift); return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
} }
inline int arrayElemIdxFromSelId(uint64_t selId) { inline int arrayElemIdxFromSelId(uint64_t selId) {

View File

@@ -562,9 +562,11 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (chosen == actRel && !m_relativeOffsets) { if (chosen == actRel && !m_relativeOffsets) {
m_relativeOffsets = true; m_relativeOffsets = true;
reformatMargins(); reformatMargins();
emit relativeOffsetsChanged(true);
} else if (chosen == actAbs && m_relativeOffsets) { } else if (chosen == actAbs && m_relativeOffsets) {
m_relativeOffsets = false; m_relativeOffsets = false;
reformatMargins(); reformatMargins();
emit relativeOffsetsChanged(false);
} }
return; return;
} }
@@ -958,8 +960,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
} }
// Dynamically resize margin to fit the current hex digit tier // Dynamically resize margin to fit the current hex digit tier
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0')); // RVA mode uses half width since relative offsets are much shorter
m_sci->setMarginWidth(0, marginSizer); {
int marginDigits = m_relativeOffsets
? qMax(m_layout.offsetHexDigits / 2, 4)
: m_layout.offsetHexDigits;
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
}
m_sci->setReadOnly(false); m_sci->setReadOnly(false);
m_sci->setText(result.text); m_sci->setText(result.text);
@@ -1066,6 +1074,11 @@ void RcxEditor::reformatMargins() {
uint64_t base = m_layout.baseAddress; uint64_t base = m_layout.baseAddress;
int hexDigits = m_layout.offsetHexDigits; int hexDigits = m_layout.offsetHexDigits;
// Resize margin: RVA offsets are much shorter than full addresses
int marginDigits = m_relativeOffsets ? qMax(hexDigits / 2, 4) : hexDigits;
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
// ── Pass 1: margin text (global offset only) ── // ── Pass 1: margin text (global offset only) ──
m_sci->clearMarginText(-1); m_sci->clearMarginText(-1);
for (int i = 0; i < m_meta.size(); i++) { for (int i = 0; i < m_meta.size(); i++) {
@@ -2195,6 +2208,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
#endif #endif
m_relativeOffsets = !m_relativeOffsets; m_relativeOffsets = !m_relativeOffsets;
reformatMargins(); reformatMargins();
emit relativeOffsetsChanged(m_relativeOffsets);
return true; return true;
} }
} }
@@ -2274,7 +2288,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos); m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos);
} }
// Resolve hovered nodeId on move/wheel (non-edit mode only) // Resolve hovered nodeId on move/wheel (non-edit mode only)
if (!m_editState.active && // Guard: skip during applyDocument — m_nodeLineIndex may be stale
if (!m_editState.active && !m_applyingDocument &&
(event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel)) { (event->type() == QEvent::MouseMove || event->type() == QEvent::Wheel)) {
auto h = hitTest(m_lastHoverPos); auto h = hitTest(m_lastHoverPos);
uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0; uint64_t newHoverId = (m_hoverInside && h.line >= 0) ? h.nodeId : 0;
@@ -3602,8 +3617,13 @@ void RcxEditor::setEditorFont(const QString& fontName) {
// Re-apply margin styles and width with new font metrics // Re-apply margin styles and width with new font metrics
allocateMarginStyles(); allocateMarginStyles();
applyTheme(ThemeManager::instance().current()); applyTheme(ThemeManager::instance().current());
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0')); {
m_sci->setMarginWidth(0, marginSizer); int marginDigits = m_relativeOffsets
? qMax(m_layout.offsetHexDigits / 2, 4)
: m_layout.offsetHexDigits;
QString marginSizer = QString(" %1 ").arg(QString(marginDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
}
} }
void RcxEditor::setGlobalFontName(const QString& fontName) { void RcxEditor::setGlobalFontName(const QString& fontName) {

View File

@@ -84,6 +84,7 @@ signals:
void typeSelectorRequested(); void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos); void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind); void insertAboveRequested(int nodeIdx, NodeKind kind);
void relativeOffsetsChanged(bool relative);
protected: protected:
bool eventFilter(QObject* obj, QEvent* event) override; bool eventFilter(QObject* obj, QEvent* event) override;

View File

@@ -73,7 +73,7 @@ QString pointerTypeName(NodeKind kind, const QString& targetName) {
// ── Value formatting ── // ── Value formatting ──
static QString hexVal(uint64_t v) { static QString hexVal(uint64_t v) {
return QStringLiteral("0x") + QString::number(v, 16); return QString::asprintf("0x%llx", (unsigned long long)v);
} }
static QString rawHex(uint64_t v, int digits) { static QString rawHex(uint64_t v, int digits) {
@@ -228,15 +228,18 @@ static QString bytesToAscii(const QByteArray& b, int slot) {
return out; return out;
} }
static const char kHexDigits[] = "0123456789ABCDEF";
static QString bytesToHex(const QByteArray& b, int slot) { static QString bytesToHex(const QByteArray& b, int slot) {
QString out; QChar buf[64]; // max slot=8 → 8*3-1=23 chars; 64 is plenty
out.reserve(slot * 3); int pos = 0;
for (int i = 0; i < slot; ++i) { for (int i = 0; i < slot; ++i) {
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0; uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
out += QString::asprintf("%02X", (unsigned)c); buf[pos++] = QLatin1Char(kHexDigits[c >> 4]);
if (i + 1 < slot) out += ' '; buf[pos++] = QLatin1Char(kHexDigits[c & 0xF]);
if (i + 1 < slot) buf[pos++] = QLatin1Char(' ');
} }
return out; return QString(buf, pos);
} }
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr, static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
@@ -715,6 +718,7 @@ uint64_t extractBits(const Provider& prov, uint64_t addr,
case NodeKind::Hex32: container = prov.readU32(addr); break; case NodeKind::Hex32: container = prov.readU32(addr); break;
default: container = prov.readU64(addr); break; default: container = prov.readU64(addr); break;
} }
Q_ASSERT(bitOffset + bitWidth <= 64);
if (bitWidth >= 64) return container >> bitOffset; if (bitWidth >= 64) return container >> bitOffset;
return (container >> bitOffset) & ((1ULL << bitWidth) - 1); return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
} }

View File

@@ -818,17 +818,17 @@ void MainWindow::createMenus() {
auto* actTreeLines = view->addAction("&Tree Lines"); auto* actTreeLines = view->addAction("&Tree Lines");
actTreeLines->setCheckable(true); actTreeLines->setCheckable(true);
actTreeLines->setChecked(settings.value("treeLines", false).toBool()); actTreeLines->setChecked(settings.value("treeLines", true).toBool());
connect(actTreeLines, &QAction::triggered, this, [this](bool checked) { connect(actTreeLines, &QAction::triggered, this, [this](bool checked) {
QSettings("Reclass", "Reclass").setValue("treeLines", checked); QSettings("Reclass", "Reclass").setValue("treeLines", checked);
for (auto& tab : m_tabs) for (auto& tab : m_tabs)
tab.ctrl->setTreeLines(checked); tab.ctrl->setTreeLines(checked);
}); });
auto* actRelOfs = view->addAction("R&elative Offsets"); m_actRelOfs = view->addAction("R&elative Offsets");
actRelOfs->setCheckable(true); m_actRelOfs->setCheckable(true);
actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool()); m_actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool());
connect(actRelOfs, &QAction::triggered, this, [this](bool checked) { connect(m_actRelOfs, &QAction::triggered, this, [this](bool checked) {
QSettings("Reclass", "Reclass").setValue("relativeOffsets", checked); QSettings("Reclass", "Reclass").setValue("relativeOffsets", checked);
for (auto& tab : m_tabs) for (auto& tab : m_tabs)
for (auto& pane : tab.panes) for (auto& pane : tab.panes)
@@ -1250,6 +1250,16 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget); pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget);
pane.editor->setRelativeOffsets( pane.editor->setRelativeOffsets(
QSettings("Reclass", "Reclass").value("relativeOffsets", true).toBool()); QSettings("Reclass", "Reclass").value("relativeOffsets", true).toBool());
// Sync View menu checkbox when editor toggles offset mode (double-click / context menu)
connect(pane.editor, &RcxEditor::relativeOffsetsChanged, this, [this](bool rel) {
QSettings("Reclass", "Reclass").setValue("relativeOffsets", rel);
if (m_actRelOfs) m_actRelOfs->setChecked(rel);
// Propagate to all other editors so they stay in sync
for (auto& tab : m_tabs)
for (auto& p : tab.panes)
if (p.editor && p.editor != sender())
p.editor->setRelativeOffsets(rel);
});
pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0 pane.tabWidget->addTab(pane.editor, "Reclass"); // index 0
// Create per-pane rendered C++ view with find bar // Create per-pane rendered C++ view with find bar
@@ -1463,6 +1473,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
dock->setFeatures(QDockWidget::DockWidgetClosable | dock->setFeatures(QDockWidget::DockWidgetClosable |
QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetMovable |
QDockWidget::DockWidgetFloatable); QDockWidget::DockWidgetFloatable);
dock->setAttribute(Qt::WA_DeleteOnClose);
// Two title bar widgets: a hidden one (docked) and a draggable one (floating) // Two title bar widgets: a hidden one (docked) and a draggable one (floating)
auto* emptyTitleBar = new QWidget(dock); auto* emptyTitleBar = new QWidget(dock);
emptyTitleBar->setFixedHeight(0); emptyTitleBar->setFixedHeight(0);
@@ -1586,7 +1597,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
// Apply global compact columns setting to new tab // Apply global compact columns setting to new tab
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool()); ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", false).toBool()); ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", true).toBool());
ctrl->setBraceWrap(QSettings("Reclass", "Reclass").value("braceWrap", false).toBool()); ctrl->setBraceWrap(QSettings("Reclass", "Reclass").value("braceWrap", false).toBool());
// Give every controller the shared document list for cross-tab type visibility // Give every controller the shared document list for cross-tab type visibility
@@ -1629,6 +1640,8 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last(); m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
rebuildAllDocs(); rebuildAllDocs();
rebuildWorkspaceModel(); rebuildWorkspaceModel();
if (m_tabs.isEmpty())
project_new();
}); });
connect(ctrl, &RcxController::nodeSelected, connect(ctrl, &RcxController::nodeSelected,
@@ -3309,16 +3322,13 @@ void MainWindow::project_close(QDockWidget* dock) {
if (!dock) dock = m_activeDocDock; if (!dock) dock = m_activeDocDock;
if (!dock) return; if (!dock) return;
dock->close(); dock->close();
rebuildWorkspaceModel();
} }
void MainWindow::closeAllDocDocks() { void MainWindow::closeAllDocDocks() {
// Take a copy since closing modifies m_docDocks via destroyed signal // Take a copy since closing modifies m_docDocks via destroyed signal
auto docks = m_docDocks; auto docks = m_docDocks;
for (auto* dock : docks) { for (auto* dock : docks)
dock->setAttribute(Qt::WA_DeleteOnClose);
dock->close(); dock->close();
}
} }

View File

@@ -92,6 +92,7 @@ private:
PluginManager m_pluginManager; PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr; McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr; QAction* m_mcpAction = nullptr;
QAction* m_actRelOfs = nullptr;
QMenu* m_sourceMenu = nullptr; QMenu* m_sourceMenu = nullptr;
QMenu* m_recentFilesMenu = nullptr; QMenu* m_recentFilesMenu = nullptr;

View File

@@ -65,6 +65,7 @@ private:
private slots: private slots:
void initTestCase(); void initTestCase();
void benchCompose(); void benchCompose();
void benchComposeLarge();
void benchApplyDocument(); void benchApplyDocument();
void benchHoverHighlight(); void benchHoverHighlight();
void benchSelectionOverlay(); void benchSelectionOverlay();
@@ -112,6 +113,36 @@ void BenchLargeClass::benchCompose()
QVERIFY(elapsed > 0); QVERIFY(elapsed > 0);
} }
void BenchLargeClass::benchComposeLarge()
{
// Build a 2000-field tree to stress-test compose at scale
NodeTree bigTree = buildLargeTree(2000);
QByteArray buf(0x40000, '\0');
for (int i = 0; i < buf.size(); ++i) buf[i] = (char)(i & 0xFF);
BufferProvider bigProv(buf, QStringLiteral("bench_large"));
// Warmup
{ ComposeResult w = rcx::compose(bigTree, bigProv); Q_UNUSED(w); }
const int ITERS = 50;
QElapsedTimer timer;
timer.start();
for (int i = 0; i < ITERS; ++i) {
ComposeResult r = rcx::compose(bigTree, bigProv);
Q_UNUSED(r);
}
qint64 elapsed = timer.elapsed();
qDebug() << "";
qDebug() << "=== Compose Benchmark (2000 fields) ===";
qDebug() << " Tree:" << bigTree.nodes.size() << "nodes";
qDebug() << " Iterations:" << ITERS;
qDebug() << " Total:" << elapsed << "ms";
qDebug() << " Per-compose:" << (double)elapsed / ITERS << "ms";
QVERIFY(elapsed > 0);
}
void BenchLargeClass::benchApplyDocument() void BenchLargeClass::benchApplyDocument()
{ {
RcxEditor editor; RcxEditor editor;