Reduce path atlas memory and text scrolling overhead#33
Conversation
dfdf0c7 to
ecd2812
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ecd2812304
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| TextRenderingMode = textRenderingMode, | ||
| TextHintingMode = textHintingMode, | ||
| UseVectorGlyphRendering = useVectorGlyphRendering, | ||
| PreferGlyphAtlas = true, |
There was a problem hiding this comment.
Preserve PreferGlyphAtlas when serializing pictures
When DrawText output is recorded and serialized through SKPicture, this new true value is lost: PictureArchive writes/reads UseVectorGlyphRendering in SKPicture.Serialization.cs but never serializes PreferGlyphAtlas, so a deserialized picture reverts to false and the capacity-recovery path enabled here can leave ordinary text on vector fallback after atlas exhaustion. Please include the flag in the archive format (with the necessary version/compat handling) so serialized DrawText keeps the same rendering behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa2c924638
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Interlocked.CompareExchange(ref _layout, prepared, null); | ||
| return revision == Volatile.Read(ref _layoutRevision); |
There was a problem hiding this comment.
Avoid publishing stale warmed layouts
When WarmDeferredLayout runs on the new background worker, a text/font/arrange invalidation can happen after the revision check above but before this CompareExchange; because ClearLayout leaves _layout as null, this can still publish the old prepared layout and only then return false. GetOrUpdateLayout later only checks width compatibility, so the stale shaped glyphs can be reused for the updated text/state; publish atomically with the revision still current, or remove prepared if the final revision check fails.
Useful? React with 👍 / 👎.
Summary
Reduce path-atlas memory growth, particularly for layered color emoji, without changing raster quality, retained-scene invalidation, or atlas-generation behavior.
This PR:
publication, while retaining bounded slices for single-threaded browser builds;
Problem and root cause
The path atlas had two related sources of excessive CPU memory retention:
PathGeometryinstances, content hashes, compiled arrays, raster requests, and atlas entries even when the outline, size, style, and coverage phase were identical.The fixed GPU atlas texture allocation was already governed by
PathAtlasSize; the unbounded behavior addressed here was retained CPU compilation data and unnecessary effective atlas occupancy.Implementation
Byte-budgeted compiled-path cache
PathAtlasnow owns one LRU ordering shared by fill and hit-test compilation entries.CompositorOptions.PathAtlasCpuCacheBudgetBytesallows applications and tests to tune the limit.GpuPathRecordandGpuPathSegmentarray payload plus conservative managed-entry overhead.O(1)time without allocating.PathAtlas.CompiledPathCacheBytes,CompiledPathCacheBudgetBytes, andCompositorMetrics.PathAtlasCpuCacheBytesexpose the resulting state.CPU-cache eviction does not move atlas UV coordinates, clear the GPU texture, or advance
PathAtlas.Generation.Position-independent color emoji layers
Color layer outlines now use the same bounded local-position model as vector glyph fallback:
Both shaped
DrawTextand explicitDrawGlyphRuncolor-layer paths use the shared implementation.Deferred text layout preparation
TextVisual.WarmDeferredLayoutnow captures the arranged width and text state,constructs a CPU-only
TextLayout, and atomically publishes it only if thelayout revision is still current. Concurrent rendering or a text/font/width
change cannot install stale preparation.
The Inter specimen starts one sequential background worker after its first
presentation on threaded desktop hosts and stops when navigation detaches the
page. Glyph-atlas allocation and upload remain visible and compositor-owned.
Browser builds without worker-thread support keep the original four-millisecond
dispatcher slicing rather than running a nominal background task on the UI
thread.
User and developer impact
PathAtlasconstructor only gains an optional budget argument.In a clean
maincomparison, 256 instances of a complex two-layer COLR glyph retain 512 atlas paths and about 19.95 MiB of managed memory. This branch retains 2 paths and about 0.55 MiB: 99.6% fewer atlas entries and 97.3% less retained managed memory.Performance and memory comparison
The focused comparison used a clean worktree at
origin/main(fc4d27a) and this branch on the same macOS arm64 host with .NET SDK 10.0.201. Each isolated process rendered 256 copies of a synthetic two-layer COLR glyph with 128 outline points per layer. Three runs were collected after a full managed collection; the table reports medians.mainThe retained-byte result varied by less than 0.05% on
mainand 1.5% on this branch. Onemaintiming sample was an outlier, so the automated regression asserts deterministic atlas residency rather than a brittle process-memory or timing threshold.The broader
Font Glyph Browserworkload used 180 warmup and 600 measured scrolling frames with presentation throttling disabled. Warmed matched runs were effectively neutral: 372.07 versus 372.02 wall FPS, 1.3912 versus 1.3976 ms compositor time, and 192,353 versus 191,440 allocated bytes per frame formainand this branch. Separate process runs had substantial system-level timing variance, so this is treated as a regression screen rather than a speedup claim. The branch reported 172 path entries, 353,232 compiled-cache bytes, and zero path-atlas resets.Navigation-sequence regression
A second benchmark reproduces real gallery navigation: it opens and scrolls
Font Glyph Browser,Text & Documents,Markdown Playground, andInter Typeface, then switches to and scrollsData Virtualizationin the same process.The preceding pages saturated the glyph atlas. Ordinary
DrawTextcommands did not request the atlas's existing LRU region reuse, so cached capacity misses permanently routed hundreds of visible DataGrid glyphs through vector fallback andPathAtlas. Currentmainreproduces the same root behavior (615 draw calls and 1,557 path entries), but the new path-cache diagnostics made the amplification measurable.Ordinary
DrawTextnow prefers bounded glyph-atlas residency. Compatible regions unused in the current frame can be recycled; atlas generation invalidates retained UV consumers, while explicit vector/CFF/transformed/color-glyph branches retain their existing coverage policy.Inter cold-scroll regression
A per-specimen trace separated the remaining Inter hitch from atlas work: 112
fixed-height layouts were being shaped inside the host update while scrolling.
In a matched unrestricted-presentation run with two warmup and 220 measured
scrolling frames, moving preparation to the sequential worker reduced the worst
host update from 17.6304 ms to 0.6410 ms (-96.4%). Short-run total allocation
still includes the same eventual retained layouts and unrestricted throughput is
scheduler-sensitive, so neither is used as an automated threshold.
The complete same-process sequence—180 scrolling frames each on Inter, Text &
Documents, and Font Glyph Browser, followed by 600 Data Virtualization frames—
reported 514.18 wall FPS, 0.5176 ms average compile time, 2.0647 ms maximum
compile time, 0.7577 ms compositor time, 69 draw calls, 117 path entries, and no
glyph-atlas evictions or clears. This verifies that the Inter worker stops after
navigation and does not transfer contention to Data Virtualization.
Why the DXF lane is not used for ordinary UI text
The DXF renderer avoids per-frame path-atlas work by compiling immutable document geometry, retained glyph records/segments, instances, brushes, and draw calls into owned GPU buffers. Pan and zoom update only a viewport uniform. This is the right architecture for static CAD content and informs the retained-scene direction.
Its analytic retained-glyph shader is not a drop-in replacement for ordinary UI text or the explicit vector/CFF fallback: it has a different per-fragment cost and antialiasing model, and does not currently implement the required 128 local phases, 16 device phases, and 8x8 coverage contract. This PR therefore uses a hybrid: glyph-atlas LRU for ordinary small text, byte-bounded PathAtlas for general paths and quality-sensitive vector/color fallbacks, and immutable DXF buffers for static documents.
Complexity and memory bounds
O(1).O(E)forEevicted entries.O(B)for configured byte budgetB, with no oversize entry retained.O(1).O(S)only on cache miss forSpath segments.Validation
Executed on macOS arm64 with .NET SDK 10.0.201:
The combined compositor and sample-performance regression selection also passes:
Additional focused atlas selection:
The focused coverage includes:
main;TextVisuallayouts;The test build also reports two existing nullable warnings in
SkRoundRectCompatibilityTests; this PR does not modify that file.The Release browser sample also builds successfully after restore, exercising
the single-threaded fallback path:
Clean-room research
The design record is in
docs/PATH_ATLAS_EMOJI_CACHE_RESEARCH.md. It compares startup/lazy initialization, layout reuse, retained scenes, visibility culling, cache identity and eviction, demand-driven upload, worker preparation, GPU batching, DPI/subpixel behavior, fallback and variable fonts, and device-loss invalidation across:The implementation is original ProGPU code. External sources informed only public contracts, architecture, algorithms, and measured tradeoffs; no foreign source text, helper structure, or data encoding was copied.
Review notes
The commits are intentionally separated:
Bound compiled path cache memory— shared byte-budgeted LRU, configuration, and metrics.Reuse color emoji atlas entries— local-phase color-layer identity and regressions.Document path atlas cache design— clean-room research and design record.Add path atlas benchmark coverage— complex emoji regression and runtime cache-residency diagnostics.Document path atlas benchmark results— reproduciblemaincomparison and measured evidence.Recover ordinary text atlas residency— prevent atlas saturation from permanently routing virtualized text through PathAtlas.Add navigation sequence performance coverage— reproduce cache state carried across text-heavy page switches.Document text fallback architecture— measured sequence results and DXF retained-buffer design decision.Prepare Inter text layouts off the UI thread— revision-safeTextVisualpublication, desktop worker, browser fallback, diagnostics, and regression coverage.Document Inter cold-scroll results— root-cause trace, measured host-update improvement, and post-navigation validation.The most important review points are the shared-budget eviction accounting, SVG brush/local-outline coordinate agreement, and preservation of existing atlas generation and vector-text phase contracts.