#include <assert.h>
#include <math.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>		/* NULL */
#include <stdlib.h>
#include <string.h>

#include "threadwrap.h"
#include "rwlockwrap.h"
#include "semwrap.h"
#include "gen_defs.h"
#include "genwrap.h"
#include "dirwrap.h"
#include "xpbeep.h"
#include "scale.h"

#if (defined CIOLIB_IMPORTS)
 #undef CIOLIB_IMPORTS
#endif
#if (defined CIOLIB_EXPORTS)
 #undef CIOLIB_EXPORTS
#endif

#include "ciolib.h"
#include "vidmodes.h"
#include "bitmap_con.h"

#ifdef _MSC_VER
#pragma warning(disable : 4244 4267 4018)
#endif

static uint32_t palette[65536];

/* Structs */

pthread_mutex_t screenlock;
struct bitmap_screen {
	//uint32_t *screen;
	int		screenwidth;
	int		screenheight;
	int		toprow;
	int		update_pixels;
	struct rectlist *rect;
};

struct bitmap_callbacks {
	void	(*drawrect)		(struct rectlist *data);
	void	(*flush)		(void);
	pthread_mutex_t lock;
	unsigned rects;
};

/* Static globals */

static int default_font=-99;
static int current_font[4]={-99, -99, -99, -99};
static int bitmap_initialized=0;
static struct bitmap_screen screena;
static struct bitmap_screen screenb;
struct video_stats vstat;
static struct bitmap_callbacks callbacks;
static unsigned char *font[4];
static int force_redraws=0;
static int force_cursor=0;
static struct rectlist *free_rects;
static pthread_mutex_t free_rect_lock;
static bool throttled;
static struct vmem_cell *bitmap_drawn;
static int outstanding_rects;
// win32gdi requires two rects...
#define MAX_OUTSTANDING 2
static protected_int32_t videoflags;
static int real_screenwidth = 80;
static bool have_blink; // true if there's any blinking characters on the screen
static pthread_mutex_t vstat_chlock;
static pthread_mutex_t prestel_hack_lock;

struct prestel_row_state {
	bool top;
	bool bottom;
};

/* Exported globals */

rwlock_t		vstatlock;

/* Forward declarations */

static int bitmap_loadfont_locked(const char *filename);
static struct vmem_cell * set_vmem_cell(size_t x, size_t y, uint16_t cell, uint32_t fg, uint32_t bg);
static int bitmap_attr2palette_locked(uint8_t attr, uint32_t *fgp, uint32_t *bgp);
static void	cb_drawrect(struct rectlist *data);
static void request_redraw_locked(void);
static void request_redraw(void);
static void cb_flush(void);
static int check_redraw(void);
static void blinker_thread(void *data);
static __inline void both_screens(int blink, struct bitmap_screen** current, struct bitmap_screen** noncurrent);
static int update_from_vmem(int force);
static uint32_t color_value(uint32_t col);
void bitmap_drv_free_rect(struct rectlist *rect);

/**************************************************************/
/* These functions get called from the driver and ciolib only */
/**************************************************************/

// vstatlock must be held
static int bitmap_loadfont_locked(const char *filename)
{
	static char current_filename[MAX_PATH];
	unsigned int fontsize;
	int fdw;
	int fw;
	int fh;
	int i;
	FILE	*fontfile=NULL;

	if(!bitmap_initialized)
		return(0);
	if(current_font[0]==-99 || current_font[0]>(sizeof(conio_fontdata)/sizeof(struct conio_font_data_struct)-2)) {
		for(i=0; conio_fontdata[i].desc != NULL; i++) {
			if(!strcmp(conio_fontdata[i].desc, "Codepage 437 English")) {
				current_font[0]=i;
				break;
			}
		}
		if(conio_fontdata[i].desc==NULL)
			current_font[0]=0;
	}
	if(current_font[0]==-1)
		filename=current_filename;
	else if(conio_fontdata[current_font[0]].desc==NULL)
		return(0);

	for (i=1; i<sizeof(current_font)/sizeof(current_font[0]); i++) {
		if(current_font[i] == -1)
			;
		else if (current_font[i] < 0)
			current_font[i]=current_font[0];
		else if(conio_fontdata[current_font[i]].desc==NULL)
			current_font[i]=current_font[0];
	}

	fh=vstat.charheight;
	fdw = vstat.charwidth - ((vstat.flags & VIDMODES_FLAG_EXPAND) ? 1 : 0);
	fw = fdw / 8 + (fdw % 8 ? 1 : 0);

	fontsize=fw*fh*256*sizeof(unsigned char);

	for (i=0; i<sizeof(font)/sizeof(font[0]); i++) {
		if(font[i])
			FREE_AND_NULL(font[i]);
		if((font[i]=(unsigned char *)malloc(fontsize))==NULL)
			goto error_return;
	}

	if(filename != NULL) {
		if(flength(filename)!=fontsize)
			goto error_return;
		if((fontfile=fopen(filename,"rb"))==NULL)
			goto error_return;
		if(fread(font[0], 1, fontsize, fontfile)!=fontsize)
			goto error_return;
		fclose(fontfile);
		fontfile=NULL;
		current_font[0]=-1;
		if(filename != current_filename)
			SAFECOPY(current_filename,filename);
		for (i=1; i<sizeof(font)/sizeof(font[0]); i++) {
			if (current_font[i]==-1)
				memcpy(font[i], font[0], fontsize);
		}
	}
	for (i=0; i<sizeof(font)/sizeof(font[0]); i++) {
		if (current_font[i] < 0)
			continue;
		switch(fdw) {
			case 8:
				switch(vstat.charheight) {
					case 8:
						if(conio_fontdata[current_font[i]].eight_by_eight==NULL) {
							if (i==0)
								goto error_return;
							else
								FREE_AND_NULL(font[i]);
						}
						else
							memcpy(font[i], conio_fontdata[current_font[i]].eight_by_eight, fontsize);
						break;
					case 14:
						if(conio_fontdata[current_font[i]].eight_by_fourteen==NULL) {
							if (i==0)
								goto error_return;
							else
								FREE_AND_NULL(font[i]);
						}
						else
							memcpy(font[i], conio_fontdata[current_font[i]].eight_by_fourteen, fontsize);
						break;
					case 16:
						if(conio_fontdata[current_font[i]].eight_by_sixteen==NULL) {
							if (i==0)
								goto error_return;
							else
								FREE_AND_NULL(font[i]);
						}
						else
							memcpy(font[i], conio_fontdata[current_font[i]].eight_by_sixteen, fontsize);
						break;
					default:
						goto error_return;
				}
				break;
			case 12:
				switch (vstat.charheight) {
					case 20:
						if(conio_fontdata[current_font[i]].twelve_by_twenty==NULL) {
							if (i==0)
								goto error_return;
							else
								FREE_AND_NULL(font[i]);
						}
						else
							memcpy(font[i], conio_fontdata[current_font[i]].twelve_by_twenty, fontsize);
						break;
					default:
						goto error_return;
				}
				break;
			default:
				goto error_return;
		}
	}

    return(1);

error_return:
	for (i=0; i<sizeof(current_font)/sizeof(current_font[0]); i++)
		current_font[i] = 0;
	for (i=1; i<sizeof(font)/sizeof(font[0]); i++)
		FREE_AND_NULL(font[i]);
	if (font[0] == NULL)
		font[0] = (unsigned char *)malloc(fontsize);
	if (font[0] != NULL) {
		const char *fallback = NULL;
		switch(vstat.charheight) {
			case 8:
				fallback = conio_fontdata[0].eight_by_eight;
				break;
			case 14:
				fallback = conio_fontdata[0].eight_by_fourteen;
				break;
			case 16:
				fallback = conio_fontdata[0].eight_by_sixteen;
				break;
			case 20:
				fallback = conio_fontdata[0].twelve_by_twenty;
				break;
		}
		if (fallback)
			memcpy(font[0], fallback, fontsize);
		else
			FREE_AND_NULL(font[0]);
	}
	if(fontfile)
		fclose(fontfile);
	return(0);
}

/***************************************************/
/* These functions get called from the driver only */
/***************************************************/

/***********************************************/
/* These functions get called from ciolib only */
/***********************************************/

static int
bitmap_vmem_puttext_locked(int sx, int sy, int ex, int ey, struct vmem_cell *fill)
{
	int x,y;
	struct vmem_cell *vc;
	struct vmem_cell *fi = fill;
	bool fullredraw = false;

	if(!bitmap_initialized)
		return(0);

	if(		   sx < 1
			|| sy < 1
			|| ex < 1
			|| ey < 1
			|| sx > ex
			|| sy > ey
			|| ex > vstat.cols
			|| ey > vstat.rows
			|| fill==NULL) {
		return(0);
	}

	assert_pthread_mutex_lock(&vstat_chlock);
	vstat.vmem->changed = true;
	assert_pthread_mutex_unlock(&vstat_chlock);
	for(y=sy-1;y<ey;y++) {
		vc = vmem_cell_ptr(vstat.vmem, sx - 1, y);
		for(x=sx-1;x<ex;x++) {
			if (vstat.mode == PRESTEL_40X25 && ((vc->bg & CIOLIB_BG_PRESTEL) || (fi->bg & CIOLIB_BG_PRESTEL))) {
				if ((vc->bg & CIOLIB_BG_DOUBLE_HEIGHT) != (fi->bg & CIOLIB_BG_DOUBLE_HEIGHT)) {
					// *ANY* change to double-height potentially changes
					// *EVERY* character on the screen
					fullredraw = true;
				}
				else if ((fi->bg & CIOLIB_BG_DOUBLE_HEIGHT) && bitmap_drawn != NULL && y + 1 < vstat.rows) {
					// The DH bit isn't toggling, but if this cell is
					// the top of a double-height pair its content
					// drives the bottom-half render of the row below.
					// The row below's own vmem isn't being touched by
					// this write, so the per-cell diff in
					// update_from_vmem would skip it. Mark the cell
					// directly below dirty so the next update redraws
					// it. (May over-invalidate when this cell turns
					// out to be a bottom rather than a top, but it
					// can't propagate further than one row.)
					if (vc->ch != fi->ch || vc->bg != fi->bg || vc->fg != fi->fg
					    || vc->legacy_attr != fi->legacy_attr || vc->font != fi->font) {
						int below = vmem_cell_offset(vstat.vmem, x, y + 1);
						bitmap_drawn[below].bg |= CIOLIB_BG_DIRTY;
					}
				}
			}
			*vc = *(fi++);
			if (vstat.mode == PRESTEL_40X25 && (vc->bg & CIOLIB_BG_PRESTEL)) {
				if (vc->legacy_attr & 0x08) {
					if (cio_api.options & CONIO_OPT_PRESTEL_REVEAL)
						vc->bg |= CIOLIB_BG_REVEAL;
					else
						vc->bg &= ~CIOLIB_BG_REVEAL;
				}
			}
			vc = vmem_next_ptr(vstat.vmem, vc);
		}
	}
	if (fullredraw)
		request_redraw_locked();
	return(1);
}

// vstatlock must be held
static struct vmem_cell *
set_vmem_cell(size_t x, size_t y, uint16_t cell, uint32_t fg, uint32_t bg)
{
	int		altfont;
	int		font;

	assert_pthread_mutex_lock(&vstat_chlock);
	vstat.vmem->changed = true;
	assert_pthread_mutex_unlock(&vstat_chlock);
	bitmap_attr2palette_locked(cell>>8, fg == 0xffffff ? &fg : NULL, bg == 0xffffff ? &bg : NULL);

	altfont = (cell>>11 & 0x01) | ((cell>>14) & 0x02);
	if (!vstat.bright_altcharset)
		altfont &= ~0x01;
	if (!vstat.blink_altcharset)
		altfont &= ~0x02;
	if (vstat.forced_font) {
		if (altfont == 1 && !vstat.forced_font2)
			altfont = 0;
		if (altfont == 2 && !vstat.forced_font3)
			altfont = 0;
		if (altfont == 3 && !vstat.forced_font4)
			altfont = 0;
		font = altfont;
	}
	else
		font=current_font[altfont];
	if (font == -99)
		font = default_font;
	if (font < 0 || font > 255)
		font = 0;

	struct vmem_cell *vc = vmem_cell_ptr(vstat.vmem, x, y);
	vc->legacy_attr = cell >> 8;
	vc->ch = cell & 0xff;
	vc->fg = fg;
	if (vstat.mode == PRESTEL_40X25 && ((vc->bg & CIOLIB_BG_PRESTEL) || (bg & CIOLIB_BG_PRESTEL))) {
		if (vc->legacy_attr & 0x08) {
			if (cio_api.options & CONIO_OPT_PRESTEL_REVEAL)
				vc->bg |= CIOLIB_BG_REVEAL;
			else
				vc->bg &= ~CIOLIB_BG_REVEAL;
		}
		if ((vc->bg & CIOLIB_BG_DOUBLE_HEIGHT) != (bg & CIOLIB_BG_DOUBLE_HEIGHT)) {
			// *ANY* change to double-height potentially changes
			// *EVERY* character on the screen
			request_redraw_locked();
		}
	}
	if (vstat.mode == PRESTEL_40X25 && (vc->bg & CIOLIB_BG_PRESTEL)) {
		if (vc->legacy_attr & 0x08) {
			if (cio_api.options & CONIO_OPT_PRESTEL_REVEAL)
				vc->bg |= CIOLIB_BG_REVEAL;
			else
				vc->bg &= ~CIOLIB_BG_REVEAL;
		}
	}
	vc->bg = bg;
	vc->font = font;
	vc->hyperlink_id = 0;
	return vc;
}

