From 596f410b96ae9c0196448181f6560b646cc98a30 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Sun, 8 Mar 2026 07:28:26 -0600 Subject: [PATCH] =?UTF-8?q?perf:=20compose=2030%=20faster=20=E2=80=94=20mo?= =?UTF-8?q?ve=20semantics,=20BFS=20offsets,=20zero-alloc=20hex=20formattin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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%) --- src/compose.cpp | 103 +++++++++++++++++++++--------------- src/core.h | 1 + src/editor.cpp | 30 +++++++++-- src/editor.h | 1 + src/format.cpp | 16 +++--- src/main.cpp | 30 +++++++---- src/mainwindow.h | 1 + tests/bench_large_class.cpp | 31 +++++++++++ 8 files changed, 150 insertions(+), 63 deletions(-) diff --git a/src/compose.cpp b/src/compose.cpp index 7153828..98ccaf1 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -53,7 +53,7 @@ struct ComposeState { siblingStack[d] = hasMoreSiblings; } - void emitLine(const QString& lineText, LineMeta lm) { + void emitLine(const QString& lineText, LineMeta&& lm) { if (currentLine > 0) text += '\n'; // 3-char fold indicator column: " - " expanded, " + " collapsed, " " other // CommandRow has no fold prefix (flush left) @@ -87,7 +87,7 @@ struct ComposeState { text += lineText; } - meta.append(lm); + meta.append(std::move(lm)); currentLine++; } }; @@ -208,7 +208,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, /*comment=*/{}, typeW, nameW, ptrTypeOverride, 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.foldLevel = computeFoldLevel(depth, false); state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") + - node.name + QStringLiteral(" */"), lm); + node.name + QStringLiteral(" */"), std::move(lm)); return; } state.visiting.insert(node.id); @@ -267,7 +267,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.arrayElementIdx = arrayElementIdx; uint64_t relOff = absAddr - arrayContainerAddr; 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 @@ -325,7 +325,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, headerText.chop(1); // Remove trailing separator spaces while (headerText.endsWith(' ')) headerText.chop(1); - state.emitLine(headerText, lm); + state.emitLine(headerText, std::move(lm)); // Emit standalone brace line LineMeta braceLm; braceLm.nodeIdx = nodeIdx; @@ -334,9 +334,9 @@ void composeParent(ComposeState& state, const NodeTree& tree, braceLm.lineKind = LineKind::Header; braceLm.foldLevel = computeFoldLevel(depth, true); braceLm.markerMask = (1u << M_STRUCT_BG); - state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm); + state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm)); } 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.offsetAddr = absAddr; 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 @@ -389,7 +389,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits); lm.offsetAddr = absAddr; 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); @@ -423,7 +423,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.offsetAddr = absAddr; lm.ptrBase = state.currentPtrBase; state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal, - childDepth, maxNameLen), lm); + childDepth, maxNameLen), std::move(lm)); } // Footer @@ -441,7 +441,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits); lm.offsetAddr = absAddr + sz; 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); @@ -501,7 +501,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0, {}, 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.effectiveNameW = nameW; state.emitLine(fmt::fmtStructHeader(child, childDepth, - /*collapsed=*/true, typeW, nameW, state.compactColumns), lm); + /*collapsed=*/true, typeW, nameW, state.compactColumns), std::move(lm)); continue; } composeNode(state, tree, prov, childIdx, childDepth, @@ -700,7 +700,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.ptrBase = state.currentPtrBase; lm.effectiveTypeW = typeName.size() + 7; // "static " prefix lm.effectiveNameW = sf.name.size(); - state.emitLine(headerLine, lm); + state.emitLine(headerLine, std::move(lm)); // ── Body + children (only when expanded) ── if (!isCollapsed) { @@ -747,7 +747,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, blm.offsetText = QString(state.offsetHexDigits, QChar(' ')); blm.offsetAddr = staticAddr; blm.ptrBase = state.currentPtrBase; - state.emitLine(bodyLine, blm); + state.emitLine(bodyLine, std::move(blm)); } // If struct/array, compose children at evaluated address @@ -780,7 +780,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, flm.offsetAddr = staticAddr; } 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.offsetAddr = absAddr + sz; 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); @@ -865,7 +865,7 @@ void composeNode(ComposeState& state, const NodeTree& tree, if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) { ptrText.chop(1); while (ptrText.endsWith(' ')) ptrText.chop(1); - state.emitLine(ptrText, lm); + state.emitLine(ptrText, std::move(lm)); LineMeta braceLm; braceLm.nodeIdx = nodeIdx; braceLm.nodeId = node.id; @@ -873,9 +873,9 @@ void composeNode(ComposeState& state, const NodeTree& tree, braceLm.lineKind = LineKind::Header; braceLm.foldLevel = computeFoldLevel(depth, true); braceLm.markerMask = lm.markerMask; - state.emitLine(fmt::indent(depth) + QStringLiteral("{"), braceLm); + state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm)); } 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.foldLevel = computeFoldLevel(depth, false); lm.markerMask = 0; - state.emitLine(fmt::indent(depth) + QStringLiteral("}"), lm); + state.emitLine(fmt::indent(depth) + QStringLiteral("}"), std::move(lm)); } } 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.fill(0); 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 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 { @@ -1020,23 +1042,21 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR return fmt::typeNameRaw(n.kind); }; - // Compute effective type column width from longest type name - // Include struct/array headers which use "struct TypeName" or "type[count]" format + // Pre-compute type name lengths (avoids re-creating temp QStrings in width loops) + QVector 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; int maxTypeLen = kMinTypeW; - for (const Node& node : tree.nodes) { - maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size()); + int maxNameLen = kMinNameW; + 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); - - // 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); // 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 if (child.kind == NodeKind::Struct) continue; - scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size()); + scopeMaxType = qMax(scopeMaxType, typeNameLens[childIdx]); // Name width (skip hex, but include containers) 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) - // Include struct/array headers - they now use columnar layout too { int rootMaxType = kMinTypeW; 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 if (child.kind == NodeKind::Struct) continue; - rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size()); + rootMaxType = qMax(rootMaxType, typeNameLens[childIdx]); // Name width (skip hex, include containers) if (!isHexPreview(child.kind)) { @@ -1115,7 +1134,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR lm.markerMask = 0; lm.effectiveTypeW = state.typeW; lm.effectiveNameW = state.nameW; - state.emitLine(cmdRowText, lm); + state.emitLine(cmdRowText, std::move(lm)); } // 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.foldLevel = SC_FOLDLEVELBASE; braceLm.markerMask = 0; - state.emitLine(QStringLiteral("{"), braceLm); + state.emitLine(QStringLiteral("{"), std::move(braceLm)); } const QVector& roots = childIndices(state, 0); diff --git a/src/core.h b/src/core.h index 6a3b7cd..08adcdb 100644 --- a/src/core.h +++ b/src/core.h @@ -575,6 +575,7 @@ static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits // Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48) inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) { + Q_ASSERT(elemIdx >= 0); return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift); } inline int arrayElemIdxFromSelId(uint64_t selId) { diff --git a/src/editor.cpp b/src/editor.cpp index bb91def..75c3a8f 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -562,9 +562,11 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { if (chosen == actRel && !m_relativeOffsets) { m_relativeOffsets = true; reformatMargins(); + emit relativeOffsetsChanged(true); } else if (chosen == actAbs && m_relativeOffsets) { m_relativeOffsets = false; reformatMargins(); + emit relativeOffsetsChanged(false); } return; } @@ -958,8 +960,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) { } // Dynamically resize margin to fit the current hex digit tier - QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0')); - m_sci->setMarginWidth(0, marginSizer); + // RVA mode uses half width since relative offsets are much shorter + { + 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->setText(result.text); @@ -1066,6 +1074,11 @@ void RcxEditor::reformatMargins() { uint64_t base = m_layout.baseAddress; 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) ── m_sci->clearMarginText(-1); for (int i = 0; i < m_meta.size(); i++) { @@ -2195,6 +2208,7 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { #endif m_relativeOffsets = !m_relativeOffsets; reformatMargins(); + emit relativeOffsetsChanged(m_relativeOffsets); return true; } } @@ -2274,7 +2288,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { m_hoverInside = m_sci->viewport()->rect().contains(m_lastHoverPos); } // 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)) { auto h = hitTest(m_lastHoverPos); 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 allocateMarginStyles(); 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) { diff --git a/src/editor.h b/src/editor.h index 13af934..2911f1c 100644 --- a/src/editor.h +++ b/src/editor.h @@ -84,6 +84,7 @@ signals: void typeSelectorRequested(); void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos); void insertAboveRequested(int nodeIdx, NodeKind kind); + void relativeOffsetsChanged(bool relative); protected: bool eventFilter(QObject* obj, QEvent* event) override; diff --git a/src/format.cpp b/src/format.cpp index 4939d1c..7a7aba6 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -73,7 +73,7 @@ QString pointerTypeName(NodeKind kind, const QString& targetName) { // ── Value formatting ── 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) { @@ -228,15 +228,18 @@ static QString bytesToAscii(const QByteArray& b, int slot) { return out; } +static const char kHexDigits[] = "0123456789ABCDEF"; + static QString bytesToHex(const QByteArray& b, int slot) { - QString out; - out.reserve(slot * 3); + QChar buf[64]; // max slot=8 → 8*3-1=23 chars; 64 is plenty + int pos = 0; for (int i = 0; i < slot; ++i) { uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0; - out += QString::asprintf("%02X", (unsigned)c); - if (i + 1 < slot) out += ' '; + buf[pos++] = QLatin1Char(kHexDigits[c >> 4]); + 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, @@ -715,6 +718,7 @@ uint64_t extractBits(const Provider& prov, uint64_t addr, case NodeKind::Hex32: container = prov.readU32(addr); break; default: container = prov.readU64(addr); break; } + Q_ASSERT(bitOffset + bitWidth <= 64); if (bitWidth >= 64) return container >> bitOffset; return (container >> bitOffset) & ((1ULL << bitWidth) - 1); } diff --git a/src/main.cpp b/src/main.cpp index d423d49..03d054f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -818,17 +818,17 @@ void MainWindow::createMenus() { auto* actTreeLines = view->addAction("&Tree Lines"); 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) { QSettings("Reclass", "Reclass").setValue("treeLines", checked); for (auto& tab : m_tabs) tab.ctrl->setTreeLines(checked); }); - auto* actRelOfs = view->addAction("R&elative Offsets"); - actRelOfs->setCheckable(true); - actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool()); - connect(actRelOfs, &QAction::triggered, this, [this](bool checked) { + m_actRelOfs = view->addAction("R&elative Offsets"); + m_actRelOfs->setCheckable(true); + m_actRelOfs->setChecked(settings.value("relativeOffsets", true).toBool()); + connect(m_actRelOfs, &QAction::triggered, this, [this](bool checked) { QSettings("Reclass", "Reclass").setValue("relativeOffsets", checked); for (auto& tab : m_tabs) for (auto& pane : tab.panes) @@ -1250,6 +1250,16 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { pane.editor = tab.ctrl->addSplitEditor(pane.tabWidget); pane.editor->setRelativeOffsets( 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 // Create per-pane rendered C++ view with find bar @@ -1463,6 +1473,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) { dock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + dock->setAttribute(Qt::WA_DeleteOnClose); // Two title bar widgets: a hidden one (docked) and a draggable one (floating) auto* emptyTitleBar = new QWidget(dock); emptyTitleBar->setFixedHeight(0); @@ -1586,7 +1597,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) { // Apply global compact columns setting to new tab 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()); // 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(); rebuildAllDocs(); rebuildWorkspaceModel(); + if (m_tabs.isEmpty()) + project_new(); }); connect(ctrl, &RcxController::nodeSelected, @@ -3309,16 +3322,13 @@ void MainWindow::project_close(QDockWidget* dock) { if (!dock) dock = m_activeDocDock; if (!dock) return; dock->close(); - rebuildWorkspaceModel(); } void MainWindow::closeAllDocDocks() { // Take a copy since closing modifies m_docDocks via destroyed signal auto docks = m_docDocks; - for (auto* dock : docks) { - dock->setAttribute(Qt::WA_DeleteOnClose); + for (auto* dock : docks) dock->close(); - } } diff --git a/src/mainwindow.h b/src/mainwindow.h index 399c928..e875393 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -92,6 +92,7 @@ private: PluginManager m_pluginManager; McpBridge* m_mcp = nullptr; QAction* m_mcpAction = nullptr; + QAction* m_actRelOfs = nullptr; QMenu* m_sourceMenu = nullptr; QMenu* m_recentFilesMenu = nullptr; diff --git a/tests/bench_large_class.cpp b/tests/bench_large_class.cpp index df2bbda..c6d5632 100644 --- a/tests/bench_large_class.cpp +++ b/tests/bench_large_class.cpp @@ -65,6 +65,7 @@ private: private slots: void initTestCase(); void benchCompose(); + void benchComposeLarge(); void benchApplyDocument(); void benchHoverHighlight(); void benchSelectionOverlay(); @@ -112,6 +113,36 @@ void BenchLargeClass::benchCompose() 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() { RcxEditor editor;