diff --git a/vr-poc/.gitignore b/vr-poc/.gitignore new file mode 100644 index 0000000..4adac5f --- /dev/null +++ b/vr-poc/.gitignore @@ -0,0 +1,11 @@ +local.properties +.gradle/ +build/ +app/build/ +app/.cxx/ +*.keystore + +# SDK files (prebuilt binaries i headery — nie w git) +libs/arm64-v8a/*.so +ovr_sdk/OpenXR/Include/openxr/*.h +ovr_sdk/OpenXR/Libs/**/*.so diff --git a/vr-poc/README.md b/vr-poc/README.md new file mode 100644 index 0000000..ff21667 --- /dev/null +++ b/vr-poc/README.md @@ -0,0 +1,147 @@ +# vr-poc — Phase 1: passthrough + quad panel (native OpenXR, Quest 3) + +Natywna apka C++/NDK, arm64-v8a. Renderuje passthrough + jeden statyczny panel +przed użytkownikiem. Brak LLM, brak TTS — czysty baseline do pomiaru FPS. + +Szczegóły architektury: `../docs/POC_VR_SPEC.md`. + +--- + +## Wymagane pliki SDK + +Przed `./gradlew assembleDebug` musisz zainstalować toolchain i wrzucić dwa +zestawy plików. Poniżej dokładna lista — bez tych plików build nie przejdzie. + +### 1. Android SDK + NDK + CMake + +Zainstaluj przez Android Studio (SDK Manager) lub `sdkmanager`: + +```bash +# Jeśli masz sdkmanager (cmdline-tools): +sdkmanager "platforms;android-35" \ + "build-tools;35.0.0" \ + "ndk;27.2.12479018" \ + "cmake;3.22.1" +``` + +Oczekiwane lokalizacje (domyślna instalacja Linux): +``` +~/Android/Sdk/platforms/android-35/ +~/Android/Sdk/build-tools/35.0.0/ +~/Android/Sdk/ndk/27.2.12479018/ +~/Android/Sdk/cmake/3.22.1/ +``` + +Następnie skopiuj `local.properties.template` → `local.properties` +(plik jest w `.gitignore`): +```bash +cp local.properties.template local.properties +# Upewnij się że sdk.dir wskazuje na Twój Android/Sdk +``` + +### 2. Meta OpenXR Mobile SDK — headery + +Pobierz **Meta OpenXR Mobile SDK** z: +> https://developer.oculus.com/downloads/package/oculus-openxr-mobile-sdk/ + +Aktualnie testowana wersja: **v71** (lub nowsza). + +Po pobraniu i rozpakowaniu skopiuj **dokładnie te 4 pliki**: + +| Źródło w archiwum SDK | Cel w projekcie | +|--------------------------------------------------|-----------------------------------------------------| +| `OpenXR/Include/openxr/openxr.h` | `ovr_sdk/OpenXR/Include/openxr/openxr.h` | +| `OpenXR/Include/openxr/openxr_platform.h` | `ovr_sdk/OpenXR/Include/openxr/openxr_platform.h` | +| `OpenXR/Include/openxr/openxr_platform_defines.h`| `ovr_sdk/OpenXR/Include/openxr/openxr_platform_defines.h` | +| `OpenXR/Include/openxr/openxr_reflection.h` | `ovr_sdk/OpenXR/Include/openxr/openxr_reflection.h`| + +Szybko przez jeden `cp -r`: +```bash +SDK_DIR=~/Downloads/ovr_sdk_mobile_openxr_v71 # dopasuj nazwę folderu +cp "$SDK_DIR"/OpenXR/Include/openxr/openxr*.h \ + /home/oskar/projects/ipin-vr/vr-poc/ovr_sdk/OpenXR/Include/openxr/ +``` + +### 3. Meta OpenXR Mobile SDK — loader `.so` + +Z tego samego archiwum SDK skopiuj **loader** do `libs/arm64-v8a/`: + +| Źródło w archiwum SDK | Cel w projekcie | +|--------------------------------------------------------------------------|----------------------------------------| +| `OpenXR/Libs/Android/arm64-v8a/Release/libopenxr_loader.so` | `libs/arm64-v8a/libopenxr_loader.so` | + +```bash +cp "$SDK_DIR"/OpenXR/Libs/Android/arm64-v8a/Release/libopenxr_loader.so \ + /home/oskar/projects/ipin-vr/vr-poc/libs/arm64-v8a/ +``` + +Ten plik musi być w **dwóch miejscach** (albo możesz zrobić symlink): +- `libs/arm64-v8a/libopenxr_loader.so` — dla Gradle (pakuje do APK jako `lib/arm64-v8a/`) +- CMakeLists.txt czyta go z tej samej lokalizacji do linkowania + +--- + +## Build i deploy + +```bash +cd vr-poc/ + +# Weryfikacja: upewnij się że te pliki istnieją +ls libs/arm64-v8a/libopenxr_loader.so +ls ovr_sdk/OpenXR/Include/openxr/openxr.h + +# Build APK (debug) +./gradlew assembleDebug + +# Install na Quest 3 (włącz Developer Mode, podłącz USB) +adb install -r app/build/outputs/apk/debug/app-debug.apk + +# Logi z apki +adb logcat -s ipin-vr-poc +``` + +APK output: `app/build/outputs/apk/debug/app-debug.apk` + +--- + +## Weryfikacja Phase 1 (Definition of Done) + +- [ ] `./gradlew assembleDebug` — SUCCESS +- [ ] `adb install` — wchodzi na urządzenie +- [ ] Apka startuje w passthrough (widać świat przez kamery) +- [ ] Pływający ciemnoniebieski panel widoczny ~1 m przed użytkownikiem +- [ ] OVR Metrics Tool pokazuje ≥ 72 FPS (brak stale frames) + +Obserwacja FPS: uruchom OVR Metrics Tool na urządzeniu przed startem apki. + +--- + +## Struktura kodu + +``` +vr-poc/ + app/ + CMakeLists.txt # CMake: native_app_glue + openxr_loader + ipin_vr_poc + src/main/ + AndroidManifest.xml # NativeActivity, com.oculus.feature.PASSTHROUGH + cpp/ + main.cpp # Cały OpenXR: EGL + passthrough + quad layer + res/values/strings.xml + libs/ + arm64-v8a/ + libopenxr_loader.so # [WRZUĆ TU — patrz §2/§3 powyżej] + ovr_sdk/ + OpenXR/Include/openxr/ # [WRZUĆ TU headery — patrz §2 powyżej] +``` + +--- + +## Następne fazy (wg POC_VR_SPEC.md §12) + +| Faza | Co dodajemy | +|------|-------------------------------------------------------| +| 2 | llama.cpp worker, generacja na trigger (log tok/s) | +| 3 | CPU affinity konfigurowalna, pomiar wpływu na FPS | +| 4 | sherpa-onnx + Oboe TTS | +| 5 | stb_truetype — tekst polecenia na panelu | +| 6 | Soak 15 min, logowanie, wykres FPS/temp | diff --git a/vr-poc/app/CMakeLists.txt b/vr-poc/app/CMakeLists.txt new file mode 100644 index 0000000..7513d75 --- /dev/null +++ b/vr-poc/app/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.22) +project(ipin_vr_poc VERSION 1 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ── Paths into ovr_sdk/ (one directory above app/) ─────────────────────────── +set(OVR_SDK_ROOT "${CMAKE_SOURCE_DIR}/../ovr_sdk") +set(OPENXR_INCLUDE "${OVR_SDK_ROOT}/OpenXR/Include") +set(OPENXR_LIB_DIR "${CMAKE_SOURCE_DIR}/../libs/arm64-v8a") + +if (NOT EXISTS "${OPENXR_INCLUDE}/openxr/openxr.h") + message(FATAL_ERROR + "openxr.h not found at ${OPENXR_INCLUDE}/openxr/openxr.h\n" + "See vr-poc/README.md §'Wymagane pliki SDK' for setup instructions.") +endif() + +# ── android_native_app_glue (from NDK) ─────────────────────────────────────── +add_library(native_app_glue STATIC + ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c +) +target_include_directories(native_app_glue PUBLIC + ${ANDROID_NDK}/sources/android/native_app_glue +) +target_link_libraries(native_app_glue PUBLIC android log) + +# ── OpenXR loader (prebuilt, from Meta OVR SDK → libs/arm64-v8a/) ───────────── +add_library(openxr_loader SHARED IMPORTED) +set_target_properties(openxr_loader PROPERTIES + IMPORTED_LOCATION "${OPENXR_LIB_DIR}/libopenxr_loader.so" +) + +# ── NDK system libraries ────────────────────────────────────────────────────── +find_library(LIB_ANDROID android) +find_library(LIB_LOG log) +find_library(LIB_EGL EGL) +find_library(LIB_GLES3 GLESv3) + +# ── Main native library ─────────────────────────────────────────────────────── +add_library(ipin_vr_poc SHARED + src/main/cpp/main.cpp +) + +target_include_directories(ipin_vr_poc PRIVATE + ${OPENXR_INCLUDE} + ${ANDROID_NDK}/sources/android/native_app_glue +) + +target_compile_definitions(ipin_vr_poc PRIVATE + XR_USE_PLATFORM_ANDROID + XR_USE_GRAPHICS_API_OPENGL_ES +) + +target_link_libraries(ipin_vr_poc PRIVATE + native_app_glue + openxr_loader + ${LIB_ANDROID} + ${LIB_LOG} + ${LIB_EGL} + ${LIB_GLES3} +) + +# Force export of android_main (entry point for NativeActivity) +target_link_options(ipin_vr_poc PRIVATE "-Wl,--undefined=android_main") diff --git a/vr-poc/app/build.gradle b/vr-poc/app/build.gradle new file mode 100644 index 0000000..bf26a32 --- /dev/null +++ b/vr-poc/app/build.gradle @@ -0,0 +1,66 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.ipin.vr.poc' + compileSdk 35 + + defaultConfig { + applicationId "com.ipin.vr.poc" + minSdk 29 + targetSdk 32 + versionCode 1 + versionName "1.0.0-phase1" + + ndk { + abiFilters "arm64-v8a" + } + + externalNativeBuild { + cmake { + cppFlags "-std=c++17 -O2 -fvisibility=hidden" + arguments "-DANDROID_STL=c++_shared", + "-DANDROID_ARM_NEON=TRUE" + } + } + } + + ndkVersion "27.2.12479018" + + externalNativeBuild { + cmake { + path "CMakeLists.txt" + version "3.22.1" + } + } + + buildTypes { + debug { + debuggable true + jniDebuggable true + minifyEnabled false + } + release { + minifyEnabled false + signingConfig signingConfigs.debug + } + } + + sourceSets { + main { + // Prebuilt libopenxr_loader.so — user drops it in libs/arm64-v8a/ + jniLibs.srcDirs += ['../libs'] + } + } + + // Keep .so symbols in debug builds for logcat crash symbolication + packagingOptions { + doNotStrip "**/*.so" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } +} diff --git a/vr-poc/app/src/main/AndroidManifest.xml b/vr-poc/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..28fd43f --- /dev/null +++ b/vr-poc/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vr-poc/app/src/main/cpp/main.cpp b/vr-poc/app/src/main/cpp/main.cpp new file mode 100644 index 0000000..4880ec7 --- /dev/null +++ b/vr-poc/app/src/main/cpp/main.cpp @@ -0,0 +1,484 @@ +// ipin-vr-poc — Phase 1: passthrough + static quad panel +// No LLM, no TTS. Render-only baseline for FPS measurement (§12 Phase 1). +// +// Build requires: +// - Meta OpenXR Mobile SDK headers in ovr_sdk/OpenXR/Include/ +// - libopenxr_loader.so in libs/arm64-v8a/ +// See README.md §"Wymagane pliki SDK". + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "ipin-vr-poc" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) + +// Aborts on XrResult failure — only used during init where recovery is not possible. +#define CHK_XR(expr) \ + do { \ + XrResult _r = (expr); \ + if (XR_FAILED(_r)) { \ + LOGE("XR FAIL %d at " __FILE__ ":%d " #expr, _r, __LINE__); \ + return false; \ + } \ + } while (0) + +// ── XR_FB_passthrough dynamic function pointers ─────────────────────────────── +static PFN_xrCreatePassthroughFB pfn_xrCreatePassthroughFB = nullptr; +static PFN_xrDestroyPassthroughFB pfn_xrDestroyPassthroughFB = nullptr; +static PFN_xrPassthroughStartFB pfn_xrPassthroughStartFB = nullptr; +static PFN_xrPassthroughPauseFB pfn_xrPassthroughPauseFB = nullptr; +static PFN_xrCreatePassthroughLayerFB pfn_xrCreatePassthroughLayerFB = nullptr; +static PFN_xrDestroyPassthroughLayerFB pfn_xrDestroyPassthroughLayerFB = nullptr; +static PFN_xrPassthroughLayerResumeFB pfn_xrPassthroughLayerResumeFB = nullptr; +static PFN_xrPassthroughLayerPauseFB pfn_xrPassthroughLayerPauseFB = nullptr; + +// ── EGL context (headless PBuffer — required by OpenXR GLES binding) ───────── +struct EglCtx { + EGLDisplay display = EGL_NO_DISPLAY; + EGLConfig config = {}; + EGLContext context = EGL_NO_CONTEXT; + EGLSurface surface = EGL_NO_SURFACE; + + bool init() { + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display == EGL_NO_DISPLAY) { LOGE("eglGetDisplay failed"); return false; } + + EGLint major = 0, minor = 0; + if (!eglInitialize(display, &major, &minor)) { + LOGE("eglInitialize failed"); return false; + } + LOGI("EGL %d.%d", major, minor); + + const EGLint cfg_attrs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 0, + EGL_NONE + }; + EGLint n = 0; + if (!eglChooseConfig(display, cfg_attrs, &config, 1, &n) || n == 0) { + LOGE("eglChooseConfig failed"); return false; + } + + const EGLint ctx_attrs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctx_attrs); + if (context == EGL_NO_CONTEXT) { LOGE("eglCreateContext failed"); return false; } + + // Tiny PBuffer — we never render to it, OpenXR swapchain images are the real targets + const EGLint pb_attrs[] = { EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE }; + surface = eglCreatePbufferSurface(display, config, pb_attrs); + if (surface == EGL_NO_SURFACE) { LOGE("eglCreatePbufferSurface failed"); return false; } + + if (!eglMakeCurrent(display, surface, surface, context)) { + LOGE("eglMakeCurrent failed"); return false; + } + LOGI("GLES3 context ready"); + return true; + } + + void destroy() { + if (display != EGL_NO_DISPLAY) { + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (context != EGL_NO_CONTEXT) eglDestroyContext(display, context); + if (surface != EGL_NO_SURFACE) eglDestroySurface(display, surface); + eglTerminate(display); + } + display = EGL_NO_DISPLAY; + context = EGL_NO_CONTEXT; + surface = EGL_NO_SURFACE; + } +}; + +// ── Application state ───────────────────────────────────────────────────────── +struct App { + android_app* native_app = nullptr; + EglCtx egl; + + XrInstance instance = XR_NULL_HANDLE; + XrSystemId system_id = XR_NULL_SYSTEM_ID; + XrSession session = XR_NULL_HANDLE; + XrSpace ref_space = XR_NULL_HANDLE; + + // XR_FB_passthrough + XrPassthroughFB passthrough = XR_NULL_HANDLE; + XrPassthroughLayerFB passthrough_layer = XR_NULL_HANDLE; + bool has_passthrough = false; + + // Quad swapchain — one 2D panel floating in front of the user + XrSwapchain quad_swapchain = XR_NULL_HANDLE; + uint32_t quad_w = 512; + uint32_t quad_h = 256; + std::vector quad_images; + + // Session state machine + XrSessionState session_state = XR_SESSION_STATE_UNKNOWN; + bool session_running = false; + bool exit_requested = false; +}; + +static App g_app; + +// ───────────────────────────────────────────────────────────────────────────── + +static void load_ext_fns(XrInstance inst) { +#define LOAD(fn) xrGetInstanceProcAddr(inst, #fn, (PFN_xrVoidFunction*)&pfn_##fn) + LOAD(xrCreatePassthroughFB); + LOAD(xrDestroyPassthroughFB); + LOAD(xrPassthroughStartFB); + LOAD(xrPassthroughPauseFB); + LOAD(xrCreatePassthroughLayerFB); + LOAD(xrDestroyPassthroughLayerFB); + LOAD(xrPassthroughLayerResumeFB); + LOAD(xrPassthroughLayerPauseFB); +#undef LOAD + if (!pfn_xrCreatePassthroughFB) + LOGW("XR_FB_passthrough functions not loaded — passthrough disabled"); +} + +// ── Init ────────────────────────────────────────────────────────────────────── + +static bool create_instance(App& app) { + XrInstanceCreateInfoAndroidKHR android_info{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR}; + android_info.applicationVM = app.native_app->activity->vm; + android_info.applicationContext = app.native_app->activity->clazz; + + const char* extensions[] = { + XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, // "XR_KHR_android_create_instance" + XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME, // "XR_KHR_opengl_es_enable" + XR_FB_PASSTHROUGH_EXTENSION_NAME, // "XR_FB_passthrough" + }; + + XrApplicationInfo app_info{}; + strncpy(app_info.applicationName, "ipin-vr-poc", XR_MAX_APPLICATION_NAME_SIZE - 1); + app_info.apiVersion = XR_CURRENT_API_VERSION; + + XrInstanceCreateInfo ci{XR_TYPE_INSTANCE_CREATE_INFO}; + ci.next = &android_info; + ci.applicationInfo = app_info; + ci.enabledExtensionCount = static_cast(std::size(extensions)); + ci.enabledExtensionNames = extensions; + + CHK_XR(xrCreateInstance(&ci, &app.instance)); + + XrInstanceProperties ip{XR_TYPE_INSTANCE_PROPERTIES}; + xrGetInstanceProperties(app.instance, &ip); + LOGI("XrInstance OK — runtime: %s", ip.runtimeName); + return true; +} + +static bool get_system(App& app) { + XrSystemGetInfo sgi{XR_TYPE_SYSTEM_GET_INFO}; + sgi.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + CHK_XR(xrGetSystem(app.instance, &sgi, &app.system_id)); + + XrSystemProperties sp{XR_TYPE_SYSTEM_PROPERTIES}; + xrGetSystemProperties(app.instance, app.system_id, &sp); + LOGI("System: %s", sp.systemName); + return true; +} + +static bool create_session(App& app) { + // Verify GLES is supported by this system + XrGraphicsRequirementsOpenGLESKHR gfx_req{XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR}; + PFN_xrGetOpenGLESGraphicsRequirementsKHR pfn_gfx_req = nullptr; + xrGetInstanceProcAddr(app.instance, "xrGetOpenGLESGraphicsRequirementsKHR", + (PFN_xrVoidFunction*)&pfn_gfx_req); + if (pfn_gfx_req) + pfn_gfx_req(app.instance, app.system_id, &gfx_req); + + XrGraphicsBindingOpenGLESAndroidKHR binding{XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR}; + binding.display = app.egl.display; + binding.config = app.egl.config; + binding.context = app.egl.context; + + XrSessionCreateInfo sci{XR_TYPE_SESSION_CREATE_INFO}; + sci.next = &binding; + sci.systemId = app.system_id; + CHK_XR(xrCreateSession(app.instance, &sci, &app.session)); + LOGI("XrSession created"); + + // LOCAL space: floating panel stays fixed relative to room origin at startup + XrReferenceSpaceCreateInfo rsci{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + rsci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; + rsci.poseInReferenceSpace = {{0.f, 0.f, 0.f, 1.f}, {0.f, 0.f, 0.f}}; + CHK_XR(xrCreateReferenceSpace(app.session, &rsci, &app.ref_space)); + return true; +} + +static bool init_passthrough(App& app) { + if (!pfn_xrCreatePassthroughFB) { + app.has_passthrough = false; + return true; + } + + XrPassthroughCreateInfoFB ptci{XR_TYPE_PASSTHROUGH_CREATE_INFO_FB}; + ptci.flags = XR_PASSTHROUGH_IS_RUNNING_AT_CREATION_BIT_FB; + if (XR_FAILED(pfn_xrCreatePassthroughFB(app.session, &ptci, &app.passthrough))) { + LOGW("xrCreatePassthroughFB failed — running without passthrough"); + app.has_passthrough = false; + return true; + } + + XrPassthroughLayerCreateInfoFB ptlci{XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB}; + ptlci.passthrough = app.passthrough; + ptlci.flags = XR_PASSTHROUGH_IS_RUNNING_AT_CREATION_BIT_FB; + ptlci.purpose = XR_PASSTHROUGH_LAYER_PURPOSE_RECONSTRUCTION_FB; + if (XR_FAILED(pfn_xrCreatePassthroughLayerFB(app.session, &ptlci, &app.passthrough_layer))) { + LOGW("xrCreatePassthroughLayerFB failed — running without passthrough"); + pfn_xrDestroyPassthroughFB(app.passthrough); + app.passthrough = XR_NULL_HANDLE; + app.has_passthrough = false; + return true; + } + + app.has_passthrough = true; + LOGI("Passthrough enabled (XR_FB_passthrough)"); + return true; +} + +static bool create_quad_swapchain(App& app) { + // Pick a supported swapchain format — prefer SRGB8_ALPHA8 + uint32_t fmt_count = 0; + xrEnumerateSwapchainFormats(app.session, 0, &fmt_count, nullptr); + std::vector fmts(fmt_count); + xrEnumerateSwapchainFormats(app.session, fmt_count, &fmt_count, fmts.data()); + + int64_t chosen = fmts.empty() ? GL_RGBA8 : fmts[0]; + for (int64_t f : fmts) { + if (f == GL_SRGB8_ALPHA8) { chosen = f; break; } + if (f == GL_RGBA8) { chosen = f; } + } + LOGI("Quad swapchain format: 0x%llx", (unsigned long long)chosen); + + XrSwapchainCreateInfo sci{XR_TYPE_SWAPCHAIN_CREATE_INFO}; + sci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_SAMPLED_BIT; + sci.format = chosen; + sci.sampleCount = 1; + sci.width = app.quad_w; + sci.height = app.quad_h; + sci.faceCount = 1; + sci.arraySize = 1; + sci.mipCount = 1; + CHK_XR(xrCreateSwapchain(app.session, &sci, &app.quad_swapchain)); + + uint32_t img_count = 0; + xrEnumerateSwapchainImages(app.quad_swapchain, 0, &img_count, nullptr); + app.quad_images.resize(img_count, {XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR}); + xrEnumerateSwapchainImages(app.quad_swapchain, img_count, &img_count, + reinterpret_cast(app.quad_images.data())); + + // Paint all swapchain images once: dark semi-transparent panel. + // Phase 5 will rasterise Polish text here with stb_truetype. + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + for (const auto& img : app.quad_images) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, img.image, 0); + glViewport(0, 0, (GLsizei)app.quad_w, (GLsizei)app.quad_h); + // Navy panel: R=0.05 G=0.05 B=0.18 A=0.88 — visible on passthrough, not blinding + glClearColor(0.05f, 0.05f, 0.18f, 0.88f); + glClear(GL_COLOR_BUFFER_BIT); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + + LOGI("Quad swapchain OK: %ux%u, %u images", app.quad_w, app.quad_h, img_count); + return true; +} + +static bool openxr_init(App& app) { + if (!app.egl.init()) return false; + if (!create_instance(app)) return false; + load_ext_fns(app.instance); + if (!get_system(app)) return false; + if (!create_session(app)) return false; + if (!init_passthrough(app)) return false; + if (!create_quad_swapchain(app)) return false; + return true; +} + +// ── Frame loop ──────────────────────────────────────────────────────────────── + +static void render_frame(App& app) { + XrFrameWaitInfo fwi{XR_TYPE_FRAME_WAIT_INFO}; + XrFrameState fs {XR_TYPE_FRAME_STATE}; + if (XR_FAILED(xrWaitFrame(app.session, &fwi, &fs))) return; + + XrFrameBeginInfo fbi{XR_TYPE_FRAME_BEGIN_INFO}; + if (XR_FAILED(xrBeginFrame(app.session, &fbi))) return; + + std::vector layers; + + // ── Layer 0: passthrough (world background) ─────────────────────────────── + XrCompositionLayerPassthroughFB pt_layer{XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB}; + if (app.has_passthrough) { + pt_layer.flags = 0; + pt_layer.space = XR_NULL_HANDLE; // required by spec + pt_layer.layerHandle = app.passthrough_layer; + layers.push_back(reinterpret_cast(&pt_layer)); + } + + // ── Layer 1: quad panel ──────────────────────────────────────────────────── + XrCompositionLayerQuad quad_layer{XR_TYPE_COMPOSITION_LAYER_QUAD}; + if (fs.shouldRender) { + XrSwapchainImageAcquireInfo ai{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; + uint32_t idx = 0; + xrAcquireSwapchainImage(app.quad_swapchain, &ai, &idx); + + XrSwapchainImageWaitInfo wi{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; + wi.timeout = XR_INFINITE_DURATION; + xrWaitSwapchainImage(app.quad_swapchain, &wi); + + // Content is static (painted at init). + // Phase 5: re-render text here when polecenie changes. + + XrSwapchainImageReleaseInfo ri{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + xrReleaseSwapchainImage(app.quad_swapchain, &ri); + + // Panel position: centred, 1 m in front, 25 cm below eye level + XrPosef pose{}; + pose.orientation = {0.f, 0.f, 0.f, 1.f}; // identity — faces the user + pose.position = {0.f, -0.25f, -1.0f}; // x=0, y=-25cm, z=-1m + + quad_layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + quad_layer.space = app.ref_space; + quad_layer.eyeVisibility = XR_EYE_VISIBILITY_BOTH; + quad_layer.subImage.swapchain = app.quad_swapchain; + quad_layer.subImage.imageRect.offset = {0, 0}; + quad_layer.subImage.imageRect.extent = {(int32_t)app.quad_w, (int32_t)app.quad_h}; + quad_layer.subImage.imageArrayIndex = 0; + quad_layer.pose = pose; + quad_layer.size = {0.80f, 0.40f}; // 80 cm × 40 cm + + layers.push_back(reinterpret_cast(&quad_layer)); + } + + XrFrameEndInfo fei{XR_TYPE_FRAME_END_INFO}; + fei.displayTime = fs.predictedDisplayTime; + // OPAQUE + passthrough layer = real world visible through FB_passthrough + fei.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + fei.layerCount = static_cast(layers.size()); + fei.layers = layers.empty() ? nullptr : layers.data(); + xrEndFrame(app.session, &fei); +} + +// ── Event handling ──────────────────────────────────────────────────────────── + +static void poll_xr_events(App& app) { + XrEventDataBuffer ev{XR_TYPE_EVENT_DATA_BUFFER}; + + while (xrPollEvent(app.instance, &ev) == XR_SUCCESS) { + switch (ev.type) { + case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { + const auto* e = reinterpret_cast(&ev); + app.session_state = e->state; + LOGI("Session state → %d", (int)app.session_state); + + switch (e->state) { + case XR_SESSION_STATE_READY: { + XrSessionBeginInfo sbi{XR_TYPE_SESSION_BEGIN_INFO}; + sbi.primaryViewConfigurationType = + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + if (XR_SUCCEEDED(xrBeginSession(app.session, &sbi))) { + app.session_running = true; + LOGI("Session started"); + } + break; + } + case XR_SESSION_STATE_STOPPING: + xrEndSession(app.session); + app.session_running = false; + break; + case XR_SESSION_STATE_EXITING: + case XR_SESSION_STATE_LOSS_PENDING: + app.exit_requested = true; + break; + default: + break; + } + break; + } + case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: + app.exit_requested = true; + break; + default: + break; + } + ev = {XR_TYPE_EVENT_DATA_BUFFER}; + } +} + +// ── Android lifecycle ───────────────────────────────────────────────────────── + +static void on_app_cmd(android_app* a, int32_t cmd) { + (void)a; (void)cmd; + // NativeActivity lifecycle: OpenXR session state machine drives rendering. +} + +// ── Entry point ─────────────────────────────────────────────────────────────── + +void android_main(android_app* state) { + LOGI("=== ipin-vr-poc phase 1 start ==="); + + g_app.native_app = state; + state->userData = &g_app; + state->onAppCmd = on_app_cmd; + + if (!openxr_init(g_app)) { + LOGE("OpenXR init failed — aborting"); + return; + } + + LOGI("Entering main loop"); + while (!g_app.exit_requested) { + // Drain Android event queue (0 = non-blocking poll) + int events = 0; + android_poll_source* src = nullptr; + while (ALooper_pollAll(0, nullptr, &events, + reinterpret_cast(&src)) >= 0) { + if (src) src->process(state, src); + if (state->destroyRequested) { + g_app.exit_requested = true; + break; + } + } + + poll_xr_events(g_app); + + const bool can_render = + g_app.session_running && + (g_app.session_state == XR_SESSION_STATE_VISIBLE || + g_app.session_state == XR_SESSION_STATE_FOCUSED); + + if (can_render) render_frame(g_app); + } + + LOGI("Shutting down"); + if (g_app.passthrough_layer != XR_NULL_HANDLE) + pfn_xrDestroyPassthroughLayerFB(g_app.passthrough_layer); + if (g_app.passthrough != XR_NULL_HANDLE) + pfn_xrDestroyPassthroughFB(g_app.passthrough); + if (g_app.quad_swapchain != XR_NULL_HANDLE) + xrDestroySwapchain(g_app.quad_swapchain); + if (g_app.ref_space != XR_NULL_HANDLE) + xrDestroySpace(g_app.ref_space); + if (g_app.session != XR_NULL_HANDLE) + xrDestroySession(g_app.session); + if (g_app.instance != XR_NULL_HANDLE) + xrDestroyInstance(g_app.instance); + g_app.egl.destroy(); + LOGI("=== ipin-vr-poc phase 1 exit ==="); +} diff --git a/vr-poc/app/src/main/res/values/strings.xml b/vr-poc/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e9b4aae --- /dev/null +++ b/vr-poc/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + ipin-vr poc + diff --git a/vr-poc/build.gradle b/vr-poc/build.gradle new file mode 100644 index 0000000..5b7ccba --- /dev/null +++ b/vr-poc/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/vr-poc/gradle/wrapper/gradle-wrapper.jar b/vr-poc/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/vr-poc/gradle/wrapper/gradle-wrapper.jar differ diff --git a/vr-poc/gradle/wrapper/gradle-wrapper.properties b/vr-poc/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cea7a79 --- /dev/null +++ b/vr-poc/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/vr-poc/gradlew b/vr-poc/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/vr-poc/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/vr-poc/gradlew.bat b/vr-poc/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/vr-poc/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/vr-poc/libs/arm64-v8a/.gitkeep b/vr-poc/libs/arm64-v8a/.gitkeep new file mode 100644 index 0000000..e69c48c --- /dev/null +++ b/vr-poc/libs/arm64-v8a/.gitkeep @@ -0,0 +1,2 @@ +# Tu wrzuć libopenxr_loader.so z Meta OpenXR Mobile SDK +# Szczegóły: vr-poc/README.md §"Wymagane pliki SDK" diff --git a/vr-poc/local.properties.template b/vr-poc/local.properties.template new file mode 100644 index 0000000..f9ad705 --- /dev/null +++ b/vr-poc/local.properties.template @@ -0,0 +1,3 @@ +# Skopiuj do local.properties (nie trafia do git) +sdk.dir=/home/oskar/Android/Sdk +# ndk.dir jest opcjonalne gdy ndkVersion jest w build.gradle diff --git a/vr-poc/ovr_sdk/OpenXR/Include/openxr/.gitkeep b/vr-poc/ovr_sdk/OpenXR/Include/openxr/.gitkeep new file mode 100644 index 0000000..5b2d732 --- /dev/null +++ b/vr-poc/ovr_sdk/OpenXR/Include/openxr/.gitkeep @@ -0,0 +1,3 @@ +# Tu wrzuć headery z Meta OpenXR Mobile SDK: +# openxr.h openxr_platform.h openxr_reflection.h openxr_platform_defines.h +# Szczegóły: vr-poc/README.md §"Wymagane pliki SDK" diff --git a/vr-poc/ovr_sdk/OpenXR/Libs/Android/arm64-v8a/.gitkeep b/vr-poc/ovr_sdk/OpenXR/Libs/Android/arm64-v8a/.gitkeep new file mode 100644 index 0000000..96b29cd --- /dev/null +++ b/vr-poc/ovr_sdk/OpenXR/Libs/Android/arm64-v8a/.gitkeep @@ -0,0 +1,3 @@ +# Tu wrzuć libopenxr_loader.so (Release) +# Ale prościej: skopiuj do vr-poc/libs/arm64-v8a/ — jeden plik, jedno miejsce. +# Szczegóły: vr-poc/README.md §"Wymagane pliki SDK" diff --git a/vr-poc/settings.gradle b/vr-poc/settings.gradle new file mode 100644 index 0000000..9b0809a --- /dev/null +++ b/vr-poc/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "ipin-vr-poc" +include ':app'