ipin-vr/docs/POC_VR_SPEC.md

5.8 KiB
Raw Permalink Blame History

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.cppadd_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.