5.8 KiB
SPEC — VR Coexistence PoC (native OpenXR, Quest 3)
Apka pomiarowa, nie produkt. Cel: udowodnić, że na Quest 3 da się jednocześnie renderować scenę VR/MR i uruchamiać lokalny LLM i mówić (TTS) — wszystko w jednym procesie, offline — oraz zmierzyć, ile to kosztuje klatki, CPU/GPU, pamięć i temperaturę.
To jest pionowy plasterek Etapu 2 z ROADMAP, zbudowany jako narzędzie do pomiaru.
1. Co mierzymy (sens istnienia apki)
Podczas pracy ciągłej (render) + cyklicznej inferencji + TTS zbieramy:
- FPS / stale frames renderera w oknie generacji vs. w spoczynku,
- CPU/GPU utilization i temperaturę w czasie (soak ~15 min),
- latencję i throughput inferencji pod obciążeniem renderera (tok/s),
- PSS całego procesu.
Pomiar: OVR Metrics Tool (nakładka FPS/CPU/GPU/temp) + log apki do logcat.
2. Kryteria „zielono" (do walidacji, nie gwarancje)
- Renderer utrzymuje docelowe ≥72 FPS w trakcie generacji, bez kaskady stale frames.
- Generacja pod obciążeniem renderera ≥ ~10 tok/s (standalone było 23,6 @4 wątki).
- Całość < 5,75 GiB PSS (limit aplikacji immersyjnej Quest 3).
- Brak throttlingu do poziomu nieużywalnego w ciągu 15 min ciągłej pracy.
3. Architektura
Natywny C++ / Android NDK, jeden proces, arm64-v8a.
Wątki:
- Render thread — pętla klatek OpenXR (passthrough + panel tekstowy). Nigdy nie blokuje się na inferencji ani TTS.
- Inference worker — llama.cpp in-process, ładuje Qwen2.5-1.5B Q4 raz, generuje na żądanie. Cap 4 wątki (wynik z benchmarku: 6 wątków regresuje przez rdzenie efficiency). CPU affinity konfigurowalna (patrz §6).
- TTS/audio thread — sherpa-onnx (offline) syntezuje PCM, oddtwarzanie przez Oboe.
Przepływ na trigger: worker generuje polecenie → tekst trafia do render threada (panel) i do TTS threada (mowa). Render leci nieprzerwanie przez cały czas.
4. Punkt startu i zależności
NIE pisać OpenXR od zera. Start z sample'a passthrough z Meta OpenXR Mobile
SDK (np. XrPassthrough); dokładamy worker LLM, TTS, panel i triggery.
Wendorowane zależności (jako podprojekty CMake / prebuilt arm64):
- llama.cpp —
add_subdirectory, build arm64-v8a, OpenMP on, bez GPU. API biblioteczne (llama+common), nie serwer. - sherpa-onnx — prebuilt Android arm64 (
OfflineTts), używa głosu pl_PL-gosia (.onnx, ten sam co na PC). Ogarnia espeak-ng. - Oboe — audio out (AAudio/OpenSL wrapper).
- stb_truetype.h — rasteryzacja tekstu polecenia do tekstury GL.
Model i głos NIE w APK: wgrywane na /sdcard/Android/data/<pkg>/files/ (lub
/data/local/tmp) i ładowane po ścieżce. Model ~935 MiB.
5. Generator treści (zgodny z zasadą gramatyki)
Port leksykonu i reguł renderowania z docs/LEXICON.md + docs/SPEC.md §5
do C++ (OBJECTS/COLORS/LOCATIONS/VERBS + render()). Zasada bez zmian: powierzchniowy
tekst tylko z leksykonu.
Rola LLM tutaj jest podwójna:
- realistyczne obciążenie obliczeniowe (prawdziwa generacja N tokenów),
- wybór elementów polecenia.
Worker odpala prawdziwą generację llama.cpp z naszym promptem; jeśli uda się sparsować JSON z ID — renderuje z nich, jeśli nie — fallback na losowy wybór z leksykonu. Tak czy siak gramatyka jest poprawna, a obciążenie CPU realne (o to chodzi w pomiarze).
6. Kontrakt wątków i CPU affinity (najważniejsze)
To tu rozstrzyga się koegzystencja. Na XR2 Gen 2 (2P + 4E):
- Render thread + kompozycja: zostawiamy je runtime'owi, ale apka nie odpala inferencji na tym samym rdzeniu.
- Inference worker: maska CPU konfigurowalna w runtime (
sched_setaffinity/pthread_setaffinity_np), domyślnie tak, by NIE zajmować rdzenia renderera. Liczba wątków konfigurowalna (domyślnie 4), priorytet niższy niż render. - TTS thread: osobny, niski priorytet.
Apka ma pozwalać zmieniać maskę i liczbę wątków bez rekompilacji (np. plik konfig albo argumenty intentu), żeby zmierzyć FPS przy różnych podziałach rdzeni.
7. UI / interakcja (minimalne)
- Passthrough on, jeden pływający panel przed użytkownikiem.
- Panel pokazuje: aktualne polecenie + live odczyt (ostatnia inferencja ms, tok/s, przybliżony FPS, PSS).
- Trigger generacji: przycisk kontrolera (np. A) oraz opcjonalny auto-loop co N sekund (do soak-testu). Bez obiektów 3D, bez scen — tylko panel. Render ma głównie istnieć i trzymać FPS.
8. Logowanie i soak
Apka loguje do logcat na każdą generację: liczba tokenów, ms, tok/s, peak VmRSS
(/proc/self/status). Tryb soak: auto-trigger co ~10 s przez 15 min; równolegle
OVR Metrics Tool zapisuje FPS/CPU/GPU/temp. Wynik = wykres FPS i temperatury w czasie.
9. Budżet pamięci (orientacyjnie, < 5,75 GiB)
- model Q4: ~1,1 GiB
- KV cache (krótki kontekst): ~50–150 MiB
- onnxruntime + głos Piper: ~0,1 GiB
- OpenXR runtime + GL + tekstury: ~0,3–0,5 GiB
- razem: ~1,6–1,9 GiB → duży zapas pod limitem.
10. Build / deploy
Android NDK + CMake + Gradle. arm64-v8a only. Loader OpenXR z Meta SDK. Podpis
debug, adb install. Min SDK zgodny z Horizon OS (API 29+). Plan budowy fazami
(patrz niżej), nie jednym strzałem.
11. Poza zakresem
STT, ocena wykonania, UX terapeutyczny, obiekty 3D, hand tracking, pakowanie do Store.
12. Fazy budowy (dla Claude Code)
- Scaffold — sample passthrough Meta uruchomiony na Queście (czysty render, FPS w OVR).
- LLM — wlinkować llama.cpp, worker ładuje model, generacja na trigger (log tok/s), na razie tekst tylko do logcat.
- Affinity — maska/wątki konfigurowalne; zmierzyć wpływ na FPS.
- TTS — sherpa-onnx + Oboe, wypowiada polecenie.
- Panel — stb_truetype, tekst + live odczyt na panelu.
- Soak + pomiar — auto-loop, logowanie, 15-min test.
Każdą fazę walidujemy na urządzeniu zanim ruszymy dalej.