Skip to content

Commit d35e932

Browse files
MaykThewessenclaude
andcommitted
HDR viewer: capture colorout input before it processes
colorout converts to the display profile in place, overwriting/recycling its own input buffer. The tap read that buffer at the end of the recursion, AFTER colorout had run, so it captured post-process scratch (large positive and negative working-space values) rather than the tone-mapped image; the EDR preview then showed a blown-out, hue-shifted result regardless of filmic/sigmoid being enabled. Move the tap to immediately after the recursion fills `input` and before colorout's process(), where the buffer still holds the fully-edited, display-referred working image (filmic/sigmoid output, super-white intact). Also gate on _hdr_viewer_pipe_incomplete(): the basic preview pipe skips enabled modules downstream of a focused tag-filtering module (dt_iop_module_is_skipped), which can drop filmic/sigmoid and leave a scene-linear buffer at colorout's input; forward only complete frames. Drop the now-unused input_host_valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9188bd1 commit d35e932

1 file changed

Lines changed: 87 additions & 69 deletions

File tree

src/develop/pixelpipe_hb.c

Lines changed: 87 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1706,6 +1706,24 @@ static inline gboolean _skip_piece_on_tags(const dt_dev_pixelpipe_iop_t *piece)
17061706
&& dt_pipe_is_basic(piece->pipe);
17071707
}
17081708

1709+
// The HDR viewer must only receive the fully-processed, display-referred image.
1710+
// The basic darkroom pipes skip enabled modules located downstream of a focused
1711+
// module that shares its operation tags (see dt_iop_module_is_skipped), so the
1712+
// focused module's effect is shown in context. That can drop filmic/sigmoid and
1713+
// leave an unbounded scene-linear buffer at colorout's input. Detect it so we
1714+
// forward only complete frames and otherwise keep the viewer's last good frame.
1715+
static gboolean _hdr_viewer_pipe_incomplete(const dt_dev_pixelpipe_t *pipe,
1716+
const dt_develop_t *dev)
1717+
{
1718+
for(const GList *n = pipe->nodes; n; n = g_list_next(n))
1719+
{
1720+
const dt_dev_pixelpipe_iop_t *p = (const dt_dev_pixelpipe_iop_t *)n->data;
1721+
if(p && p->module && p->enabled && dt_iop_module_is_skipped(dev, p->module))
1722+
return TRUE;
1723+
}
1724+
return FALSE;
1725+
}
1726+
17091727
static inline gboolean _dev_pixelpipe_early_exit(const dt_develop_t *dev,
17101728
const dt_dev_pixelpipe_t *pipe)
17111729
{
@@ -1745,10 +1763,6 @@ static gboolean _dev_pixelpipe_process_rec(dt_dev_pixelpipe_t *pipe,
17451763
dt_iop_module_t *module = NULL;
17461764
dt_dev_pixelpipe_iop_t *piece = NULL;
17471765

1748-
// tracks whether the host `input` buffer holds valid pixels (it may be left
1749-
// GPU-only after an OpenCL run); used by the HDR viewer tap below.
1750-
gboolean input_host_valid = TRUE;
1751-
17521766
const dt_iop_module_t *gui_module = dt_dev_gui_module();
17531767
// if a module is active, check if this module allow a fast pipe run
17541768
if(gui_module
@@ -2002,6 +2016,75 @@ static gboolean _dev_pixelpipe_process_rec(dt_dev_pixelpipe_t *pipe,
20022016
g_list_previous(pieces), pos - 1))
20032017
return TRUE;
20042018

2019+
// HDR viewer: forward the working-space linear image (colorout's input) to the
2020+
// external HDR preview app. We capture it HERE -- right after the recursion
2021+
// fills `input` and BEFORE colorout processes -- because colorout overwrites/
2022+
// recycles its input buffer in place while converting to the display profile;
2023+
// reading it afterwards yields scratch values, not the tone-mapped image. This
2024+
// buffer is the fully-edited image still in the working profile's linear
2025+
// primaries (Rec.2020 by default) with super-white (>1.0) intact, before
2026+
// colorout bakes in the display TRC -- exactly what an EDR/HDR display needs.
2027+
// The work-profile RGB->XYZ(D50) matrix is sent alongside for color management.
2028+
// NOTE: the socket calls below must remain outside any OMP parallel section.
2029+
if(dev->gui_attached && !dev->gui_leaving
2030+
&& pipe == dev->preview_pipe
2031+
&& dt_iop_module_is(module, "colorout")
2032+
&& input_format->datatype == TYPE_FLOAT && input_format->channels == 4
2033+
&& dt_conf_get_bool("plugins/darkroom/hdr_viewer_enabled")
2034+
// only forward fully-processed frames: when a tag-filtering module is
2035+
// focused, the basic pipe skips downstream modules (e.g. filmic/sigmoid)
2036+
// and colorout's input would be incomplete (scene-linear).
2037+
&& !_hdr_viewer_pipe_incomplete(pipe, dev))
2038+
{
2039+
// Cooldown: skip connect attempts for 2 seconds after a failed connect to
2040+
// avoid a 200ms timeout on every frame when the viewer is not running. The
2041+
// preview pipe runs on a single thread, so no synchronisation is needed.
2042+
static int64_t _hdr_viewer_next_attempt_us = 0;
2043+
const int64_t now_us = g_get_monotonic_time();
2044+
if(now_us >= _hdr_viewer_next_attempt_us)
2045+
{
2046+
const int viewer_fd = dt_hdr_viewer_connect();
2047+
if(viewer_fd >= 0)
2048+
{
2049+
const size_t w = (size_t)roi_in.width;
2050+
const size_t h = (size_t)roi_in.height;
2051+
const float *const rgba = (const float *const)input;
2052+
2053+
// working RGB -> XYZ(D50): darktable stores this as a [4][4]
2054+
// dt_colormatrix_t (rows padded to 4 for SIMD); copy the 3x3 core.
2055+
const dt_iop_order_iccprofile_info_t *const work_profile
2056+
= dt_ioppr_get_pipe_work_profile_info(pipe);
2057+
float rgb_to_xyz[9];
2058+
if(work_profile)
2059+
for(int i = 0; i < 3; i++)
2060+
for(int j = 0; j < 3; j++)
2061+
rgb_to_xyz[i * 3 + j] = work_profile->matrix_in[i][j];
2062+
2063+
// Strip alpha channel: RGBA float -> RGB float (packed, row-major)
2064+
const size_t npixels = w * h;
2065+
float *rgb = work_profile ? dt_alloc_align_float(npixels * 3) : NULL;
2066+
if(rgb)
2067+
{
2068+
DT_OMP_FOR()
2069+
for(size_t k = 0; k < npixels; k++)
2070+
{
2071+
rgb[k * 3 + 0] = rgba[k * 4 + 0];
2072+
rgb[k * 3 + 1] = rgba[k * 4 + 1];
2073+
rgb[k * 3 + 2] = rgba[k * 4 + 2];
2074+
}
2075+
dt_hdr_viewer_send_frame(viewer_fd, (uint32_t)w, (uint32_t)h, rgb, rgb_to_xyz);
2076+
dt_free_align(rgb);
2077+
}
2078+
dt_hdr_viewer_disconnect(viewer_fd);
2079+
}
2080+
else
2081+
{
2082+
// Viewer not reachable -- back off for 2 seconds before retrying.
2083+
_hdr_viewer_next_attempt_us = now_us + 2 * G_USEC_PER_SEC;
2084+
}
2085+
}
2086+
}
2087+
20052088
const size_t in_bpp = dt_iop_buffer_dsc_to_bpp(input_format);
20062089

20072090
piece->dsc_out = piece->dsc_in = *input_format;
@@ -2774,7 +2857,6 @@ static gboolean _dev_pixelpipe_process_rec(dt_dev_pixelpipe_t *pipe,
27742857
if(valid_input_on_gpu_only)
27752858
{
27762859
dt_dev_pixelpipe_invalidate_cacheline(pipe, input);
2777-
input_host_valid = FALSE;
27782860
}
27792861
}
27802862
else
@@ -2952,70 +3034,6 @@ static gboolean _dev_pixelpipe_process_rec(dt_dev_pixelpipe_t *pipe,
29523034
dt_ioppr_get_histogram_profile_info(dev));
29533035
}
29543036

2955-
// HDR viewer: forward the working-space linear image to the external HDR
2956-
// preview app (if running). We tap the *input* to the output color profile
2957-
// module ("colorout"), which is the fully-edited image still in the working
2958-
// profile's linear primaries (Rec.2020 by default) with super-white (>1.0)
2959-
// signal intact -- exactly what an EDR/HDR display needs, and before colorout
2960-
// bakes in the display TRC and gamma clips to 8-bit. The work-profile
2961-
// RGB->XYZ(D50) matrix is sent alongside so the viewer color-manages
2962-
// correctly for any working profile.
2963-
// NOTE: the socket calls below must remain outside any OMP parallel section.
2964-
if(dev->gui_attached && !dev->gui_leaving
2965-
&& pipe == dev->preview_pipe
2966-
&& input_host_valid
2967-
&& dt_iop_module_is(module, "colorout")
2968-
&& input_format->datatype == TYPE_FLOAT && input_format->channels == 4
2969-
&& dt_conf_get_bool("plugins/darkroom/hdr_viewer_enabled"))
2970-
{
2971-
// Cooldown: skip connect attempts for 2 seconds after a failed connect
2972-
// to avoid a 200ms timeout on every frame when the viewer is not running.
2973-
// The preview pipe runs on a single thread, so no synchronisation needed.
2974-
static int64_t _hdr_viewer_next_attempt_us = 0;
2975-
const int64_t now_us = g_get_monotonic_time();
2976-
if(now_us >= _hdr_viewer_next_attempt_us)
2977-
{
2978-
const int viewer_fd = dt_hdr_viewer_connect();
2979-
if(viewer_fd >= 0)
2980-
{
2981-
const size_t w = (size_t)roi_in.width;
2982-
const size_t h = (size_t)roi_in.height;
2983-
const float *const rgba = (const float *const)input;
2984-
2985-
// working RGB -> XYZ(D50): darktable stores this as a [4][4]
2986-
// dt_colormatrix_t (rows padded to 4 for SIMD); copy the 3x3 core.
2987-
const dt_iop_order_iccprofile_info_t *const work_profile
2988-
= dt_ioppr_get_pipe_work_profile_info(pipe);
2989-
float rgb_to_xyz[9];
2990-
if(work_profile)
2991-
for(int i = 0; i < 3; i++)
2992-
for(int j = 0; j < 3; j++)
2993-
rgb_to_xyz[i * 3 + j] = work_profile->matrix_in[i][j];
2994-
2995-
// Strip alpha channel: RGBA float -> RGB float (packed, row-major)
2996-
const size_t npixels = w * h;
2997-
float *rgb = work_profile ? dt_alloc_align_float(npixels * 3) : NULL;
2998-
if(rgb)
2999-
{
3000-
DT_OMP_FOR()
3001-
for(size_t k = 0; k < npixels; k++)
3002-
{
3003-
rgb[k * 3 + 0] = rgba[k * 4 + 0];
3004-
rgb[k * 3 + 1] = rgba[k * 4 + 1];
3005-
rgb[k * 3 + 2] = rgba[k * 4 + 2];
3006-
}
3007-
dt_hdr_viewer_send_frame(viewer_fd, (uint32_t)w, (uint32_t)h, rgb, rgb_to_xyz);
3008-
dt_free_align(rgb);
3009-
}
3010-
dt_hdr_viewer_disconnect(viewer_fd);
3011-
}
3012-
else
3013-
{
3014-
// Viewer not reachable -- back off for 2 seconds before retrying
3015-
_hdr_viewer_next_attempt_us = now_us + 2 * G_USEC_PER_SEC;
3016-
}
3017-
}
3018-
}
30193037
return _pipe_has_shutdown(pipe);
30203038
}
30213039

0 commit comments

Comments
 (0)