From 4bd8d86c001199364adf6410b38fd0ffcea7c2fe Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 27 Sep 2026 18:05:10 -0400 Subject: [PATCH] Record where a linear DNG too large for one texture goes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARCH §5.3 still described only a tile cache nobody built; it now says what 0.19.0 tiles and what it does not: a linear DNG past PROXY_EDGE opens on a reduced copy, a finer render samples a full-resolution window, and the export is cut into halo-grown tiles. display-and- extension.md's FR-DSP-2 row said absent, outstanding.md said the halo had nothing to read it and that such a file fell to the embedded preview. rawler now builds from third_party, which ARCH's stack table and §3.2, the root Cargo.toml's comment ("two upstream crates ... for Android") and third_party/README.md's bump procedure did not know; the README also names each vendored crate's licence. panorama.md and the manual say a composite this wide develops and exports. --- Cargo.toml | 11 ++++++----- docs/dev/architecture.md | 21 ++++++++++++++++++++- docs/dev/display-and-extension.md | 2 +- docs/dev/outstanding.md | 21 +++++++++++++-------- docs/dev/panorama.md | 10 ++++++++++ docs/manual/README.md | 5 +++++ third_party/README.md | 17 ++++++++++++----- 7 files changed, 67 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e5724ed..3439225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -276,11 +276,12 @@ opt-level = 0 lto = "thin" codegen-units = 1 -# Two upstream crates carry a local patch so that the Android build can draw -# with wgpu on a rotated display (technical-debt.md TD-1). Both are exact -# copies of the version the lockfile already resolves, plus that patch; -# third_party/README.md says what was changed and how to carry it forward -# when Slint or wgpu moves. +# Three upstream crates carry a local patch: wgpu-hal and Slint's Skia +# renderer so that the Android build can draw with wgpu on a rotated display +# (technical-debt.md TD-1), and rawler so that a linear DNG wider than 16 700 +# pixels decodes. Each is an exact copy of the version the lockfile already +# resolves, plus its patch; third_party/README.md says what was changed and +# how to carry it forward when Slint, wgpu or rawler moves. [patch.crates-io] wgpu-hal = { path = "third_party/wgpu-hal-29.0.4" } i-slint-renderer-skia = { path = "third_party/i-slint-renderer-skia-1.17.1" } diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 4560ae1..3a2d878 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -34,7 +34,7 @@ document elaborates: | UI | Slint | D1, D8 | | GPU | wgpu → Vulkan (Linux + Android) | D1 | | Shaders | Hand-written WGSL | D6 | -| RAW decode | rawler; LibRaw fallback behind a trait | D2 | +| RAW decode | rawler (0.7.2, carried patched in `third_party/`); 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 | @@ -140,6 +140,11 @@ is *not* demosaiced. Demosaic is a GPU pipeline stage (§5.2). > (§3.1's `read_range`), not the decoder. `dr_decode::Rawler` is the one implementation, and only > the places that start a job name `dr_decode::default()`; everything below them takes a > `&dyn Decoder`. +> +> rawler itself is built from `third_party/rawler-0.7.2` since 0.19.0: the crate as published, +> with its allocation guard raised so that a linear DNG wider than about 16 700 pixels (a +> stitched panorama) decodes rather than being refused +> ([third_party/README.md](../../third_party/README.md)). ### 3.3 Operation and descriptors @@ -433,6 +438,20 @@ Tile results cache keyed by `(VersionId, tile, zoom, graph_hash_prefix)`, where operations up to the first `Affects` change. Adjusting exposure reuses cached demosaic and camera profile output for every tile. +> **As built (0.19.0).** The interactive path does not tile: one fused dispatch over the viewport +> is inside the frame budget ([frame-budget.md](frame-budget.md)), and there is no scheduler or tile +> cache. What tiles is a source too large for one texture. A linear DNG whose long edge passes +> `PROXY_EDGE` (8192) — a stitched panorama — is held at full resolution on the CPU and opened on +> a box-reduced copy, from which the canvas at fit, the thumbnail, the histograms and the masks +> work. A render finer than the copy — the canvas zoomed in, a tile of the export — samples a +> window cut from the full resolution (`DemosaicedImage::linear_rgb16_window`), which the fused +> shader addresses through two source-window uniforms so that crops, warps and grain seeds stay +> where they are in the frame. The canvas keeps one window while the view stays inside it. The +> export is cut by `dr_pipeline::tiles::plan` into 4096-pixel tiles on a 16-pixel grid, each +> grown by the detail chain's reach (`ComposedDetail::reach`, the sum of its passes' radii), and +> reassembled; `core/dr-gpu/tests/source_window.rs` holds it to the untiled render within one code +> value. A CFA file too large for one texture is still refused. + ### 5.4 Mask rasterisation **All masks rasterise on the GPU, including drawn brush strokes** (§6.11). Strokes arrive as diff --git a/docs/dev/display-and-extension.md b/docs/dev/display-and-extension.md index 8933e4d..d24e371 100644 --- a/docs/dev/display-and-extension.md +++ b/docs/dev/display-and-extension.md @@ -20,7 +20,7 @@ and the reason is that some of the work is done and untagged. | Requirement | Reality | |---|---| | FR-DSP-1 proxy rendering | **Done.** The develop view renders at viewport resolution, not source. | -| FR-DSP-2 tiled computation | **Absent, and §2 now says it should stay that way.** Measured: the fused pass is inside the budget everywhere. See [frame-budget.md](frame-budget.md). | +| FR-DSP-2 tiled computation | **Absent from the interactive path, and §2 now says it should stay that way.** Measured: the fused pass is inside the budget everywhere. See [frame-budget.md](frame-budget.md). *Since 0.19.0* the export of a linear DNG too large for one texture is drawn in halo-grown tiles, and the canvas renders such a file from a reduced copy and full-resolution windows (ARCH §5.3). | | FR-DSP-3 interactive latency | **Measured and asserted** for the fused path — `core/dr-gpu/tests/frame_budget.rs`. Missed by one operation, clarity, for the reason recorded as TD-4. | | FR-DSP-4 progressive refinement | **Built** (0.15.0), although §4's condition did not fire on the fused path: a half-resolution draft while a gesture moves, one sharp frame 120 ms after it stops, the histogram dimmed while it lags, and the draft faded out over 150 ms — `ui/dr-ui/src/refine.rs`. See [frame-budget.md](frame-budget.md). | | FR-DSP-5 zoom and pan | **Done and tagged**, against tests that fail if the behaviour is removed — `core/dr-gpu/tests/zoom_resolution.rs`. `Framing::view` shrinks the sampled region while the render target keeps its size, so zooming *raises* the resolution the pipeline works at. That is FR-DSP-5's requirement, arrived at without tiles. | diff --git a/docs/dev/outstanding.md b/docs/dev/outstanding.md index 1c1a498..79ffc6b 100644 --- a/docs/dev/outstanding.md +++ b/docs/dev/outstanding.md @@ -181,7 +181,7 @@ place. ## 4. The render path — FR-DSP-2, FR-DSP-4, NFR-RES-2, NFR-ARCH-1 -**FR-DSP-2 — Tiled computation. Unbuilt, and under challenge.** [architecture.md §6.2](architecture.md) +**FR-DSP-2 — Tiled computation. Built for one case, and otherwise under challenge.** [architecture.md §6.2](architecture.md) calls for tiling "from day one" on the grounds that retrofitting it is a rewrite. It was not built, and the evidence has since moved. `core/dr-gpu/tests/frame_budget.rs` carries the argument in its own header: one fused dispatch over a viewport-sized target is comfortably inside the frame budget, @@ -191,9 +191,10 @@ path stops being supported, and this test is what says so." tiled convolution at clarity's radius reads nearly twice the taps that an untiled one does, so the stage that looks most like it wants a tile cache is the stage that would be hurt most by one. -What exists is the declaration and not the mechanism: `DetailPass::radius` is documented as the halo -a tile would have to be grown by, with a test that pins it, and there is no scheduler to read it. -That is deliberate plumbing, not an oversight. +What existed until 0.19.0 was the declaration and not the mechanism: `DetailPass::radius`, the halo +a tile would have to be grown by, with a test that pins it, and nothing to read it. The export of an +oversized linear DNG (below) is now what reads it, through `ComposedDetail::reach`; there is still +no scheduler and no tile cache. **So the open question here is not "when is tiling built" but "is FR-DSP-2 still a requirement" — and on 2026-09-19 the answer was: as written, until S6 runs.** FR-DSP-2 now carries a status note saying exactly that, and R5's note no longer claims it was rewritten. Two measurements say it costs more than it saves on the interactive path. Neither says anything @@ -201,8 +202,10 @@ about the export path or about a device under memory pressure, which is where th actually lives — and that is spike S6, which has not run. **2026-09-27:** the export path does now tile, for the one case that forced it — a linear DNG -wider than any texture. See the note under FR-DSP-2 in [requirements.md](requirements.md). The -interactive path is unchanged and S6 is still unrun. +wider than any texture, a 22 927 × 8966 Lightroom panorama in the case that prompted it. See the +note under FR-DSP-2 in [requirements.md](requirements.md) and ARCH §5.3. The interactive path +renders such a file from a reduced copy and one full-resolution window rather than tiles, and S6 +is still unrun. **FR-DSP-4 — Progressive refinement. Built in 0.15.0.** While a gesture moves the canvas renders a half-resolution draft, and the sharp frame lands once, 120 ms after the last movement: the @@ -216,8 +219,10 @@ interface could see, and a refinement that was not a jarring swap. **NFR-RES-2 — Images larger than GPU memory.** Half answered. NFR-R8's "decide explicitly" was decided on 2026-09-19: there is no CPU render pipeline, the degraded mode is the viewer on -embedded previews with develop withheld, and NFR-RES-2 no longer promises a fallback render. What -remains unbuilt is the memory half: there is no headroom budget, no allocation-failure staging, +embedded previews with develop withheld, and NFR-RES-2 no longer promises a fallback render. Since +0.19.0 that mode no longer catches a linear DNG larger than one texture, which develops from a +reduced copy and exports in tiles; a CFA file that large still falls to it. What remains unbuilt is +the memory half: there is no headroom budget, no allocation-failure staging, and no spill. Spike S6 — a tiled pipeline on a mid-range Android device with an image larger than available GPU memory — is the one that would settle both this and FR-DSP-2, and there is no evidence it has run. diff --git a/docs/dev/panorama.md b/docs/dev/panorama.md index 4dbfe4d..0faff73 100644 --- a/docs/dev/panorama.md +++ b/docs/dev/panorama.md @@ -606,3 +606,13 @@ the preview (`grey_if_blown` in `dr_ui::merge`) now write a blown sample, before the gain, as the camera value the composite's balance maps to grey — the develop pipeline's neutral, fading in from `CLIP_ONSET` exactly as the develop's does. + +**A composite this wide is past two limits, both lifted the same day.** +They were found on a 22 927 × 8966 panorama from Lightroom, and the fixture's +own composite (22 993 × 5 980, §11) is past both. rawler's allocation guard, +sized in samples but worded in pixels, refuses a three-sample DNG past about +16 700 pixels wide; the copy in `third_party/` carries it raised +([README](../../third_party/README.md)). And no texture holds such a frame: +a linear DNG past 8192 pixels now opens on a box-reduced copy, a render finer +than the copy samples a window of the full resolution, and the export is drawn +in tiles (FR-DSP-2's note in requirements.md, ARCH §5.3). diff --git a/docs/manual/README.md b/docs/manual/README.md index f6e532d..471ad0e 100644 --- a/docs/manual/README.md +++ b/docs/manual/README.md @@ -351,6 +351,11 @@ it to bring it back. A frame that cannot be placed is named there with why, and `Merge` stays off until it is unticked. Blown sky stays white in the preview and in the composite. +A panorama is usually far wider than a graphics card can draw in one piece. +It opens in develop all the same, on a reduced copy that is drawn from the +full resolution wherever you zoom in, and it exports at full size. The same +goes for a panorama Lightroom stitched and saved as a DNG. + ![Twelve frames aligned, the projections tried, and the border filled](media/panorama.gif) ![The alignment on a cylinder, each frame outlined where it landed](media/panorama-aligned.png) diff --git a/third_party/README.md b/third_party/README.md index 2d00f3e..03120bd 100644 --- a/third_party/README.md +++ b/third_party/README.md @@ -11,17 +11,24 @@ The first commit that adds a directory is the pristine copy (from crate's own `Cargo.lock`). Every later commit touching it is ours, so `git log -p -- third_party/` is the patch and nothing else. +Each directory keeps the crate's own licence files, and the crate keeps +its licence: wgpu-hal is MIT or Apache-2.0, i-slint-renderer-skia is +Slint's GPL-3.0-only or royalty-free or commercial choice, and rawler is +LGPL-2.1. + ## Carrying a patch forward -When Slint or wgpu is bumped, the version here stops matching and cargo -warns that the patch is unused — the build then silently goes back to the -unpatched crate. So a bump is: +When Slint, wgpu or rawler is bumped, the version here stops matching and +cargo warns that the patch is unused — the build then silently goes back +to the unpatched crate. So a bump is: 1. Copy the new version in beside the old one, as its own commit. 2. Re-apply the diff from `git log -p` on the old directory. 3. Point `[patch.crates-io]` at the new directory and delete the old one. -4. Re-check on the device (below) — both patches are behaviour that only a - rotated Android display exercises. +4. Re-check what the patch was for. The wgpu-hal and Skia patches are + behaviour that only a rotated Android display exercises (below); the + rawler one is a linear DNG wider than 16 700 pixels, such as a + panorama the merge writes. Drop a patch entirely once upstream has the fix; each section says what upstream change would make it unnecessary.