diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in index b2298bbd16f7..d9dc94cc14f6 100644 --- a/data/darktableconfig.xml.in +++ b/data/darktableconfig.xml.in @@ -1597,6 +1597,13 @@ show the guides widget in modules UI show the guides widget in modules UI + + plugins/darkroom/hdr_viewer_enabled + bool + false + send HDR preview to external viewer + forward float pixel data to the external HDR viewer app over a Unix domain socket before the gamma module clips to 8-bit. requires the standalone darktable-hdr-viewer app to be running. + plugins/lighttable/hide_default_presets bool diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index efda5704a7c9..450abf1080b6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -28,6 +28,7 @@ FILE(GLOB SOURCE_FILES "common/calculator.c" "common/collection.c" "common/color_harmony.c" + "common/hdr_viewer.c" "common/color_picker.c" "common/color_vocabulary.c" "common/colorlabels.c" diff --git a/src/common/hdr_viewer.c b/src/common/hdr_viewer.c new file mode 100644 index 000000000000..9f8a554286b5 --- /dev/null +++ b/src/common/hdr_viewer.c @@ -0,0 +1,193 @@ +/* + * dt_hdr_client.c + * + * Minimal POSIX-only C client for the darktable HDR Viewer. + * No external dependencies; compiles cleanly on macOS 10.13+ and Linux. + * + * See dt_hdr_client.h for API documentation. + */ + +#include "hdr_viewer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Timeout when the viewer is not yet running (milliseconds). */ +#ifndef DT_HDR_VIEWER_CONNECT_TIMEOUT_MS +# define DT_HDR_VIEWER_CONNECT_TIMEOUT_MS 200 +#endif + +/* -------------------------------------------------------------------------- + * Internal helpers + * -------------------------------------------------------------------------- */ + +/** + * Write exactly `len` bytes from `buf` to `fd`, restarting on EINTR. + * Returns 0 on success, -1 on error. + */ +static int write_exact(int fd, const void *buf, size_t len) +{ + const char *p = (const char *)buf; + size_t left = len; + + while (left > 0) { +#ifdef __linux__ + ssize_t n = send(fd, p, left, MSG_NOSIGNAL); +#else + ssize_t n = write(fd, p, left); +#endif + if (n < 0) { + if (errno == EINTR) continue; + return -1; + } + p += (size_t)n; + left -= (size_t)n; + } + return 0; +} + +/** + * Encode a uint32_t as 4 little-endian bytes into `out`. + */ +static void encode_le32(uint8_t out[4], uint32_t v) +{ + out[0] = (uint8_t)(v & 0xFFu); + out[1] = (uint8_t)((v >> 8) & 0xFFu); + out[2] = (uint8_t)((v >> 16) & 0xFFu); + out[3] = (uint8_t)((v >> 24) & 0xFFu); +} + +/* -------------------------------------------------------------------------- + * Public API + * -------------------------------------------------------------------------- */ + +int dt_hdr_viewer_connect(void) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + + /* Prevent SIGPIPE from killing the calling process if the viewer + * crashes or disconnects while we are writing a frame. */ +#ifdef SO_NOSIGPIPE + { + int yes = 1; + setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes)); + } +#endif + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, DT_HDR_VIEWER_SOCKET_PATH, + sizeof(addr.sun_path) - 1); + + /* + * Set a non-blocking connect with a short timeout so darktable does not + * stall when the viewer is not running. + */ +#if defined(__APPLE__) || defined(__linux__) + { + /* Set socket to non-blocking */ + int flags = 0; +# if defined(O_NONBLOCK) + { + /* POSIX fcntl path */ + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) flags = 0; + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + } +# endif + + int rc = connect(fd, + (const struct sockaddr *)&addr, + (socklen_t)sizeof(addr)); + + if (rc == 0) { + /* Connected immediately (unlikely for Unix sockets but possible) */ +# if defined(O_NONBLOCK) + fcntl(fd, F_SETFL, flags); /* restore blocking */ +# endif + return fd; + } + + if (errno != EINPROGRESS && errno != EAGAIN) { + close(fd); + return -1; + } + + /* Wait for the socket to become writable (= connected) */ + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(fd, &wfds); + + struct timeval tv; + tv.tv_sec = DT_HDR_VIEWER_CONNECT_TIMEOUT_MS / 1000; + tv.tv_usec = (DT_HDR_VIEWER_CONNECT_TIMEOUT_MS % 1000) * 1000; + + rc = select(fd + 1, NULL, &wfds, NULL, &tv); + if (rc <= 0) { + /* Timeout or error */ + close(fd); + return -1; + } + + /* Check that the connection actually succeeded */ + int err = 0; + socklen_t errlen = (socklen_t)sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen); + if (err != 0) { + close(fd); + return -1; + } + + /* Restore blocking mode for subsequent writes */ +# if defined(O_NONBLOCK) + fcntl(fd, F_SETFL, flags); +# endif + return fd; + } +#else + /* Fallback: plain blocking connect (may hang briefly if viewer is absent) */ + if (connect(fd, (const struct sockaddr *)&addr, + (socklen_t)sizeof(addr)) < 0) { + close(fd); + return -1; + } + return fd; +#endif +} + +void dt_hdr_viewer_send_frame(int fd, + uint32_t w, + uint32_t h, + const float *rgb_linear_bt2020) +{ + if (fd < 0 || w == 0 || h == 0 || rgb_linear_bt2020 == NULL) return; + + /* Send 8-byte header: width and height as little-endian uint32 */ + uint8_t header[8]; + encode_le32(header + 0, w); + encode_le32(header + 4, h); + + if (write_exact(fd, header, sizeof(header)) != 0) return; + + /* Send pixel data – already in host byte order (float32). + * On all modern Macs (and x86/ARM64 Linux) the host is little-endian, + * which matches what the Swift receiver expects. + * If big-endian support is ever needed, swap bytes here. */ + size_t pixel_bytes = (size_t)w * (size_t)h * 3u * sizeof(float); + write_exact(fd, rgb_linear_bt2020, pixel_bytes); + /* Errors are silently ignored; caller should reconnect if needed. */ +} + +void dt_hdr_viewer_disconnect(int fd) +{ + if (fd >= 0) close(fd); +} diff --git a/src/common/hdr_viewer.h b/src/common/hdr_viewer.h new file mode 100644 index 000000000000..08885789ec8f --- /dev/null +++ b/src/common/hdr_viewer.h @@ -0,0 +1,73 @@ +/* + * dt_hdr_client.h + * + * Minimal POSIX-only C client library for sending HDR pixel frames to the + * darktable HDR Viewer app (https://github.com/MaykThewessen/darktable-hdr-viewer). + * + * Protocol (little-endian): + * [4 bytes] width – uint32_t + * [4 bytes] height – uint32_t + * [width * height * 3 * sizeof(float)] – RGB float32, linear BT.2020, + * row-major, top-to-bottom + * + * Typical usage from darktable: + * + * int fd = dt_hdr_viewer_connect(); + * if (fd >= 0) { + * dt_hdr_viewer_send_frame(fd, width, height, rgb_linear_bt2020); + * dt_hdr_viewer_disconnect(fd); + * } + * + * Or keep `fd` open across frames for lower overhead (the server handles + * multiple frames per connection). + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default Unix-domain socket path used by the HDR Viewer app. */ +#define DT_HDR_VIEWER_SOCKET_PATH "/tmp/dt_hdr_viewer.sock" + +/** + * Connect to the HDR Viewer Unix socket. + * + * Returns a connected socket file descriptor on success, or -1 on failure + * (check errno for details). The connection attempt times out after + * DT_HDR_VIEWER_CONNECT_TIMEOUT_MS milliseconds. + */ +int dt_hdr_viewer_connect(void); + +/** + * Send one frame of linear BT.2020 RGB pixels to the HDR Viewer. + * + * @param fd File descriptor returned by dt_hdr_viewer_connect(). + * @param w Image width in pixels. + * @param h Image height in pixels. + * @param rgb_linear_bt2020 Row-major, top-to-bottom, interleaved RGB float32 + * buffer of size w * h * 3 floats. + * + * The call blocks until all data has been written. On write error the + * function returns silently; the caller should call dt_hdr_viewer_disconnect() + * and reconnect on the next frame if reliable delivery is required. + */ +void dt_hdr_viewer_send_frame(int fd, + uint32_t w, + uint32_t h, + const float *rgb_linear_bt2020); + +/** + * Close the connection to the HDR Viewer. + * + * @param fd File descriptor returned by dt_hdr_viewer_connect(), or -1 + * (no-op in that case). + */ +void dt_hdr_viewer_disconnect(int fd); + +#ifdef __cplusplus +} +#endif diff --git a/src/develop/pixelpipe_hb.c b/src/develop/pixelpipe_hb.c index 8d46100bf354..71fc4585a6d3 100644 --- a/src/develop/pixelpipe_hb.c +++ b/src/develop/pixelpipe_hb.c @@ -18,6 +18,7 @@ #include "common/color_picker.h" #include "common/colorspaces.h" +#include "common/hdr_viewer.h" #include "common/histogram.h" #include "common/opencl.h" #include "common/iop_order.h" @@ -3009,6 +3010,50 @@ static gboolean _dev_pixelpipe_process_rec(dt_dev_pixelpipe_t *pipe, roi_in.width, roi_in.height, display_profile, dt_ioppr_get_histogram_profile_info(dev)); + + // HDR viewer: forward float pixels to the external HDR preview app (if running). + // input is float RGBA in the display profile colorspace; values above 1.0 represent + // HDR signal that GTK would otherwise clip to uint8. + // NOTE: the socket calls below must remain outside any OMP parallel section. + if(dt_conf_get_bool("plugins/darkroom/hdr_viewer_enabled")) + { + // Cooldown: skip connect attempts for 2 seconds after a failed connect + // to avoid 200ms timeout on every frame when the viewer is not running. + // The preview pipe runs on a single thread, so no synchronisation needed. + static int64_t _hdr_viewer_next_attempt_us = 0; + const int64_t now_us = g_get_monotonic_time(); + if(now_us >= _hdr_viewer_next_attempt_us) + { + const size_t w = (size_t)roi_in.width; + const size_t h = (size_t)roi_in.height; + const float *const rgba = (const float *const)input; + int viewer_fd = dt_hdr_viewer_connect(); + if(viewer_fd >= 0) + { + // Strip alpha channel: RGBA float -> RGB float (packed, row-major) + const size_t npixels = w * h; + float *rgb = dt_alloc_align_float(npixels * 3); + if(rgb) + { + DT_OMP_FOR() + for(size_t k = 0; k < npixels; k++) + { + rgb[k * 3 + 0] = rgba[k * 4 + 0]; + rgb[k * 3 + 1] = rgba[k * 4 + 1]; + rgb[k * 3 + 2] = rgba[k * 4 + 2]; + } + dt_hdr_viewer_send_frame(viewer_fd, (uint32_t)w, (uint32_t)h, rgb); + dt_free_align(rgb); + } + dt_hdr_viewer_disconnect(viewer_fd); + } + else + { + // Viewer not reachable -- back off for 2 seconds before retrying + _hdr_viewer_next_attempt_us = now_us + 2 * G_USEC_PER_SEC; + } + } + } } return dt_pipe_shutdown(pipe); }