Repository navigation
Conversation
…ad of full CI per commit
…ing→catalog→decode→engine→ui, apps, libraw carve-out crate)
…s), ci (full gate writing report.json+summary.md), smoke (window proof)
…; Linux window proof under xvfb
… + carve-out, layers, tiered tests), CLAUDE.md points at it, todo §0.5 handoff
…d; changelog notes the v2 port
…at rode along in the docs commit
…ies, project.yml, contact-sheet tool)
…nly say which step failed, not why)
… cfg'd, nix unix-only, gated tests, platform-graded u64 widenings)
…lback); platform lint fixes; xtask echoes 250 lines of a failing step
…y the scanner's identity off unix
…y test opens with an explicit sessions dir
…stead of panicking
… keyed enum bytes
… across platforms
…ugh the registry; the app drives decodes and settings
…hrough the harness
Prefetch the display tier 32 ahead/12 behind, plus eight-frame heads of the neighbouring batches, bounded by the display budget. Four workers measure 54-59 display decodes/s against the 20/s a 20 Hz hold needs, while the 6-8 ms preview covers the lead-in and any batch boundary.\n\nParallelise the 24 MP display downsample (about 10 ms of the single-frame cost) and add a four-worker throughput row to bench --decode so the 20 Hz claim stays measured.
Decode JPEGs into a 4-byte-aligned RgbaBuffer and turn it into egui's Color32 with bytemuck::try_cast_vec in the decode worker, so the paint thread no longer converts 4.1 ms of pixels and clones 0.6 ms of texture data per 3240x2160 frame. Texture uploads share the decoder's Arc when the frame fits max_texture_side. Raise the automatic decode bank from four workers to one per core up to eight, and benchmark 48 decodes so the sustained row is not dominated by worker start-up. Add 5K/6K fitted-target rows. Measured on the M1 Pro, Game1JENKS/Game3KC: 85.2/79.1 fps at 2048 px, 548/441 at the 1350 PRVW fit, 76.1/65.1 at the 3240 4K fit, 65.3/55.1 at 4320, 52.9/35.9 at 5076. Exact RAW stays at 0.343-0.346 s.
The Exact RAW develop spent four full-frame passes on things that can be one: rawler's CropDefault copy (96 MB, 22 ms), its sRGB gamma step (a powf per channel, 44 ms), the f32 to RGBA8 pack (12 ms), and the orientation rotate (32 ms). The crop rectangle is now read in place (LinearFrame), the sRGB curve is a 16-bit-indexed table built from rawler's own srgb_apply_gamma, and encode + pack + rotate happen once, destination-row parallel, tiled 64x64 for the transposing orientations because a full-height column walk measured 54 ms. Warm Exact RAW on a 24 MP CR3: 0.343 s -> 0.286 s on Game1JENKS and Game3KC. The opt-in real-photo test asserts the fused output stays within one 8-bit level of rawler's stock pipeline (measured: max 1, 1% of pixels). bench --decode --stages is new: it times rawler's passes on one file in one process so the next optimisation can be aimed (decode 0.119, demosaic 0.099, tail 0.055 of the current 0.286).
v2's packaging carried a different icon — a crop bracket around an F — that was drawn for the port. The owner's mark is the v1 one, so `assets/branding/` now holds v1's own appiconset exports byte-for-byte: * `firstcut-128/256/512/1024.png` are `icon_128x128`, `icon_128x128@2x`, `icon_256x256@2x` and `icon_512x512@2x` from v1's asset catalogue, so the simplified three-frame art is kept at small sizes instead of being a downscale of the large one; * `firstcut.icns` is those ten exports through `iconutil` (verified: the round-tripped 16, 256 and 1024 px images are pixel-identical to v1's); * `firstcut.ico` is 16/32/64/128/256 from the same exports plus a 48 px LANCZOS scale of the 128 px art (v1 shipped macOS only, so no 48 px export exists). `cargo xtask package` rebuilt the macOS app and its `AppIcon.icns` is byte-identical to the new source. Attribution, the plan's icon note, the decisions table and the changelog say so explicitly, so the mark cannot drift again without a doc change.
`apply_orientation` named the source pixel for every destination pixel with a match and two checked multiplications, which made the transposing EXIF orientations the most expensive single thing after the demosaic. Every EXIF transform is affine along a destination row, so the origin and the step are hoisted out of the loop: the inner loop is one add, one checked 4-byte load and one 4-byte store. The orientation row of `bench --decode --stages` on a 24 MP CR3 (orientation 8) drops from 0.033 s to 0.015 s. The corner/orientation test still checks all eight transforms and that each is a bijection over the frame's pixels.
rawler's develop expands the CFA into a 288 MB interleaved f32 RGB buffer and then walks it four times. The fused plan replaces that with three row-parallel plane passes and one output pass: * green, which is the pixel's own sample at a green pixel and PPG's gradient choice at the rest, with rawler's 3-pixel bilinear ring at the edges; * the pixel's own colour, plus red and blue at the green pixels by hue transit; * the channel missing at every red/blue pixel, whose diagonal neighbours are the *other* colour's own samples, so it reads only the CFA and green planes. Every pass fetches the five rows it reads once and then walks its half of the row with a stride of two, so the inner loop has no index arithmetic and no per-pixel CFA lookup. The output pass reads three complete rows, applies white balance and the camera matrix, and encodes/packs in the same loop. `fused::Plan` accepts a file only when it has been proven on it (single- component Bayer, no Fuji rotation, a matrix it can size, a crop inside the active area); anything else returns `None` and `develop_fast` runs rawler's pipeline exactly as before, so a refusal is never an error. Measured on a 24 MP CR3 at 6000x4000, warm, 8 threads, `bench --decode --stages`: decode 0.1200 s (unchanged; rawler's CRX decode) demosaic 0.0936 s (rawler's PPG: 0.099 s, over a 288 MB buffer) pack 0.0237 s orientation 0.0152 s sum 0.2566 s (was 0.286 s; measured decode_raw 0.2562 s) The opt-in real-photo gate still holds: the fused output is within one 8-bit level of rawler's stock pipeline (max difference 1, 238 463 of 24 M pixels), and the unit tests compare the demosaic against rawler's PPG pixel for pixel for all four Bayer orders and an offset active area.
Exact RAW is 0.286 s -> 0.257 s on both games, and every §5 row was re-measured on this build (the display rows had not been updated since the zero-copy handoff landed): 89.4 fps at 2048 px, 638 at the 1350 px PRVW fit, 82.4 at the 4K fit, 74.9/62.4 at the 5K/6K fits on Game1JENKS, and 87.7/581/82.3/73.6/53.2 on Game3KC. The cold first-touch row keeps its cold reading, because it is the one row that depends on the page cache. The stage table now names what the fused plan actually runs, and the "what is left" paragraph changes with it: the 0.120 s CRX decode is the largest single stage, the demosaic is at parity with rawler's algorithm and division-bound, and the floor with this algorithm is ~0.24 s. The owner decision therefore narrows to accepting 0.257 s, funding a custom CRX decoder, or redefining the gate as first-frame latency.
`apply_orientation` is only called from `develop_libraw`, which is compiled on macOS and Linux. Importing it made Windows' clippy see an unused import and fail `cargo xtask ci` with `-D warnings` (run 41036436148). The call is now qualified, which is what the fused path already does. Checked with `cargo clippy --target x86_64-pc-windows-msvc -p firstcut-decode-libraw --all-targets -- -D warnings`.
The scanner accepts them as photographs, but only a RAW's embedded JPEG and a plain JPEG had a display source, so a HEIC-only folder opened and painted nothing (v1 read them through ImageIO; todo.md:978 requires those folders to cull like RAW folders). PNG and TIFF decode in-process with the image crate (pure Rust, no unsafe). HEIF is converted by the platform's own tool - sips on macOS, WIC through a static PowerShell script on Windows, heif-convert/ffmpeg/ImageMagick on Linux - and the PNG that comes back is decoded in-process. Orientation is applied exactly once: a converter that bakes the rotation in is detected by the frame's aspect against the stored sensor dimensions. A machine with no converter gets a typed error naming what to install. decode_display_at and decode_thumbnail route a file with no embedded JPEG to the still path, and the exact tier treats a non-RAW's own pixels as exact instead of asking the RAW backend for sensor data a HEIF does not have.
…e identity Move to Trash failed per file on Windows: trash_for was unix-only and returned Unsupported. The file now goes to the Recycle Bin through the trash crate, a safe wrapper over the shell API, so the no-unsafe rule holds; the bin hands back no path, so undo reports the file as 'in the Recycle Bin: restore it from there' instead of skipping it silently. free_space was None off unix, so Finish never warned about a full destination. It is fs4::available_space now, which asks statvfs on unix and GetDiskFreeSpaceExW on Windows. crosses_volume and the session's volume identity answered 'unknown' on Windows. Both now read the volume serial number through file-id, and the scanner records the (volume serial, file index) pair as the (device, inode) identity, so a rename is recognised on Windows the way it is on unix.
The strip uploads a photo's thumbnail and the loupe uploads the same photo's display bitmap, in the same frame. Keyed on the photo id alone, the second upload replaced the first, so the current photo's filmstrip cell painted a freed texture - a blank cell, reproducible on any RAW shoot. TextureKey is (photo id, [width, height]). The size in the key also keeps the existing zoomed-redecode behaviour: a size change is a new texture rather than an in-place replace. one_photo_at_two_sizes_keeps_two_textures is the regression test.
The two Windows rename tests failed because the session test helper recorded (0, 0) as device and inode while the scanner had already moved to real Windows file ids, so a rescan could not recognise the renamed file. `stat_identity` is now the scanner's exported definition; the helper calls it, so what it ingests is exactly what a rescan produces. A meta test pins the rename stability the engine relies on, on whichever platform runs it.
The parity checklist's Windows volume-identity bullet now names the shared `stat_identity` definition and the test that pins it; todo.md §0.5 records why run 41222385725 was red on Windows only and what closed it.
`free_space` has asked the OS on every platform since the M5 work (`fs4::available_space`), so Windows returns a real number, not `None`. The test kept asserting the old "cannot be checked" answer and the 3-OS run on 91bbec4 failed on that alone.
The free-space test's Windows branch was the next failure after the rename tests, hidden behind cargo's stop-at-first-failure. The handoff says so.
Six calls need a person: the Exact RAW gate (0.257 s against 0.119 s), the default viewer tier, the Game1JENKS re-press, HEIF on Linux, the Windows owner checks, and where to start on the boundary ground truth. `docs/v2-decisions.html` is one self-contained page — no network, no build step — with the measured numbers behind each option, an optional note per decision, and answers that stay in the browser and download as firstcut-v2-decisions.json. todo.md §0.2 points at it next to the photo review page, and §0.5 records that it landed with the matrix green at 744f031.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft PR for the pure-Rust rewrite of v1 on the
v2branch — the contract is docs/v2-plan.md; this stays draft until the owner moves it (agents never merge intomain), and testing is tiered per plan §9.Status (2026-10-20) — M1 vertical slice complete pending the owner's README pixels:
cargo xtask layers, never-crash lints, the LibRawunsafecarve-out enforced by CI).firstcut-cli run(headless chain, one JSON line per command),firstcut --control <port>(engine.* + ui.widgets/ui.clickWidget/ui.screenshot),firstcut-cli mcp(registry as MCP tools over stdio).apps/firstcut/tests/snapshots/*.pngvia kittest (green on all three OSes).scripts/test-with-photos.sh).cargo xtask ci→ pass (fmt, clippy -D warnings, layers, assets, full workspace tests); CI matrix green on the tip.docs/showcase/m1-screenshot.shdrives the real app through the control channel and saves the screenshot outside the repo (pixels are the owner's; README image needs the owner's word).Next: M2 (decode priority window, cancellation, Exact RAW via LibRaw, memory budget, watcher/histogram, bench rows).