Technical debt that multiplies truths
What this audit counts as debt #J97QHX
This is a source audit of luv and luvcraft at 58b0a3afb75f. It ranks code
that makes one change require several coordinated edits, preserves an obsolete
runtime path, or puts a contract on the wrong side of a subsystem boundary.
Large files and long explanations are not debt by themselves. The problem is
when they conceal several independent sources of truth.
The ranking comes from reading the systems, packages, largest implementation files, backend seams, session lifetime, shader lowering, voxel lighting, and their call sites. It is a work map, not a request to rewrite everything.
Ranked cut list #OUKQ2A
| Rank | Debt | Why it compounds | First safe cut |
|---|---|---|---|
| 1 | Luvcraft session and render lifetime | One GPU object appears in several ledgers | Extract one renderer that owns its resources |
| 2 | McCLIM GPU command dispatch | A new command edits four type tables | Put command phases behind generic functions |
| 3 | Shared shader IR lives in SPIR-V | Both backends depend on one backend's name | Extract luv.shader and backend lowerings |
| 4 | The HAL boundary is only half-owned | Backends disagree on portable inputs; video bypasses HAL | Normalize once; add an interop protocol |
| 5 | Three voxel-light runtime solvers | Oracle, experiment, and production coexist | Close dispatch; move or delete old paths |
| 6 | Serial umbrella systems hide islands | Optional features enlarge every core load | Extract only the already-visible islands |
| 7 | Compatibility entry points without clients | Tests keep obsolete surfaces alive | Delete the high-confidence queue |
Ranks 1--5 are architectural. Ranks 6--7 are useful supporting cuts, but splitting systems or deleting aliases will not repair duplicated ownership or dispatch on its own.
1. The session has several resource ownership truths #V9VH79
luvcraft-session has about one hundred slots spanning world production,
simulation, input, GPU attachments, layouts, pipelines, video, text, overlays,
and frame state. luvcraft:start-luvcraft is 623 lines. During startup
a GPU object can simultaneously be a lexical binding, a named session slot,
an element of resources, or an element of the separate pipelines list:
Resize code must replace named slots and update the resource bag. luvcraft:stop-luvcraft separately enumerates ten pipeline accessors, then destroys the resource bag, then releases video, text, canvas, and device. The teardown is careful; the debt is that correctness depends on every future resource author remembering every ledger.
The first extraction should be semantic, not a giant constructor macro:
The renderer should be the sole runtime owner of those objects. A small transactional resource helper may protect construction failure, but the finished owner must not also depend on a catch-all session resource list. Later extractions can give production/streaming and simulation the same shape; the session then coordinates owners instead of being all of them.
Referenced from code: One slot is the publication point for the complete extent-sized cohort.
Readers can therefore never observe a new colour image with an old depth
or presentation image while a live resize is being installed. The cache and both inventories are private implementation details of
this owner. A construction-time rollback list may precede it, but a
completed session has no second resource ledger. See #V9VH79. The sole owner of a session's frame attachments, layouts, pipelines, and
per-drawable GPU state. The session coordinates this owner with simulation,
streaming, overlays, and presentation; it does not duplicate its inventory.defmethod update-instance-for-redefined-class :after app.lisp:379 ↗
defclass luvcraft-renderer renderer.lisp:26 ↗
DONE Give rendering resources one transactional owner #V5V0JJ
luvcraft-renderer now solely owns named pipelines, layouts, resources, frame
state, atlas state, and one atomically published frame-attachment cohort.
Startup, resize, atlas replacement, live-session migration, and teardown all
use that owner. A failed renderer destruction retains its retry handle;
focused tests exercise partial construction, coherent publication, migration,
retry, and idempotent release. Backend-native retirement has separate durable
HAL custody described in the cut below.
2. McCLIM implements one open command vocabulary four times #A0JOG7
mcclim/gpu.lisp defines solid, analytic, relief, gradient, lattice,
image, and text command structures. Their algebra is repeated in four
etypecase tables:
| Phase | Function | Repeated decision |
|---|---|---|
| Compose pane streams | compose-gpu-mirror-media | Copy and rebase the command |
| Prepare frame data | prepare-gpu-frame-commands | Preserve or turn it into a prepared command |
| Set clipping | gpu-frame-command-clip | Select the command's clip accessor |
| Encode draws | render-gpu-mirror-frame | Select pipeline, bindings, buffer, and draw fields |
For example, the clip operation alone knows every prepared command type:
Adding one draw family therefore requires shotgun surgery across distant parts of a 2,692-line file. This is an owned, growing vocabulary, so it wants an open protocol:
The command structures can remain compact method specializers, and a common
included draw structure can hold The four command-family type tables are gone. Internal generic functions own
rebase, preparation, clipping, and encoding while the command structures and
dense arrays stay compact. Direct luvcraft replay builds one dynamic-extent
context, so cached frame state retains only its persistent destination GPU
resources. A recording encoder checks the exact pipeline, binding, buffer,
offset, and draw mapping of all seven native command families.first-vertex, vertex-count, and clip.
The dense vertex arrays should remain arrays. This follows the already useful
portable encode=/=enqueue command seam described by #D8F3QM; it does not ask
for a retained object graph or a class per vertex.DONE Make command phases an open protocol #GC3QF4
3. The shared shader language is trapped inside the SPIR-V backend #TUVTOM
hal/shader/language.lisp is 5,757 lines and begins with
(in-package #:luv.spir-v). It contains the backend-neutral type system,
source graph, parser, quantity checking, definition revision tracking, lowering
protocol, and SPIR-V lowering. Metal's direct MSL lowering consequently
specializes a SPIR-V-named protocol over SPIR-V-named portable objects:
Lower the shared shader graph directly to a structured MSL document.
The source has already outgrown that ownership: #LVKEYO describes one shared
typed graph with sibling SPIR-V and MSL lowerings. The package graph still
says that SPIR-V owns both siblings. The permanent source also carries
eval-when migrations that unintern old portable operators and repair old
accessors in a live image.
Extract three explicit layers:
luv.shader types, source graph, parser, revision, lowering protocol luv.spir-v literal SPIR-V model and SPIR-V lowering methods luv.msl structured MSL model and MSL lowering methods
Do this as a package/file move before redesigning the language. Update the
remaining The neutral type graph, parser, quantity checks, source revisions, live
definition tracking, and lowering generics now live in shader-abstraction-revision callers to shader-source-revision,
restart the durable image at the package boundary, and then delete the one-time
live migration shims. The split makes the architecture in #LVKEYO true in the
loader as well as in the prose.DONE Give the shared shader graph its own home #C18ML5
luv/shader and the
LUV.SHADER package. SPIR-V's literal model and lowering context live in the
sibling luv/spir-v system; luv/msl depends only on the shared language.
Repository callers name neutral concepts through shader: and reserve spv:
for actual modules, instructions, compilation, and lowering results. Cold
loads prove that the shared system loads neither backend and MSL loads without
SPIR-V. The one-time package/accessor migration shims are gone.
4. The HAL boundary is only half-owned #YI8ZE7
The portable descriptor types live in hal/gpu.lisp, but each backend
independently defines what their input shapes and errors mean. Vulkan accepts
a list or vector of two or three texture dimensions and canonicalizes it to
(width height 1). Metal accepts only a two-element list and leaves it
two-dimensional. Vulkan reports :unsupported-features and
:unsupported-limits separately; Metal collapses both into
:unsupported-requirements. Similar keyword/list/vector normalization is
repeated for buffer and texture usage.
Backend capability differences are legitimate. Portable syntax and error shape changing with the selected backend are not. Move descriptor canonicalization and common structural checks into the portable layer; hand a canonical descriptor to each backend for capability checks and native enum translation. Closed Vulkan and Metal enum tables should remain backend-local.
The same uncertain boundary is more serious in
luvcraft/video-screen.lisp. Game code tests private backend classes,
reads luv::vulkan-device-* and luv::metal-native-object slots, interprets
libav::av-vulkan-frame, and performs CoreVideo FFI. Reader conditionals then
choose the platform path. This contradicts the useful HAL seam in #A7N4XP:
Introduce a narrow interop extension, for example
Portable Video decoding crosses a separate open bridge. Portable
While a backend queue is live, logical GPU destruction transfers a complete,
per-call resumable native teardown into its FIFO retirement ledger before
invalidating the wrapper. Nonempty submission and retirement work
process-roots the queue; a lazy service advances it even after the application
drops its last reference. Device destruction closes admission and refuses to
discard unfinished ownership. Without a live queue, teardown must succeed
before invalidation. Vulkan leak finalizers transfer into a separate
process-global ledger before reporting the leak. Each adopted video plane retains its frame importer until that native
retirement callback succeeds. Partial picture construction, partial screen
construction, and failed startup teardown enter small process-rooted release
backlogs, so no lexical unwind can discard the final retry handle. Vulkan's
two plane wrappers also share one generation-scoped external timeline state;
post-submit layout/semaphore publication remains in the rooted submission
until every owner callback succeeds. video-decode-configuration and adopt-decoded-video-frame generic functions.
Backend/video bridge files should own native handles and return portable
textures/views plus an explicit owner. Luvcraft should own playback policy and
the screen; it should not decode Vulkan or CoreVideo ownership records.DONE Make the portable HAL boundary tell the whole truth #QYZ3XP
create and adopt-native-texture methods now copy and canonicalize
buffer/texture descriptors before backend dispatch. Size, dimension, and
usage syntax has one error vocabulary; every texture extent is (w h 1);
Metal and Vulkan retain only format/capability translation. Backend-free
probe tests cover copies, vectors, duplicates, malformed sequences, adoption,
and condition provenance, while the Darwin suite checks Metal's canonical
owned/imported extents and two-dimensional writes.video-decode-configuration and frame-importer generics own the policy seam;
Vulkan and Metal bridge files alone inspect AVVkFrame, queues, CoreVideo,
and native handles. A screen publishes one complete two-plane picture cohort,
rolls back partial import, and retires views before textures.video-screen.lisp contains no backend
or native ownership names.
5. Voxel lighting keeps oracle, experiment, and production live #S6421N
The default *voxel-light-solver* is :compiled, but the runtime still offers
three complete from-scratch paths: :legacy, handwritten :frontier, and
:compiled. The incremental protocol is more dangerous: its unspecialized
method ignores the solver and silently invokes the legacy reconciler.
A misspelled or future solver therefore succeeds using the wrong algorithm. The handwritten frontier transfer loop also duplicates the relation compiled by the production frontier realization. The differential work in #X7Q90E, #PJY6E1, and #K3WRD3 has made this duplication useful evidence; it need not remain a permanent runtime choice.
Make unknown solver names signal immediately. Delete the handwritten
Production from-scratch and incremental dispatch now accept only :frontier runtime path now that the compiled path is the production proof.
Keep the legacy solver only as a differential test oracle in a test/reference
system; if an operational recovery path is genuinely required, name it
explicitly and record its use rather than making it the fallback. Rename
comparison slots from frontier-* to candidate-* so the diagnostic does not
preserve the retired implementation in its interface.DONE Keep one production light solver #J2UE30
:compiled;
unknown and retired names signal. The handwritten frontier solver and legacy
incremental reconciler are gone. A simple from-scratch oracle and the renamed
candidate comparator remain available only through the explicit
luvcraft/light-reference system, preserving differential evidence without a
fallback runtime truth.
6. Serial umbrella systems conceal useful dependency islands #74HG89
luvcraft/core serially loads roughly forty files and unconditionally depends
on the GPU stack, Ghostty, libav, terminal/canvas, world, simulation, media,
network-facing features, rendering, and capture/tool helpers. Its 686-line package file also
defines both luvcraft.frontier and the broad luvcraft package. The luv
umbrella similarly loads the shared shader language only after Vulkan's
SPIR-V package and model.
The result is unnecessary compile/load fanout and an image that is harder to
repair when one optional dependency changes. luvcraft/world proves that a
small renderer-independent island can exist without turning the project into
dozens of micro-systems.
Extract only boundaries already demanded above: luv/shader, a frontier
kernel system, the renderer owner, and platform video bridges. Keep luv and
luvcraft/core as convenience umbrellas. ASDF decomposition should express
ownership and optionality, not merely make the component list shorter.DONE Expose the shader, frontier, and reference islands already earned #JV6E2Z
luv/shader, luv/spir-v, and luv/msl are now independent sibling systems,
and luvcraft/light-reference keeps the oracle out of production loading.
luvcraft/frontier now owns the frontier package and compiler over only
luv/arithmetic and luvcraft/world; a cold load does not acquire the
renderer, video, GPU, Ghostty, or libav umbrella. Renderer and video ownership
boundaries remain separate source components under the convenient
luvcraft/core umbrella.
7. A high-confidence deletion queue #H03JVK
These are smaller than the architectural debts, but removing them first will reduce the surface that later refactors must carry:
block-world-crosshair-vertex-module/shaderhand-builds literal SPIR-V after the same file defines the mathematical vertex specification. The live renderer uses the specification, while one HAL test still tests the literal shader. Delete the literal builder and make the test compile the production specification.find-gnomeandplace-gnomeare exported compatibility aliases with no repository callers. Delete them in favor offind-agentandspawn-agent.shader-abstraction-revisionis a compatibility alias still used by the live pipeline and one shader test. Migrate those callers and remove the alias.make-block-content-column :paletteand the palette-plus-indices branch ofinstall-world-chunk-storagehave no non-compatibility callers. Require the vocabulary-backedblock-content-columnthat production already transfers.emit-block-faceandmesh-block-worldare explicitly compatibility APIs; only tests call them. Delete them and testmesh-block-snapshotormesh-block-chunk, unless a named development tool is made their real owner.- Remove the shader package's one-time
unintern=/=fmakunboundmigrations when rank 3 deliberately restarts the image. Do not let live-upgrade scaffolding become a permanent load phase.
The persistence migration for old saved gnome edits is different: saved data
is an external boundary. Keep versioned persistence migrations until the
supported save floor is intentionally raised. The gnome aliases, literal crosshair module, shader revision alias, palette
constructor/install compatibility, and compatibility meshing entry points are
gone; their tests now exercise production specifications, chunks, snapshots,
and vocabulary-backed columns. The saved-gnome migration remains because it
serves persisted external data rather than repository callers.DONE Delete the queue without deleting history #OHTJXI
A workable order #CWKEY9
- Delete the uncalled aliases, obsolete crosshair module, and compatibility constructors; point their tests at production entry points.
- Close voxel solver dispatch and retire the handwritten runtime solver.
- Replace McCLIM's four command type tables with methods, one phase at a time.
- Extract the renderer as the sole owner of frame attachments, layouts, and pipelines; then make session start/stop orchestration use that owner.
- Split the shader package and normalize portable GPU descriptors. This is the deliberate durable-image restart boundary.
- Move native video import behind backend methods and reflect the resulting ownership islands in ASDF.
Each step has a local stopping condition: one fewer parallel dispatch table, one fewer resource ledger, one fewer selectable obsolete algorithm, or no backend-private name above HAL. That is a more useful measure than smaller files alone.
The sole owner of a session's frame attachments, layouts, pipelines, and per-drawable GPU state. The session coordinates this owner with simulation, streaming, overlays, and presentation; it does not duplicate its inventory.
Layouts: binding grids, clauses, loop rows. The selectors are the roles dexp.lisp assigns.
(component)Release every resource owned by COMPONENT. Component release is an owner boundary: callers name the component once, rather than reproducing its private resource inventory.
(component extents)Replace COMPONENT's frame-sized state at a frame boundary. EXTENTS names both the expensive game-scene resolution and the final drawable-matched resolution where native-density application UI is composed.
((session luvcraft-session) added-slots discarded-slots retired-values
&rest initargs)Logical conjunction of tests and raw truth values.
The former session slots transferred into a renderer by live migration.
The former session slots published together as one attachment cohort.
(session retired-values)Adopt a pre-renderer SESSION's discarded slot values into one owner. RETIRED-VALUES is the property list supplied by UPDATE-INSTANCE-FOR-REDEFINED-CLASS. Unbound old slots remain unbound in the new renderer; owned resource and frame-state collections retain identity.
A MuPDF context: its allocator, its store, and its error stack.
How the children of a list are arranged.
(command offsets)Copy COMMAND with its dense-stream range rebased by OFFSETS. Commands without dense-stream ranges may return themselves. There is no default method: every semantic command family must state its compose rule.
(command mirror frame-build)Prepare semantic COMMAND for MIRROR within FRAME-BUILD. Return the command to encode, or NIL when it produces no draw. There is no default method so adding a semantic command requires an explicit preparation decision.
(command pass frame-state)Encode one prepared COMMAND into PASS using destination FRAME-STATE. Dispatch is at command grain. Vertex and texture payloads remain in the dense buffers owned by FRAME-STATE.
(target specification)Lower SPECIFICATION for TARGET without changing its source graph. TARGET participates in ordinary CLOS dispatch so each backend can own its structured product and source provenance. #JDLQPN
The Metal language policy selected for one direct shader lowering.
(solver state region)Settle STATE's dirty cells, departures, and arrivals over candidate REGION with the explicitly implemented incremental relighter named by SOLVER. There is deliberately no default method: unsupported names signal rather than falling back to a different algorithm. #K3WRD3
The portable command structs and double dispatch through encode and enqueue are a real architectural seam. Command kinds are separated by scope: queue, command encoder, render pass, and compute pass. Convenience functions such as draw and prepare-texture merely construct those command objects. Unsupported…
The mathematical shader language already owns backend-independent meaning: stages, interfaces, resources, typed expressions, quantity annotations, lexical order, and source provenance. Its current compiler immediately turns that graph into SPIR-V IDs, decorations, storage classes, instructions, and a spir-v-module. …
The luv system's modules now make the intended boundaries literal without turning every implementation layer into a public ASDF system: hal/gpu.lisp contains no Vulkan or Metal calls. It names providers, devices, queues, resources, descriptors, command structs, encoders, passes, finish, submit, submitted-work-done,…
This page takes an affirmative position: frontier-shaped computation is a real organizing principle for luvcraft, and Common Lisp gives us unusually good means to make that principle executable. The intention is not merely to extract a queue utility from lighting. It is to grow a small frontier language in which a…
Intent: turn the manually staged frontier-light proof into the smallest real compiler path described by #53Q1II. Reify the local transfer, join, admission, and priority arithmetic with checked field meanings; make the program definition drive a scalar execution plan; and emit one closed Lisp loop over bound raw…
Intent: state removal and incremental reconsideration in the same compiled vocabulary after the monotone kernel establishes its source and lowering boundary. Evidence: #ZEENY3. voxel-light-removal is an :invalidation program whose compiled drain reads like unlight-light-region but is emitted from the family and the…
luvcraft-session has about one hundred slots spanning world production, simulation, input, GPU attachments, layouts, pipelines, video, text, overlays, and frame state. luvcraft:start-luvcraft is 623 lines. During startup a GPU object can simultaneously be a lexical binding, a named session slot, an element of…
A running game is valuable state. Moving the ownership boundary must not require abandoning it or leave its old GPU objects unreachable. See #V9VH79.