Skip to content

Optimize ProGPU and Avalonia renderer memory residency#37

Merged
wieslawsoltes merged 8 commits into
mainfrom
feature/sample-memory-optimization
Jul 25, 2026
Merged

Optimize ProGPU and Avalonia renderer memory residency#37
wieslawsoltes merged 8 commits into
mainfrom
feature/sample-memory-optimization

Conversation

@wieslawsoltes

@wieslawsoltes wieslawsoltes commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

This PR completes the measured ProGPU memory-optimization lane and applies the
same resource-lifetime improvements to the Avalonia renderer and Silk.NET host.
It reduces retained font payloads, managed frame allocations, persistent WebGPU
reservations, pipeline/binding duplication, and atlas over-allocation without
changing rendered content, animation work, DPI/subpixel policy, or cache
invalidation semantics.

It also adds a repeatable ControlCatalog benchmark with a native Avalonia Skia
comparison host, explicit compositor/GPU residency metrics, deterministic
screenshots, and a detailed 29-step design and measurement record.

What changed

Font and text residency

  • Memory-map system SFNT files and use assembly-backed embedded font data instead
    of retaining avoidable whole-file managed arrays.
  • Keep lightweight font metadata and cmap coverage resident while loading large
    bitmap-glyph payloads on demand.
  • Preserve stable HarfBuzz/typeface identity, shaping-table completeness,
    variable-font behavior, TTC support, and color emoji rendering.
  • Bound bidi and rich-document traversal work, eliminate the exhaustively
    verified level-zero ASCII resolver allocation path, and retain indexed
    virtualized scans.

Core renderer residency

  • Grow scene vertex/index, brush, gradient, atlas, glyph-outline, and related
    buffers geometrically from measured initial reservations.
  • Demand-create effect/chart and primary/offscreen pipeline families.
  • Share compatible primary/offscreen bind-group layouts, pipeline layouts, and
    immutable bind groups while preserving target format/sample-count pipeline
    identity.
  • Grow the path atlas independently on each axis and start monochrome glyph
    coverage at a 260x260 texture with a 256x256 usable packing area.
  • Start glyph-outline storage at 48 records and 896 segments, retaining the
    existing growth path for larger workloads.
  • Keep GPU hit testing independently configurable and disable the unused index
    only for the private sample offscreen-effects compositor.

Avalonia backend

  • Retain converted geometry, brushes, pens, images, and offscreen textures across
    repeated drawing operations with explicit invalidation and ownership rules.
  • Record ordinary solid lines and primitives through typed paths while retaining
    the general path/stroke fallback for dashed and complex cases.
  • Publish a copy-on-write Silk.NET window snapshot so the continuous event pump
    no longer allocates every iteration.
  • Add public diagnostics for per-frame compositor timing and explicit resource
    residency.

Benchmarking and documentation

  • Add a Release ControlCatalog benchmark with fixed warm-up/measurement frames,
    allocation/GC/process metrics, compositor metrics, JSON output, and optional
    screenshot capture.
  • Add a native Avalonia Skia comparison host that source-links the same pinned
    ControlCatalog.
  • Document all accepted and rejected changes, exact reservations, paired
    comparisons, performance noise, quality gates, research sources, and final
    validation in docs/SAMPLE_MEMORY_OPTIMIZATION.md.

Measured results

  • Avalonia TextBlock retained managed memory: 209.92 MiB to 28.68 MiB
    (-86.3%).
  • Avalonia Custom Drawing retained managed memory: 202.46 MiB to 17.84 MiB
    (-91.2%) after demand-resident font loading.
  • Avalonia four-page allocation average: 24.47 KiB/frame to 3.05 KiB/frame
    (-87.5%).
  • Persistent compositor scene buffers: 7.44 MiB to 252.2 KiB (-96.7%).
  • Unused base effect/chart resources: 14 shader modules, 16 pipelines, and 6
    layouts eliminated from the representative workload.
  • Glyph persistent reservation: 336 KiB eliminated before demand.
  • Path atlas texture: 4 MiB to 1 MiB (-75%) for the measured tall workload.
  • Resident scene pipelines: 10 to 5.
  • Duplicate primary/offscreen binding objects: 17 eliminated.
  • Initial monochrome glyph texture: 262,144 B to 67,600 B (-74.2%).
  • Initial glyph-outline GPU buffers: 51,200 B to 44,544 B (-13.0%), plus
    12,800 B less direct managed list-element reservation.
  • WinUI final 54-page average allocation: 7.85 KiB/frame, 31.3% below the
    Step 4 baseline, with average throughput remaining in the same noise band.

Same-binary and reverse-order comparisons keep steady-state allocation and
compositor performance equivalent within run-to-run noise. Exact WebGPU buffer
and texture descriptors are used for small residency claims where macOS process
footprint/WindowServer noise is larger than the resource being removed.

API and behavior notes

  • CompositorOptions gains independent initial-reservation controls for glyph
    atlases, glyph uniform staging, brushes, and gradient stops, plus explicit
    PrecompileBasePipelines.
  • CompositorMetrics, PathAtlas, GlyphAtlas, ComputeAccelerator, and the
    Avalonia diagnostics surface expose additive resource-residency counters.
  • Default maximum capacities and growth behavior are unchanged.
  • Pipeline precompilation is now opt-in. Normal rendering creates the required
    target family on first use.
  • No WPF-facing rendering contract or package-neutral DTO changed.

Rendering and performance validation

  • Final ControlCatalog benchmark: 352 retained commands, 113 draws, 26 paths,
    528 vector vertices, and 226 text vertices, matching the comparison workload.
  • Final glyph state: 260x260 monochrome atlas, 40/48 outline records, 813/896
    segments, with no growth in the representative run.
  • Final path state: 512x2048 atlas, retaining visible path coverage and stable
    texel coordinates.
  • Rectangular atlas growth now advances the UV generation and exercises the bounded
    same-frame reset/recompile path; the full supported resvg inventory passes 927/927
    with 37 explicitly skipped fixtures.
  • Deterministic final screenshot visually inspected for text, glyph, primitive,
    path, clip, and animation output.
  • No WebGPU validation, uncaptured, fatal, or other wgpu errors occurred in the
    final benchmark or test runs.

Validation

  • ProGPU.Tests: 2,398 passed.
  • ProGPU.Tests.Headless: 198 passed.
  • ProGPU.Xaml.Tests: 249 passed.
  • Avalonia ProGPU unit tests: 66 passed.
  • Avalonia ProGPU render tests: 1 passed.
  • Avalonia Skia-shim render tests: 2 passed.
  • Avalonia Silk.NET integration tests: 34 passed.
  • Avalonia 11 Rendering and Silk.NET Release builds succeeded.
  • Avalonia 12 Rendering and Silk.NET Release builds succeeded.
  • ProGPU and native-Skia ControlCatalog hosts, RenderDemo, ProGpuSandbox,
    SampleControls, and the WinUI desktop sample build in Release.
  • git diff --check is clean.

The remaining build output is limited to the existing Avalonia private-API and
upstream obsolete Skia overload warnings.

Source provenance

  • The comparison ControlCatalog source is linked from
    https://github.com/wieslawsoltes/Avalonia.git, tag
    progpu-avalonia-v12.0.5-preview.19, commit
    5378af03f17a4d9d2845882229ffed7f67350037.
  • Only the local comparison host, benchmark instrumentation, and documented
    source overrides are stored in this repository; no source-repository commit
    history was imported.

Research basis

The implementation follows primary architecture and API sources, including:

The detailed adopted/adapted/rejected analysis and reproducible measurements are
in docs/SAMPLE_MEMORY_OPTIMIZATION.md.

@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: b5123cb216

ℹ️ 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 +876 to +877
var lastData = Volatile.Read(ref _lastGlyphData);
if (lastData is not null && _lastGlyphIndex == glyphIndex)

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 Publish the glyph index and data as one atomic cache entry

When two threads request different glyphs from the same glyph-resident font, a reader can load the previous _lastGlyphData after a writer updates _lastGlyphIndex but before its Volatile.Write publishes the matching data. The fast path then returns another glyph's mini-sbix payload, causing intermittent missing or incorrect bitmap glyphs; publish an immutable index/data pair atomically or perform this lookup under _sync.

Useful? React with 👍 / 👎.

Comment on lines +2466 to +2469
else if (oldWidth < _maxAtlasSize)
{
newWidth = Math.Min(_maxAtlasSize, checked(oldWidth * 2));
enterRightHandStrip = 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 Avoid entering a strip narrower than the pending path

With a non-power-of-two maximum atlas size, the final right-hand strip can be narrower than a raster that still fit the old width. For example, after a 512-wide atlas reaches a configured 1000-pixel height, growing its width to 1000 creates only a 488-pixel strip; a 500-pixel path triggers this branch, then GetOrCreatePath wraps _currentX back to 2 and starts near the top, overlapping existing texels. Only enter the strip when it can contain the pending raster, or repack into the expanded texture.

Useful? React with 👍 / 👎.

@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: 477b5739b0

ℹ️ 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 +49 to +52
return new BidiParagraph(
0,
new sbyte[text.Length],
[new BidiRun(0, text.Length, 0)]);

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 numeric levels in the ASCII fast path

For an LTR or auto paragraph containing ASCII digits, such as abc 123, UAX #9 rule I1 assigns the European-number characters level 2, but this shortcut returns a single all-zero run. This produces incorrect BidiLevel metadata and merges numeric and surrounding text into one shaping run; restrict the shortcut to inputs whose resolved levels are actually zero or preserve the standard resolver's numeric levels.

Useful? React with 👍 / 👎.

Comment on lines +252 to 254
if (!FontApi.TryGetGlyphIndex(candidate, (uint)codepoint, out ushort glyphIndex) ||
!TryLoadFont(candidate, glyphIndex, out var cachedFont) ||
!cachedFont.Font.HasGlyph((uint)codepoint))

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 symbol-cmap remapping before rejecting fonts

When the requested family uses a Windows symbol cmap, its ASCII characters are commonly stored at private-use values such as U+F041. TtfFont.HasGlyph handles this by remapping U+0041, but FontApi.TryGetGlyphIndex performs only the raw cmap lookup; this new prefilter therefore skips the requested symbol font before HasGlyph can run and may select an unrelated fallback instead. The metadata lookup needs the same symbol remapping semantics.

Useful? React with 👍 / 👎.

@wieslawsoltes
wieslawsoltes merged commit 1417e7d into main Jul 25, 2026
7 checks passed
@wieslawsoltes
wieslawsoltes deleted the feature/sample-memory-optimization branch July 25, 2026 13:06
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