Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 07158574-5…●1problem

debugViz

The shared edit-mode debug-visualization core. Owns the ONE anchor entity, the ONE vertex-colour line material, the ONE batched vertex/index compute-buffer pair, and the ONE render feature that every debug-visualization gizmo draws through. Individual gizmos (bounds boxes, camera…

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

debugViz

The shared edit-mode debug-visualization core. Owns the ONE anchor entity, the ONE vertex-colour line material, the ONE batched vertex/index compute-buffer pair, and the ONE render feature that every debug-visualization gizmo draws through. Individual gizmos (bounds boxes, camera frustums, ...) are providers — small modules that append their own geometry into the shared batch every tick, instead of each owning their own GPU state.

entity.spawn("DebugViz"):component.add("DebugViz")

How it fits together

  • DebugViz.component owns the lifecycle: registers the built-in providers and calls debugViz.update(dt) every edit-mode tick; onDisable/onDestroy call debugViz.teardown().
  • debugViz.registerProvider(name, fn) registers fn(emit) under name. Registering an existing name replaces its function.
  • debugViz.update(dt) clears the batch, calls every registered provider with an emit handle, then rebuilds and uploads the combined vertex/index buffers (grow-only capacity — a stable geometry count costs one buffer write per tick, not a recreate).
  • debugViz.state() returns the latest { vtx, idx, anchor, indexCount } for the render feature to draw.
  • @builtin::renderFeatures.debugViz reads state() every frame and draws the batch through one Draw pass, using @builtin::shaders.debugGeometry — a solid-colour shader that reads each vertex's baked colour, so every provider can use its own colour in the same draw call.

The draw needs an anchor entity with an IDENTITY world transform and a render-object (so the Draw pass has a valid instance slot and a material to override) — this module owns one, spawned lazily, internal so it stays out of the inspector and excluded from scene saves.

Writing a provider

local debugViz = require("@builtin::modules.debugViz")

local function tick(emit)
    emit.box({ x = -1, y = -1, z = -1 }, { x = 1, y = 1, z = 1 }, { 1, 0, 1, 1 })
    emit.line({ x = 0, y = 0, z = 0 }, { x = 0, y = 5, z = 0 }, { 1, 0, 1, 1 })
end

debugViz.registerProvider("my_gizmo", tick)

emit.box(min, max, color?) and emit.line(a, b, color?) append world-space geometry into the shared batch for this tick; color is an {r, g, b, a} array baked per-vertex (defaults to amber if omitted). A provider that needs to exclude the shared anchor entity from its own scene queries reads debugViz.anchorId().

API

  • debugViz.registerProvider(name, fn) — register (or replace) a provider. fn(emit) runs once per edit-mode tick.
  • debugViz.anchorId() — the anchor entity's id, or nil before the first tick.
  • debugViz.update(dt) — clear the batch, run every provider, rebuild and upload the combined geometry.
  • debugViz.state() — the latest batched draw, or nil before the first tick.
  • debugViz.teardown() — tear down the feature, material, buffers, and anchor entity. Registered providers stay registered.
  • debugViz.MATERIAL — the registry key of the shared vertex-colour line material the render feature draws with.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

strict

currentMode( ) → string

The engine's mode, as the visibility is keyed: `edit` or `play`.

providerAvailable(p: any) → boolean

Whether a provider currently has anything to show. A provider with no `available` predicate is always offered; one that declares it is offered only while the thing it draws exists in the scene, so the panel lists the overlays this scene can actually use rather than every overlay the engine knows how to draw.

argtypedescription
pany

registerProvider(name: string, fn: (Emit) → void

Register (or replace) a provider under `name`. `fn(emit)` runs once per edit-mode tick and appends its geometry via `emit`, unless `name`'s category is disabled (`setCategory`). Registering an existing name replaces its function; providers otherwise persist for the module's lifetime, so re-enabling the driving component doesn't need to re-register. A provider's category defaults on the first registration and is otherwise untouched by re-registration, so a category toggled by the panel survives a component disable/enable cycle.

argtypedescription
namestring
fn(Emit

unregisterProvider(name: string) → boolean

Remove a provider and its category. A package that stops being able to draw (its system left the scene) withdraws its overlay so the panel stops offering a toggle for something with nothing behind it. The category's on/off state is dropped with it, so re-registering starts from its declared default.

argtypedescription
namestring

providers( ) → void

Every provider that currently has something to draw, in registration order: `{ name, label, enabled }`. The toggle panel builds from this, so a provider appears the moment its subject exists in the scene and disappears with it.

setCategory(name: string, on: boolean) → void

Enable or disable a provider's category. A disabled category's provider is skipped by `update()` without unregistering it — re-enabling resumes drawing on the next tick.

argtypedescription
namestring
onboolean

isCategory(name: string) → boolean

Whether `name`'s category currently draws. Categories default per `CATEGORY_DEFAULTS` the first time their provider registers; an unregistered name reports its would-be default.

argtypedescription
namestring

categories( ) → void

Every registered provider's category name, in registration order — the stable order the toggle panel lists categories in.

knownCategories( ) → void

Every category this module carries a state for: the registered providers in registration order, then any category whose state was set before its provider registered, sorted so the order is stable. A provider registers when the thing it draws enters the scene, so the registered set grows through a session — a package's overlay appears only once that package has loaded. `setCategory` accepts a name at any time and `registerProvider` keeps the state it finds, so a preference set ahead of registration is honoured when the provider arrives. Reading only the registered set therefore misses categories that are live and settable, which is what a caller means by "every category".

isRegistered(name: string) → boolean

Whether `name` has a provider registered right now — the difference between a category that draws and one whose state is recorded for a provider that has not arrived.

argtypedescription
namestring

registerIcon(key: string, opts: { glyph: (string | number) → void

Register (or replace) an icon visual under `key`. `emit.icon(center, key)` then draws it. This is how both built-in systems and user code make an entity show a gizmo icon: register a visual once, then emit it wherever it belongs. `opts`: * `glyph` — a built-in procedural glyph name (`"camera"`, `"light"`, `"audio"`, `"probe"`, `"player"`) or its numeric id. Mutually exclusive with `texture`. * `texture` — a `.texture` asset identity/ref/`AssetRef`; the icon samples its alpha, so any authored image becomes an icon. * `size` — on-screen scale as a fraction of the camera distance (default `0.06`), so the icon holds a roughly constant size regardless of range. Re-registering a key updates its visual (the material is rebuilt on the next draw). The per-entity tint comes from `emit.icon`'s `color`, not from here, so one icon can be tinted per entity.

argtypedescription
keystring
opts{ glyph: (string | number

iconKeys( ) → void

Every registered icon key, in registration order. For introspection.

anchorId( ) → string

The anchor entity's id, or nil before the first tick. Providers that need to exclude the anchor from their own scene queries (it carries a Mesh) read this rather than tracking their own copy.

adoptAnchor( ) → string

Adopt the anchor that is already in the scene, if there is one, and leave exactly one behind. `anchorId` is module state and the anchor is an ECS entity, so the two part company whenever this module is re-instantiated — a hot reload during an editor session, most often. The scene is the authority on how many anchors exist, so it is what gets asked: the first one found becomes this instance's anchor, and any others are despawned, which also drains anchors a previous instance left behind.

ensureAnchor( ) → string

The internal identity-transform entity the Draw pass resolves its instance slot + material override through. Temporary so it never lands in a scene/world save, internal so it never clutters the entity list, EditorOnly participation so world scans (bake selection, coverage reports) never see editor viz as world geometry.

ensureMaterial( ) → void

ensureIconMaterial(reg: IconReg) → void

Create (once, or after re-registration) the triangle-list material an icon draws through, from the debugBillboard shader with the icon's glyph/size (and texture, when texture-backed). Alpha-blended overlay that ignores depth, so icons read on top of the scene like the rest of the overlay.

argtypedescription
regIconReg

forgetGpuState( ) → void

Give up everything this module holds on the GPU, because the device holding it is gone. Every buffer here was made from the render device, and a lost device invalidates all of them; the handles cached above would otherwise name resources that no longer exist, and the grow-only sizing would keep them because the size still fits. The material and the render feature go with them: both are rebuilt from their spec by the `ensure*` calls the next tick makes, which is what puts the overlay back on the device the engine is now on. Nothing is destroyed here — a lost device took the resources already, and destroying a handle to one is a call into a device that is not there.

ensureFeature( ) → void

wordsFor(byteLen: number) → number

A buffer is a run of 32-bit words, so a byte count sizes one as that many quarters. Every debug payload is whole words — 92-byte vertex records and u32 indices — so this divides evenly.

argtypedescription
byteLennumber

stillThere(buf: any) → boolean

Internal: whether a cached handle still points at storage. A handle outlives the storage it points at: anything may release the buffer by the name it was created under, and every holder's handle answers `alive` false from that moment. The grow-only caches below drop such a handle, and the next call sizes a fresh buffer.

argtypedescription
bufany

bytesHeld(buf: any) → number

Internal: the bytes the buffer behind a live handle holds, as the substrate states its length. Every buffer here is `f32`, one word per record. The length is read off the buffer each time, so the size a grow-only cache compares against is the size of the buffer it holds, whichever handle that is.

argtypedescription
bufany

ensureLineVtxCapacity(vtxBytesLen: number) → any

Grow-only sizing for the line vertex buffer, recreated only when the frame's bytes no longer fit. Returns the buffer to write into, nil when the allocation failed.

argtypedescription
vtxBytesLennumber

ensureIndexCapacity(vertCount: number) → void

Grow-only sizing for the shared static sequential index buffer (0,1,2,…). It backs every debug draw — the line soup and every icon quad list — so it is sized to the largest single draw's vertex count, with 2× headroom so growth (and the one-off sequential fill) is rare; a steady tick never touches it.

argtypedescription
vertCountnumber

uploadIconBuffer(key: string, bytes: string) → string

Upload one icon's triangle-soup bytes to its own grow-only vertex buffer. Returns the name the draw binds it by, nil when the allocation failed.

argtypedescription
keystring
bytesstring

update(_dt: number) → void

Clear the batch, run every registered provider, and upload the combined geometry. Called each active frame by the debug-viz driver.

argtypedescription
_dtnumber

box(min: ?, max: ?, color: ?) → void

argtypedescription
min?
max?
color?

line(a: ?, b: ?, color: ?) → void

argtypedescription
a?
b?
color?

orientedBox(center: ?, rotation: ?, halfExtents: ?, color: ?) → void

argtypedescription
center?
rotation?
halfExtents?
color?

wireSphere(center: ?, radius: ?, color: ?) → void

argtypedescription
center?
radius?
color?

wireCapsule(center: ?, rotation: ?, radius: ?, halfHeight: ?, color: ?) → void

argtypedescription
center?
rotation?
radius?
halfHeight?
color?

cross(center: ?, size: ?, color: ?) → void

argtypedescription
center?
size?
color?

octahedron(center: ?, size: ?, color: ?) → void

argtypedescription
center?
size?
color?

rawLineSoup(vtxBytes: ?, vertexCount: ?) → void

argtypedescription
vtxBytes?
vertexCount?

icon(center: ?, key: ?, color: ?) → void

argtypedescription
center?
key?
color?

state( ) → DrawState

setVisible(on: boolean, mode: string?) → void

Show or hide the overlay in `mode` (the engine's current mode when omitted). Hiding the mode the engine is in drops the current batch so the overlay stops drawing at once.

argtypedescription
onboolean
modestring?

isVisible(mode: string?) → boolean

Whether the overlay draws in `mode` (the engine's current mode when omitted). The driver rebuilds the overlay only while this holds.

argtypedescription
modestring?

setPlaySession(on: boolean) → void

Show or hide the overlay in play mode: a debug session over the running game. `M.setVisible(on, "play")`.

argtypedescription
onboolean

isPlaySession( ) → boolean

Whether the overlay draws in play mode. `M.isVisible("play")`.

setScope(ids: { string }?) → void

Limit the overlay to a set of entities. Pass an array of ids (or nil/empty to clear). Providers consult `inScope` per entity, so this focuses every enabled category on the same objects. In-memory only, never persisted.

argtypedescription
ids{ string }?

clearScope( ) → void

Clear the scope so every entity draws again.

inScope(id: string) → boolean

Whether `id` is in scope. True when no scope is set (draw everything), else true only for ids in the scope set. Providers guard each entity with this.

argtypedescription
idstring

isScoped( ) → boolean

Whether a scope is currently set. Providers that behave differently when focused on specific objects (e.g. bounds drawing per-entity instead of one box per top-level object) branch on this.

scopeIds( ) → void

The current scope as an array of ids (empty when unscoped) — for introspection by the debug toolbox.

clear( ) → void

Drop the batched draw without tearing down GPU resources, so the render feature enqueues nothing until the next `update`. Used when leaving edit mode with no active play session — cheap enough to call every frame.

teardown( ) → void

Tear down the live feature, material, buffers, and anchor. Registered providers stay registered — re-enabling rebuilds GPU state without re-registering.

⌬ Types
Emit = {DrawState = {

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

24items
·
other · born here
▤file
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
# debugBillboard Camera-facing billboard shader for the debug overlay's icon sprites. Each instance renders a procedural glyph (camera, light, audio, reflection probe, player) selected by glyph id — drawn analytically in the fragment shader, so icons stay crisp at any zoom with no texture atlas. Consumed by the debugViz icon system.
▲ 0↑ born
▣
module · born here
❒asset
# debugDraw Procedural geometry builders for GPU debug visualization — wireframe boxes, spheres, capsules, crosses, octahedra, line segments, and camera-facing icon billboards. Pure geometry: every builder appends packed vertex bytes to a caller-supplied accumulator and returns how many vertices it added. Line geometry is a non-indexed **line soup** (each line is two consecutive vertices) and billboards are a **triangle soup** (each quad is six consecutive vertices), so both draw through one static, grow-only sequential index buffer (0,1,2,…) that never needs re-uploading per frame — the topology comes from the material. Nothing here touches `compute.*` or `renderer.*` — callers own the buffers and the Draw pass. ```lua local debugDraw = require("@builtin::modules.debugDraw") local vtxParts = {} local verts = 0 verts += debugDraw.appendBox(vtxParts, bounds.min, bounds.max, { 1, 0.8, 0, 1 }) verts += debugDraw.appendLine(vtxParts, a, b, { 0, 1, 1, 1 }) local vtxBytes = debugDraw.buildVertexBytes(vtxParts) local vtx = substrate.createBuffer({ name = "my.vtx", type = "f32", len = math.ceil(#vtxBytes / 4), kind = "gpu", usage = { "vertex" }, }) vtx:writeBytes(vtxBytes) -- One static sequential index buffer, grown only when the vertex count does. local idx = substrate.createBuffer({ name = "my.idx", type = "f32", len = verts, kind = "gpu", usage = { "index" }, }) idx:writeBytes(debugDraw.buildSequentialIndexBytes(verts)) ``` ## Vertex layout Every packed vertex matches the engine's standard `Vertex` layout (position, normal, uv, joints, weights, node_index, tangent, color — 92 bytes, little-endian) so the resulting buffer draws through the ordinary vertex pipeline via a Draw pass. Debug geometry only needs position + color; every other field is filled with a default. ## API Each builder appends line-soup vertices to `vtxParts` and returns the vertex count it added: - `appendLine(vtxParts, a, b, color?)` — one segment (2 verts). - `appendBox(vtxParts, min, max, color?)` — axis-aligned box, 12 edges (24 verts). - `appendOrientedBox(vtxParts, center, rotation, halfExtents, color?)` — a posed box. - `appendWireSphere(vtxParts, center, radius, color?)` — three orthogonal rings. - `appendWireCapsule(vtxParts, center, rotation, radius, halfHeight, color?)` — two rings + connectors. - `appendCross(vtxParts, center, size, color?)` — a 3-axis marker (6 verts). - `appendOctahedron(vtxParts, center, size, color?)` — a diamond marker (12 edges). - `appendBillboard(vtxParts, center, color?)` — one camera-facing quad (6 triangle-soup verts) at `center`; every corner stores `center` and its uv corner, and the `debugBillboard` surface shader expands it toward the camera. Helpers: - `packVertex(x, y, z, color)` — pack one vertex; for building cached geometry blobs. - `packVertexUV(x, y, z, u, v, color)` — pack one vertex carrying an explicit uv (for billboard quads). - `quatRotate(q, v)` — rotate a vector by a unit quaternion. - `buildVertexBytes(vtxParts)` — concatenate the packed vertices into buffer bytes. - `buildSequentialIndexBytes(count)` — the fixed `0,1,2,…,count-1` line-list index buffer. - `VERTEX_STRIDE` — 92, the byte stride of one packed vertex. See `@builtin::modules.debug_bounds` for a caller and `@builtin::renderFeatures.debugViz` for the Draw pass that renders the batch.
▲ 0↑ born
·
renderfeature · born here
❒asset
# debugViz render feature Draws the batched geometry buffer that `@builtin::modules.debugViz` rebuilds every edit-mode tick from every registered provider — bounds boxes, camera frustums, and any further gizmo the debugViz core gains. One Draw pass over the core-owned vertex/index buffer pair, rasterized as GPU lines through the shared vertex-colour unlit material (`@builtin::shaders.debugGeometry`). An empty state (no providers with geometry yet, or the driving `DebugViz` component not yet awake) enqueues nothing.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
·
zmsh · born here
▤file
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

1problem

Inside

1 problem across 1 item
●
dep.unresolved · L42
could not resolve `@builtin::debugViz.anchor`
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.