static int bitmap_attr2palette_locked(uint8_t attr, uint32_t *fgp, uint32_t *bgp)
{
	uint32_t fg = attr & 0x0f;
	uint32_t bg = (attr >> 4) & 0x0f;

	if(!vstat.bright_background)
		bg &= 0x07;
	if(vstat.no_bright)
		fg &= 0x07;

	if (fgp)
		*fgp = vstat.palette[fg];
	if (bgp)
		*bgp = vstat.palette[bg];

	return 1;
}

/**********************************************************************/
/* These functions get called from ciolib and the blinker thread only */
/**********************************************************************/

static int
cursor_visible_locked(void)
{
	if (!vstat.curs_visible)
		return 0;
	if (vstat.curs_start > vstat.curs_end)
		return 0;
	if (vstat.curs_blinks) {
		if (vstat.curs_blink)
			return 1;
		return 0;
	}
	return 1;
}

static void	cb_drawrect(struct rectlist *data)
{
	int x, y;
	uint32_t *pixel;
	uint32_t cv;
	int curs_start;
	int curs_end;
	int curs_row;
	int curs_col;
	int charheight;
	int charwidth;

	if (data == NULL)
		return;
	/*
	 * Draw the cursor if it's visible
	 * 1) It's located at vstat.curs_col/vstat.curs_row.
	 * 2) The size is defined by vstat.curs_start and vstat.curs_end...
	 *    the are both rows from the top of the cell.
	 *    If vstat.curs_start > vstat.curs_end, the cursor is not shown.
	 * 3) If vstat.curs_visible is false, the cursor is not shown.
	 * 4) If vstat.curs_blinks is false, the cursor does not blink.
	 * 5) When blinking, the cursor is shown when vstat.blink is true.
	 */
	assert_rwlock_rdlock(&vstatlock);
	if (cursor_visible_locked()) {
		if (vstat.mode == PRESTEL_40X25)
			cv = 0x80FFFFFF;
		else
			cv = color_value(ciolib_fg);
		curs_start = vstat.curs_start;
		curs_end = vstat.curs_end;
		curs_row = vstat.curs_row;
		curs_col = vstat.curs_col;
		charheight = vstat.charheight;
		charwidth = vstat.charwidth;
		assert_rwlock_unlock(&vstatlock);
		for (y = curs_start; y <= curs_end; y++) {
			pixel = &data->data[((curs_row - 1) * charheight + y) * data->rect.width + (curs_col - 1) * charwidth];
			for (x = 0; x < charwidth; x++) {
				*(pixel++) = cv;
			}
		}
	}
	else
		assert_rwlock_unlock(&vstatlock);
	assert_pthread_mutex_lock(&callbacks.lock);
	callbacks.drawrect(data);
	callbacks.rects++;
	assert_pthread_mutex_unlock(&callbacks.lock);
}

static void request_redraw_locked(void)
{
	force_redraws = 1;
}

static void request_redraw(void)
{
	assert_rwlock_wrlock(&vstatlock);
	request_redraw_locked();
	assert_rwlock_unlock(&vstatlock);
}

/*
 * Called with the screen lock held
 */
static struct rectlist *alloc_full_rect(struct bitmap_screen *screen, bool allow_throttle)
{
	struct rectlist * ret;

	assert_pthread_mutex_lock(&free_rect_lock);
	if (allow_throttle) {
		if (throttled) {
			assert_pthread_mutex_unlock(&free_rect_lock);
			return NULL;
		}
		if (outstanding_rects >= MAX_OUTSTANDING) {
			throttled = true;
			assert_pthread_mutex_unlock(&free_rect_lock);
			return NULL;
		}
	}
	while (free_rects) {
		if (free_rects->rect.width == screen->screenwidth && free_rects->rect.height == screen->screenheight) {
			ret = free_rects;
			free_rects = free_rects->next;
			ret->next = NULL;
			ret->rect.x = ret->rect.y = 0;
			ret->throttle = allow_throttle;
			if (allow_throttle)
				outstanding_rects++;
			assert_pthread_mutex_unlock(&free_rect_lock);
			return ret;
		}
		else {
			free(free_rects->data);
			ret = free_rects->next;
			free(free_rects);
			free_rects = ret;
		}
	}
	assert_pthread_mutex_unlock(&free_rect_lock);

	ret = malloc(sizeof(struct rectlist));
	if (ret) {
		ret->next = NULL;
		ret->throttle = allow_throttle;
		ret->rect.x = 0;
		ret->rect.y = 0;
		ret->rect.width = screen->screenwidth;
		ret->rect.height = screen->screenheight;
		ret->data = malloc(ret->rect.width * ret->rect.height * sizeof(ret->data[0]));
		if (ret->data == NULL)
			FREE_AND_NULL(ret);
	}
	assert_pthread_mutex_lock(&free_rect_lock);
	if (allow_throttle) {
		if (ret)
			outstanding_rects++;
	}
	assert_pthread_mutex_unlock(&free_rect_lock);
	return ret;
}

static uint32_t color_value(uint32_t col)
{
	if (col & CIOLIB_COLOR_RGB)
		return col & 0xffffff;
	if ((col & 0xffffff) < sizeof(palette) / sizeof(palette[0]))
		return palette[col & 0xffffff] & 0xffffff;
	fprintf(stderr, "Invalid colour value: %08x\n", col);
	return 0;
}

static struct rectlist *get_full_rectangle_locked(struct bitmap_screen *screen)
{
	struct rectlist *rect;
	size_t sz = screen->screenwidth * screen->screenheight;
	size_t pos, spos;

	// TODO: Some sort of caching here would make things faster...?
	if(callbacks.drawrect) {
		rect = alloc_full_rect(screen, true);
		if (!rect)
			return rect;
		for (pos = 0, spos = screen->screenwidth * screen->toprow; pos < sz; pos++, spos++) {
			if (spos >= sz)
				spos -= sz;
			rect->data[pos] = color_value(screen->rect->data[spos]);
		}
		return rect;
	}
	return NULL;
}

/* The read lock must be held here. */
static int
pixel_offset(struct bitmap_screen *screen, int x, int y)
{
	assert(screen->toprow < screen->screenheight);
	y += screen->toprow;
	if (y >= screen->screenheight)
		y -= screen->screenheight;
	return y * screen->screenwidth + x;
}

struct charstate {
	unsigned char *font;
	uint32_t afc;
	uint32_t bfc;
	uint32_t bg;
	uint32_t fontoffset;
	bool slow;
	bool gexpand;
	bool did_top;
	bool double_height;
	bool sep;
	bool top_half;
	bool link;
};

struct blockstate {
	int pixeloffset;
	int maxpix;
	int font_data_width;
	uint32_t cheat_colour;
	bool expand;
};

static bool
can_cheat(struct blockstate *bs, struct vmem_cell *vc)
{
	return vc->bg == bs->cheat_colour && (vc->ch == ' ') && (vc->font < CONIO_FIRST_FREE_FONT) && !(vc->bg & CIOLIB_BG_PRESTEL);
}

static void
calc_charstate(struct blockstate *bs, struct vmem_cell *vc, struct charstate *cs, int xpos, int ypos, struct prestel_row_state *prs)
{
	bool not_hidden = true;
	cs->slow = bs->font_data_width != 8;
	cs->bg = vc->bg;
	cs->did_top = false;
	cs->sep = false;
	cs->double_height = false;
	cs->top_half = true;
	cs->link = vc->hyperlink_id != 0;

	if (vstat.forced_font) {
		switch (vc->font) {
			default:
			case 0:
				cs->font = vstat.forced_font;
				break;
			case 1:
				cs->font = vstat.forced_font2;
				break;
			case 2:
				cs->font = vstat.forced_font3;
				break;
			case 3:
				cs->font = vstat.forced_font4;
				break;
		}
	}
	else {
		if (current_font[0] == -1) {
			cs->font = font[0];
		}
		else {
			switch (vstat.charheight) {
				case 8:
					cs->font = (unsigned char *)conio_fontdata[vc->font].eight_by_eight;
					break;
				case 14:
					cs->font = (unsigned char *)conio_fontdata[vc->font].eight_by_fourteen;
					break;
				case 16:
					cs->font = (unsigned char *)conio_fontdata[vc->font].eight_by_sixteen;
					break;
				case 20:
					cs->font = (unsigned char *)conio_fontdata[vc->font].twelve_by_twenty;
					break;
				default:
					assert(0);
			}
		}
	}
	if (cs->font == NULL) {
		cs->font = font[0];
	}
	assert(cs->font);
	bool draw_fg = ((!(vc->legacy_attr & 0x80)) || vstat.no_blink);
	cs->fontoffset = (vc->ch) * (vstat.charheight * ((bs->font_data_width + 7) / 8));
	if (bs->expand) {
		cs->slow = true;
		if (vc->ch >= 0xC0 && vc->ch <= 0xDF)
			cs->gexpand = true;
		else
			cs->gexpand = false;
	}
	uint32_t fg = vc->fg;

	if (vstat.mode == PRESTEL_40X25 && (vc->bg & CIOLIB_BG_PRESTEL)) {
		bool top = false;
		bool bottom = false;
		unsigned char lattr;
		struct vmem_cell *pvc;

		cs->slow = true;
		if (vc->bg & CIOLIB_BG_SEPARATED && vc->ch >= 160)
			cs->sep = true;
		if (prs) {
			top = prs[ypos - 1].top;
			bottom = prs[ypos - 1].bottom;
		}
		else {
			// Start at the first cell...
			pvc = vmem_cell_ptr(vstat.vmem, 0, 0);
			// And check all the rows including this one.
			for (int y = 0; y < ypos; y++) {
				// If the previous line was a top line, this one is a bottom.
				if (top) {
					bottom = true;
					top = false;
				}
				else {
					// If the previous line was a bottom, this is not a bottom
					if (bottom)
						bottom = false;
					// Check for any of these being tops...
					pvc = vmem_cell_ptr(vstat.vmem, 0, y);
					for (int x = 0; x < vstat.cols; x++) {
						// If there's at least one top, this is a top row
						if (pvc->bg & CIOLIB_BG_DOUBLE_HEIGHT) {
							top = true;
							break;
						}
						pvc = vmem_next_ptr(vstat.vmem, pvc);
					}
				}
			}
		}

		// If this is the last row, it can't be a top...
		if (ypos == vstat.rows)
			top = false;

		// If this is a bottom row...
		if (bottom) {
			/*
			 * TODO: With Commstar, it copies to the next
			 * row when it's initially written, so if you
			 * overwrite the double height on the first row,
			 * the second row retains the double height and
			 * becomes tops.
			 *
			 * This violates the Prestel Terminal Specification,
			 * so for now I'm doing this the specified way
			 * rather than emulating the Commstar bug because
			 * it would be too hard to implement in this
			 * implementation.  For v2 however, this should
			 * likely be fixed to have the Commstar bug since
			 * I expect that will be how the Prestel terminals
			 * that use the SAA5050 (ie: most of them) do it.
			 */
			if (vc->bg & CIOLIB_BG_PRESTEL_TERMINAL) {
				pvc = vmem_prev_row_ptr(vstat.vmem, vc);
				cs->bg = pvc->bg;
				fg = pvc->fg;
				if (pvc->bg & CIOLIB_BG_SEPARATED && pvc->ch >= 160)
					cs->sep = true;
			}
			else
				pvc = vc;
			assert(!top);
			if (pvc->bg & CIOLIB_BG_DOUBLE_HEIGHT) {
				cs->double_height = true;
				cs->fontoffset = (pvc->ch) * (vstat.charheight * ((bs->font_data_width + 7) / 8)) + ((vstat.charheight * ((bs->font_data_width + 7) / 8)) / 2);
			}
			else {
				// Draw as space if not double-bottom
				cs->fontoffset=(32) * (vstat.charheight * ((bs->font_data_width + 7) / 8));
			}
			cs->top_half = false;
			lattr = pvc->legacy_attr;
			// Handle blink
			if (vc->bg & CIOLIB_BG_PRESTEL_TERMINAL)
				draw_fg = ((!(lattr & 0x80)) || vstat.no_blink);
		}
		// If not a bottom (either a top or no doubles)
		else {
			// And it's a top row...
			if (top) {
				// And it's double-height
				if (vc->bg & CIOLIB_BG_DOUBLE_HEIGHT) {
					//assert(top);
					// Draw it as is where is
					cs->double_height = true;
				}
			}
			lattr = vc->legacy_attr;
		}
		if (lattr & 0x08) {
			if (!(cio_api.options & CONIO_OPT_PRESTEL_REVEAL)) {
				draw_fg = false;
				not_hidden = false;
			}
		}
	}
	cs->afc = draw_fg ? fg : cs->bg;
	cs->bfc = not_hidden ? fg : cs->bg;
}

/*
 * This only draws 8-bit-wide characters, doesn't mark screens as damaged,
 * doesn't support expanded or double-height, etc.
 * Basically, this is the happy path
 */
