diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml index 760a7c3..55cc1cd 100644 --- a/.gitea/workflows/build-and-test.yml +++ b/.gitea/workflows/build-and-test.yml @@ -499,6 +499,12 @@ jobs: WANT=$(ls docs/manual/media | wc -l) GOT=$(ls "$INST/manual/media" | wc -l) [ "$GOT" = "$WANT" ] || { echo "FAIL: expected $WANT manual pictures, installed $GOT"; exit 1; } + # Both bundled runtimes, each with its provider beside it + # (tools/fetch-bundled-runtimes.sh). + for f in openvino/onnxruntime.dll openvino/onnxruntime_providers_openvino.dll \ + openvino/openvino.dll webgpu/onnxruntime.dll webgpu/dxcompiler.dll; do + [ -f "$INST/runtimes/$f" ] || { echo "FAIL: runtimes/$f not installed"; exit 1; } + done wine reg query 'HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\DarkRoom' 2>/dev/null \ | grep -q DisplayVersion || { echo "FAIL: no uninstall registry key"; exit 1; } wine "$INST/darkroom.exe" --version 2>/dev/null | grep -q '^darkroom-desktop ' \ diff --git a/apps/darkroom-desktop/src/main.rs b/apps/darkroom-desktop/src/main.rs index 3da5298..e89e174 100644 --- a/apps/darkroom-desktop/src/main.rs +++ b/apps/darkroom-desktop/src/main.rs @@ -106,24 +106,37 @@ fn main() -> anyhow::Result<()> { /// providers, or against the wrong cuDNN — and a system copy whose providers /// do not load is not a problem, only a slower app: the probe builds a real /// session before believing a provider. +/// +/// The order breaks ties only. The engine opens every runtime on this list +/// and loads the one whose providers fit the GPU (inference.md §3.2), so a +/// package's bundled builds — `runtimes/openvino` and `runtimes/webgpu` +/// beside each place a package installs to, from +/// `tools/fetch-bundled-runtimes.sh` — sit beside a CUDA or ROCm runtime +/// without hiding it. fn runtime_dirs() -> Vec { + // A place a package installs to, and the bundled runtimes under it. + fn packaged(dirs: &mut Vec, base: PathBuf) { + dirs.push(base.join("runtimes/openvino")); + dirs.push(base.join("runtimes/webgpu")); + dirs.push(base); + } let mut dirs = Vec::new(); if let Some(dir) = std::env::var_os("DARKROOM_ORT_DIR") { dirs.push(PathBuf::from(dir)); } if let Ok(exe) = std::env::current_exe() { if let Some(bin) = exe.parent() { - dirs.push(bin.to_path_buf()); - dirs.push(bin.join("../lib/darkroom")); + packaged(&mut dirs, bin.to_path_buf()); + packaged(&mut dirs, bin.join("../lib/darkroom")); } } dirs.push(dr_ui::inference::user_runtime_dir()); #[cfg(target_os = "linux")] - dirs.extend([ - PathBuf::from("/app/lib/darkroom"), - PathBuf::from("/usr/lib/darkroom"), - PathBuf::from("/usr/lib"), - ]); + { + packaged(&mut dirs, PathBuf::from("/app/lib/darkroom")); + packaged(&mut dirs, PathBuf::from("/usr/lib/darkroom")); + dirs.push(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 diff --git a/core/dr-inference-engine/src/api.rs b/core/dr-inference-engine/src/api.rs index 727285c..fe03302 100644 --- a/core/dr-inference-engine/src/api.rs +++ b/core/dr-inference-engine/src/api.rs @@ -144,6 +144,19 @@ fn install_best(dirs: &[PathBuf]) -> Option { std::env::set_var("ADSP_LIBRARY_PATH", dir); } + // Windows looks for a provider's own dependencies — OpenVINO's DLLs, + // which Intel's build leaves beside it — on the DLL search path, not in + // the provider's directory. Intel's Python shim prepends to `PATH` for + // the same reason; so does this, before any provider loads. + #[cfg(target_os = "windows")] + if let Some(dir) = chosen.path.parent() { + let old = std::env::var_os("PATH").unwrap_or_default(); + let dirs = std::iter::once(dir.to_path_buf()).chain(std::env::split_paths(&old)); + if let Ok(path) = std::env::join_paths(dirs) { + std::env::set_var("PATH", path); + } + } + log::info!( "inference: ONNX Runtime {} from {}", chosen.version, diff --git a/docker/windows/Dockerfile b/docker/windows/Dockerfile index 8caee35..5627a08 100644 --- a/docker/windows/Dockerfile +++ b/docker/windows/Dockerfile @@ -31,6 +31,9 @@ ENV DEBIAN_FRONTEND=noninteractive \ # --------------------------------------------------------------------------- RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl git git-lfs \ + # The bundled ONNX Runtime builds arrive as wheels, which are zips + # (tools/fetch-bundled-runtimes.sh). + unzip \ # A *host* C compiler as well as the cross one: build scripts and # proc-macros are compiled for Linux and linked with `cc`, whatever # the target. Without it the very first build script fails with diff --git a/docker/windows/package.sh b/docker/windows/package.sh index ed04618..8b4d887 100755 --- a/docker/windows/package.sh +++ b/docker/windows/package.sh @@ -90,6 +90,13 @@ for f in "${REPO}/docs/manual/media"/*; do done echo "==> staged the manual and $(ls "${STAGE}/manual/media" | wc -l) picture(s)" +# The two ONNX Runtime builds the app chooses between at launch +# (docs/dev/inference.md §3.2): Intel's OpenVINO build for an Intel GPU and +# the WebGPU build — D3D12 — for any other, both carrying the CPU provider. +# Beside the executable under `runtimes\`, where darkroom-desktop looks. +"${REPO}/tools/fetch-bundled-runtimes.sh" windows "${STAGE}/runtimes" +echo "==> staged $(ls "${STAGE}/runtimes"/* | wc -l) runtime file(s)" + # One installer in the output directory, the one just built. The directory # is cached between CI runs, so after a version bump a glob over it would find # two and the smoke test would hand Wine both names as one path. diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index 97e4814..c1b1bfc 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -15,14 +15,17 @@ license=('GPL-3.0-or-later') # Runtime: Vulkan for wgpu, and a Secret Service implementation for the # Nextcloud credentials (FR-NC-2) — gnome-keyring or kwallet both provide it. depends=('vulkan-icd-loader' 'fontconfig' 'libxkbcommon') -makedepends=('cargo' 'git') -# ONNX Runtime is loaded from /usr/lib at launch if a package put it there -# (docs/inference.md §3): the CPU build is 8–10× the built-in tract, the -# ROCm build adds the MIGraphX rung on an AMD GPU. Neither is required. +makedepends=('cargo' 'git' 'curl' 'unzip') +# Two ONNX Runtime builds ship in /usr/lib/darkroom/runtimes — Intel's +# OpenVINO build and the generic WebGPU one, both 8–10× the built-in tract +# on the CPU alone — and the app opens every runtime it finds and keeps the +# one that fits the GPU (docs/dev/inference.md §3.2). The ROCm build in +# /usr/lib adds the MIGraphX rung on an AMD GPU and outranks both there; +# Intel's OpenCL driver is what lets OpenVINO reach an Intel GPU. optdepends=('gnome-keyring: store Nextcloud credentials' 'kwallet: store Nextcloud credentials' - 'onnxruntime-cpu: run the neural models on every core' - 'onnxruntime-rocm: run the neural models on an AMD GPU') + 'onnxruntime-rocm: run the neural models on an AMD GPU' + 'intel-compute-runtime: run the neural models on an Intel GPU') options=('!lto') # the workspace sets its own LTO in Cargo.toml _repo="$(cd "${startdir}/.." && pwd)" @@ -59,6 +62,10 @@ package() { install -Dm644 "README.md" "${pkgdir}/usr/share/doc/${pkgname}/README.md" + # The bundled runtimes, where darkroom-desktop's search finds them + # (`runtimes/` under /usr/lib/darkroom). Pinned wheels, checked by hash. + ./tools/fetch-bundled-runtimes.sh linux "${pkgdir}/usr/lib/darkroom/runtimes" + # The manual: the rendered page and its pictures, where the app's Help # opens it (dr_ui::manual, through dr_plat::system_data_dirs). Offline by # design — the help sheet's "See it" links land here, on a machine that diff --git a/packaging/flatpak/paris.tourolle.darkroom.yml b/packaging/flatpak/paris.tourolle.darkroom.yml index a682732..d0dbc31 100644 --- a/packaging/flatpak/paris.tourolle.darkroom.yml +++ b/packaging/flatpak/paris.tourolle.darkroom.yml @@ -185,6 +185,15 @@ modules: install -Dm644 "models/scene/$m" "/app/share/darkroom/models/$m" done + # The two runtimes the app chooses between at launch + # (docs/dev/inference.md §3.2): Intel's OpenVINO build and the generic + # WebGPU one, each with ONNX Runtime's CPU provider — 8–10× the + # built-in tract on any machine. Pinned wheels, fetched over the + # build's network. In the sandbox the OpenVINO rung finds no Intel + # OpenCL driver and the probe settles on the CPU; WebGPU reaches the + # GPU through the runtime's Vulkan. + - ./tools/fetch-bundled-runtimes.sh linux /app/lib/darkroom/runtimes + - install -Dm644 README.md /app/share/doc/darkroom/README.md sources: diff --git a/packaging/windows/darkroom.nsi b/packaging/windows/darkroom.nsi index e84952d..86a8233 100644 --- a/packaging/windows/darkroom.nsi +++ b/packaging/windows/darkroom.nsi @@ -84,6 +84,12 @@ Section "DarkRoom" SecMain SetOutPath "$INSTDIR\manual" File /r "${STAGE}\manual\*" + ; ONNX Runtime, twice: Intel's OpenVINO build and the WebGPU build. The + ; application opens both and keeps the one that fits the GPU + ; (docs/dev/inference.md §3.2); each also runs the models on every core. + SetOutPath "$INSTDIR\runtimes" + File /r "${STAGE}\runtimes\*" + WriteUninstaller "$INSTDIR\uninstall.exe" ; Add/Remove Programs. HKCU, to match the per-user install. @@ -123,6 +129,7 @@ Section "Uninstall" Delete "$INSTDIR\uninstall.exe" RMDir /r "$INSTDIR\models" RMDir /r "$INSTDIR\manual" + RMDir /r "$INSTDIR\runtimes" RMDir "$INSTDIR" Delete "$SMPROGRAMS\${NAME}\${NAME}.lnk" diff --git a/tools/fetch-bundled-runtimes.sh b/tools/fetch-bundled-runtimes.sh new file mode 100755 index 0000000..270f428 --- /dev/null +++ b/tools/fetch-bundled-runtimes.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Fetch the two ONNX Runtime builds a desktop package ships +# (docs/dev/inference.md §3.2), each into its own directory: +# +# DEST/openvino Intel's build: the OpenVINO rung on an Intel GPU +# DEST/webgpu Microsoft's WebGPU build: the generic rung on any other +# +# ./tools/fetch-bundled-runtimes.sh {linux|windows} DEST +# +# Only one runtime loads per process; the app opens every one it finds and +# keeps the one that fits the device's GPU (`dr_inference_engine::api`). +# Both carry ONNX Runtime's CPU provider, which is the floor either way. +# A CUDA or ROCm runtime is never bundled (§3.1) — the user's own, found +# beside these, outranks both on its vendor's GPU. +# +# The wheels are PyPI's, pinned by SHA-256: the option names the engine +# sets were read from these versions' source (CLAUDE.md, "Providers"). +# Only the native libraries are kept — not the Python bindings, not +# OpenVINO's CPU plugin (ONNX Runtime's CPU provider is the floor), not +# the duplicate versioned copies a wheel holds as files. +# +# Licences: ONNX Runtime MIT, OpenVINO Apache-2.0, oneTBB Apache-2.0, the +# DirectX shader compiler (Windows WebGPU) LLVM/MIT; the texts go beside +# the libraries. +set -euo pipefail + +PLATFORM="${1:?usage: fetch-bundled-runtimes.sh linux|windows DEST}" +DEST="${2:?usage: fetch-bundled-runtimes.sh linux|windows DEST}" +PYPI="https://files.pythonhosted.org/packages" + +case "${PLATFORM}" in +linux) + ORT_OPENVINO="${PYPI}/08/07/f225999919f56506b603aaa3ff837ad563ab26f86906ed7fa7e5abcd849e/onnxruntime_openvino-1.24.1-cp313-cp313-manylinux_2_28_x86_64.whl" + ORT_OPENVINO_SHA=2c3bb73e68ac27f4891af8a595c1faf574ec68b772e6583c90a0b997a1822782 + ORT_WEBGPU="${PYPI}/7a/4e/782b2457b863e1748866b82d918323e61c2d0108a01906a278e7a86a3a55/onnxruntime_webgpu-1.27.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" + ORT_WEBGPU_SHA=3eb30b487d2b428d2e5c1ac538177ab6336cce9b204197135dc5a16753b0258e + # The Linux wheel carries OpenVINO itself, under the names the provider + # was linked against. + OPENVINO_KEEP=(libonnxruntime.so.1.24.1 libonnxruntime_providers_shared.so + libonnxruntime_providers_openvino.so libopenvino.so.2541 + libopenvino_onnx_frontend.so.2541 libopenvino_intel_gpu_plugin.so + libtbb.so.12 libtbbmalloc.so) + WEBGPU_KEEP=(libonnxruntime.so.1.27.0 libonnxruntime_providers_shared.so) + ;; +windows) + ORT_OPENVINO="${PYPI}/3e/92/46ae2cd565961a89189900f385bb2f13a9fa731ea4674001d23720fbb1e0/onnxruntime_openvino-1.24.1-cp313-cp313-win_amd64.whl" + ORT_OPENVINO_SHA=434bf49aa71393c577a456c9d76c98e6d6958a833fa0876793e3d5437b5a511a + ORT_WEBGPU="${PYPI}/dd/f3/6294f9617e97035771593d604729a420a848150054cc1c422a47a4915412/onnxruntime_webgpu-1.27.0-cp313-cp313-win_amd64.whl" + ORT_WEBGPU_SHA=c45377099fcf23ae87427eb52e2b1d35415cabb4e3419dc645f9ee08f730e9aa + # The Windows wheel leaves OpenVINO to the `openvino` wheel; its DLLs + # go in the same directory, which the engine puts on the DLL search + # path when it chooses this runtime. + OPENVINO_LIBS="${PYPI}/3c/e5/da52a86cc5f1c86871002712429cdcca0c0dbff12dfbce730b05db60340b/openvino-2025.4.1-20426-cp313-cp313-win_amd64.whl" + OPENVINO_LIBS_SHA=a28eef35e3ed497c3238eb8f3d1ee90647c449707a8b0a7630758cd15555d8dd + OPENVINO_KEEP=(onnxruntime.dll onnxruntime_providers_shared.dll + onnxruntime_providers_openvino.dll openvino.dll + openvino_onnx_frontend.dll openvino_intel_gpu_plugin.dll + tbb12.dll tbbmalloc.dll) + WEBGPU_KEEP=(onnxruntime.dll onnxruntime_providers_shared.dll dxcompiler.dll dxil.dll) + ;; +*) + echo "error: platform is linux or windows, not ${PLATFORM}" >&2 + exit 2 + ;; +esac + +WORK="$(mktemp -d)" +trap 'rm -rf "${WORK}"' EXIT + +# fetch URL SHA256 DIR: download a wheel, check it, unpack it into DIR. +fetch() { + local url="$1" sha="$2" dir="$3" whl + whl="${WORK}/$(basename "${url}")" + echo "==> $(basename "${url}")" + curl -fsSL -o "${whl}" "${url}" + echo "${sha} ${whl}" | sha256sum -c --quiet - || { + echo "error: $(basename "${url}") does not match its pinned SHA-256" >&2 + exit 1 + } + mkdir -p "${dir}" + unzip -q -o "${whl}" -d "${dir}" +} + +# keep FROM TO NAMES...: copy only NAMES, every one of which must exist. +keep() { + local from="$1" to="$2" name + shift 2 + mkdir -p "${to}" + for name in "$@"; do + local src + src="$(find "${from}" -name "${name}" -type f | head -1)" + [[ -n "${src}" ]] || { + echo "error: ${name} is not in the wheel" >&2 + exit 1 + } + install -m755 "${src}" "${to}/${name}" + done +} + +fetch "${ORT_OPENVINO}" "${ORT_OPENVINO_SHA}" "${WORK}/openvino" +if [[ -n "${OPENVINO_LIBS:-}" ]]; then + fetch "${OPENVINO_LIBS}" "${OPENVINO_LIBS_SHA}" "${WORK}/openvino" +fi +fetch "${ORT_WEBGPU}" "${ORT_WEBGPU_SHA}" "${WORK}/webgpu" + +rm -rf "${DEST}/openvino" "${DEST}/webgpu" +keep "${WORK}/openvino" "${DEST}/openvino" "${OPENVINO_KEEP[@]}" +keep "${WORK}/webgpu" "${DEST}/webgpu" "${WEBGPU_KEEP[@]}" +# The wheels' own licence and notice files — ONNX Runtime keeps its in the +# package directory, OpenVINO in `dist-info` — beside what they cover, +# each under the name of the directory it came from: two wheels unpacked +# into one directory both carry a `LICENSE`. +for rt in openvino webgpu; do + find "${WORK}/${rt}" -maxdepth 3 -type f \ + \( -iname 'LICENSE*' -o -iname 'NOTICE*' -o -iname 'ThirdPartyNotices*' \) | + while IFS= read -r f; do + wheel="$(basename "$(dirname "${f}")" .dist-info)" + [[ "${wheel}" == licenses ]] && wheel="$(basename "$(dirname "$(dirname "${f}")")" .dist-info)" + install -m644 "${f}" "${DEST}/${rt}/${wheel}.$(basename "${f}")" + done +done + +# The Linux wheel bundles OpenVINO without its licence; Apache-2.0 asks +# for the text beside the binaries. From the tag the libraries were built at. +if [[ "${PLATFORM}" == linux ]]; then + curl -fsSL -o "${DEST}/openvino/openvino-2025.4.1.LICENSE" \ + "https://raw.githubusercontent.com/openvinotoolkit/openvino/2025.4.1/LICENSE" + echo "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ${DEST}/openvino/openvino-2025.4.1.LICENSE" | + sha256sum -c --quiet - +fi + +du -sh "${DEST}/openvino" "${DEST}/webgpu"