0407fb8d2dc0d6bd5b1a32bb5f166c808bde7465
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4395bd17c |
Run the face tests on the tablet, where the NEON kernel actually runs
The similarity scan picks its dot product per machine, and the NEON one is the kernel that ships to the phone and the tablet — and the one a desktop cargo test never executes. A wrong lane index or a mishandled tail there is a silent wrong answer on exactly the devices nobody runs the suite on, which is a poor place for the only untested code path. dr-face carries no weights and touches no display, so its tests are a plain ARM64 binary that runs under adb shell with nothing installed. The script builds it against the SDK's newest NDK, pushes it, runs it and cleans up. It checks for the device first, so a tablet that is not plugged in costs a second rather than the two minutes it takes to compile for it. Not wired into CI, which has no device attached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
69dccaa061 |
Count the platform layer's traceability tags
`platform` was missing from the traceability scanner's source roots, so every TRACES tag in `dr-plat` — the secret store, volume discovery, and now display-profile acquisition — was invisible to the matrix. The FR-PLAT-* family is exactly what that crate exists to satisfy, so the omission understated coverage by the requirements it was meant to count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
72410f39c6 |
Answer M1: tract loads both face graphs once their dims are pinned
Neither InsightFace export parses as shipped -- SCRFD fails at its input node, ArcFace at the first Conv -- which is the same wall dr-segment hit on YOLO's dynamic export. Both load cleanly with the input dims frozen, so the pure-Rust runtime holds for the face pipeline too. tools/fix-face-model-shapes.sh does the freezing, and exists so the artefact is reproducible rather than a binary someone once produced. It takes two forms because the two graphs need different ones: ArcFace's batch is a named dim_param, SCRFD's H and W are dynamic but unnamed. Also notes YuNet loading with no intervention, which matters for the licence question in faces.md 2.3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ff1a3e0e80 |
Print the black-and-white negatives instead of showing the scan
Choosing Ilford HP5 Plus showed an inverted grey frame. So did Double-X. They are negatives, and upstream leaves `target_print` null on every monochrome stock, so nothing was ever printed and the scan was all there was. A colour negative at least announces itself -- the orange mask says plainly that you are looking at a negative. A monochrome one just looks broken. They print on Kodak 2302 now, which is a monochrome print film and is what such a negative is actually printed onto; Double-X onto 2302 is the standard cine chain. For the Ilford stocks it stands in for an Ilford paper, which nobody has measured, and is at least the right kind of material. The scan is still reachable through the Scanned/Printed toggle. It is a thing to choose now rather than the only thing on offer. `every_shipped_stock_bakes` did not catch this, and could not: it derives "should this be inverted?" from the stock's kind *and whether it names a paper*, so it looked at an inverted HP5, concluded that was right for an unprinted negative, and passed. The assertion was self-consistent and the situation was still wrong. The new test asserts the thing that actually matters -- a camera negative must name a paper, that paper must be a printing stock, and it must be the same kind of material, so a monochrome negative cannot end up on colour paper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ce6458547a |
Develop longer, from the measurements rather than from a contrast slider
Pushing was not a thing to simulate. It was measured data being thrown away: Double-X and 2302 each ship five characteristic curves, one per development time, and this shipped the 6.5-minute column and discarded four. All five now ship and interpolate. The axis is real. Double-X runs 4 to 12 minutes, and across it the average gradient goes 0.472 to 1.034 while Dmax goes 1.19 to 2.56. The control is in stops, because that is what a photographer means, and one stop is a factor of about 1.41 in time. That mapping is checked rather than assumed: against Double-X's own axis it lands within 2% of the 9-minute column for +1, and near 12 minutes for +2, which are the times the datasheet gives for exactly that. There is a test. **Pushing must not recover shadow detail, and this does not.** Across the whole measured range the speed point moves about a third of a stop while the gradient doubles; three stops under mid-grey, density goes from 0.008 to 0.035, which is still nothing. Developing longer multiplies what was already recorded and cannot record what never hit the film. A push built as added exposure or global contrast brightens those shadows instead and looks convincing until someone who shoots film sees it, so that property has a test of its own. Interpolated in *log* time, because development is multiplicative: 4 to 5 minutes is the same amount of push as 9 to 12, and interpolating linearly would bunch the control at one end. Clamped at both ends, because past the published range there is no data and extrapolating a contrast curve invents an emulsion nobody tested. A stock measured at one process ignores the control entirely rather than inventing a curve for it -- Portra 800's pushes are separate *measured* profiles, which is the honest way to offer those. Costs nothing per pixel and changes no shader. The curves are a per-stock table, so the interpolation happens on the CPU at bake time, where choosing a stock and moving its sliders already rebakes. The Vulkan shader is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e9b3598841 |
Add five Ilford stocks, and say plainly that they are constructed
Build and test / Desktop (Linux) (push) Successful in 20m48s
Build and test / Layer separation (push) Successful in 27s
Traceability / Requirement traces (push) Failing after 25s
🐳 Android image / Build and push (push) Successful in 2s
Build and test / android-image (push) Successful in 2s
Build and test / Android (aarch64) (push) Failing after 33m33s
They were asked for and they are here, but not on the same footing as the
Kodak profiles, and the files say so in their first line.
What I had claimed, and had to withdraw: that Delta 100 is "quoted around 9"
and HP5 "around 12". Ilford publish no such figures. The word granularity does
not occur anywhere in their technical information -- grain is described as
"fine" and "finest" and nothing more. That claim was in this crate's
documentation as though it came from a datasheet; it is corrected there too.
Two further traps found while looking:
- Kodak colour negatives publish Print Grain Index, not RMS granularity.
PGI is a perceptual scale from viewer surveys -- 25 is roughly the
threshold of visibility, four units a just-noticeable difference -- and
Kodak state it cannot be compared to RMS. So a Portra number cannot be
dropped into the granularity field, and none has been.
- RMS proper is published mostly for black-and-white, reversal and motion
picture stocks. Every shipped stock therefore still carries the same
default, which means grain does not yet tell one film from another. That
is per-stock data, not code, and is now written down where somebody will
find it.
So the Ilford profiles are built rather than extracted, and each part rests on
something different:
speed published and exact -- ISO 400/27 for HP5 is a fact
contrast ISO 6:1993's normal development, average gradient 0.62
spectral borrowed from Kodak Double-X, a *measured* panchromatic
negative, shifted by the speed difference. Conventional
panchromatic sensitisation is much alike across black-and-white
films, and this is far better founded than reading pixels off a
printed curve
silver neutral, which is not an approximation: developed silver
absorbs flat, and Double-X's measurement is flat
granularity estimated, ordered by each film's known relative grain
They render as a film of that speed and contrast. They are not a measurement
of that emulsion, and the two stocks that share a speed differ only in the
estimated part.
`every_shipped_stock_bakes` is tightened to match, because a constructed
profile fails in a way a measured one does not: the curve parses, bakes, and
sits entirely off one end of its own exposure range, rendering every frame
black or blown while passing a finiteness check. It now asserts mid-grey lands
somewhere photographic and that the tone response runs the way the stock's
kind says it should.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3b5952769b |
Emit floats an f32 can hold, and drop the format! that formats nothing
CI runs cargo fmt --check and clippy -D warnings, and this branch had never been through either. Both would have failed it. The bulk was the generated colour tables: eight significant figures where an f32 carries about 7.2, so the eighth is noise that rounds away at compile time and clippy's excessive_precision says so 109 times over. Fixed in the generator rather than only in the file, so it stays fixed -- and the file is trimmed in place rather than re-derived, because regenerating it needs a colour-science stack that has nothing to do with the defect. The format! in the composer is mine too, from extracting the rendering tail: the braces in it were escaped because the text used to live inside a larger template, and once extracted the escapes are noise and the call formats nothing. Also here, and clearly not mine: an unused import and a shadowed binding in dr-gpu, and an unused import in a test. They are pre-existing -- clippy has been failing on master before this branch existed, on lints like is_multiple_of that arrived with a toolchain rather than with anyone's code. Fixed because CI cannot go green around them, and called out because a merge commit is a bad place to quietly edit someone else's crate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6d18517d28 |
Ship every stock that exists, black and white included
Three profiles was what the first cut needed to prove the model. This is the rest of the open data: 23 camera stocks and 9 papers, which is all of spektrafilm. Black and white was the gap, and it turned out not to be a gap in the data -- it was a gap in where I looked. Upstream's `main` has 28 colour profiles and nothing monochrome; `dev` has three more, and they are Tri-X, Double-X and the 2302 print film they go onto. So the answer to "do we have B&W" was yes all along, and it needed the dev branch rather than a fortnight digitising Ilford's datasheet graphs by eye. Those three are pinned to `dev` per stock; the colour stocks stay on the released branch. A monochrome profile is single-channel -- one emulsion, not three -- and spreading that one layer across all three is exact rather than an approximation: three layers with identical sensitivity and identical curves respond identically, which is what one layer does. The dye is the trap. The renderer *sums* the three layers' contributions, so replicating it unchanged renders every frame three times too dense -- neutrally, and therefore plausibly. A third each reconstructs the single emulsion, and two tests hold both halves: that the densities stay equal, and that they sum to one emulsion and not three. Double-X and 2302 ship five curves apiece, measured at five development times -- 4 to 12 minutes for Double-X. That is push and pull processing as measured data. The standard 6.5 minutes is what ships; the rest is in the upstream file waiting for a control to ask for it. Two stocks are `support: film` and are nevertheless what a negative is printed *onto*: the cine projection films 2383 and 2393, which the Vision3 stocks print to. Filtering the picker on support alone offered a projection stock as something to load in a camera, so it filters on stage, with a test saying so. The picker had to change shape twice over. Chips were right for three stocks and off the edge of a 280px column at twenty-four, and the column that replaced them was a thousand pixels standing between the photographer and every slider below. It is a disclosure now: one row carrying the answer, opened to change it, closed again on choosing. That is the opposite of the argument this panel used to take the lids off its sliders, and deliberately so -- an instrument you compare wants to be visible, and a list you consult once wants to be out of the way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b6a95e1965 |
Simulate a film stock from its measurements, not from someone's grade
FR-DEV-3f asks for look emulation and proposes HaldCLUT import to inherit
the free film-simulation ecosystem. This takes the other road for the
stocks where the measurements exist: run the physics.
A stock here is its manufacturer's own datasheet -- spectral sensitivity,
characteristic curves, dye densities. Light exposes three emulsion layers,
the layers develop to densities, the densities are dyes that absorb, and
what is left is what reaches the eye. A colour negative comes out orange
and upside down because that is what a colour negative is; it becomes a
photograph when a paper profile prints it, with the enlarger's filtration
solved rather than dialled.
What that buys over a LUT is that the parameters stay physical. Opening up
a stop moves the picture along the film's real characteristic curve,
shoulder and all, instead of scaling a number baked at one exposure. The
data cost runs the other way too: a stock is 17 kB of published
measurements where one HaldCLUT is 800 kB of one person's grade.
It looks like it needs a spectral integration per pixel. It does not, and
that is the whole design:
- Exposure is a 3x3 matrix. The reconstructed scene spectrum is linear
in the sRGB triple, so the integral collapses into nine numbers,
exactly -- no approximation.
- The characteristic curve is three 1D functions, sampled exactly.
- Everything after that -- dye absorption, the print through the
negative, the paper, the viewing illuminant, the adaptation -- takes
exactly three numbers in, so it bakes into one 32^3 lookup.
Per pixel: a matrix multiply, three curve taps, one fetch. Splitting the
curve out of the 3D lookup rather than baking one LUT over exposure is
measured, not assumed: the curve carries the sharp shape and the dye
mixing is smooth, so folding them together would need three times the
resolution for the same error. At 32^3 the worst error is 0.003 in linear
sRGB, under one 8-bit code value, and a test says so.
No wgpu dependency, deliberately, and the same isolation argument dr-lens
makes: the model is plain f32 with a documented layout, so every property
worth asserting is asserted on the CPU. Binding it to a texture is dr-gpu's
job and is not done here yet.
The expected values in tests/ came from a Python prototype running against
a different colour-science stack. Agreement to three decimals is evidence
about the model rather than about one implementation of it -- a transposed
matrix or a mispasted observer row would pass every unit test and fail
that one.
Profiles are converted from spektrafilm by Andrea Volpato, CC BY-SA 4.0.
The converter is in the tree and runnable, so what was changed from
upstream is auditable rather than taken on trust; profiles/CHANGELOG.txt
records it, including the one deliberate deviation -- Mallett & Yuksel's
1 kB basis instead of Hanatos's 4 MB table, which costs accuracy at the
gamut edge and saves four megabytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e6b01226eb |
Let the segmentation export script pick its own input size
Comparing a larger model against a larger input size meant re-exporting at resolutions other than the shipped 640, and the script only ever wrote that one number. `IMGSZ` is now a second positional argument, defaulted to 640 so every existing call is unchanged. The experiment this was built for found bigger input a net loss on its own merits — yolo26n-seg and yolo26s-seg at 1280 both lost track of large, frame-filling subjects (a bus's box shrank and its score nearly halved) in exchange for catching small or partially-occluded ones tiling already handles. Nothing shipped from it, but the ability to re-run that comparison is worth keeping. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5a0a9719eb |
Set the version once, in one place, for every artefact
The workspace read 0.1.0 for two releases, so every desktop binary reported a version two releases stale. The APK was worse: `AndroidManifest.xml` states no version at all, so a device showed `versionName=null` and `versionCode=0` while the library inside the APK knew exactly what it was. A version edited by hand in several files is a version that is wrong in at least one of them. `tools/set-version.sh` is now the only thing that sets one. It takes the version from the latest git tag, or is told, and writes the two files that must state it before anything is built: the workspace `Cargo.toml`, from which every crate inherits, and `packaging/PKGBUILD`, which pacman reads before a build exists. It refreshes `Cargo.lock`, because members appear there by version and CI builds `--locked`. `--commit` commits the result. Android is not in that list on purpose. `package.sh` reads the version out of `Cargo.toml` and hands it to `aapt2 link`, so the APK cannot drift from the binary it contains — there is no third file to forget. `versionCode` has to be one increasing integer, which a semantic version is not, so it is packed as MAJOR*10000 + MINOR*100 + PATCH: ordered the way Android requires, and readable at a glance. A version that is not MAJOR.MINOR.PATCH is refused rather than coerced. It is a contract with whoever reads a bug report, and silently turning "0.4" into something else is worse than being asked to type it again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0da8271836 |
Let the model say what a thing is and the watershed say where it ends
Local masking needs to know where an image's regions are. The watershed spike (S15 arm A) found the boundaries but had no idea what any of them enclosed; its coarse levels were geometric accidents. This adds the other half and the thing that joins them. `core/dr-segment` is where region reasoning now lives — the hierarchy moves out of `dr-gpu`, which keeps only the pixel passes that are genuinely shaders. The new crate is device-free and, without its default features, model-free too: 20 of its tests need neither an adapter nor 11 MB of weights. Arm B runs YOLO26n-seg through `ort`. D13 framed inference as a choice between `ort`'s C++ runtime and the pure-Rust dependency policy; that was a false choice. `ort`'s `alternative-backend` feature unlinks the C entirely and `ort-tract` supplies the API from tract, which is pure Rust. Measured before committing to it: zero unsupported operators, 420 ms for 640x640, and correct masks on bus.jpg. No NDK problem to solve, so D13's largest tolerated exception is not needed. Arm C is `prior.rs`, and it ships because the two arms fail in opposite directions. Instance membership re-weights the merge saddles, so region pairs the model believes share an object merge early and pairs straddling its edge merge late. No boundary moves — only the order in which they dissolve — which is how the result stays pixel-accurate at every level while its coarse levels become named things. Two things the spec assumed that turned out to be false, both recorded in models/LICENCE.md: there is no usable ADE20K-trained YOLO, so the shipped vocabulary is COCO's 80 subjects and *stuff* like sky and foliage must come from arm A; and tract cannot parse a dynamic-shape export, so the graph's input is fixed and tiling is the only route to more semantic resolution. Weights are AGPL-3.0, which GPLv3 §13 permits and which makes the combined work effectively AGPL. Deliberate, not accidental. They live in Git LFS, and a build script fails with an instruction rather than embedding a pointer file when the clone lacks them. |
||
|
|
7c57f490fe |
Declare a develop operation in YAML, and generate the rest
An operation was, in the overwhelming majority of cases, four facts: what its parameters are, what uniforms they compute, what WGSL those uniforms drive, and where it sits in the chain. Written in Rust those four facts arrived wrapped in ninety lines of trait implementation — a match on parameter id to a struct field, another match back, an is_active comparing each field to its default, a Vec<Uniform> built by hand. All mechanical, and each one a place to make a silent mistake: a param() arm returning the wrong field reads perfectly and breaks the sidecar round-trip. So the four facts are the file now. core/dr-pipeline/ops/<id>.yaml is a node, build.rs compiles it into the same Operation impl as before, and the result lands in OUT_DIR — the same reasoning as style.yaml -> theme.slint, including why it does not land beside the sources it would look exactly like. Nothing downstream can tell a declared node from a hand-written one: same &'static OpDescriptor, same fused-shader composition, same sidecar. Nine nodes moved: exposure, white_balance, contrast, highlights_shadows, blacks_whites, brilliance, vibrance, saturation, and the shared WGSL helper registry. Their prose came with them, and so did their tests — set/expect/expect_active/expect_wgsl in the declaration compile to real #[test]s, so a node file carries its own proof rather than leaving it behind in a file that no longer exists. Two stayed in Rust and say so with `rust:`. The tone curve's neutral is a relationship between five interpolated points rather than a set of values; the colour mixer generates thirty-six faceted parameters from twelve computed hue bands. A schema stretched to cover either would be a worse language than Rust aimed at one caller. They still declare their position here, because the chain's *order* is the one thing a reader comes to this directory to learn, and an order written half in YAML and half in Rust would be worse than either alone. default_chain() is generated from it. Uniforms are derived by a small expression language — exp2(exposure), blacks / 100 * 0.02 — compiled to Rust rather than interpreted, so an unknown name or a wrong arity is a build error naming the file and the key and the arithmetic costs nothing at runtime. The build script refuses a duplicate order, a filename disagreeing with its id, a default outside its own range, a test value the graph would clamp before the node saw it, a helper that does not define the function it names, and a declared node colliding with a file in src/ops. Verified by adding a scratch node and removing it again: one file, no other edit, and it joined the chain at its declared order with its test running. 237 tests pass in dr-pipeline, clippy and fmt clean. .yaml joins the traceability tool's scanned suffixes, because a node's Rust now lives in OUT_DIR where a tag could never be linked from the report. Coverage 47.7% -> 48.3%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
03326242a1 |
Make the CI checks say what they mean, and format the workspace
The Android job's "Verify minimum API level" step has never verified the minimum API level. It took the first `*.so` anywhere under the target directory, which is a host proc-macro from debug/deps — an x86-64 object built by the runner's gcc, whose .comment section cannot mention Android and so can never contradict the expected value. It now reads the artifact under the target triple, compares against MIN_API parsed from the Dockerfile rather than a second copy of the number, and fails on a mismatch. Both sides are checked non-empty first: two failed parses would otherwise compare equal and pass, which is the same silent success in a new costume. The Android image installs one SDK package per layer and keeps the output. sdkmanager is a JVM program that aborts when it cannot get memory, and the single `> /dev/null` step reported that as a bare "exit code 134" while a retry re-downloaded everything that had already succeeded. tools/ci-local.sh runs all four jobs — desktop, android, layering, traceability — against the host toolchain, which is pinned to the same 1.92.0 CI installs. Its matrix check compares regeneration against the working tree rather than against HEAD: CI starts from a clean checkout, so git's answer is the right one there and reports every local run stale here. The rest is rustfmt across the workspace, and the clippy findings that surfaced once it did: manual_contains in dr-thumbs and collections_ui, a map iterated as pairs for its keys, an index loop over a slice, and two runtime assertions on a constant now made at compile time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0f202fd3f9 |
Add requirements traceability gate and Gitea pipelines
Ports JellyTau's traceability tooling to Rust, carrying across the bug it was repaired for. That gate divided a traced count by frozen literal denominators; the requirements file outgrew them and it reported 158% coverage, so it could never fail its own threshold. Two rules, both enforced by the extractor's own tests: - denominators parsed from docs/requirements.md at run time - coverage is |traced ∩ defined| / |defined|, never a raw traced count The gate additionally fails hard on a misconfigured run — zero requirements parsed or zero files scanned — rather than reporting a plausible 0%, and on any orphan tag naming a requirement that does not exist. Adapted for DarkRoom: IDs are FR-CAT-1 / NFR-P13 / FR-DEV-3a shapes rather than JellyTau's fixed three digits, and decisions (D), spikes (S), milestone items (M) and test ids remain taggable while being excluded from the denominator — counting them inflated it by 25. Also adds dr-sync: the RemoteBackend trait and capability model, so the Nextcloud connector is one implementation rather than the only shape the engine understands. No mature Nextcloud crate exists (reqwest_dav is too thin), so the connector will be hand-rolled over reqwest per D7. Gitea workflows follow the same style: containerised, commented with the reasoning, desktop and Android on every push, plus a CI check that no core/ crate depends on the UI toolkit (ARCH §6.5a). Coverage today: 13.3% (19/143). 50 tests passing. |