Give a phone without a Qualcomm SoC the generic WebGPU runtime

The APK's ONNX Runtime is the QNN build, which carries no WebGPU, so a
non-Qualcomm phone had nothing above the CPU provider. The APK now also
carries Microsoft's stock onnxruntime-android 1.29.0 (32 MB) as
libonnxruntime_generic.so, and the app offers it after the QNN build.

The runtime search stops at a perfect fit, so on a Qualcomm device the
QNN build — listed first — is all that is opened, and the generic build
never loads beside it. A Qualcomm SoC is read from ro.soc.manufacturer
or, before Android 12, from Qualcomm's FastRPC library being present:
a Qualcomm device mistaken for another would trade its Hexagon for the
generic rung.
This commit is contained in:
2026-10-04 21:00:15 -04:00
parent cb97ebe7ac
commit 43402bfe5b
6 changed files with 59 additions and 19 deletions
+7 -2
View File
@@ -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 // 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`, // 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 // which is also where Qualcomm's DSP loader has to be pointed for the
// Hexagon skel (docs/dev/inference.md §3, §8). // Hexagon skel (docs/dev/inference.md §3, §8). Two of them: the QNN
dr_ui::inference::init(native_library_dir().into_iter().collect()); // 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. /// The directory the system unpacked this APK's native libraries into.
+8 -1
View File
@@ -58,7 +58,10 @@ pub fn ensure_installed() {
/// Best fit, not first found (§3.2): a device can hold several runtimes — /// 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 /// the package's OpenVINO build, a CUDA build the user fetched, the
/// distribution's ROCm build — and each carries one vendor's providers. /// 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 /// `DARKROOM_ORT_DIR`, when it loads, wins outright: it is how a person
/// says which runtime they mean. /// says which runtime they mean.
pub fn install(dirs: &[PathBuf]) -> Runtime { pub fn install(dirs: &[PathBuf]) -> Runtime {
@@ -110,7 +113,11 @@ fn install_best(dirs: &[PathBuf]) -> Option<Runtime> {
found.push(f); found.push(f);
break; break;
} }
let perfect = gpus.score(&f.providers) >= crate::hardware::PERFECT;
found.push(f); found.push(f);
if perfect {
break;
}
} }
Err(e) => log::info!("inference: no runtime in {}: {e}", dir.display()), Err(e) => log::info!("inference: no runtime in {}: {e}", dir.display()),
} }
+21 -5
View File
@@ -19,6 +19,10 @@ pub struct Gpus {
pub qualcomm: bool, 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 { impl Gpus {
/// How well a runtime offering `providers` fits this device. The vendor /// How well a runtime offering `providers` fits this device. The vendor
/// rungs score above OpenVINO because a machine with an Intel iGPU and /// rungs score above OpenVINO because a machine with an Intel iGPU and
@@ -29,10 +33,10 @@ impl Gpus {
providers providers
.iter() .iter()
.map(|p| match p.as_str() { .map(|p| match p.as_str() {
"TensorrtExecutionProvider" | "CUDAExecutionProvider" if self.nvidia => 3, "TensorrtExecutionProvider" | "CUDAExecutionProvider" if self.nvidia => PERFECT,
"MIGraphXExecutionProvider" if self.amd_rocm => 3, "MIGraphXExecutionProvider" if self.amd_rocm => PERFECT,
"QNNExecutionProvider" if self.qualcomm => 3, "QNNExecutionProvider" if self.qualcomm => PERFECT,
"CoreMLExecutionProvider" => 3, "CoreMLExecutionProvider" => PERFECT,
"OpenVINOExecutionProvider" if self.intel => 2, "OpenVINOExecutionProvider" if self.intel => 2,
"WebGpuExecutionProvider" => 1, "WebGpuExecutionProvider" => 1,
_ => 0, _ => 0,
@@ -91,9 +95,21 @@ pub fn detect() -> Gpus {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
pub fn detect() -> Gpus { 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 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 { 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() ..Gpus::default()
} }
} }
+5 -3
View File
@@ -263,10 +263,12 @@ cp "${DEX}" "${OUT}/staging/classes.dex"
# native library directory. The build links none of it — the app dlopens # 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 # `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` # 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 # builds exactly that. 206 MB for the default set — 32 MB of it the generic
# Hexagon generations that buys. # WebGPU build for a phone without a Qualcomm SoC; the script says which
# Hexagon generations the rest buys.
if [[ "${RUNTIME_DIR}" != "none" ]]; then 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}" "${REPO}/tools/fetch-android-runtime.sh" "${RUNTIME_DIR}"
fi fi
cp "${RUNTIME_DIR}"/lib/*.so "${OUT}/staging/lib/${ABI}/" cp "${RUNTIME_DIR}"/lib/*.so "${OUT}/staging/lib/${ABI}/"
+1 -1
View File
@@ -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-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-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-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-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-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) | | FR-PLAT-WIN-1 | [`platform/dr-plat/src/dirs.rs:1`](../../platform/dr-plat/src/dirs.rs#L1) |
+17 -7
View File
@@ -3,17 +3,23 @@
# #
# ./tools/fetch-android-runtime.sh [DEST] # ./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.microsoft.onnxruntime:onnxruntime-android-qnn MIT
# com.qualcomm.qti:qnn-runtime Qualcomm AI Engine Direct SDK licence # 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 # The first is ONNX Runtime built with the CPU and QNN providers; the second
# providers; the second is Qualcomm's HTP backend — the ARM-side compiler and # is Qualcomm's HTP backend — the ARM-side compiler and the per-generation
# the per-generation Hexagon "skel" the DSP loads. Both ship as AARs whose # Hexagon "skel" the DSP loads. The third is Microsoft's stock build, which
# `jni/arm64-v8a/` is what an APK's `lib/arm64-v8a/` wants, so this script # carries WebGPU (Dawn on Vulkan) and the QNN build does not: the generic
# unpacks exactly that and nothing else, plus the licence texts, which travel # rung for a phone without a Qualcomm SoC (§3.2). It lands as
# with the libraries (§3.1). # `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 # ## 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" "${DEST}/aar/onnxruntime-android-qnn-${ORT_VERSION}.aar"
fetch com.qualcomm.qti qnn-runtime "${QNN_VERSION}" \ fetch com.qualcomm.qti qnn-runtime "${QNN_VERSION}" \
"${DEST}/aar/qnn-runtime-${QNN_VERSION}.aar" "${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 # 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 # 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" mkdir -p "${DEST}/lib"
unzip -q -o -j "${DEST}/aar/onnxruntime-android-qnn-${ORT_VERSION}.aar" \ unzip -q -o -j "${DEST}/aar/onnxruntime-android-qnn-${ORT_VERSION}.aar" \
'jni/arm64-v8a/libonnxruntime.so' -d "${DEST}/lib" '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) members=(jni/arm64-v8a/libQnnHtp.so jni/arm64-v8a/libQnnHtpPrepare.so jni/arm64-v8a/libQnnSystem.so)
for arch in ${QNN_HTP_ARCHS}; do for arch in ${QNN_HTP_ARCHS}; do
members+=("jni/arm64-v8a/libQnnHtpV${arch}Skel.so" "jni/arm64-v8a/libQnnHtpV${arch}Stub.so") members+=("jni/arm64-v8a/libQnnHtpV${arch}Skel.so" "jni/arm64-v8a/libQnnHtpV${arch}Stub.so")