Reduce GPU text memory, page-switch and resize latency#34
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8d5720df36
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5bb6e96367
ℹ️ 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".
| } | ||
|
|
||
| FlushPendingOutlineUploads(); | ||
| if (_ringOffset == 0) |
There was a problem hiding this comment.
Dispose queued batch resources on empty flush
When this empty-flush path is hit after ReplaceBatchBuffer has queued old outline buffers but before any ring dispatch has advanced _ringOffset (for example, the first oversized glyph after the outline buffers grow), it returns before draining _batchBuffers and _batchBindGroups. Those GPU resources remain referenced, and Dispose() does not clean these lists either, so large-font renders can leak the old outline buffers indefinitely unless a later non-oversized glyph happens to force a non-empty flush.
Useful? React with 👍 / 👎.
|
Browser picker follow-up (42ceb47): browser open/save/folder pickers now use the File System Access API when available, with input/upload and download fallbacks. Save writes use FileSystemFileHandle.createWritable; selected handles are bounded to 32 entries and WASM byte buffers are snapshotted before asynchronous writes. Release/AOT validation covered open transfer, save write, folder selection, transient user activation, cancellation, and zero browser console errors. Focused Release tests: 19 passed. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 42ceb4722b
ℹ️ 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".
| if (mode == 2) | ||
| { | ||
| var selection = ParseHandleSelection(result); | ||
| if (selection == null) return null; | ||
| return Path.Combine(FolderDirectory, selection.Value.Token, selection.Value.Name); |
There was a problem hiding this comment.
Wire folder handles before returning browser folders
When the browser folder picker succeeds, this returns a synthetic /tmp/progpu-browser-folder/... path, but BrowserStorageServices.Initialize() still never installs EnumerateFilesAsync, EnumerateFoldersAsync, CreateFileAsync, or CreateFolderAsync, and the JS directoryHandles map is never used by C#. Any caller that uses the returned StorageFolder then falls back to local System.IO against a non-existent virtual path, so GetFilesAsync() reports an empty folder and CreateFileAsync() can throw instead of operating on the selected browser directory. Either keep returning null for mode 2 or add platform folder services backed by the directory handle.
Useful? React with 👍 / 👎.
|
Live-resize follow-up after manual validation:
Implemented in |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf96edfa51
ℹ️ 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".
| : this( | ||
| context, | ||
| atlasSize, | ||
| Math.Min(atlasSize, DefaultColorAtlasSize), |
There was a problem hiding this comment.
Keep default color atlas growable for bitmap glyphs
With the default GlyphAtlas(context, 2048), this caps _maxColorAtlasSize at 512 even though the monochrome atlas can grow to 2048. In emoji-heavy text using bitmap-only color fonts such as CBDT/sbix, once the 512² color atlas fills, TryCreateColorBitmapGlyph returns false and the caller falls through to GetGlyphOutline, which is null for those fonts, so later emoji cache as empty glyphs instead of rendering. Keep the small initial color texture, but let the default maximum follow the configured glyph atlas size or add a bitmap-specific fallback so default text rendering does not drop color bitmap glyphs.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e058ae4c3
ℹ️ 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".
| i = lastWordIdx; | ||
| lineEnd = lastWordIdx; |
There was a problem hiding this comment.
Skip boundary spaces when wrapping multi-column lines
In LayoutMultiColumn, lastWordIdx is set to the index of the space/tab separator, so assigning i and lineEnd to that same index leaves the separator unconsumed and makes it the first character processed on the next line. For multi-column FlowDocument/Markdown paragraphs that wrap at word boundaries, wrapped lines therefore start with a leading space/tab, shifting the next word and distorting column fit/alignment; consume the separator (lastWordIdx + 1) or keep it on the completed line instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 38f985723e
ℹ️ 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".
| if (!_isFlowLayoutDirty && | ||
| Math.Abs(width - _lastFlowLayoutWidth) < 0.01f && | ||
| Math.Abs(height - _lastFlowLayoutHeight) < 0.01f) |
There was a problem hiding this comment.
Include padding in FlowDocument layout cache key
When Padding changes after the first layout and the next measure/arrange uses the same size, this cache hit returns before recomputing even though LayoutMultiColumn consumes Padding below. Since the PaddingProperty callback only updates the layout field and does not mark _isFlowLayoutDirty, text and embedded children keep their old inset until some other tracked property changes; include padding in this cache key or invalidate the flow layout when padding changes.
Useful? React with 👍 / 👎.
Summary
R8Unormtextures while retaining a separate RGBA color-glyph atlasImpact
The original sample configuration reserved 42,991,616 bytes in two RGBA coverage atlases. Exact known glyph/path atlas and staging startup residency is now 1,165,312 bytes: 97.3% lower, with the same maximum capacity and rendering quality.
The complete 47-page fresh-process Release profile produced these averages:
The largest managed-memory improvements were:
The complete methodology, attribution, managed/process/GPU counters, caveats, and all 47 before/after rows are in
docs/SAMPLE_PAGE_MEMORY_PROFILE.md. The atlas design and earlier matched browser-AOT results are indocs/GPU_TEXT_COVERAGE_CACHE_ARCHITECTURE.md.Page switching and GPU uploads
Newly visible glyphs remain demand-driven; hidden pages are not pre-rasterized. First-use work is coalesced into contiguous uniform/outline writes, one dynamic-offset bind group, one compute pass, and one submission per bounded batch. A 13-glyph regression drops from 39 queue writes, 13 bind groups, and 13 compute passes to 3 writes, 1 bind group, 1 pass, and 1 submission. Atlas contents persist across navigation, avoiding renewed upload stalls without paying the memory cost of warming every hidden page.
Root causes and implementation
GC dumps for the largest pages showed whole-font OpenFontSharp CFF/layout graphs retaining glyphs and decoded arrays even when only a small glyph set was requested. The new clean-room CFF 1 reader retains compact INDEX/DICT offsets and evaluates only the requested Type 2 charstring, including subroutines and CID FDSelect/FDArray data. Deprecated
seacand unsupported malformed programs keep a lazy compatibility fallback.Text & Documents allocation stacks showed a second
TextLayoutused only to recompute decoration widths plus per-realization positioned-character objects. Rendering now uses retained shaped advances and presenter-local bounded recycling. CPU samples also showed path-atlas free-rectangle pruning dominating recovery; the deterministic algorithm now compares only new splits against the already-pruned set while preserving the same worst-case contract and exact recovery behavior.The final allocation pass preserves one retained rich-text command list and lazily creates selection scratch storage only when selection changes. Visual-state trigger evaluation, inheritance propagation, and rich-layout traversal use cached/indexed paths without closure or interface-enumerator churn. LOL/s rotates its 500 retained controls in place, keeping one run and mutable brush per control. Across all 47 pages this pass reduced allocation another 72.2%, compilation 31.0%, and summed GC pause time 89.5% relative to the previous optimized checkpoint.
Commit
9ccfad7separately fixes the retained-command regression discussed in review: compositor cleanup no longer clears a visual-owned text command context after successful replay. The control remains the sole owner, so the memory reduction is retained and second-frame text no longer disappears.Clean-room research
The implementation was written independently from specifications, public contracts, and observable architecture; no external source was copied, ported, translated, or structurally reproduced.
GC.GetGCMemoryInfo: exact counters plus phase-separated sampled allocation attributionValidation
dotnet test src/ProGPU.Tests/ProGPU.Tests.csproj -c Release --no-restore: 2,277 passed, 0 faileddotnet test src/ProGPU.Tests.Headless/ProGPU.Tests.Headless.csproj -c Release --no-restore: 196 passed, 0 failedgit diff --check: cleanWindow resize follow-up
Interactive repaint is preserved. Cocoa can hold the normal Silk loop during live resize, so every framebuffer callback renders the latest physical size synchronously. ProGPU now suppresses the immediately following scheduled render for that same size, preventing duplicate layout/submission/present while keeping content live under the pointer. Surface capability selection is cached for the native surface lifetime and refreshed after a lost/outdated surface.
Multi-column documents retain width-independent block shaping, recycle positioned characters, use index ranges instead of copying rich-character lines, and skip duplicate measure/arrange layout at the same size. Width-dependent line breaking, embedded-control measurement, HarfBuzz line-boundary reshaping, DPI/subpixel snapping, raster quality, and atlas generation contracts remain unchanged.
The corrected matched Release workload rendered 299/299 framebuffer callbacks and suppressed 299/299 redundant scheduled frames, sustaining 116.15 visible FPS. Managed allocation improved from 1,761,326 B to 422,345 B/frame (-76.0%), Gen0 collections from 58 to 15, GC pause from 36.53 to 10.72 ms, live-resize callback max from 52.83 to 41.68 ms, and physical footprint from 496.19 to 463.46 MB. The browser Release AOT gallery also passed repeated 800x600, 1280x800, and 900x650 viewport changes with zero console errors.
The research matrix, platform-dispatch rationale, rejected alternatives, methodology, and reproduction command are in
docs/WINDOW_RESIZE_PERFORMANCE.md.Additional validation after the resize changes:
git diff --check: clean