diff --git a/apps/darkroom-android/src/lib.rs b/apps/darkroom-android/src/lib.rs index fda94ea..5dcd620 100644 --- a/apps/darkroom-android/src/lib.rs +++ b/apps/darkroom-android/src/lib.rs @@ -451,8 +451,13 @@ fn unpack_bundled_models(app: &slint::android::AndroidApp) { // on a first launch they were not on disk until this line. The runtime // is in the APK's native library directory beside `libdarkroom.so`, // which is also where Qualcomm's DSP loader has to be pointed for the - // Hexagon skel (docs/dev/inference.md §3, §8). - dr_ui::inference::init(native_library_dir().into_iter().collect()); + // Hexagon skel (docs/dev/inference.md §3, §8). Two of them: the QNN + // build, and the generic WebGPU build by its file name, which the + // engine opens only when the first does not fit the SoC (§3.2). + let runtimes = native_library_dir() + .map(|dir| vec![dir.clone(), dir.join("libonnxruntime_generic.so")]) + .unwrap_or_default(); + dr_ui::inference::init(runtimes); } /// The directory the system unpacked this APK's native libraries into. diff --git a/core/dr-inference-engine/src/api.rs b/core/dr-inference-engine/src/api.rs index fe03302..93b8d0d 100644 --- a/core/dr-inference-engine/src/api.rs +++ b/core/dr-inference-engine/src/api.rs @@ -58,7 +58,10 @@ pub fn ensure_installed() { /// Best fit, not first found (§3.2): a device can hold several runtimes — /// the package's OpenVINO build, a CUDA build the user fetched, the /// distribution's ROCm build — and each carries one vendor's providers. -/// Between equals, the earlier directory wins, as it always has. +/// Between equals, the earlier directory wins, as it always has, and a +/// runtime that fits perfectly ends the search: the APK's QNN build on a +/// Qualcomm tablet is found first, and the generic build beside it is +/// never opened there. /// `DARKROOM_ORT_DIR`, when it loads, wins outright: it is how a person /// says which runtime they mean. pub fn install(dirs: &[PathBuf]) -> Runtime { @@ -110,7 +113,11 @@ fn install_best(dirs: &[PathBuf]) -> Option { found.push(f); break; } + let perfect = gpus.score(&f.providers) >= crate::hardware::PERFECT; found.push(f); + if perfect { + break; + } } Err(e) => log::info!("inference: no runtime in {}: {e}", dir.display()), } diff --git a/core/dr-inference-engine/src/hardware.rs b/core/dr-inference-engine/src/hardware.rs index 443b775..c26369c 100644 --- a/core/dr-inference-engine/src/hardware.rs +++ b/core/dr-inference-engine/src/hardware.rs @@ -19,6 +19,10 @@ pub struct Gpus { pub qualcomm: bool, } +/// The score of a runtime whose vendor rung matches the device's GPU. +/// Nothing beats it, so the search stops there. +pub const PERFECT: u32 = 3; + impl Gpus { /// How well a runtime offering `providers` fits this device. The vendor /// rungs score above OpenVINO because a machine with an Intel iGPU and @@ -29,10 +33,10 @@ impl Gpus { providers .iter() .map(|p| match p.as_str() { - "TensorrtExecutionProvider" | "CUDAExecutionProvider" if self.nvidia => 3, - "MIGraphXExecutionProvider" if self.amd_rocm => 3, - "QNNExecutionProvider" if self.qualcomm => 3, - "CoreMLExecutionProvider" => 3, + "TensorrtExecutionProvider" | "CUDAExecutionProvider" if self.nvidia => PERFECT, + "MIGraphXExecutionProvider" if self.amd_rocm => PERFECT, + "QNNExecutionProvider" if self.qualcomm => PERFECT, + "CoreMLExecutionProvider" => PERFECT, "OpenVINOExecutionProvider" if self.intel => 2, "WebGpuExecutionProvider" => 1, _ => 0, @@ -91,9 +95,21 @@ pub fn detect() -> Gpus { #[cfg(target_os = "android")] pub fn detect() -> Gpus { + // `ro.soc.manufacturer` exists from Android 12. Qualcomm's FastRPC + // library, which the Hexagon path loads anyway, is the second witness: + // a Qualcomm device read as anything else would trade its NPU for the + // generic rung. let soc = crate::probe::system_property("ro.soc.manufacturer"); + let fastrpc = [ + "/vendor/lib64/libcdsprpc.so", + "/system/vendor/lib64/libcdsprpc.so", + ] + .iter() + .any(|p| std::path::Path::new(p).exists()); Gpus { - qualcomm: soc.eq_ignore_ascii_case("QTI") || soc.eq_ignore_ascii_case("Qualcomm"), + qualcomm: soc.eq_ignore_ascii_case("QTI") + || soc.eq_ignore_ascii_case("Qualcomm") + || fastrpc, ..Gpus::default() } } diff --git a/docker/android/assemble-apk.sh b/docker/android/assemble-apk.sh index ca0aaba..1363cb1 100755 --- a/docker/android/assemble-apk.sh +++ b/docker/android/assemble-apk.sh @@ -263,10 +263,12 @@ cp "${DEX}" "${OUT}/staging/classes.dex" # native library directory. The build links none of it — the app dlopens # `libonnxruntime.so` at launch and runs on tract if it is not there — so an # APK without these is a slower app, not a broken one, and `RUNTIME_DIR=none` -# builds exactly that. 174 MB for the default set; the script says which -# Hexagon generations that buys. +# builds exactly that. 206 MB for the default set — 32 MB of it the generic +# WebGPU build for a phone without a Qualcomm SoC; the script says which +# Hexagon generations the rest buys. if [[ "${RUNTIME_DIR}" != "none" ]]; then - if [[ ! -f "${RUNTIME_DIR}/lib/libonnxruntime.so" ]]; then + if [[ ! -f "${RUNTIME_DIR}/lib/libonnxruntime.so" \ + || ! -f "${RUNTIME_DIR}/lib/libonnxruntime_generic.so" ]]; then "${REPO}/tools/fetch-android-runtime.sh" "${RUNTIME_DIR}" fi cp "${RUNTIME_DIR}"/lib/*.so "${OUT}/staging/lib/${ABI}/" diff --git a/docs/dev/traceability.md b/docs/dev/traceability.md index 373954e..12fa52d 100644 --- a/docs/dev/traceability.md +++ b/docs/dev/traceability.md @@ -141,7 +141,7 @@ _None._ | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/runner.rs:1`](../../core/dr-catalog/src/runner.rs#L1), [`core/dr-catalog/tests/every_queued_kind_has_a_consumer.rs:1`](../../core/dr-catalog/tests/every_queued_kind_has_a_consumer.rs#L1) | | FR-PLAT-AND-4 | [`core/dr-catalog/src/runner.rs:1`](../../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:166`](../../apps/darkroom-android/src/lib.rs#L166), [`core/dr-gpu/src/adjust.rs:1472`](../../core/dr-gpu/src/adjust.rs#L1472), [`core/dr-gpu/src/detail.rs:161`](../../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:533`](../../core/dr-gpu/src/detail.rs#L533), [`core/dr-gpu/tests/detail_stage.rs:415`](../../core/dr-gpu/tests/detail_stage.rs#L415), [`ui/dr-ui/src/develop/render.rs:654`](../../ui/dr-ui/src/develop/render.rs#L654), [`ui/dr-ui/src/identity_ui.rs:125`](../../ui/dr-ui/src/identity_ui.rs#L125), [`ui/dr-ui/src/lib.rs:1309`](../../ui/dr-ui/src/lib.rs#L1309), [`ui/dr-ui/src/lib.rs:1745`](../../ui/dr-ui/src/lib.rs#L1745), [`ui/dr-ui/src/memory.rs:163`](../../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../../ui/dr-ui/src/memory.rs#L1) | -| FR-PLAT-AND-6 | [`apps/darkroom-android/src/lib.rs:472`](../../apps/darkroom-android/src/lib.rs#L472) | +| FR-PLAT-AND-6 | [`apps/darkroom-android/src/lib.rs:477`](../../apps/darkroom-android/src/lib.rs#L477) | | FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/dirs.rs:1`](../../platform/dr-plat/src/dirs.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:1486`](../../ui/dr-ui/src/lib.rs#L1486), [`ui/dr-ui/src/preset_store.rs:1`](../../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../../ui/dr-ui/src/settings_store.rs#L1) | | FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../../platform/dr-plat/src/display/x11.rs#L1) | | FR-PLAT-WIN-1 | [`platform/dr-plat/src/dirs.rs:1`](../../platform/dr-plat/src/dirs.rs#L1) | diff --git a/tools/fetch-android-runtime.sh b/tools/fetch-android-runtime.sh index 3f622b8..75c693b 100755 --- a/tools/fetch-android-runtime.sh +++ b/tools/fetch-android-runtime.sh @@ -3,17 +3,23 @@ # # ./tools/fetch-android-runtime.sh [DEST] # -# Two Maven artefacts, pinned to each other by ONNX Runtime's own POM: +# Three Maven artefacts, the first two pinned to each other by ONNX Runtime's +# own POM: # # com.microsoft.onnxruntime:onnxruntime-android-qnn MIT # com.qualcomm.qti:qnn-runtime Qualcomm AI Engine Direct SDK licence +# com.microsoft.onnxruntime:onnxruntime-android MIT # -# The first is ONNX Runtime built with the CPU, QNN, XNNPACK, NNAPI and WebGPU -# providers; the second is Qualcomm's HTP backend — the ARM-side compiler and -# the per-generation Hexagon "skel" the DSP loads. Both ship as AARs whose -# `jni/arm64-v8a/` is what an APK's `lib/arm64-v8a/` wants, so this script -# unpacks exactly that and nothing else, plus the licence texts, which travel -# with the libraries (§3.1). +# The first is ONNX Runtime built with the CPU and QNN providers; the second +# is Qualcomm's HTP backend — the ARM-side compiler and the per-generation +# Hexagon "skel" the DSP loads. The third is Microsoft's stock build, which +# carries WebGPU (Dawn on Vulkan) and the QNN build does not: the generic +# rung for a phone without a Qualcomm SoC (§3.2). It lands as +# `libonnxruntime_generic.so` beside the QNN build; the engine opens the QNN +# build first, and on a Qualcomm device stops there. All three ship as AARs +# whose `jni/arm64-v8a/` is what an APK's `lib/arm64-v8a/` wants, so this +# script unpacks exactly that and nothing else, plus the licence texts, which +# travel with the libraries (§3.1). # # ## What is and is not taken from the Qualcomm package # @@ -65,6 +71,8 @@ fetch com.microsoft.onnxruntime onnxruntime-android-qnn "${ORT_VERSION}" \ "${DEST}/aar/onnxruntime-android-qnn-${ORT_VERSION}.aar" fetch com.qualcomm.qti qnn-runtime "${QNN_VERSION}" \ "${DEST}/aar/qnn-runtime-${QNN_VERSION}.aar" +fetch com.microsoft.onnxruntime onnxruntime-android "${ORT_VERSION}" \ + "${DEST}/aar/onnxruntime-android-${ORT_VERSION}.aar" # The ONNX Runtime POM names the QNN version it was built against; a pair # that disagrees loads and then fails at the first graph, which is the kind @@ -82,6 +90,8 @@ rm -rf "${DEST}/lib" mkdir -p "${DEST}/lib" unzip -q -o -j "${DEST}/aar/onnxruntime-android-qnn-${ORT_VERSION}.aar" \ 'jni/arm64-v8a/libonnxruntime.so' -d "${DEST}/lib" +unzip -q -o -p "${DEST}/aar/onnxruntime-android-${ORT_VERSION}.aar" \ + 'jni/arm64-v8a/libonnxruntime.so' > "${DEST}/lib/libonnxruntime_generic.so" members=(jni/arm64-v8a/libQnnHtp.so jni/arm64-v8a/libQnnHtpPrepare.so jni/arm64-v8a/libQnnSystem.so) for arch in ${QNN_HTP_ARCHS}; do members+=("jni/arm64-v8a/libQnnHtpV${arch}Skel.so" "jni/arm64-v8a/libQnnHtpV${arch}Stub.so")