shithub: npe

ref: af064114fd7e744e3af0cc67478aab670b5d13c6
dir: /libnpe_sdl2/sdl2.c/

View raw version
#include "plan9.h"
#include <stdint.h>
#include <tos.h>
#include <draw.h>
#include <memdraw.h>
#include <mouse.h>
#include <cursor.h>
#include <thread.h>
#include <SDL2/SDL.h>
#include <pool.h>

typedef struct Audiodev Audiodev;

enum {
	Aout = 2,
	Arec,

	/* FIXME missing plumber→dropfile and enter()→text */
	Ckey = 0,
	Ckeytype,
	Cmouse,
	Cresize,
	Numchan,

	Rdown = 0,
	Rup,
	Rrepeat,

	Audiobufsz = 16384,
};

struct SDL_Window {
	int dummy;
};

struct SDL_Renderer {
	int dummy;
};

struct SDL_Texture {
	Memimage;
};

struct SDL_PixelFormat {
	int format;
};

struct SDL_Cursor {
	Image *i;
	Image *m;
	Point hot;
};

struct SDL_Thread {
	SDL_ThreadFunction f;
	char *name;
	void *userdata;
};

static SDL_PixelFormat argb8888 = {
	.format = SDL_PIXELFORMAT_ARGB8888,
};

struct Audiodev {
	QLock;
	int paused;
	int fd;
	int pid;
	int mode;
	Uint8 buf[Audiobufsz];
	void *userdata;
	void (*cb)(void *, Uint8 *, int);
};

/* FIXME extra USB audio devices? */
static Audiodev au[4] = {
	[0] = {.fd = -1, .mode = -1},
	[1] = {.fd = -1, .mode = -1},
	[Aout] = {.fd = -1, .pid = -1, .mode = OWRITE},
	[Arec] = {.fd = -1, .pid = -1, .mode = OREAD},
};

static SDL_Window onewin;
static SDL_Renderer oneren;
static int kmod;
static Mouse mouse, oldmouse;
static Rune rune;
static Mousectl *mctl;
static Keyboardctl kctl;
static Memimage *back;
static u8int *backcopy;
static Image *front;
static int logiw, logih;
static int physw, physh;
static int forceredraw = 1;
static SDL_Cursor *oldcursor, *cursor;
static int mouseredraw = 0;
static int showcursor = SDL_ENABLE;
static int textinput;

static Cursor nocursor = {
	{0, 0},
	{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	},
	{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	},
};

static Alt salt[Numchan+1] = {
	[Ckey] = { nil, &rune, CHANRCV },
	[Ckeytype] = { nil, nil, CHANNOP },
	[Cmouse] = { nil, &mouse, CHANRCV },
	[Cresize] = { nil, nil, CHANRCV },
	[Numchan] = { nil, nil, CHANNOBLK },
};

static void
kbdproc(void *)
{
	char buf[128], buf2[128], *s;
	int kfd, n, kbin, t;
	Rune r, o;

	threadsetname("kbdproc");
	if((kfd = open("/dev/kbd", OREAD|OCEXEC)) < 0)
		sysfatal("/dev/kbd: %r");
	kbin = open("/dev/kbin", OWRITE|OCEXEC);

	buf2[0] = 0;
	buf2[1] = 0;
	buf[0] = 0;
	kmod = 0;
	o = 0;
	for(;;){
		if(buf[0] != 0){
			n = strlen(buf)+1;
			memmove(buf, buf+n, sizeof(buf)-n);
		}
		if(buf[0] == 0){
			n = read(kfd, buf, sizeof(buf)-1);
			if(n <= 0)
				break;
			buf[n-1] = 0;
			buf[n] = 0;
		}

		switch(buf[0]){
		case 'c':
			if(chartorune(&r, buf+1) > 0 && r != Runeerror){
				t = Rrepeat;
				if(!textinput)
					r = tolowerrune(r);
				if(textinput || o != r){
					send(salt[Ckey].c, &r);
					send(salt[Ckeytype].c, &t);
				}
				o = 0;
			}
		default:
			continue;

		case 'k':
			s = buf+1;
			while(*s){
				s += chartorune(&r, s);
				if(utfrune(buf2+1, r) == nil){
					if(r == Kalt){
						/* magic trick: write Alt scancode to disable the "compose" mode */
						/* FIXME: does this work in both native AND drawterm? */
						write(kbin, "\x46", 1);
						kmod |= KMOD_LALT;
					}else if (r == Kshift)
						kmod |= KMOD_LSHIFT;
					else if(r == Kctl)
						kmod |= KMOD_LCTRL;
					else{
						t = Rdown;
						send(salt[Ckey].c, &r);
						send(salt[Ckeytype].c, &t);
						o = r;
					}
				}
			}
			break;

		case 'K':
			s = buf2+1;
			while(*s){
				s += chartorune(&r, s);
				if(utfrune(buf+1, r) == nil){
					if(r == Kalt)
						kmod &= ~KMOD_LALT;
					else if(r == Kshift)
						kmod &= ~KMOD_LSHIFT;
					else if(r == Kctl)
						kmod &= ~KMOD_LCTRL;
					else{
						t = Rup;
						send(salt[Ckey].c, &r);
						send(salt[Ckeytype].c, &t);
					}
				}
			}
			break;
		}
		strcpy(buf2, buf);
	}

	threadexits(nil);
}

int
SDL_Init(int mask)
{
	/* FIXME actually use the mask? */
	USED(mask);

	if(memimageinit() < 0)
		goto err;
	if(initdraw(nil, nil, "ft2-clone") < 0)
		goto err;
	if((mctl = initmouse(nil, screen)) == nil)
		goto err;

	salt[Ckey].c = chancreate(sizeof(Rune), 20);
	salt[Ckeytype].c = chancreate(sizeof(int), 20);
	salt[Cmouse].c = mctl->c;
	salt[Cresize].c = mctl->resizec;
	kctl.c = salt[Ckey].c; /* for enter() */

	if(salt[Ckey].c == nil || salt[Ckeytype].c == nil || proccreate(kbdproc, nil, 4096) < 0)
		goto err;

	return 0;
err:
	werrstr("SDL_Init: %r");
	return -1;
}

int
SDL_EventState(Uint32, int)
{
	return 0;
}

SDL_Keymod
SDL_GetModState(void)
{
	return kmod;
}

int
SDL_ShowCursor(int toggle)
{
	if(toggle == SDL_QUERY)
		return showcursor;

	showcursor = toggle == SDL_ENABLE;
	setcursor(mctl, (cursor == nil && showcursor) ? nil : &nocursor);

	return showcursor;
}

int
SDL_GetNumAudioDevices(int iscapture)
{
	/* FIXME look for extra USB devices? */
	USED(iscapture);
	return 1;
}

char *
SDL_GetAudioDeviceName(int index, int iscapture)
{
	/* FIXME look for extra USB devices? */
	USED(index); USED(iscapture);
	return "/dev/audio";
}

void
SDL_LockAudioDevice(SDL_AudioDeviceID id)
{
	qlock(&au[id]);
}

void
SDL_UnlockAudioDevice(SDL_AudioDeviceID id)
{
	qunlock(&au[id]);
}

static void
audiothread(void *p)
{
	Audiodev *d;

	d = p;
	threadsetname("audio %s", d == &au[Aout] ? "out" : "in");

	for(;;){
		qlock(d);
		if(d->paused)
			memset(d->buf, 0, sizeof(d->buf));
		else
			d->cb(d->userdata, d->buf, sizeof(d->buf));
		qunlock(d);

		if(write(d->fd, d->buf, sizeof(d->buf)) != sizeof(d->buf))
			break;
	}

	threadexits(nil);
}

void
SDL_PauseAudioDevice(SDL_AudioDeviceID id, SDL_bool pause)
{
	Audiodev *a;

	a = &au[id];
	if(a->paused && !pause){
		if(a->pid < 0)
			a->pid = proccreate(audiothread, a, mainstacksize);
		a->paused = 0;
	}else if(!a->paused && pause){
		a->paused = 1;
	}
}

SDL_AudioDeviceID
SDL_OpenAudioDevice(char *dev, int rec, SDL_AudioSpec *want, SDL_AudioSpec *have, u32int change)
{
	Audiodev *a;
	SDL_AudioDeviceID id;

	/* FIXME look for extra USB devices? */
	USED(dev);

	if(change != SDL_AUDIO_ALLOW_ANY_CHANGE){ /* FIXME sampling in mono */
		werrstr("SDL_OpenAudioDevice: changes not implemented");
		return 0;
	}

	have->freq = 44100;
	have->format = AUDIO_S16;
	have->channels = 2;
	have->samples = want->samples;

	id = rec ? Arec : Aout;
	a = &au[id];

	if(a->fd < 0 && (a->fd = open("/dev/audio", a->mode|OCEXEC)) < 0){
		werrstr("SDL_OpenAudioDevice: %r");
		return 0;
	}

	a->userdata = want->userdata;
	a->cb = want->callback;
	a->paused = 1;

	return id;
}

void
SDL_CloseAudioDevice(SDL_AudioDeviceID id)
{
	Audiodev *a;

	a = &au[id];
	qlock(a);
	close(a->fd);
	a->fd = -1;
	a->pid = -1;
	a->userdata = nil;
	a->cb = nil;
	qunlock(a);
}

u64int
SDL_GetPerformanceFrequency(void)
{
	return _tos->cyclefreq;
}

u64int
SDL_GetPerformanceCounter(void)
{
	u64int x;

	cycles(&x);

	return x;
}

char *
SDL_GetError(void)
{
	static char err[256];

	snprint(err, sizeof(err), "%r");

	return err;
}

static void
sdlthread(void *p)
{
	SDL_Thread t;

	t = *(SDL_Thread*)p;
	if(t.name != nil)
		threadsetname(t.name);
	free(p);

	threadexits(t.f(t.userdata) == 0 ? nil : "error");
}

SDL_Thread *
SDL_CreateThread(SDL_ThreadFunction f, char *name, void *userdata)
{
	SDL_Thread *t;

	if((t = calloc(1, sizeof(*t))) == nil)
		return nil;

	t->f = f;
	t->name = name;
	t->userdata = userdata;

	if(proccreate(sdlthread, t, mainstacksize) < 0){
		free(t);
		t = nil;
	}

	return t;
}

void
SDL_DetachThread(SDL_Thread *)
{
}

static void *
readfile(char *path, int *got)
{
	void *data, *data2;
	int f, n, r, sz;

	if((f = open(path, OREAD|OCEXEC)) < 0)
		return nil;

	sz = 32768;
	data = nil;
	for(n = 0;; n += r){
		if(sz-n < 65536){
			sz *= 2;
			if((data2 = realloc(data, sz)) == nil)
				goto err;
			data = data2;
		}
		if((r = read(f, (char*)data+n, sz-n-1)) < 0)
			goto err;
		if(r == 0)
			break;
	}

	if(got != nil)
		*got = n;
	((char*)data)[n] = 0;

	return data;
err:
	free(data);
	close(f);
	return nil;
}

char *
SDL_GetClipboardText(void)
{
	return readfile("/dev/snarf", nil);
}

int
SDL_SetClipboardText(char *s)
{
	int f, n;

	n = -1;
	if((f = open("/dev/snarf", OWRITE|OTRUNC|OCEXEC)) >= 0){
		n = strlen(s);
		n = write(f, s, n) == n ? 0 : -1;
		close(f);
	}

	if(n != 0)
		werrstr("SDL_SetClipboardText: %r");

	return n;
}

SDL_Texture *
SDL_CreateTexture(SDL_Renderer *, Uint32 format, int, int w, int h)
{
	SDL_Texture *t;

	if(format != SDL_PIXELFORMAT_ARGB8888){
		werrstr("SDL_CreateTexture: only SDL_PIXELFORMAT_ARGB8888 is supported");
		goto err;
	}
	if((t = (SDL_Texture*)allocmemimage(Rect(0, 0, w, h), XRGB32)) == nil) /* FIXME ARGB32? */
		goto err;
	memfillcolor(t, DTransparent);

	return t;
err:
	werrstr("SDL_CreateTexture: %r");
	return nil;
}

int
SDL_UpdateTexture(SDL_Texture *t, SDL_Rect *re, void *pixels, int pitch)
{
	Rectangle r;

	r = re ? Rect(re->x, re->y, re->x+re->w, re->y+re->h) : t->r;
	/* FIXME non-ARGB8888 */
	/* FIXME pitch */ USED(pitch);
	if(loadmemimage(t, r, pixels, Dx(r)*Dy(r)*4) < 0){
		werrstr("SDL_UpdateTexture: %r");
		return -1;
	}

	return 0;
}

int
SDL_RenderClear(SDL_Renderer *)
{
	if(back != nil)
		memfillcolor(back, DBlack);

	return 0;
}

int
SDL_GetWindowDisplayIndex(SDL_Window *)
{
	return 0;
}

void
SDL_Quit(void)
{
	threadexitsall(nil);
}

void
SDL_free(void *p)
{
	free(p);
}

SDL_Surface *
SDL_CreateRGBSurface(Uint32, int w, int h, int bpp, Uint32 rm, Uint32 gm, Uint32 bm, Uint32 am)
{
	SDL_Surface *s;
	int n;

	USED(rm, gm, bm, am); /* FIXME masks */

	n = w*h*bpp/8;
	if((s = calloc(1, sizeof(*s)+n)) == nil){
		werrstr("SDL_CreateRGBSurface: memory");
		return nil;
	}
	s->format = &argb8888;
	s->w = w;
	s->h = h;
	s->pitch = w*bpp/8;
	s->clip_rect.x = 0;
	s->clip_rect.y = 0;
	s->clip_rect.w = w;
	s->clip_rect.h = h;

	return s;
}

SDL_Surface *
SDL_CreateRGBSurfaceFrom(Uint32 *pixels, int w, int h, int bpp, int pitch, Uint32 rm, Uint32 gm, Uint32 bm, Uint32 am)
{
	SDL_Surface *s;
	int n;

	USED(pitch); /* FIXME pitch */

	if((s = SDL_CreateRGBSurface(0, w, h, bpp, rm, gm, bm, am)) == nil)
		return nil;

	n = w*h*bpp/8;
	memmove(s->pixels, pixels, n);

	return s;
}

void
SDL_FreeSurface(SDL_Surface *surface)
{
	memset(surface, 0, sizeof(surface));
	free(surface);
}

Uint32
SDL_MapRGB(SDL_PixelFormat *, Uint8 r, Uint8 g, Uint8 b)
{
	return r<<24 | g<<16 | b<<8 | 0xff;
}

int
SDL_SetColorKey(SDL_Surface *s, int flag, Uint32 key)
{
	s->keyset = flag;
	s->key = key;
	return 0;
}

SDL_Cursor *
SDL_CreateColorCursor(SDL_Surface *s, int hot_x, int hot_y)
{
	SDL_Cursor *c;
	Rectangle r;
	uchar *m;
	int n;

	m = nil;
	if((c = calloc(1, sizeof(*c))) == nil){
		werrstr("memory");
		goto err;
	}

	r = Rect(0, 0, s->w, s->h);
	if(s->keyset){
		if((c->m = allocimage(display, r, GREY8, 0, DTransparent)) == nil)
			goto err;
		if((m = malloc(s->w * s->h)) == nil)
			goto err;
		for(n = 0; n < s->w * s->h; n++){
			m[n] = ((u32int*)s->pixels)[n] == s->key ? 0x00 : 0xff;
			if(m[n] == 0)
				((u32int*)s->pixels)[n] = 0;
		}
		if(loadimage(c->m, r, m, n) < 1)
			goto err;
		free(m);
		m = nil;
	}
	if((c->i = allocimage(display, r, s->keyset ? XRGB32 : ARGB32, 0, DTransparent)) == nil)
		goto err;
	n = s->w * s->h * 4; /* FIXME non-ARGB8888 */
	if(loadimage(c->i, r, s->pixels, n) < 1)
		goto err;

	c->hot = Pt(hot_x, hot_y);

	return c;
err:
	werrstr("SDL_CreateColorCursor: %r");
	if(c != nil){
		freeimage(c->i);
		freeimage(c->m);
	}
	free(c);
	free(m);
	return nil;
}

SDL_Cursor *
SDL_GetDefaultCursor(void)
{
	return nil;
}

void
SDL_SetCursor(SDL_Cursor *c)
{
	if(cursor != c){
		cursor = c;
		mouseredraw = 1;
		setcursor(mctl, (cursor == nil && showcursor) ? nil : &nocursor);
	}
}

void
SDL_FreeCursor(SDL_Cursor *c)
{
	freeimage(c->i);
	free(c);
	if(cursor == c){
		oldcursor = nil;
		cursor = nil;
	}
}

static int
rune2scancode(Rune r)
{
	if(r >= 'a' && r <= 'z')
		return r - 'a' + SDL_SCANCODE_A;
	if(r >= '1' && r <= '9')
		return r - '1' + SDL_SCANCODE_1;
	if(r == '0')  return SDL_SCANCODE_0;
	if(r == '\n') return SDL_SCANCODE_ENTER;
	if(r == Kesc) return SDL_SCANCODE_ESCAPE;
	if(r == Kbs)  return SDL_SCANCODE_BACKSPACE;
	if(r == '\t') return SDL_SCANCODE_TAB;
	if(r == ' ')  return SDL_SCANCODE_SPACE;
	if(r == '-')  return SDL_SCANCODE_MINUS;
	if(r == '=')  return SDL_SCANCODE_EQUALS;
	if(r == '[')  return SDL_SCANCODE_LEFTBRACKET;
	if(r == ']')  return SDL_SCANCODE_RIGHTBRACKET;
	if(r == '\\') return SDL_SCANCODE_BACKSLASH;
	if(r == ';')  return SDL_SCANCODE_SEMICOLON;
	if(r == '\'') return SDL_SCANCODE_APOSTROPHE;
	if(r == '/')  return SDL_SCANCODE_SLASH;

	if(r == Kright) return SDL_SCANCODE_RIGHT;
	if(r == Kleft) return SDL_SCANCODE_LEFT;
	if(r == Kdown) return SDL_SCANCODE_DOWN;
	if(r == Kup) return SDL_SCANCODE_UP;

/* FIXME
	SDL_SCANCODE_NUMLOCKCLEAR,
	SDL_SCANCODE_KP_DIVIDE,
	SDL_SCANCODE_KP_MULTIPLY,
	SDL_SCANCODE_KP_MINUS,
	SDL_SCANCODE_KP_PLUS,
	SDL_SCANCODE_KP_ENTER,
	SDL_SCANCODE_KP_1,
	SDL_SCANCODE_KP_2,
	SDL_SCANCODE_KP_3,
	SDL_SCANCODE_KP_4,
	SDL_SCANCODE_KP_5,
	SDL_SCANCODE_KP_6,
	SDL_SCANCODE_KP_7,
	SDL_SCANCODE_KP_8,
	SDL_SCANCODE_KP_9,
	SDL_SCANCODE_KP_0,
	SDL_SCANCODE_KP_PERIOD,
	SDL_SCANCODE_NONUSBACKSLASH,
	SDL_SCANCODE_NONUSHASH,
*/
	/* FIXME there are some missing */

	if(r == L'`' || r == L'´') /* FIXME this is most likely wrong */
		return SDL_SCANCODE_GRAVE;

	return r;
}

int
SDL_PollEvent(SDL_Event *e)
{
	int t, down;

	if(e == nil) /* FIXME need to buffer the event so it won't get lost */
		return 0;

	switch(alt(salt)){
	case Ckey:
		recv(salt[Ckeytype].c, &t);
		if(textinput && rune >= 0x20 && rune <= 0x7f){
			if(t != Rrepeat)
				break;
			e->type = SDL_TEXTINPUT;
			e->text.text[runetochar(e->text.text, &rune)] = 0;
		}else if(textinput && t != Rrepeat){
			break;
		}else{
			e->type = (t == Rup) ? SDL_KEYUP : SDL_KEYDOWN;
			e->key.repeat = !textinput && t == Rrepeat;
			e->key.keysym.scancode = rune2scancode(rune);
			e->key.keysym.sym = rune;
		}
		return 1;

	case Cmouse:
		e->motion.x = mouse.xy.x - screen->r.min.x;
		e->motion.y = mouse.xy.y - screen->r.min.y;
		if(!eqpt(mouse.xy, oldmouse.xy)){
			mouseredraw = 1;
			if(mouse.buttons == oldmouse.buttons){
				e->type = SDL_MOUSEMOTION;
				return 1;
			}
		}
		if(mouse.buttons == oldmouse.buttons)
			break;
		/* FIXME there is a lot of hope for both buttons to never change at the same time */
		if((down = (mouse.buttons & 1)) != (oldmouse.buttons & 1)){ /* left */
			e->type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
			e->button.button = SDL_BUTTON_LEFT;
			oldmouse.buttons = (oldmouse.buttons & ~1) | (mouse.buttons & 1);
			return 1;
		}
		if((down = (mouse.buttons & 4)) != (oldmouse.buttons & 4)){ /* right */
			e->type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
			e->button.button = SDL_BUTTON_RIGHT;
			oldmouse.buttons = (oldmouse.buttons & ~4) | (mouse.buttons & 4);
			return 1;
		}
		if(mouse.buttons & (8|16)){
			e->type = SDL_MOUSEWHEEL;
			e->wheel.x = 0;
			e->wheel.y = (mouse.buttons & 8) ? -1 : 1;
			return 1;
		}
		break;

	case Cresize:
		forceredraw = 1;
		if(getwindow(display, Refnone) < 0){
			fprint(2, "%r\n");
			/* FIXME do something here? */
			//threadexitsall(nil);
		}
		e->type = SDL_WINDOWEVENT;
		e->window.event = SDL_WINDOWEVENT_EXPOSED;
		return 1;
	}

	return 0;
}

int
SDL_PushEvent(SDL_Event *event)
{
	/* FIXME does it matter? */
	USED(event);
	return -1;
}

void
SDL_WarpMouseInWindow(SDL_Window *, int x, int y)
{
	moveto(mctl, Pt(screen->r.min.x+x, screen->r.min.y+y));
}

Uint32
SDL_GetGlobalMouseState(int *x, int *y)
{
	Uint32 b;

	if(x != nil)
		*x = mouse.xy.x;
	if(y != nil)
		*y = mouse.xy.y;

	b = 0;
	if(mouse.buttons & 1)
		b |= SDL_BUTTON_LMASK;
	if(mouse.buttons & 4)
		b |= SDL_BUTTON_RMASK;
	/* FIXME no middle button use AT ALL? */

	return b;
}

Uint32
SDL_GetMouseState(int *x, int *y)
{
	Uint32 b;

	b = SDL_GetGlobalMouseState(nil, nil);
	if(x != nil)
		*x = (mouse.xy.x - screen->r.min.x) * logiw / physw;
	if(y != nil)
		*y = (mouse.xy.y - screen->r.min.y) * logih / physh;

	return b;
}

void
SDL_RenderGetScale(SDL_Renderer *, float *scaleX, float *scaleY)
{
	*scaleX = 1.0;
	*scaleY = 1.0;
}

void
SDL_GetWindowSize(SDL_Window *, int *w, int *h)
{
	/* no matter what rio decides */
	*w = physw;
	*h = physh;
}

void
SDL_GetWindowPosition(SDL_Window *, int *x, int *y)
{
	*x = screen->r.min.x;
	*y = screen->r.min.y;
}

SDL_bool
SDL_IsTextInputActive(void)
{
	return textinput;
}

void
SDL_StartTextInput(void)
{
	textinput = SDL_TRUE;
}

void
SDL_StopTextInput(void)
{
	textinput = SDL_FALSE;
}

void
SDL_Delay(Uint32 ms)
{
	nsleep((uvlong)ms*1000000ULL);
}

static void *
resize(u32int *src, int iw, int ih, u32int *dst, int ow, int oh)
{
	int i, j, m, n;
	u32int *d;

	if(iw == ow && ih == oh)
		return src;

	d = dst;
	n = ow/iw;
	for(; ih > 0 && oh > 0; ih--){
		for(i = j = 0; i < iw; i++, src++)
			for(m = 0; m < n && j < ow; m++, j++)
				*dst++ = *src;
		oh--;
		for(m = 1; m < n && oh > 0; m++){
			memmove(dst, dst-j, j*4);
			dst += j;
			oh--;
		}
	}

	return d;
}

int
SDL_RenderCopy(SDL_Renderer *, SDL_Texture *t, SDL_Rect *sre, SDL_Rect *dre)
{
	Rectangle sr, dr;

	if(sre != nil){
		sr.min = Pt(sre->x, sre->y);
		sr.max = addpt(sr.min, Pt(sre->w, sre->h));
	}else
		sr = t->r;

	if(dre != nil){
		dr.min = Pt(dre->x, dre->y);
		dr.max = addpt(dr.min, Pt(dre->w, dre->h));
	}else /* stretch */
		dr = Rect(0, 0, logiw, logih);

	if(back == nil || Dx(back->r) != logiw || Dy(back->r) != logih){
		freememimage(back);
		back = allocmemimage(Rect(0, 0, logiw, logih), XRGB32);
		if(back == nil){
			werrstr("SDL_RenderCopy: %r");
			return -1;
		}
		free(backcopy);
		backcopy = malloc(logiw*logih*4);
	}

	if(dre == nil)
		resize((u32int*)byteaddr(t, ZP), Dx(sr), Dy(sr), (u32int*)byteaddr(back, ZP), logiw, logih);
	else
		memimagedraw(back, dr, t, sr.min, nil, ZP, S);

	return 0;
}

