ipin-vr/docs/POC_VR_SPEC.md

117 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
1. realistyczne **obciążenie obliczeniowe** (prawdziwa generacja N tokenów),
2. 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): ~50150 MiB
- onnxruntime + głos Piper: ~0,1 GiB
- OpenXR runtime + GL + tekstury: ~0,30,5 GiB
- razem: ~1,61,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)
1. **Scaffold** sample passthrough Meta uruchomiony na Queście (czysty render, FPS w OVR).
2. **LLM** wlinkować llama.cpp, worker ładuje model, generacja na trigger (log tok/s),
na razie tekst tylko do logcat.
3. **Affinity** maska/wątki konfigurowalne; zmierzyć wpływ na FPS.
4. **TTS** sherpa-onnx + Oboe, wypowiada polecenie.
5. **Panel** stb_truetype, tekst + live odczyt na panelu.
6. **Soak + pomiar** auto-loop, logowanie, 15-min test.
Każdą fazę walidujemy na urządzeniu zanim ruszymy dalej.