Skip to content

Reduce path atlas memory and text scrolling overhead#33

Merged
wieslawsoltes merged 10 commits into
mainfrom
agent/reduce-path-atlas-emoji-memory
Jul 21, 2026
Merged

Reduce path atlas memory and text scrolling overhead#33
wieslawsoltes merged 10 commits into
mainfrom
agent/reduce-path-atlas-emoji-memory

Conversation

@wieslawsoltes

@wieslawsoltes wieslawsoltes commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Reduce path-atlas memory growth, particularly for layered color emoji, without changing raster quality, retained-scene invalidation, or atlas-generation behavior.

This PR:

  • replaces the independent count-only compiled fill and hit-test path caches with one shared byte-budgeted LRU;
  • makes color-glyph path identity independent of absolute destination position while preserving bounded local and device subpixel phases;
  • exposes the CPU cache budget and current usage through compositor options and metrics;
  • adds focused memory, atlas-residency, rendering, configuration, and benchmark diagnostics;
  • moves deferred Inter specimen shaping off the desktop UI thread with revision-safe
    publication, while retaining bounded slices for single-threaded browser builds;
  • records the required clean-room cross-engine research and design decisions.

Problem and root cause

The path atlas had two related sources of excessive CPU memory retention:

  1. Compiled fill and hit-test data were cached independently and bounded only by entry count. A simple path and a complex SVG/COLR emoji layer both consumed one entry even though their record and segment arrays can differ by orders of magnitude. Each cache could retain up to 4,096 entries, so complex outlines made the effective memory ceiling unpredictable.
  2. Color-font layers were transformed with their full absolute glyph position before path lookup. Repeating the same emoji at different positions therefore produced distinct PathGeometry instances, content hashes, compiled arrays, raster requests, and atlas entries even when the outline, size, style, and coverage phase were identical.
  3. The Inter specimen deferred 112 fixed-height layouts, then warmed them through the UI dispatcher while scrolling. The first italic variable-font shaping plan took 34.7 ms and later individual specimens reached 7.5–9.8 ms. This was CPU shaping rather than path-atlas pressure: the completed sweep retained only 162 path entries and 157,264 bytes of compiled path data.

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

PathAtlas now owns one LRU ordering shared by fill and hit-test compilation entries.

  • The default budget is 8 MiB.
  • CompositorOptions.PathAtlasCpuCacheBudgetBytes allows applications and tests to tune the limit.
  • Entry weight includes the typed GpuPathRecord and GpuPathSegment array payload plus conservative managed-entry overhead.
  • Cache hits update recency in average O(1) time without allocating.
  • Insertion evicts the least-recently-used entries until the new payload fits.
  • A single entry larger than the configured budget is used for the current compilation but is not retained.
  • The combined cache remains additionally capped at 4,096 entries.
  • PathAtlas.CompiledPathCacheBytes, CompiledPathCacheBudgetBytes, and CompositorMetrics.PathAtlasCpuCacheBytes expose 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:

  • the local fractional position is quantized on the existing 128-phase lattice and baked into the cached outline;
  • the integral position and quantization residual are carried by the draw transform;
  • the existing four device-translation phases per axis and ten-bit vector-text scale quantization remain authoritative;
  • SVG coordinate orientation and italic shear signs remain explicit;
  • SVG gradient brush coordinates are created in the same local phase coordinate space as the cached outline, so the residual transform moves geometry and paint together;
  • final quad placement remains unsnapped.

Both shaped DrawText and explicit DrawGlyphRun color-layer paths use the shared implementation.

Deferred text layout preparation

TextVisual.WarmDeferredLayout now captures the arranged width and text state,
constructs a CPU-only TextLayout, and atomically publishes it only if the
layout 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

  • Repeated color emoji no longer consume one path-atlas entry per absolute position.
  • CPU memory retained by compiled path data has a deterministic application-configurable ceiling.
  • Applications can observe current path-cache bytes through compositor metrics.
  • Existing callers retain the current behavior by default; the PathAtlas constructor only gains an optional budget argument.
  • Raster quality, DPI policy, color-layer ordering, bitmap emoji, fallback fonts, variable-font outline identity, scene-cache validation, and capacity retry behavior are unchanged.

In a clean main comparison, 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.

Measurement main This branch Change
Retained path-atlas entries 512 2 -99.6%
Retained managed bytes 19,946,608 546,752 -97.3%
First-render time 55.040 ms 43.243 ms -21.4%

The retained-byte result varied by less than 0.05% on main and 1.5% on this branch. One main timing 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 Browser workload 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 for main and 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, and Inter Typeface, then switches to and scrolls Data Virtualization in the same process.

The preceding pages saturated the glyph atlas. Ordinary DrawText commands did not request the atlas's existing LRU region reuse, so cached capacity misses permanently routed hundreds of visible DataGrid glyphs through vector fallback and PathAtlas. Current main reproduces the same root behavior (615 draw calls and 1,557 path entries), but the new path-cache diagnostics made the amplification measurable.

Ordinary DrawText now 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.

Measurement Before After Change
Draw calls 618 70 -88.7%
Path-atlas entries 1,556 257 -83.5%
Visual-tree compile time 0.8035 ms 0.5383 ms -33.0%
Compositor frame time 1.3424 ms 0.7827 ms -41.7%
Allocated bytes per frame 23,436 18,944 -19.2%

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

  • Compiled-path cache hit and LRU recency update: average O(1).
  • Cache insertion: O(E) for E evicted entries.
  • Retained compiled payload: O(B) for configured byte budget B, with no oversize entry retained.
  • Color-layer lookup: average O(1).
  • Transformed outline construction: O(S) only on cache miss for S path segments.
  • Repeated integer-position instances of an already cached color layer: no new outline, compiled-path, rasterization, or atlas entry.

Validation

Executed on macOS arm64 with .NET SDK 10.0.201:

dotnet test src/ProGPU.Tests/ProGPU.Tests.csproj --no-restore \
  --filter "FullyQualifiedName~CompositorReviewRegressionTests"

Passed: 147, Failed: 0, Skipped: 0

The combined compositor and sample-performance regression selection also passes:

dotnet test src/ProGPU.Tests/ProGPU.Tests.csproj --no-restore \
  --filter "FullyQualifiedName~CompositorReviewRegressionTests|FullyQualifiedName~SamplePerformanceRegressionTests"

Passed: 186, Failed: 0, Skipped: 0

Additional focused atlas selection:

dotnet test src/ProGPU.Tests/ProGPU.Tests.csproj --no-restore \
  --filter "FullyQualifiedName~PathAtlas|FullyQualifiedName~ColorLayerPaths|FullyQualifiedName~ColorEmoji|FullyQualifiedName~CompositorOptionsControlEagerGpuReservations|FullyQualifiedName~FractionalTransformedVectorGlyphs|FullyQualifiedName~ParentTransformedVectorGlyphs"

Passed: 33, Failed: 0, Skipped: 0

The focused coverage includes:

  • 48 repeated two-layer COLR glyphs rendering from at most two atlas entries;
  • 256 repeated complex two-layer COLR glyphs retaining two entries instead of the 512 observed on main;
  • ordinary DrawText recovering glyph-atlas residency after an earlier capacity fallback;
  • same-process text-page navigation preconditioning before Data Virtualization scrolling;
  • concurrent background preparation and atomic publication of deferred TextVisual layouts;
  • combined fill and hit-test compiled payload staying within a deliberately small 1,024-byte budget across 24 distinct paths;
  • vector-glyph local and parent-transform phase bounds;
  • mixed color and monochrome batching;
  • onscreen and offscreen atlas capacity recovery;
  • atlas generation and UV-coordinate preservation;
  • rendered pixel presence at translated emoji positions;
  • compositor option propagation and metrics.

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:

dotnet build src/ProGPU.Samples.Browser/ProGPU.Samples.Browser.csproj \
  -c Release --no-restore

Build succeeded. 0 warnings, 0 errors.

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:

  • Skia and SkParagraph;
  • DirectWrite, Direct2D, and Win2D;
  • WebRender;
  • Vello and Parley;
  • HarfBuzz.

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:

  1. Bound compiled path cache memory — shared byte-budgeted LRU, configuration, and metrics.
  2. Reuse color emoji atlas entries — local-phase color-layer identity and regressions.
  3. Document path atlas cache design — clean-room research and design record.
  4. Add path atlas benchmark coverage — complex emoji regression and runtime cache-residency diagnostics.
  5. Document path atlas benchmark results — reproducible main comparison and measured evidence.
  6. Recover ordinary text atlas residency — prevent atlas saturation from permanently routing virtualized text through PathAtlas.
  7. Add navigation sequence performance coverage — reproduce cache state carried across text-heavy page switches.
  8. Document text fallback architecture — measured sequence results and DXF retained-buffer design decision.
  9. Prepare Inter text layouts off the UI thread — revision-safe TextVisual publication, desktop worker, browser fallback, diagnostics, and regression coverage.
  10. 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.

@wieslawsoltes
wieslawsoltes marked this pull request as ready for review July 21, 2026 19:40
@wieslawsoltes wieslawsoltes changed the title Reduce path atlas memory for color emoji Reduce path atlas memory usage for color emoji Jul 21, 2026
@wieslawsoltes
wieslawsoltes force-pushed the agent/reduce-path-atlas-emoji-memory branch from dfdf0c7 to ecd2812 Compare July 21, 2026 20:18
@wieslawsoltes wieslawsoltes changed the title Reduce path atlas memory usage for color emoji Reduce path atlas memory and text fallback overhead Jul 21, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@wieslawsoltes wieslawsoltes changed the title Reduce path atlas memory and text fallback overhead Reduce path atlas memory and text scrolling overhead Jul 21, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +235 to +236
Interlocked.CompareExchange(ref _layout, prepared, null);
return revision == Volatile.Read(ref _layoutRevision);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@wieslawsoltes
wieslawsoltes merged commit 6e0fc94 into main Jul 21, 2026
6 checks passed
@wieslawsoltes
wieslawsoltes deleted the agent/reduce-path-atlas-emoji-memory branch July 21, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant