# Meta Quest spatial roadmap

Status: Q1/Q3 physically usable; Q4 radar functionally accepted and navigable; Q7 detached production console implemented; Q9 selected-object console implemented; Q11 faction emblem implemented; physical acceptance remains open
Last updated: 2026-08-22

## Objective

The existing Meta Quest build remains a complete, conventional Android 2D
application. The Quest-only `questSpatial` variant now adds an optional spatial
experience around the same engine, imported game data, saves, settings,
multiplayer protocol, and bilingual user interface.

The first spatial release is a mixed-reality command table:

- passthrough is the default background;
- the whole 16:10 game is shown on a sharp, movable spatial panel;
- hands and Touch Plus controllers can operate that panel;
- Android import, save-transfer, keyboard, and permission screens remain
  ordinary system panels where that is the most reliable presentation;
- the user can always return to the proven flat Quest mode;
- no spatial feature may change simulation, multiplayer packets, fog of war,
  campaign data, or save compatibility.

This is deliberately not a promise to turn the 1995 game into a complete 3D
battlefield in the first milestone. The staged design proves comfort, input,
performance, and lifecycle before adding decorative or informational 3D.

## Q1 implementation record — 2026-08-20

Q0 flavor-isolation baseline and the Q1 interactive-panel milestone are
implemented in the existing `:app` module. The
unmodified flat APK was recorded before the flavor edit and archived outside
the repository as `Tiberian-Dawn-Android-flat-baseline-2026-08-20.apk`.
The text-only record is
[`docs/QUEST-SPATIAL-FLAT-BASELINE-2026-08-20.md`](QUEST-SPATIAL-FLAT-BASELINE-2026-08-20.md).
No proprietary ISO, imported game file, save, or other user data was copied
into the repository or the archive.

The app now has one `device` flavor dimension:

| Artifact | Result |
| --- | --- |
| `mobileDebug` | `arm64-v8a` + `x86_64`, conventional flat launcher; no Horizon/Meta/OpenXR runtime |
| `questSpatialDebug` | `arm64-v8a`, `QuestSpatialActivity`, Horizon/VR/passthrough manifest and Meta Spatial SDK |

Both variants remain `org.tiberiandawn.android`, version `202` /
`0.2.2-android-dev`, and use the same debug signing certificate (SHA-256
`3d0dae3634ea615e63ce60e6126b1ab7e354124c9d7188992385d9869d39ebd2`). They
therefore continue to address the same private files directory and update
path; no migration or data-copy code was added.

Meta's official add-to-existing-2D-app path and current official HybridSample
were checked on this date. The pinned versions are Spatial SDK `0.13.2`,
Gradle `9.4.1`, AGP `8.11.1`, Kotlin `2.1.0` in the reference sample, and NDK
`27.0.12077973` for this checkout. The Meta Gradle plugin is build tooling;
all Spatial runtime AARs are declared only as `questSpatialImplementation`.
The mobile variant consequently resolves neither the AARs nor their native
libraries.

`QuestSpatialActivity` is a Java `AppSystemActivity`/`VRFeature` shell. It
sets `LOCAL_FLOOR`, enables passthrough, creates the bilingual launcher, and
hosts the unchanged `TiberianDawnActivity` as one `ActivityPanel` game surface.
The game quad is `1.12m x 0.70m` at the seated table pose and owns a child
`TransformParent` toolbar for Back, reset, PAN, saves, and Flat mode. The
single `SdlEngineLease` prevents overlapping SDL owners; duplicate and
half-started activities remain inert, and partial panel failures destroy
children first before canceling or awaiting the exact generation. Import/data
manager return and the flat fallback remain available. This closes the Q1
source/build gate; physical interaction, sustained performance, thermal,
audio, and lifecycle acceptance remain open.

The first hardware run proved that a normal Android `AlertDialog` is not
composited into this immersive `AppSystemActivity`: passthrough appeared, but
the launcher did not. The implementation uses the official 0.13.2 native
`ViewPanelRegistration`/`UIPanelSettings` path (the native-View equivalent of
Meta's `ComposeViewPanelRegistration`) and, separately, spawns its panel
entity with `Entity.createPanelEntity`. Q1 keeps that launcher and adds the
interactive `ActivityPanel` game entity at the same `LOCAL_FLOOR(0,1.15,+1.4)`
pose with identity rotation, a `1.12m x 0.70m` quad, and a child toolbar
entity. Interaction SDK owns panel ray/hand input and grab/resize capture;
the toolbar stays outside the SDL surface so it does not cover classic
Options/sidebar controls. Toolbar teardown is child-first, and the hosted SDL
Activity is finished only after panel removal and exact lease release.
`onBackPressed` and legacy `KEYCODE_BACK` remain independent flat fallbacks.

### Q1 verification results

The following builds completed successfully with the installed Android Studio
JBR and the workstation's CMake `4.4.2` override:

```text
JAVA_HOME='/Applications/Android Studio.app/Contents/jbr/Contents/Home' \
./gradlew -PandroidCmakeVersion=4.4.2 clean :app:assembleQuestSpatialDebug
JAVA_HOME='/Applications/Android Studio.app/Contents/jbr/Contents/Home' \
./gradlew -PandroidCmakeVersion=4.4.2 :app:assembleMobileDebug
```

The first command is the clean Quest ARM64 build. The second regenerates the
mobile artifact after that clean (the clean removed both variant outputs).

| APK | Size | SHA-256 | ABI / manifest check |
| --- | ---: | --- | --- |
| `android/app/build/outputs/apk/mobile/debug/app-mobile-debug.apk` | 21,900,816 | `8dedbe789e5e5779be2564838dd83c65c3a7145e9392692f9c97a437a6e81a9d` | `arm64-v8a`, `x86_64`; launcher `GameDataImportActivity`; no Meta/OpenXR/Horizon/Oculus runtime |
| `android/app/build/outputs/apk/questSpatial/debug/app-questSpatial-debug.apk` | 77,135,168 | `a41fcd22e23f74d1dca5a455029ca71e1d8479dd3fa088c24322a84a698083ad` | `arm64-v8a`; launcher `QuestSpatialActivity`; Horizon min/target 69, passthrough metadata, Q1 ActivityPanel + TransformParent toolbar |

The mobile APK has no `libMetaSpatialSDK*`, `libopenxr_loader`, Horizon, Oculus,
or Spatial archive/runtime entry. A path, DEX-string, and binary-manifest scan
all returned empty for Meta/OpenXR/Horizon/Oculus/Aether runtime markers. Its
native hashes are unchanged from the recorded flat baseline:

```text
arm64-v8a/libmain.so  ecc9188e59a810c9b1e260535cd2cbe4bf5f99e24ce52b89833e127757e443d1
arm64-v8a/libSDL2.so   80cf5a631b8e86327289c7277ec3b10ec6b2c44238c966947cdcbb18df95e591
x86_64/libmain.so    e1d7ad1588bd8349be73fe9008d60ea827122273df05583e2ac376db126b2365
x86_64/libSDL2.so     8b61be7d2943134025078d14c26c682e0bd10b043934ec15070c18de0e4efdff
```

The Quest artifact contains only ARM64 plus the expected pinned runtime
libraries (`libMetaSpatialSDK*`, `libopenxr_loader`, `libAetherGlobals`, and
the Spatial shader assets). Both APK signatures verify with the same debug
certificate as the baseline.

On an ARM64 Android emulator, `adb install -r` of `mobileDebug` succeeded and launching
`GameDataImportActivity` reached `TiberianDawnActivity`. Before and after the
update there were 22 files under `files/TiberianDawn/vanillatd`, zero
`SAVEGAME.*` files, and one SharedPreferences file. The sentinels remained
unchanged: `CONQUER.MIX` inode `508235`, size `2,039,435`, mtime
`2026-08-17 12:48:02`; `gdi/GENERAL.MIX` inode `508205`, size `2,584,388`, the
same mtime; and `files/ModernArt/.asset-manifest.sha256` inode `114709`, size
`1,161`. No `pm clear` or uninstall was used.

The physical Quest 3 was subsequently authorized for USB debugging. An
`adb install -r` update of `questSpatialDebug` succeeded without uninstalling
or clearing the package. The cold `QuestSpatialActivity` launch completed in
975 ms, created an OpenXR 1.1 session, and the Quest runtime created and started
the passthrough feature and layer. The process stayed alive and no fatal Java,
native, or missing-library error was logged. The recorded `CONQUER.MIX`, GDI
`GENERAL.MIX`, and ModernArt marker inode, size, and modification time remained
unchanged after the update.

The follow-up `adb install -r` with the launcher panel and empirical front/size
fix preserved the package and data. OpenXR/passthrough initialized, the
activity registered its launcher entity at `(0, 1.15, +1.4)` with identity
rotation and a `1.15m x 0.75m`, `920 x 600dp` layout. That launcher placement
was readable and interactive. The first Flat transition exposed an SDK
teardown race: `onDestroy()` queried an entity after Spatial had already
invalidated its native DataModel. The implementation leaves scene teardown to
the SDK and only clears Java references. The Q1 source/build path now applies
the same safe boundary to the game/toolbar panel; fresh physical Q1 interaction
and sustained lifecycle checks remain open.

The final 77,135,168-byte Q1 artifact above was then update-installed on the
same Quest without uninstalling or clearing the package. The recorded
`CONQUER.MIX`, GDI `GENERAL.MIX`, and ModernArt marker inode, size, and mtime
remained unchanged; 23 imported files, zero `SAVEGAME.*` files, and two
SharedPreferences files were present before and after the update. Its idle
cold start created the OpenXR 1.1 session and passthrough layer, enabled the
ISDK grab/select arbiter, and created the `1.12m x 0.70m` game entity plus its
TransformParent toolbar without a fatal Java/native error. Because Horizon
paused the sleeping headset immediately afterwards, this is a startup/data
preservation result, not a visible Q1 interaction or performance pass.

### First worn-headset findings and corrective build

The first complete hand-input pass proved the spatial game panel, passthrough,
PAN, Back, resize, ModernArt, save/load, keyboard, and the basic game path.
Three Q1 defects remained: on one first start the visible unit and its hit test
were temporarily offset until an app restart; game audio was silent; and the
fixed toolbar offset was occluded when the game panel was enlarged.

The corrective build addresses the panel boundary rather than changing game
rules or touch sensitivity:

- Android input now compares the live SDL window and renderer-output sizes
  before mapping the first pointer event after a size generation. A pending
  ActivityPanel resize is applied immediately before hit testing.
- Android audio requests SDL's native AAudio backend first. This avoids the
  embedded Activity/volumetric-window token route that Horizon logged while
  producing silence; ordinary Android retains SDL's normal backend sequence as
  a fallback when AAudio initialization fails.
- The toolbar child offset is now `-0.62m`, derived from the declared maximum
  game-panel half-height, toolbar half-height, and a `4.5cm` hand-target gap.

Clean `questSpatialDebug` and `mobileDebug` builds passed for ARM64 and for the
mobile x86_64 ABI. The Quest corrective APK remains 77,135,168 bytes with
SHA-256 `08f9721243a1ea45ee2b324c4325fdf06e27213fb06d593089f969a1b75f4a1f`;
the mobile APK is 21,902,000 bytes with SHA-256
`8e1affb5d2daabdb63a93010c329cdc25b23726034d40427371309e6a56fe6d6`.
Both verify with APK Signature Scheme v2. The Quest update was installed with
`adb install -r`; all three recorded MIX/ModernArt inode, size, and timestamp
sentinels and the 23 imported files remained unchanged. The sleeping-headset
launch verified OpenXR/passthrough and the new `-0.62m` toolbar entity. Audible
AAudio output and the first-start/maximum-resize hand tests remain the required
worn-headset gate.

### Flat handoff hardening — 2026-08-20

The reported in-game **Flat** failure was a process-exit defect rather than a
normal scene transition. SDL posts `SDL_QUIT` when Horizon releases the hosted
ActivityPanel, and the legacy Android loop handled that event with `exit(0)`.
That terminated the complete Spatial process while Meta's global runtime was
still tearing down.

Android now converts `SDL_QUIT` into an atomic engine-quit request and lets the
game unwind normally; the Apple and desktop quit paths are unchanged. The
Spatial coordinator also handles Flat requests made while panel attachment is
still pending, retains a weak reference to the exact hosted activity, and has
a UI-timer fallback for the period in which Horizon stops scene-frame callbacks.
An on-device update test proved that the same process survives and resumes the
regular flat `TiberianDawnActivity` instead of aborting. A Flat transition from
an actively running mission remains a worn-headset acceptance check.

## Q3 comfort implementation record — 2026-08-20

The Quest-only comfort pass is implemented in the `questSpatial` source set
without changing the flat/mobile source set or the common engine:

- `QuestSpatialPreferences` stores a versioned, relative seated pose, uniform
  panel size, toolbar visibility mode, and first-run hint state. Position,
  rotation, and scale are read back from the live Spatial components at a low
  frequency and written only when they change. Values are sanitized to
  comfortable seated limits before restoring or saving them; no room anchor or
  camera image is stored.
- The game panel starts from the saved pose and size, while **Recenter**
  restores `(0, 1.15, +1.4)` with identity rotation and the proven default
  size. The panel remains bounded to approximately `0.67..1.30x` scale,
  `0.75..1.70m` height, and `0.65..2.20m` distance. Tilt is capped to a
  modest seated range if tracking returns an extreme quaternion.
- Back, Recenter, PAN, SAVES, Flat, and the compact **UI** button live in a child
  spatial toolbar below the game panel. The toolbar offset is calculated from
  the declared maximum resize height (`-0.62m`) so enlarging the game cannot
  cover Options or the classic sidebar. In **Automatic** mode the action group
  appears briefly after hand/controller input; **Always visible** and
  **Hidden (UI button remains)** are explicit bilingual overrides.
- The separate support panel contains toolbar settings, Recenter, and the
  short German/English hand/controller instructions. It is registered as its
  own, taller panel, parented `1.18m` to the left of the game, and yawed
  `12 degrees` toward the seated player (negative local Y rotation). This
  keeps all choices clear of the lower toolbar and avoids clipping while the
  panel continues to follow the user's game table.

Quest panel thumbsticks are handled below the game-mode boundary. Horizon's
fractional `ACTION_SCROLL` samples are accepted through both Android delivery
routes, retained across a delayed network frame, and applied immediately after
the SDL event poll. Campaign, skirmish, LAN, and relay multiplayer therefore
use the same map-pan implementation; PAN remains a separate hand-control mode.

The following physical gates remain open and are intentionally not claimed by
the source/build check alone: one-handed reachability in all three toolbar
modes; controller tremor versus click/drag and long-hold behavior; repeated
recenter, resize, rotation, and Home/resume cycles; bilingual accessibility
focus; and the required 20-minute functional plus 60-minute thermal run at a
stable 72 FPS. No Quest installation is performed by the Q3 source change.

## Q4 radar implementation record — 2026-08-20

The first split information panel is implemented as an optional, read-only
Quest panel. It captures only the 128x128 radar interior of the already
composed game ActivityPanel surface through Android `PixelCopy` at no more
than 10 Hz; the surrounding sidebar/title area is intentionally excluded.
It never reads cells, objects, remembered contacts, fog state, random state, or
network packets, so it cannot reveal anything that was not already visible to
the local player in the classic radar.

The producer permits only one asynchronous copy at a time and uses fixed full
frame and display buffers. The UI finishes copying the crop before the source
buffer is eligible for reuse, so the path neither blocks SDL nor allocates a
stream of per-frame bitmaps. When enabled, the spatial panel itself remains
visible and presents a clean localized `RADAR OFFLINE` state until the engine
reports an active in-game radar and two stable crops have been accepted. Only
then does it show local game pixels. It remains read-only and is destroyed
before the game panel during Flat handoff. Its square world panel can be grabbed
and resized between comfortable limits; its content is letterboxed rather than
stretched. Its pose, size, and visibility are stored only in Quest preferences,
with a versioned migration from the former identity-rotation default and a
dedicated radar-position reset.

The readiness gate is a narrow Android native signal set by the SDL game thread
only after `Map.Render()` has completed with `InMainLoop`, `GameActive`, no
special dialog, and both `Map.Is_Radar_Existing()` and
`Map.Is_Radar_Active()`. The Java producer requires two consecutive positive
samples and rechecks before presentation, clearing the panel on any loss. This
prevents intro movies, VQA, menus, loading frames, and stale transition frames
from appearing in the detached panel; it is not a time-based guess.

The accepted crop and an input-disabled Android overlay are exposed as one
coordinated presentation transaction. `PixelCopy` reads the raw SDL
`SurfaceView`, while an opaque View in the hosted Activity covers only the
original in-panel radar for the player. Because the capture source is below the
View, the cover can never feed back into the detached crop. Any readiness loss,
pause, or failed capture clears the detached game crop and removes the cover,
so the classic radar becomes the immediate fallback; the detached panel
returns to its offline state. Disabling or Flat handoff hides or destroys the
panel as appropriate. The detached panel intentionally does not forward radar
commands into the game.

The Spatial host stores its radar request even before SDL has loaded `libmain`.
The hosted Activity applies that request immediately after successful JNI
startup. This removes the former ActivityPanel-start race that could disable
automatic tactical-map mode for the complete session. While the detached radar
is enabled, a live powered radar enters tactical-map mode once, without
synthesizing a Map-button click or changing simulation state; later MAP presses
remain available for the classic player/status page.

Host policy tests verify the normalized crop, 10 Hz cap, stable readiness, and
exclusive show/hide policy. Physical Skirmish runs have confirmed offline to
powered-map transition, exact Android-overlay coverage, MAP/status switching,
and the detached panel's independent placement. The deliberately deferred
72-FPS thermal and repeated Home/Flat/Spatial lifecycle runs remain release
gates. The complete classic panel remains the failure fallback.

### Q4 follow-up — right-side pose and shared grab UX — 2026-08-20

The first Q4 default reused the support panel's `-12°` local yaw. That is
correct for the support panel on the player's left, but mirrors the right-side
radar away from the player. The radar default is now `+12°`, while the support
panel remains `-12°`. Pose version 3 migrates the known old identity and
version-2 `-12°` defaults exactly once; a stored rotation that is not one of
those known defaults is treated as an intentional user pose and is preserved.

The game ActivityPanel and radar now share one movable-panel component factory:
`IsdkGrabbable`, `IsdkPanelGrabHandle`, and `IsdkPanelResize` use the same ISDK
outer-frame grab/capture path. The radar is not parented
to the game panel, so it can be placed separately and its pose is persisted.

### Q4 follow-up — direct radar navigation — 2026-08-22

The detached radar now accepts a bounded click, controller-trigger tap or hand
pinch inside the displayed 128x128 map. Android maps only the fitted square,
rejecting title chrome and letterbox space. A 14 dp movement tolerance keeps
ordinary controller tremor from cancelling a tap, while a larger movement or
the shared outer ISDK grab mode suppresses navigation during panel placement.

The UI queues only the latest panel-local pixel. On the engine thread that
pixel is translated back to the original classic radar coordinates and passed
to the same tactical-centering calculation used by the original RadarButton.
It cannot select a unit, place a building, attack, reveal fog, or emit a
multiplayer command. The request is discarded whenever radar readiness or the
mission lifecycle is lost.

Cause and implementation files:
`android/app/src/questSpatial/java/org/tiberiandawn/android/QuestSpatialPreferences.java`
contains the mirrored default and one-time migration;
`android/app/src/questSpatial/java/org/tiberiandawn/android/QuestSpatialActivity.java`
contains the shared grab/resize path and spatial-panel arbitration. Thumbstick
and graphics-settings paths are intentionally unchanged.

Required physical Quest 3 retest after installing the clean `questSpatial`
build: confirm the right radar face points toward the player; grab its outer
frame with controller ray and hand pinch; move it to another location; resize
from a corner; release and reopen Spatial to confirm the pose persists; and
verify that tapping/pointing inside the radar never issues a game command.
Also repeat the existing game-panel grab/resize and read-only radar snapshot
checks. The source change itself cannot claim this hardware gate.

### Functional radar acceptance; decorative surround rejected — 2026-08-21

The detached radar's current functional contract is accepted on Quest 3. It
stays localized and offline before a powered Communications Center exists,
becomes the tactical-map crop when radar comes online, masks the matching
source area, and then leaves the classic MAP control free to alternate between
tactical map and player/status information. Independent grab, resize, pose
persistence, and reset remain on the same ISDK component path as the main
panel. Long stability and thermal measurements are still intentionally
deferred, not silently treated as complete.

A decorative command-table surround was then tested physically and rejected.
It added no interaction or information, visually competed with the game, and
its lower deck collided with the separate toolbar. The implementation,
geometry helper, scale synchronization, and tests were removed completely.
The proven ISDK grab/resize handle remains the only functional panel surround.
Do not reintroduce decorative geometry around the game image without a new
visual design and an explicit user-facing purpose.

## Planned spatial product extensions — 2026-08-21

Future spatial work must add information, control, or atmosphere rather than a
frame around the existing picture. The following additions are retained as the
product roadmap, in this order:

1. **Radar 2.0:** give the detached radar its own clear Map/Score switch,
   enlarged readable states, and a compact power/radar status. It remains
   movable, resizable, local-only, and subject to the same fog-of-war view as
   the player's classic radar.
2. **Layout profiles:** add saved presets such as Compact, Command Desk and
   Cinema. A preset stores only comfortable relative panel poses and sizes;
   every panel remains individually movable and has its own reset action.
3. **Detached production console and full-width tactical view (implemented,
   physical acceptance pending):** the real build strips plus REPAIR, SELL and
   MAP are rendered into a movable panel without duplicating game rules. During
   a live mission the recovered 160 pixels are actual additional battlefield
   view. The feasibility, contract and remaining hardware gates are specified
   in the next section.
4. **Selected-object console:** show one deliberately non-pixel-anchored,
   low-frequency panel for the currently selected unit or building: portrait
   or small generated hologram, name, health, owner and command state. It is an
   information console beside the table, not a marker that claims to sit on an
   exact map pixel.
5. **Spatial EVA/event strip:** present a short, calm event history for
   construction complete, unit lost, base under attack, radar offline and
   similar already-known local events. It must be bounded, rate-limited and
   dismissible; no continuous particle or notification clutter.
6. **Haptics and restrained spatial audio:** use short controller pulses for
   accepted commands and important warnings. Music remains non-spatial. EVA
   and alarms may be lightly anchored to the command console, with an explicit
   strength/off setting and no simulation dependency.
7. **Briefing/cinematic mode:** temporarily enlarge and centre the single game
   panel for mission briefings, FMV and score screens while hiding auxiliary
   panels. Restore the previous command-desk layout atomically when gameplay
   resumes.
8. **Optional command-room ambience:** only after the functional panels and
   performance gates pass, add restrained background lighting or scenery that
   never overlaps the game, toolbar or controls. Passthrough remains the
   default and no room scan or persistent anchor is silently enabled.

These items intentionally exclude pixel-anchored 3D arrows, selection corners,
decorative table frames, a mandatory full-3D battlefield, and duplicated game
controls with independent state. Those approaches have either failed physical
accuracy/usability tests or would create avoidable simulation and input risk.

## Detached production console and full-width tactical view

### Feasibility verdict

The requested result is technically possible, but it consists of two distinct
changes:

| Capability | Verdict | Reason |
| --- | --- | --- |
| Show the classic build area in a movable Quest panel | Feasible | The complete high-resolution sidebar has stable logical geometry and is already rendered by `SidebarClass`. |
| Operate REPAIR, SELL, MAP, build queues and strip scrolling there | Feasible with a proven inverse input transform | Panel-local hits can be mapped back to the existing classic gadget coordinates, retaining the original game logic. |
| Remove the duplicate classic sidebar from the main panel | Feasible | Spatial gameplay can suppress its presentation once the detached console has a valid current frame and input path. |
| Replace the removed area with genuinely more battlefield | Feasible, but a major renderer/engine milestone | Hiding or covering the old pixels creates only a blank strip; the tactical viewport itself must be widened and the sidebar must render into a second target. |
| Keep saves, campaign, skirmish and multiplayer compatible | Feasible | The split is presentation/input only if it continues to call the existing sidebar and command logic and does not alter simulation or packets. |

At 640x400 the classic sidebar occupies the rightmost 160 pixels. Its tactical
radar lies inside that region, the REPAIR/SELL/MAP controls begin around
`y=160`, and the two production strips continue below them. When the existing
sidebar is deactivated, `SidebarClass::Activate()` already asks
`DisplayClass::Set_View_Dimensions()` to widen the tactical area from 480 to
640 pixels. That method also updates tactical clipping and the map hit target.
This is strong source evidence that the engine can display more map rather than
stretching the old 480-pixel view.

The existing switch cannot be used unchanged: it widens the map **and** removes
the sidebar gadgets, radar/power buttons and production-strip input. Spatial
mode therefore needs to separate three concepts that the 1995 UI currently
couples:

- production/sidebar game state remains active;
- sidebar drawing and sidebar hit testing use a dedicated presentation target;
- the main tactical viewport uses the complete 640-pixel width.

### Recommended product shape

Keep the already accepted movable radar as one information panel and add a
second movable production console. The production console contains the actual
REPAIR, SELL and MAP controls, both build strips, queue/progress states and all
power information that would otherwise disappear with the classic right-hand
area. Radar 2.0 owns the enlarged Map/Score presentation, but its switch and
the classic MAP action remain one shared state rather than two independent
implementations.

An optional later layout may dock radar and production console into one tall
"Command Console". The first release should keep them separate because the
radar has already passed its grab/resize/readability gate and players can place
the two functions independently.

During the main menu, dialogs, FMV, briefing, score screens, importer, save
transfer, keyboard and permission flows, auxiliary gameplay panels are hidden
and the complete unified 640x400 presentation is shown. The split activates
only after the engine enters a live mission and has published valid panel
generations. On loss of readiness or spatial lifecycle focus, the application
restores the complete classic view immediately; it must never leave controls
invisible or half-interactive.

### Rendering and input architecture

The release implementation must not recreate the build system in Java or in a
second Quest-only rules model. It must continue to call the existing
`SidebarClass`, `StripClass`, gadget and map/radar state. This preserves costs,
prerequisites, flashing, scrolling, repair/sell behavior, queue timing,
localization and deterministic multiplayer.

The production path is:

```text
one simulation / one SidebarClass
        -> full-width tactical presentation -> main spatial game panel
        -> dedicated sidebar presentation   -> movable production panel
        -> radar/status presentation         -> existing movable radar panel

production-panel pointer -> inverse panel transform -> classic sidebar coordinates
                           -> existing Android/SDL gadget input queue
```

The read-only radar's current 10 Hz `PixelCopy` path is not sufficient for the
final interactive production console. Build progress, hover, scrolling and
button feedback need a current, low-latency frame. A short development-only
crop may prove geometry and input, but the release route is the Q2-style
non-blocking presentation bridge: an engine-owned offscreen sidebar buffer,
generation counters and newest-complete-frame consumption. If CPU upload
cannot hold 72 FPS, move that buffer to a supported shared GPU image rather
than reducing game simulation speed.

Pointer mapping must use the panel's current content rectangle after grab,
resize and rotation, preserve hover/down/move/up/cancel ordering, and reject
letterbox/outside hits. Strip dragging and controller/thumbstick scrolling
remain routed to the existing input path. Grabbing or resizing the spatial
console always wins over a game press and cancels any active sidebar pointer.

### Staged implementation plan

1. **S0 — Geometry and state contract:** enumerate every classic sidebar
   visual, button, strip, power/radar state and hard-coded coordinate; add host
   tests for 480/640 tactical widths and exact input transforms. No user-facing
   behavior changes.
2. **S1 — Interactive duplicate prototype:** show a development-only detached
   crop while leaving the original sidebar visible. Prove controller and hand
   hover/click/hold/scroll for REPAIR, SELL, MAP, both strips, queue cancel and
   building placement. This stage is never a release mode.
3. **S2 — Dedicated sidebar sink:** render the original sidebar into a bounded
   offscreen target and publish it without blocking the game thread. Remove
   PixelCopy from this interactive path and verify Original/Modern modes,
   palette changes and all localized feedback.
4. **S3 — True full-width tactical mode:** keep sidebar state/input active,
   widen the tactical viewport to 640 pixels, publish the map and production
   console separately, and hide the classic right-hand presentation only after
   both targets are ready. Recompute edge scrolling, map clicks, selection,
   building placement, help text and ModernArt clipping against the wider
   viewport.
5. **S4 — Scene integration:** make production console grab/resize/persist/reset
   match the radar and game panel; add Compact/Command Desk presets and atomic
   unified/split transitions for menus, FMV and dialogs.
6. **S5 — Acceptance:** test campaign, skirmish, LAN and relay multiplayer;
   hands and controller; REPAIR/SELL/MAP and every production queue operation;
   save/reload; radar offline/online and Map/Score; Original/Modern; Home/resume;
   panel transforms; 20-minute 72 FPS and deferred 60-minute thermal runs.

### Stop conditions and fallback

The split stays optional until S5 passes. A visible **Classic UI / Spatial
Console** choice and an immediate automatic fallback protect playability. Stop
and retain the classic whole-game panel if any of the following cannot be made
reliable: exact input after arbitrary panel transforms, complete restoration
after lifecycle changes, no loss of power/radar/queue information, stable
building placement, deterministic multiplayer, or the 72 FPS performance
floor. A masked blank strip or a stretched 480-pixel map does not count as
completion.

## Product variants

The Android application has two product flavors in the existing `:app` module:

| Variant | Purpose | ABIs | Meta Spatial SDK |
| --- | --- | --- | --- |
| `mobile` | Current Android and flat Quest application | `arm64-v8a`, `x86_64` | No |
| `questSpatial` | Quest-only spatial and flat-fallback application | `arm64-v8a` | Yes |

This is preferable to a second application module because it keeps one package
identity, one signature, and the existing private files directory. Replacing a
flat development APK with the spatial APK using an update install must preserve
the imported campaigns, saves, language choice, and settings.

The flavor conversion changes task and artifact names. Documentation and local
helper commands must be updated from `assembleDebug` and `app-debug.apk` to the
explicit `assembleMobileDebug` / `assembleQuestSpatialDebug` artifacts. No
release workflow should rely on Gradle choosing a variant implicitly.

The Meta plugin and libraries exist only in the Quest flavor's runtime build
surface. The exact SDK version was revalidated against Meta's official
add-to-existing-app guide and current HybridSample on 2026-08-20. Version
`0.13.2` is pinned in both the plugin and the four Quest runtime coordinates;
there is no unbounded floating dependency. Optional build-tool usage-data
collection is explicitly disabled.

### Planned project layout

The implementation is expected to use the following boundaries. Exact Meta
class names are confirmed against the pinned SDK before the first source edit.

```text
android/app/src/main/
  existing shared Android activities, resources, and SDL integration

android/app/src/mobile/
  conventional Android/flat-Quest launcher manifest and mobile-only defaults

android/app/src/questSpatial/
  AndroidManifest.xml
  java/.../QuestSpatialActivity.java   # Q1 spatial coordinator and panels
  res/values/strings.xml
  res/values-de/strings.xml

android/app/src/main/java/.../
  AndroidGameData.java                  # shared read-only validation

platform/android/
  existing Android JNI and engine adapters
  optional quest_spatial_bridge.{h,cpp} only after the Q1 decision gate
```

The spatial flavor adds a native `QUEST_SPATIAL_PORT` definition only when a
native bridge is actually required. Kotlin and Meta libraries stay outside the
portable engine. Shared Android activities must not import Meta types, so the
mobile variant remains buildable without resolving or packaging the Spatial
SDK runtime.

The spatial shell has a deliberately small contract:

- determine whether the 20 required imported files are valid using the same
  validation result as the existing launcher;
- start exactly one of import, flat game, or spatial game;
- remember the user's Spatial/Flat preference without moving saves or data;
- hand control back after the importer or save manager closes;
- prevent a second native SDL engine from starting while one is active;
- recover to the flat launcher if spatial initialization fails.

The Q1 command-table component owns only world pose, size, grab capture, panel
surface, toolbar actions, and lifecycle handoff. It must not know about
missions, units, networking, or saves. The existing Android input layer remains
the command interpreter.

If Q2 is needed, the bridge contract is likewise narrow:

```text
PublishFrame(indexedPixels, paletteGeneration, overlayGeneration, frameNumber)
AcquireLatestFrame() -> immutable completed frame or no new frame
SubmitPointer(source, id, phase, logicalX, logicalY, buttons, timestamp)
CancelAllPointers(reason)
```

This is an architectural description, not a frozen ABI. The implementation may
transport RGBA instead of indexed data when the final Android compositor has
already applied ModernArt. Whichever boundary is selected, it must publish the
same final visual frame that flat mode would show and retain the existing input
queue's ordering and cancellation semantics.

### Implementation change sets

Q0 and Q1 were landed as independently reversible change sets:

1. **Baseline record:** no behavior change; record APK, ABI, manifest, native
   hashes, installed-data preservation check, and flat Quest smoke result.
2. **Flavor isolation:** introduce `mobile` and `questSpatial`; prove mobile
   manifest/dependencies and runtime are unchanged before adding a scene.
3. **Spatial shell:** add the pinned SDK, Quest manifest, passthrough activity,
   bilingual fatal fallback, and no engine startup before the user chooses a
   mode.
4. **Experience coordinator:** connect data validation, importer return, Flat
   mode, spatial mode, and single-engine ownership.
5. **Whole-game panel:** present one unchanged 16:10 game surface and prove
   rendering, audio, lifecycle, and system panels.
6. **Input/table arbitration:** add pointer transforms, handle capture, reset,
   toolbar, controller/hand switching, and cancellation.
7. **Q1 source/build gate:** complete the interactive panel, ownership handoff,
   flavor isolation, and package/hash verification. The physical acceptance
   gate remains open for the next Quest run.

No change set combines the initial flavor conversion with a native renderer
rewrite. That separation preserves a working flat APK and makes the result of
the hardware panel experiment unambiguous.

### Technology decision

The first implementation uses Meta Spatial SDK plus its Interaction SDK, not a
new raw OpenXR renderer. Spatial SDK directly supports hybrid Android apps,
interactive panels, passthrough scenes, hands, controllers, and grabbable
objects, which matches the command-table milestone and preserves Android system
UI. Interaction SDK supplies device-independent pointer semantics while the
existing engine continues to receive logical pointer and key events.

Raw OpenXR remains an implementation detail available behind the SDK or a later
escape hatch for capabilities that cannot be expressed through the panel and
interaction APIs. It is not introduced merely to reproduce features already
provided by the hybrid application model. This avoids taking ownership of the
entire XR renderer, swapchain, hand skeletons, and Android activity integration
before the product needs them.

## Runtime architecture

### One engine owner

The process may contain only one live Tiberian Dawn engine and one owner of its
SDL lifecycle. Flat and spatial activities must never create two simultaneous
native game instances.

The Q1 `QuestSpatialActivity` enforces these states with an explicit lease and
exact-generation handoff:

```text
data missing -> Android import panel -> spatial launcher
data valid   -> spatial activity -> hosted SDL game panel (Q1)
fallback     -> leave spatial activity -> existing flat game activity
```

The current `TiberianDawnActivity` stays the authoritative renderer, input,
audio, and native-engine implementation in both modes. `QuestSpatialActivity`
uses Meta's `AppSystemActivity` for the spatial scene and hosts that same
activity as the game panel. `GameDataImportActivity` and
`SaveTransferActivity` remain ordinary Android activities and appear as system
or hybrid panels.

### First rendering path: reuse the Android panel

Q1 follows Meta's supported hybrid-app route: place the existing interactive 2D
application content in the spatial scene. This is the smallest change and
retains the mature SDL renderer, Android input translation, audio, virtual
keyboard, and document pickers. Spatial owns the external toolbar; the legacy
SDL overlay remains only in flat/mobile mode.

This remains a hardware proof, not an assumption. The source and package
implementation is complete, but the SDL `SurfaceView`, panel transformations,
focus, input, audio, and lifecycle still require the physical Quest acceptance
run. Q2 is not started unless that gate fails.

### Fallback rendering path: SpatialFrameBridge

If the existing SDL surface cannot be embedded reliably, presentation will be
split at the final render boundary rather than inside the simulation:

```text
game simulation -> indexed 640x400 frame + palette + overlays
                -> FlatPresentSink    -> current SDL window
                -> SpatialPresentSink -> Quest panel texture
```

The first bridge implementation may use a non-blocking, double-buffered RGBA
frame to prove correctness. The game thread publishes only the newest complete
frame; the spatial renderer consumes the newest available frame and never
waits for the simulation. A 640x400 RGBA frame is about 1 MB, so this is an
acceptable prototype but must be profiled before release.

If copying is materially expensive, the production bridge moves to a shared
GPU image, such as an Android hardware buffer or compatible EGL/external
texture supported by the selected SDK. It must not read back the complete Quest
eye framebuffer, and it must not render both the normal SDL presentation and
the spatial texture at the same time.

Logical game coordinates remain 640x400 even when the panel texture is rendered
at a higher resolution for legibility. Sharp, pixel-perfect, and classic
presentation remain presentation policies rather than simulation resolutions.

## Interaction model

The command table is ray-first and also supports direct hand interaction:

| Player action | Controller | Hands |
| --- | --- | --- |
| Point | controller ray | hand ray or direct poke |
| Select / command | trigger | pinch or poke |
| Selection rectangle | hold trigger and move | hold pinch and move |
| Map movement | thumbstick | explicit PAN mode and pinch-drag |
| Back / cancel | B or Y where Horizon maps Back | spatial Back control |
| Move table | grip on an outer handle | grab an outer handle |
| Scale/rotate table | two controllers on the handle | two-hand manipulation of the handle |

Game input and table manipulation must be mutually exclusive. Once a table
handle captures a pointer, its trigger or pinch cannot also select a unit or
issue a command. Tracking loss, focus loss, `ACTION_CANCEL`, or activity pause
must release capture without a ghost click.

Panel input is converted from panel-local hit or UV coordinates to the logical
640x400 game surface. The conversion must be retested after every table move,
scale, rotation, curve change, resolution change, and flat/spatial transition.

The current Back, PAN, LOG, and SAVES buttons stay available during the first
prototype. After physical validation they move to a spatial toolbar around the
table so they no longer cover classic game controls. Automatic visibility can
show hand-only controls for hands and hide them for controllers, with explicit
Always visible and Always hidden overrides. Back must remain reachable for a
single hand; B/Y are not permanently reassigned to a game command.

## Spatial presentation

The initial scene is intentionally calm and seated-friendly:

- passthrough command space, with no artificial locomotion;
- one 16:10 table approximately 0.7 to 1.0 metres wide;
- comfortable starting distance around 0.8 to 1.2 metres;
- a modest tilt rather than a vertical cinema screen;
- the dedicated ISDK outer handle used for grab, scale, and rotation;
- a recenter/reset action that restores a comfortable pose;
- no required room scanning or spatial anchors in the first milestone.

The exact size and tilt are comfort defaults, not hard-coded world rules. The
user may reposition the table, and the app remembers only a safe relative pose
unless a later opt-in anchor feature is explicitly designed.

The first version uses the complete game framebuffer as one panel. Splitting
the map, radar, sidebar, and dialogs too early would duplicate classic hit-test
logic and risk incorrect input. The physically rejected decorative surround
stays removed; spatial additions need a clear function and must remain outside
the tactical image and toolbar.

After the single-panel path is stable, read-only crops of the same final frame
may be placed on separate panels, as already proven by the enlarged radar.
The planned production console is not treated as another read-only crop: it
requires a current dedicated render target and a verified inverse input
transform. Dialogs must still be presented in full or as native spatial UI.

## Optional 3D expansion after the command table

Pixel-anchored Spatial entities above the Android game panel were prototyped
and then removed after physical Quest testing. Independent compositor timing,
panel transforms and head-motion parallax made selection and command glyphs
insufficiently accurate for an RTS. No snapshot bridge, marker pool, setting or
hidden marker resource remains in the product.

Possible additions, in increasing order of risk, are:

1. restrained explosions, smoke, and EVA status indicators outside the panel;
2. low-frequency holographic unit and building representations that do not
   claim pixel-exact correspondence with the game surface;
3. a full optional command-room environment;
4. spatially positioned effects and controller haptics.

Music should remain non-spatial. EVA can be anchored near the command console,
while unit effects may be positioned lightly relative to the table. Strong
positional effects and continuous haptics are avoided because they can reduce
clarity and comfort.

Any future spatial presentation feed must remain read-only and must not advance
game time, consume random numbers, alter object ownership, expose fogged state,
or introduce a new multiplayer message. Pixel-overlay markers are not reopened
without a compositor-synchronized texture/coordinate contract and a new
physical accuracy gate.

### Rejected experiment — screen-space stereoscopic battlefield

A Quest-only screen-space stereo experiment was physically evaluated and then
removed on 2026-08-23. Integer/nearest-neighbour disparity preserved sharp
pixels but separated the map into visible terrace-like bands. Fractional
bilinear sampling removed those bands but made the original pixel artwork
noticeably soft. Its separate visual panel also required an input proxy in
front of the proven ActivityPanel.

No stereo panel, input proxy, setting, resource, or runtime path remains in the
product. The Quest build again uses the original sharp ActivityPanel as its
single renderer and input owner. This idea should only be reconsidered with an
object-aware depth contract or a renderer that can provide stable depth masks;
another whole-frame screen-space displacement is not an acceptable approach.

## Implementation phases and gates

### Q0 — Build isolation and spatial shell (complete)

Work:

1. Record the current flat APK, native-library hashes, imported-data path, and
   smoke-test results.
2. Add `mobile` and `questSpatial` flavors in the existing app module.
3. Keep current Android dependencies and manifests in `mobile`.
4. Add the pinned Meta Spatial SDK plugin/dependencies and Quest manifest only
   to `questSpatial`.
5. Add `QuestSpatialActivity` with passthrough, a real launcher panel, and a
   safe scene before game-mode handoff.
6. Keep the current Android launcher available as the flat fallback.
7. Update Android Studio instructions and artifact names.

Acceptance:

- `mobileDebug` was rebuilt for ARM64 and x86_64; its four native library
  hashes match the recorded flat baseline, and the emulator launch reached
  `TiberianDawnActivity`;
- `questSpatialDebug` was clean-built for ARM64 and contains the
  `AppSystemActivity`/passthrough shell plus the explicitly spawned launcher,
  game `ActivityPanel`, and toolbar entities; physical visual/button and input
  checks remain open for the next headset run;
- the mobile update on the AVD preserved all recorded private-data sentinels;
  the same-ID/same-signature Quest update preserved its recorded private-data
  sentinels as well;
- no Meta class, Horizon requirement, Spatial runtime, or OpenXR library is
  packaged into `mobileDebug`;
- the bilingual importer return and safe flat fallback are in the shell, and
  no Apple source, protocol, or game-engine behavior changed.

The plugin isolation acceptance is met: the existing module produces a clean
mobile APK while the Spatial AARs, runtime libraries, and Quest manifest are
variant-scoped. A thin `:quest-spatial` application module is therefore not
needed. That option remains a fallback only if a future SDK version breaks
this proven isolation boundary.

### Q1 — Interactive game panel prototype (implemented; functional hardware gate passed)

Implemented work:

1. The current interactive game activity/content is hosted on one 16:10 panel.
2. The grab handle, reset, and Flat-mode actions are registered; the toolbar
   is a `TransformParent` child of the game panel. Decorative frame geometry
   was physically rejected and removed.
3. Hands and controller pointer events route through the existing input path.
4. Importer, save transfer, and virtual keyboard remain hybrid/system panels.
5. SDL ownership, Home/resume, focus, duplicate launch, partial creation, and
   exact-generation teardown paths are hardened in shared Android code.

Acceptance:

- launch, intro, main menu, and a live mission are usable;
- controller ray/trigger and hand ray/pinch select, command, and drag;
- thumbstick and PAN move the map;
- transformed or resized panels keep exact pointer alignment;
- table manipulation never issues a game command;
- Back, saves, keyboard, import, LAN, and Internet-room entry remain reachable;
- audio resumes without duplication or a stalled VQA movie;
- the spatial compositor remains at least 72 FPS during a 20-minute mission;
- flat mode has no functional or performance regression.

The source/build/package gate and functional physical passes are complete. The
user deliberately deferred the long stability/thermal run. Q2 remains a
fallback only if a later sustained test finds a presentation-bridge failure.

### Q2 — Presentation bridge, only when required

Work:

1. Add a final-presentation sink interface without modifying simulation.
2. Implement non-blocking double buffering and generation counters.
3. Map Spatial SDK pointer coordinates to the existing keyboard/mouse message
   queue, including source, pointer ID, button, hover, and cancellation.
4. Profile CPU copying, GPU upload, memory, latency, and thermal behavior.
5. Replace CPU copying with a supported shared GPU texture if required.

Acceptance:

- no torn frames, palette flashes, doubled ModernArt, or missing overlays;
- input-to-photon latency remains comfortable for precise RTS commands;
- a stalled spatial consumer cannot stall the game thread;
- Original and Modern artwork both work, with Original used as the performance
  baseline;
- spatial mode maintains 72 FPS and flat mode retains its existing policy.

### Q3 — Quest-native controls and comfort pass (implemented; physical gate open)

Work:

1. Move Back/PAN/LOG/SAVES to a spatial toolbar.
2. Add automatic hand/controller toolbar visibility and explicit overrides.
3. Tune controller jitter tolerance, long hold, drag threshold, deadzones, and
   pan speed with physical measurements.
4. Add recenter, seated default, panel-size limits, and comfort-safe poses.
5. Add short bilingual first-run instructions.

Acceptance:

- one hand alone can reach every essential operation;
- one controller alone can reach every essential operation;
- no classic Options/sidebar button is covered;
- minor controller tremor does not turn a command click into an unwanted drag;
- a long hold is reproducible and never creates an unintended move command;
- 60 minutes of play produces no thermal or lifecycle failure.

### Q4 — Split information panels (exclusive radar functionally accepted)

The optional radar panel is movable, resizable, and navigable. When enabled it
stays present through menus, loading and movies, but shows only a clean localized
offline state rather than capturing those frames. It switches to a crop of the
final local frame only after stable mission-radar frames have been accepted. At
that point an input-disabled opaque Android View covers only the duplicate
radar region above the SDL surface; losing readiness restores the original
immediately and returns the detached panel to offline. PixelCopy reads the raw
SDL surface below that View, so the detached source is not recursively covered.
When live, a tap/pinch inside the fitted radar square centers the main tactical
view through the original engine radar calculation. Panel chrome and outer
grab input are rejected, and no tactical selection/command is synthesized.

When the radar becomes powered and active, the detached view switches to the
tactical map exactly once. It does not force map mode on later frames: the
classic MAP button remains available to alternate the detached radar area with
the multiplayer/skirmish player-status page. Losing the radar signal resets the
one-shot handoff for the next online transition.

### Q5 — Pixel-anchored 3D tactical layer (retired)

The selection/move/attack marker prototype failed its physical accuracy gate
on Quest 3 and has been completely removed. The game keeps its accurate green
and red acknowledgements inside the SDL-rendered panel. Future 3D work is
limited to spatial elements outside the tactical image unless a later shared
GPU/compositor path can prove exact correspondence under resize, head movement
and every presentation mode.

### Q6 — Spatial audio, haptics, and environment

Add Radar 2.0, layout profiles, restrained EVA/event
presentation, short event haptics, lightly anchored EVA/warnings, and a
briefing/cinematic layout. Music remains non-spatial. Passthrough and the
unmodified flat mode remain available. Scene understanding, room meshes, and
persistent anchors require separate privacy, permission, and failure-state
design and are not silently enabled.

### Q7 — Detached production console and full-width tactical view (implemented;
physical acceptance pending)

S0 through S4 are implemented. The engine now renders the original production
area into a bounded 160x242 RGBA8888 double buffer with monotonic generations,
while the main tactical viewport uses all 640 logical pixels. Panel-local
touch, hand/controller hover, click, hold, cancel and fractional scroll are
mapped back to the original sidebar coordinates and source-tagged so main-map
and detached-console gadgets cannot consume one another's input. The Quest
panel uses the same independent ISDK grab/resize path as the radar, persists
its pose, has its own reset action, and is shown only after a complete live
mission frame is available. Menus, dialogs, FMV, focus loss and disabled mode
retain or restore the unified classic presentation.

The split defaults on for a new Quest installation, but an explicitly saved
Classic UI choice remains respected. Missing JNI/readiness or any failed panel
creation falls back to the complete classic view. No save, replay, simulation
or multiplayer packet format changed.

S5 remains the physical acceptance gate: verify arbitrary grab/resize poses,
REPAIR/SELL/MAP, both production strips and queue cancellation, strip scrolling
with hand and controller, building placement, campaign/skirmish/LAN/relay,
save/reload, radar Map/Score interaction, Home/resume, Original/Modern and the
72 FPS floor. Classic unified UI remains the permanent fallback until those
checks pass.

### Q9 — Selected-object information console (implemented; physical acceptance pending)

