# Phase 6 Render Pipeline Situation Report

## Purpose

This report explains how the Phase 6 battle-mode render failure happened in the context of the larger Game Definition Layer (GDL) and declarative UI-system work. It is not a fix proposal. Its purpose is to make the chain of assumptions visible so the next design decision starts from the actual state of the system.

## Executive Summary

The failure was not just a bad `choice.lua` size or a missing style token. Those were symptoms. The deeper problem is that Phase 6 routed the new flow-tree runtime's battle `ScreenUI` into an existing render path that still treated any non-empty `ScreenUI` as a legacy menu/flow panel.

The GDL and new UI design both say the game owns UI structure, UI chrome, interaction semantics, and token names, while the engine compiles and renders generic `UINode` trees. The live render path still contains legacy host behavior that injects menu-specific chrome (`ui.menu.backdrop`, `ui.menu.focus`) outside authored UI content. Chunk 6 crossed that boundary without auditing the downstream path from `FrameOutput.ScreenUI` to render validation.

That is how the system reached a state where the new battle runtime correctly emitted a `ScreenUI`, but the engine host path overlaid legacy menu chrome on it, submitted a zero-height focus quad, and the renderer rejected the frame.

## Strategic Context

The GDL exists because the game had no declarative model of itself. The original GDL design identifies that UI was built imperatively by Go producers that directly constructed `UINode` trees and read ECS state ([docs/game_definition_layer.md:41](docs/game_definition_layer.md:41)). It also identifies that the engine was doing the game's UI job by defining producer interfaces, calling game code to build UI, and making routing decisions that should belong to the game ([docs/game_definition_layer.md:63](docs/game_definition_layer.md:63)).

The intended replacement is a pipeline of definitions, derived context, builders/components, and engine primitives ([docs/game_definition_layer.md:84](docs/game_definition_layer.md:84)). In the later UI requirements, that becomes a clearer runtime pipeline: definitions plus ECS/session state produce derived context; schemas/components produce `UINode` trees; the engine compiles and renders those trees ([docs/ui_system_requirements.md:223](docs/ui_system_requirements.md:223)).

The ownership split is explicit:

- The engine is a rendering and spatial-query service. It does not build game UI or decide what input means; it receives `UINode` trees, compiles them, renders them, and provides focus/hit-test data back to the game ([docs/ui_system_requirements.md:203](docs/ui_system_requirements.md:203)).
- The game owns what UI exists, how it looks, and the style token catalog; the engine owns compiling and rendering UI ([docs/ui_system_requirements.md:365](docs/ui_system_requirements.md:365)).
- Engine UI capability expansion was supposed to land capabilities in the generic compile/overlay/render pipeline, not in `engine/ui/host/runtime.go` ([docs/ui_engine_ui_expandsion.md:74](docs/ui_engine_ui_expandsion.md:74)).

The render failure occurred exactly at the mismatch between that target architecture and the still-live legacy host path.

## Phase Context

Phase 4 established Lua component evaluation and primitive-tree translation. Phase 5 established definitions and derivation infrastructure. Phase 6 was then scoped to stand up the flow-tree runtime: tree manager, interpreter, routing, policies, render path, and HandleFrame integration.

The Phase 6 spec says the phase should replace `game/menu/` and `game/flow/` ([spec_phase_6_flow_tree_runtime.md:35](spec_phase_6_flow_tree_runtime.md:35)). It also says Phase 6 delivers the runtime mechanism, render path, mode roots, battle proof, and transient proof ([spec_phase_6_flow_tree_runtime.md:44](spec_phase_6_flow_tree_runtime.md:44)). But it simultaneously states that Phase 6 does not include engine-side animation, layout, query, or text changes ([spec_phase_6_flow_tree_runtime.md:58](spec_phase_6_flow_tree_runtime.md:58), [spec_phase_6_flow_tree_runtime.md:65](spec_phase_6_flow_tree_runtime.md:65)).

That split was the critical scoping mistake. The runtime was allowed to reach production for battle mode, but the downstream engine UI host/presentation/render path was treated as already compatible because "UI compilation pipeline" existed as prior infrastructure ([docs/ui_system_prereqs.md:650](docs/ui_system_prereqs.md:650)). Existence was mistaken for compatibility with the new declarative UI contract.

## Immediate Failure Chain

1. Chunk 6 enabled the tree runtime for battle mode.

   In the live boot path, `mode.ModeBattle` is mapped to `battle_root`, and `useTreeRuntime[mode.ModeBattle]` is set to `true` ([game/run.go:849](game/run.go:849)). Title and overworld remain on the legacy path.

2. `Module.HandleFrame` delegates to the tree runtime whenever an active runtime exists.

   The chunk 6 entry point says that when an active tree runtime is configured for the current mode, it delegates to `runtime.Runtime.HandleFrame`; otherwise it falls through to `legacyHandleFrame` ([game/frame_handler.go:45](game/frame_handler.go:45)). The legacy path is explicitly marked as pre-chunk-6 menu/flow behavior ([game/frame_handler.go:52](game/frame_handler.go:52)).

3. `FrameRunner` compiles the returned `ScreenUI` generically, but the stage frame runner marks any non-empty `ScreenUI` as panel/menu-active.

   `stage_frame_runner.go` passes `PanelActive: nodeIsNonEmpty(runOutput.ScreenUI)` into `BuildOverlayFromCompiled` ([engine/runtime/stage_frame_runner.go:260](engine/runtime/stage_frame_runner.go:260)).

4. `BuildOverlayFromCompiled` routes any non-empty compiled screen tree into `buildMenuDrawOps`.

   `OverlayInput.PanelActive` is documented as "true if menu or flow is active" ([engine/ui/host/overlay_builder.go:24](engine/ui/host/overlay_builder.go:24)), but the caller now derives it from generic non-empty `ScreenUI`. `BuildOverlayFromCompiled` calls `buildMenuDrawOps` for that input ([engine/ui/host/overlay_builder.go:67](engine/ui/host/overlay_builder.go:67), [engine/ui/host/overlay_builder.go:73](engine/ui/host/overlay_builder.go:73)).

5. `buildMenuDrawOps` injects legacy menu chrome.

   The function emits a full-root quad with hardcoded `ui.menu.backdrop` ([engine/ui/host/runtime.go:38](engine/ui/host/runtime.go:38)) and a focus-highlight quad with hardcoded `ui.menu.focus` ([engine/ui/host/runtime.go:56](engine/ui/host/runtime.go:56)). Those visuals are not authored by the Phase 6 battle UI; they are injected by host code based on the legacy assumption that non-empty `ScreenUI` means menu/flow panel.

6. The focused selectable can compile to a zero-height rect.

   The screen compiler permits zero-size rectangles; `validateRect` only rejects negative width/height ([engine/ui/compile/screen/compiler.go:1436](engine/ui/compile/screen/compiler.go:1436)). `SizeAuto` can resolve through intrinsic layout paths that return zero where no explicit or measured intrinsic size exists ([engine/ui/compile/screen/compiler.go:638](engine/ui/compile/screen/compiler.go:638), [engine/ui/compile/screen/compiler.go:660](engine/ui/compile/screen/compiler.go:660), [engine/ui/compile/screen/compiler.go:1027](engine/ui/compile/screen/compiler.go:1027)). The chunk 6 `choice.lua` selectables do not author explicit dimensions.

7. The renderer rejects the submitted draw item.

   The renderer validates final `FrameInput` draw items and rejects any item whose destination width or height is non-positive ([render/frame.go:121](render/frame.go:121)). The observed production error was `render: layer 2 item 18 dst rect must be positive`, matching an overlay-layer draw item with zero height.

## Why The Earlier Token Bug Was A Warning

Before the zero-height focus quad failure, battle mode also hit missing `ui.body` and `ui.text` tokens. The Phase 6 spec now documents that chunk 6 introduced Lua-canonical token consumers while the production boot path still sourced `StyleContext` from JSON theme/contract files ([spec_phase_6_flow_tree_runtime.md:10494](spec_phase_6_flow_tree_runtime.md:10494)). A tactical JSON patch added the missing tokens.

