mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
- 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%)
1205 lines
55 KiB
C++
1205 lines
55 KiB
C++
#include "core.h"
|
|
#include "addressparser.h"
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
|
|
namespace rcx {
|
|
|
|
namespace {
|
|
|
|
// Scintilla fold constants (avoid including Scintilla headers in core)
|
|
constexpr int SC_FOLDLEVELBASE = 0x400;
|
|
constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000;
|
|
constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL;
|
|
|
|
struct ComposeState {
|
|
QString text;
|
|
QVector<LineMeta> meta;
|
|
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
|
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
|
QSet<uint64_t> virtualPtrRefs; // refIds currently being virtually expanded via pointer deref
|
|
int currentLine = 0;
|
|
int typeW = kColType; // global type column width (fallback)
|
|
int nameW = kColName; // global name column width (fallback)
|
|
int offsetHexDigits = 8; // hex digit tier for offset margin
|
|
bool baseEmitted = false; // only first root struct shows base address
|
|
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
|
bool treeLines = false; // draw Unicode tree connectors in indentation
|
|
bool braceWrap = false; // opening brace on its own line
|
|
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
|
|
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
|
|
|
// Precomputed for O(1) lookups
|
|
QHash<uint64_t, QVector<int>> childMap;
|
|
QVector<int64_t> absOffsets; // indexed by node index
|
|
|
|
// Per-scope column widths (containerId -> width for direct children)
|
|
QHash<uint64_t, int> scopeTypeW;
|
|
QHash<uint64_t, int> scopeNameW;
|
|
|
|
int effectiveTypeW(uint64_t scopeId) const {
|
|
return scopeTypeW.value(scopeId, typeW);
|
|
}
|
|
int effectiveNameW(uint64_t scopeId) const {
|
|
return scopeNameW.value(scopeId, nameW);
|
|
}
|
|
|
|
// Set sibling-continuation flag for children at the given depth.
|
|
// childDepth is the depth of the children being iterated.
|
|
void setTreeSibling(int childDepth, bool hasMoreSiblings) {
|
|
if (!treeLines) return;
|
|
int d = childDepth - 1;
|
|
while (siblingStack.size() <= d) siblingStack.append(false);
|
|
siblingStack[d] = hasMoreSiblings;
|
|
}
|
|
|
|
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)
|
|
if (lm.lineKind == LineKind::CommandRow
|
|
|| (lm.lineKind == LineKind::Footer && lm.isRootHeader)) {
|
|
// no prefix — flush left
|
|
} else if (lm.foldHead)
|
|
text += lm.foldCollapsed ? QStringLiteral(" \u25B8 ") : QStringLiteral(" \u25BE ");
|
|
else
|
|
text += QStringLiteral(" ");
|
|
|
|
// Replace leading indent spaces with Unicode tree connectors
|
|
if (treeLines && lm.depth > 0) {
|
|
QString treeIndent;
|
|
int D = lm.depth;
|
|
bool isFooter = (lm.lineKind == LineKind::Footer);
|
|
for (int d = 0; d < D; d++) {
|
|
bool active = (d < siblingStack.size() && siblingStack[d]);
|
|
if (isFooter || d < D - 1) {
|
|
// Ancestor continuation or footer's own level
|
|
treeIndent += active ? QStringLiteral("\u2502 ")
|
|
: QStringLiteral(" ");
|
|
} else {
|
|
// This node's own connector (non-footer only)
|
|
treeIndent += active ? QStringLiteral("\u251C\u2500 ")
|
|
: QStringLiteral("\u2514\u2500 ");
|
|
}
|
|
}
|
|
text += treeIndent + lineText.mid(D * 3);
|
|
} else {
|
|
text += lineText;
|
|
}
|
|
|
|
meta.append(std::move(lm));
|
|
currentLine++;
|
|
}
|
|
};
|
|
|
|
int computeFoldLevel(int depth, bool isHead) {
|
|
int level = SC_FOLDLEVELBASE + depth;
|
|
if (isHead) level |= SC_FOLDLEVELHEADERFLAG;
|
|
return level;
|
|
}
|
|
|
|
uint32_t computeMarkers(const Node& node, const Provider& /*prov*/,
|
|
uint64_t /*addr*/, bool isCont, int /*depth*/) {
|
|
uint32_t mask = 0;
|
|
if (isCont) mask |= (1u << M_CONT);
|
|
// No ambient validation markers — errors only shown during inline editing.
|
|
return mask;
|
|
}
|
|
|
|
static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
|
|
if (refId == 0) return {};
|
|
int refIdx = tree.indexOfId(refId);
|
|
if (refIdx < 0) return {};
|
|
const Node& ref = tree.nodes[refIdx];
|
|
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
|
|
}
|
|
|
|
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
|
|
int64_t total = 0;
|
|
QSet<uint64_t> visited;
|
|
int cur = idx;
|
|
while (cur >= 0 && cur < tree.nodes.size()) {
|
|
uint64_t nid = tree.nodes[cur].id;
|
|
if (visited.contains(nid)) break;
|
|
visited.insert(nid);
|
|
const Node& n = tree.nodes[cur];
|
|
if (n.id == rootId) break;
|
|
total += n.offset;
|
|
if (n.parentId == 0) break;
|
|
cur = tree.indexOfId(n.parentId);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
static inline uint64_t resolveAddr(const ComposeState& state,
|
|
const NodeTree& tree,
|
|
int nodeIdx,
|
|
uint64_t base, uint64_t rootId) {
|
|
if (rootId != 0)
|
|
return base + relOffsetFromRoot(tree, nodeIdx, rootId);
|
|
return state.absOffsets[nodeIdx];
|
|
}
|
|
|
|
|
|
static const QVector<int>& childIndices(const ComposeState& state, uint64_t parentId) {
|
|
static const QVector<int> kEmpty;
|
|
auto it = state.childMap.constFind(parentId);
|
|
return it == state.childMap.constEnd() ? kEmpty : it.value();
|
|
}
|
|
|
|
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|
const Provider& prov, int nodeIdx,
|
|
int depth, uint64_t absAddr, uint64_t scopeId) {
|
|
const Node& node = tree.nodes[nodeIdx];
|
|
|
|
// Get per-scope widths (falls back to global if no scope entry)
|
|
int typeW = state.effectiveTypeW(scopeId);
|
|
int nameW = state.effectiveNameW(scopeId);
|
|
|
|
int numLines = linesForKind(node.kind);
|
|
|
|
// Resolve pointer target name for display
|
|
QString ptrTypeOverride;
|
|
QString ptrTargetName;
|
|
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
|
|
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) {
|
|
// Primitive pointer: e.g. "int32*" or "f64**"
|
|
const auto* meta = kindMeta(node.elementKind);
|
|
QString baseName = meta ? QString::fromLatin1(meta->typeName)
|
|
: QStringLiteral("void");
|
|
QString stars = (node.ptrDepth >= 2) ? QStringLiteral("**") : QStringLiteral("*");
|
|
ptrTypeOverride = baseName + stars;
|
|
} else {
|
|
ptrTargetName = resolvePointerTarget(tree, node.refId);
|
|
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
|
}
|
|
}
|
|
|
|
// Detect type overflow in compact mode (for effectiveTypeW)
|
|
QString rawType = ptrTypeOverride.isEmpty() ? fmt::typeNameRaw(node.kind) : ptrTypeOverride;
|
|
bool typeOverflow = state.compactColumns && rawType.size() > typeW;
|
|
int lineTypeW = typeOverflow ? rawType.size() : typeW;
|
|
|
|
for (int sub = 0; sub < numLines; sub++) {
|
|
bool isCont = (sub > 0);
|
|
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.subLine = sub;
|
|
lm.depth = depth;
|
|
lm.isContinuation = isCont;
|
|
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
|
lm.nodeKind = node.kind;
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
|
lm.foldLevel = computeFoldLevel(depth, false);
|
|
lm.effectiveTypeW = lineTypeW;
|
|
lm.effectiveNameW = nameW;
|
|
lm.pointerTargetName = ptrTargetName;
|
|
|
|
// Set byte count for hex preview lines (used for per-byte change highlighting)
|
|
if (isHexPreview(node.kind)) {
|
|
lm.lineByteCount = sizeForKind(node.kind);
|
|
}
|
|
|
|
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
|
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
|
state.compactColumns);
|
|
state.emitLine(lineText, std::move(lm));
|
|
}
|
|
}
|
|
|
|
// Forward declarations (base/rootId default to 0 = use precomputed offsets)
|
|
void composeNode(ComposeState& state, const NodeTree& tree,
|
|
const Provider& prov, int nodeIdx, int depth,
|
|
uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false,
|
|
uint64_t scopeId = 0, int arrayElementIdx = -1,
|
|
uint64_t arrayContainerAddr = 0);
|
|
void composeParent(ComposeState& state, const NodeTree& tree,
|
|
const Provider& prov, int nodeIdx, int depth,
|
|
uint64_t base = 0, uint64_t rootId = 0, bool isArrayChild = false,
|
|
uint64_t scopeId = 0, int arrayElementIdx = -1,
|
|
uint64_t arrayContainerAddr = 0);
|
|
|
|
void composeParent(ComposeState& state, const NodeTree& tree,
|
|
const Provider& prov, int nodeIdx, int depth,
|
|
uint64_t base, uint64_t rootId, bool isArrayChild,
|
|
uint64_t scopeId, int arrayElementIdx,
|
|
uint64_t arrayContainerAddr) {
|
|
const Node& node = tree.nodes[nodeIdx];
|
|
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
|
|
|
// Cycle detection
|
|
if (state.visiting.contains(node.id)) {
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = LineKind::Field;
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.nodeKind = node.kind;
|
|
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
|
lm.foldLevel = computeFoldLevel(depth, false);
|
|
state.emitLine(fmt::indent(depth) + QStringLiteral("/* CYCLE: ") +
|
|
node.name + QStringLiteral(" */"), std::move(lm));
|
|
return;
|
|
}
|
|
state.visiting.insert(node.id);
|
|
|
|
// Array element separator: show [N] to indicate which element this is
|
|
if (isArrayChild && arrayElementIdx >= 0) {
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = LineKind::ArrayElementSeparator;
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.nodeKind = node.kind;
|
|
lm.foldLevel = computeFoldLevel(depth, false);
|
|
lm.markerMask = 0;
|
|
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), std::move(lm));
|
|
}
|
|
|
|
// Detect root header: first root-level struct — suppressed from display
|
|
// (CommandRow already shows the root class type + name)
|
|
bool isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct && !state.baseEmitted);
|
|
if (isRootHeader)
|
|
state.baseEmitted = true;
|
|
|
|
// Header line (skip for array element structs and root struct)
|
|
// Root struct header is on CommandRow (type + name + {)
|
|
if (!isArrayChild && !isRootHeader) {
|
|
// Get per-scope widths for this header's parent scope
|
|
int typeW = state.effectiveTypeW(scopeId);
|
|
int nameW = state.effectiveNameW(scopeId);
|
|
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = LineKind::Header;
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.nodeKind = node.kind;
|
|
lm.isRootHeader = false;
|
|
lm.foldHead = true;
|
|
lm.foldCollapsed = node.collapsed;
|
|
lm.foldLevel = computeFoldLevel(depth, true);
|
|
lm.markerMask = (1u << M_STRUCT_BG);
|
|
|
|
QString headerText;
|
|
if (node.kind == NodeKind::Array) {
|
|
// Array header with navigation: "uint32_t[16] name {" (no brace when collapsed)
|
|
lm.isArrayHeader = true;
|
|
lm.elementKind = node.elementKind;
|
|
lm.arrayViewIdx = node.viewIndex;
|
|
lm.arrayCount = node.arrayLen;
|
|
QString elemStructName = (node.elementKind == NodeKind::Struct)
|
|
? resolvePointerTarget(tree, node.refId) : QString();
|
|
QString rawType = fmt::arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
|
bool overflow = state.compactColumns && rawType.size() > typeW;
|
|
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
|
lm.effectiveNameW = nameW;
|
|
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName, state.compactColumns);
|
|
} else {
|
|
// All structs (root and nested) use the same header format
|
|
QString rawType = fmt::structTypeName(node);
|
|
bool overflow = state.compactColumns && rawType.size() > typeW;
|
|
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
|
lm.effectiveNameW = nameW;
|
|
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
|
}
|
|
// Brace wrapping: move trailing '{' to its own line
|
|
if (state.braceWrap && !node.collapsed && headerText.endsWith(QChar('{'))) {
|
|
headerText.chop(1);
|
|
// Remove trailing separator spaces
|
|
while (headerText.endsWith(' ')) headerText.chop(1);
|
|
state.emitLine(headerText, std::move(lm));
|
|
// Emit standalone brace line
|
|
LineMeta braceLm;
|
|
braceLm.nodeIdx = nodeIdx;
|
|
braceLm.nodeId = node.id;
|
|
braceLm.depth = depth;
|
|
braceLm.lineKind = LineKind::Header;
|
|
braceLm.foldLevel = computeFoldLevel(depth, true);
|
|
braceLm.markerMask = (1u << M_STRUCT_BG);
|
|
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
|
|
} else {
|
|
state.emitLine(headerText, std::move(lm));
|
|
}
|
|
}
|
|
|
|
if (!node.collapsed || isArrayChild || isRootHeader) {
|
|
// Enum with members: render name = value lines instead of offset-based fields
|
|
if (node.resolvedClassKeyword() == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
|
int childDepth = depth + 1;
|
|
int maxNameLen = 4;
|
|
for (const auto& m : node.enumMembers)
|
|
maxNameLen = qMax(maxNameLen, (int)m.first.size());
|
|
|
|
// Build display order sorted by value
|
|
QVector<int> order(node.enumMembers.size());
|
|
std::iota(order.begin(), order.end(), 0);
|
|
std::sort(order.begin(), order.end(), [&](int a, int b) {
|
|
return node.enumMembers[a].second < node.enumMembers[b].second;
|
|
});
|
|
|
|
for (int oi = 0; oi < order.size(); oi++) {
|
|
state.setTreeSibling(childDepth, oi < order.size() - 1);
|
|
int mi = order[oi];
|
|
const auto& m = node.enumMembers[mi];
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.subLine = mi;
|
|
lm.depth = childDepth;
|
|
lm.lineKind = LineKind::Field;
|
|
lm.isMemberLine = true;
|
|
lm.nodeKind = NodeKind::UInt32;
|
|
lm.foldLevel = computeFoldLevel(childDepth, false);
|
|
lm.markerMask = 0;
|
|
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), std::move(lm));
|
|
}
|
|
|
|
// Footer
|
|
if (!isArrayChild) {
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = LineKind::Footer;
|
|
lm.nodeKind = node.kind;
|
|
lm.isRootHeader = isRootHeader;
|
|
lm.foldLevel = computeFoldLevel(depth, false);
|
|
lm.markerMask = 0;
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
state.emitLine(fmt::fmtStructFooter(node, depth, 0), std::move(lm));
|
|
}
|
|
|
|
state.visiting.remove(node.id);
|
|
return;
|
|
}
|
|
|
|
// Bitfield with members: render name : width = value lines
|
|
if (node.resolvedClassKeyword() == QStringLiteral("bitfield")
|
|
&& !node.bitfieldMembers.isEmpty()) {
|
|
int childDepth = depth + 1;
|
|
int maxNameLen = 4;
|
|
for (const auto& m : node.bitfieldMembers)
|
|
maxNameLen = qMax(maxNameLen, (int)m.name.size());
|
|
|
|
for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) {
|
|
state.setTreeSibling(childDepth, mi < node.bitfieldMembers.size() - 1);
|
|
const auto& m = node.bitfieldMembers[mi];
|
|
uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind,
|
|
m.bitOffset, m.bitWidth);
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.subLine = mi;
|
|
lm.depth = childDepth;
|
|
lm.lineKind = LineKind::Field;
|
|
lm.isMemberLine = true;
|
|
lm.nodeKind = node.elementKind;
|
|
lm.foldLevel = computeFoldLevel(childDepth, false);
|
|
lm.markerMask = 0;
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal,
|
|
childDepth, maxNameLen), std::move(lm));
|
|
}
|
|
|
|
// Footer
|
|
if (!isArrayChild) {
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = LineKind::Footer;
|
|
lm.nodeKind = node.kind;
|
|
lm.isRootHeader = isRootHeader;
|
|
lm.foldLevel = computeFoldLevel(depth, false);
|
|
lm.markerMask = 0;
|
|
int sz = sizeForKind(node.elementKind);
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr + sz;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
|
|
}
|
|
|
|
state.visiting.remove(node.id);
|
|
return;
|
|
}
|
|
|
|
const QVector<int>& allChildren = childIndices(state, node.id);
|
|
|
|
// Split children into regular nodes and static fields (static fields render at the end)
|
|
QVector<int> regular, staticIdxs;
|
|
for (int ci : allChildren) {
|
|
if (tree.nodes[ci].isStatic)
|
|
staticIdxs.append(ci);
|
|
else
|
|
regular.append(ci);
|
|
}
|
|
|
|
int childDepth = depth + 1;
|
|
|
|
// Primitive arrays with no child nodes: synthesize element lines dynamically
|
|
if (node.kind == NodeKind::Array && regular.isEmpty()
|
|
&& node.elementKind != NodeKind::Struct && node.elementKind != NodeKind::Array) {
|
|
int elemSize = sizeForKind(node.elementKind);
|
|
int eTW = state.effectiveTypeW(node.id);
|
|
int eNW = state.effectiveNameW(node.id);
|
|
for (int i = 0; i < node.arrayLen; i++) {
|
|
state.setTreeSibling(childDepth, i < node.arrayLen - 1);
|
|
uint64_t elemAddr = absAddr + i * elemSize;
|
|
|
|
// Type override: "float[0]", "uint32_t[1]", etc.
|
|
QString elemTypeStr = fmt::typeNameRaw(node.elementKind)
|
|
+ QStringLiteral("[%1]").arg(i);
|
|
|
|
Node elem;
|
|
elem.kind = node.elementKind;
|
|
elem.name = QString(); // no name for array elements
|
|
elem.offset = node.offset + i * elemSize;
|
|
elem.parentId = node.id;
|
|
elem.id = 0;
|
|
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = childDepth;
|
|
lm.lineKind = LineKind::Field;
|
|
lm.nodeKind = node.elementKind;
|
|
lm.isArrayElement = true;
|
|
lm.arrayElementIdx = i;
|
|
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
|
|
lm.offsetAddr = elemAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
|
lm.foldLevel = computeFoldLevel(childDepth, false);
|
|
bool elemOverflow = state.compactColumns && elemTypeStr.size() > eTW;
|
|
lm.effectiveTypeW = elemOverflow ? elemTypeStr.size() : eTW;
|
|
lm.effectiveNameW = eNW;
|
|
|
|
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
|
|
{}, eTW, eNW, elemTypeStr,
|
|
state.compactColumns), std::move(lm));
|
|
}
|
|
}
|
|
|
|
// Struct arrays with refId but no child nodes: synthesize by expanding the
|
|
// referenced struct for each element (like repeated pointer deref)
|
|
if (node.kind == NodeKind::Array && regular.isEmpty()
|
|
&& node.elementKind == NodeKind::Struct && node.refId != 0) {
|
|
int refIdx = tree.indexOfId(node.refId);
|
|
if (refIdx >= 0) {
|
|
int elemSize = tree.structSpan(node.refId, &state.childMap);
|
|
if (elemSize <= 0) elemSize = 1;
|
|
for (int i = 0; i < node.arrayLen; i++) {
|
|
state.setTreeSibling(childDepth, i < node.arrayLen - 1);
|
|
uint64_t elemBase = absAddr + (uint64_t)i * elemSize;
|
|
// Use base offset that maps refStruct's children to the right provider address
|
|
composeParent(state, tree, prov, refIdx, childDepth, elemBase, node.refId,
|
|
/*isArrayChild=*/true, node.id, i, absAddr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Embedded struct with refId but no child nodes: expand referenced struct's
|
|
// children at this node's offset (single instance, like array with count=1)
|
|
if (node.kind == NodeKind::Struct && regular.isEmpty() && node.refId != 0) {
|
|
int refIdx = tree.indexOfId(node.refId);
|
|
if (refIdx >= 0) {
|
|
const QVector<int>& refChildren = childIndices(state, node.refId);
|
|
// Use the referenced struct's scope widths (children come from there)
|
|
uint64_t refScopeId = node.refId;
|
|
for (int rci = 0; rci < refChildren.size(); rci++) {
|
|
int childIdx = refChildren[rci];
|
|
state.setTreeSibling(childDepth, rci < refChildren.size() - 1);
|
|
const Node& child = tree.nodes[childIdx];
|
|
// Self-referential child → show as collapsed struct (non-expandable)
|
|
if (state.visiting.contains(child.id)) {
|
|
int typeW = state.effectiveTypeW(refScopeId);
|
|
int nameW = state.effectiveNameW(refScopeId);
|
|
QString rawType = fmt::structTypeName(child);
|
|
bool overflow = state.compactColumns && rawType.size() > typeW;
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx; // parent struct — materialize target
|
|
lm.nodeId = child.id;
|
|
lm.depth = childDepth;
|
|
lm.lineKind = LineKind::Header;
|
|
lm.offsetText = fmt::fmtOffsetMargin(
|
|
absAddr + child.offset, false,
|
|
state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr + child.offset;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.nodeKind = child.kind;
|
|
lm.foldHead = true;
|
|
lm.foldCollapsed = true;
|
|
lm.foldLevel = computeFoldLevel(childDepth, true);
|
|
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
|
|
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
|
lm.effectiveNameW = nameW;
|
|
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
|
/*collapsed=*/true, typeW, nameW, state.compactColumns), std::move(lm));
|
|
continue;
|
|
}
|
|
composeNode(state, tree, prov, childIdx, childDepth,
|
|
absAddr, node.refId, false, refScopeId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// For arrays, render children as condensed (no header/footer for struct elements)
|
|
bool childrenAreArrayElements = (node.kind == NodeKind::Array);
|
|
int elementIdx = 0;
|
|
for (int ri = 0; ri < regular.size(); ri++) {
|
|
int childIdx = regular[ri];
|
|
// A regular child has more siblings if there are more regular children
|
|
// or if static fields follow after all regular children
|
|
bool hasMore = (ri < regular.size() - 1)
|
|
|| (!staticIdxs.isEmpty() && !node.collapsed);
|
|
state.setTreeSibling(childDepth, hasMore);
|
|
// Pass this container's id as the scope for children (for per-scope widths)
|
|
// For array elements, also pass the element index for [N] separator
|
|
composeNode(state, tree, prov, childIdx, childDepth, base, rootId,
|
|
childrenAreArrayElements, node.id,
|
|
childrenAreArrayElements ? elementIdx++ : -1,
|
|
childrenAreArrayElements ? absAddr : 0);
|
|
}
|
|
|
|
// ── Static fields: render after regular children, before footer ──
|
|
if (!staticIdxs.isEmpty() && (!node.collapsed || isRootHeader)) {
|
|
// Build identifier resolver for static field expressions
|
|
auto makeResolver = [&](uint64_t parentAbsAddr) {
|
|
AddressParserCallbacks cbs;
|
|
cbs.resolveIdentifier = [&tree, &prov, ®ular, parentAbsAddr]
|
|
(const QString& name, bool* ok) -> uint64_t {
|
|
if (name == QStringLiteral("base")) {
|
|
*ok = true;
|
|
return parentAbsAddr;
|
|
}
|
|
// Find sibling field by name, read its value
|
|
for (int ci : regular) {
|
|
const Node& sib = tree.nodes[ci];
|
|
if (sib.name == name) {
|
|
int sz = sib.byteSize();
|
|
uint64_t sibAddr = parentAbsAddr + sib.offset;
|
|
if (sz > 0 && prov.isValid() && prov.isReadable(sibAddr, sz)) {
|
|
*ok = true;
|
|
if (sz == 1) return (uint64_t)prov.readU8(sibAddr);
|
|
if (sz == 2) return (uint64_t)prov.readU16(sibAddr);
|
|
if (sz == 4) return (uint64_t)prov.readU32(sibAddr);
|
|
return prov.readU64(sibAddr);
|
|
}
|
|
*ok = false;
|
|
return 0;
|
|
}
|
|
}
|
|
*ok = false;
|
|
return 0;
|
|
};
|
|
int ps = tree.pointerSize;
|
|
cbs.readPointer = [&prov, ps](uint64_t addr, bool* ok) -> uint64_t {
|
|
if (prov.isValid() && prov.isReadable(addr, ps)) {
|
|
*ok = true;
|
|
return (ps >= 8) ? prov.readU64(addr)
|
|
: (uint64_t)prov.readU32(addr);
|
|
}
|
|
*ok = false;
|
|
return 0;
|
|
};
|
|
return cbs;
|
|
};
|
|
|
|
auto cbs = makeResolver(absAddr);
|
|
|
|
for (int sii = 0; sii < staticIdxs.size(); sii++) {
|
|
int si = staticIdxs[sii];
|
|
state.setTreeSibling(childDepth, sii < staticIdxs.size() - 1);
|
|
const Node& sf = tree.nodes[si];
|
|
|
|
// Evaluate expression → absolute address
|
|
uint64_t staticAddr = 0;
|
|
bool exprOk = false;
|
|
if (!sf.offsetExpr.isEmpty()) {
|
|
auto result = AddressParser::evaluate(sf.offsetExpr, tree.pointerSize, &cbs);
|
|
exprOk = result.ok;
|
|
if (result.ok)
|
|
staticAddr = result.value;
|
|
}
|
|
|
|
// Resolve type name
|
|
QString typeName;
|
|
if (sf.kind == NodeKind::Struct)
|
|
typeName = fmt::structTypeName(sf);
|
|
else if (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)
|
|
typeName = fmt::pointerTypeName(sf.kind, resolvePointerTarget(tree, sf.refId));
|
|
else
|
|
typeName = fmt::typeNameRaw(sf.kind);
|
|
|
|
bool isCollapsed = sf.collapsed;
|
|
|
|
// ── Header line: "static <type> <name> {" or collapsed: "static <type> <name> { return <expr>; }"
|
|
QString headerLine;
|
|
if (isCollapsed) {
|
|
QString exprPart;
|
|
if (!sf.offsetExpr.isEmpty()) {
|
|
if (exprOk)
|
|
exprPart = QStringLiteral("return %1 } \u2192 0x%2")
|
|
.arg(sf.offsetExpr)
|
|
.arg(QString::number(staticAddr, 16).toUpper());
|
|
else
|
|
exprPart = QStringLiteral("return %1 } (error)").arg(sf.offsetExpr);
|
|
} else {
|
|
exprPart = QStringLiteral("}");
|
|
}
|
|
headerLine = fmt::indent(childDepth)
|
|
+ QStringLiteral("static ") + typeName
|
|
+ QStringLiteral(" ") + sf.name
|
|
+ QStringLiteral(" { ") + exprPart;
|
|
} else {
|
|
headerLine = fmt::indent(childDepth)
|
|
+ QStringLiteral("static ") + typeName
|
|
+ QStringLiteral(" ") + sf.name
|
|
+ QStringLiteral(" {");
|
|
}
|
|
|
|
LineMeta lm;
|
|
lm.nodeIdx = si;
|
|
lm.nodeId = sf.id;
|
|
lm.depth = childDepth;
|
|
lm.lineKind = LineKind::Header;
|
|
lm.nodeKind = sf.kind;
|
|
lm.foldHead = true;
|
|
lm.foldCollapsed = isCollapsed;
|
|
lm.isStaticLine = true;
|
|
lm.foldLevel = computeFoldLevel(childDepth, true);
|
|
lm.markerMask = (1u << M_STRUCT_BG);
|
|
lm.offsetText = QStringLiteral("~") + QString::number(staticAddr, 16)
|
|
.toUpper().rightJustified(state.offsetHexDigits - 1, '0');
|
|
lm.offsetAddr = staticAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
|
|
lm.effectiveNameW = sf.name.size();
|
|
state.emitLine(headerLine, std::move(lm));
|
|
|
|
// ── Body + children (only when expanded) ──
|
|
if (!isCollapsed) {
|
|
// Determine if struct children follow the body line
|
|
bool hasStructKids = exprOk
|
|
&& (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array);
|
|
const QVector<int> staticKids = hasStructKids
|
|
? childIndices(state, sf.id) : QVector<int>();
|
|
hasStructKids = hasStructKids && !staticKids.isEmpty();
|
|
|
|
// Body line: " return <expr> → 0xADDR"
|
|
{
|
|
// Body has more siblings if struct children follow
|
|
state.setTreeSibling(childDepth + 1, hasStructKids);
|
|
|
|
QString bodyLine;
|
|
if (!sf.offsetExpr.isEmpty()) {
|
|
if (exprOk)
|
|
bodyLine = fmt::indent(childDepth + 1)
|
|
+ QStringLiteral("return %1").arg(sf.offsetExpr);
|
|
else
|
|
bodyLine = fmt::indent(childDepth + 1)
|
|
+ QStringLiteral("return %1 (error)").arg(sf.offsetExpr);
|
|
} else {
|
|
bodyLine = fmt::indent(childDepth + 1)
|
|
+ QStringLiteral("return 0");
|
|
}
|
|
|
|
// Right-align resolved address
|
|
if (exprOk && !sf.offsetExpr.isEmpty()) {
|
|
bodyLine += QStringLiteral(" \u2192 0x")
|
|
+ QString::number(staticAddr, 16).toUpper();
|
|
}
|
|
|
|
LineMeta blm;
|
|
blm.nodeIdx = si;
|
|
blm.nodeId = sf.id;
|
|
blm.depth = childDepth + 1;
|
|
blm.lineKind = LineKind::Field;
|
|
blm.nodeKind = sf.kind;
|
|
blm.isStaticLine = true;
|
|
blm.foldLevel = computeFoldLevel(childDepth + 1, false);
|
|
blm.markerMask = 0;
|
|
blm.offsetText = QString(state.offsetHexDigits, QChar(' '));
|
|
blm.offsetAddr = staticAddr;
|
|
blm.ptrBase = state.currentPtrBase;
|
|
state.emitLine(bodyLine, std::move(blm));
|
|
}
|
|
|
|
// If struct/array, compose children at evaluated address
|
|
if (hasStructKids) {
|
|
for (int ski = 0; ski < staticKids.size(); ski++) {
|
|
state.setTreeSibling(childDepth + 1, ski < staticKids.size() - 1);
|
|
composeNode(state, tree, prov, staticKids[ski], childDepth + 1,
|
|
staticAddr, sf.id, false, sf.id);
|
|
}
|
|
}
|
|
|
|
// Footer line: "};"
|
|
{
|
|
LineMeta flm;
|
|
flm.nodeIdx = si;
|
|
flm.nodeId = sf.id;
|
|
flm.depth = childDepth;
|
|
flm.lineKind = LineKind::Footer;
|
|
flm.nodeKind = sf.kind;
|
|
flm.isStaticLine = true;
|
|
flm.foldLevel = computeFoldLevel(childDepth, false);
|
|
flm.markerMask = 0;
|
|
if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
|
|
int sSpan = tree.structSpan(sf.id, &state.childMap);
|
|
flm.offsetText = fmt::fmtOffsetMargin(staticAddr + sSpan, false,
|
|
state.offsetHexDigits);
|
|
flm.offsetAddr = staticAddr + sSpan;
|
|
} else {
|
|
flm.offsetText = QString(state.offsetHexDigits, QChar(' '));
|
|
flm.offsetAddr = staticAddr;
|
|
}
|
|
flm.ptrBase = state.currentPtrBase;
|
|
state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), std::move(flm));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Footer line: skip when collapsed or for array element structs
|
|
if (!isArrayChild && (!node.collapsed || isRootHeader)) {
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = LineKind::Footer;
|
|
lm.nodeKind = node.kind;
|
|
lm.isRootHeader = isRootHeader; // root footer: flush left (no fold prefix)
|
|
lm.foldLevel = computeFoldLevel(depth, false);
|
|
lm.markerMask = 0;
|
|
int sz = tree.structSpan(node.id, &state.childMap);
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr + sz;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
state.emitLine(fmt::fmtStructFooter(node, depth, sz), std::move(lm));
|
|
}
|
|
|
|
state.visiting.remove(node.id);
|
|
}
|
|
|
|
void composeNode(ComposeState& state, const NodeTree& tree,
|
|
const Provider& prov, int nodeIdx, int depth,
|
|
uint64_t base, uint64_t rootId, bool isArrayChild,
|
|
uint64_t scopeId, int arrayElementIdx,
|
|
uint64_t arrayContainerAddr) {
|
|
const Node& node = tree.nodes[nodeIdx];
|
|
uint64_t absAddr = resolveAddr(state, tree, nodeIdx, base, rootId);
|
|
|
|
// Get per-scope widths for this node
|
|
int typeW = state.effectiveTypeW(scopeId);
|
|
int nameW = state.effectiveNameW(scopeId);
|
|
|
|
// Pointer deref expansion — single fold header merges pointer + struct header
|
|
if ((node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64)
|
|
&& node.refId != 0) {
|
|
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
|
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
|
|
|
// Check if this pointer has materialized children (from materializeRefChildren)
|
|
const QVector<int>& ptrChildren = childIndices(state, node.id);
|
|
bool hasMaterialized = !ptrChildren.isEmpty();
|
|
|
|
// Force collapsed if this refId is already being virtually expanded
|
|
// (prevents infinite recursion in virtual expansion mode).
|
|
// Materialized children bypass this — they are real tree nodes with
|
|
// independent collapsed state, so recursion is bounded by the tree.
|
|
bool forceCollapsed = !hasMaterialized
|
|
&& state.virtualPtrRefs.contains(node.refId);
|
|
bool effectiveCollapsed = node.collapsed || forceCollapsed;
|
|
|
|
// Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
|
|
{
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
|
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
|
lm.offsetAddr = absAddr;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.nodeKind = node.kind;
|
|
lm.foldHead = true;
|
|
lm.foldCollapsed = effectiveCollapsed;
|
|
lm.foldLevel = computeFoldLevel(depth, true);
|
|
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
|
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
|
bool ptrOverflow = state.compactColumns && ptrTypeOverride.size() > typeW;
|
|
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
|
lm.effectiveNameW = nameW;
|
|
lm.pointerTargetName = ptrTargetName;
|
|
{
|
|
QString ptrText = fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
|
prov, absAddr, ptrTypeOverride,
|
|
typeW, nameW, state.compactColumns);
|
|
if (state.braceWrap && !effectiveCollapsed && ptrText.endsWith(QChar('{'))) {
|
|
ptrText.chop(1);
|
|
while (ptrText.endsWith(' ')) ptrText.chop(1);
|
|
state.emitLine(ptrText, std::move(lm));
|
|
LineMeta braceLm;
|
|
braceLm.nodeIdx = nodeIdx;
|
|
braceLm.nodeId = node.id;
|
|
braceLm.depth = depth;
|
|
braceLm.lineKind = LineKind::Header;
|
|
braceLm.foldLevel = computeFoldLevel(depth, true);
|
|
braceLm.markerMask = lm.markerMask;
|
|
state.emitLine(fmt::indent(depth) + QStringLiteral("{"), std::move(braceLm));
|
|
} else {
|
|
state.emitLine(ptrText, std::move(lm));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!effectiveCollapsed) {
|
|
int sz = node.byteSize();
|
|
uint64_t ptrVal = 0;
|
|
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
|
|
ptrVal = (node.kind == NodeKind::Pointer32)
|
|
? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr);
|
|
if (ptrVal != 0) {
|
|
// Treat sentinel values as invalid pointers
|
|
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
|
|
ptrVal = 0;
|
|
}
|
|
}
|
|
|
|
// Pointer target address is used directly (absolute)
|
|
uint64_t pBase = ptrVal;
|
|
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
|
|
|
|
// For invalid/unreadable pointers: use NullProvider (shows zeros)
|
|
static NullProvider s_nullProv;
|
|
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
|
|
if (!ptrReadable)
|
|
pBase = 0;
|
|
|
|
uint64_t savedPtrBase = state.currentPtrBase;
|
|
state.currentPtrBase = pBase;
|
|
|
|
if (hasMaterialized) {
|
|
// Render materialized children at the pointer target address.
|
|
// These are real tree nodes with independent state — use rootId
|
|
// so resolveAddr computes offsets relative to the pointer target.
|
|
for (int pci = 0; pci < ptrChildren.size(); pci++) {
|
|
state.setTreeSibling(depth + 1, pci < ptrChildren.size() - 1);
|
|
composeNode(state, tree, childProv, ptrChildren[pci], depth + 1,
|
|
pBase, node.id, false, node.id);
|
|
}
|
|
} else {
|
|
// Virtual expansion via ref struct definition.
|
|
// Temporarily remove the ref struct from visiting so composeParent
|
|
// doesn't hit the struct-level cycle guard. The ptrVisiting mechanism
|
|
// handles actual address-level pointer cycles, and virtualPtrRefs
|
|
// prevents infinite virtual recursion (inner self-referential pointers
|
|
// are force-collapsed with M_CYCLE for the user to materialize).
|
|
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
|
if (!state.ptrVisiting.contains(key)) {
|
|
state.ptrVisiting.insert(key);
|
|
int refIdx = tree.indexOfId(node.refId);
|
|
if (refIdx >= 0) {
|
|
const Node& ref = tree.nodes[refIdx];
|
|
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
|
|
bool wasVisiting = state.visiting.remove(node.refId);
|
|
state.virtualPtrRefs.insert(node.refId);
|
|
composeParent(state, tree, childProv, refIdx,
|
|
depth, pBase, ref.id,
|
|
/*isArrayChild=*/true);
|
|
state.virtualPtrRefs.remove(node.refId);
|
|
if (wasVisiting) state.visiting.insert(node.refId);
|
|
}
|
|
}
|
|
state.ptrVisiting.remove(key);
|
|
}
|
|
}
|
|
|
|
state.currentPtrBase = savedPtrBase;
|
|
|
|
// Footer for pointer fold
|
|
{
|
|
LineMeta lm;
|
|
lm.nodeIdx = nodeIdx;
|
|
lm.nodeId = node.id;
|
|
lm.depth = depth;
|
|
lm.lineKind = LineKind::Footer;
|
|
lm.nodeKind = node.kind;
|
|
lm.offsetText.clear();
|
|
lm.foldLevel = computeFoldLevel(depth, false);
|
|
lm.markerMask = 0;
|
|
state.emitLine(fmt::indent(depth) + QStringLiteral("}"), std::move(lm));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
|
|
composeParent(state, tree, prov, nodeIdx, depth, base, rootId, isArrayChild, scopeId, arrayElementIdx, arrayContainerAddr);
|
|
} else {
|
|
composeLeaf(state, tree, prov, nodeIdx, depth, absAddr, scopeId);
|
|
}
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
|
bool compactColumns, bool treeLines, bool braceWrap) {
|
|
ComposeState state;
|
|
state.compactColumns = compactColumns;
|
|
state.treeLines = treeLines;
|
|
state.braceWrap = braceWrap;
|
|
|
|
// Precompute parent→children map
|
|
for (int i = 0; i < tree.nodes.size(); i++)
|
|
state.childMap[tree.nodes[i].parentId].append(i);
|
|
|
|
for (auto it = state.childMap.begin(); it != state.childMap.end(); ++it) {
|
|
QVector<int>& children = it.value();
|
|
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
|
return tree.nodes[a].offset < tree.nodes[b].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++)
|
|
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
|
|
{
|
|
uint64_t maxAddr = tree.baseAddress;
|
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
|
uint64_t addr = (uint64_t)state.absOffsets[i];
|
|
if (addr > maxAddr) maxAddr = addr;
|
|
}
|
|
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;
|
|
else if (maxAddr <= 0xFFFFFFFFULL) state.offsetHexDigits = 8;
|
|
else if (maxAddr <= 0xFFFFFFFFFFFFULL) state.offsetHexDigits = 12;
|
|
else state.offsetHexDigits = 16;
|
|
}
|
|
|
|
// Helper: compute the display type string for a node (for width calculation)
|
|
auto nodeTypeName = [&](const Node& n) -> QString {
|
|
if (n.kind == NodeKind::Array) {
|
|
QString sn = (n.elementKind == NodeKind::Struct)
|
|
? resolvePointerTarget(tree, n.refId) : QString();
|
|
return fmt::arrayTypeName(n.elementKind, n.arrayLen, sn);
|
|
}
|
|
if (n.kind == NodeKind::Struct)
|
|
return fmt::structTypeName(n);
|
|
if (n.kind == NodeKind::Pointer32 || n.kind == NodeKind::Pointer64)
|
|
return fmt::pointerTypeName(n.kind, resolvePointerTarget(tree, n.refId));
|
|
return fmt::typeNameRaw(n.kind);
|
|
};
|
|
|
|
// Pre-compute type name lengths (avoids re-creating temp QStrings in width loops)
|
|
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;
|
|
int maxTypeLen = kMinTypeW;
|
|
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);
|
|
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
|
|
|
// Pre-compute per-scope widths (each container gets widths based on direct children only)
|
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
|
const Node& container = tree.nodes[i];
|
|
if (container.kind != NodeKind::Struct && container.kind != NodeKind::Array)
|
|
continue;
|
|
|
|
int scopeMaxType = kMinTypeW;
|
|
int scopeMaxName = kMinNameW;
|
|
|
|
for (int childIdx : state.childMap.value(container.id)) {
|
|
const Node& child = tree.nodes[childIdx];
|
|
// Skip struct children — pointer headers shouldn't inflate sibling widths
|
|
if (child.kind == NodeKind::Struct)
|
|
continue;
|
|
scopeMaxType = qMax(scopeMaxType, typeNameLens[childIdx]);
|
|
|
|
// Name width (skip hex, but include containers)
|
|
if (!isHexPreview(child.kind)) {
|
|
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
|
}
|
|
}
|
|
|
|
// Primitive arrays with no tree children: account for synthesized element types
|
|
// e.g. "uint32_t[0]", "uint32_t[99]" — longest index determines width
|
|
if (container.kind == NodeKind::Array
|
|
&& state.childMap.value(container.id).isEmpty()
|
|
&& container.elementKind != NodeKind::Struct
|
|
&& container.elementKind != NodeKind::Array
|
|
&& container.arrayLen > 0) {
|
|
int maxIdx = container.arrayLen - 1;
|
|
QString longestElemType = fmt::typeNameRaw(container.elementKind)
|
|
+ QStringLiteral("[%1]").arg(maxIdx);
|
|
scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size());
|
|
}
|
|
|
|
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, typeCap);
|
|
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
|
}
|
|
|
|
// Compute scope widths for root level (parentId == 0)
|
|
{
|
|
int rootMaxType = kMinTypeW;
|
|
int rootMaxName = kMinNameW;
|
|
for (int childIdx : state.childMap.value(0)) {
|
|
const Node& child = tree.nodes[childIdx];
|
|
// Skip struct children — pointer headers shouldn't inflate sibling widths
|
|
if (child.kind == NodeKind::Struct)
|
|
continue;
|
|
rootMaxType = qMax(rootMaxType, typeNameLens[childIdx]);
|
|
|
|
// Name width (skip hex, include containers)
|
|
if (!isHexPreview(child.kind)) {
|
|
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
|
}
|
|
}
|
|
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, typeCap);
|
|
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
|
|
}
|
|
|
|
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
|
|
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE 0x0 struct NoName {");
|
|
{
|
|
LineMeta lm;
|
|
lm.nodeIdx = -1;
|
|
lm.nodeId = kCommandRowId;
|
|
lm.depth = 0;
|
|
lm.lineKind = LineKind::CommandRow;
|
|
lm.foldLevel = SC_FOLDLEVELBASE;
|
|
lm.foldHead = false;
|
|
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits);
|
|
lm.offsetAddr = tree.baseAddress;
|
|
lm.ptrBase = state.currentPtrBase;
|
|
lm.markerMask = 0;
|
|
lm.effectiveTypeW = state.typeW;
|
|
lm.effectiveNameW = state.nameW;
|
|
state.emitLine(cmdRowText, std::move(lm));
|
|
}
|
|
|
|
// Brace wrapping: emit standalone "{" after CommandRow
|
|
if (state.braceWrap) {
|
|
LineMeta braceLm;
|
|
braceLm.nodeIdx = -1;
|
|
braceLm.nodeId = 0; // not associated with any node (no hover)
|
|
braceLm.depth = 0;
|
|
braceLm.lineKind = LineKind::Header;
|
|
braceLm.foldLevel = SC_FOLDLEVELBASE;
|
|
braceLm.markerMask = 0;
|
|
state.emitLine(QStringLiteral("{"), std::move(braceLm));
|
|
}
|
|
|
|
const QVector<int>& roots = childIndices(state, 0);
|
|
|
|
for (int idx : roots) {
|
|
// If viewRootId is set, skip roots that don't match
|
|
if (viewRootId != 0 && tree.nodes[idx].id != viewRootId)
|
|
continue;
|
|
composeNode(state, tree, prov, idx, 0);
|
|
}
|
|
|
|
return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits, tree.baseAddress, treeLines} };
|
|
}
|
|
|
|
QSet<uint64_t> NodeTree::normalizePreferAncestors(const QSet<uint64_t>& ids) const {
|
|
QSet<uint64_t> result;
|
|
for (uint64_t id : ids) {
|
|
int idx = indexOfId(id);
|
|
if (idx < 0) continue;
|
|
bool ancestorSelected = false;
|
|
uint64_t cur = nodes[idx].parentId;
|
|
QSet<uint64_t> visited;
|
|
while (cur != 0 && !visited.contains(cur)) {
|
|
visited.insert(cur);
|
|
if (ids.contains(cur)) { ancestorSelected = true; break; }
|
|
int pi = indexOfId(cur);
|
|
if (pi < 0) break;
|
|
cur = nodes[pi].parentId;
|
|
}
|
|
if (!ancestorSelected)
|
|
result.insert(id);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) const {
|
|
QSet<uint64_t> result;
|
|
for (uint64_t id : ids) {
|
|
QVector<int> sub = subtreeIndices(id);
|
|
bool hasSelectedDescendant = false;
|
|
for (int si : sub) {
|
|
uint64_t sid = nodes[si].id;
|
|
if (sid != id && ids.contains(sid)) {
|
|
hasSelectedDescendant = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasSelectedDescendant)
|
|
result.insert(id);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
} // namespace rcx
|