The Quest-only workspace has a fourth independent panel for the current local
selection. Its default pose is directly below the detached radar, with at
least ten centimetres of clear vertical space in every automatic layout. It
participates in the same individual/group movement, thumbstick depth/scale
adjustment, layout profiles, undo, recenter, persistence and reset model as
the game, radar and production panels.

The panel is deliberately read-only. For a single object it shows the
localized name, category, owner, bounded current/maximum health, a color-coded
health bar and a localized mission state. For a group it shows the total and
aggregate health plus up to six compact name/health rows and a localized
`+N more` remainder. It contains no command controls, world coordinates,
sprite extraction or tactical marker. While enabled it remains visibly stable
for the whole live, focused mission and shows a localized empty state after
deselection instead of appearing and disappearing. A bilingual support-panel
button explicitly hides or restores the console; the preference defaults on
and is persisted. Menus, dialogs, movies, focus loss, support-panel display
and Flat handoff still hide the surface and stop its reader. German/English
changes rebind all panel labels and mission-state names immediately.

The native producer executes only on the game thread after a completed mission
frame and publishes a fixed 1440-byte little-endian version-3 value snapshot at
no more than roughly five updates per second. It reads only `CurrentObject` for
the local house, validates every selected entry as a live, down, non-limbo
`TechnoClass` owned and selected by the local player, and exposes at most the
first sixteen entries in deterministic selection order. The total selection count
and aggregate health include the complete validated group. It never scans
opponent heaps, remembered contacts, fog cells, network state or simulation
pointers. Up to sixteen selected local objects are available to the compact,
scrollable panel before an honest `+N` remainder is shown. Java receives only
copied scalar values and bounded strings through
a generation-based direct buffer; a stalled or missing consumer cannot block
the simulation.

