/* asynchronous screen sink
 *
 * 1/1/10
 * 	- from im_render.c
 * 25/11/10
 * 	- in synchronous mode, use a single region for input and save huge
 * 	  mem use
 * 20/1/14
 * 	- bg render thread quits on shutdown
 * 1/12/15
 * 	- don't do anything to out or mask after they have closed
 * 	- only run the bg render thread when there's work to do
 */

/*

	This file is part of VIPS.

	VIPS is free software; you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation; either version 2 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 Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
	02110-1301  USA

 */

/*

	These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

 */

/* Verbose debugging output.
#define VIPS_DEBUG
 */

/* Trace allocate/free.
#define VIPS_DEBUG_AMBER
 */

/* Trace reschedule
#define VIPS_DEBUG_GREEN
 */

/* Trace serious problems.
#define VIPS_DEBUG_RED
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <glib/gi18n-lib.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif /*HAVE_UNISTD_H*/

#include <vips/vips.h>
#include <vips/internal.h>
#include <vips/debug.h>

#ifdef VIPS_DEBUG_AMBER
static int render_num_renders = 0;
#endif /*VIPS_DEBUG_AMBER*/

/* A tile in our cache.
 */
typedef struct {
	struct _Render *render;

	VipsRect area;		/* Place here (unclipped) */
	VipsRegion *region; /* VipsRegion with the pixels */

	/* The tile contains calculated pixels. Though the region may have been
	 * invalidated behind our backs: we have to check that too.
	 */
	gboolean painted;

	/* The tile is on the dirty list. This saves us having to search the
	 * dirty list all the time.
	 */
	gboolean dirty;

	/* Time of last use, for LRU flush
	 */
	int ticks;
} Tile;

/* Per-call state.
 */
typedef struct _Render {
	/* Reference count this, since we use these things from several
	 * threads. We can't easily use the gobject ref count system since we
	 * need a lock around operations.
	 */
#if GLIB_CHECK_VERSION(2, 58, 0)
	gatomicrefcount ref_count;
#else
	int ref_count;
	GMutex ref_count_lock;
#endif

	/* Parameters.
	 */
	VipsImage *in;			/* Image we render */
	VipsImage *out;			/* Write tiles here on demand */
	VipsImage *mask;		/* Set valid pixels here */
	int tile_width;			/* Tile size */
	int tile_height;
	int max_tiles;		   /* Maximum number of tiles */
	int priority;		   /* Larger numbers done sooner */
	VipsSinkNotify notify; /* Tell caller about paints here */
	void *a;

	/* This render has it's own threadpool and is not on the shared list.
	 *
	 * This private threadpool needs a semaphore to wait on for dirty tiles
	 * to arrive.
	 */
	gboolean private_threadpool;
	VipsSemaphore dirty_sem;

	/* Lock here before reading or modifying the tile structure.
	 */
	GMutex lock;

	/* Tile cache.
	 */
	GSList *all;			/* All our tiles */
	int ntiles;				/* Number of tiles */
	int ticks;				/* Inc. on each access ... used for LRU */

	/* List of dirty tiles. Most recent at the front.
	 */
	GSList *dirty;

	/* Hash of tiles with positions. Tiles can be dirty or painted.
	 */
	GHashTable *tiles;

	/* A shutdown flag. If ->out or ->mask close, we must no longer do
	 * anything to them until we shut down too.
	 */
	gboolean shutdown;
} Render;

/* Our per-thread state.
 */
typedef struct _RenderThreadState {
	VipsThreadState parent_object;

	/* The tile that should be calculated.
	 */
	Tile *tile;
} RenderThreadState;

typedef struct _RenderThreadStateClass {
	VipsThreadStateClass parent_class;

} RenderThreadStateClass;

G_DEFINE_TYPE(RenderThreadState, render_thread_state, VIPS_TYPE_THREAD_STATE);

/* The BG thread which sits waiting to do some calculations, and the semaphore
 * it waits on holding the number of renders with dirty tiles.
 */
static GThread *render_thread = NULL;

/* Set this to ask the render thread to quit.
 */
static gboolean render_kill = FALSE;

/* All the renders with dirty tiles, and a semaphore that the bg render thread
 * waits on.
 */
static GMutex render_dirty_lock;
static GSList *render_dirty_all = NULL;
static VipsSemaphore n_render_dirty_sem;

