Repository navigation
Description
Summary
cargo test runs in CI only on FreeBSD. Nothing runs the tests on macOS, Windows or Linux x64, the three platforms people install LightCraft on, so platform-specific breakage reaches main and stays there until someone builds on that platform by hand. The CPU/GPU equivalence tests, the guard that keeps the GPU twin matching the CPU reference, never run in CI on any platform: they skip when there is no GPU adapter, and the code refuses the software adapters CI runners have.
This is gap 9 in docs/gaps.md (beta blocker, 6–12 h). This issue proposes a fix that stays inside the current design and adds no new runner types.
Evidence
.github/workflows/hasfreebsd.yml(the only workflow that runscargo test),packaging-lint.yml,release.ymlandwindows-arm64.yml.release.ymlbuilds packages on macOS, Windows and Linux but runs no tests.windows-arm64.ymlruns the CLI natively and has nocargo testjob (its header comment explains why).- main does not build or pass its tests on macOS (995d5ea4): app won't compile, 12 tests fail, one clippy lint #989: on 2026-10-11,
maindid not compile on macOS (eframe::App::on_exitcalled with one argument in the macOS-only quit path), and 12 tests failed there. Nothing in CI could have caught it. The follow-ups (Issue #989: the lock test finds true on the PATH #990, Issue #989: clippy 1.99, fill_box reads its buffer with as_chunks::<3>() (item 4) #997, Issue #989: odd_folders_never_fail_the_check skips non-UTF-8 names on APFS (item 3, one of eleven) #1003, Issue #989: the ten sidecar repair tests also pass on a case-insensitive volume (item 3, ten of eleven) #1007, Issue #989: the Export dialog height test expects the macOS row (Add to Apple Photos, 876 pt) #1018) are still open. crates/gpu/tests/equivalence.rs:19-25:gpu()printsskipped: no GPU adapterand the test passes.fallback.rs,memory.rs,shutdown.rsandshutdown_first.rsdo the same.crates/gpu/src/ctx.rs:299-302:Gpu::newreturnsErrforwgpu::DeviceType::Cpu("the CPU pipeline is faster"). That is the right call for the app. The side effect is that lavapipe (Linux) and WARP (Windows), the only adapters on GitHub's standard runners, are never used, so the equivalence suite has never run in CI.
What a test run on the three platforms finds today
I ran the workflow below on my fork, on main at 76b5377. All runners were standard GitHub-hosted ones with a cold cache.
main alone |
main + #990, #997, #1003, #1007, #1018 |
|
|---|---|---|
Linux x86_64, cargo xtask ci |
fails at clippy (clippy::chunks_exact_to_as_chunks, rustc 1.99, #989 item 4) |
all 8 steps pass (39 min cold) |
| macOS arm64, clippy + test | clippy as above; lightcraft does not compile (#989 item 1) |
pass (clippy 3 min, tests 19 min) |
| Windows x86_64, clippy + test | clippy as above | 2 new Windows-only failures, below |
| GPU tests on a software adapter / Metal | lavapipe: 16/16 equivalence tests pass | lavapipe, WARP and Metal all pass |
Two Windows failures that nobody had reported:
- clippy, Windows only:
method descriptor_copy is never usedatcrates/catalog/src/lock.rs:140(-D dead-code). The method is#[cfg(test)], and its only caller is a#[cfg(unix)]test (crates/catalog/src/tests_lock.rs:114-119). editors::tests::desktop_entries_give_their_program(apps/lightcraft/src/editors.rs:222) fails on Windows:left: None, right: Some("/home/simon/Apps/photocraft-1.2-linux-x86_64.AppImage"). The.desktopparser presumably checks for an absolute path, and/home/...is not absolute on Windows. That run stopped at the first failing test binary, so there may be more. The workflow now uses--no-fail-faston macOS and Windows so every failure shows.
These two are not fixed here. They belong in their own PRs, the same way the #989 items were split.
The equivalence suite on software adapters
With the opt-in described below, all 134 compared renders (16 tests) stay inside the documented bounds (mean |Δ| < 0.5 LSB, max ≤ 3 LSB). The results match the Apple M4 Pro numbers in docs/gpu-pipeline.md, and no tolerance was changed:
| adapter | renders | max |Δ| | worst mean |Δ| | equivalence time |
|---|---|---|---|---|
| lavapipe (Mesa, LLVM 20), local | 134 | 1 LSB (110 renders), 0 (24) | 0.0003 | 19 s |
lavapipe, ubuntu-latest |
134 | 1 LSB | 0.0003 | 50 s |
WARP (DX12, FXC), windows-latest |
134 | 1 LSB (109), 0 (25) | 0.0003 | 446 s |
Metal (paravirtual GPU), macos-latest |
16 tests pass | (not printed) | 29 s |
One test assumed more than a WebGPU-sized device. windows_of_a_big_photo_share_one_uploaded_source failed on lavapipe (left: 0, right: 1 uploads). lavapipe's max_storage_buffer_binding_size is 128 MiB, which is the WebGPU default. The test's 24 MP source is 288 MB, so by design (render.rs:619) geometry ran on the CPU and nothing was uploaded. This is not a drift between CPU and GPU. Its sibling, a_window_does_not_keep_the_whole_source_on_the_device, passed on lavapipe without checking anything, for the same reason.
Proposed fix (within the current design)
- Test-only opt-in for software adapters. Add
LIGHTCRAFT_GPU_ALLOW_SOFTWARE=1, read inGpu::new. The app never sets it, and the default behaviour does not change: software adapters are still refused. This followsLIGHTCRAFT_GPU_LIMITS, which also exists to reproduce an adapter. A unit test covers the decision (refuse_adapter). - Size the two big-source tests to fit a 128 MiB binding. Use 3200 × 3200 (117 MiB): above the 96 MiB
RETAIN_SOURCE_BYTES, so the source is still shared, and within the WebGPU default, so it is uploaded on every adapter. On a real GPU they test the same code path. The first test still catches the Missing a native full resolution preview of pictures #323 regression: a 117 MiB source kept in the stages is more than its 100 MiB bound. .github/workflows/ci.yml, standard runners only, on every pull request and every push tomain, withconcurrencycancel-in-progress:- Linux x86_64 (
ubuntu-latest):cargo xtask ci, the same gate contributors run locally, so new xtask steps are checked automatically. Then the GPU test targets with lavapipe (mesa-vulkan-drivers). - macOS arm64 (
macos-latest) and Windows x86_64 (windows-latest): clippy andcargo test --workspace --no-fail-fast. These are the steps whose result depends on the platform:cfg(target_os)code and file-system behaviour. fmt, parity, layers, assets and wasm give the same answer everywhere, so they run once, on Linux. Then the GPU test targets on Metal and WARP. - The GPU step names the test targets with
--workspace(--test equivalence --test fallback ...) so it reuses the workspace build, and fails if anything still printsskipped: no GPU adapter. Without that check, a runner-image change could silently turn the guard off again. Swatinem/rust-cache(the pinrelease.ymlalready uses) caches dependencies only, and saves only frommain(save-if). Pull requests restore from it, so the 10 GB repository cache holds one entry per job.- It uses only the actions and pins already in
release.yml(checkout, dtolnay/rust-toolchain, rust-cache). No new third-party actions. xtaskalready sizes build jobs and test threads by available RAM (ci_jobs). The macOS runner (3 cores, 7 GB) passed with the defaults.
- Linux x86_64 (
- Docs: gap 9 and the CI mentions in
ROADMAP.md/docs/target-app-parity.md, plus the new variable indocs/gpu-pipeline.md.
Cost: standard GitHub-hosted runners are free for public repositories. Measured cold-cache durations: Linux 41 min, Windows 28 min, macOS 23 min, all in parallel. Warm runs should be shorter, because dependencies are restored from main's cache.
For the maintainers to decide (not in the PR)
- Per-PR or not. CI: FreeBSD and Windows ARM64 nightly and on release, not per PR #901 moves FreeBSD and Windows ARM64 to nightly because PR CI waits on runners shared by ~14 craft apps. This workflow uses the same org-wide pool: 1 macOS and 2 standard jobs per PR. If that is too much, two lines change it: run macOS and Windows on
pushtomainplus a nightlyschedule, and keep Linux per-PR. main does not build or pass its tests on macOS (995d5ea4): app won't compile, 12 tests fail, one clippy lint #989 suggests that macOS per-PR is what would have caught it. - WARP time. The equivalence suite takes ~7.5 min on WARP. If that is too slow per PR, run the Windows GPU step only on
mainand keep lavapipe per-PR. - Toolchain. The workflow uses
stable, likerelease.yml, so a new clippy lint can turnmainred overnight (main does not build or pass its tests on macOS (995d5ea4): app won't compile, 12 tests fail, one clippy lint #989 item 4). Arust-toolchain.tomlpin, bumped deliberately, would avoid that. That is a project-wide decision, so it is not part of this proposal. - Required checks. Once
mainis green on all three (after the main does not build or pass its tests on macOS (995d5ea4): app won't compile, 12 tests fail, one clippy lint #989 PRs and the two Windows fixes), the three jobs could become required status checks. - Relation to ci: add Linux x64 CI workflow (partially closes gap #9) #941 (Linux-only CI, open, with review comments). This proposal covers its Linux job and addresses those comments: no
retry:inputs, no/usr/libcache, no path filter, trailing newline. It also adds the wasm target, which that run lacked (ci failed at wasm), plus macOS, Windows and the GPU guard. Both add.github/workflows/ci.yml, so only one of them should land.
Test plan
cargo test -p lightcraft-gpu, default environment: the GPU tests skip as before, andsoftware_adapters_need_the_opt_inpasses.LIGHTCRAFT_GPU_ALLOW_SOFTWARE=1 VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json cargo test -p lightcraft-gpu -- --nocapture: all 16 equivalence tests and the fallback, memory and shutdown tests run on llvmpipe and pass.- The workflow, run on a fork: the Linux, macOS and Windows jobs and their GPU steps. Results are in the table above.
actionlintreports nothing for the workflow file.
Activity
test(gpu): storytold#1050 opt-in software adapter for the equivalence…
ci: storytold#1050 run the tests on Linux, macOS and Windows
docs: storytold#1050 gap 9 shrinks: tests in CI on the three desktop …
ci: storytold#1050 run the tests on Linux, macOS and Windows
docs: storytold#1050 gap 9 shrinks: tests in CI on the three desktop …
THEtheChad commentedon Oct 11, 2026
Update: the full Windows picture, and where each failure is being fixed
The first Windows run stopped at the first failing test binary. The workflow now runs
cargo test --workspace --no-fail-faston macOS and Windows. A second fork run (mainat 76b5377 + the workflow + #990, #997, #1003, #1007, #1018) shows everything that fails there:cargo xtask ci+ GPU tests on lavapipedead_codeonLibraryLock::descriptor_copy(crates/catalog/src/lock.rs:140)Windows test failures, all on
mainbefore this change (the PR touches no engine or app code):lightcraft(bin):editors::tests::desktop_entries_give_their_program(apps/lightcraft/src/editors.rs:222,left: None). A/home/...Execpath is not absolute toPath::is_absoluteon Windows.lightcraft-engine(lib), 21 tests: 762 passed and 21 failed. They fall into two groups:/and the code joins with\. For example,rename.rs:1314expected...\Archive/Trip\b.jpgand got...\Archive\Trip\b.jpg;tests_filesystem_history.rs:346expected/p/Trip.jpgand got/p\Trip.jpg;tests_folders.rs:624expected...\out/a.jpg. The others in this group aremerged_rename_failure_keeps_rolled_back_photo_identityandmerged_renames_coalesce_physical_moves_and_keep_retry_inverses_and_journal.sidecar::tests_repair_companions(3),tests_repair_provenance(7) andtests_repair_saves(2) tests fail, plus 4tests_filesystem_historyretries. The injected failure never fires (left: 4, right: 1sidecars moved;unwrap_err()onOk("Rename 1 Photo")). This is the Rename Photos: on a case-insensitive volume the sidecar a.xmp becomes Trip.XMP #1006 / Issue #989: the ten sidecar repair tests also pass on a case-insensitive volume (item 3, ten of eleven) #1007 mechanism. Issue #989: the ten sidecar repair tests also pass on a case-insensitive volume (item 3, ten of eleven) #1007's test-side fix covers APFS but not NTFS: Windows still fails with Issue #989: the ten sidecar repair tests also pass on a case-insensitive volume (item 3, ten of eleven) #1007 merged.#1052 (OneSlips, open) already fixes all of these: the clippy lint, the editors test, the case-insensitive rename / save bugs (#1006) and the separator fixtures. So nothing new needs filing. I confirmed this with a third fork run that also had #1052 merged on top (run 38174552427). All three jobs are green:
cargo xtask ci: all 8 steps, 39 mincargo xtask cion the same merge also passes all 8 steps locally (Linux, rustc 1.99). #1052 and #997 both changefill_boxincrates/pipeline/src/spots.rs, and #1052 and #1007 change the same assertion intests_repair_saves.rs, so whichever lands second needs a small rebase.Change to the workflow since the issue
The Linux GPU step now runs
cargo test -p lightcraft-gpu --tests. Its earlier--workspace --test …form rebuilt about 190 crates aftercargo xtask ci(7 min); this form takes about 2 min. macOS and Windows keep the--workspaceform, which reuses the test step's build (2–8 s of building).Prior art since the issue
#1083 and #1084 (a nightly
macos.yml) were opened and then withdrawn by their author in favour of the open #989 PRs and #901's nightly direction. Per-PR or nightly remains the maintainers' call (see "For the maintainers to decide" in the issue body). The PR keeps the platform jobs per-PR and explains how to move them.test(gpu): storytold#1050 opt-in software adapter for the equivalence…
ci: storytold#1050 run the tests on Linux, macOS and Windows
docs: storytold#1050 gap 9 shrinks: tests in CI on the three desktop …
ci: storytold#1050 run the tests on Linux, macOS and Windows
docs: storytold#1050 gap 9 shrinks: tests in CI on the three desktop …
fix: WindowsのCI失敗修正を統合 storytold#1050
ci: 3OSとGPUのCIを既存PRへ統合 storytold#1050
francoisgn commentedon Oct 16, 2026
Sorry, I missed this issue and the existing CI PRs (#1148, #1159, #941) before opening #2005, which duplicates them. I'll close #2005 in favour of whichever one you pick.
What's still useful from it: #2004 fixes 3 tests that fail on windows-latest (path separators in the folder-drop and export tests, and a missing wait in the offline test). Any of these CI PRs will hit those failures on Windows.
Green run on all 3 OS for reference: https://github.com/francoisgn/lightcraft/actions/runs/38873633880
fix: storytold#1050 clippy on macOS and Windows: one Quit / About / S…
6 remaining items
docs: storytold#1050 gap 9 closed: tests in CI on macOS, Windows and …
test(gpu): storytold#1050 opt-in software adapter for the equivalence…
ci: storytold#1050 run the gate on macOS and Windows, and the GPU tes…
fix: storytold#1050 clippy on macOS and Windows: one Quit / About / S…
docs: storytold#1050 gap 9 closed: tests in CI on macOS, Windows and …
test(gpu): storytold#1050 opt-in software adapter for the equivalence…
fix: storytold#1050 clippy on macOS and Windows: one Quit / About / S…
docs(gpu): storytold#1050 run the GPU tests locally on a software ada…
docs(gpu): storytold#1050 the skip-log check catches every GPU skip a…
test(gpu): storytold#1050 opt-in software adapter for the equivalence…
docs(gpu): storytold#1050 run the GPU tests locally on a software ada…
docs(gpu): storytold#1050 the skip-log check catches every GPU skip a…
docs(gpu): storytold#1050 the skip check also expects the device-memo…
AlexChambers commentedon Nov 9, 2026
One more gap to fold in here. The Linux gate runs the tests now, but it has no corpus. This run on
mainreportscorpus: 0 of 462 pinned raws, 0 images, 0 PngSuite files, so every corpus-backed test skips and passes. That includes the per-model checks behind the verified-models count inROADMAP.md, which CI therefore never exercises.A nightly job that runs
cargo xtask corpus --downloadbefore the tests would cover it without adding time to pull requests.THEtheChad commentedon Nov 11, 2026
Agreed. I ran it locally to see what the gap is hiding. With the full pinned corpus (
LIGHTCRAFT_CORPUSset, all 460 raws plus the D750 set),lightcraft-raw,-codecsand-engineonmain(8ef35987d) had 12 failures, all green in CI because their tests skip without the corpus:baseline_exposurethatprobe_infocan't, so 10 CR3 tests fail the probe-equals-decode check.corpus_raw_decodesstops at the first failing file, so the rest of the 460-file sweep isn't checked until that's fixed.corpus_raf_colour_and_white_balancestill expects them relative.corpus_rw2_distortion_matches_the_camera_jpeg. Bisected; it passes with dithering off.So corpus-only breakage is already landing within days.
Size, which decides how a nightly can work:
Options, roughly in order:
storytold/photocraft-corpus, which takes the load off raw.pixls.us but still downloads 14 GB per run.Whichever is chosen, the job would call the shared gate (it already has
commandandfree-disk-space), and any corpus caching support belongs in craft-common-actions, per the CI rule. I can open that issue there once there's a preferred option.