Don't invalidate the pixelpipe cache on every commit when a raster mask is used#21519
Don't invalidate the pixelpipe cache on every commit when a raster mask is used#21519masterpiga wants to merge 2 commits into
Conversation
|
|
Thanks, Hanno, updating the milestone accordingly. |
|
I think this is correct after source reading & checking. I remember working on that code and even doing all the comments why "new" is important :-) Three thoughts;
|
…sk is used dt_iop_commit_blend_params() registers a module as a user of a raster-mask source, and is supposed to report only genuinely new registrations so the caller can invalidate that source's cache once. Instead it returned the source module unconditionally on every call, so any history commit anywhere in the pipe was treated as "a source just gained a raster user," discarding every cacheline from the source onward — even though almost all of those calls were re-registering an existing user, not adding a new one. The invalidation itself is legitimate for the one case it exists for: a source that gains a new user doesn't change its own params, so its cacheline hash stays the same and it could otherwise be served from cache without ever writing the mask the new user needs. But firing on every commit instead of once means that a raster source sitting early in the pipe — a common setup, e.g. a mask painted on an early exposure/tone module and consumed by a later one — forces a full re-render of the pipe tail on every unrelated slider move downstream. This is invisible on small images and short pipes, and increasingly costly as either grows, since a full-resolution intermediate buffer can be hundreds of MB and won't re-establish itself between edits. The fix makes the "new" check (already computed via g_hash_table_insert's return value, but previously used only for a debug print) gate the return value, so the function does what its own doc comment already promised: return NULL except for a genuinely new source registration. Found while profiling a downstream fork, where a raster-masked pipe re-executed from the source on every mask edit (~2.8s) instead of only recomputing the touched module (~0.17s). The invalidation mechanism and the -d pipe evidence are identical on master, so stock darktable pays the same unnecessary cost, scaled to image size and how early the raster source sits in the pipe.
…ed registration flag The previous fix (cec8ba9) decided whether to invalidate a raster-mask source's cacheline by checking whether the consumer's registration in the source module's shared users table was brand new. That flag lives on the module and is shared across every pipe (full preview, thumbnail preview, second window), and it is consumed the first time anything touches that table. In practice, the consumer usually gets added to that shared table by GUI code, at the moment the user picks a raster mask from the blend panel combo box, before any pixel pipe actually commits the new blend parameters. By the time each pipe's own commit runs afterward, the registration is no longer new, so the flag is already false for every pipe. None of them invalidate the source's cacheline. If the source's own parameters have not changed, its cacheline is still warm, so the source is never reprocessed, and it never writes the mask the consumer needs. The consumer silently blends with no mask at all until something unrelated forces a full reprocessing later. The same problem shows up when raster mask assignments are replayed from history, such as during undo, redo, or loading an image with existing history: the registration is consumed while replaying history, before the real per-pipe commit that would need to react to it. The fix stops relying on that shared, one-shot flag entirely. When a pipe commits a module that consumes a raster mask, it now looks at that specific pipe's own copy of the source module and checks whether a mask has already been stored there for the requested mask id. If not, only that pipe's cacheline for the source is invalidated, so the source reprocesses and writes the mask on this pipe. If the mask is already present, nothing is invalidated, which keeps the original goal of not forcing a full reprocess on every unrelated edit. Because the check is now local to each pipe instead of a single flag shared across all of them, every pipe correctly detects and recovers from the case where it is missing a mask, regardless of whether the registration happened through the GUI, through history replay, or through a normal pipe commit, and regardless of which pipe reaches the code first. Verified interactively with pipe and mask debug logging: assigning a raster mask to a module for the first time now triggers a cacheline invalidation on both the full and preview pipes at the moment of assignment, the source module reprocesses and writes the mask immediately, and the consuming module picks it up in the same run without requiring any further unrelated edit.
da111dd to
a297709
Compare
|
Thanks, Hanno. Done. I also added a follow up commit suggested by @kofa73's army of agents. |
|
The more i think about this, the lesser i think it's as trivial as the tag implies :-) First: i don't think the "simple interactive test" is good enough by far. Multiple masks, en-disabling modules before, between and after raster creators/consumers, toggling HQ mode, mask visualizing passthru, ... The big question to me is, why do we have to invalidate cachelines at all? So we should try to get that sorted out. After that, all cacheline invalidations would only be helpful to reduce stress on the caching for a better hit rate. |
|
Changed difficulty tag to "average" as this is no longer a one-liner. |
I stumbled onto this while profiling some mask changes. There, a raster-masked pipe re-executed from the raster source on every mask edit (~2.8 s), instead of recomputing only the edited last module (AgX, where the raster mask was being used, ~0.17 s). The wall-clock cost will scale with image size and how early the raster source sits.
dt_iop_commit_blend_params()computes whether a raster-mask user registration is actually new:…uses
newonly in a debug print, and then returnscandidateunconditionally.The caller treats any non-NULL return as "a source just gained a raster user" and invalidates the pipe cache downstream of that source:
Since the return is non-NULL on every commit — not just the first — any edit anywhere invalidates every cacheline at or after the raster source's
iop_order.Why this matters
The invalidation exists for a narrow, real case: a source that gains a new user doesn't change its own params, so its cacheline hash is unchanged and it could be served from cache without ever writing the raster mask the new user depends on. That's correct. But it should fire once, when the user is genuinely added.
As written, it fires on every history commit. If the raster source sits early in the pipe (a common setup — mask painted on an early exposure/tone module, consumed by a later one), every slider move discards the cached output of the source and everything after it, forcing a full re-render of the pipe tail. The user is editing one late module, but pays for re-computing modules they never touched.
This is invisible on small images and short pipes, and increasingly punishing as either grows: a full-resolution intermediate buffer on a 30 MP image is several hundred MB, so the cache cannot re-establish itself between edits.
The fix
This is what the function already advertises — its doc comment states it "either returns NULL or the source module."
Correctness
The return value is captured in exactly one place (
new_raster,imageop.c:2186) and consumed in exactly one place (the invalidation,imageop.c:2241-2242). The remaining call sites (imageop.c:82,431,2330) discard it. Genuine registrations still invalidate:g_hash_table_insert()returns TRUE the first time a module is added as a user, which is precisely the case the invalidation guards.Note that
g_hash_table_insert()reports key novelty, not value change, so a retarget to a differentraster_mask_idon an existing source would not invalidate. This cannot occur today: a source exports exactly one raster slot (BLEND_RASTER_ID == 0), soraster_mask_idis constant for a given source.Reproducing
With a raster mask whose source is early in the pipe and whose consumer is late, run with
-d pipeand move any slider:This repeats on every commit. With the fix it appears once, when the raster user is first registered.
@TurboGit pending @jenshannoschwalm approval, I am flagging this for 5.6.1 as (unless I am barking at the wrong tree, which is always possible when it comes to cache invalidation) it is a genuine bugfix.
Co-authored with Claude.