Assets

Load, create, and update host-owned textures.

Open a store and load long-lived textures during init!:

init! = App.init(
    App.default,
    |io| {
        bundle = io.files().beside_executable!()?
        store = Assets.open!(bundle.subdir("assets")?, IgnoreManifest)?
        Ok({ logo: Assets.load_texture!(store, "logo.png")?, store })
    },
)

render! = |model, frame| {
    frame.texture!(Draw.texture_at(model.logo, { x: 32, y: 32 }))
    Ok({})
}

A store is opened from a Files.ReadDir, so it can access exactly what that handle can access: the bundle beside the executable needs no permission, and any other directory needs one. Asset paths are relative to the store; a path that is not plainly relative, or that meets a symbolic link, is refused as PathInvalid rather than rewritten.

Textures are platform-owned values, also named Assets.Texture and Draw.Texture. Releasing the final reference to one unloads the native texture automatically, so there is no unload to remember.

The two effects that read the disk -- open! and load_texture! -- wait: each is legal in init!, where it blocks startup, and in tasks, where it parks the task; both are refused in update! and render!. Everything else here builds a texture from bytes the app already holds -- texture_from_bytes!, generate_color_texture!, generate_checked_texture!, update_texture! -- and is legal in init!, update!, and tasks, and refused in render!, where a decode or an upload would land in the middle of drawing a frame.

ResourceLimit on any of them means the host's fixed texture table is full. Release textures the app no longer needs before loading more.

open! : ReadDir, ManifestPolicy => Try(Store, [PermissionDenied, PathInvalid, RootNotFound, RootNotDirectory, RootUnreadable, InvalidExpectedContentHash, ManifestMissing, ManifestUnreadable, ManifestMalformed, AssetSetMismatch, SchemaMismatch, ContentVersionMismatch, ContentHashMismatch, ResourceLimit])

Open the directory a handle names as an asset store, checking its manifest if the policy requires one.

bundle = io.files().beside_executable!()?
store = Assets.open!(bundle.subdir("assets")?, IgnoreManifest)?

Legal in init!, where it blocks startup, and in tasks, where it parks the task; refused in update! and render!. Opening the directory and reading the manifest are filesystem work, so the host does both off the frame thread and returns when they are done.

A Sha256 expectation compares against the manifest's declaration only. Nothing walks or hashes the loose files, so opening a store stays constant-time in the number of assets.

rgba_byte_size : List(Rgba) -> U64

How many bytes an RGBA pixel list occupies on the wire: four per pixel.

An upload borrows the list rather than copying it, so this is what an app measures a update_texture! or update_texture_region! against when it is budgeting per-frame pixel traffic.

load_texture! : Store, Str => Try(Texture, [PathInvalid, NotFound, ReadFailed, TextureLoadFailed, ResourceLimit])

Load an image relative to an explicit store.

Legal in init!, where it blocks startup, and in tasks, where it parks the task; refused in update! and render!. The file is read off the frame thread; the decode and the GPU upload happen when the bytes are back. To load after startup, call this inside Task.spawn!, or read the bytes on a task and call texture_from_bytes! from update!.

path must be relative; PathInvalid is an absolute path, one holding a NUL, or a lexical .. escape, and is returned before any file I/O. NotFound is no such file under the store, ReadFailed is a file that is there and could not be read, and TextureLoadFailed is bytes raylib would not decode as an image, including any file whose extension is not one of the ImageFormat ones.

texture_from_bytes! : TextureBytes => Try(Texture, [TextureLoadFailed, ResourceLimit])

Decode an authored image embedded with a compile-time file import.

Legal in init!, update!, and tasks; refused in render!.

generate_color_texture! : GenerateColorTexture => Try(Texture, [TextureGenerationFailed, ResourceLimit])

Generate a solid-color GPU texture. The temporary CPU image is released inside the host; only the host-owned texture crosses back.

Legal in init!, update!, and tasks; refused in render!.

generate_checked_texture! : GenerateCheckedTexture => Try(Texture, [TextureGenerationFailed, ResourceLimit])

Generate a checkerboard GPU texture without retaining a CPU image.

Legal in init!, update!, and tasks; refused in render!.

update_texture! : Texture, List(Rgba) => Try({}, [PixelCountMismatch])

Replace every pixel. The row-major RGBA list must exactly match the texture dimensions and is borrowed only for this host call.

Legal in init!, update!, and tasks; refused in render!.

update_texture_region! : Texture, Region => Try({}, [PixelCountMismatch, RegionOutOfBounds])

Replace one rectangle of a texture, paying only for that rectangle.

Legal in init!, update!, and tasks; refused in render!.

set_texture_filter! : Texture, TextureFilter => {}

Change how this texture is sampled when scaled.

Legal in init!, update!, and tasks; refused in render!.

set_texture_wrap! : Texture, TextureWrap => {}

Change how out-of-range texture coordinates are wrapped.

Legal in init!, update!, and tasks; refused in render!.

Texture : Texture

A host-owned GPU texture: an opaque, reference-counted native handle plus the pixel width and height, kept on the value so layout and source-rectangle math stays pure.

This is the platform's Texture value. Draw.Texture names the same type, so loading and drawing need no conversion.

Store

Assets.Store :: # (opaque)

An opened, explicitly located disk asset store. The host retains the directory handle, not the process working directory; every relative asset lookup is made through that handle.

stub : Store

Resource-free store value for pure tests.

stub is what every host resource in the platform calls its resource-free test value, so a model full of assets can be written down in an expect.

The handle never resolves to an open directory, so every load made through it fails the way a load through a released store does. It exists for the app that keeps a store in its model, to let a pure expect build that model. Do not use it to test asset resolution or resource lifetime.

ManifestPolicy

:= [IgnoreManifest, RequireManifest(ManifestExpectation)]

Whether opening a store checks the asset-set manifest named roc-assets.manifest beside it. IgnoreManifest does not look; RequireManifest fails the open unless the manifest is there and matches the expectation.

ContentExpectation

:= [AnyContent, Sha256(Str)]

How closely a manifest's declared content has to match. AnyContent deliberately leaves it unconstrained, which is what a directory of loose files under development wants. Sha256 holds the 64-character hexadecimal digest the manifest must declare.

ManifestExpectation : { asset_set : Str, schema : U32, content_version : U32, content : ContentExpectation }

What a RequireManifest open expects the manifest to say: which asset set it describes, which schema version it was written to, which content version it is, and which content it declares.

OpenError : [PermissionDenied, PathInvalid, RootNotFound, RootNotDirectory, RootUnreadable, InvalidExpectedContentHash, ManifestMissing, ManifestUnreadable, ManifestMalformed, AssetSetMismatch, SchemaMismatch, ContentVersionMismatch, ContentHashMismatch, ResourceLimit]

Why a store could not be opened.

PathInvalid is a store directory whose path beneath the handle meets a symbolic link. PermissionDenied is a stub handle, which gives no access. The next three are about the directory itself: RootNotFound is nothing there, RootNotDirectory is something there that is not a directory, and RootUnreadable is a directory the process may not open.

The rest are about the roc-assets.manifest a RequireManifest policy asked for. ManifestMissing is no manifest beside the assets, ManifestUnreadable is one that could not be read, and ManifestMalformed is one that is not a manifest. Of the four comparisons, AssetSetMismatch is a manifest describing a different asset set than the one expected, SchemaMismatch a manifest written to a different schema version, ContentVersionMismatch a different content version, and ContentHashMismatch a declared content hash that is not the expected one. InvalidExpectedContentHash is the expectation itself being unusable -- a Sha256 string that is not 64 hexadecimal characters. ResourceLimit is the host's store table being full.

ImageFormat

:= [Png, Jpeg, Bmp, Gif, Qoi]

Image formats the bundled raylib decodes: PNG, JPEG, BMP, GIF (its first frame), and QOI. load_texture! reads the same five, by the file's extension: .png, .jpg or .jpeg, .bmp, .gif, and .qoi.

TextureBytes : { format : ImageFormat, bytes : List(U8) }

An authored image embedded with a compile-time file import, tagged with its format. The format is stated rather than sniffed, so a mislabelled file fails to decode instead of decoding as something else.

Region : {
    x : I32,
    y : I32,
    width : I32,
    height : I32,
    pixels : List(Rgba),
}

A rectangle of a texture and the pixels to put in it, in row-major RGBA order. pixels must hold exactly width * height entries.

TextureFilter

:= [Point, Bilinear, Trilinear, Anisotropic4x, Anisotropic8x, Anisotropic16x]

Texture sampling filter used when an image is scaled. Point keeps pixel art crisp; the rest smooth it.

is_eq : _

Compare two filters.

TextureWrap

:= [Repeat, Clamp, MirrorRepeat, MirrorClamp]

Texture-coordinate behavior outside the normal 0-to-1 range.

is_eq : _

Compare two wrap modes.

GenerateCheckedTexture : {
    width : I32,
    height : I32,
    checks_x : I32,
    checks_y : I32,
    color_a : Rgba,
    color_b : Rgba,
}

Configuration for a generated checkerboard texture.