perf(console): wrap only new console lines via a per-line cache
Each batch of new output re-wrapped all N visible lines glyph by glyph, allocating a fresh segment vector per line; a line evicted at the 10,000-line cap shifted every index, so nothing could be kept. And on every frame, idle or not, seeding the scanline shading summed the wrap rows of every line above the view. - The model numbers its lines and the view keeps one wrap per line, aligned by that number, so an eviction drops a prefix instead of forcing a rebuild. New output wraps only the new lines; the layout is then a prefix sum over the N visible lines, with no measuring and no per-line allocation. - The wraps are kept while the wrap width, line height, font size, font (by ImFont::FontId) and rasterizer density are exactly unchanged. A line-spacing change only re-sums heights; filters and fold toggles reuse the cached wraps. A resize drag still re-wraps every visible line per frame, as before, but reuses their storage. - The scanline seed is read from the prefix sum: O(1) per frame. Heights, offsets and segments are bit-identical to the old rebuild. A font reload at an unchanged size (the colour-emoji toggle) now re-wraps the console at once; before, it waited for the next line of output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -26,12 +30,27 @@ struct ConsoleWrapSegment {
|
||||
float height = 0; // visual height of this sub-row
|
||||
};
|
||||
|
||||
// Non-owning, read-only view of one line's wrap segments (points into a ConsoleWrapCache entry). Mirrors the
|
||||
// subset of std::vector the readers use (size/empty/[]/back/range-for), so HitTestConsoleLayout,
|
||||
// ConsoleTab::drawVisibleLines and the existing tests compile unchanged. begin()/end() are null-safe.
|
||||
struct ConsoleSegRef {
|
||||
const std::vector<ConsoleWrapSegment>* v = nullptr;
|
||||
std::size_t size() const { return v ? v->size() : 0; }
|
||||
bool empty() const { return size() == 0; }
|
||||
const ConsoleWrapSegment& operator[](std::size_t i) const { return (*v)[i]; }
|
||||
const ConsoleWrapSegment& back() const { return v->back(); }
|
||||
const ConsoleWrapSegment* begin() const { return v ? v->data() : nullptr; }
|
||||
const ConsoleWrapSegment* end() const { return v ? v->data() + v->size() : nullptr; }
|
||||
};
|
||||
|
||||
// The wrap layout for a set of visible lines.
|
||||
struct ConsoleLayout {
|
||||
std::vector<std::vector<ConsoleWrapSegment>> segments; // per visible line
|
||||
std::vector<ConsoleSegRef> segments; // per visible line; points into a ConsoleWrapCache (see owner)
|
||||
std::vector<float> heights; // per visible line (includes the inter-line gap)
|
||||
std::vector<float> cumulativeY; // cumulative Y offset of each visible line
|
||||
std::vector<int> rowStart; // V+1 entries: # wrap sub-rows before each visible line (scanline seed)
|
||||
float totalHeight = 0.0f;
|
||||
std::shared_ptr<const void> owner; // keeps the cache alive for BuildConsoleLayout results; null in ConsoleTab
|
||||
};
|
||||
|
||||
struct ConsoleTextPos { int line = -1; int col = 0; }; // line = raw index; col = byte offset
|
||||
@@ -50,11 +69,75 @@ struct ConsoleTextMeasure {
|
||||
// Accessor returning a visible/raw line's text by index (avoids copying the buffer).
|
||||
using ConsoleLineAccessor = std::function<const std::string&(int)>;
|
||||
|
||||
// Build the wrap layout for `visibleCount` lines (text via getVisibleText(0..N-1)).
|
||||
// Build the wrap layout for `visibleCount` lines (text via getVisibleText(0..N-1)). One-shot: fresh cache, every
|
||||
// line wrapped once. ConsoleTab uses the incremental ConsoleWrapCache + AssembleConsoleLayout path below.
|
||||
ConsoleLayout BuildConsoleLayout(int visibleCount, const ConsoleLineAccessor& getVisibleText,
|
||||
float wrapWidth, float lineHeight, float interLineGap,
|
||||
const ConsoleTextMeasure& measure);
|
||||
|
||||
// Exact inputs of WrapConsoleLine besides the text. Compared with == (never quantized: a quantized key would keep
|
||||
// old lines wrapped at the old geometry next to new lines wrapped at the new one, forever). The inter-line gap is
|
||||
// deliberately absent (added at assembly). `fontId` is ImFont::FontId: unique per AddFont and never reused across
|
||||
// an atlas Clear()/reload, unlike the ImFont* address (Typography::reload re-adds fonts in the same order/sizes).
|
||||
struct ConsoleWrapKey {
|
||||
float wrapWidth = -1.0f;
|
||||
float lineHeight = -1.0f;
|
||||
float fontSize = -1.0f;
|
||||
float rasterDensity = -1.0f;
|
||||
const void* font = nullptr;
|
||||
std::uint32_t fontId = 0;
|
||||
bool operator==(const ConsoleWrapKey& o) const {
|
||||
return wrapWidth == o.wrapWidth && lineHeight == o.lineHeight && fontSize == o.fontSize &&
|
||||
rasterDensity == o.rasterDensity && font == o.font && fontId == o.fontId;
|
||||
}
|
||||
bool operator!=(const ConsoleWrapKey& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// One raw line's cached wrap.
|
||||
struct ConsoleWrapLine {
|
||||
std::vector<ConsoleWrapSegment> segs;
|
||||
float heightNoGap = 0.0f; // sum of sub-row heights (the former builder's segY); gap added at assembly
|
||||
bool wrapped = false; // false = never wrapped, or invalidated by a key change
|
||||
};
|
||||
|
||||
// Per-RAW-line wrap cache aligned with a ConsoleModel window by absolute sequence number (index == raw model index
|
||||
// after sync()). Main thread only. ConsoleLayout::segments point INTO it, so every sync()/reset() must be followed
|
||||
// by AssembleConsoleLayout (or by clearing the layout) before the layout is read again. Appends (emplace_back) and
|
||||
// front evictions (pop_front) leave the other entries in place.
|
||||
class ConsoleWrapCache {
|
||||
public:
|
||||
// Align with the model window [modelFirstSeq, modelFirstSeq + modelSize): drop the evicted prefix (O(popped)),
|
||||
// append unwrapped entries for new lines (O(added)), mark every entry unwrapped when `key` changed (keeping the
|
||||
// vectors' capacity), and start over when the window does not chain with the cached one.
|
||||
void sync(std::uint64_t modelFirstSeq, std::size_t modelSize, const ConsoleWrapKey& key);
|
||||
// Wrap-on-demand: calls getRawText(raw) + WrapConsoleLine only when the entry is not wrapped. The geometry and
|
||||
// measure passed here must be the ones the synced key describes.
|
||||
const ConsoleWrapLine& ensure(std::size_t raw, const ConsoleLineAccessor& getRawText, float wrapWidth,
|
||||
float lineHeight, const ConsoleTextMeasure& measure);
|
||||
bool matches(const ConsoleWrapKey& key) const { return key_valid_ && key_ == key; }
|
||||
void reset(); // forget everything (ConsoleTab::clear)
|
||||
std::size_t wrapCount() const { return wrap_count_; } // lifetime WrapConsoleLine calls (tests)
|
||||
std::size_t size() const { return lines_.size(); }
|
||||
|
||||
private:
|
||||
ConsoleWrapKey key_;
|
||||
bool key_valid_ = false;
|
||||
std::uint64_t first_seq_ = 0;
|
||||
std::size_t wrap_count_ = 0;
|
||||
std::deque<ConsoleWrapLine> lines_;
|
||||
};
|
||||
|
||||
// Wrap one line (the exact former loop body of BuildConsoleLayout).
|
||||
void WrapConsoleLine(const std::string& text, float wrapWidth, float lineHeight,
|
||||
const ConsoleTextMeasure& measure, ConsoleWrapLine& out);
|
||||
|
||||
// Re-point `out` at the cached wraps of `visibleRaw` (raw model indices, ascending), wrapping only uncached lines,
|
||||
// and recompute heights / cumulativeY / rowStart / totalHeight with a fresh prefix sum (same float ops in the same
|
||||
// order as the former builder => bit-identical). Reuses out's capacity: no per-line allocation.
|
||||
void AssembleConsoleLayout(ConsoleLayout& out, ConsoleWrapCache& cache, const std::vector<int>& visibleRaw,
|
||||
const ConsoleLineAccessor& getRawText, float wrapWidth, float lineHeight,
|
||||
float interLineGap, const ConsoleTextMeasure& measure);
|
||||
|
||||
// Map a point relative to the layout origin to a (visibleRow, byte col). The caller maps
|
||||
// visibleRow -> a raw line index. Returns {0,0} when there are no visible lines.
|
||||
ConsoleHit HitTestConsoleLayout(const ConsoleLayout& layout, int visibleCount,
|
||||
|
||||
Reference in New Issue
Block a user