Record where a linear DNG too large for one texture goes

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.
This commit is contained in:
2026-09-27 19:42:42 -04:00
parent 6c749b469d
commit 4bd8d86c00
7 changed files with 67 additions and 20 deletions
+6 -5
View File
@@ -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" }
+20 -1
View File
@@ -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
+1 -1
View File
@@ -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. |
+13 -8
View File
@@ -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.
+10
View File
@@ -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).
+5
View File
@@ -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)
+12 -5
View File
@@ -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/<dir>` 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.