LavaUI docs

enum ImageStore

Swift

enum ImageStore

Path → UIImage cache with a VRAM budget, async decode, and LRU eviction.

UI thread only, except for the decode itself.

Why the budget evicts and visibility does not. The obvious policy — drop a poster once it scrolls off — thrashes: reverse direction and every image you just discarded has to be fetched and decoded again. Visibility belongs in priority; a byte budget belongs in lifetime. An entry is only evicted when the cache is over budget, and never on the frame it was drawn, so a grid larger than the budget degrades to re-decoding rather than flickering mid-frame.

Sources/LavaUI/Image.swift:79

Properties

nonisolated(unsafe) static var budgetBytes: Int

Bytes of decoded image the cache will hold before evicting. Generous by default: a screenful of covers is a few MB, and evicting sooner than necessary just means decoding again.

Sources/LavaUI/Image.swift:105

static var count: Int { get }

Not documented.

Sources/LavaUI/Image.swift:328

static var residentByteCount: Int { get }

Bytes currently held, for tests and diagnostics.

Sources/LavaUI/Image.swift:327

Methods

static func contentKey(data: [UInt8], maxPixelSize: UInt32) -> String

Cache identity for bytes with no path: a hash of the content, so the same image registered twice is one texture and the caller invents no name.

Public for the same reason as key, and for one more: the compositor derives this key independently, from the bytes it received, and a client that spelled it differently would be talking about a different texture. One implementation, both sides.

FNV-1a with the length mixed in. A collision means two unrelated images share a texture, which is worth caring about — and takes roughly 2³² distinct images in one session to become likely, which is why 64 bits is enough here and would not be for an untrusted store.

Sources/LavaUI/Image.swift:159

static func endFrame(into editor: Editor)

Called by LavaApp.run once a frame has been emitted.

Evicting after emit rather than before is what makes "not drawn this frame" mean anything: at the start of a frame nothing has been drawn yet, so every entry would look idle and the cache would throw away the images it is about to paint. Running it here also means a cache that is over budget with nothing new arriving still drains — eviction on insert alone stops the moment loading does, and leaves the cache permanently over its limit.

Sources/LavaUI/Image.swift:116

@discardableResult static func imageIfLoaded(path: String, maxPixelSize: UInt32 = 0, turn: ImageTurn = .none, into editor: Editor) -> UIImage?

Cached image, or nil while it loads.

Returns nil the first time and decodes on a worker; when the pixels arrive they are uploaded on the main thread and a redraw is requested, so the next frame gets the image. Callers draw a placeholder for the nil case — which they need anyway, because a real client is waiting on the network too.

maxPixelSize caps the longer edge at decode time (0 = native). Pass the size it will be drawn at. Two reasons, and the second is the one that bites: the pixels you don't decode cost nothing to hold, and ImageAtlas refuses anything wider than one cell, so an oversized decode silently costs a whole texture binding per image.

Sources/LavaUI/Image.swift:212

static func isLoading(path: String, maxPixelSize: UInt32 = 0, turn: ImageTurn = .none) -> Bool

Whether a decode for this file is running right now.

The one thing imageIfLoaded's nil does not distinguish: still on its way, or asked for and come back empty. A caller that keeps the previous picture on screen while the next one decodes has to tell those apart, or a file that will never arrive leaves a stale frame up for ever. Ask before calling imageIfLoaded — that call starts a new decode, after which the answer is trivially yes.

Sources/LavaUI/Image.swift:192

static func key(path: String, maxPixelSize: UInt32, turn: ImageTurn = .none) -> String

Cache identity for a file decoded at a given cap and turn. 0 and .none add no segment, so existing callers keep their key and two callers that both want a picture the right way up share one texture.

Public because a GPUResourceHost outside this module has to stamp the same key into the UIImage it returns — this cache looks entries up by it, so a host that spelled it differently would register an image and then miss it on every subsequent frame.

Sources/LavaUI/Image.swift:139

@discardableResult static func load(path: String, into editor: Editor) -> UIImage?

Cached image, loaded synchronously. Kept for assets an app needs before its first frame — an icon, a brand mark — where a placeholder would be worse than a stall.

Sources/LavaUI/Image.swift:172

static func loadAsset(named name: String, assetsRoot: String, into editor: Editor) -> UIImage?

Resolve a file under assetsRoot (fonts/ / assets/ / root).

Sources/LavaUI/Image.swift:284

static func loadAsset(named name: String, bundle: Bundle, into editor: Editor) -> UIImage?

Load a resource from a SwiftPM / app Bundle (prefer the app's Bundle.module for brand art — not the engine or LavaUI bundles).

Sources/LavaUI/Image.swift:306

static func releaseAll(into editor: Editor)

Hands every cached image back to the host and empties the cache.

Called on the way out of LavaApp.run, and that is not tidiness. A texture the renderer holds for a client is not freed when the client's process ends: under a compositor the registration counts a user, and there is nothing on the wire tying that user to the process that asked — ReleaseImage says so itself. So an app that exits with a full cache leaves every image it ever opened resident for as long as the desktop lives. A photograph viewer walking a library measured 2 GiB of that, with no viewer running.

It does not close the hole, only the common half of it: an exit through the frame loop reaches here, a crash or a kill does not. The other half belongs on the compositor's side of the wire, where a registration should be leased to the session that made it.

Sources/LavaUI/Image.swift:345

static func touch(_ image: UIImage)

Marks an image as used. Called from the draw list on every emit, which is what makes "least recently used" mean "least recently drawn" rather than least recently asked for.

Sources/LavaUI/Image.swift:124