0.22.0's first launch on the tablet: QNN could not create its device
(QNN_DEVICE_ERROR_INVALID_CONFIG), the session built anyway with every
node on the CPU behind the provider, and the probe timed that - 28.5 ms
against the CPU's own 19.4 - and rejected the Hexagon. The verdict was
cached under the fingerprint, so every model stayed on the CPU on every
later launch: AI denoise took 30-131 s a photograph instead of seconds.
The same A16W8 detector with the APK's own libraries runs on the HTP in
4.4 ms.
The probe's Hexagon session now sets session.disable_cpu_ep_fallback, so
a device that cannot take the graph fails the probe instead of being timed
as the CPU. Only the probe: shipped graphs may keep nodes on the CPU on
purpose. And a selection that fell back to the CPU after an accelerator
failed or lost is probed again on the next launches, up to three probes
per fingerprint; a cache written by 0.22.0 reads as never retried, so the
tablet probes again once this is installed.
AI Denoise's Apply switch becomes Method: Bilinear, Fast, Medium, Best,
default Best, so an untouched raw writes nothing and develops through the
mixture. `apply` is still read and never written: 0 is Bilinear, 1 keeps
a network already chosen.
- Best is the mixture of a flat and an edge expert with a learned gate;
Medium and Fast are students distilled from it. 2.48 s, 0.79 s and
0.57 s for a 20 MP frame on TensorRT fp16.
- Each network carries its own tile border (256 for the mixture, 192 for
the students) through `dr_denoise::Shipped` and `TileNet::halo`.
- The file is hashed once at open and each network keys its own cached
result; Bilinear keeps the result in memory for the way back.
- Each has an .a16w16 sibling for the Hexagon: 0.00 dB on the 6D gate,
at most 0.11 dB with the noise scaled x0.5 to x4.
- APK BUNDLED 19 -> 23; the PKGBUILD installs all three.
A 20 MP frame spent 0.32 s outside the network: each tile's mosaic and
sigma gathered on one thread, then its 24 MB output copied out of the
runtime and back into the frame, all in series with the device. Tiles are
now gathered on every core by a producer thread one tile ahead, so the
gather overlaps the run; the centre is written back across cores; and the
tile interface hands its inputs over and lends its output, so neither
side is copied. With a stand-in network that does nothing, the tiler's own
time falls to 0.14 s at the 1408 tile and 0.09 s at 2048. The exactness
and Bayer-phase tests are unchanged and pass.
The app's hot-pixel pass takes gross defects only; at ISO 6400-25600 a 6D
frame keeps 1000-2000 photosites more than 8 sigma beyond all their
same-colour and adjacent neighbours, which the network turned into specks.
The same two tests with the threshold in the photosite's own sigma, plus
the factor of two that keeps a bright point of light (where 8 sigma is a
sliver of the signal). The next model is trained behind exactly this; on
an ISO 25600 frame the Rust and training code both repair 935.
Photographs opened with an earlier Lightroom edit now import its HSL
saturation as fitted against the library's own Lightroom 6 exports, rather
than one band to one band.
Measured on two looks' exports and their raws (darkroom-lrfit, hsl_map_fit),
by encoded hue: Lightroom's saturation bands act about 45 degrees either
side on our wheel, wider than ours, and not all at our strength. Each is now
shared between two or three of our bands — Aqua mostly cyan and azure, where
skies are; Blue mostly blue and violet; Orange, where skin is, at about 0.4
of its value. Values add when two of Lightroom's bands share one of ours.
On the measured skies the import now lifts muted sky blues about 1.9× against
Lightroom's 2.1×, where it gave 1.15×. Hue and luminance still go one band to
the band of the same hue; they were not measured.
Every photograph with a colour-mixer saturation edit now renders differently:
a raised band is stronger, most of all on muted colours.
The mixer matched bands and judged saturation on scene-linear values, and
scaled chroma by the same factor whatever a colour started at. Against the
photographer's earlier exports of two looks (~90 photographs, their raws, by
encoded hue), a sky band raised by 58 there lifted muted sky blues about
2.1×; here the mixer gave 1.15×, and less in the muted tones that carry most
of a sky or a shadowed snowfield.
Bands are now matched and saturation judged on display-encoded values. A
raised band pushes muted colours hardest and tapers to nothing at full
saturation, at a gain of 3.0, which at the same value lifts muted sky blues
about as those exports did. Lowering saturation still scales every colour
alike. Hue shifts work on the same encoded colour; luminance still scales in
linear light.
The bundled presets that use the mixer, and those whose colour was tuned
against the default rendering, are rescaled to the amount of colour they
had: Vivid 1.30, Vivid warm 1.30, Vivid landscape 1.38, Vivid, strong 1.45,
Vivid portrait 1.15, Punch 1.12, Blue sky 1.08, Deep blue sky 1.12, Polariser
1.26, Blue sky, golden land 1.13 — mean CIELAB chroma over the default
rendering, on 30 raws from the library. Negative values (skin protection)
are left as written.
Every raw rendered through a camera profile — the library's DNGs with an
embedded profile, and CR2s given one — now renders differently: more
colourful in near-neutral tones. The profile's look table is no longer
applied unless its slider is raised; PROFILE_LOOK names the strength the
profile states.
Against the photographer's earlier exports with no look applied, the default
rendering scores the same with the look table at 100, 50 or 0 (held-out MSE
140, 140, 143), and is 9 % more colourful at 0: the table lowers the
saturation of near-neutral tones, which is exactly where the default
rendering was short of those exports. The user chose more colour.
The engine knew f32 and int8, and gave the Hexagon int8 for every role it
served. Measured on the tablet itself (inference.md §1.5), int8 lost
5% of the detector's faces at 40-80 px, moved the landmarks 1.5 px,
emptied the segmenter's scores and cost the denoiser 5-9 dB; fp16 the HTP
refuses outright. `Form` gains A16W8 and A16W16, and `Rung::form` now
names one per role: detectors and landmarks A16W8, the segmenter, scene
model, border filler and denoiser A16W16, XFeat int8. The embedder and
the eye classifiers stay on the CPU.
Each loader resolves its `<stem>.<form>.onnx` sibling; the segmenter and
XFeat, compiled into the binary, embed their quantised forms on Android
only and pick through `choose_embedded`. The probe, the compile step and
the cache fingerprint follow the form instead of assuming int8. Detectors
on the new form write `scrfd_*_a16+w600k_mbf`, and `model_ids` answers
for all three spellings.
On the tablet (ORT 1.29 + QNN 2.42), each shipped file against f32 on the
same inputs, and against the CPU's f32 time:
SCRFD 500m/2.5g/10g A16W8 100% of faces in every band 4.2/5.1/9.0 ms vs 17/56/198
landmarks A16W8 0.25 px in the 192 crop 0.5 ms vs 2.8
YOLO26n-seg A16W16 98.2% found, mask IoU 0.994 12.9 ms vs 90
scene model A16W16 98.9% of cells agree 15 ms vs 151
MI-GAN A16W16 41 dB from f32 in the fill 87 ms vs 488
XFeat int8 pano alignment 0.45 px (f32's own spread 0.41) 6.5 ms vs 58
denoiser A16W16 0.00 dB at every ISO 95 ms vs 1510 a tile
Face numbers are over public COCO val2017 photographs, not a library.
The APK carries the siblings (BUNDLED 15 -> 19; the old int8 detectors
removed), about 43 MB more. The Windows installer and its CI count skip
them; the Arch and Flatpak packages list their files and never had them.
The ladder example takes a role per model, which is how the per-role
forms above were seen landing on the NPU from the real probe.
The hot-pixel pass could only repair: it returned how many photosites it
changed and threw away which. find_hot_pixels runs the same pass and
returns them as sensor coordinates, leaving the frame alone, so a sensor's
defects can be tracked across frames.
sensor_scan prints each frame's candidates, and with --probe reads a list
of coordinates back out of every frame. Run over 53 6D raws from 2015 to
2026, it found 32 persistent defects, 2 in 2015 and 32 by 2026, and showed
what a defect map has to account for: a frame that does not flag a
photosite proves nothing unless its neighbourhood is dark, and the 6D
hides some of its defects itself above ISO 5000. docs/dev/sensor-health.md
records the findings and the design they argue for.
Presets were the one piece of the photographer's work that never left
the device: faces, sidecars, albums, collections, keywords and camera
profiles all travel with the sync pass, the preset library did not.
It now goes to <derived>/presets/library.drpl. PresetLibrary::merge
decides each name against the base the last exchange left (kept per
library beside place.json), so presets added on two devices both
survive, a deletion reaches the other device instead of being restored
by it, and an edit outlives a deletion made elsewhere. The upload is
If-Match / If-None-Match on the server's copy, and a 412 reads and
merges again, so two devices exchanging at once cannot save over each
other. A server copy that will not parse (a newer build's) is left
alone, and a local file that will not read stops the exchange rather
than being taken for an empty library.
The develop view's save merges with the file when the sync changed it
since the view read it, and a sync that brought presets reloads and
redraws the list.
Also corrects the register, which still said camera profiles do not
sync.
On the default rendering, Vivid added 22 % more chroma than the rendering
itself, and Punch 5 % — less than the photographer's earlier exports show
with no look applied (14 % over ours) and well below their everyday look
(27 %). Each preset's colour values (vibrance, saturation, the mixer's
saturation bands) are now scaled together, tone values untouched and
negative ones — Vivid portrait's skin protection — left as written, until
the preset measures: Vivid and Vivid warm 1.30, Vivid landscape 1.38, Vivid,
strong 1.45, Vivid portrait 1.15, Punch 1.12. Measured as mean CIELAB chroma
over 30 raws from the library, as a ratio to the default rendering.
Contrast2012 and the four recovery sliders were imported one to one. They do
not mean the same thing here: fitted on the library's Lightroom 6 exports and
their raws — each photograph's sliders carried across as slider × factor, one
factor per slider, on about 90 exports with no look applied, on the Camera Raw
default rendering — ours needed contrast at about a tenth (Lightroom's −100
imported as ours flattens a frame to grey), highlights ×1.4, shadows ×1.9 and
blacks ×1.25. Whites fitted below 1 every time without agreeing where; 0.5 is
a hedge, and says so. Vibrance stays one to one: the op itself is now
calibrated to Lightroom's.
The learned demosaic was an option under Detail, off by default. It is
now how a Bayer raw is developed: on by default at full strength on
every device — which hardware runs it is the inference engine's choice
— and first in the Adjust panel, since it decides what every control
below is applied to.
Strength (0-100, default 100) replaces Keep grain: grain = 100 -
strength, the same luminance-only blend, so moving it is one GPU pass
and never a re-run. 0.21.0's sidecars stored grain; it is still read,
as the inverse, and never written.
With it on for every photograph, the result is now kept on disk
(denoise.md §7.1, §12): the network's output as half floats, keyed on
a SHA-256 of the file's bytes and the model, oldest first past a 5 GB
budget, beside the inference engine's cache. A reopened photograph and
an export of one already developed read it back instead of running the
network again; a damaged entry is a miss.
The macOS ladder was the CPU provider alone, with CoreML listed as a gap.
It is now CoreML, then the CPU, then tract — unmeasured, since nobody here
has a Mac, and safe to ship unmeasured because the probe's clock rejects a
CoreML slower than the CPU and `attempt` refuses one that crashes.
- `Rung::CoreMl`, a compiling rung like TensorRT: an ML Program with every
compute unit allowed, falling back to the CPU until each model's program
is built. The embedder stays on the CPU, as on the Hexagon (§7).
- The cache is one directory per model and runtime version. CoreML keys a
model committed from memory on its input and node names, not its
weights (ONNX Runtime 1.29, coreml_execution_provider.cc), so two
exports of one architecture would otherwise share a program.
- The fingerprint on macOS is the chip and the OS release, which ships
CoreML.
- The desktop looks for the runtime in the bundle's Contents/Frameworks
and Homebrew's prefixes; fetch-desktop-runtime.sh on a Mac downloads
ONNX Runtime 1.29.0 for Apple silicon, which carries CoreML.
docs/dev/macos.md says what exists, how to build it, and which log lines
to ask a Mac user for.
The probe runs in the app's process, and a provider can fail by aborting
rather than by returning an error — XNNPACK did on SCRFD. A rung that does
that once would do it on every launch, before the first photograph is on
screen.
Every session build above the CPU, the probe's and each background
compile's, now writes what it is attempting to `attempt` in the cache
directory first and removes it after. After two launches in a row that
died inside the same attempt it is refused and recorded — a rung in
`failed`, an engine in the new `refused` — until the fingerprint changes.
Two, not one, because quitting during a TensorRT compile leaves the same
file.
A native session's messages went to ONNX Runtime's stdio logger, which is
nowhere once the app is launched from a menu — and what a provider says
while partitioning a graph (nodes taken, operators declined, a library that
failed to load) is most of what a failed rung tells you. Each session now
forwards them to `log` under the target `onnxruntime`: warnings always,
the runtime's info lines at `debug`, its verbose lines at `trace`.
The calibration commits named DarkRoom's default curve after another
product and described vibrance as doing what another editor's does at
the same value. The curve is the DNG SDK's reference, so it is called
that; vibrance is scaled to deliver the strength its value names, as
measured against the photographer's earlier exports. Two test names
follow. The measurements and where they came from are unchanged.
Vibrance delivered about a third of its nominal effect. Its falloff measured
saturation on scene-linear values, where an ordinary tan reads as 0.78 and
keeps a twentieth of the effect; and its skin guard halved it wherever red led
green led blue — 38 % of the pixels of the gallery's exports, every warm colour
rather than skin. The Vivid presets lean on vibrance, which is why they added
less colour than their values promised.
Saturation is now judged on display-encoded values, the guard covers skin hues
(about 10-50 degrees, not strongly saturated), and the gain is fitted: on 45
Lightroom exports whose only colour setting was Vibrance (about +24), the
measured-to-nominal scale was 1.9 before and 1.08 at a gain of 1.2, so 1.3.
Decides D21 by measurement. The photo gallery holds Lightroom 6 exports of
raws in the library, each carrying its Camera Raw settings; clustered by
those settings, 663 had no look applied. On 60 of them with their raws,
a third held out, the held-out MSE against Lightroom's JPEG was about 1200
for 0.20.0's sigmoid (0.7 EV darker and flatter), 224 for the DNG reference curve
after baseline exposure, and about 140 once its input is bent by 1.5/1.4
about grey.
So the curve choice defaults to the DNG reference, keeping its index (sidecars
record it), and the default contrast is 1.5. Contrast under the DNG reference curve is
now a power relative to REFERENCE_CONTRAST (1.4), where the table is
untouched; the sigmoid at that contrast still matches the retired base
curve. JPEGs are unaffected: the view transform skips a rendered source.
The learned-denoise branch merged while this one was open, and two of
its test fixtures build a RawImage without the baseline_exposure field
this branch added; zero is the no-op value.
One sample of the ACR3 table is 0.70711, which clippy reads as an
approximation of 1/sqrt(2). It is the curve's published value, so the
lint is allowed on the table with that reason rather than the number
replaced. And the pair-count check uses is_multiple_of.
The view transform's second curve is the DNG SDK's published reference
rendering — the ACR3 default curve applied by RefBaselineRGBTone — so
it is the "DNG Reference" curve in the panel, D21 and the code, not a
name borrowed from another product. Comments and docs that justified a
choice by another editor doing it ("as their Amount", "so a
photographer arriving from it finds the name") now give the actual
reason. The Vivid presets no longer describe themselves as reaching
for another editor's look; they are DarkRoom's own.
Factual mentions stay: which program wrote the library's DNGs, what
was measured against, and preset import. camera-profiles.md gains §15,
on starting a photograph from the edit it already carries.
The library's DNGs carry a Lightroom house look in their embedded XMP
— Blue +58, Aqua +50, Yellow and Purple +23, Highlights -40, Blacks
-20 on most — and that, not the camera profile, is why the same files
look richer in Lightroom. The importer skipped exactly that part: the
HSL panel was on its list of structures it did not translate.
Lightroom's eight HSL bands now map onto the colour mixer, hue,
saturation and luminance each one for one: Aqua to our cyan and Purple
to our violet, the nearest of our twelve bands by hue; chartreuse,
spring, azure and rose are left alone. The figures are a first
translation that lr-fit's measurement against Lightroom's output may
yet scale.
read_embedded finds the XMP packet in a photograph's bytes by its
delimiters and translates it, or answers None for a file whose XMP has
no Camera Raw settings — darktable's sidecars, a camera's own packet.
A test reads the library's _MG_9080.dng when it is present.
The Camera Raw default rested on comparing against Lightroom previews
of photographs that carry the user's Lightroom edits — HSL saturation
Blue +58, Aqua +50 and more, Highlights -40, Blacks -20, in every
DNG's XMP — so it measured the house look, not Camera Raw's base
rendering. Under the ACR3 curve _MG_9080 renders brighter than its
Lightroom preview (mean 0.39 against 0.31).
So the curve choice's first variant, the default, is the sigmoid again
and every raw renders as in 0.20.0 apart from baseline exposure. D21
and camera-profiles.md §12 now say the default is open, to be decided
by measuring against Lightroom exports of unedited photographs. Tests
that are about Camera Raw's tone choose it explicitly.
The rendering half of camera-profiles.md §11-§12 (D21). The view
transform gains a curve choice — Camera Raw (the default) or D19's
sigmoid. Camera Raw converts to linear ProPhoto, clips to [0, 1], runs
the curve on the largest and smallest channel and places the middle
one at its old fraction between them (RefBaselineRGBTone), and
converts back: hue kept, saturation raised where the curve is steep,
which is where Adobe Standard's look desaturated. White sets the input
scale (1 at its default, so sensor white is display white) and
contrast bends the input about grey (1 at its default).
The curve rides in the profile buffer after the tables: the profile's
own, else the ACR3 default, which the placeholder every profile-less
source binds also carries — so a CR2 with no .dcp still gets Camera
Raw's tone. Baseline exposure is a gain folded into the rendering
matrix at upload; RawImage::color_matrix stays the file's for the
merge's linear DNG.
camera_raw::apply_reference is the CPU statement; GPU tests hold the
shader to it on 256 colours and on greys against the ACR3 table. The
sigmoid's own tests now choose it explicitly.
The decoding half of camera-profiles.md §11-§12. RawImage gains
baseline_exposure: the file's BaselineExposure plus the chosen
profile's BaselineExposureOffset, as the DNG SDK sums them (+0.25 for
the library's 6D DNGs). A profile copied out of a DNG carries that
DNG's baseline as its offset, so the body's CR2s, which have none,
land at the same total.
ProfileTables gains the profile's ProfileToneCurve, resampled at
decode onto 1025 points with a natural cubic spline; an identity curve
counts as none. dr-types now holds Camera Raw's ACR3 default curve,
RawTherapee's adobe_camera_raw_default_curve copied value for value,
for every raw whose profile has no curve. Nothing renders through
either yet.
A Bayer photograph keeps its mosaic in the session and is offered the AI
Denoise switch. Asked for, the network runs on the decode executor from a
hot-pixel-repaired copy — the app's own pass — with the frame's noise from
its best source, and its progress in the activity bar; the classical
demosaic shows until the result lands, and the finished job says where the
noise figures came from. Keep grain is a GrainBlend of the two, made once
per value; the render draws it as its source and the adjust pass never
knows. demosaiced stays the classical result, so the raw histogram, the
white balance picker, masks and segmentation still read the sensor.
The develop view reconciles on a 250 ms poll rather than on each way an
edit can change (slider, undo, preset, version, a sidecar from another
device): two comparisons when nothing changed, and no path that can forget.
A failure is not retried until the switch is toggled. An export of a
photograph that asks for it waits for a running job or computes it.
Whether to use the learned denoise, and how much grain to keep, are what a
photographer sets, so they travel the one road every setting does: published
as a capability, captured by Preset, stored in the sidecar, replayed by the
undo stack (FR-DEV-3c). Published only on a photograph that can take it, for
the lens switch's reason; the availability is derived from the file and is
not in the state. Off by default, grain 0; a reset returns both.
DemosaicedImage::from_rgb_f32 takes the network's linear camera RGB and
stands it beside the classical source of the same photograph: the matrix,
profile tables and as-shot balance are that source's, the id is new, so
nothing downstream can tell which demosaic ran and every cache keyed on the
source sees a new one.
GrainBlend is the denoise's live control. It returns only the brightness of
the noise the network removed, taken after the as-shot balance and handed
back divided by it, so the grain is neutral in the finished picture; colour
speckle and demosaic false colour stay out. It writes a new source rather
than adding a term to the adjust shader: the blend depends on two images and
one number, a 20 MP pass is milliseconds, and a fresh source id is all the
adjust pass's caches need. The test reads it back: at 0 the network's
result, at 1 the same white-balanced step in every channel.
The noise model takes the best source the frame has: the body's measured
table (the Canon EOS 6D's, from the library), the DNG's NoiseProfile, or
the frame itself — read, row and column noise from its masked border, and
only the shot gain estimated, from the quietest flat patches. Checked on
130 6D frames, the estimate is within 10 % from ISO 1000 up; the network
loses under 0.3 dB for a sigma off by 15-20 %, so every Bayer body is
eligible.
Tiles of 1408 keep their central 1024 behind a 192-photosite halo, past the
185-photosite receptive field, and the frame is extended by reflection,
which keeps every photosite's colour; a pattern that starts on another
colour is read from one photosite up or left so the network sees RGGB, and
nothing is cropped. The tests run every Bayer phase, tiled against whole,
with a stand-in network of known reach.
The model ships as models/denoise/mosaic-1408.onnx (LFS), trained in
darkroom-denoise on the maintainer's own photographs, GPL like the code.
denoise_raw runs a file end to end: on a 6D frame at ISO 8000 the result
matches the training repository's own path to 2.5e-4 at worst, and takes
3.1 s on TensorRT fp16 (75 dB from f32) or 14.4 s on the CPU.
The probe built one zero tensor from the first input and ran the session
with it. Every model so far had one input; the denoiser has two (mosaic and
sigma), so every rung failed with "Missing Input: sigma" and the role was
left on the CPU: 14.4 s for a 20 MP frame where TensorRT fp16 takes 3.1 s.
Zeros now go to each input by name.
The converter's measured noise for the body at that ISO, (S, O) per CFA
plane: the learned denoise's best source for a body with no table of its
own (denoise.md §3.3). Read from the header beside the colour tags, and
printed by rawinfo. Checked against tifffile on a 6D DNG at ISO 5000: all
six values agree.
The learned demosaic-and-denoise (denoise.md) runs through the engine like
every other model. fp16 cost it nothing measurable (0.00 dB at every ISO on
validation tiles), so it takes TensorRT's and MIGraphX's fp16 like the
detectors. int8 cost it 6 to 9 dB, far past a 0.5 dB gate, so the Hexagon
refuses the role outright rather than relying on no int8 sibling existing,
and the tablet runs it on the CPU.
The learned demosaic replaces the classical one and takes its input, the
mosaic hot_pixels.wgsl leaves (denoise.md §2), so its training data and its
input in the app must come through that pass and not a lookalike. The pass
was recorded inline in Demosaicer::run; it is now built by hot_pass and
recorded by record_hot_pass, which run still uses unchanged, and
Demosaicer::repair_hot_pixels runs it on its own and reads the mosaic back.
mosaic_dump moves to dr-gpu to call it, records how many photosites changed,
and keeps --unrepaired for a raw readout.
denoise.md §4.4 requires the training repo to read photosites through
dr-decode, not LibRaw, so black and white levels, the active area and the
CFA phase match what the app will feed the network. mosaic_dump reads
`input<TAB>prefix` lines and writes the whole readout as .npy plus a JSON
of what decode and metadata report. The masked border is kept: its
optically black photosites are a free dark frame for the noise profile.
Measured after building them, on four of the library's 6D DNGs: Adobe
Standard's tables lower mean saturation by 3-9 % at defaults, and the
look at 200 % lowers it further. The 6D's look table scales saturation
by 0.925 in its darkest value rows; it was tuned to sit under Camera
Raw's default RGB tone curve, which DarkRoom does not apply, and the
dark-tone desaturation is what is left without it. On _MG_9080 the
Lightroom preview measures 0.49, the matrix alone 0.38, the profile
0.35.
So the spec's "that gap is most of why the same file looks flatter"
was wrong: the gap is tone. The tables stay — they put each hue where
Adobe put it — and the spec, D20 and the Vivid file now say so.
"Stronger camera look" is removed: a stronger Adobe Standard look is a
less saturated picture, the opposite of its name. The Vivid presets,
measured at 0.40-0.46 on the same frame, are what answers "more
colourful" today.
Diagnostic only. "matrix" switches the camera profile off and
"look200" doubles its look, so a DNG's tables can be judged against
the matrix render; "preset:<name>" applies a shipped preset as the
menu does. The example now renders through render_detailed, the path
every frontend takes, because a preset with clarity in it composes a
detail stage that plain render refuses.
Five looks for "more colourful than the default": Vivid, Vivid strong,
Vivid landscape, Vivid warm and Vivid portrait. They lean on vibrance,
which lifts muted colours most and holds skin back, and use saturation
sparingly on top; landscape and portrait work the colour mixer's bands
so foliage and sky get richer while skin does not. A sixth, Stronger
camera look, pushes the camera profile's look table to 175 %, about the
step from Adobe Standard to Adobe Vivid, and only moves vibrance where
a photograph has no profile.
All change only what they name, so they keep a corrected exposure or
white balance, and the shipped-preset tests bound every key and value.
The second half of D20: a camera_profile scene operation at order 25
that converts working colour into linear ProPhoto, runs the DNG SDK's
HSV lookup through the HueSatMap and then the LookTable, and converts
back. Hue and saturation do not change under the uniform gains before
it, so a 2.5-D HueSatMap gives the same answer as straight after the
matrix, and the look sees the photographer's exposure as it does in
the SDK. Two departures for scene-referred values: value is not
clamped on the way out, and a colour outside ProPhoto passes through.
The operation holds only the switch (on by default) and a look
strength of 0-200 %. It is composed while the switch is on — a new
Operation::composes() separates "does something" from "moved from the
defaults", so an untouched raw renders through its profile and still
writes nothing. The tables come from the source: dr-gpu uploads the
ones DemosaicedImage carries into a storage buffer at @binding(8),
whose two-entry header tells the fragment whether there is anything to
apply, and binds a header of zeros for every other source.
apply_reference is the lookup on the CPU. The GPU test holds the
shader to it over 256 colours, through synthetic tables strong enough
that a wrong index shows, and through the library's real Adobe
Standard tables when the 6D DNG is present.
The first half of D20. dr-decode now finds a camera profile's HueSatMap
and LookTable in the order camera-profiles.md §4 gives: the profile a
DNG embeds, then a .dcp in the profiles directory whose
UniqueCameraModel names the body, then none. A .dcp brings its own
matrices, since its tables were measured against its forward matrix.
The HueSatMap is blended for the frame's colour temperature with the
same mired weight the matrices use, once per decode, and the result
rides on RawImage as profile_tables beside color_matrix, so every path
that renders a decoded file gets the same profile without a setter to
forget. Nothing applies the tables yet.
A profile whose embed policy allows copying can be written back out as
a .dcp (rawler's TIFF writer with the RC magic patched in), which is how
the library's 6D CR2s will get the Adobe Standard their DNGs carry. The
table type lives in dr-types because decode, pipeline and GPU all need
its layout. Tests read the library's 6D DNG when it is present.
A queued export could not see the server, so its name check always
answered "free" and the upload PUT over whatever was there: Increment
and Skip behaved as Overwrite on Nextcloud, and two exports of the same
name queued before either uploaded landed on one file.
The batch now names around what the album records of earlier exports
and what the outbox already holds for that folder. The outbox record
carries the policy, and the drain lists each destination folder once
and applies it against what the server holds: Increment steps past a
taken name and re-points the album's row, Skip drops the entry. A
record without a policy (older builds, a merge's composite) is sent as
named, as before. The album is recorded before the drain starts so a
rename has a row to move.
The merge weighted every overlap pixel by its distance from each frame's
edge, a 200 px linear cross-fade. Anything the frames disagreed on —
parallax in the near foreground, grass in the wind, a walker — came out
twice at half strength: a soft double edge at 1:1.
dr_pano::seam picks, per output texel at proxy resolution, which frame a
pixel comes from. Where a new frame overlaps the composite the cost is the
gain-corrected difference plus local detail plus nearness to either
frame's edge, taken as the worst over a small window, and the cut is a
dynamic-programming path across the overlap. merge.wgsl weights each frame
by its tent-filtered share of that map, a 64 px blend that follows the
seam, with the edge feather kept as the fallback. The page's preview uses
the same map, and examples/merge.rs takes --feather-only for comparison.
WhatsApp strips every EXIF tag and names the file "WhatsApp Image
2023-06-15 at 07.00.42.jpeg"; Windows Phone, Android cameras and
darktable's import put the date in the name too. Those images sorted
after everything else and were absent from the timeline.
name_dates reads a date (and a time, when one follows) from the file
name, then from the innermost folder that states one. A sequence number
after a date is not read as a time, and a bare year folder is not a date.
EXIF always wins: only examined rows still undated are filled.
The sweep and the metadata repair fill as they mark an image examined,
and the open backfill fills catalogs examined by earlier builds. On the
reference library that takes 274 undated images to 10; the no-op case is
a seek on images_captured, 0.6 ms an open.
Four bar handles at the midpoints of the sides, each moving only its
own side along the axis across it. The overlay reports an edge as 0.5
on the axis it does not move, so the anchor Rust takes is the middle
of the far side.
Under a ratio lock the dragged axis leads. with_aspect grew the short
axis onto the ratio, which for an edge pulled inward made the untouched
axis the leader and pushed the edge straight back out.
One lookup, natural_span, maps a photograph's aspect to the columns its
cell spans, and the same number names its thumbnail class, Wide2, Wide3
or Wide4, 512 pixels of long edge per column, so a 4:1 panorama is as
sharp across four columns as a frame is in one. The boundaries are
where the two neighbouring cells would leave the same share of
themselves undrawn, sqrt(s(s+1)): 2.45 and 3.46, with the first at 1.9
so a 3:2 frame stays a frame. A grid too narrow for the class falls
back to the widest that fits, the tablet gives the whole row, and a
cell asks for the class it is actually drawn at: its span, but never
more than its own class. The merge renders each wide class up to the
composite's own, which covers every fallback.
A 4:1 composite drawn in one square cell is a strip a few pixels high.
A photograph about twice as wide as it is tall (1.9 and up) now spans
two columns, three from 2.9, with a thumbnail class of its own whose
long edge is sized for that width; on the tablet, or where the columns
are too few to put it beside anything, it takes the whole row.
Rows are computed in one place, library_ui::layout. The grid is a
lattice of slots: each cell is drawn at the slot Rust gives it, and a
wide cell that would not fit in what is left of a row starts the next
one, leaving the gap empty so the grid still reads in capture order.
The scrollbar spans the slots, a scroll reports a slot that the layout
turns back into an ordinal, and scrubs, restores and the cursor go
through the same conversion. Up and down step by rows through the
layout rather than by a row's worth of ordinals; left and right, a
shift-click's run, burst folding and the timeline are ordinal-based
and unchanged.
The window's own read carries each photograph's w and h, so the cells
know their shape with no query per cell. Where the wide ones sit in
the whole list is one query, run when what the grid lists changes or
when a window finds the layout out of date, and a library with no
panorama answers it from a partial index created on first use
(images_wide), not a schema bump. The merge makes the wide thumbnail
for a wide composite along with the others.
A finished merge drained the outbox and started a rescan beside it. The
scan raced the upload: on a folder library the 800 MB copy was still
running when the folder was listed, on Nextcloud the upload takes
minutes, and either way the listing lacked the composite, recorded the
folder's validator, and nothing looked again until the next sync pass.
The job now reports what the catalog needs (the name, the size of the
picture it opens on, the capture time it wrote into the DNG) and the
library writes the row at once, in one transaction, keyed where the scan
will list the file; a composite with no time of its own takes its
sources' earliest. The grid reloads and shows it beside its sources.
The upload then gives the row what only the server knows: after sending
a file the catalog already has a row for, it lists the folder once, takes
the file id the server assigned, and records it (and, once the merge
makes them, the thumbnails waiting beside the payload) under that id.
Every drain now rescans when something landed in the library, after the
upload rather than beside it. A second merge of the same frames is no
longer named over the first: the name is checked against the catalog's
names in that folder, which is all that knows it once the outbox is empty.
D19 retired the per-body base curve, moved the matrix ahead of the
edits and the film into the view transform's place, but a dozen doc
comments still listed the curve among what a pixel passes through, or
said the film skipped it. The detail stage's module doc still drew the
matrix after the edits and the last detail pass encoding, which the
view pass took over. The film crate's README gave the base curves as
its reason for being data, and the ops README's list of hand-written
nodes had neither the view transform nor three of the five kernels.
FR-MRG-2 gave the base curve as why the merge cuts below the profile;
the view transform is why now. The decision table still said colour
defaults were a per-body curve, and FR-DEV-3j said only the default
view transform skips a JPEG, where the node skips one whatever its
sliders say. frame-budget.md records the view pass as unmeasured.
The presets menu and sheet listed every preset under flat section
headings, seventy rows to scroll past. They now list folders, closed
until opened, with how many presets each holds; opening one shows what
is inside it, folders and presets indented beneath.
A category is spelled in the name: "Portraits/Warm skin" is Warm skin
in a Portraits folder under Yours. The file format does not change, so
an older build lists the whole path as the name; renaming a preset is
how it moves, and saving or renaming into a folder opens the way to it.
A Lightroom import names what it reads after the folders below the one
chosen, and a "/" in a displayed name becomes "∕" so it files nothing.
The shipped film sections become Film › Colour, Cinema and Black and
white.
The tree is built and flattened in Rust (PresetTree), each row carrying
its depth, and which folders are open is remembered for the session.
A PopupWindow keeps the size it was shown at, so a folder opened in the
menu pushed its contents under "Save or manage…"; the menu is shown
again after each toggle to take its new height. That is a function on
the rail because Slint 1.17 generates Rust that does not compile for a
popup's close() reached from inside the popup. The sheet's list takes a
preferred height of up to 400px, since a Flickable reports next to
nothing and an opened folder showed three rows.
The manual describes the folders and naming. Its pictures show the menu
with Film › Colour open, and a black-and-white stock applied from Film
› Black and white; the scenes aim popup rows from the rail's entry, and
pick the menu's "Film" over the develop column's film chooser.