diff --git a/docs/POC_VR_SPEC.md b/docs/POC_VR_SPEC.md new file mode 100644 index 0000000..83ad9e2 --- /dev/null +++ b/docs/POC_VR_SPEC.md @@ -0,0 +1,116 @@ +# 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//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): ~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) +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.