Assets

Host-owned textures and the store they are loaded from.

Open a store, load textures from it, keep them in the model, and draw them. All three steps happen in init! in most apps:

init! = App.init(
    App.default,
    |_startup| {
        store = Assets.Store.open!(Assets.working_directory("assets"))?
        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 an explicitly located directory the host holds a handle to, so every relative asset path means the same thing however the process was launched. The host never calls chdir, and a path that would escape the store is refused rather than rewritten.

Textures are the shared texture type from the companion roc-ray-types package, re-exported here as Assets.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 -- Store.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.

That split is what a texture loaded after startup goes through: read the file on a task with Files.read_bytes!, return the bytes as the task's message, and call texture_from_bytes! from update! when the message arrives. Calling load_texture! inside the task itself does the same in one step, which is what a hot reload wants -- poll Files.metadata! on a task, and load again when the modification time moves.

ResourceLimit on any of them means the host's fixed texture table is full. It is a bound on how many textures exist at once, not on how fast they are made, so it is answered rather than retried: release a texture the app no longer draws, or load fewer.

beside_executable : Str -> StoreConfig

Start from an application/executable-relative asset directory. This is the normal packaged-app choice: the assets travel with the executable, so the store resolves the same way however the app was launched.

working_directory : Str -> StoreConfig

Start from a directory relative to the process working directory. This is what running an example from the repository root wants, and what a tool invoked from a project directory wants; it moves with the shell rather than with the executable.

absolute_directory : Str -> StoreConfig

Start from an absolute path, for a store the app was told about at runtime -- a mod directory, or a content pack chosen from argv.

with_manifest : StoreConfig, ManifestExpectation -> StoreConfig

Require this store's roc-assets.manifest to match an expectation, so a mismatched or half-updated asset set fails at startup rather than as a missing texture later.

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 answered 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.

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 shared texture type from the companion roc-ray-types package, re-exported so an app can name it without depending on that package as well. Draw.Texture is the same type under a second name, and a package written against the package's own Texture unifies with both.

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.

open! : StoreConfig => Try(Store, [RootNotFound, RootNotDirectory, RootUnreadable, InvalidRootPath, InvalidExpectedContentHash, ManifestMissing, ManifestUnreadable, ManifestMalformed, AssetSetMismatch, SchemaMismatch, ContentVersionMismatch, ContentHashMismatch, ResourceLimit, ..])

Open the store described by a StoreConfig, checking its manifest if one was required.

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 answers when they are done.

The first four failures are about the root directory: RootNotFound is nothing at that path, RootNotDirectory is something there that is not a directory, RootUnreadable is a directory the process may not open, and InvalidRootPath is a path this host will not accept at all -- one holding a NUL, or a relative form that escapes.

The rest are about the roc-assets.manifest a RequireManifest config 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.

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.

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.

StoreLocation

:= [BesideExecutable(Str), WorkingDirectory(Str), AbsoluteDirectory(Str)]

How a disk store root is resolved. These choices are explicit so moving an executable, changing CWD, and selecting a mod directory cannot silently change one another's meaning. The host never calls chdir.

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 carries 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.

StoreConfig : { root : StoreLocation, manifest : ManifestPolicy }

Where a store's root is and whether its manifest is checked. A plain record; build it with beside_executable, working_directory or absolute_directory, and add an expectation with with_manifest.

ImageFormat

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

Image bytes accepted by raylib's in-memory image loader.

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.