/* This file is part of darktable, Copyright (C) 2010-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include #include #include #include #include "bauhaus/bauhaus.h" #include "common/math.h" #include "control/control.h" #include "develop/develop.h" #include "develop/imageop.h" #include "develop/imageop_gui.h" #include "develop/tiling.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "iop/iop_api.h" #include "common/opencl.h" #define GRAIN_LIGHTNESS_STRENGTH_SCALE 0.15f // (m_pi/2)/4 = half hue colorspan #define GRAIN_HUE_COLORRANGE 0.392699082 #define GRAIN_HUE_STRENGTH_SCALE 0.25 #define GRAIN_SATURATION_STRENGTH_SCALE 0.25 #define GRAIN_RGB_STRENGTH_SCALE 0.25 #define GRAIN_SCALE_FACTOR 213.2 #define GRAIN_LUT_SIZE 128 #define GRAIN_LUT_DELTA_MAX 2.0 #define GRAIN_LUT_DELTA_MIN 0.0001 #define GRAIN_LUT_PAPER_GAMMA 1.0 DT_MODULE_INTROSPECTION(2, dt_iop_grain_params_t) typedef enum _dt_iop_grain_channel_t { DT_GRAIN_CHANNEL_HUE = 0, DT_GRAIN_CHANNEL_SATURATION, DT_GRAIN_CHANNEL_LIGHTNESS, DT_GRAIN_CHANNEL_RGB } _dt_iop_grain_channel_t; typedef struct dt_iop_grain_params_t { _dt_iop_grain_channel_t channel; // $DEFAULT: DT_GRAIN_CHANNEL_LIGHTNESS float scale; /* $MIN: 20.0/GRAIN_SCALE_FACTOR $MAX: 6400.0/GRAIN_SCALE_FACTOR $DEFAULT: 1600.0/GRAIN_SCALE_FACTOR $DESCRIPTION: "coarseness" */ float strength; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 25.0 float midtones_bias; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "mid-tones bias" } dt_iop_grain_params_t; typedef struct dt_iop_grain_gui_data_t { GtkWidget *scale, *strength, *midtones_bias; // scale, strength, midtones_bias } dt_iop_grain_gui_data_t; typedef struct dt_iop_grain_global_data_t { int kernel_grain; } dt_iop_grain_global_data_t; typedef struct dt_iop_grain_data_t { _dt_iop_grain_channel_t channel; float scale; float strength; float midtones_bias; float grain_lut[GRAIN_LUT_SIZE * GRAIN_LUT_SIZE]; } dt_iop_grain_data_t; int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void **new_params, int32_t *new_params_size, int *new_version) { typedef struct dt_iop_grain_params_v2_t { _dt_iop_grain_channel_t channel; float scale; float strength; float midtones_bias; } dt_iop_grain_params_v2_t; if(old_version == 1) { typedef struct dt_iop_grain_params_v1_t { _dt_iop_grain_channel_t channel; float scale; float strength; } dt_iop_grain_params_v1_t; const dt_iop_grain_params_v1_t *o = old_params; dt_iop_grain_params_v2_t *n = malloc(sizeof(dt_iop_grain_params_v2_t)); n->channel = o->channel; n->scale = o->scale; n->strength = o->strength; n->midtones_bias = 0.0; // it produces the same results as the old version *new_params = n; *new_params_size = sizeof(dt_iop_grain_params_v2_t); *new_version = 2; return 0; } return 1; } static const double grad3[12][3] = { { 1, 1, 0 }, { -1, 1, 0 }, { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 }, { -1, 0, 1 }, { 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 }, { 0, -1, 1 }, { 0, 1, -1 }, { 0, -1, -1 } }; static const int permutation[] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 }; static size_t perm[512]; // permutation lookup table static size_t perm_mod[512]; // same as above, but all values mod 12 for selection from grad3 static void _simplex_noise_init() { for(int i = 0; i < 512; i++) { perm[i] = permutation[i & 255]; perm_mod[i] = perm[i] % 12; } } static double dot(const double g[], const double x, const double y, const double z) { return g[0] * x + g[1] * y + g[2] * z; } #define FASTFLOOR(x) (x > 0 ? (int)(x) : (int)(x)-1) static double _simplex_noise(const double xin, const double yin, const double zin) { double n0, n1, n2, n3; // Noise contributions from the four corners // Skew the input space to determine which simplex cell we're in const double F3 = 1.0 / 3.0; const double s = (xin + yin + zin) * F3; // Very nice and simple skew factor for 3D const int i = FASTFLOOR(xin + s); const int j = FASTFLOOR(yin + s); const int k = FASTFLOOR(zin + s); const double G3 = 1.0 / 6.0; // Very nice and simple unskew factor, too const double t = (i + j + k) * G3; const double X0 = i - t; // Unskew the cell origin back to (x,y,z) space const double Y0 = j - t; const double Z0 = k - t; const double x0 = xin - X0; // The x,y,z distances from the cell origin const double y0 = yin - Y0; const double z0 = zin - Z0; // For the 3D case, the simplex shape is a slightly irregular tetrahedron. // Determine which simplex we are in. int i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords int i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords if(x0 >= y0) { if(y0 >= z0) { i1 = 1; // X Y Z order j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0; } else if(x0 >= z0) { i1 = 1; // X Z Y order j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1; } else { i1 = 0; // Z X Y order j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1; } } else // x0= 0; i--) hash = ((hash << 5) + hash) ^ str[i]; return hash; } void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) return; dt_iop_grain_data_t *data = piece->data; const unsigned int hash = _hash_string(piece->pipe->image.filename) % (int)fmax(roi_out->width * 0.3, 1.0); const gboolean fastmode = dt_pipe_is_fast(piece->pipe); // Apply grain to image const float strength = (data->strength / 100.0f); // double zoom=1.0+(8*(data->scale/100.0)); const double wd = fminf(piece->buf_in.width, piece->buf_in.height); const double zoom = (1.0 + 8 * data->scale / 100) / 800.0; // in fastpipe mode, skip the downsampling for zoomed-out views const int filter = !fastmode && fabsf(roi_out->scale - 1.0f) > 0.01f; // filter width depends on world space (i.e. reverse wd norm and // roi->scale, as well as buffer input to pixelpipe iscale) const double filtermul = piece->iscale / (roi_out->scale * wd); const float fib1 = 34.0f, fib2 = 21.0f; const float fib1div2 = fib1 / fib2; const double scale = roi_out->scale; // is only used in double expressions, so avoid conversion const double fib2inv = 1.0 / fib2; DT_OMP_FOR() for(int j = 0; j < roi_out->height; j++) { float *in = ((float *)ivoid) + (size_t)4 * roi_out->width * j; float *out = ((float *)ovoid) + (size_t)4 * roi_out->width * j; const double wy = (roi_out->y + j) / scale; const double y = wy / wd; // y: normalized to shorter side of image, so with pixel aspect = 1. for(int i = 0; i < roi_out->width; i++) { // calculate x, y in a resolution independent way: // wx,wy: worldspace in full image pixel coords: const double wx = (roi_out->x + i) / scale; // x: normalized to shorter side of image, so with pixel aspect = 1. const double x = wx / wd; float noise = 0.0; if(filter) { // if zoomed out a lot, use rank-1 lattice downsampling for(int l = 0; l < fib2; l++) { float px = l / fib2, py = l * fib1div2; py -= (int)py; float dx = px * filtermul, dy = py * filtermul; noise += fib2inv * _simplex_2d_noise(x + dx + hash, y + dy, zoom); } } else { noise = _simplex_2d_noise(x + hash, y, zoom); } out[0] = in[0] + dt_lut_lookup_2d_1c(data->grain_lut, (noise * strength) * GRAIN_LIGHTNESS_STRENGTH_SCALE, in[0] / 100.0f); out[1] = in[1]; out[2] = in[2]; out += 4; in += 4; } } } #ifdef HAVE_OPENCL // v mod 768 as (integer part, fraction): the simplex noise repeats every // 768 units along x and along y, so this is all a GPU kernel needs of v static void _reduce_coord(const double v, float c[2]) { const double r = fmod(v, 768.0); const double n = floor(r); c[0] = n; c[1] = MIN((float)(r - n), 0x1.fffffep-1f); } // the noise coordinates of process() for a GPU kernel, reduced: for each // octave, those of the columns, the rows and the filter samples (hash // included). Returns the number of samples, or 0 when out of memory. static int _noise_coords(const dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *const roi_out, float **cols, float **rows, float **xoffs, float **yoffs) { const dt_iop_grain_data_t *data = piece->data; const int width = roi_out->width; const int height = roi_out->height; // the same world space coordinates as process(), see there const unsigned int hash = _hash_string(piece->pipe->image.filename) % (int)fmax(roi_out->width * 0.3, 1.0); const gboolean fastmode = dt_pipe_is_fast(piece->pipe); const double wd = fminf(piece->buf_in.width, piece->buf_in.height); const double zoom = (1.0 + 8 * data->scale / 100) / 800.0; const int filter = !fastmode && fabsf(roi_out->scale - 1.0f) > 0.01f; const double filtermul = piece->iscale / (roi_out->scale * wd); const float fib1 = 34.0f, fib2 = 21.0f; const float fib1div2 = fib1 / fib2; const double scale = roi_out->scale; const int samples = filter ? fib2 : 1; *cols = dt_alloc_align_float((size_t)6 * width); *rows = dt_alloc_align_float((size_t)6 * height); *xoffs = dt_alloc_align_float((size_t)6 * samples); *yoffs = dt_alloc_align_float((size_t)6 * samples); if(!*cols || !*rows || !*xoffs || !*yoffs) return 0; for(int o = 0; o < 3; o++) { for(int i = 0; i < width; i++) _reduce_coord(((roi_out->x + i) / scale / wd) * _octave_f[o] / zoom, *cols + 6 * i + 2 * o); for(int j = 0; j < height; j++) _reduce_coord(((roi_out->y + j) / scale / wd) * _octave_f[o] / zoom, *rows + 6 * j + 2 * o); for(int l = 0; l < samples; l++) { float dx = 0.0f, dy = 0.0f; if(filter) { float px = l / fib2, py = l * fib1div2; py -= (int)py; dx = px * filtermul; dy = py * filtermul; } _reduce_coord(((double)dx + hash) * _octave_f[o] / zoom, *xoffs + 6 * l + 2 * o); _reduce_coord((double)dy * _octave_f[o] / zoom, *yoffs + 6 * l + 2 * o); } } return samples; } int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { dt_iop_grain_data_t *data = piece->data; dt_iop_grain_global_data_t *gd = self->global_data; const int devid = piece->pipe->devid; const int width = roi_out->width; const int height = roi_out->height; const float strength = (data->strength / 100.0f); cl_int err = DT_OPENCL_SYSMEM_ALLOCATION; cl_mem dev_cols = NULL, dev_rows = NULL, dev_xoffs = NULL, dev_yoffs = NULL, dev_lut = NULL; float *cols = NULL, *rows = NULL, *xoffs = NULL, *yoffs = NULL; const int samples = _noise_coords(piece, roi_out, &cols, &rows, &xoffs, &yoffs); if(!samples) goto cleanup; err = DT_OPENCL_DEFAULT_ERROR; dev_cols = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 6 * width, cols); dev_rows = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 6 * height, rows); dev_xoffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 6 * samples, xoffs); dev_yoffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 6 * samples, yoffs); dev_lut = dt_opencl_copy_host_to_device_constant(devid, sizeof(data->grain_lut), data->grain_lut); if(!dev_cols || !dev_rows || !dev_xoffs || !dev_yoffs || !dev_lut) goto cleanup; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_grain, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(dev_cols), CLARG(dev_rows), CLARG(dev_xoffs), CLARG(dev_yoffs), CLARG(samples), CLARG(strength), CLARG(dev_lut)); cleanup: dt_opencl_release_mem_object(dev_cols); dt_opencl_release_mem_object(dev_rows); dt_opencl_release_mem_object(dev_xoffs); dt_opencl_release_mem_object(dev_yoffs); dt_opencl_release_mem_object(dev_lut); dt_free_align(cols); dt_free_align(rows); dt_free_align(xoffs); dt_free_align(yoffs); return err; } #endif void tiling_callback(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *roi_in, const dt_iop_roi_t *roi_out, struct dt_develop_tiling_t *tiling) { // grain does not allow tiling: process() offsets the noise by a hash // modulo the roi width, so tiles would not line up. This only decides // whether the OpenCL path fits: input and output, plus the noise // coordinates of _noise_coords() (up to 21 samples) and the LUT tiling->factor = 2.0f; tiling->factor_cl = 2.0f; tiling->maxbuf = 1.0f; tiling->maxbuf_cl = 1.0f; tiling->overhead = sizeof(float) * 6 * (roi_out->width + roi_out->height + 2 * 21) + sizeof(float) * GRAIN_LUT_SIZE * GRAIN_LUT_SIZE; tiling->overlap = 0; tiling->align = 1; } void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { dt_iop_grain_params_t *p = (dt_iop_grain_params_t *)p1; dt_iop_grain_data_t *d = piece->data; d->channel = p->channel; d->scale = p->scale; d->strength = p->strength; d->midtones_bias = p->midtones_bias; evaluate_grain_lut(d->grain_lut, d->midtones_bias); } void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = calloc(1, sizeof(dt_iop_grain_data_t)); } void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { free(piece->data); piece->data = NULL; } void init_global(dt_iop_module_so_t *self) { _simplex_noise_init(); const int program = 48; // grain.cl, from programs.conf dt_iop_grain_global_data_t *gd = malloc(sizeof(dt_iop_grain_global_data_t)); self->data = gd; gd->kernel_grain = dt_opencl_create_kernel(program, "grain"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_grain_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_grain); free(self->data); self->data = NULL; } void gui_init(dt_iop_module_t *self) { dt_iop_grain_gui_data_t *g = IOP_GUI_ALLOC(grain); /* courseness */ g->scale = dt_bauhaus_slider_from_params(self, "scale"); dt_bauhaus_slider_set_factor(g->scale, GRAIN_SCALE_FACTOR); dt_bauhaus_slider_set_digits(g->scale, 0); dt_bauhaus_slider_set_format(g->scale, " ISO"); gtk_widget_set_tooltip_text(g->scale, _("the grain size (~ISO of the film)")); g->strength = dt_bauhaus_slider_from_params(self, N_("strength")); dt_bauhaus_slider_set_format(g->strength, "%"); gtk_widget_set_tooltip_text(g->strength, _("the strength of applied grain")); g->midtones_bias = dt_bauhaus_slider_from_params(self, "midtones_bias"); dt_bauhaus_slider_set_format(g->midtones_bias, "%"); gtk_widget_set_tooltip_text (g->midtones_bias, _("amount of mid-tones bias from the photographic paper response modeling. the greater the bias, the more pronounced the fall off of the grain in shadows and highlights")); } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on