/* Set this to make the bg thread stop and reschedule.
 */
static gboolean render_reschedule = FALSE;

/* Set this GPrivate to link a thread back to its Render struct.
 */
static GPrivate render_worker_key;

gboolean
vips__worker_exit(void)
{
	Render *render = (Render *) g_private_get(&render_worker_key);

	return render && render->shutdown;
}

static void
render_thread_state_class_init(RenderThreadStateClass *class)
{
	VipsObjectClass *object_class = VIPS_OBJECT_CLASS(class);

	object_class->nickname = "renderthreadstate";
	object_class->description = _("per-thread state for render");
}

static void
render_thread_state_init(RenderThreadState *state)
{
	state->tile = NULL;
}

static VipsThreadState *
render_thread_state_new(VipsImage *im, void *a)
{
	return VIPS_THREAD_STATE(vips_object_new(render_thread_state_get_type(),
		vips_thread_state_set, im, a));
}

static void *
tile_free(Tile *tile, void *a, void *b)
{
	VIPS_DEBUG_MSG_AMBER("tile_free\n");

	VIPS_UNREF(tile->region);
	g_free(tile);

	return NULL;
}

static int
render_free(Render *render)
{
	VIPS_DEBUG_MSG_AMBER("render_free: %p\n", render);

#if GLIB_CHECK_VERSION(2, 58, 0)
	g_assert(g_atomic_ref_count_compare(&render->ref_count, 0));
#else
	g_assert(render->ref_count == 0);
#endif

	g_mutex_lock(&render_dirty_lock);
	if (g_slist_find(render_dirty_all, render)) {
		render_dirty_all = g_slist_remove(render_dirty_all, render);

		/* We don't need to adjust the semaphore: if it's too high,
		 * the render thread will just loop and decrement next time
		 * render_dirty_all is NULL.
		 */
	}
	g_mutex_unlock(&render_dirty_lock);

#if !GLIB_CHECK_VERSION(2, 58, 0)
	g_mutex_clear(&render->ref_count_lock);
#endif
	g_mutex_clear(&render->lock);

	if (render->private_threadpool)
		vips_semaphore_destroy(&render->dirty_sem);

	vips_slist_map2(render->all, (VipsSListMap2Fn) tile_free, NULL, NULL);
	VIPS_FREEF(g_slist_free, render->all);
	render->ntiles = 0;
	VIPS_FREEF(g_slist_free, render->dirty);
	VIPS_FREEF(g_hash_table_destroy, render->tiles);

	VIPS_UNREF(render->in);

	g_free(render);

#ifdef VIPS_DEBUG_AMBER
	render_num_renders -= 1;
	printf("%d active renders\n", render_num_renders);
#endif /*VIPS_DEBUG_AMBER*/

	return 0;
}

/* Ref and unref a Render ... free on last unref.
 */
static int
render_ref(Render *render)
{
#if GLIB_CHECK_VERSION(2, 58, 0)
	g_assert(!g_atomic_ref_count_compare(&render->ref_count, 0));
	g_atomic_ref_count_inc(&render->ref_count);
#else
	g_mutex_lock(&render->ref_count_lock);
	g_assert(render->ref_count != 0);
	render->ref_count += 1;
	g_mutex_unlock(&render->ref_count_lock);
#endif

	return 0;
}

static int
render_unref(Render *render)
{
	gboolean kill;

#if GLIB_CHECK_VERSION(2, 58, 0)
	g_assert(!g_atomic_ref_count_compare(&render->ref_count, 0));
	kill = g_atomic_ref_count_dec(&render->ref_count);
#else
	g_mutex_lock(&render->ref_count_lock);
	g_assert(render->ref_count > 0);
	render->ref_count -= 1;
	kill = render->ref_count == 0;
	g_mutex_unlock(&render->ref_count_lock);
#endif

	if (kill)
		render_free(render);

	return 0;
}

/* Get the next tile to paint off the dirty list.
 */
static Tile *
render_tile_dirty_get(Render *render)
{
	Tile *tile;

	if (!render->dirty)
		tile = NULL;
	else {
		tile = (Tile *) render->dirty->data;
		g_assert(tile->dirty);
		render->dirty = g_slist_remove(render->dirty, tile);
		tile->dirty = FALSE;
	}

	return tile;
}

/* Pick a dirty tile to reuse. We could potentially get the tile that
 * render_work() is working on in the background :-( but I don't think we'll
 * get a crash, just a mis-paint. It should be vanishingly impossible anyway.
 */
static Tile *
render_tile_dirty_reuse(Render *render)
{
	Tile *tile;

	if (!render->dirty)
		tile = NULL;
	else {
		tile = (Tile *) g_slist_last(render->dirty)->data;
		render->dirty = g_slist_remove(render->dirty, tile);
		g_assert(tile->dirty);
		tile->dirty = FALSE;

		VIPS_DEBUG_MSG("render_tile_get_dirty_reuse: reusing dirty %p\n", tile);
	}

	return tile;
}

/* Add a tile to the dirty list.
 */
static void
tile_dirty_set(Tile *tile)
{
	Render *render = tile->render;

	if (!tile->dirty) {
		g_assert(!g_slist_find(render->dirty, tile));
		render->dirty = g_slist_prepend(render->dirty, tile);
		tile->dirty = TRUE;
		tile->painted = FALSE;
	}
	else
		g_assert(g_slist_find(render->dirty, tile));
}

/* Bump a tile to the front of the dirty list, if it's there.
 */
static void
tile_dirty_bump(Tile *tile)
{
	Render *render = tile->render;

	if (tile->dirty) {
		g_assert(g_slist_find(render->dirty, tile));

		render->dirty = g_slist_remove(render->dirty, tile);
		render->dirty = g_slist_prepend(render->dirty, tile);
	}
	else
		g_assert(!g_slist_find(render->dirty, tile));
}

static int
render_allocate(VipsThreadState *state, void *a, gboolean *stop)
{
	Render *render = (Render *) a;
	RenderThreadState *rstate = (RenderThreadState *) state;

	Tile *tile;

	g_mutex_lock(&render->lock);

	if (render_reschedule ||
		!(tile = render_tile_dirty_get(render))) {
		VIPS_DEBUG_MSG_GREEN("render_allocate: stopping\n");
		*stop = TRUE;
		rstate->tile = NULL;
	}
	else
		rstate->tile = tile;

	g_mutex_unlock(&render->lock);

	return 0;
}

static int
render_work(VipsThreadState *state, void *a)
{
	Render *render = (Render *) a;
	RenderThreadState *rstate = (RenderThreadState *) state;
	Tile *tile = rstate->tile;

	g_assert(tile);

	VIPS_DEBUG_MSG("calculating tile %p %dx%d\n",
		tile, tile->area.left, tile->area.top);

	/* vips__worker_exit() uses this to find the render to check for
	 * shutdown.
	 */
	g_private_set(&render_worker_key, render);

	if (vips_region_prepare_to(state->reg, tile->region,
			&tile->area, tile->area.left, tile->area.top)) {
		VIPS_DEBUG_MSG_RED("render_work: vips_region_prepare_to() failed: %s\n",
			vips_error_buffer());
		g_private_set(&render_worker_key, NULL);
		return -1;
	}
	tile->painted = TRUE;

	if (!render->shutdown &&
		render->notify)
		render->notify(render->out, &tile->area, render->a);

	g_private_set(&render_worker_key, NULL);

	return 0;
}

static void render_dirty_put(Render *render);

/* Called from vips_shutdown().
 */
void
vips__render_shutdown(void)
{
	/* We may come here without having inited.
	 */
	if (render_thread) {
		g_mutex_lock(&render_dirty_lock);

		GThread *thread;

		thread = render_thread;
		render_reschedule = TRUE;
		render_kill = TRUE;

		g_mutex_unlock(&render_dirty_lock);

		vips_semaphore_up(&n_render_dirty_sem);

		(void) g_thread_join(thread);

		vips_semaphore_destroy(&n_render_dirty_sem);
	}
}

static int
render_dirty_sort(Render *a, Render *b, void *user_data)
{
	return b->priority - a->priority;
}

/* Add to the jobs list, if it has work to be done.
 */
static void
render_dirty_put(Render *render)
{
	if (render->private_threadpool)
		// set our private renderer going
		vips_semaphore_up(&render->dirty_sem);
	else {
		// add to the worklist for thwe global renderer
		g_mutex_lock(&render_dirty_lock);

		if (render->dirty) {
			if (!g_slist_find(render_dirty_all, render)) {
				render_dirty_all = g_slist_prepend(render_dirty_all, render);
				render_dirty_all = g_slist_sort(render_dirty_all,
					(GCompareFunc) render_dirty_sort);

				/* Tell the bg render thread we have one more dirty
				 * render on there.
				 */
				vips_semaphore_up(&n_render_dirty_sem);
			}
		}

		g_mutex_unlock(&render_dirty_lock);
	}
}

static guint
tile_hash(gconstpointer key)
{
	VipsRect *rect = (VipsRect *) key;

	int x = rect->left / rect->width;
	int y = rect->top / rect->height;

	return x << 16 ^ y;
}

static gboolean
tile_equal(gconstpointer a, gconstpointer b)
{
	VipsRect *rect1 = (VipsRect *) a;
	VipsRect *rect2 = (VipsRect *) b;

	return rect1->left == rect2->left &&
		rect1->top == rect2->top;
}

static void
render_close_cb(VipsImage *image, Render *render)
{
	VIPS_DEBUG_MSG_AMBER("render_close_cb\n");

	/* The output image or mask are closing. This render will stick
	 * around for a while, since threads can still be running, but it
	 * must no longer reference ->out or ->mask (for example, invalidating
	 * them).
	 */
	render->shutdown = TRUE;

	if (render->private_threadpool) {
		/* Nudge the bg thread (if any) for this pool.
		 */
		VIPS_DEBUG_MSG_GREEN("render_close_cb: nudge private worker\n");
		vips_semaphore_up(&render->dirty_sem);
	}
	else {
		/* If this render is being worked on, we want to jog the bg thread,
		 * make it drop it's ref and think again.
		 */
		VIPS_DEBUG_MSG_GREEN("render_close_cb: reschedule\n");
		render_reschedule = TRUE;
	}

	render_unref(render);
}

static Render *
render_new(VipsImage *in, VipsImage *out, VipsImage *mask,
	int tile_width, int tile_height,
	int max_tiles,
	int priority,
	VipsSinkNotify notify, void *a)
{
	Render *render;

	/* Don't use auto-free for render, we do our own lifetime management
	 * with _ref() and _unref().
	 */
	if (!(render = VIPS_NEW(NULL, Render)))
		return NULL;

	/* render must hold a ref to in. This is dropped in render_free().
	 */
	g_object_ref(in);

#if GLIB_CHECK_VERSION(2, 58, 0)
	g_atomic_ref_count_init(&render->ref_count);
#else
	render->ref_count = 1;
	g_mutex_init(&render->ref_count_lock);
#endif

	render->in = in;
	render->out = out;
	render->mask = mask;
	render->tile_width = tile_width;
	render->tile_height = tile_height;
	render->max_tiles = max_tiles;
	render->priority = priority;
	render->notify = notify;
	render->a = a;

	if (render->priority < 0) {
		render->private_threadpool = TRUE;
		vips_semaphore_init(&render->dirty_sem, 0, "dirty_sem");
	}

	g_mutex_init(&render->lock);

	render->all = NULL;
	render->ntiles = 0;
	render->ticks = 0;

	render->tiles = g_hash_table_new(tile_hash, tile_equal);

	render->dirty = NULL;

	render->shutdown = FALSE;

	/* Both out and mask must close before we can free the render.
	 */
	g_signal_connect(out, "close", G_CALLBACK(render_close_cb), render);

	if (mask) {
		g_signal_connect(mask, "close", G_CALLBACK(render_close_cb), render);
		render_ref(render);
	}

	VIPS_DEBUG_MSG_AMBER("render_new: %p\n", render);

#ifdef VIPS_DEBUG_AMBER
	render_num_renders += 1;
	printf("%d active renders\n", render_num_renders);
#endif /*VIPS_DEBUG_AMBER*/

	return render;
}

/* Make a Tile.
 */
static Tile *
tile_new(Render *render)
{
	Tile *tile;

	VIPS_DEBUG_MSG_AMBER("tile_new\n");

	/* Don't use auto-free: we need to make sure we free the tile after
	 * Render.
	 */
	if (!(tile = VIPS_NEW(NULL, Tile)))
		return NULL;

	tile->render = render;
	tile->area.left = 0;
	tile->area.top = 0;
	tile->area.width = render->tile_width;
	tile->area.height = render->tile_height;
	tile->region = NULL;
	tile->painted = FALSE;
	tile->dirty = FALSE;
	tile->ticks = render->ticks;

	if (!(tile->region = vips_region_new(render->in))) {
		(void) tile_free(tile, NULL, NULL);
		return NULL;
	}

	// tiles are shared between threads
	vips__region_no_ownership(tile->region);

	render->all = g_slist_prepend(render->all, tile);
	render->ntiles += 1;

	return tile;
}

/* Search the cache for a tile by position.
 */
static Tile *
render_tile_lookup(Render *render, VipsRect *area)
{
	return (Tile *) g_hash_table_lookup(render->tiles, area);
}

/* Add a new tile to the table.
 */
static void
render_tile_add(Tile *tile, VipsRect *area)
{
	Render *render = tile->render;

	g_assert(!render_tile_lookup(render, area));

	tile->area = *area;
	tile->painted = FALSE;

	/* Ignore buffer allocate errors, there's not much we could do with
	 * them.
	 */
	if (vips_region_buffer(tile->region, &tile->area))
		VIPS_DEBUG_MSG_RED("render_tile_add: buffer allocate failed\n");

	g_hash_table_insert(render->tiles, &tile->area, tile);
}

/* Move a tile to a new position.
 */
static void
render_tile_move(Tile *tile, VipsRect *area)
{
	Render *render = tile->render;

	g_assert(render_tile_lookup(render, &tile->area));

	if (tile->area.left != area->left ||
		tile->area.top != area->top) {
		g_assert(!render_tile_lookup(render, area));

		g_hash_table_remove(render->tiles, &tile->area);
		render_tile_add(tile, area);
	}
}

/* We've looked at a tile ... bump to end of LRU and front of dirty.
 */
static void
tile_touch(Tile *tile)
{
	Render *render = tile->render;

	tile->ticks = render->ticks;
	render->ticks += 1;
	tile_dirty_bump(tile);
}

/* Queue a tile for calculation.
 */
static void
tile_queue(Tile *tile, VipsRegion *reg)
{
	Render *render = tile->render;

	VIPS_DEBUG_MSG("tile_queue: adding tile %p %dx%d to dirty\n",
		tile, tile->area.left, tile->area.top);

	tile->painted = FALSE;
	tile_touch(tile);

	if (render->notify) {
		/* Add to the list of renders with dirty tiles. The bg
		 * thread will pick it up and paint it. It can be already on
		 * the dirty list.
		 */
		tile_dirty_set(tile);
		render_dirty_put(render);
	}
	else {
		/* no notify ... paint the tile ourselves
		 * synchronously. No need to notify the client since they'll
		 * never see black tiles.
		 */
		VIPS_DEBUG_MSG("tile_queue: painting tile %p %dx%d synchronously\n",
			tile, tile->area.left, tile->area.top);

		/* While we're computing, let other threads use the cache.
		 * This tile won't get pulled out from under us since it's not
		 * marked as "painted", and it's not on the dirty list.
		 */
		g_mutex_unlock(&render->lock);

		if (vips_region_prepare_to(reg, tile->region,
				&tile->area, tile->area.left, tile->area.top))
			VIPS_DEBUG_MSG_RED("tile_queue: prepare failed\n");

		g_mutex_lock(&render->lock);

		tile->painted = TRUE;
	}
}

static void
tile_test_clean_ticks(VipsRect *key, Tile *value, Tile **best)
{
	if (value->painted)
		if (!*best || value->ticks < (*best)->ticks)
			*best = value;
}

/* Pick a painted tile to reuse. Search for LRU (slow!).
 */
static Tile *
render_tile_get_painted(Render *render)
{
	Tile *tile;

	tile = NULL;
	g_hash_table_foreach(render->tiles, (GHFunc) tile_test_clean_ticks, &tile);

	if (tile)
		VIPS_DEBUG_MSG("render_tile_get_painted: reusing painted %p\n", tile);

	return tile;
}

/* Ask for an area of calculated pixels. Get from cache, request calculation,
 * or if we've no threads or no notify, calculate immediately.
 */
