Merge: master at 0.13.6, with the drag-ghost file and the shared model lookup ported into the split modules
This commit is contained in:
@@ -53,6 +53,10 @@ dr-face = { workspace = true, features = ["inference"] }
|
||||
# runtime file; the build stays C-free either way (docs/dev/inference.md §3).
|
||||
dr-inference-engine = { workspace = true, features = ["native"] }
|
||||
dr-thumbs.workspace = true
|
||||
# For the drag ghost only: the composite under the cursor has to reach the
|
||||
# renderer through a file, and the fan of thumbnails needs alpha, which the
|
||||
# thumbnail store's JPEG cannot carry. See `collections_ui::drag_image_via_file`.
|
||||
png = "0.18"
|
||||
# The library module writes scan results straight into the catalog, so it
|
||||
# needs the same SQLite types dr-catalog exposes.
|
||||
rusqlite.workspace = true
|
||||
|
||||
@@ -86,6 +86,107 @@ pub(super) fn compose_drag_image(thumbs: &[slint::Image]) -> slint::Image {
|
||||
slint::Image::from_rgba8_premultiplied(canvas)
|
||||
}
|
||||
|
||||
/// Hand the composite to the renderer by way of a file.
|
||||
///
|
||||
/// **A workaround for a renderer fault, and it should read as one.** The
|
||||
/// ghost under the cursor is drawn by Slint's own drag overlay, which
|
||||
/// uploads the image as a texture, draws it, and drops the texture in the
|
||||
/// same call. With the wgpu FemtoVG renderer that drop is immediate and
|
||||
/// the draw is deferred to the frame's flush, so by the time the frame is
|
||||
/// rendered the texture is gone and the renderer binds its placeholder
|
||||
/// instead — a solid red rectangle the size of the ghost. An image with a
|
||||
/// cache key is kept in the renderer's texture cache until after the
|
||||
/// flush; an image built from pixels has none, and only a path gives one.
|
||||
/// So the composite goes to disk as a PNG and comes back through
|
||||
/// `load_from_path`. Slint 1.17.1, `draw_image_direct` in the FemtoVG
|
||||
/// item renderer; the GL FemtoVG renderer is not affected.
|
||||
///
|
||||
/// One file per drag, named uniquely: the core caches decoded images by
|
||||
/// path, so reusing a name would show the previous drag's ghost. The file
|
||||
/// is removed when the drag ends, or when the next one begins.
|
||||
///
|
||||
/// If anything on the way fails the composite is handed over as it is,
|
||||
/// which on the affected renderer draws the placeholder — no worse than
|
||||
/// before, and a log line says why.
|
||||
pub(super) fn drag_image_via_file(composite: slint::Image) -> slint::Image {
|
||||
let Some(buffer) = composite.to_rgba8() else {
|
||||
log::warn!("drag ghost: the composite has no pixels to write");
|
||||
return composite;
|
||||
};
|
||||
log::debug!("drag ghost: {}×{}", buffer.width(), buffer.height());
|
||||
if buffer.width() == 0 || buffer.height() == 0 {
|
||||
return composite;
|
||||
}
|
||||
match write_drag_image(&buffer) {
|
||||
Ok(path) => match slint::Image::load_from_path(&path) {
|
||||
Ok(image) => {
|
||||
forget_drag_image_file();
|
||||
*DRAG_IMAGE_FILE.lock().unwrap() = Some(path);
|
||||
image
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!("drag ghost: {} did not load back", path.display());
|
||||
let _ = std::fs::remove_file(&path);
|
||||
composite
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("drag ghost: {e}");
|
||||
composite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The file the current drag's ghost is loaded from, if any.
|
||||
static DRAG_IMAGE_FILE: std::sync::Mutex<Option<std::path::PathBuf>> = std::sync::Mutex::new(None);
|
||||
|
||||
fn write_drag_image(
|
||||
buffer: &slint::SharedPixelBuffer<slint::Rgba8Pixel>,
|
||||
) -> std::io::Result<std::path::PathBuf> {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static SERIAL: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
let dir = crate::library::scratch_dir();
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let path = dir.join(format!(
|
||||
"drag-{}-{}.png",
|
||||
std::process::id(),
|
||||
SERIAL.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
let file = std::fs::File::create(&path)?;
|
||||
let mut encoder = png::Encoder::new(
|
||||
std::io::BufWriter::new(file),
|
||||
buffer.width(),
|
||||
buffer.height(),
|
||||
);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
// Fastest: this is a 160px bitmap written once per drag and read once.
|
||||
encoder.set_compression(png::Compression::Fastest);
|
||||
let mut writer = encoder.write_header().map_err(std::io::Error::other)?;
|
||||
writer
|
||||
.write_image_data(buffer.as_bytes())
|
||||
.map_err(std::io::Error::other)?;
|
||||
writer.finish().map_err(std::io::Error::other)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub(super) fn forget_drag_image_file() {
|
||||
if let Some(path) = DRAG_IMAGE_FILE.lock().unwrap().take() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw one thumbnail into the composite, scaled to `tw`×`th` at `dx`,`dy`.
|
||||
///
|
||||
/// Nearest-neighbour: this is a transient 160px cursor bitmap, and a filtered
|
||||
/// resample would cost more than it could visibly buy. `dim` darkens the layers
|
||||
/// beneath the top one so the stack reads as depth rather than as a smear.
|
||||
///
|
||||
/// The buffer is premultiplied, so the alpha applied here is baked into the
|
||||
/// colour channels as well.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
/// Draw one thumbnail into the composite, scaled to `tw`×`th` at `dx`,`dy`.
|
||||
///
|
||||
/// Nearest-neighbour: this is a transient 160px cursor bitmap, and a filtered
|
||||
|
||||
@@ -19,7 +19,10 @@ use crate::library;
|
||||
use crate::{AppWindow, Collections};
|
||||
|
||||
use super::controller::{decide_drop, CollectionsController, Drop};
|
||||
use super::drag::{arm_hold, arm_spring, collapse_spring_opened, compose_drag_image};
|
||||
use super::drag::{
|
||||
arm_hold, arm_spring, collapse_spring_opened, compose_drag_image, drag_image_via_file,
|
||||
forget_drag_image_file,
|
||||
};
|
||||
use super::press::select_row;
|
||||
use super::rename_menu::unique_name;
|
||||
use super::trash::{drain_trash, start_restore, start_trash};
|
||||
@@ -603,7 +606,7 @@ fn wire_drag(
|
||||
.map(|c| c.thumbnail)
|
||||
.collect()
|
||||
};
|
||||
w.set_library_drag_image(compose_drag_image(&thumbs));
|
||||
w.set_library_drag_image(drag_image_via_file(compose_drag_image(&thumbs)));
|
||||
|
||||
*ctl.dragging.borrow_mut() = carried.clone();
|
||||
sync_selection(&w, &ctl, &ids);
|
||||
@@ -763,6 +766,7 @@ fn wire_drag(
|
||||
// released — it holds a copy of every thumbnail it composited.
|
||||
sync_lifted(&w, &[], &visible());
|
||||
w.set_library_drag_image(slint::Image::default());
|
||||
forget_drag_image_file();
|
||||
|
||||
if landed.is_some() {
|
||||
let borrow = catalog.borrow();
|
||||
|
||||
+24
-20
@@ -19,23 +19,26 @@ use dr_types::FaceDetector;
|
||||
/// 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
|
||||
// Each file where the app will actually load it from — the user's
|
||||
// shared directory, else the package's — so a fresh install with models
|
||||
// only under `/usr/share` probes and compiles for them rather than
|
||||
// finding nothing and settling on the CPU.
|
||||
let mut wanted: Vec<(Role, &str)> = FaceDetector::ALL
|
||||
.iter()
|
||||
.map(|d| (Role::Detector, dir.join(d.file_name())))
|
||||
.map(|d| (Role::Detector, d.file_name()))
|
||||
.collect();
|
||||
wanted.extend([
|
||||
(Role::Embedder, "arcface_mbf_b1.onnx"),
|
||||
(Role::Scene, "yolo26s-sem-ade20k.onnx"),
|
||||
(Role::Landmarks, crate::library::LANDMARK_MODEL),
|
||||
(Role::EyeClassifier, crate::library::EYE_MODEL),
|
||||
(Role::EyeClassifier, crate::library::SUNGLASSES_MODEL),
|
||||
(Role::Inpainter, crate::library::INPAINT_MODEL),
|
||||
]);
|
||||
let models: Vec<(Role, PathBuf)> = wanted
|
||||
.into_iter()
|
||||
.filter_map(|(role, name)| Some((role, crate::library::shared_model(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),
|
||||
));
|
||||
if let Some(p) = crate::library::inpaint_model() {
|
||||
models.push((Role::Inpainter, p));
|
||||
}
|
||||
models.retain(|(_, p)| p.is_file());
|
||||
|
||||
dr_inference_engine::init(dr_inference_engine::Config {
|
||||
runtime_dirs,
|
||||
@@ -75,12 +78,13 @@ pub fn user_runtime_dir() -> PathBuf {
|
||||
/// 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.
|
||||
/// Reads the shared and system directories 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());
|
||||
let canonical = crate::library::shared_model(detector.file_name())
|
||||
.unwrap_or_else(|| crate::library::shared_face_models_dir().join(detector.file_name()));
|
||||
dr_inference_engine::resolve_model(Role::Detector, &canonical).1
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,15 @@ pub fn inference_cache_dir() -> PathBuf {
|
||||
data_root().join("inference")
|
||||
}
|
||||
|
||||
/// Somewhere to put a file that only needs to exist for a moment.
|
||||
///
|
||||
/// Under the data root rather than `std::env::temp_dir()`, which on Android
|
||||
/// names a directory the app cannot write to. Nothing here survives a
|
||||
/// launch on purpose: whoever writes into it deletes what they wrote.
|
||||
pub fn scratch_dir() -> PathBuf {
|
||||
data_root().join("scratch")
|
||||
}
|
||||
|
||||
/// The detector and embedder files, if both are present — and the eye
|
||||
/// models beside them, if those are.
|
||||
///
|
||||
@@ -341,6 +350,18 @@ pub(super) fn system_face_models_dirs() -> Vec<PathBuf> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The file `name` in the shared user models directory, else in the first
|
||||
/// system directory that has it — the search every account-independent
|
||||
/// model lookup makes, and the one the inference engine is told about, so
|
||||
/// that a package's models under `/usr/share` are probed and compiled for
|
||||
/// exactly as a user's own would be.
|
||||
pub fn shared_model(name: &str) -> Option<PathBuf> {
|
||||
std::iter::once(shared_face_models_dir())
|
||||
.chain(system_face_models_dirs())
|
||||
.map(|d| d.join(name))
|
||||
.find(|p| p.is_file())
|
||||
}
|
||||
|
||||
/// TRACES: FR-MRG-4
|
||||
/// The panorama border filler, as shipped in `models/inpaint/`.
|
||||
pub const INPAINT_MODEL: &str = "migan-512.onnx";
|
||||
@@ -351,10 +372,7 @@ pub const INPAINT_MODEL: &str = "migan-512.onnx";
|
||||
/// the per-account step, because a fill is not identity-bearing and no
|
||||
/// library has a reason to pin its own.
|
||||
pub fn inpaint_model() -> Option<PathBuf> {
|
||||
std::iter::once(shared_face_models_dir())
|
||||
.chain(system_face_models_dirs())
|
||||
.map(|d| d.join(INPAINT_MODEL))
|
||||
.find(|p| p.is_file())
|
||||
shared_model(INPAINT_MODEL)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1771,6 +1771,13 @@ export component LibraryGrid inherits Rectangle {
|
||||
// Reported out so Rust can place month headings: a heading belongs on a
|
||||
// cell that begins a row, and only the grid knows how wide a row is.
|
||||
changed columns => { root.columns-changed(root.columns); }
|
||||
// And once on creation. `changed` reports a change, and the first
|
||||
// evaluation is not one — so a grid built after the window has already
|
||||
// settled at its size never said how wide it was, and Rust went on
|
||||
// placing headings for the one column it had been told about at
|
||||
// start-up. Every month then began a row and was announced wherever
|
||||
// its first cell fell, mid-row included.
|
||||
init => { root.columns-changed(root.columns); }
|
||||
property <int> row-count: ceil(root.cells.length / max(1, columns));
|
||||
|
||||
// --- while a pinch is happening, and just after -----------------------
|
||||
|
||||
Reference in New Issue
Block a user