That was the first sign that the downstream render path had not been audited against the new UI output. The patch fixed the immediate missing-token symptom but did not examine the whole `ScreenUI -> compile -> overlay -> presentation -> render` path. The zero-height focus quad exposed the same underlying gap one stage later.

## What Was Missed

The missed boundary was not just "rendering." It was the exact seam where a game-authored `UINode` tree becomes pixels:

`FrameOutput.ScreenUI -> FrameRunner compile -> BuildOverlayFromCompiled -> buildMenuDrawOps/collectors -> submit.Overlay -> ApplyOverlayFromSubmit -> presentation.RenderFrame -> render.FrameInput validation -> renderer`.

The Phase 6 reviews focused heavily on the new game-side runtime: schemas, tree state, derivation, component evaluation, interpreter directives, focus routing, policies, and mode integration. Those were legitimate hard problems. But the reviews did not give equal scrutiny to the already-existing downstream UI host/presentation/render path. The old path was treated as a neutral engine renderer when it still contained game/menu-specific behavior.

The GDL and UI docs explicitly warned that the engine should not build game UI. The live host path still did.

## Why Tests Did Not Catch It

The chunk 6 tests validated that `HandleFrame` and the tree runtime returned `UINode` trees. They did not run the full downstream render path. In particular, they did not prove that the battle runtime's `ScreenUI` could:

- compile through `engine/ui/compile/screen.Compiler.Compile`,
- build overlay ops through `engine/ui/host.BuildOverlayFromCompiled`,
- resolve every token through the production `StyleContext`,
- apply to `presentation.RenderFrame`,
- compile to `render.FrameInput`, and
- pass render validation.

The tests therefore proved the game-side runtime shape but not production renderability. That is why the first failures appeared only when running the actual game.

## Main Contributing Factors

### 1. Existing infrastructure was mistaken for compatible infrastructure

The prereqs document listed the UI compilation pipeline as already built ([docs/ui_system_prereqs.md:650](docs/ui_system_prereqs.md:650)). That was true in a narrow sense. But the pipeline had been built around legacy menu/flow assumptions, including host-injected menu chrome. It was not audited as a generic authored-UI renderer.

### 2. Phase 6 crossed an engine boundary while declaring engine rendering changes out of scope

Chunk 6 routed new UI output into production, but the Phase 6 scope still said no engine-side layout/query/text changes ([spec_phase_6_flow_tree_runtime.md:65](spec_phase_6_flow_tree_runtime.md:65)). That made it too easy to assume the render side did not need design attention.

### 3. The old and new UI paths were temporarily kept side-by-side

The spec describes a staged cutover where old menu/flow paths continue while the tree runtime matures ([spec_phase_6_flow_tree_runtime.md:296](spec_phase_6_flow_tree_runtime.md:296)). Chunk 8 is supposed to remove the side-by-side arrangement ([spec_phase_6_flow_tree_runtime.md:314](spec_phase_6_flow_tree_runtime.md:314), [spec_phase_6_flow_tree_runtime.md:322](spec_phase_6_flow_tree_runtime.md:322)). In practice, the side-by-side period hid the fact that shared host rendering code still encoded legacy assumptions.

### 4. "ScreenUI" had two incompatible meanings

In the target design, `ScreenUI` means generic screen-space authored UI. In the existing host path, non-empty `ScreenUI` still effectively meant "menu or flow panel is active." That mismatch is visible in `OverlayInput.PanelActive` and its caller: the field says menu/flow active ([engine/ui/host/overlay_builder.go:24](engine/ui/host/overlay_builder.go:24)), but chunk 6 passes non-empty generic `ScreenUI` as the condition ([engine/runtime/stage_frame_runner.go:260](engine/runtime/stage_frame_runner.go:260)).

### 5. Primitive/layout contracts were under-specified at the renderability boundary

`choice.lua` emits selectables with labels and actions but no explicit dimensions. That may be an authoring miss, a layout/compiler gap, or a missing primitive contract. The current compiler allows zero-height focusable geometry to reach the overlay stage, while the renderer requires positive dimensions for submitted draw items. That invariant mismatch was not captured in Phase 6 acceptance tests.

### 6. Reviews were too local to the chunk under review

The review process was rigorous inside each chunk's immediate scope, but it did not force end-to-end validation across subsystem boundaries after Chunk 6 changed production routing. The render side should have been audited as soon as battle mode was routed through the new runtime.

## What This Says About The Architecture, Not Just The Bug

The target architecture is still coherent. The GDL direction remains sound: definitions and derivations shape data, components/schemas author UI, the flow runtime owns interaction, and the engine compiles/renders generic primitives.

The current codebase is partway through that migration. The failure happened because a new game-owned UI runtime was connected to an old host path that still contains game/menu-specific rendering behavior. The solution space should therefore be judged against the target architecture, not against preserving the temporary compatibility layer.

The key architectural correction is that legacy host chrome cannot remain an implicit side effect of non-empty `ScreenUI`. Either chrome is authored as UI content, or the render pipeline exposes generic primitives that authored content can use. It should not be injected by a menu-specific host function that does not know whether the tree is a battle HUD, title menu, pause menu, flow prompt, or toast.

## Current State

- Battle mode uses the tree runtime in production ([game/run.go:849](game/run.go:849)).
- Title and overworld still fall through to the legacy path unless/until their tree runtimes are enabled ([game/frame_handler.go:45](game/frame_handler.go:45)).
- `buildMenuDrawOps` still emits hardcoded menu backdrop and focus quads ([engine/ui/host/runtime.go:38](engine/ui/host/runtime.go:38), [engine/ui/host/runtime.go:56](engine/ui/host/runtime.go:56)).
- Renderer-side temporary debug instrumentation is present in `render/frame.go` around the positive-destination-rect validation ([render/frame.go:121](render/frame.go:121)).
- The dedicated render-pipeline audit prompt now correctly frames the next audit as deletion/generalization of legacy assumptions and explicit authoring/primitive contracts, not as preserving a dual legacy/new path ([prompt_audit_phase6_render_pipeline.md:90](prompt_audit_phase6_render_pipeline.md:90)).

## Lessons

1. A subsystem boundary is not proven until the real downstream consumer is exercised.

   For UI runtime work, returning a `UINode` tree is not enough. The acceptance path must include compile, overlay build, style/token resolution, presentation, and render validation.

2. "Already built" infrastructure still needs contract audit when its producer changes.

   The screen compiler and overlay path existed, but their implicit input contract was legacy menu/flow shaped. Chunk 6 changed the producer without revalidating the consumer contract.

3. Temporary dual paths create hidden semantic overload.

   Keeping old and new UI paths alive made it easy for `ScreenUI` to retain legacy meaning in some places while gaining target-architecture meaning elsewhere.

4. Hardcoded style tokens in engine/host code are architectural coupling.

   `ui.menu.backdrop` and `ui.menu.focus` in host rendering are not just implementation details. They encode game-specific chrome outside authored UI and outside the game-owned token catalog contract.

5. Primitive contracts need renderability invariants.

   Focusable/hit-testable/drawable output must not silently compile into invalid render items. Whether this is enforced by primitive defaults, compiler validation, authored dimensions, or some combination, the invariant has to be explicit.

## Bottom Line

We got here because Phase 6 correctly attacked the game-side runtime problem but did not audit the downstream rendering pipeline that would consume its output. The GDL/new UI service work changed the meaning of `ScreenUI` from "legacy menu/flow panel" to "generic authored UI tree." The existing host render path had not made that transition. It still injected menu chrome and relied on legacy shape assumptions.

The next step should be the render-pipeline coupling audit already drafted. That audit should identify every legacy assumption that must be deleted or generalized, and every authored UI/component/primitive contract that must become explicit, before further Chunk 7/8 work proceeds.
