luv

Workshop wiki

domains-and-bundles.org

Domains, bundles, and materialized worlds

The apparent CLOS–SoA contradiction #D5M8BZ

The little block world began with CLOS because generic functions, inspection, class redefinition, and multiple dispatch are unusually pleasant tools for a live engine. A later interest in structure-of-arrays storage can sound like a retreat from that object-oriented view: if data belongs in columns, were the objects a mistake?

There is no contradiction once object identity and repeated numerical storage are placed at different levels. CLOS says how the engine's meaningful things relate and which protocols they support. SoA says how many values of one representation are arranged for traversal. A world, domain, chunk, bundle, field specification, block kind, generator, and solver can be objects while the values at ten thousand sites occupy compact specialized arrays.

The stronger claim is that a domain/bundle model is itself object-oriented. It chooses domains, fields, materializations, and policies as the natural objects rather than assuming every sample is an independently allocated object.

Dispatch on descriptions; compute on domains #J7A2KD

The intended division is not "CLOS outside, low-level code inside." It is more precise:

This makes the high level more semantic while leaving the numerical middle legible to the compiler, SIMD backends, profilers, and eventually GPU kernels. It also gives inspectors and interactive tools excellent CLOS objects to show without requiring the simulation to manufacture one such object for every site it visits.

The naming should reveal which view is being requested. In the current block world, world-block-at is the single-site world-coordinate accessor: it constructs coordinate descriptors, performs a resident-chunk lookup, and decodes the palette value into a presentable shared object. with-block-content-storage is the ordinary aggregate path: it dispatches once for a chunk and borrows its domain, palette, and dense index vector. Borrowed vectors are read-only unless a separate mutation protocol explicitly owns revision and boundary invalidation.

The current dense mutation primitive is chunk-block-at-offset. It accepts a site offset from the chunk's domain and updates the compact column without constructing coordinates, but remains chunk-owned so content revision, exact boundary revision, and world invalidation cannot be skipped. Terrain materialization uses this route. Directly mutating a borrowed index vector is not part of the protocol.

Neither API is shameful. A ray hit, inspector focus, editor selection, or error report really is row-shaped and benefits from description objects. A mesher, generator, lighting pass, fluid step, collision broad phase, or scientific field operation is domain-shaped; repeated calls to the descriptive API inside such a loop are an architectural smell even when their answers are correct.

Identity does not come from being addressable #C9L2WA

A cell can be addressed without being an individual in the engine's object model. It normally has no independent allocation, ownership, or lifecycle; its identity is the site's identity inside a domain. Its block content, light, moisture, or temperature are values borne at that site.

This suggests three useful views:

Rows and foci can be ephemeral CLOS objects without turning the stored world into an array of row objects. They are projections onto columnar storage.

Persistent objects remain appropriate where a thing really has a separate lifecycle: a player, a block entity, a resident chunk, an edit journal, or a mesh waiting for its GPU submission to retire.

What Moppe's Bundle demonstrates #E4N7PX

Moppe's current spatial::Bundle is an eager, finite store of typed columns over one domain. The durable storage contract is deliberately small:

Generic local operations live separately in bundle_operations.hh. There a focus combines one row with its domain index; the domain or an explicit policy supplies neighborhoods. This split is important evidence: Bundle survived the deletion of a much broader terrain expression/program system because finite typed storage and domain-owned spatial meaning had independent value.

Moppe also keeps ground geometry, derived readings, and water in separate bundles even though they share one terrain domain. The reason is ownership and lifecycle, not layout. See docs/surface-storage.md.

A finite domain is more than dimensions #B8R3KF

At minimum a finite domain answers:

Additional capabilities should be explicit rather than presumed:

Moppe's periodic terrain domain can own its neighborhood because its torus is closed. A voxel chunk is not closed: a face neighbor across an edge belongs to another chunk. A local chunk-domain should therefore own local indexing without pretending that an absent neighbor is intrinsically air. #H2W9DJ separates local storage from cross-chunk access.

A domain's sites need not be places. A vocabulary — the atlas tiles, the block materials — answers the same four questions, and #SICRZO takes that seriously enough to ask who owns the mapping from a symbolic identity to the offset a hot loop indexes.

Different domains may cover the same region. A full-resolution block domain, a coarse occupancy domain, a body domain, and a contact-constraint domain have different sites and legal operations even when all ultimately describe one world.

Bundle means a checked materialization #V6T1QS

A bundle is an eager answer to a finite question: for every site in this domain, what are the values of these fields? It is neither the entire world nor a universal database.

A useful tentative contract for luv is:

bundle
  domain
  closed schema of semantic field specifications
  one representation object or projection per logical field
  exactly domain-size logical values in every representation

The schema should be closed by a class or other inspectable definition. A hash table into which arbitrary properties can appear would lose the main benefit: an algorithm should be able to know which fields exist, what they mean, and how they are represented.

Whole-column algorithms may borrow storage directly. Row-oriented code may ask for a site view. Neither form should require copying the other.

Composition must check domain identity, not merely equal array lengths. Two 4096-element columns can describe different chunks, resolutions, placements, or kinds of site. Equal cardinality is not shared meaning.

Logical fields and physical representations #L7C4GM

A semantic field and a storage lane are not the same thing. A field specification can name:

Several representations may implement the same logical field:

Mutation may promote a constant representation to a palette or dense one. The logical single-site description or position-at protocol need not change when that happens, while aggregate operations can still select the new representation once and traverse it directly.

This separation is where the Bundle and quantity ideas meet. Two fields can share an (unsigned-byte 4) representation without becoming semantically interchangeable. Conversely, one vector-valued position field can occupy three scalar SIMD lanes without becoming three unrelated meanings.

The representation line is deliberately not "one field equals one array." A dense scalar field can use one array, but a constant field may store one value, a palette field needs a palette plus codes, a bit field packs many readings into a word, and one vector quantity may project through several SoA lanes. The checked materialization says how logical fields project into those physical representations; kernels borrow whichever raw pieces they require.

Columnar layout, materialization, and policy are separate #Y0TPND

The packed lighting frontier in #QS1ERH is the first small dynamic relative of a bundle. Its current sites are the ordinal domain [0, length); pushing and popping changes membership, while three parallel arrays materialize one entry, offset, and level reading for every member. The level buckets decide which buffer receives or yields a row. That scheduling policy is not part of the storage representation.

A reusable columnar substrate therefore has three parts:

layout           physical lanes plus fixed and bound row meaning
materialization  one domain plus concrete storage satisfying that layout
policy           how membership and ownership change

A dynamic columnar-buffer combines the layout with a changing ordinal domain and append/pop/reset policy. Its storage contract is:

Push validates every incoming Common Lisp representation before modifying a lane, writes the row, and publishes the new length last. Reset clears the active prefix of reference-bearing lanes before setting the shared length to zero. A growth failure leaves the old vectors installed. These rules make the correspondence between columns an invariant of the abstraction rather than a convention repeated by each client.

Stacks, level buckets, FIFO rings, and eventually swap-dense solver sets can compose this storage, but they should remain separate policies. The first substrate need only earn the operations used by lighting. Stable identities, forwarding tables, and swap removal belong to the later body-domain client described by #Y5H8KC; fixed-point algebra still waits for its second client as in #BA9MCA.

A fixed materialization instead receives an explicit finite domain, asks its domain-cardinality once, and allocates exact specialized lanes. It has no capacity or independent membership mutation. Both policies retain the same columnar-layout-definition and columnar-row-declaration vocabulary, so semantic checks stay at the aggregate boundary while array loops remain raw.

Row meaning binds once; raw lanes run #327W2B

A columnar definition has two related descriptions which must not collapse into one.

The physical lane layout fixes lane names, Common Lisp element types, initial values, and reference-clearing policy. It is sufficiently static for a defining macro to generate a concrete structure, specialized arrays, and inline operations. The row declaration says what values those lanes mean. It consists of represented-value declarations and may group several physical lanes into one quantity projection. Both descriptions are inspectable at the aggregate boundary; neither accompanies an individual runtime value.

The existing arithmetic vocabulary already supplies most of the semantic half. A scalar lane can carry a represented-value-declaration with a representation type and optional quantity-specification. A quantity-layout can say that three columns together are one :world-position vector rather than three unrelated scalars. Lanes such as a CLOS entry reference or a chunk-domain offset remain outside that layout: an integer site key is not thereby a dimensionless measured quantity.

Some row declarations are fixed by the definition site. An awake-body buffer might always group three single-float lanes as a world position in metres. Others are bound by the owner of one materialization. The lighting buffer's level lane is physically (unsigned-byte 8) in both cases, but a sky-light frontier should retain the concrete :sky-light field declaration while a block-light frontier retains :block-light. Binding either field checks that its represented value fits the generated lane and records its exact quantity specification and definition revision. It does not introduce a runtime quantity variable or a tag on each byte.

This yields three deliberate checking boundaries:

  1. Defining the layout checks lane names, element types, defaults, retention, and the disjoint named projections of any fixed row declaration.
  2. Constructing a buffer checks owner-supplied declarations against its physical lanes and retains the resulting concrete row declaration.
  3. Binding a kernel checks its parameter declarations against that row once, then borrows the raw arrays for a closed loop.

Ordinary push, pop, and row iteration consequently enforce representation types but perform no dimensional dispatch. Quantity correctness comes from the declarations at the producer, materialization, and kernel boundaries, just as CPU arithmetic lowering erases checked quantity construction before executing ordinary numbers. An inspector can still show the buffer's schema, concrete field bindings, length, capacity, and lanes.

A tentative definition site illustrates the division without settling the surface syntax:

define-columnar-bufferlight-site-buffer
entry:type
ornulllight-region-entry
:clear-on-removet
offset:type
unsigned-byte32
level:type
unsigned-byte8

FIELD-DEFINITION supplies LEVEL's concrete represented-value declaration.

make-light-site-buffer:declarations`
level,field-definition

Generated names are preferable to a runtime-configured universal container: SBCL can see the element types and inline array access, while the schema and its quantity judgments remain ordinary semantic objects. This is the same object-at-the-boundary, dense-data-inside seam as #J7A2KD, now applied to a dynamic row domain.

defclass columnar-row-declaration columnar.lisp:99
defclasscolumnar-row-declaration
layout-definition:initarg:layout-definition:readercolumnar-row-declaration-layout-definition
lane-declarations:initarg:lane-declarations:readercolumnar-row-declaration-lane-declarations
quantity-layout:initarg:quantity-layout:readercolumnar-row-declaration-quantity-layout
revision:initform
gensym"COLUMNAR-ROW-"
:readercolumnar-row-declaration-revision
:documentation

Concrete per-lane declarations retained once by a columnar materialization.

Rows remain raw array elements; this object carries their checked meaning at the aggregate boundary. #327W2B

DONE Extract a quantity-aware columnar buffer #LDP5UR

Intent: extract the synchronized specialized storage proven by the lighting frontier into a generated columnar-buffer substrate, while making its physical lanes and concrete row quantity declaration inspectable. Keep level scheduling, spatial stepping, and fixed-point behavior in lighting.

Evidence: define-columnar-buffer now publishes an inspectable physical definition and generates an ordinary structure, specialized lanes, one shared length and capacity, coherent allocate-copy-publish growth, and inline push and pop operations. Push checks every representation before writing and advances the length last. Pop and reset clear only declared reference-bearing lanes. The row macros expand into direct array access without constructing a row or dispatching on a runtime schema.

A columnar-row-declaration binds concrete represented-value declarations to those lanes once. Fixed named lane groups lower to the existing quantity-layout vocabulary; a two-column test retains one :position vector quantity. Dynamic bindings reject incompatible representations. The lighting worklist binds its level lane to the retained :sky-light or :block-light field definition, so equal u8 storage does not erase their different quantity names or definition revisions.

The generated light-worklist-bucket replaces three adjustable vectors and independent fill pointers. Level buckets and their brighter-first policy stay in lighting. On SBCL 2.6.6, five warmed runs of the same seed-121 81-chunk solve visited exactly 340,050 sites, took 0.177--0.181 seconds, and consed 13,126,672--13,137,168 bytes. This matches or slightly improves the recorded 0.180--0.181 seconds and about 13.3 MB before extraction.

Done when: the lighting buckets use one shared length and capacity over generated specialized lanes; growth is coherent; pop and reset clear retained entries; push, pop, and iteration allocate no row and dispatch on no schema; tests reject an incompatible represented declaration and distinguish concrete sky-light from block-light bindings; a small split-vector declaration proves that several columns can retain one vector quantity; the current lighting correctness suites pass; and the packed level profile shows no material regression. Do not add stable ids, swap removal, a generalized solver, or a schema metaclass in this iteration.

Done: all conditions above are satisfied without adding the deferred policies. The full make test path passes, including strict Parinfer, 62 luv tests, 149 luvcraft tests, the wiki suite, and generated SPIR-V validation. The standalone smoke render remains byte-identical at SHA-256 0c85d4550638418cb88d507c6541a49147aeeea5697175d13904942b65a689ec.

defmacro define-columnar-buffer columnar.lisp:239
defmacrodefine-columnar-buffer
name-and-options&bodylane-descriptions

Define a concrete synchronized structure-of-arrays buffer.

Each lane is (NAME INITIAL-ELEMENT :type type [:CLEAR-ON-REMOVE T]). Optional :QUANTITIES on name-and-options groups named physical lanes into fixed quantity projections. The generated MAKE-, -PUSH, -POP, and -RESET functions operate on raw specialized arrays with one shared length and capacity. #LDP5UR

multiple-value-bind
namequantities
parse-columnar-name-and-quantitiesname-and-options
let*
lanes
mapcar#'parse-columnar-lanelane-descriptions
lane-names
mapcar
lambda
lane
getflane:name
lanes
internal-constructor
columnar-generated-symbolname"%%MAKE-~A"name
constructor
columnar-generated-symbolname"MAKE-~A"name
grow
columnar-generated-symbolname"%~A-GROW"name
push-name
columnar-generated-symbolname"~A-PUSH"name
pop-name
columnar-generated-symbolname"~A-POP"name
reset-name
columnar-generated-symbolname"~A-RESET"name
length-slot
columnar-generated-symbolname"LENGTH"
capacity-slot
columnar-generated-symbolname"CAPACITY"
row-slot
columnar-generated-symbolname"ROW-DECLARATION"
length-reader
columnar-generated-symbolname"~A-LENGTH"name
capacity-reader
columnar-generated-symbolname"~A-CAPACITY"name
lane-slots
mapcar
lambda
lane
columnar-generated-symbolname"~A-LANE"
getflane:name
lanes
lane-readers
mapcar
lambda
slot
columnar-generated-symbolname"~A-~A"nameslot
lane-slots
new-lane-variables
looprepeat
lengthlanes
collect
gensym"NEW-LANE"
source-form`
define-columnar-buffer,name-and-options,@lane-descriptions
layout-form
columnar-quantity-layout-formquantitieslanes
unless
=
lengthlane-names
length
remove-duplicateslane-names:test#'eq
error"Columnar lane names must be distinct: ~S"lane-names
`
progn
eval-when
:compile-toplevel:load-toplevel:execute
defmethodcolumnar-layout-definition-for
layout-name
eql',name
declare
ignorelayout-name
load-time-value
make-instance'columnar-layout-definition:name',name:lanes
list,@
loopforlaneinlanescollect`
make-instance'columnar-lane-definition:name',
getflane:name
:representation-type',
getflane:type
:initial-element,
getflane:initial-element
:clear-on-remove-p,
getflane:clear-on-remove-p
:source-form',
getflane:source-form
:quantity-layout,layout-form:source-form',source-form
defstruct
,name
:constructor,internal-constructor
,length-slot,capacity-slot,row-slot,@lane-slots
:copiernil
,length-slot0:typefixnum
,capacity-slot0:typefixnum
,row-slotnil:typecolumnar-row-declaration:read-onlyt

Each lane slot carries its precise specialized array type, so

the generated push and every direct lane access store and

load raw elements without boxing them.

,@
loopforslotinlane-slotsforlaneinlanescollect`
,slot
make-array0:element-type',
getflane:type
:type
simple-array,
upgraded-array-element-type
getflane:type
defun,constructor
&key
capacity16
declarationsrow-declaration
check-typecapacity
integer0#.most-positive-fixnum
when
anddeclarationsrow-declaration
error"Supply DECLARATIONS or ROW-DECLARATION, not both."
let*
unless
eqdefinition
columnar-row-declaration-layout-definitionrow
signal-columnar-declaration-errordefinitionnilrow:foreign-row-declaration
,internal-constructor0capacityrow,@
loopforlaneinlanescollect`
make-arraycapacity:element-type',
getflane:type
:initial-element,
getflane:initial-element
defun,grow
bufferminimum-capacity

Growth is rare, and one inlined REPLACE per lane over precisely

typed arrays makes SBCL's constraint propagation take minutes on

a buffer with a few dozen lanes. Call REPLACE instead.

declare
notinlinereplace
let*
old-capacity
,capacity-readerbuffer
new-capacity
maxminimum-capacity1
*2old-capacity
length
,length-readerbuffer
,@
loopforlaneinlanesforreaderinlane-readersfornewinnew-lane-variablescollect`
,new
let
array
make-arraynew-capacity:element-type',
getflane:type
:initial-element,
getflane:initial-element
replacearray
,readerbuffer
:end1length:end2length
array
setf,@
loopforreaderinlane-readersfornewinnew-lane-variablesappend
list`
,readerbuffer
new
setf
,capacity-readerbuffer
new-capacity
buffer
declaim
inline,push-name,pop-name
defun,push-name
buffer,@lane-names
,@
loopforlaneinlanescollect`
check-type,
getflane:name
,
getflane:type
let
position
,length-readerbuffer
when
=position
,capacity-readerbuffer
,growbuffer
setf,@
loopforreaderinlane-readersforlane-nameinlane-namesappend`
aref
,readerbuffer
position
,lane-name
setf
,length-readerbuffer
buffer
defun,pop-name
buffer
let
position
1-
,length-readerbuffer
when
minuspposition
return-from,pop-name
values,@
looprepeat
lengthlanes
collectnil
nil
let
,@
loopforlane-nameinlane-namesforreaderinlane-readerscollect`
,lane-name
aref
,readerbuffer
position
,@
loopforlaneinlanesforreaderinlane-readerswhen
getflane:clear-on-remove-p
collect`
setf
aref
,readerbuffer
position
,
getflane:initial-element
setf
,length-readerbuffer
position
values,@lane-namest
defun,reset-name
buffer
,@
let
clear-lanes
loopforlaneinlanesforreaderinlane-readerswhen
getflane:clear-on-remove-p
collect
conslanereader
whenclear-lanes`
let
length
,length-readerbuffer
,@
loopfor
lane.reader
inclear-lanescollect`
fill
,readerbuffer
,
getflane:initial-element
:endlength
setf
,length-readerbuffer
0
buffer
',name
defun make-light-worklist light-reference.lisp:43
defunmake-light-worklist
&key
scheduling:lifo
field-definition

Make the reference solver's packed LIFO or level-scheduled worklist.

This machinery belongs to the test-only voxel-light oracle. Both modes retain ENTRY plus dense OFFSET and LEVEL lanes, never a cons or coordinate object per item. #QS1ERH #LDP5UR

check-typescheduling
member:lifo:level
let*
row-declaration
records:make-columnar-row-declarationlayout-definition
andfield-definition`
level.,field-definition
buckets
dotimes
level
lengthbuckets
setf
arefbucketslevel
make-light-worklist-bucket:capacity256:row-declarationrow-declaration
%make-light-worklist:schedulingscheduling:field-definitionfield-definition:bucketsbuckets

DONE Bind fixed columnar materializations to finite domains #LC5ULQ

Intent: make the dynamic lighting buffer one storage policy over reusable columnar layout metadata, add a fixed domain-bound policy, and prove the split with the meshing snapshot rather than a synthetic universal bundle.

Evidence: columnar-layout-definition now describes physical lanes and row meaning independently of storage policy. define-columnar-buffer still generates the growing frontier operations, while define-columnar-materialization generates an exact set of specialized lanes whose extent comes from luv.domains:domain-cardinality. The matching borrow macro binds domain, extent, row declaration, and selected precisely typed arrays once for an ordinary scalar or SIMD kernel.

The one-cell meshing halo is now a concrete block-mesh-halo-domain and its content code, sky level, and block level arrays are one generated fixed materialization. Sky and block lanes retain the existing distinct quantity definitions. The content u16 lane intentionally does not claim to be the logical :block-content field: it is a code projected through the snapshot's palette, demonstrating why a field cannot always equal one array. The snapshot keeps its domain, palette, logical content definition, and columnar storage together across the worker boundary.

Done when: dynamic buffers use domain-independent layout metadata; a minimal finite-domain cardinality protocol allocates fixed lanes; fixed borrowing retains exact array types and row meaning; the meshing halo is the second real client; sky and block definitions remain distinguishable; direct and snapshot meshes remain bit-identical; and the normal test and smoke paths pass.

Done: the cold make test path passes with strict Parinfer, 63 luv tests, 149 luvcraft tests, 14 wiki tests, and generated SPIR-V validation. The standalone smoke render remains byte-identical at SHA-256 0c85d4550638418cb88d507c6541a49147aeeea5697175d13904942b65a689ec.

DONE Percolate domain-bound representations through voxel storage #1RF48O

Intent: make the fixed materialization vocabulary describe the ordinary resident and captured voxel fields too, and transfer palette-backed block content as one representation rather than as an accidental pair of arrays.

Evidence: resident chunk-light-field objects and captured light-region-entry objects now retain the same generated voxel-light-columns representation over their respective chunk domains. Sky and block levels remain separate u8 lanes with distinct quantity-aware field declarations; revisions and boundary state remain on the owning derived product rather than being forced into the columns.

Palette-derived propagation loss and emission are another fixed materialization. Their explicit block-palette-domain has one site per palette code, and their u8 lanes project the dimensional declarations on the block-kind opacity and emission slots. Thus the solver performs CLOS dispatch once per palette entry and then sees only dense typed lookup arrays.

block-content-column now binds its palette and u16 codes to the authoritative chunk domain. Async production transfers that aggregate object; publication preserves the arrays while rebinding them to the owner world's fresh resident domain. materialized-field-representation and field-representation-domain make this relationship inspectable without claiming that every logical field is exactly one array.

Done when: resident and captured light use one representation type; palette properties retain their quantities over an explicit finite domain; block content cannot be constructed with a mismatched extent or palette code; async loading transfers the representation and preserves single-writer ownership; and the complete 149-test luvcraft suite passes.

NEXT Give dynamic columnar populations explicit compaction #34G2K8

The block-particle population is the next honest client. Its block reference, position, velocity, size, age, and lifetime naturally form quantity-aware SoA lanes, but its update removes arbitrary expired rows while preserving survivor order, and overflow discards an oldest prefix. The current generated dynamic buffer owns push, pop, and reset only.

Do not hide a second private length protocol in particles.lisp. First give define-columnar-buffer checked truncate/prefix-discard and an in-place stable compaction vocabulary that moves every lane together and clears removed reference lanes. Then migrate rendering and integration to borrowed lanes, decide whether the exported row-object accessor becomes a presentation view, and test stable ordering, bounded bursts, expiry, and retained-object release.

A columnar buffer is not one hot sequence #IUTMUY

SBCL's extensible sequence protocol can make an active lane view behave like an ordinary sequence. A small probe subclassed sequence, supplied sb-sequence:length and sb-sequence:elt methods, and then worked with both cl:length and sb-sequence:reduce. Such a view could be pleasant for inspectors, generic algorithms, and REPL tools because it presents only the active prefix while retaining the buffer's own capacity underneath.

It is not the hot representation of a columnar row. An element of the whole buffer is a heterogeneous product, so elt would have to allocate a row, return an aliasing cursor, or invent another tuple representation. Even the homogeneous single-lane probe pays generic iterator and operation calls per element: ten sums of 1,048,579 floats took 0.425--0.458 seconds, versus 0.006--0.007 seconds for a typed loop over the borrowed array. Specializing reduce could recover that loop, but then the useful abstraction is the kernel itself, not generic sequence iteration.

The production seams are therefore with-columnar-buffer-storage for a changing ordinal domain and with-columnar-materialization-storage for a fixed explicit domain. They bind the active extent, retained row declaration, and selected lanes with precise simple-array types. Tooling may later layer SB-SEQUENCE views over that boundary when a concrete client wants them; scalar and SIMD kernels borrow the arrays directly. #VKLLPR records the first native pack experiment.

Chunk boundaries need an environment #H2W9DJ

Meshing, ambient occlusion, lighting, fluids, and collision all inspect neighbors. At a chunk edge, the required value belongs to an environment larger than the chunk's finite storage.

Possible explicit contexts include:

The domain should say which local site an offset denotes. The environment should say what exists beyond this materialization. Keeping those questions apart avoids making streaming state look like terrain truth.

A derived mesh or collider can record the source generations of the center and neighboring chunks it observed. A boundary edit then invalidates every artifact whose proof included the old generations.

Level of detail changes the schema as well as the spacing #O3P8NV

Level of detail is not always the same field sampled less frequently. Exact block content is categorical; averaging stone and air does not produce a half-stone block. A coarse domain may need fields such as:

Those are new semantic readings derived from a finer materialization or directly from the procedural source. A coarse bundle can therefore have a different schema while sharing a world-space extent.

This also suggests that procedural generation should answer domain-shaped requests. The generator need not construct the finest blocks for a distant coarse chunk if it can produce honest coarse readings directly.

Products follow ownership and lifecycle #A6X2RT

One universal voxel bundle would combine values with very different causes and invalidation rules. A more legible arrangement might eventually include:

This list is illustrative. A field should not exist because a framework has a slot for it. It should appear when an operation produces or consumes it.

Narrow computations can return narrow products. The world owner may retain them side by side, compose checked views, or materialize a wider persistence record. Lisp aliasing and mutation make a literal port of Moppe's consuming join operation unnecessary until a real owner wants it.

The proposed terminal viewport #RD8AEI is a compact non-voxel client of the same distinction: Ghostty remains the authoritative source, while the renderer retains a narrow cell-presentation materialization and derives separate glyph and decoration populations from it.

The first Lisp atelier should stay ordinary #R3F8YC

A small experiment should attach ordinary inspectable field definitions to the block-content and light columns which already exist, without committing the game to a general framework or changing their storage. The concrete spatial extraction and field/kernel bridge are developed in #18MPLA.

A later metaclass experiment might look like:

defclassvoxel-content-bundle
finite-bundle
block-content:field-value-typeblock-state:field-representationpalette-index:field-default*air-block*
:metaclassbundle-class

Such a metaclass could turn field slot definitions into inspectable schema objects and allocate columns from the instance's domain. It could reject duplicate field meanings and teach describe or an inspector how to show both logical values and physical storage.

Common Lisp class redefinition would offer a particularly playful experiment: update-instance-for-redefined-class could add a newly declared column to already resident chunks with the proper domain-sized default. Whether that remains delightful after real use is an empirical question. Start there only after block content, voxel light, and another real field have shown that class-level schema, allocation, and migration repeatedly want the same machinery.

The hot path need not use generic dispatch per site. A generic operation can select a bundle representation or numerical phase once, borrow specialized arrays, and run an ordinary optimized loop. #R9M4PV applies that arrangement to physics and possible wide backends.

What this page does not decide #N6Q2SH

The first proofs are now present: the current world occupies finite domains with palette-backed block content, and voxel lighting publishes separate sky and blocklight columns with its own revisions. Their common indexing did not make one universal bundle desirable. Instead, the working light region, meshing neighborhood, and halo snapshot revealed repeated location, stepping, boundary, and offset machinery. #K3KZTG records the smaller next proof: extract that shared spatial vocabulary before introducing a schema metaclass or a generalized solver.