Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions data/darktableconfig.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,13 @@
<shortdescription>show the guides widget in modules UI</shortdescription>
<longdescription>show the guides widget in modules UI</longdescription>
</dtconfig>
<dtconfig>
<name>plugins/darkroom/hdr_viewer_enabled</name>
<type>bool</type>
<default>false</default>
<shortdescription>send HDR preview to external viewer</shortdescription>
<longdescription>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.</longdescription>
</dtconfig>
<dtconfig prefs="lighttable" section="general">
<name>plugins/lighttable/hide_default_presets</name>
<type>bool</type>
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
193 changes: 193 additions & 0 deletions src/common/hdr_viewer.c
Original file line number Diff line number Diff line change
@@ -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 <sys/socket.h>
#include <sys/select.h>
#include <sys/un.h>
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

/* 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);
}
73 changes: 73 additions & 0 deletions src/common/hdr_viewer.h
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>

#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
45 changes: 45 additions & 0 deletions src/develop/pixelpipe_hb.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);
}
Expand Down