Physical acceptance must verify the persistent empty state at mission start,
single-selection details, deselection without window flicker, compact group
rows and the `+N` remainder, health/status changes, the UI hide/show preference,
German/English switching, the ten-centimetre radar gap, independent and
grouped arrangement, thumbstick size/depth adjustment, all layout presets,
Home/resume, menus and Flat handoff. Campaign, skirmish, LAN and relay should
expose identical local information without changing saves, replays,
deterministic CRCs or packets.

The compact group presentation uses fixed-height rows and identical thin
aggregate/per-unit health bars instead of distributing spare panel height
between entries. The list can be scrolled by hand drag or controller wheel/
thumbstick through the first sixteen selected local objects and reports any
larger remainder as `+N`. Single-object health and status remain grouped with
the name/category/owner/action block at the top of the panel. Existing saved
radar/info pairs migrate once from the former near-touching five-centimetre
layout to a ten-centimetre minimum gap; independently placed windows are not
moved.

Choosing the classic `Exit Game` command now distinguishes a natural engine
exit from Toolbar Back/Flat. A natural exit removes the complete Spatial task
and returns to Quest Home; Back/Flat retains the existing lease-aware flat
handoff. Language still defaults to the explicit System preference: a German
Quest locale resolves to German, every other/missing locale resolves to
English, and a manually selected German or English preference remains saved.

### Q10 — Quest-native spatial startup splash (implemented; device gate open)

Every new Quest Spatial game start now creates an application-owned portrait
`UIPanel` in passthrough before exposing the SDL ActivityPanel. The supplied
3:4 collage is packaged only in the `questSpatial` source set and displayed
with `FIT_CENTER` on a black 0.60 x 0.80 metre quad at the established seated
origin `(0, 1.15, 1.40)`. The game ActivityPanel starts concurrently at a
temporary hidden pose, so SDL, imported content and the native engine can warm
up without a white TaskContainer surface. Toolbar, first-run help, radar,
production console and selection information remain deferred until reveal.

The testable state machine is `WAITING_FOR_PANEL -> VISIBLE -> REVEALING ->
FINISHED`, with `CANCELLED` as the terminal lifecycle/handoff route. A bound
splash remains visible for at least 1.2 seconds and reveals only once the
ActivityPanel plus native engine are ready. Five seconds is the hard startup
ceiling. A splash that fails to bind yields immediately when the game is ready
or after a short 750 ms attachment grace, preserving the previous launch path.
Reveal removes the panel entity and its SDK-owned collider. Back, Flat,
Activity destruction and partial panel creation cancel timers and references.
Pause stops the transition poll without confusing Meta's normal hosted-panel
pause with an abort; resume continues the same deadline and never creates a
second splash for the Activity instance.

Missing commercial game data still opens the existing importer directly. The
splash is created only after validated data exists, including the successful
return from that importer. No JNI, engine, save, multiplayer, Apple or mobile
Android contract changed. Physical acceptance on Meta Quest 3 and 3S must
verify complete uncropped artwork, no white intermediate quads, cold start,
import return, Back/Flat, Home/resume and unchanged app-private data.

### Q11 — Floating local-faction emblem (implemented; physical acceptance open)

The Quest build now contains a non-interactive faction emblem above the main
game panel. It is decoration derived only from the local
player's already-known house; it must never expose opponent, fog-of-war or
network state. Unknown/menu/cinematic/import states show no emblem. GDI and Nod
appear when a real campaign, skirmish, LAN or relay match identifies the local
side, and the emblem updates or disappears cleanly when that state changes.

The assets are true 2.5D reliefs rather than flat rectangles or continuously
animated sculptures:

- a dark graphite backing plate with a raised, bevelled emblem;
- brushed gold metal and a restrained warm rim for GDI;
- dark metal, raised red detail and a restrained red rim for Nod;
- approximately 0.15--0.18 m tall and 0.01--0.02 m deep;
- centred 0.04--0.06 m above the current top edge of the main panel;
- one small static mesh and material set per faction, with no continuous spin,
  particle effect, dynamic shadow or gameplay-rate animation;
- a short 0.25--0.40 second scale/fade reveal only when the faction becomes
  known.

The emblem follows the main game panel's transform and all workspace presets.
It moves, recentres and resizes with the screen, but has no collider, ray target
or grab affordance and therefore cannot block game input. Separate-window mode
does not turn it into another independently movable window. A bilingual
**Faction emblem / Fraktionslogo** toggle in the Spatial UI defaults to on and
is saved with the other comfort preferences.

Native code exposes a tiny read-only Quest presentation state instead of
copying pixels or game objects: `UNKNOWN`, `GDI`, or `NOD`, plus whether mission
presentation is active. The value is computed from the local player's house /
`ActLike` identity on the simulation thread and published atomically. The
Spatial shell polls only on a low-frequency presentation cadence and performs
all entity creation, material changes and teardown on its scene thread. The
contract is Quest-only, has no packet/save/replay effect, and is absent from the
mobile APK. Lifecycle teardown covers Exit Game, Back, Flat, import handoff,
pause/destruction and partial scene creation.

Both release GLBs are reproducibly exported with Blender 5.2 LTS from the
maintainer-authored sources under `tools/quest_faction_badges/sources` by
`tools/export_quest_faction_badges.py`. The GDI source PNG is embedded into its
GLB; the Nod mark is geometry converted from the included SVG. Cameras, lights,
animations and unrelated scene data are stripped, physical size/orientation is
normalized, collision remains disabled, and the assets are packaged only by
`questSpatial`. The older procedural generator remains a texture-free fallback.
Their asset notice records project authorship and trademark context. Before release, verify
recognisability and rights/trademark presentation separately. Acceptance covers both factions in
campaign, skirmish, LAN and relay; unknown/menu/cutscene hiding; every layout
preset; separate/together movement; resize/recentre; UI toggle persistence;
Home/resume; Flat/Exit cleanup; 72 FPS; and proof that `mobileDebug` contains
neither mesh nor Spatial implementation.

### Spatial polish backlog after Q11

