/* aynchronous 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 <vips/intl.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/thread.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;

	/* 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 = NULL;
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;

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 )
	vips_g_mutex_free( render->ref_count_lock );
#endif
	vips_g_mutex_free( render->lock );

	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;
#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 )
{
	int 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 );

	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() ); 
		return( -1 );
	}
	tile->painted = TRUE;

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

	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_dirty_lock ) {
		g_mutex_lock( render_dirty_lock );

		if( render_thread ) { 
			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) vips_g_thread_join( thread );
		}
		else
			g_mutex_unlock( render_dirty_lock );

		VIPS_FREEF( vips_g_mutex_free, render_dirty_lock );
		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 )
{
	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;

	render_unref( render );

	/* 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;
}

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;
	render->ref_count_lock = vips_g_mutex_new();
#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;

	render->lock = vips_g_mutex_new();

	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;
#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 );
	}

	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 
		 * sychronously. 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 teh 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 void *
vips__sink_screen_once( void *data )
{
	g_assert( !render_thread ); 
	g_assert( !render_dirty_lock ); 

	render_dirty_lock = vips_g_mutex_new();
	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) (nullable): pixels are ready notification callback
 * @a: (closure notify_fn) (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.
 *
 * Only a single sink is calculated at any one time, though many may be
 * alive. Use @priority to indicate which renders are more important:  
 * zero means normal
 * priority, negative numbers are low priority, positive numbers high
 * priority.
 *
 * Calls to vips_region_prepare() on @out return immediately and hold 
 * whatever is
 * currently in cache for that #VipsRect (check @mask to see which parts of the
 * #VipsRect are valid). Any pixels in the #VipsRect 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 g_idle_add().
 *
 * If @notify_fn is %NULL then vips_sink_screen() runs synchronously.
 * vips_region_prepare() on @out will always block until the pixels have been
 * calculated.
 *
 * See also: vips_tilecache(), vips_region_prepare(), 
 * vips_sink_disc(), vips_sink().
 *
 * Returns: 0 on sucess, -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( 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*/

	if( render_dirty_lock ) { 
		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 );
}
