Context
With :phosphor-lumos established (#29), this ticket introduces the renderer abstraction and frame data type that downstream consumers (Socket on Compose Multiplatform, eventually AMPERE CLI in Wave 2) will bind to.
Phosphor's existing renderer abstraction is PhosphorRenderer<T> (phosphor-core/renderer/PhosphorRenderer.kt:6-11), which consumes a 2D cell-based SimulationFrame. That's the right abstraction for terminal/Compose Canvas surfaces that draw character grids — but it's the wrong unit for voxel rendering. A voxel orb is a 3D lattice of discrete cubes with per-voxel color, scale, and visibility, not a 2D cell grid.
Rather than extend PhosphorRenderer<T> with a second frame type (which would force two associated types onto every existing implementation), this ticket introduces a parallel renderer abstraction: LumosRenderer<T> consumes VoxelFrame and produces a target-specific output. Both abstractions coexist; the Wave 0 recon's "thin adapter pattern" (docs/COGNITIVE_SCENE_RUNTIME_MIGRATION.md) maps naturally onto either.
The frame DTO follows the precedent set by ComposeRenderer.kt:10-52: a framework-free data class that consuming apps map to their own draw primitives. Socket will map VoxelFrame to Compose Multiplatform 3D primitives (Three.js on web, native Compose Canvas on Android/iOS) in a follow-up Socket-side ticket — not in this Phosphor ticket.
Objective
Add the LumosRenderer<T> interface and the VoxelFrame data class to :phosphor-lumos. Add a LumosRenderTarget enum to distinguish target surfaces. Establish the public API surface; the frame builder that produces VoxelFrame from atmosphere + voxel sphere + transition is PHO-17, not this ticket.
Expected outcomes
A new file phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderTarget.kt:
/**
* Identifies a target surface for [LumosRenderer] output. Sibling to
* [link.socket.phosphor.renderer.RenderTarget]; the two enums are
* intentionally separate because the Lumos voxel rendering pipeline and
* the Phosphor cell-based pipeline produce different frame shapes.
*/
enum class LumosRenderTarget {
/** Native 3D voxel rendering via a host-provided 3D library (Three.js, Compose Canvas + custom voxel impl, etc.). */
VOXEL_NATIVE,
/** ANSI-projected voxel orb for terminal output. Reserved for Wave 2; declared now so consumers can match on the enum exhaustively. */
VOXEL_TERMINAL,
}
A new file phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/VoxelFrame.kt:
@Serializable
data class VoxelFrame(
val tick: Long,
val timestampEpochMillis: Long,
/** Resolution at which this frame's voxel lattice was constructed. */
val resolution: Int,
/** Per-voxel render data, ordered identically to VoxelSphere.voxels. */
val cells: List<VoxelCell>,
/** Per-frame derived parameters useful to renderers: glow intensity,
* overall rotation, atmospheric color (for halo/glow effects). */
val ambient: VoxelAmbient,
/** Active glyph state, if any. Null when no glyph is being rendered.
* Populated by PHO-18; included in the DTO now so the shape is stable. */
val glyph: VoxelGlyphState? = null,
)
@Serializable
data class VoxelCell(
/** Voxel position in lattice space, post-noise post-bump post-pulse. */
val x: Float,
val y: Float,
val z: Float,
/** Scale multiplier applied to the voxel cube. 1.0 = full size, 0.0 = invisible.
* Combines voxel-gap, breath pulse, bipolar boundary thinning, and glyph carving. */
val scale: Float,
/** Final rendered color in sRGB, 0..1 per channel. */
val red: Float,
val green: Float,
val blue: Float,
/** Optional alpha; null means "use 1.0". Reserved for future fade effects. */
val alpha: Float? = null,
)
@Serializable
data class VoxelAmbient(
/** Atmospheric glow color for a halo/backdrop rendered behind the orb.
* Computed as an OKLab-mid of primary and secondary atmosphere hues. */
val glowRed: Float,
val glowGreen: Float,
val glowBlue: Float,
/** Glow intensity multiplier, drawn from AtmosphereState.glow. */
val glowIntensity: Float,
/** Continuous orb rotation in radians (Euler XYZ), applied uniformly to all voxels. */
val orbRotationX: Float,
val orbRotationY: Float,
val orbRotationZ: Float,
)
@Serializable
data class VoxelGlyphState(
/** Identifier of the active glyph (see PHO-18 LumosGlyph enum). */
val glyphName: String,
/** Glyph rendering progress, 0..1. Renderers may use this to fade glyph
* voxels in/out across the glyph's display window. */
val progress: Float,
/** Color in sRGB for glyph-member voxels, distinct from base voxel color. */
val red: Float,
val green: Float,
val blue: Float,
)
A new file phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderer.kt:
/**
* Renders a [VoxelFrame] into a target-specific output type [T].
*
* Sibling to [link.socket.phosphor.renderer.PhosphorRenderer]; both abstractions
* coexist because the Lumos voxel pipeline and the Phosphor cell-based pipeline
* produce different geometric primitives. Consumers binding to Lumos do not need
* to interact with [link.socket.phosphor.renderer.PhosphorRenderer], and vice versa.
*
* Implementations are framework-free in the sense that they should not depend on
* a specific UI toolkit. Concrete output types (Compose draw commands, JSON for
* web bridge, ANSI text in Wave 2) live in consuming modules or platforms.
*/
interface LumosRenderer<out T> {
val target: LumosRenderTarget
val preferredFps: Int
fun render(frame: VoxelFrame): T
}
Unit tests in phosphor-lumos/src/commonTest/kotlin/link/socket/phosphor/lumos/:
VoxelFrameTest.kt — VoxelFrame round-trips correctly via kotlinx-serialization. Equality respects all fields. Empty cells is valid (degenerate frame).
LumosRenderTargetTest.kt — enum contains exactly the documented values; valueOf resolves them.
No concrete LumosRenderer<T> implementations in this ticket — those arrive in consuming modules or in PHO-17 if the agent finds a natural reference implementation belongs in Phosphor.
Technical constraints
- KMP common code only, no platform-specific dependencies.
- All public types
@Serializable — matches the convention from SimulationFrame and Wave 0 atmosphere types. This is what lets the frame cross WebView/JS boundaries cleanly when Socket eventually integrates.
- File locations:
phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderTarget.kt
phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/VoxelFrame.kt
phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderer.kt
- KDoc on each public type. Explicit cross-references to the sibling
PhosphorRenderer and SimulationFrame types in phosphor-core so future readers understand the two parallel pipelines.
- No anthropomorphizing language anywhere. No "mood", "feels", "wants". Lumos is referred to with the pronoun "it".
- The
VoxelGlyphState field on VoxelFrame is declared with a null default so frames produced before PHO-18 lands are valid.
- Pure-core principle inherits to Lumos: no UI framework dependencies in
:phosphor-lumos either.
- Run
./gradlew :phosphor-lumos:jvmTest and ./gradlew ktlintFormat before completion. iOS compile gate (CI-enforced).
API surface verification
Before implementing, verify against the current code:
kotlinx-serialization-core is already a dependency of :phosphor-core (per phosphor-core/build.gradle.kts:53-55). Confirm it's reachable through the implementation(project(":phosphor-core")) declaration in :phosphor-lumos or add it explicitly.
- The
@Serializable annotation requires either a serializer plugin or explicit KSerializer<T> declarations. Match whatever pattern phosphor-core/src/commonMain/.../signal/AtmosphereState.kt uses — likely the kotlinx serialization Gradle plugin is already applied at root.
Out of scope
- The
VoxelFrameBuilder that produces VoxelFrame from AtmosphereState + AtmosphereTransition + VoxelSphere — that's PHO-17. This ticket defines the DTO; PHO-17 fills it.
- Glyph computation, glyph shape predicates, glyph queueing — PHO-18.
- Any concrete
LumosRenderer<T> implementation — Socket-side or platform-specific.
- CLI projection (
VOXEL_TERMINAL target) — Wave 2; the enum value is declared now for forward-compat but has no implementation.
- Changes to
PhosphorRenderer<T>, SimulationFrame, RendererRegistry, or any phosphor-core types.
Verification
After this ticket:
./gradlew :phosphor-lumos:build succeeds.
- The three new files exist and compile in
commonMain.
./gradlew :phosphor-lumos:allTests includes the new serialization and enum tests, all passing.
- No types or methods exist for actually producing a
VoxelFrame — that's by design; PHO-17 adds the builder.
Context
With
:phosphor-lumosestablished (#29), this ticket introduces the renderer abstraction and frame data type that downstream consumers (Socket on Compose Multiplatform, eventually AMPERE CLI in Wave 2) will bind to.Phosphor's existing renderer abstraction is
PhosphorRenderer<T>(phosphor-core/renderer/PhosphorRenderer.kt:6-11), which consumes a 2D cell-basedSimulationFrame. That's the right abstraction for terminal/Compose Canvas surfaces that draw character grids — but it's the wrong unit for voxel rendering. A voxel orb is a 3D lattice of discrete cubes with per-voxel color, scale, and visibility, not a 2D cell grid.Rather than extend
PhosphorRenderer<T>with a second frame type (which would force two associated types onto every existing implementation), this ticket introduces a parallel renderer abstraction:LumosRenderer<T>consumesVoxelFrameand produces a target-specific output. Both abstractions coexist; the Wave 0 recon's "thin adapter pattern" (docs/COGNITIVE_SCENE_RUNTIME_MIGRATION.md) maps naturally onto either.The frame DTO follows the precedent set by
ComposeRenderer.kt:10-52: a framework-free data class that consuming apps map to their own draw primitives. Socket will mapVoxelFrameto Compose Multiplatform 3D primitives (Three.js on web, native Compose Canvas on Android/iOS) in a follow-up Socket-side ticket — not in this Phosphor ticket.Objective
Add the
LumosRenderer<T>interface and theVoxelFramedata class to:phosphor-lumos. Add aLumosRenderTargetenum to distinguish target surfaces. Establish the public API surface; the frame builder that producesVoxelFramefrom atmosphere + voxel sphere + transition is PHO-17, not this ticket.Expected outcomes
A new file
phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderTarget.kt:A new file
phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/VoxelFrame.kt:A new file
phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderer.kt:Unit tests in
phosphor-lumos/src/commonTest/kotlin/link/socket/phosphor/lumos/:VoxelFrameTest.kt—VoxelFrameround-trips correctly viakotlinx-serialization. Equality respects all fields. Emptycellsis valid (degenerate frame).LumosRenderTargetTest.kt— enum contains exactly the documented values;valueOfresolves them.No concrete
LumosRenderer<T>implementations in this ticket — those arrive in consuming modules or in PHO-17 if the agent finds a natural reference implementation belongs in Phosphor.Technical constraints
@Serializable— matches the convention fromSimulationFrameand Wave 0 atmosphere types. This is what lets the frame cross WebView/JS boundaries cleanly when Socket eventually integrates.phosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderTarget.ktphosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/VoxelFrame.ktphosphor-lumos/src/commonMain/kotlin/link/socket/phosphor/lumos/LumosRenderer.ktPhosphorRendererandSimulationFrametypes inphosphor-coreso future readers understand the two parallel pipelines.VoxelGlyphStatefield onVoxelFrameis declared with anulldefault so frames produced before PHO-18 lands are valid.:phosphor-lumoseither../gradlew :phosphor-lumos:jvmTestand./gradlew ktlintFormatbefore completion. iOS compile gate (CI-enforced).API surface verification
Before implementing, verify against the current code:
kotlinx-serialization-coreis already a dependency of:phosphor-core(perphosphor-core/build.gradle.kts:53-55). Confirm it's reachable through theimplementation(project(":phosphor-core"))declaration in:phosphor-lumosor add it explicitly.@Serializableannotation requires either a serializer plugin or explicitKSerializer<T>declarations. Match whatever patternphosphor-core/src/commonMain/.../signal/AtmosphereState.ktuses — likely the kotlinx serialization Gradle plugin is already applied at root.Out of scope
VoxelFrameBuilderthat producesVoxelFramefromAtmosphereState+AtmosphereTransition+VoxelSphere— that's PHO-17. This ticket defines the DTO; PHO-17 fills it.LumosRenderer<T>implementation — Socket-side or platform-specific.VOXEL_TERMINALtarget) — Wave 2; the enum value is declared now for forward-compat but has no implementation.PhosphorRenderer<T>,SimulationFrame,RendererRegistry, or anyphosphor-coretypes.Verification
After this ticket:
./gradlew :phosphor-lumos:buildsucceeds.commonMain../gradlew :phosphor-lumos:allTestsincludes the new serialization and enum tests, all passing.VoxelFrame— that's by design; PHO-17 adds the builder.