Skip to content

Awake Compose (:awake:compose)

A retained, single-pass UI engine with a Compose-shaped API, built specifically for a game loop that redraws every frame behind a live 3D scene.


Architectural Philosophy

Awake Compose delivers the ergonomic familiarity of declarative Jetpack/Multiplatform Compose while answering to the performance constraints of real-time game engines:

  • Game-Loop Native: Runs within a hard 16.67 ms (or 8.33 ms) frame budget shared with simulation, rendering, and physics.
  • Zero Steady-State Allocations: Layout measurement (Constraints), draw command emission, modifier chains, and pointer dispatching avoid heap allocations during normal 60/120 fps execution.
  • Compiler-Plugin Free: Calling conventions use Kotlin context parameters (context(_: Composer)). This eliminates version-locked compiler plugin artifacts across Kotlin Multiplatform targets (Desktop JVM, Android, WasmJs, iOS).
  • Synchronous & Predictable: Runs a single-threaded synchronous frame pump (ComposeHost.frame) without multiversion concurrency control, asynchronous snapshot dispatchers, or frame lags. Nothing skips and nothing is observed; a value that must change between frames is read in the phase that consumes it -- measure or draw -- rather than captured at composition.
  • Shared Input Ownership: Input is shared with gameplay systems. Every frame reports UI claims (isCaptured, isTextInputFocused, isScrollConsumed) rather than assuming the UI owns the device.

Module Hierarchy

graph TD
    A[":awake:compose:runtime"] --> B[":awake:compose:ui"]
    B --> C[":awake:compose:foundation"]
    B --> D[":awake:compose:ui-testing"]

1. :awake:compose:runtime

The core composition and reconciliation engine. - Composer & Applier: Retained node hierarchy construction, positional slot identity, and reconciled tree mutations. - remember: Value caching across frames keyed on identity or explicit state parameters. - CompositionLocal: Ambient dependency propagation down the composition tree (LocalDensity, LocalLayoutDirection, LocalViewportSize).

2. :awake:compose:ui

The retained node layout, modifier, drawing, and input infrastructure. - LayoutNode: Retained layout tree element maintaining measured bounds, absolute positions, and modifier node chains. - Constraints & MeasurePolicy: Packed 64-bit zero-allocation layout constraints with intrinsic measurement support. - Modifier & Modifier.Node: Chain composition (then, foldIn) with segregated node lifecycle (ModifierNodeElement, LayoutModifierNode, DrawModifierNode, PointerInputNode, FocusTargetNode, SemanticsModifierNode). - Painter & DrawScope: Node-local coordinate painting emitting backend-neutral UiDrawPrimitive lists, offscreen graphicsLayer frames, drawWithCache, and zIndex sibling sorting. - Input & Focus: Multi-pass pointer hit-testing (Initial, Main, Final), multi-touch contact tracking, 1D tab ring and 2D spatial focus beam search.

3. :awake:compose:foundation

Standard UI layout primitives, text, styling, and gestures. - Layouts: Row, Column, Box, Spacer, SubcomposeLayout, BoxWithConstraints, LazyColumn, LazyRow. - Text & Input: Text, BasicTextField with caret tracking, text selection, and IME composition bridges. - Styling: Modifier.styleable, background, border (with partial edge support), clip, alpha, and state rules (hovered, pressed, focused, disabled, selected, checked). - Gestures & Scrolling: clickable, draggable, scrollable, verticalScroll, horizontalScroll, transformable, and nestedScroll.

4. :awake:compose:ui-testing

Headless testing harnesses and validation utilities. - ComposeHost: Headless test driver for executing layout frames, simulating pointer events, and verifying emitted draw primitives. - Cross-Engine Differ: Field-level primitive diffing against legacy pipelines to prevent visual drift.


Common Patterns

1. Composing a Basic UI

context(Composer)
fun UserProfile(username: String, avatarUrl: String) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .background(Color(0.1f, 0.1f, 0.1f, 1f), RoundedCornerShape(8.dp)),
        horizontalArrangement = Arrangement.spacedBy(12.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Box(Modifier.size(40.dp).background(Color(0.3f, 0.3f, 0.3f, 1f), CircleShape))
        Text(text = username, color = Color.White)
    }
}

2. State Across Frames

// A plain class with plain vars. There is no observable state and no skipping: the whole tree
// composes every frame, so a value only has to survive one -- which is what `remember` does.
private class CounterState { var count = 0 }

context(Composer)
fun Counter() {
    val state = remember { CounterState() }

    Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
        Text(text = "Count: ${state.count}")
        Button(onClick = { state.count++ }) {
            Text("+")
        }
    }
}

3. Custom Drawing with Cache

context(Composer)
fun CustomBadge(color: Color) {
    Box(
        modifier = Modifier
            .size(64.dp)
            .drawWithCache {
                // Computed once upon size/density change, not every 60fps frame
                val path = drawPath {
                    moveTo(0f, 0f)
                    lineTo(size.width, size.height / 2f)
                    lineTo(0f, size.height)
                    close()
                }
                onDrawBehind {
                    drawPath(path, color)
                }
            }
    )
}

4. Sibling Z-Index Ordering

context(Composer)
fun OverlappingLayers() {
    Box(Modifier.size(100.dp)) {
        // Declared first, but drawn on top and hit-tested first due to higher zIndex
        Box(Modifier.size(80.dp).zIndex(1f).background(Color.Red))
        // Declared second, but drawn underneath
        Box(Modifier.size(100.dp).zIndex(0f).background(Color.Blue))
    }
}

Detailed Specifications

For deep-dive architectural decisions, lifecycle rules, and parity ledgers, refer to: