Assets
A game is more than code — it is images, sounds, levels, fonts. Those are assets, and pit treats them the same way it treats everything else: they belong to packages, they are named by locator, and they are addressed by content hash.
Assets are package files
An asset lives in a package and is reached by the same <package>::<path> locator a module
uses:
var sprite = load('game::art/player.png')
There is no separate asset namespace and no asset registry to maintain. A file is in your package, so it has a locator; that is all it takes to refer to it.
A package is the drive letter. <package>:: says which one, and there is no path outside
one — the filesystem, as far as a program is concerned, is the set of packages the
clerk has mounted.
Asking is literal; searching is yours
The clerk answers literal requests. You name <package>::<path> and it finds those
bytes, or it tells you it could not. It does not apply a search order, and deliberately so:
the chain — the caller’s package, its aliases, the shop’s aliases, the fallback packages —
is one shop’s naming policy, and baking it into file access would leak that policy into
every program that loads a texture.
So the clerk offers primitives, and search policies are built on them:
$clerk literal: get <package>::<path>
list the files in a package
list the aliases a package declares
list what is mounted
├─ use() the chain, built on those primitives
├─ pitfs the standard search policy we ship
└─ a game mod packages before base, or whatever it wants
That inversion is the point: use() is a consumer of the clerk, not a sibling of it. A
game that wants mods to shadow base assets can express that, which it could not if the order
were fixed underneath it.
One difference from use() follows from the same reasoning the language already applies.
use() must take a literal so an executable’s closure can be computed. An asset lookup must
be allowed to compute — "levels/" + n + ".png" is the ordinary case — so it sits on the
$start side of that line rather than the use() side. See Shop.
When two mounts have the same file
A locator can be satisfied from more than one place: the cart the runtime booted from, a store beside it, something fetched from a repository.
Identical bytes are not a conflict — content addressing means the same hash found twice is one answer. A real conflict is the same locator resolving to different hashes in different mounts, and the answer is mount order: the clerk searches its mounts in the order they were mounted, and the first hit wins.
That is a decision rather than a rule. Nothing is silently preferred; you read what was mounted, in what order, for that build. The cart the runtime booted from is mounted first, so it wins by default, and a development shop that wants a working tree to shadow it mounts that ahead — visibly, in one place.
A differing shadow is worth reporting — a build-time lint saying “game::art/player.png
resolves in two mounts to different hashes” catches the accidental case. It is not worth
failing a frame draw over, which is why this is not the build-time rule that two modules
resolving to different bytes is an error.
Shipping is a matter of including the assets you want. Building a cart, you choose the
actors it carries and the assets those actors need — from your own package or from any
package you depend on. That choice is made by the invocation that composes the
cart, alongside which actors ride along and which payload form they ship in. Including
game::art/player.png in the cart is what makes those bytes available in the shipped build;
leaving it out leaves it out.
The asset index
Inside a store or cart, assets are reached through a small index:
asset locator -> content hash -> offset, length, alignment, range
That is the names-to-hashes-to-offsets gradient the whole system uses, drawn small. The locator is a name and lives in the manifest; the hash names the whole object; the offset and length are physical facts inside the container and mean nothing outside it.
The index is compact and readable at startup — it is how the runtime knows what assets
exist and where each one sits. The payloads themselves are cold: an asset’s bytes are read
by range only when something asks for them, through the store’s ordinary
get_content(hash, range), so a cart full of art does not have to be resident to be
startable.
Because assets are content-addressed, two packages shipping the same file share one stored copy while each keeps its own locator. The bytes are deduplicated; the names are not.
Reaching an asset is a capability
An asset’s bytes have to go somewhere — a decode buffer, video memory, an audio ring, a DMA target — and every one of those destinations is an explicit endowment the actor holds. The asset store supplies an authenticated range; a video, audio, decoder, or DMA provider decides whether the platform can transfer that range directly or must stage it through a buffer first.
This keeps assets inside the capability model rather than beside it. An actor reads an asset range because it was granted the store, and it lands the bytes in video memory because it was granted a graphics provider. Nothing about an asset escapes the rule that reaching outside the actor’s own heap requires an endowment — see Capabilities.
That is also what lets a console cart stream a large asset set from ROM: the index is resident and small, the provider transfers ranges on demand, and the whole path — locator to hash to range to a granted destination — is checkable before the cart ships.