static Tile *
render_tile_request(Render *render, VipsRegion *reg, VipsRect *area)
{
	Tile *tile;

	VIPS_DEBUG_MSG("render_tile_request: asking for %dx%d\n",
		area->left, area->top);

	if ((tile = render_tile_lookup(render, area))) {
		/* We already have a tile at this position. If it's invalid,
		 * ask for a repaint.
		 */
		if (tile->region->invalid)
			tile_queue(tile, reg);
		else
			tile_touch(tile);
	}
	else if (render->ntiles < render->max_tiles ||
		render->max_tiles == -1) {
		/* We have fewer tiles than the max. We can just make a new
		 * tile.
		 */
		if (!(tile = tile_new(render)))
			return NULL;

		render_tile_add(tile, area);

		tile_queue(tile, reg);
	}
	else {
		/* Need to reuse a tile. Try for an old painted tile first,
		 * then if that fails, reuse a dirty tile.
		 */
		if (!(tile = render_tile_get_painted(render)) &&
			!(tile = render_tile_dirty_reuse(render))) {
			VIPS_DEBUG_MSG("render_tile_request: no tiles to reuse\n");
			return NULL;
		}

		render_tile_move(tile, area);

		tile_queue(tile, reg);
	}

	return tile;
}

/* Copy what we can from the tile into the region.
 */
static void
tile_copy(Tile *tile, VipsRegion *to)
{
	VipsRect ovlap;

	/* Find common pixels.
	 */
	vips_rect_intersectrect(&tile->area, &to->valid, &ovlap);
	g_assert(!vips_rect_isempty(&ovlap));

	/* If the tile is painted, copy over the pixels. Otherwise, fill with
	 * zero.
	 */
	if (tile->painted && !tile->region->invalid) {
		int len = VIPS_IMAGE_SIZEOF_PEL(to->im) * ovlap.width;

		int y;

		VIPS_DEBUG_MSG("tile_copy: copying calculated pixels for %p %dx%d\n",
			tile, tile->area.left, tile->area.top);

		for (y = ovlap.top; y < VIPS_RECT_BOTTOM(&ovlap); y++) {
			VipsPel *p = VIPS_REGION_ADDR(tile->region, ovlap.left, y);
			VipsPel *q = VIPS_REGION_ADDR(to, ovlap.left, y);

			memcpy(q, p, len);
		}
	}
	else {
		VIPS_DEBUG_MSG("tile_copy: zero filling for %p %dx%d\n",
			tile, tile->area.left, tile->area.top);
		vips_region_paint(to, &ovlap, 0);
	}
}

/* Loop over the output region, filling with data from cache.
 */
static int
image_fill(VipsRegion *out, void *seq, void *a, void *b, gboolean *stop)
{
	Render *render = (Render *) b;
	int tile_width = render->tile_width;
	int tile_height = render->tile_height;
	VipsRegion *reg = (VipsRegion *) seq;
	VipsRect *r = &out->valid;

	int x, y;

	/* Find top left of tiles we need.
	 */
	int xs = (r->left / tile_width) * tile_width;
	int ys = (r->top / tile_height) * tile_height;

	VIPS_DEBUG_MSG("image_fill: left = %d, top = %d, width = %d, height = %d\n",
		r->left, r->top, r->width, r->height);

	g_mutex_lock(&render->lock);

	/*

		FIXME ... if r fits inside a single tile, we could skip the
		copy.

	 */

	for (y = ys; y < VIPS_RECT_BOTTOM(r); y += tile_height)
		for (x = xs; x < VIPS_RECT_RIGHT(r); x += tile_width) {
			VipsRect area;
			Tile *tile;

			area.left = x;
			area.top = y;
			area.width = tile_width;
			area.height = tile_height;

			tile = render_tile_request(render, reg, &area);
			if (tile)
				tile_copy(tile, out);
			else
				VIPS_DEBUG_MSG_RED("image_fill: argh!\n");
		}

	g_mutex_unlock(&render->lock);

	return 0;
}

/* The mask image is 255 / 0 for the state of painted for each tile.
 */
static int
mask_fill(VipsRegion *out, void *seq, void *a, void *b, gboolean *stop)
{
	Render *render = (Render *) a;
	int tile_width = render->tile_width;
	int tile_height = render->tile_height;
	VipsRect *r = &out->valid;

	int x, y;

	/* Find top left of tiles we need.
	 */
	int xs = (r->left / tile_width) * tile_width;
	int ys = (r->top / tile_height) * tile_height;

	VIPS_DEBUG_MSG("mask_fill: left = %d, top = %d, width = %d, height = %d\n",
		r->left, r->top, r->width, r->height);

	g_mutex_lock(&render->lock);

	for (y = ys; y < VIPS_RECT_BOTTOM(r); y += tile_height)
		for (x = xs; x < VIPS_RECT_RIGHT(r); x += tile_width) {
			VipsRect area;
			Tile *tile;
			int value;

			area.left = x;
			area.top = y;
			area.width = tile_width;
			area.height = tile_height;

			tile = render_tile_lookup(render, &area);
			value = (tile &&
						tile->painted &&
						!tile->region->invalid)
				? 255
				: 0;

			/* Only mark painted tiles containing valid pixels.
			 */
			vips_region_paint(out, &area, value);
		}

	g_mutex_unlock(&render->lock);

	return 0;
}

/* Get the first render with dirty tiles.
 */
static Render *
render_dirty_get(void)
{
	Render *render;

	/* Wait for a render with dirty tiles.
	 */
	vips_semaphore_down(&n_render_dirty_sem);

	g_mutex_lock(&render_dirty_lock);

	/* Just take the head of the jobs list ... we sort when we add.
	 */
	render = NULL;
	if (render_dirty_all) {
		render = (Render *) render_dirty_all->data;

		/* Ref the render to make sure it can't die while we're
		 * working on it.
		 */
		render_ref(render);

		render_dirty_all = g_slist_remove(render_dirty_all, render);
	}

	g_mutex_unlock(&render_dirty_lock);

	return render;
}

/* Loop for the background render manager thread.
 */
static void *
render_thread_main(void *client)
{
	Render *render;

	while (!render_kill) {
		VIPS_DEBUG_MSG_GREEN("render_thread_main: threadpool start\n");

		render_reschedule = FALSE;

		if ((render = render_dirty_get())) {
			if (vips_threadpool_run(render->in,
					render_thread_state_new,
					render_allocate,
					render_work,
					NULL,
					render))
				VIPS_DEBUG_MSG_RED("render_thread_main: "
								   "threadpool_run failed\n");

			VIPS_DEBUG_MSG_GREEN("render_thread_main: threadpool return\n");

			/* Add back to the jobs list, if we need to.
			 */
			render_dirty_put(render);

			/* _get() does a ref to make sure we keep the render
			 * alive during processing ... unref before we loop.
			 * This can kill off the render.
			 */
			render_unref(render);
		}
	}

	/* We are exiting, so render_thread must now be NULL.
	 */
	render_thread = NULL;

	return NULL;
}

static int
render_allocate_private(VipsThreadState *state, void *a, gboolean *stop)
{
	Render *render = (Render *) a;
	RenderThreadState *rstate = (RenderThreadState *) state;

	/* Wait for a dirty tile to arrive on this render.
	 */
	vips_semaphore_down(&render->dirty_sem);

	/* The mask or image is closing, we must exit.
	 */
	if (render->shutdown) {
		*stop = TRUE;
		rstate->tile = NULL;
		return 0;
	}

	/* Get the dirty tile, if any.
	 */
	g_mutex_lock(&render->lock);
	rstate->tile = render_tile_dirty_get(render);
	VIPS_DEBUG_MSG_AMBER("render_allocate_private: allocated tile %p\n",
		rstate->tile);
	g_mutex_unlock(&render->lock);

	return 0;
}

/* A worker for a private render.
 */
static void
render_work_private(void *data, void *null)
{
	VIPS_DEBUG_MSG_AMBER("render_work_private: start\n");

	Render *render = (Render *) data;

	/* vips__worker_exit() uses this to find the render to check for
	 * shutdown.
	 */
	g_private_set(&render_worker_key, render);

	// this will quit on ->shutdown == TRUE
	if (vips_threadpool_run(render->in,
			render_thread_state_new,
			render_allocate_private,
			render_work,
			NULL,
			render))
		VIPS_DEBUG_MSG_RED("render_work_private: threadpool_run failed\n");

	g_private_set(&render_worker_key, NULL);

	render_unref(render);

	VIPS_DEBUG_MSG_AMBER("render_work_private: stop\n");
}

static void *
vips__sink_screen_once(void *data)
{
	g_assert(!render_thread);

	vips_semaphore_init(&n_render_dirty_sem, 0, "n_render_dirty");

	/* Don't use vips_thread_execute(), since this thread will only be
	 * ended by vips_shutdown, and that isn't always called.
	 */
	render_thread = vips_g_thread_new("sink_screen", render_thread_main, NULL);

	return NULL;
}

/**
 * vips_sink_screen: (method)
 * @in: input image
 * @out: (out): output image
 * @mask: mask image indicating valid pixels
 * @tile_width: tile width
 * @tile_height: tile height
 * @max_tiles: maximum tiles to cache
 * @priority: rendering priority
 * @notify_fn: (scope call) (closure a) (nullable): pixels are ready notification callback
 * @a: (nullable): client data for callback
 *
 * This operation renders @in in the background, making pixels available
 * on @out as they are calculated. The @notify_fn callback is run every
 * time a new set of pixels are available. Calculated pixels are kept in
 * a cache with tiles sized @tile_width by @tile_height pixels and with at
 * most @max_tiles tiles.  If @max_tiles is -1, the cache is of unlimited
 * size (up to the maximum image * size). The @mask image is a one-band
 * uchar image and has 255 for pixels which are currently in cache and 0
 * for uncalculated pixels.
 *
 * Renders with a positive priority are assumed to be large, high-priority,
 * foreground images. Although there can be many of these, only one is ever
 * active, to avoid overcommitting threads.
 *
 * Renders with a negative priority are assumed to be small, thumbnail images,
 * consisting of a single tile. Single tile images are effectively
 * single-threaded, so all these renders are evaluated together.
 *
 * Calls to [method@Region.prepare] on @out return immediately and hold
 * whatever is currently in cache for that [struct@Rect] (check @mask to see
 * which parts of the [struct@Rect] are valid). Any pixels in the [struct@Rect]
 * which are not in cache are added to a queue, and the @notify_fn
 * callback will trigger when those pixels are ready.
 *
 * The @notify_fn callback is run from one of the background threads. In the
 * callback you need to somehow send a message to the main thread that the
 * pixels are ready. In a glib-based application, this is easily done with
 * [func@GLib.idle_add].
 *
 * If @notify_fn is `NULL` then [method@Image.sink_screen] runs synchronously.
 * [method@Region.prepare] on @out will always block until the pixels have been
 * calculated.
 *
 * ::: seealso
 *     [method@Image.tilecache], [method@Region.prepare],
 *     [method@Image.sink_disc], [method@Image.sink].
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_sink_screen(VipsImage *in, VipsImage *out, VipsImage *mask,
	int tile_width, int tile_height,
	int max_tiles,
	int priority,
	VipsSinkNotify notify_fn, void *a)
{
	static GOnce once = G_ONCE_INIT;

	Render *render;

	VIPS_ONCE(&once, vips__sink_screen_once, NULL);

	if (tile_width <= 0 || tile_height <= 0 ||
		max_tiles < -1) {
		vips_error("vips_sink_screen", "%s", _("bad parameters"));
		return -1;
	}

	if (vips_image_pio_input(in) ||
		vips_image_pipelinev(out, VIPS_DEMAND_STYLE_SMALLTILE, in, NULL))
		return -1;

	if (mask) {
		if (vips_image_pipelinev(mask, VIPS_DEMAND_STYLE_SMALLTILE, in, NULL))
			return -1;

		mask->Bands = 1;
		mask->BandFmt = VIPS_FORMAT_UCHAR;
		mask->Type = VIPS_INTERPRETATION_B_W;
		mask->Coding = VIPS_CODING_NONE;
	}

	if (!(render = render_new(in, out, mask,
			  tile_width, tile_height, max_tiles, priority, notify_fn, a)))
		return -1;

	VIPS_DEBUG_MSG("vips_sink_screen: max = %d, %p\n", max_tiles, render);

	if (render->private_threadpool) {
		render_ref(render);
		vips_thread_execute("private threadpool", render_work_private, render);
	}

	if (vips_image_generate(out,
			vips_start_one, image_fill, vips_stop_one, in, render))
		return -1;
	if (mask &&
		vips_image_generate(mask, NULL, mask_fill, NULL, render, NULL))
		return -1;

	return 0;
}

int
vips__print_renders(void)
{
	int n_leaks;

	n_leaks = 0;

#ifdef VIPS_DEBUG_AMBER
	if (render_num_renders > 0) {
		printf("%d active renders\n", render_num_renders);
		n_leaks += render_num_renders;
	}
#endif /*VIPS_DEBUG_AMBER*/

	g_mutex_lock(&render_dirty_lock);

	n_leaks += g_slist_length(render_dirty_all);
	if (render_dirty_all)
		printf("dirty renders\n");

	g_mutex_unlock(&render_dirty_lock);

	return n_leaks;
}
