Summary
Fission's native GPU path inherited a very high fixed memory floor from Vello's dynamic GPU buffers. On a simple 800x600 Fission screen, release builds were using hundreds of MiB before scene complexity justified it.
This issue records the investigation, the fork we are carrying for the 0.5.1 release path, and why we are not planning to PR this exact fix upstream at this point.
Evidence
Measured on macOS / Apple M1 Pro with release builds and vmmap -summary <pid> physical footprint:
| Case |
Renderer / mode |
Physical footprint |
Notes |
examples/inbox |
default Metal/Vello |
214.4 MiB |
Simple inbox screen, 800x600 default window. |
examples/inbox |
FISSION_RENDERER=software |
57.5 MiB |
Same app without Vello GPU path. |
examples/counter |
default Metal/Vello |
247.3 MiB |
Minimal app still paid the Vello fixed dynamic-buffer cost. |
examples/motion-memory-repro |
default Metal/Vello |
262.9 MiB |
Plain/default repro path. |
| Minimal Vello two-rect probe |
after renderer creation |
~17 MiB |
Vello initialized, no render submitted. |
| Minimal Vello two-rect probe |
after first render_to_texture |
~192 MiB |
Large allocation comes from Vello render-time buffers, not Fission display-list complexity. |
The root cause we found is Vello's fixed bump-allocated dynamic buffer sizing in vello_encoding/src/config.rs::BufferSizes::new. The fixed dynamic working set was about 165 MiB before ordinary scene complexity mattered, with large allocations for tiles, lines, segment counts, path segments, blend spill, and ptcl.
External context reviewed
We looked at the existing Vello/Xilem memory discussions and adjacent renderer work:
Vello issue 366 is the key reason we should not try to upstream this exact Fission fork as-is. The fork uses the broad direction of profiling, validation, and retry/readback. That is one of the solution shapes Vello has explicitly debated and not adopted as its preferred production strategy because of the tradeoffs around readback, async behavior, and robust dynamic memory.
So for now we should treat this as a Fission product decision: the memory difference is large enough to justify carrying a fork, but we should not spend time trying to convince upstream to accept this exact design.
What we tried
1. Software renderer comparison
The software path brought examples/inbox down to ~57.5 MiB, proving the app/display-list itself was not inherently 200+ MiB.
2. Minimal Vello probe
A minimal two-rectangle Vello render jumped from ~17 MiB after renderer creation to ~192 MiB after first render. This isolated the problem to Vello render-time GPU buffers rather than Fission widgets, text, image cache, retained display list, or app state.
3. Initial Worka/Fission Vello fork
Fork: https://github.com/worka-ai/vello
Branch: worka/dynamic-gpu-buffers
The first experiment replaced the fixed dynamic buffer constants with scene/viewport-derived initial sizing. It proved the source of the memory floor:
| Case |
Physical footprint |
Peak |
Notes |
examples/inbox |
65.2 MiB |
79.0 MiB |
800x600 launch. |
examples/counter |
52.5 MiB |
66.1 MiB |
800x600 launch. |
examples/motion-memory-repro |
88.3 MiB |
106.5 MiB |
800x600 launch. |
This was not complete because it had no grow/retry validation.
4. Profiled dynamic-buffer fork
The current fork adds:
RenderWorkloadProfile / DynamicBufferPolicy so callers can provide target size, tile coverage, scene complexity, and sizing policy.
- Vello grow/retry validation by reading GPU bump counters after the coarse pass.
- Bounds checks for
ptcl and segments writes in the coarse shader so failed allocations are reported instead of producing undefined output.
- Retry-counter sanitising so counters from stages after an earlier failed stage are not blindly trusted.
- Fission integration that calculates the workload profile while walking the retained display list and passes it to Vello per render.
Measured result:
| Case |
Physical footprint |
Peak |
Notes |
examples/inbox |
50.0 MiB |
64.6 MiB |
800x600 launch. |
examples/counter |
42.4 MiB |
56.5 MiB |
800x600 launch. |
examples/motion-memory-repro |
74.1 MiB |
88.7 MiB |
800x600 launch. |
Inbox stability over 60s stayed around 47.6-49.3 MiB physical footprint with a 63.6 MiB peak.
Resize screenshots after the final profile/grow path:
| Logical viewport |
Physical footprint after screenshot |
Peak |
| 800x600 |
61.3 MiB |
66.4 MiB |
| 1200x900 |
95.8 MiB |
104.1 MiB |
| 1600x1200 |
113.9 MiB |
125.3 MiB |
Screenshots were non-flat and manually inspected; the UI rendered correctly rather than a grey/blank frame.
5. wgpu::MemoryHints::MemoryUsage
We also set wgpu::MemoryHints::MemoryUsage in Vello's RenderContext device creation path.
On the tested macOS/Metal host, this did not materially reduce memory compared with the profiled buffer fork:
| Case |
Physical footprint |
Peak |
examples/inbox |
62.5 MiB |
88.6 MiB |
examples/counter |
52.5 MiB |
81.2 MiB |
examples/motion-memory-repro |
78.9 MiB |
90.6 MiB |
It may still matter on Vulkan/DX12 backends where WGPU allocator chunk sizing is the larger contributor.
6. Direct WGPU2D prototype
We prototyped a selectable fission-render-wgpu2d path. It currently renders solid rectangles/strokes and placeholders for text, images, paths, SVG, surfaces, gradients, clips, transforms, and opacity.
It is useful as an isolation/prototype path but not a release-quality alternative yet:
| Case |
Physical footprint |
Peak |
Notes |
examples/inbox |
84.8 MiB |
98.1 MiB |
Placeholder-heavy renderer. |
examples/counter |
57.0 MiB |
86.7 MiB |
Placeholder-heavy renderer. |
examples/motion-memory-repro |
111.0 MiB |
163.3 MiB |
Placeholder-heavy renderer. |
We have parked this prototype for later and are moving forward with the Vello fork for now.
Current decision
For the 0.5.1 path we should publish and depend on Fission-owned Vello fork packages:
fission-vello-encoding
fission-vello-shaders
fission-vello
Fission can depend on fission-vello using Cargo package rename so the code can continue importing vello::....
Risk / known state
No obvious correctness regression has been seen in normal tested app/viewports, and the memory improvement is large enough to justify the fork.
There is still a high-DPI/very-large-render-target stress case around 2400x1800 logical on a 2x display. The fork reports allocation failure instead of silently going grey or allocating without bound, but the shell still expects render success. That should be tracked as follow-up hardening, not as a blocker for adopting the fork for normal app sizes.
Acceptance criteria
- Fission 0.5.1 uses the Fission-owned Vello fork packages instead of crates.io
vello directly.
- The memory profile doc remains updated with pre/post measurements.
examples/inbox, examples/counter, and examples/motion-memory-repro stay within the measured post-fork range on the same harness.
- Android/winit upgrade work continues separately so the 0.5.1 release also avoids
android-activity 0.5.2 startup crashes.
Summary
Fission's native GPU path inherited a very high fixed memory floor from Vello's dynamic GPU buffers. On a simple 800x600 Fission screen, release builds were using hundreds of MiB before scene complexity justified it.
This issue records the investigation, the fork we are carrying for the 0.5.1 release path, and why we are not planning to PR this exact fix upstream at this point.
Evidence
Measured on macOS / Apple M1 Pro with release builds and
vmmap -summary <pid>physical footprint:examples/inboxexamples/inboxFISSION_RENDERER=softwareexamples/counterexamples/motion-memory-reprorender_to_textureThe root cause we found is Vello's fixed bump-allocated dynamic buffer sizing in
vello_encoding/src/config.rs::BufferSizes::new. The fixed dynamic working set was about 165 MiB before ordinary scene complexity mattered, with large allocations for tiles, lines, segment counts, path segments, blend spill, andptcl.External context reviewed
We looked at the existing Vello/Xilem memory discussions and adjacent renderer work:
Vello issue 366 is the key reason we should not try to upstream this exact Fission fork as-is. The fork uses the broad direction of profiling, validation, and retry/readback. That is one of the solution shapes Vello has explicitly debated and not adopted as its preferred production strategy because of the tradeoffs around readback, async behavior, and robust dynamic memory.
So for now we should treat this as a Fission product decision: the memory difference is large enough to justify carrying a fork, but we should not spend time trying to convince upstream to accept this exact design.
What we tried
1. Software renderer comparison
The software path brought
examples/inboxdown to ~57.5 MiB, proving the app/display-list itself was not inherently 200+ MiB.2. Minimal Vello probe
A minimal two-rectangle Vello render jumped from ~17 MiB after renderer creation to ~192 MiB after first render. This isolated the problem to Vello render-time GPU buffers rather than Fission widgets, text, image cache, retained display list, or app state.
3. Initial Worka/Fission Vello fork
Fork: https://github.com/worka-ai/vello
Branch:
worka/dynamic-gpu-buffersThe first experiment replaced the fixed dynamic buffer constants with scene/viewport-derived initial sizing. It proved the source of the memory floor:
examples/inboxexamples/counterexamples/motion-memory-reproThis was not complete because it had no grow/retry validation.
4. Profiled dynamic-buffer fork
The current fork adds:
RenderWorkloadProfile/DynamicBufferPolicyso callers can provide target size, tile coverage, scene complexity, and sizing policy.ptclandsegmentswrites in the coarse shader so failed allocations are reported instead of producing undefined output.Measured result:
examples/inboxexamples/counterexamples/motion-memory-reproInbox stability over 60s stayed around 47.6-49.3 MiB physical footprint with a 63.6 MiB peak.
Resize screenshots after the final profile/grow path:
Screenshots were non-flat and manually inspected; the UI rendered correctly rather than a grey/blank frame.
5.
wgpu::MemoryHints::MemoryUsageWe also set
wgpu::MemoryHints::MemoryUsagein Vello'sRenderContextdevice creation path.On the tested macOS/Metal host, this did not materially reduce memory compared with the profiled buffer fork:
examples/inboxexamples/counterexamples/motion-memory-reproIt may still matter on Vulkan/DX12 backends where WGPU allocator chunk sizing is the larger contributor.
6. Direct WGPU2D prototype
We prototyped a selectable
fission-render-wgpu2dpath. It currently renders solid rectangles/strokes and placeholders for text, images, paths, SVG, surfaces, gradients, clips, transforms, and opacity.It is useful as an isolation/prototype path but not a release-quality alternative yet:
examples/inboxexamples/counterexamples/motion-memory-reproWe have parked this prototype for later and are moving forward with the Vello fork for now.
Current decision
For the 0.5.1 path we should publish and depend on Fission-owned Vello fork packages:
fission-vello-encodingfission-vello-shadersfission-velloFission can depend on
fission-vellousing Cargo package rename so the code can continue importingvello::....Risk / known state
No obvious correctness regression has been seen in normal tested app/viewports, and the memory improvement is large enough to justify the fork.
There is still a high-DPI/very-large-render-target stress case around 2400x1800 logical on a 2x display. The fork reports allocation failure instead of silently going grey or allocating without bound, but the shell still expects render success. That should be tracked as follow-up hardening, not as a blocker for adopting the fork for normal app sizes.
Acceptance criteria
vellodirectly.examples/inbox,examples/counter, andexamples/motion-memory-reprostay within the measured post-fork range on the same harness.android-activity 0.5.2startup crashes.