PIKApp/app/core/pikachunkiterator.c

560 lines
15 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* Copyright (C) 1995-1999 Spencer Kimball and Peter Mattis
*
* pikachunkiterator.c
* Copyright (C) 2019 Ell
*
* 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 <cairo.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>
#include "libpikamath/pikamath.h"
#include "core-types.h"
#include "pikachunkiterator.h"
/* the maximal chunk size */
#define MAX_CHUNK_WIDTH 4096
#define MAX_CHUNK_HEIGHT 4096
/* the default iteration interval */
#define DEFAULT_INTERVAL (1.0 / 15.0) /* seconds */
/* the minimal area to process per iteration */
#define MIN_AREA_PER_ITERATION 4096
/* the maximal ratio between the actual processed area and the target area,
* above which the current chunk height is readjusted, even in the middle of a
* row, to better match the target area
*/
#define MAX_AREA_RATIO 2.0
/* the width of the target-area sliding window */
#define TARGET_AREA_HISTORY_SIZE 3
struct _PikaChunkIterator
{
cairo_region_t *region;
cairo_region_t *priority_region;
GeglRectangle tile_rect;
GeglRectangle priority_rect;
gdouble interval;
cairo_region_t *current_region;
GeglRectangle current_rect;
gint current_x;
gint current_y;
gint current_height;
gint64 iteration_time;
gint64 last_time;
gint last_area;
gdouble target_area;
gdouble target_area_min;
gdouble target_area_history[TARGET_AREA_HISTORY_SIZE];
gint target_area_history_i;
gint target_area_history_n;
};
/* local function prototypes */
static void pika_chunk_iterator_set_current_rect (PikaChunkIterator *iter,
const GeglRectangle *rect);
static void pika_chunk_iterator_merge_current_rect (PikaChunkIterator *iter);
static void pika_chunk_iterator_merge (PikaChunkIterator *iter);
static gboolean pika_chunk_iterator_prepare (PikaChunkIterator *iter);
static void pika_chunk_iterator_set_target_area (PikaChunkIterator *iter,
gdouble target_area);
static gdouble pika_chunk_iterator_get_target_area (PikaChunkIterator *iter);
static void pika_chunk_iterator_reset_target_area (PikaChunkIterator *iter);
static void pika_chunk_iterator_calc_rect (PikaChunkIterator *iter,
GeglRectangle *rect,
gboolean readjust_height);
/* private functions */
static void
pika_chunk_iterator_set_current_rect (PikaChunkIterator *iter,
const GeglRectangle *rect)
{
cairo_region_subtract_rectangle (iter->current_region,
(const cairo_rectangle_int_t *) rect);
iter->current_rect = *rect;
iter->current_x = rect->x;
iter->current_y = rect->y;
iter->current_height = 0;
}
static void
pika_chunk_iterator_merge_current_rect (PikaChunkIterator *iter)
{
GeglRectangle rect;
if (gegl_rectangle_is_empty (&iter->current_rect))
return;
/* merge the remainder of the current row */
rect.x = iter->current_x;
rect.y = iter->current_y;
rect.width = iter->current_rect.x + iter->current_rect.width -
iter->current_x;
rect.height = iter->current_height;
cairo_region_union_rectangle (iter->current_region,
(const cairo_rectangle_int_t *) &rect);
/* merge the remainder of the current rect */
rect.x = iter->current_rect.x;
rect.y = iter->current_y + iter->current_height;
rect.width = iter->current_rect.width;
rect.height = iter->current_rect.y + iter->current_rect.height - rect.y;
cairo_region_union_rectangle (iter->current_region,
(const cairo_rectangle_int_t *) &rect);
/* reset the current rect and coordinates */
iter->current_rect.x = 0;
iter->current_rect.y = 0;
iter->current_rect.width = 0;
iter->current_rect.height = 0;
iter->current_x = 0;
iter->current_y = 0;
iter->current_height = 0;
}
static void
pika_chunk_iterator_merge (PikaChunkIterator *iter)
{
/* merge the current rect back to the current region */
pika_chunk_iterator_merge_current_rect (iter);
/* merge the priority region back to the global region */
if (iter->priority_region)
{
cairo_region_union (iter->region, iter->priority_region);
g_clear_pointer (&iter->priority_region, cairo_region_destroy);
iter->current_region = iter->region;
}
}
static gboolean
pika_chunk_iterator_prepare (PikaChunkIterator *iter)
{
if (iter->current_x == iter->current_rect.x + iter->current_rect.width)
{
iter->current_x = iter->current_rect.x;
iter->current_y += iter->current_height;
iter->current_height = 0;
if (iter->current_y == iter->current_rect.y + iter->current_rect.height)
{
GeglRectangle rect;
if (! iter->priority_region &&
! gegl_rectangle_is_empty (&iter->priority_rect))
{
iter->priority_region = cairo_region_copy (iter->region);
cairo_region_intersect_rectangle (
iter->priority_region,
(const cairo_rectangle_int_t *) &iter->priority_rect);
cairo_region_subtract_rectangle (
iter->region,
(const cairo_rectangle_int_t *) &iter->priority_rect);
}
if (! iter->priority_region ||
cairo_region_is_empty (iter->priority_region))
{
iter->current_region = iter->region;
}
else
{
iter->current_region = iter->priority_region;
}
if (cairo_region_is_empty (iter->current_region))
{
iter->current_rect.x = 0;
iter->current_rect.y = 0;
iter->current_rect.width = 0;
iter->current_rect.height = 0;
iter->current_x = 0;
iter->current_y = 0;
iter->current_height = 0;
return FALSE;
}
cairo_region_get_rectangle (iter->current_region, 0,
(cairo_rectangle_int_t *) &rect);
pika_chunk_iterator_set_current_rect (iter, &rect);
}
}
return TRUE;
}
static gint
compare_double (const gdouble *x,
const gdouble *y)
{
return (*x > *y) - (*x < *y);
}
static void
pika_chunk_iterator_set_target_area (PikaChunkIterator *iter,
gdouble target_area)
{
gdouble target_area_history[TARGET_AREA_HISTORY_SIZE];
iter->target_area_min = MIN (iter->target_area_min, target_area);
iter->target_area_history[iter->target_area_history_i++] = target_area;
iter->target_area_history_n = MAX (iter->target_area_history_n,
iter->target_area_history_i);
iter->target_area_history_i %= TARGET_AREA_HISTORY_SIZE;
memcpy (target_area_history, iter->target_area_history,
iter->target_area_history_n * sizeof (gdouble));
qsort (target_area_history, iter->target_area_history_n, sizeof (gdouble),
(gpointer) compare_double);
iter->target_area = target_area_history[iter->target_area_history_n / 2];
}
static gdouble
pika_chunk_iterator_get_target_area (PikaChunkIterator *iter)
{
if (iter->target_area)
return iter->target_area;
else
return iter->tile_rect.width * iter->tile_rect.height;
}
static void
pika_chunk_iterator_reset_target_area (PikaChunkIterator *iter)
{
if (iter->target_area_history_n)
{
iter->target_area = iter->target_area_min;
iter->target_area_min = MAX_CHUNK_WIDTH * MAX_CHUNK_HEIGHT;
iter->target_area_history_i = 0;
iter->target_area_history_n = 0;
}
}
static void
pika_chunk_iterator_calc_rect (PikaChunkIterator *iter,
GeglRectangle *rect,
gboolean readjust_height)
{
gdouble target_area;
gdouble aspect_ratio;
gint offset_x;
gint offset_y;
if (readjust_height)
pika_chunk_iterator_reset_target_area (iter);
target_area = pika_chunk_iterator_get_target_area (iter);
aspect_ratio = (gdouble) iter->tile_rect.height /
(gdouble) iter->tile_rect.width;
rect->x = iter->current_x;
rect->y = iter->current_y;
offset_x = rect->x - iter->tile_rect.x;
offset_y = rect->y - iter->tile_rect.y;
if (readjust_height)
{
rect->height = RINT ((offset_y + sqrt (target_area * aspect_ratio)) /
iter->tile_rect.height) *
iter->tile_rect.height -
offset_y;
if (rect->height <= 0)
rect->height += iter->tile_rect.height;
rect->height = MIN (rect->height,
iter->current_rect.y + iter->current_rect.height -
rect->y);
rect->height = MIN (rect->height, MAX_CHUNK_HEIGHT);
}
else
{
rect->height = iter->current_height;
}
rect->width = RINT ((offset_x + (gdouble) target_area /
(gdouble) rect->height) /
iter->tile_rect.width) *
iter->tile_rect.width -
offset_x;
if (rect->width <= 0)
rect->width += iter->tile_rect.width;
rect->width = MIN (rect->width,
iter->current_rect.x + iter->current_rect.width -
rect->x);
rect->width = MIN (rect->width, MAX_CHUNK_WIDTH);
}
/* public functions */
PikaChunkIterator *
pika_chunk_iterator_new (cairo_region_t *region)
{
PikaChunkIterator *iter;
g_return_val_if_fail (region != NULL, NULL);
iter = g_slice_new0 (PikaChunkIterator);
iter->region = region;
iter->current_region = region;
g_object_get (gegl_config (),
"tile-width", &iter->tile_rect.width,
"tile-height", &iter->tile_rect.height,
NULL);
iter->interval = DEFAULT_INTERVAL;
return iter;
}
void
pika_chunk_iterator_set_tile_rect (PikaChunkIterator *iter,
const GeglRectangle *rect)
{
g_return_if_fail (iter != NULL);
g_return_if_fail (rect != NULL);
g_return_if_fail (! gegl_rectangle_is_empty (rect));
iter->tile_rect = *rect;
}
void
pika_chunk_iterator_set_priority_rect (PikaChunkIterator *iter,
const GeglRectangle *rect)
{
const GeglRectangle empty_rect = {};
g_return_if_fail (iter != NULL);
if (! rect)
rect = &empty_rect;
if (! gegl_rectangle_equal (rect, &iter->priority_rect))
{
iter->priority_rect = *rect;
pika_chunk_iterator_merge (iter);
}
}
void
pika_chunk_iterator_set_interval (PikaChunkIterator *iter,
gdouble interval)
{
g_return_if_fail (iter != NULL);
interval = MAX (interval, 0.0);
if (interval != iter->interval)
{
if (iter->interval)
{
gdouble ratio = interval / iter->interval;
gint i;
iter->target_area *= ratio;
for (i = 0; i < TARGET_AREA_HISTORY_SIZE; i++)
iter->target_area_history[i] *= ratio;
}
iter->interval = interval;
}
}
gboolean
pika_chunk_iterator_next (PikaChunkIterator *iter)
{
g_return_val_if_fail (iter != NULL, FALSE);
if (! pika_chunk_iterator_prepare (iter))
{
pika_chunk_iterator_stop (iter, TRUE);
return FALSE;
}
iter->iteration_time = g_get_monotonic_time ();
iter->last_time = iter->iteration_time;
iter->last_area = 0;
return TRUE;
}
gboolean
pika_chunk_iterator_get_rect (PikaChunkIterator *iter,
GeglRectangle *rect)
{
gint64 time;
g_return_val_if_fail (iter != NULL, FALSE);
g_return_val_if_fail (rect != NULL, FALSE);
if (! pika_chunk_iterator_prepare (iter))
return FALSE;
time = g_get_monotonic_time ();
if (iter->last_area >= MIN_AREA_PER_ITERATION)
{
gdouble interval;
interval = (gdouble) (time - iter->last_time) / G_TIME_SPAN_SECOND;
pika_chunk_iterator_set_target_area (
iter,
iter->last_area * iter->interval / interval);
interval = (gdouble) (time - iter->iteration_time) / G_TIME_SPAN_SECOND;
if (interval > iter->interval)
return FALSE;
}
if (iter->current_x == iter->current_rect.x)
{
pika_chunk_iterator_calc_rect (iter, rect, TRUE);
}
else
{
pika_chunk_iterator_calc_rect (iter, rect, FALSE);
if (rect->width * rect->height >=
MAX_AREA_RATIO * pika_chunk_iterator_get_target_area (iter))
{
GeglRectangle old_rect = *rect;
pika_chunk_iterator_calc_rect (iter, rect, TRUE);
if (rect->height >= old_rect.height)
*rect = old_rect;
}
}
if (rect->height != iter->current_height)
{
/* if the chunk height changed in the middle of a row, merge the
* remaining area back into the current region, and reset the current
* area to the remainder of the row, using the new chunk height
*/
if (rect->x != iter->current_rect.x)
{
GeglRectangle rem;
rem.x = rect->x;
rem.y = rect->y;
rem.width = iter->current_rect.x + iter->current_rect.width -
rect->x;
rem.height = rect->height;
pika_chunk_iterator_merge_current_rect (iter);
pika_chunk_iterator_set_current_rect (iter, &rem);
}
iter->current_height = rect->height;
}
iter->current_x += rect->width;
iter->last_time = time;
iter->last_area = rect->width * rect->height;
return TRUE;
}
cairo_region_t *
pika_chunk_iterator_stop (PikaChunkIterator *iter,
gboolean free_region)
{
cairo_region_t *result = NULL;
g_return_val_if_fail (iter != NULL, NULL);
if (free_region)
{
cairo_region_destroy (iter->region);
}
else
{
pika_chunk_iterator_merge (iter);
result = iter->region;
}
g_clear_pointer (&iter->priority_region, cairo_region_destroy);
g_slice_free (PikaChunkIterator, iter);
return result;
}