LavaUI docs GitHub

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:86

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:112

static var count: Int { get }

How many images the cache holds.

Sources/LavaUI/Image.swift:360

static var residentByteCount: Int { get }

Bytes currently held, for tests and diagnostics.

Sources/LavaUI/Image.swift:358

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:166

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:123

@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:243

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:223

static func isPixelKey(_ key: String) -> Bool

Whether key came from pixelKey().

Sources/LavaUI/Image.swift:191

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:146

@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:203

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

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

Sources/LavaUI/Image.swift:315

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:337

static func pixelKey() -> String

Cache identity for pixels the caller rasterised: a counter, because nothing else would ever ask for the same ones, and hashing a tile to find that out costs more than the tile.

Not one of this cache's keys: an upload is owned by whoever made it and released by them, never looked up. The prefix is how a host tells, on release, that the texture should be freed rather than kept dormant — see GPUResourceHost.uploadPixels.

Sources/LavaUI/Image.swift:183

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:377

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:131