summaryrefslogtreecommitdiffstats
path: root/app/core/gimpprojection.c
diff options
context:
space:
mode:
Diffstat (limited to 'app/core/gimpprojection.c')
-rw-r--r--app/core/gimpprojection.c1132
1 files changed, 1132 insertions, 0 deletions
diff --git a/app/core/gimpprojection.c b/app/core/gimpprojection.c
new file mode 100644
index 0000000..c249b55
--- /dev/null
+++ b/app/core/gimpprojection.c
@@ -0,0 +1,1132 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <cairo.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gegl.h>
+
+#include "libgimpbase/gimpbase.h"
+#include "libgimpmath/gimpmath.h"
+
+#include "core-types.h"
+
+#include "gegl/gimp-babl.h"
+#include "gegl/gimp-gegl-loops.h"
+#include "gegl/gimp-gegl-utils.h"
+
+#include "gimp.h"
+#include "gimp-memsize.h"
+#include "gimpchunkiterator.h"
+#include "gimpimage.h"
+#include "gimpmarshal.h"
+#include "gimppickable.h"
+#include "gimpprojectable.h"
+#include "gimpprojection.h"
+#include "gimptilehandlerprojectable.h"
+
+#include "gimp-log.h"
+#include "gimp-priorities.h"
+
+
+/* chunk size for area updates */
+#define GIMP_PROJECTION_UPDATE_CHUNK_WIDTH 32
+#define GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT 32
+
+
+enum
+{
+ UPDATE,
+ LAST_SIGNAL
+};
+
+enum
+{
+ PROP_0,
+ PROP_BUFFER
+};
+
+
+struct _GimpProjectionPrivate
+{
+ GimpProjectable *projectable;
+
+ GeglBuffer *buffer;
+ GimpTileHandlerValidate *validate_handler;
+
+ gint priority;
+
+ cairo_region_t *update_region;
+ GeglRectangle priority_rect;
+ GimpChunkIterator *iter;
+ guint idle_id;
+
+ gboolean invalidate_preview;
+};
+
+
+/* local function prototypes */
+
+static void gimp_projection_pickable_iface_init (GimpPickableInterface *iface);
+
+static void gimp_projection_finalize (GObject *object);
+static void gimp_projection_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+static void gimp_projection_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec);
+
+static gint64 gimp_projection_get_memsize (GimpObject *object,
+ gint64 *gui_size);
+
+static void gimp_projection_pickable_flush (GimpPickable *pickable);
+static GimpImage * gimp_projection_get_image (GimpPickable *pickable);
+static const Babl * gimp_projection_get_format (GimpPickable *pickable);
+static GeglBuffer * gimp_projection_get_buffer (GimpPickable *pickable);
+static gboolean gimp_projection_get_pixel_at (GimpPickable *pickable,
+ gint x,
+ gint y,
+ const Babl *format,
+ gpointer pixel);
+static gdouble gimp_projection_get_opacity_at (GimpPickable *pickable,
+ gint x,
+ gint y);
+static void gimp_projection_get_pixel_average (GimpPickable *pickable,
+ const GeglRectangle *rect,
+ const Babl *format,
+ gpointer pixel);
+static void gimp_projection_pixel_to_srgb (GimpPickable *pickable,
+ const Babl *format,
+ gpointer pixel,
+ GimpRGB *color);
+static void gimp_projection_srgb_to_pixel (GimpPickable *pickable,
+ const GimpRGB *color,
+ const Babl *format,
+ gpointer pixel);
+
+static void gimp_projection_allocate_buffer (GimpProjection *proj);
+static void gimp_projection_free_buffer (GimpProjection *proj);
+static void gimp_projection_add_update_area (GimpProjection *proj,
+ gint x,
+ gint y,
+ gint w,
+ gint h);
+static void gimp_projection_flush_whenever (GimpProjection *proj,
+ gboolean now,
+ gboolean direct);
+static void gimp_projection_update_priority_rect (GimpProjection *proj);
+static void gimp_projection_chunk_render_start (GimpProjection *proj);
+static void gimp_projection_chunk_render_stop (GimpProjection *proj,
+ gboolean merge);
+static gboolean gimp_projection_chunk_render_callback (GimpProjection *proj);
+static gboolean gimp_projection_chunk_render_iteration(GimpProjection *proj);
+static void gimp_projection_paint_area (GimpProjection *proj,
+ gboolean now,
+ gint x,
+ gint y,
+ gint w,
+ gint h);
+
+static void gimp_projection_projectable_invalidate(GimpProjectable *projectable,
+ gint x,
+ gint y,
+ gint w,
+ gint h,
+ GimpProjection *proj);
+static void gimp_projection_projectable_flush (GimpProjectable *projectable,
+ gboolean invalidate_preview,
+ GimpProjection *proj);
+static void
+ gimp_projection_projectable_structure_changed (GimpProjectable *projectable,
+ GimpProjection *proj);
+static void gimp_projection_projectable_bounds_changed (GimpProjectable *projectable,
+ gint old_x,
+ gint old_y,
+ GimpProjection *proj);
+
+
+G_DEFINE_TYPE_WITH_CODE (GimpProjection, gimp_projection, GIMP_TYPE_OBJECT,
+ G_ADD_PRIVATE (GimpProjection)
+ G_IMPLEMENT_INTERFACE (GIMP_TYPE_PICKABLE,
+ gimp_projection_pickable_iface_init))
+
+#define parent_class gimp_projection_parent_class
+
+static guint projection_signals[LAST_SIGNAL] = { 0 };
+
+
+static void
+gimp_projection_class_init (GimpProjectionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GimpObjectClass *gimp_object_class = GIMP_OBJECT_CLASS (klass);
+
+ projection_signals[UPDATE] =
+ g_signal_new ("update",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (GimpProjectionClass, update),
+ NULL, NULL,
+ gimp_marshal_VOID__BOOLEAN_INT_INT_INT_INT,
+ G_TYPE_NONE, 5,
+ G_TYPE_BOOLEAN,
+ G_TYPE_INT,
+ G_TYPE_INT,
+ G_TYPE_INT,
+ G_TYPE_INT);
+
+ object_class->finalize = gimp_projection_finalize;
+ object_class->set_property = gimp_projection_set_property;
+ object_class->get_property = gimp_projection_get_property;
+
+ gimp_object_class->get_memsize = gimp_projection_get_memsize;
+
+ g_object_class_override_property (object_class, PROP_BUFFER, "buffer");
+}
+
+static void
+gimp_projection_init (GimpProjection *proj)
+{
+ proj->priv = gimp_projection_get_instance_private (proj);
+}
+
+static void
+gimp_projection_pickable_iface_init (GimpPickableInterface *iface)
+{
+ iface->flush = gimp_projection_pickable_flush;
+ iface->get_image = gimp_projection_get_image;
+ iface->get_format = gimp_projection_get_format;
+ iface->get_format_with_alpha = gimp_projection_get_format; /* sic */
+ iface->get_buffer = gimp_projection_get_buffer;
+ iface->get_pixel_at = gimp_projection_get_pixel_at;
+ iface->get_opacity_at = gimp_projection_get_opacity_at;
+ iface->get_pixel_average = gimp_projection_get_pixel_average;
+ iface->pixel_to_srgb = gimp_projection_pixel_to_srgb;
+ iface->srgb_to_pixel = gimp_projection_srgb_to_pixel;
+}
+
+static void
+gimp_projection_finalize (GObject *object)
+{
+ GimpProjection *proj = GIMP_PROJECTION (object);
+
+ gimp_projection_free_buffer (proj);
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static void
+gimp_projection_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id)
+ {
+ case PROP_BUFFER:
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gimp_projection_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GimpProjection *projection = GIMP_PROJECTION (object);
+
+ switch (property_id)
+ {
+ case PROP_BUFFER:
+ g_value_set_object (value, projection->priv->buffer);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static gint64
+gimp_projection_get_memsize (GimpObject *object,
+ gint64 *gui_size)
+{
+ GimpProjection *projection = GIMP_PROJECTION (object);
+ gint64 memsize = 0;
+
+ memsize += gimp_gegl_pyramid_get_memsize (projection->priv->buffer);
+
+ return memsize + GIMP_OBJECT_CLASS (parent_class)->get_memsize (object,
+ gui_size);
+}
+
+/**
+ * gimp_projection_estimate_memsize:
+ * @type: the projectable's base type
+ * @component_type: the projectable's component type
+ * @width: projection width
+ * @height: projection height
+ *
+ * Calculates a rough estimate of the memory that is required for the
+ * projection of an image with the given @width and @height.
+ *
+ * Return value: a rough estimate of the memory requirements.
+ **/
+gint64
+gimp_projection_estimate_memsize (GimpImageBaseType type,
+ GimpComponentType component_type,
+ gint width,
+ gint height)
+{
+ const Babl *format;
+ gint64 bytes;
+
+ if (type == GIMP_INDEXED)
+ type = GIMP_RGB;
+
+ format = gimp_babl_format (type,
+ gimp_babl_precision (component_type, FALSE),
+ TRUE);
+ bytes = babl_format_get_bytes_per_pixel (format);
+
+ /* The pyramid levels constitute a geometric sum with a ratio of 1/4. */
+ return bytes * (gint64) width * (gint64) height * 1.33;
+}
+
+
+static void
+gimp_projection_pickable_flush (GimpPickable *pickable)
+{
+ GimpProjection *proj = GIMP_PROJECTION (pickable);
+
+ /* create the buffer if it doesn't exist */
+ gimp_projection_get_buffer (pickable);
+
+ gimp_projection_finish_draw (proj);
+ gimp_projection_flush_now (proj, FALSE);
+
+ if (proj->priv->invalidate_preview)
+ {
+ /* invalidate the preview here since it is constructed from
+ * the projection
+ */
+ proj->priv->invalidate_preview = FALSE;
+
+ gimp_projectable_invalidate_preview (proj->priv->projectable);
+ }
+}
+
+static GimpImage *
+gimp_projection_get_image (GimpPickable *pickable)
+{
+ GimpProjection *proj = GIMP_PROJECTION (pickable);
+
+ return gimp_projectable_get_image (proj->priv->projectable);
+}
+
+static const Babl *
+gimp_projection_get_format (GimpPickable *pickable)
+{
+ GimpProjection *proj = GIMP_PROJECTION (pickable);
+
+ return gimp_projectable_get_format (proj->priv->projectable);
+}
+
+static GeglBuffer *
+gimp_projection_get_buffer (GimpPickable *pickable)
+{
+ GimpProjection *proj = GIMP_PROJECTION (pickable);
+
+ if (! proj->priv->buffer)
+ {
+ GeglRectangle bounding_box;
+
+ bounding_box =
+ gimp_projectable_get_bounding_box (proj->priv->projectable);
+
+ gimp_projection_allocate_buffer (proj);
+
+ /* This used to call gimp_tile_handler_validate_invalidate()
+ * which forced the entire projection to be constructed in one
+ * go for new images, causing a potentially huge delay. Now we
+ * initially validate stuff the normal way, which makes the
+ * image appear incrementally, but it keeps everything
+ * responsive.
+ */
+ gimp_projection_add_update_area (proj,
+ bounding_box.x, bounding_box.y,
+ bounding_box.width, bounding_box.height);
+ proj->priv->invalidate_preview = TRUE;
+ gimp_projection_flush (proj);
+ }
+
+ return proj->priv->buffer;
+}
+
+static gboolean
+gimp_projection_get_pixel_at (GimpPickable *pickable,
+ gint x,
+ gint y,
+ const Babl *format,
+ gpointer pixel)
+{
+ GimpProjection *proj = GIMP_PROJECTION (pickable);
+ GeglBuffer *buffer = gimp_projection_get_buffer (pickable);
+ GeglRectangle bounding_box;
+
+ bounding_box = gimp_projectable_get_bounding_box (proj->priv->projectable);
+
+ if (x < bounding_box.x ||
+ y < bounding_box.y ||
+ x >= bounding_box.x + bounding_box.width ||
+ y >= bounding_box.y + bounding_box.height)
+ {
+ return FALSE;
+ }
+
+ gegl_buffer_sample (buffer, x, y, NULL, pixel, format,
+ GEGL_SAMPLER_NEAREST, GEGL_ABYSS_NONE);
+
+ return TRUE;
+}
+
+static gdouble
+gimp_projection_get_opacity_at (GimpPickable *pickable,
+ gint x,
+ gint y)
+{
+ return GIMP_OPACITY_OPAQUE;
+}
+
+static void
+gimp_projection_get_pixel_average (GimpPickable *pickable,
+ const GeglRectangle *rect,
+ const Babl *format,
+ gpointer pixel)
+{
+ GeglBuffer *buffer = gimp_projection_get_buffer (pickable);
+
+ return gimp_gegl_average_color (buffer, rect, TRUE, GEGL_ABYSS_NONE, format,
+ pixel);
+}
+
+static void
+gimp_projection_pixel_to_srgb (GimpPickable *pickable,
+ const Babl *format,
+ gpointer pixel,
+ GimpRGB *color)
+{
+ GimpProjection *proj = GIMP_PROJECTION (pickable);
+ GimpImage *image = gimp_projectable_get_image (proj->priv->projectable);
+
+ gimp_pickable_pixel_to_srgb (GIMP_PICKABLE (image), format, pixel, color);
+}
+
+static void
+gimp_projection_srgb_to_pixel (GimpPickable *pickable,
+ const GimpRGB *color,
+ const Babl *format,
+ gpointer pixel)
+{
+ GimpProjection *proj = GIMP_PROJECTION (pickable);
+ GimpImage *image = gimp_projectable_get_image (proj->priv->projectable);
+
+ gimp_pickable_srgb_to_pixel (GIMP_PICKABLE (image), color, format, pixel);
+}
+
+
+/* public functions */
+
+GimpProjection *
+gimp_projection_new (GimpProjectable *projectable)
+{
+ GimpProjection *proj;
+
+ g_return_val_if_fail (GIMP_IS_PROJECTABLE (projectable), NULL);
+
+ proj = g_object_new (GIMP_TYPE_PROJECTION, NULL);
+
+ proj->priv->projectable = projectable;
+
+ g_signal_connect_object (projectable, "invalidate",
+ G_CALLBACK (gimp_projection_projectable_invalidate),
+ proj, 0);
+ g_signal_connect_object (projectable, "flush",
+ G_CALLBACK (gimp_projection_projectable_flush),
+ proj, 0);
+ g_signal_connect_object (projectable, "structure-changed",
+ G_CALLBACK (gimp_projection_projectable_structure_changed),
+ proj, 0);
+ g_signal_connect_object (projectable, "bounds-changed",
+ G_CALLBACK (gimp_projection_projectable_bounds_changed),
+ proj, 0);
+
+ return proj;
+}
+
+void
+gimp_projection_set_priority (GimpProjection *proj,
+ gint priority)
+{
+ g_return_if_fail (GIMP_IS_PROJECTION (proj));
+
+ proj->priv->priority = priority;
+}
+
+gint
+gimp_projection_get_priority (GimpProjection *proj)
+{
+ g_return_val_if_fail (GIMP_IS_PROJECTION (proj), 0);
+
+ return proj->priv->priority;
+}
+
+void
+gimp_projection_set_priority_rect (GimpProjection *proj,
+ gint x,
+ gint y,
+ gint w,
+ gint h)
+{
+ g_return_if_fail (GIMP_IS_PROJECTION (proj));
+
+ proj->priv->priority_rect = *GEGL_RECTANGLE (x, y, w, h);
+
+ gimp_projection_update_priority_rect (proj);
+}
+
+void
+gimp_projection_stop_rendering (GimpProjection *proj)
+{
+ g_return_if_fail (GIMP_IS_PROJECTION (proj));
+
+ gimp_projection_chunk_render_stop (proj, TRUE);
+}
+
+void
+gimp_projection_flush (GimpProjection *proj)
+{
+ g_return_if_fail (GIMP_IS_PROJECTION (proj));
+
+ /* Construct in chunks */
+ gimp_projection_flush_whenever (proj, FALSE, FALSE);
+}
+
+void
+gimp_projection_flush_now (GimpProjection *proj,
+ gboolean direct)
+{
+ g_return_if_fail (GIMP_IS_PROJECTION (proj));
+
+ /* Construct NOW */
+ gimp_projection_flush_whenever (proj, TRUE, direct);
+}
+
+void
+gimp_projection_finish_draw (GimpProjection *proj)
+{
+ g_return_if_fail (GIMP_IS_PROJECTION (proj));
+
+ if (proj->priv->iter)
+ {
+ gimp_chunk_iterator_set_priority_rect (proj->priv->iter, NULL);
+
+ gimp_tile_handler_validate_begin_validate (proj->priv->validate_handler);
+
+ while (gimp_projection_chunk_render_iteration (proj));
+
+ gimp_tile_handler_validate_end_validate (proj->priv->validate_handler);
+
+ gimp_projection_chunk_render_stop (proj, FALSE);
+ }
+}
+
+
+/* private functions */
+
+static void
+gimp_projection_allocate_buffer (GimpProjection *proj)
+{
+ const Babl *format;
+ GeglRectangle bounding_box;
+
+ if (proj->priv->buffer)
+ return;
+
+ format = gimp_projection_get_format (GIMP_PICKABLE (proj));
+ bounding_box =
+ gimp_projectable_get_bounding_box (proj->priv->projectable);
+
+ proj->priv->buffer = gegl_buffer_new (&bounding_box, format);
+
+ proj->priv->validate_handler =
+ GIMP_TILE_HANDLER_VALIDATE (
+ gimp_tile_handler_projectable_new (proj->priv->projectable));
+
+ gimp_tile_handler_validate_assign (proj->priv->validate_handler,
+ proj->priv->buffer);
+
+ g_object_notify (G_OBJECT (proj), "buffer");
+}
+
+static void
+gimp_projection_free_buffer (GimpProjection *proj)
+{
+ gimp_projection_chunk_render_stop (proj, FALSE);
+
+ g_clear_pointer (&proj->priv->update_region, cairo_region_destroy);
+
+ if (proj->priv->buffer)
+ {
+ gimp_tile_handler_validate_unassign (proj->priv->validate_handler,
+ proj->priv->buffer);
+
+ g_clear_object (&proj->priv->buffer);
+ g_clear_object (&proj->priv->validate_handler);
+
+ g_object_notify (G_OBJECT (proj), "buffer");
+ }
+}
+
+static void
+gimp_projection_add_update_area (GimpProjection *proj,
+ gint x,
+ gint y,
+ gint w,
+ gint h)
+{
+ cairo_rectangle_int_t rect;
+ GeglRectangle bounding_box;
+
+ bounding_box = gimp_projectable_get_bounding_box (proj->priv->projectable);
+
+ /* align the rectangle to the UPDATE_CHUNK_WIDTH x UPDATE_CHUNK_HEIGHT grid,
+ * to decrease the complexity of the update area.
+ */
+ w = ceil ((gdouble) (x + w) / GIMP_PROJECTION_UPDATE_CHUNK_WIDTH ) * GIMP_PROJECTION_UPDATE_CHUNK_WIDTH;
+ h = ceil ((gdouble) (y + h) / GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT) * GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT;
+ x = floor ((gdouble) x / GIMP_PROJECTION_UPDATE_CHUNK_WIDTH ) * GIMP_PROJECTION_UPDATE_CHUNK_WIDTH;
+ y = floor ((gdouble) y / GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT) * GIMP_PROJECTION_UPDATE_CHUNK_HEIGHT;
+
+ w -= x;
+ h -= y;
+
+ if (gegl_rectangle_intersect ((GeglRectangle *) &rect,
+ GEGL_RECTANGLE (x, y, w, h), &bounding_box))
+ {
+ if (proj->priv->update_region)
+ cairo_region_union_rectangle (proj->priv->update_region, &rect);
+ else
+ proj->priv->update_region = cairo_region_create_rectangle (&rect);
+ }
+}
+
+static void
+gimp_projection_flush_whenever (GimpProjection *proj,
+ gboolean now,
+ gboolean direct)
+{
+ if (proj->priv->update_region)
+ {
+ /* Make sure we have a buffer */
+ gimp_projection_allocate_buffer (proj);
+
+ if (now) /* Synchronous */
+ {
+ gint n_rects = cairo_region_num_rectangles (proj->priv->update_region);
+ gint i;
+
+ for (i = 0; i < n_rects; i++)
+ {
+ cairo_rectangle_int_t rect;
+
+ cairo_region_get_rectangle (proj->priv->update_region,
+ i, &rect);
+
+ gimp_projection_paint_area (proj,
+ direct,
+ rect.x,
+ rect.y,
+ rect.width,
+ rect.height);
+ }
+
+ /* Free the update region */
+ g_clear_pointer (&proj->priv->update_region, cairo_region_destroy);
+ }
+ else /* Asynchronous */
+ {
+ /* Consumes the update region */
+ gimp_projection_chunk_render_start (proj);
+ }
+ }
+ else if (! now && ! proj->priv->iter && proj->priv->invalidate_preview)
+ {
+ /* invalidate the preview here since it is constructed from
+ * the projection
+ */
+ proj->priv->invalidate_preview = FALSE;
+
+ gimp_projectable_invalidate_preview (proj->priv->projectable);
+ }
+}
+
+static void
+gimp_projection_update_priority_rect (GimpProjection *proj)
+{
+ if (proj->priv->iter)
+ {
+ GeglRectangle rect;
+ GeglRectangle bounding_box;
+ gint off_x, off_y;
+
+ rect = proj->priv->priority_rect;
+
+ gimp_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);
+ bounding_box = gimp_projectable_get_bounding_box (proj->priv->projectable);
+
+ /* subtract the projectable's offsets because the list of update
+ * areas is in tile-pyramid coordinates, but our external API is
+ * always in terms of image coordinates.
+ */
+ rect.x -= off_x;
+ rect.y -= off_y;
+
+ gegl_rectangle_intersect (&rect, &rect, &bounding_box);
+
+ gimp_chunk_iterator_set_priority_rect (proj->priv->iter, &rect);
+ }
+}
+
+static void
+gimp_projection_chunk_render_start (GimpProjection *proj)
+{
+ cairo_region_t *region = proj->priv->update_region;
+ gboolean invalidate_preview = FALSE;
+
+ if (proj->priv->iter)
+ {
+ region = gimp_chunk_iterator_stop (proj->priv->iter, FALSE);
+
+ proj->priv->iter = NULL;
+
+ if (cairo_region_is_empty (region))
+ invalidate_preview = proj->priv->invalidate_preview;
+
+ if (proj->priv->update_region)
+ {
+ cairo_region_union (region, proj->priv->update_region);
+
+ cairo_region_destroy (proj->priv->update_region);
+ }
+ }
+
+ proj->priv->update_region = NULL;
+
+ if (region && ! cairo_region_is_empty (region))
+ {
+ proj->priv->iter = gimp_chunk_iterator_new (region);
+
+ gimp_projection_update_priority_rect (proj);
+
+ if (! proj->priv->idle_id)
+ {
+ proj->priv->idle_id = g_idle_add_full (
+ GIMP_PRIORITY_PROJECTION_IDLE + proj->priv->priority,
+ (GSourceFunc) gimp_projection_chunk_render_callback,
+ proj, NULL);
+ }
+ }
+ else
+ {
+ if (region)
+ cairo_region_destroy (region);
+
+ if (proj->priv->idle_id)
+ {
+ g_source_remove (proj->priv->idle_id);
+ proj->priv->idle_id = 0;
+ }
+
+ if (invalidate_preview)
+ {
+ /* invalidate the preview here since it is constructed from
+ * the projection
+ */
+ proj->priv->invalidate_preview = FALSE;
+
+ gimp_projectable_invalidate_preview (proj->priv->projectable);
+ }
+ }
+}
+
+static void
+gimp_projection_chunk_render_stop (GimpProjection *proj,
+ gboolean merge)
+{
+ if (proj->priv->idle_id)
+ {
+ g_source_remove (proj->priv->idle_id);
+ proj->priv->idle_id = 0;
+ }
+
+ if (proj->priv->iter)
+ {
+ if (merge)
+ {
+ cairo_region_t *region;
+
+ region = gimp_chunk_iterator_stop (proj->priv->iter, FALSE);
+
+ if (proj->priv->update_region)
+ {
+ cairo_region_union (proj->priv->update_region, region);
+
+ cairo_region_destroy (region);
+ }
+ else
+ {
+ proj->priv->update_region = region;
+ }
+ }
+ else
+ {
+ gimp_chunk_iterator_stop (proj->priv->iter, TRUE);
+ }
+
+ proj->priv->iter = NULL;
+ }
+}
+
+static gboolean
+gimp_projection_chunk_render_callback (GimpProjection *proj)
+{
+ if (gimp_projection_chunk_render_iteration (proj))
+ {
+ return G_SOURCE_CONTINUE;
+ }
+ else
+ {
+ proj->priv->idle_id = 0;
+
+ return G_SOURCE_REMOVE;
+ }
+}
+
+static gboolean
+gimp_projection_chunk_render_iteration (GimpProjection *proj)
+{
+ if (gimp_chunk_iterator_next (proj->priv->iter))
+ {
+ GeglRectangle rect;
+
+ gimp_tile_handler_validate_begin_validate (proj->priv->validate_handler);
+
+ while (gimp_chunk_iterator_get_rect (proj->priv->iter, &rect))
+ {
+ gimp_projection_paint_area (proj, TRUE,
+ rect.x, rect.y, rect.width, rect.height);
+ }
+
+ gimp_tile_handler_validate_end_validate (proj->priv->validate_handler);
+
+ /* Still work to do. */
+ return TRUE;
+ }
+ else
+ {
+ proj->priv->iter = NULL;
+
+ if (proj->priv->invalidate_preview)
+ {
+ /* invalidate the preview here since it is constructed from
+ * the projection
+ */
+ proj->priv->invalidate_preview = FALSE;
+
+ gimp_projectable_invalidate_preview (proj->priv->projectable);
+ }
+
+ /* FINISHED */
+ return FALSE;
+ }
+}
+
+static void
+gimp_projection_paint_area (GimpProjection *proj,
+ gboolean now,
+ gint x,
+ gint y,
+ gint w,
+ gint h)
+{
+ gint off_x, off_y;
+ GeglRectangle bounding_box;
+ GeglRectangle rect;
+
+ gimp_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);
+ bounding_box = gimp_projectable_get_bounding_box (proj->priv->projectable);
+
+ if (gegl_rectangle_intersect (&rect,
+ GEGL_RECTANGLE (x, y, w, h), &bounding_box))
+ {
+ if (now)
+ {
+ gimp_tile_handler_validate_validate (
+ proj->priv->validate_handler,
+ proj->priv->buffer,
+ &rect,
+ FALSE, FALSE);
+ }
+ else
+ {
+ gimp_tile_handler_validate_invalidate (
+ proj->priv->validate_handler,
+ &rect);
+ }
+
+ /* add the projectable's offsets because the list of update areas
+ * is in tile-pyramid coordinates, but our external API is always
+ * in terms of image coordinates.
+ */
+ g_signal_emit (proj, projection_signals[UPDATE], 0,
+ now,
+ rect.x + off_x,
+ rect.y + off_y,
+ rect.width,
+ rect.height);
+ }
+}
+
+
+/* image callbacks */
+
+static void
+gimp_projection_projectable_invalidate (GimpProjectable *projectable,
+ gint x,
+ gint y,
+ gint w,
+ gint h,
+ GimpProjection *proj)
+{
+ gint off_x, off_y;
+
+ gimp_projectable_get_offset (proj->priv->projectable, &off_x, &off_y);
+
+ /* subtract the projectable's offsets because the list of update
+ * areas is in tile-pyramid coordinates, but our external API is
+ * always in terms of image coordinates.
+ */
+ x -= off_x;
+ y -= off_y;
+
+ gimp_projection_add_update_area (proj, x, y, w, h);
+}
+
+static void
+gimp_projection_projectable_flush (GimpProjectable *projectable,
+ gboolean invalidate_preview,
+ GimpProjection *proj)
+{
+ if (invalidate_preview)
+ proj->priv->invalidate_preview = TRUE;
+
+ gimp_projection_flush (proj);
+}
+
+static void
+gimp_projection_projectable_structure_changed (GimpProjectable *projectable,
+ GimpProjection *proj)
+{
+ GeglRectangle bounding_box;
+
+ gimp_projection_free_buffer (proj);
+
+ bounding_box = gimp_projectable_get_bounding_box (projectable);
+
+ gimp_projection_add_update_area (proj,
+ bounding_box.x, bounding_box.y,
+ bounding_box.width, bounding_box.height);
+}
+
+static void
+gimp_projection_projectable_bounds_changed (GimpProjectable *projectable,
+ gint old_x,
+ gint old_y,
+ GimpProjection *proj)
+{
+ GeglBuffer *old_buffer = proj->priv->buffer;
+ GimpTileHandlerValidate *old_validate_handler;
+ GeglRectangle old_bounding_box;
+ GeglRectangle bounding_box;
+ GeglRectangle old_bounds;
+ GeglRectangle bounds;
+ GeglRectangle int_bounds;
+ gint x, y;
+ gint dx, dy;
+
+ if (! old_buffer)
+ {
+ gimp_projection_projectable_structure_changed (projectable, proj);
+
+ return;
+ }
+
+ old_bounding_box = *gegl_buffer_get_extent (old_buffer);
+
+ gimp_projectable_get_offset (projectable, &x, &y);
+ bounding_box = gimp_projectable_get_bounding_box (projectable);
+
+ if (x == old_x && y == old_y &&
+ gegl_rectangle_equal (&bounding_box, &old_bounding_box))
+ {
+ return;
+ }
+
+ old_bounds = old_bounding_box;
+ old_bounds.x += old_x;
+ old_bounds.y += old_y;
+
+ bounds = bounding_box;
+ bounds.x += x;
+ bounds.y += y;
+
+ if (! gegl_rectangle_intersect (&int_bounds, &bounds, &old_bounds))
+ {
+ gimp_projection_projectable_structure_changed (projectable, proj);
+
+ return;
+ }
+
+ dx = x - old_x;
+ dy = y - old_y;
+
+#if 1
+ /* FIXME: when there's an offset between the new bounds and the old bounds,
+ * use gimp_projection_projectable_structure_changed(), instead of copying a
+ * shifted version of the old buffer, since the synchronous copy can take a
+ * notable amount of time for big buffers, when the offset is such that tiles
+ * are not COW-ed. while gimp_projection_projectable_structure_changed()
+ * causes the projection to be re-rendered, which is overall slower, it's
+ * done asynchronously.
+ *
+ * this needs to be improved.
+ */
+ if (dx || dy)
+ {
+ gimp_projection_projectable_structure_changed (projectable, proj);
+
+ return;
+ }
+#endif
+
+ /* reallocate the buffer, and copy the old buffer to the corresponding
+ * region of the new buffer.
+ */
+
+ gimp_projection_chunk_render_stop (proj, TRUE);
+
+ if (dx == 0 && dy == 0)
+ {
+ gimp_tile_handler_validate_buffer_set_extent (old_buffer, &bounding_box);
+ }
+ else
+ {
+ old_validate_handler = proj->priv->validate_handler;
+
+ proj->priv->buffer = NULL;
+ proj->priv->validate_handler = NULL;
+
+ gimp_projection_allocate_buffer (proj);
+
+ gimp_tile_handler_validate_buffer_copy (
+ old_buffer,
+ GEGL_RECTANGLE (int_bounds.x - old_x,
+ int_bounds.y - old_y,
+ int_bounds.width,
+ int_bounds.height),
+ proj->priv->buffer,
+ GEGL_RECTANGLE (int_bounds.x - x,
+ int_bounds.y - y,
+ int_bounds.width,
+ int_bounds.height));
+
+ gimp_tile_handler_validate_unassign (old_validate_handler,
+ old_buffer);
+
+ g_object_unref (old_validate_handler);
+ g_object_unref (old_buffer);
+ }
+
+ if (proj->priv->update_region)
+ {
+ cairo_region_translate (proj->priv->update_region, dx, dy);
+ cairo_region_intersect_rectangle (
+ proj->priv->update_region,
+ (const cairo_rectangle_int_t *) &bounding_box);
+ }
+
+ int_bounds.x -= x;
+ int_bounds.y -= y;
+
+ if (int_bounds.x > bounding_box.x)
+ {
+ gimp_projection_add_update_area (proj,
+ bounding_box.x,
+ bounding_box.y,
+ int_bounds.x - bounding_box.x,
+ bounding_box.height);
+ }
+ if (int_bounds.y > bounding_box.y)
+ {
+ gimp_projection_add_update_area (proj,
+ bounding_box.x,
+ bounding_box.y,
+ bounding_box.width,
+ int_bounds.y - bounding_box.y);
+ }
+ if (int_bounds.x + int_bounds.width < bounding_box.x + bounding_box.width)
+ {
+ gimp_projection_add_update_area (proj,
+ int_bounds.x + int_bounds.width,
+ bounding_box.y,
+ bounding_box.x + bounding_box.width -
+ (int_bounds.x + int_bounds.width),
+ bounding_box.height);
+ }
+ if (int_bounds.y + int_bounds.height < bounding_box.y + bounding_box.height)
+ {
+ gimp_projection_add_update_area (proj,
+ bounding_box.x,
+ int_bounds.y + int_bounds.height,
+ bounding_box.width,
+ bounding_box.y + bounding_box.height -
+ (int_bounds.y + int_bounds.height));
+ }
+
+ proj->priv->invalidate_preview = TRUE;
+}