Files
DarkRoom/docker/windows/package.sh
T
dtourolle 5a8c3e4c40 Run each model on the Hexagon in the form measured to hold it
The engine knew f32 and int8, and gave the Hexagon int8 for every role it
served. Measured on the tablet itself (inference.md §1.5), int8 lost
5% of the detector's faces at 40-80 px, moved the landmarks 1.5 px,
emptied the segmenter's scores and cost the denoiser 5-9 dB; fp16 the HTP
refuses outright. `Form` gains A16W8 and A16W16, and `Rung::form` now
names one per role: detectors and landmarks A16W8, the segmenter, scene
model, border filler and denoiser A16W16, XFeat int8. The embedder and
the eye classifiers stay on the CPU.

Each loader resolves its `<stem>.<form>.onnx` sibling; the segmenter and
XFeat, compiled into the binary, embed their quantised forms on Android
only and pick through `choose_embedded`. The probe, the compile step and
the cache fingerprint follow the form instead of assuming int8. Detectors
on the new form write `scrfd_*_a16+w600k_mbf`, and `model_ids` answers
for all three spellings.

On the tablet (ORT 1.29 + QNN 2.42), each shipped file against f32 on the
same inputs, and against the CPU's f32 time:
  SCRFD 500m/2.5g/10g  A16W8   100% of faces in every band   4.2/5.1/9.0 ms vs 17/56/198
  landmarks            A16W8   0.25 px in the 192 crop        0.5 ms vs 2.8
  YOLO26n-seg          A16W16  98.2% found, mask IoU 0.994    12.9 ms vs 90
  scene model          A16W16  98.9% of cells agree           15 ms vs 151
  MI-GAN               A16W16  41 dB from f32 in the fill     87 ms vs 488
  XFeat                int8    pano alignment 0.45 px (f32's own spread 0.41)  6.5 ms vs 58
  denoiser             A16W16  0.00 dB at every ISO            95 ms vs 1510 a tile
Face numbers are over public COCO val2017 photographs, not a library.

The APK carries the siblings (BUNDLED 15 -> 19; the old int8 detectors
removed), about 43 MB more. The Windows installer and its CI count skip
them; the Arch and Flatpak packages list their files and never had them.
The ladder example takes a role per model, which is how the per-role
forms above were seen landing on the NPU from the real probe.
2026-10-04 03:45:46 -04:00

109 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Assemble the Windows installer from a finished cross-build.
#
# ./docker/windows/build.sh docker/windows/package.sh
#
# Expects `cargo build --release --target x86_64-pc-windows-gnu -p darkroom-desktop`
# to have run in the same target directory. Produces
# DarkRoom-<version>-x86_64-setup.exe in $OUT (default: target-windows/installer).
#
# Runs inside the container, where makensis is; docs/dev/windows.md §5 is the
# specification this implements.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "${HERE}/../.." && pwd)"
# Absolute, whatever CARGO_TARGET_DIR was. NSIS on a POSIX host translates
# the backslashes in `${STAGE}\LICENSE` only when it can tell the path is a
# POSIX one, which it decides from a leading `/`; a relative `target-windows`
# — which is exactly what CI sets — reaches it as a name with a literal
# backslash in it and "LicenseData: open failed". The first CI run died there,
# after every step before it had passed.
TARGET_DIR="$(realpath -m "${CARGO_TARGET_DIR:-${REPO}/target}")"
TARGET="${TARGET_DIR}/x86_64-pc-windows-gnu/release"
OUT="$(realpath -m "${OUT:-${TARGET_DIR}/installer}")"
EXE="${TARGET}/darkroom-desktop.exe"
[[ -f "${EXE}" ]] || { echo "error: ${EXE} not built" >&2; exit 1; }
# The version, from the workspace rather than restated here — the same single
# source tools/set-version.sh writes and the APK packager reads.
VERSION="$(sed -n 's/^version = "\(.*\)"$/\1/p' "${REPO}/Cargo.toml" | head -1)"
[[ -n "${VERSION}" ]] || { echo "error: no version in Cargo.toml" >&2; exit 1; }
echo "==> version ${VERSION}"
# Staging: exactly what the installer carries (windows.md §5.2), and nothing
# from a previous run — a model dropped from the tree must not linger here
# and go on shipping.
STAGE="${OUT}/stage"
rm -rf "${STAGE}"
mkdir -p "${STAGE}/models"
cp "${EXE}" "${STAGE}/darkroom.exe"
# CRLF for the licence page: it is shown in a Windows edit control, which
# draws a bare LF as nothing and runs the whole GPL together.
sed 's/$/\r/' "${REPO}/LICENSE" > "${STAGE}/LICENSE"
# The models, with the guard every other packager carries: an LFS pointer is
# ~130 bytes and looks exactly like a model to `cp`. Shipped, it fails inside
# tract on the user's machine with a message about a broken graph rather than
# a checkout that needed `git lfs pull`. Only the weights are checked; the
# scene model's vocabulary and category descriptor are legitimately small.
#
# The directories are the ones the APK stages (assemble-apk.sh) and the Arch
# package installs: the face pair and its eye-state models, the scene model
# with its two descriptors, the panorama border filler and the denoiser. The installer
# smoke test counts the same directories, so a model added here is expected
# there without a number to update.
#
# Not the quantised siblings (`*.int8.onnx`, `*.a16w8.onnx`, `*.a16w16.onnx`):
# they are the Hexagon's forms (docs/dev/inference.md §1.5), and a Windows
# machine has no Hexagon to load them.
for dir in face scene inpaint denoise; do
for f in "${REPO}/models/${dir}"/*; do
case "$(basename "${f}")" in
README.md | *.int8.onnx | *.a16w8.onnx | *.a16w16.onnx) continue ;;
esac
if [[ "${f}" == *.onnx && "$(stat -c%s "${f}")" -lt 100000 ]]; then
echo "error: $(basename "${f}") is $(stat -c%s "${f}") bytes — an LFS pointer, not a model." >&2
echo " run: git lfs pull" >&2
exit 1
fi
cp "${f}" "${STAGE}/models/"
done
done
echo "==> staged $(ls "${STAGE}/models" | wc -l) model file(s)"
# The manual: the rendered page and its pictures, beside the executable where
# dr_ui::manual looks on Windows (dr_plat::system_data_dirs is the exe's own
# directory there). The pictures are LFS objects; a pointer is ~130 bytes of
# text that every browser draws as a broken image, so refuse it here rather
# than ship a manual with no pictures in it.
mkdir -p "${STAGE}/manual/media"
cp "${REPO}/docs/manual/index.html" "${STAGE}/manual/"
for f in "${REPO}/docs/manual/media"/*; do
if head -c 40 "${f}" | grep -q '^version https://git-lfs'; then
echo "error: $(basename "${f}") is an LFS pointer, not a picture." >&2
echo " run: git lfs pull --include='docs/manual/media/**'" >&2
exit 1
fi
cp "${f}" "${STAGE}/manual/media/"
done
echo "==> staged the manual and $(ls "${STAGE}/manual/media" | wc -l) picture(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.
rm -f "${OUT}"/DarkRoom-*-x86_64-setup.exe
INSTALLER="${OUT}/DarkRoom-${VERSION}-x86_64-setup.exe"
# NSIS wants Windows-style paths in File directives even on a POSIX host, and
# the script takes its inputs by define so nothing about the layout here is
# written into it.
makensis -V2 \
-DVERSION="${VERSION}" \
-DSTAGE="${STAGE}" \
-DOUT="${INSTALLER}" \
"${REPO}/packaging/windows/darkroom.nsi"
ls -la "${INSTALLER}"
echo "==> ${INSTALLER}"