void
SDL_RenderPresent(SDL_Renderer *)
{
	Rectangle r, clipr;
	static u32int *b;
	uchar *rb;

	if(!forceredraw && (forceredraw = memcmp(backcopy, byteaddr(back, ZP), logiw*logih*4)) == 0 && !mouseredraw)
		return;

	r = Rect(0, 0, physw, physh);
	if(front != nil && (Dx(front->r) != physw || Dy(front->r) != physh)){
		freeimage(front);
		front = nil;
		free(b);
		b = nil;
	}
	if(b == nil && (b = realloc(b, physw*physh*4)) == nil){
		fprint(2, "SDL_RenderPresent: %r\n");
		return;
	}
	if(forceredraw || front == nil){
		rb = resize((u32int*)byteaddr(back, ZP), Dx(back->r), Dy(back->r), b, physw, physh);
		if(front == nil && (front = allocimage(display, r, XRGB32, 0, DNofill)) == nil){
			fprint(2, "SDL_RenderPresent: %r\n");
			return;
		}
		if(loadimage(front, r, rb, Dx(r)*Dy(r)*4) < 0){
			fprint(2, "SDL_RenderPresent: %r\n");
			return;
		}
	}

	if(cursor != nil && showcursor){
		r.min = subpt(mouse.xy, cursor->hot);
		r.max = addpt(r.min, cursor->i->r.max);
		if(!forceredraw && oldcursor != nil){
			clipr.min = subpt(oldmouse.xy, oldcursor->hot);
			clipr.max = addpt(clipr.min, oldcursor->i->r.max);
			combinerect(&clipr, r);
			replclipr(screen, 0, clipr);
		}
	}
	draw(screen, screen->r, front, nil, ZP);
	if(cursor != nil && showcursor)
		draw(screen, r, cursor->i, cursor->m, ZP);
	mouseredraw = 0;
	oldmouse.xy = mouse.xy;
	oldcursor = cursor;

	flushimage(display, 1);

	if(forceredraw)
		memmove(backcopy, byteaddr(back, ZP), logiw*logih*4);
	else
		replclipr(screen, 0, screen->r);
	forceredraw = 0;
}

Uint32
SDL_GetWindowFlags(SDL_Window *)
{
	/* FIXME is this correct? */
	return SDL_WINDOW_INPUT_FOCUS;
}

int
SDL_RenderSetLogicalSize(SDL_Renderer *, int w, int h)
{
	if(logiw != w || logih != h){
		logiw = w;
		logih = h;
		forceredraw = 1;
	}

	return 0;
}

int
SDL_GetRendererOutputSize(SDL_Renderer *, int *w, int *h)
{
	if(w != nil)
		*w = logiw;
	if(h != nil)
		*h = logih;

	return 0;
}

void
SDL_SetWindowSize(SDL_Window *, int w, int h)
{
	int f, n;

	if(physw != w || physh != h){
		physw = w;
		physh = h;
		if((f = open("/dev/wctl", OWRITE|OCEXEC)) >= 0){
			n = fprint(f, "resize -dx %d -dy %d", w+Borderwidth*2, h+Borderwidth*2);
			close(f);
			if(n > 0){
				getwindow(display, Refnone);
				forceredraw = 1;
			}
		}
	}
}

int
SDL_ShowSimpleMessageBox(Uint32, char *title, char *message, SDL_Window *)
{
	/* FIXME display a GUI window? */
	fprint(2, "%s: %s\n", title, message);
	return 0;
}

int
SDL_SetWindowFullscreen(SDL_Window *, Uint32)
{
	/* FIXME again, ft2 does NOT check the error code, figure something out */
	werrstr("SDL_SetWindowFullscreen: not implemented");
	return -1;
}

void
SDL_SetWindowGrab(SDL_Window *, SDL_bool grabbed)
{
	/* FIXME not sure if it's worth anything */
	USED(grabbed);
}

void
SDL_SetWindowPosition(SDL_Window *, int x, int y)
{
	int f, n;

	if((f = open("/dev/wctl", OWRITE|OCEXEC)) >= 0){
		n = fprint(f, "move -minx %d -miny %d", x, y);
		close(f);
		if(n > 0){
			getwindow(display, Refnone);
			forceredraw = 1;
		}
	}
}

int
SDL_GetDesktopDisplayMode(int displayIndex, SDL_DisplayMode *mode)
{
	if(displayIndex != 0)
		return -1;

	mode->w = physw;
	mode->h = physh;
	mode->format = SDL_PIXELFORMAT_ARGB8888;
	mode->refresh_rate = 0;

	return 0;
}

