Skip to content

perf(dng): benchmark encode and decode against the Adobe DNG SDK - #558

Open
justin13888 wants to merge 24 commits into
feat/442-dng-c2pa-manifest-storefrom
perf/163-dng-benchmark-harness
Open

justin13888 wants to merge 24 commits into
feat/442-dng-c2pa-manifest-storefrom
perf/163-dng-benchmark-harness

Conversation

@justin13888

@justin13888 justin13888 commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Issue #163 asks for a DNG benchmark harness compared against the Adobe DNG SDK. This adds
cargo bench -p gamut-dng --bench codec: encode and decode throughput across the whole shipped
codec matrix — uncompressed, Deflate and lossless JPEG, each for CFA and LinearRaw photometry —
with gamut's decode placed next to the reference implementation's.

This section is current as of the sixth pass, and it has been corrected in place. Four review
passes changed what the harness measures and withdrew figures earlier revisions of this section
stated, and a fifth corrected a justification without changing any measurement; each is recorded
below under its own heading — ## Second pass, ## Third pass, ## Fourth pass, ## Fifth pass,
## Sixth pass — and those headings, not this one, are the append-only record.
The convention protects the decision record, not the summary: a reader meets the summary first, so
it says what is true now rather than what was true when it was first written. Every correction made
here is named where it was made.

Stacked on PR #508 (feat/442-dng-c2pa-manifest-store at 677d38ea), not on master: #508 is
open, delivered and green, and basing on it avoids a crates/gamut-dng/STATUS.md collision. This
run does not merge anything. Review #508 first; this PR's own diff is everything above
677d38eagit log --oneline 677d38ea..HEAD. The count is given as a command rather than as a
number because it has been wrong in this body twice.

What landed:

  • crates/gamut-dng/benches/codec.rs — the harness. It follows benches/compression.rs's shape
    (divan, a printed table then the timed benchmarks) rather than inventing a second one.
  • tooling/gamut-dng-oracle — three dev-only entry points and their C++ shim: decode_dng_in_memory
    (the same parse → build-negative → ReadStage1Image flow the oracle already runs, but over a
    dng_stream on the caller's bytes and stopping before the export copy), decode_lossless_jpeg_extent
    (the no-export arm that bounds the FFI export path), and zlib_identity/zlib_path, which report
    the libz the SDK's Deflate reader actually called.
  • crates/gamut-dng/STATUS.md — a "Codec benchmark harness (Setup benchmark harnesses for DNG #163)" section.
  • crates/gamut-dng/tests/roundtrip.rs and src/lossless_jpeg.rs's test module — the pins for those
    oracle entry points, inside the gated crate so a gate runs them.

What the harness runs

Three benchmarks, not six: encode_gamut, decode_dng, decode_lossless_jpeg. Each decode
benchmark takes the implementation as a divan argument rather than living in one benchmark per
arm. divan runs benchmarks in name order, so separate benchmarks would measure every reference case
minutes away from its counterpart, and on a shared machine that drifts a ratio measured minutes
apart is not a ratio. The argument names sort adjacent, so a pair runs back to back:

decode_dng            cfa/deflate adobe-sdk · cfa/deflate gamut · cfa/lossless-jpeg adobe-sdk · …
decode_lossless_jpeg  cfa adobe-sdk · cfa adobe-sdk-no-export · cfa gamut · linear-raw … 
encode_gamut          cfa/deflate · cfa/lossless-jpeg · cfa/uncompressed · linear-raw/… 

So the ratio for a row is read inside one benchmark, between its gamut and adobe-sdk
arguments — cargo bench -p gamut-dng --bench codec -- decode_lossless_jpeg is a complete
invocation. --sortr name reverses the sort and therefore which arm runs first; a published ratio
is the mean of one run each way.

There is no encode arm for the SDK: the oracle shim wraps the SDK's reader, not its writer, so
no reference encode number exists and none is fabricated. Encode is reported for gamut alone.

What is inside each timed region, and what is not

Inside, every benchmark: the codec call, the allocation and growth of the buffer it produces,
and that buffer's teardown. The teardown is placed explicitly (each closure returns () and drops
its result) because divan otherwise defers a returned value's drop past the timed region — which
would charge gamut nothing for freeing a decoded image while the SDK, whose dng_negative
destructor runs inside its own call, pays in full.

Outside, every benchmark: synthesising the sensor samples, building the RawImage and
CameraProfile, and the encode that produces the bytes a decode benchmark reads. No benchmark
touches the filesystem.

Is the gamut-versus-SDK comparison fair?

Every asymmetry is either removed or measured; none is left as an adjective. Which
direction each one favours is deliberately not claimed. An earlier revision of this section said
decode_dng favours the SDK and decode_lossless_jpeg favours gamut "by less"; the second is
withdrawn — the export-path gap that would justify it sits at this harness's measurement floor
and its sign is not resolved (see ## Fourth pass, N5).

  • Neither side pays for the FFI boundary on the way in. decode_dng_in_memory hands the SDK a
    dng_stream over the caller's own bytes, so the reference implementation parses the very buffer
    gamut parses: no temporary file, no import copy on either side.
  • Neither side pays for it on the way out, in the container comparison. That entry point
    reports the decoded image's extent and exports no samples, so the SDK is not charged for a
    malloc + memcpy that exists only because the caller is in Rust.
  • The one asymmetry left in decode_dng is the IFD-0 preview, plus the metadata
    reconstruction: DngDecoder::decode is a whole-file decode and ReadStage1Image is not. Its
    volume is exact, and it is the volume the decoder materialises, not the one the file stores —
    the preview is written at 8 bits, but every sub-image surfaces as SubImageData::Decoded(Vec<u16>),
    so the buffer gamut allocates, fills and frees is ⌊w/2⌋ × ⌊h/2⌋ × 3 × 2 bytes against the raw's
    w × h × planes × 2: 75 % of a 16-bit CFA frame and 25 % of a LinearRaw one. (An earlier
    revision of this section modelled it at the stored width and so halved both figures; corrected in
    ## Fourth pass, N4.) The fixture table prints that volume per case together with whether the
    correction was applied — it is applied on the uncompressed rows, where both paths do comparable
    work per byte, and suppressed on the compressed rows, where the same arithmetic would print a
    lower bound where a reader takes a measurement. It is not normalised away: gamut exposes no
    raw-image-only decode entry point, and adding one so a benchmark reads better would be the wrong
    direction of causation.
  • The one asymmetry left in decode_lossless_jpeg is the FFI export path, and a third arm bounds
    it.
    adobe-sdk-no-export runs the identical DecodeLosslessJPEG<Scalar> into the identical
    spool buffer and stops before the malloc/memcpy/Vec copies. Read that gap as a magnitude
    only: it sits at the measurement floor, so what it supports is "the codestream comparison is fair
    to within the bound", not a price for the export path.
  • On the two */deflate rows neither arm's inflate is gamut-authored. gamut-deflate is
    deliberately encoder-only, so this crate inflates with miniz_oxide, while the reference arm
    calls the system libz the oracle links dynamically (-lz, because the SDK includes <zlib.h>
    unconditionally). What separates the two is pinning, not authorship: miniz_oxide is pinned
    by Cargo.lock to one version and one checksum, and the system libz is pinned by nothing — not by
    a version, since the loader picks between a copy under target/ and whatever the platform
    installed and zlibVersion() separates those only when the platform's build renamed itself, and not even by the
    machine, since cargo exports every build script's native search path and this crate dev-depends
    on libtiff-oracle, which builds a libz.so of its own under target/. The harness prints the
    resolved library and warns when its path lies inside a build directory. Pinning it is filed as
    tooling/gamut-dng-oracle: pin the zlib the benchmark's reference arm links, keeping -lz for conformance #618.

Numbers

None are pinned in the repository. The measured run is ## Fourth pass — eight runs across two
libz builds and both arm orders, with raw divan output and per-run load averages. This box is
shared with other concurrent sessions; a throughput figure taken under load is not a throughput
figure, so absolute MB/s is reported as indicative only and STATUS.md tells a reader to run the
harness on the box they care about. The #196 ratios already in STATUS.md are unaffected — they
compare two encoders inside one process, which survives a loaded machine; MB/s does not.

Validation

Commands verbatim, with outcome. Correcting two claims this paragraph made in earlier passes.
It said nothing in the gate set executes this file: that is false. mise run bench-test runs every
bench in the workspace once, without timings, precisely so a bench cannot rot silently (issue
#437), and it is in this lane's gate set — its run is recorded below. What no gate does is time
the harness, which is deliberate: a timing number from a shared runner would be noise. It also said
the one piece of the diff a gate executes is the oracle's new entry point: that was false too, for
the reason under N2 below — the oracle crate is outside the workspace, so its tests never ran. Both
of that entry point's pins now live inside gamut-dng, where mise run test runs them. Nothing
under .github/ is touched.

Commands verbatim, with outcome, are recorded per pass: ## Second pass, ## Third pass,
## Fourth pass, ## Fifth pass and ## Sixth pass each carry a Validation, this pass table.
The gate set run on the current head is this one (## Sixth pass; the diff is two commits of
comments, doc comments, a printed string and a STATUS.md paragraph — no source behaviour changes):

command outcome
__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt, then the same prefix for mise run fmt-check pass, exit 0 — fmt changed nothing
mise run check-tests pass — "module docs, pinned proptest seeds and oracle filenames all conform"
convco check origin/feat/442-dng-c2pa-manifest-store..HEAD pass — "no errors in 24 commits"
mise run bench-test pass, exit 0 on both commits — every bench executes; the corrected epilogue and the build-tree warning are in its output
mise run lint pass, exit 0 on both commits
cargo test -p gamut-dng --all-features pass, exit 0 — 13 test binaries, 0 failures (run on the first of the two commits; the second changes comments only)
GAMUT_MUTANTS_BASE=origin/feat/442-dng-c2pa-manifest-store mise run mutants-diff pass, exit 0 — "No mutants to filter": the diff is comments and strings, so nothing is mutable. Base stated because it changes the answer on a stacked branch
the pre-publish tooling-path check, run on both the pre-redaction body and the body published here refuses the first (naming all seven lines), passes the second

__CARGO_TEST_ROOT is set because cargo otherwise walks past this nested worktree's root when
loading the tooling/* manifests and fmt-tooling-check exits 101 on an untouched tree; it changes
nothing about what is formatted. The mutation base is stated beside the count because this branch is
stacked and the default base folds in the mutants of the pull request underneath.
mise run check-release-deps and mise run check-ffi-features were not re-run: no manifest changed
this pass, and both passed on the manifest change recorded in ## Third pass. No timing is
published in this pass
— the measured run remains ## Fourth pass.

Risks and rollout

  • Additive and dev-only. No shipped code path changes: the diff is one bench target, one
    dev-only oracle entry point (tooling/**, never published), and documentation. No new
    dependency.
  • Build cost. --bench codec links the Adobe SDK oracle, which cargo test -p gamut-dng --all-features already builds, so mise run test/lint gain a link target and no new native
    build.
  • The measurement is the risk. A benchmark that measures the wrong thing gets cited. The two
    mitigations are structural rather than promissory: the asymmetry that cannot be removed is
    printed by the harness itself next to the numbers it distorts, and the second comparison is
    biased the other way so a reader is never left with one flattering ratio.
  • Rollback is deleting benches/codec.rs, its [[bench]] stanza, the STATUS section and the
    oracle entry point; nothing depends on them.

Issue

Refs #163. Not Closes: the issue asks to "still keep it comprehensive", and two axes of the
format are measurable but not measured here — JPEG XL (encoding it needs the jxl-encode feature
and a C++ toolchain, so a default cargo bench cannot build its fixture) and the real-camera corpus
behind mise run fetch-dng-samples (a ~178 MiB submodule, which would make the harness unrunnable
by default — rejected in the decision record below). Remainder filed as #556.

Two further issues are filed by the fifth pass and are not closed by this PR: #617 corrects the record in #583 and #584 (withdrawn figures, and a verification command that matches no benchmark), which this run may not edit in place; #618 proposes pinning the zlib the oracle's reference arm links, keeping -lz for conformance. The sixth pass files a third, #620, correcting one sentence of #618's justification: zlibVersion() does separate this box's two libz builds, and the claim that holds is the narrower one about two stock builds of a version. #618's proposal is unaffected.

Decisions taken

No human approved this plan. This is an unattended run; the record below is what a human reads
afterwards. The frozen record this lane was given, verbatim:

Issue 163  Plan: v1  Branch: perf/163-dng-benchmark-harness  Base: PR #508 head 677d38ea
Touches: crates/gamut-dng/benches/codec.rs (new), crates/gamut-dng/Cargo.toml ([[bench]]),
         crates/gamut-dng/STATUS.md; tooling/gamut-dng-oracle (a timed decode entry point,
         dev-only, plus its C++ shim)
Will not: change the codec; add an external dependency; vendor a sample corpus
Settled: S1 (no new dependency), S3 (tests follow docs/testing.md)

Decisions taken.
1. Whole issue: gamut decode and encode throughput measured against the Adobe SDK's decode,
   on synthetic fixtures, so the harness depends on no sample corpus.
   Rejected: benchmarking against the real-camera DNG corpus - it is a ~178 MiB submodule
   behind `mise run fetch-dng-samples`, which would make the bench unrunnable by default.
   Reverses: drop the oracle timing entry point.
2. Coverage: decode across uncompressed, Deflate and lossless-JPEG, for both CFA and
   LinearRaw photometry; encode across the same.

Appended by this lane, in the same shape:

3. The whole-file decode comparison keeps an asymmetry that cannot be removed: gamut's
   `DngDecoder::decode` also decodes IFD 0's preview and rebuilds the metadata, which the
   SDK's `ReadStage1Image` does not. Report it exactly - the harness prints the preview's
   byte volume per fixture - and publish a second, exactly-matched comparison at the
   lossless-JPEG codestream, whose residual bias runs the other way.
   Rejected: adding a raw-image-only decode entry point to gamut-dng so the two sides match.
   That changes the shipped API to make a benchmark read better, and the record's "will not"
   forbids changing the codec.
   Rejected: publishing the whole-file ratio alone. It flatters the SDK by a margin a reader
   cannot see.
   Reverses: drop the codestream comparison and report only the whole-file pair, with the
   asymmetry stated but unquantified.

4. Fixture teardown is inside the timed region on both sides, not deferred. divan defers a
   returned value's drop; the SDK's `dng_negative` destructor cannot be deferred because it
   runs inside the C++ call. Leaving divan's default would have charged gamut nothing for
   freeing a decoded image.
   Rejected: divan's default (return the value, let divan drop it later) - it is the shape
   `benches/compression.rs` uses, but that bench has no cross-implementation comparison for
   the asymmetry to distort.
   Reverses: return the values and note the bias instead of removing it.

5. The oracle's timed entry point stops before `copy_short_image` - it reports the decoded
   image's extent, not its samples. Excluding the export copy makes the SDK look faster,
   which is the conservative direction (it cannot manufacture a win for gamut) and is also
   the more accurate model of "decode".
   Rejected: including the export copy, which would charge the reference implementation for
   the FFI boundary.

6. The sensor-sample generator is duplicated in spirit, not shared, with
   `benches/compression.rs`. The two benches are separate binaries producing different
   things - packed `u8` payloads for `gamut-deflate` there, `u16` samples for a `RawImage`
   here - so sharing needs a third file or an edit to a bench outside this lane's manifest.
   The relationship is named in the new bench's comment.
   Rejected: adding `benches/fixture.rs`. It widens the manifest for ~15 lines of arithmetic.

7. JPEG XL is absent from the matrix. Encoding it needs the `jxl-encode` feature and a C++
   toolchain, so a default `cargo bench` could not build its fixture, and a benchmark that
   silently skips half its matrix is worse than one that states its bounds.
   Reverses: gate the JXL cases behind `#[cfg(feature = "jxl-encode")]` - filed as #556
   instead.

Second pass: the table fix, and what the harness found

The harness's first full run left one thing unfinished and turned up two defects. Neither defect is
fixed here — the record's "will not" forbids changing the codec, and a benchmark is the wrong place
to change what it measures — so both are filed with their evidence.

The fix that landed (a589d61). The fixture table's first column was ragged: {case:<26} sets
a width on the formatter, but both Display impls wrote through write_str/write!, which go
straight to the underlying buffer and discard width and alignment. Formatter::pad is the method
that honours them. Cosmetic; it changes no measurement, and the fixture byte counts are identical
before and after.

#583 — lossless-JPEG decode is ~60x slower than the reference implementation. Medians of 100
samples, 512x384 at 16 bits:

benchmark gamut DNG SDK ratio
decode_lossless_jpeg cfa 65.32 ms 1.142 ms 57x
decode_lossless_jpeg linear-raw 188.7 ms 2.934 ms 64x
decode_dng cfa/deflate 888.2 us 717.4 us 1.24x
decode_dng linear-raw/deflate 2.832 ms 2.269 ms 1.25x

That every other scheme is within 1.25x and only this one is 60x out is what localises the cost:
lossless_jpeg::decode_symbol scans the whole 256-entry code table once per candidate bit length,
so a symbol costs ~1000 comparisons against the reference implementation's single table probe.

#584 — CFA lossless-JPEG encode expands the payload past uncompressed. From the fixture table
the harness prints: cfa/lossless-jpeg is 618 800 bytes against cfa/uncompressed's 541 440 —
turning compression on makes the file 14.3 % larger. The encoder hands the mosaic to
lossless_jpeg::encode as one full-width component, so predictor 1 differences a red photosite
against its green neighbour. Declaring the same samples as (width / 2, height, 2) — the reshape
DNG 1.7.1.0 p. 20 describes, which needs no sample reordering and which this crate's decoder
already reads — takes the payload from 119.7 % of raw to 91.5 %, measured through the crate's own
public lossless_jpeg::encode.

STATUS.md records both (3ec14d6), as ratios and byte counts rather than absolute times.

On the numbers, and the box that produced them

This machine is shared and was under heavy concurrent load for most of this session. The
measurement above was taken in a quiet window — uptime at the start of the run reported a 1-minute
load average of 7.65 on 16 cores, and 9.60 at the end — and that uptime bracket is
recorded in the run log rather than inferred. Even so: no absolute MB/s figure from this run is
published anywhere in the repository
, because a throughput figure taken on a shared box is not a
throughput figure. What is published is (a) ratios between two implementations measured in the same
process, which reproduced at 57-71x across two runs an order of magnitude apart in load, and
(b) fixture byte counts, which are deterministic.

Fairness of the comparison, re-established this pass

  • Same bytes on both sides: decode_dng_* both parse the same in-memory Vec<u8>.
  • Same denominator: the divan counter is the raw sample volume for gamut, the SDK and encode alike.
  • Same numeric class on the SDK side: the shim calls DecodeLosslessJPEG<Scalar>, the reference
    implementation's non-vectorised path, so the 60x is scalar against scalar.
  • The residual asymmetries are the two already in the record (whole-file decode favours the SDK,
    the codestream pair favours gamut) and they still point in opposite directions.
  • DngDecoder::decode does not verify NewRawImageDigest — that is a separate opt-in method —
    so gamut is not being charged for hidden work in the timed region.

Validation, this pass

All from the worktree, all completed in this run:

command result
mise run fmt-check (with __CARGO_TEST_ROOT) pass
mise run check-tests pass
convco check <base>..HEAD pass, 7 commits
mise run lint pass, exit 0
mise run test pass, exit 0, 203 suites ok
mise run check-release-deps pass
mise run check-ffi-features pass
mise run mutants-diff 78 mutants, 69 caught, 9 unviable, 0 missed

cargo bench -p gamut-dng --bench codec is not a gate: benchmarks are not tests, and no CI check
runs this file. What does cover it is mise run lint and mise run test, both of which build
--all-targets and so compile the bench under -D warnings; nothing asserts on its output.

Decisions appended to the record, in the record's shape

8. The `Display` width bug is fixed in the `Display` impls (`Formatter::pad`), not worked around
   at the one call site that noticed it. A `Display` impl that silently discards the formatter's
   flags is the defect; a `.to_string()` at the call site leaves the next caller to rediscover it.
   Rejected: `println!("{:<26}", case.to_string())` with an explanatory comment - correct output,
   but it moves the knowledge away from the impl and leaves the impl wrong.
   Reverses: revert to `write!` and drop the width from the column.

9. Both defects the harness found are filed, not fixed, and the crate's STATUS records them. The
   record's "will not" is "change the codec", and both fixes are codec changes: #583 rewrites the
   Huffman decode, #584 changes the bytes the encoder emits. Fixing either inside a benchmarking
   change would also make the benchmark's own baseline unreviewable.
   Rejected: fixing #584 here. It is ~15 lines and the payload is 23.5% smaller - but it changes
   every lossless-JPEG CFA file this crate writes, which needs the Adobe differential re-run as
   the point of the change rather than as a side effect.
   Rejected: reporting the findings only in this pull request. A finding that lives in a merged
   PR body is a finding nobody will act on.
   Reverses: drop the STATUS paragraph and leave #583/#584 to stand alone.

This is an unattended run: no human approved this plan or these decisions. The record above is
what a human reads afterwards.


Third pass: the table published in full, and each pair measured against itself

A review of the second pass raised eight findings. All eight are addressed below. Two of them
changed what the harness measures, and one of those changed a published conclusion. Corrections are
appended rather than written over the entry they correct, and each names its entry.

F4 (Medium) — each pair is now one benchmark, and that moved every small ratio

benches/codec.rs:353 and :373 put gamut and the reference implementation in separate divan
benchmarks. Divan runs benchmarks in name order, so every reference case was measured minutes away
from its counterpart on a shared box that drifts, and every conclusion at the 1.25x scale sat
inside its own noise.

decode_dng and decode_lossless_jpeg now take the implementation as a benchmark argument,
with the argument names ordered so divan's own name sort keeps the members of a pair adjacent. The
pair runs back to back, under the same instantaneous load. This repository's position is that
ratios are the durable quantity; a ratio measured minutes apart on a shared box is not one.

Correction, naming its entry. The second pass's table (this body, "#583 — lossless-JPEG decode
is ~60x slower than the reference implementation") reported decode_dng cfa/deflate at 1.24x
and linear-raw/deflate at 1.25x. Interleaved, both sit below 1.0x — 0.94-0.97x across two
runs, i.e. gamut decodes Deflate DNGs slightly faster than the reference implementation. Those
two figures were the split-group measurement moving, not the codec. The ~60x lossless-JPEG result
is unaffected and reproduces.

F1 (High) — the isolating sentence was false, and the rows that falsify it were missing

crates/gamut-dng/STATUS.md:397 claimed every decode path was within 1.25x of the SDK except
lossless JPEG. It is not true — both uncompressed cases are well outside it — and those two rows
appeared in no table published anywhere. Omitting the contradicting rows was the defect, not the
ratio.
The whole matrix is now published, in this body and in STATUS.md.

Whole-file decode, gamut / Adobe DNG SDK, median of 100 samples, 512x384 at 16 bits. "Corrected"
divides out the IFD-0 preview volume gamut additionally unpacks:

decode_dng case run A run B corrected A corrected B
cfa/uncompressed 2.44x 2.39x 1.78x 1.74x
cfa/deflate 0.97x 0.94x 0.71x 0.69x
cfa/lossless-jpeg 60.1x 57.5x 43.7x 41.8x
linear-raw/uncompressed 1.85x 1.76x 1.65x 1.57x
linear-raw/deflate 0.95x 0.96x 0.85x 0.85x
linear-raw/lossless-jpeg 59.0x 58.8x 52.4x 52.2x

Bare codestream decode — same SOF3 stream in, same samples out, no container work on either side,
one counter for all three arms:

decode_lossless_jpeg case gamut / SDK, A gamut / SDK, B SDK export path, A B
cfa 56.7x 56.4x 1.9 % 0.3 %
linear-raw 58.6x 57.5x 1.1 % 0.7 %

Correction, naming its entry. The STATUS.md sentence "Every decode path is within 1.25x of
the SDK except lossless JPEG, which is ~60x slower" (second pass, commit 3ec14d6) is withdrawn.
#583's isolation now rests on the codestream pair, which carries no container asymmetry at all
and whose one residual bias is measured at under 2 %: gamut is 56-59x the reference implementation
there, in both runs, on both photometries. The whole-file rows are published in full beside it. The
preview correction explains part of the uncompressed gap (2.4x -> 1.7-1.8x, 1.8x -> 1.6x); the rest
is the fixed IFD and metadata reconstruction, which does not scale with the frame and so dominates
exactly where the raw path is little more than a memcpy. The harness measures that gap and does
not attribute it further — and #583 does not depend on it.

Issues #583 and #584 carry the same incomplete table in their bodies. This run does not edit an
existing issue, so the corrected table lives here and in crates/gamut-dng/STATUS.md, which is the
durable record that gets cited.

The preview-corrected ratio is now printed, so no reader does the subtraction. The harness
applies one counter rule: every benchmark's divan counter is the pixel volume that implementation
actually moves — the raw sample volume for every SDK arm and for gamut's codestream decode, and the
raw volume plus the IFD-0 preview for gamut's whole-file decode and for gamut's encode, both of
which handle the preview too. In decode_dng the median-time column is therefore the uncorrected
ratio and the throughput column the preview-corrected one. The correction charges preview bytes
at the raw path's per-byte rate: close to exact on the uncompressed rows (both paths just move
bytes) and generous to gamut on the compressed ones, where the corrected figure is a lower bound.
The fixture table prints the raw volume, the preview volume and the resulting factor per case.

Design question 6 — the fairness framing, replaced by what was measured

The second pass called one comparison "the SDK is favoured" and the other "gamut is favoured", both
unquantified. Neither adjective survives:

  • Neither side pays for the FFI boundary on the way in. decode_dng_in_memory hands the SDK a
    dng_stream over the caller's own bytes — the same buffer gamut parses. No temporary file, no
    import copy, on either side.
  • Neither side pays for it on the way out, in the container comparison. That entry point
    reports the decoded image's extent and exports no samples.
  • The export path in the codestream comparison is now priced, not asserted. A third arm,
    adobe-sdk-no-export, runs the identical DecodeLosslessJPEG<Scalar> into the identical spool
    buffer and stops before the malloc/memcpy/Vec copies. Measured, that path costs the
    reference implementation 1.9 % / 0.3 % (cfa) and 1.1 % / 0.7 % (linear-raw) across the two
    runs — so the codestream comparison is fair to within 2 %, a number rather than a claim. It
    required one new dev-only oracle entry point (decode_lossless_jpeg_extent) plus its C++ shim,
    pinned by a differential test that the two entry points reach the same decode.

F2 (Medium) — "no CI check runs this file" was false

Correction, naming its entry. The second pass's Validation section said "cargo bench -p gamut-dng --bench codec is not a gate: benchmarks are not tests, and no CI check runs this file."
The second clause is wrong. .github/workflows/extended.yml defines a benches job ("Benches
still run") whose final step is mise run bench-test (mise.toml:243-245, cargo bench --workspace --benches -- --test), which exists precisely to catch a bench that builds and then panics in
setup. The [[bench]] stanza this PR adds moves this file into that job's scope. Corrected
statement: this bench is not one of the four required per-PR checks, but it is run once per
extended-lane CI run, and that gate was not listed or executed in the second pass. It is run and
listed below: mise run bench-test, exit 0, all 24 cases execute.

F3 (Medium) — one denominator, and it is named

Correction, naming its entry. STATUS.md:403-407 (second pass) quoted cfa/lossless-jpeg as
"157.4 % of the raw samples against 137.7 %" — whole encoded files — and then, in the same bullet,
the remedy as "119.7 % to 91.5 %" — bare codestreams. Read in sequence that inflates the remedy
about fivefold. Restated against one denominator, the raw sample volume (393 216 bytes for this
fixture):

  • cfa/uncompressed writes 541 440 bytes, 137.7 % of raw.
  • cfa/lossless-jpeg writes 618 800 bytes, 157.4 % — turning compression on makes the file
    14.3 % larger.
  • The reshape takes the codestream from 470 576 to 359 888 bytes. Both files carry an identical
    148 224 bytes of preview and directory, so the reshaped file is 508 112 bytes, 129.2 % of raw:
    6.2 % below the uncompressed baseline, not ~33 %.

Re-measured this pass through the crate's own public lossless_jpeg::encode; the byte counts
reproduce exactly.

F5 (Low-Medium) — the oracle test now names what it asserts

in_memory_decode_reaches_the_same_image_as_the_file_decode compared extents, not pixels. The
entry point deliberately carries no pixels, so the name was the defect, not the assertion. Renamed
to in_memory_decode_reports_the_same_extent_as_the_file_decode, with the reason in its doc
comment.

F6 (Low) — the commit range

Correction, naming its entry. The second pass's summary sent a reviewer to "the three commits
above 677d38ea". This PR's own diff is the ten commits above 677d38ea
(git log --oneline 677d38ea..HEAD). The two the second pass skipped are 83461e7
(fix(dng): give the benchmark's blue plane blue's gain) and a589d61 (style(dng): pad the
benchmark's case column through the formatter
) — the ones that changed what is measured and that
carry the fixture byte counts quoted above. This pass adds three more.

F7 (Low) — the load factor

Correction, naming its entry. The second pass wrote that the ~60x result "reproduced at 57-71x
across two runs an order of magnitude apart in load". "An order of magnitude" overstates the
difference by roughly 2.5x; the two runs differed by about a factor of four in one-minute load
average. The claim that survives is narrower and is the one made here: the result reproduced across
two runs taken in the same quiet window, at 56-60x.

F8 (Low) — the fixture table is not promoted to a codec gate

#584's verification section proposes pinning the encoder to the sizes this harness prints. It
should not be, and STATUS.md now records why in-repo: the margin is a property of frame-uniform
synthetic gains — one gain per CFA colour across the entire frame, which is what makes the
interleaved-component reshape win so cleanly — and pinning an encoder requirement to a single
synthetic fixture is exactly the failure this harness exists to avoid. The recommendation recorded
in-repo is to re-measure on the real-camera corpus (mise run fetch-dng-samples, then mise run test-dng-real) before the encoder changes, and to gate on that if anything is gated. This run does
not edit #584.

Measurement discipline

Both runs above were taken in a quiet window on a shared machine, each bracketed by uptime (the
one-minute load average, 16 cores):

measurement load before load after
--test ordering check 3.58
run A (--sample-count 100) 3.18 2.91
run B (--sample-count 100) 2.89 2.85
fixture byte re-measurement 2.85

No absolute throughput figure from either run is published, here or in the repository: on a
shared box a MB/s figure is a property of the box. What is published is same-process ratios and
deterministic byte counts.

Validation, this pass

All from the worktree, all completed in this run. Workspace-wide gates ran inside a 16 GiB
memory-capped systemd scope with CARGO_BUILD_JOBS=2.

command result
cargo test -p gamut-dng --all-features --lib sdk_differential pass, 7 tests
cargo test -p gamut-dng-oracle pass, 4 tests
cargo clippy -p gamut-dng -p gamut-dng-oracle --all-targets --all-features -- -D warnings pass
mise run fmt then mise run fmt-check (with __CARGO_TEST_ROOT) pass, exit 0
mise run check-tests pass, exit 0
convco check 677d38ea..HEAD pass, no errors in 10 commits
mise run lint pass, exit 0
mise run test pass, exit 0, 203 suites ok
mise run bench-test pass, exit 0 — the gate F2 says was unlisted; all 24 codec cases execute
mise run check-release-deps pass, exit 0
mise run check-ffi-features pass, exit 0
mise run mutants-diff 78 mutants, 69 caught, 9 unviable, 0 missed

What these gates cover and what they do not: mise run test and mise run lint cover the new
oracle entry point (decode_lossless_jpeg_extent, via the differential test that it reaches the
same decode as the exporting entry) and compile the bench under -D warnings. mise run bench-test executes every case of the bench once, which is what catches a fixture or dimension
mistake. Nothing asserts on the bench's timings, by design — a timing assertion on a shared box
is a flaky test — so the ratios above are evidence from a recorded run, not a gate. mise run coverage was not run: no new module was added with low test reach.

Decisions appended to the record, in the record's shape

10. Each cross-implementation pair is one benchmark taking the implementation as an argument,
    not two benchmarks. divan runs benchmarks in name order, so separate benchmarks measure a
    reference case minutes from its counterpart; the argument names are ordered so divan's own
    name sort keeps a pair adjacent.
    Rejected: keeping the two-benchmark shape and stating the drift as a caveat - the drift is
    larger than the effects being reported (a 1.24x figure became 0.94x once interleaved), so
    the caveat would swallow the result.
    Reverses: split the benchmarks again and publish only the 60x result, which is far larger
    than the drift.

11. The preview asymmetry is corrected inside the harness through divan's counter rather than
    left as arithmetic for the reader: every counter is the pixel volume that implementation
    actually moves, so the throughput column is the preview-corrected ratio and the median-time
    column the uncorrected one.
    Rejected: adding a raw-image-only decode entry point to gamut-dng so the two sides match -
    the record's "will not" forbids changing the codec, and it is the wrong direction of
    causation.
    Rejected: printing only the preview's byte share and leaving the division to the reader.
    That is the state the review found and faulted.
    Known limitation, stated at both sites: the correction charges preview bytes at the raw
    path's per-byte rate, so on the compressed rows it is generous to gamut and the corrected
    ratio is a lower bound.

12. The codestream comparison's export bias is measured by a third arm rather than described.
    `decode_lossless_jpeg_extent` runs the identical SDK decode into the identical spool buffer
    and stops before the FFI copies, so the gap between the two SDK arms is the export path and
    nothing else: 0.3-1.9% across two runs.
    Rejected: leaving the bias as prose ("gamut is favoured, by less"). An unquantified bias in
    the same paragraph as a 57x ratio invites a reader to assume it could matter.
    Reverses: drop the third arm and the shim entry, and restore the prose.

13. #583's isolation rests on the codestream pair, not on the whole-file rows, and every
    whole-file row is published including the two that do not fit. Omitting a contradicting row
    is the defect; the ratio was not.
    Rejected: keeping the "within 1.25x" sentence and adding the missing rows underneath. The
    sentence is false as written and a correction that leaves it standing is not a correction.

14. The fixture table is not promoted to a codec size gate, and the reason is recorded in
    crates/gamut-dng/STATUS.md rather than only in this pull request. The margin is a property
    of frame-uniform synthetic gains; pinning an encoder requirement to one synthetic fixture is
    the failure this harness exists to avoid. Recommend re-measuring on the real-camera corpus
    before the encoder changes.
    Rejected: adding the sizes as an assertion in the crate's test suite, as #584's
    verification section proposes.

This is an unattended run: no human approved this plan, these decisions, or these corrections. The
record above is what a human reads afterwards.

Correction to F6, appended

F6 above says this PR's own diff is "the ten commits above 677d38ea". One further commit
(docs(dng): describe the benchmark's counter rule where it is stated) landed after that sentence
was written, so the count is now eleven. The durable form is the command, not the number:
git log --oneline 677d38ea..HEAD. The two commits the second pass skipped are unchanged:
83461e7 and a589d61.

Fourth pass: the disputed rows reproduce — both of them, and here is why

An independent re-measurement could not reproduce two third-pass ratios: cfa/deflate and
linear-raw/deflate, published at 0.94–0.97 (gamut faster), re-measured eighteen times at
1.20–1.26 (the SDK faster), flat in load from 2.8 to 49, ±2 %, on identical fixture bytes. Its
argument was that if interleaved measurement is stable to ±2 %, 0.95 and 1.24 cannot both be
measurements of the same thing on the same box.

That argument is correct, and it is what finds the defect. They are not measurements of the
same thing. Both figures reproduce here on demand, and what selects between them is which libz
the reference arm called.

tooling/gamut-dng-oracle/build.rs links the system zlib dynamically (-lz), because the SDK
includes <zlib.h> unconditionally. So on the two */deflate rows — and on no other row — the
reference arm runs code not built from source committed here. Worse, which libz is not even a
property of the machine alone: cargo puts every build script's native search path on
LD_LIBRARY_PATH, so the loader picks whichever stock zlib some other dev oracle built under
target/, while running the same binary directly picks the platform's. Here those are stock zlib
1.3.1 and zlib-ng 2.3.3 — and mise run bench-test resolves a third copy again (libpng-oracle's),
because the set of oracles in the graph differs.

The measurement

Eight runs of decode_dng + decode_lossless_jpeg, 100 samples each, 512×384 at 16 bits: two
repetitions of {stock zlib 1.3.1, zlib-ng 2.3.3} × {reference arm first, gamut arm first}, each
bracketed by uptime. One-minute load 15.5 → 38.1; the box did not go quiet in the window and is
reported as it was. Every ratio below is derived from the raw output reproduced further down —
nothing here is published alone.

decode_dng, gamut ÷ Adobe DNG SDK, median time; the four cells per column are
{rep 1, rep 2} × {reference first, gamut first}:

case stock zlib 1.3.1 zlib-ng 2.3.3
cfa/uncompressed 2.17, 2.27, 2.23, 2.21 2.52, 2.26, 2.14, 2.34
cfa/deflate 0.94, 0.94, 0.94, (2.71) 1.23, 1.25, 1.26, 1.17
cfa/lossless-jpeg 83, 50, 89, (18) 87, 45, 66, 92
linear-raw/uncompressed 1.73, 1.72, 1.79, 1.79 1.84, 1.70, 1.78, 1.82
linear-raw/deflate 1.00, 0.90, 0.97, 0.84 1.41, 1.28, 1.28, 1.28
linear-raw/lossless-jpeg 35, 101, 60, 47 47, 47, 75, 57

The parenthesised cfa cells are the noisiest run (load 36.8); its fastest-sample ratios are 0.97
and 27, in line with the rest.

Only one arm moves — that is the isolating evidence. cfa/deflate medians from the dedicated
A/B (three repetitions back to back at a fixed load of 13.2 → 12.3, raw output below):

arm stock zlib 1.3.1 zlib-ng 2.3.3
gamut 1.044, 0.925, 1.052 ms 1.053, 1.051, 1.038 ms
adobe-sdk 0.974, 0.991, 1.113 ms 0.830, 0.829, 0.837 ms

gamut's arm is the control and does not move; the reference arm moves by 1.2–1.3×, exactly enough
to carry the ratio across 1.0. Neither implementation changed; the harness never said which inflate
it had measured. It now prints that, above its own output.

Answering the ±2 % argument directly. The stability claim holds, and this round corroborates it
from the other side: within a library the Deflate ratio is stable to ±2 % across loads 15–38 and
both arm orders (0.94, 0.94, 0.94 stock; 1.23, 1.25, 1.26 zlib-ng). That stability is what makes the
inference sound, and why the explanation had to be a difference in what was measured rather than
in how well — and why waiting for a quieter box was never going to settle it.

Withdrawn, and standing

  • Withdrawn: that 0.94–0.97 is the Deflate ratio. It is the ratio against stock zlib 1.3.1;
    1.20–1.26 is the ratio against a zlib-ng-class libz, and this round reproduces both. Both are
    published, each with its library.
  • Withdrawn: that interleaving accounted for a 25–30 % shift in those rows. Same two rows, same
    magnitude and sign as the libz effect, and the non-interleaved arrangement was not re-run under a
    pinned library. Interleaving is kept on its own argument — a ratio measured minutes apart on a
    drifting machine is not a ratio — and is no longer offered as an explanation of that shift.
  • Withdrawn: "the export path costs the SDK 0.3–1.9 %" (N5), and "56–59× slower" for lossless
    JPEG.
  • Stands: gamut-dng: lossless-JPEG decode is ~60x slower than the reference implementation #583. Across all eight runs, both photometries, both arm orders, no fastest-sample
    ratio is below 34×
    ; medians centre near 50–60×. The precision is withdrawn, the finding is not
    near parity under any measurement here, and its measured path contains no code from outside this
    repository — the SDK's lossless-JPEG decoder is built here from the committed SDK source — so
    unlike the Deflate rows it does not depend on what the machine has installed.
  • Stands: gamut-dng: lossless-JPEG CFA encode expands the payload past uncompressed #584. Byte quantities, printed by the fixture table: 541 440 uncompressed vs 618 800
    lossless-JPEG.
  • Stands: the two uncompressed rows, 2.1–2.5× and 1.7–1.8× uncorrected, published in full
    though they do not fit a tidy story.

N2 — the pin now sits where a gate runs it

tooling/gamut-dng-oracle is under [workspace].exclude, so a #[cfg(test)] test in it is never
compiled by cargo test --workspace, and the one pinning decode_dng_in_memory never ran. Its
sibling decode_lossless_jpeg_extent was already pinned inside gamut-dng, which makes the
omission an inconsistency rather than a judgement call. Moved to
crates/gamut-dng/tests/roundtrip.rs as adobe_in_memory_decode_matches_the_file_decode: encode a
DNG with this crate, decode it both ways, assert the memory-stream entry reports the extent the
exporting one produces and that this is the encoded image's own extent. Both oracle entry points now
carry a doc line naming where their pin lives and why it is not beside them. The false claim under
## Validation is corrected in place, above.

N3 — one flow, not two

gdng_decode_dng_in_memory repeated the seven-call SDK sequence (ParsePostParse
IsValidDNGMake_dng_negativeParsePostParseReadStage1Image) that
read_negative already ran for the file path. read_negative now takes a dng_stream & and the
path form is a two-line overload that opens the file and delegates — so opening the stream is the
whole of the difference between the two flows, which is exactly what the fairness claim asserts.

N4 — the correction is suppressed where the model is a bound

The preview is now modelled at the width the decoder materialises: it is stored at 8 bits, but
every sub-image surfaces as SubImageData::Decoded(Vec<u16>) whatever the stored depth, so the
buffer gamut allocates, fills and frees is twice the stored size. The model under-subtracted by
half; it reads ⌊w/2⌋ × ⌊h/2⌋ × 3 × 2.

And the correction is applied only on the uncompressed rows, where both paths unpack and store
at comparable cost per byte, and suppressed on the compressed ones, where a raw byte carries
entropy-coding work a preview byte does not and the arithmetic yields a lower bound dressed as a
measurement. There gamut's counter is the raw volume and both divan columns are uncorrected. The
fixture table gained a correction column reading applied or SUPPRESSED per case, and the
terminal epilogue printed on every run states why, and what a reader should take a compressed row
to mean instead.

N5 — the bound, not the sign

Across sixteen case-runs the gap between adobe-sdk and adobe-sdk-no-export spans −4 % to
+64 %
: three above 30 %, two negative, the rest 0.2–3.1 %. An effect below this harness's
run-to-run spread, whose sign is not resolved — exactly as the review said. What the fairness claim
needs is a bound (the export path cannot account for a 30×-plus ratio), and that is what is now
published. 0.3–1.9 % is withdrawn: two samples of a quantity at the noise floor.

N7 — the guard its sibling has

gdng_decode_lossless_jpeg_extent narrowed its size_t length to the uint32 dng_stream takes
without checking it fits; gdng_decode_dng_in_memory guards the same narrowing. Added.

Decision 8 — alternating the arm order

divan cannot interleave a pair per sample, so one arm always runs first and the bias points one way
for a whole run. divan's sort is already reversible, so no machinery was invented: --sortr name
runs the gamut arm first (verified from the listing — pair members stay adjacent, only their order
flips), the module header and the printed epilogue both instruct an operator to take one run each
way and publish the mean, and the eight runs above are four of each. Across them the order is worth
about a percent, well under the run-to-run spread; it is corrected for because it is
one-directional, not because it is large.

Validation, this pass

command outcome
__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-check pass
mise run check-tests pass — "module docs, pinned proptest seeds and oracle filenames all conform"
convco check origin/feat/442-dng-c2pa-manifest-store..HEAD pass — "no errors in 17 commits"
mise run lint pass — cargo clippy --workspace --all-targets --all-features -- -D warnings
mise run test pass — 203 test result: ok, 0 failed
mise run bench-test pass — every bench in the workspace executed once
GAMUT_MUTANTS_BASE=origin/feat/442-dng-c2pa-manifest-store mise run mutants-diff pass — "No mutants to filter": 0 missed, and 0 in the diff, the only shipped-source change being inside a #[cfg(test)] module
mise run check-release-deps pass
mise run check-ffi-features pass
cargo test -p gamut-dng --all-features --test roundtrip adobe_in_memory pass (focused)
cargo test -p gamut-dng --all-features --lib sdk_extent pass (focused)

Workspace-wide gates ran inside a systemd-run --user --scope -p MemoryMax=16G scope with
CARGO_BUILD_JOBS=2 and ulimit -v. fmt-check needs __CARGO_TEST_ROOT in a nested worktree —
an environment artefact, not a manifest problem.

Raw output

Every ratio in this pass is derived from the output below. Three edits, all stated: the throughput
(MB/s) columns are dropped (this run publishes no absolute throughput figure), the constant
samples/iters columns are dropped (100 for every row), and the column padding is collapsed —
all three because the body would otherwise exceed GitHub's 65 536-character limit. Every timing,
load average and library identity is verbatim.

Eight runs: {stock zlib, zlib-ng} × {reference first, gamut first} × 2 reps
===== rep=1 zlib=ng order=fwd | before: 07:43:28 up 51 days, 12:28, 31 users, load average: 15.46, 17.16, 20.22 =====
The SDK's Deflate arm calls the system zlib: 1.3.1.zlib-ng from /usr/lib64/libz.so.1.3.1.zlib-ng.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 729 µs | 837.8 µs | 741.4 µs | 746.9 µs
cfa/deflate gamut 899 µs | 987.2 µs | 909.6 µs | 916.5 µs
cfa/lossless-jpeg adobe-sdk 1.083 ms | 2.912 ms | 1.173 ms | 1.63 ms
cfa/lossless-jpeg gamut 66.38 ms | 475.1 ms | 101.6 ms | 110.3 ms
cfa/uncompressed adobe-sdk 17.12 µs | 54.61 µs | 17.56 µs | 19.75 µs
cfa/uncompressed gamut 41.28 µs | 52.9 µs | 44.32 µs | 44.36 µs
linear-raw/deflate adobe-sdk 2.312 ms | 2.675 ms | 2.339 ms | 2.358 ms
linear-raw/deflate gamut 2.871 ms | 5.581 ms | 3.307 ms | 3.212 ms
linear-raw/lossless-jpeg adobe-sdk 4.607 ms | 43.19 ms | 5.458 ms | 6.873 ms
linear-raw/lossless-jpeg gamut 173.3 ms | 711.5 ms | 257.7 ms | 265 ms
linear-raw/uncompressed adobe-sdk 55.62 µs | 108.4 µs | 59.95 µs | 61.27 µs
linear-raw/uncompressed gamut 102.6 µs | 162.2 µs | 110.2 µs | 112 µs
-- decode_lossless_jpeg
cfa adobe-sdk 1.952 ms | 6.648 ms | 2.235 ms | 2.373 ms
cfa adobe-sdk-no-export 1.949 ms | 5.376 ms | 2.165 ms | 2.309 ms
cfa gamut 66.84 ms | 291.5 ms | 104.2 ms | 106.7 ms
linear-raw adobe-sdk 2.835 ms | 3.577 ms | 2.885 ms | 2.932 ms
linear-raw adobe-sdk-no-export 2.8 ms | 5.833 ms | 2.861 ms | 3.026 ms
linear-raw gamut 173.2 ms | 892.2 ms | 261.7 ms | 269.5 ms
after: 07:44:46 up 51 days, 12:29, 31 users, load average: 21.99, 18.89, 20.58
===== rep=1 zlib=ng order=rev | before: 07:44:46 up 51 days, 12:29, 31 users, load average: 21.99, 18.89, 20.58 =====
The SDK's Deflate arm calls the system zlib: 1.3.1.zlib-ng from /usr/lib64/libz.so.1.3.1.zlib-ng.
codec fastest | slowest | median | mean
-- decode_lossless_jpeg
linear-raw gamut 173.3 ms | 572.8 ms | 269.2 ms | 271.1 ms
linear-raw adobe-sdk-no-export 2.787 ms | 5.782 ms | 2.88 ms | 3.808 ms
linear-raw adobe-sdk 2.874 ms | 31.07 ms | 5.668 ms | 7.059 ms
cfa gamut 66.56 ms | 352.3 ms | 96.78 ms | 102.1 ms
cfa adobe-sdk-no-export 1.089 ms | 33.7 ms | 3.313 ms | 4.408 ms
cfa adobe-sdk 1.78 ms | 8.305 ms | 2.157 ms | 2.552 ms
-- decode_dng
linear-raw/uncompressed gamut 99.96 µs | 2.257 ms | 104.7 µs | 155.3 µs
linear-raw/uncompressed adobe-sdk 58.64 µs | 175.9 µs | 61.72 µs | 63.41 µs
linear-raw/lossless-jpeg gamut 172.7 ms | 494 ms | 258.2 ms | 251.9 ms
linear-raw/lossless-jpeg adobe-sdk 2.958 ms | 25.84 ms | 5.506 ms | 6.983 ms
linear-raw/deflate gamut 2.984 ms | 6.174 ms | 3.337 ms | 3.47 ms
linear-raw/deflate adobe-sdk 2.537 ms | 3.439 ms | 2.613 ms | 2.626 ms
cfa/uncompressed gamut 55.22 µs | 79.79 µs | 58.87 µs | 58.9 µs
cfa/uncompressed adobe-sdk 24.05 µs | 62.48 µs | 26.07 µs | 26.68 µs
cfa/lossless-jpeg gamut 66.47 ms | 214.4 ms | 98.34 ms | 97.56 ms
cfa/lossless-jpeg adobe-sdk 1.815 ms | 15.27 ms | 2.165 ms | 2.737 ms
cfa/deflate gamut 1.02 ms | 5.411 ms | 1.053 ms | 1.111 ms
cfa/deflate adobe-sdk 812.9 µs | 1.416 ms | 841.2 µs | 861.7 µs
after: 07:46:02 up 51 days, 12:30, 31 users, load average: 24.73, 20.37, 20.97
===== rep=1 zlib=stock order=fwd | before: 07:46:02 up 51 days, 12:30, 31 users, load average: 24.73, 20.37, 20.97 =====
The SDK's Deflate arm calls the system zlib: 1.3.1 from <worktree>/target/release/build/libtiff-oracle-2023f5bbd35851c7/out/zlib-prefix/lib/libz.so.1.3.1.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 1.054 ms | 16.05 ms | 1.11 ms | 1.307 ms
cfa/deflate gamut 925.6 µs | 1.093 ms | 1.042 ms | 1.015 ms
cfa/lossless-jpeg adobe-sdk 1.086 ms | 2.407 ms | 1.205 ms | 1.508 ms
cfa/lossless-jpeg gamut 66.92 ms | 178.6 ms | 99.83 ms | 100.1 ms
cfa/uncompressed adobe-sdk 25.44 µs | 83.51 µs | 27.18 µs | 28.43 µs
cfa/uncompressed gamut 52.68 µs | 90.74 µs | 58.96 µs | 59.37 µs
linear-raw/deflate adobe-sdk 3.037 ms | 4.089 ms | 3.097 ms | 3.204 ms
linear-raw/deflate gamut 2.9 ms | 4.36 ms | 3.108 ms | 3.179 ms
linear-raw/lossless-jpeg adobe-sdk 2.907 ms | 11.65 ms | 5.608 ms | 5.88 ms
linear-raw/lossless-jpeg gamut 172.9 ms | 692.6 ms | 197 ms | 239 ms
linear-raw/uncompressed adobe-sdk 45.69 µs | 83.48 µs | 47.32 µs | 49.61 µs
linear-raw/uncompressed gamut 78.26 µs | 99.78 µs | 81.84 µs | 82.83 µs
-- decode_lossless_jpeg
cfa adobe-sdk 1.106 ms | 1.748 ms | 1.129 ms | 1.179 ms
cfa adobe-sdk-no-export 1.085 ms | 1.678 ms | 1.109 ms | 1.166 ms
cfa gamut 67.19 ms | 338.4 ms | 82.95 ms | 97.29 ms
linear-raw adobe-sdk 2.858 ms | 30.34 ms | 5.048 ms | 6.112 ms
linear-raw adobe-sdk-no-export 2.799 ms | 38 ms | 5.068 ms | 6.385 ms
linear-raw gamut 180.5 ms | 769 ms | 310.2 ms | 336.2 ms
after: 07:47:23 up 51 days, 12:32, 31 users, load average: 28.36, 22.19, 21.53
===== rep=1 zlib=stock order=rev | before: 07:47:23 up 51 days, 12:32, 31 users, load average: 28.36, 22.19, 21.53 =====
The SDK's Deflate arm calls the system zlib: 1.3.1 from <worktree>/target/release/build/libtiff-oracle-2023f5bbd35851c7/out/zlib-prefix/lib/libz.so.1.3.1.
codec fastest | slowest | median | mean
-- decode_lossless_jpeg
linear-raw gamut 186 ms | 936.6 ms | 295.3 ms | 314.4 ms
linear-raw adobe-sdk-no-export 2.935 ms | 17.76 ms | 5.348 ms | 6.616 ms
linear-raw adobe-sdk 2.959 ms | 33.87 ms | 6.72 ms | 9.739 ms
cfa gamut 77.68 ms | 327.2 ms | 112.5 ms | 129.8 ms
cfa adobe-sdk-no-export 1.6 ms | 14.14 ms | 1.996 ms | 2.207 ms
cfa adobe-sdk 1.702 ms | 26.93 ms | 2.074 ms | 3.168 ms
-- decode_dng
linear-raw/uncompressed gamut 82.62 µs | 282 µs | 102.9 µs | 104.7 µs
linear-raw/uncompressed adobe-sdk 57.45 µs | 153.4 µs | 59.78 µs | 61.46 µs
linear-raw/lossless-jpeg gamut 181.5 ms | 1.088 s | 303 ms | 354.4 ms
linear-raw/lossless-jpeg adobe-sdk 2.815 ms | 5.339 ms | 2.993 ms | 3.558 ms
linear-raw/deflate gamut 2.9 ms | 3.549 ms | 3.245 ms | 3.178 ms
linear-raw/deflate adobe-sdk 3.08 ms | 10.36 ms | 3.602 ms | 4.783 ms
cfa/uncompressed gamut 40.76 µs | 59.58 µs | 41.55 µs | 42.32 µs
cfa/uncompressed adobe-sdk 17.81 µs | 30.73 µs | 18.34 µs | 19.33 µs
cfa/lossless-jpeg gamut 66.49 ms | 270.9 ms | 94.34 ms | 102.4 ms
cfa/lossless-jpeg adobe-sdk 1.755 ms | 12.4 ms | 1.873 ms | 2.152 ms
cfa/deflate gamut 990.4 µs | 3.013 ms | 1.006 ms | 1.056 ms
cfa/deflate adobe-sdk 977.2 µs | 5.518 ms | 1.066 ms | 1.226 ms
after: 07:48:57 up 51 days, 12:33, 31 users, load average: 30.05, 24.97, 22.62
===== rep=2 zlib=ng order=fwd | before: 07:48:57 up 51 days, 12:33, 31 users, load average: 30.05, 24.97, 22.62 =====
The SDK's Deflate arm calls the system zlib: 1.3.1.zlib-ng from /usr/lib64/libz.so.1.3.1.zlib-ng.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 814.4 µs | 3.72 ms | 842.6 µs | 1.046 ms
cfa/deflate gamut 929.6 µs | 2.876 ms | 1.058 ms | 1.116 ms
cfa/lossless-jpeg adobe-sdk 1.112 ms | 2.17 ms | 1.2 ms | 1.514 ms
cfa/lossless-jpeg gamut 67.37 ms | 305.8 ms | 78.69 ms | 92.22 ms
cfa/uncompressed adobe-sdk 18.7 µs | 68.73 µs | 26.84 µs | 27.2 µs
cfa/uncompressed gamut 53.4 µs | 72.61 µs | 57.44 µs | 58.03 µs
linear-raw/deflate adobe-sdk 2.525 ms | 5.48 ms | 2.623 ms | 2.802 ms
linear-raw/deflate gamut 3.182 ms | 6.247 ms | 3.352 ms | 3.388 ms
linear-raw/lossless-jpeg adobe-sdk 2.8 ms | 6.123 ms | 2.877 ms | 3.183 ms
linear-raw/lossless-jpeg gamut 174.7 ms | 448.2 ms | 217.1 ms | 236.1 ms
linear-raw/uncompressed adobe-sdk 58.11 µs | 98.21 µs | 59.32 µs | 60.31 µs
linear-raw/uncompressed gamut 80.58 µs | 141.4 µs | 105.8 µs | 98.37 µs
-- decode_lossless_jpeg
cfa adobe-sdk 1.089 ms | 1.267 ms | 1.106 ms | 1.121 ms
cfa adobe-sdk-no-export 1.075 ms | 1.592 ms | 1.134 ms | 1.162 ms
cfa gamut 66.51 ms | 460.7 ms | 101.4 ms | 106.8 ms
linear-raw adobe-sdk 2.842 ms | 21.04 ms | 5.145 ms | 4.704 ms
linear-raw adobe-sdk-no-export 2.798 ms | 5.135 ms | 2.862 ms | 2.989 ms
linear-raw gamut 174.9 ms | 840.3 ms | 238.1 ms | 261.2 ms
after: 07:50:10 up 51 days, 12:34, 31 users, load average: 28.62, 25.51, 22.99
===== rep=2 zlib=ng order=rev | before: 07:50:10 up 51 days, 12:34, 31 users, load average: 28.62, 25.51, 22.99 =====
The SDK's Deflate arm calls the system zlib: 1.3.1.zlib-ng from /usr/lib64/libz.so.1.3.1.zlib-ng.
codec fastest | slowest | median | mean
-- decode_lossless_jpeg
linear-raw gamut 172.6 ms | 864.1 ms | 282.6 ms | 316.3 ms
linear-raw adobe-sdk-no-export 2.837 ms | 9.826 ms | 5.404 ms | 4.936 ms
linear-raw adobe-sdk 4.024 ms | 15.96 ms | 5.391 ms | 6.094 ms
cfa gamut 68.12 ms | 697.7 ms | 112.5 ms | 138.4 ms
cfa adobe-sdk-no-export 1.082 ms | 2.824 ms | 1.731 ms | 1.608 ms
cfa adobe-sdk 1.11 ms | 5.568 ms | 2.002 ms | 2.093 ms
-- decode_dng
linear-raw/uncompressed gamut 89.48 µs | 167.5 µs | 107.7 µs | 107.4 µs
linear-raw/uncompressed adobe-sdk 55.37 µs | 140.4 µs | 59.15 µs | 60.03 µs
linear-raw/lossless-jpeg gamut 177.1 ms | 973.7 ms | 296.8 ms | 340.2 ms
linear-raw/lossless-jpeg adobe-sdk 2.882 ms | 20.2 ms | 5.169 ms | 6.077 ms
linear-raw/deflate gamut 3.111 ms | 39.16 ms | 3.356 ms | 4.347 ms
linear-raw/deflate adobe-sdk 2.36 ms | 2.759 ms | 2.614 ms | 2.591 ms
cfa/uncompressed gamut 51.25 µs | 304.3 µs | 56.46 µs | 60.81 µs
cfa/uncompressed adobe-sdk 23.55 µs | 59.26 µs | 24.12 µs | 24.92 µs
cfa/lossless-jpeg gamut 71.02 ms | 451.6 ms | 112.9 ms | 134.2 ms
cfa/lossless-jpeg adobe-sdk 1.096 ms | 2.386 ms | 1.222 ms | 1.465 ms
cfa/deflate gamut 906.9 µs | 1.112 ms | 976.7 µs | 973 µs
cfa/deflate adobe-sdk 739.5 µs | 900.4 µs | 833.8 µs | 806.8 µs
after: 07:51:47 up 51 days, 12:36, 31 users, load average: 38.13, 29.89, 24.81
===== rep=2 zlib=stock order=fwd | before: 07:51:47 up 51 days, 12:36, 31 users, load average: 38.13, 29.89, 24.81 =====
The SDK's Deflate arm calls the system zlib: 1.3.1 from <worktree>/target/release/build/libtiff-oracle-2023f5bbd35851c7/out/zlib-prefix/lib/libz.so.1.3.1.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 965.7 µs | 1.113 ms | 1.027 ms | 1.018 ms
cfa/deflate gamut 916.8 µs | 1.636 ms | 962.8 µs | 992.4 µs
cfa/lossless-jpeg adobe-sdk 1.077 ms | 2.044 ms | 1.124 ms | 1.255 ms
cfa/lossless-jpeg gamut 67.67 ms | 378.9 ms | 100.1 ms | 107.8 ms
cfa/uncompressed adobe-sdk 18.15 µs | 56.71 µs | 19.53 µs | 21.07 µs
cfa/uncompressed gamut 42.44 µs | 59.45 µs | 43.54 µs | 46.66 µs
linear-raw/deflate adobe-sdk 3.067 ms | 3.561 ms | 3.099 ms | 3.155 ms
linear-raw/deflate gamut 2.884 ms | 3.424 ms | 3.005 ms | 3.032 ms
linear-raw/lossless-jpeg adobe-sdk 2.799 ms | 19.61 ms | 4.242 ms | 4.905 ms
linear-raw/lossless-jpeg gamut 175.9 ms | 477.1 ms | 254.1 ms | 249.9 ms
linear-raw/uncompressed adobe-sdk 54.61 µs | 212.1 µs | 55.78 µs | 59.32 µs
linear-raw/uncompressed gamut 94.3 µs | 6.632 ms | 99.96 µs | 166.1 µs
-- decode_lossless_jpeg
cfa adobe-sdk 1.82 ms | 11.2 ms | 2.152 ms | 2.319 ms
cfa adobe-sdk-no-export 1.793 ms | 4.386 ms | 2.129 ms | 2.168 ms
cfa gamut 67.17 ms | 504 ms | 100.1 ms | 115.1 ms
linear-raw adobe-sdk 4.336 ms | 15.84 ms | 5.61 ms | 6.5 ms
linear-raw adobe-sdk-no-export 4.52 ms | 17.3 ms | 5.531 ms | 5.867 ms
linear-raw gamut 177.5 ms | 821.9 ms | 316.9 ms | 349.2 ms
after: 07:53:12 up 51 days, 12:37, 31 users, load average: 36.77, 31.01, 25.62
===== rep=2 zlib=stock order=rev | before: 07:53:12 up 51 days, 12:37, 31 users, load average: 36.77, 31.01, 25.62 =====
The SDK's Deflate arm calls the system zlib: 1.3.1 from <worktree>/target/release/build/libtiff-oracle-2023f5bbd35851c7/out/zlib-prefix/lib/libz.so.1.3.1.
codec fastest | slowest | median | mean
-- decode_lossless_jpeg
linear-raw gamut 175.7 ms | 551.2 ms | 260.4 ms | 269.8 ms
linear-raw adobe-sdk-no-export 2.823 ms | 10.14 ms | 5.223 ms | 4.722 ms
linear-raw adobe-sdk 3.734 ms | 37.32 ms | 6.181 ms | 8.46 ms
cfa gamut 67.11 ms | 276.6 ms | 100.2 ms | 101.8 ms
cfa adobe-sdk-no-export 1.106 ms | 2.49 ms | 2.166 ms | 2.102 ms
cfa adobe-sdk 1.092 ms | 2.578 ms | 1.986 ms | 1.739 ms
-- decode_dng
linear-raw/uncompressed gamut 95.02 µs | 144.3 µs | 108.5 µs | 108.6 µs
linear-raw/uncompressed adobe-sdk 57.98 µs | 134.7 µs | 60.73 µs | 62.13 µs
linear-raw/lossless-jpeg gamut 175.9 ms | 754.6 ms | 263.5 ms | 288.8 ms
linear-raw/lossless-jpeg adobe-sdk 3.04 ms | 14.32 ms | 5.656 ms | 5.757 ms
linear-raw/deflate gamut 2.911 ms | 6.805 ms | 3.345 ms | 3.534 ms
linear-raw/deflate adobe-sdk 3.645 ms | 8.548 ms | 3.971 ms | 4.23 ms
cfa/uncompressed gamut 54.9 µs | 79.65 µs | 58.5 µs | 58.88 µs
cfa/uncompressed adobe-sdk 24.18 µs | 2.175 ms | 26.53 µs | 48.32 µs
cfa/lossless-jpeg gamut 68.53 ms | 220.4 ms | 102.9 ms | 110.6 ms
cfa/lossless-jpeg adobe-sdk 2.5 ms | 18.52 ms | 5.817 ms | 6.639 ms
cfa/deflate gamut 1.03 ms | 43.96 ms | 3.034 ms | 4.199 ms
cfa/deflate adobe-sdk 1.062 ms | 8.907 ms | 1.12 ms | 1.307 ms
after: 07:54:34 up 51 days, 12:39, 31 users, load average: 27.75, 29.57, 25.56
The isolating A/B: {zlib-ng, stock zlib} back to back, three reps, Deflate rows
===== rep=1 zlib=ng load-before=13.11 =====
The SDK's Deflate arm calls the system zlib: 1.3.1.zlib-ng from /usr/lib64/libz.so.1.3.1.zlib-ng.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 740.2 µs | 892.9 µs | 829.6 µs | 811.7 µs
cfa/deflate gamut 921 µs | 10.83 ms | 1.053 ms | 1.291 ms
linear-raw/deflate adobe-sdk 2.32 ms | 2.775 ms | 2.622 ms | 2.589 ms
linear-raw/deflate gamut 2.839 ms | 3.081 ms | 2.885 ms | 2.896 ms
load-after=13.11
===== rep=1 zlib=stock load-before=13.11 =====
The SDK's Deflate arm calls the system zlib: 1.3.1 from <worktree>/target/release/build/libtiff-oracle-2023f5bbd35851c7/out/zlib-prefix/lib/libz.so.1.3.1.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 954.9 µs | 1.118 ms | 974 µs | 1.009 ms
cfa/deflate gamut 911 µs | 1.09 ms | 1.044 ms | 1.007 ms
linear-raw/deflate adobe-sdk 3.072 ms | 5.156 ms | 3.535 ms | 3.475 ms
linear-raw/deflate gamut 3.214 ms | 6.39 ms | 3.346 ms | 3.656 ms
load-after=12.70
===== rep=2 zlib=ng load-before=12.70 =====
The SDK's Deflate arm calls the system zlib: 1.3.1.zlib-ng from /usr/lib64/libz.so.1.3.1.zlib-ng.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 810.5 µs | 1.811 ms | 829.4 µs | 842.9 µs
cfa/deflate gamut 999.7 µs | 2.086 ms | 1.051 ms | 1.068 ms
linear-raw/deflate adobe-sdk 2.488 ms | 5.768 ms | 2.669 ms | 2.809 ms
linear-raw/deflate gamut 3.008 ms | 7.149 ms | 3.425 ms | 3.609 ms
load-after=12.70
===== rep=2 zlib=stock load-before=12.70 =====
The SDK's Deflate arm calls the system zlib: 1.3.1 from <worktree>/target/release/build/libtiff-oracle-2023f5bbd35851c7/out/zlib-prefix/lib/libz.so.1.3.1.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 967.6 µs | 1.213 ms | 991.1 µs | 1.032 ms
cfa/deflate gamut 912.3 µs | 1.051 ms | 924.7 µs | 932 µs
linear-raw/deflate adobe-sdk 3.016 ms | 3.621 ms | 3.073 ms | 3.134 ms
linear-raw/deflate gamut 2.919 ms | 40.32 ms | 3.314 ms | 3.674 ms
load-after=12.70
===== rep=3 zlib=ng load-before=12.70 =====
The SDK's Deflate arm calls the system zlib: 1.3.1.zlib-ng from /usr/lib64/libz.so.1.3.1.zlib-ng.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 800.9 µs | 3.736 ms | 836.8 µs | 996 µs
cfa/deflate gamut 1.021 ms | 1.288 ms | 1.038 ms | 1.047 ms
linear-raw/deflate adobe-sdk 2.552 ms | 22.79 ms | 2.708 ms | 3.97 ms
linear-raw/deflate gamut 2.975 ms | 6.472 ms | 3.354 ms | 3.459 ms
load-after=12.70
===== rep=3 zlib=stock load-before=12.70 =====
The SDK's Deflate arm calls the system zlib: 1.3.1 from <worktree>/target/release/build/libtiff-oracle-2023f5bbd35851c7/out/zlib-prefix/lib/libz.so.1.3.1.
codec fastest | slowest | median | mean
-- decode_dng
cfa/deflate adobe-sdk 1.078 ms | 1.217 ms | 1.113 ms | 1.116 ms
cfa/deflate gamut 1.029 ms | 1.12 ms | 1.052 ms | 1.055 ms
linear-raw/deflate adobe-sdk 3.047 ms | 3.607 ms | 3.105 ms | 3.171 ms
linear-raw/deflate gamut 2.84 ms | 3.392 ms | 2.881 ms | 2.912 ms
load-after=12.32

Decisions appended to the record, in the record's shape

Taken — the harness reports the libz its reference arm called, and does not pin it.
The SDK includes <zlib.h> unconditionally and build.rs links -lz; vendoring a zlib into the
oracle would change what the oracle is — a build of the reference implementation as the platform
builds it — to make one benchmark row tidier. zlib_identity() returns zlibVersion() plus the
path dladdr reports, because zlib-ng's compatibility build answers "1.3.1" exactly as stock
zlib does and the version alone does not discriminate.
Rejected with evidence: vendoring a zlib (changes the reference implementation to suit a
measurement); publishing one library's Deflate figure as the figure (that is the defect being
repaired).
Reverses: nothing.

Taken — the preview correction is applied only where it is a measurement.
Charging preview bytes at the raw path's per-byte rate is sound where both paths unpack and store,
unsound where the raw path also entropy-decodes. On the latter the harness prints no corrected
number and says why, at the fixture table and at the terminal epilogue.
Rejected with evidence: keeping the correction everywhere and labelling it a lower bound in prose
— a printed number is read as measured, and the two prose sites were not where an operator reads.
Reverses: the third pass's counter rule, which corrected every row.

Taken — the arm order is alternated with divan's own reversible sort, not new machinery.
--sortr name flips the arm order within every pair while keeping them adjacent; verified from the
listing. A published ratio is the mean of one run each way, stated in the module header and the
printed epilogue.
Rejected with evidence: an env var or a rank prefix in the argument names — both invent a control
divan already has, and the prefix would change the row labels between the two runs a reader is
meant to compare. Reverses: nothing.

Taken — record the withdrawn attributions rather than quietly replacing them.
STATUS.md names which claims are withdrawn and what replaced them; a benchmark section whose
history is invisible is how a superseded number gets cited.
Rejected with evidence: silently rewriting the tables. Reverses: nothing.

This is an unattended run: no human approved this plan, these decisions, or these corrections. The
record above is what a human reads afterwards.

Fifth pass: where the correction did not reach

The fourth pass's mechanism was re-established independently, and tighter than this body stated:
the interfering build is libtiff-oracle, a direct dev-dependency of gamut-dng itself, so
cargo bench -p gamut-dng alone is enough to flip which zlib the reference arm calls — no unrelated
crate need be involved. Reproduced again this pass: cargo bench -p gamut-dng --bench codec prints
1.3.1 from …/target/release/build/libtiff-oracle-*/out/zlib-prefix/lib/libz.so.1.3.1, while the
platform's /usr/lib64/libz.so.1 is libz.so.1.3.1.zlib-ng (zlib-ng-compat-2.3.3-3.fc44), and
mise run bench-test resolves a third copy again (libpng-oracle's). No shipped code needed
changing for that.
What follows is the four places the correction had not reached.

F3 — name what each arm actually runs

On the disputed rows neither arm's inflate is gamut-authored: gamut-deflate is deliberately
encoder-only, so this crate inflates with miniz_oxide. The module header, the printed epilogue and
the STATUS.md section all said "one measured path is not built from this repository" and
"every other row runs only code built here", which a reader takes as a claim about gamut's own
codec. Corrected in all three. The distinction that carries the section's real content is now the
one stated: miniz_oxide is pinned by Cargo.lock to one version and one checksum, so every run of
this harness anywhere inflates with the same code; the system libz is pinned by nothing — not by a
version, since zlib-ng answers zlibVersion() with stock zlib's own string, and not by the machine.

Decision 5 (design question 2) — the harness flags the accident, not just the identity

Printing the resolved library makes a Deflate ratio interpretable; it does not make it
reproducible. print_zlib_identity now warns when the resolved path has a target component,
because a resolution that came out of the build graph belongs to that build graph and to nobody
else. The path is split out of the identity string into gamut_dng_oracle::zlib_path so a caller
can test it rather than parse a printed line. Observed firing on both cargo bench -p gamut-dng --bench codec and mise run bench-test — on two different oracles' copies of zlib, which is itself
the point.

F4 — the guard is symmetric now, and its rationale had named the wrong sibling

gdng_decode_lossless_jpeg_extent rejects a size_t length that does not fit the uint32
dng_stream takes; the exporting arm it is timed against, gdng_decode_lossless_jpeg, narrowed
the same value unguarded. The guard's rationale cited gdng_decode_dng_in_memory as the sibling to
match — the wrong one. Both arms carry the identical check now, and both sites state why: these two
entry points are subtracted from each other, so a check one runs and the other does not is a
difference inside the measured region as well as a difference in what each accepts. Unreachable from
this crate's fixtures either way; the streams are kilobytes.

F1 / decision 2 — the two filed issues, corrected by a third

#583 and #584 are what someone will actually act on, and both still carry withdrawn material. This
run may not edit, comment on, label or close an existing issue under any authority, so the
correction is filed as #617, which states per issue which figure is withdrawn and what replaced
it:

Both findings themselves stand, and neither issue is edited by this run. STATUS.md now points at
#617, and at #618 for the linkage question.

Decision 6 (design question 1) — the row stays, the linkage question is filed

Pinning a zlib for benchmarking while keeping -lz for conformance is the right idea, and a real
build-system change to a crate every gamut-dng test links; taking it here would widen this round's
blast radius across the run. The Deflate row keeps its place with the accurate label F3 gives it,
and the proposal is filed as #618 with its evidence.

The mutation counts published in earlier passes were taken against the wrong base

## Third pass and ## Fourth pass both report mise run mutants-diff as "78 mutants, 69 caught,
9 unviable, 0 missed". That selection used the runner's default base, origin/master, and this
branch is stacked — so it folded in every mutant belonging to #508 underneath. Re-run this pass
with the correct base, GAMUT_MUTANTS_BASE=origin/feat/442-dng-c2pa-manifest-store, the selection
is 0 mutants, which is the honest answer for this diff: its only src/ change lives inside a
#[cfg(test)] mod tests block, bench targets produce no mutants, and tooling/** is excluded by
.cargo/mutants.toml. The tool is working — cargo mutants -p gamut-dng --list lists 1964 mutants
in the crate. The 78/69/9/0 figures are withdrawn as evidence about this pull request. Nothing
this PR claims rested on them.

Validation, this pass

command outcome
__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt, then the same prefix for mise run fmt-check pass, exit 0
mise run check-tests pass — "module docs, pinned proptest seeds and oracle filenames all conform"
convco check origin/feat/442-dng-c2pa-manifest-store..HEAD pass — "no errors in 22 commits"
cargo test -p gamut-dng --all-features pass, exit 0
mise run lint pass, exit 0
mise run bench-test pass, exit 0 — every bench executes; the new warning fires inside it
GAMUT_MUTANTS_BASE=origin/feat/442-dng-c2pa-manifest-store mise run mutants-diff pass, exit 0 — 0 mutants selected, base stated because it changes the answer
cargo bench -p gamut-dng --bench codec -- --test pass — fixture table, zlib identity and build-tree warning as described

One-minute load averages bracketing the runs: 27.3 → 33.9, on a shared box. No timing claim is
made in this pass
— no ratio is published here, so the load bounds what a re-measurement would
have cost rather than what this pass concluded; the measured run remains ## Fourth pass.
mise run check-release-deps and mise run check-ffi-features were not re-run: no manifest changed
this pass, and both passed on the manifest change recorded in ## Third pass.

Decisions appended to the record, in the record's shape

Taken — correct ## Summary in place rather than appending a fifth correction to it. The
append-only convention protects the decision record, not the summary; the summary is what a reader
meets first, and it still carried the pre-N4 preview model (halved), two withdrawn adjectives, and a
description of a benchmark layout that no longer exists. It now describes what the harness runs, and
carries a forward pointer to all five passes.
Rejected with evidence: appending a fifth "what the summary should now say" section — four such
sections already exist, and the finding is precisely that a reader does not reach them.
Reverses: restore the superseded text from this PR's edit history.

Taken — file the record correction as a new issue rather than editing #583/#584. This run may
not edit, comment on, label or close an existing issue under any authority. #617 names both issues,
states which figures are withdrawn and what replaced them, gives the working verification command,
and says why the originals could not be corrected in place. It is linked from ## Issue and left as
an unresolved note, because closing that loop needs a human with write access.
Rejected with evidence: leaving the issues as they stand — #583's localisation sentence is false
as written and its verification command runs nothing, so an implementer follows it into an empty
result.
Reverses: close #617 once #583 and #584 carry the corrections.

Taken — name miniz_oxide and make pinning, not authorship, the section's claim. Saying "one
measured path is not built from this repository" is true and misleading: it implies the other path
is gamut's codec. Neither arm's inflate is gamut-authored on those rows; what makes one of them
reproducible is Cargo.lock.
Rejected with evidence: dropping the Deflate rows instead — they are the rows a DNG user's files
actually take, and an unlabelled absence is worse than a labelled dependency.
Reverses: restore the one-path wording.

Taken — warn when the resolved libz path lies inside a build directory. The print made the row
interpretable; the warning makes its irreproducibility visible without a reader having to recognise
a target/ path. Implemented as a path test on a value the oracle now returns unformatted, not as
string-matching on a printed line.
Rejected with evidence: comparing against this build's own target/ only — the loader may resolve
any build script's copy in the graph, and every one of them is equally unreproducible elsewhere.
Reverses: drop the warning.

Taken — give the exporting lossless-JPEG arm the guard its timed twin has. An asymmetric guard
is the shape a later reader mistakes for a deliberate distinction, and here the two entry points
exist to be subtracted from each other.
Rejected with evidence: removing the guard from the extent arm instead, which would restore
symmetry by deleting a correct check.
Reverses: revert the guard commit.

Taken — keep the Deflate row and file the oracle-linkage question rather than changing -lz.
Pinning the benchmark's zlib while keeping -lz for conformance is right, and it is a build-system
change to a crate every gamut-dng test links; taking it inside a benchmark PR widens the blast
radius across concurrent work.
Rejected with evidence: pinning it here — the conformance oracle's behaviour is not this PR's to
change, and the row is usable once labelled.
Reverses: implement the pinned link under #618.

Taken — publish the mutation selection against the stacked base, and withdraw the earlier
counts.
A count taken against origin/master on a stacked branch includes the base PR's mutants
and is not evidence about this diff. The correct base selects zero, and zero is stated with its
reason rather than presented as coverage.
Rejected with evidence: re-publishing the 78/69/9/0 figures with a caveat — they measure #508 and
this PR together, so no caveat makes them evidence about this one.
Reverses: nothing.

This remains an unattended run: no human approved this plan, these decisions, or these corrections.
The record above is what a human reads afterwards.

Sixth pass: the redaction that was clobbered, and a justification that was never executed

Two findings, both about text rather than about what the harness measures. No source behaviour
changes in this pass
— the diff is one commit of comments, doc comments, a printed string and a
STATUS.md paragraph.

F1 (High) — the seven raw-output lines named the tooling again, and this pass's own publish did it

The ### Raw output block under ## Fourth pass prints the harness's own zlib-identity line seven
times, once per recorded run, and each carried an absolute path whose leading components name the
tooling that produced this pull request rather than anything about the measurement. That path was
redacted on the published body out of band between rounds — and the next publish restored it,
because this lane's publish step rebuilds the body from a local draft file and overwrites
whatever is live. Any edit made to the published body is silently clobbered by the next publish; the
draft never saw the redaction. Confirmed by comparing the live body against the draft that produced
it: byte-identical apart from a trailing newline, seven occurrences of the full path in each, zero
of the redacted form.

That also refutes something the fifth round reported when it handed this pull request on — not in
this body, but in its own account of what it had changed: that everything below the Summary was
byte-identical to the previous publish. The prose was. The raw-output block was not: the seven
lines it re-emitted differed from what was live at the time, because they came from the draft.

The seven lines are now redacted to <worktree>/target/release/build/libtiff-oracle-…, which
keeps the whole of the fact those lines carry — that the loader resolved libz from inside a Cargo
build directory, out of libtiff-oracle's build script, rather than from the platform — and drops
only the prefix that identifies the machine and the tooling. The measurement is unchanged; nothing
below the redacted prefix is edited. This is declared rather than done silently, because it edits
text inside an append-only record: the replaced substring, the seven lines it appears on, and the
reason are all stated here.

The pipeline, not the habit, is what changed. The publish step now runs a check on the assembled
body before it is sent, which greps for a tooling path and exits non-zero without publishing if it
finds one, printing every offending line number. Executed both ways: it refuses on the pre-redaction
body, naming all seven lines, and passes on the body this pass publishes. A check that runs after
publishing, or a resolution to remember, would both have failed the same way the last six rounds
did.

F2 (Medium) — the stated reason for the path print is false on this box

Every artefact that explains why the harness prints the resolved libz path justified it the same
way: that a zlib-ng compatibility build answers zlibVersion() with stock zlib's string, so the
version cannot separate the two builds. Executed here:

/usr/lib64/libz.so.1                                   -> 1.3.1.zlib-ng
<worktree>/target/release/build/libtiff-oracle-…/libz.so.1.3.1 -> 1.3.1

(dlopen, dlsym("zlibVersion"), print; strings on both objects agrees.) The platform build
answers 1.3.1.zlib-ng and the build-tree copy answers 1.3.1, so for that pair the version
string is sufficient — and ## Fourth pass's own raw output is labelled with exactly that split,
three lines from where the claim was made. The worked example in the oracle source was worse than
unsupported: it showed a version of 1.3.1 resolving to a path ending .zlib-ng, a composite that
cannot occur.

The mechanism and the print survive; only the justification was wrong. What is true of every
box is the shape, not the pair: the loader chooses between a copy some dev oracle built under
target/ and whatever the platform installed, and zlibVersion() separates those two only when the
platform's build renamed itself. This box's did. A box shipping stock zlib 1.3.1 gives two
resolutions that answer identically, so the identification cannot rest on a fork choosing to rename
itself — the path tells them apart either way. That is what every site now says.

The first correction commit did not get this right either: it replaced the false claim with a
second unexecuted one — that the pair the loader collides is two stock builds — which contradicts
its own commit message three lines down. 8b34b973 fixes that to the shape above. Both commits are
in the diff; the second is named here rather than folded into the first, because the first is
pushed.

Corrected at every site:

site what it said what it says now
crates/gamut-dng/STATUS.md (Deflate-row paragraph) "zlib-ng's compatibility build answers zlibVersion() with "1.3.1", exactly as stock zlib does" the loader chooses between a target/ copy and the platform's; the version separates them only if the platform renamed itself, which this box's did and a stock box's does not
crates/gamut-dng/STATUS.md (why the path is printed) "since the version string cannot tell the two builds apart" "the path is what identifies the resolution, because two stock builds of one version are indistinguishable by version string"
crates/gamut-dng/benches/codec.rs module header "zlib-ng's compatibility build answers zlibVersion() with stock zlib's own string" the same shape: a box shipping stock zlib 1.3.1 gives two resolutions that answer identically, so the identification rests on the path
crates/gamut-dng/benches/codec.rs printed epilogue (FIXTURE_TABLE_EPILOGUE) "not by a version (zlib-ng answers zlibVersion() with stock zlib's string)" "two stock builds of one zlib version answer zlibVersion() identically, so the path printed above, not the version, is what says which one was loaded"
tooling/gamut-dng-oracle/src/lib.rs doc comment worked example "1.3.1 from /usr/lib64/libz.so.1.3.1.zlib-ng", justified by the zlib-ng claim worked example "1.3.1 from /usr/lib64/libz.so.1.3.1" — a resolution that can occur — and the corrected reason
tooling/gamut-dng-oracle/src/oracle_shim.cpp comment same composite example, "cannot tell the two apart" the two candidate paths spelled out, and the version described as separating them only when the platform's build renamed itself
this body, ## Summary (Deflate bullet) "not by a version, since zlib-ng answers zlibVersion() with stock zlib's string" the corrected reason; the Summary is the current description, so it is corrected in place
issue #618 "zlibVersion() cannot tell them apart… Only the path discriminates" cannot be edited by this run — corrected in a new issue, below

Two dated entries in this body carry the superseded wording and are deliberately left standing.
## Fourth passDecisions appended to the record says "because zlib-ng's compatibility build
answers "1.3.1" exactly as stock zlib does and the version alone does not discriminate", and
## Fifth pass says "the system libz is pinned by nothing — not by a version, since zlib-ng answers
zlibVersion() with stock zlib's own string". A correction may not rewrite the text it corrects: an
edit in place would leave this pass pointing at sentences that no longer say what it says they said.
Both decisions stand on the corrected reason — the path is what identifies the resolution — and
neither figure they were taken beside is affected.

The claim's origin is recorded, not hidden: it was not invented here. It was carried verbatim in
the shared contract this run's lanes read before writing anything, and it propagated into four
in-repo sites, a printed epilogue, this body and a filed issue before anyone executed
zlibVersion() against both libraries. The contract has since been corrected at its source. The
next reader deserves to know a shared contract carried it into three artefacts before it was
checked — not as an excuse, but because "four artefacts agree" is exactly the evidence that made it
look settled.

F2's filed-issue site — a new issue, not a fold into #617

#618 carries the false sentence and this run may not edit an existing issue under any authority.
Filed as a new correction issue rather than folded into #617: #617 is itself already published,
so folding would mean editing it, which is the same prohibition. #617 corrects #583 and #584; the
new issue corrects #618. Both are linked from ## Issue and both need a human to fold them in and
close them.

F3 (Low) — a withdrawn direction claim survives in a commit message

8956ad00 ("perf(dng): benchmark encode and decode against the Adobe DNG SDK") says "the
lossless-JPEG codestream decode favours gamut (the oracle's export path costs the SDK two extra
passes)", and c6620b29 promises to state "which of the two gamut-versus-SDK comparisons favours
which side and by how much". Both were withdrawn by ## Fourth pass (N5): the export-path gap spans
−4 % to +64 % over sixteen case-runs, so it is a bound at the measurement floor, not a signed cost.
Later commits do retract in-message — 31f01840 and 347c8ade each withdraw a different claim —
so the pattern exists and simply was not applied here. Both commits are pushed and this run may not
rewrite pushed history, so it is recorded in the unresolved notes with the commits named rather
than fixed.

F4 (Low, pre-existing) — ## Validation promised commands and carried a placeholder

## Validation opened with "Commands verbatim, with outcome" and then contained an unsubstituted
template marker instead of any command. Substituted, with this pass's gate set; earlier passes'
commands stay under their own Validation, this pass headings, which the section now names.

Design questions 1, 2 and 3 — declined, and recorded for a human

A version-mismatch warning, a build-time recorded path to compare the run-time one against, and
printing every dynamic library the reference arm links are each reasonable, and each adds machinery
to a loop that is closing. None is taken here. All three are in ## Unresolved review notes for
a human to pick up, with what each would buy.

Design question 4 — restated, not taken

This body is ~84 KB across six dated passes. Whether to collapse it into a current description plus
a short changelog is a maintainer's call, not this run's: the append-only convention is what
makes every withdrawn figure traceable, and trading it for readability is a decision about the
repository's conventions rather than about this change. Restated here so it is not lost.

Decisions appended to the record, in the record's shape

Taken — re-redact the seven raw-output paths to <worktree>/target/… and declare the redaction
rather than making it silently.
The lines sit inside an append-only dated pass, so editing them
without saying so leaves a reader who diffs two revisions of this body with an unexplained change
inside a record that promises not to change. The replaced substring, the seven lines, and the reason
are stated in ## Sixth pass.
Rejected with evidence: leaving the paths and noting them as a known artefact — the rule is about
what is published, and eleven bodies in this run have already carried the same prefix inside
otherwise-correct provenance notes.
Rejected with evidence: deleting the seven lines outright — they carry the fact that the loader
resolved libz out of libtiff-oracle's build directory, which is the finding those runs exist to
show. Only the prefix is dropped.
Reverses: restore the paths.

Taken — put the check in the publish step, before the bytes are sent. The redaction was lost
because the publish step rebuilds the body from a local draft and overwrites the live one; a
redaction applied to the live body cannot survive that, and neither can a resolution to be careful.
The step now refuses to publish a body containing a tooling path and prints every offending line.
Verified in both directions on real inputs.
Rejected with evidence: checking the published body afterwards — that is what has been happening,
and it detects the sixth instance rather than preventing the seventh.
Reverses: drop the check.

Taken — correct the zlib justification at every site and state the narrower claim that holds.
Two stock builds of one version are indistinguishable by version string; a fork that renames itself
is separable and cannot be what the identification rests on. Six in-repo sites including the printed
epilogue, plus the worked example, which was a composite resolution that cannot occur.
Rejected with evidence: dropping the path print, or softening the sentence to "the version may not
be enough" — the mechanism is real and reproduces, and a hedge is not a claim an implementer can
act on.
Reverses: restore the wording.

Taken — record that the false claim was inherited from the run's shared contract. It reached
four in-repo sites, a printed epilogue, this body and a filed issue before anyone executed
zlibVersion(). Naming the source is what tells the next reader why four agreeing artefacts were
not evidence.
Rejected with evidence: correcting silently — the propagation is the more useful finding, and
without it the correction reads as one lane's slip.
Reverses: remove the origin note.

Taken — file the #618 correction as a new issue rather than folding it into #617. #617 is
already published, and this run may not edit an existing issue under any authority, including one it
filed itself; folding would be the same prohibition by another name. #617 corrects #583 and #584,
the new issue corrects #618, and both are linked from ## Issue for a human to fold in and close.
Rejected with evidence: editing #618 in place — forbidden, and the proposal there is otherwise
sound, so a replacement issue would discard a valid filing.
Reverses: close the new issue once #618 carries the correction.

Taken — substitute ## Validation's placeholder with this pass's gate set rather than removing
the section's promise.
A section that says "commands verbatim, with outcome" and contains none is
a defect in the artefact a reader checks first. Earlier passes' commands stay under their own
Validation, this pass headings, which the section now names, so nothing is duplicated or moved.
Rejected with evidence: deleting the promise instead — the commands exist and are recorded; the
placeholder was the only thing missing.
Reverses: restore the placeholder.

Taken — decline design questions 1, 2 and 3 and record them for a human. A version-mismatch
warning, a build-time recorded path, and printing every linked dynamic library each add machinery to
a change that is closing, and none of them repairs a defect this round found.
Rejected with evidence: implementing them here — each widens a benchmark PR's blast radius for a
diagnostic nobody has asked a question of yet.
Reverses: implement any of them under a new issue.

Not taken — design question 4, collapsing this body. It is a maintainer's call about the
repository's append-only convention, not a decision this run may take on its own authority.
Restated in ## Sixth pass so it is not lost.

This remains an unattended run: no human approved this plan, these decisions, or these corrections.
The record above is what a human reads afterwards.

Unresolved review notes

  • The box was not quiet. Every figure in the fourth pass was taken at a one-minute load average
    of 15.5–38.1; the box did not go quiet during the window and the load is attached to each run in
    the raw output. This matters least for the finding it is used for — the Deflate result is stable
    to ±2 % within a library across that whole load range, and eighteen independent runs found the
    same flatness from 2.8 to 49 — and most for the two lossless-jpeg median columns, whose
    spread (18 to 101) is dominated by scheduling, which is why the fastest-sample ratio is quoted
    beside them.
  • Only two libz builds were measured, stock zlib 1.3.1 and zlib-ng 2.3.3, as they happened to
    exist on this machine. The claim is that the choice of libz moves the Deflate rows across 1.0,
    which two points establish; it is not a claim about any third build.
  • The non-interleaved arrangement was not re-run under a pinned libz. The third pass's
    attribution of a 25–30 % Deflate shift to interleaving is withdrawn rather than replaced, and
    interleaving is retained on its own argument. Settling what that shift was would mean reverting
    the interleaving commit and re-measuring under both libraries; nothing this PR claims needs it.
  • The preview correction remains a model, not a measurement, even where it is applied. It is
    now taken at the width the decoder materialises and applied only to the uncompressed rows, where
    both paths do comparable work per byte. That argues the model is sound there; it does not measure
    the preview's cost, which would need a decode entry point skipping the preview that gamut
    deliberately does not expose.
  • decode_lossless_jpeg's medians are not usable on a loaded machine. gamut's arm is ~100 ms,
    long enough to absorb a scheduling event whole. The bound quoted for gamut-dng: lossless-JPEG decode is ~60x slower than the reference implementation #583 — no fastest-sample
    ratio below 34× — is the defensible form; a tighter figure needs a quiet box.
  • gamut-dng: correct the record in #583 and #584 — withdrawn benchmark figures and a verification command that matches no benchmark #617 and tooling/gamut-dng-oracle: pin the zlib the benchmark's reference arm links, keeping -lz for conformance #618 are open and this pull request does not close them. gamut-dng: correct the record in #583 and #584 — withdrawn benchmark figures and a verification command that matches no benchmark #617 carries the correction
    to gamut-dng: lossless-JPEG decode is ~60x slower than the reference implementation #583 and gamut-dng: lossless-JPEG CFA encode expands the payload past uncompressed #584 that this run may not apply in place; until a human folds it in, an implementer
    who opens gamut-dng: lossless-JPEG decode is ~60x slower than the reference implementation #583 reads a false localisation sentence and a verification command that matches no
    benchmark. tooling/gamut-dng-oracle: pin the zlib the benchmark's reference arm links, keeping -lz for conformance #618 carries the oracle-linkage proposal. Both are linked from ## Issue.
  • libstdc++ is also unpinned, and linked asymmetrically. Only the reference arm links it —
    the SDK is C++ and gamut is not — so it is an asymmetry of the same kind as the libz one but
    without the symmetric-comparison excuse. The work it does inside the measured region (the
    std::vector spool, the dng_negative destructor) is orders of magnitude below inflate, so no
    row published here is plausibly moved by it; that is an argument from magnitude, not a
    measurement. Nothing in this pass measured a second libstdc++.
  • Both lossless-JPEG oracle arms still narrow expected_samples * sizeof(uint16_t) to uint32
    unguarded.
    This pass made the length guard symmetric, which is what the finding named. The
    sample-count narrowing is identical in both arms, so it does not distort the subtraction, and it
    is unreachable from fixtures three orders of magnitude below the boundary — but it is the same
    class of defect and is recorded rather than fixed silently.
  • The build-tree warning is a path test, not a proof of provenance. It fires on a target
    component. A platform library installed under a directory named target would be flagged
    spuriously, and a build-graph library resolved from a directory named something else would not be
    flagged at all. It is a flag on the accident this repository actually produces, not a general
    guarantee.
  • A withdrawn direction claim survives in two pushed commit messages. 8956ad00 states "the
    lossless-JPEG codestream decode favours gamut (the oracle's export path costs the SDK two extra
    passes)" and c6620b29 promises to say "which of the two gamut-versus-SDK comparisons favours
    which side and by how much". ## Fourth pass (N5) withdrew both: over sixteen case-runs the
    export-path gap spans −4 % to +64 %, so the pair supports a bound at the measurement floor, not a
    signed cost. Later commits in this branch do retract in-message (31f01840, 347c8ade), so the
    pattern exists; these two predate it and are pushed. This run may not rewrite pushed history, so
    the retraction lives here and in ## Fourth pass rather than in the commits themselves. A reader
    of git log alone meets the withdrawn wording with nothing beside it.
  • Design question 1 — warn when the two libz builds differ in version, not only in path — is not
    taken.
    It would catch the case where a reader compares two Deflate figures taken against
    different implementations, which is the failure this whole thread is about, and it needs a second
    recorded identity to compare against, which the harness does not have. Declined as machinery added
    to a closing change; it belongs with tooling/gamut-dng-oracle: pin the zlib the benchmark's reference arm links, keeping -lz for conformance #618, which would give it the pinned identity to compare to.
  • Design question 2 — record the libz path at build time and compare it to the run-time
    resolution — is not taken.
    It would turn the build-directory warning from a heuristic on a path
    component into an exact statement about whether the loader picked the library the build script
    chose. It needs a build-script change to gamut-dng-oracle, which is tooling/gamut-dng-oracle: pin the zlib the benchmark's reference arm links, keeping -lz for conformance #618's territory.
  • Design question 3 — print the identity of every dynamic library the reference arm links, not
    just libz — is not taken.
    ldd on the built bench binary lists five shared objects:
    libstdc++.so.6, libz.so.1, libgcc_s.so.1, libm.so.6 and libc.so.6. Only libz is
    produced by a build script in this graph and so only libz can be resolved out of target/; the
    other four come from the platform, and none of them does work inside the measured region within
    orders of magnitude of inflate (see the libstdc++ note above). Printing all five would make the
    one that actually moves a row harder to see, not easier.
  • Design question 4 — whether to collapse this six-pass body — is restated, not decided. At
    ~84 KB the record is longer than the change, and every withdrawn figure in it is traceable to the
    pass that withdrew it. Trading that for a current description plus a short changelog is a decision
    about the repository's append-only convention and belongs to a maintainer.
  • The publish check is a grep on known patterns, not a proof. It refuses a body containing a
    worktree path, a long agent identifier, or a checkout path with a hidden tooling directory in it.
    A tooling path in a shape none of those match would pass. It is a guard against the accident this
    run actually produces, and it is asserted in both directions before every publish, not a general
    guarantee.

`read_raw_dng` writes the bytes to a temporary file and exports the
decoded samples across the FFI boundary. Both are fine for a conformance
check and wrong for a timed one: they charge the reference implementation
for file I/O and for a full-image `malloc` + `memcpy` that gamut's
in-memory `DngDecoder` never pays.

`decode_dng_in_memory` runs the same parse -> build-negative ->
`ReadStage1Image` flow over a `dng_stream` on the caller's bytes and
reports only the extent of the image it produced. Pinned against the
file-stream path on an Adobe sample DNG, so a benchmark cannot be timing
a cheaper, different decode.

Refs #163
`--bench compression` answers the #196 question on packed payloads only.
This adds `--bench codec`: encode and decode throughput across the whole
shipped matrix -- uncompressed, Deflate and lossless JPEG, for CFA and
LinearRaw -- with gamut's decode next to the reference implementation's.

Fixtures are synthesised in-process, so the harness needs no sample
corpus and runs by default. The codec call, the buffer it produces and
that buffer's teardown are inside the timed region; fixture synthesis and
the encode a decode benchmark reads are outside it. The teardown is
placed explicitly because divan otherwise defers a returned value's drop
past the timed region, which would charge gamut nothing for freeing a
decoded image while the SDK's negative destructor runs inside its call.

Two gamut-versus-SDK comparisons are published because their biases point
in opposite directions and neither can be normalised away: the whole-file
decode favours the SDK (gamut also unpacks IFD 0's preview and rebuilds
the metadata, a margin the fixture table prints), the lossless-JPEG
codestream decode favours gamut (the oracle's export path costs the SDK
two extra passes). No encode comparison: the shim wraps the SDK's reader,
not its writer, so no reference number exists to compare against.

Refs #163
State what the harness measures, what sits inside and outside each timed
region, and -- the part a reader cannot re-derive -- which of the two
gamut-versus-SDK comparisons favours which side and by how much. Also
state that no absolute throughput figure is pinned: unlike the #196
ratios, MB/s is a property of the machine that produced it.

Refs #163
The linear-raw branch derived a colour from `(plane % 2, plane / 2)`,
which sends plane 2 to the fall-through arm -- so blue photosites were
generated at green's gain. Harmless to a timing number, but the fixture
no longer matched what its own comment claimed it was, and a fixture
nobody can read is one nobody can check.

Index a named `GAINS` table by colour instead, derived from the RGGB tile
for a mosaic and from the plane for a linear image.

Refs #163
The fixture table's first column did not line up. `{case:<26}` sets a
width on the formatter, but both `Display` impls wrote through
`write_str`/`write!`, which go straight to the underlying buffer and
ignore width and alignment -- so every row was ragged and the table was
harder to read than the plain text beside it.

`Formatter::pad` is the method that honours those flags. `Case` has to
build its composite string first, which costs an allocation in a
function that runs once per case outside every timed region.

Refs #163
A harness that is never read is worth nothing, so its first run is
written down where the crate's other measurements live.

Lossless JPEG is the outlier at both ends. Decode is ~60x slower than
the reference implementation while every other scheme is within 1.25x,
which localises the cost to `decode_symbol`'s linear scan of the whole
code table rather than to decode overhead (#583). And a CFA file gets
*larger* when the scheme is turned on, because the mosaic goes to the
encoder as one full-width component and predictor 1 then differences a
red photosite against its green neighbour; the spec's reshape takes the
payload from 119.7% of raw to 91.5% (#584).

Both quantities are bytes or ratios rather than absolute times, so both
reproduce off the box that measured them.

Refs #163
The codestream comparison in the codec benchmark carried one residual bias
that was described rather than measured: crossing the FFI boundary costs the
Adobe DNG SDK a `malloc`, a `memcpy` out of its spool buffer and a copy into
a `Vec`, none of which gamut's single `Vec` pays.

Add `decode_lossless_jpeg_extent`, which runs the identical
`DecodeLosslessJPEG<Scalar>` into the identical spool buffer and stops before
those copies, so the gap between the two entry points is the export path and
nothing else. A differential test pins that the two reach the same decode.

Also rename the memory-stream oracle test to what it asserts: it compares the
stage-1 *extent*, not the pixels, because the entry point it covers
deliberately exports none.
The two implementations sat in separate divan benchmarks, which run in name
order, so every reference case was measured minutes away from its counterpart.
On a shared machine that drifts, a ratio measured minutes apart is not a ratio:
across two runs of the split harness a 1.24x Deflate figure moved to below 1.0x.

Take the implementation as a benchmark argument instead, naming the arguments
so divan's own name sort keeps the members of a pair adjacent. The codestream
benchmark gains a third arm, `adobe-sdk-no-export`, whose distance from
`adobe-sdk` prices the FFI export path.

Put the IFD-0 preview volume into gamut's divan counter for the whole-file
decode and for encode, both of which handle the preview while the SDK's
stage-1 read does not. The median-time column is then the uncorrected ratio
and the throughput column the preview-corrected one, so no reader has a
subtraction to do. The fixture table prints both volumes and the factor.
The isolation claim for #583 was stated as "every decode path is within 1.25x
except lossless JPEG", and the two rows that contradict it -- both uncompressed
cases, at 1.8x and 2.4x -- were absent from the table. Publish the whole matrix,
both runs, with the preview-corrected column beside it, and rest the isolation
on the codestream pair instead: that pair carries no container asymmetry and
its one residual bias is now measured at under 2%.

Quote #584 against a single denominator throughout. Read in sequence the
previous bullet switched from the whole file to the codestream mid-sentence,
which inflates the remedy about fivefold; on the file denominator the reshape
lands 6.2% below the uncompressed baseline.

Record that the fixture table must not become a codec gate: the margin is a
property of frame-uniform synthetic gains, and pinning an encoder requirement
to one synthetic fixture is the failure a benchmark harness exists to avoid.
Two header sentences still described the previous shape: that every counter
is the raw sample volume, and that the preview asymmetry belongs to two
benchmarks that no longer exist by those names.
The in-memory decode entry point repeated the SDK's parse -> post-parse ->
validate -> make-negative -> parse -> post-parse -> read-stage-1 sequence that
`read_negative` already ran for the file-stream entry point, so the two could
drift apart silently.

Take a `dng_stream &` in `read_negative` and keep the path form as a two-line
overload that opens the file and delegates. Opening the stream is then the whole
of the difference between the two flows, which is what the benchmark's fairness
claim rests on.
`gdng_decode_lossless_jpeg_extent` narrowed its `size_t` length to the `uint32`
`dng_stream` takes without checking it fits, so a buffer above 4 GiB would have
been decoded from a silently truncated view. Its sibling
`gdng_decode_dng_in_memory` guards the same narrowing; this one did not.
The pin for `decode_dng_in_memory` sat in `tooling/gamut-dng-oracle`, which is
excluded from the workspace, so it never ran in automation and could not detect
the drift it was written to detect. Its sibling `decode_lossless_jpeg_extent`
was already pinned inside `gamut-dng`.

Move it there too: encode a DNG with this crate, decode it both ways, and assert
the memory-stream entry point reports the extent the exporting one produces and
that this is the encoded image's own extent. Note on both oracle entry points
where their pin now lives, and why it is not beside them.
Three of the harness's published quantities were not what they claimed.

The preview correction modelled the IFD-0 preview at its stored width, one byte
per sample. The decoder surfaces every sub-image as `SubImageData::Decoded(
Vec<u16>)` whatever the stored depth, so the buffer it allocates, fills and tears
down is twice that. Model the width the decoder materialises.

The correction charges preview bytes at the raw path's per-byte rate, which
holds only where both paths do comparable work per byte. Under Deflate and
lossless JPEG a raw byte carries entropy-coding work a preview byte does not, so
there the arithmetic yields a lower bound on gamut's ratio rather than a
measurement of it. Suppress it on those rows -- gamut's counter is the raw volume
and both divan columns are uncorrected -- and print which rows those are, in the
fixture table and in the epilogue an operator actually reads.

The export-path arm bounds the FFI copy; it does not price it. Its magnitude
sits at the measurement floor, where its sign is not resolved. Say bound.

Also record, at the epilogue, that a pair's arms cannot be interleaved per
sample: one always runs first and the bias points one way for a whole run, so a
published ratio is the mean of one run each way (`--sortr name` reverses divan's
sort and with it the arm order).
The oracle links the system libz dynamically, because the SDK includes
<zlib.h> unconditionally. So on the two `*/deflate` rows -- and only there --
the reference arm runs code that is not built from source committed to this
repository, and which libz it runs is a property of the machine and even of the
launcher: cargo puts every build script's native search path on
`LD_LIBRARY_PATH`, so `cargo bench` resolves the stock zlib that another dev
oracle happens to have built under `target/`, while running the same binary
directly resolves the platform's, which on this box is a zlib-ng fork.

Inflate implementations differ by more than the margin that decides which side
of 1.0 a Deflate ratio falls on, so two correct runs of this harness can
disagree on those rows with no defect in either. Print the resolved library --
version plus the path `dladdr` reports, since zlib-ng's compatibility build
answers "1.3.1" exactly as stock zlib does -- above the divan output, and say in
the epilogue that a Deflate figure travels with it or not at all.
Re-measured across eight runs -- two repetitions of {stock zlib, zlib-ng} x
{reference arm first, gamut arm first}, 100 samples each, at loads from 15 to
38, each bracketed by `uptime`.

The two Deflate ratios an independent re-measurement could not reproduce are
reproduced here, both of them: 0.94 under stock zlib 1.3.1 and 1.17-1.26 under
zlib-ng 2.3.3, flat in load and in arm order. Neither figure was wrong. The
gamut arm does not move between the two libraries; only the reference arm does,
by 1.2-1.3x, which is enough to reverse which side of 1.0 the row falls on. Say
so, and say which library each figure belongs to.

The lossless-JPEG finding survives -- no fastest-sample ratio below 34x in any
run -- but not at the precision "56-59x" claimed from two runs; quote the bound.
The export-path figure does not survive at all: over sixteen case-runs it spans
-4% to +64%, so publish it as a bound below the run-to-run spread rather than as
a cost with a sign. Correct the preview volume to the width the decoder
materialises, record that the correction is now suppressed where it would be a
lower bound, and withdraw the claim that interleaving accounted for the Deflate
shift, which is not this section's to explain.
Two doc sites still called the gap between the two SDK lossless-JPEG arms the
export path's cost. Measured over sixteen case-runs it spans -4% to +64%, so
what the pair supports is a bound at the measurement floor, not a signed price.
The module header already said so; these did not.
`gdng_decode_lossless_jpeg_extent` rejects a length that does not fit the
`uint32` `dng_stream` takes; `gdng_decode_lossless_jpeg` narrowed the same
`size_t` unguarded. The guard's own rationale named `gdng_decode_dng_in_memory`
as the sibling to match, which is the wrong one: the entry point the extent arm
is *timed against* is the exporting decode, and an asymmetric guard is both a
difference in what the two accept and a difference inside the measured region.

Give the exporting arm the identical check and state the reason at both sites.
Unreachable from this crate's fixtures either way -- the streams are kilobytes.
The harness printed which zlib the reference arm called, which makes a Deflate
ratio interpretable but not reproducible: a resolution that came from the build
graph rather than from the platform is one nobody else gets. `cargo` exports
every build script's native search path on the runner's library path, and
`gamut-dng` dev-depends on `libtiff-oracle`, which builds a `libz.so` of its own
-- so `cargo bench -p gamut-dng` alone is enough to measure stock zlib where the
same binary run directly measures the platform's zlib-ng, and the two move that
row by 1.2-1.3x.

Split the resolved path out of the identity string so a caller can test it
instead of reading it, and warn when it has a `target` component.
The header, the printed epilogue and the STATUS section all said that on the
`*/deflate` rows "one measured path is not built from this repository" and that
"every other row runs only code built here". A reader takes that as a claim
about gamut's own codec, and it is not one: `gamut-deflate` is deliberately
encoder-only, so this crate inflates with `miniz_oxide`. Neither arm on those
rows is gamut-authored.

Say so, and give the distinction that carries the section's real content --
`miniz_oxide` is pinned by `Cargo.lock` to one version and one checksum, and the
system libz is pinned by nothing, not even by the machine.
The section's two findings are filed as #583 and #584, which were written from
its first revision and still quote figures three later passes withdrew -- the
Deflate ratios, the localisation argument resting on them, a fixture-table
column the harness no longer prints, and a verification command naming
benchmarks that no longer exist. Neither issue can be corrected in place from
here, so #617 carries the correction; name it, and name #618 for pinning the
libz the reference arm links.
Four doc sites and the benchmark's printed epilogue justified the resolved
path print by claiming a zlib-ng compatibility build answers zlibVersion()
with stock zlib's string. Executed on this box, it does not: the platform
build answers "1.3.1.zlib-ng" and the stock copy under target/ answers
"1.3.1", so the version separates that particular pair. The worked example
in the oracle -- version "1.3.1" resolving to a path ending .zlib-ng -- is a
composite that cannot occur.

The mechanism and the print survive on the narrower true claim: the pair the
loader actually collides is two *stock* builds of one version, the copy a dev
oracle left under target/ and an installed libz.so.1.3.1, and those are
indistinguishable by version string. The path is what identifies the
resolution; a fork renaming itself is not something the identification may
rest on.

Refs #163
… they are

The previous commit replaced one unexecuted claim with a second one: that the
pair the loader collides is two stock builds of a version. On this box it is
not -- the platform ships a fork that renames itself, which the same commit's
own message says. What is true of every box is the shape: the loader chooses
between a copy some dev oracle built under target/ and whatever the platform
installed, and zlibVersion() separates those only when the platform's build
renamed itself. A box shipping stock zlib 1.3.1 gives two resolutions that
answer identically, so the identification rests on the path either way.

Refs #163
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant