Skip to content

Develop T5b+T5c: the host-cache seal becomes a pure decision with sampled inputs - #1149

Merged
aurelienpierre merged 2 commits into
masterfrom
refactor/develop-t5b
Aug 14, 2026
Merged

Develop T5b+T5c: the host-cache seal becomes a pure decision with sampled inputs#1149
aurelienpierre merged 2 commits into
masterfrom
refactor/develop-t5b

Conversation

@aurelienpierre

@aurelienpierre aurelienpierre commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

The rest of the T5 tranche line ("_seal_opencl_cache_policy reads pipe-owned flags"), in two commits. _seal_opencl_cache_policy() walks a pipe backwards and decides, per node, whether that node must keep a host-RAM copy of its output.

T5b — the decision becomes a pure function, and a test pins it

It gathered nine facts and decided in four lines. The decision moves to develop/pipe_cache_policy.h as a pure function of those nine named inputs; gathering them stays in dev_pixelpipe.c, which is the part that needs a pipe and a dev to look at. Same inputs, same boolean algebra, same order.

Why, which is not tidiness. The tranche plan requires preserving the per-piece cache_output_on_ram outcomes, and nothing this project runs can observe them: it changes no exported pixel (the export path drives the pipe directly, imageio_core.c, and never reaches the seal), changes no hash, and logs nothing unless someone is already looking. When it was last wrong, the symptom was a downstream module silently reading stale host bytes from a rekeyed cacheline, only with OpenCL enabled, found by dumping GPU buffers.

A pure function can be pinned by a test. That is the whole argument for the shape.

src/tests/unittests/test_pipe_cache_policy.c states the truth table. The important case is _gpu_node_propagates_an_inherited_requirementthe bug that shipped: an = rather than an ||, so toggling on a GPU module with no host needs of its own (rawoverexposed) erased the TRUE a CPU-only module further downstream (dither) had correctly established; colorout then skipped its readback and dither read the cacheline's previous life, with every hash and ROI in the chain individually correct. I checked the test catches it rather than assuming: reintroducing the = fails that test and only that test.

T5c — the seal samples the GUI's two facts once, and the picker predicate comes home

dev->gui_module and dev->color_picker.module were read live, once per node, from the worker thread. A focus change or picker toggle landing mid-walk gave the early nodes one answer and the later nodes another, so the sealed pipe described no single moment. Both are now sampled once, before the walk.

dt_iop_color_picker_force_cache() leaves gui/color_picker_proxy.c and becomes _module_feeds_color_picker() beside its only caller. Nothing about it was GUI code — it walks pipe nodes and compares module identities — and the seal reaching down into gui/ for it is a call the T6 flip turns into a violation. Its doc comment claimed a second caller "when deciding whether a cache miss should trigger a preview recompute"; no such caller exists.

Equivalent by construction, not inspection: the added NULL early-out matches the old expression exactly, because dt_dev_pixelpipe_get_module_piece() returns NULL for a NULL module and dt_dev_pixelpipe_get_prev_enabled_piece() returns NULL for a NULL piece — so the old form already yielded FALSE for every non-NULL module when no picker was active.

What is deliberately NOT here, and why it is measured

Having the GUI publish those two facts — the T4b viewport shape — is the other half of the tranche line, and it is blocked on T6. I wrote it (a seqlock record, publishers at all 13 writers) and tools/check_layering.sh rejected it: +1, 187 → 188, because gui/ is layer 4 and develop/ is layer 5, so today the pipeline may read the GUI but not the reverse. It becomes legal — and is then the natural first use of the flipped table — only after T6. Recorded against the T5 bullet in doc/develop-split.md so the next pass does not rediscover it.

Verification

Release, Debug (-Werror), nofeatures build. ctest 8/8. cycles 0, layering_violations 187 (baseline), module boundaries hold.

No pixel A/B, and that is a statement rather than an omission: the seal is not reached by the export path, and headlessly both sampled fields are NULL, so an A/B cannot execute the changed code at all. It would print 0 differing pixels and mean nothing — the same calibration T4b produced, where the harness printed 0 through all five real defects.

🤖 Generated with Claude Code

…est pins it

_seal_opencl_cache_policy() gathered nine facts about a pipeline node and decided,
in four lines, whether that node must keep a host-RAM copy of its output. The
decision moves to develop/pipe_cache_policy.h as a pure function of those nine
named inputs; gathering them stays in dev_pixelpipe.c, which is the part that
needs a pipe and a dev to look at.

The reason is not tidiness. That flag is invisible to every check this project
runs: it changes no exported pixel -- the export path drives the pipe directly
(imageio_core.c) and never reaches the seal -- it changes no hash, and it logs
nothing unless someone is already looking. The tranche plan requires preserving
the per-piece outcomes, and nothing here could observe them. A pure function can
be pinned by a test, so now it is.

src/tests/unittests/test_pipe_cache_policy.c states the truth table: a CPU-only
node raises the requirement for the node before it; a GPU node with no needs of
its own raises nothing; each of the four own-output reasons raises this node's own
flag and says nothing about upstream; each of the four own-input reasons raises
upstream; and a NULL out-param is allowed, for the last node in the walk.

The important one is _gpu_node_propagates_an_inherited_requirement, which is the
bug that shipped: this was an `=' rather than an `||', so toggling on a GPU module
with no host needs of its own erased the requirement a CPU-only module further
downstream had correctly established, and that module then read the previous life
of a rekeyed cacheline. I verified the test catches it rather than assuming so --
reintroducing the `=' fails that test and only that test.

No behaviour change: same nine inputs, same boolean algebra, same order.

Release, Debug (-Werror), nofeatures build; ctest 8/8; cycles 0, layering 187,
module boundaries hold; export A/B 0 differing pixels, which as the header now
says is not evidence about this flag either way.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…r predicate comes home

_seal_opencl_cache_policy() read dev->gui_module and dev->color_picker.module LIVE, once per
node, from the worker thread. A focus change or a picker toggle landing mid-walk therefore gave
the early nodes one answer and the later nodes another, and the sealed pipe described no single
moment. Both are now sampled once, before the walk.

dt_iop_color_picker_force_cache() moves out of gui/color_picker_proxy.c and becomes
_module_feeds_color_picker() next to its only caller. Nothing about it was GUI code -- it walks
pipe nodes and compares module identities -- and the seal reaching down into gui/ for it is a
call the T6 layer flip turns into a violation. Its doc comment claimed a second caller "when
deciding whether a cache miss should trigger a preview recompute"; no such caller exists.

Equivalent by construction, not by inspection alone: the added NULL early-out matches the old
expression exactly, because dt_dev_pixelpipe_get_module_piece() returns NULL for a NULL module
and dt_dev_pixelpipe_get_prev_enabled_piece() returns NULL for a NULL piece, so the old form
already yielded FALSE for every non-NULL module when no picker was active.

The other half of the tranche line -- having the GUI PUBLISH these two facts rather than the
pipeline sample them -- is blocked on T6, and that is measured rather than assumed:
tools/check_layering.sh fails the publisher wiring (+1, 187 -> 188) because gui/ is layer 4 and
develop/ is layer 5, so today the pipeline may read the GUI but not the reverse. Noted in
doc/develop-split.md against the T5 bullet.

No pixel A/B accompanies this, and that is a statement rather than an omission: the export path
drives the pipe directly and never reaches the seal, and headlessly both sampled fields are NULL,
so an A/B here cannot execute the changed code at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aurelienpierre aurelienpierre changed the title Develop T5b: the host-cache decision becomes a pure function, and a test pins it Develop T5b+T5c: the host-cache seal becomes a pure decision with sampled inputs Aug 14, 2026
@sonarqubecloud

Copy link
Copy Markdown

@aurelienpierre
aurelienpierre merged commit d28bbc8 into master Aug 14, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant