LayerDesk / retained rendering

The picture remembers how it was built.

The previous renderer remembered the final answer. The new architecture also remembers useful intermediate answers, so a layer toggle does not restart the whole calculation.

This explains the frozen target design. “Previous” means commit 3f2a9f3. The current working tree is part-way through the transition; this page does not claim every target layer is shipped.

01 / CONTENT + MASKThe source ingredients.
02 / PROJECTIONOne layer or group, already prepared.
03 / BACKDROPA saved partial sum of the layer stack.
04 / COMPOSITEThe final document pixels.
05 / ATOMIC FRAMEOne complete, texture-ready handoff.

Start here / one real picture

Follow one tile from layer to screen.

Forget the cache names for a moment. Begin with three transparent sheets: a sun above mountains above a sky. We will cut every sheet into tiles and follow the same tile coordinate through the renderer.

Upper node · SunContent
Attached to SunMask
Middle node · MountainsContent
Lower node · SkyContent

The document model

Layers are separate sheets, not one image.

The final picture is produced by looking upward from the bottom sheet and combining each visible sheet in order.

  1. 1Sky Content supplies the bottom pixels.
  2. 2Mountain Content is blended over the sky.
  3. 3Sun Content is first shaped by its Mask, then blended over the accumulated lower picture.
KEY IDEA

Visibility does not erase the Sun Content or Mask. It only decides whether the already-prepared Sun Projection participates in the parent stack.

1 · One large plane The Sun Content is conceptually one transparent image plane.
2 · Cut into addressable tiles Every plane is split at each LOD. Work can now stay inside visible or damaged squares.
3 · Follow one address The same coordinate is requested from Content, Mask, Projection, Backdrop, and Composite.
LOD 2 · col 2 · row 0
UPPER LAYER · SUN CONTENThighlighted: LOD 2 / 2,0

Question answered by this level

The bigger system / architecture playground

Layers × planes × LODs × tiles.

A PSD does not become one giant flattened bitmap inside the editor. Each layer keeps authoritative source information. Derived planes are built at the LOD and tile coordinates demanded by the viewport, then retained only while useful.

25%
Starting cache state

Read the title as four questions

One square,
four coordinates.

Layers say whose pixels they are. Planes say what processing stage they represent. LOD says the working resolution. Tile says which square of that resolution. Together, those four answers identify one reusable piece of image data.

The PSD owns a layer stackSun / Warmth / Mountains / Sky
Sun + mask
Warmth adjustment
Mountains + filter
Sky
Tile address
Document → Sun → Content → LOD 2 → (column 2, row 0)
First question · who owns the pixels?

Begin with ordinary PSD layers.

Input
Work performed
Output
Cache rule
CANONICAL TRUTH

Raster edits commit to Content at LOD 0. Text, vector, fill, adjustment, and filter nodes keep authoritative recipes. Higher Content mips plus every Projection, Backdrop, and Composite tile are derived: they may be invalidated, evicted, and reconstructed.

A real document / measured, not guessed

What does Luffy cost?

Luffy-Colour-Updated-Effect.psd demonstrates why “one PSD” is not one bitmap in memory. The numbers below come from LayerDesk’s current inspector and cache constants. Its 21 PSD layer records include structural divider records; the parsed tree contains 17 pixel nodes and 2 groups. Tile totals describe a full-canvas plane; real layers can have smaller bounds and only demanded tiles become resident.

1.59 GiB1,709,364,916-byte PSD
10,800 × 14,400155.52 million canvas pixels
17 + 221 PSD records → pixel + group nodes
2 masksone pixel node and one group
7 LODsLOD 0 through LOD 6
25% · LOD 2
1 plane

Current selection

LOD 2

Hypothetical resident bytes

38.30 MiB

LODOutput image64px store gridStore tilesRGBA plane256px transfer gridTransfer tilesFull payload
THE TWO TILE SIZES

64×64 store tiles are the engine’s cache and composition units: 16 KiB for RGBA8 or 4 KiB for an 8-bit mask. 256×256 transfer tiles are viewport transport units: 256 KiB RGBA8. One transfer tile covers 4×4 store tiles at the same LOD. At LOD 2 the whole Luffy canvas is 165 transfer tiles, but the historical visibility acceptance needed only 48 intersecting viewport tiles—not the whole document.

Current implementation / RAM ceilings

Hot pixels live in several independent budgets.

Source Content/Mask TileStoreLuffy-sized hard ceiling; soft pressure begins around 11.9 MiB594 MiB
Imported preview cacheIndependent per-document derived TileStore384 MiB
Live Projection/Composite cacheIndependent per-document derived TileStore384 MiB
Backdrop cacheBounded checkpoints with its own hard limit128 MiB
Renderer transfer-byte cacheUp to 4,096 full 256 KiB tile payloads1,024 MiB
WebGL texturesActive painter; unified RAM on Apple Silicon256 MiB
One-document sum of configured ceilings: about 2.70 GiB, excluding process overhead, transient decode/composition buffers, the original file, and OS file cache. This is a ceiling sum—not expected steady-state use and not a unified whole-app limit.

Disk caches and the two meanings of swap

LayerDesk swap is not macOS swap.

Original PSDLazy source reads; the file remains on disk1.59 GiB
Package source-mip cachePersistent reconstructible tiles, bounded from source size≤ 1 GiB
LayerDesk temporary tile swapLZ4 chunks, 64 MiB file-growth slabs, freed chunks reusedno byte cap
macOS virtual-memory swapOS-controlled paging of process memorynot planned
PSD / package disk
authoritative or reconstructible
hot RAM + GPU
focused viewport
LayerDesk scratch
evicted source tiles

Do not design around macOS swap. It is an emergency consequence of whole-system pressure. LayerDesk’s own tile swap is explicit application scratch, currently created in the system temporary directory. Current source gives its file allocator no explicit maximum; it grows to a high-water mark and reuses freed ranges.

The naive answer

“Keep every tile” does not fit.

One full-canvas RGBA plane, LOD 0 only38,025 padded 64px store tiles594.14 MiB
One full-canvas RGBA plane, all LODs50,944 store tiles across LOD 0–6796 MiB
17 RGBA Content pyramids + 2 Gray masksWorst case if every node covered the entire canvas13.6 GiB

That 13.6 GiB is an intentionally conservative upper bound, not measured Luffy residency. Real layer bounds can be smaller. It also excludes Projection, Backdrop, Composite, renderer bytes, and GPU textures—so eagerly retaining every derived plane would be substantially worse.

The practical answer

Retain the working set, not the document.

Visible target firstViewport ∩ damage at the chosen LODHOT
Current and complete candidate framesPinned until atomic handoffPINNED
Expensive focused Projections/BackdropsRetain while reuse probability and recompute cost are highWARM
Offscreen derived pixelsReconstruct rather than hoardEVICT

At 25% zoom LayerDesk selects LOD 2. A full Luffy RGBA plane at that LOD is only 38.30 MiB in store tiles, and a viewport usually touches less than the full 165-transfer-tile canvas.

GOOD DIRECTION
NOT FINAL POLICY

Is this the best possible memory design?

No—not yet.

The architectural direction is right: sparse tiles, demand-driven LOD, reconstructible derived planes, dependency-directed invalidation, pinning, and atomic publication. The current budget implementation is still fragmented. Independent fixed limits do not form one enforceable application memory budget.

What should remain

64px internal tiles and 256px transport tiles separate composition granularity from IPC/GPU efficiency.
LOD 0–6 generated on demand prevents full-resolution work at fit-screen zoom.
Derived outputs are evictable. Projection, Backdrop, and Composite are caches, not document truth.
Current/candidate resources are pinned so eviction cannot create partial frames.

What should change

1Unify preview, Projection, Composite, and Backdrop accounting. The frozen design says “same render-memory budget”; current code gives them separate limits.
2Use one global budget across open documents. Per-document ceilings can multiply even though only one document is focused.
3Make budgets adaptive. Derive a bounded app share from physical RAM and react to OS pressure instead of fixed constants alone.
4Bound scratch disk. Add free-space reserve, global byte cap, stale-session cleanup, and deterministic behavior when scratch is full.
5Coordinate engine, renderer, and GPU residency. A 1 GiB renderer byte cache beside a 256 MiB texture cache should participate in the same working-set policy.
6Evict by value, not only recency. Prefer active viewport, coarse fallback, and expensive-to-recompute tiles; discard cheap derived tiles before durable dirty source.

