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`.
A crop handle straddles the picture's edge. With the photograph fitted
to the whole canvas, the bottom handles sat in the photo roll's band,
whose swipe handler takes every press inside it, and the bottom-left
one under the "Done Composing" row, which is declared after the overlay
and whose buttons press 44px tall. Both were drawn and could not be
grabbed: on a laptop-shaped window, one or the other for nearly any
photograph.
While composing, the canvas box is inset 16px from its clip and stops
above the band, the row's margin and the row's hit height. The band is
measured once, on the canvas, and the row reads it from there.
The panorama scene now ends in the grid, on the composite in its wide
cell beside the twelve frames it was made from, with the thumbnail the
merge made: the picture is taken straight after the merge, before any
rescan could have found the file. The manual's library section says how
a panorama's cell is sized and packed, and its panorama section that the
composite is in the grid the moment it is written. panorama.md §9 records
the race that kept it out, the catalogue-then-register path that
replaced it, how the thumbnail is developed, and the layout's classes.
A design draft for the learned stage outstanding.md §3 lists as missing:
a joint demosaic-and-denoise network that runs on the mosaic, in the
slot architecture.md §5.2 reserves for it, with its result kept as a
cache rather than a new file in the library. It covers the model and
tiling, synthetic training pairs from the library and a per-body noise
calibration, evaluation on real pairs, the Amount control, speed on the
tablet, X-Trans, and the decisions still open. Nothing in it is built;
figures marked "estimate" wait for the measurements that replace them.
The develop paragraph counted eighteen operations fused into one
dispatch, before the view transform made nineteen and a detail stage
made the fusion two passes, and said nothing of editing on the scene
with Tone Mapping last. Presets are now a menu on the tool rail, a
panorama frame can be left out, and a linear DNG larger than one
texture develops. The requirement count and coverage are
traceability.md's (193, 85.5%), and tiled rendering is no longer
wholly unbuilt. outstanding.md gains its sweep note for 0.19.0 and
says in §11 what the frame choice changed.
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.
panorama.md still had a frame that did not fit end the job, and FR-MRG-5
still said the merge stops; since 621a6b83 the frame is named on its
row and unticked, and the rest are solved again from the pairs already
matched. §15 records the split of align into match_pairs and solve and
why re-aligning a subset from scratch moved every RANSAC seed, and the
rule e98b1def gave a blown sample, which §8's "nothing about being a
RAW is lost" now mentions. The manual says where the boxes are and
what an unplaceable frame does to Merge.
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.
FR-DSP-2 and NFR-RES-2 said tiling was unbuilt; the export half of it
now exists for a linear DNG past one texture. The interactive path and
spike S6 are unchanged.
A 22927×8966 Lightroom panorama opened as its embedded preview with
develop withheld, because no texture could hold it. DevelopSession now
opens a linear DNG past PROXY_EDGE (8192) on a box-reduced copy, and
keeps the full resolution on the CPU. The canvas at fit, the thumbnail,
the histograms and the masks work from the copy; a render finer than
it — the canvas zoomed in, a tile of the export — samples a window cut
from the full resolution, kept while the view stays inside it. The
export renders in halo-grown tiles of 4096 and assembles them.
On the panorama: decode 1.6 s, open 210 ms, canvas 37 ms, a zoomed
window 130 ms, the full-size export 5.9 s.
DemosaicedImage::linear_rgb16_window uploads part of a linear DNG, or a
box-reduced copy of it, and says where it sits in the frame; size() now
reports the frame and texture_size() the texels, and the fused pass
writes the window into the shader's uniforms. EditGraph::source_region
finds the part of the source a view reads, and tiles::plan cuts a render
too large for one texture into halo-grown, grid-aligned tiles.
The GPU test renders frames a tile at a time from their own windows and
compares them with the whole: identical for point operations, within one
code value when straightened with clarity on.
A frame fraction was taken of the render's short edge, and render_scale
folds the zoom into the region the render stands for. Zoomed to 1:1 on a
corner, clarity, texture and dehaze drew a quarter of the halo the export
gets, though the docs promise they preview honestly at any zoom.
RenderScale now carries the whole framed frame (within), and a frame
fraction is measured against it; at fit nothing changes.
A photograph larger than one texture has to be developed from a part of
it or from a reduced copy of it. The generated prologue measured the frame
by the bound texture, so either would have moved every crop, warp and
grain seed. Two uniform vec4s now say which part of the frame the texture
holds and how large the frame is; the sampler maps into the texture
through to_window, and a texture holding the whole frame samples exactly
as before ((uv - 0) / 1 is uv to the bit).
The tone curve clamped its input to [0, 1] and its output back to 1.0
around a 2.2 gamma, mid-chain — master and per-channel both. So any
edit with a curve made every scene value above 1.0 the same number
before the view transform ever saw it: D19's fourth finding. Beyond its
last point the curve now continues along its last span, whose secant
is the tangent the spline already gives that point, so the join is
smooth and an identity curve stays the identity to any height. The only
clamp left is the floor at zero, where there is no light to curve.
FR-DEV-2's acceptance test is how the rule stays true.
`scene_referred_until_the_view` wraps every point operation, at
non-neutral settings, between a gain of sixteen and one of a
sixty-fourth, with an identity in the view transform's place, and
renders a ramp from 0.4 to 15.2 times sensor saturation through it. The
output must still increase with the input and still separate the top of
the ramp. Run against the previous commit's tone curve it fails with
[21, 29, 33, 33, 33, 33, 33, 33]; every other operation already passed.
It renders on a device because dr-pipeline has none, and the spec's
pointer to it says so.
A film stock ran at order 25, after white balance and exposure, and
everything after it — contrast, the curves, the colour mixer, the
grading, every mask layer's tone — acted on the film's display-referred
output, as though the frame had been scanned and then worked on. That
was a display-referred rendering in the middle of the chain, which D19
removes (FR-DEV-3f, FR-DEV-3j).
`film_sim` is now in `Stage::View` beside `view_transform`. While a
stock is loaded the composer emits it in the view transform's place and
not the sigmoid; otherwise the sigmoid. So every edit is a decision
about the exposure the negative receives, and the film is the last
thing that happens to the picture — after the detail stage too, in the
view pass, which already binds the film's tables, the mask array and
the grain's source position. Its per-layer settings blend there as they
did in the fused pass.
`op_renders` goes: a rendering is chosen, not suppressed, and the only
render with no view transform is the camera-space tap. The YAML order
moves to 190 so the panel reads in pipeline order; the stage, not the
number, is what places it.
Existing edits that combine a stock with tone or colour operations now
render differently: those operations used to act on the print, and now
act on the scene.
The fused pass stops at "linear working values" when a sharpener, a
blur or a repair follows, and the detail passes convolve what it hands
on. Until now it handed on the rendering: the base curve, and since the
last commit the view transform, ran before the store. So every kernel
worked on display-referred values while its comments promised the
opposite — D19's second finding.
A fused pass composed for a detail stage now stops before the view
transform, and carries a second shader, `ComposedShader::view`, composed
from the same inputs. It runs the same prologue, for the positions a
fragment reads (a film's grain seeds from `source_px`) and the corners
it blacks out, takes its colour from the detail stage's result bound
where the sample cache would be, and runs the view transform, the
output transform and the mask reveal. `render_detailed` dispatches it
after the last detail pass, in the same encoder.
So no detail pass encodes any more. Every pass writes an intermediate,
the last one included, which retires three things that existed only to
make the last pass encode: `writes_output` and the runner's second
layout, the body-less resolve pass for an active kernel with nothing to
draw at this scale, and capture sharpening's pass-through, which now
emits no pass at all. An empty chain is a whole render: the view pass
reads the fused result directly. The detail stage no longer takes an
output space either, so `compose_detail_for` folds into
`compose_detail` and the space is named once, on the fused half.
The cost is one full-render read and write per frame when a detail
stage exists, and a third intermediate for a one-pass chain.
FR-DEV-3j gives the view transform two controls, contrast and the white
point in stops above middle grey, persisted and held per mask layer like
any other setting. A scene-referred pipeline whose white point cannot be
moved hands the photographer a shoulder they cannot place.
`view_transform` is a hand-written node in `Stage::View`, a new stage
the composer emits last and emits whatever the node's state: a neutral
operation is otherwise left out of the shader, but a photograph with no
view transform is a scan. "Active" keeps meaning "moved from the
defaults", so an untouched photograph writes nothing for it and
`every_node_starts_neutral` still holds. A caller whose chain holds no
view operation gets a default one.
The composer's loop becomes an ordered list of steps — camera nodes, the
matrix, scene nodes, layer-only nodes, the view — so each is emitted in
exactly one place. The base curve could not be a node because it
belonged to the camera; the ops README records why that argument went
with it (D19). The panel shows it in the Light group as "Tone Mapping".
Tests that counted the blocks of a neutral graph now count one, the
view transform, and the two chain-wide tests that load a film expect the
view transform to be absent, since a stock replaces it.
The base curve was a five-point spline on the unit square, flat past its
last point: every value above 1.0 left it as the same number, per
channel. Exposure and highlight recovery put values up there, and the
curve threw them away, then handed the result on as though it were
still scene-linear. The six per-body curves were also, by their own
file's account, hand-tuned shapes rather than measurements, and not
enough is known about where they came from to keep them (D19).
In their place, one view transform for every body (FR-DEV-3j): a
log-logistic sigmoid per channel, with the middle channel put back
between the other two so a hue survives the shoulder. Its two free
constants are solved from two conditions rather than set: scene grey
0.13, where the retired default curve put it, lands on display 0.18,
and the scene white four stops above grey lands on 1.0. So a highlight
a stop past sensor saturation still rolls into white, and the midtones
stay within 0.26 EV of the retired default between scene 0.03 and 1.0.
`dr_pipeline::view` holds the CPU reference and the WGSL, and the tests
there are FR-DEV-3j's acceptance criteria.
It is still fixed and still in the fused pass's tail, so a detail stage
still sees rendered values; the next commits make it an operation and
move it after the detail stage. It is skipped for a JPEG, as the base
curve was, and absent from the camera-space tap.
The base curve's database, its lookup and its twelve uniform slots go.
`RawImage` and `DemosaicedImage` lose the field, and the GPU test that
proved a curve reached the shader is replaced by one that renders the
view transform against the CPU reference and shows two highlights above
1.0 still render apart. The JPEG-and-sensor test now asserts the two
differ by exactly the view transform, where before an identity fixture
curve had made them match.
Every point operation ran in camera RGB and the camera matrix came after
them all, against the order ARCH §5.2 draws. So `luminance()` applied
Rec.709 weights to a body's own primaries, a band in the colour mixer
was a different hue on every make of sensor, and the vibrance skin guard
tested channel order in a space where skin does not have one.
Operations now declare a `Stage`. White balance is the only camera-stage
node — its multipliers scale the sensor's channels, and after a matrix
that mixes them the same numbers are a different correction — and says
so with `stage: camera` in its YAML, a key both the build-time generator
and the load-time declared op read. The composer emits the camera nodes,
then the matrix, then the rest, each group in graph order; an empty
chain still gets the matrix.
Film simulation stops converting out of camera space itself, since it is
now handed working-space colour like every other scene node. The base
curve stays where it was, after the operations, and so now acts on
working-space colour; the next commit replaces it (D19).
The spec missed Ansel, and with it Aurélien Pierre's argument that a
display-referred curve early in the pipeline throws away what every
later stage needs. Read against that argument, the code broke it four
ways: the base curve was flat past its last point and so clipped every
recovered highlight; the detail stage was handed curved, clipped values
while its comments promised linear ones; the edits ran in camera RGB
because the matrix had drifted to after them, against ARCH §5.2; and the
tone curve clamped to [0, 1] around a 2.2 gamma mid-chain.
D19 records the decision: unbounded scene-linear colour from the matrix
to one view transform, last, after the detail stage. FR-DEV-3j specifies
that view transform as an adjustable operation (contrast, white) with a
default fitted to where the retired default curve put middle grey.
FR-DEV-3e retires the per-body base curves, whose own file called them
hand-tuned shapes of unknown provenance. FR-DEV-3f makes a film stock
the view transform when one is chosen, instead of a rendering at order
25 that the later edits acted on. FR-DEV-2 gains the acceptance test
that holds the rule, and ARCH gains §6.14 and the corrected §5.2 order.
A frame that did not fit ended the job with its name, and the only way
on was Back, a smaller selection and every frame read, demosaiced and
searched for keypoints again. Each row on the page now has a box. An
unticked frame is left out and the rest are solved again from what the
first pass measured.
dr_pano::align is split for it: match_pairs does the matching and the
pairwise RANSAC once over every frame (about 5 s for twelve), and solve
takes a subset of the frames and uses only the links among them (about
0.1 s). Solving a subset by re-aligning it also moved every RANSAC seed,
which are keyed on frame position, and on the fixture set that was
enough to lose a marginal link and strand a neighbour of the frame left
out.
A frame that cannot be placed no longer stops the job either. Its row
names it and Merge stays off until it is unticked; the headless example
leaves such frames out the same way, and takes --leave-out N to try it.
A clipped photosite reaches the merge as (1, 1, 1), which the as-shot
balance turns magenta. The preview balanced it with no highlight rule at
all, so every blown cloud was pink on the alignment page. The DNG had the
quieter form of the same fault: a frame's gain below one moved a blown
sample off the white level, and a feather mixed it into a neighbour's
real sky, so the develop's own desaturation no longer recognised it.
The merge shader and the preview now write a blown sample, before the
gain, as the camera value the composite's balance calls grey — the
develop pipeline's neutral, fading in from CLIP_ONSET.
Presets were a "Presets…" button in develop's top bar that opened a
sheet over the photograph, so applying one was two clicks with the
picture covered. They are now an entry pinned to the bottom of the tool
rail, apart from the tools because a preset arms nothing, and it opens a
menu beside the rail: the same sectioned rows the sheet lists, one click
to apply to the open photograph. The menu's last row, "Save or manage…",
opens the sheet, which keeps saving, renaming, reverting and importing,
since those take a name or a path.
The menu is a PopupWindow for the film list's reasons, with the list in
a clipped box of its own; the Flickable alone let its last row draw over
the manage row. Choosing from it zeroes preset-apply-count before
applying, since the grid may have left its selection count there.
The manual says where presets are now and gains a picture of the open
menu. The scenes reach the sheet through the menu and aim the manage
row from the rail's entry, as the film list's rows are aimed, because
the automation reports a popup's elements in the popup's coordinates.
windows.md §7 weighed the Windows leg against the desktop leg in CPU
time only. What actually stopped a release was disk: the one 99 GB disk
is shared with the container images, v0.18.0's desktop run ended at
92 GB used, and v0.18.1's release build ran out, which is why that tag
has no release page. §7 now gives those figures, says what the desktop
leg deletes before its release build since 209ae361 and why that costs
the cache nothing, and names a second target's cache as the first thing
to check if a leg runs out again.
Adds the release's line to the sweep notes at the head: FR-CULL-13's
write-path half and NFR-ARCH-1's executors, which now have entries, and
three things that closed without ever having had one — a mask layer's
film settings (FR-DEV-3f), hot and dead photosites repaired before the
demosaic (FR-RAW-3, with the defect-map reader still unwired), and the
tablet's scroll cue already described in §4a.
NFR-ARCH-1's register note says what b1d1c472 and 7be1efff met and what
they left, but outstanding.md, which exists to show the distance between
the register and the binary, had no entry, and catalog.md §6 still said
interactive work runs "on the decode pool with the I/O pool behind it"
when there are no pools.
outstanding.md §4 gains the entry: named and guarded, not bounded — the
counts are a budget, the guard covers block_on only, the two mask
workers and the core crates' threads are outside the module. catalog.md
names the executors the thumbnail and metadata work starts on and says
the counts are not yet enforced.
§2 still said FR-CULL-1 through -5 and -8 through -13 were built, while
the register's own status note (adbb9ac9) says only the write-path half
of -13 is met. The count now says -12, with -13 half built, and §2 gains
an entry for it: what verdicts.rs enumerates and holds to a reviewed
list, what fails the test, and the evidence that is not built — chips
for clipping, focus, burst membership and face counts, shown as absent
rather than zero, and a filter per signal.
fc54523 (0.18.1) stopped running a layer as a second chain after every
global operation and made its settings offsets applied at each
operation's own place, and nothing outside the code said so. The
manual still read as though a mask's slider were a setting of its own,
frame-budget.md described the masks as "a separate chain per layer",
and no document said how a layer is blended at all.
architecture.md §5.2 now says it: the offset, the blend by the layer's
weighted difference from the global result, what a photograph with no
layers composes to, and the film as the one operation whose settings
are averaged instead (6b99f67). FR-DEV-3 records the change as resolved
beside its local-adjustments bullet, frame-budget.md's note on what it
does not measure describes the cost as it now is, and the manual gives
the arithmetic in one sentence: -20 in a mask over -30 is -50 there.
The bundled manual is regenerated to match.
c50d96e added a repair pass ahead of the demosaic and no document said
so: FR-RAW-3's text names levels, the CFA and the colour matrices, and
architecture.md §5.2's stage diagram went from the upload straight to
black and white levels.
FR-RAW-3 now carries a status note: what counts as hot or dead, what
the photosite becomes, that Bayer and X-Trans share the pass, that
export and every other demosaicing path get it with no setting, the
test that holds it, and that the DNG defect map dr-decode can read is
still not used. §5.2 gains the stage and a paragraph on why it sits
before the demosaic, and its pointer to the raw histogram's tap names
the demosaic box rather than a row count the new stage would have made
wrong.
The manual said the lists on a tablet scroll by flick alone, and
outstanding.md §4a that Android had no scrollbars by design. Both now
describe the cue from #69: a thin line while the view moves, gone once
it stops, that a flick starting on it passes through; and §4a notes
DR_SCROLL_CUE for looking at it on a desktop, and that it is not yet
checked on the tablet. The bundled manual is regenerated to match.
On Android the desktop scrollbar is off, so every scroller that has one
on a desktop (the develop column, the grid, the collections sidebar,
Settings, the help sheet and the film list) gave no sign of how long it
was or where the view was in it. That is how the black-and-white film
stocks came to look deleted when the list could not scroll.
ScrollBar now has a second mode, chosen in the one Scrolling global:
where bars are off and `cue` is on, it draws the thumb alone, 3 px wide
against the edge, while the viewport moves, and fades it 500 ms after
the last move. It has no TouchArea, so a flick that starts on it
scrolls the content. Rust sets cue on touch-first builds; a desktop
build shows it when DR_SCROLL_CUE is set, to look at it without a
device. Desktop is otherwise unchanged.
Test: on the testing backend's help sheet, a press on the cue moves
nothing, a drag starting on it carries the list with the finger, the
cue is drawn while the list moves (drag, fling, wheel) and not once it
is idle; with bars on there is no cue and the thumb still takes a drag.
apply_judgement and apply_label carry TRACES: R7, the burst
representative callback FR-CULL-13 with a note that choosing it writes
the grouping and no verdict. The register's status for FR-CULL-13 says
the write-path test is met and the evidence chips are outstanding.
Traceability matrix regenerated.
`a_resaved_file_owes_a_reread_and_loses_its_stale_hash` failed once in a
full dr-catalog run (updated 0, expected 1) and passed three times alone.
The incremental scan tells a changed file by its mtime at whole-second
resolution, and the `resave` helper wrote and renamed the file within
the same second as the scan before it, so on a fast enough pass the
resave looked like no change at all.
The helper now sets the file's and its folder's mtime two seconds ahead,
which is what a real resave some time after a scan looks like. Only the
test helper changes; the scan's rule is right for real files.
§7.1 now says where its table lives in code, how the UI thread is
marked and guarded, and that the counts are a budget rather than a
bound until work is pooled by executor. The register's status note says
what is met — named, spawned through one module, block_on guarded and
tested — and what is not: pooling, a guard for synchronous file reads
and catalog queries, the two mask workers, and threads the core crates
start.
A hot photosite went into the demosaic as it was read, and came out as a
coloured cross three pixels wide that nothing later could take back
out. Night and long exposures showed them; the defect-map reader added
for FR-RAW-3 was never wired in, and a CR2 carries no map anyway.
A pass over the mosaic now runs ahead of the demosaic, into a second
buffer. A photosite is hot when it reads more than twice every
same-colour photosite in its 5x5 window plus 2% of the range, and more
than twice each of its eight immediate neighbours of any colour. The
second half keeps stars and glints: real light reaches the sensor
through a lens and an anti-aliasing filter and lights a patch, so the
photosites beside it are lit too, where a hot photosite's are dark. It
is replaced by its brightest same-colour neighbour, which invents
nothing. Dead photosites are the mirror case, judged only where the
neighbourhood is above 5%, so shadow noise clipped at black is left
alone.
The colour of each photosite comes from a 6x6 sensor-anchored tile, so
Bayer and X-Trans share the pass. Export and every other path that
demosaics get it too, and there is no setting: the repair only fires
where a single photosite disagrees with everything around it.
Cost, warm, on a Canon 6D frame (RTX 3050): 91-99 ms to demosaic
before, 94-98 ms after; the extra pass is inside the run-to-run noise.
Tests render a frame with and without the defect and compare the
finished pixels. Without the repair a hot photosite showed by 230 and a
dead one by 168; with it neither shows, and a 3x3 highlight at white
survives.
A layer offered the film's sliders and they moved nothing: its copy of
the node was never given the stock, so it stayed inactive. Film now
works in a layer the way the other adjustments do, as offsets to the
photograph's settings, but blended as settings rather than as results,
since a film is a rendering and cross-fading two developments is not
what a region on a pushed film looks like.
- dr-film bakes no slider. Exposure is a gain in the shader; push
interpolates the stock's measured processes, one curve row each; the
print is split at the paper's log exposure, so print exposure is an
addition between two lookups and exact at any setting. The enlarger
stays balanced at the photograph's exposure.
- film_sim reads all four settings as uniforms, format one-hot over a
grain count per format, so every uniform is linear in what it does.
- Operation::blends_settings lets the composer average each overlapping
layer's uniforms with the global ones by mask weight, the global
setting taking whatever weight the layers leave, and run the fragment
once. Three layers at full weight give the mean of their settings.
- The stock picker is hidden on a layer. Only the photograph's exposure
re-solves the print balance; push, print exposure and format need no
rebake at all now.
Reducing contrast turned every black in a night photograph pink. The
fragment lifted each pixel's luminance to its target by multiplying the
colour by target/luma. For a pixel at 0.001 on the way to 0.09 that is
a gain of ninety, and in the deepest shadows the channels are sensor
noise: after white balance the red and blue noise sits above the green,
their multipliers being nearly twice its, so ninety times that noise is
magenta.
Flattening now mixes the colour toward middle grey, which gives the
same luminance and adds the lift as a neutral. A black goes to grey and
its noise stays the size it was.
The same fragment clamped luma/0.36 into the curve's 0..1 domain, which
scaled every tone above twice middle grey down to 0.36 in either
direction: contrast +10 took a 230 grey to 162. Those tones are now left
where they are, which is continuous with the curve's top (value 1,
slope 0).
catalog.md §8.2 still said confirmed and rejected faces were matched to
local faces "by box", and that the merge reads only a remote face's box
and model; faces.md said `match_faces` "still matches by overlap alone
across devices". Since #77 the match falls back to embeddings, on the
photographs where a box leaves a remote face over, and since #78
`dedup_people` folds people of one name whose faces agree after every
sync. Both now say so, with the thresholds and the reason a less
decisive pair stays unmatched, taken from the code's own documentation.
Seven names are two or three live people on both devices: Ian (756
confirmed faces, and a second Ian with none), Jessie three times,
Claudine, Mathias, Noemi, Pascal and PJ. Each was typed on its own
device and carried across by sync, which keys people on their uuid and
so keeps both. Each half of a person shows half their photographs.
dedup_people::run, in one transaction:
- Same-name people (trimmed, case-folded as the Identity screen folds
them) merge into the one with the most confirmed faces, ties to the
smaller uuid, through faces::merge_people_within, so confirmations,
rejections and the survivor's name are kept. A person holding no
faces at all merges: there is nothing to compare or to carry. Anyone
else needs >= 2 confirmed faces per shared embedder on both sides and
centroids at cosine >= 0.7 in each. A face confirmed as one and
rejected as the other keeps them apart. Unnamed and set-aside people
are never merged by name.
- Faces held twice (one image, one embedder, IoU >= 0.5, cosine >= 0.7)
keep the stronger detector's row (FaceDetector::outranks), then the
confirmed one, then the older. The survivor takes the confirmed
assignment and both rows' rejections. A pair confirmed as two
different people is left and counted.
- Judgements still on a merged-away person move to the person at the
end of its redirects, and a redirect cycle (two devices merging one
pair in opposite directions) is broken at the smaller uuid.
Measured on copies of the desktop catalog and the tablet's server
snapshot, w600k_mbf, confirmed faces only:
- Centroids of differently named people: 2,699 pairs, median 0.02,
99.9th percentile 0.41. One pair reaches 0.70 (0.700 desktop, 0.705
tablet), "Michelle Casanonve" and "Michelle Casanova", one person
typed two ways. Next is 0.62/0.64, "Boris Jost" and "Boris". The
highest pair that is plainly two people is 0.43/0.44.
- One person split in random halves: minimum 0.69, median 0.91 over 72
people. Four faces against twenty-two reach 0.7 in 97% of draws.
One face against twenty of somebody else's reached 0.74 in 3,000
draws, and two faces reached 0.61, hence the two-face minimum.
- Pascal (22 and 4 confirmed) is at 0.57 and PJ (14 and 7) at 0.50,
under 0.7 on both devices, so both pairs stay apart and are logged.
The desktop's second Ian holds 4 suggestions and no confirmations,
at 0.38 against Ian's centroid, and stays apart. On the tablet it
holds nothing and merges.
Why a merge made here survives a peer on 0.17.0: the merged-away
person stays as a merged_into redirect with a bumped revision, which
the catalog merge has always taken on revision. The peer hides the
duplicate and never sends it back as a live person. Its own
confirmations of that person stay on the redirect, because a merge
never overwrites a local confirmation. The manual merge has always
left them there too. They follow the redirect when the peer runs this
job. A test syncs two catalog files through the previous merge code
and back, and the people converge and stay converged.
Once a catalog is clean the job reads 80 redirects, the named people,
and the face boxes from the covering faces_box index. That is ~10 ms
on the reference library. There is no schema change. The index is
created IF NOT EXISTS, as the merge already does.
merge_people moved a person's faces onto the target and left the
"not this person" rejections on the redirect. A rejection there binds
nothing: once Annie is Anna, the grouping pass is free to suggest the
face the user pushed away from Annie as Anna, which is the behaviour
rejections exist to prevent. The Identity screen's merge has done this
since it was written, and the deduplication job for #78 merges through
the same function, so it would have done it for every same-name pair.
The source's rejections now move to the target (INSERT OR IGNORE, so
one the target already holds is not doubled). Where the two halves
disagree about one face, confirmed as one and rejected as the other,
the confirmation stands, as `confirm` already rules for one face; and a
moved rejection withdraws a suggestion of the same face, as `reject`
already does. Confirmations and names are unchanged.
The body is split into merge_people_within, taking the caller's
transaction, so a job that merges several pairs commits once
(unchecked_transaction cannot nest). merge_people keeps its signature
and its one transaction.
saf.rs and the export path's SAF branch shipped tagged FR-PLAT-AND-1,
and the matrix counted the requirement as covered. Its subject is the
library — reached through SAF grants — and Android still reaches a
library over a Nextcloud account or a folder path. What the SAF code
does is give an album a folder on the tablet, which is FR-EXP-10.
outstanding.md said the figure overstated it and should be read with
this one subtracted; it now says the tags were narrowed, and coverage
reads 161 of 192.
The sidebar's "All photographs" row was bound to library-total, which
is the scope's count: the header's "412 images" and the scrollbar's
size. Under a collection, and now under an album, the row read as the
album's size — 4 where the library holds 70.
It reads a separate library-whole-total now: the same number as the
scope's when nothing is scoped (no second count), and otherwise the
unscoped count under the same filter, read only when the view's facts
move, so scrolling inside an album does not recount the library.
NFR-COMPAT-2's table has CI building every channel, the Flatpak
included. No workflow in .gitea/workflows/ builds it, and the sweep
before this one records that none has been built by hand either. A
status note under the table says so, so the decision and the state are
not read as the same thing.
benchmarks.md and catalog_open.rs both said schema::backfill runs on
every Catalog::open. Since ffdd640 it runs on the first open of a path
in a process and is skipped while the stamp matches, so in dr-bench
catalog_open_ms still includes it and catalog_open_warm_ms, the second
open in the same process, no longer does. That is what a library
reopened in one session costs, and both now say which figure is which.
The comment keeps its line count, so no tag below it moves.
Since 6683c14 every folder on the desktop is chosen through rfd, which
on Windows is the common item dialogue. windows.md's list of what is
already handled now says so, and that nothing has opened one under
Wine or on Windows. "The import flow's path picker" is now the import
page's Browse… button.