The following are deliberately small presentation refinements, not another
gameplay or renderer expansion:

1. **Implemented; physical acceptance open.** Radar, production and
   selected-object panels now use one visual system: matching header rhythm,
   border weight, rounded dark surface and a restrained local accent. Campaign
   uses GDI gold or Nod red; skirmish, LAN and relay use the local player's
   actual gold, light-blue, red, green, orange or blue-green remap. The
   production chrome reserves a separate fitted content rectangle, so its
   native 160x242 pointer mapping remains exact. Header/content spacing and
   the radar inset prevent either live text or the square radar image from
   colliding with the separator.
2. **Implemented; physical acceptance open.** Arrangement mode dims inactive
   frames, highlights only the selected window (or the complete grouped
   workspace), turns a side-panel frame green while it is inside a magnetic
   dock target, and commits that snap on release. A short Meta controller
   haptic acknowledges the commit; hand input remains silently supported.
3. Offer compact/normal/large information-density presets, stronger text size
   where needed, minimum panel gaps and a **Rearrange side panels only** action
   that leaves the game screen fixed.
4. Run a final focused acceptance matrix for 1/2/5/10/16+ selected units,
   hand/controller list scrolling, GDI/Nod and all game modes, persisted poses,
   language, Exit/Flat and Home/resume before beginning the next large feature.

Quest map navigation is additionally hardened for ray use: ordinary clamped
hover at the exact ActivityPanel edge no longer starts classic autoscroll;
held selection-edge scrolling, PAN and thumbstick scrolling remain available.
Sub-0.04 neutral thumbstick residue is ignored while the proven minimum 0.05
Quest scroll sample remains active. The toolbar's **Base/Basis** command routes
through the original configurable `KeyBase` behavior and centers on the local
Construction Yard, MCV or recorded starting base.

Context-sensitive panel visibility is not a new backlog feature: the current
implementation already defers radar, production and selected-object panels
until the spatial game reveal, suppresses inappropriate presentation during
startup/import/menu transitions, keeps the information console stable during
ordinary deselection, and tears panels down on handoff. Only transition audits
and any concrete defects found by them remain; this item must not be rebuilt as
a separate phase.

## Performance policy

Quest immersive presentation must sustain at least 72 FPS. The game simulation
and game-frame publication do not have to run at the headset refresh rate; the
spatial shell may repeat the newest complete game frame. A 90 FPS shell is a
stretch target on Quest 3 after the 72 FPS and thermal gates are stable.

Performance is measured in this order:

1. passthrough plus launcher and complete game panel;
2. Original artwork on the complete panel;
3. Modern artwork on the complete panel;
4. panel transformation, toolbar, and input;
5. optional extra panels;
6. optional 3D presentation objects.

If a step misses 72 FPS, later steps stay disabled. The response is to profile,
reduce panel texture update rate or resolution, or remove spatial decoration,
not to slow the deterministic game simulation. Modern rendering starts disabled
in early performance tests because it has previously exposed a severe
presentation slowdown on Quest, even though the later flat build improved it.

Every 20-minute functional run is followed by a 60-minute thermal run before a
phase is accepted. Measure compositor misses, CPU/GPU time, temperature trend,
memory, frame-copy time, and input latency rather than judging only by visible
smoothness.

## Data, privacy, and compatibility

- The APK contains no commercial game data. ISO import remains local and
  user-initiated.
- Basic passthrough composition does not require the game to read or retain raw
  camera images.
- Room meshes, scene understanding, and anchors stay off until they have an
  explicit user benefit, permission explanation, and data-retention policy.
- Flat and spatial variants share the same save format and imported-data
  validation.
- Multiplayer remains one protocol and one deterministic simulation across
  Android, Quest, iPadOS, macOS, and future visionOS work.
- The spatial layer may display only local, already-visible game state.
- Generated command-table artwork and UI belong under Android/Quest resources;
  original game assets are not copied into the repository.

## Verification matrix

| Area | Mobile AVD | Physical Quest flat | Physical Quest spatial | Apple parity gate |
| --- | --- | --- | --- | --- |
| Build/package | ARM64 + x86_64 | ARM64 | ARM64 | Required for shared changes |
| Import/save/keyboard | Yes | Yes | Hybrid panels | Required when shared code changes |
| Mouse/touch | Yes | Panel | Panel/direct | iPad touch + macOS pointer |
| Hands/controllers | No | Android panel events | Spatial pointer events | Controller parity where applicable |
| Audio/lifecycle | Basic | Device route | Spatial + panel transitions | Both Apple targets for shared audio |
| LAN/Internet multiplayer | Protocol smoke | Cross-device | Cross-device | Cross-play and deterministic CRC |
| Performance/thermal | Diagnostic only | 20/60 minutes | 20/60 minutes at 72 FPS | Existing platform frame gates |
| Fog-of-war privacy | Host tests | Visual | Snapshot audit | Deterministic shared tests |

Any shared C++ engine change triggers the repository's full host tests, Android
ABI builds, unsigned iPadOS compile, and Universal 2 macOS package verification.
Quest-only Kotlin, manifest, or scene changes do not require artificial Apple
implementations, but must remain below a Quest-specific source set and must not
change the common multiplayer protocol.

## Known risks and prepared fallbacks

| Risk | Detection | Fallback |
| --- | --- | --- |
| Meta plugin affects normal Android builds | Q0 dependency/manifest diff (passed for 0.13.2) | isolate in separate Quest app module if a future SDK changes this |
| SDL surface cannot be a reliable spatial panel | Q1 device test | Q2 `SpatialFrameBridge` |
| Panel transform offsets input | coordinate grid and edge tests | explicit UV-to-640x400 transform |
| Tracking loss creates a command | cancel/focus test during hold | central pointer capture cancellation |
| Spatial and SDL lifecycles fight for ownership | repeated Flat/Spatial/Home transitions | single engine owner and explicit handoff |
| Frame copies miss 72 FPS | frame timing and thermal trace | shared GPU texture or lower publication rate |
| Modern art becomes expensive | Original/Modern A/B trace | default Original; optimize Modern separately |
| Hybrid document picker cannot return cleanly | import/save round trip | exit to 2D activity and return by explicit intent |
| Direct touch is imprecise | physical target tests | ray-first UI and larger hand targets |
| Future 3D layer leaks hidden information | fog/replay audit | keep it outside tactical state; retain panel-only gameplay |

## First implementation package

Q0 is the completed, reversible flavor-isolation baseline:

1. the flat build baseline is recorded and archived externally;
2. isolated `mobile` and `questSpatial` variants build with explicit tasks;
3. the Quest artifact contains the passthrough shell and Q1 panel path;
4. the existing importer, data validation, and Flat fallback are reachable;
5. mobile APK dependency/manifest isolation and AVD data preservation are
   verified.

Q1 is the completed source/build/package milestone: the existing game is one
interactive spatial panel with a child toolbar, lease-aware lifecycle handoff,
and flat fallback. Q3 comfort controls and the optional exclusive Q4 radar are
implemented and retain that fallback. The Q5 pixel-marker prototype was
removed after failing its physical accuracy gate. The remaining visual,
performance and thermal gates remain open;
the user deliberately deferred the long stability runs while functional work
continues. No 3D units, spatial audio, anchors, or command-room asset work is
accepted for release before those measurements pass. This keeps the
implementation reversible and honest about what remains unverified.

## Official references

- [Add Meta Spatial SDK to an existing 2D Android app](https://developers.meta.com/horizon/documentation/spatial-sdk/add-spatial-sdk-to-app/)
- [Hybrid apps overview](https://developers.meta.com/horizon/documentation/spatial-sdk/hybrid-apps-overview/)
- [Meta Spatial SDK overview](https://developers.meta.com/horizon/documentation/spatial-sdk/spatial-sdk-explainer/)
- [Interaction SDK overview](https://developers.meta.com/horizon/documentation/spatial-sdk/spatial-sdk-isdk-overview/)
- [Interactive panels](https://developers.meta.com/horizon/documentation/spatial-sdk/spatial-sdk-isdk-panels/)
- [Hands and controller input mappings](https://developers.meta.com/horizon/design/interactions-input-mappings/)
- [Meta Quest mobile performance guidance](https://developers.meta.com/horizon/documentation/unreal/po-perf-opt-mobile/)
