/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup bke
 */

#include <algorithm>
#include <cstring>

#include "MEM_guardedalloc.h"

#include "DNA_brush_types.h"
#include "DNA_scene_types.h"
#include "DNA_space_types.h"

#include "BLI_bitmap.h"
#include "BLI_listbase.h"
#include "BLI_math_color.h"
#include "BLI_math_color_blend.h"
#include "BLI_stack.h"
#include "BLI_task.h"

#include "BKE_brush.hh"
#include "BKE_colorband.hh"
#include "BKE_context.hh"
#include "BKE_image.hh"
#include "BKE_paint.hh"
#include "BKE_paint_types.hh"
#include "BKE_report.hh"

#include "DEG_depsgraph.hh"

#include "ED_paint.hh"
#include "ED_screen.hh"

#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"

#include "WM_api.hh"
#include "WM_types.hh"

#include "UI_view2d.hh"

#include "../paint_intern.hh"

namespace blender {

/* Brush Painting for 2D image editor */

/* Defines and Structs */

struct BrushPainterCache {
  bool is_float; /* need float imbuf? */
  bool is_data;  /* is non-color data? */
  bool is_srgb;  /* is the byte colorspace sRGB? */
  const ColorSpace *byte_colorspace;
  bool invert;

  bool is_texbrush;
  bool is_maskbrush;

  int lastdiameter;
  float last_tex_rotation;
  float last_mask_rotation;
  float last_pressure;

  ImBuf *ibuf;
  ImBuf *texibuf;
  ushort *tex_mask;
  ushort *tex_mask_old;
  uint tex_mask_old_w;
  uint tex_mask_old_h;

  CurveMaskCache curve_mask_cache;

  // int image_size[2]; /* UNUSED. */
};

struct BrushPainter {
  Scene *scene;
  const Paint *paint;
  Brush *brush;

  /* Store initial starting points for perlin noise on the beginning of each stroke when using
   * color jitter. */
  std::optional<float3> initial_hsv_jitter;

  bool firsttouch; /* first paint op */

  ImagePool *pool;   /* image pool */
  rctf tex_mapping;  /* texture coordinate mapping */
  rctf mask_mapping; /* mask texture coordinate mapping */

  bool cache_invert;
};

struct ImagePaintRegion {
  int destx, desty;
  int srcx, srcy;
  int width, height;
};

enum ImagePaintTileState {
  PAINT2D_TILE_UNINITIALIZED = 0,
  PAINT2D_TILE_MISSING,
  PAINT2D_TILE_READY,
};

struct ImagePaintTile {
  ImageUser iuser;
  ImBuf *canvas = nullptr;
  float radius_fac = 0.0f;
  int size[2] = {};
  float uv_origin[2] = {}; /* Stores the position of this tile in UV space. */
  bool need_redraw = false;
  BrushPainterCache cache = {};

  ImagePaintTileState state = PAINT2D_TILE_UNINITIALIZED;

  float last_paintpos[2] = {};  /* position of last paint op */
  float start_paintpos[2] = {}; /* position of first paint */
};

struct ImagePaintState {
  BrushPainter *painter;
  SpaceImage *sima;
  View2D *v2d;
  Scene *scene;
  const Paint *paint;

  Brush *brush;
  short brush_type, blend;
  Image *image;
  ImBuf *clonecanvas;

  bool do_masking;

  int symmetry;

  ImagePaintTile *tiles;
  int num_tiles;

  BlurKernel *blurkernel;
};

static BrushPainter *brush_painter_2d_new(Scene *scene,
                                          const Paint *paint,
                                          Brush *brush,
                                          bool invert)
{
  BrushPainter *painter = MEM_new<BrushPainter>(__func__);

  painter->brush = brush;
  painter->scene = scene;
  painter->paint = paint;
  if (BKE_brush_color_jitter_get_settings(paint, brush)) {
    painter->initial_hsv_jitter = seed_hsv_jitter();
  }
  painter->firsttouch = true;
  painter->cache_invert = invert;

  return painter;
}

static void brush_painter_2d_require_imbuf(Brush *brush,
                                           ImagePaintTile *tile,
                                           bool is_float,
                                           bool is_data,
                                           bool is_srgb,
                                           const ColorSpace *byte_colorspace,
                                           bool invert)
{
  BrushPainterCache *cache = &tile->cache;

  if (cache->is_float != is_float) {
    if (cache->ibuf) {
      IMB_freeImBuf(cache->ibuf);
    }
    if (cache->tex_mask) {
      MEM_delete(cache->tex_mask);
    }
    if (cache->tex_mask_old) {
      MEM_delete(cache->tex_mask_old);
    }
    cache->ibuf = nullptr;
    cache->tex_mask = nullptr;
    cache->lastdiameter = -1; /* force ibuf create in refresh */
  }

  cache->is_float = is_float;
  cache->is_data = is_data;
  cache->is_srgb = is_srgb;
  cache->byte_colorspace = byte_colorspace;
  cache->invert = invert;
  cache->is_texbrush = (brush->mtex.tex &&
                        brush->image_brush_type == IMAGE_PAINT_BRUSH_TYPE_DRAW) ?
                           true :
                           false;
  cache->is_maskbrush = (brush->mask_mtex.tex) ? true : false;
}

static void brush_painter_cache_2d_free(BrushPainterCache *cache)
{
  if (cache->ibuf) {
    IMB_freeImBuf(cache->ibuf);
  }
  if (cache->texibuf) {
    IMB_freeImBuf(cache->texibuf);
  }
  paint_curve_mask_cache_free_data(&cache->curve_mask_cache);
  if (cache->tex_mask) {
    MEM_delete(cache->tex_mask);
  }
  if (cache->tex_mask_old) {
    MEM_delete(cache->tex_mask_old);
  }
}

static void brush_imbuf_tex_co(const rctf *mapping, int x, int y, float texco[3])
{
  texco[0] = mapping->xmin + x * mapping->xmax;
  texco[1] = mapping->ymin + y * mapping->ymax;
  texco[2] = 0.0f;
}

/* create a mask with the mask texture */
static ushort *brush_painter_mask_ibuf_new(BrushPainter *painter, const int size)
{
  Brush *brush = painter->brush;
  rctf mask_mapping = painter->mask_mapping;
  ImagePool *pool = painter->pool;

  float texco[3];
  ushort *mask, *m;
  int x, y, thread = 0;

  mask = MEM_new_array_uninitialized<ushort>(size * size, __func__);
  m = mask;

  for (y = 0; y < size; y++) {
    for (x = 0; x < size; x++, m++) {
      float res;
      brush_imbuf_tex_co(&mask_mapping, x, y, texco);
      res = BKE_brush_sample_masktex(painter->paint, brush, texco, thread, pool);
      *m = ushort(65535.0f * res);
    }
  }

  return mask;
}

/* update rectangular section of the brush image */
static void brush_painter_mask_imbuf_update(BrushPainter *painter,
                                            ImagePaintTile *tile,
                                            const ushort *tex_mask_old,
                                            int origx,
                                            int origy,
                                            int w,
                                            int h,
                                            int xt,
                                            int yt,
                                            const int diameter)
{
  Brush *brush = painter->brush;
  BrushPainterCache *cache = &tile->cache;
  rctf tex_mapping = painter->mask_mapping;
  ImagePool *pool = painter->pool;
  ushort res;

  bool use_texture_old = (tex_mask_old != nullptr);

  int x, y, thread = 0;

  ushort *tex_mask = cache->tex_mask;
  ushort *tex_mask_cur = cache->tex_mask_old;

  /* fill pixels */
  for (y = origy; y < h; y++) {
    for (x = origx; x < w; x++) {
      /* sample texture */
      float texco[3];

      /* handle byte pixel */
      ushort *b = tex_mask + (y * diameter + x);
      ushort *t = tex_mask_cur + (y * diameter + x);

      if (!use_texture_old) {
        brush_imbuf_tex_co(&tex_mapping, x, y, texco);
        res = ushort(65535.0f *
                     BKE_brush_sample_masktex(painter->paint, brush, texco, thread, pool));
      }

      /* read from old texture buffer */
      if (use_texture_old) {
        res = *(tex_mask_old + ((y - origy + yt) * cache->tex_mask_old_w + (x - origx + xt)));
      }

      /* write to new texture mask */
      *t = res;
      /* write to mask image buffer */
      *b = res;
    }
  }
}

/**
 * Update the brush mask image by trying to reuse the cached texture result.
 * This can be considerably faster for brushes that change size due to pressure or
 * textures that stick to the surface where only part of the pixels are new
 */
static void brush_painter_mask_imbuf_partial_update(BrushPainter *painter,
                                                    ImagePaintTile *tile,
                                                    const float pos[2],
                                                    const int diameter)
{
  BrushPainterCache *cache = &tile->cache;
  ushort *tex_mask_old;
  int destx, desty, srcx, srcy, w, h, x1, y1, x2, y2;

  /* create brush image buffer if it didn't exist yet */
  if (!cache->tex_mask) {
    cache->tex_mask = MEM_new_array_uninitialized<ushort>(diameter * diameter, __func__);
  }

  /* create new texture image buffer with coordinates relative to old */
  tex_mask_old = cache->tex_mask_old;
  cache->tex_mask_old = MEM_new_array_uninitialized<ushort>(diameter * diameter, __func__);

  if (tex_mask_old) {
    ImBuf maskibuf;
    ImBuf maskibuf_old;
    maskibuf.x = diameter;
    maskibuf.y = diameter;
    maskibuf_old.x = cache->tex_mask_old_w;
    maskibuf_old.y = cache->tex_mask_old_h;

    srcx = srcy = 0;
    w = cache->tex_mask_old_w;
    h = cache->tex_mask_old_h;
    destx = int(floorf(tile->last_paintpos[0])) - int(floorf(pos[0])) + (diameter / 2 - w / 2);
    desty = int(floorf(tile->last_paintpos[1])) - int(floorf(pos[1])) + (diameter / 2 - h / 2);

    /* hack, use temporary rects so that clipping works */
    IMB_rectclip(&maskibuf, &maskibuf_old, &destx, &desty, &srcx, &srcy, &w, &h);
  }
  else {
    srcx = srcy = 0;
    destx = desty = 0;
    w = h = 0;
  }

  x1 = min_ii(destx, diameter);
  y1 = min_ii(desty, diameter);
  x2 = min_ii(destx + w, diameter);
  y2 = min_ii(desty + h, diameter);

  /* blend existing texture in new position */
  if ((x1 < x2) && (y1 < y2)) {
    brush_painter_mask_imbuf_update(
        painter, tile, tex_mask_old, x1, y1, x2, y2, srcx, srcy, diameter);
  }

  if (tex_mask_old) {
    MEM_delete(tex_mask_old);
  }

  /* sample texture in new areas */
  if ((0 < x1) && (0 < diameter)) {
    brush_painter_mask_imbuf_update(painter, tile, nullptr, 0, 0, x1, diameter, 0, 0, diameter);
  }
  if ((x2 < diameter) && (0 < diameter)) {
    brush_painter_mask_imbuf_update(
        painter, tile, nullptr, x2, 0, diameter, diameter, 0, 0, diameter);
  }
  if ((x1 < x2) && (0 < y1)) {
    brush_painter_mask_imbuf_update(painter, tile, nullptr, x1, 0, x2, y1, 0, 0, diameter);
  }
  if ((x1 < x2) && (y2 < diameter)) {
    brush_painter_mask_imbuf_update(painter, tile, nullptr, x1, y2, x2, diameter, 0, 0, diameter);
  }

  /* through with sampling, now update sizes */
  cache->tex_mask_old_w = diameter;
  cache->tex_mask_old_h = diameter;
}

/* create imbuf with brush color */
static ImBuf *brush_painter_imbuf_new(
    BrushPainter *painter, ImagePaintTile *tile, const int size, float pressure, float distance)
{
  const Paint *paint = painter->paint;
  Brush *brush = painter->brush;
  BrushPainterCache *cache = &tile->cache;

  rctf tex_mapping = painter->tex_mapping;
  ImagePool *pool = painter->pool;

  const bool is_float = cache->is_float;
  const bool is_texbrush = cache->is_texbrush;

  int x, y, thread = 0;
  float brush_rgb[3];

  /* allocate image buffer */
  ImBuf *ibuf = IMB_allocImBuf(
      size, size, (is_float) ? ImBufFlags::FloatData : ImBufFlags::ByteData);

  /* get brush color */
  if (brush->image_brush_type == IMAGE_PAINT_BRUSH_TYPE_DRAW) {
    paint_brush_color_get(
        paint, brush, painter->initial_hsv_jitter, cache->invert, distance, pressure, brush_rgb);

    if (cache->is_srgb) {
      IMB_colormanagement_scene_linear_to_srgb_v3(brush_rgb, brush_rgb);
    }
    else if (cache->byte_colorspace) {
      IMB_colormanagement_scene_linear_to_colorspace_v3(brush_rgb, cache->byte_colorspace);
    }
  }
  else {
    brush_rgb[0] = 1.0f;
    brush_rgb[1] = 1.0f;
    brush_rgb[2] = 1.0f;
  }

  /* fill image buffer */
  uchar *byte_data = ibuf->byte_data_for_write();
  float *float_data = ibuf->float_data_for_write();
  for (y = 0; y < size; y++) {
    for (x = 0; x < size; x++) {
      /* sample texture and multiply with brush color */
      float3 texco;
      float4 rgba;

      if (is_texbrush) {
        brush_imbuf_tex_co(&tex_mapping, x, y, texco);
        const MTex *mtex = &brush->mtex;
        BKE_brush_sample_tex_3d(painter->paint, brush, mtex, texco, rgba, thread, pool);
        if (cache->is_srgb) {
          IMB_colormanagement_scene_linear_to_srgb_v3(rgba, rgba);
        }
        else if (cache->byte_colorspace) {
          IMB_colormanagement_scene_linear_to_colorspace_v3(rgba, cache->byte_colorspace);
        }

        mul_v3_v3(rgba, brush_rgb);
      }
      else {
        copy_v3_v3(rgba, brush_rgb);
        rgba[3] = 1.0f;
      }

      if (is_float) {
        /* write to float pixel */
        float *dstf = float_data + (y * size + x) * 4;
        mul_v3_v3fl(dstf, rgba, rgba[3]); /* premultiply */
        dstf[3] = rgba[3];
      }
      else {
        /* write to byte pixel */
        uchar *dst = byte_data + (y * size + x) * 4;

        rgb_float_to_uchar(dst, rgba);
        dst[3] = unit_float_to_uchar_clamp(rgba[3]);
      }
    }
  }

  return ibuf;
}

/* update rectangular section of the brush image */
static void brush_painter_imbuf_update(BrushPainter *painter,
                                       ImagePaintTile *tile,
                                       ImBuf *oldtexibuf,
                                       int origx,
                                       int origy,
                                       int w,
                                       int h,
                                       int xt,
                                       int yt)
{
  const Paint *paint = painter->paint;
  Brush *brush = painter->brush;
  const MTex *mtex = &brush->mtex;
  BrushPainterCache *cache = &tile->cache;

  rctf tex_mapping = painter->tex_mapping;
  ImagePool *pool = painter->pool;

  const bool is_float = cache->is_float;
  const bool is_texbrush = cache->is_texbrush;
  const bool use_texture_old = (oldtexibuf != nullptr);

  int x, y, thread = 0;
  float brush_rgb[3];

  ImBuf *ibuf = cache->ibuf;
  ImBuf *texibuf = cache->texibuf;

  /* get brush color */
  if (brush->image_brush_type == IMAGE_PAINT_BRUSH_TYPE_DRAW) {
    paint_brush_color_get(
        paint, brush, painter->initial_hsv_jitter, cache->invert, 0.0f, 1.0f, brush_rgb);

    if (cache->is_srgb) {
      IMB_colormanagement_scene_linear_to_srgb_v3(brush_rgb, brush_rgb);
    }
    else if (cache->byte_colorspace) {
      IMB_colormanagement_scene_linear_to_colorspace_v3(brush_rgb, cache->byte_colorspace);
    }
  }
  else {
    brush_rgb[0] = 1.0f;
    brush_rgb[1] = 1.0f;
    brush_rgb[2] = 1.0f;
  }

  /* fill pixels */
  uchar *ibuf_byte_data = ibuf->byte_data_for_write();
  float *ibuf_float_data = ibuf->float_data_for_write();
  uchar *texibuf_byte_data = texibuf->byte_data_for_write();
  float *texibuf_float_data = texibuf->float_data_for_write();
  const uchar *oldtexibuf_byte_data = (oldtexibuf) ? oldtexibuf->byte_data() : nullptr;
  const float *oldtexibuf_float_data = (oldtexibuf) ? oldtexibuf->float_data() : nullptr;
  for (y = origy; y < h; y++) {
    for (x = origx; x < w; x++) {
      /* sample texture and multiply with brush color */
      float3 texco;
      float4 rgba;

      if (!use_texture_old) {
        if (is_texbrush) {
          brush_imbuf_tex_co(&tex_mapping, x, y, texco);
          BKE_brush_sample_tex_3d(painter->paint, brush, mtex, texco, rgba, thread, pool);
          if (cache->is_srgb) {
            IMB_colormanagement_scene_linear_to_srgb_v3(rgba, rgba);
          }
          else if (cache->byte_colorspace) {
            IMB_colormanagement_scene_linear_to_colorspace_v3(rgba, cache->byte_colorspace);
          }

          mul_v3_v3(rgba, brush_rgb);
        }
        else {
          copy_v3_v3(rgba, brush_rgb);
          rgba[3] = 1.0f;
        }
      }

      if (is_float) {
        /* handle float pixel */
        float *bf = ibuf_float_data + (y * ibuf->x + x) * 4;
        float *tf = texibuf_float_data + (y * texibuf->x + x) * 4;

        /* read from old texture buffer */
        if (use_texture_old) {
          const float *otf = oldtexibuf_float_data +
                             ((y - origy + yt) * oldtexibuf->x + (x - origx + xt)) * 4;
          copy_v4_v4(rgba, otf);
        }

        /* write to new texture buffer */
        copy_v4_v4(tf, rgba);

        /* output premultiplied float image, mf was already premultiplied */
        mul_v3_v3fl(bf, rgba, rgba[3]);
        bf[3] = rgba[3];
      }
      else {
        uchar crgba[4];

        /* handle byte pixel */
        uchar *b = ibuf_byte_data + (y * ibuf->x + x) * 4;
        uchar *t = texibuf_byte_data + (y * texibuf->x + x) * 4;

        /* read from old texture buffer */
        if (use_texture_old) {
          const uchar *ot = oldtexibuf_byte_data +
                            ((y - origy + yt) * oldtexibuf->x + (x - origx + xt)) * 4;
          crgba[0] = ot[0];
          crgba[1] = ot[1];
          crgba[2] = ot[2];
          crgba[3] = ot[3];
        }
        else {
          rgba_float_to_uchar(crgba, rgba);
        }

        /* write to new texture buffer */
        t[0] = crgba[0];
        t[1] = crgba[1];
        t[2] = crgba[2];
        t[3] = crgba[3];

        /* write to brush image buffer */
        b[0] = crgba[0];
        b[1] = crgba[1];
        b[2] = crgba[2];
        b[3] = crgba[3];
      }
    }
  }
}

/* update the brush image by trying to reuse the cached texture result. this
 * can be considerably faster for brushes that change size due to pressure or
 * textures that stick to the surface where only part of the pixels are new */
static void brush_painter_imbuf_partial_update(BrushPainter *painter,
                                               ImagePaintTile *tile,
                                               const float pos[2],
                                               const int diameter)
{
  BrushPainterCache *cache = &tile->cache;
  ImBuf *oldtexibuf, *ibuf;
  int destx, desty, srcx, srcy, w, h, x1, y1, x2, y2;

  /* create brush image buffer if it didn't exist yet */
  ImBufFlags imbflag = (cache->is_float) ? ImBufFlags::FloatData : ImBufFlags::ByteData;
  if (!cache->ibuf) {
    cache->ibuf = IMB_allocImBuf(diameter, diameter, imbflag);
  }
  ibuf = cache->ibuf;

  /* create new texture image buffer with coordinates relative to old */
  oldtexibuf = cache->texibuf;
  cache->texibuf = IMB_allocImBuf(diameter, diameter, imbflag);

  if (oldtexibuf) {
    srcx = srcy = 0;
    w = oldtexibuf->x;
    h = oldtexibuf->y;
    destx = int(floorf(tile->last_paintpos[0])) - int(floorf(pos[0])) + (diameter / 2 - w / 2);
    desty = int(floorf(tile->last_paintpos[1])) - int(floorf(pos[1])) + (diameter / 2 - h / 2);

    IMB_rectclip(cache->texibuf, oldtexibuf, &destx, &desty, &srcx, &srcy, &w, &h);
  }
  else {
    srcx = srcy = 0;
    destx = desty = 0;
    w = h = 0;
  }

  x1 = min_ii(destx, ibuf->x);
  y1 = min_ii(desty, ibuf->y);
  x2 = min_ii(destx + w, ibuf->x);
  y2 = min_ii(desty + h, ibuf->y);

  /* blend existing texture in new position */
  if ((x1 < x2) && (y1 < y2)) {
    brush_painter_imbuf_update(painter, tile, oldtexibuf, x1, y1, x2, y2, srcx, srcy);
  }

  if (oldtexibuf) {
    IMB_freeImBuf(oldtexibuf);
  }

  /* sample texture in new areas */
  if ((0 < x1) && (0 < ibuf->y)) {
    brush_painter_imbuf_update(painter, tile, nullptr, 0, 0, x1, ibuf->y, 0, 0);
  }
  if ((x2 < ibuf->x) && (0 < ibuf->y)) {
    brush_painter_imbuf_update(painter, tile, nullptr, x2, 0, ibuf->x, ibuf->y, 0, 0);
  }
  if ((x1 < x2) && (0 < y1)) {
    brush_painter_imbuf_update(painter, tile, nullptr, x1, 0, x2, y1, 0, 0);
  }
  if ((x1 < x2) && (y2 < ibuf->y)) {
    brush_painter_imbuf_update(painter, tile, nullptr, x1, y2, x2, ibuf->y, 0, 0);
  }
}

static void brush_painter_2d_tex_mapping(ImagePaintState *s,
                                         ImagePaintTile *tile,
                                         const int diameter,
                                         const float pos[2],
                                         const float mouse[2],
                                         int mapmode,
                                         rctf *r_mapping)
{
  float invw = 1.0f / float(tile->canvas->x);
  float invh = 1.0f / float(tile->canvas->y);
  float start[2];

  /* find start coordinate of brush in canvas */
  start[0] = pos[0] - diameter / 2.0f;
  start[1] = pos[1] - diameter / 2.0f;

  if (mapmode == MTEX_MAP_MODE_STENCIL) {
    /* map from view coordinates of brush to region coordinates */
    float xmin, ymin, xmax, ymax;
    ui::view2d_view_to_region_fl(s->v2d, start[0] * invw, start[1] * invh, &xmin, &ymin);
    ui::view2d_view_to_region_fl(
        s->v2d, (start[0] + diameter) * invw, (start[1] + diameter) * invh, &xmax, &ymax);

    /* output r_mapping from brush ibuf x/y to region coordinates */
    r_mapping->xmax = (xmax - xmin) / float(diameter);
    r_mapping->ymax = (ymax - ymin) / float(diameter);
    r_mapping->xmin = xmin + (tile->uv_origin[0] * tile->size[0] * r_mapping->xmax);
    r_mapping->ymin = ymin + (tile->uv_origin[1] * tile->size[1] * r_mapping->ymax);
  }
  else if (mapmode == MTEX_MAP_MODE_3D) {
    /* 3D mapping, just mapping to canvas 0..1. */
    r_mapping->xmin = 2.0f * (start[0] * invw - 0.5f);
    r_mapping->ymin = 2.0f * (start[1] * invh - 0.5f);
    r_mapping->xmax = 2.0f * invw;
    r_mapping->ymax = 2.0f * invh;
  }
  else if (ELEM(mapmode, MTEX_MAP_MODE_VIEW, MTEX_MAP_MODE_RANDOM)) {
    /* view mapping */
    r_mapping->xmin = mouse[0] - diameter * 0.5f + 0.5f;
    r_mapping->ymin = mouse[1] - diameter * 0.5f + 0.5f;
    r_mapping->xmax = 1.0f;
    r_mapping->ymax = 1.0f;
  }
  else /* if (mapmode == MTEX_MAP_MODE_TILED) */ {
    r_mapping->xmin = int(-diameter * 0.5) + int(floorf(pos[0])) -
                      int(floorf(tile->start_paintpos[0]));
    r_mapping->ymin = int(-diameter * 0.5) + int(floorf(pos[1])) -
                      int(floorf(tile->start_paintpos[1]));
    r_mapping->xmax = 1.0f;
    r_mapping->ymax = 1.0f;
  }
}

static void brush_painter_2d_refresh_cache(ImagePaintState *s,
                                           BrushPainter *painter,
                                           ImagePaintTile *tile,
                                           const float pos[2],
                                           const float mouse[2],
                                           float pressure,
                                           float distance,
                                           float size)
{
  const bke::PaintRuntime *paint_runtime = painter->paint->runtime;
  Brush *brush = painter->brush;
  BrushPainterCache *cache = &tile->cache;
  /* Adding 4 pixels of padding for brush anti-aliasing. */
  const int diameter = std::max(1, int(size * 2)) + 4;

  bool do_random = false;
  bool do_partial_update = false;
  bool update_color = ((brush->flag & BRUSH_USE_GRADIENT) &&
                       (ELEM(brush->gradient_stroke_mode,
                             BRUSH_GRADIENT_SPACING_REPEAT,
                             BRUSH_GRADIENT_SPACING_CLAMP) ||
                        (cache->last_pressure != pressure))) ||
                      BKE_brush_color_jitter_get_settings(painter->paint, brush);
  float tex_rotation = -brush->mtex.rot;
  float mask_rotation = -brush->mask_mtex.rot;

  painter->pool = BKE_image_pool_new();

  /* determine how can update based on textures used */
  if (cache->is_texbrush) {
    if (brush->mtex.brush_map_mode == MTEX_MAP_MODE_VIEW) {
      tex_rotation += paint_runtime->brush_rotation;
    }
    else if (brush->mtex.brush_map_mode == MTEX_MAP_MODE_RANDOM) {
      do_random = true;
    }
    else if (!((brush->stroke_method == BRUSH_STROKE_ANCHORED) || update_color)) {
      do_partial_update = true;
    }

    brush_painter_2d_tex_mapping(
        s, tile, diameter, pos, mouse, brush->mtex.brush_map_mode, &painter->tex_mapping);
  }

  if (cache->is_maskbrush) {
    bool renew_maxmask = false;
    bool do_partial_update_mask = false;
    /* invalidate case for all mapping modes */
    if (brush->mask_mtex.brush_map_mode == MTEX_MAP_MODE_VIEW) {
      mask_rotation += paint_runtime->brush_rotation_sec;
    }
    else if (brush->mask_mtex.brush_map_mode == MTEX_MAP_MODE_RANDOM) {
      renew_maxmask = true;
    }
    else if (!(brush->stroke_method == BRUSH_STROKE_ANCHORED)) {
      do_partial_update_mask = true;
      renew_maxmask = true;
    }
    /* explicitly disable partial update even if it has been enabled above */
    if (brush->mask_pressure) {
      do_partial_update_mask = false;
      renew_maxmask = true;
    }

    if (diameter != cache->lastdiameter || (mask_rotation != cache->last_mask_rotation) ||
        renew_maxmask)
    {
      MEM_SAFE_DELETE(cache->tex_mask);

      brush_painter_2d_tex_mapping(
          s, tile, diameter, pos, mouse, brush->mask_mtex.brush_map_mode, &painter->mask_mapping);

      if (do_partial_update_mask) {
        brush_painter_mask_imbuf_partial_update(painter, tile, pos, diameter);
      }
      else {
        cache->tex_mask = brush_painter_mask_ibuf_new(painter, diameter);
      }
      cache->last_mask_rotation = mask_rotation;
    }
  }

  /* Re-initialize the curve mask. Mask is always recreated due to the change of position. */
  paint_curve_mask_cache_update(&cache->curve_mask_cache, brush, diameter, size, pos);

  /* detect if we need to recreate image brush buffer */
  if (diameter != cache->lastdiameter || (tex_rotation != cache->last_tex_rotation) || do_random ||
      update_color)
  {
    if (cache->ibuf) {
      IMB_freeImBuf(cache->ibuf);
      cache->ibuf = nullptr;
    }

    if (do_partial_update) {
      /* do partial update of texture */
      brush_painter_imbuf_partial_update(painter, tile, pos, diameter);
    }
    else {
      /* create brush from scratch */
      cache->ibuf = brush_painter_imbuf_new(painter, tile, diameter, pressure, distance);
    }

    cache->lastdiameter = diameter;
    cache->last_tex_rotation = tex_rotation;
    cache->last_pressure = pressure;
  }
  else if (do_partial_update) {
    /* do only partial update of texture */
    int dx = int(floorf(tile->last_paintpos[0])) - int(floorf(pos[0]));
    int dy = int(floorf(tile->last_paintpos[1])) - int(floorf(pos[1]));

    if ((dx != 0) || (dy != 0)) {
      brush_painter_imbuf_partial_update(painter, tile, pos, diameter);
    }
  }

  BKE_image_pool_free(painter->pool);
  painter->pool = nullptr;
}

static bool paint_2d_ensure_tile_canvas(ImagePaintState *s, int i)
{
  if (i == 0) {
    return true;
  }
  if (i >= s->num_tiles) {
    return false;
  }

  if (s->tiles[i].state == PAINT2D_TILE_READY) {
    return true;
  }
  if (s->tiles[i].state == PAINT2D_TILE_MISSING) {
    return false;
  }

  s->tiles[i].cache.lastdiameter = -1;

  ImBuf *ibuf = BKE_image_acquire_ibuf(s->image, &s->tiles[i].iuser, nullptr);
  if (ibuf != nullptr) {
    if (ibuf->channels != 4) {
      s->tiles[i].state = PAINT2D_TILE_MISSING;
    }
    else if ((s->tiles[0].canvas->byte_data() && !ibuf->byte_data()) ||
             (s->tiles[0].canvas->float_data() && !ibuf->float_data()))
    {
      s->tiles[i].state = PAINT2D_TILE_MISSING;
    }
    else {
      s->tiles[i].size[0] = ibuf->x;
      s->tiles[i].size[1] = ibuf->y;
      s->tiles[i].radius_fac = sqrtf((float(ibuf->x) * float(ibuf->y)) /
                                     (s->tiles[0].size[0] * s->tiles[0].size[1]));
      s->tiles[i].state = PAINT2D_TILE_READY;
    }
  }
  else {
    s->tiles[i].state = PAINT2D_TILE_MISSING;
  }

  if (s->tiles[i].state == PAINT2D_TILE_MISSING) {
    BKE_image_release_ibuf(s->image, ibuf, nullptr);
    return false;
  }

  s->tiles[i].canvas = ibuf;
  return true;
}

/* keep these functions in sync */
static void paint_2d_ibuf_rgb_get(ImBuf *ibuf, int x, int y, float r_rgb[4])
{
  if (ibuf->float_data()) {
    const float *rrgbf = ibuf->float_data() + (ibuf->x * y + x) * 4;
    copy_v4_v4(r_rgb, rrgbf);
  }
  else {
    const uchar *rrgb = ibuf->byte_data() + (ibuf->x * y + x) * 4;
    straight_uchar_to_premul_float(r_rgb, rrgb);
  }
}
static void paint_2d_ibuf_rgb_set(
    ImBuf *ibuf, int x, int y, const bool is_torus, const float rgb[4])
{
  if (is_torus) {
    x %= ibuf->x;
    if (x < 0) {
      x += ibuf->x;
    }
    y %= ibuf->y;
    if (y < 0) {
      y += ibuf->y;
    }
  }

  if (float *float_data = ibuf->float_data_for_write()) {
    float *rrgbf = float_data + (ibuf->x * y + x) * 4;
    float map_alpha = (rgb[3] == 0.0f) ? rrgbf[3] : rrgbf[3] / rgb[3];

    mul_v3_v3fl(rrgbf, rgb, map_alpha);
    rrgbf[3] = rgb[3];
  }
  else {
    uchar straight[4];
    uchar *rrgb = ibuf->byte_data_for_write() + (ibuf->x * y + x) * 4;

    premul_float_to_straight_uchar(straight, rgb);
    rrgb[0] = straight[0];
    rrgb[1] = straight[1];
    rrgb[2] = straight[2];
    rrgb[3] = straight[3];
  }
}

static void paint_2d_ibuf_tile_convert(ImBuf *ibuf, int *x, int *y, short paint_tile)
{
  if (paint_tile & PAINT_TILE_X) {
    *x %= ibuf->x;
    if (*x < 0) {
      *x += ibuf->x;
    }
  }
  if (paint_tile & PAINT_TILE_Y) {
    *y %= ibuf->y;
    if (*y < 0) {
      *y += ibuf->y;
    }
  }
}

static float paint_2d_ibuf_add_if(
    ImBuf *ibuf, int x, int y, float *outrgb, short paint_tile, float w)
{
  float inrgb[4];

  if (paint_tile) {
    paint_2d_ibuf_tile_convert(ibuf, &x, &y, paint_tile);
  }
  /* need to also do clipping here always since tiled coordinates
   * are not always within bounds */
  if (x < ibuf->x && x >= 0 && y < ibuf->y && y >= 0) {
    paint_2d_ibuf_rgb_get(ibuf, x, y, inrgb);
  }
  else {
    return 0.0f;
  }

  mul_v4_fl(inrgb, w);
  add_v4_v4(outrgb, inrgb);

  return w;
}

static void paint_2d_lift_soften(ImagePaintState *s,
                                 ImagePaintTile *tile,
                                 ImBuf *ibuf,
                                 ImBuf *ibufb,
                                 const int *pos,
                                 const short paint_tile)
{
  bool sharpen = (tile->cache.invert ^ ((s->brush->flag & BRUSH_DIR_IN) != 0));
  float threshold = s->brush->sharp_threshold;
  int x, y, xi, yi, xo, yo, xk, yk;
  float count;
  int out_off[2], in_off[2], dim[2];
  int diff_pos[2];
  float outrgb[4];
  float rgba[4];
  BlurKernel *kernel = s->blurkernel;

  dim[0] = ibufb->x;
  dim[1] = ibufb->y;
  in_off[0] = pos[0];
  in_off[1] = pos[1];
  out_off[0] = out_off[1] = 0;

  if (!paint_tile) {
    IMB_rectclip(ibuf, ibufb, &in_off[0], &in_off[1], &out_off[0], &out_off[1], &dim[0], &dim[1]);

    if ((dim[0] == 0) || (dim[1] == 0)) {
      return;
    }
  }

  /* find offset inside mask buffers to sample them */
  sub_v2_v2v2_int(diff_pos, out_off, in_off);

  for (y = 0; y < dim[1]; y++) {
    for (x = 0; x < dim[0]; x++) {
      /* get input pixel */
      xi = in_off[0] + x;
      yi = in_off[1] + y;

      count = 0.0;
      if (paint_tile) {
        paint_2d_ibuf_tile_convert(ibuf, &xi, &yi, paint_tile);
        if (xi < ibuf->x && xi >= 0 && yi < ibuf->y && yi >= 0) {
          paint_2d_ibuf_rgb_get(ibuf, xi, yi, rgba);
        }
        else {
          zero_v4(rgba);
        }
      }
      else {
        /* coordinates have been clipped properly here, it should be safe to do this */
        paint_2d_ibuf_rgb_get(ibuf, xi, yi, rgba);
      }
      zero_v4(outrgb);

      for (yk = 0; yk < kernel->side; yk++) {
        for (xk = 0; xk < kernel->side; xk++) {
          count += paint_2d_ibuf_add_if(ibuf,
                                        xi + xk - kernel->pixel_len,
                                        yi + yk - kernel->pixel_len,
                                        outrgb,
                                        paint_tile,
                                        kernel->wdata[xk + yk * kernel->side]);
        }
      }

      if (count > 0.0f) {
        mul_v4_fl(outrgb, 1.0f / count);

        if (sharpen) {
          /* subtract blurred image from normal image gives high pass filter */
          sub_v3_v3v3(outrgb, rgba, outrgb);

          /* Now rgba_ub contains the edge result, but this should be converted to luminance to
           * avoid colored speckles appearing in final image, and also to check for threshold. */
          outrgb[0] = outrgb[1] = outrgb[2] = IMB_colormanagement_get_luminance(outrgb);
          if (fabsf(outrgb[0]) > threshold) {
            float mask = BKE_brush_alpha_get(s->paint, s->brush);
            float alpha = rgba[3];
            rgba[3] = outrgb[3] = mask;

            /* add to enhance edges */
            blend_color_add_float(outrgb, rgba, outrgb);
            outrgb[3] = alpha;
          }
          else {
            copy_v4_v4(outrgb, rgba);
          }
        }
      }
      else {
        copy_v4_v4(outrgb, rgba);
      }
      /* write into brush buffer */
      xo = out_off[0] + x;
      yo = out_off[1] + y;
      paint_2d_ibuf_rgb_set(ibufb, xo, yo, false, outrgb);
    }
  }
}

static void paint_2d_set_region(
    ImagePaintRegion *region, int destx, int desty, int srcx, int srcy, int width, int height)
{
  region->destx = destx;
  region->desty = desty;
  region->srcx = srcx;
  region->srcy = srcy;
  region->width = width;
  region->height = height;
}

static int paint_2d_torus_split_region(ImagePaintRegion region[4],
                                       ImBuf *dbuf,
                                       const ImBuf *sbuf,
                                       short paint_tile)
{
  int destx = region->destx;
  int desty = region->desty;
  int srcx = region->srcx;
  int srcy = region->srcy;
  int width = region->width;
  int height = region->height;
  int origw, origh, w, h, tot = 0;

  /* convert destination and source coordinates to be within image */
  if (paint_tile & PAINT_TILE_X) {
    destx = destx % dbuf->x;
    if (destx < 0) {
      destx += dbuf->x;
    }
    srcx = srcx % sbuf->x;
    if (srcx < 0) {
      srcx += sbuf->x;
    }
  }
  if (paint_tile & PAINT_TILE_Y) {
    desty = desty % dbuf->y;
    if (desty < 0) {
      desty += dbuf->y;
    }
    srcy = srcy % sbuf->y;
    if (srcy < 0) {
      srcy += sbuf->y;
    }
  }
  /* clip width of blending area to destination imbuf, to avoid writing the
   * same pixel twice */
  origw = w = (width > dbuf->x) ? dbuf->x : width;
  origh = h = (height > dbuf->y) ? dbuf->y : height;

  /* clip within image */
  IMB_rectclip(dbuf, sbuf, &destx, &desty, &srcx, &srcy, &w, &h);
  paint_2d_set_region(&region[tot++], destx, desty, srcx, srcy, w, h);

  /* do 3 other rects if needed */
  if ((paint_tile & PAINT_TILE_X) && w < origw) {
    paint_2d_set_region(
        &region[tot++], (destx + w) % dbuf->x, desty, (srcx + w) % sbuf->x, srcy, origw - w, h);
  }
  if ((paint_tile & PAINT_TILE_Y) && h < origh) {
    paint_2d_set_region(
        &region[tot++], destx, (desty + h) % dbuf->y, srcx, (srcy + h) % sbuf->y, w, origh - h);
  }
  if ((paint_tile & PAINT_TILE_X) && (paint_tile & PAINT_TILE_Y) && (w < origw) && (h < origh)) {
    paint_2d_set_region(&region[tot++],
                        (destx + w) % dbuf->x,
                        (desty + h) % dbuf->y,
                        (srcx + w) % sbuf->x,
                        (srcy + h) % sbuf->y,
                        origw - w,
                        origh - h);
  }

  return tot;
}

static void paint_2d_lift_smear(const ImBuf *ibuf, ImBuf *ibufb, int *pos, short paint_tile)
{
  ImagePaintRegion region[4];
  int a, tot;

  paint_2d_set_region(region, 0, 0, pos[0], pos[1], ibufb->x, ibufb->y);
  tot = paint_2d_torus_split_region(region, ibufb, ibuf, paint_tile);

  for (a = 0; a < tot; a++) {
    IMB_copy_rect(ibufb,
                  ibuf,
                  int2(region[a].srcx, region[a].srcy),
                  int2(region[a].destx, region[a].desty),
                  int2(region[a].width, region[a].height));
  }
}

static ImBuf *paint_2d_lift_clone(ImBuf *ibuf, ImBuf *ibufb, const int *pos)
{
  /* NOTE: #allocImbuf returns zeroed memory, so regions outside image will
   * have zero alpha, and hence not be blended onto the image */
  int w = ibufb->x, h = ibufb->y, destx = 0, desty = 0, srcx = pos[0], srcy = pos[1];
  ImBufFlags ibflags = ibufb->flags;
  if (ibufb->byte_data()) {
    ibflags |= ImBufFlags::ByteData;
  }
  if (ibufb->float_data()) {
    ibflags |= ImBufFlags::FloatData;
  }
  ImBuf *clonebuf = IMB_allocImBuf(w, h, ibflags);
  clonebuf->color_mode = ibufb->color_mode;

  IMB_rectclip(clonebuf, ibuf, &destx, &desty, &srcx, &srcy, &w, &h);
  IMB_rectblend(clonebuf,
                clonebuf,
                ibufb,
                nullptr,
                nullptr,
                nullptr,
                0,
                destx,
                desty,
                destx,
                desty,
                destx,
                desty,
                w,
                h,
                IMB_BLEND_COPY_ALPHA,
                false);
  IMB_rectblend(clonebuf,
                clonebuf,
                ibuf,
                nullptr,
                nullptr,
                nullptr,
                0,
                destx,
                desty,
                destx,
                desty,
                srcx,
                srcy,
                w,
                h,
                IMB_BLEND_COPY_RGB,
                false);

  return clonebuf;
}

static void paint_2d_convert_brushco(ImBuf *ibufb, const float pos[2], int ipos[2])
{
  ipos[0] = int(floorf(pos[0] - ibufb->x / 2));
  ipos[1] = int(floorf(pos[1] - ibufb->y / 2));
}

static void paint_2d_do_making_brush(ImagePaintState *s,
                                     ImagePaintTile *tile,
                                     ImagePaintRegion *region,
                                     ImBuf *frombuf,
                                     float mask_max,
                                     short blend,
                                     int tilex,
                                     int tiley,
                                     int tilew,
                                     int tileh)
{
  ImBuf tmpbuf;
  IMB_initImBuf(&tmpbuf, ED_IMAGE_UNDO_TILE_SIZE, ED_IMAGE_UNDO_TILE_SIZE, ImBufFlags::Zero);

  PaintTileMap *undo_tiles = ED_image_paint_tile_map_get();

  for (int ty = tiley; ty <= tileh; ty++) {
    for (int tx = tilex; tx <= tilew; tx++) {
      /* retrieve original pixels + mask from undo buffer */
      ushort *mask;
      int origx = region->destx - tx * ED_IMAGE_UNDO_TILE_SIZE;
      int origy = region->desty - ty * ED_IMAGE_UNDO_TILE_SIZE;

      if (const ImBuf *data = ED_image_paint_tile_find(
              undo_tiles, s->image, tile->canvas, &tile->iuser, tx, ty, &mask, false))
      {
        if (tile->canvas->float_data()) {
          tmpbuf.float_buffer = data->float_buffer;
        }
        else {
          tmpbuf.byte_buffer = data->byte_buffer;
        }
      }

      IMB_rectblend(tile->canvas,
                    &tmpbuf,
                    frombuf,
                    mask,
                    tile->cache.curve_mask_cache.curve_mask,
                    tile->cache.tex_mask,
                    mask_max,
                    region->destx,
                    region->desty,
                    origx,
                    origy,
                    region->srcx,
                    region->srcy,
                    region->width,
                    region->height,
                    IMB_BlendMode(blend),
                    ((s->brush->flag & BRUSH_ACCUMULATE) != 0));
    }
  }
}

struct Paint2DForeachData {
  ImagePaintState *s;
  ImagePaintTile *tile;
  ImagePaintRegion *region;
  ImBuf *frombuf;
  float mask_max;
  short blend;
  int tilex;
  int tilew;
};

static void paint_2d_op_foreach_do(void *__restrict data_v,
                                   const int iter,
                                   const TaskParallelTLS *__restrict /*tls*/)
{
  Paint2DForeachData *data = static_cast<Paint2DForeachData *>(data_v);
  paint_2d_do_making_brush(data->s,
                           data->tile,
                           data->region,
                           data->frombuf,
                           data->mask_max,
                           data->blend,
                           data->tilex,
                           iter,
                           data->tilew,
                           iter);
}

static int paint_2d_op(void *state,
                       ImagePaintTile *tile,
                       const float lastpos[2],
                       const float pos[2])
{
  ImagePaintState *s = (static_cast<ImagePaintState *>(state));
  const ImagePaintSettings &image_paint_settings = s->scene->toolsettings->imapaint;
  ImBuf *clonebuf = nullptr, *frombuf;
  ImBuf *canvas = tile->canvas;
  ImBuf *ibufb = tile->cache.ibuf;
  ImagePaintRegion region[4];
  short paint_tile = s->symmetry & (PAINT_TILE_X | PAINT_TILE_Y);
  short blend = s->blend;
  const float *offset = image_paint_settings.clone_offset;
  float liftpos[2];
  float mask_max = BKE_brush_alpha_get(s->paint, s->brush);
  int bpos[2], blastpos[2], bliftpos[2];
  int a, tot;

  paint_2d_convert_brushco(ibufb, pos, bpos);

  /* lift from canvas */
  if (s->brush_type == IMAGE_PAINT_BRUSH_TYPE_SOFTEN) {
    paint_2d_lift_soften(s, tile, canvas, ibufb, bpos, paint_tile);
    blend = IMB_BLEND_INTERPOLATE;
  }
  else if (s->brush_type == IMAGE_PAINT_BRUSH_TYPE_SMEAR) {
    if (lastpos[0] == pos[0] && lastpos[1] == pos[1]) {
      return 0;
    }

    paint_2d_convert_brushco(ibufb, lastpos, blastpos);
    paint_2d_lift_smear(canvas, ibufb, blastpos, paint_tile);
    blend = IMB_BLEND_INTERPOLATE;
  }
  else if (s->brush_type == IMAGE_PAINT_BRUSH_TYPE_CLONE && s->clonecanvas) {
    liftpos[0] = pos[0] - offset[0] * canvas->x;
    liftpos[1] = pos[1] - offset[1] * canvas->y;

    paint_2d_convert_brushco(ibufb, liftpos, bliftpos);
    clonebuf = paint_2d_lift_clone(s->clonecanvas, ibufb, bliftpos);
  }

  frombuf = (clonebuf) ? clonebuf : ibufb;

  if (paint_tile) {
    paint_2d_set_region(region, bpos[0], bpos[1], 0, 0, frombuf->x, frombuf->y);
    tot = paint_2d_torus_split_region(region, canvas, frombuf, paint_tile);
  }
  else {
    paint_2d_set_region(region, bpos[0], bpos[1], 0, 0, frombuf->x, frombuf->y);
    tot = 1;
  }

  /* blend into canvas */
  for (a = 0; a < tot; a++) {
    ED_imapaint_dirty_region(s->image,
                             canvas,
                             &tile->iuser,
                             region[a].destx,
                             region[a].desty,
                             region[a].width,
                             region[a].height,
                             true);

    if (s->do_masking) {
      /* masking, find original pixels tiles from undo buffer to composite over */
      int tilex, tiley, tilew, tileh;

      imapaint_region_tiles(canvas,
                            region[a].destx,
                            region[a].desty,
                            region[a].width,
                            region[a].height,
                            &tilex,
                            &tiley,
                            &tilew,
                            &tileh);

      if (tiley == tileh) {
        paint_2d_do_making_brush(
            s, tile, &region[a], frombuf, mask_max, blend, tilex, tiley, tilew, tileh);
      }
      else {
        Paint2DForeachData data;
        data.s = s;
        data.tile = tile;
        data.region = &region[a];
        data.frombuf = frombuf;
        data.mask_max = mask_max;
        data.blend = blend;
        data.tilex = tilex;
        data.tilew = tilew;

        TaskParallelSettings settings;
        BLI_parallel_range_settings_defaults(&settings);
        BLI_task_parallel_range(tiley, tileh + 1, &data, paint_2d_op_foreach_do, &settings);
      }
    }
    else {
      /* no masking, composite brush directly onto canvas */
      IMB_rectblend_threaded(canvas,
                             canvas,
                             frombuf,
                             nullptr,
                             tile->cache.curve_mask_cache.curve_mask,
                             tile->cache.tex_mask,
                             mask_max,
                             region[a].destx,
                             region[a].desty,
                             region[a].destx,
                             region[a].desty,
                             region[a].srcx,
                             region[a].srcy,
                             region[a].width,
                             region[a].height,
                             IMB_BlendMode(blend),
                             false);
    }
  }

  if (clonebuf) {
    IMB_freeImBuf(clonebuf);
  }

  return 1;
}

static int paint_2d_canvas_set(ImagePaintState *s, const Paint *paint)
{
  /* set clone canvas */
  if (s->brush_type == IMAGE_PAINT_BRUSH_TYPE_CLONE) {
    const ImagePaintSettings &image_paint_settings = s->scene->toolsettings->imapaint;
    Image *ima = image_paint_settings.clone;
    ImBuf *ibuf = BKE_image_acquire_ibuf(ima, nullptr, nullptr);

    if (!ima || !ibuf || !(ibuf->byte_data() || ibuf->float_data())) {
      BKE_image_release_ibuf(ima, ibuf, nullptr);
      return 0;
    }

    s->clonecanvas = ibuf;

    /* temporarily add float rect for cloning */
    if (s->tiles[0].canvas->float_data() && !s->clonecanvas->float_data()) {
      IMB_float_from_byte(s->clonecanvas);
    }
    else if (!s->tiles[0].canvas->float_data() && !s->clonecanvas->byte_data()) {
      IMB_byte_from_float(s->clonecanvas);
    }
  }

  /* set masking */
  s->do_masking = paint_use_opacity_masking(paint, s->brush);

  return 1;
}

static void paint_2d_canvas_free(ImagePaintState *s)
{
  for (int i = 0; i < s->num_tiles; i++) {
    BKE_image_release_ibuf(s->image, s->tiles[i].canvas, nullptr);
  }
  const ImagePaintSettings &image_paint_settings = s->scene->toolsettings->imapaint;
  BKE_image_release_ibuf(image_paint_settings.clone, s->clonecanvas, nullptr);

  if (s->blurkernel) {
    paint_delete_blur_kernel(s->blurkernel);
    MEM_delete(s->blurkernel);
  }
}

static void paint_2d_transform_mouse(View2D *v2d, const float in[2], float out[2])
{
  ui::view2d_region_to_view(v2d, in[0], in[1], &out[0], &out[1]);
}

static bool is_inside_tile(const int size[2], const float pos[2], const float brush[2])
{
  return (pos[0] >= -brush[0]) && (pos[0] < size[0] + brush[0]) && (pos[1] >= -brush[1]) &&
         (pos[1] < size[1] + brush[1]);
}

static void paint_2d_uv_to_coord(ImagePaintTile *tile, const float uv[2], float coord[2])
{
  coord[0] = (uv[0] - tile->uv_origin[0]) * tile->size[0];
  coord[1] = (uv[1] - tile->uv_origin[1]) * tile->size[1];
}

void paint_2d_stroke(void *ps,
                     const float prev_mval[2],
                     const float mval[2],
                     const bool eraser,
                     float pressure,
                     float distance,
                     float base_size)
{
  float new_uv[2], old_uv[2];
  ImagePaintState *s = static_cast<ImagePaintState *>(ps);
  BrushPainter *painter = s->painter;

  s->blend = s->brush->blend;
  if (eraser) {
    s->blend = IMB_BLEND_ERASE_ALPHA;
  }

  ui::view2d_region_to_view(s->v2d, mval[0], mval[1], &new_uv[0], &new_uv[1]);
  ui::view2d_region_to_view(s->v2d, prev_mval[0], prev_mval[1], &old_uv[0], &old_uv[1]);

  float last_uv[2], start_uv[2];
  ui::view2d_region_to_view(s->v2d, 0.0f, 0.0f, &start_uv[0], &start_uv[1]);
  if (painter->firsttouch) {
    /* paint exactly once on first touch */
    copy_v2_v2(last_uv, new_uv);
  }
  else {
    copy_v2_v2(last_uv, old_uv);
  }

  const float uv_brush_size[2] = {
      (s->symmetry & PAINT_TILE_X) ? FLT_MAX : base_size / s->tiles[0].size[0],
      (s->symmetry & PAINT_TILE_Y) ? FLT_MAX : base_size / s->tiles[0].size[1]};

  for (int i = 0; i < s->num_tiles; i++) {
    ImagePaintTile *tile = &s->tiles[i];

    /* First test: Project brush into UV space, clip against tile. */
    const int uv_size[2] = {1, 1};
    float local_new_uv[2], local_old_uv[2];
    sub_v2_v2v2(local_new_uv, new_uv, tile->uv_origin);
    sub_v2_v2v2(local_old_uv, old_uv, tile->uv_origin);
    if (!(is_inside_tile(uv_size, local_new_uv, uv_brush_size) ||
          is_inside_tile(uv_size, local_old_uv, uv_brush_size)))
    {
      continue;
    }

    /* Lazy tile loading to get size in pixels. */
    if (!paint_2d_ensure_tile_canvas(s, i)) {
      continue;
    }

    float size = base_size * tile->radius_fac;

    float new_coord[2], old_coord[2];
    paint_2d_uv_to_coord(tile, new_uv, new_coord);
    paint_2d_uv_to_coord(tile, old_uv, old_coord);
    if (painter->firsttouch) {
      paint_2d_uv_to_coord(tile, start_uv, tile->start_paintpos);
    }
    paint_2d_uv_to_coord(tile, last_uv, tile->last_paintpos);

    /* Second check in pixel coordinates. */
    const float pixel_brush_size[] = {(s->symmetry & PAINT_TILE_X) ? FLT_MAX : size,
                                      (s->symmetry & PAINT_TILE_Y) ? FLT_MAX : size};
    if (!(is_inside_tile(tile->size, new_coord, pixel_brush_size) ||
          is_inside_tile(tile->size, old_coord, pixel_brush_size)))
    {
      continue;
    }

    ImBuf *ibuf = tile->canvas;

    const bool is_data = ibuf->colorspace_is_data();
    const bool is_float = (ibuf->float_data() != nullptr);
    const ColorSpace *byte_colorspace = (is_float || is_data) ? nullptr :
                                                                ibuf->byte_buffer.colorspace;
    const bool is_srgb = (is_float || is_data) ?
                             false :
                             IMB_colormanagement_space_is_srgb(byte_colorspace);

    /* OCIO_TODO: float buffers are now always linear, so always use color correction
     *            this should probably be changed when texture painting color space is supported
     */
    brush_painter_2d_require_imbuf(painter->brush,
                                   tile,
                                   (ibuf->float_data() != nullptr),
                                   is_data,
                                   is_srgb,
                                   byte_colorspace,
                                   painter->cache_invert);

    brush_painter_2d_refresh_cache(s, painter, tile, new_coord, mval, pressure, distance, size);

    if (paint_2d_op(s, tile, old_coord, new_coord)) {
      tile->need_redraw = true;
    }
  }

  painter->firsttouch = false;
}

void *paint_2d_new_stroke(bContext *C, wmOperator *op, const BrushStrokeMode mode)
{
  Scene *scene = CTX_data_scene(C);
  SpaceImage *sima = CTX_wm_space_image(C);
  ToolSettings *settings = scene->toolsettings;
  const Paint *paint = BKE_paint_get_active_from_context(C);
  Brush *brush = BKE_paint_brush(&settings->imapaint.paint);

  ImagePaintState *s = MEM_new_zeroed<ImagePaintState>(__func__);

  s->sima = CTX_wm_space_image(C);
  s->v2d = &CTX_wm_region(C)->v2d;
  s->scene = scene;
  s->paint = paint;

  s->brush = brush;
  s->brush_type = brush->image_brush_type;
  s->blend = brush->blend;

  s->image = s->sima->image;
  s->symmetry = settings->imapaint.paint.symmetry_flags;

  if (s->image == nullptr) {
    MEM_delete(s);
    return nullptr;
  }
  if (BKE_image_has_packedfile(s->image) && s->image->rr != nullptr) {
    BKE_report(op->reports, RPT_WARNING, "Packed MultiLayer files cannot be painted");
    MEM_delete(s);
    return nullptr;
  }

  s->num_tiles = s->image->tiles.count();
  s->tiles = MEM_new_array<ImagePaintTile>(s->num_tiles, __func__);
  for (int i = 0; i < s->num_tiles; i++) {
    s->tiles[i].iuser = sima->iuser;
  }

  zero_v2(s->tiles[0].uv_origin);

  ImBuf *ibuf = BKE_image_acquire_ibuf(s->image, &s->tiles[0].iuser, nullptr);
  if (ibuf == nullptr) {
    MEM_delete(s->tiles);
    MEM_delete(s);
    return nullptr;
  }

  if (ibuf->channels != 4) {
    BKE_image_release_ibuf(s->image, ibuf, nullptr);
    BKE_report(op->reports, RPT_WARNING, "Image requires 4 color channels to paint");
    MEM_delete(s->tiles);
    MEM_delete(s);
    return nullptr;
  }

  s->tiles[0].size[0] = ibuf->x;
  s->tiles[0].size[1] = ibuf->y;
  s->tiles[0].radius_fac = 1.0f;

  s->tiles[0].canvas = ibuf;
  s->tiles[0].state = PAINT2D_TILE_READY;

  /* Initialize offsets here, they're needed for the uv space clip test before lazy-loading the
   * tile properly. */
  int tile_idx = 0;
  for (ImageTile *tile = static_cast<ImageTile *>(s->image->tiles.first); tile;
       tile = tile->next, tile_idx++)
  {
    s->tiles[tile_idx].iuser.tile = tile->tile_number;
    s->tiles[tile_idx].uv_origin[0] = ((tile->tile_number - 1001) % 10);
    s->tiles[tile_idx].uv_origin[1] = ((tile->tile_number - 1001) / 10);
  }

  if (!paint_2d_canvas_set(s, paint)) {
    MEM_delete(s->tiles);

    MEM_delete(s);
    return nullptr;
  }

  if (brush->image_brush_type == IMAGE_PAINT_BRUSH_TYPE_SOFTEN) {
    s->blurkernel = paint_new_blur_kernel(brush, false);
  }

  paint_brush_init_tex(s->brush);

  /* create painter */
  s->painter = brush_painter_2d_new(scene, paint, s->brush, mode == BrushStrokeMode::Invert);

  return s;
}

void paint_2d_redraw(const bContext *C, void *ps, bool final)
{
  ImagePaintState *s = static_cast<ImagePaintState *>(ps);

  bool had_redraw = false;
  for (int i = 0; i < s->num_tiles; i++) {
    if (s->tiles[i].need_redraw) {
      ImBuf *ibuf = BKE_image_acquire_ibuf(s->image, &s->tiles[i].iuser, nullptr);

      imapaint_image_update(s->sima, s->image, ibuf, &s->tiles[i].iuser, false);

      BKE_image_release_ibuf(s->image, ibuf, nullptr);

      s->tiles[i].need_redraw = false;
      had_redraw = true;
    }
  }

  if (had_redraw) {
    ED_imapaint_clear_partial_redraw();
    if (s->sima == nullptr || !s->sima->lock) {
      ED_region_tag_redraw(CTX_wm_region(C));
    }
    else {
      WM_event_add_notifier(C, NC_IMAGE | NA_PAINTING, s->image);
    }
  }

  if (final) {
    if (s->image && !(s->sima && s->sima->lock)) {
      BKE_image_free_gputextures(s->image);
    }

    /* compositor listener deals with updating */
    WM_event_add_notifier(C, NC_IMAGE | NA_EDITED, s->image);
    DEG_id_tag_update(&s->image->id, 0);

    /* Ideally, we shouldn't have to tag the object as needing to be recalculated if using this
     * paint mode, however, because the image isn't connected as part of the shader nodes, the draw
     * code is unaware of the corresponding image tag. See #150957 for more details. */
    const Scene *scene = CTX_data_scene(C);
    Object *object = CTX_data_active_object(C);
    if (object && object->type == OB_MESH && scene &&
        scene->toolsettings->imapaint.mode == IMAGEPAINT_MODE_IMAGE)
    {
      DEG_id_tag_update(&object->id, ID_RECALC_SHADING);
    }
  }
}

void paint_2d_stroke_done(void *ps)
{
  ImagePaintState *s = static_cast<ImagePaintState *>(ps);

  paint_2d_canvas_free(s);
  for (int i = 0; i < s->num_tiles; i++) {
    brush_painter_cache_2d_free(&s->tiles[i].cache);
  }
  MEM_delete(s->painter);
  MEM_delete(s->tiles);
  paint_brush_exit_tex(s->brush);

  MEM_delete(s);
}

static void paint_2d_fill_add_pixel_byte(const int x_px,
                                         const int y_px,
                                         ImBuf *ibuf,
                                         BLI_Stack *stack,
                                         BLI_bitmap *touched,
                                         const float color[4],
                                         float threshold_sq)
{
  size_t coordinate;

  if (x_px >= ibuf->x || x_px < 0 || y_px >= ibuf->y || y_px < 0) {
    return;
  }

  coordinate = size_t(y_px) * ibuf->x + x_px;

  if (!BLI_BITMAP_TEST(touched, coordinate)) {
    float color_f[4];
    const uchar *color_b = ibuf->byte_data() + 4 * coordinate;
    rgba_uchar_to_float(color_f, color_b);
    straight_to_premul_v4(color_f);

    if (len_squared_v4v4(color_f, color) <= threshold_sq) {
      BLI_stack_push(stack, &coordinate);
    }
    BLI_BITMAP_SET(touched, coordinate, true);
  }
}

static void paint_2d_fill_add_pixel_float(const int x_px,
                                          const int y_px,
                                          ImBuf *ibuf,
                                          BLI_Stack *stack,
                                          BLI_bitmap *touched,
                                          const float color[4],
                                          float threshold_sq)
{
  size_t coordinate;

  if (x_px >= ibuf->x || x_px < 0 || y_px >= ibuf->y || y_px < 0) {
    return;
  }

  coordinate = size_t(y_px) * ibuf->x + x_px;

  if (!BLI_BITMAP_TEST(touched, coordinate)) {
    if (len_squared_v4v4(ibuf->float_data() + 4 * coordinate, color) <= threshold_sq) {
      BLI_stack_push(stack, &coordinate);
    }
    BLI_BITMAP_SET(touched, coordinate, true);
  }
}

static ImageUser *paint_2d_get_tile_iuser(ImagePaintState *s, int tile_number)
{
  ImageUser *iuser = &s->tiles[0].iuser;
  for (int i = 0; i < s->num_tiles; i++) {
    if (s->tiles[i].iuser.tile == tile_number) {
      if (!paint_2d_ensure_tile_canvas(s, i)) {
        return nullptr;
      }
      iuser = &s->tiles[i].iuser;
      break;
    }
  }

  return iuser;
}

void paint_2d_bucket_fill(const bContext *C,
                          const float color[3],
                          Brush *br,
                          const float mouse_init[2],
                          const float mouse_final[2],
                          void *ps)
{
  SpaceImage *sima = CTX_wm_space_image(C);
  Paint *paint = BKE_paint_get_active_from_context(C);
  Image *ima = sima->image;

  ImagePaintState *s = static_cast<ImagePaintState *>(ps);

  ImBuf *ibuf;
  int x_px, y_px;
  uint color_b;
  float color_f[4];
  float strength = (s && br) ? BKE_brush_alpha_get(paint, br) : 1.0f;

  bool do_float;

  if (!ima) {
    return;
  }

  View2D *v2d = s ? s->v2d : &CTX_wm_region(C)->v2d;
  float uv_origin[2];
  float image_init[2];
  paint_2d_transform_mouse(v2d, mouse_init, image_init);

  int tile_number = BKE_image_get_tile_from_pos(ima, image_init, image_init, uv_origin);

  ImageUser local_iuser, *iuser;
  if (s != nullptr) {
    iuser = paint_2d_get_tile_iuser(s, tile_number);
    if (iuser == nullptr) {
      return;
    }
  }
  else {
    iuser = &local_iuser;
    BKE_imageuser_default(iuser);
    iuser->tile = tile_number;
  }

  ibuf = BKE_image_acquire_ibuf(ima, iuser, nullptr);
  if (!ibuf) {
    return;
  }

  do_float = (ibuf->float_data() != nullptr);
  /* First check if our image is float. If it is we should correct the color to be in linear space.
   */
  if (!do_float) {
    float3 ibuf_color = color;
    IMB_colormanagement_scene_linear_to_colorspace_v3(ibuf_color, ibuf->byte_buffer.colorspace);
    rgb_float_to_uchar(reinterpret_cast<uchar *>(&color_b), ibuf_color);
    *((reinterpret_cast<char *>(&color_b)) + 3) = strength * 255;
  }
  else {
    copy_v3_v3(color_f, color);
    color_f[3] = strength;
  }

  if (!mouse_final || !br) {
    /* first case, no image UV, fill the whole image */
    ED_imapaint_dirty_region(ima, ibuf, iuser, 0, 0, ibuf->x, ibuf->y, false);

    if (do_float) {
      float *float_data = ibuf->float_data_for_write();
      for (x_px = 0; x_px < ibuf->x; x_px++) {
        for (y_px = 0; y_px < ibuf->y; y_px++) {
          blend_color_mix_float(float_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                                float_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                                color_f);
        }
      }
    }
    else {
      uchar *byte_data = ibuf->byte_data_for_write();
      for (x_px = 0; x_px < ibuf->x; x_px++) {
        for (y_px = 0; y_px < ibuf->y; y_px++) {
          blend_color_mix_byte(byte_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                               byte_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                               reinterpret_cast<uchar *>(&color_b));
        }
      }
    }
  }
  else {
    /* second case, start sweeping the neighboring pixels, looking for pixels whose
     * value is within the brush fill threshold from the fill color */
    BLI_Stack *stack;
    BLI_bitmap *touched;
    size_t coordinate;
    int width = ibuf->x;
    float pixel_color[4];
    /* We are comparing to sum of three squared values
     * (assumed in range [0,1]), so need to multiply... */
    float threshold_sq = br->fill_threshold * br->fill_threshold * 3;

    x_px = image_init[0] * ibuf->x;
    y_px = image_init[1] * ibuf->y;

    if (x_px >= ibuf->x || x_px < 0 || y_px > ibuf->y || y_px < 0) {
      BKE_image_release_ibuf(ima, ibuf, nullptr);
      return;
    }

    /* change image invalidation method later */
    ED_imapaint_dirty_region(ima, ibuf, iuser, 0, 0, ibuf->x, ibuf->y, false);

    stack = BLI_stack_new(sizeof(size_t), __func__);
    touched = BLI_BITMAP_NEW(size_t(ibuf->x) * ibuf->y, "bucket_fill_bitmap");

    coordinate = (size_t(y_px) * ibuf->x + x_px);

    if (do_float) {
      copy_v4_v4(pixel_color, ibuf->float_data() + 4 * coordinate);
    }
    else {
      const uchar *pixel_color_b = ibuf->byte_data() + 4 * coordinate;
      rgba_uchar_to_float(pixel_color, pixel_color_b);
      straight_to_premul_v4(pixel_color);
    }

    BLI_stack_push(stack, &coordinate);
    BLI_BITMAP_SET(touched, coordinate, true);

    if (do_float) {
      while (!BLI_stack_is_empty(stack)) {
        BLI_stack_pop(stack, &coordinate);

        IMB_blend_color_float(ibuf->float_data_for_write() + 4 * (coordinate),
                              ibuf->float_data_for_write() + 4 * (coordinate),
                              color_f,
                              IMB_BlendMode(br->blend));

        /* reconstruct the coordinates here */
        x_px = coordinate % width;
        y_px = coordinate / width;

        paint_2d_fill_add_pixel_float(
            x_px - 1, y_px - 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_float(
            x_px - 1, y_px, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_float(
            x_px - 1, y_px + 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_float(
            x_px, y_px + 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_float(
            x_px, y_px - 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_float(
            x_px + 1, y_px - 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_float(
            x_px + 1, y_px, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_float(
            x_px + 1, y_px + 1, ibuf, stack, touched, pixel_color, threshold_sq);
      }
    }
    else {
      while (!BLI_stack_is_empty(stack)) {
        BLI_stack_pop(stack, &coordinate);

        IMB_blend_color_byte(ibuf->byte_data_for_write() + 4 * coordinate,
                             ibuf->byte_data_for_write() + 4 * coordinate,
                             reinterpret_cast<uchar *>(&color_b),
                             IMB_BlendMode(br->blend));

        /* reconstruct the coordinates here */
        x_px = coordinate % width;
        y_px = coordinate / width;

        paint_2d_fill_add_pixel_byte(
            x_px - 1, y_px - 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_byte(
            x_px - 1, y_px, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_byte(
            x_px - 1, y_px + 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_byte(
            x_px, y_px + 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_byte(
            x_px, y_px - 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_byte(
            x_px + 1, y_px - 1, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_byte(
            x_px + 1, y_px, ibuf, stack, touched, pixel_color, threshold_sq);
        paint_2d_fill_add_pixel_byte(
            x_px + 1, y_px + 1, ibuf, stack, touched, pixel_color, threshold_sq);
      }
    }

    MEM_delete(touched);
    BLI_stack_free(stack);
  }

  imapaint_image_update(sima, ima, ibuf, iuser, false);
  ED_imapaint_clear_partial_redraw();

  BKE_image_release_ibuf(ima, ibuf, nullptr);

  WM_event_add_notifier(C, NC_IMAGE | NA_EDITED, ima);
}

void paint_2d_gradient_fill(
    const bContext *C, Brush *br, const float mouse_init[2], const float mouse_final[2], void *ps)
{
  SpaceImage *sima = CTX_wm_space_image(C);
  Image *ima = sima->image;
  ImagePaintState *s = static_cast<ImagePaintState *>(ps);

  ImBuf *ibuf;
  int x_px, y_px;
  uint color_b;
  float color_f[4];
  float image_init[2], image_final[2];
  float tangent[2];
  float line_len_sq_inv, line_len;
  const float brush_alpha = BKE_brush_alpha_get(s->paint, br);

  bool do_float;

  if (ima == nullptr) {
    return;
  }

  float uv_origin[2];
  int tile_number = BKE_image_get_tile_from_pos(ima, image_init, image_init, uv_origin);
  ImageUser *iuser = paint_2d_get_tile_iuser(s, tile_number);
  if (!iuser) {
    return;
  }

  ibuf = BKE_image_acquire_ibuf(ima, iuser, nullptr);
  if (ibuf == nullptr) {
    return;
  }

  paint_2d_transform_mouse(s->v2d, mouse_final, image_final);
  paint_2d_transform_mouse(s->v2d, mouse_init, image_init);
  sub_v2_v2(image_init, uv_origin);
  sub_v2_v2(image_final, uv_origin);

  image_final[0] *= ibuf->x;
  image_final[1] *= ibuf->y;

  image_init[0] *= ibuf->x;
  image_init[1] *= ibuf->y;

  /* some math to get needed gradient variables */
  sub_v2_v2v2(tangent, image_final, image_init);
  line_len = len_squared_v2(tangent);
  line_len_sq_inv = 1.0f / line_len;
  line_len = sqrtf(line_len);

  do_float = (ibuf->float_data() != nullptr);

  /* this will be substituted by something else when selection is available */
  ED_imapaint_dirty_region(ima, ibuf, iuser, 0, 0, ibuf->x, ibuf->y, false);

  if (do_float) {
    float *float_data = ibuf->float_data_for_write();
    for (x_px = 0; x_px < ibuf->x; x_px++) {
      for (y_px = 0; y_px < ibuf->y; y_px++) {
        float f;
        const float p[2] = {x_px - image_init[0], y_px - image_init[1]};

        switch (br->gradient_fill_mode) {
          case BRUSH_GRADIENT_LINEAR: {
            f = dot_v2v2(p, tangent) * line_len_sq_inv;
            break;
          }
          case BRUSH_GRADIENT_RADIAL:
          default: {
            f = len_v2(p) / line_len;
            break;
          }
        }
        BKE_colorband_evaluate(br->gradient, f, color_f);
        /* convert to premultiplied */
        mul_v3_fl(color_f, color_f[3]);
        color_f[3] *= brush_alpha;
        IMB_blend_color_float(float_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                              float_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                              color_f,
                              IMB_BlendMode(br->blend));
      }
    }
  }
  else {
    uchar *byte_data = ibuf->byte_data_for_write();
    for (x_px = 0; x_px < ibuf->x; x_px++) {
      for (y_px = 0; y_px < ibuf->y; y_px++) {
        float f;
        const float p[2] = {x_px - image_init[0], y_px - image_init[1]};

        switch (br->gradient_fill_mode) {
          case BRUSH_GRADIENT_LINEAR: {
            f = dot_v2v2(p, tangent) * line_len_sq_inv;
            break;
          }
          case BRUSH_GRADIENT_RADIAL:
          default: {
            f = len_v2(p) / line_len;
            break;
          }
        }

        BKE_colorband_evaluate(br->gradient, f, color_f);
        IMB_colormanagement_scene_linear_to_colorspace_v3(color_f, ibuf->byte_buffer.colorspace);
        rgba_float_to_uchar(reinterpret_cast<uchar *>(&color_b), color_f);
        (reinterpret_cast<uchar *>(&color_b))[3] *= brush_alpha;
        IMB_blend_color_byte(byte_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                             byte_data + 4 * (size_t(y_px) * ibuf->x + x_px),
                             reinterpret_cast<uchar *>(&color_b),
                             IMB_BlendMode(br->blend));
      }
    }
  }

  imapaint_image_update(sima, ima, ibuf, iuser, false);
  ED_imapaint_clear_partial_redraw();

  BKE_image_release_ibuf(ima, ibuf, nullptr);

  WM_event_add_notifier(C, NC_IMAGE | NA_EDITED, ima);
}

}  // namespace blender