static void
draw_char_row_fast(struct blockstate *bs, struct charstate *cs, bool last)
{
	const uint8_t mask[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
	const uint8_t fb = cs->font[cs->fontoffset];
	int pixeloffset = bs->pixeloffset;

	for(unsigned x = 0; x < 8; x++) {
		const bool fbb = (fb & mask[x]) | (last && cs->link && !(x & 3));

		if (fbb) {
			screena.rect->data[pixeloffset] = cs->afc;
			screenb.rect->data[pixeloffset] = cs->bfc;
		}
		else {
			screena.rect->data[pixeloffset] = screenb.rect->data[pixeloffset] = cs->bg;
		}

		pixeloffset++;
		assert(pixeloffset < bs->maxpix || x == (vstat.charwidth - 1));
	}
	bs->pixeloffset = pixeloffset;
	cs->fontoffset++;
}

static void
draw_char_row_slow(struct blockstate *bs, struct charstate *cs, uint32_t y, bool last)
{
	bool fbb;
	int pixeloffset;

	pixeloffset = bs->pixeloffset;
	if (cs->double_height) {
		y /= 2;
		if (!cs->top_half)
			y += vstat.charheight / 2;
	}

	uint8_t fb = cs->font[cs->fontoffset];
	for(unsigned x = 0; x < vstat.charwidth; x++) {
		unsigned bitnum = x & 0x07;
		if (bs->expand && x == bs->font_data_width) {
			if (cs->gexpand && x > 0)
				fbb = cs->font[cs->fontoffset - 1] & (0x80 >> ((x - 1) & 7));
			else
				fbb = 0;
		}
		else {
			if (bitnum == 0 && x != 0) {
				cs->fontoffset++;
				fb = cs->font[cs->fontoffset];
			}
			fbb = fb & (0x80 >> bitnum);
		}

		if (x == (bs->font_data_width - 1)) {
			cs->fontoffset++;
			fb = cs->font[cs->fontoffset];
		}

		if (cs->sep) {
			if (x == 0 || x == 1 || x == 6 || x == 7 || y == 4 || y == 5 || y == 12 || y == 13 || y == 18 || y == 19)
				fbb = false;
		}
		if ((!fbb) && last && cs->link && !(x & 3))
			fbb = true;

		uint32_t ac, bc;

		if (fbb) {
			ac = cs->afc;
			bc = cs->bfc;
		}
		else {
			ac = cs->bg;
			bc = cs->bg;
		}

		if (screena.rect->data[pixeloffset] != ac) {
			screena.rect->data[pixeloffset] = ac;
			screena.update_pixels = 1;
		}
		if (screenb.rect->data[pixeloffset] != bc) {
			screenb.rect->data[pixeloffset] = bc;
			screenb.update_pixels = 1;
		}
		pixeloffset++;
		assert(pixeloffset < bs->maxpix || x == (vstat.charwidth - 1));
	}
	if (cs->double_height) {
		// Go back for next row
		if (!cs->did_top) {
			cs->fontoffset -= ((bs->font_data_width + 7) / 8);
		}
		cs->did_top = !cs->did_top;
	}
	bs->pixeloffset = pixeloffset;
}

static void
bitmap_draw_vmem_locked(int sx, int sy, int ex, int ey, struct vmem_cell *fill, struct prestel_row_state *prs)
{
	assert(sx <= ex);
	assert(sy <= ey);
	struct charstate charstate[510]; // ciolib only supports 255 columns, but syncview goes to 510!
	struct blockstate bs;
	unsigned vwidth = ex - sx + 1;
	unsigned vheight  = ey - sy + 1;

	unsigned xoffset = (sx-1) * vstat.charwidth;
	unsigned yoffset = (sy-1) * vstat.charheight;
	bs.expand = vstat.flags & VIDMODES_FLAG_EXPAND;
	bs.font_data_width = vstat.charwidth - (bs.expand ? 1 : 0);

	assert(xoffset + vstat.charwidth <= screena.screenwidth);
	assert(xoffset + vstat.charwidth <= screenb.screenwidth);
	assert(yoffset + vstat.charheight <= screena.screenheight);
	assert(yoffset + vstat.charheight <= screenb.screenheight);
	bs.maxpix = screena.screenwidth * screena.screenheight;

	bs.pixeloffset = pixel_offset(&screena, xoffset, yoffset);
	bs.cheat_colour = fill[0].bg;
	unsigned rsz = screena.screenwidth - vstat.charwidth * vwidth;

	// Fill in charstate for this pass
	bool cheat = true;
	if (vstat.mode == PRESTEL_40X25) {
		cheat = false;
	}
	else {
		for (unsigned vy = 0; vy < vheight; vy++) {
			for (unsigned vx = 0; vx < vwidth; vx++) {
				if (!can_cheat(&bs, &fill[vy * vwidth + vx])) {
					cheat = false;
					break;
				}
			}
			if (!cheat)
				break;
		}
	}
	if (cheat) {
		const unsigned ylim = vheight * vstat.charheight;
		const unsigned xlim = vwidth * vstat.charwidth;
		const int opo = bs.pixeloffset;
		for (unsigned y = 0; y < ylim; y++) {
			for (unsigned vx = 0; vx < xlim; vx++) {
				screena.rect->data[bs.pixeloffset] = bs.cheat_colour;
				bs.pixeloffset++;
				assert(bs.pixeloffset < bs.maxpix || vx == (xlim - 1));
			}
			bs.pixeloffset += rsz;
			if (bs.pixeloffset >= bs.maxpix)
				bs.pixeloffset -= bs.maxpix;
		}
		screena.update_pixels = 1;
		bs.pixeloffset = opo;
		for (unsigned y = 0; y < ylim; y++) {
			for (unsigned vx = 0; vx < xlim; vx++) {
				screenb.rect->data[bs.pixeloffset] = bs.cheat_colour;
				bs.pixeloffset++;
				assert(bs.pixeloffset < bs.maxpix || vx == (xlim - 1));
			}
			bs.pixeloffset += rsz;
			if (bs.pixeloffset >= bs.maxpix)
				bs.pixeloffset -= bs.maxpix;
		}
		screenb.update_pixels = 1;
		int foff = 0;
		for (unsigned vy = 0; vy < vheight; vy++) {
			int coff = vmem_cell_offset(vstat.vmem, sx - 1, sy - 1 + vy);
			for (unsigned vx = 0; vx < vwidth; vx++) {
				if (bitmap_drawn)
					bitmap_drawn[coff] = fill[foff++];
				coff = vmem_next_offset(vstat.vmem, coff);
			}
		}
	}
	else {
		int foff = 0;
		bool didfast = false;
		for (unsigned vy = 0; vy < vheight; vy++) {
			// Fill in charstate for this pass
			int coff = vmem_cell_offset(vstat.vmem, sx - 1, sy - 1 + vy);
			for (unsigned vx = 0; vx < vwidth; vx++) {
				if (bitmap_drawn)
					bitmap_drawn[coff] = fill[foff++];
				coff = vmem_next_offset(vstat.vmem, coff);
				calc_charstate(&bs, &fill[vy * vwidth + vx], &charstate[vx], sx + vx, sy + vy, prs);
				if (charstate[vx].slow == false)
					didfast = true;
			}
			// Draw the characters...
			for (unsigned y = 0; y < vstat.charheight; y++) {
				const bool last = (y == vstat.charheight - 1);
				for (unsigned vx = 0; vx < vwidth; vx++) {
					if (charstate[vx].slow)
						draw_char_row_slow(&bs, &charstate[vx], y, last);
					else {
						draw_char_row_fast(&bs, &charstate[vx], last);
					}
				}
				bs.pixeloffset += rsz;
				if (bs.pixeloffset >= bs.maxpix)
					bs.pixeloffset -= bs.maxpix;
			}
		}
		if (didfast) {
			screena.update_pixels = true;
			screenb.update_pixels = true;
		}
	}
}

/***********************************************************/
/* These functions get called from the blinker thread only */
/***********************************************************/

static void cb_flush(void)
{
	assert_pthread_mutex_lock(&callbacks.lock);
	if (callbacks.rects) {
		callbacks.flush();
		callbacks.rects = 0;
	}
	assert_pthread_mutex_unlock(&callbacks.lock);
}

static int check_redraw(void)
{
	int ret;

	assert_rwlock_wrlock(&vstatlock);
	ret = force_redraws;
	force_redraws = 0;
	assert_rwlock_unlock(&vstatlock);
	return ret;
}

/* Blinker Thread */
static void blinker_thread(void *data)
{
	void *rect;
	uint64_t next_blink = 0;
	uint64_t next_cursor = 0;
	int curs_changed;
	int blink_changed;
	struct bitmap_screen *screen;
	struct bitmap_screen *ncscreen;
	int lfc;
	int blink;

	SetThreadName("Blinker");
	while(1) {
		curs_changed = 0;
		blink_changed = 0;
		SLEEP(10);
		uint64_t now = xp_timer64();

		assert_rwlock_wrlock(&vstatlock);
		switch (vstat.mode) {
			case PRESTEL_40X25:
				if (next_cursor < now) {
					curs_changed = cursor_visible_locked();
					if (vstat.curs_blink) {
						vstat.curs_blink=FALSE;
					}
					else {
						vstat.curs_blink=TRUE;
					}
					// Timings derived from Keyops Model B
					next_cursor = now + 314;
					curs_changed = (curs_changed != cursor_visible_locked());
					if (next_blink < now) {
						if (vstat.blink) {
							vstat.blink=FALSE;
							vstat.curs_blink = TRUE;
							next_blink = now + 942;
						}
						else {
							vstat.blink=TRUE;
							vstat.curs_blink = FALSE;
							next_blink = now + 314;
						}
						blink_changed = have_blink;
					}
				}
				break;
			case C64_40X25:
			case C128_40X25:
			case C128_80X25:
				if (next_cursor < now) {
					curs_changed = cursor_visible_locked();
					if (vstat.curs_blink) {
						vstat.curs_blink=FALSE;
						vstat.blink=TRUE;
					}
					else {
						vstat.curs_blink=TRUE;
						vstat.blink=FALSE;
					}
					blink_changed = have_blink;
					curs_changed = (curs_changed != cursor_visible_locked());
					next_cursor = now + 333;
				}
				break;
			case ATARI_40X24:
			case ATARI_80X25:
				// No blinking!
				vstat.curs_blink=TRUE;
				vstat.blink=FALSE;
				break;
			case ATARIST_40X25:
			case ATARIST_80X25:
			case ATARIST_80X25_MONO:
				// Cursor blinks
				if (next_cursor < now) {
					vstat.curs_blink = !vstat.curs_blink;
					curs_changed = true;
					next_cursor = now + 500;
				}
				// No text blinking!
				vstat.blink=FALSE;
				break;
			default:
				if (next_cursor < now) {
					curs_changed = cursor_visible_locked();
					if (vstat.curs_blink) {
						vstat.curs_blink=FALSE;
					}
					else {
						vstat.curs_blink=TRUE;
					}
					curs_changed = (curs_changed != cursor_visible_locked());
					next_cursor = now + 133;
					if (next_blink < now) {
						if (vstat.blink) {
							vstat.blink=FALSE;
						}
						else {
							vstat.blink=TRUE;
						}
						next_blink = now + 266;
						blink_changed = have_blink;
					}
				}
				break;
		}
		lfc = force_cursor;
		force_cursor = 0;
		blink = vstat.blink;
		assert_pthread_mutex_lock(&screenlock);
		if (screena.rect == NULL) {
			assert_pthread_mutex_unlock(&screenlock);
			assert_rwlock_unlock(&vstatlock);
			continue;
		}
		assert_pthread_mutex_unlock(&screenlock);
		if (curs_changed || blink_changed || lfc) {
			assert_pthread_mutex_lock(&vstat_chlock);
			vstat.vmem->changed = true;
			assert_pthread_mutex_unlock(&vstat_chlock);
		}
		assert_rwlock_unlock(&vstatlock);

		if (check_redraw()) {
			if (update_from_vmem(TRUE))
				request_redraw();
		}
		else {
			if (update_from_vmem(FALSE))
				request_redraw();
		}
		assert_pthread_mutex_lock(&screenlock);
		both_screens(blink, &screen, &ncscreen);
		if (screen->rect == NULL) {
			assert_pthread_mutex_unlock(&screenlock);
			continue;
		}
		// TODO: Maybe we can optimize the blink_changed forced update?
		if (screen->update_pixels || curs_changed || blink_changed || lfc) {
			rect = get_full_rectangle_locked(screen);
			/*
			 * TODO: It would be more effective to wait when we're bing throttled
			 *       and make up for cursor/blink based on elapsed time, but that's
			 *       getting complicated enough that I don't want do do a quick
			 *       hack for it.  Ideally this would be done as part of pegging
			 *       the blink rate to the wall clock rather than the free-running
			 *       sleep-based method it currently uses.
			 */
			if (rect) {
				// If the other screen is update_pixels == 2, clear it.
				if (ncscreen->update_pixels == 2)
					ncscreen->update_pixels = 0;
				screen->update_pixels = 0;
				assert_pthread_mutex_unlock(&screenlock);
				cb_drawrect(rect);
				cb_flush();
			}
			else
				assert_pthread_mutex_unlock(&screenlock);
		}
		else {
			assert_pthread_mutex_unlock(&screenlock);
		}
	}
}

static __inline struct bitmap_screen *noncurrent_screen_locked(int blink)
{
	if (blink)
		return &screenb;
	return &screena;
}

static __inline struct bitmap_screen *current_screen_locked(int blink)
{
	if (blink)
		return &screena;
	return(&screenb);
}

static __inline void both_screens(int blink, struct bitmap_screen** current, struct bitmap_screen** noncurrent)
{
	*current = current_screen_locked(blink);
	*noncurrent = noncurrent_screen_locked(blink);
}

static bool
same_cell(struct vmem_cell *bitmap_cell, struct vmem_cell *c2)
{
	if (bitmap_cell->ch != c2->ch)
		return false;
	// Handles reveal/unreveal updates, modifies vmem
	if (vstat.mode == PRESTEL_40X25 && (c2->bg & CIOLIB_BG_PRESTEL)) {
		if (c2->legacy_attr & 0x08) {
			// This is an ugly hack to patch things up later...
			// This mutex is only for this specific function since
			// all other calls hold the vstatlock for write and this
			// will at least hold it for read.
			assert_pthread_mutex_lock(&prestel_hack_lock);
			if (cio_api.options & CONIO_OPT_PRESTEL_REVEAL)
				c2->bg |= CIOLIB_BG_REVEAL;
			else
				c2->bg &= ~CIOLIB_BG_REVEAL;
			assert_pthread_mutex_unlock(&prestel_hack_lock);
		}
	}
	if (bitmap_cell->bg & CIOLIB_BG_DIRTY)	// Dirty.
		return false;
	if (bitmap_cell->bg != c2->bg)
		return false;
	if (bitmap_cell->fg != c2->fg)
		return false;
	if (bitmap_cell->font != c2->font)
		return false;
	if (bitmap_cell->legacy_attr != c2->legacy_attr)
		return false;
	if ((bitmap_cell->hyperlink_id == 0) != (c2->hyperlink_id == 0))
		return false;
	return true;
}

static void
bitmap_draw_from_vmem(int sx, int sy, int ex, int ey, bool locked, struct prestel_row_state *prs)
{
	int so = vmem_cell_offset(vstat.vmem, sx - 1, sy - 1);
	int eo = vmem_cell_offset(vstat.vmem, ex - 1, ey - 1);
	// Draw first chunk
	if (!locked)
		assert_pthread_mutex_lock(&screenlock);
	if (eo < so) {
		int rows = vstat.vmem->top_row - sy + 1;
		int ney = ey - rows;
		bitmap_draw_vmem_locked(sx, sy, ex, ney, &vstat.vmem->vmem[so], prs);
		so = 0;
		sy = ney + 1;
	}

	// Draw last chunk
	bitmap_draw_vmem_locked(sx, sy, ex, ey, &vstat.vmem->vmem[so], prs);
	if (!locked)
		assert_pthread_mutex_unlock(&screenlock);
}

/*
 * Checks if there's a blinking character anywhere on the screen and sets
 * have_blink appropriately.
 */
static void
check_blink_locked(void)
{
	have_blink = false;
	if (vstat.no_blink)
		return;

	struct vmem_cell *vc = vstat.vmem->vmem;
	size_t cells = vstat.rows * vstat.cols;
	for (int cell = 0; cell < cells; cell++) {
		if ((vc->legacy_attr & 0x80) && vc->bg != vc->fg) {
			have_blink = true;
			return;
		}
		vc++;
	}
}

/*
 * Updates any changed cells... blinking, modified flags, and the cursor
 * Is also used (with force = TRUE) to completely redraw the screen from
 * vmem (such as in the case of a font load).
 */
static int update_from_vmem(int force)
{
	static struct {
		int cols;
		int rows;
		int bright_background;
		int no_blink;
		int blink_altcharset;
		int no_bright;
		int bright_altcharset;
	} vs;
	int width,height;

	if(!bitmap_initialized)
		return(-1);

	assert_rwlock_rdlock(&vstatlock);

	if (vstat.vmem == NULL) {
		assert_rwlock_unlock(&vstatlock);
		return -1;
	}

	if(vstat.vmem->vmem == NULL) {
		assert_rwlock_unlock(&vstatlock);
		return -1;
	}

	assert_pthread_mutex_lock(&vstat_chlock);
	if(!(force || vstat.vmem->changed)) {
		assert_pthread_mutex_unlock(&vstat_chlock);
		assert_rwlock_unlock(&vstatlock);
		return 0;
	}
	vstat.vmem->changed = false;
	assert_pthread_mutex_unlock(&vstat_chlock);

	/* If we change window size, redraw everything */
	if(bitmap_drawn == NULL || vs.cols!=vstat.cols || vs.rows != vstat.rows) {
		struct vmem_cell *newl = realloc(bitmap_drawn, sizeof(struct vmem_cell) * vstat.cols * vstat.rows);
		if (newl == NULL) {
			vs.cols = 0;
			vs.rows = 0;
			free(bitmap_drawn);
			assert_rwlock_unlock(&vstatlock);
			return -1;
		}
		bitmap_drawn = newl;
		memset(bitmap_drawn, 0x04, sizeof(struct vmem_cell) * vstat.cols * vstat.rows);
		/* Force a full redraw */
		force=1;
	}
	width=vstat.cols;
	height=vstat.rows;

	check_blink_locked();

	// Pre-compute Prestel double-height row states once per frame.
	struct prestel_row_state prestel_rows[256];
	struct prestel_row_state *prs = NULL;
	if (vstat.mode == PRESTEL_40X25) {
		bool top = false, bottom = false;
		for (int y = 0; y < height; y++) {
			if (top) {
				bottom = true;
				top = false;
			}
			else {
				if (bottom)
					bottom = false;
				struct vmem_cell *pvc = vmem_cell_ptr(vstat.vmem, 0, y);
				for (int x = 0; x < vstat.cols; x++) {
					if (pvc->bg & CIOLIB_BG_DOUBLE_HEIGHT) {
						top = true;
						break;
					}
					pvc = vmem_next_ptr(vstat.vmem, pvc);
				}
			}
			prestel_rows[y].top = top;
			prestel_rows[y].bottom = bottom;
		}
		prs = prestel_rows;
	}

	if (force || bitmap_drawn == NULL) {
		bitmap_draw_from_vmem(1, 1, width, height, false, prs);
	}
	else {
		unsigned int pos = vmem_cell_offset(vstat.vmem, 0, 0);
		bool bb_attr_changed = false;

		/* Did the meaning of the blink bit change? */
		if (vstat.bright_background != vs.bright_background ||
				vstat.no_blink != vs.no_blink ||
				vstat.blink_altcharset != vs.blink_altcharset)
			bb_attr_changed = true;

		/* Did the meaning of the bright bit change? */
		if (vstat.no_bright != vs.no_bright ||
				vstat.bright_altcharset != vs.bright_altcharset)
			bb_attr_changed = true;

		/*
		 * Now we go through each character seeing if it's changed (or force is set)
		 * We combine updates into rectangles by lines...
		 *
		 * First, in the same line, we build this_rect.
		 * At the end of the line, if this_rect is the same width as the screen,
		 * we add it to last_rect.
		 */

		int sx = 0;
		int ex = 0;
		for(int y=0;y<height;y++) {
			for(int x=0;x<width;x++) {
				/* Has this char been updated? */
				bool blink_set = vstat.vmem->vmem[pos].legacy_attr & 0x80;
				if(!same_cell(&bitmap_drawn[pos], &vstat.vmem->vmem[pos])
				    || (bb_attr_changed && blink_set)
				    ) {
					ex = x + 1;
					if (sx == 0) {
						sx = ex;
					}
				}
				else {
					if (sx) {
						bitmap_draw_from_vmem(sx, y + 1, ex, y + 1, false, prs);
						sx = ex = 0;
					}
				}
				pos = vmem_next_offset(vstat.vmem, pos);
			}
			if (sx) {
				bitmap_draw_from_vmem(sx, y + 1, ex, y + 1, false, prs);
				sx = ex = 0;
			}
		}
	}
	vs.cols = vstat.cols;
	vs.rows = vstat.rows;
	vs.bright_background = vstat.bright_background;
	vs.no_blink = vstat.no_blink;
	vs.blink_altcharset = vstat.blink_altcharset;
	vs.no_bright = vstat.no_bright;
	vs.bright_altcharset = vstat.bright_altcharset;
	assert_rwlock_unlock(&vstatlock);

	return(0);
}

/*************************************/

/**********************/
/* Called from ciolib */
/**********************/
int bitmap_puttext(int sx, int sy, int ex, int ey, void *fill)
{
	size_t x, y;
	int ret = 1;
	uint16_t *buf = fill;

	if(!bitmap_initialized)
		return(0);

	if (sx < 1
	    || sy < 1
	    || ex < 1
	    || ey < 1
	    || sx > ex
	    || sy > ey
	    || ex > real_screenwidth
	    || ey > cio_textinfo.screenheight
	    || fill==NULL) {
		return(0);
	}

	assert_rwlock_wrlock(&vstatlock);
	for (y = sy - 1; y < ey; y++) {
		for (x = sx - 1; x < ex; x++) {
			set_vmem_cell(x, y, *(buf++), 0x00ffffff, 0x00ffffff);
		}
	}
	assert_rwlock_unlock(&vstatlock);
	return ret;
}

int
bitmap_vmem_puttext(int sx, int sy, int ex, int ey, struct vmem_cell *fill)
{
	int ret;

	assert_rwlock_wrlock(&vstatlock);
	ret = bitmap_vmem_puttext_locked(sx, sy, ex, ey, fill);
	assert_rwlock_unlock(&vstatlock);
	return ret;
}

static bool
bitmap_vmem_gettext_invalid(int sx, int sy, int ex, int ey, struct vmem_cell *fill)
{
	if(!bitmap_initialized)
		return true;

	if(		   sx < 1
			|| sy < 1
			|| ex < 1
			|| ey < 1
			|| sx > ex
			|| sy > ey
			|| ex > real_screenwidth
			|| ey > cio_textinfo.screenheight
			|| fill==NULL) {
		return true;
	}
	return false;
}

static int
bitmap_vmem_gettext_locked_inner(int sx, int sy, int ex, int ey, struct vmem_cell *fill)
{
	int x,y;

	for(y=sy-1;y<ey;y++) {
		struct vmem_cell *vc = vmem_cell_ptr(vstat.vmem, sx - 1, y);
		for(x=sx-1;x<ex;x++) {
			*(fill++) = *vc;
			vc = vmem_next_ptr(vstat.vmem, vc);
		}
	}
	return(1);
}

static int
bitmap_vmem_gettext_locked(int sx, int sy, int ex, int ey, struct vmem_cell *fill)
{
	if (bitmap_vmem_gettext_invalid(sx, sy, ex, ey, fill))
		return 0;

	return bitmap_vmem_gettext_locked_inner(sx, sy, ex, ey, fill);
}

int bitmap_vmem_gettext(int sx, int sy, int ex, int ey, struct vmem_cell *fill)
{
	if (bitmap_vmem_gettext_invalid(sx, sy, ex, ey, fill))
		return 0;

	assert_rwlock_rdlock(&vstatlock);
	int ret = bitmap_vmem_gettext_locked_inner(sx, sy, ex, ey, fill);
	assert_rwlock_unlock(&vstatlock);
	return(ret);
}

void bitmap_gotoxy(int x, int y)
{
	if(!bitmap_initialized)
		return;
	/* Move cursor location */
	assert_rwlock_wrlock(&vstatlock);
	if (vstat.curs_col != x + cio_textinfo.winleft - 1 || vstat.curs_row != y + cio_textinfo.wintop - 1) {
		cio_textinfo.curx=x;
		cio_textinfo.cury=y;
		vstat.curs_col = x + cio_textinfo.winleft - 1;
		vstat.curs_row = y + cio_textinfo.wintop - 1;
		if (cursor_visible_locked())
			force_cursor = 1;
	}
	assert_rwlock_unlock(&vstatlock);
}

void bitmap_setcursortype(int type)
{
	if(!bitmap_initialized)
		return;
	assert_rwlock_wrlock(&vstatlock);
	force_cursor = 1;
	switch(type) {
		case _NOCURSOR:
			vstat.curs_start=0xff;
			vstat.curs_end=0;
			break;
		case _SOLIDCURSOR:
			vstat.curs_start=0;
			vstat.curs_end=vstat.charheight-1;
			break;
		default:
		    vstat.curs_start = vstat.default_curs_start;
		    vstat.curs_end = vstat.default_curs_end;
			break;
	}
	assert_rwlock_unlock(&vstatlock);
}

int bitmap_setfont(int font, int force, int font_num)
{
	int changemode=0;
	int	newmode=-1;
	struct text_info ti;
	int		ow,oh;
	int		row,col;
	int		attr;
	struct vmem_cell	*old;
	struct vmem_cell	*new;
	struct vmem_cell	*pold;
	struct vmem_cell	*pnew;

	if(!bitmap_initialized)
		return(0);
	if(font < 0 || font>(sizeof(conio_fontdata)/sizeof(struct conio_font_data_struct)-2))
		return(0);

	if(conio_fontdata[font].eight_by_sixteen!=NULL) {
		newmode=C80;
		if (conio_fontdata[font].cp == CIOLIB_PRESTEL)
			newmode=PRESTEL_40X25;
	}
	else if(conio_fontdata[font].eight_by_fourteen!=NULL)
		newmode=C80X28;
	else if(conio_fontdata[font].eight_by_eight!=NULL)
		newmode=C80X50;

	assert_rwlock_wrlock(&vstatlock);
	switch(vstat.charheight) {
		case 8:
			if(conio_fontdata[font].eight_by_eight==NULL) {
				if(!force)
					goto error_return;
				else
					changemode=1;
			}
			break;
		case 14:
			if(conio_fontdata[font].eight_by_fourteen==NULL) {
				if(!force)
					goto error_return;
				else
					changemode=1;
			}
			break;
		case 16:
			if(conio_fontdata[font].eight_by_sixteen==NULL) {
				if(!force)
					goto error_return;
				else
					changemode=1;
			}
			break;
		case 20:
			if(conio_fontdata[font].twelve_by_twenty==NULL) {
				if(!force)
					goto error_return;
				else
					changemode=1;
			}
			break;
	}
	if(changemode && (newmode==-1 || font_num > 1))
		goto error_return;
	switch(font_num) {
		case 0:
			default_font=font;
			/* Fall-through */
		case 1:
			current_font[0]=font;
			break;
		case 2:
		case 3:
		case 4:
			current_font[font_num-1]=font;
			break;
	}

	if(changemode) {
		ti = cio_textinfo;

		attr=ti.attribute;
		ow=ti.screenwidth;
		oh=ti.screenheight;

		// Tell coverity this assignment is not protected by the lock
		// coverity[def:SUPPRESS]
		old=malloc(ow*oh*sizeof(*old));
		if(old) {
			/*
			 * Passing old to bitmap_vmem_gettext_locked()
			 * gets Coverity worked up about old being accessed
			 * after an unlock/lock, so we have the suppressions
			 * below.
			 */
			bitmap_vmem_gettext_locked(1,1,ow,oh,old);
			assert_rwlock_unlock(&vstatlock);
			textmode(newmode);
			assert_rwlock_wrlock(&vstatlock);
			new=malloc(ti.screenwidth*ti.screenheight*sizeof(*new));
			if(!new) {
				free(old);
				assert_rwlock_unlock(&vstatlock);
				return 0;
			}
			pold=old;
			pnew=new;
			for(row=0; row<ti.screenheight; row++) {
				for(col=0; col<ti.screenwidth; col++) {
					if(row < oh) {
						if(col < ow) {
							memcpy(new, old, sizeof(*old));
							new->font = font;
							new++;
							old++;
						}
						else {
							new->ch=' ';
							new->legacy_attr=attr;
							new->font = font;
							new->fg = ciolib_fg;
							new->bg = ciolib_bg;
							new++;
						}
					}
					else {
							new->ch=' ';
							new->legacy_attr=attr;
							new->font = font;
							new->fg = ciolib_fg;
							new->bg = ciolib_bg;
							new++;
					}
				}
				if(row < oh) {
					for(;col<ow;col++)
						old++;
				}
			}
			bitmap_vmem_puttext_locked(1,1,ti.screenwidth,ti.screenheight,pnew);
			free(pnew);
			free(pold);
		}
		else {
			FREE_AND_NULL(old);
		}
	}
	bitmap_loadfont_locked(NULL);
	assert_rwlock_unlock(&vstatlock);
	return(1);

error_return:
	assert_rwlock_unlock(&vstatlock);
	return(0);
}

int bitmap_getfont(int font_num)
{
	int ret;

	if (font_num == 0)
		ret = default_font;
	else if (font_num > 4)
		ret = -1;
	else
		ret = current_font[font_num - 1];

	return ret;
}

int bitmap_loadfont(const char *filename)
{
	int ret;

	assert_rwlock_wrlock(&vstatlock);
	ret = bitmap_loadfont_locked(filename);
	assert_rwlock_unlock(&vstatlock);
	return ret;
}

static void
bitmap_movetext_screen(int x, int y, int tox, int toy, int direction, int height, int width, int scroll_shift, int clear_sy, int clear_rows, uint32_t clear_color)
{
	int32_t sdestoffset;
	ssize_t ssourcepos;
	int step;
	int32_t screeny;

	int pheight = height * vstat.charheight;
	int ptoy = (toy - 1) * vstat.charheight;
	int py = (y - 1) * vstat.charheight;
	int ptox = (tox - 1) * vstat.charwidth;
	int px = (x - 1) * vstat.charwidth;
	assert_pthread_mutex_lock(&screenlock);
	if (scroll_shift != 0) {
		int pixel_shift = scroll_shift * vstat.charheight;
		screena.toprow += pixel_shift;
		if (screena.toprow >= screena.screenheight)
			screena.toprow -= screena.screenheight;
		if (screena.toprow < 0)
			screena.toprow += screena.screenheight;
		screenb.toprow += pixel_shift;
		if (screenb.toprow >= screenb.screenheight)
			screenb.toprow -= screenb.screenheight;
		if (screenb.toprow < 0)
			screenb.toprow += screenb.screenheight;
		/* Include gap rows below text area (e.g. EGA 350 - 43*8 = 6) */
		pheight += screena.screenheight - vstat.rows * vstat.charheight;
	}

	int maxpos = screena.screenwidth * screena.screenheight;
	if (direction == -1) {
		ssourcepos =   (py   + pheight - 1) * vstat.scrnwidth + px;
		sdestoffset = ((ptoy + pheight - 1) * vstat.scrnwidth + ptox) - ssourcepos;
	}
	else {
		ssourcepos =   py   * vstat.scrnwidth + px;
		sdestoffset = (ptoy * vstat.scrnwidth + ptox) - ssourcepos;
	}
	ssourcepos += screena.toprow * screena.screenwidth;
	if (ssourcepos >= maxpos)
		ssourcepos -= maxpos;
	step = direction * vstat.scrnwidth;
	const size_t mvsz = sizeof(screena.rect->data[0])*width*vstat.charwidth;
	for(screeny=0; screeny < pheight; screeny++) {
		if (ssourcepos >= maxpos)
			ssourcepos -= maxpos;
		if (ssourcepos < 0)
			ssourcepos += maxpos;
		int dest = ssourcepos + sdestoffset;
		if (dest >= maxpos)
			dest -= maxpos;
		if (dest < 0)
			dest += maxpos;
		memmove(&(screena.rect->data[dest]), &(screena.rect->data[ssourcepos]), mvsz);
		memmove(&(screenb.rect->data[dest]), &(screenb.rect->data[ssourcepos]), mvsz);
		ssourcepos += step;
	}
	/* Clear exposed pixel rows from fast scroll (ticket 228) */
	if (clear_rows > 0) {
		int clear_pheight = clear_rows * vstat.charheight;
		int clear_py = (clear_sy - 1) * vstat.charheight;
		ssize_t pos = clear_py * vstat.scrnwidth + screena.toprow * screena.screenwidth;
		if (pos >= maxpos)
			pos -= maxpos;
		for (screeny = 0; screeny < clear_pheight; screeny++) {
			if (pos >= maxpos)
				pos -= maxpos;
			for (int px = 0; px < vstat.scrnwidth; px++) {
				screena.rect->data[pos + px] = clear_color;
				screenb.rect->data[pos + px] = clear_color;
			}
			pos += vstat.scrnwidth;
		}
	}
	screena.update_pixels = 1;
	screenb.update_pixels = 1;
	assert_pthread_mutex_unlock(&screenlock);
}

/*
 * Fill a rectangle in vmem and bitmap_drawn with a cell.
 * Caller must hold vstatlock for writing.
 */
static void
vmem_fill_rect(int sx, int sy, int ex, int ey, struct vmem_cell *fill)
{
	for (int row = sy; row <= ey; row++) {
		int off = vmem_cell_offset(vstat.vmem, sx - 1, row - 1);
		for (int col = sx; col <= ex; col++) {
			vstat.vmem->vmem[off] = *fill;
			if (bitmap_drawn)
				bitmap_drawn[off] = *fill;
			off = vmem_next_offset(vstat.vmem, off);
		}
	}
}

int bitmap_movetext_clear(int x, int y, int ex, int ey, int tox, int toy, struct vmem_cell *fill)
{
	bool scrolldown = false;
	int	cy;
	int width=ex-x+1;
	int height=ey-y+1;
	int soff;
	int doff;
	/* Save originals before fast scroll modifies locals */
	const int orig_x = x;
	const int orig_y = y;
	const int orig_ex = ex;
	const int orig_ey = ey;
	const int orig_tox = tox;
	const int orig_toy = toy;
	int pixel_clear_sy = 0;
	int pixel_clear_rows = 0;
	uint32_t pixel_clear_color = 0;

	if(		   x<1
			|| y<1
			|| ex<1
			|| ey<1
			|| tox<1
			|| toy<1
			|| x>real_screenwidth
			|| ex>real_screenwidth
			|| tox>real_screenwidth
			|| (tox + width - 1) > real_screenwidth
			|| y>cio_textinfo.screenheight
			|| ey>cio_textinfo.screenheight
			|| toy>cio_textinfo.screenheight
			|| (toy + height - 1) > cio_textinfo.screenheight
			|| ex < x
			|| ey < y
			) {
		return(0);
	}

	if(toy > y)
		scrolldown = true;
	int scroll_shift = 0;

	assert_rwlock_wrlock(&vstatlock);
	assert_pthread_mutex_lock(&vstat_chlock);
	vstat.vmem->changed = true;
	assert_pthread_mutex_unlock(&vstat_chlock);
	if (width == vstat.cols && height > vstat.rows / 2 && toy == 1) {
		scroll_shift = y - toy;

		/* Compute pixel clear area before fast scroll modifies params */
		if (fill) {
			pixel_clear_sy = orig_ey - scroll_shift + 1;
			pixel_clear_rows = scroll_shift;
			pixel_clear_color = fill->bg;
		}

		vstat.vmem->top_row += scroll_shift;
		if (vstat.vmem->top_row >= vstat.vmem->height)
			vstat.vmem->top_row -= vstat.vmem->height;
		if (vstat.vmem->top_row < 0)
			vstat.vmem->top_row += vstat.vmem->height;

		// Set up the correction copy
		scrolldown = !scrolldown;
		height = scroll_shift;
		y = ey;
		toy = vstat.rows - height + 1;
	}
	if (scrolldown) {
		soff = vmem_cell_offset(vstat.vmem, x - 1, y + height - 2);
		doff = vmem_cell_offset(vstat.vmem, tox - 1, toy + height - 2);
	}
	else {
		soff = vmem_cell_offset(vstat.vmem, x - 1, y - 1);
		doff = vmem_cell_offset(vstat.vmem, tox - 1, toy - 1);
	}
	for(cy=0; cy<height; cy++) {
		memmove(&vstat.vmem->vmem[doff], &vstat.vmem->vmem[soff], sizeof(vstat.vmem->vmem[0])*width);
		if (bitmap_drawn)
			memmove(&bitmap_drawn[doff], &bitmap_drawn[soff], sizeof(vstat.vmem->vmem[0])*width);
		if (scrolldown) {
			soff = vmem_prev_row_offset(vstat.vmem, soff);
			doff = vmem_prev_row_offset(vstat.vmem, doff);
		}
		else {
			soff = vmem_next_row_offset(vstat.vmem, soff);
			doff = vmem_next_row_offset(vstat.vmem, doff);
		}
	}

	/*
	 * Fill the non-overlapping source area in vmem.
	 *
	 * Source rect: (orig_x, orig_y) - (orig_ex, orig_ey)
	 * Dest rect:   (orig_tox, orig_toy) - (orig_tox+w-1, orig_toy+h-1)
	 * Overlap:     (max(sx,dx), max(sy,dy)) - (min(sex,dex), min(sey,dey))
	 *
	 * Non-overlapping source = up to two rectangles:
	 *   1) Horizontal strip (full width, rows exposed by vertical shift)
	 *   2) Vertical strip (remaining columns, rows that overlap vertically)
	 */
	if (fill) {
		int dx = orig_tox - orig_x;
		int dy = orig_toy - orig_y;

		/* Horizontal strip: rows exposed by vertical shift */
		if (dy < 0) {
			/* Moved up: bottom rows exposed */
			vmem_fill_rect(orig_x, orig_ey + dy + 1, orig_ex, orig_ey, fill);
		}
		else if (dy > 0) {
			/* Moved down: top rows exposed */
			vmem_fill_rect(orig_x, orig_y, orig_ex, orig_y + dy - 1, fill);
		}

		/* Vertical strip: columns exposed by horizontal shift */
		/* Only the rows NOT already filled by the horizontal strip */
		if (dx != 0) {
			int vsy = orig_y + (dy > 0 ? dy : 0);
			int vey = orig_ey + (dy < 0 ? dy : 0);
			if (vsy <= vey) {
				if (dx < 0)
					vmem_fill_rect(orig_ex + dx + 1, vsy, orig_ex, vey, fill);
				else
					vmem_fill_rect(orig_x, vsy, orig_x + dx - 1, vey, fill);
			}
		}
	}

	// Make the whole thing redraw
	if (vstat.mode == PRESTEL_40X25) {
		if (bitmap_drawn)
			memset(bitmap_drawn, 0x04, sizeof(struct vmem_cell) * vstat.cols * vstat.rows);
	}
	else
		bitmap_movetext_screen(x, y, tox, toy, scrolldown ? -1 : 1, height, width, scroll_shift, pixel_clear_sy, pixel_clear_rows, pixel_clear_color);
	assert_rwlock_unlock(&vstatlock);

	return(1);
}

int bitmap_movetext(int x, int y, int ex, int ey, int tox, int toy)
{
	return bitmap_movetext_clear(x, y, ex, ey, tox, toy, NULL);
}

void bitmap_clreol(void)
{
	int x;
	WORD fill=(cio_textinfo.attribute<<8)|' ';
	int row;

	if(!bitmap_initialized)
		return;

	row = cio_textinfo.cury + cio_textinfo.wintop - 1;
	assert_rwlock_wrlock(&vstatlock);
	for(x=cio_textinfo.curx+cio_textinfo.winleft-2; x<cio_textinfo.winright; x++) {
		set_vmem_cell(x, row - 1, fill, ciolib_fg, ciolib_bg);
	}
	assert_rwlock_unlock(&vstatlock);
}

void bitmap_clrscr(void)
{
	size_t x, y;
	WORD fill = (cio_textinfo.attribute << 8) | ' ';
	int rows, cols;

	if(!bitmap_initialized)
		return;
	assert_rwlock_wrlock(&vstatlock);
	rows = vstat.rows;
	cols = vstat.cols;
	for (y = cio_textinfo.wintop - 1; y < cio_textinfo.winbottom && y < rows; y++) {
		for (x = cio_textinfo.winleft - 1; x < cio_textinfo.winright && x < cols; x++) {
			set_vmem_cell(x, y, fill, ciolib_fg, ciolib_bg);
		}
	}
	assert_rwlock_unlock(&vstatlock);
}

void bitmap_getcustomcursor(int *s, int *e, int *r, int *b, int *v)
{
	assert_rwlock_rdlock(&vstatlock);
	if(s)
		*s=vstat.curs_start;
	if(e)
		*e=vstat.curs_end;
	if(r)
		*r=vstat.charheight;
	if(b)
		*b=vstat.curs_blinks;
	if(v)
		*v=vstat.curs_visible;
	assert_rwlock_unlock(&vstatlock);
}

void bitmap_setcustomcursor(int s, int e, int r, int b, int v)
{
	double ratio;

	assert_rwlock_wrlock(&vstatlock);
	if(r==0)
		ratio=1;
	else
		ratio=(double)vstat.charheight/r;
	if(s>=0)
		vstat.curs_start=s*ratio;
	if(e>=0)
		vstat.curs_end=e*ratio;
	if(b>=0)
		vstat.curs_blinks=b;
	if(v>=0)
		vstat.curs_visible=v;
	force_cursor = 1;
	assert_rwlock_unlock(&vstatlock);
}

static void
setvideoflags_from_vstat(void)
{
	int flags = 0;

	if(vstat.bright_background)
		flags |= CIOLIB_VIDEO_BGBRIGHT;
	if(vstat.no_bright)
		flags |= CIOLIB_VIDEO_NOBRIGHT;
	if(vstat.bright_altcharset)
		flags |= CIOLIB_VIDEO_ALTCHARS;
	if(vstat.no_blink)
		flags |= CIOLIB_VIDEO_NOBLINK;
	if(vstat.blink_altcharset)
		flags |= CIOLIB_VIDEO_BLINKALTCHARS;
	protected_int32_set(&videoflags, flags);
}

int bitmap_getvideoflags(void)
{
	return protected_int32_value(videoflags);
}

void bitmap_setvideoflags(int flags)
{
	assert_rwlock_wrlock(&vstatlock);
	protected_int32_set(&videoflags, flags);
	if(flags & CIOLIB_VIDEO_BGBRIGHT)
		vstat.bright_background=1;
	else
		vstat.bright_background=0;

	if(flags & CIOLIB_VIDEO_NOBRIGHT)
		vstat.no_bright=1;
	else
		vstat.no_bright=0;

	if(flags & CIOLIB_VIDEO_ALTCHARS)
		vstat.bright_altcharset=1;
	else
		vstat.bright_altcharset=0;

	if(flags & CIOLIB_VIDEO_NOBLINK)
		vstat.no_blink=1;
	else
		vstat.no_blink=0;

	if(flags & CIOLIB_VIDEO_BLINKALTCHARS)
		vstat.blink_altcharset=1;
	else
		vstat.blink_altcharset=0;
	assert_rwlock_unlock(&vstatlock);
}

int bitmap_attr2palette(uint8_t attr, uint32_t *fgp, uint32_t *bgp)
{
	int ret;

	assert_rwlock_rdlock(&vstatlock);
	ret = bitmap_attr2palette_locked(attr, fgp, bgp);
	assert_rwlock_unlock(&vstatlock);

	return ret;
}

int bitmap_setpixel(uint32_t x, uint32_t y, uint32_t colour)
{
	assert_rwlock_wrlock(&vstatlock);
	int xchar = x / vstat.charwidth;
	int ychar = y / vstat.charheight;
	assert_pthread_mutex_lock(&vstat_chlock);
	vstat.vmem->changed = true;
	assert_pthread_mutex_unlock(&vstat_chlock);
	assert_pthread_mutex_lock(&screenlock);
	if (screena.rect == NULL || screenb.rect == NULL || x >= screena.screenwidth || y >= screena.screenheight) {
		assert_pthread_mutex_unlock(&screenlock);
		assert_rwlock_unlock(&vstatlock);
		return 0;
	}
	if (xchar < vstat.cols && ychar < vstat.rows) {
		int off = vmem_cell_offset(vstat.vmem, xchar, ychar);
		if (bitmap_drawn == NULL || !same_cell(&bitmap_drawn[off], &vstat.vmem->vmem[off])) {
			bitmap_draw_from_vmem(xchar + 1, ychar + 1, xchar + 1, ychar + 1, true, NULL);
		}
		vstat.vmem->vmem[off].bg |= CIOLIB_BG_PIXEL_GRAPHICS;
		if (bitmap_drawn)
			bitmap_drawn[off].bg |= CIOLIB_BG_PIXEL_GRAPHICS;
	}
	if (x < screena.screenwidth && y < screena.screenheight) {
		if (screena.rect->data[pixel_offset(&screena, x, y)] != colour) {
			screena.update_pixels = 1;
			screena.rect->data[pixel_offset(&screena, x, y)] = colour;
		}
	}

	if (x < screenb.screenwidth && y < screenb.screenheight) {
		if (screenb.rect->data[pixel_offset(&screenb, x, y)] != colour) {
			screenb.update_pixels = 1;
			screenb.rect->data[pixel_offset(&screenb, x, y)] = colour;
		}
	}
	assert_pthread_mutex_unlock(&screenlock);
	assert_rwlock_unlock(&vstatlock);

	return 1;
}

struct bitmap_blit_clip {
	int x;
	int y;
	uint32_t width;
	uint32_t height;
};

static int
bitmap_validate_blit_source(struct ciolib_pixels *pixels, struct ciolib_mask *mask, const struct ciolib_blit *blit)
{
	uint64_t dst_width;
	uint64_t dst_height;

	if (pixels == NULL || pixels->pixels == NULL || blit == NULL)
		return 0;
	if (blit->flags & ~(CIOLIB_BLIT_FLIP_X | CIOLIB_BLIT_FLIP_Y))
		return 0;
	if (blit->sw == 0 || blit->sh == 0 || blit->scale_x == 0 || blit->scale_y == 0)
		return 0;
	if (blit->sx >= pixels->width || blit->sy >= pixels->height)
		return 0;
	if (blit->sw > pixels->width - blit->sx)
		return 0;
	if (blit->sh > pixels->height - blit->sy)
		return 0;
	if (mask != NULL) {
		if (mask->bits == NULL)
			return 0;
		if (blit->mx >= mask->width || blit->my >= mask->height)
			return 0;
		if (blit->sw > mask->width - blit->mx)
			return 0;
		if (blit->sh > mask->height - blit->my)
			return 0;
	}

	dst_width = (uint64_t)blit->sw * blit->scale_x;
	dst_height = (uint64_t)blit->sh * blit->scale_y;
	if (dst_width > INT32_MAX || dst_height > INT32_MAX)
		return 0;

	return 1;
}

static bool
bitmap_clip_blit(const struct ciolib_blit *blit, int screen_width, int screen_height, struct bitmap_blit_clip *clip)
{
	int64_t left = blit->dx;
	int64_t top = blit->dy;
	int64_t right = left + (int64_t)blit->sw * blit->scale_x;
	int64_t bottom = top + (int64_t)blit->sh * blit->scale_y;

	if (right <= 0 || bottom <= 0 || left >= screen_width || top >= screen_height)
		return false;
	if (left < 0)
		left = 0;
	if (top < 0)
		top = 0;
	if (right > screen_width)
		right = screen_width;
	if (bottom > screen_height)
		bottom = screen_height;
	if (left >= right || top >= bottom)
		return false;

	clip->x = (int)left;
	clip->y = (int)top;
	clip->width = (uint32_t)(right - left);
	clip->height = (uint32_t)(bottom - top);
	return true;
}

static inline bool
bitmap_mask_getbit(struct ciolib_mask *mask, uint32_t x, uint32_t y)
{
	size_t pos = (size_t)y * mask->width + x;
	return (mask->bits[pos / 8] & (0x80 >> (pos % 8))) != 0;
}

static inline void
bitmap_put_blit_pixel(size_t dstpos, uint32_t coloura, uint32_t colourb)
{
	if (screena.rect->data[dstpos] != coloura) {
		screena.rect->data[dstpos] = coloura;
		screena.update_pixels = 1;
	}
	if (screenb.rect->data[dstpos] != colourb) {
		screenb.rect->data[dstpos] = colourb;
		screenb.update_pixels = 1;
	}
}

static void
bitmap_note_vmem_changed(void)
{
	assert_pthread_mutex_lock(&vstat_chlock);
	if (vstat.vmem != NULL)
		vstat.vmem->changed = true;
	assert_pthread_mutex_unlock(&vstat_chlock);
}

static void
bitmap_mark_pixel_cells(int x, int y, uint32_t width, uint32_t height)
{
	int first_col;
	int last_col;
	int first_row;
	int last_row;
	int text_width;
	int text_height;

	if (vstat.vmem == NULL || vstat.vmem->vmem == NULL || vstat.charwidth <= 0 || vstat.charheight <= 0)
		return;

	text_width = vstat.cols * vstat.charwidth;
	text_height = vstat.rows * vstat.charheight;
	if (x >= text_width || y >= text_height)
		return;
	if ((int64_t)x + width > text_width)
		width = text_width - x;
	if ((int64_t)y + height > text_height)
		height = text_height - y;

	first_col = x / vstat.charwidth;
	last_col = (x + width - 1) / vstat.charwidth;
	first_row = y / vstat.charheight;
	last_row = (y + height - 1) / vstat.charheight;

	if (bitmap_drawn == NULL) {
		bitmap_draw_from_vmem(first_col + 1, first_row + 1, last_col + 1, last_row + 1, true, NULL);
		for (int row = first_row; row <= last_row; row++) {
			int off = vmem_cell_offset(vstat.vmem, first_col, row);
			for (int col = first_col; col <= last_col; col++) {
				vstat.vmem->vmem[off].bg |= CIOLIB_BG_PIXEL_GRAPHICS;
				off = vmem_next_offset(vstat.vmem, off);
			}
		}
		return;
	}

	for (int row = first_row; row <= last_row; row++) {
		int off = vmem_cell_offset(vstat.vmem, first_col, row);
		for (int col = first_col; col <= last_col; col++) {
			if (!same_cell(&bitmap_drawn[off], &vstat.vmem->vmem[off]))
				bitmap_draw_from_vmem(col + 1, row + 1, col + 1, row + 1, true, NULL);
			vstat.vmem->vmem[off].bg |= CIOLIB_BG_PIXEL_GRAPHICS;
			bitmap_drawn[off].bg |= CIOLIB_BG_PIXEL_GRAPHICS;
			off = vmem_next_offset(vstat.vmem, off);
		}
	}
}

static void
bitmap_blit_1x1(struct ciolib_pixels *pixels, struct ciolib_mask *mask, const struct ciolib_blit *blit, const struct bitmap_blit_clip *clip)
{
	uint32_t *pixelsb = pixels->pixelsb ? pixels->pixelsb : pixels->pixels;

	for (uint32_t y = 0; y < clip->height; y++) {
		uint32_t src_y = blit->sy + (uint32_t)(clip->y - blit->dy) + y;
		uint32_t src_x = blit->sx + (uint32_t)(clip->x - blit->dx);
		size_t srcpos = (size_t)src_y * pixels->width + src_x;
		size_t dstpos = pixel_offset(&screena, clip->x, clip->y + y);

		if (mask == NULL) {
			for (uint32_t x = 0; x < clip->width; x++) {
				bitmap_put_blit_pixel(dstpos, pixels->pixels[srcpos], pixelsb[srcpos]);
				srcpos++;
				dstpos++;
			}
		}
		else {
			size_t maskpos = (size_t)(blit->my + (uint32_t)(clip->y - blit->dy) + y) * mask->width + blit->mx + (uint32_t)(clip->x - blit->dx);

			for (uint32_t x = 0; x < clip->width; x++) {
				if ((mask->bits[maskpos / 8] & (0x80 >> (maskpos % 8))) != 0)
					bitmap_put_blit_pixel(dstpos, pixels->pixels[srcpos], pixelsb[srcpos]);
				srcpos++;
				dstpos++;
				maskpos++;
			}
		}
	}
}

static void
bitmap_blit_scaled(struct ciolib_pixels *pixels, struct ciolib_mask *mask, const struct ciolib_blit *blit, const struct bitmap_blit_clip *clip)
{
	uint32_t *pixelsb = pixels->pixelsb ? pixels->pixelsb : pixels->pixels;
	uint32_t src_rel_y = (uint32_t)((clip->y - blit->dy) / blit->scale_y);
	uint32_t y_rem = (uint32_t)((clip->y - blit->dy) % blit->scale_y);
	uint32_t first_src_rel_x = (uint32_t)((clip->x - blit->dx) / blit->scale_x);
	uint32_t first_x_rem = (uint32_t)((clip->x - blit->dx) % blit->scale_x);
	bool flip_x = (blit->flags & CIOLIB_BLIT_FLIP_X) != 0;
	bool flip_y = (blit->flags & CIOLIB_BLIT_FLIP_Y) != 0;

	for (uint32_t y = 0; y < clip->height; y++) {
		uint32_t row_rel_y = src_rel_y;
		uint32_t src_rel_x = first_src_rel_x;
		uint32_t x_rem = first_x_rem;
		size_t dstpos = pixel_offset(&screena, clip->x, clip->y + y);

		if (flip_y)
			row_rel_y = blit->sh - 1 - row_rel_y;

		if (mask == NULL) {
			for (uint32_t x = 0; x < clip->width; x++) {
				uint32_t src_x = src_rel_x;
				size_t srcpos;

				if (flip_x)
					src_x = blit->sw - 1 - src_x;

				srcpos = (size_t)(blit->sy + row_rel_y) * pixels->width + blit->sx + src_x;
				bitmap_put_blit_pixel(dstpos, pixels->pixels[srcpos], pixelsb[srcpos]);
				dstpos++;

				x_rem++;
				if (x_rem >= blit->scale_x) {
					x_rem = 0;
					src_rel_x++;
				}
			}
		}
		else {
			for (uint32_t x = 0; x < clip->width; x++) {
				uint32_t src_x = src_rel_x;
				size_t srcpos;

				if (flip_x)
					src_x = blit->sw - 1 - src_x;

				if (bitmap_mask_getbit(mask, blit->mx + src_x, blit->my + row_rel_y)) {
					srcpos = (size_t)(blit->sy + row_rel_y) * pixels->width + blit->sx + src_x;
					bitmap_put_blit_pixel(dstpos, pixels->pixels[srcpos], pixelsb[srcpos]);
				}
				dstpos++;

				x_rem++;
				if (x_rem >= blit->scale_x) {
					x_rem = 0;
					src_rel_x++;
				}
			}
		}

		y_rem++;
		if (y_rem >= blit->scale_y) {
			y_rem = 0;
			src_rel_y++;
		}
	}
}

static int
bitmap_blitpixels_locked(struct ciolib_pixels *pixels, struct ciolib_mask *mask, const struct ciolib_blit *blit)
{
	struct bitmap_blit_clip clip;
	int screen_width;
	int screen_height;

	if (!bitmap_validate_blit_source(pixels, mask, blit))
		return 0;
	if (screena.rect == NULL || screenb.rect == NULL)
		return 0;

	screen_width = screena.screenwidth < screenb.screenwidth ? screena.screenwidth : screenb.screenwidth;
	screen_height = screena.screenheight < screenb.screenheight ? screena.screenheight : screenb.screenheight;
	if (!bitmap_clip_blit(blit, screen_width, screen_height, &clip))
		return 1;

	if (blit->scale_x == 1 && blit->scale_y == 1 && !(blit->flags & (CIOLIB_BLIT_FLIP_X | CIOLIB_BLIT_FLIP_Y))) {
		bitmap_note_vmem_changed();
		bitmap_mark_pixel_cells(clip.x, clip.y, clip.width, clip.height);
		bitmap_blit_1x1(pixels, mask, blit, &clip);
		return 1;
	}

	bitmap_note_vmem_changed();
	bitmap_mark_pixel_cells(clip.x, clip.y, clip.width, clip.height);
	bitmap_blit_scaled(pixels, mask, blit, &clip);
	return 1;
}

int
bitmap_blitpixels(struct ciolib_pixels *pixels, struct ciolib_mask *mask, const struct ciolib_blit *blit)
{
	int ret;

	assert_rwlock_wrlock(&vstatlock);
	assert_pthread_mutex_lock(&screenlock);
	ret = bitmap_blitpixels_locked(pixels, mask, blit);
	assert_pthread_mutex_unlock(&screenlock);
	assert_rwlock_unlock(&vstatlock);

	return ret;
}

int
bitmap_setpixels(uint32_t sx, uint32_t sy, uint32_t ex, uint32_t ey, uint32_t x_off, uint32_t y_off, uint32_t mx_off, uint32_t my_off, struct ciolib_pixels *pixels, struct ciolib_mask *mask)
{
	struct ciolib_blit blit;
	uint32_t width;
	uint32_t height;
	int ret;

	if (pixels == NULL)
		return 0;
	if (sx > ex || sy > ey)
		return 0;
	if (x_off > pixels->width || y_off > pixels->height)
		return 0;

	width = ex - sx + 1;
	height = ey - sy + 1;
	if (width > pixels->width - x_off)
		return 0;
	if (height > pixels->height - y_off)
		return 0;

	if (mask != NULL) {
		if (mx_off > mask->width || my_off > mask->height)
			return 0;
		if (width > mask->width - mx_off)
			return 0;
		if (height > mask->height - my_off)
			return 0;
	}

	blit = (struct ciolib_blit) {
		.sx = x_off,
		.sy = y_off,
		.sw = width,
		.sh = height,
		.dx = sx,
		.dy = sy,
		.scale_x = 1,
		.scale_y = 1,
		.mx = mx_off,
		.my = my_off,
		.flags = 0,
	};

	assert_rwlock_wrlock(&vstatlock);
	assert_pthread_mutex_lock(&screenlock);
	if (screena.rect == NULL || screenb.rect == NULL
	    || ex >= (uint32_t)screena.screenwidth || ey >= (uint32_t)screena.screenheight
	    || ex >= (uint32_t)screenb.screenwidth || ey >= (uint32_t)screenb.screenheight) {
		assert_pthread_mutex_unlock(&screenlock);
		assert_rwlock_unlock(&vstatlock);
		return 0;
	}
	ret = bitmap_blitpixels_locked(pixels, mask, &blit);
	assert_pthread_mutex_unlock(&screenlock);
	assert_rwlock_unlock(&vstatlock);

	return ret;
}

struct ciolib_pixels *bitmap_getpixels(uint32_t sx, uint32_t sy, uint32_t ex, uint32_t ey, int force)
{
	struct ciolib_pixels *pixels;
	uint32_t width,height;
	size_t y;

	if (sx > ex || sy > ey)
		return NULL;

	width = ex - sx + 1;
	height = ey - sy + 1;

	pixels = malloc(sizeof(*pixels));
	if (pixels == NULL)
		return NULL;

	pixels->width = width;
	pixels->height = height;

	pixels->pixels = malloc(sizeof(pixels->pixels[0])*(width)*(height));
	if (pixels->pixels == NULL) {
		free(pixels);
		return NULL;
	}

	pixels->pixelsb = malloc(sizeof(pixels->pixelsb[0])*(width)*(height));
	if (pixels->pixelsb == NULL) {
		free(pixels->pixels);
		free(pixels);
		return NULL;
	}

	update_from_vmem(force);
	assert_pthread_mutex_lock(&screenlock);
	if (ex >= screena.screenwidth || ey >= screena.screenheight ||
	    ex >= screenb.screenwidth || ey >= screenb.screenheight) {
		assert_pthread_mutex_unlock(&screenlock);
		free(pixels->pixelsb);
		free(pixels->pixels);
		free(pixels);
		return NULL;
	}

	for (y = sy; y <= ey; y++) {
		// TODO: This is the place where screen vs. buffer matters. :(
		memcpy(&pixels->pixels[width*(y-sy)], &screena.rect->data[pixel_offset(&screena, sx, y)], width * sizeof(pixels->pixels[0]));
		memcpy(&pixels->pixelsb[width*(y-sy)], &screenb.rect->data[pixel_offset(&screenb, sx, y)], width * sizeof(pixels->pixelsb[0]));
	}
	assert_pthread_mutex_unlock(&screenlock);

	return pixels;
}

int bitmap_get_modepalette(uint32_t p[16])
{
	assert_rwlock_rdlock(&vstatlock);
	memcpy(p, vstat.palette, sizeof(vstat.palette));
	assert_rwlock_unlock(&vstatlock);
	return 1;
}

int bitmap_set_modepalette(uint32_t p[16])
{
	assert_rwlock_wrlock(&vstatlock);
	memcpy(vstat.palette, p, sizeof(vstat.palette));
	assert_rwlock_unlock(&vstatlock);
	return 1;
}

uint32_t bitmap_map_rgb(uint16_t r, uint16_t g, uint16_t b)
{
	return (0xff << 24) | ((r & 0xff00) << 8) | ((g & 0xff00)) | (b >> 8);
}

void bitmap_replace_font(uint8_t id, char *name, void *data, size_t size)
{
	if (id < CONIO_FIRST_FREE_FONT) {
		free(name);
		free(data);
		return;
	}

	assert_pthread_mutex_lock(&screenlock);
	switch (size) {
		case 10240:
			FREE_AND_NULL(conio_fontdata[id].twelve_by_twenty);
			conio_fontdata[id].twelve_by_twenty=data;
			FREE_AND_NULL(conio_fontdata[id].desc);
			conio_fontdata[id].desc=name;
			break;
		case 4096:
			FREE_AND_NULL(conio_fontdata[id].eight_by_sixteen);
			conio_fontdata[id].eight_by_sixteen=data;
			FREE_AND_NULL(conio_fontdata[id].desc);
			conio_fontdata[id].desc=name;
			break;
		case 3584:
			FREE_AND_NULL(conio_fontdata[id].eight_by_fourteen);
			conio_fontdata[id].eight_by_fourteen=data;
			FREE_AND_NULL(conio_fontdata[id].desc);
			conio_fontdata[id].desc=name;
			break;
		case 2048:
			FREE_AND_NULL(conio_fontdata[id].eight_by_eight);
			conio_fontdata[id].eight_by_eight=data;
			FREE_AND_NULL(conio_fontdata[id].desc);
			conio_fontdata[id].desc=name;
			break;
		default:
			free(name);
			free(data);
	}
	assert_pthread_mutex_unlock(&screenlock);
	request_redraw();
}

int bitmap_setpalette(uint32_t index, uint16_t r, uint16_t g, uint16_t b)
{
	if (index > 65535)
		return 0;

	assert_pthread_mutex_lock(&screenlock);
	palette[index] = (0xff << 24) | ((r>>8) << 16) | ((g>>8) << 8) | (b>>8);
	screena.update_pixels = 1;
	screenb.update_pixels = 1;
	assert_pthread_mutex_unlock(&screenlock);
	return 1;
}

int bitmap_getpalette(uint32_t index, uint8_t *r, uint8_t *g, uint8_t *b)
{
	if (index > 65535)
		return 0;

	assert_pthread_mutex_lock(&screenlock);
	uint32_t c = palette[index];
	assert_pthread_mutex_unlock(&screenlock);
	if (r)
		*r = (c >> 16) & 0xff;
	if (g)
		*g = (c >> 8) & 0xff;
	if (b)
		*b = c & 0xff;
	return 1;
}

// Called with vstatlock
static int init_screens(int *width, int *height)
{
	assert_pthread_mutex_lock(&screenlock);
	screena.screenwidth = vstat.scrnwidth;
	screenb.screenwidth = vstat.scrnwidth;
	if (width)
		*width = screena.screenwidth;
	screena.screenheight = vstat.scrnheight;
	screenb.screenheight = vstat.scrnheight;
	if (height)
		*height = screena.screenheight;
	screena.update_pixels = 1;
	screenb.update_pixels = 1;
	bitmap_drv_free_rect(screena.rect);
	bitmap_drv_free_rect(screenb.rect);
	screena.rect = alloc_full_rect(&screena, false);
	if (screena.rect == NULL) {
		assert_pthread_mutex_unlock(&screenlock);
		return(-1);
	}
	screena.toprow = 0;
	screenb.rect = alloc_full_rect(&screenb, false);
	if (screenb.rect == NULL) {
		bitmap_drv_free_rect(screena.rect);
		screena.rect = NULL;
		assert_pthread_mutex_unlock(&screenlock);
		return(-1);
	}
	screenb.toprow = 0;
	uint32_t cv = color_value(vstat.palette[0]);
	size_t sz = screena.rect->rect.width * screena.rect->rect.height;
	for (size_t i = 0; i < sz; i++) {
		screena.rect->data[i] = cv;
		screenb.rect->data[i] = cv;
	}
	assert_pthread_mutex_unlock(&screenlock);
	return(0);
}

/***********************/
/* Called from drivers */
/***********************/

// Must be called with vstatlock
static bool
bitmap_width_controls(void)
{
	bool wc;

	if (vstat.aspect_width == 0 || vstat.aspect_height == 0)
		wc = true;
	else
		wc = lround((double)(vstat.scrnheight * vstat.aspect_width) / vstat.aspect_height) <= vstat.scrnwidth;
	return wc;
}

// Must be called with vstatlock
void
bitmap_get_scaled_win_size_nomax(double scale, int *w, int *h)
{
	bool wc = bitmap_width_controls();

	if (scale < 1.0)
		scale = 1.0;
	*w = lround(vstat.scrnwidth * scale);
	*h = lround(vstat.scrnheight * scale);
	if (wc)
		*h = INT_MAX;
	else
		*w = INT_MAX;
	aspect_fix_wc(w, h, wc, vstat.aspect_width, vstat.aspect_height);
}

// Must be called with vstatlock
double
bitmap_double_mult_inside(int maxwidth, int maxheight)
{
	double mult = 1.0;
	double wmult = 1.0;
	double hmult = 1.0;
	int wmw, wmh;
	int hmw, hmh;
	int w, h;

	bitmap_get_scaled_win_size_nomax(1.0, &w, &h);
	wmult = (double)maxwidth / w;
	hmult = (double)maxheight / h;
	bitmap_get_scaled_win_size_nomax(wmult, &wmw, &wmh);
	bitmap_get_scaled_win_size_nomax(hmult, &hmw, &hmh);
	if (wmult < hmult) {
		if (hmw <= maxwidth && hmh <= maxheight)
			mult = hmult;
		else
			mult = wmult;
	}
	else if(hmult < wmult) {
		if (wmw <= maxwidth && wmh <= maxheight)
			mult = wmult;
		else
			mult = hmult;
	}
	else
		mult = hmult;
	// TODO: Allow below 1.0?
	if (mult < 1.0)
		mult = 1.0;
	return mult;
}

// Must be called with vstatlock
int
bitmap_largest_mult_inside(int maxwidth, int maxheight)
{
	return (int)bitmap_double_mult_inside(maxwidth, maxheight);
}

// Must be called with vstatlock
void
bitmap_get_scaled_win_size(double scale, int *w, int *h, int maxwidth, int maxheight)
{
	bool wc = bitmap_width_controls();
	double max;

	if (maxwidth == 0 && maxheight == 0) {
		bitmap_get_scaled_win_size_nomax(scale, w, h);
		return;
	}
	if (maxwidth < vstat.scrnwidth)
		maxwidth = vstat.scrnwidth;
	if (maxheight < vstat.scrnheight)
		maxheight = vstat.scrnheight;
	max = bitmap_double_mult_inside(maxwidth, maxheight);
	if (max < 1.0)
		max = 1.0;
	if (scale < 1.0)
		scale = 1.0;
	if (scale > max)
		scale = max;
	*w = lround(vstat.scrnwidth * scale);
	*h = lround(vstat.scrnheight * scale);
	if (wc)
		*h = INT_MAX;
	else
		*w = INT_MAX;
	if (*w > maxwidth && maxwidth > 0)
		*w = maxwidth;
	if (*h > maxheight && maxheight > 0)
		*h = maxheight;
	aspect_fix_wc(w, h, wc, vstat.aspect_width, vstat.aspect_height);
}

// Must be called with vstatlock
void
bitmap_snap(bool grow, int maxwidth, int maxheight)
{
	int mult;
	int wc;
	int cw;
	int cs;

	wc = bitmap_width_controls();
	if (wc) {
		mult = vstat.winwidth / vstat.scrnwidth;
		cw = vstat.winwidth;
		cs = vstat.scrnwidth;
	}
	else {
		mult = vstat.winheight / vstat.scrnheight;
		cw = vstat.winheight;
		cs = vstat.scrnheight;
	}
	if (grow) {
		mult++;
	}
	else {
		if (cw % cs == 0)
			mult--;
	}
	if (mult < 1)
		mult = 1;
	do {
		bitmap_get_scaled_win_size(mult, &vstat.winwidth, &vstat.winheight, maxwidth, maxheight);
		mult--;
	} while ((vstat.winwidth > maxwidth || vstat.winheight > maxheight) && mult > 1);
}

static void
integer_scale(int maxwidth, int maxheight, int64_t os)
{
	int64_t bs;
	int64_t ls;
	int64_t ns;
	int w, h;

	int mult = 1;
	bitmap_get_scaled_win_size(mult, &w, &h, maxwidth, maxheight);
	bs = ((int64_t)w * w) + ((int64_t)h * h);
	ls = bs;
	ns = bs;
	while (ns < os) {
		mult++;
		bitmap_get_scaled_win_size(mult, &w, &h, 0, 0);
		if ((maxwidth > 0) && (w > maxwidth)) {
			mult--;
			ns = ls;
			break;
		}
		if (w == maxwidth)
			break;
		if ((maxheight > 0) && (h > maxheight)) {
			mult--;
			ns = ls;
			break;
		}
		if (h == maxheight)
			break;
		bs = ((int64_t)w * w) + ((int64_t)h * h);
		ls = ns;
		ns = bs;
	}
	if ((os - ls) <= (ns - os)) {
		if (mult > 1)
			mult--;
	}
	bitmap_get_scaled_win_size(mult, &w, &h, maxwidth, maxheight);
	vstat.winwidth = w;
	vstat.winheight = h;
	vstat.scaling = mult;
}

const double precision = 0.00005;
const double tolerance = 0.0125;

static void
double_scale(int maxwidth, int maxheight, int64_t os)
{
	int64_t bs;
	int64_t ls;
	int64_t ns;
	int w, h;

	double mult = 1.0;
	double step = 1.0;
	bitmap_get_scaled_win_size(mult, &w, &h, maxwidth, maxheight);
	bs = ((int64_t)w * w) + ((int64_t)h * h);
	ls = bs;
	ns = bs;
	while (ns < os) {
		mult += step;
		bitmap_get_scaled_win_size(mult, &w, &h, 0, 0);
		if ((maxwidth > 0) && (w > maxwidth)) {
			mult -= step;
			if (step <= precision) {
				ns = ls;
				break;
			}
			step /= 2;
			continue;
		}
		if (w == maxwidth)
			break;
		if ((maxheight > 0) && (h > maxheight)) {
			mult -= step;
			if (step <= precision) {
				ns = ls;
				break;
			}
			step /= 2;
			continue;
		}
		if (h == maxheight)
			break;
		bs = ((int64_t)w * w) + ((int64_t)h * h);
		if (bs >= os && step > precision) {
			mult -= step;
			step /= 2;
			continue;
		}
		ls = ns;
		ns = bs;
	}
	long long lastDelta = llabs(os - ls);
	long long currDelta = llabs(ns - os);
	if (lastDelta <= currDelta) {
		if (mult > 1) {
			mult -= step;
			currDelta = lastDelta;
		}
	}
	bitmap_get_scaled_win_size(mult, &w, &h, maxwidth, maxheight);
	vstat.winwidth = w;
	vstat.winheight = h;
	vstat.scaling = mult;
}

static double oldMult = 0.0;
static double newMult = 0.0;
static int oldWWidth = 0;
static int oldWHeight = 0;
static int oldSWidth = 0;
static int oldSHeight = 0;

/*
 * This function is intended to be called from the driver.
 * as a result, it cannot block waiting for driver status
 *
 * Must be called with vstatlock held.
 * Care MUST be taken to avoid deadlocks...
 * This is where the vmode bits used by the driver are modified...
 * the driver must be aware of this.
 * This is where the driver should grab vstatlock, then it should copy
 * out after this and only grab that lock again briefly to update
 * vstat.scaling.
 */
int bitmap_drv_init_mode(int mode, int *width, int *height, int maxwidth, int maxheight)
{
	int i;
	int64_t os;

	if(!bitmap_initialized)
		return(-1);

	int prevSWidth = vstat.scrnwidth;
	int prevSHeight = vstat.scrnheight;

	if (mode == _ORIGMODE)
		mode = C80;
	if(load_vmode(&vstat, mode)) {
		return(-1);
	}
	setvideoflags_from_vstat();

	// Save the old diagonal (no point is sqrting here)
	os = ((int64_t)vstat.winwidth * vstat.winwidth) + ((int64_t)vstat.winheight * vstat.winheight);

	/* Initialize video memory with black background, white foreground */
	assert_pthread_mutex_lock(&vstat_chlock);
	vstat.vmem->changed = true;
	assert_pthread_mutex_unlock(&vstat_chlock);
	for (i = 0; i < vstat.cols*vstat.rows; ++i) {
		if (i > 0)
			vstat.vmem->vmem[i] = vstat.vmem->vmem[0];
		else {
			vstat.vmem->vmem[i].ch = 0;
			vstat.vmem->vmem[i].legacy_attr = vstat.currattr;
			vstat.vmem->vmem[i].font = default_font == -99 ? 0 : default_font;
			vstat.vmem->vmem[i].hyperlink_id = 0;
			bitmap_attr2palette_locked(vstat.currattr, &vstat.vmem->vmem[i].fg, &vstat.vmem->vmem[i].bg);
		}
	}
	// Clear the bitmap draw cache
	FREE_AND_NULL(bitmap_drawn);

	for (i=0; i<sizeof(current_font)/sizeof(current_font[0]); i++)
		current_font[i]=default_font;
	bitmap_loadfont_locked(NULL);
	if (init_screens(width, height))
		return -1;

	cio_textinfo.attribute=vstat.currattr;
	cio_textinfo.normattr=vstat.currattr;
	cio_textinfo.currmode=mode;

	if (vstat.rows > 0xff)
		cio_textinfo.screenheight = 0xff;
	else
		cio_textinfo.screenheight = vstat.rows;

	if (vstat.cols > 0xff) {
		real_screenwidth = vstat.cols;
		cio_textinfo.screenwidth = 0xff;
	}
	else
		real_screenwidth = cio_textinfo.screenwidth = vstat.cols;

	cio_textinfo.curx=1;
	cio_textinfo.cury=1;
	cio_textinfo.winleft=1;
	cio_textinfo.wintop=1;
	cio_textinfo.winright=cio_textinfo.screenwidth;
	cio_textinfo.winbottom=cio_textinfo.screenheight;

	// Now calculate the closest diagonal new size that's smaller than max...
	double delta = fabs(vstat.scaling - round(vstat.scaling));
	if (delta < tolerance)
		integer_scale(maxwidth, maxheight, os);
	else {
		// If we're going back to the old scaling, and we haven't scaled, just restore the damn thing.
		if (oldSWidth == vstat.scrnwidth && oldSHeight == vstat.scrnheight && vstat.scaling == newMult) {
			vstat.winwidth = oldWWidth;
			vstat.winheight = oldWHeight;
			vstat.scaling = oldMult;
		}
		else {
			int w, h;
			// Tuck this away...
			oldSWidth = prevSWidth;
			oldSHeight = prevSHeight;
			oldWWidth = vstat.winwidth;
			oldWHeight = vstat.winheight;
			oldMult = vstat.scaling;
			double_scale(maxwidth, maxheight, os);
			// Round-trip the scaling so it doesn't change randomly later...
			bitmap_get_scaled_win_size(vstat.scaling, &w, &h, 0, 0);
			vstat.scaling = bitmap_double_mult_inside(w, h);
		}
		newMult = vstat.scaling;
	}

	return(0);
}

/*
 * MUST be called only once and before any other bitmap functions
 */
int bitmap_drv_init(void (*drawrect_cb) (struct rectlist *data)
				,void (*flush_cb) (void))
{
	int i;

	if(bitmap_initialized)
		return(-1);
	cio_api.options |= CONIO_OPT_LOADABLE_FONTS | CONIO_OPT_BLINK_ALT_FONT
			| CONIO_OPT_BOLD_ALT_FONT | CONIO_OPT_BRIGHT_BACKGROUND
			| CONIO_OPT_SET_PIXEL | CONIO_OPT_CUSTOM_CURSOR
			| CONIO_OPT_FONT_SELECT | CONIO_OPT_EXTENDED_PALETTE | CONIO_OPT_PALETTE_SETTING
			| CONIO_OPT_BLOCKY_SCALING;
	protected_int32_init(&videoflags, 0);
	assert_pthread_mutex_init(&callbacks.lock, NULL);
	assert_rwlock_init(&vstatlock);
	assert_pthread_mutex_init(&screenlock, NULL);
	assert_pthread_mutex_init(&vstat_chlock, NULL);
	assert_pthread_mutex_init(&prestel_hack_lock, NULL);
	assert_pthread_mutex_init(&free_rect_lock, NULL);
	assert_rwlock_wrlock(&vstatlock);
	vstat.flags = VIDMODES_FLAG_PALETTE_VMEM;
	assert_pthread_mutex_lock(&screenlock);
	for (i = 0; i < sizeof(dac_default)/sizeof(struct dac_colors); i++) {
		palette[i] = (0xffU << 24) | (dac_default[i].red << 16) | (dac_default[i].green << 8) | dac_default[i].blue;
	}
	assert_pthread_mutex_unlock(&screenlock);
	assert_rwlock_unlock(&vstatlock);

	callbacks.drawrect=drawrect_cb;
	callbacks.flush=flush_cb;
	assert_pthread_mutex_lock(&callbacks.lock);
	callbacks.rects = 0;
	assert_pthread_mutex_unlock(&callbacks.lock);
	bitmap_initialized=1;
	_beginthread(blinker_thread,0,NULL);

	return(0);
}

void bitmap_drv_request_pixels(void)
{
	assert_pthread_mutex_lock(&screenlock);
	if (screena.update_pixels == 0)
		screena.update_pixels = 2;
	if (screenb.update_pixels == 0)
		screenb.update_pixels = 2;
	assert_pthread_mutex_unlock(&screenlock);
}

void bitmap_drv_request_some_pixels(int x, int y, int width, int height)
{
	/* TODO: Some sort of queue here? */
	bitmap_drv_request_pixels();
}

void bitmap_drv_free_rect(struct rectlist *rect)
{
	if (rect == NULL)
		return;
	assert_pthread_mutex_lock(&free_rect_lock);
	if (rect->throttle) {
		outstanding_rects--;
		if (outstanding_rects < MAX_OUTSTANDING && throttled) {
			throttled = false;
		}
	}
	rect->next = free_rects;
	free_rects = rect;
	assert_pthread_mutex_unlock(&free_rect_lock);
}