void
SDL_SetWindowTitle(SDL_Window *, char *title)
{
	int f;

	if((f = open("/dev/label", OWRITE|OTRUNC|OCEXEC)) >= 0 || (f = open("/mnt/term/dev/label", OWRITE|OTRUNC|OCEXEC)) >= 0){
		write(f, title, strlen(title));
		close(f);
	}
}

void
SDL_DestroyTexture(SDL_Texture *t)
{
	freememimage(t);
}

int
SDL_SetTextureBlendMode(SDL_Texture *, SDL_BlendMode blendMode)
{
	if(blendMode != SDL_BLENDMODE_NONE){
		werrstr("SDL_SetTextureBlendMode: only SDL_BLENDMODE_NONE is supported");
		return -1;
	}

	return 0;
}

SDL_bool
SDL_SetHint(char *name, char *value)
{
	/* FIXME anyone cares about name="SDL_RENDER_SCALE_QUALITY" value="(best|nearest)"? */
	USED(name);
	USED(value);

	return SDL_FALSE;
}

SDL_Window *
SDL_CreateWindow(char *title, int x, int y, int w, int h, Uint32)
{
	SDL_SetWindowTitle(&onewin, title);
	SDL_SetWindowSize(&onewin, w, h);

	if(x != SDL_WINDOWPOS_UNDEFINED && y != SDL_WINDOWPOS_UNDEFINED){ /* FIXME either of these can be undefined */
		if(x == SDL_WINDOWPOS_CENTERED)
			x = display->image->r.min.x + (Dx(display->image->r) - w)/2;
		if(y == SDL_WINDOWPOS_CENTERED)
			y = display->image->r.min.y + (Dx(display->image->r) - w)/2;
		SDL_SetWindowPosition(&onewin, x, y);
	}

	return &onewin;
}

SDL_Renderer *
SDL_CreateRenderer(SDL_Window *, int, Uint32)
{
	SDL_RenderSetLogicalSize(nil, physw, physh);
	return &oneren;
}

int
SDL_SetRenderDrawBlendMode(SDL_Renderer *, SDL_BlendMode blendMode)
{
	if(blendMode != SDL_BLENDMODE_NONE){
		werrstr("SDL_SetRenderDrawBlendMode: only SDL_BLENDMODE_NONE is supported");
		return -1;
	}

	return 0;
}

char *
SDL_GetCurrentVideoDriver(void)
{
	return "/dev/draw";
}

Uint32
SDL_GetTicks(void)
{
	return nanosec()/1000000ULL;
}

SDL_bool
SDL_HasClipboardText(void)
{
	/* most def */
	return SDL_TRUE;
}

void
SDL_SetThreadPriority(int)
{
	/* nothing to do here */
}

void
SDL_RestoreWindow(SDL_Window *)
{
	/* nothing to do here */
}

void
SDL_RaiseWindow(SDL_Window *)
{
	/* nothing to do here */
}

int
SDL_SetSurfaceRLE(SDL_Surface *, int)
{
	/* nothing to do here */
	return 0;
}

int
SDL_SetSurfaceBlendMode(SDL_Surface *, SDL_BlendMode blendMode)
{
	if(blendMode != SDL_BLENDMODE_NONE){
		werrstr("SDL_SetSurfaceBlendMode: only SDL_BLENDMODE_NONE is supported");
		return -1;
	}

	return 0;
}

int
SDL_LockSurface(SDL_Surface *)
{
	/* nothing to do here */
	return 0;
}

int
SDL_UnlockSurface(SDL_Surface *)
{
	/* nothing to do here */
	return 0;
}

void
SDL_DestroyRenderer(SDL_Renderer *)
{
	/* nothing to do here */
}

void
SDL_DestroyWindow(SDL_Window *)
{
}

SDL_bool
SDL_HasSSE(void)
{
	/* it's not like we have builtins anyway */
	return SDL_FALSE;
}

SDL_bool
SDL_HasSSE2(void)
{
	/* it's not like we have builtins anyway */
	return SDL_FALSE;
}

void
SDL_EnableScreenSaver(void)
{
}

int
SDL_SaveBMP(SDL_Surface *s, const char *file)
{
	/* FIXME implement this */
	USED(s, file);

	return -1;
}