luv

Workshop wiki

block-world.org

The little block world and its possible worlds

A game-shaped path through the engine #B4K7WR

The little block world is the first luv experiment whose success is mostly felt rather than inferred. It opens a window, gives the player a place to look around, and makes changes in world representation, meshing, lighting, and physics visible immediately. That makes it an unusually good path through otherwise abstract engine questions.

This page is a map, not a specification. The aim is to preserve a sequence that stays fun while allowing the underlying ideas to become serious. A Minecraft-like game is useful as a recognizable constraint, but luv need not reproduce Minecraft's implementation or close off stranger worlds.

defparameter *gameplay-smash-targets* gameplay-actions.lisp:8

Two authored play sequences through the ordinary centre-ray edit verb. The wall and scaffold are merely deterministic terrain; every filmed removal and placement still crosses luvcraft's player mutation, particle, lighting, mesh-publication, and checkpoint-notification path. #B4K7WR #P3L8YX

defparameter*gameplay-smash-targets*'
14311
15311
16311
16211
15211
14211
14111
15111
16111
"The brick panel swept from top left to bottom right by the mining film."

The current proof #P3L8YX

The current implementation lives in luvcraft/, split between the renderer-independent model in luvcraft/world.lisp and the visible slice above it (blocks, terrain, meshing, simulation, and the application files):

The whole slice remains live and inspectable:

defparameter*luvcraft*
luv:block-mesh-face-count
luv:capture-luvcraft-screenshot*luvcraft*#P"/tmp/luv-block-world.png"

This implementation is evidence that the renderer and lifecycle are ready for game-shaped work. The resident model is a first substrate, not a claim that residency, generation, edits, lighting, or derived products have become one finished system.

defparameter *gameplay-smash-targets* gameplay-actions.lisp:8

Two authored play sequences through the ordinary centre-ray edit verb. The wall and scaffold are merely deterministic terrain; every filmed removal and placement still crosses luvcraft's player mutation, particle, lighting, mesh-publication, and checkpoint-notification path. #B4K7WR #P3L8YX

defparameter*gameplay-smash-targets*'
14311
15311
16311
16211
15211
14211
14111
15111
16111
"The brick panel swept from top left to bottom right by the mining film."

The screenshot gazetteer names gameplay views #2SKLGB

The gazetteer is a small set of semantic screenshot descriptors. Each entry names a gameplay-ish view, builds or streams the world it needs, pins the sky clock, chooses a camera, places any animals it wants in shot, and captures through the real hidden SDL/Vulkan path. The views cover generated terrain at noon and dusk, a placed emitter on a dark floor, a crystal placed on a chunk seam, a yard of legible cast shadows, and a meadow of posed turtles (#KTRMAO). A capture never runs the frame simulation, which is what keeps it byte-deterministic and why the animals in a view arrive already placed rather than growing around a player.

scripts/luv gazetteer build/gazetteer
scripts/luv gazetteer build/gazetteer --view glow-floor --width 640 --height 480

This is not a replacement for tests. It is a stable visual vocabulary for asking whether a change still looks playable: the names are semantic, while the PNGs remain generated artifacts under build/.

What the first extension establishes #E7N4ZS

The visible example keeps 81 resident chunks rather than one handwritten block array. Chunk residency is established first; smooth multi-scale value-noise terrain is materialized second; features are populated third; sparse player edits are replayed last. The seed and world coordinates are the generator inputs, so materializing the same region twice is idempotent and two worlds with the same seed agree site for site.

This is already a useful separation of concerns:

little-world-source + seed + sparse edits
          |
          v
        resident chunk domains -- dense palette-index fields
          |                         |
          | edits                   | own + boundary revisions
          |                         | derived light fields + revisions
          v                         v
coalesced world revision ---> chunk CPU/GPU mesh products

A resident chunk notifies its owning world when its content actually changes, including through the lower-level chunk-block-at writer. Assigning the same value is a no-op. Bulk materialization nests world-change transactions, so thousands of site writes advance the general world revision once while chunk and residency revisions retain their finer meaning. Its dense inner loop writes chunk-block-at-offset: this avoids per-cell coordinates and resident world lookups without granting raw array mutation permission or losing those revision consequences.

The API also makes the hot/cold boundary conspicuous. CLOS owns the meaningful objects—world and source, chunk and domain, block-kind descriptor, mesher and policy—and generic dispatch selects a representation at those boundaries. The actual meshing pass then traverses a whole domain's dense unsigned index column. Cells do not gain object identity merely because they are addressable. world-block-at resolves a single world coordinate for inspectors, sparse edits, ray interaction, collision probes, and other genuinely row-shaped work; its appearance in a dense loop should look suspicious in code review.

Each resident chunk has its own CPU mesh and GPU vertex buffer. Its dependency stamp contains the source chunk revision and light revision, plus only the opposing content and light boundary revisions of each resident neighbor. Thus an interior edit rebuilds one product, while a chunk-face edit or light-boundary change rebuilds the products that actually sample that face. The new pair is published together; the old GPU buffer is destroyed immediately at the API level, whose queue completion frontier defers native teardown if a submitted frame still owns its last use.

A checkpoint saves the world description, not its materialization #TR2JNQ

A luvcraft save is a readable, versioned s-expression containing the block-world space, its procedural source, and the sparse authored edit overlay. Block values use durable semantic names such as :stone and :crystal rather than printed CLOS identities. Explicit air remains a value, distinct from the absence of an edit. Resident chunks, light fields, meshes, and GPU objects are omitted because the source and overlay can rematerialize them.

The container format and the procedural source have separate versions. The first allows the file schema to evolve; the second prevents a changed terrain algorithm from silently presenting a different base world as the same saved one. Unknown versions and block names fail visibly rather than discarding meaning. Edits are sorted by absolute world coordinate, keeping the file stable under chunk residency, future materialization changes, and ordinary text diffs.

Saving crosses a small ownership boundary. The render owner captures an immutable description after a successful edit and submits it without doing file I/O. A sleeping latest-value worker coalesces checkpoints, prints to a sibling temporary file, and atomically replaces the target. Orderly shutdown submits the current player position, look direction, and selected material, then joins the writer. Standalone luvcraft resumes an XDG data-directory default or a path selected with --world FILE; screenshot and benchmark paths remain ephemeral.

An append-only edit log remains a reasonable later storage policy. It would make each edit cheaper to durably append and retain useful history, but it also needs torn-tail recovery, event-version semantics, replay limits, and compaction. The current checkpoint is the smaller first proof. Its explicit owner-to-writer transfer is the seam at which edit events and periodic compact checkpoints can later coexist without moving disk I/O into a frame.

defun make-luvcraft-save-description persistence.lisp:241
defunmake-luvcraft-save-description
world&keycameraplayerselected-blockcarried

Capture one immutable, printable checkpoint description. See #TR2JNQ.

list:luvcraft-world:format-version+luvcraft-save-format-version+:world:resume
luvcraft-resume-save-descriptioncameraplayerselected-blockcarried

DONE Persist and resume the authored little world #IY83CY

Intent. Let building survive quitting and restarting without treating resident chunks as canonical world storage or blocking a frame on file I/O.

Evidence. luvcraft/persistence.lisp implements the versioned description protocol, validation, atomic replacement, and coalescing worker. The standalone accepts --world FILE and checkpoints each successful edit plus final resume state. The luvcraft suite round-trips negative-coordinate edits, explicit air, source and resume state, rejects unknown meaning, and proves the latest asynchronous request is flushed; all 41 tests pass. The standalone executable also builds and reports the new option. The full make test path passes, and make smoke retains MD5 226c4ffdd4515837d5c5a2d9fc415fa4.

Done when. A placed or removed block and the player's location survive a standalone restart, while rendering and derived chunk products remain absent from the file.

Chunk production is an ownership boundary #A8Y3WN

Generation and meshing are now ordinary asynchronous CPU production, not part of rendering's compute budget. luvcraft/production.lisp owns a deliberately small single-worker system built from SBCL's SB-CONCURRENCY:MAILBOX. A blocking request mailbox lets the worker sleep without polling; the render owner drains the result mailbox without blocking. A latest-value table keyed by (:load chunk-key) or (:mesh chunk-key) supplies coalescing and priority, so walking does not retain every superseded request. At most one completed value waits for publication: an active request itself suppresses another wake until its result has been received, bounding result memory when the window pauses.

This is not a generally concurrent world. The canvas/render thread is the single writer of the resident hash table, chunk contents after publication, sparse edit journal, product table, and every Vulkan object. Worker inputs are immutable transfers:

The palettes intentionally retain shared CLOS block-kind descriptors. Those objects are immutable presentable meanings; the repeated sites remain compact indices and never acquire per-cell identity. Meshing dispatches once at the snapshot/mesher boundary and then traverses aggregates.

Publication is optimistic. Every chunk residency receives a monotonically increasing incarnation; mesh stamps contain coordinate, incarnation, content revision, and only the relevant neighbor boundary incarnations/revisions. A returned mesh is accepted only if its stamp still equals the live stamp. A load is accepted only if its residency-demand token is still desired and the coordinate remains absent. Editing, eviction, rapid travel, and evict/reload therefore make obsolete work harmless rather than requiring cancellation in the worker's inner loop.

The frame still performs small bounded publication work. Its default limits schedule four loads, capture one mesh snapshot, and upload two completed products per frame. GPU buffer construction and writes stay there because the render thread owns the device. The old mesh remains renderable until a valid replacement is completely uploaded. These limits are policy knobs, while the ownership and validation rules are invariants.

Procedural little worlds own their moving desired window. A caller-supplied world without that source remains caller-owned: its current resident chunks are meshed asynchronously, but this production layer neither invents absent chunks nor evicts resident ones.

The first implementation uses one worker rather than a pool. This removes the large computation from rendering while keeping order and failure diagnosis intelligible and avoiding CPU oversubscription. It also does not make GC irrelevant: SBCL may stop all Lisp threads, so allocation-efficient aggregate meshing remains part of the latency story.

Shutdown is cooperative: the owner stops new scheduling, clears desired work, sends :stop through the mailbox, joins the worker, and only then destroys GPU destinations. Worker conditions cross back as structured production results and are retained on the demo instead of silently killing the producer.

The square is now a camera-centered resident window rather than the whole world. When the player crosses a chunk boundary, one deterministic strip is materialized and populated, sparse edits are replayed, and the opposite strip is evicted in one coalesced world transaction. The next mesh refresh builds products for the entering chunks and destroys the leaving buffers through the GPU completion frontier. Thus travel is not bounded by the original square, while resident world data and render products remain bounded at 81 chunks.

The tests keep these layers visible. :luvcraft/world exercises coordinates, domains, palette storage, revisions, residency, and ray traversal without SDL or Vulkan. :luvcraft adds deterministic generation, cross-chunk face culling, and camera-directed remove/place behavior.

Landmarks from working voxel engines #V4K8LN

Several established engines converge on the same boundaries without sharing one representation:

The shared lesson is not “all voxel worlds use 16-cubed arrays.” It is that coordinate spaces, resident bulk values, missingness, exceptional metadata, generation, and derived render products are different things. Compression can then vary behind the logical field interface.

A world is not necessarily its finest materialization #W8D2MT

It is tempting to say that a block world is a three-dimensional array of block objects. That description becomes strained as soon as the world is larger than memory, generated on demand, edited sparsely, or represented at more than one resolution.

A more general reading is:

world description
  + procedural sources
  + persistent edits
  + materialization policy
        |
        +-- resident finite domains and their bundles
        +-- derived meshes, colliders, and navigation products
        `-- absent, loading, stale, and evicted regions

At full resolution, one site may have a categorical block state. A coarse site may instead carry surface bounds, occupancy, dominant material, material coverage, or an approximation error. There need not be a one-to-one coarse "block object." What remains stable is that a finite domain identifies sites and semantic fields have values over those sites.

The eager current resident set is then one finite materialization of a particularly small description, not the definition of worldhood.

Things with identity and values at sites #F6R9QA

CLOS does not require one heap object per cell. It helps express which things really have identity, ownership, behaviour, and independently inspectable lifetimes.

Likely objects include:

A site in a dense domain is usually not such an object. It is an address at which one or more fields can be read. A temporary row or focus object may make that site pleasant to inspect without allocating a permanent object for every cell. #C9L2WA develops this distinction.

Block kinds, states, and exceptional entities #K5T2VF

The existing block-kind instances behave like palette entries: many sites share one object that owns colour and generic behaviour. A chunk now stores compact unsigned 16-bit indices into a chunk-local vector of those semantic objects. world-block-at returns the shared object after decoding, while a whole-column algorithm borrows the contiguous index array through with-block-content-storage.

A useful distinction is:

Solidity, opacity, and ordinary face material may be derived from a palette entry rather than copied into fields. Lighting, fluids, or ecological readings may deserve separate fields when algorithms actually consume them.

The first animal is an entity, not a field #KTRMAO

A turtle is the first thing in the little world with a life of its own, and it is built the way #F6R9QA says such a thing should be: a CLOS instance with a position, a heading, a small mind, and an independently inspectable lifetime, rather than a reading of a dense field over sites. There are a handful of them alive at a time, so identity costs nothing. What stays dense is what is always dense here — the per-frame render product, one interleaved vertex array over the ordinary block atlas and block surface pipeline, like the smash fragments before it.

Three protocols meet in an animal, kept apart because they change for different reasons:

Its hide is painted by the same per-tile arithmetic as stone and moss — carapace scutes, plastron plates, and pebbled skin are three more paint-block-atlas-tile and paint-block-atlas-relief methods. One thing a terrain material never needs: a block face takes a whole tile because it is a whole cell, while an animal's faces are each a different fraction of a cell, so a critter face takes the centred fraction of its tile which keeps texels roughly the same size everywhere on the animal. Without that, a shell wears scutes the size of itself and smears the same scutes into bands around its rim.

An animal also answers a ray for itself. The lattice traversal of #B4K7WR is no use here, because a critter is not on the lattice; what the crosshair asks is whether the ray enters the same upright box the animal's feet walk in, and whichever of the animal and the terrain the ray reaches first is what the player is looking at.

Riding is a focus that moves #W2E5HR

Mounting a turtle is the same session transition as reading a wall terminal (#8JCMA5), and it is meant to feel like one: TAB enters the interaction with whatever the crosshair is on, shift-TAB leaves it, ordinary player input stands down for the duration, and the camera eases out of wherever it was into the pose the interaction asks for — the same easing, at the same rate, into the same narrowed field of view.

Two things distinguish a mount from a wall, and each is one generic function:

A ridden animal is reined rather than driven. urge-critter hands the animal the rider's wishes as bounded values in -1..1 and the animal makes of them what it can, which for a turtle is a slow walk and a slower turn; the wanted heading is held within a bounded lead of the animal's own yaw, so a held rein cannot run ahead of what the animal is able to follow. Getting off is a small search for room, because an animal can walk under a ledge its rider could not stand under.

TODO Let the animals be part of the saved world #L4OMIZ

Intent. A checkpoint currently saves the world description and the player (#TR2JNQ) and says nothing about who lives in it, so every session grows a fresh population around the player and yesterday's turtle is not the one you meet today. An animal with an inspectable lifetime should have a lifetime longer than a process.

Evidence. The population is small, sparse, and already deterministic given its seed, which makes it exactly the sparse exceptional state #K5T2VF describes rather than a field: a save entry per living animal, naming its species, position, heading, and seed. The open question is what the world description owes an animal that has wandered into a chunk which is no longer resident.

Done when. Riding a turtle, checkpointing, restarting, and finding the same animal where it was left, with a test that round-trips a population through a save description.

The player has a body, and the body holds things #S27JKR

Until now the player was a camera: an eye at a height above a collision box, with an inventory of materials and nothing to carry them in. The body in body.lisp gives the eye two arms. They hang into the bottom corners of the view, sleeve and wrist and hand, but are no longer little boxes. player-body owns a live ray-marched SDF pipeline like the embodied gnome and cat: capsules make the forearms, ellipsoids make palms and thumbs, and smooth unions make the wrist one silhouette. Five player-group knobs expose arm radius, hand size, sleeve length, shoulder spread, and smoothing; the thumb derives from hand size so turning one knob does not pull the figure apart.

The complete neutral avatar is authored as the same analytic field -- head, torso, legs, arms, and hands -- while first person launches rays only through its lower-screen, view-local arm slice. In that frame x is camera-right, y is up, z is forward, and the eye is the origin, so hands stay attached to the body while the head looks around. Walking bobs their published palm points on a phase accumulated from ground speed, so standing still stands still, and a breath moves them when nothing else does. The explicit :viewmodel stage draws after every world participant and before held-item geometry, rather than letting a later-attached agent blend over the player's own hands. A future third-person or reflected view can launch rays through the full avatar instead of inventing another body.

A hand can hold something. The held-item protocol is a few generic functions -- map-hand-item-boxes for the item's boxes in the grip frame, hand-item-carry-pose for where the right hand holds it, hand-item-name for the title, hand-item-taken-out and hand-item-put-away for what the item does about being held -- and the body eases between pocket and hand rather than cutting, so taking a thing out is a motion. Held-item geometry draws after the analytic hands in the same grip frame; the phone can remain a round-corner slab with a live display without dictating how flesh is shaped. F takes the phone out or puts it away; a pocketed item keeps its state.

The phone is a wall terminal you can carry #IK8PIN

The first thing to hold is a phone, because it is funny: a slab of near-black glass a little too large for the hand, held up in front of the face the way everyone holds theirs, and on it a live shell. It is not a new kind of screen. phone.lisp builds the very display TERMINAL-WALL.LISP builds for a wall (#8JCMA5) -- Ghostty terminal, PTY, Slug glyph run, cell backgrounds, screen panel, faceplate glass -- on a surface of its own kind.

The trick is where that surface lives. A wall's surface is a rectangle of blocks in world space. The phone's is a rectangle in the grip frame, the right hand's own coordinate space, where the screen never moves. What moves is the hand, and therefore, seen from the phone, the camera: each frame the display draws against a frame uniform whose camera lanes are the real camera re-expressed in grip coordinates. Nothing about the runs is rebuilt for motion. The same glyph instances that were right last frame are right this frame, because in their own space nothing happened. The projection and near and far planes are the real ones, so depth is honest against the scene and the phone's own slab. This took only making the surface a protocol (terminal-surface-axes, the lower-left point, physical width and height, currency, focus score, focus camera pose) and letting a display say which uniform it draws against.

Focus is the same transition as a wall's: TAB with the phone out enters its shell and keys go to the PTY; shift-TAB leaves. The phone offers no camera pose of its own, so the camera stays where the player left it and only the field of view narrows; the hand answers instead, easing the phone square to the centre of the view and a little further off, where the narrowed view frames the whole screen. Putting the phone away pockets it with its shell still running.

The phone is not a tube, and its glass says so. The wall's screen and faceplate materials are the whole analogue argument of a CRT -- bezel in world cells, raster, hum, grain, a convex bulge -- and on a hand-sized slab every one of those reads as a fault. The phone draws the same two panels with materials of its own (:phone-screen, :phone-glass): a flat black panel behind the text with rounded corners resolved analytically, and coated glass in front that only reflects -- Fresnel sky, one tight glint -- plus fingerprints, patches of oil that scatter a little of the room forward where the clean coating would not, so they show only where there is light to scatter and never as paint. The slab itself is one even matte tone with the least relief a tile is allowed; its corners are a real mesh -- two fans and a rim of quads around a rounded-rectangle outline (emit-rounded-slab), the one thing in the body that is not a box. The grip takes the sides, never the screen. The screen is proportioned for a real terminal: thirty-five rows of Input Mono Condensed (from the user's own fonts when installed, the wall's Monaspace otherwise) give sixty columns, on a slab proportioned a little like a small handheld console. Carried, it hangs at arm's length and low, tipped back to be glanced at; focused, it comes up square and close.

The wall's three modes -- shell, film, browser -- are not what the phone inherited; it is a shell and only a shell. That mode switch is a kludge waiting to be redesigned from the ground up, and when it is, the phone should be one of the things it is redesigned for.

The sequence that stays fun #S2K5HN

The order below is meant to keep each architectural proof attached to a new experience:

  1. Look around a little world. Done: the camera inhabits generated terrain.
  2. Touch it. Done as a first correctness pass: ray traversal selects, removes, and places blocks, and revisions make the edit visible to the renderer.
  3. Walk into another chunk. Done: the 81-chunk materialization follows the player, deterministically generating an entering strip and evicting the opposite strip. Chunk-owned CPU/GPU products arrive and retire with it.
  4. Let it glow. The sky and voxel-light proof is done: animated sky parameters, block-face atlas selection, procedural texels, geometric normals, corner occlusion, derived sky/blocklight fields, raw vertex light, material emission input, a player-placeable glowing crystal, and longer distance fog all run through the real Vulkan path.
  5. Collide with it. Done as a playable scalar reference: a distinct player body walks, falls, grounds, stops at voxel faces, and jumps under a fixed step. The voxel lattice itself is the static acceleration structure and absent horizontal terrain is a solid simulation boundary. General body and contact domains, persistent manifolds, sleeping, and constraint coloring remain the path described by #Y5H8KC.
  6. Let the world arrive. Done as a first durable proof: deterministic staged generation, sparse edit replay, bounded residency, eviction, recovery, and asynchronous versioned checkpoints now preserve building and resume state across standalone sessions. #TR2JNQ records the boundary and the possible event-log successor.
  7. See farther. Introduce a coarse domain only when there is enough world for level of detail to solve a visible problem.
  8. Let processes inhabit it. Fluids, growth, weathering, or ecology can produce narrow typed fields rather than swelling one universal cell record.
  9. Let something live in it. Done as a first small proof: a turtle wanders the resident surface on the player's own body protocol, is drawn as turned boxes on the block pipeline, and can be mounted and ridden through the modal focus the wall terminals already use. #KTRMAO and #W2E5HR record what an animal is and what riding one turned out to be.
  10. Give things weight. Done as a first pile: spheres in columnar bodies, coloured contacts, a soft-step solver with four-wide lanes, and a ball to throw, a fountain, and a lava spring in the world; the player and the animals push and are struck. #V090DQ and #JAU0EF record the engine and what the game does with it.

This is not a dependency graph. A particularly delightful experiment may pull a later item forward.

Paths into the design #M9C4DJ

Questions to keep open #Q7V3LC