Repository navigation
Design memory for pictures
The owner asked to shrink an open document's memory, and chose one overall limit first (Q221). This note says why the caches add up to so much, what the limit is, and how six caches that each manage themselves give memory back under it.
The perf lab's memory-session (#620) uses the owner-shaped document on a 32 GB
machine. At the end of an ordinary session the process held 3.1 GB:
| end of session | its own cap here | |
|---|---|---|
still images (FrameBitmapCache) |
612 MB | 4 GB (1/8 of RAM) |
layer textures (LayerTextureCache, GPU) |
~1 GB, inferred | 1 GB (1/16) |
flatten cache (TileFlattenCache) |
491 MB | 512 MB (1/64) |
playback tiles (TileFrameCache) |
172 MB | 1 GB (1/32) |
compose cache (ComposeCache) |
92 MB | 1 GB (1/12) |
undo pixels (MarkSnapshot) |
0 here | 512 MB (1/64) |
Each cap was sized on its own merits and nothing sums them. Their fractions add up to a third of RAM: 10.7 GB here and 2.7 GB on the 8 GB minimum spec. Each cache also evicts only its own entries, and only when something new arrives.
Memory for pictures: 1/8 of RAM by default. That is 4 GB on a 32 GB machine and 1 GB on the minimum spec. The artist may set it between 512 MB and half the machine (at most 16 GB). A figure from the settings file is held to that range on load, since one set on a bigger machine arrives unchanged. It is one setting, in Configure → Performance, and it replaces the still-image "frame cache budget" (Q221). That figure was never saved, so nothing carries over; the new one is saved only once the artist sets it. Undo's document copies are not pictures; they are the next piece of work, not this one.
The owner set the aim plainly: responsiveness, not memory. Memory only matters when it makes the app slower. The lab then surveyed a 30-layer, 200-drawing document. At 4K, flipping between drawings rendered 200 of 203 drawings two to four times each (542 renders, 204 s of UI time), because a few frames' worth of drawings outgrew the eighth-of-the-machine limit and were evicted before the playhead came back.
So the default is now a quarter of the machine: 8 GB on a 32 GB computer and 2 GB on the 8 GB minimum spec. The still cache follows the limit instead of its own older 4 GB ceiling. The setting still goes either way. The ceiling of half the machine keeps it well clear of paging, which would cost far more time than any re-render.
A 30-layer 4K frame with onion ghosts needs about 3 GB of stills, more than the still cache's budget. So each publish evicted drawings it had just made and rendered them again. The idle warm did the same forever (B425): it took one still in, which evicted another of the same frame, which it then asked for again.
So the stills the most recent publish fetched are held even past the budget. The overshoot is one frame's worth, and only one. A first version also held the publish before, which during playback (a publish per tick) meant two frames past the budget, about 6 GB at 4K, more than the 8 GB minimum spec can spare. The leak review caught it. A publish evicts when it ends, not when it starts, so a frame the playhead has left goes back under the budget at once, and a republish of the same frame does not evict its own stills before fetching them. Fetches outside a publish (exports, background renders) are never held.
The four UI-thread stores are brokered. Still images, playback tiles,
flattened views and undo pixels each implement IPictureStore:
-
Bytes: what they hold. -
OldestEvictable: when their least recently used entry that may go was last used, on one shared clock (PictureMemory.Clock), or null if nothing may go. Pinned entries, entries in use, and a store's own minimum (the still cache keeps 6 frames) never count as evictable. -
EvictOldest(): drop that entry, and return the bytes freed.
Each is already LRU within itself, so the oldest entry overall is always one
store's oldest. PictureMemory.Enforce() runs on the UI thread after a publish.
While the total is over the limit, it evicts from whichever store's oldest entry
is the oldest, and it stops when nothing more may go.
The two render-thread caches get a slice each. Layer textures and finished
frames live on the render thread, where the UI thread cannot evict safely. Their
budgets become fixed slices of the limit: an eighth each, which is 512 MB on this
machine and 128 MB on the minimum spec, against 1 GB each before. A slice never
goes below the cache's own floor (128 MB for finished frames, 64 MB for textures),
where it would stop holding a loop or a frame's layers at all. So on a very small
limit, the six together can exceed it by the difference. The brokered
stores share the remaining three quarters (PictureMemory.Brokered), so the six
together stay within the limit. The still cache's own cap is set to that share:
which store gives way is the broker's call, not a fixed split.
Playback is left alone. While playing, the still and tile caches evict the most recent entry on purpose (B28: an LRU thrashes a loop longer than the cache). A global LRU would undo that. During play each cache keeps to its own cap, and the broker catches up at Stop. Between Play and Stop the total can exceed the limit by whatever playback brought in.
- More re-rendering on scenes bigger than the limit. That is the point: before, the same scene simply used more memory.
- One pass per publish while over the limit. It is a few comparisons per store, and it does nothing when under the limit.
-
PictureMemoryTestscovers the broker against fake stores: oldest-first across stores, pinned and floor entries kept, stops when nothing may go. - Each store's
IPictureStoreis tested against its pins and floors. -
memory-sessionis re-run with a limit below what the session used: process memory follows the limit, and the session still completes.
- Getting started
- Documents and projects
- Tools and strokes
- Brushes
- Colour
- Layers, selections and guides
- The timeline
- Onion skin, references and the camera
- Symbols
- Saving and recovery
- Exporting to a game engine
- AI assistance
- Keyboard, performance and what is planned
- Bones and rigging
- Effects — fire, smoke and water
- A simplified 3D space to draw in: planes, parallax first, orbit second
- Brush performance beyond 4k: what the GPU can and cannot buy us
- The add-on boundary
- Making the AI trustworthy
- What an AI request actually costs
- Bones and spline deformers for 2D — design
- Brush tips — a generated library, not a live calculation
- Cloud and SaaS: what the architecture already fits, and what it does not
- Colour mixing — paint that picks up what it is laid on
- DESIGN: Effects
- DESIGN: Fluid effects — fire, smoke and water as drawn line and fill
- Fluid and viscous media — notes for the next brush-engine pass
- Compositing on the GPU
- Decomposing the two big files: which pieces come out, and with which tool
- One memory limit for pictures
- .NET 10: what is safe, what is unverified, and what it narrows
- One picture cache: the paused canvas on tiles
- The paint path: one mark, made once, under the pen
- Pen dynamics: tilt and speed
- A pencil is graphite on the paper's tooth
- Performance: the ratchet and the map
- Playback's first loop
- Pressure curves — a shape, not an exponent
- Importing Procreate brushes
- A project as a game's asset set
- One hierarchy: folders scope everything
- Puppet pins — design and plan
- The raster checkpoint: making a painting cheap to reopen
- Scoped resources — palettes, references and everything else shared
- Sensitive topics, and the three-track flow
- Design: pose-assisted frame-by-frame animation (rigid bones + AI gap filling)
- Stroke groups: why a merge should never turn strokes into pixels
- The project window: what you do between drawings
- Reading the subject before drawing on it
- The ceiling of a swept soft mark (B349)
- Q171 — a symbol owns a layer stack
- Pillar 3 — reusable assets, as live symbols
- What a template is
- Text, and where fonts sit
- The tiled compositor, and getting it off the CPU
- Design tokens
- Transform tool design (M14)
- Why a brush stamps twice, and when it does not
- Migrating to the designed UI
- Undo stores only what changed
- Vector tooling: making the lines you already drew editable
- The smudge/blur family, and aligning brush settings with Photoshop
Generated from the repository. Edit the files under docs/, not these pages.