Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
064be89a73 | ||
|
|
bff12f81a9 | ||
|
|
07e85cf0b2 | ||
|
|
b23527d5dc | ||
|
|
3b783d7bd4 | ||
|
|
fcccc2c2e0 | ||
|
|
1bc04870c3 | ||
|
|
4da2ec39b3 | ||
|
|
9558b77759 |
Generated
+25
-25
@@ -1265,7 +1265,7 @@ checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darkroom-android"
|
name = "darkroom-android"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_logger",
|
"android_logger",
|
||||||
"dr-plat",
|
"dr-plat",
|
||||||
@@ -1278,7 +1278,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darkroom-desktop"
|
name = "darkroom-desktop"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dr-plat",
|
"dr-plat",
|
||||||
@@ -1454,7 +1454,7 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-bench"
|
name = "dr-bench"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dr-catalog",
|
"dr-catalog",
|
||||||
@@ -1471,7 +1471,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-catalog"
|
name = "dr-catalog"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-face",
|
"dr-face",
|
||||||
"dr-plat",
|
"dr-plat",
|
||||||
@@ -1486,7 +1486,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-decode"
|
name = "dr-decode"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-types",
|
"dr-types",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@@ -1500,7 +1500,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-export"
|
name = "dr-export"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-decode",
|
"dr-decode",
|
||||||
"dr-gpu",
|
"dr-gpu",
|
||||||
@@ -1519,7 +1519,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-face"
|
name = "dr-face"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-inference-engine",
|
"dr-inference-engine",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@@ -1532,7 +1532,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-film"
|
name = "dr-film"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1541,7 +1541,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-gpu"
|
name = "dr-gpu"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"dr-decode",
|
"dr-decode",
|
||||||
@@ -1559,7 +1559,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-inference-engine"
|
name = "dr-inference-engine"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"libloading",
|
"libloading",
|
||||||
@@ -1574,7 +1574,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-ingest"
|
name = "dr-ingest"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-plat",
|
"dr-plat",
|
||||||
"dr-types",
|
"dr-types",
|
||||||
@@ -1586,7 +1586,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-lens"
|
name = "dr-lens"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lensfun",
|
"lensfun",
|
||||||
"log",
|
"log",
|
||||||
@@ -1594,7 +1594,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-pano"
|
name = "dr-pano"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-decode",
|
"dr-decode",
|
||||||
"dr-inference-engine",
|
"dr-inference-engine",
|
||||||
@@ -1608,7 +1608,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-pipeline"
|
name = "dr-pipeline"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-types",
|
"dr-types",
|
||||||
"log",
|
"log",
|
||||||
@@ -1617,7 +1617,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-plat"
|
name = "dr-plat"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-native-keyring-store",
|
"android-native-keyring-store",
|
||||||
"dr-types",
|
"dr-types",
|
||||||
@@ -1633,7 +1633,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-preset-xmp"
|
name = "dr-preset-xmp"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-pipeline",
|
"dr-pipeline",
|
||||||
"log",
|
"log",
|
||||||
@@ -1643,7 +1643,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-segment"
|
name = "dr-segment"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-inference-engine",
|
"dr-inference-engine",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
@@ -1656,7 +1656,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-sync"
|
name = "dr-sync"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dr-plat",
|
"dr-plat",
|
||||||
@@ -1670,7 +1670,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-sync-folder"
|
name = "dr-sync-folder"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dr-sync",
|
"dr-sync",
|
||||||
@@ -1682,7 +1682,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-sync-nextcloud"
|
name = "dr-sync-nextcloud"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dr-decode",
|
"dr-decode",
|
||||||
@@ -1704,7 +1704,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-thumbs"
|
name = "dr-thumbs"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-types",
|
"dr-types",
|
||||||
"jpeg-encoder",
|
"jpeg-encoder",
|
||||||
@@ -1716,7 +1716,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-types"
|
name = "dr-types"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1725,7 +1725,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-ui"
|
name = "dr-ui"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1773,7 +1773,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dr-xmp"
|
name = "dr-xmp"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dr-types",
|
"dr-types",
|
||||||
"log",
|
"log",
|
||||||
@@ -7107,7 +7107,7 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "traceability"
|
name = "traceability"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|||||||
+13
-1
@@ -32,7 +32,7 @@ members = [
|
|||||||
exclude = ["third_party"]
|
exclude = ["third_party"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.92"
|
rust-version = "1.92"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
@@ -276,6 +276,18 @@ opt-level = 0
|
|||||||
lto = "thin"
|
lto = "thin"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
|
# A release build that can say where it panicked: line tables, so a crash
|
||||||
|
# record's backtrace (`dr_plat::crash`) reads `file.rs:123` rather than bare
|
||||||
|
# addresses. The macOS build uses it (docs/dev/macos.md) — no one here can
|
||||||
|
# reproduce a Mac bug, so its reports carry what a debugger would have — at
|
||||||
|
# the price of a larger binary and no slower code. On macOS the tables land
|
||||||
|
# in a `.dSYM` beside the executable (rustc's default `packed`), and the
|
||||||
|
# bundle must carry that directory next to the binary for the backtrace to
|
||||||
|
# find it.
|
||||||
|
[profile.diagnostic]
|
||||||
|
inherits = "release"
|
||||||
|
debug = "line-tables-only"
|
||||||
|
|
||||||
# Three upstream crates carry a local patch: wgpu-hal and Slint's Skia
|
# Three upstream crates carry a local patch: wgpu-hal and Slint's Skia
|
||||||
# renderer so that the Android build can draw with wgpu on a rotated display
|
# renderer so that the Android build can draw with wgpu on a rotated display
|
||||||
# (technical-debt.md TD-1), and rawler so that a linear DNG wider than 16 700
|
# (technical-debt.md TD-1), and rawler so that a linear DNG wider than 16 700
|
||||||
|
|||||||
@@ -83,28 +83,125 @@ texture directly — no readback between the GPU and the screen.
|
|||||||
| Windows | `DarkRoom-<version>-x86_64-setup.exe`, cross-built by CI ([windows.md](docs/dev/windows.md)) | Verified under Wine only; unsigned |
|
| Windows | `DarkRoom-<version>-x86_64-setup.exe`, cross-built by CI ([windows.md](docs/dev/windows.md)) | Verified under Wine only; unsigned |
|
||||||
| Flatpak | [`packaging/flatpak/`](packaging/flatpak/) | Manifest in tree; folders are chosen through the portal, but no Flatpak has been built to prove it |
|
| Flatpak | [`packaging/flatpak/`](packaging/flatpak/) | Manifest in tree; folders are chosen through the portal, but no Flatpak has been built to prove it |
|
||||||
|
|
||||||
Or build it. Git LFS is required for the model weights, and the toolchain
|
## Building from source
|
||||||
pins itself to 1.92.0:
|
|
||||||
|
**Before anything.** Git LFS holds the model weights and the manual's
|
||||||
|
pictures; a clone without it has ~130-byte pointers in their place, and every
|
||||||
|
packager below refuses to ship one. The Rust toolchain pins itself to 1.92.0
|
||||||
|
through `rust-toolchain.toml`, so rustup is all you install. Slint needs a few
|
||||||
|
system headers, and the app needs a Vulkan driver at runtime:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
git clone https://gitea.tourolle.paris/dtourolle/DarkRoom.git && cd DarkRoom
|
||||||
git lfs install && git lfs pull
|
git lfs install && git lfs pull
|
||||||
|
|
||||||
|
# Debian / Ubuntu
|
||||||
|
sudo apt-get install pkg-config libfontconfig1-dev libxkbcommon-dev libvulkan1
|
||||||
|
# Arch
|
||||||
|
sudo pacman -S --needed pkgconf fontconfig libxkbcommon vulkan-icd-loader
|
||||||
|
```
|
||||||
|
|
||||||
|
**To try it** from the checkout, without installing anything:
|
||||||
|
|
||||||
|
```bash
|
||||||
cargo run --release -p darkroom-desktop
|
cargo run --release -p darkroom-desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
Android, through the containerised toolchain ([docker/android](docker/android/README.md)):
|
This is for development. The binary under `target/` finds no face, scene or
|
||||||
|
panorama-fill models, and a release build does not find the manual either:
|
||||||
|
it looks for all of them in the system data directories an install creates
|
||||||
|
(`$XDG_DATA_DIRS/darkroom`, by default `/usr/local/share/darkroom` and
|
||||||
|
`/usr/share/darkroom`), never in the checkout. Those features show as
|
||||||
|
unavailable until it is installed.
|
||||||
|
|
||||||
|
### Linux: build and install
|
||||||
|
|
||||||
|
**On Arch**, build a package from the checkout and install it with pacman,
|
||||||
|
so it can be upgraded and removed like any other:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./docker/android/build.sh cargo ndk -t arm64-v8a build --release
|
cd packaging && makepkg -si
|
||||||
```
|
```
|
||||||
|
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md) has the system packages, the four
|
**Elsewhere**, build the release binary and install it under `/usr/local`
|
||||||
|
by hand. These are the same files, in the same places, as the Arch package
|
||||||
|
([`packaging/PKGBUILD`](packaging/PKGBUILD)'s `package()` is the reference):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release --locked -p darkroom-desktop
|
||||||
|
# -> target/release/darkroom-desktop
|
||||||
|
|
||||||
|
P=/usr/local
|
||||||
|
sudo install -Dm755 target/release/darkroom-desktop $P/bin/darkroom-desktop
|
||||||
|
|
||||||
|
# The models: faces and eye state, scene categories, panorama border fill
|
||||||
|
sudo install -d $P/share/darkroom/models
|
||||||
|
sudo install -m644 models/face/*.onnx models/scene/* models/inpaint/*.onnx \
|
||||||
|
$P/share/darkroom/models/
|
||||||
|
|
||||||
|
# The offline manual the Help menu opens
|
||||||
|
sudo install -Dm644 docs/manual/index.html $P/share/darkroom/manual/index.html
|
||||||
|
sudo install -Dm644 -t $P/share/darkroom/manual/media docs/manual/media/*
|
||||||
|
|
||||||
|
# Launcher entry, icon and software-centre description
|
||||||
|
sudo install -Dm644 packaging/paris.tourolle.darkroom.desktop \
|
||||||
|
$P/share/applications/paris.tourolle.darkroom.desktop
|
||||||
|
sudo install -Dm644 ui/dr-ui/ui/app-icon.png \
|
||||||
|
$P/share/icons/hicolor/256x256/apps/paris.tourolle.darkroom.png
|
||||||
|
sudo install -Dm644 packaging/paris.tourolle.darkroom.metainfo.xml \
|
||||||
|
$P/share/metainfo/paris.tourolle.darkroom.metainfo.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `darkroom-desktop`, or open it from the application menu. To
|
||||||
|
uninstall, remove those files and `/usr/local/share/darkroom`. Your catalog,
|
||||||
|
settings and thumbnails live in `darkroom/` under your own XDG data, config
|
||||||
|
and cache directories (`~/.local/share`, `~/.config`, `~/.cache`) and are
|
||||||
|
not touched by either.
|
||||||
|
|
||||||
|
Optional at runtime: `gnome-keyring` or `kwallet` to remember Nextcloud
|
||||||
|
credentials, and an ONNX Runtime in `/usr/lib` (CPU, or ROCm on an AMD GPU) to
|
||||||
|
run the models on every core rather than on the built-in engine.
|
||||||
|
|
||||||
|
### Windows: build the installer
|
||||||
|
|
||||||
|
The `.exe` is cross-built from Linux in a container (podman or docker), with
|
||||||
|
no Windows machine involved. Two steps — the executable, then the NSIS
|
||||||
|
installer that carries it with its models and manual:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker/windows/build.sh cargo build --release --target x86_64-pc-windows-gnu -p darkroom-desktop
|
||||||
|
./docker/windows/build.sh docker/windows/package.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Both land in the container's cache on the host, `~/.cache/darkroom-windows/target/`:
|
||||||
|
the bare executable under `x86_64-pc-windows-gnu/release/darkroom-desktop.exe`,
|
||||||
|
the installer under `installer/DarkRoom-<version>-x86_64-setup.exe`.
|
||||||
|
Copy that to the Windows machine and run it — it installs per user, needs no
|
||||||
|
administrator rights, and adds an uninstaller. Run on its own, the bare
|
||||||
|
`.exe` looks for `models\` and `manual\` beside itself, so use the installer.
|
||||||
|
[docker/windows](docker/windows/README.md) has the details.
|
||||||
|
|
||||||
|
### Android: build the APK
|
||||||
|
|
||||||
|
Also containerised ([docker/android](docker/android/README.md)). This
|
||||||
|
builds, packages and debug-signs the APK, and with `--install` puts it on a
|
||||||
|
device connected over adb:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker/android/package.sh --install
|
||||||
|
```
|
||||||
|
|
||||||
|
A debug-signed APK cannot replace one installed from a release; uninstall
|
||||||
|
that first.
|
||||||
|
|
||||||
|
[CONTRIBUTING.md](CONTRIBUTING.md) has the four
|
||||||
commands CI runs against what you send, and the shortest useful
|
commands CI runs against what you send, and the shortest useful
|
||||||
contribution — a develop operation is one YAML file, and it arrives with its
|
contribution — a develop operation is one YAML file, and it arrives with its
|
||||||
controls, its place in the chain and its tests.
|
controls, its place in the chain and its tests.
|
||||||
|
|
||||||
## Where it stands
|
## Where it stands
|
||||||
|
|
||||||
**0.19.1**, thirty tagged releases in. 193 numbered requirements in
|
**0.19.2**, thirty-one tagged releases in. 193 numbered requirements in
|
||||||
scope, 85% of them claimed by code and [traced to it](docs/dev/traceability.md);
|
scope, 85% of them claimed by code and [traced to it](docs/dev/traceability.md);
|
||||||
the rest are written down rather than merely absent.
|
the rest are written down rather than merely absent.
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use dr_plat::diagnostics::Installed;
|
use dr_plat::diagnostics::Installed;
|
||||||
|
|
||||||
|
/// What the log keeps when `RUST_LOG` does not say.
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
const DEFAULT_LOG: &str =
|
||||||
|
"info,wgpu_core=warn,wgpu_hal=warn,zbus=warn,tracing=warn,calloop=warn,rawler=warn";
|
||||||
|
|
||||||
|
/// The same, and `debug` from this application's own crates and from ONNX
|
||||||
|
/// Runtime, whose `debug` is how many nodes each provider took
|
||||||
|
/// (docs/dev/macos.md). Nobody here runs a Mac: every macOS build is in
|
||||||
|
/// the hands of someone who can send us a log and cannot attach a debugger,
|
||||||
|
/// so the log is written as if for a debug build. `dr_` is a prefix, and
|
||||||
|
/// `env_logger` matches directives by prefix, so it names every `dr-*`
|
||||||
|
/// crate — present and future — without naming a dependency.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
const DEFAULT_LOG: &str = "info,dr_=debug,darkroom_desktop=debug,onnxruntime=debug,\
|
||||||
|
wgpu_core=warn,wgpu_hal=warn,zbus=warn,tracing=warn,calloop=warn,rawler=warn";
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
// TRACES: FR-PLAT-WIN-3
|
// TRACES: FR-PLAT-WIN-3
|
||||||
// Before the logger, the crash hook and everything else: this exists so a
|
// Before the logger, the crash hook and everything else: this exists so a
|
||||||
@@ -33,9 +49,8 @@ fn main() -> anyhow::Result<()> {
|
|||||||
// (NFR-OPS-1). `filter()` is asked afterwards because the environment may
|
// (NFR-OPS-1). `filter()` is asked afterwards because the environment may
|
||||||
// have overridden the default below, and the file must not be quieter than
|
// have overridden the default below, and the file must not be quieter than
|
||||||
// the terminal.
|
// the terminal.
|
||||||
let console = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(
|
let console =
|
||||||
"info,wgpu_core=warn,wgpu_hal=warn,zbus=warn,tracing=warn,calloop=warn,rawler=warn",
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(DEFAULT_LOG))
|
||||||
))
|
|
||||||
.build();
|
.build();
|
||||||
let level = console.filter();
|
let level = console.filter();
|
||||||
let logging = dr_plat::diagnostics::install(Box::new(console), level);
|
let logging = dr_plat::diagnostics::install(Box::new(console), level);
|
||||||
@@ -109,5 +124,21 @@ fn runtime_dirs() -> Vec<PathBuf> {
|
|||||||
PathBuf::from("/usr/lib/darkroom"),
|
PathBuf::from("/usr/lib/darkroom"),
|
||||||
PathBuf::from("/usr/lib"),
|
PathBuf::from("/usr/lib"),
|
||||||
]);
|
]);
|
||||||
|
// An app bundle keeps its libraries in `Contents/Frameworks`, beside
|
||||||
|
// the `Contents/MacOS` the executable is in; then Homebrew's
|
||||||
|
// `onnxruntime`, Apple silicon's prefix before Intel's. Homebrew's build
|
||||||
|
// may lack CoreML, which the probe finds out for itself.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(bin) = exe.parent() {
|
||||||
|
dirs.push(bin.join("../Frameworks"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dirs.extend([
|
||||||
|
PathBuf::from("/opt/homebrew/lib"),
|
||||||
|
PathBuf::from("/usr/local/lib"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
dirs
|
dirs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ ort = { workspace = true, features = ["cuda", "tensorrt"] }
|
|||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
ort = { workspace = true, features = ["qnn"] }
|
ort = { workspace = true, features = ["qnn"] }
|
||||||
|
|
||||||
|
# The Apple rung: CoreML's option builder, which fills the runtime's generic
|
||||||
|
# key/value map. `ort-sys`'s `coreml` feature is empty; nothing links.
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
ort = { workspace = true, features = ["coreml"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# The floor: `tract` supplies the API table when no runtime file is found, or
|
# The floor: `tract` supplies the API table when no runtime file is found, or
|
||||||
# always, in a build without `native`. Tests want this and nothing else.
|
# always, in a build without `native`. Tests want this and nothing else.
|
||||||
|
|||||||
@@ -44,6 +44,20 @@ pub fn context_path(cfg: &Config, bytes: &[u8]) -> PathBuf {
|
|||||||
.join(format!("{:016x}_ctx.onnx", hash(bytes)))
|
.join(format!("{:016x}_ctx.onnx", hash(bytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where CoreML compiles `bytes` to: one directory per model, because
|
||||||
|
/// CoreML's own cache key leaves out the weights of a model loaded from
|
||||||
|
/// memory (`session::coreml`), and one per runtime version, which wrote it.
|
||||||
|
pub fn coreml_dir(cfg: &Config, bytes: &[u8]) -> PathBuf {
|
||||||
|
let runtime = match crate::api::runtime() {
|
||||||
|
crate::Runtime::OnnxRuntime { version, .. } => version,
|
||||||
|
crate::Runtime::Tract => "tract".into(),
|
||||||
|
};
|
||||||
|
cfg.cache_dir
|
||||||
|
.join("coreml")
|
||||||
|
.join(runtime)
|
||||||
|
.join(format!("{:016x}", hash(bytes)))
|
||||||
|
}
|
||||||
|
|
||||||
/// After the probe: compile every configured model the selected rung can
|
/// After the probe: compile every configured model the selected rung can
|
||||||
/// take, smallest first, recording each as it lands.
|
/// take, smallest first, recording each as it lands.
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -90,12 +104,27 @@ pub fn run() {
|
|||||||
Source::Bytes(b) => (b.to_vec(), format!("embedded {role:?}")),
|
Source::Bytes(b) => (b.to_vec(), format!("embedded {role:?}")),
|
||||||
};
|
};
|
||||||
let key = key(rung, &bytes);
|
let key = key(rung, &bytes);
|
||||||
if state().lock().unwrap().cache.compiled.contains(&key) {
|
{
|
||||||
|
let s = state().lock().unwrap();
|
||||||
|
if s.cache.compiled.contains(&key) || s.cache.refused.contains(&key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
log::info!("inference: compiling {name} for {}", rung.label());
|
log::info!("inference: compiling {name} for {}", rung.label());
|
||||||
let started = std::time::Instant::now();
|
let started = std::time::Instant::now();
|
||||||
match crate::session::build(rung, role, &bytes, &cfg) {
|
let built = match crate::probe::attempt(&cfg, &key, || {
|
||||||
|
crate::session::build(rung, role, &bytes, &cfg)
|
||||||
|
}) {
|
||||||
|
Ok(built) => built,
|
||||||
|
Err(_) => {
|
||||||
|
// Refused: the process died inside this compile before.
|
||||||
|
let mut s = state().lock().unwrap();
|
||||||
|
s.cache.refused.insert(key);
|
||||||
|
crate::probe::write_cache(&s.config, &s.cache);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match built {
|
||||||
Ok(session) => {
|
Ok(session) => {
|
||||||
drop(session);
|
drop(session);
|
||||||
let mut s = state().lock().unwrap();
|
let mut s = state().lock().unwrap();
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ pub enum Rung {
|
|||||||
MiGraphX,
|
MiGraphX,
|
||||||
/// Qualcomm's Hexagon NPU through QNN, int8 models only. Android only.
|
/// Qualcomm's Hexagon NPU through QNN, int8 models only. Android only.
|
||||||
Hexagon,
|
Hexagon,
|
||||||
|
/// Apple, through CoreML: the Neural Engine, the GPU or the CPU, as
|
||||||
|
/// CoreML schedules it. macOS only. Compiles an ML Program per model on
|
||||||
|
/// first use, so it is a compiling rung with the CPU below it. The
|
||||||
|
/// embedder stays on the CPU, as on the Hexagon: the Neural Engine
|
||||||
|
/// computes in fp16 (§7).
|
||||||
|
CoreMl,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rung {
|
impl Rung {
|
||||||
@@ -88,6 +94,7 @@ impl Rung {
|
|||||||
Rung::TensorRt => "TensorRT",
|
Rung::TensorRt => "TensorRT",
|
||||||
Rung::MiGraphX => "MIGraphX",
|
Rung::MiGraphX => "MIGraphX",
|
||||||
Rung::Hexagon => "Hexagon NPU",
|
Rung::Hexagon => "Hexagon NPU",
|
||||||
|
Rung::CoreMl => "CoreML",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,13 +103,16 @@ impl Rung {
|
|||||||
fn fallback(self) -> Rung {
|
fn fallback(self) -> Rung {
|
||||||
match self {
|
match self {
|
||||||
Rung::TensorRt => Rung::Cuda,
|
Rung::TensorRt => Rung::Cuda,
|
||||||
Rung::MiGraphX | Rung::Hexagon | Rung::Cuda | Rung::Cpu => Rung::Cpu,
|
Rung::MiGraphX | Rung::Hexagon | Rung::CoreMl | Rung::Cuda | Rung::Cpu => Rung::Cpu,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a session on this rung needs an engine built first.
|
/// Whether a session on this rung needs an engine built first.
|
||||||
fn compiles(self) -> bool {
|
fn compiles(self) -> bool {
|
||||||
matches!(self, Rung::TensorRt | Rung::MiGraphX | Rung::Hexagon)
|
matches!(
|
||||||
|
self,
|
||||||
|
Rung::TensorRt | Rung::MiGraphX | Rung::Hexagon | Rung::CoreMl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The model form this rung wants for a role.
|
/// The model form this rung wants for a role.
|
||||||
@@ -116,9 +126,11 @@ impl Rung {
|
|||||||
/// Whether this rung runs `role` at all. The Hexagon takes int8 graphs
|
/// Whether this rung runs `role` at all. The Hexagon takes int8 graphs
|
||||||
/// only, and the embedder is never int8 (§7) — it runs on the CPU
|
/// only, and the embedder is never int8 (§7) — it runs on the CPU
|
||||||
/// beside a detector on the NPU, so its vectors compare across devices.
|
/// beside a detector on the NPU, so its vectors compare across devices.
|
||||||
|
/// CoreML is kept off the embedder for the same reason: the Neural
|
||||||
|
/// Engine is fp16, and which unit runs a graph is CoreML's choice.
|
||||||
fn serves(self, role: Role) -> bool {
|
fn serves(self, role: Role) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Rung::Hexagon => role != Role::Embedder,
|
Rung::Hexagon | Rung::CoreMl => role != Role::Embedder,
|
||||||
_ => true,
|
_ => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,6 +360,12 @@ struct Cache {
|
|||||||
/// the fingerprint changes: a wedged driver must not cost every launch
|
/// the fingerprint changes: a wedged driver must not cost every launch
|
||||||
/// thirty seconds.
|
/// thirty seconds.
|
||||||
failed: Vec<(Rung, String)>,
|
failed: Vec<(Rung, String)>,
|
||||||
|
/// Engine keys whose compile the process died inside, launch after
|
||||||
|
/// launch (`probe::attempt`). Left on the fallback until the
|
||||||
|
/// fingerprint changes. Defaulted, so a cache from before this field
|
||||||
|
/// still reads.
|
||||||
|
#[serde(default)]
|
||||||
|
refused: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
@@ -631,6 +649,27 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn coreml_takes_a_compiled_detector_and_never_the_embedder() {
|
||||||
|
let hash = engines::hash(b"detector");
|
||||||
|
let mut s = State {
|
||||||
|
config: Config::default(),
|
||||||
|
cache: Cache {
|
||||||
|
rung: Some(Rung::CoreMl),
|
||||||
|
..Cache::default()
|
||||||
|
},
|
||||||
|
probing: false,
|
||||||
|
wanted: 0,
|
||||||
|
};
|
||||||
|
let on = |s: &State, role| effective_rung(s, Rung::CoreMl, role, Form::F32, hash);
|
||||||
|
// Before its program is compiled the detector waits on the CPU.
|
||||||
|
assert_eq!(on(&s, Role::Detector), Rung::Cpu);
|
||||||
|
s.cache.compiled.insert(engines::key_of(Rung::CoreMl, hash));
|
||||||
|
assert_eq!(on(&s, Role::Detector), Rung::CoreMl);
|
||||||
|
// The embedder does not move, compiled or not (§7).
|
||||||
|
assert_eq!(on(&s, Role::Embedder), Rung::Cpu);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_status_reports_only_the_rungs_above_the_selection() {
|
fn the_status_reports_only_the_rungs_above_the_selection() {
|
||||||
let _serial = serial();
|
let _serial = serial();
|
||||||
|
|||||||
@@ -16,10 +16,15 @@ use crate::{api::Runtime, state, Cache, Config, Form, Role, Rung};
|
|||||||
fn ladder(ceiling: Option<Rung>) -> Vec<Rung> {
|
fn ladder(ceiling: Option<Rung>) -> Vec<Rung> {
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
let all = [Rung::Hexagon];
|
let all = [Rung::Hexagon];
|
||||||
|
// Unmeasured (§2 ⁵): it is on the ladder because the probe's clock and
|
||||||
|
// `attempt` make a wrong guess cost one slow or failed probe, not a
|
||||||
|
// slow or crashing app.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let all = [Rung::CoreMl];
|
||||||
// A desktop has one vendor's GPU; the other vendor's providers are
|
// A desktop has one vendor's GPU; the other vendor's providers are
|
||||||
// "not enabled in this build" or a library that fails to load, and
|
// "not enabled in this build" or a library that fails to load, and
|
||||||
// either answer arrives in milliseconds.
|
// either answer arrives in milliseconds.
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(any(target_os = "android", target_os = "macos")))]
|
||||||
let all = [Rung::TensorRt, Rung::Cuda, Rung::MiGraphX];
|
let all = [Rung::TensorRt, Rung::Cuda, Rung::MiGraphX];
|
||||||
all.into_iter()
|
all.into_iter()
|
||||||
.filter(|r| ceiling.is_none_or(|c| *r <= c))
|
.filter(|r| ceiling.is_none_or(|c| *r <= c))
|
||||||
@@ -81,7 +86,11 @@ pub fn run(runtime: Runtime) {
|
|||||||
log::info!("inference: floor {floor:.1} ms on the CPU provider");
|
log::info!("inference: floor {floor:.1} ms on the CPU provider");
|
||||||
|
|
||||||
for rung in ladder(cfg.ceiling) {
|
for rung in ladder(cfg.ceiling) {
|
||||||
match time_rung(rung, role, &canonical, &cfg) {
|
let timed = attempt(&cfg, &format!("probe {}", rung.label()), || {
|
||||||
|
time_rung(rung, role, &canonical, &cfg)
|
||||||
|
})
|
||||||
|
.and_then(|timed| timed);
|
||||||
|
match timed {
|
||||||
Ok((ms, key)) if ms < floor => {
|
Ok((ms, key)) if ms < floor => {
|
||||||
cache.rung = Some(rung);
|
cache.rung = Some(rung);
|
||||||
cache.reason = format!("{ms:.1} ms against {floor:.1} ms on the CPU");
|
cache.reason = format!("{ms:.1} ms against {floor:.1} ms on the CPU");
|
||||||
@@ -118,6 +127,52 @@ fn finish(cache: Cache) {
|
|||||||
s.probing = false;
|
s.probing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many launches in a row may die inside one attempt before it is
|
||||||
|
/// refused. Two, not one: quitting the app while TensorRT spends forty
|
||||||
|
/// seconds on an engine leaves the same trace as a provider that aborted.
|
||||||
|
const STRIKES: u32 = 2;
|
||||||
|
|
||||||
|
/// Run `f` — a session build on a provider — with `what` written down
|
||||||
|
/// first, so that if the provider takes the process with it the next launch
|
||||||
|
/// knows what to stop trying.
|
||||||
|
///
|
||||||
|
/// A provider can fail by aborting rather than by returning an error:
|
||||||
|
/// XNNPACK did on SCRFD (§2), and a C++ exception or a panic across the C
|
||||||
|
/// API is an abort. The probe runs in the app's own process, so a rung that
|
||||||
|
/// does this once would do it on every launch, before the first photograph
|
||||||
|
/// is on screen. The file (`attempt` in the cache directory) holds the
|
||||||
|
/// attempt and how many launches have started it without finishing;
|
||||||
|
/// finishing, by success or by error, removes it. After [`STRIKES`] the
|
||||||
|
/// attempt is refused, and the caller records the refusal in the cache,
|
||||||
|
/// where it lasts until the fingerprint changes like any other failure.
|
||||||
|
pub fn attempt<T>(cfg: &Config, what: &str, f: impl FnOnce() -> T) -> Result<T, String> {
|
||||||
|
if cfg.cache_dir.as_os_str().is_empty() {
|
||||||
|
return Ok(f());
|
||||||
|
}
|
||||||
|
let path = cfg.cache_dir.join("attempt");
|
||||||
|
let died = std::fs::read_to_string(&path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| {
|
||||||
|
let (w, n) = s.split_once('\t')?;
|
||||||
|
(w == what).then(|| n.trim().parse::<u32>().ok())?
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
if died >= STRIKES {
|
||||||
|
log::error!("inference: the app died during `{what}` on the last {died} launches; not trying it again");
|
||||||
|
return Err(format!(
|
||||||
|
"the app died while trying this on {died} launches in a row"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if died > 0 {
|
||||||
|
log::warn!("inference: the last launch died during `{what}`; trying it once more");
|
||||||
|
}
|
||||||
|
let _ = std::fs::create_dir_all(&cfg.cache_dir);
|
||||||
|
let _ = std::fs::write(&path, format!("{what}\t{}", died + 1));
|
||||||
|
let out = f();
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// The smallest detector, or the smallest model of any role if there is
|
/// The smallest detector, or the smallest model of any role if there is
|
||||||
/// none. A ~2 MB detector is the cheapest real test of a provider, and the
|
/// none. A ~2 MB detector is the cheapest real test of a provider, and the
|
||||||
/// detector is the role the int8 forms exist for — the eye classifiers are
|
/// detector is the role the int8 forms exist for — the eye classifiers are
|
||||||
@@ -310,7 +365,50 @@ fn system_property(name: &str) -> String {
|
|||||||
String::from_utf8_lossy(&buf[..n.max(0) as usize]).into_owned()
|
String::from_utf8_lossy(&buf[..n.max(0) as usize]).into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
#[cfg(target_os = "macos")]
|
||||||
|
fn device_identity() -> String {
|
||||||
|
// The chip, and the OS release: CoreML ships with the OS, so a macOS
|
||||||
|
// update is a new provider as surely as a new driver is on Linux.
|
||||||
|
format!(
|
||||||
|
"{} macOS {}",
|
||||||
|
sysctl("machdep.cpu.brand_string"),
|
||||||
|
sysctl("kern.osproductversion")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn sysctl(name: &str) -> String {
|
||||||
|
extern "C" {
|
||||||
|
fn sysctlbyname(
|
||||||
|
name: *const std::ffi::c_char,
|
||||||
|
oldp: *mut std::ffi::c_void,
|
||||||
|
oldlenp: *mut usize,
|
||||||
|
newp: *mut std::ffi::c_void,
|
||||||
|
newlen: usize,
|
||||||
|
) -> i32;
|
||||||
|
}
|
||||||
|
let name = std::ffi::CString::new(name).unwrap();
|
||||||
|
let mut buf = [0u8; 256];
|
||||||
|
let mut len = buf.len();
|
||||||
|
// SAFETY: libSystem's documented call; `len` is the buffer's size in and
|
||||||
|
// the string's length, with its terminator, out.
|
||||||
|
let rc = unsafe {
|
||||||
|
sysctlbyname(
|
||||||
|
name.as_ptr(),
|
||||||
|
buf.as_mut_ptr().cast(),
|
||||||
|
&mut len,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if rc != 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let s = &buf[..len.min(buf.len())];
|
||||||
|
String::from_utf8_lossy(s.strip_suffix(&[0]).unwrap_or(s)).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
|
||||||
fn device_identity() -> String {
|
fn device_identity() -> String {
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
@@ -338,3 +436,49 @@ pub fn write_cache(cfg: &Config, cache: &Cache) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn a_cache_dir(name: &str) -> Config {
|
||||||
|
let dir = std::env::temp_dir().join(format!("dr-attempt-{}-{name}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
Config {
|
||||||
|
cache_dir: dir,
|
||||||
|
..Config::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a launch that died inside `what` leaves behind.
|
||||||
|
fn died_inside(cfg: &Config, what: &str, launches: u32) {
|
||||||
|
std::fs::create_dir_all(&cfg.cache_dir).unwrap();
|
||||||
|
std::fs::write(cfg.cache_dir.join("attempt"), format!("{what}\t{launches}")).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_finished_attempt_leaves_no_trace() {
|
||||||
|
let cfg = a_cache_dir("finished");
|
||||||
|
assert_eq!(attempt(&cfg, "probe CoreML", || 7), Ok(7));
|
||||||
|
assert!(!cfg.cache_dir.join("attempt").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_death_is_forgiven_and_two_are_not() {
|
||||||
|
let cfg = a_cache_dir("strikes");
|
||||||
|
died_inside(&cfg, "probe CoreML", 1);
|
||||||
|
assert_eq!(attempt(&cfg, "probe CoreML", || 7), Ok(7));
|
||||||
|
|
||||||
|
died_inside(&cfg, "probe CoreML", 2);
|
||||||
|
let mut ran = false;
|
||||||
|
assert!(attempt(&cfg, "probe CoreML", || ran = true).is_err());
|
||||||
|
assert!(!ran, "a refused attempt must not run");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn another_attempts_deaths_do_not_count() {
|
||||||
|
let cfg = a_cache_dir("other");
|
||||||
|
died_inside(&cfg, "probe TensorRT", 2);
|
||||||
|
assert_eq!(attempt(&cfg, "probe CUDA", || 7), Ok(7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,24 +19,61 @@ pub fn build(rung: Rung, role: Role, bytes: &[u8], cfg: &Config) -> ort::Result<
|
|||||||
// `stack_tensors`) — a panic across the C API, which is an abort. The
|
// `stack_tensors`) — a panic across the C API, which is an abort. The
|
||||||
// app never asked tract for that and does not start now.
|
// app never asked tract for that and does not start now.
|
||||||
let mut b = Session::builder()?.with_intra_threads(threads(cfg))?;
|
let mut b = Session::builder()?.with_intra_threads(threads(cfg))?;
|
||||||
|
if crate::api::runtime().is_native() {
|
||||||
|
b = with_runtime_log(b)?;
|
||||||
|
}
|
||||||
// A Hexagon session loads the compiled context when there is one and
|
// A Hexagon session loads the compiled context when there is one and
|
||||||
// compiles it from the model when there is not; the engine thread is
|
// compiles it from the model when there is not; the engine thread is
|
||||||
// what makes the second case rare (§6).
|
// what makes the second case rare (§6).
|
||||||
let context = (rung == Rung::Hexagon).then(|| crate::engines::context_path(cfg, bytes));
|
let context = (rung == Rung::Hexagon).then(|| crate::engines::context_path(cfg, bytes));
|
||||||
let ready = context.as_ref().is_some_and(|p| p.is_file());
|
let ready = context.as_ref().is_some_and(|p| p.is_file());
|
||||||
b = providers(
|
// What the rung keeps for this model: the context the Hexagon is to
|
||||||
b,
|
// write, or the directory CoreML compiles into.
|
||||||
rung,
|
let per_model = match rung {
|
||||||
role,
|
Rung::CoreMl => Some(crate::engines::coreml_dir(cfg, bytes)),
|
||||||
cfg,
|
_ if ready => None,
|
||||||
if ready { None } else { context.as_deref() },
|
_ => context.clone(),
|
||||||
)?;
|
};
|
||||||
|
b = providers(b, rung, role, cfg, per_model.as_deref())?;
|
||||||
match (ready, context) {
|
match (ready, context) {
|
||||||
(true, Some(path)) => b.commit_from_file(path),
|
(true, Some(path)) => b.commit_from_file(path),
|
||||||
_ => b.commit_from_memory(bytes),
|
_ => b.commit_from_memory(bytes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send the runtime's own messages for this session to `log`, under the
|
||||||
|
/// target `onnxruntime`, instead of to ONNX Runtime's stdio logger.
|
||||||
|
///
|
||||||
|
/// Its stderr is nowhere once the app is launched from a menu, and what a
|
||||||
|
/// provider says while it partitions a graph — how many nodes it took, which
|
||||||
|
/// operator it declined, the library it failed to load — is most of what a
|
||||||
|
/// failed rung tells you (docs/dev/inference.md §4). The level follows the
|
||||||
|
/// filter: warnings always, `debug` adds the runtime's info lines (the
|
||||||
|
/// partition counts), `trace` its verbose ones (every node placement).
|
||||||
|
fn with_runtime_log(
|
||||||
|
b: ort::session::builder::SessionBuilder,
|
||||||
|
) -> ort::Result<ort::session::builder::SessionBuilder> {
|
||||||
|
use ort::logging::LogLevel;
|
||||||
|
let level = if log::log_enabled!(target: "onnxruntime", log::Level::Trace) {
|
||||||
|
LogLevel::Verbose
|
||||||
|
} else if log::log_enabled!(target: "onnxruntime", log::Level::Debug) {
|
||||||
|
LogLevel::Info
|
||||||
|
} else {
|
||||||
|
LogLevel::Warning
|
||||||
|
};
|
||||||
|
let forward = |level: LogLevel, _category: &str, _id: &str, location: &str, message: &str| {
|
||||||
|
let level = match level {
|
||||||
|
LogLevel::Verbose => log::Level::Trace,
|
||||||
|
LogLevel::Info => log::Level::Debug,
|
||||||
|
LogLevel::Warning => log::Level::Warn,
|
||||||
|
LogLevel::Error | LogLevel::Fatal => log::Level::Error,
|
||||||
|
};
|
||||||
|
log::log!(target: "onnxruntime", level, "{message} ({location})");
|
||||||
|
};
|
||||||
|
Ok(b.with_logger(std::sync::Arc::new(forward))?
|
||||||
|
.with_log_level(level)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// The intra-op pool: what the config says, else the cores less two for
|
/// The intra-op pool: what the config says, else the cores less two for
|
||||||
/// the compositor and the decoder (§9). tract ignores it.
|
/// the compositor and the decoder (§9). tract ignores it.
|
||||||
fn threads(cfg: &Config) -> usize {
|
fn threads(cfg: &Config) -> usize {
|
||||||
@@ -54,11 +91,12 @@ fn providers(
|
|||||||
rung: Rung,
|
rung: Rung,
|
||||||
role: Role,
|
role: Role,
|
||||||
cfg: &Config,
|
cfg: &Config,
|
||||||
_generate_context: Option<&std::path::Path>,
|
per_model: Option<&std::path::Path>,
|
||||||
) -> ort::Result<ort::session::builder::SessionBuilder> {
|
) -> ort::Result<ort::session::builder::SessionBuilder> {
|
||||||
use ort::ep;
|
use ort::ep;
|
||||||
match rung {
|
match rung {
|
||||||
Rung::Cpu => Ok(b),
|
Rung::Cpu => Ok(b),
|
||||||
|
Rung::CoreMl => coreml(b, per_model),
|
||||||
Rung::Cuda => {
|
Rung::Cuda => {
|
||||||
Ok(b.with_execution_providers([ep::CUDA::default().build().error_on_failure()])?)
|
Ok(b.with_execution_providers([ep::CUDA::default().build().error_on_failure()])?)
|
||||||
}
|
}
|
||||||
@@ -103,6 +141,43 @@ fn providers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CoreML, compiling an ML Program — the format with the operators these
|
||||||
|
/// graphs use and the one that reaches the Neural Engine — into `cache`.
|
||||||
|
///
|
||||||
|
/// The option names are those ONNX Runtime 1.29 reads from the generic
|
||||||
|
/// key/value map (`coreml_options.cc`), which is what `ort`'s builder
|
||||||
|
/// fills. The cache is per model because of how CoreML keys it: a model
|
||||||
|
/// committed from memory, as every session here is, has no path, and the
|
||||||
|
/// key falls back to a hash of the graph's input and node names — not its
|
||||||
|
/// weights. Two exports of one architecture would share a program. The
|
||||||
|
/// directory `engines::coreml_dir` names is the hash of the bytes.
|
||||||
|
///
|
||||||
|
/// Every compute unit is allowed, so CoreML may place a graph on the
|
||||||
|
/// Neural Engine, the GPU or the CPU; the probe's clock judges the result.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn coreml(
|
||||||
|
b: ort::session::builder::SessionBuilder,
|
||||||
|
cache: Option<&std::path::Path>,
|
||||||
|
) -> ort::Result<ort::session::builder::SessionBuilder> {
|
||||||
|
use ort::ep::{self, coreml};
|
||||||
|
let mut ep = ep::CoreML::default()
|
||||||
|
.with_model_format(coreml::ModelFormat::MLProgram)
|
||||||
|
.with_compute_units(coreml::ComputeUnits::All);
|
||||||
|
if let Some(dir) = cache {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
ep = ep.with_model_cache_dir(dir.to_string_lossy());
|
||||||
|
}
|
||||||
|
Ok(b.with_execution_providers([ep.build().error_on_failure()])?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "macos")))]
|
||||||
|
fn coreml(
|
||||||
|
_b: ort::session::builder::SessionBuilder,
|
||||||
|
_cache: Option<&std::path::Path>,
|
||||||
|
) -> ort::Result<ort::session::builder::SessionBuilder> {
|
||||||
|
unreachable!("the CoreML rung is on the macOS ladder only")
|
||||||
|
}
|
||||||
|
|
||||||
/// Register MIGraphX through ONNX Runtime's generic key/value entry point.
|
/// Register MIGraphX through ONNX Runtime's generic key/value entry point.
|
||||||
///
|
///
|
||||||
/// `ort`'s own builder (`ep::MIGraphX`) fills the legacy
|
/// `ort`'s own builder (`ep::MIGraphX`) fills the legacy
|
||||||
@@ -175,8 +250,8 @@ fn providers(
|
|||||||
.build()
|
.build()
|
||||||
.error_on_failure()])?)
|
.error_on_failure()])?)
|
||||||
}
|
}
|
||||||
Rung::Cuda | Rung::TensorRt | Rung::MiGraphX => {
|
Rung::Cuda | Rung::TensorRt | Rung::MiGraphX | Rung::CoreMl => {
|
||||||
unreachable!("no desktop GPU rung on Android")
|
unreachable!("no desktop rung on Android")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,8 +260,9 @@ impl CropRect {
|
|||||||
///
|
///
|
||||||
/// `anchor` is the point of the rect that stays put, in the rect's own
|
/// `anchor` is the point of the rect that stays put, in the rect's own
|
||||||
/// `0..1` coordinates: `(1.0, 1.0)` while the top-left handle is dragged,
|
/// `0..1` coordinates: `(1.0, 1.0)` while the top-left handle is dragged,
|
||||||
/// so the far corner is the one that does not move, and `(0.5, 0.5)` when
|
/// so the far corner is the one that does not move, `(0.0, 0.5)` while
|
||||||
/// a ratio is chosen and the composition should stay where it is.
|
/// the right-hand edge is dragged, and `(0.5, 0.5)` when a ratio is
|
||||||
|
/// chosen and the composition should stay where it is.
|
||||||
///
|
///
|
||||||
/// **The rect grows onto the ratio rather than shrinking onto it.** The
|
/// **The rect grows onto the ratio rather than shrinking onto it.** The
|
||||||
/// axis that is short is extended; the long one is never trimmed. Fitting
|
/// axis that is short is extended; the long one is never trimmed. Fitting
|
||||||
@@ -269,6 +270,7 @@ impl CropRect {
|
|||||||
/// along one axis alone would be immediately clamped back by the other,
|
/// along one axis alone would be immediately clamped back by the other,
|
||||||
/// and the handle would simply refuse to move. The result is then scaled
|
/// and the handle would simply refuse to move. The result is then scaled
|
||||||
/// down, both axes together, only as far as the frame's edge demands.
|
/// down, both axes together, only as far as the frame's edge demands.
|
||||||
|
/// The exception is an edge: see the note in the body.
|
||||||
pub fn with_aspect(self, frame_w: u32, frame_h: u32, ratio: f32, anchor: (f32, f32)) -> Self {
|
pub fn with_aspect(self, frame_w: u32, frame_h: u32, ratio: f32, anchor: (f32, f32)) -> Self {
|
||||||
let rect = self.normalised();
|
let rect = self.normalised();
|
||||||
let ratio = finite(ratio, 0.0);
|
let ratio = finite(ratio, 0.0);
|
||||||
@@ -286,8 +288,19 @@ impl CropRect {
|
|||||||
let px = rect.x + ax * rect.width;
|
let px = rect.x + ax * rect.width;
|
||||||
let py = rect.y + ay * rect.height;
|
let py = rect.y + ay * rect.height;
|
||||||
|
|
||||||
let mut w = rect.width.max(rect.height * r);
|
// An anchor in the middle of one side is an *edge* being dragged, and
|
||||||
let mut h = w / r;
|
// then the axis across that edge leads: it is the only one the user
|
||||||
|
// moved. Growing the short axis instead would take the other side
|
||||||
|
// for the leader whenever the edge went inward, and the edge would be
|
||||||
|
// pushed straight back out — a handle that only ever grows the crop.
|
||||||
|
let (mut w, mut h) = if ax == 0.5 && ay != 0.5 {
|
||||||
|
(rect.height * r, rect.height)
|
||||||
|
} else if ay == 0.5 && ax != 0.5 {
|
||||||
|
(rect.width, rect.width / r)
|
||||||
|
} else {
|
||||||
|
let w = rect.width.max(rect.height * r);
|
||||||
|
(w, w / r)
|
||||||
|
};
|
||||||
|
|
||||||
// Scaled to fit, never clamped to fit: clamping one axis against the
|
// Scaled to fit, never clamped to fit: clamping one axis against the
|
||||||
// frame would break the very ratio this exists to hold.
|
// frame would break the very ratio this exists to hold.
|
||||||
@@ -2255,6 +2268,42 @@ mod tests {
|
|||||||
assert!((c.y - start.y).abs() < 1e-5, "{c:?}");
|
assert!((c.y - start.y).abs() < 1e-5, "{c:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_locked_edge_leads_and_the_far_side_stays_put() {
|
||||||
|
// An edge dragged inward under a lock must narrow the crop. With the
|
||||||
|
// short axis leading, the untouched height would win and push the
|
||||||
|
// edge straight back out.
|
||||||
|
let start = CropRect {
|
||||||
|
x: 0.2,
|
||||||
|
y: 0.2,
|
||||||
|
width: 0.4,
|
||||||
|
height: 0.6,
|
||||||
|
};
|
||||||
|
// Right edge held, dragged in: the left side and the vertical
|
||||||
|
// centre stay, the width is what was asked for.
|
||||||
|
let c = start.with_aspect(4000, 4000, 1.0, (0.0, 0.5));
|
||||||
|
assert!((c.x - start.x).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!((c.width - start.width).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!((c.height - start.width).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!(
|
||||||
|
(c.y + c.height / 2.0 - (start.y + start.height / 2.0)).abs() < 1e-5,
|
||||||
|
"{c:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Top edge held: the bottom and the horizontal centre stay, the
|
||||||
|
// height is what was asked for.
|
||||||
|
let c = start.with_aspect(4000, 4000, 1.0, (0.5, 1.0));
|
||||||
|
assert!(
|
||||||
|
(c.y + c.height - (start.y + start.height)).abs() < 1e-5,
|
||||||
|
"{c:?}"
|
||||||
|
);
|
||||||
|
assert!((c.width - c.height).abs() < 1e-5, "{c:?}");
|
||||||
|
assert!(
|
||||||
|
(c.x + c.width / 2.0 - (start.x + start.width / 2.0)).abs() < 1e-5,
|
||||||
|
"{c:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_locked_rect_grows_onto_the_ratio_rather_than_shrinking_onto_it() {
|
fn a_locked_rect_grows_onto_the_ratio_rather_than_shrinking_onto_it() {
|
||||||
// Shrinking to fit makes a one-axis drag do nothing at all: the other
|
// Shrinking to fit makes a one-axis drag do nothing at all: the other
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# DarkRoom — macOS link check
|
||||||
|
#
|
||||||
|
# Compiles and links for macOS from Linux, with zig as the linker
|
||||||
|
# (cargo-zigbuild). Zig carries macOS's libSystem stubs and C headers, so the
|
||||||
|
# crates that need only libSystem — the inference engine, dr-plat — build,
|
||||||
|
# link and produce Mach-O test binaries here. Nothing runs: there is no macOS
|
||||||
|
# to run them on (docs/dev/macos.md §2). The desktop app needs Apple's
|
||||||
|
# framework headers (AppKit, Metal, Security), which only the Xcode SDK
|
||||||
|
# carries, so it does not link here.
|
||||||
|
#
|
||||||
|
# Build: docker build -t darkroom-macos:latest docker/macos
|
||||||
|
# Use: ./docker/macos/build.sh cargo zigbuild --target aarch64-apple-darwin -p dr-inference-engine --all-targets
|
||||||
|
|
||||||
|
FROM docker.io/library/debian:trixie-slim
|
||||||
|
|
||||||
|
# Pinned, like the Windows and Android images. Rust matches rust-toolchain.toml.
|
||||||
|
ARG RUST_VERSION=1.92.0
|
||||||
|
ARG ZIG_VERSION=0.15.2
|
||||||
|
ARG ZIG_SHA256=02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239
|
||||||
|
ARG CARGO_ZIGBUILD_VERSION=0.23.4
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
|
CARGO_HOME=/opt/cargo \
|
||||||
|
RUSTUP_HOME=/opt/rustup \
|
||||||
|
PATH=/opt/zig:/opt/cargo/bin:$PATH
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates curl git xz-utils \
|
||||||
|
# A host C compiler: build scripts and proc-macros are Linux binaries.
|
||||||
|
gcc libc6-dev \
|
||||||
|
# `file` says Mach-O; the smoke check in build.sh reads it.
|
||||||
|
file \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz" -o /tmp/zig.tar.xz \
|
||||||
|
&& echo "${ZIG_SHA256} /tmp/zig.tar.xz" | sha256sum -c - \
|
||||||
|
&& mkdir /opt/zig && tar xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1 \
|
||||||
|
&& rm /tmp/zig.tar.xz && zig version
|
||||||
|
|
||||||
|
# The components rust-toolchain.toml lists, baked in so rustup does not fetch
|
||||||
|
# them inside every run.
|
||||||
|
RUN curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal \
|
||||||
|
--default-toolchain "${RUST_VERSION}" \
|
||||||
|
--component rustfmt,clippy,rust-analyzer \
|
||||||
|
--target aarch64-apple-darwin,x86_64-apple-darwin \
|
||||||
|
&& cargo install --locked "cargo-zigbuild@${CARGO_ZIGBUILD_VERSION}" \
|
||||||
|
&& rm -rf /opt/cargo/registry \
|
||||||
|
&& chmod -R a+rwX /opt/cargo /opt/rustup
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run a command inside the DarkRoom macOS link-check container.
|
||||||
|
#
|
||||||
|
# ./docker/macos/build.sh cargo zigbuild --target aarch64-apple-darwin -p dr-inference-engine --all-targets
|
||||||
|
# ./docker/macos/build.sh # interactive shell
|
||||||
|
#
|
||||||
|
# Builds the image on first use; `--rebuild` after editing the Dockerfile.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE="darkroom-macos:latest"
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO="$(cd "${HERE}/../.." && pwd)"
|
||||||
|
|
||||||
|
if command -v podman >/dev/null 2>&1; then
|
||||||
|
ENGINE=podman
|
||||||
|
elif command -v docker >/dev/null 2>&1; then
|
||||||
|
ENGINE=docker
|
||||||
|
else
|
||||||
|
echo "error: neither podman nor docker found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--rebuild" ]]; then
|
||||||
|
shift
|
||||||
|
"${ENGINE}" build -t "${IMAGE}" "${HERE}"
|
||||||
|
elif ! "${ENGINE}" image inspect "${IMAGE}" >/dev/null 2>&1; then
|
||||||
|
echo "==> building ${IMAGE} (first run; a few minutes)"
|
||||||
|
"${ENGINE}" build -t "${IMAGE}" "${HERE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Registry, target and zig's own cache persist across runs.
|
||||||
|
CACHE="${XDG_CACHE_HOME:-${HOME}/.cache}/darkroom-macos"
|
||||||
|
mkdir -p "${CACHE}/registry" "${CACHE}/target" "${CACHE}/home"
|
||||||
|
|
||||||
|
ARGS=(
|
||||||
|
--rm
|
||||||
|
-v "${REPO}:/work:z"
|
||||||
|
-v "${CACHE}/registry:/opt/cargo/registry:z"
|
||||||
|
-v "${CACHE}/target:/work/target-macos:z"
|
||||||
|
-v "${CACHE}/home:/tmp/home:z"
|
||||||
|
-e HOME=/tmp/home
|
||||||
|
-e CARGO_TARGET_DIR=/work/target-macos
|
||||||
|
-w /work
|
||||||
|
)
|
||||||
|
|
||||||
|
# Capped for the same reason as the Windows image: a cross build otherwise
|
||||||
|
# takes every thread on the host.
|
||||||
|
JOBS="${DARKROOM_BUILD_JOBS:-8}"
|
||||||
|
if [[ "${JOBS}" != "0" ]]; then
|
||||||
|
ARGS+=(--cpus "${JOBS}" -e "CARGO_BUILD_JOBS=${JOBS}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${ENGINE}" == "docker" ]]; then
|
||||||
|
ARGS+=(--user "$(id -u):$(id -g)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
ARGS+=(-it)
|
||||||
|
set -- /bin/bash
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "${ENGINE}" run "${ARGS[@]}" "${IMAGE}" "$@"
|
||||||
+15
-3
@@ -151,10 +151,14 @@ winning:
|
|||||||
| Linux / Windows, NVIDIA GPU | TensorRT, f32 model, fp16 engine | CUDA provider, f32 | ORT CPU, f32 | tract |
|
| Linux / Windows, NVIDIA GPU | TensorRT, f32 model, fp16 engine | CUDA provider, f32 | ORT CPU, f32 | tract |
|
||||||
| Linux, AMD GPU with ROCm | MIGraphX, f32 model, fp16 program | ORT CPU, f32 | — | tract |
|
| Linux, AMD GPU with ROCm | MIGraphX, f32 model, fp16 program | ORT CPU, f32 | — | tract |
|
||||||
| Linux / Windows, no GPU stack | ORT CPU, f32 | — | — | tract |
|
| Linux / Windows, no GPU stack | ORT CPU, f32 | — | — | tract |
|
||||||
| macOS ⁵ | ORT CPU, f32 | — | — | tract |
|
| macOS ⁵ | CoreML, f32 model, ML Program | ORT CPU, f32 | — | tract |
|
||||||
|
|
||||||
⁵ CoreML is the obvious rung and is unmeasured; it is listed so its absence is a gap and not an
|
⁵ **Unmeasured**, and the one exception to the rule below: nobody here has a Mac. The rung is on
|
||||||
oversight.
|
the ladder because the probe makes a wrong guess cheap — a CoreML that is slower than the CPU is
|
||||||
|
rejected by §4's clock, one that errors is recorded as failed, and one that takes the process
|
||||||
|
down is refused on the third launch (§4, `attempt`). The embedder stays on the CPU (§7). The first
|
||||||
|
macOS log that shows a probe line is this row's measurement; [macos.md](macos.md) says what to
|
||||||
|
ask for.
|
||||||
|
|
||||||
Deliberately **not** on any ladder, with the measurement that excluded each: NNAPI (no driver),
|
Deliberately **not** on any ladder, with the measurement that excluded each: NNAPI (no driver),
|
||||||
XNNPACK (slower than CPU, aborts on SCRFD), WebGPU (slower than CPU), the Adreno through QNN (works,
|
XNNPACK (slower than CPU, aborts on SCRFD), WebGPU (slower than CPU), the Adreno through QNN (works,
|
||||||
@@ -270,6 +274,14 @@ What the probe may not do:
|
|||||||
- **Retry a rung that failed within a session.** A failed probe is cached as a failure with the
|
- **Retry a rung that failed within a session.** A failed probe is cached as a failure with the
|
||||||
same inputs; the rung is tried again when an input changes. Otherwise a wedged driver means a
|
same inputs; the rung is tried again when an input changes. Otherwise a wedged driver means a
|
||||||
thirty-second stall on every launch.
|
thirty-second stall on every launch.
|
||||||
|
- **Crash the app twice for the same reason.** The probe runs in the app's process, and a provider
|
||||||
|
can fail by aborting rather than by returning an error (XNNPACK on SCRFD, §2). Every session build
|
||||||
|
on a rung above the CPU — the probe's, and each background compile of §6 — writes what it is
|
||||||
|
attempting to `attempt` in the cache directory first and removes it after. A launch that finds the
|
||||||
|
file knows the last one died inside that attempt; after two such launches in a row the attempt is
|
||||||
|
refused and recorded like any other failure (a rung in `failed`, an engine in `refused`), until
|
||||||
|
the fingerprint changes. Two, not one, because quitting during a forty-second TensorRT compile
|
||||||
|
leaves the same file.
|
||||||
- **Choose for the user without saying so.** Settings gains one row, *Inference backend*, showing
|
- **Choose for the user without saying so.** Settings gains one row, *Inference backend*, showing
|
||||||
what was chosen and why in one line ("Hexagon NPU · int8 · QNN 2.42"; "CPU · ONNX Runtime 1.30 ·
|
what was chosen and why in one line ("Hexagon NPU · int8 · QNN 2.42"; "CPU · ONNX Runtime 1.30 ·
|
||||||
TensorRT probe failed: cuDNN 8 required"), with an override to force any lower rung. The about
|
TensorRT probe failed: cuDNN 8 required"), with an override to force any lower rung. The about
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# macOS
|
||||||
|
|
||||||
|
macOS is out of scope for v1 ([requirements.md](requirements.md)), and nobody working on
|
||||||
|
DarkRoom has a Mac. This page records what exists anyway, and how a macOS build is set up so
|
||||||
|
that someone who does have one can send back enough to fix what they hit.
|
||||||
|
|
||||||
|
## 1. What exists
|
||||||
|
|
||||||
|
- **Inference** ([inference.md §2](inference.md)). The macOS ladder is CoreML, then ONNX
|
||||||
|
Runtime's CPU provider, then tract. CoreML is unmeasured. The probe decides whether it is used,
|
||||||
|
and the crash guard (§4, `attempt`) covers the case where the provider takes the process down.
|
||||||
|
The device fingerprint is the chip (`machdep.cpu.brand_string`) and the OS release, because
|
||||||
|
CoreML ships with the OS.
|
||||||
|
- **Where files go** ([`dr_plat::dirs`](../../platform/dr-plat/src/dirs.rs)). The Unix rules,
|
||||||
|
except the state directory (the log and crash records), which is `~/Library/Logs/darkroom`.
|
||||||
|
- **A diagnostic build**, described in §3.
|
||||||
|
|
||||||
|
The rest is not built, packaged or run on macOS by anyone here. This covers the window,
|
||||||
|
Metal through wgpu, the display profile (FR-DSP-8 asks X11 and Wayland), the keyring, the
|
||||||
|
bundle, and signing. `dr-plat` sends every non-Android Unix to the X11/Wayland dependencies.
|
||||||
|
|
||||||
|
## 2. Building
|
||||||
|
|
||||||
|
`docker/macos` compiles and links for macOS from Linux, using zig as the linker
|
||||||
|
(`cargo-zigbuild`). Zig carries libSystem's stubs and the C headers, so tract's SIMD kernels
|
||||||
|
compile and anything that needs only libSystem links:
|
||||||
|
|
||||||
|
./docker/macos/build.sh cargo zigbuild --target aarch64-apple-darwin -p dr-inference-engine --features native --all-targets
|
||||||
|
./docker/macos/build.sh cargo-zigbuild clippy --target aarch64-apple-darwin -p dr-inference-engine --features native --all-targets -- -D warnings
|
||||||
|
|
||||||
|
That produces Mach-O arm64 test binaries and the `ladder` and `ep_probe` examples. Nothing runs
|
||||||
|
them. Anything that links an Apple framework needs the Xcode SDK, which zig does not carry. That
|
||||||
|
includes `dr-plat` (through the keyring's Security and CoreFoundation) and so the desktop app, and
|
||||||
|
its link fails with `unable to find framework`. `cargo check` for those still works in the
|
||||||
|
container.
|
||||||
|
|
||||||
|
Linking the app needs Apple's SDK, which means a Mac. On one:
|
||||||
|
|
||||||
|
cargo build --profile diagnostic -p darkroom-desktop
|
||||||
|
./tools/fetch-desktop-runtime.sh # ONNX Runtime 1.29.0 with CoreML, Apple silicon only
|
||||||
|
|
||||||
|
The fetch script puts `libonnxruntime.dylib` in the user's `runtime/` directory, next to the
|
||||||
|
models. The app also looks in `Contents/Frameworks` of its own bundle, and in Homebrew's
|
||||||
|
`/opt/homebrew/lib` and `/usr/local/lib`. Homebrew's build may not include CoreML; the probe
|
||||||
|
reports that as a failed rung and uses the CPU.
|
||||||
|
|
||||||
|
**For whoever packages it.** A notarised app runs with the hardened runtime, whose library
|
||||||
|
validation refuses to `dlopen` a library signed by another team. A bundled
|
||||||
|
`Contents/Frameworks/libonnxruntime.dylib` must be signed with the app. A runtime the user
|
||||||
|
fetched needs the `com.apple.security.cs.disable-library-validation` entitlement, or it will not
|
||||||
|
load, and the app will be the tract build without saying why beyond one log line.
|
||||||
|
|
||||||
|
## 3. The diagnostic build
|
||||||
|
|
||||||
|
Every macOS build is in the hands of someone who can send a log but cannot attach a debugger,
|
||||||
|
so it is set up to log like a debug build while running at release speed.
|
||||||
|
|
||||||
|
- **The log says more.** With no `RUST_LOG`, the desktop's default filter is `debug` for every
|
||||||
|
`dr_*` crate, for `darkroom_desktop`, and for `onnxruntime`. That last one is ONNX Runtime's
|
||||||
|
own session log, which the engine forwards into `log` on every platform (`session.rs`,
|
||||||
|
`with_runtime_log`). At `debug` it includes how many nodes each provider took. At `trace`
|
||||||
|
(`RUST_LOG=onnxruntime=trace`) it lists every node's placement, which is long. The log cap is
|
||||||
|
the same as everywhere (two files of 4 MiB).
|
||||||
|
- **Backtraces have line numbers.** `--profile diagnostic` is release plus line tables. On
|
||||||
|
macOS the tables go into a `.dSYM` beside the executable, and the backtrace in a crash record
|
||||||
|
finds them only if the `.dSYM` stays next to the binary. Keep it in the bundle.
|
||||||
|
|
||||||
|
## 4. What to ask a Mac user for
|
||||||
|
|
||||||
|
`~/Library/Logs/darkroom/darkroom.log`, plus `darkroom.log.1` if present, after the first launch
|
||||||
|
and after the first scan with faces. Console.app lists it under *Log Reports*. The Settings
|
||||||
|
diagnostics bundle collects the same files. The lines that answer the open questions are:
|
||||||
|
|
||||||
|
| Line | What it tells us |
|
||||||
|
|---|---|
|
||||||
|
| `inference: ONNX Runtime … from …` / `inference: runtime tract` | Whether a runtime was found, and which one |
|
||||||
|
| `inference: floor … ms on the CPU provider` | The CPU number for §2's table |
|
||||||
|
| `inference: CoreML session built in … s` | CoreML's first compile of the probe model |
|
||||||
|
| `inference: CoreML rejected: …` / `failed: …` | Why the CPU was kept |
|
||||||
|
| `onnxruntime` lines naming `CoreMLExecutionProvider::GetCapability` | How much of the graph CoreML took |
|
||||||
|
| `inference: the app died during …` | The crash guard fired, and on what |
|
||||||
|
| `inference: compiling … for CoreML` / `ready on CoreML in … s` | Each model's compile, and any that CoreML refused |
|
||||||
|
|
||||||
|
Also ask for the settings row (*Settings › About › Inference*), which is one line and says the
|
||||||
|
same in short. When one of these logs comes back with CoreML numbers, they go into
|
||||||
|
[inference.md §1–2](inference.md), and footnote ⁵ becomes a measurement.
|
||||||
+31
-31
File diff suppressed because one or more lines are too long
+22
-22
@@ -39,7 +39,7 @@ The list is longer than it is tall, so a way to walk it that cannot be lost to t
|
|||||||
|
|
||||||
Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as magnifying the picture rather than sliding it about. Double-tap is the way to an exact 1:1; this is the way to everything in between. Past 1:1 the pixels are shown as they are, square and unsmoothed; below it, filtered.
|
Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as magnifying the picture rather than sliding it about. Double-tap is the way to an exact 1:1; this is the way to everything in between. Past 1:1 the pixels are shown as they are, square and unsmoothed; below it, filtered.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:1982`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2025`</sub>
|
||||||
|
|
||||||
### Move a magnified photograph about
|
### Move a magnified photograph about
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as m
|
|||||||
|
|
||||||
Only once there is something outside the viewport to reach, which is why the cursor becomes a hand exactly then. The view is clamped to the frame: panning past the edge would show undefined area beside the photograph, and that reads as a rendering fault rather than as the end of the picture.
|
Only once there is something outside the viewport to reach, which is why the cursor becomes a hand exactly then. The view is clamped to the frame: panning past the edge would show undefined area beside the photograph, and that reads as a rendering fault rather than as the end of the picture.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2078`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2121`</sub>
|
||||||
|
|
||||||
### Paint a mask by hand
|
### Paint a mask by hand
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ Only once there is something outside the viewport to reach, which is why the cur
|
|||||||
|
|
||||||
A model's mask stops inside a shoulder and leaks into the hair, and no single edge control fixes two errors that go opposite ways. The whole stroke is one step in the history, so taking a mark back costs one press however long it took to make.
|
A model's mask stops inside a shoulder and leaks into the hair, and no single edge control fixes two errors that go opposite ways. The whole stroke is one step in the history, so taking a mark back costs one press however long it took to make.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2169`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2212`</sub>
|
||||||
|
|
||||||
### Open this list
|
### Open this list
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ A model's mask stops inside a shoulder and leaks into the hair, and no single ed
|
|||||||
|
|
||||||
Most of the keys are develop's, and a reference that could only be opened from the grid had to be looked up before opening the photograph they were wanted for.
|
Most of the keys are develop's, and a reference that could only be opened from the grid had to be looked up before opening the photograph they were wanted for.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2395`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2438`</sub>
|
||||||
|
|
||||||
### Take back the last change
|
### Take back the last change
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ Most of the keys are develop's, and a reference that could only be opened from t
|
|||||||
|
|
||||||
A whole drag is one step, so undo takes back a decision rather than a frame of a gesture. The list is there because arriving six steps back costs what arriving from one does.
|
A whole drag is one step, so undo takes back a decision rather than a frame of a gesture. The list is there because arriving six steps back costs what arriving from one does.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2425`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2468`</sub>
|
||||||
|
|
||||||
### Do it again after taking it back
|
### Do it again after taking it back
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ A whole drag is one step, so undo takes back a decision rather than a frame of a
|
|||||||
- **Keyboard** — `Ctrl+Shift+Z`, or `Ctrl+Y`
|
- **Keyboard** — `Ctrl+Shift+Z`, or `Ctrl+Y`
|
||||||
- **See it** — [in the manual](manual/README.md#history-snapshots-presets)
|
- **See it** — [in the manual](manual/README.md#history-snapshots-presets)
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2439`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2482`</sub>
|
||||||
|
|
||||||
### Remove a repair
|
### Remove a repair
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ A whole drag is one step, so undo takes back a decision rather than a frame of a
|
|||||||
- **Pointer** — Click it, then Delete Repair
|
- **Pointer** — Click it, then Delete Repair
|
||||||
- **Keyboard** — `Delete` or `Backspace`, while repairing
|
- **Keyboard** — `Delete` or `Backspace`, while repairing
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2459`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2502`</sub>
|
||||||
|
|
||||||
### Copy the settings from this photograph
|
### Copy the settings from this photograph
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ A whole drag is one step, so undo takes back a decision rather than a frame of a
|
|||||||
|
|
||||||
The button is the copy that has to work: a tablet has no modifier key to hold and no menu bar to hang the action from. The shortcut is an accelerator for a control that is on screen either way.
|
The button is the copy that has to work: a tablet has no modifier key to hold and no menu bar to hang the action from. The shortcut is an accelerator for a control that is on screen either way.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2478`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2521`</sub>
|
||||||
|
|
||||||
### Paste the settings onto this photograph
|
### Paste the settings onto this photograph
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ The button is the copy that has to work: a tablet has no modifier key to hold an
|
|||||||
|
|
||||||
The button names what would be pasted — "3 adjustments", and whether the crop is coming with it — which the shortcut cannot say. Both paste the same scope.
|
The button names what would be pasted — "3 adjustments", and whether the crop is coming with it — which the shortcut cannot say. Both paste the same scope.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2491`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2534`</sub>
|
||||||
|
|
||||||
### Choose which kinds of edit a copy carries
|
### Choose which kinds of edit a copy carries
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ The button names what would be pasted — "3 adjustments", and whether the crop
|
|||||||
|
|
||||||
Lightroom's Copy Settings. Pasting a look across a shoot usually means leaving each frame's crop and rotation alone, and that is a choice to make at the moment of copying.
|
Lightroom's Copy Settings. Pasting a look across a shoot usually means leaving each frame's crop and rotation alone, and that is a choice to make at the moment of copying.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2509`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2552`</sub>
|
||||||
|
|
||||||
### Export this photograph as the last one was
|
### Export this photograph as the last one was
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ Lightroom's Copy Settings. Pasting a look across a shoot usually means leaving e
|
|||||||
|
|
||||||
Every export runs on the defaults in Settings, so "as the last one was" is what the button already does. The chord is Lightroom's and darktable's, kept so hands that learned it there need not learn it again.
|
Every export runs on the defaults in Settings, so "as the last one was" is what the button already does. The chord is Lightroom's and darktable's, kept so hands that learned it there need not learn it again.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2534`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2577`</sub>
|
||||||
|
|
||||||
### Choose how to export, then export
|
### Choose how to export, then export
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ Every export runs on the defaults in Settings, so "as the last one was" is what
|
|||||||
|
|
||||||
The export sheet is the export defaults alone with an Export button. What is chosen there is kept, so it is also what the next Ctrl+Shift+E uses.
|
The export sheet is the export defaults alone with an Export button. What is chosen there is kept, so it is also what the next Ctrl+Shift+E uses.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2547`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2590`</sub>
|
||||||
|
|
||||||
### Keep a crop that leaves a mask outside
|
### Keep a crop that leaves a mask outside
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ The export sheet is the export defaults alone with an Export button. What is cho
|
|||||||
- **Pointer** — Press "Keep crop" on the notice, or "Undo crop" to take it back
|
- **Pointer** — Press "Keep crop" on the notice, or "Undo crop" to take it back
|
||||||
- **Keyboard** — `Enter` keeps it; `Ctrl+Z` takes the crop back, like any other step
|
- **Keyboard** — `Enter` keeps it; `Ctrl+Z` takes the crop back, like any other step
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2612`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2655`</sub>
|
||||||
|
|
||||||
### Go back to the grid
|
### Go back to the grid
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ The export sheet is the export defaults alone with an Export button. What is cho
|
|||||||
|
|
||||||
Lightroom's key for the grid. Escape gets there too, but a step at a time — out of a mode, then out of a zoom — where this goes straight back.
|
Lightroom's key for the grid. Escape gets there too, but a step at a time — out of a mode, then out of a zoom — where this goes straight back.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2629`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2672`</sub>
|
||||||
|
|
||||||
### Nudge the control last moved
|
### Nudge the control last moved
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ Lightroom's key for the grid. Escape gets there too, but a step at a time — ou
|
|||||||
|
|
||||||
Lightroom's keys for the selected slider. There is no focus ring on a slider here, so "selected" is the last one moved — the same control `R` puts back — which covers the framing sliders, perspective included, as well as the adjustments.
|
Lightroom's keys for the selected slider. There is no focus ring on a slider here, so "selected" is the last one moved — the same control `R` puts back — which covers the framing sliders, perspective included, as well as the adjustments.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2658`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2701`</sub>
|
||||||
|
|
||||||
### Change which group of adjustments is on screen
|
### Change which group of adjustments is on screen
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ Lightroom's keys for the selected slider. There is no focus ring on a slider her
|
|||||||
|
|
||||||
The groups are whatever the operation set declares itself to be about, so there are as many as the pipeline has and no key can be assigned to one of them by name. Stepping is the binding that survives a node being added.
|
The groups are whatever the operation set declares itself to be about, so there are as many as the pipeline has and no key can be assigned to one of them by name. Stepping is the binding that survives a node being added.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2686`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2729`</sub>
|
||||||
|
|
||||||
### Look at the photograph at 1:1
|
### Look at the photograph at 1:1
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ The groups are whatever the operation set declares itself to be about, so there
|
|||||||
|
|
||||||
Noise reduction and capture sharpening are judgements about single pixels, and a fitted view averages several of the file's into each one on screen — so the frame looks softer than it is and the correction goes too far. The point and the magnification survive opening the next photograph, which is what makes checking the same eye across forty portraits forty keystrokes rather than forty pans. From 1:1 on the photograph is drawn as its own pixels, each a hard-edged square, rather than smoothed into a blur.
|
Noise reduction and capture sharpening are judgements about single pixels, and a fitted view averages several of the file's into each one on screen — so the frame looks softer than it is and the correction goes too far. The point and the magnification survive opening the next photograph, which is what makes checking the same eye across forty portraits forty keystrokes rather than forty pans. From 1:1 on the photograph is drawn as its own pixels, each a hard-edged square, rather than smoothed into a blur.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2722`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2765`</sub>
|
||||||
|
|
||||||
### Rate this photograph
|
### Rate this photograph
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ Noise reduction and capture sharpening are judgements about single pixels, and a
|
|||||||
- **Pointer** — Click a star in the top bar
|
- **Pointer** — Click a star in the top bar
|
||||||
- **Keyboard** — `0`–`5`
|
- **Keyboard** — `0`–`5`
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2779`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2822`</sub>
|
||||||
|
|
||||||
### Pick or reject this photograph
|
### Pick or reject this photograph
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ Noise reduction and capture sharpening are judgements about single pixels, and a
|
|||||||
|
|
||||||
The grid's keys, on the photograph that is open (FR-UI-5, 2026-09-19). Judging here does not move on to the next frame: that belongs to culling, and in develop the photograph in front of you is the one being worked on.
|
The grid's keys, on the photograph that is open (FR-UI-5, 2026-09-19). Judging here does not move on to the next frame: that belongs to culling, and in develop the photograph in front of you is the one being worked on.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2785`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2828`</sub>
|
||||||
|
|
||||||
### Give this photograph a colour label
|
### Give this photograph a colour label
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ The grid's keys, on the photograph that is open (FR-UI-5, 2026-09-19). Judging h
|
|||||||
|
|
||||||
The grid's keys, on the photograph that is open, so labelling while stepping through a folder is one hand's work. The bar names the label in words beside its mark.
|
The grid's keys, on the photograph that is open, so labelling while stepping through a folder is one hand's work. The bar names the label in words beside its mark.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2815`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2858`</sub>
|
||||||
|
|
||||||
### Move to the next or previous photograph
|
### Move to the next or previous photograph
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ The grid's keys, on the photograph that is open, so labelling while stepping thr
|
|||||||
|
|
||||||
The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing. A and D as well as the arrows, so the left hand steps along the roll while the right stays on the mouse.
|
The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing. A and D as well as the arrows, so the left hand steps along the roll while the right stays on the mouse.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2840`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2883`</sub>
|
||||||
|
|
||||||
### See the photograph before you edited it
|
### See the photograph before you edited it
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ The edit on screen is saved on the way out, so stepping through a folder is as m
|
|||||||
|
|
||||||
Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards.
|
Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2970`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:3008`</sub>
|
||||||
|
|
||||||
### Put one control back to its default
|
### Put one control back to its default
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# makes `makepkg -si` in this directory install what you are actually working
|
# makes `makepkg -si` in this directory install what you are actually working
|
||||||
# on. Swap `source` for a tagged tarball when there is something to release.
|
# on. Swap `source` for a tagged tarball when there is something to release.
|
||||||
pkgname=darkroom
|
pkgname=darkroom
|
||||||
pkgver=0.19.1
|
pkgver=0.19.2
|
||||||
# Back to 1 with the version: a new pkgver is a new archive name, so there is
|
# Back to 1 with the version: a new pkgver is a new archive name, so there is
|
||||||
# nothing for makepkg to reuse and nothing for a release number to disambiguate.
|
# nothing for makepkg to reuse and nothing for a release number to disambiguate.
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ pub fn set_state_dir(dir: PathBuf) {
|
|||||||
|
|
||||||
/// Where this application keeps state that is neither configuration nor cache.
|
/// Where this application keeps state that is neither configuration nor cache.
|
||||||
///
|
///
|
||||||
/// `$XDG_STATE_HOME/darkroom`, falling back to `~/.local/state/darkroom`.
|
/// `$XDG_STATE_HOME/darkroom`, falling back to `~/.local/state/darkroom` —
|
||||||
|
/// `~/Library/Logs/darkroom` on macOS (`dirs`).
|
||||||
/// State rather than cache because a crash record must survive the sweep that
|
/// State rather than cache because a crash record must survive the sweep that
|
||||||
/// a cache directory exists to permit, and rather than config because it is
|
/// a cache directory exists to permit, and rather than config because it is
|
||||||
/// not something the user edits.
|
/// not something the user edits.
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
//!
|
//!
|
||||||
//! * Linux: `$XDG_STATE_HOME/darkroom/darkroom.log`, else
|
//! * Linux: `$XDG_STATE_HOME/darkroom/darkroom.log`, else
|
||||||
//! `~/.local/state/darkroom/darkroom.log`.
|
//! `~/.local/state/darkroom/darkroom.log`.
|
||||||
|
//! * macOS: `$XDG_STATE_HOME/darkroom/darkroom.log`, else
|
||||||
|
//! `~/Library/Logs/darkroom/darkroom.log`, where Console.app lists it.
|
||||||
//! * Android: `/sdcard/Android/data/paris.tourolle.darkroom/files/darkroom.log`,
|
//! * Android: `/sdcard/Android/data/paris.tourolle.darkroom/files/darkroom.log`,
|
||||||
//! which `adb pull` reads from an ordinary release build. See
|
//! which `adb pull` reads from an ordinary release build. See
|
||||||
//! [`crate::state`] for why not the internal directory, and
|
//! [`crate::state`] for why not the internal directory, and
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
//! | Unix default | `~/.config` | `~/.local/share` | `~/.local/state` |
|
//! | Unix default | `~/.config` | `~/.local/share` | `~/.local/state` |
|
||||||
//! | Windows | `%APPDATA%` | `%LOCALAPPDATA%` | `%LOCALAPPDATA%`, then `state` |
|
//! | Windows | `%APPDATA%` | `%LOCALAPPDATA%` | `%LOCALAPPDATA%`, then `state` |
|
||||||
//! | Windows default | `%USERPROFILE%\AppData\Roaming` | `…\AppData\Local` | `…\AppData\Local` |
|
//! | Windows default | `%USERPROFILE%\AppData\Roaming` | `…\AppData\Local` | `…\AppData\Local` |
|
||||||
|
//! | macOS default | `~/.config` | `~/.local/share` | `~/Library/Logs` |
|
||||||
|
//!
|
||||||
|
//! macOS follows the Unix rules except for the one directory a user is asked
|
||||||
|
//! to find by hand: the log. Finder hides `~/.local`, and `~/Library/Logs`
|
||||||
|
//! is where Console.app and a Mac user already look (docs/dev/macos.md).
|
||||||
//!
|
//!
|
||||||
//! then `darkroom` under each. Config roams on Windows and the rest does not,
|
//! then `darkroom` under each. Config roams on Windows and the rest does not,
|
||||||
//! which is the same split XDG makes between config and everything else, and
|
//! which is the same split XDG makes between config and everything else, and
|
||||||
@@ -100,11 +105,18 @@ fn resolve(kind: Base, env: impl Fn(&str) -> Option<OsString>) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where state goes under `$HOME` when `XDG_STATE_HOME` does not say.
|
||||||
|
const STATE_UNDER_HOME: &str = if cfg!(target_os = "macos") {
|
||||||
|
"Library/Logs"
|
||||||
|
} else {
|
||||||
|
".local/state"
|
||||||
|
};
|
||||||
|
|
||||||
fn xdg_base(kind: Base, env: &impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
|
fn xdg_base(kind: Base, env: &impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
|
||||||
let (var, under_home) = match kind {
|
let (var, under_home) = match kind {
|
||||||
Base::Config => ("XDG_CONFIG_HOME", ".config"),
|
Base::Config => ("XDG_CONFIG_HOME", ".config"),
|
||||||
Base::Data => ("XDG_DATA_HOME", ".local/share"),
|
Base::Data => ("XDG_DATA_HOME", ".local/share"),
|
||||||
Base::State => ("XDG_STATE_HOME", ".local/state"),
|
Base::State => ("XDG_STATE_HOME", STATE_UNDER_HOME),
|
||||||
};
|
};
|
||||||
absolute(env(var)).or_else(|| absolute(env("HOME")).map(|h| h.join(under_home)))
|
absolute(env(var)).or_else(|| absolute(env("HOME")).map(|h| h.join(under_home)))
|
||||||
}
|
}
|
||||||
@@ -150,7 +162,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
xdg_base(Base::State, &e),
|
xdg_base(Base::State, &e),
|
||||||
Some(PathBuf::from("/home/someone/.local/state"))
|
Some(PathBuf::from("/home/someone").join(STATE_UNDER_HOME))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +174,7 @@ mod tests {
|
|||||||
let e = env(&[("XDG_STATE_HOME", "state"), ("HOME", "/home/someone")]);
|
let e = env(&[("XDG_STATE_HOME", "state"), ("HOME", "/home/someone")]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
xdg_base(Base::State, &e),
|
xdg_base(Base::State, &e),
|
||||||
Some(PathBuf::from("/home/someone/.local/state"))
|
Some(PathBuf::from("/home/someone").join(STATE_UNDER_HOME))
|
||||||
);
|
);
|
||||||
assert_eq!(xdg_base(Base::State, &env(&[("HOME", "")])), None);
|
assert_eq!(xdg_base(Base::State, &env(&[("HOME", "")])), None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,32 @@
|
|||||||
# directory (docs/inference.md §1.3).
|
# directory (docs/inference.md §1.3).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
DEST="${1:-${XDG_DATA_HOME:-${HOME}/.local/share}/darkroom/runtime}"
|
DEST="${1:-${XDG_DATA_HOME:-${HOME}/.local/share}/darkroom/runtime}"
|
||||||
|
|
||||||
|
# macOS: Microsoft's release archive, which carries the CoreML provider in the
|
||||||
|
# one library. Pinned, because the CoreML options the engine sets were read
|
||||||
|
# from this version's source (docs/dev/macos.md, CLAUDE.md "Providers").
|
||||||
|
# Apple silicon only: no Intel archive is published since 1.29; an Intel Mac
|
||||||
|
# takes Homebrew's `onnxruntime` or stays on tract.
|
||||||
|
if [[ "$(uname -s)" == Darwin ]]; then
|
||||||
|
ORT_VERSION=1.29.0
|
||||||
|
[[ "$(uname -m)" == arm64 ]] || {
|
||||||
|
echo "error: no ONNX Runtime ${ORT_VERSION} archive for $(uname -m); try: brew install onnxruntime" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
NAME="onnxruntime-osx-arm64-${ORT_VERSION}"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${WORK}"' EXIT
|
||||||
|
echo "==> downloading ${NAME}"
|
||||||
|
curl -fsSL "https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/${NAME}.tgz" \
|
||||||
|
| tar xz -C "${WORK}"
|
||||||
|
mkdir -p "${DEST}"
|
||||||
|
cp "${WORK}/${NAME}/lib/libonnxruntime.dylib" "${WORK}/${NAME}/LICENSE" "${DEST}/"
|
||||||
|
echo "==> runtime in ${DEST}:"
|
||||||
|
ls -1 "${DEST}" | sed 's/^/ /'
|
||||||
|
echo " (the app finds it on its next launch; Settings › About › Inference says what it chose)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
WORK="$(mktemp -d -p /var/tmp fetch-desktop-runtime.XXXXXX)"
|
WORK="$(mktemp -d -p /var/tmp fetch-desktop-runtime.XXXXXX)"
|
||||||
trap 'rm -rf "${WORK}"' EXIT
|
trap 'rm -rf "${WORK}"' EXIT
|
||||||
|
|
||||||
|
|||||||
@@ -1042,9 +1042,10 @@ fn wire_zoom_pan_crop(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
};
|
};
|
||||||
// The overlay reports the corner it is *holding*; the point
|
// The overlay reports the handle it is *holding*; the point
|
||||||
// that must not move is the opposite one. A move reports no
|
// that must not move is the opposite one — the far corner, or
|
||||||
// corner at all, and keeps the shape it already has — there is
|
// for an edge the middle of the far side. A move reports no
|
||||||
|
// handle at all, and keeps the shape it already has — there is
|
||||||
// nothing to reshape, and reshaping about a centre would drag
|
// nothing to reshape, and reshaping about a centre would drag
|
||||||
// an over-moved rect smaller instead of sliding it along the
|
// an over-moved rect smaller instead of sliding it along the
|
||||||
// edge.
|
// edge.
|
||||||
|
|||||||
+55
-17
@@ -1823,8 +1823,10 @@ in property <bool> panel-visible: true;
|
|||||||
clip: true;
|
clip: true;
|
||||||
|
|
||||||
canvas-image := Image {
|
canvas-image := Image {
|
||||||
width: 100%;
|
x: parent.fit-x;
|
||||||
height: 100%;
|
y: parent.fit-y;
|
||||||
|
width: parent.fit-w;
|
||||||
|
height: parent.fit-h;
|
||||||
source: root.canvas;
|
source: root.canvas;
|
||||||
image-fit: contain;
|
image-fit: contain;
|
||||||
// TRACES: FR-UI-4
|
// TRACES: FR-UI-4
|
||||||
@@ -1872,8 +1874,10 @@ in property <bool> panel-visible: true;
|
|||||||
// original straight away, and the last draft of the edit
|
// original straight away, and the last draft of the edit
|
||||||
// fades out over it rather than vanishing.
|
// fades out over it rather than vanishing.
|
||||||
previous-frame := Image {
|
previous-frame := Image {
|
||||||
width: 100%;
|
x: canvas-image.x;
|
||||||
height: 100%;
|
y: canvas-image.y;
|
||||||
|
width: canvas-image.width;
|
||||||
|
height: canvas-image.height;
|
||||||
source: root.canvas-previous;
|
source: root.canvas-previous;
|
||||||
image-fit: contain;
|
image-fit: contain;
|
||||||
image-rendering: canvas-image.image-rendering;
|
image-rendering: canvas-image.image-rendering;
|
||||||
@@ -1941,15 +1945,54 @@ in property <bool> panel-visible: true;
|
|||||||
// The overlay is placed against *this*, not against the whole
|
// The overlay is placed against *this*, not against the whole
|
||||||
// area — otherwise the crop rect drifts off the picture on any
|
// area — otherwise the crop rect drifts off the picture on any
|
||||||
// window whose shape differs from the image's.
|
// window whose shape differs from the image's.
|
||||||
|
//
|
||||||
|
// **While composing, the box is inset and stops above the
|
||||||
|
// photo roll.** A crop handle straddles the picture's edge,
|
||||||
|
// half of it outside, and a photograph fitted to the full
|
||||||
|
// box puts that half under this area's clip — and, along
|
||||||
|
// the bottom, under the roll's band, whose swipe handler
|
||||||
|
// takes every press inside it (see `PhotoRoll`). The bottom
|
||||||
|
// corners were drawn there, lit up, and could not be
|
||||||
|
// grabbed. Fitting the picture clear of both is the only
|
||||||
|
// fix: the handler has to be where it is, and moving the
|
||||||
|
// handles inward would leave them off the corner they
|
||||||
|
// move.
|
||||||
|
property <length> roll-band:
|
||||||
|
Library.library-total > 0
|
||||||
|
? (root.roll-open
|
||||||
|
? Theme.roll-strip + Theme.roll-reach
|
||||||
|
: Theme.roll-reach)
|
||||||
|
: 0px;
|
||||||
|
// The roll's own travel, so what stands on it rises with
|
||||||
|
// the strip rather than jumping ahead of it.
|
||||||
|
animate roll-band { duration: 180ms; easing: ease-out; }
|
||||||
|
//
|
||||||
|
// The floating "Done Composing" row stands on that band,
|
||||||
|
// and is declared after the crop overlay so it takes a
|
||||||
|
// press first: a photograph filling the width put the
|
||||||
|
// bottom-left handle under it. So the reserve is the band,
|
||||||
|
// the row's 12px margin, and the row's hit height — a
|
||||||
|
// `Button`'s press area is the touch target, not its ink.
|
||||||
|
// Every other side keeps half a handle and a finger's
|
||||||
|
// slop clear of this area's clip.
|
||||||
|
property <length> fit-inset: Develop.cropping ? 16px : 0px;
|
||||||
|
property <length> fit-below: Develop.cropping
|
||||||
|
? self.roll-band + 12px + Theme.touch-target
|
||||||
|
: 0px;
|
||||||
|
property <length> fit-x: self.fit-inset;
|
||||||
|
property <length> fit-y: self.fit-inset;
|
||||||
|
property <length> fit-w: max(1px, self.width - 2 * self.fit-inset);
|
||||||
|
property <length> fit-h: max(1px, self.height - 2 * self.fit-inset - self.fit-below);
|
||||||
|
|
||||||
property <float> src-w: root.canvas.width > 0 ? root.canvas.width : 1;
|
property <float> src-w: root.canvas.width > 0 ? root.canvas.width : 1;
|
||||||
property <float> src-h: root.canvas.height > 0 ? root.canvas.height : 1;
|
property <float> src-h: root.canvas.height > 0 ? root.canvas.height : 1;
|
||||||
property <float> box-w: self.width / 1px;
|
property <float> box-w: self.fit-w / 1px;
|
||||||
property <float> box-h: self.height / 1px;
|
property <float> box-h: self.fit-h / 1px;
|
||||||
property <float> fit-scale: min(self.box-w / self.src-w, self.box-h / self.src-h);
|
property <float> fit-scale: min(self.box-w / self.src-w, self.box-h / self.src-h);
|
||||||
property <length> shown-w: self.src-w * self.fit-scale * 1px;
|
property <length> shown-w: self.src-w * self.fit-scale * 1px;
|
||||||
property <length> shown-h: self.src-h * self.fit-scale * 1px;
|
property <length> shown-h: self.src-h * self.fit-scale * 1px;
|
||||||
property <length> shown-x: (self.width - self.shown-w) / 2;
|
property <length> shown-x: self.fit-x + (self.fit-w - self.shown-w) / 2;
|
||||||
property <length> shown-y: (self.height - self.shown-h) / 2;
|
property <length> shown-y: self.fit-y + (self.fit-h - self.shown-h) / 2;
|
||||||
|
|
||||||
// Empty and error states say what happened rather than
|
// Empty and error states say what happened rather than
|
||||||
// showing a blank canvas.
|
// showing a blank canvas.
|
||||||
@@ -2904,15 +2947,10 @@ in property <bool> panel-visible: true;
|
|||||||
// the thing that could not be reached. They stack
|
// the thing that could not be reached. They stack
|
||||||
// instead — the controls ride on top of the roll's
|
// instead — the controls ride on top of the roll's
|
||||||
// band, at whatever height it currently stands.
|
// band, at whatever height it currently stands.
|
||||||
property <length> roll-clear:
|
// The canvas's measure of the band, animated there
|
||||||
Library.library-total > 0
|
// with the roll's own travel so these rise with the
|
||||||
? (root.roll-open
|
// strip rather than jumping ahead of it.
|
||||||
? Theme.roll-strip + Theme.roll-reach
|
property <length> roll-clear: canvas-area.roll-band;
|
||||||
: Theme.roll-reach)
|
|
||||||
: 0px;
|
|
||||||
// The roll's own travel, so they rise with the strip
|
|
||||||
// rather than jumping ahead of it.
|
|
||||||
animate roll-clear { duration: 180ms; easing: ease-out; }
|
|
||||||
|
|
||||||
x: 12px;
|
x: 12px;
|
||||||
y: parent.height - self.preferred-height - 12px
|
y: parent.height - self.preferred-height - 12px
|
||||||
|
|||||||
+59
-34
@@ -37,15 +37,17 @@ export component CropOverlay inherits Rectangle {
|
|||||||
in property <float> crop-w: 1.0;
|
in property <float> crop-w: 1.0;
|
||||||
in property <float> crop-h: 1.0;
|
in property <float> crop-h: 1.0;
|
||||||
|
|
||||||
/// A dragged crop rect — `x`, `y`, `width`, `height`, then the corner the
|
/// A dragged crop rect — `x`, `y`, `width`, `height`, then the handle the
|
||||||
/// drag is holding as a `0`/`1` pair, or `-1, -1` where the whole rect is
|
/// drag is holding as a point of the rect: a `0`/`1` pair for a corner,
|
||||||
/// being moved and its shape must not be touched.
|
/// `0.5` on the axis an edge does not move, or `-1, -1` where the whole
|
||||||
|
/// rect is being moved and its shape must not be touched.
|
||||||
///
|
///
|
||||||
/// The corner is what an aspect lock needs and cannot infer: reshaping a
|
/// The handle is what an aspect lock needs and cannot infer: reshaping a
|
||||||
/// rect onto a ratio has to know which of its corners is nailed down, and
|
/// rect onto a ratio has to know which point of it is nailed down, and
|
||||||
/// only the handle that took the press knows that. It is reported as the
|
/// only the handle that took the press knows that. It is reported as the
|
||||||
/// held corner rather than as the fixed one because that is what is
|
/// held point rather than as the fixed one because that is what is
|
||||||
/// written here — Rust takes the opposite corner.
|
/// written here — Rust takes the opposite one, which for an edge is the
|
||||||
|
/// middle of the far side.
|
||||||
callback crop-changed(float, float, float, float, float, float);
|
callback crop-changed(float, float, float, float, float, float);
|
||||||
|
|
||||||
/// TRACES: FR-DEV-17
|
/// TRACES: FR-DEV-17
|
||||||
@@ -189,24 +191,43 @@ export component CropOverlay inherits Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Corner handles. Each drags one corner while the opposite
|
// Handles: the four edges, then the four corners.
|
||||||
// stays put, which is the only behaviour that lets a crop
|
//
|
||||||
// be shaped rather than merely moved.
|
// A corner drags one corner while the opposite stays put, which is the
|
||||||
for corner in [
|
// only behaviour that lets a crop be shaped rather than merely moved. An
|
||||||
|
// edge drags that one side along the axis across it and leaves the other
|
||||||
|
// three where they are — the way to trim a strip off one side without
|
||||||
|
// disturbing the corner you had already placed.
|
||||||
|
//
|
||||||
|
// Each is a point of the rect in its own `0..1` coordinates; `0.5` on an
|
||||||
|
// axis is an edge's midpoint and means "this axis does not move". The
|
||||||
|
// edges come first because among overlapping siblings the last declared
|
||||||
|
// takes the press, and on a rect dragged small the corners are the ones
|
||||||
|
// that must stay reachable.
|
||||||
|
for handle in [
|
||||||
|
{ hx: 0.5, hy: 0.0 },
|
||||||
|
{ hx: 0.5, hy: 1.0 },
|
||||||
|
{ hx: 0.0, hy: 0.5 },
|
||||||
|
{ hx: 1.0, hy: 0.5 },
|
||||||
{ hx: 0.0, hy: 0.0 },
|
{ hx: 0.0, hy: 0.0 },
|
||||||
{ hx: 1.0, hy: 0.0 },
|
{ hx: 1.0, hy: 0.0 },
|
||||||
{ hx: 0.0, hy: 1.0 },
|
{ hx: 0.0, hy: 1.0 },
|
||||||
{ hx: 1.0, hy: 1.0 },
|
{ hx: 1.0, hy: 1.0 },
|
||||||
]: Rectangle {
|
]: Rectangle {
|
||||||
property <length> size: 18px;
|
property <bool> across: handle.hx == 0.5;
|
||||||
x: parent.rx + corner.hx * parent.rw - self.size / 2;
|
property <bool> along: handle.hy == 0.5;
|
||||||
y: parent.ry + corner.hy * parent.rh - self.size / 2;
|
// A corner is a square; an edge is a bar lying along its side, with
|
||||||
width: self.size;
|
// a hit area as long as the bar and as deep as a corner's.
|
||||||
height: self.size;
|
property <length> grip: 18px;
|
||||||
|
property <length> reach: 36px;
|
||||||
|
width: self.across ? self.reach : self.grip;
|
||||||
|
height: self.along ? self.reach : self.grip;
|
||||||
|
x: parent.rx + handle.hx * parent.rw - self.width / 2;
|
||||||
|
y: parent.ry + handle.hy * parent.rh - self.height / 2;
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 12px;
|
width: parent.across ? 24px : (parent.along ? 5px : 12px);
|
||||||
height: 12px;
|
height: parent.along ? 24px : (parent.across ? 5px : 12px);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
@@ -214,7 +235,11 @@ export component CropOverlay inherits Rectangle {
|
|||||||
TouchArea {
|
TouchArea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
mouse-cursor: (corner.hx == corner.hy)
|
mouse-cursor: parent.across
|
||||||
|
? MouseCursor.ns-resize
|
||||||
|
: parent.along
|
||||||
|
? MouseCursor.ew-resize
|
||||||
|
: (handle.hx == handle.hy)
|
||||||
? MouseCursor.nwse-resize
|
? MouseCursor.nwse-resize
|
||||||
: MouseCursor.nesw-resize;
|
: MouseCursor.nesw-resize;
|
||||||
|
|
||||||
@@ -246,27 +271,27 @@ export component CropOverlay inherits Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Movement as a fraction of the frame, live while
|
// Movement as a fraction of the frame, live while the handle is
|
||||||
// the handle is held.
|
// held — and nothing along an axis the handle does not move, so
|
||||||
property <float> dx: self.fraction-x(self.mouse-x) - self.from-x;
|
// an edge dragged at a slant still only moves its own side.
|
||||||
property <float> dy: self.fraction-y(self.mouse-y) - self.from-y;
|
property <float> dx: parent.across ? 0 : self.fraction-x(self.mouse-x) - self.from-x;
|
||||||
|
property <float> dy: parent.along ? 0 : self.fraction-y(self.mouse-y) - self.from-y;
|
||||||
|
|
||||||
moved => {
|
moved => {
|
||||||
if (!self.pressed) {
|
if (!self.pressed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Dragging a left/top handle moves the origin
|
// Dragging a left/top handle moves the origin and shrinks the
|
||||||
// and shrinks the extent by the same amount;
|
// extent by the same amount; a right/bottom handle moves only
|
||||||
// a right/bottom handle moves only the extent.
|
// the extent. Rust clamps the result, so an over-drag slides
|
||||||
// Rust clamps the result, so an over-drag
|
// rather than inverting.
|
||||||
// slides rather than inverting.
|
|
||||||
root.crop-changed(
|
root.crop-changed(
|
||||||
corner.hx == 0 ? self.ox + self.dx : self.ox,
|
handle.hx == 0 ? self.ox + self.dx : self.ox,
|
||||||
corner.hy == 0 ? self.oy + self.dy : self.oy,
|
handle.hy == 0 ? self.oy + self.dy : self.oy,
|
||||||
corner.hx == 0 ? self.ow - self.dx : self.ow + self.dx,
|
handle.hx == 0 ? self.ow - self.dx : self.ow + self.dx,
|
||||||
corner.hy == 0 ? self.oh - self.dy : self.oh + self.dy,
|
handle.hy == 0 ? self.oh - self.dy : self.oh + self.dy,
|
||||||
corner.hx,
|
handle.hx,
|
||||||
corner.hy,
|
handle.hy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user