fix(#2341): fill every curve-set channel in the V5 device-link writer - #2352
Merged
Conversation
colourbill-ctrl
requested review from
ChrisCoxArt,
maxderhak and
xsscx
as code owners
September 1, 2026 21:10
Contributor
Author
|
The Docker leg is not this branch;
#2344 passed the same leg concurrently: it touched Flagging rather than patching — shared CI. Detail in the description. |
CDevLinkWriter::begin() attaches a CIccMpeCurveSet when the input range is
not the default [0,1], sharing one CIccSingleSampledCurve across every input
channel. The loop that filled slots 1..n-1 ignored its own counter and wrote
index 1 on every pass, so slots 2..n-1 were left NULL.
CIccMpeCurveSet::Write() skips a NULL slot rather than failing, so those
channels kept the calloc'd {offset 0, size 0} position-table entry and the
writer still returned true. iccApplyToLink therefore printed "LUT
successfully written" and exited 0 while emitting a device link every reader
rejects -- iccDumpProfile reports "AToB0Tag - Tag has invalid structure!" --
and CIccMpeCurveSet::Begin() refuses the same file, so it could not be
applied either.
Sharing one curve across all channels is what the class is built for and is
kept: SetCurve() only deletes an outgoing pointer that no other slot still
aliases, ~CIccMpeCurveSet() dedupes before deleting, and Write() emits the
curve once with every position entry pointing at it. The fixed RGB link is
byte-for-byte the same size as the broken one (716 bytes); only the two
zero-filled position entries become real.
A 1-channel source never enters the loop and a 2-channel source has slot 1
as its only iteration, so both were already correct -- which is why this
survived from 889db62 (2023-11-03).
The regression coverage asserts the read-back file, not the writer: on the
unfixed tool all four registered cases exit 0 and report a successful write,
so an assertion on the writer is vacuous here by construction. It also does
not stop at the validation verdict, because "Profile is valid" is satisfied
by a link that simply omits the structure under test. Each case is pinned by
mutation instead, all four measured:
original bug (slot 1 rewritten) -> RGB red
curve set dropped entirely -> RGB red
right count, wrong per-channel domain-> RGB red
curve set attached unconditionally -> default-range red
so EXPECT_DUMP_COUNT pins one curve carrying the requested domain per input
channel, and the default-range case asserts the curve set is absent. An
earlier verdict-only version of these tests stayed green through the second
and fourth of those, which is what motivated the structural assertions.
CMYK and Gray cases were written and then withdrawn: they passed locally and
turned all three Windows legs red with "Unable to add ... to transform chain
(status 3: Invalid profile)" for the two generated profiles they used. Both
validate clean on Linux and both are written by CreateAllProfiles.bat
reporting success, so that divergence is in reading the profiles back rather
than in this change, and it is reported separately. The two cases that ship
use only git-tracked fixtures, and are jointly sufficient for every mutation
above.
The existing single-tool drivers cannot express write-then-read, so
RunDeviceLinkRoundTripTest.cmake is added alongside them. The tests are
registered from both call lists, since the if(WIN32) branch ends in return()
and the two lists are mutually exclusive (#2237).
xsscx
force-pushed
the
fix/issue-2341-devlink-curveset
branch
from
September 1, 2026 22:11
078e6f3 to
a74cf44
Compare
Member
PR Status2026-09-01 22:13:56 UTC
|
xsscx
enabled auto-merge (squash)
September 1, 2026 22:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2341
Root cause
CDevLinkWriter::begin()attaches aCIccMpeCurveSetwhen the input range is not the default[0,1], sharing oneCIccSingleSampledCurveacross every input channel. The loop that filled slots1..n-1ignored its own counter:so slot 1 was rewritten
nSrcSamples-1times and slots2..n-1stayedNULL.CIccMpeCurveSet::Write()skips a NULL slot rather than failing, so those channels kept thecalloc'd{offset 0, size 0}position-table entry andWrite()still returnedtrue. The tool printedLUT successfully writtenand exited 0 while emitting a link every reader rejects;CIccMpeCurveSet::Begin()refuses the same file, so it could not be applied either.Visible in the 716-byte RGB link, at the
cvstelement's position table:ch0
{0x24,0x20}, ch1{0x24,0x20}, ch2{0,0}— the third entry is the defect.A 1-channel source never enters the loop, and a 2-channel source has slot 1 as its only iteration, so both were already correct. That is why this survived from
889db62b(2023-11-03) until now: it needs a source space with at least three channels.Fix
The one-line correction the issue proposes,
SetCurve(1, …)→SetCurve(i, …).Sharing one curve across all channels is deliberate and is kept — the class is built for it.
SetCurve()only deletes an outgoing pointer that no other slot still aliases;~CIccMpeCurveSet()→SetSize(0)dedupes throughicCurveMapbefore deleting; andWrite()emits the curve once with every position entry pointing at it. Ownership is unchanged: one allocation, one delete, before and after. The fixed RGB link is byte-for-byte the same size as the broken one (716 bytes) — only the two zero-filled position entries become real.Reproduction
From the issue, no fuzz artifact needed:
On master the writer reports success and exits 0, then:
With the fix:
Profile is valid for version 5.00.Verification
The regression coverage asserts the read-back file, not the writer. On the unfixed tool both registered cases exit 0 and print
LUT successfully written, so an exit-status or message assertion — the shape every othericcApplyToLinktest in the suite uses — is vacuous here by construction.It also does not stop at the validation verdict.
Profile is validis satisfied by a link that simply omits the structure under test, so each case is pinned by mutation instead. All four measured:rgb-channelsif (false))rgb-channelsrgb-channelsif (true))default-rangeAn earlier verdict-only version of these tests stayed green through the second and fourth of those.
EXPECT_DUMP_COUNT=3now pins one curve carrying the requested domain per input channel — that count is what carries the general claim, so a wider source is not needed to state it — and the default-range case asserts the curve set is absent, which is what gives that control a job.CMYK (4-channel) and Gray (1-channel) cases were written and withdrawn. They passed locally and turned all three Windows legs red with
Unable to add ... to transform chain (status 3: Invalid profile)forTesting/CMYK-3DLUTs/CMYK-3DLUTs.iccandTesting/Display/GrayGSDF.icc. Both validate clean on Linux, andCreateAllProfiles.batreports "Profile parsed and saved correctly" for both, so the divergence is in reading the profiles back rather than in this change — reported separately rather than gated to non-Windows, which would have buried it. Every fixture that ships here is tracked in git rather than generated, which is what makes the pair portable.RunDeviceLinkRoundTripTest.cmakeis new because the existing single-tool drivers cannot express write-then-read:RunBenchApplyCliTestasserts an exit status and a message from one process, which is exactly what this defect agrees with. Registered from both call lists, since theif(WIN32)branch ends inreturn()and the two lists are mutually exclusive (#2237).Pre-flight
strict warnings ENABLED, 0 warnings.ghcr.io/internationalcolorconsortium/iccdev:latest—strict warnings ENABLED, 0 warnings.detect_leaks=1(CI runsdetect_leaks=0) clean on every case measured, including a 4-channel CMYK source — relevant here because the fix aliases one pointer into N slots, so the dedupe inSetCurve()/SetSize(0)is what keeps it to one delete. LeakSanitizer was confirmed to fire in that same configuration, so the clean result is not a dead detector.iccdev.spectral-tiff-preview, which dies on a missing localimagecodecsPython module — pre-existing and unrelated.Out of scope
CIccMpeCurveSet::SetCurvebounds-checksnIndex > m_nInputChannelswhere>=is required, so index== m_nInputChannelswould write one past thecalloc'd array (IccMpeBasic.cpp:3305). Not touched here: it is unreachable from every caller in the tree, and this change does not widen its reachability — the old code passed indices{0,1}and the new code passes0..n-1, both strictly inside. Noted for a maintainer ruling rather than fixed in a bug-fix PR.CI evidence
Pre-PR dispatch,
ci_scope=source: run 33558430421. Green on GCC 15.2 Strict Release LTO, Windows MSVC, Windows ClangCL, MinGW UCRT64, macOS Clang Debug and Release, Tool Smoke Tests (ASAN+UBSAN), and the Workflow Security Audit. Both new tests are confirmed to have run on Windows MSVC —Test #127and#128of 137, 0.04s each — rather than being absent from that lane.Docker Clang 22 Verificationis red for a reason outside this branch, andPR Summaryis only the rollup reporting it — its own inputs areDOCKER_RESULT: failurewith every other resultsuccessorskipped, so this is one condition, not two.The job fails in step 7
Pull trusted base imagewithmanifest unknownonghcr.io/…/iccdev:sha-$BASE_SHA. Two things combine:ci-docker, which publishes thosesha-base images, has been cancelled on every master run since 2026-08-30 —f8c48f1b,045003f9,0e6b0ede,7c872606; last successfaa6b311. So no recent master commit has a published image.detect-srcnevertheless reports one as available.trusted_base_image_availableis set from a branch-name test, not an image probe —if [ "$base_ref" = "master" ](ci-pr-action.yml:496-498) — so for any PR targeting masterbuild_pr_image(:1160) collapses toimage_definition_changed. A PR that does not touch the image definition therefore takes the pull path (ci-docker-pr.yml:236) and asks for a tag that was never pushed, instead of falling back to building it as thetrusted_base_image_available != 'true'clause intends.That is why this is not visible on every PR: #2344 passed the same leg at the same time because it changed
.github/ci/requirements/docker-spectral-preview.txt, which matches theimage_definition_changedpattern at:566and sent it down the build path. This PR touches onlyBuild/andTools/, so it pulls.Flagging rather than patching, since
ci-pr-action.ymlis shared CI and the standing guidance is not to modify it in a bug-fix PR.