From 82a5e21ec6fb86701e51b29ef03e0dbfbf5531e2 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 9 Aug 2026 07:42:05 +0200 Subject: [PATCH] Initial workspace: GPU context, compute pass, adaptive Slint shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establishes the v0.1 foundations on both platforms: - dr-types: SourceRef (never a filesystem path — Android SAF has none), Format, Availability, Validator with ETag quote normalisation - dr-gpu: wgpu device, compute pass writing a storage texture, resize - dr-ui: Slint shell with FR-UI-1 adaptive layout, computed in Rust to avoid a binding loop - docker/android: pinned toolchain, verified producing API 28 ARM binaries Measured the cost of the temporary CPU readback path (dr-gpu bench): compute is 0.06-0.28ms across sizes while readback is 0.63-7.43ms, so readback is 90-96% of frame time and scales with area. Recorded in ARCH §6.1 — this is why spike S1 is the priority. Mitigations pending S1: reuse the staging buffer, apply at most one resize per frame, and cap render resolution at 2048 on the long edge. 10 tests passing; core crates cross-compile for aarch64-linux-android. --- .claude/settings.json | 41 + .gitignore | 4 + Cargo.lock | 6562 +++++++++++++++++++++++++ Cargo.toml | 46 + README.md | 48 + apps/darkroom-desktop/Cargo.toml | 17 + apps/darkroom-desktop/src/main.rs | 11 + core/dr-gpu/Cargo.toml | 26 + core/dr-gpu/examples/bench.rs | 33 + core/dr-gpu/src/error.rs | 23 + core/dr-gpu/src/lib.rs | 494 ++ core/dr-gpu/src/shaders/gradient.wgsl | 40 + core/dr-types/Cargo.toml | 9 + core/dr-types/src/lib.rs | 223 + docker/android/Dockerfile | 120 + docker/android/README.md | 73 + docker/android/build.sh | 59 + docs/architecture.md | 946 ++++ docs/milestone-v0.1.md | 321 ++ docs/requirements.md | 1142 +++++ ui/dr-ui/Cargo.toml | 23 + ui/dr-ui/build.rs | 3 + ui/dr-ui/src/lib.rs | 248 + ui/dr-ui/ui/app.slint | 166 + ui/dr-ui/ui/theme.slint | 29 + 25 files changed, 10707 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 apps/darkroom-desktop/Cargo.toml create mode 100644 apps/darkroom-desktop/src/main.rs create mode 100644 core/dr-gpu/Cargo.toml create mode 100644 core/dr-gpu/examples/bench.rs create mode 100644 core/dr-gpu/src/error.rs create mode 100644 core/dr-gpu/src/lib.rs create mode 100644 core/dr-gpu/src/shaders/gradient.wgsl create mode 100644 core/dr-types/Cargo.toml create mode 100644 core/dr-types/src/lib.rs create mode 100644 docker/android/Dockerfile create mode 100644 docker/android/README.md create mode 100755 docker/android/build.sh create mode 100644 docs/architecture.md create mode 100644 docs/milestone-v0.1.md create mode 100644 docs/requirements.md create mode 100644 ui/dr-ui/Cargo.toml create mode 100644 ui/dr-ui/build.rs create mode 100644 ui/dr-ui/src/lib.rs create mode 100644 ui/dr-ui/ui/app.slint create mode 100644 ui/dr-ui/ui/theme.slint diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..d8eab09 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,41 @@ +{ + "permissions": { + "allow": [ + "Bash(curl -s \"https://api.github.com/search/code?q=WrapTexture+org:Noesis\" -H \"Accept: application/vnd.github+json\")", + "Bash(curl -s \"https://api.github.com/orgs/Noesis/repos?per_page=100\")", + "WebFetch(domain:wiki.wxwidgets.org)", + "Bash(curl -sS \"https://gitlab.com/sciter-engine/sciter-js-sdk/-/raw/main/samples.gpu/hello-es-triangle.htm\")", + "Bash(curl -sS \"https://gitlab.com/api/v4/projects/sciter-engine%2Fsciter-js-sdk/repository/tree?path=include&recursive=true&per_page=100&ref=main\")", + "Bash(python3 -c ' *)", + "Bash(curl -sL --max-time 40 \"https://api.github.com/repos/wxWidgets/wxWidgets/contents/src?ref=master\")", + "Bash(curl -sL --max-time 40 \"https://api.github.com/repos/wxWidgets/wxWidgets/contents/include/wx/android?ref=master\")", + "Bash(curl -sL --max-time 40 -H \"Accept: application/vnd.github.text-match+json\" \"https://api.github.com/search/code?q=vulkan+repo:wxWidgets/wxWidgets\")", + "Bash(curl -sS \"https://gitlab.com/sciter-engine/sciter-js-sdk/-/raw/main/include/sciter-x-video-api.h\")", + "WebFetch(domain:docs.wxwidgets.org)", + "Bash(curl -sS \"https://gitlab.com/sciter-engine/sciter-js-sdk/-/raw/main/CHANGELOG.md\")", + "Bash(curl -sL --max-time 40 \"https://raw.githubusercontent.com/wxWidgets/wxWidgets/master/docs/readme.txt\")", + "Bash(curl -sL --max-time 40 \"https://raw.githubusercontent.com/wxWidgets/wxWidgets/master/docs/licence.txt\")", + "Bash(curl -sL --max-time 40 \"https://raw.githubusercontent.com/wxWidgets/wxWidgets/master/docs/licendu.txt\")", + "Bash(curl -sS \"https://gitlab.com/api/v4/projects/sciter-engine%2Fsciter-js-sdk/repository/tree?path=build&recursive=true&per_page=100&ref=main\")", + "Bash(curl -sS \"https://gitlab.com/sciter-engine/sciter-js-sdk/-/raw/main/premake5.lua\")", + "WebFetch(domain:slack-chats.kotlinlang.org)", + "Bash(curl -sS -L \"https://sciter.com/\")", + "Bash(curl -sL --max-time 45 \"https://api.github.com/orgs/ultralight-ux/repos?per_page=100&sort=pushed\")", + "Bash(curl -sL --max-time 40 \"https://gitlab.gnome.org/GNOME/gtk/-/raw/main/gdk/gdkdmabuftexturebuilder.h\")", + "Bash(curl -sL --max-time 40 \"https://gitlab.gnome.org/GNOME/gtk/-/raw/main/gdk/gdkgltexturebuilder.h\")", + "Bash(curl -sL --max-time 40 \"https://gitlab.gnome.org/api/v4/projects/GNOME%2Fgtk/repository/tree?path=gdk&ref=main&per_page=100\")", + "WebFetch(domain:docs.slint.dev)", + "WebFetch(domain:releases.slint.dev)", + "WebFetch(domain:flutter.dev)", + "Bash(curl -sS -L \"https://sciter.com/forums/topic/status-of-quark-sciter-lite-sciterjs-android-ios/\")", + "Bash(curl -sL --max-time 45 \"https://api.github.com/repos/ultralight-ux/AppCore/git/trees/master?recursive=1\")", + "Bash(curl -sL --max-time 40 \"https://gitlab.gnome.org/GNOME/gtk/-/raw/main/gdk/meson.build\")", + "WebFetch(domain:www.jetbrains.com)", + "Bash(curl -sS \"https://gitlab.com/api/v4/projects/sciter-engine%2Fsciter-js-sdk/repository/tree?path=demos.lite&recursive=true&per_page=100&ref=main\")", + "Bash(curl -sL --max-time 40 \"https://gitlab.gnome.org/api/v4/projects/GNOME%2Fgtk/repository/commits?path=gdk/android/gdkandroidglcontext.c&ref_name=main&per_page=20\")", + "Bash(curl -sL --max-time 40 \"https://gitlab.gnome.org/GNOME/gtk/-/raw/main/gdk/android/meson.build\")", + "WebFetch(domain:docs.sciter.com)", + "Bash(curl -sS -L \"https://sciter.com/support-of-displayflex-and-displaygrid-in-sciter/\")" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c888b21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/target-android +Cargo.lock.bak +*.log diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9b24c4c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6562 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 2.0.20", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width 0.2.2", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_enums" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3091d68264354f211516b91dce6f71046e444fab1867716035f736667243affb" +dependencies = [ + "derive_utils", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.20", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "bytes", + "cfg_aliases 0.2.2", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.14", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-field-offset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9713489fbc45a2d113f58d17448c73115d533ed27fd2f2d0e5e5c69663eaf19b" +dependencies = [ + "const-field-offset-macro", + "field-offset", +] + +[[package]] +name = "const-field-offset-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b5dc851b7ea80cc4f69aa28632385ffd3e0f88014d53db3f2e5a781896ca783" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "copypasta" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6811e17f81fe246ef2bc553f76b6ee6ab41a694845df1d37e52a92b7bbd38a" +dependencies = [ + "clipboard-win", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "ctor" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" +dependencies = [ + "dtor", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "darkroom-desktop" +version = "0.1.0" +dependencies = [ + "anyhow", + "dr-ui", + "env_logger", + "log", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "derive_utils" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc05a5d33db20c784f873e84934ad94bb209a090987ac5f62fede2c178234f23" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dr-gpu" +version = "0.1.0" +dependencies = [ + "bytemuck", + "dr-types", + "env_logger", + "log", + "pollster", + "thiserror 2.0.20", + "wgpu", +] + +[[package]] +name = "dr-types" +version = "0.1.0" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "dr-ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "dr-gpu", + "dr-types", + "log", + "pollster", + "slint", + "slint-build", + "wgpu", +] + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.4", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "dtor" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "femtovg" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43d05da42e81724c16d34a150fb7fda53d3f786e5a94673ee526ff8602f0f6a" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "fnv", + "glow 0.17.0", + "imgref", + "itertools 0.14.0", + "log", + "rgb", + "slotmap", + "swash", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "fixed_decimal" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7299a780854a6d391be2ae1c8521c9368471b559dbfd6a8dbd9f407eaff100" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "log", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "fontique" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "274fa4f0f0a926ae182c7c076c078cce8a38471d15e61a102a02cac984be9813" +dependencies = [ + "hashbrown 0.17.1", + "linebender_resource_handle", + "memmap2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "parlance", + "read-fonts 0.39.2", + "roxmltree", + "smallvec", + "windows 0.62.2", + "windows-core 0.62.2", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.13.1", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "glow" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.1", + "cfg_aliases 0.2.2", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases 0.2.2", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "grid" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d12c7c642d4ce8c2e784b4751a6634bd89583912265add4a679a8882d123fbcd" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "read-fonts 0.39.2", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "htmlparser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ce8546b993eaf241d69ded33b1be6d205dd9857ec879d9d18bd05d3676e144" + +[[package]] +name = "i-slint-backend-linuxkms" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68e76f2b13846ca54bba6b8eb3b5454b26879ad987396d0a2d8abedc31a6c469" +dependencies = [ + "calloop 0.14.4", + "cfg_aliases 0.2.2", + "drm", + "gbm", + "glutin", + "i-slint-common", + "i-slint-core", + "i-slint-renderer-femtovg", + "input", + "nix", + "raw-window-handle", + "xkbcommon", +] + +[[package]] +name = "i-slint-backend-selector" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2247a4590a02a466b2f4916cc789dfe66247f6af826f00eb84a2685259a45c1c" +dependencies = [ + "cfg-if", + "cfg_aliases 0.2.2", + "i-slint-backend-linuxkms", + "i-slint-backend-winit", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", +] + +[[package]] +name = "i-slint-backend-winit" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27c7e8f53418d3017362465367b6612b6e439a68097d627d1b91c94b885f0a6" +dependencies = [ + "block2 0.6.2", + "cfg-if", + "cfg_aliases 0.2.2", + "copypasta", + "derive_more", + "futures", + "glutin", + "glutin-winit", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", + "i-slint-renderer-skia", + "lyon_path", + "muda", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-ui-kit 0.3.2", + "pin-weak", + "raw-window-handle", + "scoped-tls-hkt", + "scopeguard", + "softbuffer", + "strum", + "vtable", + "wasm-bindgen", + "web-sys", + "webbrowser", + "windows 0.62.2", + "winit", + "zbus", +] + +[[package]] +name = "i-slint-common" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e63f8ea55ba21e44567ae67d2b01a4f5d09bb1fab693b1e6c79d96bef9e456" +dependencies = [ + "derive_more", + "fontique", + "htmlparser", + "icu_decimal", + "icu_locale_core", + "icu_provider", + "pulldown-cmark", + "resvg", + "skrifa 0.42.1", +] + +[[package]] +name = "i-slint-compiler" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ea275b15a425c7f2f77481151e1f5f8f1ea83feae580273090ef6b9e192218" +dependencies = [ + "annotate-snippets", + "by_address", + "data-url", + "derive_more", + "i-slint-common", + "icu_normalizer", + "image", + "itertools 0.14.0", + "linked_hash_set", + "lyon_extra", + "lyon_path", + "num_enum", + "proc-macro2", + "quote", + "rayon", + "resvg", + "rowan", + "rspolib", + "skrifa 0.42.1", + "smol_str 0.3.6", + "strum", + "swash", + "typed-index-collections", + "unicode-segmentation", + "url", +] + +[[package]] +name = "i-slint-core" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e407dc2bd5385dcbdd9c1cf927213d2e4bacda667545abf8839c60f8290f754" +dependencies = [ + "auto_enums", + "bitflags 2.13.1", + "cfg-if", + "chrono", + "clru", + "const-field-offset", + "derive_more", + "euclid", + "i-slint-common", + "i-slint-core-macros", + "icu_normalizer", + "image", + "lyon_algorithms", + "lyon_extra", + "lyon_geom", + "lyon_path", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "once_cell", + "parley", + "pin-project", + "pin-weak", + "portable-atomic", + "raw-window-handle", + "resvg", + "rgb", + "scopeguard", + "skrifa 0.42.1", + "slab", + "strum", + "swash", + "sys-locale", + "taffy", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", + "vtable", + "wasm-bindgen", + "web-sys", + "web-time", + "windows 0.62.2", +] + +[[package]] +name = "i-slint-core-macros" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a842640da4c4d0909d25cc5a51e1f9cde02bd48f7fdfa94df10d595d4e3c80a" +dependencies = [ + "quote", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "i-slint-renderer-femtovg" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d0d99a18d66738b684758fe8460743a0aa0f64a96e05940942231a340085b1" +dependencies = [ + "cfg-if", + "const-field-offset", + "derive_more", + "femtovg", + "glow 0.17.0", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "imgref", + "lyon_path", + "pin-weak", + "rgb", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "i-slint-renderer-skia" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6eed7f3f0a9a3d3ca6e8b9d4ca233371d989351fdb2a7ab88ec368b99e7b57" +dependencies = [ + "bytemuck", + "cfg-if", + "cfg_aliases 0.2.2", + "clru", + "const-field-offset", + "derive_more", + "glow 0.17.0", + "glutin", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "lyon_path", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "pin-weak", + "raw-window-handle", + "raw-window-metal", + "read-fonts 0.39.2", + "scoped-tls-hkt", + "skia-safe", + "softbuffer", + "unicode-segmentation", + "vtable", + "windows 0.62.2", + "write-fonts", +] + +[[package]] +name = "i-slint-renderer-software" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73496f164f8e471877c35e0fadb996dcc674922712abd9c2be2cbe80c58859bd" +dependencies = [ + "bytemuck", + "clru", + "derive_more", + "euclid", + "i-slint-common", + "i-slint-core", + "integer-sqrt", + "lyon_path", + "num-traits", + "skrifa 0.42.1", + "swash", + "zeno", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_decimal" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal_data", + "icu_locale", + "icu_locale_core", + "icu_plurals", + "icu_provider", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_decimal_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22" + +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_plurals" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103" +dependencies = [ + "fixed_decimal", + "icu_locale", + "icu_plurals_data", + "icu_provider", + "zerovec", +] + +[[package]] +name = "icu_plurals_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0794db0b1a86193ac9c48768d0e6c52c54448e0870ad87907d456ee0dac964" +dependencies = [ + "icu_collections", + "icu_locale", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a2c462a4d927d512f5f882a033ddd62f33a05bb9f230d98f736ac3dc85938f" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "input" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9793345a65d71317763a33066b5d8351f8760dde8d4930fe9e39b5f14a7959d" +dependencies = [ + "bitflags 2.13.1", + "input-sys", + "libc", + "log", + "udev", +] + +[[package]] +name = "input-sys" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eee07d8e02bd95bf52b2e642cf13d33701b94c6e4b04fbf1d1fb07e9cb19e7" + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "bitflags 2.13.1", + "libc", + "plain", + "redox_syscall 0.9.1", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8575c0d003ae459399623c4def180c63b77f343b1a7fee64f249b349e7699a31" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_extra" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7755f08423275157ad1680aaecc9ccb7e0cc633da3240fea2d1522935cc15c72" +dependencies = [ + "lyon_path", + "thiserror 2.0.20", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "naga" +version = "23.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.1", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases 0.2.2", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "parlance" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5" + +[[package]] +name = "parley" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1cfcf399c774719fb1fa51bc6b91e86bdf003b03202a0902f1827ba6750746" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.17.1", + "icu_normalizer", + "icu_properties", + "icu_segmenter", + "linebender_resource_handle", + "parlance", + "parley_data", + "skrifa 0.42.1", +] + +[[package]] +name = "parley_data" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d3755e2cc12c0625b0cd2f2773c6a37dd11d1531940c945ade4aeb78f2b145" +dependencies = [ + "icu_properties", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pin-weak" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b330c9d1b92dfe68442ca20b009c717d5f0b1e3cf4965e62f704c3c6e95a1305" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +dependencies = [ + "critical-section", +] + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags 2.13.1", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.20", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.2", + "once_cell", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_syscall" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia 0.12.0", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rspolib" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fda9a7796aff63a7b1b39ccc93fffaaf65e20042984b4843041a49ca4677535" +dependencies = [ + "lazy_static", + "natord", + "snafu", + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scoped-tls-hkt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9603871ffe5df3ac39cb624790c296dbd47a400d202f56bf3e414045099524d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia 0.11.4", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skia-bindings" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2d1c3ebd697c0cbded0145e9204a38fa6b268446051b7196d0a096414ea7f3" +dependencies = [ + "bindgen", + "cc", + "flate2", + "heck", + "pkg-config", + "regex", + "serde_json", + "tar", + "toml", +] + +[[package]] +name = "skia-safe" +version = "0.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f512ac418a64194842dd05566320805dad1c957c521039db2486fd6368865bc" +dependencies = [ + "bitflags 2.13.1", + "skia-bindings", + "windows 0.62.2", +] + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts 0.39.2", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slint" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce12038f57f8f3a423564b0c9301a2ab3fda4246e4cbadd6b97acf9124bdc315" +dependencies = [ + "const-field-offset", + "i-slint-backend-selector", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", + "i-slint-renderer-software", + "num-traits", + "once_cell", + "pin-weak", + "slint-macros", + "unicode-segmentation", + "vtable", +] + +[[package]] +name = "slint-build" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76684c11441f775b10bbc7b0e6b3016a39f17efdd659ffca112f17cfd6bdaed1" +dependencies = [ + "derive_more", + "fontique", + "i-slint-compiler", + "spin_on", + "toml_edit", +] + +[[package]] +name = "slint-macros" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f2a763e72e1e53c496f571d36327e777da1eb0d9e58fceb83890c2a83ee7223" +dependencies = [ + "i-slint-compiler", + "proc-macro2", + "quote", + "spin_on", +] + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.20", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "spin_on" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076e103ed41b9864aa838287efe5f4e3a7a0362dd00671ae62a212e5e4612da2" +dependencies = [ + "pin-utils", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "taffy" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path 0.11.4", +] + +[[package]] +name = "tiny-skia" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path 0.12.0", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typed-index-collections" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898160f1dfd383b4e92e17f0512a7d62f3c51c44937b23b6ffc3a1614a8eaccd" +dependencies = [ + "bincode", + "serde", +] + +[[package]] +name = "udev" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path 0.12.0", + "ttf-parser", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtable" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea953f65593feb5287b022b83c35046d1ff49c7417b6f5851a079d8a231fe31" +dependencies = [ + "const-field-offset", + "portable-atomic", + "stable_deref_trait", + "vtable-macro", +] + +[[package]] +name = "vtable-macro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482a37f9777480673d3a99c80ac2edc69f92382230440946855fa27ebc47cba6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" +dependencies = [ + "arrayvec", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.13.1", + "cfg_aliases 0.1.1", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.1", + "block", + "bytemuck", + "cfg_aliases 0.1.1", + "core-graphics-types", + "glow 0.14.2", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" +dependencies = [ + "bitflags 2.13.1", + "js-sys", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.1", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.2", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str 0.2.2", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "write-fonts" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" +dependencies = [ + "font-types 0.11.3", + "indexmap", + "kurbo", + "log", + "read-fonts 0.39.2", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d02a2e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,46 @@ +[workspace] +resolver = "2" +members = [ + "core/dr-types", + "core/dr-gpu", + "ui/dr-ui", + "apps/darkroom-desktop", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +rust-version = "1.92" +license = "GPL-3.0-or-later" +repository = "https://github.com/dtourolle/DarkRoom" + +[workspace.dependencies] +# Internal +dr-types = { path = "core/dr-types" } +dr-gpu = { path = "core/dr-gpu" } +dr-ui = { path = "ui/dr-ui" } + +# GPU + UI +wgpu = "23" +slint = { version = "1.9", default-features = false } +slint-build = "1.9" + +# Foundations +anyhow = "1" +thiserror = "2" +log = "0.4" +env_logger = "0.11" +pollster = "0.4" +bytemuck = { version = "1", features = ["derive"] } + +[profile.dev] +# Dependencies optimised even in dev builds — wgpu and image decoding are +# unusably slow otherwise, and they rarely need debugging. +opt-level = 0 + +[profile.dev.package."*"] +opt-level = 2 + +[profile.release] +lto = "thin" +codegen-units = 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1ef0308 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# DarkRoom + +A cross-platform, non-destructive RAW photo editor for Linux and Android. + +**Status:** early. v0.1 is a remote library viewer — see +[docs/milestone-v0.1.md](docs/milestone-v0.1.md). + +## Documentation + +| Document | Contents | +|---|---| +| [requirements.md](docs/requirements.md) | What the software must do — 122 numbered requirements | +| [architecture.md](docs/architecture.md) | How it is built — crates, GPU pipeline, data model, sync | +| [milestone-v0.1.md](docs/milestone-v0.1.md) | The first buildable milestone | + +## Building + +Desktop: + +```bash +cargo run -p darkroom-desktop +``` + +Android (containerised toolchain, see [docker/android](docker/android/README.md)): + +```bash +./docker/android/build.sh cargo ndk -t arm64-v8a build --release +``` + +## Current state + +Working: workspace, GPU context and compute pass, adaptive Slint shell, Android +cross-compilation of the core crates. + +**Not yet working:** the zero-copy display path. The build currently uploads +frames through the CPU, which is exactly what +[ARCH §6.1](docs/architecture.md) forbids — measured at 96% of frame time at +4K. Replacing it is spike S1, the project's highest priority. + +``` +cargo run -p dr-gpu --example bench --features readback +``` + +reproduces that measurement. + +## Licence + +GPL-3.0-or-later. diff --git a/apps/darkroom-desktop/Cargo.toml b/apps/darkroom-desktop/Cargo.toml new file mode 100644 index 0000000..a6c2cec --- /dev/null +++ b/apps/darkroom-desktop/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "darkroom-desktop" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +dr-ui.workspace = true +anyhow.workspace = true +env_logger.workspace = true +log.workspace = true + +[features] +default = ["readback"] +# Temporary CPU upload path until spike S1 lands zero-copy adoption. +readback = ["dr-ui/readback"] diff --git a/apps/darkroom-desktop/src/main.rs b/apps/darkroom-desktop/src/main.rs new file mode 100644 index 0000000..a9e8b05 --- /dev/null +++ b/apps/darkroom-desktop/src/main.rs @@ -0,0 +1,11 @@ +//! DarkRoom desktop entry point. + +fn main() -> anyhow::Result<()> { + env_logger::Builder::from_env( + env_logger::Env::default().default_filter_or("info,wgpu_core=warn,wgpu_hal=warn,zbus=warn,tracing=warn,calloop=warn"), + ) + .init(); + + log::info!("DarkRoom v{}", env!("CARGO_PKG_VERSION")); + dr_ui::run() +} diff --git a/core/dr-gpu/Cargo.toml b/core/dr-gpu/Cargo.toml new file mode 100644 index 0000000..7831b5f --- /dev/null +++ b/core/dr-gpu/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dr-gpu" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +dr-types.workspace = true +wgpu.workspace = true +thiserror.workspace = true +log.workspace = true +bytemuck.workspace = true + +[dev-dependencies] +pollster.workspace = true +env_logger.workspace = true + +[[example]] +name = "bench" +required-features = ["readback"] + +[features] +default = [] +# Exposes read_pixels outside tests. Production must not enable this. +readback = [] diff --git a/core/dr-gpu/examples/bench.rs b/core/dr-gpu/examples/bench.rs new file mode 100644 index 0000000..8636db1 --- /dev/null +++ b/core/dr-gpu/examples/bench.rs @@ -0,0 +1,33 @@ +// Measures the per-frame cost at several canvas sizes, isolating the readback +// path from windowing. +use dr_gpu::{GpuContext, RenderTarget}; +use std::time::Instant; + +fn main() { + env_logger::init(); + let ctx = pollster::block_on(GpuContext::new_headless()).unwrap(); + println!("adapter: {}\n", ctx.adapter_name()); + println!("{:>12} {:>10} {:>10} {:>8}", "size", "compute", "+readback", "fps"); + + for &(w, h) in &[(840u32, 692u32), (1280, 720), (1920, 1080), (2048, 1152), (3840, 2160)] { + let rt = RenderTarget::new(&ctx, w, h).unwrap(); + // warm + for i in 0..10 { rt.render(i as f32 * 0.01); let _ = pollster::block_on(rt.read_pixels()); } + + let n = 40; + let t0 = Instant::now(); + for i in 0..n { rt.render(i as f32 * 0.01); } + ctx.device.poll(wgpu::Maintain::Wait); + let compute = t0.elapsed().as_secs_f64() / n as f64; + + let t1 = Instant::now(); + for i in 0..n { + rt.render(i as f32 * 0.01); + let _ = pollster::block_on(rt.read_pixels()).unwrap(); + } + let full = t1.elapsed().as_secs_f64() / n as f64; + + println!("{:>5}x{:<6} {:>8.2}ms {:>8.2}ms {:>8.0}", + w, h, compute * 1000.0, full * 1000.0, 1.0 / full); + } +} diff --git a/core/dr-gpu/src/error.rs b/core/dr-gpu/src/error.rs new file mode 100644 index 0000000..762080b --- /dev/null +++ b/core/dr-gpu/src/error.rs @@ -0,0 +1,23 @@ +/// Failures from the GPU layer. +/// +/// `DeviceLost` is deliberately a distinct variant rather than folded into a +/// generic error: it is an expected event on Android (ARCH §6.10), not an +/// exceptional one, and callers recover from it by rebuilding the device and +/// re-driving from the edit graph. +#[derive(Debug, thiserror::Error)] +pub enum GpuError { + #[error("no suitable GPU adapter found")] + NoAdapter, + + #[error("failed to request device: {0}")] + DeviceRequest(String), + + #[error("GPU device lost — recreate and re-render from the edit graph")] + DeviceLost, + + #[error("shader compilation failed: {0}")] + ShaderCompilation(String), + + #[error("readback failed: {0}")] + Readback(String), +} diff --git a/core/dr-gpu/src/lib.rs b/core/dr-gpu/src/lib.rs new file mode 100644 index 0000000..7b2ddf8 --- /dev/null +++ b/core/dr-gpu/src/lib.rs @@ -0,0 +1,494 @@ +//! GPU device and compute for DarkRoom. +//! +//! In v0.1 this exists to prove one thing: a compute shader can write a +//! texture that reaches the screen without a CPU round-trip (ARCH §6.1). It +//! holds no pipeline, no tiling, and no masks — those arrive in v0.2. +//! +//! Deliberately free of UI dependencies (ARCH §6.5a). The texture is handed +//! out as a `wgpu::Texture`; who composites it is not this crate's concern. + +use std::sync::Arc; + +use wgpu::util::DeviceExt; + +mod error; +pub use error::GpuError; + +/// Owns the wgpu device and queue. +/// +/// One device is shared by the compute pipeline and the UI, which is what +/// allows compositing with no interop layer. Cloning is cheap and shares the +/// same underlying device. +#[derive(Clone)] +pub struct GpuContext { + pub device: Arc, + pub queue: Arc, + adapter_info: wgpu::AdapterInfo, +} + +impl GpuContext { + /// Create a headless context — no surface, no window. + /// + /// Used by tests and by the Slint path, which supplies its own surface. + pub async fn new_headless() -> Result { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + // Vulkan on both targets (D1). GL is allowed as a fallback so a + // machine without a Vulkan loader still runs the tests. + backends: wgpu::Backends::VULKAN | wgpu::Backends::GL, + ..Default::default() + }); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + force_fallback_adapter: false, + }) + .await + .ok_or(GpuError::NoAdapter)?; + + let adapter_info = adapter.get_info(); + log::info!( + "gpu: {} ({:?}, {:?})", + adapter_info.name, + adapter_info.device_type, + adapter_info.backend + ); + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: Some("darkroom-device"), + required_features: wgpu::Features::empty(), + // Defaults, not `downlevel_defaults`: storage textures + // in compute shaders are required, and the downlevel tier + // does not guarantee them. This is effectively our GPU + // floor (NFR-COMPAT-1). + required_limits: wgpu::Limits::default(), + memory_hints: wgpu::MemoryHints::Performance, + }, + None, + ) + .await + .map_err(|e| GpuError::DeviceRequest(e.to_string()))?; + + Ok(Self { + device: Arc::new(device), + queue: Arc::new(queue), + adapter_info, + }) + } + + /// Build a context from a device and queue owned by someone else — the + /// path used when Slint has already created them. + pub fn from_parts( + device: Arc, + queue: Arc, + adapter_info: wgpu::AdapterInfo, + ) -> Self { + Self { + device, + queue, + adapter_info, + } + } + + pub fn adapter_name(&self) -> &str { + &self.adapter_info.name + } + + pub fn backend(&self) -> wgpu::Backend { + self.adapter_info.backend + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct Params { + width: u32, + height: u32, + phase: f32, + _pad: f32, +} + +/// A compute pass writing into a storage texture. +/// +/// Stands in for the develop pipeline in v0.1. What matters is the shape: +/// compute writes a texture, the texture is handed to the compositor, and +/// pixels never travel back through the CPU. +pub struct RenderTarget { + ctx: GpuContext, + texture: wgpu::Texture, + view: wgpu::TextureView, + pipeline: wgpu::ComputePipeline, + bind_group_layout: wgpu::BindGroupLayout, + bind_group: wgpu::BindGroup, + params_buf: wgpu::Buffer, + width: u32, + height: u32, + /// Reused staging buffer for the temporary readback path. Allocating one + /// per frame is a significant cost at large window sizes. + #[cfg(any(test, feature = "readback"))] + readback_buf: std::cell::RefCell>, +} + +impl RenderTarget { + pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; + + pub fn new(ctx: &GpuContext, width: u32, height: u32) -> Result { + let (width, height) = (width.max(1), height.max(1)); + + let shader = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("gradient"), + source: wgpu::ShaderSource::Wgsl( + include_str!("shaders/gradient.wgsl").into(), + ), + }); + + let bind_group_layout = + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("render-target-bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::WriteOnly, + format: Self::FORMAT, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + let layout = ctx + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("render-target-layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = ctx + .device + .create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some("gradient-pipeline"), + layout: Some(&layout), + module: &shader, + entry_point: Some("main"), + compilation_options: Default::default(), + cache: None, + }); + + let params_buf = ctx + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("params"), + contents: bytemuck::bytes_of(&Params { + width, + height, + phase: 0.0, + _pad: 0.0, + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let (texture, view) = Self::create_texture(ctx, width, height); + let bind_group = Self::create_bind_group(ctx, &bind_group_layout, &view, ¶ms_buf); + + Ok(Self { + ctx: ctx.clone(), + texture, + view, + pipeline, + bind_group_layout, + bind_group, + params_buf, + width, + height, + #[cfg(any(test, feature = "readback"))] + readback_buf: std::cell::RefCell::new(None), + }) + } + + fn create_texture( + ctx: &GpuContext, + width: u32, + height: u32, + ) -> (wgpu::Texture, wgpu::TextureView) { + let texture = ctx.device.create_texture(&wgpu::TextureDescriptor { + label: Some("render-target"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: Self::FORMAT, + // STORAGE_BINDING to write from compute; TEXTURE_BINDING so the + // compositor can sample it. COPY_SRC exists only for tests — + // production never reads this back (ARCH §6.1). + usage: wgpu::TextureUsages::STORAGE_BINDING + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let view = texture.create_view(&Default::default()); + (texture, view) + } + + fn create_bind_group( + ctx: &GpuContext, + layout: &wgpu::BindGroupLayout, + view: &wgpu::TextureView, + params: &wgpu::Buffer, + ) -> wgpu::BindGroup { + ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("render-target-bg"), + layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: params.as_entire_binding(), + }, + ], + }) + } + + /// Resize, reallocating the texture. No-op when unchanged. + pub fn resize(&mut self, width: u32, height: u32) { + let (width, height) = (width.max(1), height.max(1)); + if width == self.width && height == self.height { + return; + } + let (texture, view) = Self::create_texture(&self.ctx, width, height); + self.bind_group = Self::create_bind_group( + &self.ctx, + &self.bind_group_layout, + &view, + &self.params_buf, + ); + self.texture = texture; + self.view = view; + self.width = width; + self.height = height; + #[cfg(any(test, feature = "readback"))] + { + // Size changed, so the staging buffer no longer fits. + *self.readback_buf.borrow_mut() = None; + } + } + + /// Run the compute pass. Results stay on the GPU. + pub fn render(&self, phase: f32) { + self.ctx.queue.write_buffer( + &self.params_buf, + 0, + bytemuck::bytes_of(&Params { + width: self.width, + height: self.height, + phase, + _pad: 0.0, + }), + ); + + let mut enc = self + .ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("render-encoder"), + }); + { + let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("gradient-pass"), + timestamp_writes: None, + }); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + // 8x8 workgroups, rounded up so edge pixels are covered. + pass.dispatch_workgroups(self.width.div_ceil(8), self.height.div_ceil(8), 1); + } + self.ctx.queue.submit(Some(enc.finish())); + } + + pub fn texture(&self) -> &wgpu::Texture { + &self.texture + } + + pub fn view(&self) -> &wgpu::TextureView { + &self.view + } + + pub fn size(&self) -> (u32, u32) { + (self.width, self.height) + } + + /// Read pixels back to the CPU. + /// + /// **Tests only.** Production code must never call this — it is exactly + /// the round-trip ARCH §6.1 forbids, and AC-8 asserts it does not happen. + #[cfg(any(test, feature = "readback"))] + pub async fn read_pixels(&self) -> Result, GpuError> { + // Buffer rows must be aligned to COPY_BYTES_PER_ROW_ALIGNMENT (256). + let unpadded = self.width * 4; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + let padded = unpadded.div_ceil(align) * align; + + let needed = (padded * self.height) as u64; + let mut slot = self.readback_buf.borrow_mut(); + if slot.as_ref().map(|(_, p)| *p) != Some(padded) { + *slot = Some(( + self.ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("readback"), + size: needed, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }), + padded, + )); + } + let buf = &slot.as_ref().unwrap().0; + + let mut enc = self + .ctx + .device + .create_command_encoder(&Default::default()); + enc.copy_texture_to_buffer( + wgpu::ImageCopyTexture { + texture: &self.texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::ImageCopyBuffer { + buffer: buf, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padded), + rows_per_image: Some(self.height), + }, + }, + wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + }, + ); + self.ctx.queue.submit(Some(enc.finish())); + + let slice = buf.slice(..); + let (tx, rx) = std::sync::mpsc::channel(); + slice.map_async(wgpu::MapMode::Read, move |r| { + let _ = tx.send(r); + }); + self.ctx.device.poll(wgpu::Maintain::Wait); + rx.recv() + .map_err(|e| GpuError::Readback(e.to_string()))? + .map_err(|e| GpuError::Readback(e.to_string()))?; + + // Strip row padding. + let data = slice.get_mapped_range(); + let mut out = Vec::with_capacity((unpadded * self.height) as usize); + for row in 0..self.height { + let start = (row * padded) as usize; + out.extend_from_slice(&data[start..start + unpadded as usize]); + } + drop(data); + buf.unmap(); + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ctx() -> Option { + // CI runners and headless machines may have no usable adapter. Skip + // rather than fail — the device-dependent assertions still run + // wherever a GPU exists. + match pollster::block_on(GpuContext::new_headless()) { + Ok(c) => Some(c), + Err(e) => { + eprintln!("skipping: no GPU adapter ({e})"); + None + } + } + } + + #[test] + fn compute_writes_the_texture() { + let Some(ctx) = ctx() else { return }; + let rt = RenderTarget::new(&ctx, 64, 64).expect("render target"); + rt.render(0.0); + + let px = pollster::block_on(rt.read_pixels()).expect("readback"); + assert_eq!(px.len(), 64 * 64 * 4); + + // The shader writes opaque pixels everywhere; an all-zero buffer would + // mean the dispatch silently did nothing. + assert!( + px.chunks_exact(4).all(|p| p[3] == 255), + "every pixel should be opaque" + ); + assert!( + px.iter().any(|&b| b != 0), + "texture should not be uniformly zero" + ); + } + + #[test] + fn phase_changes_output() { + let Some(ctx) = ctx() else { return }; + let rt = RenderTarget::new(&ctx, 32, 32).expect("render target"); + + rt.render(0.0); + let a = pollster::block_on(rt.read_pixels()).expect("readback"); + rt.render(std::f32::consts::PI); + let b = pollster::block_on(rt.read_pixels()).expect("readback"); + + assert_ne!(a, b, "moving the highlight should change the image"); + } + + #[test] + fn resize_reallocates() { + let Some(ctx) = ctx() else { return }; + let mut rt = RenderTarget::new(&ctx, 16, 16).expect("render target"); + assert_eq!(rt.size(), (16, 16)); + + rt.resize(48, 24); + assert_eq!(rt.size(), (48, 24)); + + rt.render(0.0); + let px = pollster::block_on(rt.read_pixels()).expect("readback"); + assert_eq!(px.len(), 48 * 24 * 4); + } + + #[test] + fn zero_size_is_clamped() { + let Some(ctx) = ctx() else { return }; + // A minimised window reports zero; texture creation would panic. + let rt = RenderTarget::new(&ctx, 0, 0).expect("render target"); + assert_eq!(rt.size(), (1, 1)); + } +} diff --git a/core/dr-gpu/src/shaders/gradient.wgsl b/core/dr-gpu/src/shaders/gradient.wgsl new file mode 100644 index 0000000..042e4ca --- /dev/null +++ b/core/dr-gpu/src/shaders/gradient.wgsl @@ -0,0 +1,40 @@ +// Placeholder compute shader — stands in for the develop pipeline. +// +// Its only job is to prove the path: a compute shader writes a storage +// texture, and that texture reaches the screen without a CPU round-trip +// (ARCH §6.1). Replaced by real pipeline stages in v0.2. + +struct Params { + width: u32, + height: u32, + // Animates so it is visually obvious the compute pass runs every frame + // rather than a stale texture being redisplayed. + phase: f32, + _pad: f32, +} + +@group(0) @binding(0) var output: texture_storage_2d; +@group(0) @binding(1) var params: Params; + +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) gid: vec3) { + if (gid.x >= params.width || gid.y >= params.height) { + return; + } + + let uv = vec2( + f32(gid.x) / f32(params.width), + f32(gid.y) / f32(params.height), + ); + + // Warm dark ground with a moving highlight — deliberately unlike a test + // pattern, so a stuck frame is obvious at a glance. + let d = distance(uv, vec2(0.5 + 0.25 * cos(params.phase), 0.5 + 0.25 * sin(params.phase))); + let glow = 1.0 - smoothstep(0.0, 0.55, d); + + let base = vec3(0.08, 0.07, 0.06); + let accent = vec3(0.69, 0.23, 0.15); + let rgb = base + accent * glow * (0.35 + 0.65 * uv.y); + + textureStore(output, vec2(gid.xy), vec4(rgb, 1.0)); +} diff --git a/core/dr-types/Cargo.toml b/core/dr-types/Cargo.toml new file mode 100644 index 0000000..6d3b337 --- /dev/null +++ b/core/dr-types/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dr-types" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +thiserror.workspace = true diff --git a/core/dr-types/src/lib.rs b/core/dr-types/src/lib.rs new file mode 100644 index 0000000..1aa4c5c --- /dev/null +++ b/core/dr-types/src/lib.rs @@ -0,0 +1,223 @@ +//! Shared vocabulary for DarkRoom. +//! +//! Deliberately dependency-light: no platform code, no UI, no GPU. Everything +//! else in `core/` builds on these types, so anything added here is paid for +//! everywhere. + +use std::fmt; +use std::ops::Range; + +/// Identifies a granted library location — a directory on Linux, a persisted +/// document tree on Android. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct RootId(pub u64); + +/// Identifies an image within a catalog. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct ImageId(pub u64); + +/// Identifies one edit variant of an image (a virtual copy). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct VersionId(pub u64); + +/// An opaque, re-resolvable reference to source image data. +/// +/// **Never a filesystem path.** Android's Storage Access Framework provides no +/// usable path (ARCH §6.9), so a `Path`-based API would not be portable. This +/// is the type that keeps `core/` platform-agnostic. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SourceRef { + /// Desktop: a path relative to a granted root. + Local { root: RootId, relative: String }, + /// Android: a SAF document URI under a persisted tree grant. + Document { tree: RootId, document_id: String }, + /// Remote: resolved through the sync layer; may support range reads only. + Remote { file_id: u64, path: String }, +} + +impl SourceRef { + /// A stable, display-friendly name — the last path component. + pub fn display_name(&self) -> &str { + let full = match self { + SourceRef::Local { relative, .. } => relative.as_str(), + SourceRef::Document { document_id, .. } => document_id.as_str(), + SourceRef::Remote { path, .. } => path.as_str(), + }; + // SAF document ids use ':' as a separator; paths use '/'. Split on + // whichever appears last so both yield a sensible name. + full.rsplit(['/', ':']).next().unwrap_or(full) + } + + /// Lowercase file extension, if any. + pub fn extension(&self) -> Option { + let name = self.display_name(); + let (_, ext) = name.rsplit_once('.')?; + if ext.is_empty() { + None + } else { + Some(ext.to_ascii_lowercase()) + } + } +} + +impl fmt::Display for SourceRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.display_name()) + } +} + +/// Image formats recognised at the catalog level. +/// +/// Recognition is by extension only; whether a decoder can actually handle the +/// file is a separate question answered by `dr-decode`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Format { + Cr2, + Cr3, + Nef, + Arw, + Raf, + Rw2, + Orf, + Dng, + Jpeg, +} + +impl Format { + /// Recognise from a lowercase extension. + pub fn from_extension(ext: &str) -> Option { + Some(match ext { + "cr2" => Format::Cr2, + "cr3" => Format::Cr3, + "nef" => Format::Nef, + "arw" => Format::Arw, + "raf" => Format::Raf, + "rw2" => Format::Rw2, + "orf" => Format::Orf, + "dng" => Format::Dng, + "jpg" | "jpeg" => Format::Jpeg, + _ => return None, + }) + } + + /// Whether this is a camera RAW format needing demosaic. + pub fn is_raw(self) -> bool { + !matches!(self, Format::Jpeg) + } +} + +/// How much of an image is available locally (FR-NC-6c). +/// +/// Surfaced in the UI so a user always knows what they have — the failure +/// Lightroom makes by showing an original's filename beside a 2560px proxy. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Availability { + /// Catalogued, nothing cached. + MetadataOnly, + /// A preview is cached; enough to browse, not to export. + Preview, + /// Full source available locally. + Original, + /// Source unreachable — drive unplugged, permission revoked, offline. + Offline, +} + +/// An opaque change-validator for a remote entry (an ETag, or an mtime where +/// no ETag exists). +/// +/// Quoting is inconsistent across servers, so comparison normalises rather +/// than testing raw equality — otherwise spurious full rescans result. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Validator(String); + +impl Validator { + pub fn new(raw: impl Into) -> Self { + let raw: String = raw.into(); + let trimmed = raw.trim().trim_start_matches("W/").trim_matches('"'); + Validator(trimmed.to_string()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// A byte range request against a source. +pub type ByteRange = Range; + +/// Errors from resolving or reading a source. +#[derive(Debug, thiserror::Error)] +pub enum SourceError { + #[error("source is offline or unreachable")] + Offline, + #[error("permission denied or revoked")] + PermissionDenied, + #[error("source not found")] + NotFound, + #[error("range requests unsupported by this source")] + RangeUnsupported, + #[error("io error: {0}")] + Io(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_name_handles_paths_and_saf_ids() { + let local = SourceRef::Local { + root: RootId(1), + relative: "2026/08/IMG_0042.CR3".into(), + }; + assert_eq!(local.display_name(), "IMG_0042.CR3"); + + // SAF document ids are colon-separated, not slash-separated. + let saf = SourceRef::Document { + tree: RootId(1), + document_id: "primary:DCIM/Camera/IMG_0042.CR3".into(), + }; + assert_eq!(saf.display_name(), "IMG_0042.CR3"); + } + + #[test] + fn extension_is_lowercased() { + let s = SourceRef::Local { + root: RootId(1), + relative: "IMG.CR3".into(), + }; + assert_eq!(s.extension().as_deref(), Some("cr3")); + } + + #[test] + fn extension_absent_when_no_dot() { + let s = SourceRef::Local { + root: RootId(1), + relative: "NOEXTENSION".into(), + }; + assert_eq!(s.extension(), None); + } + + #[test] + fn formats_round_trip_and_classify() { + assert_eq!(Format::from_extension("cr3"), Some(Format::Cr3)); + assert_eq!(Format::from_extension("jpeg"), Some(Format::Jpeg)); + assert_eq!(Format::from_extension("txt"), None); + assert!(Format::Cr3.is_raw()); + assert!(!Format::Jpeg.is_raw()); + } + + #[test] + fn validator_normalises_quoting() { + // Nextcloud quotes ETags; some servers add a weak prefix. All three + // spellings must compare equal or every sync looks like a change. + assert_eq!(Validator::new("\"abc123\""), Validator::new("abc123")); + assert_eq!(Validator::new("W/\"abc123\""), Validator::new("abc123")); + } + + #[test] + fn availability_orders_by_completeness() { + assert!(Availability::Original > Availability::Preview); + assert!(Availability::Preview > Availability::MetadataOnly); + } +} diff --git a/docker/android/Dockerfile b/docker/android/Dockerfile new file mode 100644 index 0000000..f702656 --- /dev/null +++ b/docker/android/Dockerfile @@ -0,0 +1,120 @@ +# DarkRoom — reproducible Android build environment +# +# Pins the entire toolchain: JDK, Android SDK, NDK, Rust, and the four Android +# targets. Both CI and local builds use this image, so "works on my machine" +# and "works in CI" are the same machine. +# +# Build: podman build -t darkroom-android:latest docker/android +# Use: ./docker/android/build.sh cargo ndk -t arm64-v8a build --release + +FROM docker.io/library/debian:bookworm-slim + +# --------------------------------------------------------------------------- +# Versions — pinned deliberately. Bumping any of these is a reviewable change, +# not something that drifts underneath the build. +# --------------------------------------------------------------------------- +ARG JDK_VERSION=17 +# Compile SDK / target API. +ARG ANDROID_API=36 +# Minimum supported API — the level native code links against (NFR-COMPAT-1). +# 28 (Android 9) matches the floor where Vulkan support is dependable. +# cargo-ndk otherwise defaults to 21, which is far below what this app needs. +ARG MIN_API=28 +ARG BUILD_TOOLS=36.0.0 +ARG NDK_VERSION=27.2.12479018 +ARG CMDLINE_TOOLS=13114758 +# Must satisfy cargo-ndk's MSRV (4.1.x needs >= 1.86) as well as our own crates. +ARG RUST_VERSION=1.92.0 + +# JDK 17, not the host's 25: the Android Gradle Plugin supports 17 as its +# stable target, and newer JDKs regularly break Gradle in ways that cost more +# time than they save. +ENV DEBIAN_FRONTEND=noninteractive \ + ANDROID_HOME=/opt/android-sdk \ + ANDROID_SDK_ROOT=/opt/android-sdk \ + JAVA_HOME=/usr/lib/jvm/java-${JDK_VERSION}-openjdk-amd64 \ + CARGO_HOME=/opt/cargo \ + RUSTUP_HOME=/opt/rustup \ + PATH=/opt/cargo/bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:$PATH + +# --------------------------------------------------------------------------- +# System packages +# --------------------------------------------------------------------------- +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl unzip git \ + openjdk-${JDK_VERSION}-jdk-headless \ + # Slint / winit build-time needs + pkg-config libfontconfig1-dev \ + # native deps that may need building for host-side tooling + build-essential cmake python3 \ + && rm -rf /var/lib/apt/lists/* + +# --------------------------------------------------------------------------- +# Android SDK + NDK +# --------------------------------------------------------------------------- +RUN mkdir -p ${ANDROID_HOME}/cmdline-tools \ + && curl -fsSL -o /tmp/tools.zip \ + "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS}_latest.zip" \ + && unzip -q /tmp/tools.zip -d ${ANDROID_HOME}/cmdline-tools \ + && mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \ + && rm /tmp/tools.zip + +RUN yes | sdkmanager --licenses > /dev/null 2>&1 || true \ + && sdkmanager --install \ + "platform-tools" \ + "platforms;android-${ANDROID_API}" \ + "build-tools;${BUILD_TOOLS}" \ + "ndk;${NDK_VERSION}" \ + > /dev/null + +ENV ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${NDK_VERSION} \ + ANDROID_NDK_ROOT=${ANDROID_HOME}/ndk/${NDK_VERSION} + +# --------------------------------------------------------------------------- +# Rust + Android targets +# +# All four ABIs. arm64-v8a covers essentially every current device; the others +# exist so an ABI-specific build break is caught here rather than at release. +# --------------------------------------------------------------------------- +RUN curl -fsSL https://sh.rustup.rs | sh -s -- \ + -y --no-modify-path --profile minimal --default-toolchain ${RUST_VERSION} \ + && rustup target add \ + aarch64-linux-android \ + armv7-linux-androideabi \ + x86_64-linux-android \ + i686-linux-android \ + && rustup component add rustfmt clippy \ + && cargo install cargo-ndk --locked \ + && chmod -R a+rwX ${CARGO_HOME} ${RUSTUP_HOME} + +# --------------------------------------------------------------------------- +# Linker configuration +# +# cargo-ndk normally handles this, but setting it explicitly means plain +# `cargo build --target …` works too, which matters for tooling that shells +# out to cargo directly (rust-analyzer, cargo-metadata). +# --------------------------------------------------------------------------- +ENV NDK_BIN=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin + +# Linkers target MIN_API, not ANDROID_API — the binary must run on the oldest +# supported device, while the SDK compiles against the newest. +ENV CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${NDK_BIN}/aarch64-linux-android${MIN_API}-clang \ + CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=${NDK_BIN}/armv7a-linux-androideabi${MIN_API}-clang \ + CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=${NDK_BIN}/x86_64-linux-android${MIN_API}-clang \ + CARGO_TARGET_I686_LINUX_ANDROID_LINKER=${NDK_BIN}/i686-linux-android${MIN_API}-clang + +# cargo-ndk reads this; without it it defaults to API 21. +ENV CARGO_NDK_PLATFORM=${MIN_API} \ + ANDROID_PLATFORM=${MIN_API} + +# Shared cargo registry cache — bind-mount over this to persist across runs. +VOLUME ["/opt/cargo/registry"] + +WORKDIR /work + +# Rootless podman maps the host user into the container, so the image must not +# assume a fixed uid. Keep world-writable toolchain dirs and let the caller +# pass --user. +RUN chmod -R a+rwX ${ANDROID_HOME} + +CMD ["/bin/bash"] diff --git a/docker/android/README.md b/docker/android/README.md new file mode 100644 index 0000000..b667803 --- /dev/null +++ b/docker/android/README.md @@ -0,0 +1,73 @@ +# Android build environment + +Reproducible container for cross-compiling DarkRoom to Android. CI and local builds use the same +image, so a break appears in one place rather than two. + +## Use + +```bash +# Cross-compile for a device ABI +./docker/android/build.sh cargo ndk -t arm64-v8a build --release + +# All four ABIs +./docker/android/build.sh cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -t x86 build --release + +# Type-check without linking +./docker/android/build.sh cargo check --target aarch64-linux-android + +# Interactive shell +./docker/android/build.sh + +# After editing the Dockerfile +./docker/android/build.sh --rebuild +``` + +Prefers `podman`, falls back to `docker`. First run builds the image, which takes several minutes; +after that it is cached. + +## Pinned versions + +| Component | Version | Why this one | +|---|---|---| +| JDK | 17 | AGP's stable target. The host's JDK 25 breaks Gradle. | +| Compile SDK | API 36 | Required for Play distribution | +| **Min API** | **28** (Android 9) | Floor where Vulkan support is dependable (NFR-COMPAT-1) | +| Build tools | 36.0.0 | Matches compile SDK | +| NDK | 27.2.12479018 (r27c) | Current LTS | +| Rust | 1.92.0 | Slint 1.17 requires it; cargo-ndk 4.1.x needs ≥ 1.86 | +| cargo-ndk | 4.1.2 | | + +Bumping any of these is a reviewable change, not silent drift. + +### Compile SDK versus min API + +These are different and both matter. `ANDROID_API` (36) is what the SDK compiles against — the +newest APIs available. `MIN_API` (28) is what native code *links* against, and determines the +oldest device that can run the result. + +cargo-ndk defaults to **API 21** if not told otherwise, which is far below what this app needs. +`CARGO_NDK_PLATFORM` and the per-target linker paths both pin it to `MIN_API`. Verify with: + +```bash +file target/aarch64-linux-android/release/*.so +# → "... for Android 28, built by NDK r27c" +``` + +## Caching + +The cargo registry and target directory persist in `~/.cache/darkroom-android`, so rebuilds do not +re-download the crate index. Clear with: + +```bash +rm -rf ~/.cache/darkroom-android +``` + +The Android target dir is kept separate from the host's `target/` — sharing one directory between +host and container builds causes constant rebuilds as they invalidate each other's fingerprints. + +## What is not here + +No emulator, no `adb` device access. This image cross-compiles; it does not run or deploy. Device +testing (spikes S2 and S10, which need two GPU vendors) happens on real hardware — the emulator's +GPU behaviour is not representative of Adreno or Mali, which is precisely what those spikes exist +to test. diff --git a/docker/android/build.sh b/docker/android/build.sh new file mode 100755 index 0000000..93c5e8d --- /dev/null +++ b/docker/android/build.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Run a command inside the DarkRoom Android build container. +# +# ./docker/android/build.sh cargo ndk -t arm64-v8a build --release +# ./docker/android/build.sh cargo check --target aarch64-linux-android +# ./docker/android/build.sh # interactive shell +# +# Builds the image on first use. Rebuild after editing the Dockerfile with: +# ./docker/android/build.sh --rebuild +set -euo pipefail + +IMAGE="darkroom-android:latest" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$(cd "${HERE}/../.." && pwd)" + +# Prefer podman (rootless by default); fall back to docker. +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 exists "${IMAGE}" 2>/dev/null && \ + ! "${ENGINE}" image inspect "${IMAGE}" >/dev/null 2>&1; then + echo "==> building ${IMAGE} (first run; several minutes)" + "${ENGINE}" build -t "${IMAGE}" "${HERE}" +fi + +# Persist the cargo registry and target dir across runs, or every build +# re-downloads the crate index. +CACHE="${XDG_CACHE_HOME:-${HOME}/.cache}/darkroom-android" +mkdir -p "${CACHE}/registry" "${CACHE}/target" + +ARGS=( + --rm + -v "${REPO}:/work:z" + -v "${CACHE}/registry:/opt/cargo/registry:z" + -v "${CACHE}/target:/work/target-android:z" + -e CARGO_TARGET_DIR=/work/target-android + -w /work +) + +# Rootless podman already maps the host user; docker needs it stated. +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}" "$@" diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..6acd056 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,946 @@ +# DarkRoom — Architecture + +**Status:** Draft v0.1 · 2026-08-08 +**Companion to:** [requirements.md](requirements.md) + +How DarkRoom is built. The requirements document says *what* the software must do; this says *how*, +and records the decisions and constraints behind the design. + +--- + +## 1. Overview + +DarkRoom is a Rust application with a Slint interface, rendering through wgpu to Vulkan on both +Linux and Android. The design is organised around four ideas, each of which the rest of this +document elaborates: + +1. **Pixels stay on the GPU.** From decode to display, image data never round-trips through the + CPU. This is the constraint that most shapes the codebase (§6.1). +2. **Sidecars are authoritative; the catalog is a rebuildable index.** Durability comes from plain + text files next to the images, not from a database (§6.12). +3. **Operations describe themselves.** The develop pipeline emits parameter descriptors as data; + the UI generates controls from them. The core never depends on the UI toolkit (§4.3, §6.5a). +4. **Everything is tiled and cancellable.** Work is decomposed into tiles scheduled by visibility, + so interaction always preempts background work (§5.3). + +### 1.1 Stack + +| Layer | Choice | Decision | +|---|---|---| +| Language | Rust | D1 | +| UI | Slint | D1, D8 | +| GPU | wgpu → Vulkan (Linux + Android) | D1 | +| Shaders | Hand-written WGSL | D6 | +| RAW decode | rawler; LibRaw fallback behind a trait | D2 | +| Catalog | SQLite (WAL) — a rebuildable index | D5, §6.12 | +| Colour | lcms2 + GPU-side matrix/LUT transforms | D5 | +| Network | reqwest + quick-xml | D7 | +| Licence | GPLv3 | D8 | + +--- + +## 2. Crate layout + +Dependency direction encodes the layering rule: nothing in `core/` may depend on anything in `ui/`. + +``` +darkroom/ +├── core/ +│ ├── dr-types SourceRef, ImageId, VersionId, ParamValue — shared vocabulary +│ ├── dr-catalog SQLite index, scan, query, metadata +│ ├── dr-sidecar the authoritative edit store (§6.12) +│ ├── dr-decode RawDecoder trait, rawler impl, embedded-preview extraction +│ ├── dr-pipeline Operation trait, descriptors, edit graph, registry +│ ├── dr-gpu wgpu device, tile scheduler, WGSL shaders, mask rasteriser +│ ├── dr-colour lcms2 bindings, camera profiles, working-space transforms +│ ├── dr-export encoders, resampling, output sizing +│ ├── dr-sync RemoteBackend trait, sync engine, cache rules, merge +│ └── dr-sync-nextcloud the only backend implementation (§8.4) +├── ui/ +│ ├── dr-ui Slint components, adaptive layout, descriptor→control mapping +│ └── dr-widgets custom controls per WidgetKind (curve, wheel, crop, brush) +├── platform/ +│ ├── dr-plat trait definitions: storage, secrets, lifecycle, power +│ ├── dr-plat-linux XDG dirs, Secret Service, filesystem +│ └── dr-plat-android SAF, Keystore, WorkManager, lifecycle +└── apps/ + ├── darkroom-desktop + └── darkroom-android +``` + +**CI asserts the dependency rule.** A UI dependency appearing in any `core/` crate's tree fails the +build. This is easy to violate accidentally — one `use slint::` undoes headless testability and the +one-operation-two-presentations property together. + +`platform/` exists so `core/` contains no `#[cfg(target_os)]`. Platform traits are defined in +`dr-plat` and injected at construction by the app crates. + +--- + +## 3. Core abstractions + +### 3.1 SourceRef — addressing image data + +Android's Storage Access Framework provides no filesystem path (§6.9), so **no core API takes a +`Path`**. + +```rust +/// An opaque, re-resolvable reference to source image data. +pub struct SourceRef(SourceRefInner); + +enum SourceRefInner { + /// Desktop: an absolute path under a granted root. + Path { root: RootId, relative: PathBuf }, + /// Android: a SAF document URI under a persisted tree grant. + Document { tree: RootId, document_id: String }, + /// Remote: resolved through the sync layer, possibly byte-range only. + Remote { file_id: u64, path: String }, +} + +impl SourceRef { + /// Open a seekable stream. May fail if the source is offline (FR-CAT-9). + pub fn open(&self, plat: &dyn Storage) -> Result, SourceError>; + /// Read a byte range without opening the whole source — the fast path for + /// embedded preview extraction (FR-CULL-2) and remote browsing (FR-NC-3). + pub fn read_range(&self, plat: &dyn Storage, r: Range) -> Result, SourceError>; +} +``` + +`read_range` is deliberately first-class rather than a convenience over `open`. Extracting an +embedded JPEG preview from a remote 80 MB RAW must transfer ~1–3 MB, and the culling preview ladder +depends on the same primitive locally. + +### 3.2 RawDecoder + +```rust +pub trait RawDecoder: Send + Sync { + fn probe(&self, header: &[u8]) -> Option; + fn embedded_preview(&self, src: &mut dyn SeekableRead) -> Result, DecodeError>; + fn decode(&self, src: &mut dyn SeekableRead) -> Result; + fn metadata(&self, src: &mut dyn SeekableRead) -> Result; +} +``` + +Four separate entry points because the caller's needs differ sharply by phase. Culling wants +`embedded_preview` and nothing else; the grid wants `metadata`; only develop and export need +`decode`. Fusing them would force full decode where a 200 KB range read suffices. + +`RawImage` carries CFA-pattern sensor data plus black/white levels and camera colour matrices — it +is *not* demosaiced. Demosaic is a GPU pipeline stage (§5.2). + +### 3.3 Operation and descriptors + +The develop pipeline is a sequence of operations with a uniform interface. Polymorphism is by +trait, never inheritance — there is no shared implementation to inherit, only a shared shape. + +```rust +pub trait Operation: Send + Sync { + /// Static parameter description. Drives UI generation (FR-DEV-3a). + fn descriptor() -> OpDescriptor where Self: Sized; + + /// Encode GPU work for one tile. No UI types cross this boundary. + fn encode(&self, enc: &mut ComputeEncoder, ctx: &TileContext); + + /// Identity for cache invalidation. Integer state only — exactly + /// deterministic, unlike GPU float output (§6.13). + fn params_hash(&self) -> u64; + + /// What this operation's parameters affect, for invalidation scoping. + fn affects(&self) -> Affects; +} +``` + +Descriptors are pure data: + +```rust +pub struct ParamDescriptor { + pub id: ParamId, + pub label: LocalizedKey, // a key, not a string — core has no localiser + pub kind: ParamKind, + pub default: ParamValue, + pub affects: Affects, +} + +pub enum ParamKind { + Scalar { min: f32, max: f32, scale: Scale, unit: Unit, precision: u8 }, + Bool, + Enum { variants: Vec<(EnumId, LocalizedKey)> }, + Colour { has_alpha: bool }, + /// Controls that don't reduce to primitives. Core names the *kind*; + /// dr-widgets owns the implementation. + Custom { widget: WidgetKind, schema: CustomParamSchema }, +} +``` + +`LocalizedKey` rather than a resolved string is what lets NFR-A11Y-1 work without `core/` depending +on a localisation library that pulls in UI concerns. The UI resolves keys against its catalogue. + +### 3.4 Edit graph + +```rust +pub struct EditGraph { + version: VersionId, + ops: Vec, // ordered; order is data, not code + masks: Vec, +} + +pub struct OpInstance { + kind: OpKind, + enabled: bool, + params: BTreeMap, + mask: Option, // None = global +} +``` + +`BTreeMap` rather than `HashMap` so serialisation is deterministic — the sidecar format is diffable +and the graph hash is stable across runs. + +The graph is **CPU-side state**, which is what makes GPU device-loss recovery tractable (§6.10): the +device can be destroyed and rebuilt, and the render re-driven from the graph with nothing lost. + +--- + +## 4. Layering + +### 4.1 The four layers + +``` +┌──────────────────────────────────────────────────────┐ +│ apps/ wiring, platform injection │ +├──────────────────────────────────────────────────────┤ +│ ui/ Slint; descriptor → control mapping │ +│ owns: presentation, gestures, layout │ +├──────────────────────────────────────────────────────┤ +│ core/ catalog, pipeline, sync, export │ +│ owns: all behaviour and state │ +├──────────────────────────────────────────────────────┤ +│ platform/ storage, secrets, lifecycle, power │ +└──────────────────────────────────────────────────────┘ +``` + +Calls go downward. `core/` reaches `platform/` through traits; `ui/` reaches `core/` through a +session API. Nothing calls upward — the UI observes change through subscriptions, not callbacks +registered into the core. + +### 4.2 The session API + +`ui/` sees a small surface, not the internals: + +```rust +pub struct App { /* … */ } + +impl App { + pub fn catalog(&self) -> &Catalog; + pub fn open_develop(&self, v: VersionId) -> DevelopSession; + pub fn open_cull(&self, filter: Filter) -> CullSession; + pub fn export(&self, sel: &[VersionId], preset: &ExportPreset) -> JobHandle; + pub fn sync(&self) -> &SyncEngine; + /// Change notifications. The UI subscribes; the core never holds UI callbacks. + pub fn subscribe(&self) -> Receiver; +} + +pub struct DevelopSession { /* … */ } + +impl DevelopSession { + pub fn descriptors(&self) -> &[OpDescriptor]; // drives panel generation + pub fn set_param(&mut self, op: OpId, p: ParamId, v: ParamValue); + pub fn set_viewport(&mut self, vp: Viewport); + /// The rendered result as a GPU texture handle. Never pixels. + pub fn texture(&self) -> TextureHandle; + pub fn histogram(&self) -> &HistogramBuffer; // GPU reduction, not readback + pub fn undo(&mut self); pub fn redo(&mut self); +} +``` + +`texture()` returning a handle rather than pixel data is the API-level expression of §6.1. + +### 4.3 Descriptor → control mapping + +`dr-ui` maps `ParamKind` to a control by input modality (FR-DEV-3b). The mapping table lives in the +UI; the core is unaware presentation varies. + +| `ParamKind` | Pointer | Touch | +|---|---|---| +| `Scalar` | Slider + numeric entry, wheel fine-adjust | Drag-strip, double-tap reset | +| `Bool` | Checkbox | Switch, ≥44pt | +| `Enum` | Dropdown | Segmented control or sheet | +| `Colour` | Swatch → popover | Swatch → sheet | +| `Custom` | `dr-widgets` control | Same control, touch hit-targets | + +Adding an operation therefore requires no UI change (FR-DEV-3c) unless it needs a new `WidgetKind`. + +--- + +## 5. GPU architecture + +### 5.1 Device and surface + +One `wgpu::Device` shared by the pipeline and Slint, so compute output composites without an +interop layer. Slint's `create_texture_from_hal` imports the texture directly. + +**Spike S1 validates this**, and it is the project's highest-risk assumption. If it fails, D1's +recorded fallbacks apply. + +### 5.2 Pipeline stages + +Ordered; each consumes and produces GPU textures. Order is data (§3.4), so operations can be +reordered without code changes. + +``` +RawImage (sensor data, CPU) + │ upload + ▼ +┌─────────────────────┐ +│ black/white levels │ integer normalise +├─────────────────────┤ +│ demosaic │ Bayer or Markesteijn (X-Trans, FR-RAW-5) +├─────────────────────┤ +│ AI denoise │ optional; raw-domain, joint with demosaic where possible +├─────────────────────┤ +│ camera profile │ matrices + per-body base curve (FR-DEV-3e) +├─────────────────────┤ +│ → working space │ linear, wide-gamut, f16 +├─────────────────────┤ +│ white balance │ +│ exposure/contrast │ +│ highlights/shadows │ ← masks apply per-op from here down +│ tone curve │ +│ HSL / colour mixer │ +│ texture / clarity │ +│ spot removal │ +│ sharpen / NR │ +│ lens corrections │ +│ look (HaldCLUT) │ FR-DEV-3f +├─────────────────────┤ +│ geometry │ crop, straighten, rotate +├─────────────────────┤ +│ output transform │ → display or export profile +└─────────────────────┘ + │ + ├─→ display texture (composited by Slint — never read back) + └─→ histogram reduction (compute → small buffer, §5.5) +``` + +Working precision is f16 in a linear wide-gamut space, quantising once at the output transform. + +### 5.3 Tiling and scheduling + +Work decomposes into tiles (default 256×256) scheduled by priority class: + +| Class | Work | Preempts | +|---|---|---| +| `Interactive` | Visible tiles at current viewport | everything | +| `Prefetch` | Tiles just outside viewport; next cull image | Background | +| `Background` | Export, thumbnail generation, proxy building | — | + +`Interactive` **strictly preempts** `Background`. Without this, moving a slider during a batch +export misses its frame budget — the common case, not an edge case. + +Tile results cache keyed by `(VersionId, tile, zoom, graph_hash_prefix)`, where the prefix covers +operations up to the first `Affects` change. Adjusting exposure reuses cached demosaic and camera +profile output for every tile. + +### 5.4 Mask rasterisation + +**All masks rasterise on the GPU, including drawn brush strokes** (§6.11). Strokes arrive as +parameters — points, radius, hardness, flow — and a compute shader rasterises them. The mask never +exists in CPU memory. + +This is a direct response to darktable, where drawn masks are CPU-rasterised and users consistently +describe brush lag as making them "unworkable". The problem is architectural, not performance +tuning, and not fixable with a faster CPU. + +### 5.5 Histogram without readback + +The histogram is a compute-shader reduction into a small storage buffer, read once per frame at +most, and only the *bins* — never image data. A per-frame CPU readback of pixels would reintroduce +exactly the stall §6.1 exists to prevent. + +Raw-domain histograms for culling (FR-CULL-3) reduce over the pre-demosaic texture, which is why +they can report headroom the embedded JPEG's histogram cannot. + +### 5.6 Device loss + +Handled as expected, not exceptional (§6.10): + +1. Detect `wgpu::SurfaceError::Lost` or device-lost callback +2. Discard all GPU-side state — textures, buffers, pipelines +3. Recreate device, recompile from the shader cache +4. Re-drive the current render from the edit graph + +No user edit is lost, because the graph is CPU-side. Shader pipelines cache on disk keyed by device, +driver version, and shader hash (NFR-P12). + +--- + +## 6. Data architecture + +### 6.1 Sidecars are authoritative + +The durability model inverts the usual arrangement (§6.12): + +``` +image.CR3 ← never written to +image.CR3.drsc ← authoritative edit state, plain text + (or app-managed store where the location is read-only) +catalog.sqlite ← index; deletable and rebuildable at any time +``` + +Sidecar format is versioned plain text holding **all** Versions for an image: + +```toml +schema = 1 +image_hash = "blake3:..." + +[versions.default] +name = "Default" +created = 2026-08-08T14:02:00Z +revision = 7 +device = "…" + +[versions.default.ops.exposure] +enabled = true +exposure = 0.35 +contrast = 12.0 +``` + +Writing discipline, each point addressing a documented failure elsewhere: + +- **Atomic** — temp plus rename, never partial +- **Debounced** — not per slider tick +- **Only on real content change** — darktable rewrites sidecars without edits, breaking backup + deduplication and mtime-based sync +- **Never embedded in the RAW** — would force re-backup of the whole file + +### 6.2 The catalog as index + +SQLite in WAL mode, holding what is needed for interactive query over 50k images and nothing +authoritative: + +```sql +roots(id, kind, grant_blob, label, last_seen) +images(id, root_id, source_ref, content_hash, format, w, h, + captured_at, camera, lens, iso, aperture, shutter, + availability, sidecar_mtime) +versions(id, image_id, name, is_default, graph_hash, rating, label, flag) +keywords(version_id, keyword) +folders(id, root_id, path, etag, parent_id) -- ETag pruning (§6.6) +remote(image_id, file_id, etag, sync_state, remote_path) +cache(version_id, kind, resolution, graph_hash, path, bytes, last_used) +jobs(id, kind, state, payload, attempts) -- resumable across process death +``` + +`folders.etag` must exist from schema v1 — sync pruning cannot work without persisted folder ETags, +and adding it later means a migration plus a full re-scan of every library (§6.6). + +Rebuild reads sidecars and re-derives everything else. This makes NFR-R6's recovery path the normal +mechanism rather than a last resort. + +### 6.3 Identity + +| Entity | Key | Stable across | +|---|---|---| +| Image | content hash | rename, move, re-import | +| Version | UUID | everything | +| Remote file | `oc:fileid` | server-side rename/move | +| Cache entry | `(version, kind, resolution, graph_hash)` | — | + +Content-hash identity is what makes FR-CAT-9's reconnection work: a moved file is recognised rather +than re-imported, and a server-side move is not a re-download of 80 MB. + +--- + +## 7. Concurrency + +### 7.1 Executors + +| Executor | Threads | Work | +|---|---|---| +| UI | 1 | Slint event loop. **Never blocks.** | +| GPU submit | 1 | Command encoding and queue submission | +| Decode | cores − 2 | RAW decode, preview extraction | +| I/O | 4 | Catalog, sidecar, cache, filesystem | +| Network | 2 | Nextcloud transfer | + +The UI executor never blocks — this is the mechanism behind R4 and NFR-P9, which the requirements +state as outcomes without saying how. + +### 7.2 Cancellation + +Cooperative, with tokens threaded through every long operation. Observed within 100 ms +(NFR-ARCH-3), including in-flight GPU submissions. Cancelling a scan, export, or sync leaves no +partial state beyond what is separately resumable. + +### 7.3 Errors + +No worker panics the process. Errors are typed and attach to the affected image or job: + +```rust +pub enum ImageError { Decode(DecodeError), SourceOffline, Gpu(GpuError), … } +``` + +A decode failure marks one image and continues the batch (FR-RAW-4). A GPU error triggers §5.6 +recovery. Panics in decode are caught at the boundary, since RAW parsing handles untrusted input. + +--- + +## 8. Sync architecture + +Sync is pluggable. `dr-sync` defines a `RemoteBackend` trait; **only the Nextcloud connector is +implemented**, but the boundary is designed so S3, generic WebDAV, or a self-hosted photo server can +be added without touching the sync engine. + +### 8.1 Why capability negotiation, not a common denominator + +The obvious abstraction — a trait exposing only what every backend can do — would be a mistake here, +and it is worth being explicit about why. + +Nextcloud's fast path depends on a behaviour that is **not** a WebDAV guarantee: it propagates ETag +changes *up* the folder tree, so an unchanged root ETag proves nothing anywhere in the library +changed. That single property is what turns a 50k-image no-op sync into **one HTTP request**. A +plain WebDAV server (Apache `mod_dav`, an SFTP mount, a raw S3 bucket) offers no such guarantee, and +a trait built to their common subset would force full enumeration on every sync — the same 50k +requests the design exists to avoid. + +So backends **declare capabilities**, and the sync engine picks the best available strategy: + +```rust +pub struct Capabilities { + /// How the backend reports what changed. Determines sync cost. + pub change_detection: ChangeDetection, + /// Stable identity across server-side rename/move. + pub stable_ids: bool, + /// Byte-range reads — required for embedded-preview extraction. + pub range_reads: bool, + /// Resumable upload for large files. + pub chunked_upload: Option, + /// Many-small-files upload in one request (sidecars). + pub bulk_upload: bool, + /// Conditional write for optimistic concurrency. + pub conditional_write: bool, + /// Server-rendered thumbnails, if any. + pub server_previews: ServerPreviews, +} + +pub enum ChangeDetection { + /// Backend hands us a cursor; we ask "what changed since?". + /// Cheapest possible. (No current backend, but the shape most + /// object stores and delta APIs fit.) + DeltaCursor, + /// Directory ETags propagate upward — unchanged parent proves + /// unchanged subtree. Nextcloud. One request for a no-op sync. + PropagatingEtags, + /// ETags exist but only on the entry itself. Must enumerate the + /// tree; ETags then avoid re-downloading unchanged content. + LocalEtags, + /// Nothing but modification times. Enumerate and compare. + Timestamps, +} +``` + +### 8.2 Strategy per capability tier + +The engine has one strategy per `ChangeDetection` variant, chosen at connect time: + +| Tier | Detection | No-op sync cost, 50k images | Notes | +|---|---|---|---| +| 1 | `DeltaCursor` | 1 request | Cursor persisted in `remote_state` | +| 2 | `PropagatingEtags` | **1 request** | Nextcloud. Root ETag unchanged → done | +| 3 | `LocalEtags` | ~1 per folder | Enumerate tree; ETags prevent re-download | +| 4 | `Timestamps` | ~1 per folder + clock-skew risk | Degraded; warn the user | + +Tiers 3 and 4 are honest degradations, not silent ones — the UI reports the sync strategy in use so +a slow backend is visibly slow rather than mysteriously slow. + +**Capability absence never breaks correctness, only speed** — with two exceptions the engine must +handle explicitly: + +- **No `range_reads`** → embedded-preview extraction is impossible, so remote browsing must fall + back to server previews or full download. On a metered connection the engine refuses full + downloads for browsing and reports why. +- **No `conditional_write`** → sidecar conflict detection loses its guarantee. The engine falls back + to revision-counter comparison inside the sidecar body, which narrows but does not close the race. + This is reported as a reduced-safety mode. + +### 8.3 The trait + +```rust +#[async_trait] +pub trait RemoteBackend: Send + Sync { + fn capabilities(&self) -> &Capabilities; + fn connect(&mut self, auth: AuthHandle) -> Result; + + // ---- discovery ------------------------------------------------- + /// One level. `since` carries a per-entry validator (ETag/mtime) + /// so unchanged entries can be skipped by the backend where possible. + async fn list(&self, dir: &RemotePath, since: Option<&Validator>) + -> Result, RemoteError>; + + /// Tier 1 only — capability-gated, returns Unsupported otherwise. + async fn delta(&self, cursor: &Cursor) + -> Result<(Vec, Cursor), RemoteError>; + + /// Tier 2 only — cheap validator for a directory, without listing it. + async fn dir_validator(&self, dir: &RemotePath) + -> Result; + + // ---- transfer -------------------------------------------------- + async fn get(&self, id: &RemoteId, range: Option>) + -> Result; + async fn put(&self, path: &RemotePath, body: Body, precond: Option) + -> Result; + async fn put_many(&self, items: Vec<(RemotePath, Bytes)>) + -> Result>, RemoteError>; + async fn delete(&self, id: &RemoteId, precond: Option) + -> Result<(), RemoteError>; + + // ---- optional -------------------------------------------------- + async fn thumbnail(&self, id: &RemoteId, size: u32) + -> Result, RemoteError>; +} +``` + +Design notes worth keeping: + +- **`get` takes an optional range** rather than having a separate `get_range`. Callers always + express what they need; backends without range support return the whole object and the engine + slices, so correctness holds while the capability flag tells the caller whether it was cheap. +- **`put` takes a `Precondition`**, not a bare ETag — so `IfMatch`, `IfNoneMatch`, and `None` + are all expressible, and a backend without conditional writes can reject at compile-time-visible + runtime rather than silently ignoring. +- **`put_many` is a required method** with a default implementation looping over `put`. Nextcloud + overrides it with bulk upload; other backends get correct behaviour for free. +- **Chunked upload is not in the trait.** It is an implementation detail of `put` — the backend + decides based on body size and its own constraints. Exposing it would leak Nextcloud's protocol + into the interface. + +### 8.4 The Nextcloud connector + +The only implementation. Mapping to the trait: + +| Trait method | Nextcloud | +|---|---| +| `capabilities` | `PropagatingEtags`, stable ids, ranges, chunked (5MB–5GB), bulk, conditional | +| `list` | `PROPFIND Depth:1` with `oc:fileid`, `getetag`, `nc:has-preview` | +| `dir_validator` | `PROPFIND Depth:0` requesting `getetag` only | +| `delta` | `Unsupported` — no RFC 6578 for files (ARCH §6.6) | +| `get` + range | `GET` with `Range:`; detect support by `206` vs `200`, never `HEAD` | +| `put` large | Chunked v2: `MKCOL` upload dir → `PUT` chunks → `MOVE .file` | +| `put_many` | `POST /remote.php/dav/bulk`, `multipart/related` | +| `thumbnail` | `/core/preview?fileId=…&forceIcon=false` — `false` is mandatory, or a server that cannot render RAW returns a generic mimetype icon that we would cache as a thumbnail | +| auth | Login Flow v2, system browser, app password | + +Pruning walk, the Tier 2 strategy in full: + +``` +dir_validator(root) + ├─ unchanged vs stored → done. One request, whole library. + └─ changed → list(dir, since) + ├─ child dirs whose validator differs → recurse + └─ files whose validator differs → queue transfer +``` + +`folders.etag` must exist from schema v1. Adding it later means a migration plus a full re-scan of +every user's library (ARCH §6.6). + +### 8.5 Sidecar conflict resolution + +`put` with `Precondition::IfMatch(validator)`. On precondition failure: + +1. `get` the remote sidecar +2. **Merge per Version, per operation** — a crop on one device and an exposure change on another + both survive, because they touch disjoint operations +3. Genuinely conflicting operations resolve by device timestamp +4. Retry with the new validator, bounded retry count + +No `(conflicted copy)` files. Sidecars are a few KB of structured data, so read-merge-rewrite is +cheap and preserves intent — a meaningful improvement over file-level conflict copies, which is what +the sidecar-authoritative model (ARCH §6.12) buys. + +--- + +## 9. Caching and availability policy + +Sync decides *what changed*. Caching policy decides *what is kept locally* — a separate concern, and +the one that determines whether the app is usable on a tablet with a 2 TB library behind it. + +### 9.1 The three tiers, restated as policy + +| Tier | Content | Default | +|---|---|---| +| Metadata | Catalog rows, sidecars, ratings | Always synced; kilobytes; even on metered | +| Preview | Embedded JPEGs, display proxies | LRU within a size cap | +| Original | Full RAW | Never by default — explicit pin or on-demand | + +Never bulk-sync originals. A 2 TB library against a 128 GB tablet makes mirror-style sync unusable +(ARCH §6.8) — which is precisely where the official Nextcloud desktop client's model fails. + +### 9.2 Cache rules + +A **cache rule** pins a *set* of images at a *tier*. Rules are user-authored, evaluated against the +catalog, and re-evaluated as it changes — so "the last three months" stays current without the user +touching it. + +```rust +pub struct CacheRule { + pub id: RuleId, + pub selector: Selector, + pub tier: Tier, // Preview | Original + pub priority: u8, // eviction order; higher survives longer + pub enabled: bool, +} + +pub enum Selector { + Collection(CollectionId), + Folder { root: RootId, path: PathBuf, recursive: bool }, + /// Absolute or rolling. `Rolling` re-evaluates daily. + DateRange(DateSelector), + Rating { min: u8 }, + Label(ColourLabel), + Flag(FlagState), + Keyword(String), + /// Boolean composition, so "5-star OR flagged, in the last year" works. + All(Vec), + Any(Vec), + Not(Box), +} + +pub enum DateSelector { + Between { from: Date, to: Date }, + /// "Last 90 days" — window moves with the clock. + Rolling { days: u32 }, + /// "This trip" — bounded by a collection's own capture range. + CollectionSpan(CollectionId), +} +``` + +Worked examples of what this expresses: + +| Intent | Rule | +|---|---| +| Trip on the tablet | `Collection(trip) → Original` | +| Recent work at full quality | `Rolling{90} → Original` | +| Portfolio always available | `Rating{min:5} → Original`, high priority | +| Everything browsable | `All → Preview` (the implicit default rule) | +| Client edit on the road | `All([Collection(job), Flag(Pick)]) → Original` | + +### 9.3 Evaluation and eviction + +Rules produce a **desired state** per image; the sync engine reconciles actual against desired: + +``` +for each image: + desired = max(tier of every matching enabled rule) // most generous wins + if desired > actual → enqueue fetch (priority = rule priority) + if desired < actual → mark evictable (not evicted immediately) +``` + +Eviction is lazy and only under pressure — cache cap reached (NFR-RES-4) or platform memory pressure +(FR-PLAT-AND-5). Order: unpinned originals by last-used, then proxies, then thumbnails. **Never +metadata or sidecars** — those are authoritative (ARCH §6.12) and tiny. + +An image that leaves a rolling window is not deleted at midnight; it becomes the first candidate when +space is actually needed. Fetching is likewise opportunistic: queued at the rule's priority behind +anything interactive, and on Android gated by the unmetered-and-charging constraints in FR-NC-6. + +### 9.4 Honesty about what is local + +Adobe's sync fails on *legibility* more than transport: Lightroom Classic syncs only 2560px proxies +while displaying the original's filename, extension, and size, so users genuinely do not know what +they have. DarkRoom shall not repeat this. + +- Every image carries a visible availability state: `Original` · `Preview` · `Metadata only` · + `Offline` +- Operations needing data that is absent say so *before* starting, with the transfer size +- Export from a preview-only image is refused, not silently degraded +- A pinned set reports its true byte cost before the user commits to it + +### 9.5 Storage + +```sql +cache_rules(id, selector_json, tier, priority, enabled) +image_cache(image_id, tier_actual, tier_desired, bytes, last_used, pinned_by_rule) +``` + +`tier_desired` is materialised rather than recomputed per query, so the grid can render availability +badges without evaluating every rule for every visible cell. + +--- + +## 10. Platform abstraction + +```rust +pub trait Storage: Send + Sync { + fn roots(&self) -> Vec; + fn enumerate(&self, root: RootId, cb: &mut dyn FnMut(SourceRef)) -> Result<(), StorageError>; + fn open(&self, src: &SourceRef) -> Result, StorageError>; + fn read_range(&self, src: &SourceRef, r: Range) -> Result, StorageError>; + fn writable_sidecar_dir(&self, src: &SourceRef) -> Option; +} + +pub trait Secrets: Send + Sync { /* Secret Service | Android Keystore */ } +pub trait Lifecycle: Send + Sync { /* process death, memory pressure, background */ } +``` + +| Concern | Linux | Android | +|---|---|---| +| Storage | Filesystem under granted roots | SAF tree URIs, `DocumentsContract` | +| Secrets | Secret Service (libsecret) | Keystore + DataStore + Tink | +| Background | Threads | WorkManager, foreground service | +| Lifecycle | Process lives | Death at any moment; `onTrimMemory` | + +`writable_sidecar_dir` returns `None` on read-only mounts and most SAF trees, which is when the +app-managed sidecar store is used. + +--- + +## 11. Build order + +Independent of D12's resolution, the foundations are shared. What differs is what gets built on top +first. + +**Phase 0 — spikes.** S1 (Slint+wgpu zero-copy), S2 (Android, two GPU vendors), S10 (SAF at 10k +files), S11 (Play permissions), S9 (cross-platform tolerance). These can invalidate the +architecture; everything else assumes they pass. + +**Phase 1 — foundations, needed either way.** `dr-types`, `dr-plat` + both implementations, +`dr-catalog`, `dr-sidecar`, `dr-decode` (preview path first), `dr-gpu` device and tile scheduler. + +**Phase 2 — depends on D12:** + +- *Culler-first:* preview ladder, culling mode, raw histogram, focus peaking, ratings, burst + grouping. Ships something usable without any develop chain. +- *Vertical-slice-first:* minimal develop chain (exposure, curve), export, proving the full + architecture end to end but not yet useful. + +**Phase 3+:** the remaining develop operations, masking, sync, ingest, AI denoise. + +--- + +## 12. Architectural constraints + +Decisions treated as fixed because reversing them is expensive. Each is evidence-backed. + +### 6.1 GPU results never round-trip through the CPU + +darktable identifies this as their single biggest bottleneck: OpenCL output returns to the GTK +thread, which paints it on one CPU core. It constrains the UI framework choice more than any other +requirement — whatever renders the UI must composite our GPU texture directly. + +**Measured on this project** (Radeon RX 7900 XTX, `cargo run -p dr-gpu --example bench +--features readback`), comparing the compute pass alone against compute plus a CPU readback: + +| Canvas | Compute | With readback | Readback share | +|---|---|---|---| +| 840×692 | 0.06 ms | 0.63 ms | 90% | +| 1920×1080 | 0.11 ms | 2.35 ms | 95% | +| 3840×2160 | 0.28 ms | 7.43 ms | **96%** | + +At 4K the shader finishes in 0.28 ms and then 7.15 ms is spent moving pixels through the CPU — a +26× overhead that scales with area, which is why an uncapped window resize falls off a cliff. The +constraint is not a stylistic preference; it is the dominant cost in the frame. + +### 6.2 Tiling from day one + +Mobile GPUs have far less memory. Retrofitting tiling into a whole-image pipeline is a rewrite. + +### 6.3 Non-destructive edit graph, GPU-resident + +Recompute from the graph rather than caching CPU bitmaps between stages. + +### 6.4 GPU-first, not GPU-accelerated + +RawTherapee shows excellent quality is achievable on CPU — and that retrofitting GPU into a mature +CPU pipeline effectively never happens. + +### 6.5 The catalog is the UI's source of truth for queries + +The UI never touches storage or network synchronously. + +### 6.5a The core has no UI dependency + +Operations describe parameters as data. This buys headless golden-image testing, one-operation-two- +presentations, and UI replaceability. CI-enforced. + +### 6.6 Sync cannot rely on server-side change tokens + +**Verified:** Nextcloud's `Directory.php` does not implement `ISyncCollection`; sync tokens exist +only for CalDAV/CardDAV (`nextcloud/server#22584`, open since 2020). Folder ETags must be persisted +from schema v1. + +### 6.7 Server-side RAW previews are not assumable + +**Verified:** no RAW preview provider ships with Nextcloud. Range-based embedded extraction is the +primary path; server previews are opportunistic. `preview_max_filesize_image` defaults to 50 MB, +excluding many RAWs even where a provider exists. + +### 6.8 Sync is selective, not mirror-style + +### 6.9 Android forbids the filesystem-scan model + +**Verified:** `MANAGE_EXTERNAL_STORAGE` is not grantable — Play policy explicitly disallows "media +files access" as a permitted use. `READ_MEDIA_IMAGES` would not help: proprietary RAW is not typed +`image/*` by the platform scanner, so it does not appear in `MediaStore.Images`. SAF is the only +path, and it provides no filesystem path — hence `SourceRef` (§3.1). + +### 6.10 GPU device loss is expected + +Routine on Android: backgrounding, driver resets, thermal events. Recovery is tractable because the +edit graph is CPU-side. + +### 6.11 Drawn masks are GPU-rasterised + +darktable rasterises parametric masks on GPU and drawn masks on CPU; users describe drawn masking as +"unworkable" and the lag is not fixable with more cores. Highest-value architectural win available +relative to the incumbents, and free if designed in now. + +### 6.12 Sidecars authoritative, catalog disposable + +darktable maintains both a database and sidecars while achieving the reliability of neither — +documented lockouts, corruption where all three recovery paths failed, sidecars silently not written +in 5.0.0. RawTherapee's `.pp3` is the model: plain text, next to the file, no database in the trust +path. + +### 6.13 Bit-identity applies to integer state only + +GPU float results diverge across vendors — transcendental implementations differ, drivers optimise +differently, f16 rounding varies. Cache keys and graph hashes are computed over CPU-side integer +state, which is exactly deterministic. Cross-platform *rendering* equality is a bounded tolerance +(R1), not a checksum. + +--- + +## 13. Decisions + +Full rationale in [requirements.md §8](requirements.md). Summary: + +| # | Decision | Status | +|---|---|---| +| D1 | Rust + Slint + wgpu | Decided | +| D2 | rawler, LibRaw fallback behind a trait | Decided | +| D3 | First milestone | **Provisional — depends on D12** | +| D4 | Nextcloud: ETag pruning, chunked upload, Login Flow v2 | Resolved | +| D5 | lcms2 + GPU-side transforms | Decided | +| D6 | Hand-written WGSL | Decided | +| D7 | reqwest + quick-xml | Decided | +| D8 | GPLv3 | Decided | +| D9 | Declarative operation descriptors | Decided | +| D10 | Single adaptive interface | Decided | +| D11 | Product positioning | Decided | +| D12 | Scope versus pace | **Open** | + +--- + +## 14. Open questions + +**D12 — scope versus pace.** The selected feature set implies years of full-time work against a +stated evenings-and-weekends budget. Resolving it sets D3's milestone and §11's Phase 2. + +**NFR-R8 — CPU fallback extent.** §6.4 says GPU-first; NFR-RES-2 assumes a CPU fallback on +allocation failure. Decide whether that means a full CPU pipeline (a second implementation) or only +tile-spill staging. Recommend the latter. + +**Slint accessibility on Android.** Unverified; may be a toolkit gap (NFR-A11Y-2). Cheaper to +discover before the UI is built. + +**Play Store versus GPLv3.** Generally workable but should be confirmed, with F-Droid as fallback. diff --git a/docs/milestone-v0.1.md b/docs/milestone-v0.1.md new file mode 100644 index 0000000..13adb65 --- /dev/null +++ b/docs/milestone-v0.1.md @@ -0,0 +1,321 @@ +# DarkRoom v0.1 — Remote library viewer + +**Status:** Draft · 2026-08-08 +**Companion to:** [requirements.md](requirements.md) · [architecture.md](architecture.md) + +The first buildable milestone: connect to a Nextcloud folder, index it locally, and display RAW +previews on both Linux and Android. + +--- + +## 1. What this is + +A photo *viewer*, not an editor. It connects to a Nextcloud account, lets the user pick a folder, +indexes what is there into a local catalog, and displays images by extracting their embedded JPEG +previews over byte-range requests. + +**It exists to prove the architecture is sound before anything is built on it.** Four assumptions +in the design would each be expensive to discover wrong later, and this milestone tests all four +against real servers, real files, and real devices. + +### 1.1 Why this shape + +The alternative first milestone (D3's vertical slice: local scan → grid → open → two edits → +export) proves the pipeline but is useful to nobody and tests nothing about sync or SAF. This +milestone is comparable in size, exercises the genuinely risky parts, and produces something you +can actually point at a server and use. + +### 1.2 What it deliberately excludes + +No editing. No develop pipeline, no sidecars, no export, no culling mode, no ratings. Those depend +on foundations this milestone establishes; adding them before the foundations are proven risks +building on sand. + +--- + +## 2. The four assumptions under test + +Each maps to a spike in [requirements.md §9](requirements.md). + +| # | Assumption | If wrong | Spike | +|---|---|---|---| +| **A1** | Slint can composite a wgpu compute texture zero-copy, on both platforms | ARCH §6.1 fails; D1's fallbacks apply; **the framework choice is wrong** | S1, S2 | +| **A2** | Android SAF can enumerate and range-read at library scale | FR-CAT-1a and the Android performance targets fail | S10 | +| **A3** | HTTP Range extraction of embedded previews works against Nextcloud | Remote browsing on mobile data is not viable; ARCH §6.7 fails | S4 | +| **A4** | reqwest does TLS on Android without unmanageable pain | D7's escape hatch needed | S3 | + +**A1 is the one that would hurt most.** It determines whether Rust + Slint was the right call at +all, so it should be proven in the first week, before catalog or sync work begins. + +--- + +## 3. Functional scope + +Requirement IDs reference [requirements.md](requirements.md); a v0.1 suffix marks a reduced subset +of the full requirement. + +### 3.1 Account and connection + +**M-1 — Login.** Connect to a Nextcloud instance via Login Flow v2 (FR-NC-1): POST to +`/index.php/login/v2`, open the returned URL in the **system browser**, poll until an app password +arrives. The app never handles the user's primary password. + +`User-Agent` identifies the device so the resulting app password is revocable per-device. + +**M-2 — Credential storage.** Store the app password in platform secure storage (FR-NC-2) — Secret +Service on Linux, Keystore-backed on Android. Never in the catalog, never in logs. + +**M-3 — Folder selection.** Browse the remote tree and choose one folder as the library root. +Recursive descent is in scope; multiple roots are not. + +**M-4 — Disconnect.** Revoke via `DELETE /ocs/v2.php/core/apppassword` and clear local credentials. + +### 3.2 Indexing + +**M-5 — Remote listing.** `PROPFIND Depth:1` walking the chosen folder recursively, requesting +`oc:fileid`, `getetag`, `getcontentlength`, `getlastmodified`, `resourcetype`, and +`nc:has-preview`. Files whose extension matches the supported set (§3.3) are catalogued; others are +ignored. + +**M-6 — Catalog.** Persist to SQLite in WAL mode. v0.1 schema is a strict subset of ARCH §6.2: + +```sql +schema_version(version) + +accounts(id, server_url, login_name, user_id) + +folders(id, account_id, parent_id, remote_path, etag, last_listed) + +images(id, account_id, file_id, remote_path, etag, size, remote_mtime, + format, captured_at, camera, lens, iso, aperture, shutter, + width, height, availability) + +previews(image_id, kind, width, height, bytes, path, last_used) +``` + +`folders.etag` is present **from schema v1** — ARCH §6.6 requires it, and retrofitting means a +migration plus a full re-scan of every library. + +`schema_version` exists from the first commit so NFR-R5's migration machinery has somewhere to +start. + +**M-7 — Incremental re-listing.** On subsequent syncs use ETag pruning (FR-NC-4): `PROPFIND +Depth:0` on the root, and if its ETag is unchanged, **stop** — one request proves the whole library +is unchanged. Where changed, `Depth:1` and recurse only into folders whose ETags differ. + +This is the mechanism that has to work for the library to scale, so v0.1 exercises it deliberately +rather than always re-listing. + +**M-8 — Offline browsing.** The catalog is queryable with no network. Previously indexed images +appear with their metadata and any cached previews. Availability is visible per image (FR-NC-6c): +*Preview* or *Metadata only*. + +### 3.3 RAW handling + +**M-9 — Formats.** The FR-RAW-1 launch set: CR2, CR3, NEF, ARW, RAF, RW2, ORF, DNG. Plus JPEG, so +a mixed folder displays sensibly. + +**M-10 — Range-based preview extraction.** Display without downloading whole files: + +1. Range-read the first 256 KB of the file +2. Parse the container to locate the embedded JPEG preview (offset and length) +3. Range-read exactly those bytes +4. Decode the JPEG and cache it locally + +Typical cost 1–3 MB against 25–100 MB for the full file. **This is what makes the app usable on +mobile data**, and it is assumption A3. + +Range support is detected by issuing a `Range` request and checking for `206` versus `200` — +**never** by probing with `HEAD`, since Nextcloud does not advertise `Accept-Ranges` (ARCH §6.7). + +**M-11 — Fallbacks, in order.** Where step 2 finds no usable preview: + +1. Server preview via `/core/preview?fileId=…&forceIcon=false` where `nc:has-preview` is true. + **`forceIcon=false` is mandatory** — the default returns a generic mimetype icon for files the + server cannot render, which would otherwise be cached as though it were a thumbnail. +2. Full download and decode via rawler, on explicit user action only, never automatically. +3. Placeholder with an explanatory state. + +**M-12 — Metadata.** Extract from the same header range already fetched in M-10: camera make and +model, lens, capture time, ISO, aperture, shutter, dimensions. No second request. + +### 3.4 Display + +**M-13 — Grid.** Virtualised thumbnail grid (FR-CAT-4) rendering only visible cells plus a prefetch +margin, with bounded memory independent of folder size. + +**M-14 — Single image view.** Full-window display of one image, with fit and 1:1 zoom, pan, and +next/previous navigation. + +**M-15 — GPU display path.** Decoded previews upload to GPU textures and composite through Slint +via `create_texture_from_hal`. **Pixels are never read back to the CPU** (ARCH §6.1). + +This is assumption A1, and the reason it is in v0.1 at all: it is far cheaper to discover a +compositing problem now than after a develop pipeline is written against it. + +**M-16 — Adaptive layout.** Compact and expanded layout classes (FR-UI-1) driven by window size, +not device type. Touch targets meet 44pt when touch is the active modality (FR-UI-3). + +### 3.5 Platform + +**M-17 — Linux.** X11 and Wayland. XDG base directories for catalog, cache, and config. Secret +Service for credentials. + +**M-18 — Android.** Storage Access Framework only (FR-PLAT-AND-1) — no `MANAGE_EXTERNAL_STORAGE`, +no `READ_MEDIA_IMAGES`. v0.1 reads remote content, so SAF matters for the *cache* location and for +proving the `SourceRef` abstraction holds before local library support arrives. + +Keystore-backed credential storage. Process death mid-browse restores the current folder and scroll +position (FR-PLAT-AND-3). + +--- + +## 4. Explicitly out of scope + +Listed so absence reads as a decision. + +| Excluded | Arrives in | +|---|---| +| Any editing, develop pipeline, sidecars | v0.2 | +| Export | v0.2 | +| Culling mode, ratings, flags, labels | v0.3 | +| Focus peaking, raw histogram | v0.3 | +| Local (non-Nextcloud) library scanning | v0.2 | +| Upload, bi-directional sync, conflict merge | v0.4 | +| Cache rules and pinning (FR-NC-6a) | v0.4 | +| Multiple accounts or roots | later | +| Ingest from card | later | +| Search and filter beyond folder navigation | v0.3 | + +**Read-only against the server.** v0.1 performs no `PUT`, `MOVE`, or `DELETE` on remote content. +This removes conflict handling and chunked upload from scope entirely, and means a bug cannot +damage the user's library. + +--- + +## 5. Acceptance criteria + +Measured against a reference Nextcloud instance holding **≥5,000 RAW files**, on the reference +desktop and **two Android devices with different GPU vendors** (Adreno and Mali). + +| # | Criterion | Target | +|---|---|---| +| **AC-1** | Login through to first grid render | < 30 s for 5,000 files | +| **AC-2** | Re-sync with nothing changed | **1 HTTP request** | +| **AC-3** | Grid scroll, cached previews | 60 fps sustained, both platforms | +| **AC-4** | Bytes transferred per image, preview path | < 3 MB average | +| **AC-5** | Single-image display from cache | < 100 ms | +| **AC-6** | Offline launch → browsable grid | < 2 s desktop, < 4 s Android | +| **AC-7** | Memory, 5,000-image folder open | < 400 MB desktop, < 200 MB Android | +| **AC-8** | GPU readback of image data | **Zero occurrences** — asserted by instrumentation | +| **AC-9** | Android process death mid-browse | Folder and scroll position restored | +| **AC-10** | Catalog after forced kill during sync | Opens clean, resumes | + +**AC-2 and AC-8 are the load-bearing ones.** AC-2 proves ETag pruning works, which is what lets the +library scale. AC-8 proves ARCH §6.1 holds — and it is asserted by instrumentation rather than +inspection, because a readback introduced later would otherwise pass unnoticed until it showed up +as unexplained slowness. + +--- + +## 6. Build order + +### Phase 0 — de-risk (before anything else) + +Throwaway code answering the four assumptions. If A1 fails, stop and revisit D1 rather than +building on it. + +1. **A1 / S1** — wgpu compute writes a texture; Slint composites UI over it; Linux. Test on + Mesa/AMD, Intel, and NVIDIA proprietary, under X11 and Wayland. +2. **A1 / S2** — the same on both Android devices. +3. **A4 / S3** — reqwest HTTPS PROPFIND from an Android device, including the + `rustls-platform-verifier` Kotlin init. +4. **A3 / S4** — range-extract an embedded JPEG from a CR3, NEF, and ARW on a real server; measure + bytes. +5. **A2 / S10** — SAF enumeration and range reads over a 10k-file tree; measure against the + NFR-P1/P3 figures. + +### Phase 1 — foundations + +`dr-types` (`SourceRef`, ids) · `dr-plat` traits and both implementations · `dr-catalog` (v0.1 +schema, migrations from commit one) · `dr-gpu` (device, texture upload, Slint bridge). + +### Phase 2 — connectivity + +`dr-sync` (`RemoteBackend` trait) · `dr-sync-nextcloud` (Login Flow v2, PROPFIND, ETag pruning, +range GET) · credential storage. + +### Phase 3 — imaging + +`dr-decode` (container parsing, embedded-preview location, JPEG decode, metadata) · preview cache. + +### Phase 4 — interface + +`dr-ui` (grid, single-image view, adaptive layout, folder picker, connection flow). + +### Phase 5 — hardening + +Offline behaviour · process death · cancellation · error surfaces · the AC suite in CI. + +**Both platforms build in CI from the first commit.** This is the point of choosing both from day +one: an Android break is caught the day it lands, not at a porting milestone. + +--- + +## 7. Crates in play + +A subset of ARCH §2. Crates not listed are not created yet. + +``` +core/ + dr-types ✓ SourceRef, ImageId, AccountId, Validator + dr-catalog ✓ v0.1 schema subset, queries, migrations + dr-decode ✓ preview extraction + metadata; no demosaic + dr-gpu ✓ device, texture upload, Slint bridge; no pipeline + dr-sync ✓ RemoteBackend trait, ETag pruning engine + dr-sync-nextcloud ✓ the connector + dr-pipeline ✗ v0.2 + dr-colour ✗ v0.2 + dr-export ✗ v0.2 + dr-sidecar ✗ v0.2 +ui/ + dr-ui ✓ grid, viewer, connection flow + dr-widgets ✗ v0.2 (no custom controls yet) +platform/ + dr-plat ✓ Storage, Secrets, Lifecycle traits + dr-plat-linux ✓ + dr-plat-android ✓ +apps/ + darkroom-desktop ✓ + darkroom-android ✓ +``` + +`dr-gpu` exists in v0.1 **only** to upload decoded JPEGs and hand textures to Slint. No compute +pipeline, no tiling, no masks. It is deliberately the thinnest thing that still proves A1. + +--- + +## 8. Decisions this milestone informs + +| Decision | What v0.1 tells us | +|---|---| +| **D12** — scope vs pace | How long a milestone of this size actually takes at the available pace. The single most useful output. | +| **D3** — first milestone | Supersedes the vertical slice, if this proves the better shape. | +| **D1** — Rust + Slint | AC-8 and A1 either confirm the framework choice or reopen it. | +| **NFR-COMPAT-1** — hardware baseline | Two real Android devices give the baseline actual numbers instead of a placeholder. | +| **D7** — network stack | Whether the reqwest Android TLS path is a half-day or a fortnight. | + +--- + +## 9. Risks + +| Risk | Likelihood | Mitigation | +|---|---|---| +| Slint `create_texture_from_hal` doesn't work as documented | Medium | Phase 0 first; D1 records fallbacks | +| SAF enumeration too slow at 10k files | Medium | S10 measures before commitment; batch and cache aggressively | +| Embedded previews too small or absent on some bodies | High | Known — Sony embeds small previews, some bodies none. M-11's fallback chain handles it; detect per camera model | +| reqwest Android TLS worse than expected | Medium | D7 escape hatch: `tls_certs_only` with `webpki-roots` | +| GPU vendor divergence on Android | Medium | Two vendors in CI from the start | +| Scope creeps toward editing | **High** | §4 is explicit; v0.1 is read-only against the server | + +The last one is the real risk. A viewer that works is a strong temptation to add "just one slider." diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..5082453 --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,1142 @@ +# DarkRoom — Requirements Specification + +**Status:** Draft v0.1 · 2026-08-08 +**Owner:** Duncan Tourolle + +A cross-platform, non-destructive RAW photo editor for Linux desktop and Android, in the +Lightroom idiom: a catalog of many thousands of images, a develop module with GPU-accelerated +adjustments, and export to standard 8-bit (or higher) deliverables. + +--- + +## 1. Scope and intent + +### 1.1 What this is + +DarkRoom is a photo *library* and *develop* application. It manages large collections of camera +RAW files, renders them to screen with GPU acceleration, applies non-destructive edits stored as +metadata, and exports finished images. + +### 1.2 Primary platforms + +| Platform | Priority | Notes | +|---|---|---| +| Linux desktop | Primary | X11 and Wayland. Development and reference platform. | +| Android | Primary | Tablet-first; phone supported. Shares the image core. | + +Other platforms (Windows, macOS, iOS) are explicitly out of scope for v1, but the architecture +must not foreclose them. In practice this means the GPU abstraction and the image core must not +hard-code Vulkan-only or Linux-only assumptions at their public interfaces. + +### 1.3 What this is not + +- Not a DAM with server-side multi-user collaboration +- Not a pixel editor (no layers, no brushes in v1 beyond local-adjustment masks) +- Not a printing/soft-proofing suite in v1 + +### 1.4 Architecture + +The technology stack, crate layout, and design are specified separately in +[architecture.md](architecture.md). This document states *what* the software must do; +the architecture document states *how*. + +Requirements here reference architectural constraints as `ARCH §n` where the constraint +materially shapes what is testable. + +--- + +## 2. Core requirements (from the brief) + +These are the user's stated requirements, restated as testable criteria. + +| ID | Requirement | Acceptance criterion | +|---|---|---| +| **R1** | Cross-platform: Linux + Android | Same image core compiles and runs on both. For the same input and edit graph, output is **perceptually identical within a bounded tolerance** — see below. | +| **R2** | Efficient display of huge RAW libraries | A 50,000-image catalog scrolls at 60fps sustained, with a stated prefetch margin and cache-hit rate sufficient that no cell renders as a placeholder at a scroll velocity of *(figure TBD)* rows/second. Catalog opens in under 2s. | +| **R3** | Make a RAW beautiful at 8-bit output | Full non-destructive develop chain at high internal precision, with camera input profiles (FR-DEV-3e) and a colour-managed path to 8/16-bit export. | +| **R4** | HW acceleration and parallelism | All per-pixel work runs on GPU compute. CPU work (decode, I/O) is parallelised across cores. The UI executor never blocks on image work (NFR-ARCH-1). | +| **R5** | Work on downscaled proxies for display | Display pipeline operates at viewport resolution, not source resolution. Only visible tiles are computed; panning recomputes only newly exposed tiles. | +| **R6** | Nextcloud integration | Browse, download, and upload images and edit metadata against a Nextcloud instance, offline-capable. | + +**On R1's tolerance.** An earlier draft required output to be *bit-identical* across platforms. +That is not achievable and the requirement has been corrected. Floating-point compute results +differ between GPU vendors: transcendental function implementations vary, drivers apply different +optimisations, and f16 rounding diverges. A checksum comparison across Adreno and Mesa would fail +for reasons that have nothing to do with correctness. + +R1 is therefore stated as a bounded tolerance — a defined maximum per-pixel deviation, expressed in +ΔE2000 for colour or ULPs at the working precision. **The threshold must be fixed before spike S9**, +because S9 both validates R1 and calibrates what the achievable tolerance actually is. + +Where genuine bit-identity is required — cache keys, edit-graph hashing (§5.2 invariant 3) — it +applies to *integer* operations on CPU-side state, which are deterministic, never to GPU float +results. + +--- + +## 3. Functional requirements + +### 3.1 Catalog and library management + +**FR-CAT-1 — Scan.** The app shall scan one or more user-granted library roots for supported image +files, recursively, without blocking the UI. Progress is reported and the scan is cancellable and +resumable. A "root" is a platform-specific grant (a directory on Linux, a persisted document tree +on Android — see FR-PLAT-AND-1), not necessarily a filesystem path. + +**FR-CAT-1a — Source addressing.** The catalog and decode layers shall address source data through +an opaque `SourceRef` that resolves to a seekable byte stream, **never through a filesystem path**. +Android's Storage Access Framework provides no usable path (ARCH §6.9), so a path-based API would not be +portable. `SourceRef` carries enough information to re-resolve after an app restart or a permission +re-grant. + +**FR-CAT-2 — Catalog store.** All catalog metadata (source references, EXIF, ratings, labels, edit +graphs, sync state) is stored in a local embedded database. The database is the source of truth for +the UI; sources are scanned into it, never queried directly on the UI path. + +**FR-CAT-3 — Thumbnail pyramid.** For each image the app maintains a cached multi-resolution +thumbnail set. Initial thumbnails are extracted from the RAW's embedded JPEG preview where +present (fast path, no demosaic). Higher-quality proxies are generated lazily from the full +decode when the image is first opened in develop. + +**FR-CAT-4 — Virtualised grid.** The library grid shall render only visible cells plus a small +prefetch margin. Memory use is bounded and independent of catalog size. + +**FR-CAT-5 — Metadata.** Read EXIF, camera make/model, lens, capture time, ISO/aperture/shutter, +GPS. Support user-assigned star ratings, colour labels, flags, and keywords. + +**FR-CAT-6 — Search and filter.** Filter the catalog by any indexed metadata field, rating, +label, folder, and keyword, with results updating interactively on a 50k catalog. + +**FR-CAT-7 — Collections.** User-defined collections that reference images without moving files. + +**FR-CAT-8 — Sidecar persistence, independent of sync.** Edit graphs shall be written to per-image +sidecars for **all** catalogued images, whether or not a Nextcloud account exists. Invariant 5.2.4 +(catalog rebuildable from sources plus sidecars) otherwise fails for local-only users, leaving every +edit in a single SQLite file with no recovery path. + +Where the source location is not writable — read-only mounts, and commonly Android SAF trees — +sidecars are written to an app-managed store keyed by `SourceRef`, and the app shall state which +location is in use. + +**FR-CAT-9 — Offline and relocated sources.** An image whose source is unreachable shall be marked +*offline*, never silently removed. Cached previews, metadata, ratings, and edits remain browsable +and editable while offline; edits queue and apply when the source returns. + +The app shall support folder-level and image-level reconnection, matching candidates by content +hash and filename, and shall auto-reconnect a volume or tree when it reappears. A source deleted +outside the app shall be distinguished from one merely unreachable before any destructive catalog +action is offered. + +This matters more than it appears: external drives, SD cards, and network mounts disappear +routinely, and on Android a tree permission can be revoked or lost on reinstall. + +**FR-CAT-10 — Import and ingest.** Copy or move files from a source volume into a destination +structured by a date/metadata template, with rename-on-import, an optional simultaneous +second-destination backup copy, and per-file verification against a checksum. Removable-volume +insertion is detected where the platform permits. + +Distinct from FR-CAT-1: scanning catalogues files where they already are; import moves them from a +card into the library. Both are needed. + +**FR-CAT-11 — Duplicate detection.** Detect duplicates on import by (capture time + camera serial + +original filename) and by content hash, offering skip or import-as-new. Camera filenames wrap at +`IMG_9999`, so filename alone is insufficient. Existing catalog duplicates are detectable on demand. + +**FR-CAT-12 — Versions (virtual copies).** An image may carry multiple named `Version`s, each with +an independent edit graph, without duplicating source data. Versions are creatable, nameable, +deletable, and independently exportable; one is the default. + +**FR-CAT-13 — XMP interoperability.** Read and write standard XMP sidecars for ratings, colour +labels, keywords and hierarchical subjects, title, description, copyright, and GPS, using standard +`xmp:`/`dc:`/`lr:` schemas so other tools interoperate. + +DarkRoom's edit graph lives in a private namespace and shall neither be interpreted by, nor +corrupt, other tools' XMP. Writing to source-adjacent XMP is off by default (NFR-R4). External +modification of an XMP sidecar shall be detected and a metadata reload offered. + +**FR-CAT-14 — Migration import.** Import ratings, labels, keywords, and collections from a +Lightroom `.lrcat` and a darktable `library.db`. Edit graphs are explicitly **not** migrated — +develop parameters do not translate meaningfully between pipelines, and a partial translation is +worse than none. This is the path in for users with existing libraries. + +### 3.2 RAW decoding + +**FR-RAW-1 — Format support.** Decode mainstream RAW formats. Minimum launch set: Canon (CR2, +CR3), Nikon (NEF), Sony (ARW), Fujifilm (RAF, including X-Trans), Panasonic (RW2), Olympus (ORF), +Adobe DNG. Additional formats are a coverage goal, not a launch blocker. + +**FR-RAW-2 — Decoder abstraction.** RAW decoding sits behind a trait taking a `SourceRef` +(FR-CAT-1a), not a filesystem path, so the same decoder works over a local file, an Android SAF +document, or a byte range fetched from Nextcloud. A second implementation may be added for broader +camera coverage without changing callers (D2). + +**FR-RAW-3 — Sensor data handling.** Correctly apply per-camera black/white levels, CFA pattern +identification, and camera-native colour matrices. Demosaic quality shall be selectable, with at +least a fast method for preview and a high-quality method for export (FR-EXP-9 requires export to +use the latter). + +**FR-RAW-4 — Robustness.** A malformed or hostile RAW file shall not crash the application or +compromise the process. Decode failures are reported per-file and do not abort a batch. + +**FR-RAW-5 — X-Trans as a first-class path.** Per D11, Fujifilm is explicitly targeted: + +- **Markesteijn-class demosaic as the default** for X-Trans sensors, not an opt-in advanced setting +- **X-Trans-aware sharpening**, since the non-Bayer CFA responds differently +- In-RAF film simulation tag read and matched (FR-DEV-3f) + +This targets the market's best-documented colour grievance. Adobe's X-Trans "worms" artefact is a +decade-old unresolved complaint; darktable and RawTherapee have the better algorithm but poor +defaults; Capture One has the best film-simulation support but drops X-Trans I and II. + +*Cost to note:* X-Trans demosaic is documented at **at least 2× the processing cost of Bayer**, +which affects the NFR-P4 and NFR-P7 budgets for Fuji files specifically. + +### 3.3 Develop pipeline + +**FR-DEV-1 — Non-destructive edit graph.** All edits are stored as parameters in an ordered edit +graph attached to the image. Source files are never modified. Any rendered output is reproducible +from source + graph. + +**FR-DEV-2 — Internal precision.** The pipeline operates internally at a minimum of 16-bit float +per channel in a wide-gamut linear working space. Quantisation to the output bit depth happens +once, at the final export or display stage. + +**FR-DEV-3 — Adjustment set (v1).** + +- White balance (temperature/tint, and picker) +- Exposure, contrast +- Highlights / shadows / whites / blacks recovery +- Tone curve (RGB and per-channel) +- HSL / colour mixer per colour band +- Vibrance and saturation +- Texture / clarity +- Sharpening and noise reduction (luminance and chroma) +- Lens corrections: distortion, chromatic aberration, vignetting +- Crop, straighten, rotate, flip +- Local adjustments: linear gradient, radial gradient, and brush masks + +**FR-DEV-3a — Self-describing operations.** Every processing operation shall declare its own +parameters through a descriptor, so that adding an operation requires no changes to frontend code. +An operation declares *what* its parameters are; the frontend decides *how* to present them. + +```rust +pub trait Operation: Send + Sync { + /// Static description of this op's parameters. Drives UI generation. + fn descriptor() -> OpDescriptor where Self: Sized; + + /// Parameter values → GPU work. No UI types cross this boundary. + fn encode(&self, enc: &mut ComputeEncoder, ctx: &TileContext); + + /// Identity for cache invalidation (see §5.2 invariant 3). + fn params_hash(&self) -> u64; +} + +pub struct ParamDescriptor { + pub id: ParamId, + pub label: LocalizedString, + pub kind: ParamKind, + pub default: ParamValue, + pub affects: Affects, // Geometry | Colour | Detail — drives invalidation scope +} + +pub enum ParamKind { + /// Ordinary numeric parameter. Frontend picks slider / drag-strip / dial by modality. + Scalar { min: f32, max: f32, scale: Scale, unit: Unit, precision: u8 }, + Bool, + Enum { variants: Vec<(EnumId, LocalizedString)> }, + Colour { has_alpha: bool }, + /// Escape hatch: a control that does not reduce to a primitive. + /// The frontend owns the implementation; the op only names the kind + /// and defines the data it exchanges. + Custom { widget: WidgetKind, data: CustomParamSchema }, +} + +pub enum WidgetKind { + ToneCurve, // per-channel curve editor + ColourWheel, // colour grading wheels + CropOverlay, // on-canvas crop and straighten handles + GradientHandle, // on-canvas linear/radial mask placement + BrushMask, // on-canvas brush strokes + WhiteBalancePick, // eyedropper bound to canvas +} +``` + +**The pipeline crate shall not depend on the UI toolkit.** Descriptors carry data, never widgets. +This keeps the edit chain testable headless (see §9's golden-image tests, which must link no UI) +and is what allows one operation to render differently on touch and desktop. + +**FR-DEV-3b — Frontend presentation mapping.** The frontend maps `ParamKind` to a concrete control +based on input modality and available space. The same descriptor yields different presentations: + +| `ParamKind` | Desktop | Touch (tablet) | +|---|---|---| +| `Scalar` | Slider with numeric entry, scroll-wheel fine adjust | Large drag-strip, double-tap to reset, no keyboard entry | +| `Bool` | Checkbox | Switch, minimum 44pt target | +| `Enum` | Dropdown | Segmented control or sheet | +| `Colour` | Swatch opening a picker popover | Swatch opening a full-width sheet | +| `Custom` | Frontend-supplied control for that `WidgetKind` | Same control, touch-tuned hit targets | + +**FR-DEV-3c — Operation registry.** Operations register themselves at startup. The develop panel is +generated by walking the registry, so a new operation appears in the UI without any frontend +change. Registration order defines default pipeline order; the ordering itself is data, not code. + +**FR-DEV-3d — Invalidation scope.** Each parameter declares what it `affects`, so a change +invalidates only the necessary part of the pipeline. Adjusting exposure shall not re-run lens +correction or re-tile geometry. This is what makes FR-DSP-3's one-frame slider response achievable. + +**FR-DEV-3e — Camera input profiles.** The pipeline shall include a camera-profile stage between +demosaic and the working-space conversion. + +**v1 scope** (per D11 — good defaults rather than exhaustive colour science): + +1. Embedded DNG `ColorMatrix1/2` and `ForwardMatrix1/2` tags +2. A hand-tuned base curve per launch camera body, shipped with the app +3. HaldCLUT import (FR-DEV-3f) + +**Deferred but not foreclosed:** full `.dcp` support with `HueSatDeltas`, `ProfileLookTable`, and +dual-illuminant interpolation. The stage shall be structured so these are additions rather than a +pipeline reordering. + +Rationale for the reduced scope: a bare 3×3 matrix produces the flat, poor-skin-tone rendering +characteristic of dcraw defaults, which is the documented reason people abandon darktable in the +first hour. A per-body base curve fixes most of that at a fraction of the cost of a full DCP +implementation. The profile database ships **versioned independently of the app binary** so bodies +and curves can be added without a release — and, under D8's GPLv3, contributed by users. + +*Acceptance:* for each launch body, the default render is subjectively comparable to the camera's +own JPEG. ΔE2000 validation against ColorChecker references applies once DCP support lands. + +**FR-DEV-3f — Look emulation.** Support HaldCLUT import, which inherits the existing free film +simulation ecosystem at near-zero implementation cost, plus reading the in-RAF film simulation tag +to auto-apply a matching render for Fujifilm files. + +**FR-DEV-3g — AI denoise.** Learned denoising operating in the raw domain, ideally jointly with +demosaic. + +Promoted into v1 scope per D11. The reasoning: unlike AI masking, denoise has **no manual fallback** +— it reaches a quality ceiling no conventional method matches, which is why photographers run a +second application for it. Raw-domain joint demosaic-and-denoise is also markedly easier to build +into a new pipeline than to retrofit, and the same component attacks the X-Trans artefact problem +(FR-RAW-5). + +Inference is **local only** — no cloud, no telemetry (NFR-SEC-4). The stage is optional at runtime +and its absence degrades gracefully. + +**FR-DEV-4 — Ordered, GPU-resident execution.** The pipeline executes as a sequence of GPU +compute stages. Intermediate results remain in GPU memory between stages. **Processed pixels +shall reach the display without a CPU round-trip.** *(This is a hard architectural constraint — +see ARCH §6.1.)* + +**FR-DEV-5 — Edit history.** Per-image undo/redo of edit operations, persisted with the catalog +so history survives a restart. Named snapshots of an edit state. + +**FR-DEV-6 — Presets.** Save, apply, and manage named presets covering a subset of the edit +graph. Copy/paste settings between images. Batch-apply to a selection. + +**FR-DEV-7 — Before/after.** Compare current edit state against the unedited original or against +a chosen history state. + +**FR-DEV-8 — Spot removal.** Non-destructive clone and heal spots stored as parameters in the edit +graph (target, radius, feather, source offset, opacity, mode), with automatic source placement and +manual override, plus a visualise-spots mode. + +Sensor dust is unavoidable with interchangeable lenses, and dust spots are the most common reason a +photographer leaves a RAW editor for a pixel editor mid-workflow. This is not the layer-based +pixel editing excluded by §1.3 — it is a standard parameterised develop operation, and the brush +infrastructure required by FR-DEV-3's masks already covers most of the cost. + +### 3.4 Display and interaction + +**FR-DSP-1 — Proxy-resolution rendering.** The develop view renders at the resolution actually +required by the viewport, not the source resolution. A 60MP image displayed in a 2000px viewport +processes approximately 2000px of data, not 60MP. + +**FR-DSP-2 — Tiled computation.** The visible region is divided into tiles. Only tiles +intersecting the viewport are computed. Panning computes only newly exposed tiles; already-valid +tiles are reused. + +**FR-DSP-3 — Interactive latency.** Moving a slider updates the visible region within one frame +budget at proxy resolution. When a full-resolution result is needed it is computed +asynchronously, and the proxy result remains on screen until it is ready. + +**FR-DSP-4 — Progressive refinement.** During rapid interaction the app may render at reduced +quality or resolution, refining to full quality when interaction settles. Refinement is visually +smooth, not a jarring swap. + +**FR-DSP-5 — Zoom and pan.** Fit, 1:1, and arbitrary zoom levels. At 1:1 and above, the pipeline +operates on the visible crop at full source resolution. + +**FR-DSP-6 — Colour management.** The display path is colour-managed via the output device +profile. Where the platform and display support it, output at greater than 8 bits per channel +and in a wide gamut. On Android this means using the wide-gamut display path where available. + +**FR-DSP-7 — Image evaluation.** Provide a live histogram (luminance and per-channel, in the output +colour space), highlight and shadow clipping indicators, and a pixel colour readout under the +cursor or touch point. + +**These derive from a GPU-side reduction into a small buffer. Per-frame CPU readback of image data +is prohibited** — it would violate ARCH §6.1 on every frame, which is precisely the bottleneck darktable +documents. Histogram computation shall not extend the FR-DSP-3 frame budget. + +Without this a photographer cannot see what highlight recovery is actually doing, which makes the +FR-DEV-3 adjustment set substantially less usable. + +**FR-DSP-8 — Per-display colour and scaling.** The display transform is selected per the display +currently showing the canvas, and updates when the window moves between displays. Fractional and +mixed DPI scaling are handled without resampling artefacts in the canvas. + +The profile-acquisition mechanism is stated per display server, with a defined fallback where +Wayland provides no profile. On a multi-monitor desktop with differing profiles, showing wrong +colours on the second display is a correctness defect, not a polish item. + +### 3.5 Adaptive interface + +DarkRoom ships **one adaptive interface**, not separate touch and desktop applications. A single +Slint codebase reflows by available space and input modality, guaranteeing feature parity by +construction. Phones are out of scope for v1 (§1.3); the layout family spans tablet and desktop. + +**FR-UI-1 — Layout breakpoints.** The interface adapts across at least two layout classes: + +| Class | Typical | Develop layout | +|---|---|---| +| Compact | Tablet portrait, narrow desktop window | Canvas full-width; one collapsible panel at a time; filmstrip on demand | +| Expanded | Tablet landscape, desktop | Filmstrip, canvas, and adjustment panel simultaneously | + +Layout class is a function of window size, not device type — a narrow window on desktop uses the +compact layout, and the transition is continuous rather than a mode switch. + +**FR-UI-2 — Input modality.** The interface detects and adapts to the active input method, which +is independent of layout class: a tablet may have a keyboard and pointer attached, and a desktop +may have a touchscreen. Modality affects control sizing and affordances (FR-DEV-3b), not layout. +Switching input mid-session shall be handled without restart. + +**FR-UI-3 — Touch targets.** Interactive controls present a minimum 44pt hit target when touch is +the active modality. Hit targets may exceed the drawn control bounds. + +**FR-UI-4 — Gestures.** The canvas supports pinch-zoom, two-finger pan, and double-tap to toggle +fit/1:1. Gestures are additive: every gesture-driven action has a non-gesture equivalent, so no +functionality is touch-only. + +**FR-UI-5 — Pointer and keyboard.** Where a pointer is present: hover states, right-click context +menus, and scroll-wheel adjustment on numeric controls. Keyboard shortcuts cover navigation, +rating, and common adjustments. Neither is required for any operation to be reachable. + +**FR-UI-6 — Shared component library.** Touch and desktop presentations are variants of shared +components, not parallel implementations. A new operation (FR-DEV-3c) becomes usable on both +without frontend work. + +**FR-UI-7 — On-canvas controls.** Custom controls that operate on the canvas — crop handles, +gradient placement, brush strokes (`WidgetKind` in FR-DEV-3a) — size their interaction regions to +the active modality while rendering identically. A crop handle drawn at 8px may carry a 44pt touch +region. + +### 3.6 Export + +**FR-EXP-1 — Formats.** Export to JPEG, PNG, TIFF (8 and 16-bit), and AVIF or JPEG XL. Quality, +chroma subsampling, and bit depth are configurable. + +**FR-EXP-2 — Colour space.** Export in a selectable output colour space (sRGB, Display P3, +Adobe RGB, ProPhoto), with the correct ICC profile embedded. + +**FR-EXP-3 — Output sizing.** Export size shall be specifiable by any of the following modes: + +| Mode | Behaviour | +|---|---| +| Original | Full source resolution after crop | +| Long edge | Specified pixels on the longer dimension; aspect preserved | +| Short edge | Specified pixels on the shorter dimension; aspect preserved | +| Width × Height (fit) | Scaled to fit within the box; aspect preserved; result may be smaller in one dimension | +| Width × Height (fill) | Scaled to cover the box and centre-cropped to exactly those dimensions | +| Percentage | Scaled by a factor of the source | +| Megapixels | Scaled so the result approximates a target pixel count | +| Print dimensions | Physical size (mm or inches) at a specified DPI, resolved to pixels | + +Additional constraints: + +- **Upscaling** is permitted but shall be off by default, with an explicit opt-in. Where disabled, + a request larger than the source exports at source size rather than failing. +- **DPI metadata** is settable independently of pixel dimensions, for print workflows. +- **File-size ceiling** (JPEG/AVIF/JPEG XL): optionally target a maximum output size in KB/MB, with + the encoder iterating quality to meet it. Useful for upload limits. +- Sizing operates on the **cropped** result, so the crop rectangle defines the aspect ratio unless + a fill mode overrides it. + +**FR-EXP-4 — Resampling and output sharpening.** Resizing uses a quality resampler (Lanczos or +equivalent) operating on linear-light data at pipeline precision, before quantisation to the output +bit depth. Output sharpening is selectable (none / screen / matte paper / glossy paper) and its +strength scales with the resize factor, since downscaling softens. + +**FR-EXP-5 — Export presets.** Named presets capture format, quality, colour space, sizing mode, +sharpening, metadata policy, and destination. A preset is applicable to a single image or a batch. +Multiple presets may be applied in one operation, producing several outputs per image — e.g. a +full-size TIFF alongside a 2048px sRGB JPEG. + +**FR-EXP-6 — Naming and destination.** Output filenames are generated from a template supporting at +minimum: original filename, sequence number, capture date, export dimensions, and preset name. +Collision policy (overwrite / skip / auto-increment) is configurable. Destinations include a local +path and a Nextcloud remote path (FR-NC-7). + +**FR-EXP-7 — Batch export.** Export a selection with one or more presets, running in the background +with progress and cancellation. Uses all available cores and the GPU. A failure on one image is +reported and does not abort the batch. + +**FR-EXP-8 — Metadata on export.** Configurable EXIF/IPTC/XMP retention, including an option to +strip GPS and personal metadata. Copyright and contact fields are settable per-preset. + +**FR-EXP-9 — Full-quality path.** Export always uses the full-resolution, highest-quality pipeline +regardless of what the display was showing — including the high-quality demosaic (FR-RAW-3), never +the fast preview method. + +### 3.7 Nextcloud integration + +Mechanics below are verified against Nextcloud 34 documentation and server/desktop-client source. +Three findings shape this section and are recorded as constraints in ARCH §6.6–ARCH §6.8. + +**FR-NC-1 — Account setup.** Connect via **Login Flow v2**: `POST /index.php/login/v2` returns a +browser URL and a poll token; the app opens the URL in the *system browser* (never an embedded +webview) and polls `POST /login/v2/poll` until it returns an app password. The token is valid 20 +minutes and the success response is returned exactly once. The app never sees the user's primary +password. + +The `User-Agent` sent during the flow names the resulting app password in the user's security +settings, so it shall identify the device (e.g. `DarkRoom (Linux desktop)`), allowing per-device +revocation. Logout shall call `DELETE /ocs/v2.php/core/apppassword` to revoke cleanly. + +Manual app passwords are supported as a fallback for unusual server configurations. + +**FR-NC-2 — Credential storage.** Linux: Secret Service via libsecret. KDE exposes the same +interface through `ksecretd` since KF5.97, so one code path covers GNOME and KDE. Where no +secrets daemon is running, the app shall enter an explicit degraded mode rather than silently +storing credentials in plaintext. + +Android: Keystore-backed encryption. Note `EncryptedSharedPreferences` is deprecated; the current +approach is DataStore for persistence with Tink for encryption and Keystore for key protection. +Keys must not require user authentication, or background sync will fail. + +**FR-NC-3 — Remote browsing without full download.** The app shall display a remote library's +thumbnails without transferring full RAW files. Two mechanisms, selected per-account by capability +probe at setup: + +1. *Server previews* — `GET /core/preview?fileId=…` where available. **`forceIcon=false` is + mandatory**: the default returns a generic mimetype icon when the server cannot render the + file, which would otherwise be cached as though it were a thumbnail. The `nc:has-preview` + property in PROPFIND indicates per-file availability. +2. *Range-based embedded preview extraction* — the required fallback (see ARCH §6.7). Fetch the first + 64–256KB via HTTP `Range`, parse the container to locate the embedded JPEG preview, then fetch + exactly that byte range. Typical cost 1–3MB versus 25–100MB for the full file. + +**FR-NC-4 — Change detection.** Sync shall use recursive ETag pruning, matching the official +desktop client's discovery algorithm: + +1. `PROPFIND Depth: 0` on the sync root requesting `getetag`. If unchanged from the stored value, + nothing anywhere in the library has changed — sync completes in one request. +2. Where changed, `PROPFIND Depth: 1` and recurse only into child folders whose ETag differs. + +Cost is proportional to the changed subtree, not to library size. `Depth: infinity` shall not be +relied upon (frequently disabled or prohibitively expensive). ETags shall be normalised for quote +inconsistencies before comparison, or spurious full rescans result. + +**FR-NC-5 — Identity.** The catalog shall key remote files on Nextcloud's `oc:fileid`, which is +stable across renames and moves, so that a server-side move is detected as a move rather than as a +delete plus a re-download of a 100MB file. + +**FR-NC-6 — Selective download.** Downloads are on-demand and resumable, running in the +background. RAW files are never bulk-synced by default. On Android, transfers respect +unmetered-network and charging constraints. + +Three storage tiers: + +| Tier | Content | Policy | +|---|---|---| +| Metadata | Catalog rows, edit-graph sidecars | Always synced; kilobytes; sync even on metered connections | +| Previews | Embedded JPEGs or server previews | LRU-evicted, size-capped; what the grid browses | +| Full RAW | Source files | Explicit pin or on-demand open only | + +**FR-NC-6a — Cache rules.** The user shall be able to pin a *set* of images at a chosen tier, with +the set defined by a rule that the app re-evaluates as the catalog changes. Selectors shall include +at minimum: + +- **Collection** — "this trip is available offline" +- **Folder**, optionally recursive +- **Date range**, absolute or **rolling** ("the last 90 days", which moves with the clock) +- **Rating**, **colour label**, **flag**, or **keyword** — "every 5-star image, always" +- **Boolean composition** of the above + +A rolling window shall stay current without user intervention. Where rules disagree about an image, +the most generous tier wins. + +*Rationale:* selective sync is only usable if the selection can be expressed as intent rather than +enumerated by hand. "Keep this shoot and everything from the last three months" is a sentence a +photographer will say; selecting four thousand files individually is not. + +**FR-NC-6b — Lazy eviction.** An image that stops matching a rule is **not** deleted immediately; it +becomes the first candidate for eviction when the cache cap (NFR-RES-4) or platform memory pressure +(FR-PLAT-AND-5) actually requires space. Eviction order is unpinned originals by last use, then +proxies, then thumbnails. **Metadata and sidecars are never evicted** — they are authoritative +(ARCH §6.12) and small. + +**FR-NC-6c — Availability is visible.** Every image shall carry a visible availability state: +*Original*, *Preview*, *Metadata only*, or *Offline*. + +- An operation requiring absent data shall say so, with the transfer size, *before* starting +- Export from a preview-only image is **refused**, not silently degraded +- A pinned set reports its true byte cost before the user commits + +*Rationale:* Lightroom Classic syncs 2560px proxies while displaying the original's filename, +extension, and size, so users do not know what they actually have. Sync failures of legibility are +more damaging than failures of transport. + +**FR-NC-7 — Upload.** Files above 5MB use **chunked upload v2** against +`/remote.php/dav/uploads//`: `MKCOL` to create the upload folder, `PUT` each chunk, then +`MOVE` the `.file` pseudo-entry to the destination. Chunks are 5MB–5GB and named 1–10000. +`OC-Total-Length` shall always be sent so quota is checked up front rather than at assembly time. +Upload folders expire after 24h of inactivity; the app shall persist upload state and either +resume or `DELETE` stranded uploads on startup. + +Small files (sidecars) use **bulk upload** via `POST /remote.php/dav/bulk` with a +`multipart/related` body, allowing hundreds of edit-graph sidecars in a single request. + +**FR-NC-8 — Edit metadata sync.** Edit graphs sync bidirectionally as sidecars, one per image, +named deterministically from `oc:fileid`. Each sidecar carries a monotonic revision counter, a +per-device UUID, and a last-edit timestamp. + +A sidecar holds a **keyed set of Versions** (FR-CAT-12), not a single edit graph — one image may +carry several virtual copies, and a single-graph format could not represent them. Version identity +is part of the sidecar schema, so conflict merge (FR-NC-9) operates per-version. + +**FR-NC-9 — Conflict handling.** Sidecar updates use `If-Match` with the known ETag for optimistic +concurrency (`If-None-Match: *` for creates). On `412 Precondition Failed` the app shall fetch the +remote sidecar and **merge at the edit-graph node level** — disjoint edits (e.g. a crop on one +device, an exposure change on the other) both survive; genuinely conflicting nodes resolve by +timestamp — then retry with the new ETag under a bounded retry count. + +The app shall **not** replicate the desktop client's `(conflicted copy)` file behaviour. Sidecars +are structured data of a few KB; a read-merge-rewrite cycle is cheap and preserves user intent. +Only genuinely ambiguous merges surface to the UI. Source RAW files are write-once and shall never +generate a conflict. + +**FR-NC-10 — Offline-first.** The app is fully functional offline against cached content. Local +sidecar writes are atomic (temp file plus rename) and committed locally *before* any network +round-trip, so editing never blocks on connectivity. Sync resumes automatically when connectivity +returns. + +**FR-NC-11 — Initial catalog build.** For first sync of a large remote library, the app may use +WebDAV `SEARCH` (RFC 5323) against `/remote.php/dav/` filtered by mimetype and paginated via +`d:limit`/`d:nresults`, in preference to walking thousands of folders with PROPFIND. + +**FR-NC-12 — Backend independence.** Sync shall be implemented against a backend interface, with +Nextcloud as the only implementation in v1. No protocol detail specific to Nextcloud may appear +outside its connector. + +Backends **declare capabilities** rather than conforming to a lowest common denominator, because +the property that makes Nextcloud sync fast — directory ETags propagating up the tree, so an +unchanged root proves an unchanged library — is not a general guarantee. An interface built to the +common subset would force full enumeration on every sync (ARCH §8.1). + +Where a capability is absent the app shall **degrade visibly, not silently**: + +- Sync strategy in use is reportable to the user, so a slow backend is visibly slow +- Without byte-range reads, remote browsing cannot extract embedded previews; the app shall refuse + full downloads for browsing on a metered connection and explain why +- Without conditional writes, sidecar conflict detection falls back to revision comparison, which + narrows but does not close the race; this is surfaced as a reduced-safety mode + +### 3.8 Platform integration + +#### Android + +**FR-PLAT-AND-1 — Storage access.** Library access is obtained **exclusively via the Storage Access +Framework**: the user grants one or more document trees through `ACTION_OPEN_DOCUMENT_TREE`, +persisted with `takePersistableUriPermission` and enumerated via `DocumentsContract`. + +The app shall **not** request `MANAGE_EXTERNAL_STORAGE` and shall **not** depend on +`READ_MEDIA_IMAGES` for RAW discovery. See ARCH §6.9 for why neither is viable. + +**FR-PLAT-AND-2 — Permission loss.** Loss of a previously granted tree permission — revocation, +reinstall, removed SD card — shall be detected and surfaced, marking affected images offline per +FR-CAT-9 rather than deleting catalog rows. + +**FR-PLAT-AND-3 — Process lifecycle.** An Android process may be killed at any moment. Edit state +shall be durable such that process death loses at most the last uncommitted parameter change. On +resume the app restores the develop session, its image, and its viewport. + +**FR-PLAT-AND-4 — Background execution.** Long-running sync and export use the platform's managed +background execution with the constraints in FR-NC-6, and a foreground service with notification +for user-initiated exports. Behaviour under Doze and battery-saver is specified and tested. + +**FR-PLAT-AND-5 — Memory pressure.** The app shall respond to `onTrimMemory` / +`ComponentCallbacks2` by evicting caches per NFR-RES-1, in a stated eviction order (GPU tiles +first, then proxies, then thumbnails). + +**FR-PLAT-AND-6 — Intents.** Register as a receiver for image view and share intents, and provide +share-out of exported results via `FileProvider`. + +#### Linux + +**FR-PLAT-LIN-1 — Desktop integration.** Follow the XDG Base Directory specification for config, +data, cache, and state. Register MIME associations for supported RAW types and ship a `.desktop` +entry. + +**FR-PLAT-LIN-2 — Display server.** Support X11 and Wayland. Where Wayland's colour-management +protocol is unavailable, FR-DSP-8's stated fallback applies. + +**FR-PLAT-LIN-3 — Sandboxed distribution.** Where distributed as Flatpak, filesystem access uses +portals and credential storage uses the Secret Service portal, both verified to satisfy FR-NC-2 and +FR-CAT-1 within the sandbox. + +### 3.9 Culling + +Per D11 this is the product's primary differentiator, not an incidental capability. + +**The opportunity, stated plainly.** Photo Mechanic is fast because it displays the camera's +embedded JPEG — no demosaic, no database, no import step. FastRawViewer is *truthful* because it +shows a genuine raw histogram, raw-derived clipping, and focus peaking. **No shipping tool combines +both.** The culler/editor split exists only because Lightroom's culling is slow — it is a workaround +photographers tolerate, not a workflow they want. A tool that is genuinely Photo Mechanic-fast +eliminates the handoff rather than improving it. + +**FR-CULL-1 — Instant display.** Displaying the next image shall not wait on demosaic, catalog +import, or full decode. The embedded JPEG preview is shown immediately; higher-quality renders +replace it progressively. + +*Acceptance:* next-image display within **50 ms** of the input event, sustained across a +3,000-image folder, on both platforms. This is the single most important performance figure in the +document — Lightroom's ~2 s stall is the entire reason a competing product category exists. + +**FR-CULL-2 — Preview ladder.** Previews resolve through tiers, each falling through to the next: + +1. Embedded JPEG preview from the RAW container (instant) +2. Cached proxy from a previous visit +3. Background full decode, promoted when ready + +Some cameras embed previews below sensor resolution, and some embed none. The app shall **detect +this per camera model** and pre-emptively background-render where the embedded preview is +insufficient, rather than showing the user a soft image and letting them discover it at zoom. + +**FR-CULL-3 — Raw-truth overlays.** Culling decisions are made against raw data, not the embedded +JPEG: + +- **Raw histogram** — computed from sensor data, not the preview. The embedded JPEG's histogram + misrepresents available highlight headroom. +- **Raw-derived clipping indicators** — a JPEG's clipping warnings systematically lie about what is + recoverable in the raw. +- **Focus peaking** — overlays in-focus regions on the preview, so focus is verifiable **without + zooming to 100%**. This removes the largest single source of culling latency from the critical + path. + +Per D11, zoom-to-100% remains available for certainty; peaking makes it optional rather than +mandatory. + +**FR-CULL-4 — Culling mode.** A dedicated full-screen mode with: + +- **Auto-advance** on judgement — users independently reinvent this in darktable, Lightroom desktop, + and Lightroom mobile, which is strong evidence it should be the default rather than an option +- **One-key reject**, plus the full rating, flag, and colour-label axes +- **Filter to unjudged**, so a session resumes where it stopped +- Keyboard-driven on desktop; single-thumb reachable on tablet + +**FR-CULL-5 — Burst and near-duplicate grouping.** Group frames by capture-time proximity and image +similarity, allowing a burst to collapse to one representative and be judged as a unit. + +This is the one automated capability photographers consistently praise, precisely because it is a +mechanical grouping problem rather than a taste judgement. Automated *selection* is distrusted — the +documented failure is rejecting the only frame of an important moment because someone blinked. + +**FR-CULL-6 — Comparison.** Side-by-side and survey comparison of a selection, with synchronised +zoom and pan, for choosing among near-identical frames. + +**FR-CULL-7 — Tablet culling.** Culling shall be fully usable on tablet, as the validated +multi-device workflow (§3.5, D11). Requires only ratings and small proxies to sync, not full +originals — a substantially smaller sync problem than develop parity. + +*Design note:* pinch-zoom accidentally triggering ratings is a documented defect in Lightroom +mobile. Gesture and rating targets must not overlap. + +--- + +## 4. Non-functional requirements + +### 4.1 Performance targets + +These are targets to design against and measure, on the reference desktop +(AMD Threadripper 2920X, 24 threads, discrete GPU) and a mid-range Android device. + +| ID | Metric | Desktop target | Android target | +|---|---|---|---| +| **NFR-P1** | Catalog open (50k images) | < 2 s | < 4 s | +| **NFR-P2** | Grid scroll | Sustained 60 fps | Sustained 60 fps | +| **NFR-P3** | Thumbnail generation throughput | ≥ 100 img/s (embedded preview path) | ≥ 25 img/s | +| **NFR-P4** | Open image in develop (to first proxy on screen) | < 400 ms | < 1000 ms | +| **NFR-P5** | Slider adjustment → visible update | < 16 ms (one frame) | < 33 ms | +| **NFR-P6** | Pan/zoom responsiveness | No dropped frames at 60 fps | No dropped frames | +| **NFR-P7** | Full-resolution export (24MP, full chain) | < 2 s | < 8 s | +| **NFR-P8** | Idle memory (50k catalog, nothing open) | < 500 MB | < 250 MB | +| **NFR-P9** | **UI-executor** blocking (not "any operation") | Never > 16 ms | Never > 16 ms | +| **NFR-P13** | **Next image in culling mode** (FR-CULL-1) | **< 50 ms** | **< 50 ms** | +| **NFR-P14** | Focus peaking overlay ready | < 100 ms after preview | < 150 ms | +| **NFR-P15** | Drawn mask stroke → visible (ARCH §6.11) | < 16 ms, no cursor lag | < 16 ms | +| **NFR-P10** | Touch gesture → visual response | < 16 ms | < 16 ms | +| **NFR-P11** | Layout class transition (window resize) | No dropped frames, no state loss | n/a | +| **NFR-P12** | Warm-start shader pipeline setup (cached) | < 100 ms | < 100 ms | + +Every target above requires a stated measurement method, workload, and pass threshold before it is +testable. NFR-P8 in particular must state whether it measures RSS inclusive or exclusive of GPU +allocations, and whether it holds after SQLite's page cache warms on a 50k catalog. + +**Performance regressions fail the build.** §9's benchmark suite runs per-commit; a regression +beyond a stated tolerance is a build failure, not a notification. Performance work rots otherwise. + +### 4.2 Reliability + +**NFR-R1** — No operation shall lose user edit data. The catalog database uses write-ahead +logging and survives power loss without corruption. + +**NFR-R2** — The catalog is backed up automatically on a schedule and before schema migrations. + +**NFR-R3** — A crash in decode or GPU work shall not take down the application where it can be +isolated; the affected image is marked as failed and the app continues. + +**NFR-R4** — Source image files are strictly read-only to the application, except where the user +explicitly requests a destructive operation (e.g. delete, or writing XMP sidecars). + +**NFR-R5 — Schema versioning.** The catalog carries a monotonic schema version. Migrations are +forward-only, transactional, and idempotent on retry. Each migration ships with a test that migrates +a fixture catalog from **every** prior released version. The app refuses to open a catalog with a +newer schema version rather than corrupting it, and says so. + +ARCH §6.6 already anticipates one migration (folder ETags); there will be others, and the machinery must +exist before the first one. + +**NFR-R6 — Corruption recovery.** On failing an integrity check at startup, the app offers restore +from the NFR-R2 backup, and failing that, rebuild from sources plus sidecars per invariant 5.2.4. +FR-CAT-8 is what makes that second path real for local-only users. + +**NFR-R7 — GPU device loss.** The GPU layer treats device loss as an expected event (ARCH §6.10): detect +it, tear down and recreate the device and all derived resources, and re-drive the current render +from the edit graph. **No user edit is lost.** Recovery is exercised by a test that induces device +loss mid-render. + +**NFR-R8 — No suitable GPU.** Where no Vulkan device meeting the NFR-COMPAT-1 baseline is +available, the app starts in a stated degraded mode with defined capability limits rather than +failing to launch. + +*This requires reconciling ARCH §6.4 and NFR-RES-2:* ARCH §6.4 says the GPU path is primary rather than an +optimisation, while NFR-RES-2 assumes a CPU fallback on allocation failure. **Decide explicitly** +whether v1 includes a full CPU pipeline, or whether "CPU fallback" means only tile-spill staging +with no independent CPU render path. The latter is recommended; the former is a second full +implementation. + +### 4.3 Resource behaviour + +**NFR-RES-1 — Bounded memory.** Memory use is bounded and configurable, independent of catalog +size and image count. Caches are evictable under pressure. + +**NFR-RES-2 — GPU memory.** The pipeline shall handle images larger than available GPU memory by +tiling. GPU memory headroom is configurable, with a CPU fallback path if allocation fails. + +**NFR-RES-3 — Mobile power.** On Android the app shall not render continuously when idle. Battery +and thermal behaviour are first-class concerns; background sync respects metered-connection and +battery-saver settings. + +**NFR-RES-4 — Disk cache.** Thumbnail and proxy caches have a configurable size cap with LRU +eviction. + +### 4.4 Portability + +**NFR-PORT-1** — Platform-specific code is isolated behind interfaces. The image core, catalog, +and edit pipeline contain no platform conditionals. + +**NFR-PORT-2** — GPU shaders are authored once and used on both platforms. + +**NFR-PORT-3** — Adding a third platform requires implementing the platform interfaces only, not +changes to the core. + +### 4.5 Security and privacy + +**NFR-SEC-1** — RAW parsing treats input as untrusted. Parser hardening, fuzzing, and where +practical process or memory isolation for decode. + +**NFR-SEC-2** — Credentials are never written to the catalog, logs, or plain files. Platform +secure storage only. + +**NFR-SEC-3** — All network traffic uses TLS with certificate validation. No option to disable +validation in release builds. + +**NFR-SEC-4** — No telemetry without explicit opt-in. + +### 4.6 Execution model + +**NFR-ARCH-1 — Named executors.** The app defines distinct executors — UI, GPU submission, decode +pool, I/O pool, network — with stated thread counts and the invariant that **no blocking call +occurs on the UI executor**. This is the mechanism behind R4 and NFR-P9, which currently assert an +outcome with no stated means. + +**NFR-ARCH-2 — Scheduler priority.** The tiling scheduler assigns priority classes, with +visible-tile work **strictly preempting** background export and thumbnail work. Without this, +NFR-P5's slider latency fails during a batch export — the common case, not an edge case. + +**NFR-ARCH-3 — Cancellation.** Cancellation is cooperative with a bounded worst-case latency +(target: observed within 100 ms), and covers in-flight GPU submissions. Every long-running +operation named in FR-CAT-1, FR-EXP-7, and FR-NC-6 is cancellable under this model. + +**NFR-ARCH-4 — Error propagation.** No worker error may panic the process. Errors surface as typed +results attached to the affected image or job, consistent with NFR-R3 and FR-RAW-4. + +### 4.7 Operations + +**NFR-OPS-1 — Diagnostics.** Structured levelled logging to a rotating, size-capped on-disk log in +the XDG state directory or Android app directory, with **automatic redaction of credentials and +tokens** (required by NFR-SEC-2, which currently forbids credentials in logs that are never +otherwise specified). A one-click diagnostics bundle includes log, schema version, GPU and driver +identification, and app version — with an explicit preview-and-consent step before anything leaves +the device. + +**NFR-OPS-2 — Crash reporting.** Local crash capture always; upload only on explicit opt-in +(NFR-SEC-4). + +**NFR-OPS-3 — Preferences.** A single versioned preferences store, **separate from the catalog**, so +preferences survive catalog rebuild and multiple catalogs. At least eight requirements refer to +configurable settings with no store defined. Device-specific settings (GPU headroom, cache caps) do +not sync between devices. + +**NFR-OPS-4 — Update and first run.** State delivery channels and their update mechanisms. This +matters concretely because D2 pins rawler at an alpha, non-SemVer version whose camera-support fixes +users will need. First-run flow is defined, including platform permission acquisition (FR-PLAT-AND-1 +makes first run a permission negotiation on Android, not a welcome screen) and initial root +selection. + +### 4.8 Compatibility baseline + +**NFR-COMPAT-1 — Supported hardware.** Every §4.1 Android figure is meaningless without this. State: + +- Minimum and target Android API level (targetSdk 36 is currently required for Play distribution) +- Minimum Vulkan version and the required feature and limit set — including whether `shaderFloat16` + and 16-bit storage are required, since **FR-DEV-2's f16 pipeline depends on them and their + absence would jeopardise R1** +- Minimum device RAM, and minimum desktop Vulkan/Mesa versions +- The **specific** reference Android device the §4.1 column is measured on, plus a secondary device + from a different GPU vendor + +Adreno, Mali, and PowerVR diverge significantly in compute behaviour and in external-memory interop +— exactly what spike S1 tests. The spec already applies this reasoning to desktop drivers; it +applies at least as strongly on Android. + +**NFR-COMPAT-2 — Distribution channels.** State the v1 channels (e.g. Flatpak and AppImage on Linux; +Play Store and/or F-Droid on Android). Play distribution is what makes ARCH §6.9's constraints binding — +a sideloaded or F-Droid build could use different permissions, so the channel decision and the +storage design are coupled. + +### 4.9 Accessibility and internationalisation + +**NFR-A11Y-1 — Localisation.** All user-facing strings, including operation and parameter labels +resolved from `LocalizedString` (FR-DEV-3a), are externalised and translatable without +recompilation. Note the constraint this creates: those labels live in core crates that **cannot +depend on the UI** (ARCH §6.5a), so the localisation mechanism must itself be UI-independent. State the +format, the locale-resolution rule, and whether RTL layout is in v1 scope. + +**NFR-A11Y-2 — Accessibility.** Controls expose accessible names, roles, and values to the platform +accessibility layer (AT-SPI on Linux, TalkBack on Android). Platform font scaling is honoured +without clipping. Non-canvas UI meets WCAG AA contrast. + +**Slint's accessibility support on Android requires verification** — this may be a toolkit gap, and +it is far cheaper to discover now than after the UI is built. + +**NFR-A11Y-3 — Colour-independent status.** No status is conveyed by hue alone. Colour labels, +clipping indicators (FR-DSP-7), and the HSL mixer carry a shape or text affordance. This matters +more in a colour-grading application than in most software. + +--- + +## 5. Data model and architecture + +Entity definitions, invariants, and all architectural constraints are specified in +[architecture.md](architecture.md) — §3 (core abstractions), §6 (data architecture), and +§11 (architectural constraints). + +Requirements in this document that depend on an architectural guarantee cite it inline. The +constraints most load-bearing for testability are: + +| Constraint | Why a requirement depends on it | +|---|---| +| ARCH §6.1 — no CPU round-trip | FR-DSP-3, FR-DSP-7, NFR-P5 are unachievable without it | +| ARCH §6.11 — GPU-rasterised masks | NFR-P15 (no brush lag) | +| ARCH §6.12 — sidecars authoritative | NFR-R6, invariant behind FR-CAT-8 | +| ARCH §6.9 — Android SAF only | FR-CAT-1a, FR-PLAT-AND-1, and the NFR-P1/P3 Android figures | +| ARCH §6.6 — no sync tokens | FR-NC-4 | +| ARCH §6.13 — integer-only bit-identity | R1's tolerance, §9 golden images | + +## 6. Decisions + +Rationale, evidence, and the eliminated alternatives are recorded in +[architecture.md §12](architecture.md). Outcomes only: + +| # | Decision | Outcome | +|---|---|---| +| D1 | Language and UI framework | Rust + Slint, rendering through wgpu | +| D2 | RAW decoder | rawler; LibRaw fallback behind a trait | +| D3 | First milestone | **Provisional — depends on D12** | +| D4 | Nextcloud sync mechanism | ETag pruning, chunked upload v2, Login Flow v2 | +| D5 | Colour management | lcms2 + GPU-side matrix/LUT transforms | +| D6 | Shader authoring | Hand-written WGSL | +| D7 | Network stack | reqwest + quick-xml | +| D8 | Licence | **GPLv3** | +| D9 | Operation UI model | Declarative parameter descriptors | +| D10 | Interface strategy | One adaptive UI, tablet + desktop | +| D11 | Product positioning | Culling-first differentiator; see below | +| D12 | Scope versus pace | **OPEN** | + +### D11 — product positioning + +Settled by requirements calibration, 2026-08-08. + +| Dimension | Decision | +|---|---| +| Audience | RAW-literate photographers, Linux-comfortable. Docs matter; hand-holding does not. | +| Library scale | 10k–50k images | +| Culling | **The core differentiator** (§3.9) | +| Focus checking | Peaking *and* zoom | +| Ingest | Full workflow — template rename, checksum verify, dual-destination | +| Colour defaults | Good, not obsessive — matrices plus per-body base curve | +| Film simulation | Fujifilm explicitly targeted | +| AI | Denoise in v1; masking deferred | +| Local adjustments | Full masking, GPU-rasterised | +| Sync | The reason the project exists | +| Durability | Sidecar-first | +| Licence | GPLv3 | +| Pace | Evenings and weekends, indefinite | + +### D12 — scope versus pace · **OPEN** + +The calibration selected an ambitious feature set — full tablet editing, full ingest, culling as a +differentiator, complete GPU masking, AI denoise, Fuji-first colour, deep sync, sidecar durability +— against a stated pace of evenings and weekends, indefinitely. + +Those are not compatible as stated. This is not an argument against any individual choice; it is +that the two must be reconciled before a build order can be set. + +Two specific tensions: + +**1. Full tablet editing is the most expensive selection**, chosen against the research finding +that volume photographers do not edit on tablets. It carries SAF storage at unproven scale (S10), +process-death durability, background execution limits, and two GPU vendors to validate. Tablet +*culling* is the validated workflow, is what the differentiator points at, and costs a fraction as +much. + +**2. The v1 milestone and the differentiator disagree.** D3's vertical slice proves the +architecture but is useful to nobody. If culling is what no existing tool does well, a culler is +both a smaller build and a usable one — it needs no develop chain. + +Resolving D12 sets D3 and [architecture.md §10](architecture.md)'s Phase 2. + +--- + +## 7. Out of scope for v1 + +Deferred deliberately. Listed so their absence reads as a decision rather than an oversight, with a +note where deferring now constrains the design later. + +| Deferred | Note | +|---|---| +| Tethered shooting | — | +| Panorama and HDR merge | **Keep the schema open** — these produce images derived from multiple sources, which §5.1's single-source `Image` cannot express. | +| Focus stacking | Same provenance consideration. | +| Print layout | — | +| Soft proofing | Parameterise the output colour stage by an arbitrary profile so this becomes a UI addition, not a pipeline change. FR-EXP-3's print-dimension mode already half-commits to print workflows. | +| Face recognition | — | +| AI subject masking | Deferred per D11. Note darktable shipped this in 5.6 (June 2026), so the gap is now visible. **Conditions for deferring safely:** AI denoise ships in v1 (FR-DEV-3g ✓), manual masking is excellent including GPU-rasterised drawn masks (ARCH §6.11 ✓), and the product has a clear differentiator (culling, §3.9 ✓). When it does land, copy darktable's shape — prompt-point segmentation producing an *editable* mask that behaves like a hand-drawn one — not Adobe's opaque version. | +| AI upscaling | Deferred. Lower priority than denoise, which has no manual fallback. | +| Video | — | +| Plugin API | — | +| Multi-user / server-side catalog | — | +| Watermarking | Cheap *if* the export pipeline anticipates a compositing stage; expensive to retrofit otherwise. Consider reserving the stage now. | +| Multiple catalogs, catalog merge | Interacts with NFR-OPS-3: preferences must not live in the catalog. | +| Geotagging and map view | — | +| Web gallery, slideshow | — | +| DNG conversion | — | + +--- + +## 8. Verification approach + +| Requirement class | How verified | +|---|---| +| Performance (§4.1) | Automated benchmark suite against a synthetic 50k catalog, run per-commit on the reference desktop and periodically on the named reference Android devices. **A regression beyond stated tolerance fails the build.** | +| Rendering correctness | Golden-image tests: fixed source + fixed edit graph → comparison **within R1's stated tolerance**, not checksum equality. Run on both platforms and both Android GPU vendors. | +| Colour accuracy (FR-DEV-3e) | ColorChecker exposures per launch body, asserting ΔE2000 within threshold against reference values. | +| RAW decode coverage | Corpus of sample files per supported camera body; decode-and-checksum regression suite. | +| Robustness (NFR-SEC-1) | Continuous fuzzing of the decode path. | +| Sync correctness | Simulated two-device scenarios including conflict, offline edit, and interrupted transfer. | +| Memory bounds | Long-running soak test scrolling a large catalog, asserting bounded RSS and GPU memory. | +| Schema migration (NFR-R5) | Fixture catalogs from every prior released version, migrated forward and verified. | +| Device loss (NFR-R7) | Induced `VK_ERROR_DEVICE_LOST` mid-render; assert recovery with no lost edits. | +| Process death (FR-PLAT-AND-3) | Kill the Android process mid-edit; assert session and viewport restore with at most the last uncommitted change lost. | +| Source relocation (FR-CAT-9) | Move, rename, and disconnect sources; assert offline marking, reconnection by hash, and no catalog row loss. | +| Cancellation (NFR-ARCH-3) | Assert every long-running operation observes cancellation within the stated bound, including in-flight GPU work. | +| Layer separation (ARCH §6.5a) | CI dependency-tree assertion: no `core/*` crate may transitively depend on a UI toolkit. | +| Operation self-description (FR-DEV-3c) | A test operation added to the registry appears in a generated panel with no frontend change. | +| Adaptive layout (§3.5) | Snapshot tests at each breakpoint, and a resize test asserting no state loss across a layout-class transition. | +| Touch targets (FR-UI-3) | Automated check that interactive elements meet the 44pt minimum in touch modality. | +| Export sizing (FR-EXP-3) | Per-mode dimension assertions, including aspect preservation, fill-crop centring, and the upscale-disabled fallback. | + +--- + +## 9. Validation spikes + +Small experiments that de-risk the highest-uncertainty assumptions before substantial build work. +Ordered by risk. With D1 settled these validate the chosen stack rather than choosing between +stacks. + +### Tier 1 — before any substantial build work + +| # | Spike | Answers | Relates to | +|---|---|---|---| +| **S1** | **Slint + wgpu zero-copy on Linux:** a compute shader writes a texture, `create_texture_from_hal` imports it, Slint composites UI over it. Drag a slider for 10 minutes watching for tearing, leaks, and sync bugs | Whether ARCH §6.1 holds in the chosen stack | D1, ARCH §6.1 | +| **S2** | **Slint + wgpu on Android**, on two devices from **different GPU vendors** (Adreno and Mali) | Whether the Android GPU path holds across vendor divergence | D1 | +| **S10** | **Android SAF at scale:** enumerate a 10k-file document tree and perform random-access range reads over a document fd. Measure against NFR-P1 and NFR-P3 | Whether ARCH §6.9's forced storage model meets the stated Android performance targets | ARCH §6.9, FR-PLAT-AND-1 | +| **S11** | **Play Console permissions dry-run:** submit an actual declaration for this app category before committing to the storage design | Whether Google approves anything beyond SAF | ARCH §6.9 | +| **S9** | **Golden-image comparison** of one edit graph rendered on desktop and Android; **calibrate the achievable tolerance** | What R1's tolerance threshold should actually be | R1 | + +### Tier 2 — before the corresponding subsystem is built + +| # | Spike | Answers | Relates to | +|---|---|---|---| +| **S3** | **reqwest HTTPS PROPFIND on a real Android device**, including the `rustls-platform-verifier` Kotlin init | The largest known Rust-on-Android networking risk | D7 | +| **S4** | **Range-extract an embedded JPEG** from CR3/NEF/ARW over WebDAV; measure bytes transferred | Whether remote browsing on mobile data is viable | ARCH §6.7, FR-NC-3 | +| **S5** | **ETag pruning against a 10k-file library:** confirm one-request no-op sync, and correct propagation on a single deep-file change | Whether FR-NC-4 scales as designed | ARCH §6.6 | +| **S6** | **Tiled GPU pipeline on a mid-range Android device**, with an image larger than available GPU memory | Whether ARCH §6.2 holds on constrained hardware | NFR-RES-2 | +| **S7** | **rawler decode coverage** across the FR-RAW-1 launch set, on real files from each body | Whether the LibRaw fallback is needed at launch or later | D2, FR-RAW-1 | +| **S8** | **Chunked upload v2** round-trip of a 100MB RAW, including resume after process kill | FR-NC-7 correctness | FR-NC-7 | +| **S12** | **GPU device loss recovery:** induce `VK_ERROR_DEVICE_LOST` mid-render, verify recreation from the edit graph with no lost edits | Whether ARCH §6.10 and NFR-R7 hold | ARCH §6.10 | +| **S13** | **Slint accessibility on Android:** verify TalkBack exposure of names, roles, and values | Whether NFR-A11Y-2 is achievable in the chosen toolkit | NFR-A11Y-2 | + +### Why this order + +**S1, S2, and S10 are the three that can invalidate the architecture.** S1 and S2 test ARCH §6.1 — the +constraint the whole design is built around, and the one darktable's documentation identifies as +their biggest bottleneck. S10 tests whether the Android storage model forced by ARCH §6.9 can actually +meet the performance targets; it is the highest-uncertainty assumption in the document because until +this revision it was unstated. + +**S11 costs almost nothing and de-risks S10 definitively.** Confirming what Google will approve for +this app category before designing around it is far cheaper than discovering it at submission. + +**S9 moved to Tier 1** because it does not merely test R1 — it *calibrates* it. R1's tolerance +threshold cannot be fixed sensibly without knowing the real cross-vendor deviation, and the §9 +golden-image strategy depends on that number. + +Test S1 on Mesa/AMD, Intel, and NVIDIA proprietary drivers, under both X11 and Wayland. FD-based +external memory has well-documented driver divergence, and the reference machine's discrete GPU will +not surface Intel or Mesa-specific issues on its own. The same reasoning is why S2 requires two +Android GPU vendors. + +--- + +## 10. Glossary + +- **Edit graph** — the ordered set of parameterised operations defining how an image is rendered. +- **Proxy** — a reduced-resolution render used for display. +- **Tile** — a sub-rectangle of an image processed independently. +- **Demosaic** — reconstructing full RGB from a colour-filter-array sensor capture. +- **CFA** — colour filter array (Bayer, X-Trans). +- **Sidecar** — a small file alongside the source holding edit metadata. +- **Pixel pipeline** — the ordered chain of processing stages from sensor data to output. diff --git a/ui/dr-ui/Cargo.toml b/ui/dr-ui/Cargo.toml new file mode 100644 index 0000000..1dc617f --- /dev/null +++ b/ui/dr-ui/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dr-ui" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +dr-types.workspace = true +dr-gpu.workspace = true +slint = { workspace = true, features = ["compat-1-2", "renderer-femtovg", "backend-winit"] } +wgpu.workspace = true +anyhow.workspace = true +log.workspace = true +pollster.workspace = true + +[build-dependencies] +slint-build.workspace = true + +[features] +default = [] +# Temporary CPU readback path; see dr-ui docs and spike S1. +readback = ["dr-gpu/readback"] diff --git a/ui/dr-ui/build.rs b/ui/dr-ui/build.rs new file mode 100644 index 0000000..0017917 --- /dev/null +++ b/ui/dr-ui/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("ui/app.slint").expect("compiling app.slint"); +} diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs new file mode 100644 index 0000000..202bc90 --- /dev/null +++ b/ui/dr-ui/src/lib.rs @@ -0,0 +1,248 @@ +//! Slint interface for DarkRoom. +//! +//! v0.1 exists to validate assumption A1: compute output reaching the screen +//! without a CPU round-trip (ARCH §6.1). +//! +//! **Current state — read this before assuming A1 is proven.** Slint's public +//! API for adopting an externally-created wgpu texture is not yet wired up +//! here; this build uploads through `SharedPixelBuffer`, which *is* a CPU +//! round-trip. It is correct and cross-platform, but it is explicitly the +//! thing the architecture forbids in production. +//! +//! Spike S1 replaces this with the zero-copy path. Until it does, `A1` is +//! unvalidated and the `readback` feature makes the temporary path visible +//! rather than silent. + +use std::cell::RefCell; +use std::rc::Rc; +use std::time::Instant; + +use anyhow::Result; +use dr_gpu::{GpuContext, RenderTarget}; + +slint::include_modules!(); + +/// Frame timing, averaged over a short window so the readout is stable enough +/// to read. +struct FrameClock { + last: Instant, + accum: f32, + frames: u32, + fps: i32, + start: Instant, +} + +impl FrameClock { + fn new() -> Self { + let now = Instant::now(); + Self { + last: now, + accum: 0.0, + frames: 0, + fps: 0, + start: now, + } + } + + /// Advance one frame; returns elapsed seconds since start. + fn tick(&mut self) -> f32 { + let now = Instant::now(); + let dt = now.duration_since(self.last).as_secs_f32(); + self.last = now; + + self.accum += dt; + self.frames += 1; + if self.accum >= 0.5 { + self.fps = (self.frames as f32 / self.accum).round() as i32; + self.accum = 0.0; + self.frames = 0; + } + + now.duration_since(self.start).as_secs_f32() + } +} + +/// Build and run the application window. +pub fn run() -> Result<()> { + let ctx = pollster::block_on(GpuContext::new_headless())?; + log::info!("adapter: {} ({:?})", ctx.adapter_name(), ctx.backend()); + + let window = AppWindow::new()?; + window.set_adapter(ctx.adapter_name().into()); + window.set_backend(format!("{:?}", ctx.backend()).to_uppercase().into()); + + let target = Rc::new(RefCell::new(RenderTarget::new(&ctx, 1280, 720)?)); + let clock = Rc::new(RefCell::new(FrameClock::new())); + // Desired canvas size, applied once per frame rather than per resize + // event. A window drag emits dozens of events a second, and each one + // would otherwise reallocate the texture. + let pending_size = Rc::new(std::cell::Cell::new((1280u32, 720u32))); + + // Resize the render target when the canvas area changes. Slint delivers + // this per-dimension, so both callbacks land on the same handler. + { + let pending = pending_size.clone(); + window.on_canvas_resized(move |w, h| { + if w > 0 && h > 0 { + pending.set((w as u32, h as u32)); + } + }); + } + + // FR-UI-1: layout class from window width. Computed here rather than in + // Slint because a property that both derives from and feeds the layout is + // a binding loop. + { + let weak = window.as_weak(); + window.on_window_resized(move |width| { + let Some(window) = weak.upgrade() else { return }; + apply_layout_class(&window, width); + }); + } + // Seed from the initial window size; `window-resized` maintains it after. + { + let size = window.window().size(); + let scale = window.window().scale_factor().max(0.01); + apply_layout_class(&window, size.width as f32 / scale); + } + + // Drive rendering from a timer rather than a redraw hook: v0.1 animates + // continuously to make a stalled frame obvious. Real rendering is + // event-driven (NFR-RES-3 forbids continuous redraw when idle). + let timer = slint::Timer::default(); + { + let weak = window.as_weak(); + let target = target.clone(); + let clock = clock.clone(); + let pending_size = pending_size.clone(); + + timer.start( + slint::TimerMode::Repeated, + std::time::Duration::from_millis(16), + move || { + let Some(window) = weak.upgrade() else { return }; + + let elapsed = clock.borrow_mut().tick(); + + // Apply at most one resize per frame, and cap the render + // resolution. FR-DSP-1 renders at what the viewport needs, + // not at whatever size the window happens to be — on a large + // display an uncapped canvas costs far more than it shows. + { + let (w, h) = pending_size.get(); + let (w, h) = clamp_render_size(w, h); + let mut t = target.borrow_mut(); + if t.size() != (w, h) { + t.resize(w, h); + } + } + + let target = target.borrow(); + target.render(elapsed); + + match to_slint_image(&target) { + Ok(img) => window.set_canvas(img), + Err(e) => log::error!("frame failed: {e}"), + } + let fps = clock.borrow().fps; + window.set_fps(fps); + if std::env::var_os("DR_LOG_FPS").is_some() && fps > 0 { + log::info!("frame: {fps} fps, canvas {:?}", target.size()); + } + }, + ); + } + + window.run()?; + Ok(()) +} + +/// Upper bound on render resolution. +/// +/// FR-DSP-1: the display pipeline works at the resolution the viewport needs, +/// not the source resolution. The same reasoning applies to the window — a +/// maximised 4K canvas costs 4× a 1080p one for detail nobody is looking at +/// while dragging. Real zoom-to-1:1 will render the visible crop at full +/// resolution instead of scaling the whole canvas up. +const MAX_RENDER_DIM: u32 = 2048; + +fn clamp_render_size(w: u32, h: u32) -> (u32, u32) { + let w = w.max(1); + let h = h.max(1); + let longest = w.max(h); + if longest <= MAX_RENDER_DIM { + return (w, h); + } + let scale = MAX_RENDER_DIM as f32 / longest as f32; + ( + ((w as f32 * scale).round() as u32).max(1), + ((h as f32 * scale).round() as u32).max(1), + ) +} + +/// Width at which the expanded layout appears (FR-UI-1). +/// +/// A threshold in logical pixels, not a device check — a narrow desktop window +/// gets the compact layout exactly as a tablet in portrait would. +const EXPANDED_MIN_WIDTH: f32 = 820.0; + +fn apply_layout_class(window: &AppWindow, width: f32) { + let expanded = width >= EXPANDED_MIN_WIDTH; + window.set_expanded(expanded); + window.set_layout_class(if expanded { "expanded" } else { "compact" }.into()); +} + +/// Convert the render target into something Slint can display. +/// +/// **This is the temporary path.** It reads pixels back to the CPU, which +/// ARCH §6.1 forbids in production. Spike S1 replaces it with texture +/// adoption; until then this keeps the app runnable on both platforms so the +/// rest of the shell can be built. +#[cfg(feature = "readback")] +fn to_slint_image(target: &RenderTarget) -> Result { + use slint::{Rgba8Pixel, SharedPixelBuffer}; + + let (w, h) = target.size(); + let pixels = pollster::block_on(target.read_pixels())?; + let buffer = SharedPixelBuffer::::clone_from_slice(&pixels, w, h); + Ok(slint::Image::from_rgba8(buffer)) +} + +#[cfg(not(feature = "readback"))] +fn to_slint_image(_target: &RenderTarget) -> Result { + anyhow::bail!( + "zero-copy texture adoption is not implemented yet (spike S1). \ + Build with --features readback for the temporary CPU path." + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn render_size_is_capped_preserving_aspect() { + // Under the cap, untouched. + assert_eq!(clamp_render_size(1600, 900), (1600, 900)); + + // Over the cap, scaled down with aspect preserved. + let (w, h) = clamp_render_size(3840, 2160); + assert_eq!(w, MAX_RENDER_DIM); + assert!((h as f32 - 1152.0).abs() < 2.0, "got {h}"); + + // Degenerate sizes never produce a zero dimension. + assert_eq!(clamp_render_size(0, 0), (1, 1)); + let (w, h) = clamp_render_size(4000, 1); + assert_eq!(w, MAX_RENDER_DIM); + assert!(h >= 1); + } + + #[test] + fn frame_clock_reports_after_window() { + let mut c = FrameClock::new(); + // Before half a second elapses there is no average to report. + assert_eq!(c.fps, 0); + let t = c.tick(); + assert!(t >= 0.0); + } +} diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint new file mode 100644 index 0000000..aec47fc --- /dev/null +++ b/ui/dr-ui/ui/app.slint @@ -0,0 +1,166 @@ +import { Theme } from "theme.slint"; + +// Status strip — surfaces the GPU backend and adapter, which matters during +// v0.1 because assumption A1 is exactly "does this compositing path work on +// this hardware". Seeing the adapter at a glance makes vendor differences +// obvious during the S1/S2 spikes. +component StatusBar inherits Rectangle { + in property adapter; + in property backend; + in property layout-class; + in property fps; + + height: 28px; + background: Theme.surface; + + HorizontalLayout { + padding-left: Theme.gap; + padding-right: Theme.gap; + spacing: Theme.gap; + alignment: start; + + Text { + text: root.backend; + color: Theme.accent; + font-size: Theme.text-sm; + font-weight: 700; + vertical-alignment: center; + } + + Text { + text: root.adapter; + color: Theme.ink-dim; + font-size: Theme.text-sm; + vertical-alignment: center; + overflow: elide; + } + + Rectangle { horizontal-stretch: 1; } + + Text { + text: root.layout-class; + color: Theme.ink-faint; + font-size: Theme.text-sm; + vertical-alignment: center; + } + + Text { + text: root.fps + " fps"; + color: root.fps >= 55 ? Theme.ink-dim : Theme.accent; + font-size: Theme.text-sm; + vertical-alignment: center; + } + } + + Rectangle { + y: parent.height - 1px; + height: 1px; + background: Theme.rule; + } +} + +// A placeholder panel standing in for the adjustment controls that FR-DEV-3a +// will generate from operation descriptors. +component SidePanel inherits Rectangle { + background: Theme.surface; + + VerticalLayout { + padding: Theme.gap; + spacing: Theme.gap; + alignment: start; + + Text { + text: "DEVELOP"; + color: Theme.accent; + font-size: Theme.text-sm; + font-weight: 700; + letter-spacing: 1.2px; + } + + Text { + text: "Controls are generated from operation\ndescriptors in v0.2 (FR-DEV-3a)."; + color: Theme.ink-faint; + font-size: Theme.text-sm; + wrap: word-wrap; + } + } + + Rectangle { + width: 1px; + background: Theme.rule; + } +} + +export component AppWindow inherits Window { + title: "DarkRoom"; + background: Theme.ground; + preferred-width: 1100px; + preferred-height: 720px; + min-width: 360px; + min-height: 320px; + + // Set from Rust each frame: the compute output, delivered as a texture. + in property canvas; + in property adapter: "detecting…"; + in property backend: "—"; + in property fps: 0; + + // FR-UI-1: layout class follows window width, not device type. A narrow + // desktop window gets the compact layout, exactly as a tablet would. + // + // Set from Rust rather than derived from `root.width` here: a property + // read inside the layout and also feeding it creates a binding loop, + // which Slint warns about and which can panic at runtime. + in property expanded: true; + in property layout-class: "expanded"; + + callback canvas-resized(int, int); + callback window-resized(length); + + // One-way: report width outward, never read layout back into it. + changed width => { root.window-resized(self.width); } + + VerticalLayout { + StatusBar { + adapter: root.adapter; + backend: root.backend; + layout-class: root.layout-class; + fps: root.fps; + } + + HorizontalLayout { + // The canvas: compute output composited directly. No CPU + // round-trip anywhere in this path (ARCH §6.1). + canvas-area := Rectangle { + horizontal-stretch: 1; + background: Theme.ground; + clip: true; + + Image { + width: 100%; + height: 100%; + source: root.canvas; + image-fit: contain; + } + + // Report size changes so the render target can be resized to + // match. Width and height are tracked separately because + // Slint has no single "geometry changed" hook. + property px-w: Math.round(self.width / 1px); + property px-h: Math.round(self.height / 1px); + changed px-w => { root.canvas-resized(self.px-w, self.px-h); } + changed px-h => { root.canvas-resized(self.px-w, self.px-h); } + } + + // Always instantiated, width collapsed to zero in compact mode. + // A conditional `if` here creates a binding loop — the layout + // depends on `expanded`, which derives from the window width, + // which the layout then influences. Slint flags it, and it can + // panic at runtime. + SidePanel { + width: root.expanded ? 260px : 0px; + visible: root.expanded; + } + } + } +} diff --git a/ui/dr-ui/ui/theme.slint b/ui/dr-ui/ui/theme.slint new file mode 100644 index 0000000..cb1012e --- /dev/null +++ b/ui/dr-ui/ui/theme.slint @@ -0,0 +1,29 @@ +// Darkroom safelight palette. Warm neutrals, single red accent. +// Committed to a dark ground — this is a photo editor, and a light UI +// surrounding an image biases how that image is judged. + +export global Theme { + out property ground: #14120F; + out property surface: #1D1A16; + out property surface-raised: #262119; + out property rule: #332C24; + + out property ink: #F0EAE0; + out property ink-dim: #A79E91; + out property ink-faint: #7C7367; + + out property accent: #D9543C; + out property accent-dim: #8F2E1E; + + out property gap-sm: 6px; + out property gap: 12px; + out property gap-lg: 20px; + + out property text-sm: 11px; + out property text: 13px; + out property text-lg: 17px; + out property text-xl: 24px; + + // FR-UI-3: minimum 44pt hit target under touch. + out property touch-target: 44px; +}