Log inGet started

The resource model: asset, CPU, and GPU

Read this before working with any mesh, texture, material, or shader.

In Zero a mesh, texture, material, or shader is not a single thing. It exists in up to three forms at once, and knowing which form you are holding is the difference between code that draws and code that silently does nothing. The same split explains where memory goes and how content reaches the screen.

The three forms

Every GPU-backed resource can exist as:

  1. An asset. The authored, typed form, identified by a stable guid and referenced by identity. gold.material, hero.mesh, brick.texture, water.shader. An asset can be persisted into the world's source, and from there published through ZeroMind, but it need not be: asset.create(...) at runtime mints a real asset that lives only for the session unless it is written to source. Either way it is content addressed by identity, not memory on the GPU.
  2. A CPU form. The decoded bytes in engine memory: a mesh's vertex and index arrays, a texture's pixels. Read it to inspect content and edit it for one-off or authoring-time changes. It is not the place for per-frame mutation: editing the CPU form and re-uploading every frame is exactly the round-trip to avoid.
  3. A GPU form. The live resource the renderer draws from: vertex and index buffers, sampled textures, a compiled material pipeline. It is what the frame actually uses, and you can modify it in place on the GPU (a compute shader writing a mesh's vertex buffer, or renderer.mesh uploading from a kind="gpu" substrate buffer). Per-frame geometry or pixel work belongs here, on the GPU, not in a CPU edit that re-uploads each frame.

The public API is organized the same way, under renderer:

ResourceAssetWork with it through
Mesh.meshrenderer.mesh (create, loadCpu, getVertices, setVertices, isCpuResident, isResident, unloadCpu, readback)
Texture.texturerenderer.texture (create, cpuCreate, loadCpu, isResident, readback, capture)
Material.materialrenderer.material (create, setProperty, setTexture, describe, destroy)
Shader.shader / .computeShaderdispatched or referenced by identity; see types/shader and types/computeShader

Identity is a guid

You reference a resource by identity, and the engine resolves that to the resource's stable guid. Asset-backed resources are guid-keyed, so a string like @builtin::materials.gold or @builtin/textures/array_texture is a lookup that resolves to a guid. The guid is the durable identity that survives renames and travels through ZeroMind, which is why saved content stores references by guid, not by name. The plain string key you pass to renderer.material.create is the exception: a runtime name for a resource that is not asset-backed and lives only for the session.

The flow: Luau to CPU to GPU

To be drawn, content moves in one direction: authored asset, into CPU memory, uploaded to the GPU.

-- mesh: asset ref -> CPU-resident -> inspected/edited -> GPU-resident
renderer.mesh.loadCpu(ref)                 -- decode into CPU memory
local verts = renderer.mesh.getVertices(guid)
renderer.mesh.setVertices(guid, positions) -- edit on the CPU
renderer.mesh.create(guid)                 -- upload to the GPU; now it draws

renderer.texture and renderer.material follow the same shape. renderer.texture.cpuCreate(w, h, fill) builds a texture directly in CPU memory (no asset needed), which you then upload. Readback is the reverse direction, GPU back to CPU: renderer.mesh.readback(guid) and renderer.texture.readback(guid).

Residency is memory management

A resource can be CPU-resident, GPU-resident, both, or neither, and residency is the lever you use to control memory:

  • Bringing it in: renderer.mesh.loadCpu / renderer.texture.cpuCreate make it CPU-resident; uploading makes it GPU-resident.
  • Querying: renderer.mesh.isCpuResident(guid) and renderer.mesh.isResident(guid) (GPU) report each form independently. renderer.texture.isResident(guid) is the GPU query for textures.
  • Freeing: renderer.mesh.unloadCpu(guid) releases the CPU copy while the GPU copy keeps drawing. To evict the GPU resource itself, renderer.destroy(handle) is the universal call: pass any resource handle (a mesh, texture, or material handle) and it frees that resource, routing by the handle's category. The type-specific drops exist too, but renderer.destroy is the one call that takes anything.

Once a resource is on the GPU it renders on its own, so the common pattern is to load it, upload it, then unload the CPU copy to reclaim memory. Load only what you need, and renderer.destroy what you are finished with, rather than keeping every form of every resource resident at once.

Materials: asset vs runtime

A material is a shader plus concrete values. It comes in two forms, and picking the right one is a real decision:

Asset material (gold.material). A declarative .material asset. Its mat.yaml names a shader by identity and supplies property values and render state:

# gold.material / mat.yaml
shader: "@builtin::shaders.pbr"
floats: { roughness: 0.2, metallic: 1.0 }
colors: { base_color: { r: 1.0, g: 0.84, b: 0.0, a: 1.0 } }
textures:                                        # each slot references a texture asset by identity
  base_color_texture: { ref: "@builtin/textures/array_texture" }
  normal_texture:     { ref: "@builtin/textures/ScratchedGold-Normal" }

Textures live in the textures: block, keyed by the shader's texture slot names and referenced by identity (the same way the material references its shader). On a live material you bind or swap one with renderer.material.setTexture(key, slot, ref). It has a stable guid, can be saved to the world's source, and from there published. Use it for any look that is authored, shared, reused, or should survive a restart. You reference it by identity and the engine owns its GPU upload.

Runtime material (renderer.material.create). A live GPU material built imperatively in Luau and identified by a runtime key. It is not an asset: it is not saved, not published, and gone when you drop it.

local mat = renderer.material.create({
  shader = "@builtin::shaders.extended_material",
  properties = { roughness = 0.2, metallic = 1.0 },
}, "runtime_gold")
renderer.material.setProperty("runtime_gold", "roughness", 0.5)  -- mutate the live material
renderer.material.setTexture("runtime_gold", "base_color", brickGuid)
renderer.material.destroy("runtime_gold")                        -- free it

Use it for procedural, dynamic, or per-instance looks generated at runtime. Rule of thumb: if the look should be authored, shared, or persisted, make a .material asset; if it is generated on the fly for this session only, use renderer.material.create. Both resolve to the same kind of live GPU material. The difference is who owns its lifetime and whether it persists.

The asset graph

An asset material references a shader by identity, and references its textures by identity too (in the yaml textures: block, or setTexture at runtime). So resolving one material pulls a small graph onto the GPU: the compiled shader pipeline, every referenced texture, and the property values. The shader is itself an asset (a .shader with a WGSL body); the engine compiles it to a GPU pipeline on first use. Authoring each of these is covered by their own reference: types/material, types/shader, types/texture, types/mesh, types/computeShader.

Render textures and surfaces

A render texture is a GPU texture you draw into rather than load from an asset. Create one by passing dimensions with no pixel source to renderer.texture.create:

local target = renderer.texture.create({ width = 1024, height = 1024, name = "mirror" })  -- an empty GPU texture

Point a camera or a render pass at it, then sample it like any other texture: bind it into a material's textures: slot, or with renderer.material.setTexture. That is how mirrors, security-camera feeds, portals, and multi-pass effects work. renderer.texture.capture(handle) reads a target back to the CPU. Pass a colour format for an HDR target, and screen = true (with an optional screenScale) for a full-screen render-feature target the engine resizes to match whatever it is drawing. The topics/render-textures guide covers wiring a camera to a target in full.

A surface is the shader domain that shades a mesh: a surface shader decides how a pixel of a material looks and lights, and materials wrap surface shaders. Post-process, screen, and compute are the other shader domains. See types/shader.

Seeing what is resident: the runtime VFS

Live resources show up in the engine's virtual filesystem under /zero/runtime/, which reflects the running world rather than saved source. You ls and cat it the same way you inspect entities:

  • /zero/runtime/layers/<layer>/entities/ is the live entities and their components.
  • /zero/runtime/ also carries live GPU state: generated meshes, render surfaces (render-texture targets), lighting, and logs.

So "where did my mesh or render target go" has a literal answer: a folder under /zero/runtime/. That is the fastest way to confirm a resource is actually resident, as distinct from its authored .mesh / .texture asset under /zero/source/. The core/vfs guide has the full layout.

The same pattern beyond the GPU

The asset-vs-runtime split is not GPU-specific. A .soundClip asset is the authored sound; playing it spins up a runtime voice. The persistent authored thing and the live ephemeral instance are always distinct. GPU-backed resources add the CPU and GPU residency layers on top of that same idea.

The model

For any mesh, texture, material, or shader, hold three questions. Which form am I holding: the asset, the CPU copy, or the GPU copy? Which way is the data moving: loading up into CPU memory, uploading to the GPU, or reading back down? And who owns its lifetime: an asset addressed by identity (which the world persists once it is written to source), or a runtime handle I created and must drop? Answer those three and the memory use and rendering behavior stop being surprising.