Start the inference engine from both apps and show its choice in Settings

The desktop names where a package may have put libonnxruntime — an
override variable, beside the executable, the package's own library
directory, the Flatpak prefix, the system library directory — and
Android points at the APK's native library directory, which is also
what Qualcomm's DSP loader must be told for the Hexagon skel. Android
starts the engine at the end of the model unpack rather than at launch,
because the probe fingerprints the model files and a first launch has
none until then.

The About panel gains an Inference row beside Graphics, re-read every
two seconds while the probe runs and engines land, and faces.model_id
carries the detector's form: an int8 detector finds a different set of
faces and is a different population (docs/inference.md §7). A
low-memory signal drops every idle session with the GPU caches.

The APK assembly bundles ONNX Runtime and the Qualcomm HTP libraries
from Maven, fetched by tools/fetch-android-runtime.sh with their
published checksums; RUNTIME_DIR=none builds the tract-only APK, which
is a slower app and not a broken one. The desktop packages carry no
runtime yet.

Two probe fixes from the first desktop run: the floor must not be
built with CPU fallback disabled, and a versioned libonnxruntime.so is
a runtime too. On the reference desktop the probe now loads ONNX
Runtime 1.30, measures 30 ms on the CPU provider, and selects TensorRT
at 1.5 ms.
This commit is contained in:
2026-09-19 16:02:37 +02:00
parent d15c41e699
commit 05508741af
23 changed files with 465 additions and 57 deletions
+3
View File
@@ -49,6 +49,9 @@ dr-catalog.workspace = true
# The face pipeline, with the ONNX runtime: this is the layer that actually
# runs the models over the library (docs/faces.md).
dr-face = { workspace = true, features = ["inference"] }
# The engine behind both. `native` here means the *app* may look for a
# runtime file; the build stays C-free either way (docs/inference.md §3).
dr-inference-engine = { workspace = true, features = ["native"] }
dr-thumbs.workspace = true
# The library module writes scan results straight into the catalog, so it
# needs the same SQLite types dr-catalog exposes.
+1 -1
View File
@@ -27,7 +27,7 @@ use crate::{AppWindow, IdentityFace, IdentityPerson};
/// use rather than captured once, because the page can change it while the
/// screen is open.
pub fn model_id(settings: &crate::settings_ui::SettingsController) -> String {
settings.snapshot().faces.detector.model_id().to_string()
crate::inference::model_id(settings.snapshot().faces.detector).to_string()
}
/// Screen state that outlives a single callback.
+91
View File
@@ -0,0 +1,91 @@
//! The app's side of `dr-inference-engine` (docs/inference.md §8).
//!
//! What lives here is what only the app knows: where the runtime file might
//! be, where the disposable cache goes, which model files this device has,
//! and how the engine's status becomes a line on the settings page. What
//! runs the models does not.
use std::path::PathBuf;
use dr_inference_engine::{Form, Role, Status};
use dr_types::FaceDetector;
/// Start the engine: choose the runtime, probe in the background, compile
/// engines for whatever this device turns out to have.
///
/// `runtime_dirs` is where the platform put `libonnxruntime`: an empty list
/// is the tract build. Called once, after the models are on disk — on
/// Android that is the end of `install_bundled_models`, since the probe
/// fingerprints the model files and a probe before they land would be a
/// probe of nothing.
pub fn init(runtime_dirs: Vec<PathBuf>) {
let dir = crate::library::shared_face_models_dir();
let mut models: Vec<(Role, PathBuf)> = FaceDetector::ALL
.iter()
.map(|d| (Role::Detector, dir.join(d.file_name())))
.collect();
models.push((Role::Embedder, dir.join("arcface_mbf_b1.onnx")));
models.push((Role::Scene, dir.join("yolo26s-sem-ade20k.onnx")));
models.push((Role::Landmarks, dir.join(crate::library::LANDMARK_MODEL)));
models.push((Role::EyeClassifier, dir.join(crate::library::EYE_MODEL)));
models.push((
Role::EyeClassifier,
dir.join(crate::library::SUNGLASSES_MODEL),
));
models.retain(|(_, p)| p.is_file());
dr_inference_engine::init(dr_inference_engine::Config {
runtime_dirs,
cache_dir: crate::library::inference_cache_dir(),
models,
embedded: vec![(Role::Segmenter, dr_segment::embedded_model_bytes())],
ceiling: None,
threads: 0,
decay: std::time::Duration::ZERO,
});
// A low-memory signal drops every session nobody is mid-run with; the
// next use loads again. Same tier as the GPU caches: rebuilt from data
// the process still holds, and on a mobile GPU or NPU the largest pool.
crate::memory::evict_at(crate::memory::Tier::Gpu, dr_inference_engine::release_all);
}
/// Which form the current backend loads `detector` in, given the files on
/// this device — the fact `faces.model_id` has to carry (§7).
///
/// Reads the shared directory only. An account-private model directory can
/// override the file `library::face_models` loads, but not which form the
/// backend wants, and the int8 sibling is something a packager ships, not
/// something a user drops in.
pub fn detector_form(detector: FaceDetector) -> Form {
let canonical = crate::library::shared_face_models_dir().join(detector.file_name());
dr_inference_engine::resolve_model(Role::Detector, &canonical).1
}
/// The `faces.model_id` this device indexes under with `detector`.
pub fn model_id(detector: FaceDetector) -> &'static str {
match detector_form(detector) {
Form::F32 => detector.model_id(),
Form::Int8 => detector.model_id_int8(),
}
}
/// The two lines the About panel shows: what is running the models, and
/// why or how far along.
pub fn about_lines() -> (String, String) {
let status: Status = dr_inference_engine::status();
let line = status.line();
let detail = if status.probing {
"Checking what this device can run the models on…".to_string()
} else if status.engines.1 > 0 && status.engines.0 < status.engines.1 {
format!(
"Preparing {} engines · {} of {}",
status.rung.label(),
status.engines.0,
status.engines.1
)
} else {
status.reason
};
(line, detail)
}
+30 -3
View File
@@ -39,6 +39,7 @@ pub mod identity;
mod identity_ui;
mod import;
mod import_ui;
pub mod inference;
mod labels;
mod library;
mod library_ui;
@@ -1209,6 +1210,32 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
None => window.set_backend("NO GPU".into()),
}
// TRACES: FR-INF-1
// What the models run on. Re-read every two seconds because the answer
// changes twice after launch — when the probe reports and as each
// engine lands — and the page is open for longer than either takes.
{
let set = |w: &AppWindow| {
let (line, detail) = inference::about_lines();
w.set_inference_backend(line.into());
w.set_inference_detail(detail.into());
};
set(&window);
let weak = window.as_weak();
let timer = Rc::new(slint::Timer::default());
let held = timer.clone();
timer.start(
slint::TimerMode::Repeated,
std::time::Duration::from_secs(2),
move || {
let _keep = &held;
if let Some(w) = weak.upgrade() {
set(&w);
}
},
);
}
// TRACES: NFR-OPS-1
// The diagnostics bundle, wired as the two presses the requirement
// describes. Preparing gathers the log and the crash records into memory
@@ -1638,7 +1665,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
library.set_fetch_ahead(stored.cache.fetch_ahead);
library.set_write_xmp_sidecars(stored.library.write_xmp_sidecars);
library.set_timeline_bars(stored.library.timeline_bars);
library.set_face_model_id(stored.faces.detector.model_id());
library.set_face_model_id(inference::model_id(stored.faces.detector));
}
// --- the export folder picker ----------------------------------------
@@ -1790,7 +1817,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
// file is installed, and how much of the library that
// pipeline has covered — which for a freshly chosen one is
// nothing, and saying so is the point.
lib.set_face_model_id(s.faces.detector.model_id());
lib.set_face_model_id(inference::model_id(s.faces.detector));
if let Some(w) = weak.upgrade() {
refresh_face_status(&w, &lib, s.faces.detector);
}
@@ -3933,7 +3960,7 @@ fn refresh_face_status(
window,
&library.catalog(),
store.as_ref(),
detector.model_id(),
inference::model_id(detector),
models.as_ref().is_some_and(|m| m.eyes.is_some()),
);
window.set_identity_model_missing(models.is_none());
+7
View File
@@ -5191,6 +5191,13 @@ impl FaceModelPaths {
}
}
/// Where the inference engine keeps what it derives per device: the probe
/// result and compiled engines (docs/inference.md §4, §5). A peer of
/// `thumbs`, not of the catalog: disposable, regenerable, never synced.
pub fn inference_cache_dir() -> PathBuf {
data_root().join("inference")
}
/// The detector and embedder files, if both are present — and the eye
/// models beside them, if those are.
///
+3 -1
View File
@@ -484,7 +484,9 @@ impl LibraryController {
dr_types::LibrarySettings::default().write_xmp_sidecars,
),
timeline_bars: std::cell::Cell::new(dr_types::LibrarySettings::default().timeline_bars),
face_model_id: RefCell::new(dr_types::FaceDetector::default().model_id().to_string()),
face_model_id: RefCell::new(
crate::inference::model_id(dr_types::FaceDetector::default()).to_string(),
),
})
}
+4
View File
@@ -58,6 +58,8 @@ export component AppWindow inherits Window {
in property <image> canvas;
in property <string> adapter: "detecting…";
in property <string> backend: "—";
in property <string> inference-backend: "detecting…";
in property <string> inference-detail: "";
in property <int> fps: 0;
/// TRACES: FR-DSP-8
/// The display showing the canvas and the colour transform it is getting.
@@ -1322,6 +1324,8 @@ in property <bool> panel-visible: true;
adapter: root.adapter;
backend: root.backend;
inference-backend: root.inference-backend;
inference-detail: root.inference-detail;
fps: root.fps;
layout-class: root.layout-class;
app-version: root.app-version;
+24
View File
@@ -137,6 +137,10 @@ export component SettingsPage inherits Rectangle {
/// something up, not where they are looked at all day.
in property <string> adapter;
in property <string> backend;
/// What runs the neural models and how it was chosen — the two lines
/// `dr_ui::inference::about_lines` produces (docs/inference.md §4).
in property <string> inference-backend;
in property <string> inference-detail;
in property <int> fps;
in property <string> layout-class;
in property <string> app-version;
@@ -872,6 +876,26 @@ export component SettingsPage inherits Rectangle {
}
}
// TRACES: FR-INF-1
// The backend the models run on, and why. Beside
// Graphics because it is the same kind of fact: a
// property of this device, chosen by measurement,
// that a bug report about a slow index should quote.
HorizontalLayout {
spacing: Theme.gap;
Label { text: "Inference"; }
Value {
text: root.inference-backend;
horizontal-stretch: 1;
overflow: elide;
}
}
if root.inference-detail != "": Caption {
text: root.inference-detail;
wrap: word-wrap;
}
HorizontalLayout {
spacing: Theme.gap;
Label { text: "Frame rate"; }