Recommended long-term memory ownership

AuthoritativeDirty LOD 0 pixels and recipes. Journal/package them durably; never lose them under pressure.
Reconstructible sourceOriginal PSD channels and source mips. Re-read or use bounded package disk cache.
Derived render cacheProjection, Backdrop, Composite, and previews under one cost-aware global RAM budget.
PresentationCurrent frame, complete candidate, renderer bytes, and textures admitted through explicit reservations.

00 / The problem

We kept the result, not the work.

A root Composite cache is excellent until an edit invalidates it. On a damaged cache miss, the previous renderer still had to walk and blend much of the stack from transparent—even when every source tile was already warm.

Previous commit
“I know the final pixel—or I start over.”

Content and masks survive. Composite tiles outside damage survive. But the renderer retains no operational per-node output and no reusable stack checkpoint inside the damaged region.

Content Projection Backdrop Composite Atomic frame
Frozen target design
“Reuse the nearest valid answer.”

Each cache level answers a different question: where are the source pixels, what does this node look like, what was below it, what is the final image, and when may the user see it?

Content Projection Backdrop Composite Atomic frame

01 / Five responsibilities

Click through the memory hierarchy.

These are not five copies of the same image. Each level owns a narrower promise. Select a level to see what it stores, what it solves, and what it deliberately does not solve.

LEVEL 01 / SOURCE

Content / Mask

What it stores

Lifetime

Problem it solves

It does not solve

02 / Toggle simulator

Move the edit through the stack.

Choose a layer, then run the conceptual simulation. The counters are work units—not benchmark milliseconds. They show why Backdrop helps most near the top of a deep stack and least near the bottom.

Target cache
Presentation
Previous architectureRoot Composite miss → rebuild from transparent
oldnewmixed
Frozen targetReuse Projection + nearest Backdrop → compose suffix
oldnewmixed
Ready. A visibility toggle preserves source Content and intrinsic Projection. It invalidates the parent stack from the node boundary upward and the affected Composite tiles.

03 / Validity

Do not throw away what did not change.

Revision orders work; damage determines stale pixels. The mutation type tells us which dependency edges changed. Explore how the preservation boundary moves.

THE VALIDITY LAW
A derived tile is valid while present because invalidation already removed every affected entry. No visibility-state combination is part of the key.

04 / “Seamless” decoded

Backdrop fixes waiting, not tearing.

Two problems can feel like one. The cache hierarchy controls how quickly the candidate frame is calculated. Atomic publication controls whether an incomplete candidate is ever visible.

Computation problem

The new frame takes too long.

Projection avoids repeating intrinsic node work. Backdrop avoids re-blending the unaffected lower prefix. Together they shorten the period for which the old frame must remain visible.

  • Owned by the Rust render engine.
  • Measured as operations visited, cache hits, and compose wall time.
  • Backdrop is the decisive optimization for deep flat stacks.
speed = reusable node output + nearest valid partial stack

Presentation problem

The user sees a mixed or broken frame.

Keep the last acknowledged sharp frame. Build target tiles offscreen. Validate revision, graph sequence, operation ownership, cancellation, complete coverage, and texture readiness. Then swap once.

  • Backdrop cannot guarantee this.
  • Tile gutters/access rects solve pixel-edge artifacts.
  • Transactional texture residency prevents checkerboard holes.
seamless = complete candidate + valid ownership + one atomic handoff

05 / Bounded memory

Retained does not mean retained forever.

Caching every intermediate after every layer would trade latency for an unbounded memory problem. Checkpoints are opportunistic and derived outputs are disposable.

Pinned until handoff
  • The currently acknowledged sharp viewport frame.
  • The complete candidate target set being published.
  • Content, Projection, and Backdrop leases held by active compose jobs.
Evictable and reconstructible
  • Offscreen Composite tiles.
  • Unused Backdrop checkpoints.
  • Unused node Projections.
  • Unpinned source tiles when package/source backing exists.

Previous: cache the answer. New: cache the reusable reasoning.

Content prevents repeated source decoding. Projection prevents repeated node preparation. Backdrop prevents repeated lower-stack blending. Composite prevents repeated final rendering. Atomic frame prevents partial presentation. SeamlessUpdate is the transaction that validates and connects all five.