shithub: npe

Download patch

ref: 979ae53cfee913c2d303ab0f95aaa65dace1c7b5
parent: 2aee89298cbbf5c1b4d50e70733b52ae75c706fd
author: qwx <qwx@sciops.net>
date: Tue Feb 17 19:34:19 EST 2026

wip sdl3 port

--- /dev/null
+++ b/include/npe/SDL3/SDL.h
@@ -1,0 +1,298 @@
+#ifndef _npe_SDL_h_
+#define _npe_SDL_h_
+
+#pragma lib "libnpe_sdl3.a"
+
+#include <npe.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+typedef u8int Uint8;
+typedef u16int Uint16;
+typedef u32int Uint32;
+typedef u64int Uint64;
+typedef s8int Sint8;
+typedef s16int Sint16;
+typedef s32int Sint32;
+typedef s64int Sint64;
+typedef enum {SDL_FALSE, SDL_TRUE} SDL_bool;
+
+typedef struct SDL_Window SDL_Window;
+typedef int SDL_BlendMode;
+typedef struct SDL_Cursor SDL_Cursor;
+typedef struct SDL_Point SDL_Point;
+typedef int SDL_RendererFlip;
+typedef struct SDL_DisplayMode SDL_DisplayMode;
+typedef int SDL_SystemCursor;
+typedef struct SDL_Color SDL_Color;
+typedef struct SDL_Palette SDL_Palette;
+typedef struct SDL_RendererInfo SDL_RendererInfo;
+
+struct SDL_Color {
+    Uint8 r;
+    Uint8 g;
+    Uint8 b;
+    Uint8 a;
+};
+
+struct SDL_Palette {
+	int ncolors;
+	SDL_Color *colors;
+};
+
+#pragma incomplete SDL_Cursor
+#pragma incomplete SDL_Window
+
+#define SDLCALL
+
+#define SDL_zero(x) do{ memset(&(x), 0, sizeof(x)); }while(0)
+#define SDL_atoi atoi
+#define SDL_memset memset
+#define SDL_malloc malloc
+#define SDL_realloc realloc
+#define SDL_calloc calloc
+#define	SDL_free free
+#define SDL_memcpy memcpy
+#define SDL_memmove memmove
+#define SDL_memcmp memcmp
+#define SDL_printf printf
+#define SDL_snprintf snprintf
+#define SDL_strcmp strcmp
+#define SDL_strcasecmp cistrcmp
+#define SDL_strdup strdup
+#define SDL_strlen strlen
+#define SDL_strlcpy strlcpy
+#define SDL_strstr strstr
+#define SDL_strncmp strncmp
+
+size_t SDL_strlcpy(char *dst, const char *src, size_t maxlen);
+char* SDL_strcasestr(const char *haystack, const char *needle);
+
+#include <SDL3/SDL_init.h>
+#include <SDL3/SDL_mutex.h>
+#include <SDL3/SDL_iostream.h>
+#include <SDL3/SDL_keycode.h>
+#include <SDL3/SDL_scancode.h>
+#include <SDL3/SDL_keyboard.h>
+#include <SDL3/SDL_audio.h>
+#include <SDL3/SDL_joystick.h>
+#include <SDL3/SDL_gamepad.h>
+#include <SDL3/SDL_thread.h>
+#include <SDL3/SDL_quit.h>
+#include <SDL3/SDL_version.h>
+#include <SDL3/SDL_video.h>
+#include <SDL3/SDL_log.h>
+#include <SDL3/SDL_render.h>
+#include <SDL3/SDL_surface.h>
+#include <SDL3/SDL_properties.h>
+#include <SDL3/SDL_events.h>
+
+void SDL_StopTextInput(void);
+SDL_bool SDL_HasSSE(void);
+SDL_bool SDL_HasSSE2(void);
+bool SDL_Init(int);
+bool SDL_InitSubSystem(int);
+bool SDL_QuitSubSystem(int);
+int SDL_SetRelativeMouseMode(SDL_bool enabled);
+int SDL_GetRelativeMouseMode(void);
+void SDL_SetWindowIcon(SDL_Window*,SDL_Surface*);
+void SDL_SetWindowBordered(SDL_Window*,SDL_bool);
+SDL_Keymod SDL_GetModState(void);
+int SDL_ShowCursor(void);
+bool SDL_HideCursor(void);
+Uint64 SDL_GetPerformanceFrequency(void);
+Uint64 SDL_GetPerformanceCounter(void);
+char *SDL_GetError(void);
+char *SDL_GetClipboardText(void);
+int SDL_SetClipboardText(char *);
+SDL_bool SDL_HasClipboardText(void);
+void SDL_RestoreWindow(SDL_Window *window);
+void SDL_RaiseWindow(SDL_Window *window);
+int SDL_GetWindowDisplayIndex(SDL_Window *window);
+Uint32 SDL_GetGlobalMouseState(int *x, int *y);
+void SDL_Quit(void);
+void SDL_free(void *);
+SDL_Cursor *SDL_GetDefaultCursor(void);
+void SDL_SetCursor(SDL_Cursor *cursor);
+void SDL_FreeCursor(SDL_Cursor *cursor);
+void SDL_GetRGB(Uint32 pixel, const SDL_PixelFormat *format, Uint8 *r, Uint8 *g, Uint8 *b);
+void SDL_WarpMouseInWindow(SDL_Window *window, int x, int y);
+void SDL_GetWindowSize(SDL_Window *window, int *w, int *h);
+void SDL_GetWindowPosition(SDL_Window *window, int *x, int *y);
+void SDL_SetWindowMinimumSize(SDL_Window *window, int min_w, int min_h);
+Uint32 SDL_GetWindowPixelFormat(SDL_Window *window);
+SDL_bool SDL_PixelFormatEnumToMasks(Uint32 format, int *bpp, Uint32 *Rmask, Uint32 *Gmask, Uint32 *Bmask, Uint32 *Amask);
+Uint32 SDL_GetRelativeMouseState(int *x, int *y);
+Uint32 SDL_GetMouseState(int *x, int *y);
+SDL_bool SDL_IsTextInputActive(void);
+void SDL_StartTextInput(void);
+void SDL_Delay(Uint32 ms);
+void SDL_SetMainReady(void);
+Uint32 SDL_GetWindowFlags(SDL_Window *window);
+void SDL_SetWindowSize(SDL_Window *window, int w, int h);
+int SDL_ShowSimpleMessageBox(Uint32 flags, char *title, char *message, SDL_Window *window);
+int SDL_SetWindowFullscreen(SDL_Window *window, Uint32 flags);
+void SDL_SetWindowGrab(SDL_Window *window, SDL_bool grabbed);
+void SDL_SetWindowPosition(SDL_Window *window, int x, int y);
+void SDL_DestroyWindow(SDL_Window *window);
+int SDL_GetDisplayUsableBounds(int displayIndex, SDL_Rect *rect);
+int SDL_GetDisplayBounds(int displayIndex, SDL_Rect *rect);
+int SDL_GetDesktopDisplayMode(int displayIndex, SDL_DisplayMode *mode);
+void SDL_SetWindowTitle(SDL_Window *window, char *title);
+SDL_bool SDL_SetHint(char *name, char *value);
+SDL_Window *SDL_CreateWindow(char *title, int x, int y, int w, int h, Uint32 flags);
+char *SDL_GetCurrentVideoDriver(void);
+void SDL_EnableScreenSaver(void);
+Uint32 SDL_GetTicks(void);
+void SDL_ClearError(void);
+int SDL_OpenURL(char *url);
+SDL_Cursor *SDL_CreateSystemCursor(SDL_SystemCursor id);
+char *SDL_GetBasePath(void);
+char *SDL_GetPrefPath(char *org, char *app);
+int SDL_GetCurrentDisplayMode(int displayIndex, SDL_DisplayMode *mode);
+int SDL_GetNumDisplayModes(int displayIndex);
+void SDL_ShowWindow(SDL_Window *w);
+int SDL_GetNumVideoDisplays(void);
+void SDL_SetModState(SDL_Keymod modstate);
+int SDL_FillRect(SDL_Surface *dst, const SDL_Rect *rect, Uint32 color);
+SDL_Palette *SDL_AllocPalette(int ncolors);
+int SDL_SetPaletteColors(SDL_Palette *palette, const SDL_Color *colors, int firstcolor, int ncolors);
+int SDL_GetWindowBordersSize(SDL_Window *window, int *top, int *left, int *bot, int *right);
+
+#define SDL_min(x, y) (((x) < (y)) ? (x) : (y))
+#define SDL_max(x, y) (((x) > (y)) ? (x) : (y))
+#define SDL_abs(x) ((x) < 0 ? -(x) : (x))
+#define SDL_cosf(x) cos(x)
+#define SDL_sinf(x) sin(x)
+#define SDL_sqrtf(x) sqrt(x)
+#define SDL_atanf(x) atan(x)
+
+enum {
+	SDL_QUERY = -1,
+	SDL_DISABLE,
+	SDL_ENABLE,
+
+	SDL_MESSAGEBOX_ERROR = 0,
+
+	SDL_WINDOW_MINIMIZED = 1<<0,
+	SDL_WINDOW_FULLSCREEN_DESKTOP = 1<<1,
+	SDL_WINDOW_INPUT_FOCUS = 1<<2,
+	SDL_WINDOW_ALLOW_HIGHDPI = 1<<3,
+	SDL_WINDOW_SHOWN = 1<<4,
+	SDL_WINDOW_RESIZABLE = 1<<5,
+	SDL_WINDOW_HIDDEN = 1<<6,
+	SDL_WINDOW_MAXIMIZED = 1<<7,
+	SDL_WINDOW_FULLSCREEN = 1<<8,
+	SDL_WINDOW_BORDERLESS = 1<<9,
+
+	SDL_WINDOWPOS_CENTERED = -1,
+	SDL_WINDOWPOS_UNDEFINED = -2,
+
+	SDL_INIT_TIMER = 1<<0,
+	SDL_INIT_AUDIO = 1<<1,
+	SDL_INIT_VIDEO = 1<<2,
+	SDL_INIT_GAMEPAD = 1<<13,
+	SDL_INIT_EVENTS = 1<<14,
+	SDL_INIT_JOYSTICK = 0,
+	SDL_INIT_GAMECONTROLLER = 0,
+
+	SDL_BLENDMODE_NONE = 0,
+	SDL_BLENDMODE_BLEND,
+
+	SDL_FLIP_NONE = 0,
+	SDL_FLIP_HORIZONTAL,
+	SDL_FLIP_VERTICAL,
+
+	SDL_PIXELFORMAT_ARGB8888 = 0x30128888,
+	SDL_PIXELFORMAT_XRGB8888 = 0x16161804,
+	SDL_PIXELFORMAT_INDEX8 = 0x13000801,
+	SDL_PIXELFORMAT_RGB24 = 0x17101803,
+	SDL_PIXELFORMAT_ABGR8888 = 0x16762004,
+	SDL_PIXELFORMAT_XBGR8888 = 0x16561804,
+	SDL_PIXELFORMAT_BGR24 = 0x17401803,
+	SDL_PIXELFORMAT_RGB888 = SDL_PIXELFORMAT_XRGB8888,
+	SDL_PIXELFORMAT_BGR888 = SDL_PIXELFORMAT_XBGR8888,
+
+	/* shit no one cares about */
+	SDL_TEXTUREACCESS_STREAMING = 0,
+	SDL_TEXTUREACCESS_STATIC = 0,
+	SDL_TEXTUREACCESS_TARGET = 0,
+	SDL_RENDERER_ACCELERATED = 0,
+	SDL_RENDERER_PRESENTVSYNC = 0,
+	SDL_RENDERER_TARGETTEXTURE = 0x8,
+	SDL_INIT_NOPARACHUTE = 0,
+	SDL_RENDERER_SOFTWARE = 0,
+	SDL_SWSURFACE = 0,
+
+	/* FIXME steal from rio and add missing? */
+	SDL_SYSTEM_CURSOR_ARROW = 0,
+	SDL_SYSTEM_CURSOR_IBEAM,
+	SDL_SYSTEM_CURSOR_WAIT,
+	SDL_SYSTEM_CURSOR_CROSSHAIR,
+	SDL_SYSTEM_CURSOR_WAITARROW,
+	SDL_SYSTEM_CURSOR_SIZENWSE,
+	SDL_SYSTEM_CURSOR_SIZENESW,
+	SDL_SYSTEM_CURSOR_SIZEWE,
+	SDL_SYSTEM_CURSOR_SIZENS,
+
+	SDL_ALPHA_OPAQUE = 0xff,
+	SDL_ALPHA_TRANSPARENT = 0x00,
+};
+
+enum {
+	SDL_BUTTON_LEFT = 0,
+	SDL_BUTTON_MIDDLE = 1,
+	SDL_BUTTON_RIGHT = 2,
+
+	SDL_BUTTON_LMASK = 1<<SDL_BUTTON_LEFT,
+	SDL_BUTTON_MMASK = 1<<SDL_BUTTON_MIDDLE,
+	SDL_BUTTON_RMASK = 1<<SDL_BUTTON_RIGHT,
+
+	KMOD_NONE = 0,
+};
+
+
+#define SDL_BUTTON(x) (1<<(x))
+
+#define SDL_MUSTLOCK(surface) (SDL_FALSE)
+
+#define SDL_HINT_RENDER_SCALE_QUALITY "SDL_HINT_RENDER_SCALE_QUALITY"
+#define SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4 "SDL_WINDOWS_NO_CLOSE_ON_ALT_F4"
+#define SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH "SDL_MOUSE_FOCUS_CLICKTHROUGH"
+#define SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING "SDL_WINDOWS_DISABLE_THREAD_NAMING"
+#define SDL_HINT_RENDER_VSYNC "SDL_RENDER_VSYNC"
+#define SDL_HINT_VIDEO_ALLOW_SCREENSAVER "SDL_VIDEO_ALLOW_SCREENSAVER"
+#define SDL_HINT_MAIN_CALLBACK_RATE "SDL_MAIN_CALLBACK_RATE"
+#define SDL_HINT_IOS_HIDE_HOME_INDICATOR "SDL_IOS_HIDE_HOME_INDICATOR"
+#define SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES "SDL_AUDIO_DEVICE_SAMPLE_FRAMES"
+
+struct SDL_Point {
+	int x, y;
+};
+
+struct SDL_DisplayMode {
+	int format;
+	int w;
+	int h;
+	int refresh_rate;
+};
+
+struct SDL_RendererInfo {
+	int max_texture_width;
+	int max_texture_height;
+};
+
+typedef enum {
+	SDL_HITTEST_NORMAL,
+	SDL_HITTEST_DRAGGABLE,
+	SDL_HITTEST_RESIZE_TOPLEFT,
+	SDL_HITTEST_RESIZE_TOP,
+	SDL_HITTEST_RESIZE_TOPRIGHT,
+	SDL_HITTEST_RESIZE_RIGHT,
+	SDL_HITTEST_RESIZE_BOTTOMRIGHT,
+	SDL_HITTEST_RESIZE_BOTTOM,
+	SDL_HITTEST_RESIZE_BOTTOMLEFT,
+	SDL_HITTEST_RESIZE_LEFT,
+} SDL_HitTestResult;
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_audio.h
@@ -1,0 +1,78 @@
+#ifndef _npe_SDL_audio_h_
+#define _npe_SDL_audio_h_
+
+/* FIXME: clean up old shit */
+
+typedef enum SDL_AudioFormat {
+	SDL_AUDIO_U8 = 1,
+	SDL_AUDIO_S8,
+	SDL_AUDIO_S16LE,
+	SDL_AUDIO_S16BE,
+	SDL_AUDIO_S32LE,
+	SDL_AUDIO_S32BE,
+	SDL_AUDIO_F32LE,
+	SDL_AUDIO_F32BE, /* FIXME not supported */
+	/* show me that BIG endian device of yours */
+	SDL_AUDIO_S16 = SDL_AUDIO_S16LE,
+	SDL_AUDIO_S32 = SDL_AUDIO_S32LE,
+	SDL_AUDIO_F32 = SDL_AUDIO_F32LE,
+} SDL_AudioFormat;
+
+typedef struct SDL_AudioSpec SDL_AudioSpec;
+typedef int SDL_AudioDeviceID;
+typedef struct SDL_AudioStream SDL_AudioStream;
+
+#pragma incomplete SDL_AudioStream
+
+typedef void (*SDL_AudioStreamCallback)(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount);
+
+extern SDL_AudioDeviceID SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
+extern SDL_AudioDeviceID SDL_AUDIO_DEVICE_DEFAULT_RECORDING;
+
+/* fuck this and fuck you */
+//#define SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK ((SDL_AudioDeviceID) 0xFFFFFFFFu)
+//#define SDL_AUDIO_DEVICE_DEFAULT_RECORDING ((SDL_AudioDeviceID) 0xFFFFFFFEu)
+
+struct SDL_AudioSpec {
+    SDL_AudioFormat format;
+    int channels;
+    int freq;
+};
+
+int SDL_GetNumAudioDevices(int);
+
+void SDL_LockAudioDevice(SDL_AudioDeviceID);
+void SDL_UnlockAudioDevice(SDL_AudioDeviceID);
+
+SDL_AudioDeviceID SDL_OpenAudioDevice(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec);
+void SDL_PauseAudioDevice(SDL_AudioDeviceID, SDL_bool);
+void SDL_CloseAudioDevice(SDL_AudioDeviceID);
+
+void SDL_PauseAudio(int pause_on);
+
+typedef struct SDL_AudioCVT SDL_AudioCVT;
+struct SDL_AudioCVT {
+	SDL_AudioFormat src_format;
+	SDL_AudioFormat dst_format;
+	Uint8 *buf;
+	int len;
+	int len_mult;
+};
+
+int SDL_BuildAudioCVT(SDL_AudioCVT *cvt, SDL_AudioFormat src_format, Uint8 src_channels, int src_rate, SDL_AudioFormat dst_format, Uint8 dst_channels, int dst_rate);
+int SDL_ConvertAudio(SDL_AudioCVT *cvt);
+
+int SDL_GetAudioStreamAvailable(SDL_AudioStream *stream);
+int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *buf, int len);
+bool SDL_PutAudioStreamData(SDL_AudioStream *stream, const void *buf, int len);
+bool SDL_ResumeAudioStreamDevice(SDL_AudioStream *stream);
+bool SDL_PauseAudioStreamDevice(SDL_AudioStream *stream);
+SDL_AudioDeviceID* SDL_GetAudioRecordingDevices(int *count);
+const char* SDL_GetAudioDeviceName(SDL_AudioDeviceID devid);
+SDL_AudioDeviceID* SDL_GetAudioPlaybackDevices(int *count);
+SDL_AudioStream* SDL_OpenAudioDeviceStream(SDL_AudioDeviceID devid, const SDL_AudioSpec *spec, SDL_AudioStreamCallback callback, void *userdata);
+bool SDL_GetAudioDeviceFormat(SDL_AudioDeviceID devid, SDL_AudioSpec *spec, int *sample_frames);
+bool SDL_SetAudioStreamFormat(SDL_AudioStream *stream, const SDL_AudioSpec *src_spec, const SDL_AudioSpec *dst_spec);
+void SDL_DestroyAudioStream(SDL_AudioStream *stream);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_endian.h
@@ -1,0 +1,60 @@
+#ifndef _npe_SDL_endian_h_
+#define _npe_SDL_endian_h_
+
+#define SDL_LIL_ENDIAN  1234
+#define SDL_BIG_ENDIAN  4321
+
+#if defined(__amd64__) || defined(__386__) || defined(__arm__) || defined(__arm64__) || defined(__spim__)
+#define SDL_BYTEORDER	SDL_LIL_ENDIAN
+#elif defined(__mips__) || defined(__power__)
+#define SDL_BYTEORDER	SDL_BIG_ENDIAN
+#endif
+
+static u16int
+SDL_Swap16(u16int x)
+{
+	return (x<<8) | (x>>8);
+}
+
+static u32int
+SDL_Swap32(u32int x)
+{
+	return ((x << 24) | ((x << 8) & 0x00FF0000) | ((x >> 8) & 0x0000FF00) | (x >> 24));
+}
+
+static u64int
+SDL_Swap64(u64int x)
+{
+    u32int hi, lo;
+
+    /* Separate into high and low 32-bit values and swap them */
+    lo = x & 0xFFFFFFFF;
+    x >>= 32;
+    hi = x & 0xFFFFFFFF;
+    x = SDL_Swap32(lo);
+    x <<= 32;
+    x |= SDL_Swap32(hi);
+    return (x);
+}
+
+#if SDL_BYTEORDER == SDL_LIL_ENDIAN
+#define SDL_SwapLE16(X) (X)
+#define SDL_SwapLE32(X) (X)
+#define SDL_SwapLE64(X) (X)
+#define SDL_SwapFloatLE(X)  (X)
+#define SDL_SwapBE16(X) SDL_Swap16(X)
+#define SDL_SwapBE32(X) SDL_Swap32(X)
+#define SDL_SwapBE64(X) SDL_Swap64(X)
+#define SDL_SwapFloatBE(X)  SDL_SwapFloat(X)
+#else
+#define SDL_SwapLE16(X) SDL_Swap16(X)
+#define SDL_SwapLE32(X) SDL_Swap32(X)
+#define SDL_SwapLE64(X) SDL_Swap64(X)
+#define SDL_SwapFloatLE(X)  SDL_SwapFloat(X)
+#define SDL_SwapBE16(X) (X)
+#define SDL_SwapBE32(X) (X)
+#define SDL_SwapBE64(X) (X)
+#define SDL_SwapFloatBE(X)  (X)
+#endif
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_events.h
@@ -1,0 +1,233 @@
+#ifndef _npe_SDL_events_h_
+#define _npe_SDL_events_h_
+
+#include <SDL3/SDL.h>
+
+/* FIXME: cleanup: remove old shit */
+enum {
+	SDL_FIRSTEVENT,
+	SDL_KEYDOWN,
+	SDL_KEYUP,
+	SDL_JOYAXISMOTION,
+	SDL_JOYBUTTONDOWN,
+	SDL_JOYBUTTONUP,
+	SDL_MOUSEBUTTONDOWN,
+	SDL_MOUSEBUTTONUP,
+	SDL_MOUSEWHEEL,
+	SDL_MOUSEMOTION,
+	SDL_QUIT,
+	SDL_DROPFILE,
+	SDL_TEXTINPUT,
+	SDL_WINDOWEVENT,
+	SDL_WINDOWEVENT_HIDDEN,
+	SDL_WINDOWEVENT_SHOWN,
+	SDL_WINDOWEVENT_FOCUS_LOST,
+	SDL_WINDOWEVENT_FOCUS_GAINED,
+	SDL_WINDOWEVENT_MOVED,
+	SDL_WINDOWEVENT_EXPOSED,
+	SDL_WINDOWEVENT_SIZE_CHANGED,
+	SDL_WINDOWEVENT_RESIZED = SDL_WINDOWEVENT_SIZE_CHANGED, /* FIXME I don't even fucking know... */
+	SDL_WINDOWEVENT_MINIMIZED,
+	SDL_WINDOWEVENT_MAXIMIZED,
+	SDL_WINDOWEVENT_RESTORED,
+	SDL_WINDOWEVENT_ENTER,
+	SDL_WINDOWEVENT_LEAVE,
+	SDL_WINDOWEVENT_CLOSE,
+	SDL_JOYBALLMOTION,
+	SDL_JOYHATMOTION,
+	SDL_LASTEVENT,
+
+	SDL_PRESSED = SDL_KEYDOWN,
+	SDL_RELEASED = SDL_KEYUP,
+
+	SDL_TEXTINPUTEVENT_TEXT_SIZE = UTFmax,
+
+	SDL_ADDEVENT = 0,
+	SDL_PEEKEVENT,
+	SDL_GETEVENT,
+
+	SDL_HAT_LEFTUP = 0,
+	SDL_HAT_LEFT,
+	SDL_HAT_LEFTDOWN,
+	SDL_HAT_UP,
+	SDL_HAT_CENTERED,
+	SDL_HAT_DOWN,
+	SDL_HAT_RIGHTUP,
+	SDL_HAT_RIGHT,
+	SDL_HAT_RIGHTDOWN,
+};
+
+typedef struct SDL_WindowEvent SDL_WindowEvent;
+typedef struct SDL_MouseWheelEvent SDL_MouseWheelEvent;
+typedef struct SDL_MouseButtonEvent SDL_MouseButtonEvent;
+typedef struct SDL_Keysym SDL_Keysym;
+typedef struct SDL_KeyboardEvent SDL_KeyboardEvent;
+typedef struct SDL_GamepadAxisEvent SDL_GamepadAxisEvent;
+typedef struct SDL_GamepadButtonEvent SDL_GamepadButtonEvent;
+typedef int SDL_eventaction;
+
+struct SDL_WindowEvent {
+	Uint32	type;
+	Uint32	timestamp;
+	Uint32	windowID;
+	Uint8	event;
+	Sint32	data1;
+	Sint32	data;
+};
+
+struct SDL_Keysym {
+	SDL_Scancode scancode;
+	SDL_Keycode sym;
+	Uint16 mod;
+};
+
+struct SDL_MouseWheelEvent {
+	Uint32	type;
+	Uint32	timestamp;
+	Uint32	windowID;
+	Uint32	which;
+	Sint32	x;
+	Sint32	y;
+	Uint32	direction;
+};
+
+struct SDL_MouseButtonEvent {
+	Uint32	type;
+	Uint32	timestamp;
+	Uint32	windowID;
+	Uint32	which;
+	Uint8	button;
+	Uint8	state;
+	Uint8	clicks;
+	Sint32	x;
+	Sint32	y;
+};
+
+typedef enum SDL_EventType {
+	SDL_EVENT_QUIT = 0x100,
+	SDL_EVENT_TERMINATING,
+	SDL_EVENT_LOW_MEMORY,
+	SDL_EVENT_WILL_ENTER_BACKGROUND,
+	SDL_EVENT_DID_ENTER_BACKGROUND,
+	SDL_EVENT_WILL_ENTER_FOREGROUND,
+	SDL_EVENT_DID_ENTER_FOREGROUND,
+	SDL_EVENT_WINDOW_SHOWN = 0x202,
+	SDL_EVENT_WINDOW_HIDDEN,
+	SDL_EVENT_WINDOW_EXPOSED,
+	SDL_EVENT_WINDOW_MOVED,
+	SDL_EVENT_WINDOW_RESIZED,
+	SDL_EVENT_KEY_DOWN = 0x300,
+	SDL_EVENT_KEY_UP,
+	SDL_EVENT_GAMEPAD_AXIS_MOTION = 0x650,
+	SDL_EVENT_GAMEPAD_BUTTON_DOWN,
+	SDL_EVENT_GAMEPAD_BUTTON_UP,
+	SDL_EVENT_GAMEPAD_ADDED,
+	SDL_EVENT_GAMEPAD_REMOVED,
+	SDL_EVENT_USER = 0x8000,
+	SDL_EVENT_LAST = 0xFFFF,
+} SDL_EventType;
+
+struct SDL_KeyboardEvent {
+	SDL_EventType type;	 /**< SDL_EVENT_KEY_DOWN or SDL_EVENT_KEY_UP */
+	//Uint32 reserved;
+	Uint64 timestamp;	   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+	SDL_WindowID windowID;  /**< The window with keyboard focus, if any */
+	SDL_KeyboardID which;   /**< The keyboard instance id, or 0 if unknown or virtual */
+	SDL_Scancode scancode;  /**< SDL physical key code */
+	SDL_Keycode key;		/**< SDL virtual key code */
+	SDL_Keymod mod;		 /**< current key modifiers */
+	Rune raw;			   /**< The platform dependent scancode for this event */
+	bool down;			  /**< true if the key is pressed */
+	bool repeat;			/**< true if this is a key repeat */
+};
+struct SDL_GamepadAxisEvent {
+	SDL_EventType type; /**< SDL_EVENT_GAMEPAD_AXIS_MOTION */
+	//Uint32 reserved;
+	Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+	SDL_JoystickID which; /**< The joystick instance id */
+	Uint8 axis;		 /**< The gamepad axis (SDL_GamepadAxis) */
+	//Uint8 padding1;
+	//Uint8 padding2;
+	//Uint8 padding3;
+	Sint16 value;	   /**< The axis value (range: -32768 to 32767) */
+	//Uint16 padding4;
+};
+struct SDL_GamepadButtonEvent {
+	SDL_EventType type; /**< SDL_EVENT_GAMEPAD_BUTTON_DOWN or SDL_EVENT_GAMEPAD_BUTTON_UP */
+	//Uint32 reserved;
+	Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+	SDL_JoystickID which; /**< The joystick instance id */
+	Uint8 button;	   /**< The gamepad button (SDL_GamepadButton) */
+	bool down;	  /**< true if the button is pressed */
+	//Uint8 padding1;
+	//Uint8 padding2;
+};
+typedef union SDL_Event {
+	Uint32 type;
+	SDL_KeyboardEvent key;
+	SDL_GamepadAxisEvent gaxis;
+	SDL_GamepadButtonEvent gbutton;
+	//Uint8 padding[128];
+} SDL_Event;
+
+typedef int (*SDL_EventFilter)(void *userdata, SDL_Event *event);
+
+/*
+struct SDL_Event {
+	int type;
+	SDL_WindowEvent window;
+	struct {
+		SDL_Keysym keysym;
+		int repeat;
+		int state;
+	}key;
+	SDL_MouseButtonEvent button;
+	struct {
+		int button;
+		int state;
+	}cbutton;
+	struct {
+		int button;
+	}jbutton;
+	struct {
+		int value;
+		int axis;
+	}jaxis;
+	struct {
+		int xrel;
+		int yrel;
+	}jball;
+	struct {
+		int hat;
+		int value;
+	}jhat;
+	struct {
+		int x, y;
+		int xrel, yrel;
+		int state;
+	}motion;
+	struct {
+		char text[SDL_TEXTINPUTEVENT_TEXT_SIZE+1];
+	}text;
+	SDL_MouseWheelEvent wheel;
+	struct {
+		char *file;
+	}drop;
+};
+*/
+
+int SDL_EventState(Uint32, int);
+int SDL_PollEvent(SDL_Event *event);
+int SDL_PushEvent(SDL_Event *event);
+int SDL_WaitEvent(SDL_Event *event);
+Uint32 SDL_RegisterEvents(int);
+int SDL_PeepEvents(SDL_Event *events, int numevents, SDL_eventaction action, Uint32 minType, Uint32 maxType);
+Uint32 SDL_GetWindowID(SDL_Window *window);
+void SDL_PumpEvents(void);
+void SDL_SetEventFilter(SDL_EventFilter filter, void *userdata);
+SDL_Scancode SDL_GetScancodeFromKey(SDL_Keycode key);
+Uint8* SDL_GetKeyboardState(int *numkeys);
+char* SDL_GetKeyName(SDL_Keycode key);
+SDL_Keycode SDL_GetKeyFromName(char *name);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_gamepad.h
@@ -1,0 +1,89 @@
+#ifndef _npe_SDL_gamepad_h_
+#define _npe_SDL_gamepad_h_
+
+typedef struct SDL_Gamepad SDL_Gamepad;
+
+#pragma incomplete SDL_Gamepad
+
+typedef enum SDL_GamepadButton {
+	SDL_GAMEPAD_BUTTON_INVALID = -1,
+	SDL_GAMEPAD_BUTTON_SOUTH,
+	SDL_GAMEPAD_BUTTON_EAST,
+	SDL_GAMEPAD_BUTTON_WEST,
+	SDL_GAMEPAD_BUTTON_NORTH,
+	SDL_GAMEPAD_BUTTON_BACK,
+	SDL_GAMEPAD_BUTTON_GUIDE,
+	SDL_GAMEPAD_BUTTON_START,
+	SDL_GAMEPAD_BUTTON_LEFT_STICK,
+	SDL_GAMEPAD_BUTTON_RIGHT_STICK,
+	SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
+	SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER,
+	SDL_GAMEPAD_BUTTON_DPAD_UP,
+	SDL_GAMEPAD_BUTTON_DPAD_DOWN,
+	SDL_GAMEPAD_BUTTON_DPAD_LEFT,
+	SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
+	SDL_GAMEPAD_BUTTON_MISC1,
+	SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
+	SDL_GAMEPAD_BUTTON_LEFT_PADDLE1,
+	SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
+	SDL_GAMEPAD_BUTTON_LEFT_PADDLE2,
+	SDL_GAMEPAD_BUTTON_TOUCHPAD,
+	SDL_GAMEPAD_BUTTON_MISC2,
+	SDL_GAMEPAD_BUTTON_MISC3,
+	SDL_GAMEPAD_BUTTON_MISC4,
+	SDL_GAMEPAD_BUTTON_MISC5,
+	SDL_GAMEPAD_BUTTON_MISC6,
+	SDL_GAMEPAD_BUTTON_COUNT
+} SDL_GamepadButton;
+
+typedef enum SDL_GamepadAxis {
+	SDL_GAMEPAD_AXIS_INVALID = -1,
+	SDL_GAMEPAD_AXIS_LEFTX,
+	SDL_GAMEPAD_AXIS_LEFTY,
+	SDL_GAMEPAD_AXIS_RIGHTX,
+	SDL_GAMEPAD_AXIS_RIGHTY,
+	SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
+	SDL_GAMEPAD_AXIS_RIGHT_TRIGGER,
+	SDL_GAMEPAD_AXIS_COUNT
+} SDL_GamepadAxis;
+
+typedef enum SDL_GamepadButtonLabel {
+    SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN,
+    SDL_GAMEPAD_BUTTON_LABEL_A,
+    SDL_GAMEPAD_BUTTON_LABEL_B,
+    SDL_GAMEPAD_BUTTON_LABEL_X,
+    SDL_GAMEPAD_BUTTON_LABEL_Y,
+    SDL_GAMEPAD_BUTTON_LABEL_CROSS,
+    SDL_GAMEPAD_BUTTON_LABEL_CIRCLE,
+    SDL_GAMEPAD_BUTTON_LABEL_SQUARE,
+    SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE
+} SDL_GamepadButtonLabel;
+
+typedef enum SDL_GamepadType {
+    SDL_GAMEPAD_TYPE_UNKNOWN = 0,
+    SDL_GAMEPAD_TYPE_STANDARD,
+    SDL_GAMEPAD_TYPE_XBOX360,
+    SDL_GAMEPAD_TYPE_XBOXONE,
+    SDL_GAMEPAD_TYPE_PS3,
+    SDL_GAMEPAD_TYPE_PS4,
+    SDL_GAMEPAD_TYPE_PS5,
+    SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO,
+    SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT,
+    SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT,
+    SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR,
+    SDL_GAMEPAD_TYPE_GAMECUBE,
+    SDL_GAMEPAD_TYPE_COUNT
+} SDL_GamepadType;
+
+SDL_JoystickID* SDL_GetGamepads(int *count);
+int SDL_AddGamepadMappingsFromIO(SDL_IOStream *src, bool closeio);
+bool SDL_IsGamepad(SDL_JoystickID instance_id);
+SDL_Gamepad* SDL_OpenGamepad(SDL_JoystickID instance_id);
+const char* SDL_GetGamepadName(SDL_Gamepad *gamepad);
+const char* SDL_GetGamepadStringForAxis(SDL_GamepadAxis axis);
+const char* SDL_GetGamepadStringForButton(SDL_GamepadButton button);
+SDL_GamepadButtonLabel SDL_GetGamepadButtonLabelForType(SDL_GamepadType type, SDL_GamepadButton button);
+SDL_GamepadButtonLabel SDL_GetGamepadButtonLabel(SDL_Gamepad *gamepad, SDL_GamepadButton button);
+void SDL_CloseGamepad(SDL_Gamepad *gamepad);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_image.h
@@ -1,0 +1,41 @@
+#ifndef _npe_SDL_image_h_
+#define _npe_SDL_image_h_
+
+#define SDL_IMAGE_MAJOR_VERSION 2
+#define SDL_IMAGE_MINOR_VERSION 0
+#define SDL_IMAGE_PATCHLEVEL 6
+
+enum {
+	IMG_INIT_JPG = 1<<0,
+	IMG_INIT_PNG = 1<<1,
+	IMG_INIT_TIF = 1<<2,
+};
+
+int IMG_Init(int flags);
+void IMG_Quit(void);
+
+SDL_Surface *IMG_LoadTyped_RW(SDL_IOStream *src, int freesrc, const char *type);
+SDL_Surface *IMG_Load(const char *file);
+SDL_Surface *IMG_Load_RW(SDL_IOStream *src, int freesrc);
+
+SDL_Texture *IMG_LoadTexture(SDL_Renderer *renderer, const char *file);
+SDL_Texture *IMG_LoadTexture_RW(SDL_Renderer *renderer, SDL_IOStream *src, int freesrc);
+SDL_Texture *IMG_LoadTextureTyped_RW(SDL_Renderer *renderer, SDL_IOStream *src, int freesrc, const char *type);
+
+SDL_Surface *IMG_LoadBMP_RW(SDL_IOStream *src);
+SDL_Surface *IMG_LoadGIF_RW(SDL_IOStream *src);
+SDL_Surface *IMG_LoadJPG_RW(SDL_IOStream *src);
+SDL_Surface *IMG_LoadPNG_RW(SDL_IOStream *src);
+SDL_Surface *IMG_LoadPNM_RW(SDL_IOStream *src);
+SDL_Surface *IMG_LoadTGA_RW(SDL_IOStream *src);
+SDL_Surface *IMG_LoadTIF_RW(SDL_IOStream *src);
+
+int IMG_SavePNG(SDL_Surface *surface, const char *file);
+int IMG_SavePNG_RW(SDL_Surface *surface, SDL_IOStream *dst, int freedst);
+int IMG_SaveJPG(SDL_Surface *surface, const char *file, int quality);
+int IMG_SaveJPG_RW(SDL_Surface *surface, SDL_IOStream *dst, int freedst, int quality);
+
+#define IMG_SetError SDL_SetError
+#define IMG_GetError SDL_GetError
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_init.h
@@ -1,0 +1,12 @@
+#ifndef _npe_SDL_init_h_
+#define _npe_SDL_init_h_
+
+typedef enum SDL_AppResult {
+	SDL_APP_CONTINUE,
+	SDL_APP_SUCCESS,
+	SDL_APP_FAILURE,
+} SDL_AppResult;
+
+bool SDL_SetAppMetadata(const char *appname, const char *appversion, const char *appidentifier);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_iostream.h
@@ -1,0 +1,23 @@
+#ifndef _npe_SDL_iostream_h_
+#define _npe_SDL_iostream_h_
+
+typedef struct SDL_IOStream SDL_IOStream;
+
+#pragma incomplete SDL_IOStream
+
+enum {
+	SDL_IO_SEEK_SET,
+	SDL_IO_SEEK_CUR,
+	SDL_IO_SEEK_END,
+};
+
+SDL_IOStream* SDL_IOFromFile(const char *file, const char *mode);
+SDL_IOStream* SDL_IOFromConstMem(const void *mem, size_t size);
+size_t SDL_WriteIO(SDL_IOStream *context, const void *ptr, size_t size);
+bool SDL_CloseIO(SDL_IOStream *context);
+
+/* FIXME */
+size_t	SDL_ReadIO(SDL_IOStream *, void *, size_t);
+
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_joystick.h
@@ -1,0 +1,22 @@
+#ifndef _npe_SDL_joystick_h_
+#define _npe_SDL_joystick_h_
+
+typedef Uint32 SDL_JoystickID;
+
+typedef struct SDL_Joystick SDL_Joystick;
+
+int SDL_NumJoysticks(void);
+SDL_Joystick *SDL_JoystickOpen(int n);
+void SDL_JoystickClose(SDL_Joystick*);
+int SDL_JoystickNumAxes(SDL_Joystick*);
+int SDL_JoystickNumButtons(SDL_Joystick*);
+int SDL_JoystickNumHats(SDL_Joystick*);
+int SDL_JoystickNumBalls(SDL_Joystick*);
+int SDL_JoystickEventState(int);
+void SDL_JoystickUpdate(void);
+Uint8 SDL_JoystickGetHat(SDL_Joystick*,int);
+Sint16 SDL_JoystickGetAxis(SDL_Joystick*,int);
+Uint8 SDL_JoystickGetButton(SDL_Joystick*,int);
+char* SDL_JoystickName(SDL_Joystick*);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_keyboard.h
@@ -1,0 +1,8 @@
+#ifndef _npe_SDL_keyboard_h_
+#define _npe_SDL_keyboard_h_
+
+typedef Uint32 SDL_KeyboardID;
+
+const char* SDL_GetScancodeName(SDL_Scancode scancode);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_keycode.h
@@ -1,0 +1,137 @@
+#ifndef _npe_SDL_keycode_h_
+#define _npe_SDL_keycode_h_
+
+#include <keyboard.h>
+
+typedef int SDL_Keycode;
+typedef int SDL_Keymod;
+
+enum {
+	SDLK_a = 'a',
+	SDLK_b,
+	SDLK_c,
+	SDLK_d,
+	SDLK_e,
+	SDLK_f,
+	SDLK_g,
+	SDLK_h,
+	SDLK_i,
+	SDLK_j,
+	SDLK_k,
+	SDLK_l,
+	SDLK_m,
+	SDLK_n,
+	SDLK_o,
+	SDLK_p,
+	SDLK_q,
+	SDLK_r,
+	SDLK_s,
+	SDLK_t,
+	SDLK_u,
+	SDLK_v,
+	SDLK_w,
+	SDLK_x,
+	SDLK_y,
+	SDLK_z,
+	SDLK_0 = '0',
+	SDLK_1,
+	SDLK_2,
+	SDLK_3,
+	SDLK_4,
+	SDLK_5,
+	SDLK_6,
+	SDLK_7,
+	SDLK_8,
+	SDLK_9,
+	SDLK_DELETE = Kdel,
+	SDLK_RETURN = '\r',
+	SDLK_ESCAPE = Kesc,
+	SDLK_LESS = '<',
+	SDLK_SPACE = ' ',
+	SDLK_TAB = '\t',
+	SDLK_LEFT = Kleft,
+	SDLK_RIGHT = Kright,
+	SDLK_DOWN = Kdown,
+	SDLK_UP = Kup,
+	SDLK_F1 = KF|1,
+	SDLK_F2,
+	SDLK_F3,
+	SDLK_F4,
+	SDLK_F5,
+	SDLK_F6,
+	SDLK_F7,
+	SDLK_F8,
+	SDLK_F9,
+	SDLK_F10,
+	SDLK_F11,
+	SDLK_F12,
+
+	SDLK_INSERT = Kins,
+	SDLK_PAGEUP = Kpgup,
+	SDLK_PAGEDOWN = Kpgdown,
+	SDLK_HOME = Khome,
+	SDLK_END = Kend,
+	SDLK_BACKSPACE = Kbs,
+	SDLK_MINUS = '-',
+	SDLK_PLUS = '+',
+	SDLK_EQUALS = '=',
+	SDLK_UNDERSCORE = '_',
+	SDLK_LEFTBRACKET = '[',
+	SDLK_RIGHTBRACKET = ']',
+	SDLK_SEMICOLON = ';',
+	SDLK_QUOTE = '\'',
+	SDLK_BACKQUOTE = '`',
+	SDLK_BACKSLASH = '\\',
+	SDLK_COMMA = ',',
+	SDLK_PERIOD = '.',
+	SDLK_SLASH = '/',
+
+	SDLK_LALT = Kalt,
+	SDLK_RALT = Kaltgr, /* FIXME what about keyboards without it? */
+	SDLK_LSHIFT = Kshift,
+	SDLK_RSHIFT = Kshift,
+	SDLK_LCTRL = Kctl,
+	SDLK_RCTRL = Kctl,
+	SDLK_CAPSLOCK = Kcaps,
+
+
+	/* FIXME not bound to anything */
+	SDLK_UNKNOWN = -99999,
+	SDLK_KP_ENTER,
+	SDLK_AC_BACK,
+	SDLK_PAUSE,
+	SDLK_KP_DIVIDE,
+	SDLK_MODE,
+	SDLK_KP_PLUS,
+	SDLK_NUMLOCKCLEAR,
+	SDLK_SCROLLLOCK,
+	SDLK_KP_PERIOD,
+	SDLK_KP_7,
+	SDLK_KP_8,
+	SDLK_KP_9,
+	SDLK_KP_MINUS,
+	SDLK_KP_4,
+	SDLK_KP_5,
+	SDLK_KP_6,
+	SDLK_KP_1,
+	SDLK_KP_2,
+	SDLK_KP_3,
+	SDLK_KP_0,
+
+	SDL_KMOD_LSHIFT = 1<<0,
+	SDL_KMOD_RSHIFT = 1<<1,
+	SDL_KMOD_LCTRL = 1<<6,
+	SDL_KMOD_RCTRL = 1<<7,
+	SDL_KMOD_LALT = 1<<8,
+	SDL_KMOD_RALT = 1<<9,
+	SDL_KMOD_LGUI = 1<<10,
+	SDL_KMOD_RGUI = SDL_KMOD_LGUI,
+	SDL_KMOD_CAPS = 1<<13,
+
+	SDL_KMOD_SHIFT = SDL_KMOD_LSHIFT|SDL_KMOD_RSHIFT,
+	SDL_KMOD_CTRL = SDL_KMOD_LCTRL|SDL_KMOD_RCTRL,
+	SDL_KMOD_ALT = SDL_KMOD_LALT|SDL_KMOD_RALT,
+	SDL_KMOD_GUI = SDL_KMOD_LGUI|SDL_KMOD_RGUI,
+};
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_log.h
@@ -1,0 +1,46 @@
+#ifndef _npe_SDL_log_h_
+#define _npe_SDL_log_h_
+
+typedef enum SDL_LogCategory {
+	SDL_LOG_CATEGORY_APPLICATION,
+	SDL_LOG_CATEGORY_ERROR,
+	SDL_LOG_CATEGORY_ASSERT,
+	SDL_LOG_CATEGORY_SYSTEM,
+	SDL_LOG_CATEGORY_AUDIO,
+	SDL_LOG_CATEGORY_VIDEO,
+	SDL_LOG_CATEGORY_RENDER,
+	SDL_LOG_CATEGORY_INPUT,
+	SDL_LOG_CATEGORY_TEST,
+	SDL_LOG_CATEGORY_COUNT,
+} SDL_LogCategory;
+
+typedef enum SDL_LogPriority {
+    SDL_LOG_PRIORITY_INVALID,
+    SDL_LOG_PRIORITY_TRACE,
+    SDL_LOG_PRIORITY_VERBOSE,
+    SDL_LOG_PRIORITY_DEBUG,
+    SDL_LOG_PRIORITY_INFO,
+    SDL_LOG_PRIORITY_WARN,
+    SDL_LOG_PRIORITY_ERROR,
+    SDL_LOG_PRIORITY_CRITICAL,
+    SDL_LOG_PRIORITY_COUNT
+} SDL_LogPriority;
+
+typedef void (*SDL_LogOutputFunction)(void *userdata, int category, SDL_LogPriority priority, const char *message);
+
+void SDL_Log(const char *fmt, ...);
+void SDL_LogDebug(int category, const char *fmt, ...);
+void SDL_LogInfo(int category, const char *fmt, ...);
+void SDL_LogError(int category, const char *fmt, ...);
+void SDL_LogCritical(int category, const char *fmt, ...);
+void SDL_GetLogOutputFunction(SDL_LogOutputFunction *callback, void **userdata);
+void SDL_SetLogOutputFunction(SDL_LogOutputFunction callback, void *userdata);
+void SDL_SetLogPriorities(SDL_LogPriority priority);
+
+#pragma	varargck	argpos	SDL_Log	1
+#pragma	varargck	argpos	SDL_LogDebug	2
+#pragma	varargck	argpos	SDL_LogInfo	2
+#pragma	varargck	argpos	SDL_LogError	2
+#pragma	varargck	argpos	SDL_LogCritical	2
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_main.h
@@ -1,0 +1,36 @@
+#ifndef _npe_SDL_main_h_
+#define _npe_SDL_main_h_
+
+SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]);
+SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event);
+SDL_AppResult SDL_AppIterate(void *appstate);
+void SDL_AppQuit(void *appstate, SDL_AppResult result);
+
+#ifdef SDL_MAIN_USE_CALLBACKS
+int
+npe_main_renamed(int argc, char **argv)
+{
+	int r;
+	void *p;
+	SDL_Event e;
+
+	p = nil;
+	r = SDL_AppInit(&p, argc, argv);
+	/* FIXME: start event readers, or do ifdefs */
+	for(;r == SDL_APP_CONTINUE;){
+		while(SDL_PollEvent(&e))
+			if((r = SDL_AppEvent(p, &e)) != SDL_APP_CONTINUE)
+				break;
+		if(r != SDL_APP_CONTINUE)
+			break;
+		r = SDL_AppIterate(p);
+	}
+	SDL_AppQuit(p, r);
+	if(r == SDL_APP_FAILURE)
+		sysfatal("%r");
+	SDL_Quit();
+	return 0;
+}
+#endif
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_mixer.h
@@ -1,0 +1,60 @@
+#ifndef _npe_SDL_mixer_h_
+#define _npe_SDL_mixer_h_
+
+#include <SDL2/SDL_io.h>
+
+/* The internal format for an audio chunk */
+typedef struct Mix_Chunk {
+	int allocated;
+	Uint8 *abuf;
+	Uint32 alen;
+	Uint8 volume;		/* Per-sample volume, 0-128 */
+} Mix_Chunk;
+
+typedef struct Mix_Music {
+	int type;
+	int loops;
+	int fd;
+	char *loc;
+} Mix_Music;
+
+typedef void (*Mix_EffectFunc_t)(int chan, void *stream, int len, void *udata);
+typedef void (*Mix_EffectDone_t)(int chan, void *udata);
+typedef void (*Mix_MixCallback)(void *udata, Uint8 *stream, int len);
+
+int Mix_OpenAudio(int,Uint16,int,int);
+char* Mix_GetError(void);
+int Mix_RegisterEffect(int,Mix_EffectFunc_t,Mix_EffectDone_t,void*);
+Mix_Chunk* Mix_QuickLoad_RAW(Uint8*, Uint32);
+int Mix_PlayChannel(int,Mix_Chunk*,int);
+int Mix_HaltChannel(int);
+void Mix_FreeChunk(Mix_Chunk*);
+void Mix_CloseAudio(void);
+int Mix_Init(int);
+int Mix_VolumeMusic(int);
+int Mix_PlayingMusic(void);
+int Mix_PausedMusic(void);
+void Mix_ResumeMusic(void);
+void Mix_PauseMusic(void);
+int Mix_PlayingMusic(void);
+int Mix_PausedMusic(void);
+int Mix_HaltMusic(void);
+int Mix_PlayMusic(Mix_Music *music, int loops);
+Mix_Music* Mix_LoadMUS_RW(SDL_IOStream *src, int freesrc);
+Mix_Music* Mix_LoadMUS(char *filename);
+int Mix_SetPanning(int channel, Uint8 left, Uint8 right);
+int Mix_Playing(int channel);
+int Mix_QuerySpec(int *frequency, Uint16 *format, int *channels);
+int Mix_AllocateChannels(int numchans);
+void Mix_HookMusic(Mix_MixCallback mix_func, void *arg);
+
+enum {
+	MIX_INIT_MID = 1,
+
+	MIX_DEFAULT_FORMAT = 1,
+
+	SDL_MIX_MAXVOLUME = 100,
+	MIX_MAX_VOLUME = SDL_MIX_MAXVOLUME,
+};
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_mutex.h
@@ -1,0 +1,26 @@
+#ifndef _npe_SDL_mutex_h_
+#define _npe_SDL_mutex_h_
+
+typedef struct SDL_Mutex SDL_Mutex;
+typedef struct SDL_Condition SDL_Condition;
+
+struct SDL_Mutex {
+	Lock l;
+};
+
+struct SDL_Condition {
+	QLock;
+	Rendez;
+};
+
+SDL_Mutex* SDL_CreateMutex(void);
+void SDL_DestroyMutex(SDL_Mutex*);
+int SDL_LockMutex(SDL_Mutex*);
+int SDL_UnlockMutex(SDL_Mutex*);
+
+SDL_Condition* SDL_CreateCondition(void);
+void SDL_SignalCondition(SDL_Condition *cond);
+void SDL_WaitCondition(SDL_Condition *cond, SDL_Mutex *mutex);
+void SDL_DestroyCondition(SDL_Condition *cond);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_properties.h
@@ -1,0 +1,13 @@
+#ifndef _npe_SDL_properties_h_
+#define _npe_SDL_properties_h_
+
+//typedef Uint32 SDL_PropertiesID;
+typedef SDL_Texture* SDL_PropertiesID;
+
+#define SDL_PROP_TEXTURE_WIDTH_NUMBER	"SDL.texture.width"
+#define SDL_PROP_TEXTURE_HEIGHT_NUMBER	"SDL.texture.height"
+
+SDL_PropertiesID SDL_GetTextureProperties(SDL_Texture *texture);
+Sint64 SDL_GetNumberProperty(SDL_PropertiesID props, const char *name, Sint64 default_value);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_quit.h
@@ -1,0 +1,6 @@
+#ifndef _npe_SDL_quit_h_
+#define _npe_SDL_quit_h_
+
+int SDL_QuitRequested(void);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_render.h
@@ -1,0 +1,78 @@
+#ifndef _npe_SDL_render_h_
+#define _npe_SDL_render_h_
+
+typedef struct SDL_Renderer SDL_Renderer;
+typedef struct SDL_Rect SDL_Rect;
+typedef struct SDL_FPoint SDL_FPoint;
+typedef struct SDL_FRect SDL_FRect;
+typedef struct SDL_Texture SDL_Texture;
+
+#pragma incomplete SDL_Renderer
+#pragma incomplete SDL_Texture
+
+struct SDL_Rect {
+	int x, y, w, h;
+};
+
+struct SDL_FPoint {
+    float x;
+    float y;
+};
+
+struct SDL_FRect {
+    float x;
+    float y;
+    float w;
+    float h;
+};
+
+typedef enum SDL_ScaleMode {
+    SDL_SCALEMODE_INVALID = -1,
+    SDL_SCALEMODE_NEAREST,  /**< nearest pixel sampling */
+    SDL_SCALEMODE_LINEAR,   /**< linear filtering */
+    SDL_SCALEMODE_PIXELART  /**< nearest pixel sampling with improved scaling for pixel art, available since SDL 3.4.0 */
+} SDL_ScaleMode;
+
+typedef enum SDL_RendererLogicalPresentation {
+    SDL_LOGICAL_PRESENTATION_DISABLED,
+    SDL_LOGICAL_PRESENTATION_STRETCH,
+    SDL_LOGICAL_PRESENTATION_LETTERBOX,
+    SDL_LOGICAL_PRESENTATION_OVERSCAN,
+    SDL_LOGICAL_PRESENTATION_INTEGER_SCALE
+} SDL_RendererLogicalPresentation;
+
+bool SDL_CreateWindowAndRenderer(const char *title, int width, int height, SDL_WindowFlags window_flags, SDL_Window **window, SDL_Renderer **renderer);
+int SDL_RenderClear(SDL_Renderer *renderer);
+int SDL_RenderFillRect(SDL_Renderer *r, SDL_FRect *rect);
+void SDL_RenderGetScale(SDL_Renderer *renderer, float *scaleX, float *scaleY);
+int SDL_RenderReadPixels(SDL_Renderer *rend, SDL_Rect *rect, Uint32 fmt, void *pixels, int pitch);
+bool SDL_RenderPresent(SDL_Renderer *renderer);
+bool SDL_SetRenderLogicalPresentation(SDL_Renderer *renderer, int w, int h, SDL_RendererLogicalPresentation mode);
+void SDL_DestroyRenderer(SDL_Renderer *renderer);
+SDL_Renderer *SDL_CreateRenderer(SDL_Window *window, char *name);
+int SDL_SetRenderDrawBlendMode(SDL_Renderer *renderer, SDL_BlendMode blendMode);
+int SDL_GetRendererOutputSize(SDL_Renderer *renderer, int *w, int *h);
+int SDL_GetRendererInfo(SDL_Renderer *renderer, SDL_RendererInfo *info);
+void SDL_RenderGetViewport(SDL_Renderer *rebderer, SDL_Rect *rect);
+int SDL_SetRenderDrawColor(SDL_Renderer *r, Uint8 r, Uint8 g, Uint8 b, Uint8 a);
+int SDL_RenderSetIntegerScale(SDL_Renderer *r, SDL_bool enable);
+int SDL_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture);
+SDL_Texture* SDL_GetRenderTarget(SDL_Renderer *renderer);
+bool SDL_RenderTexture(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_FRect *srcrect, const SDL_FRect *dstrect);
+bool SDL_RenderLines(SDL_Renderer *renderer, const SDL_FPoint *points, int count);
+bool SDL_RenderPoints(SDL_Renderer *renderer, const SDL_FPoint *points, int count);
+bool SDL_SetRenderVSync(SDL_Renderer *renderer, int vsync);
+
+/* FIXME */
+int SDL_UpdateTexture(SDL_Texture *texture, SDL_Rect *rect, void *pixels, int pitch);
+SDL_Texture *SDL_CreateTexture(SDL_Renderer *renderer, Uint32 format, int access, int w, int h);
+int SDL_SetTextureBlendMode(SDL_Texture *texture, SDL_BlendMode blendMode);
+bool SDL_SetTextureScaleMode(SDL_Texture *texture, SDL_ScaleMode scaleMode);
+int SDL_SetTextureAlphaMod(SDL_Texture *texture, Uint8 alpha);
+int SDL_SetTextureColorMod(SDL_Texture *texture, Uint8 r, Uint8 g, Uint8 b);
+bool SDL_GetTextureSize(SDL_Texture *texture, float *w, float *h);
+int SDL_LockTexture(SDL_Texture *texture, const SDL_Rect *rect, void **pixels, int *pitch);
+int SDL_UnlockTexture(SDL_Texture *texture);
+void SDL_DestroyTexture(SDL_Texture *texture);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_rwops.h
@@ -1,0 +1,32 @@
+#ifndef _npe_SDL_rwops_h_
+#define _npe_SDL_rwops_h_
+
+typedef struct SDL_RWops SDL_RWops;
+typedef struct npe_sdl_rwops npe_sdl_rwops;
+#pragma incomplete npe_sdl_rwops
+
+enum {
+	RW_SEEK_SET,
+	RW_SEEK_CUR,
+	RW_SEEK_END,
+};
+
+struct SDL_RWops {
+	vlong (*size)(struct SDL_RWops *);
+	vlong (*seek)(struct SDL_RWops *, vlong, int);
+	size_t (*read)(struct SDL_RWops *, void *, size_t, size_t);
+	size_t (*write)(struct SDL_RWops *, const void *, size_t, size_t);
+	int (*close)(struct SDL_RWops *);
+	npe_sdl_rwops *p;
+};
+
+SDL_RWops *SDL_RWFromFile(const char *, const char *);
+SDL_RWops *SDL_RWFromMem(void*, int);
+size_t SDL_RWread(SDL_RWops *, void *, size_t, size_t);
+size_t SDL_RWwrite(SDL_RWops *, const void *, size_t, size_t);
+vlong SDL_RWseek(SDL_RWops *, vlong, int);
+vlong SDL_RWtell(SDL_RWops *);
+vlong SDL_RWsize(SDL_RWops *);
+int SDL_RWclose(SDL_RWops *);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_scancode.h
@@ -1,0 +1,132 @@
+#ifndef _npe_SDL_scancode_h_
+#define _npe_SDL_scancode_h_
+
+typedef enum SDL_Scancode {
+	/* these HAVE to be in this order for notes to work in ft2-clone */
+	SDL_SCANCODE_UNKNOWN,
+	SDL_SCANCODE_A = 0x04,
+	SDL_SCANCODE_B,
+	SDL_SCANCODE_C,
+	SDL_SCANCODE_D,
+	SDL_SCANCODE_E,
+	SDL_SCANCODE_F,
+	SDL_SCANCODE_G,
+	SDL_SCANCODE_H,
+	SDL_SCANCODE_I,
+	SDL_SCANCODE_J,
+	SDL_SCANCODE_K,
+	SDL_SCANCODE_L,
+	SDL_SCANCODE_M,
+	SDL_SCANCODE_N,
+	SDL_SCANCODE_O,
+	SDL_SCANCODE_P,
+	SDL_SCANCODE_Q,
+	SDL_SCANCODE_R,
+	SDL_SCANCODE_S,
+	SDL_SCANCODE_T,
+	SDL_SCANCODE_U,
+	SDL_SCANCODE_V,
+	SDL_SCANCODE_W,
+	SDL_SCANCODE_X,
+	SDL_SCANCODE_Y,
+	SDL_SCANCODE_Z,
+	SDL_SCANCODE_1,
+	SDL_SCANCODE_2,
+	SDL_SCANCODE_3,
+	SDL_SCANCODE_4,
+	SDL_SCANCODE_5,
+	SDL_SCANCODE_6,
+	SDL_SCANCODE_7,
+	SDL_SCANCODE_8,
+	SDL_SCANCODE_9,
+	SDL_SCANCODE_0,
+	SDL_SCANCODE_RETURN,
+	SDL_SCANCODE_ESCAPE,
+	SDL_SCANCODE_BACKSPACE,
+	SDL_SCANCODE_TAB,
+	SDL_SCANCODE_SPACE,
+	SDL_SCANCODE_MINUS,
+	SDL_SCANCODE_EQUALS,
+	SDL_SCANCODE_LEFTBRACKET,
+	SDL_SCANCODE_RIGHTBRACKET,
+	SDL_SCANCODE_BACKSLASH,
+	SDL_SCANCODE_NONUSHASH,
+	SDL_SCANCODE_SEMICOLON,
+	SDL_SCANCODE_APOSTROPHE,
+	SDL_SCANCODE_GRAVE,
+	SDL_SCANCODE_COMMA,
+	SDL_SCANCODE_PERIOD,
+	SDL_SCANCODE_SLASH,
+	SDL_SCANCODE_CAPSLOCK,
+	SDL_SCANCODE_F1,
+	SDL_SCANCODE_F2,
+	SDL_SCANCODE_F3,
+	SDL_SCANCODE_F4,
+	SDL_SCANCODE_F5,
+	SDL_SCANCODE_F6,
+	SDL_SCANCODE_F7,
+	SDL_SCANCODE_F8,
+	SDL_SCANCODE_F9,
+	SDL_SCANCODE_F10,
+	SDL_SCANCODE_F11,
+	SDL_SCANCODE_F12,
+	SDL_SCANCODE_PRINTSCREEN,
+	SDL_SCANCODE_SCROLLLOCK,
+	SDL_SCANCODE_PAUSE,
+	SDL_SCANCODE_INSERT,
+	SDL_SCANCODE_HOME,
+	SDL_SCANCODE_PAGEUP,
+	SDL_SCANCODE_DELETE,
+	SDL_SCANCODE_END,
+	SDL_SCANCODE_PAGEDOWN,
+	SDL_SCANCODE_RIGHT,
+	SDL_SCANCODE_LEFT,
+	SDL_SCANCODE_DOWN,
+	SDL_SCANCODE_UP,
+	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_APPLICATION,
+	SDL_SCANCODE_POWER,
+	SDL_SCANCODE_KP_EQUALS,
+
+	SDL_SCANCODE_SYSREQ = 0x9a,
+
+	SDL_SCANCODE_MENU = 0x76,
+
+	SDL_SCANCODE_MUTE = 0x7f,
+	SDL_SCANCODE_VOLUMEUP,
+	SDL_SCANCODE_VOLUMEDOWN,
+	SDL_SCANCODE_PLAYPAUSE,
+
+	SDL_SCANCODE_LCTRL = 0xe0,
+	SDL_SCANCODE_LSHIFT,
+	SDL_SCANCODE_LALT,
+	SDL_SCANCODE_LGUI,
+	SDL_SCANCODE_RCTRL,
+	SDL_SCANCODE_RSHIFT,
+	SDL_SCANCODE_RALT,
+	SDL_SCANCODE_RGUI,
+
+	SDL_SCANCODE_MODE = 0x101,
+	SDL_SCANCODE_AUDIOMUTE = 0x106,
+
+	SDL_NUM_SCANCODES,
+} SDL_Scancode;
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_surface.h
@@ -1,0 +1,47 @@
+#ifndef _npe_SDL_surface_h_
+#define _npe_SDL_surface_h_
+
+typedef struct SDL_PixelFormat SDL_PixelFormat;
+typedef struct SDL_Surface SDL_Surface;
+
+struct SDL_PixelFormat {
+	SDL_Palette *palette;
+	int format;
+	int BytesPerPixel;
+};
+
+struct SDL_Surface {
+	SDL_PixelFormat *format;
+	SDL_Rect clip_rect;
+	Uint32 flags;
+	Uint32 key;
+	int keyset;
+	int w, h;
+	int pitch;
+	int n;
+	void *i;
+	uchar *pixels;
+};
+
+SDL_Surface *SDL_CreateRGBSurfaceWithFormat(Uint32 flags, int w, int h, int bpp, Uint32 fmt);
+SDL_Surface *SDL_CreateRGBSurface(Uint32 flags, int width, int height, int depth, Uint32 rm, Uint32 gm, Uint32 bm, Uint32 am);
+SDL_Surface *SDL_CreateRGBSurfaceFrom(void *pixels, int w, int h, int bpp, int pitch, Uint32 rm, Uint32 gm, Uint32 bm, Uint32 am);
+Uint32 SDL_MapRGB(SDL_PixelFormat *format, Uint8 r, Uint8 g, Uint8 b);
+Uint32 SDL_MapSurfaceRGB(SDL_Surface *surface, Uint8 r, Uint8 g, Uint8 b);
+bool SDL_SetSurfaceColorKey(SDL_Surface *surface, bool enabled, Uint32 key);
+int SDL_SetSurfaceRLE(SDL_Surface *surface, int flag);
+int SDL_LockSurface(SDL_Surface *surface);
+int SDL_UnlockSurface(SDL_Surface *surface);
+int SDL_SetSurfaceBlendMode(SDL_Surface *surface, SDL_BlendMode blendMode);
+SDL_Cursor *SDL_CreateColorCursor(SDL_Surface *surface, int hot_x, int hot_y);
+void SDL_DestroySurface(SDL_Surface *surface);
+SDL_Surface* SDL_LoadBMP_IO(SDL_IOStream *src, bool closeio);
+int SDL_SaveBMP(SDL_Surface *s, const char *file);
+int SDL_SetSurfacePalette(SDL_Surface *s, SDL_Palette *palette);
+int SDL_BlitSurface(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);
+int SDL_LowerBlit(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);
+int SDL_SoftStretch(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, const SDL_Rect *dstrect);
+
+SDL_Texture *SDL_CreateTextureFromSurface(SDL_Renderer *r, SDL_Surface *s);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_thread.h
@@ -1,0 +1,23 @@
+#ifndef _npe_SDL_thread_h_
+#define _npe_SDL_thread_h_
+
+enum {
+	SDL_THREAD_PRIORITY_LOW,
+	SDL_THREAD_PRIORITY_NORMAL,
+	SDL_THREAD_PRIORITY_HIGH,
+	SDL_THREAD_PRIORITY_TIME_CRITICAL,
+};
+
+typedef struct SDL_Thread SDL_Thread;
+typedef int (*SDL_ThreadFunction)(void *);
+#pragma incomplete SDL_Thread
+
+SDL_Thread *SDL_CreateThread(SDL_ThreadFunction, char *, void *);
+SDL_Thread *SDL_CreateThreadWithStackSize(SDL_ThreadFunction, const char *, size_t, void *);
+
+void SDL_DetachThread(SDL_Thread *);
+void SDL_WaitThread(SDL_Thread *, int *);
+
+void SDL_SetCurrentThreadPriority(int);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_version.h
@@ -1,0 +1,18 @@
+#ifndef _npe_SDL_version_h_
+#define _npe_SDL_version_h_
+
+typedef struct SDL_version SDL_version;
+
+struct SDL_version {
+	u8int major, minor, patch;
+};
+
+void SDL_GetVersion(SDL_version *v);
+
+#define SDL_VERSIONNUM(X, Y, Z)	((X)*1000 + (Y)*100 + (Z))
+#define SDL_MAJOR_VERSION 3
+#define SDL_MINOR_VERSION 3
+#define SDL_MICRO_VERSION 7
+#define SDL_COMPILEDVERSION SDL_VERSIONNUM(SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL)
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/SDL_video.h
@@ -1,0 +1,15 @@
+#ifndef _npe_SDL_video_h_
+#define _npe_SDL_video_h_
+
+typedef Uint32 SDL_WindowID;
+typedef Uint64 SDL_WindowFlags;
+
+enum {
+	SDL_WINDOW_OPENGL = 1ULL<<1,
+	SDL_WINDOW_HIGH_PIXEL_DENSITY = 1ULL<<13,
+};
+
+bool SDL_GetWindowSizeInPixels(SDL_Window *window, int *w, int *h);
+bool SDL_SyncWindow(SDL_Window *window);
+
+#endif
--- /dev/null
+++ b/include/npe/SDL3/audio.c
@@ -1,0 +1,299 @@
+#include "_sdl.h"
+
+enum {
+	Aout = 2,
+	Arec,
+
+	Audiosamples = 8192,
+};
+
+typedef struct Audiodev Audiodev;
+
+struct Audiodev {
+	Lock;
+	void (*cb)(void *, Uint8 *, int);
+	void *userdata;
+	char *name;
+	Channel *wait;
+	Uint8 *buf;
+	int bufsz;
+	int paused;
+	int fd;
+	int pid;
+	int pidconv;
+	int mode;
+};
+
+/* FIXME extra USB audio devices? */
+static Audiodev au[4] = {
+	[0] = {.fd = -1, .mode = -1},
+	[1] = {.fd = -1, .mode = -1},
+	[Aout] = {.name = "/dev/audio", .fd = -1, .pid = -1, .mode = OWRITE},
+	[Arec] = {.name = "/dev/audio", .fd = -1, .pid = -1, .mode = OREAD},
+};
+
+static struct {
+	char *spec;
+	int ssz; /* samples size */
+}fmts[] = {
+	[SDL_AUDIO_U8] = {"u8", 1},
+	[SDL_AUDIO_S8] = {"s8", 1},
+	[SDL_AUDIO_S16LE] = {"s16", 2},
+	[SDL_AUDIO_S16BE] = {"S16", 2},
+	[SDL_AUDIO_S32LE] = {"s32", 4},
+	[SDL_AUDIO_S32BE] = {"S32", 4},
+	[SDL_AUDIO_F32LE] = {"f32", 4},
+	[SDL_AUDIO_F32BE] = {"F32", -1}, /* FIXME big endian f32 not supported by pcmconv */
+};
+
+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);
+	return au[iscapture ? Arec : Aout].name;
+}
+
+void
+SDL_LockAudioDevice(SDL_AudioDeviceID id)
+{
+	lock(&au[id]);
+}
+
+void
+SDL_UnlockAudioDevice(SDL_AudioDeviceID id)
+{
+	unlock(&au[id]);
+}
+
+static void
+audiothread(void *p)
+{
+	Audiodev *a;
+
+	a = p;
+	threadsetname("%s (%s)", a->name, a->mode == OREAD ? "out" : "in");
+
+	for(;;){
+		if(a->mode == OREAD && readn(a->fd, a->buf, a->bufsz) != a->bufsz)
+			break;
+
+		lock(a);
+		if(a->mode == OWRITE && a->paused)
+			memset(a->buf, 0, a->bufsz);
+		else
+			a->cb(a->userdata, a->buf, a->bufsz);
+		unlock(a);
+
+		if(a->mode == OWRITE && write(a->fd, a->buf, a->bufsz) != a->bufsz)
+			break;
+	}
+
+	lock(a);
+	(a->mode == OWRITE ? write : read)(a->fd, a->buf, 0);
+	chanclose(a->wait);
+	unlock(a);
+
+	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, 4096);
+		a->paused = 0;
+	}else if(!a->paused && pause){
+		a->paused = 1;
+	}
+}
+
+void
+SDL_PauseAudio(int pause_on)
+{
+	SDL_PauseAudioDevice(1, pause_on);
+}
+
+static int
+convspec(SDL_AudioSpec *s, char *spec, int n)
+{
+	int ssz;
+
+	ssz = -1;
+	if(s->format < 0 || s->format >= nelem(fmts))
+		werrstr("invalid audio format: #%d", s->format);
+	else if(fmts[s->format].ssz < 1)
+		werrstr("unsupported audio format: #%d", s->format);
+	else if(s->channels < 1)
+		werrstr("invalid number of channels: %d", s->channels);
+	else if(s->freq < 1)
+		werrstr("invalid sampling rate: %d", s->freq);
+	else if(snprint(spec, n, "%sc%dr%d", fmts[s->format].spec, s->channels, s->freq) >= n)
+		werrstr("audio spec does not fit");
+	else
+		ssz = fmts[s->format].ssz;
+
+	return ssz;
+}
+
+// https://wiki.libsdl.org/SDL3/SDL_ConvertAudio
+int
+SDL_ConvertAudio(SDL_AudioCVT *cvt)
+{
+	USED(cvt);
+	return 0;
+}
+
+// https://wiki.libsdl.org/SDL3/SDL_BuildAudioCVT
+int
+SDL_BuildAudioCVT(SDL_AudioCVT *cvt, SDL_AudioFormat src_format, Uint8 src_channels, int src_rate, SDL_AudioFormat dst_format, Uint8 dst_channels, int dst_rate)
+{
+	USED(cvt);
+	USED(src_format, src_channels, src_rate);
+	USED(dst_format, dst_channels, dst_rate);
+	return 0;
+}
+
+static void
+setpipebuf(int f, int sz)
+{
+	Dir d;
+
+	nulldir(&d);
+	d.length = sz;
+	dirfwstat(f, &d);
+}
+
+SDL_AudioDeviceID
+SDL_OpenAudioDevice(char *dev, int rec, SDL_AudioSpec *want, SDL_AudioSpec *have, u32int change)
+{
+	SDL_AudioDeviceID id;
+	int p[2], ssz, fd;
+	char spec[16];
+	Audiodev *a;
+
+	/* FIXME look for extra USB devices? */
+	USED(dev);
+
+	id = rec ? Arec : Aout;
+	a = &au[id];
+
+	if(have == nil)
+		have = want;
+	*have = *want;
+	if(have->freq < 44100 && (change & SDL_AUDIO_ALLOW_FREQUENCY_CHANGE) != 0)
+		have->freq = 44100;
+	if(have->format <= 0 || have->format >= nelem(fmts) || fmts[have->format].ssz < 1 && (change & SDL_AUDIO_ALLOW_FORMAT_CHANGE) != 0)
+		have->format = AUDIO_S16;
+	if(have->channels < 1 && (change & SDL_AUDIO_ALLOW_CHANNELS_CHANGE) != 0)
+		have->channels = 2;
+	if(have->samples < 2 || (have->samples & (have->samples-1)) != 0){
+		if(change & SDL_AUDIO_ALLOW_SAMPLES_CHANGE)
+			have->samples = Audiosamples;
+		else{
+			werrstr("invalid number of samples: %d", have->samples);
+			goto err;
+		}
+	}
+
+	if((ssz = convspec(have, spec, sizeof(spec))) < 1)
+		goto err;
+
+	a->userdata = have->userdata;
+	a->cb = have->callback;
+	a->wait = chancreate(sizeof(ulong), 0);
+	a->bufsz = have->samples * ssz * have->channels;
+	a->buf = malloc(a->bufsz);
+	if(a->wait == nil || a->buf == nil){
+		werrstr("memory");
+		goto err;
+	}
+
+	a->paused = 1;
+	a->pid = -1;
+	a->pidconv = -1;
+	if(have->freq != 44100 || have->format != AUDIO_S16 || have->channels != 2){
+		if((fd = open(a->name, a->mode)) < 0)
+			goto err;
+		pipe(p);
+		setpipebuf(p[0], a->bufsz);
+		if((a->pidconv = rfork(RFPROC|RFNOTEG|RFFDG|RFCENVG)) == 0){
+			dup(fd, rec ? 0 : 1); close(fd);
+			dup(p[0], rec ? 1 : 0); close(p[0]);
+			close(p[1]);
+			//close(2);
+			if(execl("/bin/audio/pcmconv", "pcmconv", rec ? "-o" : "-i", spec, nil) != 0)
+				exits("%r");
+		}else if(a->pidconv < 0){
+			werrstr("pcmconv: %r");
+			close(fd);
+			goto err;
+		}
+		a->fd = p[1];
+		close(p[0]);
+		close(fd);
+	}else if(a->fd < 0 && (a->fd = open(a->name, a->mode|OCEXEC)) < 0)
+		goto err;
+
+	return id;
+err:
+	werrstr("SDL_OpenAudioDevice: %r");
+	if(a->fd >= 0){
+		close(a->fd);
+		a->fd = -1;
+	}
+	free(a->buf);
+	a->buf = nil;
+	if(a->wait != nil){
+		chanfree(a->wait);
+		a->wait = nil;
+	}
+
+	return 0;
+}
+
+void
+SDL_CloseAudioDevice(SDL_AudioDeviceID id)
+{
+	Audiodev *a;
+	Waitmsg *w;
+	int pid;
+
+	a = &au[id];
+
+	if(a->fd < 0)
+		return;
+
+	lock(a);
+		close(a->fd);
+	unlock(a);
+
+	if(a->pid >= 0)
+		recvul(a->wait);
+	chanfree(a->wait);
+
+	free(a->buf);
+	a->fd = -1;
+	a->pid = -1;
+again:
+	if(a->pidconv >= 0 && (w = wait()) != nil){
+		if(w->msg[0])
+			fprint(2, "SDL_CloseAudioDevice: %s: %s\n", a->name, w->msg);
+		pid = w->pid;
+		free(w);
+		if(pid != a->pidconv)
+			goto again;
+	}
+}
--- /dev/null
+++ b/libnpe_sdl3/_sdl.h
@@ -1,0 +1,67 @@
+#include <SDL3/SDL.h>
+#include <sys/stat.h>
+#include <stdint.h>
+#include <string.h>
+#include <tos.h>
+#include <thread.h>
+#include <draw.h>
+#include <memdraw.h>
+#include <mouse.h>
+#include <cursor.h>
+#include <plumb.h>
+#include "_npe.h"
+#undef waitpid
+
+struct SDL_Texture {
+	Memimage *m;
+	Memimage *mod;
+	SDL_BlendMode blend;
+};
+struct SDL_Renderer {
+	int logiw;
+	int logih;
+};
+
+struct npe_sdl {
+	Mousectl *mctl;
+	Rectangle grabout;
+	Point center;
+	int mgrab;
+	Mouse m, om;
+	Point Δ;
+	int hints;
+	int mredraw;
+	int fullredraw;
+	int textinput;
+	int physw, physh;
+	float scale;
+	struct {
+		Uint32 r, g, b, a;
+	} defmask;
+	Memimage *back;
+	Memimage *rendcol;
+	SDL_Texture *target;
+};
+
+enum {
+	Altf4noclose = 1<<0,
+};
+
+extern struct npe_sdl npe_sdl;
+
+int	npe_sdl_init_input(void);
+int	npe_sdl_init_draw(void);
+int	npe_sdl_init_audio(void);
+int	npe_sdl_init_tex(void);
+int	npe_sdl_init_gamepad(void);
+void	npe_sdl_kill_audio(void);
+void	npe_sdl_kill_draw(void);
+void	npe_sdl_kill_gamepad(void);
+void	npe_sdl_kill_input(void);
+void*	npe_sdl_scale(u32int*, int, int, u32int*, int, int);
+void	npe_draw_cursor(void);
+
+ulong	mask2chan(int, Uint32, Uint32, Uint32, Uint32);
+int	chan2mask(Uint32, int*, Uint32*, Uint32*, Uint32*, Uint32*);
+Uint32	chan2pixel(ulong);
+ulong	pixel2chan(Uint32);
--- /dev/null
+++ b/libnpe_sdl3/audio.c
@@ -1,0 +1,743 @@
+#include "_sdl.h"
+
+#ifdef FIXME
+enum {
+	Aout = 2,
+	Arec,
+
+	Audiosamples = 8192,
+};
+
+typedef struct Audiodev Audiodev;
+
+struct Audiodev {
+	Lock;
+	void (*cb)(void *, Uint8 *, int);
+	void *userdata;
+	char *name;
+	Channel *wait;
+	Uint8 *buf;
+	int bufsz;
+	int paused;
+	int fd;
+	int pid;
+	int pidconv;
+	int mode;
+};
+
+/* FIXME extra USB audio devices? */
+static Audiodev au[4] = {
+	[0] = {.fd = -1, .mode = -1},
+	[1] = {.fd = -1, .mode = -1},
+	[Aout] = {.name = "/dev/audio", .fd = -1, .pid = -1, .mode = OWRITE},
+	[Arec] = {.name = "/dev/audio", .fd = -1, .pid = -1, .mode = OREAD},
+};
+
+static struct {
+	char *spec;
+	int ssz; /* samples size */
+}fmts[] = {
+	[SDL_AUDIO_U8] = {"u8", 1},
+	[SDL_AUDIO_S8] = {"s8", 1},
+	[SDL_AUDIO_S16LE] = {"s16", 2},
+	[SDL_AUDIO_S16BE] = {"S16", 2},
+	[SDL_AUDIO_S32LE] = {"s32", 4},
+	[SDL_AUDIO_S32BE] = {"S32", 4},
+	[SDL_AUDIO_F32LE] = {"f32", 4},
+	[SDL_AUDIO_F32BE] = {"F32", -1}, /* FIXME big endian f32 not supported by pcmconv */
+};
+
+static void
+audiothread(void *p)
+{
+	Audiodev *a;
+
+	a = p;
+	threadsetname("%s (%s)", a->name, a->mode == OREAD ? "out" : "in");
+
+	for(;;){
+		if(a->mode == OREAD && readn(a->fd, a->buf, a->bufsz) != a->bufsz)
+			break;
+
+		lock(a);
+		if(a->mode == OWRITE && a->paused)
+			memset(a->buf, 0, a->bufsz);
+		else
+			a->cb(a->userdata, a->buf, a->bufsz);
+		unlock(a);
+
+		if(a->mode == OWRITE && write(a->fd, a->buf, a->bufsz) != a->bufsz)
+			break;
+	}
+
+	lock(a);
+	(a->mode == OWRITE ? write : read)(a->fd, a->buf, 0);
+	chanclose(a->wait);
+	unlock(a);
+
+	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(, a, 4096);
+		a->paused = 0;
+	}else if(!a->paused && pause){
+		a->paused = 1;
+	}
+}
+
+void
+SDL_PauseAudio(int pause_on)
+{
+	SDL_PauseAudioDevice(1, pause_on);
+}
+
+static int
+convspec(SDL_AudioSpec *s, char *spec, int n)
+{
+	int ssz;
+
+	ssz = -1;
+	if(s->format < 0 || s->format >= nelem(fmts))
+		werrstr("invalid audio format: #%d", s->format);
+	else if(fmts[s->format].ssz < 1)
+		werrstr("unsupported audio format: #%d", s->format);
+	else if(s->channels < 1)
+		werrstr("invalid number of channels: %d", s->channels);
+	else if(s->freq < 1)
+		werrstr("invalid sampling rate: %d", s->freq);
+	else if(snprint(spec, n, "%sc%dr%d", fmts[s->format].spec, s->channels, s->freq) >= n)
+		werrstr("audio spec does not fit");
+	else
+		ssz = fmts[s->format].ssz;
+
+	return ssz;
+}
+
+// https://wiki.libsdl.org/SDL3/SDL_ConvertAudio
+int
+SDL_ConvertAudio(SDL_AudioCVT *cvt)
+{
+	USED(cvt);
+	return 0;
+}
+
+// https://wiki.libsdl.org/SDL3/SDL_BuildAudioCVT
+int
+SDL_BuildAudioCVT(SDL_AudioCVT *cvt, SDL_AudioFormat src_format, Uint8 src_channels, int src_rate, SDL_AudioFormat dst_format, Uint8 dst_channels, int dst_rate)
+{
+	USED(cvt);
+	USED(src_format, src_channels, src_rate);
+	USED(dst_format, dst_channels, dst_rate);
+	return 0;
+}
+
+static void
+setpipebuf(int f, int sz)
+{
+	Dir d;
+
+	nulldir(&d);
+	d.length = sz;
+	dirfwstat(f, &d);
+}
+
+void
+SDL_CloseAudioDevice(SDL_AudioDeviceID id)
+{
+	Audiodev *a;
+	Waitmsg *w;
+	int pid;
+
+	a = &au[id];
+
+	if(a->fd < 0)
+		return;
+
+	lock(a);
+		close(a->fd);
+	unlock(a);
+
+	if(a->pid >= 0)
+		recvul(a->wait);
+	chanfree(a->wait);
+
+	free(a->buf);
+	a->fd = -1;
+	a->pid = -1;
+again:
+	if(a->pidconv >= 0 && (w = wait()) != nil){
+		if(w->msg[0])
+			fprint(2, "SDL_CloseAudioDevice: %s: %s\n", a->name, w->msg);
+		pid = w->pid;
+		free(w);
+		if(pid != a->pidconv)
+			goto again;
+	}
+}
+
+SDL_AudioDeviceID
+SDL_OpenAudioDevice(char *dev, int rec, SDL_AudioSpec *want, SDL_AudioSpec *have, u32int change)
+{
+	SDL_AudioDeviceID id;
+	int p[2], ssz, fd;
+	char spec[16];
+	Audiodev *a;
+
+	/* FIXME look for extra USB devices? */
+	USED(dev);
+
+	id = rec ? Arec : Aout;
+	a = &au[id];
+
+	if(have == nil)
+		have = want;
+	*have = *want;
+	if(have->freq < 44100 && (change & SDL_AUDIO_ALLOW_FREQUENCY_CHANGE) != 0)
+		have->freq = 44100;
+	if(have->format <= 0 || have->format >= nelem(fmts) || fmts[have->format].ssz < 1 && (change & SDL_AUDIO_ALLOW_FORMAT_CHANGE) != 0)
+		have->format = SDL_AUDIO_S16;
+	if(have->channels < 1 && (change & SDL_AUDIO_ALLOW_CHANNELS_CHANGE) != 0)
+		have->channels = 2;
+	if(have->samples < 2 || (have->samples & (have->samples-1)) != 0){
+		if(change & SDL_AUDIO_ALLOW_SAMPLES_CHANGE)
+			have->samples = Audiosamples;
+		else{
+			werrstr("invalid number of samples: %d", have->samples);
+			goto err;
+		}
+	}
+
+	if((ssz = convspec(have, spec, sizeof(spec))) < 1)
+		goto err;
+
+	a->userdata = have->userdata;
+	a->cb = have->callback;
+	a->wait = chancreate(sizeof(ulong), 0);
+	a->bufsz = have->samples * ssz * have->channels;
+	a->buf = malloc(a->bufsz);
+	if(a->wait == nil || a->buf == nil){
+		werrstr("memory");
+		goto err;
+	}
+
+	a->paused = 1;
+	a->pid = -1;
+	a->pidconv = -1;
+	if(have->freq != 44100 || have->format != AUDIO_S16 || have->channels != 2){
+		if((fd = open(a->name, a->mode)) < 0)
+			goto err;
+		pipe(p);
+		setpipebuf(p[0], a->bufsz);
+		if((a->pidconv = rfork(RFPROC|RFNOTEG|RFFDG|RFCENVG)) == 0){
+			dup(fd, rec ? 0 : 1); close(fd);
+			dup(p[0], rec ? 1 : 0); close(p[0]);
+			close(p[1]);
+			//close(2);
+			if(execl("/bin/audio/pcmconv", "pcmconv", rec ? "-o" : "-i", spec, nil) != 0)
+				exits("%r");
+		}else if(a->pidconv < 0){
+			werrstr("pcmconv: %r");
+			close(fd);
+			goto err;
+		}
+		a->fd = p[1];
+		close(p[0]);
+		close(fd);
+	}else if(a->fd < 0 && (a->fd = open(a->name, a->mode|OCEXEC)) < 0)
+		goto err;
+
+	return id;
+err:
+	werrstr("SDL_OpenAudioDevice: %r");
+	if(a->fd >= 0){
+		close(a->fd);
+		a->fd = -1;
+	}
+	free(a->buf);
+	a->buf = nil;
+	if(a->wait != nil){
+		chanfree(a->wait);
+		a->wait = nil;
+	}
+
+	return 0;
+}
+#endif
+
+
+
+
+SDL_AudioDeviceID SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK = -1;	/* 0 is null/invalid */
+SDL_AudioDeviceID SDL_AUDIO_DEVICE_DEFAULT_RECORDING = -1;
+
+struct SDL_AudioStream {
+	QLock;
+	int fd;
+	int type;
+	char *name;
+	SDL_AudioStreamCallback fn;
+	void *aux;
+	SDL_AudioSpec;
+	int delay;
+	int tail;
+	int framesz;
+	int nframes;
+	uchar *buf;
+	int avail;
+	int paused;	/* ugh */
+	Channel *c;
+	Channel *pausec;
+};
+static SDL_AudioStream *devs;
+static Channel *ach;
+static int ndevs;
+
+bool
+SDL_GetAudioDeviceFormat(SDL_AudioDeviceID id, SDL_AudioSpec *spec, int *nsamp)
+{
+	SDL_AudioStream *as;
+
+	if(--id < 0 || id >= ndevs){
+		werrstr("invalid device id %d", id);
+		return false;
+	}
+	as = devs + id;
+	if(spec != nil)
+		*spec = as->SDL_AudioSpec;
+	if(nsamp != nil)
+		*nsamp = as->delay;
+	return true;
+}
+
+bool
+SDL_GetAudioStreamFormat(SDL_AudioStream *as, SDL_AudioSpec *in, SDL_AudioSpec *out)
+{
+	if(in != nil)
+		*in = as->SDL_AudioSpec;
+	if(out != nil)
+		*out = as->SDL_AudioSpec;
+	return true;
+}
+
+bool
+SDL_SetAudioStreamFormat(SDL_AudioStream *as, SDL_AudioSpec *in, SDL_AudioSpec *out)
+{
+	/* FIXME: set new input and output formats from ptr */
+	if(in != nil){
+		if(memcmp(&as->SDL_AudioSpec, in, sizeof *in) != 0){
+			/* FIXME */
+		}
+		as->SDL_AudioSpec = *in;
+	}
+	if(out != nil){
+		if(memcmp(&as->SDL_AudioSpec, out, sizeof *out) != 0){
+			/* FIXME */
+		}
+		as->SDL_AudioSpec = *out;
+	}
+	return true;
+}
+
+int
+SDL_GetAudioStreamAvailable(SDL_AudioStream *as)
+{
+	return as->avail;
+}
+
+int
+SDL_GetAudioStreamData(SDL_AudioStream *as, void *buf, int n)
+{
+	uchar *p;
+
+	if(n <= 0 || as->avail < as->framesz)
+		return 0;
+	if(as->avail < n)
+		n = as->avail;
+	else if(n > as->framesz)
+		n = as->framesz;
+	qlock(as);
+	p = as->buf + as->tail * as->framesz;
+	memcpy(buf, p, n);
+	as->avail -= n;
+	as->tail = (as->tail + 1) % as->nframes;
+	qunlock(as);
+	return n;
+}
+
+bool
+SDL_PutAudioStreamData(SDL_AudioStream *as, const void *buf, int n)
+{
+	int i, m;
+	uchar *p, *t, *e;
+
+	if(as->buf == nil)
+		return false;
+	i = as->tail;	/* FIXME: actually head */
+	for(; n>0; n-=m){
+		p = as->buf + i * as->framesz;
+		t = p + as->avail;
+		e = as->buf + as->nframes * as->framesz;
+		m = e - t < n ? e - t : n;
+		qlock(as);
+		memcpy(t, buf, m);
+		as->avail += m;
+		qunlock(as);
+		while(as->avail >= as->framesz){
+			if(send(as->c, &i) < 0)
+				return false;	/* FIXME: recovery? */
+			i = (i + 1) % as->nframes;
+			qlock(as);
+			as->avail -= as->framesz;
+			qunlock(as);
+		}
+	}
+	return true;
+}
+
+SDL_AudioDeviceID *
+SDL_GetAudioRecordingDevices(int *count)
+{
+	int nbuf;
+	SDL_AudioDeviceID *buf;
+	SDL_AudioStream *as, *ae;
+
+	nbuf = 0;
+	buf = nil;
+	if(count != nil)
+		*count = 0;
+	for(as=devs, ae=as+ndevs; as<ae; as++){
+		if(as->type == OWRITE)
+			continue;
+		if((buf = realloc(buf, (nbuf+1) * sizeof *buf)) == nil)
+			return nil;
+		buf[nbuf++] = as - devs + 1;
+		if(count != nil)
+			(*count)++;
+	}
+	if((buf = realloc(buf, (nbuf+1) * sizeof *buf)) == nil)
+		return nil;
+	buf[nbuf] = 0;
+	return buf;
+}
+
+SDL_AudioDeviceID *
+SDL_GetAudioPlaybackDevices(int *count)
+{
+	int nbuf;
+	SDL_AudioDeviceID *buf;
+	SDL_AudioStream *as, *ae;
+
+	nbuf = 0;
+	buf = nil;
+	if(count != nil)
+		*count = 0;
+	for(as=devs, ae=as+ndevs; as<ae; as++){
+		if(as->type == OREAD)
+			continue;
+		if((buf = realloc(buf, (nbuf+1) * sizeof *buf)) == nil)
+			return nil;
+		buf[nbuf++] = as - devs + 1;
+		if(count != nil)
+			(*count)++;
+	}
+	if((buf = realloc(buf, (nbuf+1) * sizeof *buf)) == nil)
+		return nil;
+	buf[nbuf] = 0;
+	return buf;
+}
+
+const char*
+SDL_GetAudioDeviceName(SDL_AudioDeviceID id)
+{
+	SDL_AudioStream *as;
+
+	if(--id < 0 || id >= ndevs){
+		werrstr("invalid device id %d", id);
+		return nil;
+	}
+	as = devs + id;
+	return as->name;
+}
+
+static void
+arproc(void *arg)
+{
+	int i, n;
+	uchar *p;
+	SDL_AudioStream *as, *ad, *ae;
+
+	for(ad=nil, as=devs, ae=as+ndevs; as<ae; as++)
+		if(strcmp(as->name, "/dev/audio") == 0){
+			ad = as;
+			break;
+		}
+	as = arg;
+	i = 0;
+	for(;;){
+		if(as->paused)
+			recvul(as->pausec);
+		p = as->buf + i * as->framesz;
+		if((n = read(as->fd, p, as->framesz)) < 0)	/* FIXME: no eof? */
+			break;
+		if(ad->fd >= 0){
+			fprint(2, "wrote directly %d\n", n);
+			write(ad->fd, p, n);
+			continue;
+		}
+		if(n == 0)
+			continue;
+		qlock(as);
+		if(as->fd < 0){
+			qunlock(as);
+			break;
+		}
+		as->avail += n;
+		if(as->avail > as->nframes * as->framesz)
+			as->avail = as->nframes * as->framesz;
+		nbsend(ach, &n);
+		i = (i + 1) % as->nframes;
+		if(i == as->tail)
+			as->tail = (i + 1) % as->nframes;
+		qunlock(as);
+	}
+	qlock(as);
+	chanfree(as->pausec);
+	chanfree(as->c);
+	as->pausec = as->c = nil;
+	free(as->buf);
+	as->buf = nil;
+	qunlock(as);
+}
+
+static void
+awproc(void *arg)
+{
+	int i, n;
+	uchar *p;
+	SDL_AudioStream *as;
+
+	as = arg;
+	for(;;){
+		if(as->paused)
+			recvul(as->pausec);
+		if(recv(as->c, &i) < 0)
+			break;
+		n = as->framesz;
+		p = as->buf + i * n;
+		fprint(2, "write %d to %s\n", n, as->name);
+		if(write(as->fd, p, n) != n){
+			fprint(2, "awproc: %r\n");
+			break;
+		}
+	}
+	qlock(as);
+	chanfree(as->pausec);
+	chanfree(as->c);
+	as->pausec = as->c = nil;
+	free(as->buf);
+	as->buf = nil;
+	qunlock(as);
+}
+
+/* FIXME: bullshit */
+static void
+acbproc(void *arg)
+{
+	int n;
+	SDL_AudioStream *as, *ae;
+	Channel *c;
+
+	c = arg;
+	for(;;){
+		if(recv(c, &n) < 0)
+			break;
+		for(as=devs, ae=as+ndevs; as<ae; as++)
+			if(as->fd >= 0 && as->fn != nil)
+				as->fn(as->aux, as, n, as->avail);
+	}
+	chanfree(c);
+}
+
+bool
+SDL_ResumeAudioStreamDevice(SDL_AudioStream *as)
+{
+	if(!as->paused)
+		return true;
+	as->paused = 0;
+	nbsendul(as->pausec, 1);
+	return true;
+}
+
+bool
+SDL_PauseAudioStreamDevice(SDL_AudioStream *as)
+{
+	if(as->paused)
+		return true;
+	as->paused = 1;
+	return true;
+}
+
+SDL_AudioStream*
+SDL_OpenAudioDeviceStream(SDL_AudioDeviceID id, const SDL_AudioSpec *f, SDL_AudioStreamCallback fn, void *aux)
+{
+	SDL_AudioStream *as;
+
+	if(--id < 0 || id >= ndevs){
+		werrstr("invalid device id %d", id);
+		return nil;
+	}
+	as = devs + id;
+	/* FIXME: duplicate entries for rw devices? */
+	//if(as->fd >= 0 || !canqlock(as)){
+	if(as->fd >= 0){
+		werrstr("in use");
+		return nil;
+	}
+	if((as->fd = open(as->name, as->type)) < 0)
+		return nil;
+	as->aux = aux;
+	as->tail = 0;
+	/* FIXME: format */
+	if((as->buf = mallocz(as->nframes * as->framesz, 1)) == nil)
+		goto Err;
+	if((as->c = chancreate(sizeof(int), 8)) == nil)
+		goto Err;
+	if((as->pausec = chancreate(sizeof(int), 0)) == nil)
+		goto Err;
+	if(proccreate(as->type != OREAD ? awproc : arproc, as, 8192) < 0)
+		goto Err;
+	USED(f);	/* FIXME: format conversion */
+	as->fn = fn;
+	return as;
+Err:
+	if(as->c != nil){
+		chanfree(as->c);
+		as->c = nil;
+	}
+	if(as->pausec != nil){
+		chanfree(as->pausec);
+		as->pausec = nil;
+	}
+	close(as->fd);
+	as->fd = -1;
+	free(as->buf);
+	return nil;
+}
+
+void
+SDL_DestroyAudioStream(SDL_AudioStream *as)
+{
+	if(as->fd < 0)
+		return;
+	qlock(as);
+	close(as->fd);
+	as->fd = -1;
+	chanclose(as->c);
+	chanclose(as->pausec);
+	as->fn = nil;
+	as->aux = nil;
+	qunlock(as);
+}
+
+static int
+probe(void)
+{
+	int i, n, dfd, type;
+	ulong len;
+	char *name, dir[] = "/dev/";
+	SDL_AudioStream as;
+	Dir *d;
+
+	if((dfd = open(dir, OREAD)) < 0)
+		return -1;
+	while((n = dirread(dfd, &d)) > 0){
+		for(i = 0; i < n; i++){
+			len = strlen(d[i].name);
+			if(d[i].mode & DMDIR
+			|| len < 5
+			|| strncmp(d[i].name, "audio", 5) != 0
+			|| strncmp(d[i].name+5, "ctl", 3) == 0
+			|| strncmp(d[i].name+5, "stat", 4) == 0)
+				continue;
+			if((name = smprint("%s%s", dir, d[i].name)) == nil){
+				free(d);
+				return -1;
+			}
+			type = -1;
+			if(access(name, AREAD) == 0)
+				type = OREAD;
+			if(access(name, AWRITE) == 0){
+				/*
+				if(type != -1)
+					type = ORDWR;
+				else
+				*/
+					type = OWRITE;
+			}
+			if(type == -1){
+				free(name);
+				continue;
+			}
+			if(strcmp(d[i].name, "audio") == 0){
+				if(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK == -1 && type != OREAD)
+					SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK = ndevs + 1;
+				if(SDL_AUDIO_DEVICE_DEFAULT_RECORDING == -1 && type != OWRITE)
+					SDL_AUDIO_DEVICE_DEFAULT_RECORDING = ndevs + 1;
+			}
+			memset(&as, 0, sizeof as);
+			as.fd = -1;
+			as.type = type;
+			as.name = name;
+			/* FIXME: read */
+			as.freq = 44100;
+			as.channels = 2;
+			as.format = SDL_AUDIO_S16LE;
+			as.delay = 1764;
+			as.framesz = as.delay * 2 * as.channels;
+			as.nframes = 8;
+			if((devs = realloc(devs, (ndevs+1) * sizeof *devs)) == nil){
+				free(name);
+				free(d);
+				return -1;	/* FIXME: devs is fucked now, we'll just segfault */
+			}
+			devs[ndevs++] = as;
+		}
+		free(d);
+	}
+	close(dfd);
+	if(ndevs == 0){
+		werrstr("no devices found");
+		return -1;
+	}
+	return 0;
+}
+
+void
+npe_sdl_kill_audio(void)
+{
+	SDL_AudioStream *as, *ae;
+
+	chanclose(ach);
+	for(as=devs, ae=as+ndevs; as<ae; as++)
+		SDL_DestroyAudioStream(as);
+}
+
+int
+npe_sdl_init_audio(void)
+{
+	if(probe() < 0)
+		return -1;
+	if((ach = chancreate(sizeof(int), 16)) == nil)
+		return -1;
+	if(proccreate(acbproc, ach, 8192) < 0)
+		return -1;
+	return 0;
+}
--- /dev/null
+++ b/libnpe_sdl3/cursor.c
@@ -1,0 +1,144 @@
+#include "_sdl.h"
+
+struct SDL_Cursor {
+	Image *i;
+	Image *m;
+	Point hot;
+};
+static SDL_Cursor *oldcursor, *cursor;
+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 int showcursor = SDL_ENABLE;
+
+bool
+SDL_ShowCursor(void)
+{
+	showcursor = 1;
+	setcursor(npe_sdl.mctl, cursor == nil ? nil : &nocursor);
+	return true;
+}
+
+bool
+SDL_HideCursor(void)
+{
+	showcursor = 0;
+	setcursor(npe_sdl.mctl, &nocursor);
+	return true;
+}
+
+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;
+}
+
+SDL_Cursor *
+SDL_CreateSystemCursor(SDL_SystemCursor id)
+{
+	/* FIXME */
+	USED(id);
+
+	return nil;
+}
+
+void
+SDL_SetCursor(SDL_Cursor *c)
+{
+	if(cursor != c){
+		cursor = c;
+		npe_sdl.mredraw = 1;
+		setcursor(npe_sdl.mctl, (cursor == nil && showcursor) ? nil : &nocursor);
+	}
+}
+
+void
+SDL_FreeCursor(SDL_Cursor *c)
+{
+	freeimage(c->i);
+	free(c);
+	if(cursor == c){
+		oldcursor = nil;
+		cursor = nil;
+	}
+}
+
+void
+npe_draw_cursor(void)
+{
+	Rectangle r, clipr;
+
+	if(cursor == nil || !showcursor)
+		return;
+	r.min = subpt(npe_sdl.m.xy, cursor->hot);
+	r.max = addpt(r.min, cursor->i->r.max);
+	if(!npe_sdl.fullredraw && oldcursor != nil){
+		clipr.min = subpt(npe_sdl.om.xy, oldcursor->hot);
+		clipr.max = addpt(clipr.min, oldcursor->i->r.max);
+		combinerect(&clipr, r);
+		replclipr(screen, 0, clipr);
+		npe_sdl.om.xy = npe_sdl.m.xy;
+	}
+	draw(screen, r, cursor->i, cursor->m, ZP);
+	oldcursor = cursor;
+}
--- /dev/null
+++ b/libnpe_sdl3/events.c
@@ -1,0 +1,637 @@
+#include "_sdl.h"
+
+enum {
+	/* FIXME missing plumber→dropfile */
+	Ckey,
+	Ckeytype,
+	Cmouse,
+	Cresize,
+	Cevent,
+	Numchan,
+
+	Rdown = 0,
+	Rup,
+	Rrepeat,
+};
+
+static int kmod;
+static Rune rune;
+static Keyboardctl kctl;
+static int quitreq;
+static SDL_Event evt;
+
+static Alt salt[Numchan+1] = {
+	[Ckey] = { nil, &rune, CHANRCV },
+	[Ckeytype] = { nil, nil, CHANNOP },
+	[Cmouse] = { nil, &npe_sdl.m, CHANRCV },
+	[Cresize] = { nil, nil, CHANRCV },
+	[Cevent] = { nil, &evt, CHANRCV },
+	[Numchan] = { nil, nil, CHANNOBLK },
+};
+
+static void kbdproc(void *);
+static void mouseproc(void *);
+
+void
+npe_sdl_kill_input(void)
+{
+}
+
+int
+npe_sdl_init_input(void)
+{
+	if((npe_sdl.mctl = initmouse(nil, screen)) == nil)
+		return -1;
+
+	salt[Ckey].c = chancreate(sizeof(Rune), 20);
+	salt[Ckeytype].c = chancreate(sizeof(int), 20);
+	salt[Cmouse].c = chancreate(sizeof(Mouse), 20);
+	salt[Cresize].c = npe_sdl.mctl->resizec;
+	salt[Cevent].c = chancreate(sizeof(SDL_Event), 20);
+	kctl.c = salt[Ckey].c; /* for enter() */
+
+	if(salt[Ckey].c == nil || salt[Ckeytype].c == nil || salt[Cmouse].c == nil)
+		return -1;
+
+	if(proccreate(kbdproc, nil, 4096) < 0 || proccreate(mouseproc, nil, 4096) < 0)
+		return -1;
+
+	return 0;
+}
+
+Uint32
+SDL_GetWindowID(SDL_Window *win)
+{
+	USED(win);
+	return 1;
+}
+
+SDL_Keymod
+SDL_GetModState(void)
+{
+	return kmod;
+}
+
+int
+SDL_EventState(Uint32, int)
+{
+	return 0;
+}
+
+Uint32
+SDL_RegisterEvents(int n)
+{
+	static Uint32 userevent = SDL_EVENT_USER;
+	Uint32 u;
+
+	if(userevent+n > SDL_EVENT_LAST || n < 0)
+		u = (Uint32)-1;
+	else
+		u = userevent += n;
+
+	return u;
+}
+
+int
+SDL_PushEvent(SDL_Event *event)
+{
+	fprint(2, "push event %d\n", event->type);
+	return send(salt[Cevent].c, event) > 0 ? 1 : -1;
+}
+
+int
+SDL_QuitRequested(void)
+{
+	return quitreq;
+}
+
+struct {
+	SDL_EventFilter f;
+	void *aux;
+} filter = {
+	nil,
+	nil,
+};
+
+void
+SDL_SetEventFilter(SDL_EventFilter f, void *userdata)
+{
+	filter.f = f;
+	filter.aux = userdata;
+}
+
+void
+SDL_PumpEvents(void)
+{
+	/* FIXME does it matter? */
+}
+
+/* FIXME */
+static Uint8
+runetojoy(Rune r)
+{
+	/* FIXME */
+	switch(r){
+	case Kleft: return SDL_GAMEPAD_BUTTON_DPAD_LEFT;
+	case Kright: return SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
+	case Kup: return SDL_GAMEPAD_BUTTON_DPAD_UP;
+	case Kdown: return SDL_GAMEPAD_BUTTON_DPAD_DOWN;
+	case 'z': return SDL_GAMEPAD_BUTTON_SOUTH;
+	case 'x': return SDL_GAMEPAD_BUTTON_EAST;
+	case ' ': return SDL_GAMEPAD_BUTTON_BACK;
+	case '\n': return SDL_GAMEPAD_BUTTON_START;
+	case 'r': return SDL_GAMEPAD_BUTTON_LEFT_STICK;
+	case 'q': return SDL_GAMEPAD_BUTTON_RIGHT_STICK;
+	}
+	sysfatal("runetojoy: %r");
+}
+
+#define ISTEXT(r) ((r) >= 0x20 && ((r) < KF || (r) >= KF+0x1000))
+
+int
+SDL_PollEvent(SDL_Event *e)
+{
+	int x, t;
+
+	if(e == nil) /* FIXME need to buffer the event so it won't get lost */
+		return 0;
+
+	switch(x = alt(salt)){
+	case -1:
+		fprint(2, "alt: %r");
+		break;
+	Quit:
+		e->type = SDL_EVENT_QUIT;
+		quitreq = 1;
+		goto Filter;
+	Joybut:
+		e->gbutton.down = t != Rup;
+		e->gbutton.type = e->gbutton.down ? SDL_EVENT_GAMEPAD_BUTTON_DOWN : SDL_EVENT_GAMEPAD_BUTTON_UP;
+		e->type = e->gbutton.type;
+		e->gbutton.timestamp = nsec();
+		e->gbutton.which = 0;
+		e->gbutton.button = runetojoy(rune);
+		goto Filter;
+	case Ckey:
+		memset(e, 0, sizeof(*e));
+		recv(salt[Ckeytype].c, &t);
+		/* FIXME
+		if(npe_sdl.textinput && ISTEXT(rune)){
+			if(t == Rup)
+				break;
+			e->type = SDL_TEXTINPUT;
+			e->text.text[runetochar(e->text.text, &rune)] = 0;
+			goto Filter;
+		}else */
+		if((npe_sdl.hints & Altf4noclose) == 0
+		&& (kmod & SDL_KMOD_LALT) != 0 && rune == (KF|4)){
+			goto Quit;
+		}else{
+			/* FIXME */
+			switch(rune){
+			case Kdel:
+				goto Quit;
+			case Kleft:
+			case Kright:
+			case Kup:
+			case Kdown:
+			case 'z':
+			case 'x':
+			case ' ':
+			case '\n':
+			case 'q':
+			case 'r':
+				goto Joybut;
+			}
+			e->key.down = t != Rup;
+			e->type = e->key.down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP;
+			e->key.repeat = t == Rrepeat;
+			e->key.scancode = SDL_GetScancodeFromKey(rune);
+			e->key.raw = rune;
+			if(rune == '\n')
+				rune = SDLK_RETURN;
+			e->key.key = rune;
+			goto Filter;
+		}
+		break;
+
+	case Cmouse:
+		break;
+#ifdef fuck
+		if(screen == nil)
+			break;
+
+		if(eqpt(npe_sdl.m.xy, Pt(-1, -1))){
+			npe_sdl.m.xy = npe_sdl.center;
+			npe_sdl.om.xy = npe_sdl.center;
+			return 0; /* swallow */
+		}
+
+		memset(e, 0, sizeof(*e));
+		e->motion.x = (npe_sdl.m.xy.x - screen->r.min.x) * npe_sdl.scale;
+		e->motion.y = (npe_sdl.m.xy.y - screen->r.min.y) * npe_sdl.scale;
+		e->motion.xrel = (npe_sdl.m.xy.x - npe_sdl.om.xy.x) * npe_sdl.scale;
+		e->motion.yrel = (npe_sdl.m.xy.y - npe_sdl.om.xy.y) * npe_sdl.scale;
+
+		if(!eqpt(npe_sdl.m.xy, npe_sdl.om.xy)){
+			npe_sdl.mredraw = 1;
+			if(npe_sdl.mgrab){1
+				npe_sdl.Δ.x += npe_sdl.m.xy.x - npe_sdl.om.xy.x;
+				npe_sdl.Δ.y += npe_sdl.m.xy.y - npe_sdl.om.xy.y;
+				npe_sdl.om.xy = npe_sdl.m.xy;
+			}
+			if(npe_sdl.m.buttons == npe_sdl.om.buttons){
+				e->type = SDL_MOUSEMOTION;
+				e->motion.state = npe_sdl.m.buttons;
+				goto Filter;
+			}
+		}
+		if(npe_sdl.m.buttons == npe_sdl.om.buttons)
+			break;
+		/* FIXME there is a lot of hope for multiple buttons to never change its state at the same time */
+		if((down = (npe_sdl.m.buttons & 1)) != (npe_sdl.om.buttons & 1)){ /* left */
+			e->type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
+			e->button.button = SDL_BUTTON_LEFT;
+			npe_sdl.om.buttons = (npe_sdl.om.buttons & ~1) | (npe_sdl.m.buttons & 1);
+			goto Filter;
+		}
+		if((down = (npe_sdl.m.buttons & 2)) != (npe_sdl.om.buttons & 2)){ /* middle */
+			e->type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
+			e->button.button = SDL_BUTTON_MIDDLE;
+			npe_sdl.om.buttons = (npe_sdl.om.buttons & ~2) | (npe_sdl.m.buttons & 2);
+			goto Filter;
+		}
+		if((down = (npe_sdl.m.buttons & 4)) != (npe_sdl.om.buttons & 4)){ /* right */
+			e->type = down ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP;
+			e->button.button = SDL_BUTTON_RIGHT;
+			npe_sdl.om.buttons = (npe_sdl.om.buttons & ~4) | (npe_sdl.m.buttons & 4);
+			goto Filter;
+		}
+		if(npe_sdl.m.buttons & (8|16)){
+			e->type = SDL_MOUSEWHEEL;
+			e->wheel.x = 0;
+			e->wheel.y = (npe_sdl.m.buttons & 8) ? 1 : -1;
+			goto Filter;
+		}
+		break;
+#endif
+
+	case Cresize:
+		memset(e, 0, sizeof(*e));
+		npe_sdl.fullredraw = 1;
+		while(getwindow(display, Refnone) != 1)
+			;
+		e->type = SDL_EVENT_WINDOW_RESIZED;
+		/*
+		e->window.event = SDL_WINDOWEVENT_EXPOSED;
+		e->window.windowID = 1; //TODO more then one?
+		*/
+		goto Filter;
+
+	case Cevent:
+		memcpy(e, &evt, sizeof(*e));
+		goto Filter;
+	}
+
+	return 0;
+
+Filter:
+	if(filter.f != nil && filter.f(filter.aux, e) == 0)
+		return 0;
+	return 1;
+}
+
+int
+SDL_WaitEvent(SDL_Event *e)
+{
+	int r;
+	SDL_Event ee;
+
+	salt[Numchan].op = CHANEND;
+	if(e == nil){
+		r = SDL_PollEvent(&ee);
+		SDL_PushEvent(&ee);
+	} else
+		r = SDL_PollEvent(e);
+	salt[Numchan].op = CHANNOBLK;
+
+	return r;
+}
+
+SDL_Scancode
+SDL_GetScancodeFromKey(SDL_Keycode 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_RETURN;
+	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;
+	if(r == Kins) return SDL_SCANCODE_INSERT;
+	if(r == Khome) return SDL_SCANCODE_HOME;
+	if(r == Kpgup) return SDL_SCANCODE_PAGEUP;
+	if(r == Kdel) return SDL_SCANCODE_DELETE;
+	if(r == Kend) return SDL_SCANCODE_END;
+	if(r == Kpgdown) return SDL_SCANCODE_PAGEDOWN;
+	if(r == Kctl) return SDL_SCANCODE_LCTRL;
+	if(r == Kshift) return SDL_SCANCODE_LSHIFT;
+	if(r == Kalt) return SDL_SCANCODE_LALT;
+	if(r == Kmod4) return SDL_SCANCODE_LGUI;
+	if(r == Kaltgr) return SDL_SCANCODE_RALT;
+
+	if(r >= (KF|1) && r <= (KF|12)) return SDL_SCANCODE_F1 + r - (KF|1);
+
+/* FIXME
+	SDL_SCANCODE_PRINTSCREEN = 0x46,
+	SDL_SCANCODE_SCROLLLOCK,
+	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;
+}
+
+static Uint8 kbdstate[SDL_NUM_SCANCODES];
+
+Uint8*
+SDL_GetKeyboardState(int *numkeys)
+{
+	if(numkeys != nil)
+		*numkeys = SDL_NUM_SCANCODES;
+	return kbdstate;
+}
+
+char*
+SDL_GetKeyName(SDL_Keycode key)
+{
+	/* upstream quirk: return value is valid only until next call */
+	static char res[64];
+
+	res[0] = res[1] = '\0';
+	if(key >= '0' && key <= '9' || key == '-' || key == '=' || key == '\'')
+		res[0] = key;
+	else if(key == '[' || key == ']' || key == '\\' || key == ';' || key == '/')
+		res[0] = key;
+	else if(key >= 'a' && key <= 'z')
+		res[0] = toupper(key);
+	else if(key >= (KF|1) && key <= (KF|12))
+		snprint(res, sizeof(res), "F%d", key - KF);
+	else if(key == '\n')
+		return "Return";
+	else if(key == Kesc)
+		return "Escape";
+	else if(key == Kbs)
+		return "Backspace";
+	else if(key == '\t')
+		return "Tab";
+	else if(key == ' ')
+		return "Space";
+	else if(key == Kright)
+		return "Right";
+	else if(key == Kleft)
+		return "Left";
+	else if(key == Kup)
+		return "Up";
+	else if(key == Kdown)
+		return "Down";
+	else if(key == Kins)
+		return "Insert";
+	else if(key == Khome)
+		return "Home";
+	else if(key == Kpgup)
+		return "PageUp";
+	else if(key == Kpgdown)
+		return "PageDown";
+	else if(key == Kdel)
+		return "Delete";
+	else if(key == Kend)
+		return "End";
+	else if(key == Kalt)
+		return "Left Alt";
+	else if(key == Kctl)
+		return "Left Ctrl";
+	else if(key == Kmod4)
+		return "Left GUI";
+	else if(key == Kaltgr)
+		return "Right Alt";
+	else if(key == Kshift)
+		return "Left Shift";
+
+	return res;
+}
+
+char *
+SDL_GetScancodeName(SDL_Scancode)
+{
+	/* FIXME */
+	return "";
+}
+
+SDL_Keycode
+SDL_GetKeyFromName(char *name)
+{
+	char *e;
+	long p;
+
+	if(strcmp(name, "Return") == 0) return '\n';
+	if(strcmp(name, "Escape") == 0) return Kesc;
+	if(strcmp(name, "Backspace") == 0) return Kbs;
+	if(strcmp(name, "Tab") == 0) return '\t';
+	if(strcmp(name, "Space") == 0) return ' ';
+	if(strcmp(name, "Right") == 0) return Kright;
+	if(strcmp(name, "Left") == 0) return Kleft;
+	if(strcmp(name, "Up") == 0) return Kup;
+	if(strcmp(name, "Down") == 0) return Kdown;
+	if(strcmp(name, "Insert") == 0) return Kins;
+	if(strcmp(name, "Home") == 0) return Khome;
+	if(strcmp(name, "PageUp") == 0) return Kpgup;
+	if(strcmp(name, "PageDown") == 0) return Kpgdown;
+	if(strcmp(name, "Delete") == 0) return Kdel;
+	if(strcmp(name, "End") == 0) return Kend;
+	if(strcmp(name, "Left Alt") == 0) return Kalt;
+	if(strcmp(name, "Left Ctrl") == 0) return Kctl;
+	if(strcmp(name, "Left GUI") == 0) return Kmod4;
+	if(strcmp(name, "Right Alt") == 0) return Kaltgr;
+	if(strcmp(name, "Left Shift") == 0) return Kshift;
+	if(name[0] == 'F' && name[1] != '\0'){
+		p = strtol(name+1, &e, 10);
+		if(e == name + 1 || p < 1 || p > 12)
+			return SDLK_UNKNOWN;
+		return KF | p;
+	}
+	if(name[0] >= '0' && name[0] <= '9')
+		return name[0];
+	if(name[0] >= 'A' && name[0] <= 'Z')
+		return tolower(name[0]);
+	if(name[0] >= 'a' && name[0] <= 'z')
+		return name[0];
+	if(name[0] == '-' || name[0] == '=' || name[0] == '\'')
+		return name[0];
+	if(name[0] == '[' || name[0] == ']' || name[0] == '\\' || name[0] == ';' || name[0] == '/')
+		return name[0];
+	return SDLK_UNKNOWN;
+}
+
+static void
+kbdproc(void *)
+{
+	char buf[128], buf2[128], *s;
+	int kfd, n, kbin, t;
+	Rune r, scan, o;
+
+	threadsetname("kbdproc");
+	if((kfd = open("/dev/kbd", OREAD|OCEXEC)) < 0)
+		sysfatal("/dev/kbd: %r");
+	if((kbin = open("/dev/kbin", OWRITE|OCEXEC)) < 0)
+		sysfatal("kbdproc: %r");
+
+	buf2[0] = 0;
+	buf2[1] = 0;
+	buf[0] = 0;
+	kmod = 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){
+				if(ISTEXT(r))
+					o = r;
+				send(salt[Ckey].c, &o);
+				send(salt[Ckeytype].c, &t);
+				t = Rrepeat;
+			}
+		default:
+			continue;
+
+		case 'k':
+			s = buf+1;
+			memset(kbdstate, 0, sizeof(kbdstate));
+			while(*s){
+				s += chartorune(&r, s);
+				scan = SDL_GetScancodeFromKey(r);
+				if(scan < nelem(kbdstate))
+					kbdstate[scan] = 1;
+				if(utfrune(buf2+1, r) == nil){
+					t = Rdown;
+					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 |= SDL_KMOD_LALT;
+					}else if (r == Kshift)
+						kmod |= SDL_KMOD_LSHIFT;
+					else if(r == Kctl)
+						kmod |= SDL_KMOD_LCTRL;
+					else if(r == Kaltgr)
+						kmod |= SDL_KMOD_RALT;
+					else if(r == Kmod4)
+						kmod |= SDL_KMOD_LGUI;
+					else{
+						o = npe_sdl.textinput ? r : tolowerrune(r);
+						continue;
+					}
+					o = r;
+					send(salt[Ckey].c, &r);
+					send(salt[Ckeytype].c, &t);
+					t = Rrepeat;
+				}
+			}
+			break;
+
+		case 'K':
+			s = buf2+1;
+			memset(kbdstate, 0, sizeof(kbdstate));
+			while(*s){
+				s += chartorune(&r, s);
+				scan = SDL_GetScancodeFromKey(r);
+				if(scan < nelem(kbdstate))
+					kbdstate[scan] = 1;
+				if(utfrune(buf+1, r) == nil){
+					if(r == Kalt)
+						kmod &= ~SDL_KMOD_LALT;
+					else if(r == Kshift)
+						kmod &= ~SDL_KMOD_LSHIFT;
+					else if(r == Kctl)
+						kmod &= ~SDL_KMOD_LCTRL;
+
+					t = Rup;
+					send(salt[Ckey].c, &r);
+					send(salt[Ckeytype].c, &t);
+				}
+			}
+			break;
+		}
+		strcpy(buf2, buf);
+	}
+	if(n < 0)
+		fprint(2, "kbdproc: %r\n");
+	threadexits(nil);
+}
+
+static void
+mouseproc(void *)
+{
+	Mouse m;
+	for(;;){
+		recv(npe_sdl.mctl->c, &m);
+		send(salt[Cmouse].c, &m);
+		if(npe_sdl.mgrab == SDL_TRUE){
+			if(!ptinrect(m.xy, npe_sdl.grabout)){
+				moveto(npe_sdl.mctl, npe_sdl.center);
+				/* both events need to be pushed to make sure that the
+				 * next delta makes sense; this one is discarded */
+				m.xy = Pt(-1,-1);
+				send(salt[Cmouse].c, &m);
+			}
+		}
+	}
+}
--- /dev/null
+++ b/libnpe_sdl3/gamepad.c
@@ -1,0 +1,99 @@
+#include "_sdl.h"
+
+enum{
+	Dummy,
+};
+struct SDL_Gamepad{
+	char *name;
+};
+static SDL_Gamepad dummy = {
+	.name "dummy",
+};
+
+SDL_JoystickID*
+SDL_GetGamepads(int *count)
+{
+	int n;
+	SDL_JoystickID *js;
+
+	//werrstr("SDL_GetGamepads: not implemented");
+	//return nil;	// nil on failure or null terminated array to be freed with SDL_free
+
+	n = 1;
+	if((js = mallocz((n+1) * sizeof *js, 1)) == nil)
+		sysfatal("SDL_GetGamepads: %r");
+	js[0] = Dummy;
+	if(count != nil)
+		*count = n;
+	return js;
+}
+
+int
+SDL_AddGamepadMappingsFromIO(SDL_IOStream *src, bool closeio)
+{
+	return 8;
+	//return 0;	// number of mappings or -1 on fail
+}
+
+bool
+SDL_IsGamepad(SDL_JoystickID id)
+{
+	if(id == Dummy)
+		return true;
+	return false;
+}
+
+SDL_Gamepad*
+SDL_OpenGamepad(SDL_JoystickID id)
+{
+	if(id != Dummy)
+		return nil;
+	return &dummy;
+}
+
+const char*
+SDL_GetGamepadName(SDL_Gamepad *g)
+{
+	return g->name;
+}
+
+const char*
+SDL_GetGamepadStringForAxis(SDL_GamepadAxis a)
+{
+	return "";
+}
+
+const char*
+SDL_GetGamepadStringForButton(SDL_GamepadButton b)
+{
+	return "";
+}
+
+SDL_GamepadButtonLabel
+SDL_GetGamepadButtonLabelForType(SDL_GamepadType t, SDL_GamepadButton b)
+{
+	return SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN;
+}
+
+SDL_GamepadButtonLabel
+SDL_GetGamepadButtonLabel(SDL_Gamepad *g, SDL_GamepadButton b)
+{
+	return SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN;
+}
+
+void
+SDL_CloseGamepad(SDL_Gamepad *g)
+{
+	USED(g);
+}
+
+void
+npe_sdl_kill_gamepad(void)
+{
+}
+
+int
+npe_sdl_init_gamepad(void)
+{
+	return 0;
+}
--- /dev/null
+++ b/libnpe_sdl3/iostream.c
@@ -1,0 +1,265 @@
+#include "_sdl.h"
+#include <bio.h>
+
+typedef struct npe_sdl_io npe_sdl_io;
+
+typedef struct {
+	const uchar *memdata;
+	int memn;
+	int mempos;
+} Membuf;
+
+static vlong memsize(struct SDL_IOStream *);
+static vlong memseek(struct SDL_IOStream *, vlong, int);
+static size_t memread(struct SDL_IOStream *, void *, size_t);
+static size_t memwrite(struct SDL_IOStream *, const void *, size_t);
+static int memclose(struct SDL_IOStream *);
+
+static vlong bsize(struct SDL_IOStream *);
+static vlong bseek(struct SDL_IOStream *, vlong, int);
+static size_t bread(struct SDL_IOStream *, void *, size_t);
+static size_t bwrite(struct SDL_IOStream *, const void *, size_t);
+static int bclose(struct SDL_IOStream *);
+
+struct npe_sdl_io {
+	union {
+		Biobuf;
+		Membuf;
+	};
+};
+
+struct SDL_IOStream {
+	vlong (*size)(struct SDL_IOStream *);
+	vlong (*seek)(struct SDL_IOStream *, vlong, int);
+	size_t (*read)(struct SDL_IOStream *, void *, size_t);
+	size_t (*write)(struct SDL_IOStream *, const void *, size_t);
+	int (*close)(struct SDL_IOStream *);
+	npe_sdl_io *p;
+};
+
+SDL_IOStream *
+SDL_IOFromFile(const char *file, const char *m)
+{
+	SDL_IOStream *o;
+	int f, mode;
+
+	o = nil;
+	mode = -1;
+	for(; m != nil && *m; m++){
+		if(*m == 'r'){
+			if(mode == OWRITE){
+badmode:
+				werrstr("either read or write supported only");
+				return nil;
+			}
+			mode = OREAD;
+		}else if(*m == 'w'){
+			if(mode == OREAD)
+				goto badmode;
+			mode = OWRITE;
+		}
+	}
+	if(mode < 0)
+		goto badmode;
+
+	mode |= OCEXEC;
+	f = mode & OREAD ? open(file, mode) : create(file, mode, 0644);
+	if(f >= 0 &&
+	    (o = calloc(1, sizeof(*o)+sizeof(npe_sdl_io))) != nil &&
+		Binit((o->p = (void*)(o+1)), f, mode) == 0){
+		o->size = bsize;
+		o->seek = bseek;
+		o->read = bread;
+		o->write = bwrite;
+		o->close = bclose;
+		return o;
+	}
+
+	if(f >= 0)
+		close(f);
+	free(o);
+
+	return nil;
+}
+
+SDL_IOStream*
+SDL_IOFromConstMem(const void *mem, size_t size)
+{
+	SDL_IOStream *o;
+	Membuf *b;
+
+	o = calloc(1, sizeof(*o)+sizeof(npe_sdl_io));
+	if(o == nil)
+		return nil;
+	o->p = (void*)(o+1);
+	b = (void*)o->p;
+	b->memdata = mem;
+	b->memn = size;
+	b->mempos = 0;
+
+	o->size = memsize;
+	o->seek = memseek;
+	o->read = memread;
+	o->write = memwrite;
+	o->close = memclose;
+	return o;
+}
+
+size_t
+SDL_ReadIO(SDL_IOStream *o, void *b, size_t n)
+{
+	return o->read ? o->read(o, b, n) : 0;
+}
+
+size_t
+SDL_WriteIO(SDL_IOStream *o, const void *b, size_t n)
+{
+	return o->write ? o->write(o, b, n) : 0;
+}
+
+vlong
+SDL_SeekIO(SDL_IOStream *o, vlong off, int whence)
+{
+	return o->seek ? o->seek(o, off, whence) : -1;
+}
+
+vlong
+SDL_TellIO(SDL_IOStream *o)
+{
+	return o->seek ? o->seek(o, 0, 1) : -1;
+}
+
+vlong
+SDL_GetIOSize(SDL_IOStream *o)
+{
+	return o->size ? o->size(o) : -1;
+}
+
+bool
+SDL_CloseIO(SDL_IOStream *o)
+{
+	int r;
+
+	r = o->close ? o->close(o) : 0;
+	if(r == 0)
+		free(o);
+	return r < 0 ? false : true;
+}
+
+static vlong
+bseek(struct SDL_IOStream *o, vlong off, int whence)
+{
+	return Bseek(o->p, off, whence);
+}
+
+static size_t
+bread(struct SDL_IOStream *o, void *b, size_t n)
+{
+	vlong x;
+
+	if((x = Bread(o->p, b, n)) != n){
+		if(x > 0)
+			Bseek(o->p, -x, 1);
+	}
+	return x < 0 ? 0 : x;
+}
+
+static size_t
+bwrite(struct SDL_IOStream *o, const void *b, size_t n)
+{
+	vlong x;
+
+	x = Bwrite(o->p, b, n); /* FIXME dunno what to do with partial writes */
+	return x < 0 ? 0 : x;
+}
+
+static vlong
+bsize(struct SDL_IOStream *o)
+{
+	Dir *s;
+	vlong sz;
+
+	sz = -1;
+	if((s = dirfstat(Bfildes(o->p))) != nil){
+		sz = s->length;
+		free(s);
+	}
+
+	return sz;
+}
+
+static int
+bclose(struct SDL_IOStream *o)
+{
+	return Bterm(o->p);
+}
+
+static vlong
+memseek(struct SDL_IOStream *o, vlong off, int whence)
+{
+	Membuf *b;
+
+	b = (Membuf*)o->p;
+	switch(whence){
+	case 0:
+		b->mempos = off;
+		break;
+	case 1:
+		b->mempos += off;
+		break;
+	case 2:
+		b->mempos = b->memn - 1;
+		b->mempos -= off;
+		break;
+	}
+	if(b->mempos < 0)
+		b->mempos = 0;
+
+	return b->mempos;
+}
+
+static size_t
+memread(struct SDL_IOStream *o, void *b, size_t n)
+{
+	Membuf *buf;
+	const uchar *dot, *end;
+
+	buf = (Membuf*)o->p;
+	end = buf->memdata + buf->memn;
+	dot = buf->memdata + buf->mempos;
+	assert(dot <= end);
+	if(dot + n >= end)
+		n = end - dot;
+	memmove(b, dot, n);
+	buf->mempos += n;
+	return n;
+}
+
+static size_t
+memwrite(struct SDL_IOStream *o, const void *b, size_t n)
+{
+	Membuf *buf;
+
+	if(n <= 0)
+		return 0;
+	buf = (Membuf*)o->p;
+	memmove(buf->mempos + buf->memdata, b, n);
+	buf->mempos += n;
+	return n;
+}
+
+static vlong
+memsize(struct SDL_IOStream *o)
+{
+	Membuf *b;
+	b = (Membuf*)o->p;
+
+	return b->memn;
+}
+
+static int
+memclose(struct SDL_IOStream *o)
+{
+	USED(o);
+	return 0;
+}
--- /dev/null
+++ b/libnpe_sdl3/log.c
@@ -1,0 +1,145 @@
+#include "_sdl.h"
+
+static SDL_LogPriority logpri[SDL_LOG_CATEGORY_COUNT] = {
+	[SDL_LOG_CATEGORY_APPLICATION] SDL_LOG_PRIORITY_INFO,
+	[SDL_LOG_CATEGORY_ASSERT] SDL_LOG_PRIORITY_WARN,
+	[SDL_LOG_CATEGORY_TEST] SDL_LOG_PRIORITY_VERBOSE,
+};
+static char *logcat[SDL_LOG_CATEGORY_COUNT] = {
+	[SDL_LOG_CATEGORY_APPLICATION] "app",
+	[SDL_LOG_CATEGORY_ERROR] "error",
+	[SDL_LOG_CATEGORY_ASSERT] "assert",
+	[SDL_LOG_CATEGORY_SYSTEM] "system",
+	[SDL_LOG_CATEGORY_AUDIO] "audio",
+	[SDL_LOG_CATEGORY_VIDEO] "video",
+	[SDL_LOG_CATEGORY_RENDER] "render",
+	[SDL_LOG_CATEGORY_INPUT] "input",
+	[SDL_LOG_CATEGORY_TEST] "test",
+};
+
+static void	warn(void*, int, SDL_LogPriority, const char*);
+
+static SDL_LogOutputFunction logfn = warn;
+static void *logaux;
+
+static char*
+evsmprint(char *fmt, va_list arg)
+{
+	char *s;
+
+	if((s = vsmprint(fmt, arg)) == nil)
+		sysfatal("smprint: %r");
+	setmalloctag(s, getcallerpc(&fmt));
+	return s;
+}
+
+static void
+warn(void *, int cat, SDL_LogPriority p, const char *msg)
+{
+	char *l;
+
+	if(cat < 0 || cat >= nelem(logcat))
+		l = "user";
+	else{
+		if(logpri[cat] == SDL_LOG_PRIORITY_INVALID)
+			logpri[cat] = SDL_LOG_PRIORITY_ERROR;
+		if(p < logpri[cat])
+			return;
+		l = logcat[cat];
+	}
+	fprint(2, "[%s] %s\n", l, msg);
+}
+
+void
+SDL_SetLogPriorities(SDL_LogPriority p)
+{
+	SDL_LogPriority *lp;
+
+	if(p <= SDL_LOG_PRIORITY_INVALID || p >= SDL_LOG_PRIORITY_COUNT){
+		SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_SetLogPriorities: invalid priority %d", p);
+		return;
+	}
+	for(lp=logpri; lp<logpri+nelem(logpri);)
+		*lp++ = p;
+}
+
+void
+SDL_GetLogOutputFunction(SDL_LogOutputFunction *callback, void **userdata)
+{
+	if(callback != nil)
+		*callback = logfn;
+	if(userdata != nil)
+		*userdata = logaux;
+}
+
+void
+SDL_SetLogOutputFunction(SDL_LogOutputFunction callback, void *userdata)
+{
+	if(callback == nil){	/* FIXME: verify */
+		SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_SetLogOutputFunction: invalid callback");
+		return;
+	}
+	logfn = callback;
+	logaux = userdata;
+}
+
+static void
+logmsg(int cat, SDL_LogPriority p, const char *fmt, va_list arg)
+{
+	char *s;
+
+	s = evsmprint(fmt, arg);
+	logfn(logaux, cat, p, s);
+	free(s);
+}
+
+void
+SDL_Log(const char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	logmsg(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, fmt, arg);
+	va_end(arg);
+}
+
+void
+SDL_LogDebug(int cat, const char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	logmsg(cat, SDL_LOG_PRIORITY_DEBUG, fmt, arg);
+	va_end(arg);
+}
+
+void
+SDL_LogInfo(int cat, const char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	logmsg(cat, SDL_LOG_PRIORITY_INFO, fmt, arg);
+	va_end(arg);
+}
+
+void
+SDL_LogError(int cat, const char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	logmsg(cat, SDL_LOG_PRIORITY_ERROR, fmt, arg);
+	va_end(arg);
+}
+
+void
+SDL_LogCritical(int cat, const char *fmt, ...)
+{
+	va_list arg;
+
+	va_start(arg, fmt);
+	logmsg(cat, SDL_LOG_PRIORITY_CRITICAL, fmt, arg);
+	va_end(arg);
+}
+
--- /dev/null
+++ b/libnpe_sdl3/mkfile
@@ -1,0 +1,33 @@
+</$objtype/mkfile
+
+LIB=/$objtype/lib/libnpe_sdl3.a
+CFLAGS=$CFLAGS -p -I/sys/include/npe -I../libnpe -D__plan9__ -D__${objtype}__
+
+HFILES=\
+	_sdl.h\
+
+OFILES=\
+	audio.$O\
+	cursor.$O\
+	events.$O\
+	gamepad.$O\
+	iostream.$O\
+	log.$O\
+	mutex.$O\
+	pixels.$O\
+	render.$O\
+	scale.$O\
+	sdl3.$O\
+	surface.$O\
+	texture.$O\
+	threads.$O\
+	window.$O\
+
+# FIXME: ???
+UPDATE=\
+	mkfile\
+	$HFILES\
+	${OFILES:%.$O=%.c}\
+	${LIB:/$objtype/%=/386/%}\
+
+</sys/src/cmd/mksyslib
--- /dev/null
+++ b/libnpe_sdl3/mutex.c
@@ -1,0 +1,65 @@
+#include "_sdl.h"
+
+SDL_Mutex*
+SDL_CreateMutex(void)
+{
+	SDL_Mutex *m;
+
+	m = mallocz(sizeof(*m), 1);
+	return m;
+}
+
+void
+SDL_DestroyMutex(SDL_Mutex *m)
+{
+	free(m);
+}
+
+bool
+SDL_LockMutex(SDL_Mutex *m)
+{
+	lock(&m->l);
+	return true;
+}
+
+bool
+SDL_UnlockMutex(SDL_Mutex *m)
+{
+	unlock(&m->l);
+	return true;
+}
+
+SDL_Condition*
+SDL_CreateCondition(void)
+{
+	SDL_Condition *p;
+
+	if((p = mallocz(sizeof *p, 1)) == nil)
+		sysfatal("SDL_CreateCondition: %r");
+	p->l = &p->QLock;
+	return p;
+}
+
+void
+SDL_SignalCondition(SDL_Condition *p)
+{
+	qlock(p);
+	rwakeup(p);
+	qunlock(p);
+}
+
+void
+SDL_WaitCondition(SDL_Condition *p, SDL_Mutex *m)
+{
+	SDL_UnlockMutex(m);
+	qlock(p);
+	rsleep(p);
+	qunlock(p);
+	SDL_LockMutex(m);
+}
+
+void
+SDL_DestroyCondition(SDL_Condition *p)
+{
+	free(p);
+}
--- /dev/null
+++ b/libnpe_sdl3/pixels.c
@@ -1,0 +1,232 @@
+#include "_sdl.h"
+
+int
+chan2mask(Uint32 chan, int *bpp, Uint32 *rm, Uint32 *gm, Uint32 *bm, Uint32 *am)
+{
+	switch(chan){
+	case ARGB32:
+		*am = 0xFF000000;
+		*rm = 0x00FF0000;
+		*gm = 0x0000FF00;
+		*bm = 0x000000FF;
+		*bpp = 32;
+		break;
+	case XRGB32:
+		*am = 0x00000000;
+		*rm = 0x00FF0000;
+		*gm = 0x0000FF00;
+		*bm = 0x000000FF;
+		*bpp = 32;
+		break;
+	case ABGR32:
+		*am = 0xFF000000;
+		*rm = 0x000000FF;
+		*gm = 0x0000FF00;
+		*bm = 0x00FF0000;
+		*bpp = 32;
+	case XBGR32:
+		*am = 0x00000000;
+		*rm = 0x000000FF;
+		*gm = 0x0000FF00;
+		*bm = 0x00FF0000;
+		*bpp = 32;
+		break;
+	case RGB24:
+		*am = 0x00000000;
+		*rm = 0x00FF0000;
+		*gm = 0x0000FF00;
+		*bm = 0x000000FF;
+		*bpp = 24;
+		break;
+	case BGR24:
+		*am = 0x00000000;
+		*rm = 0x000000FF;
+		*gm = 0x0000FF00;
+		*bm = 0x00FF0000;
+		*bpp = 24;
+		break;
+	case CMAP8:
+		*am = *rm = *gm = *bm = 0x00000000;
+		*bpp = 8;
+		break;
+	default:
+		assert(0);
+	}
+	return 0;
+}
+
+ulong
+mask2chan(int bpp, Uint32 rm, Uint32 gm, Uint32 bm, Uint32 am)
+{
+	USED(gm, bm);
+
+	switch(bpp){
+	case 8:
+		return CMAP8;
+	case 24:
+		if(rm & 0xFF0000)
+			return RGB24;
+		else
+			return BGR24;
+	case 32:
+		if(am == 0){
+			if(rm & 0xFF0000)
+				return XRGB32;
+			else
+				return XBGR32;
+		} else {
+			if(rm & 0xFF0000)
+				return ARGB32;
+			else
+				return ABGR32;
+		}
+	}
+	assert(0);
+	return 0;
+}
+
+Uint32
+chan2pixel(ulong chan)
+{
+	switch(chan){
+	case ARGB32:
+		return SDL_PIXELFORMAT_ARGB8888;
+	case XRGB32:
+		return SDL_PIXELFORMAT_XRGB8888;
+	case RGB24:
+		return SDL_PIXELFORMAT_RGB24;
+	case ABGR32:
+		return SDL_PIXELFORMAT_ABGR8888;
+	case XBGR32:
+		return SDL_PIXELFORMAT_XBGR8888;
+	case BGR24:
+		return SDL_PIXELFORMAT_BGR24;
+	case CMAP8:
+		return SDL_PIXELFORMAT_INDEX8;
+	}
+	assert(0);
+	return 0;
+}
+
+ulong
+pixel2chan(Uint32 format)
+{
+	switch(format){
+	case SDL_PIXELFORMAT_ARGB8888:
+		return ARGB32;
+	case SDL_PIXELFORMAT_XRGB8888:
+		return XRGB32;
+	case SDL_PIXELFORMAT_RGB24:
+		return RGB24;
+	case SDL_PIXELFORMAT_ABGR8888:
+		return ABGR32;
+	case SDL_PIXELFORMAT_XBGR8888:
+		return XBGR32;
+	case SDL_PIXELFORMAT_BGR24:
+		return BGR24;
+	case SDL_PIXELFORMAT_INDEX8:
+		return CMAP8;
+	}
+	assert(0);
+	return 0;
+}
+
+void
+SDL_GetRGB(Uint32 pixel, SDL_PixelFormat *fmt, Uint8 *r, Uint8 *g, Uint8 *b)
+{
+	SDL_Color *c;
+
+	switch(fmt->format){
+	case SDL_PIXELFORMAT_ARGB8888:
+	case SDL_PIXELFORMAT_XRGB8888:
+	case SDL_PIXELFORMAT_RGB24:
+		*r = pixel>>16;
+		*g = pixel>>8;
+		*b = pixel;
+		break;
+	case SDL_PIXELFORMAT_ABGR8888:
+	case SDL_PIXELFORMAT_XBGR8888:
+	case SDL_PIXELFORMAT_BGR24:
+		*b = pixel>>16;
+		*g = pixel>>8;
+		*r = pixel;
+		break;
+	case SDL_PIXELFORMAT_INDEX8:
+		assert(fmt->palette);
+		assert(pixel < fmt->palette->ncolors);
+		c = fmt->palette->colors + pixel;
+		*r = c->r;
+		*g = c->g;
+		*b = c->b;
+		break;
+	default:
+		assert(0);
+	}
+}
+
+SDL_bool
+SDL_PixelFormatEnumToMasks(Uint32 format, int *bpp, Uint32 *Rmask, Uint32 *Gmask, Uint32 *Bmask, Uint32 *Amask)
+{
+	ulong c;
+
+	if(bpp == nil || Rmask == nil || Gmask == nil || Bmask == nil || Amask == nil)
+		return SDL_FALSE;
+	c = pixel2chan(format);
+	chan2mask(c, bpp, Rmask, Gmask, Bmask, Amask);
+	return SDL_TRUE;
+}
+
+Uint32
+SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b)
+{
+	SDL_Color *c;
+
+	switch(fmt->format){
+	case SDL_PIXELFORMAT_ARGB8888:
+	case SDL_PIXELFORMAT_XRGB8888:
+	case SDL_PIXELFORMAT_RGB24:
+		return 0xff<<24 | r<<16 | g<<8 | b;
+	case SDL_PIXELFORMAT_ABGR8888:
+	case SDL_PIXELFORMAT_XBGR8888:
+	case SDL_PIXELFORMAT_BGR24:
+		return 0xff<<24 | b<<16 | g<<8 | r;
+	case SDL_PIXELFORMAT_INDEX8:
+		assert(fmt->palette);
+		for(c = fmt->palette->colors; c < fmt->palette->colors + fmt->palette->ncolors; c++){
+			if(c->r == r && c->g == g && c->b == b)
+				return c - fmt->palette->colors;
+		}
+	default:
+		assert(0);
+		return 0;
+	}
+}
+
+void
+npe_sdl_kill_draw(void)
+{
+}
+
+int
+npe_sdl_init_draw(void)
+{
+	int bpp;
+
+	if(screen != nil)
+		return 0;
+	if(memimageinit() < 0 || initdraw(nil, nil, argv0) < 0)
+		return -1;
+	draw(screen, screen->r, display->black, nil, ZP);
+	if(flushimage(display, 1) < 0)
+		fprint(2, "npe_sdl_init_draw: %r\n");
+	npe_sdl.physw = Dx(screen->r);
+	npe_sdl.physh = Dy(screen->r);
+	npe_sdl.scale = 1;
+	if(npe_sdl_init_tex() < 0)
+		return -1;
+	if(chan2mask(screen->chan, &bpp, &npe_sdl.defmask.r, &npe_sdl.defmask.g, &npe_sdl.defmask.b, &npe_sdl.defmask.a) < 0){
+		werrstr("unsupported screen channel");
+		return -1;
+	}
+	return 0;
+}
--- /dev/null
+++ b/libnpe_sdl3/render.c
@@ -1,0 +1,326 @@
+#include "_sdl.h"
+
+static SDL_Renderer oneren;
+static u8int *backcopy;
+static Image *front;
+static u32int renddrawcol = DBlack;
+
+bool
+SDL_GetRenderVSync(SDL_Renderer *, int *)
+{
+	return false;
+}
+
+bool
+SDL_SetRenderVSync(SDL_Renderer *, int)
+{
+	return true;
+}
+
+bool
+SDL_SetRenderDrawBlendMode(SDL_Renderer *, SDL_BlendMode blendMode)
+{
+	if(blendMode != SDL_BLENDMODE_NONE){
+		werrstr("SDL_SetRenderDrawBlendMode: only SDL_BLENDMODE_NONE is supported");
+		return false;
+	}
+	return true;
+}
+
+bool
+SDL_GetRendererInfo(SDL_Renderer *, SDL_RendererInfo *info)
+{
+	if(info == nil)	
+		return false;
+	info->max_texture_width = npe_sdl.physw;
+	info->max_texture_height = npe_sdl.physh;
+	return true;
+}
+
+bool
+SDL_SetRenderDrawColor(SDL_Renderer *, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
+{
+	renddrawcol = r<<24 | g<<16 | b<<8 | a;
+	if(npe_sdl.rendcol != nil)
+		memfillcolor(npe_sdl.rendcol, renddrawcol);
+	return true;
+}
+
+bool
+SDL_SetRenderLogicalPresentation(SDL_Renderer *r, int w, int h, SDL_RendererLogicalPresentation)
+{
+	if(r->logiw != w || r->logih != h){
+		r->logiw = w;
+		r->logih = h;
+		npe_sdl.fullredraw = 1;
+	}
+	return true;
+}
+
+bool
+SDL_GetRendererOutputSize(SDL_Renderer *r, int *w, int *h)
+{
+	if(w != nil)
+		*w = r->logiw;
+	if(h != nil)
+		*h = r->logih;
+	return true;
+}
+
+void
+SDL_RenderGetViewport(SDL_Renderer *r, SDL_Rect *rect)
+{
+	rect->x = rect->y = 0;
+	rect->w = r->logiw;
+	rect->h = r->logih;
+}
+
+bool
+SDL_RenderClear(SDL_Renderer *)
+{
+	if(npe_sdl.back != nil)
+		memfillcolor(npe_sdl.back, renddrawcol);
+	return true;
+}
+
+bool
+SDL_RenderFillRect(SDL_Renderer *r, SDL_FRect *rect)
+{
+	Rectangle rr;
+
+	if(npe_sdl.back == nil)
+		return true;
+	if(rect == nil)
+		return SDL_RenderClear(r);
+	if(npe_sdl.rendcol == nil)
+		return true;
+	rr = Rect(rect->x, rect->y, rect->x+rect->w, rect->y+rect->h);
+	memimagedraw(npe_sdl.back, rr, npe_sdl.rendcol, ZP, nil, ZP, SoverD);
+	return true;
+}
+
+bool
+SDL_RenderLines(SDL_Renderer *, SDL_FPoint *pp, int n)
+{
+	Point p0, p1;
+	SDL_FPoint *p;
+
+	if(npe_sdl.rendcol == nil)
+		return false;
+	for(p=pp; n>1; n--, p++){
+		if(p == pp){
+			p0 = Pt(p->x, p->y);
+			p++;
+		}
+		p1 = Pt(p->x, p->y);
+		memimageline(npe_sdl.back, p0, p1, 0, 0, 0, npe_sdl.rendcol, ZP, SoverD);
+		p0 = p1;
+	}
+	return true;
+}
+
+bool
+SDL_RenderPoints(SDL_Renderer *, SDL_FPoint *pp, int n)
+{
+	Point c;
+	SDL_FPoint *p;
+
+	if(npe_sdl.rendcol == nil)
+		return false;
+	for(p=pp; n>0; n--, p++){
+		c = Pt(p->x, p->y);
+		memellipse(npe_sdl.back, c, 0, 0, 0, npe_sdl.rendcol, ZP, SoverD);
+	}
+	return true;
+}
+
+static int
+duff(SDL_BlendMode mode)
+{
+	if(mode == SDL_BLENDMODE_BLEND)
+		return SoverD;
+	return S;
+}
+
+bool
+SDL_RenderTexture(SDL_Renderer *rend, SDL_Texture *t, const SDL_FRect *sre, const SDL_FRect *dre)
+{
+	Rectangle sr, dr;
+	int logiw, logih;
+	ulong chan;
+	Memimage *m;
+
+	if(rend->logiw > 0 && rend->logih > 0){
+		logiw = rend->logiw;
+		logih = rend->logih;
+	}else{
+		logiw = npe_sdl.physw;
+		logih = npe_sdl.physh;
+	}
+
+	sr = t->m->r;
+	if(sre != nil){
+		sr.min = Pt(sre->x, sre->y);
+		sr.max = addpt(sr.min, Pt(sre->w, sre->h));
+	}
+
+	dr = Rect(0, 0, logiw, logih);
+	if(dre != nil){
+		dr.min = Pt(dre->x, dre->y);
+		dr.max = addpt(dr.min, Pt(dre->w, dre->h));
+	}
+
+	if(npe_sdl.back == nil || Dx(npe_sdl.back->r) != logiw || Dy(npe_sdl.back->r) != logih){
+		freememimage(npe_sdl.back);
+		if(screen)
+			chan = screen->chan;
+		else
+			chan = ARGB32;
+		npe_sdl.back = allocmemimage(Rect(0, 0, logiw, logih), chan);
+		if(npe_sdl.back == nil){
+			werrstr("SDL_RenderCopy: %r");
+			return false;
+		}
+		npe_sdl.rendcol = allocmemimage(Rect(0, 0, logiw, 1), npe_sdl.back->chan);
+		if(npe_sdl.rendcol != nil){
+			memfillcolor(npe_sdl.rendcol, renddrawcol);
+			npe_sdl.rendcol->flags |= Frepl;
+			npe_sdl.rendcol->clipr = memwhite->clipr;
+		}
+		npe_sdl.target->m = npe_sdl.back;
+		free(backcopy);
+		backcopy = malloc(logiw*logih*4);
+	}
+
+	m = t->mod == nil ? t->m : t->mod;
+
+	if(Dx(dr)/Dx(sr) > 1 || Dy(dr)/Dy(sr) > 1)
+		npe_sdl_scale((u32int*)byteaddr(m, ZP), Dx(sr), Dy(sr), (u32int*)byteaddr(npe_sdl.back, ZP), logiw, logih);
+	else
+		memimagedraw(npe_sdl.back, dr, m, sr.min, nil, ZP, duff(t->blend));
+	return true;
+}
+
+bool
+SDL_RenderPresent(SDL_Renderer *rend)
+{
+	Rectangle r;
+	static u32int *b;
+	uchar *rb;
+	int logiw, logih;
+
+	/* may be called before SDL_RenderCopy is ever called;
+	 * nanobsp does this during initialization */
+	if(npe_sdl.back == nil)
+		return false;
+
+	if(rend->logiw > 0 && rend->logih > 0){
+		logiw = rend->logiw;
+		logih = rend->logih;
+	}else{
+		logiw = npe_sdl.physw;
+		logih = npe_sdl.physh;
+	}
+
+	npe_sdl.scale = (float)logiw / (float)npe_sdl.physw;
+
+	if(!npe_sdl.fullredraw && (npe_sdl.fullredraw = memcmp(backcopy, byteaddr(npe_sdl.back, ZP), logiw*logih*4)) == 0 && !npe_sdl.mredraw)
+		return false;
+
+	r = Rect(0, 0, npe_sdl.physw, npe_sdl.physh);
+	if(front != nil && (Dx(front->r) != npe_sdl.physw || Dy(front->r) != npe_sdl.physh)){
+		freeimage(front);
+		front = nil;
+		free(b);
+		b = nil;
+	}
+	if(b == nil && (b = realloc(b, npe_sdl.physw*npe_sdl.physh*4)) == nil){
+		fprint(2, "SDL_RenderPresent: %r\n");
+		return false;
+	}
+	if(npe_sdl.fullredraw || front == nil){
+		rb = npe_sdl_scale((u32int*)byteaddr(npe_sdl.back, ZP), Dx(npe_sdl.back->r), Dy(npe_sdl.back->r), b, npe_sdl.physw, npe_sdl.physh);
+		if(front == nil && (front = allocimage(display, r, XRGB32, 0, DNofill)) == nil){
+			fprint(2, "SDL_RenderPresent: %r\n");
+			return false;
+		}
+		if(loadimage(front, r, rb, Dx(r)*Dy(r)*4) < 0){
+			fprint(2, "SDL_RenderPresent: %r\n");
+			return false;
+		}
+	}
+	while(screen == nil && getwindow(display, Refnone) != 1)
+		/* drawterm window change lag */;
+	draw(screen, screen->r, front, nil, ZP);
+	npe_draw_cursor();
+	npe_sdl.mredraw = 0;
+
+	flushimage(display, 1);
+
+	if(npe_sdl.fullredraw)
+		memmove(backcopy, byteaddr(npe_sdl.back, ZP), logiw*logih*4);
+	else
+		replclipr(screen, 0, screen->r);
+	npe_sdl.fullredraw = 0;
+	return true;
+}
+
+bool
+SDL_RenderReadPixels(SDL_Renderer *rend, SDL_Rect *rect, Uint32 fmt, void *pixels, int pitch)
+{
+	Rectangle r, r2;
+	Memimage *m;
+	int n;
+
+	USED(pitch); /* FIXME pitch */
+
+	if(rect != nil)
+		r = Rect(rect->x, rect->y, rect->x+rect->w, rect->y+rect->h);
+	else
+		r = Rect(0, 0, rend->logiw, rend->logih);
+
+	n = Dx(r)*Dy(r);
+	switch(fmt){
+	case SDL_PIXELFORMAT_ARGB8888:
+	case SDL_PIXELFORMAT_XRGB8888:
+		n *= 4;
+		m = npe_sdl.back;
+		break;
+	case SDL_PIXELFORMAT_ABGR8888:
+		n *= 4;
+		r2 = Rect(0,0,Dx(r),Dy(r));
+		if((m = allocmemimage(r2, ABGR32)) == nil)
+			return false;
+		memfillcolor(m, DBlack);
+		memimagedraw(m, r2, npe_sdl.back, r.min, nil, ZP, S);
+		break;
+	case SDL_PIXELFORMAT_RGB24:
+		n *= 3;
+		r2 = Rect(0,0,Dx(r),Dy(r));
+		if((m = allocmemimage(r2, RGB24)) == nil)
+			return false;
+		memfillcolor(m, DBlack);
+		memimagedraw(m, r2, npe_sdl.back, r.min, nil, ZP, S);
+		break;
+	default:
+		werrstr("SDL_RenderReadPixels: format not supported");
+		return false;
+	}
+
+	unloadmemimage(m, r, pixels, n);
+	if(m != npe_sdl.back)
+		freememimage(m);
+	return true;
+}
+
+void
+SDL_DestroyRenderer(SDL_Renderer *)
+{
+	/* nothing to do here */
+}
+
+SDL_Renderer *
+SDL_CreateRenderer(SDL_Window *, char *)
+{
+	return &oneren;
+}
--- /dev/null
+++ b/libnpe_sdl3/scale.c
@@ -1,0 +1,27 @@
+#include "_sdl.h"
+
+void *
+npe_sdl_scale(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;
+}
--- /dev/null
+++ b/libnpe_sdl3/sdl3.c
@@ -1,0 +1,618 @@
+#include "_sdl.h"
+#include <bio.h>
+
+struct npe_sdl npe_sdl;
+
+static char basepath[PATH_MAX];
+
+bool
+SDL_InitSubSystem(int mask)
+{
+	if(basepath[0] == 0){
+		if(getwd(basepath, sizeof(basepath)) == nil)
+			strcpy(basepath, "/");
+	}
+	if(mask & SDL_INIT_VIDEO && npe_sdl_init_draw() < 0)
+		goto err;
+	if(mask & SDL_INIT_AUDIO && npe_sdl_init_audio() < 0)
+		goto err;
+	if(mask & SDL_INIT_EVENTS && npe_sdl_init_input() < 0)
+		goto err;
+	if(mask & SDL_INIT_GAMEPAD && npe_sdl_init_gamepad() < 0)
+		goto err;
+	return true;
+err:
+	return false;
+}
+
+bool
+SDL_QuitSubSystem(int mask)
+{
+	if(mask & SDL_INIT_AUDIO)
+		npe_sdl_kill_audio();
+	if(mask & SDL_INIT_VIDEO)
+		npe_sdl_kill_draw();
+	if(mask & SDL_INIT_EVENTS)
+		npe_sdl_kill_input();
+	if(mask & SDL_INIT_GAMEPAD)
+		npe_sdl_kill_gamepad();
+	/* FIXME: ... */
+	return true;
+}
+
+bool
+SDL_Init(int mask)
+{
+	return SDL_InitSubSystem(mask);
+}
+
+bool
+SDL_SetAppMetadata(const char *name, const char *ver, const char *id)
+{
+	USED(name, ver, id);
+	return true;
+}
+
+Uint64
+SDL_GetPerformanceFrequency(void)
+{
+	return _tos->cyclefreq;
+}
+
+Uint64
+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 *
+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;
+}
+
+bool
+SDL_GetWindowDisplayIndex(SDL_Window *)
+{
+	return true;
+}
+
+void
+SDL_Quit(void)
+{
+	/* FIXME deinitialize */
+}
+
+bool
+SDL_FillRect(SDL_Surface *dst, const SDL_Rect *rect, Uint32 color)
+{
+	Uint32 *p;
+	int i;
+	USED(rect);
+
+	switch(dst->format->format){
+	case SDL_PIXELFORMAT_XRGB8888:
+	case SDL_PIXELFORMAT_ARGB8888:
+	case SDL_PIXELFORMAT_XBGR8888:
+	case SDL_PIXELFORMAT_ABGR8888:
+		p = (Uint32*)dst->pixels;
+		for(i = 0; i < dst->n / sizeof(*p); i++)
+			p[i] = color;
+		break;
+	case SDL_PIXELFORMAT_RGB24:
+		for(i = 0; i < dst->n; i += 3){
+			dst->pixels[i+0] = color;
+			dst->pixels[i+1] = color>>8;
+			dst->pixels[i+2] = color>>16;
+		}
+		break;
+	case SDL_PIXELFORMAT_INDEX8:
+		for(i = 0; i < dst->n; i++)
+			dst->pixels[i] = color;
+		break;
+	}
+	return true;
+}
+
+SDL_Palette*
+SDL_AllocPalette(int ncolors)
+{
+	SDL_Palette *p;
+
+	p = malloc(sizeof(*p));
+	p->ncolors = ncolors;
+	p->colors = mallocz(sizeof(SDL_Color)*ncolors, 1);
+	return p;
+}
+
+bool
+SDL_SetPaletteColors(SDL_Palette *palette, const SDL_Color *colors, int firstcolor, int ncolors)
+{
+	int i;
+
+	assert(palette->ncolors >= firstcolor + ncolors);
+	for(i = firstcolor; i < firstcolor + ncolors; i++)
+		palette->colors[i] = colors[i - firstcolor];
+	return true;
+}
+
+void
+SDL_WarpMouseInWindow(SDL_Window *, int x, int y)
+{
+	moveto(npe_sdl.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 = npe_sdl.m.xy.x;
+	if(y != nil)
+		*y = npe_sdl.m.xy.y;
+
+	b = 0;
+	if(npe_sdl.m.buttons & 1)
+		b |= SDL_BUTTON_LMASK;
+	if(npe_sdl.m.buttons & 2)
+		b |= SDL_BUTTON_MMASK;
+	if(npe_sdl.m.buttons & 4)
+		b |= SDL_BUTTON_RMASK;
+
+	return b;
+}
+
+Uint32
+SDL_GetMouseState(int *x, int *y)
+{
+	Uint32 b;
+
+	b = SDL_GetGlobalMouseState(nil, nil);
+	if(x != nil)
+		*x = npe_sdl.m.xy.x - screen->r.min.x;
+	if(y != nil)
+		*y = npe_sdl.m.xy.y - screen->r.min.y;
+
+	return b;
+}
+
+void
+SDL_RenderGetScale(SDL_Renderer *, float *scaleX, float *scaleY)
+{
+	*scaleX = 1.0;
+	*scaleY = 1.0;
+}
+
+bool
+SDL_RenderSetIntegerScale(SDL_Renderer *, SDL_bool enable)
+{
+	/* FIXME */
+	USED(enable);
+	return true;
+}
+
+SDL_bool
+SDL_IsTextInputActive(void)
+{
+	return npe_sdl.textinput;
+}
+
+void
+SDL_StartTextInput(void)
+{
+	npe_sdl.textinput = SDL_TRUE;
+}
+
+void
+SDL_StopTextInput(void)
+{
+	npe_sdl.textinput = SDL_FALSE;
+}
+
+void
+SDL_Delay(Uint32 ms)
+{
+	npe_nsleep((uvlong)ms * Nmsec);
+}
+
+Uint32
+SDL_GetWindowFlags(SDL_Window *)
+{
+	/* FIXME is this correct? */
+	return SDL_WINDOW_INPUT_FOCUS;
+}
+
+bool
+SDL_GetDisplayUsableBounds(int displayIndex, SDL_Rect *r)
+{
+	if(displayIndex != 0)
+		return false;
+
+	if(r == nil)
+		return true;
+
+	r->x = display->image->r.min.x;
+	r->y = display->image->r.min.y;
+	r->w = Dx(display->image->r);
+	r->h = Dy(display->image->r);
+	return true;
+}
+
+bool
+SDL_GetDisplayBounds(int displayIndex, SDL_Rect *r)
+{
+	return SDL_GetDisplayUsableBounds(displayIndex, r);
+}
+
+bool
+SDL_GetDesktopDisplayMode(int displayIndex, SDL_DisplayMode *mode)
+{
+	if(displayIndex != 0)
+		return false;
+	mode->w = Dx(display->image->r);
+	mode->h = Dy(display->image->r);
+	mode->format = SDL_PIXELFORMAT_ARGB8888;
+	mode->refresh_rate = 0;
+	return true;
+}
+
+int
+SDL_GetCurrentDisplayMode(int displayIndex, SDL_DisplayMode *mode)
+{
+	return SDL_GetDesktopDisplayMode(displayIndex, mode);
+}
+
+int
+SDL_GetNumDisplayModes(int displayIndex)
+{
+	if(displayIndex != 0)
+		return -1;
+	return 1;
+}
+
+bool
+SDL_GetDisplayMode(int displayIndex, int modeIndex, SDL_DisplayMode *mode)
+{
+	USED(modeIndex);
+	return SDL_GetDesktopDisplayMode(displayIndex, mode);
+}
+
+void
+SDL_SetWindowTitle(SDL_Window *, char *title)
+{
+	int f;
+
+	if(title == nil)
+		return;
+	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);
+	}
+}
+
+int
+SDL_GetNumVideoDisplays(void)
+{
+	/* FIXME implement multihead for plan9 */
+	return 1;
+}
+
+SDL_bool
+SDL_SetHint(char *name, char *value)
+{
+	/* FIXME anyone cares about name="SDL_RENDER_SCALE_QUALITY" value="(best|nearest)"? */
+	if(strcmp(name, SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4) == 0){
+		npe_sdl.hints = (npe_sdl.hints & ~Altf4noclose) | (atoi(value) ? Altf4noclose : 0);
+		return SDL_TRUE;
+	}
+
+	return SDL_FALSE;
+}
+
+char *
+SDL_GetCurrentVideoDriver(void)
+{
+	return "/dev/draw";
+}
+
+Uint32
+SDL_GetTicks(void)
+{
+	return npe_nanosec() / Nmsec;
+}
+
+bool
+SDL_OpenURL(char *url)
+{
+	char tmp[PATH_MAX];
+	Plumbmsg m;
+	int f, r;
+
+	if((f = plumbopen("send", OWRITE|OCEXEC)) < 0)
+		return false;
+
+	memset(&m, 0, sizeof(m));
+	m.src = argv0;
+	m.wdir = getwd(tmp, sizeof(tmp));
+	m.type = "text";
+	m.data = url;
+	m.ndata = -1;
+	r = plumbsend(f, &m);
+	close(f);
+
+	return r < 0 ? false : true;
+}
+
+char *
+SDL_GetBasePath(void)
+{
+	return strdup(basepath);
+}
+
+char *
+SDL_GetPrefPath(char *, char *app)
+{
+	char *home, *p;
+
+	p = nil;
+	if((home = getenv("home")) != nil){
+		if((p = smprint("%s/lib/%s/", home, app)) != nil)
+			npe_mkdirp(p, 0755);
+	}
+
+	return p;
+}
+
+SDL_bool
+SDL_HasClipboardText(void)
+{
+	/* most def */
+	return SDL_TRUE;
+}
+
+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)
+{
+}
+
+void
+SDL_ClearError(void)
+{
+}
+
+int
+SDL_PeepEvents(SDL_Event *events, int numevents, SDL_eventaction action, Uint32 minType, Uint32 maxType)
+{
+	/* FIXME implement */
+	USED(events, numevents, action, minType, maxType);
+	return 0;
+}
+
+int
+SDL_NumJoysticks(void)
+{
+	/* FIXME implement */
+	return 0;
+}
+
+SDL_Joystick *
+SDL_JoystickOpen(int n)
+{
+	/* FIXME implement */
+	USED(n);
+	return nil;
+}
+
+void
+SDL_JoystickClose(SDL_Joystick *js)
+{
+	USED(js);
+}
+
+int
+SDL_JoystickNumAxes(SDL_Joystick *js)
+{
+	USED(js);
+	return -1;
+}
+
+int
+SDL_JoystickNumButtons(SDL_Joystick *js)
+{
+	USED(js);
+	return -1;
+}
+
+int
+SDL_JoystickNumHats(SDL_Joystick *js)
+{
+	USED(js);
+	return -1;
+}
+
+int
+SDL_JoystickNumBalls(SDL_Joystick *js)
+{
+	USED(js);
+	return -1;
+}
+
+int
+SDL_JoystickEventState(int state)
+{
+	USED(state);
+	return 0;
+}
+
+void
+SDL_JoystickUpdate(void)
+{
+}
+
+char*
+SDL_JoystickName(SDL_Joystick *js)
+{
+	USED(js);
+	return nil;
+}
+
+Sint16
+SDL_JoystickGetAxis(SDL_Joystick *js, int axis)
+{
+	USED(js); USED(axis);
+	return 0;
+}
+
+Uint8
+SDL_JoystickGetHat(SDL_Joystick *js, int hat)
+{
+	USED(js); USED(hat);
+	return 0;
+}
+
+Uint8
+SDL_JoystickGetButton(SDL_Joystick *js, int button)
+{
+	USED(js); USED(button);
+	return 0;
+}
+
+/* FIXME: not sdl3 */
+int
+SDL_SetRelativeMouseMode(SDL_bool enabled)
+{
+	if(screen){
+		npe_sdl.grabout = insetrect(screen->r, Dx(screen->r)/8);
+		npe_sdl.center = addpt(screen->r.min, Pt(Dx(screen->r)/2, Dy(screen->r)/2));
+		if(enabled)
+			SDL_HideCursor();
+		else
+			SDL_ShowCursor();
+	}
+	npe_sdl.mgrab = enabled;
+	return 0;
+}
+
+void
+SDL_SetMainReady(void)
+{
+}
+
+int
+SDL_GetRelativeMouseMode(void)
+{
+	return npe_sdl.mgrab;
+}
+
+Uint32
+SDL_GetRelativeMouseState(int *x, int *y)
+{
+	Uint32 b;
+
+	b = SDL_GetGlobalMouseState(nil, nil);
+	if(x != nil)
+		*x = npe_sdl.Δ.x;
+	if(y != nil)
+		*y = npe_sdl.Δ.y;
+	npe_sdl.Δ = ZP;
+	return b;
+}
+
+void
+SDL_SetModState(SDL_Keymod modstate)
+{
+	/* FIXME: do we care? */
+	USED(modstate);
+}
+
+/* FIXME */
+void
+SDL_GetVersion(SDL_version *v)
+{
+	/* these are arbitrary */
+	v->major = 2;
+	v->minor = 24;
+	v->patch = 1;
+}
--- /dev/null
+++ b/libnpe_sdl3/surface.c
@@ -1,0 +1,410 @@
+#include "_sdl.h"
+#include <SDL3/SDL_iostream.h>
+#include <bio.h>
+
+static void
+setformat(SDL_Surface *s, ulong chan, int bpp)
+{
+	if((s->format = calloc(1, sizeof(SDL_PixelFormat))) == nil)
+		sysfatal("setformat: %r");
+	s->format->BytesPerPixel = bpp / 8;
+	s->format->format = chan2pixel(chan);
+	if(chan == CMAP8){
+		if((s->format->palette = calloc(1, sizeof(SDL_Palette))) == nil)
+			sysfatal("setformat: %r");
+		s->format->palette->ncolors = 256;
+		if((s->format->palette->colors = calloc(1, sizeof(SDL_Color) * 256)) == nil)
+			sysfatal("setformat: %r");
+	}
+}
+
+static SDL_Surface *
+sfrommem(Memimage *i)
+{
+	int bpp;
+	SDL_Surface *s;
+
+	if((s = mallocz(sizeof *s, 1)) == nil)
+		return nil;
+	s->i = i;
+	s->w = Dx(i->r);
+	s->h = Dy(i->r);
+	bpp = i->depth;
+	s->pitch = s->w * (bpp / 8);
+	s->clip_rect.x = 0;
+	s->clip_rect.y = 0;
+	s->clip_rect.w = s->w;
+	s->clip_rect.h = s->h;
+	s->n = s->pitch * s->h;
+	if(i->chan == CMAP8)
+		s->pixels = calloc(1, s->n);
+	else
+		s->pixels = i->data->bdata;
+	if(s->pixels == nil)
+		return nil;
+	setformat(s, i->chan, bpp);
+	return s;
+}
+
+SDL_Surface *
+SDL_CreateRGBSurface(Uint32, int w, int h, int bpp, Uint32 rm, Uint32 gm, Uint32 bm, Uint32 am)
+{
+	SDL_Surface *s;
+	int n;
+	ulong chan;
+
+	rm = rm ? rm : npe_sdl.defmask.r;
+	gm = gm ? gm : npe_sdl.defmask.g;
+	bm = bm ? bm : npe_sdl.defmask.b;
+	if((chan = mask2chan(bpp, rm, gm, bm, am)) == 0){
+		werrstr("bad bpp and/or mask");
+		return nil;
+	}
+	n = w*h*bpp/8;
+	if((s = calloc(1, sizeof(*s))) == nil){
+		werrstr("SDL_CreateRGBSurface: memory");
+		return nil;
+	}
+	if(chan == CMAP8){
+		s->i = allocmemimage(Rect(0,0,w,h), screen->chan);
+		s->format->palette = calloc(1, sizeof(SDL_Palette));
+		s->format->palette->ncolors = 256;
+		s->format->palette->colors = calloc(1, sizeof(SDL_Color) * 256);
+		s->pixels = calloc(1, s->n);
+	}else{
+		s->i = allocmemimage(Rect(0,0,s->w,s->h), chan);
+		s->pixels = ((Memimage*)s->i)->data->bdata;
+	}
+	
+	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;
+	s->n = n;
+
+	return s;
+}
+
+SDL_Surface *
+SDL_CreateRGBSurfaceFrom(void *pixels, int w, int h, int bpp, int pitch, Uint32 rm, Uint32 gm, Uint32 bm, Uint32 am)
+{
+	SDL_Surface *s;
+	u8int *p;
+	int n, y;
+
+	if((s = SDL_CreateRGBSurface(0, w, h, bpp, rm, gm, bm, am)) == nil)
+		return nil;
+
+	n = w*bpp/8;
+	for(y = 0, p = pixels; y < h; y++, p += pitch)
+		memmove(s->pixels + y*n, p, n);
+
+	return s;
+}
+
+SDL_Surface *
+SDL_CreateRGBSurfaceWithFormat(Uint32 flags, int w, int h, int fbpp, Uint32 fmt)
+{
+	SDL_Surface *s;
+	int bpp;
+	ulong chan;
+	Uint32 rm, gm, bm, am;
+
+	if((chan = pixel2chan(fmt)) == 0){
+		werrstr("SDL_CreateRGBSurfaceWithFormat: FIXME format %8ux not implemented", fmt);
+		return nil;
+	}
+	chan2mask(chan, &bpp, &rm, &gm, &bm, &am);
+	if(bpp != fbpp && fbpp != 0)
+		sysfatal("FIXME SDL_CreateRGBSurfaceWithFormat passes wrong bpp for format: %d not %d", fbpp, bpp);
+	if((s = SDL_CreateRGBSurface(flags, w, h, bpp, rm, bm, gm, am)) == nil)
+		return nil;
+	return s;
+}
+
+Uint32
+SDL_MapSurfaceRGB(SDL_Surface *surface, Uint8 r, Uint8 g, Uint8 b)
+{
+	return SDL_MapRGB(surface->format, r, g, b);
+}
+
+bool
+SDL_SetSurfacePalette(SDL_Surface *s, SDL_Palette *palette)
+{
+	s->format->palette = palette;
+	return true;
+}
+
+bool
+SDL_SetSurfaceColorKey(SDL_Surface *s, bool enable, Uint32 key)
+{
+	s->keyset = enable;
+	s->key = key;
+	return true;
+}
+
+static void
+syncpalette(SDL_Surface *s)
+{
+	SDL_Color *c;
+	Uint8 *to;
+	int j;
+
+	to = ((Memimage*)s->i)->data->bdata;
+	for(j = 0; j < s->n; j++){
+		c = s->format->palette->colors + s->pixels[j];
+		*to++ = c->b;
+		*to++ = c->g;
+		*to++ = c->r;
+		*to++ = c->a;
+	}
+}
+
+static void
+synctopalette(SDL_Surface *s)
+{
+	SDL_Color c, *f;
+	Uint32 *from;
+	int j, k;
+	Memimage *i;
+	SDL_PixelFormat fmt;
+
+	i = s->i;
+	fmt.format = chan2pixel(screen->chan);
+	from = (void*)i->data->bdata;
+	for(j = 0; j < s->n; j++){
+		SDL_GetRGB(from[j], &fmt, &c.r, &c.g, &c.b);
+		for(k = 0; k < s->format->palette->ncolors; k++){
+			f = s->format->palette->colors + k;
+			if(c.r == f->r && c.g == f->g && c.b == f->b)
+				break;
+		}
+		if(k == s->format->palette->ncolors)
+			s->pixels[j] = 0; /* FIXME */
+		else
+			s->pixels[j] = k;
+	}
+}
+
+bool
+SDL_BlitSurface(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect)
+{
+	Rectangle r, r2;
+
+	r = srcrect == nil ? Rect(0, 0, src->w, src->h) : Rect(srcrect->x, srcrect->y, srcrect->x+srcrect->w, srcrect->y+srcrect->h);
+	r2 = dstrect == nil ? Rect(0, 0, dst->w, dst->h) : Rect(dstrect->x, dstrect->y, dstrect->x+dstrect->w, dstrect->y+dstrect->h);
+
+	if(src->format->format == SDL_PIXELFORMAT_INDEX8)
+		syncpalette(src);
+
+	memimagedraw(dst->i, r2, src->i, ZP, nil, ZP, S);
+
+	if(dst->format->format == SDL_PIXELFORMAT_INDEX8)
+		synctopalette(dst);
+	return true;
+}
+
+bool
+SDL_SetSurfaceRLE(SDL_Surface *, int)
+{
+	/* nothing to do here */
+	return true;
+}
+
+bool
+SDL_SetSurfaceBlendMode(SDL_Surface *, SDL_BlendMode blendMode)
+{
+	if(blendMode != SDL_BLENDMODE_NONE){
+		werrstr("SDL_SetSurfaceBlendMode: only SDL_BLENDMODE_NONE is supported");
+		return false;
+	}
+	return true;
+}
+
+bool
+SDL_LockSurface(SDL_Surface *)
+{
+	/* nothing to do here */
+	return true;
+}
+
+bool
+SDL_UnlockSurface(SDL_Surface *)
+{
+	/* nothing to do here */
+	return true;
+}
+
+// https://wiki.libsdl.org/SDL3/SDL_LowerBlit
+bool
+SDL_LowerBlit(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect)
+{
+	return SDL_BlitSurface(src, srcrect, dst, dstrect);
+}
+
+bool
+SDL_SoftStretch(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, const SDL_Rect *dstrect)
+{
+	Rectangle r, r2;
+	Memimage *rowimg;
+	int w, h;
+	int scale;
+	ulong *s, *d, *e;
+	ulong *out;
+	int i, y;
+
+	r = srcrect == nil ? Rect(0, 0, src->w, src->h) : Rect(srcrect->x, srcrect->y, srcrect->x+srcrect->w, srcrect->y+srcrect->h);
+	r2 = dstrect == nil ? Rect(0, 0, dst->w, dst->h) : Rect(dstrect->x, dstrect->y, dstrect->x+dstrect->w, dstrect->y+dstrect->h);
+
+	w = Dx(r);
+	h = Dy(r);
+
+	scale = Dx(r2)/w;
+	if(scale <= 0)
+		scale = 1;
+	else if(scale > 12)
+		scale = 12;
+
+	rowimg = allocmemimage(Rect(0, 0, scale*w, 1), ((Memimage*)src->i)->chan);
+
+	assert(dst->format->format != SDL_PIXELFORMAT_INDEX8);
+	if(src->format->format == SDL_PIXELFORMAT_INDEX8)
+		syncpalette(src);
+
+	for(y = 0; y < h; y++){
+		s = wordaddr(src->i, Pt(0, y));
+		d = (ulong*)rowimg->data->bdata;
+		e = s + w;
+		for(; s < e; s++){
+			switch(scale){
+			case 12:
+				*d++ = *s;
+			case 11:
+				*d++ = *s;
+			case 10:
+				*d++ = *s;
+			case 9:
+				*d++ = *s;
+			case 8:
+				*d++ = *s;
+			case 7:
+				*d++ = *s;
+			case 6:
+				*d++ = *s;
+			case 5:
+				*d++ = *s;
+			case 4:
+				*d++ = *s;
+			case 3:
+				*d++ = *s;
+			case 2:
+				*d++ = *s;
+			case 1:
+				*d++ = *s;
+			}
+		}
+		d = (ulong*)rowimg->data->bdata;
+		for(i = 0; i < scale; i++){
+			out = wordaddr(dst->i, Pt(0, y*scale + i));
+			memcpy(out, d, scale*w*4);
+		}
+	}
+	freememimage(rowimg);
+	return true;
+}
+
+void
+SDL_DestroySurface(SDL_Surface *surface)
+{
+	freememimage(surface->i);
+	memset(surface, 0, sizeof(surface));
+	free(surface);
+}
+
+bool
+SDL_SaveBMP(SDL_Surface *s, const char *file)
+{
+	u8int h[54];
+	Biobuf *f;
+	int sz, i;
+
+	if(s->format->format != SDL_PIXELFORMAT_RGB24){
+		werrstr("SDL_SaveBMP: not rgb24");
+		return false;
+	}
+	if((f = Bopen(file, OWRITE|OTRUNC)) == nil)
+		return false;
+	sz = sizeof(h) + s->n;
+	memset(h, 0, sizeof(h));
+	h[0] = 'B';
+	h[1] = 'M';
+	h[0x02+0] = sz;
+	h[0x02+1] = sz>>8;
+	h[0x02+2] = sz>>16;
+	h[0x02+3] = sz>>24;
+	h[0x0a] = sizeof(h);
+	h[0x0e] = sizeof(h) - 14;
+	h[0x12+0] = s->w;
+	h[0x12+1] = s->w>>8;
+	h[0x12+2] = s->w>>16;
+	h[0x12+3] = s->w>>24;
+	h[0x16+0] = s->h;
+	h[0x16+1] = s->h>>8;
+	h[0x16+2] = s->h>>16;
+	h[0x16+3] = s->h>>24;
+	h[0x1a] = 1;
+	h[0x1c] = 24;
+	Bwrite(f, h, sizeof(h));
+	memset(h, 0, 4);
+	for(i = s->h-1; i >= 0; i--){
+		Bwrite(f, s->pixels+i*s->w*3, s->w*3);
+		if(s->w & 3)
+			Bwrite(f, h, 4-(s->w&3));
+	}
+	Bterm(f);
+	return true;
+}
+
+/* fight me */
+SDL_Surface*
+SDL_LoadBMP_IO(SDL_IOStream *o, bool closeio)
+{
+	int n, pfd[2];
+	uchar buf[8192];
+	Memimage *i, *i2;
+
+	if(pipe(pfd) < 0)
+		sysfatal("SDL_LoadBMP_IO: %r");
+	switch(fork()){
+	case -1: sysfatal("SDL_LoadBMP_IO: %r");
+	case 0:
+		dup(pfd[0], 0);
+		dup(pfd[0], 1);
+		close(pfd[0]);
+		close(pfd[1]);
+		execl("/bin/bmp", "bmp", "-9tv", nil);
+		sysfatal("SDL_LoadBMP_IO: %r");
+	default:
+		close(pfd[0]);
+	}
+	while((n = SDL_ReadIO(o, buf, sizeof buf)) > 0)
+		if(write(pfd[1], buf, n) != n)
+			sysfatal("SDL_LoadBMP_IO: %r");
+	if(closeio)
+		SDL_CloseIO(o);
+	write(pfd[1], buf, 0);
+	if((i = readmemimage(pfd[1])) == nil)
+		sysfatal("SDL_LoadBMP_IO: %r");
+	close(pfd[1]);
+	/* bmp(1) doesn't do RGBA, SDL3 only does ARGB1555, RGB24 and ARGB32 */
+	if((i2 = allocmemimage(i->r, ARGB32)) == nil){
+		freememimage(i);
+		return nil;
+	}
+	memimagedraw(i2, i2->r, i, ZP, nil, ZP, S);
+	freememimage(i);
+	return sfrommem(i2);
+}
--- /dev/null
+++ b/libnpe_sdl3/texture.c
@@ -1,0 +1,240 @@
+#include "_sdl.h"
+
+static SDL_Texture backtex;
+
+bool
+SDL_SetTextureScaleMode(SDL_Texture *, SDL_ScaleMode)
+{
+	return true;
+}
+
+// https://wiki.libsdl.org/SDL3/SDL_SetRenderTarget
+bool
+SDL_SetRenderTarget(SDL_Renderer *, SDL_Texture *texture)
+{
+	if(texture == nil)
+		npe_sdl.target = &backtex;
+	else
+		npe_sdl.target = texture;
+	npe_sdl.back = npe_sdl.target->m;
+	npe_sdl.fullredraw = 1;
+	return true;
+}
+
+SDL_Texture*
+SDL_GetRenderTarget(SDL_Renderer *)
+{
+	return npe_sdl.target;
+}
+
+bool
+SDL_GetTextureSize(SDL_Texture *t, float *w, float *h)
+{
+	if(t == nil){
+		werrstr("null texture");
+		return false;
+	}
+	if(w != nil)
+		*w = Dx(t->m->r) * (t->m->depth / 8);
+	if(h != nil)
+		*h = Dy(t->m->r);
+	return true;
+}
+
+SDL_PropertiesID
+SDL_GetTextureProperties(SDL_Texture *t)
+{
+	return t;
+}
+
+Sint64
+SDL_GetNumberProperty(SDL_PropertiesID t, const char *name, Sint64 def)
+{
+	if(strcmp(SDL_PROP_TEXTURE_WIDTH_NUMBER, name) == 0)
+		return Dx(t->m->r) * (t->m->depth / 8);
+	else if(strcmp(SDL_PROP_TEXTURE_HEIGHT_NUMBER, name) == 0)
+		return Dy(t->m->r);
+	else
+		return def;
+}
+
+SDL_Texture *
+SDL_CreateTexture(SDL_Renderer *, Uint32 format, int, int w, int h)
+{
+	SDL_Texture *t;
+	int dformat;
+
+	if((dformat = pixel2chan(format)) == 0){
+		werrstr("SDL_CreateTexture: format is not supported");
+		goto err;
+	}
+	if((t = malloc(sizeof(*t))) == nil)
+		goto err;
+	if((t->m = allocmemimage(Rect(0, 0, w, h), dformat)) == nil){
+		free(t);
+		goto err;
+	}
+	t->mod = nil;
+	memfillcolor(t->m, DBlack);
+
+	return t;
+err:
+	werrstr("SDL_CreateTexture: %r");
+	return nil;
+}
+
+SDL_Texture *
+SDL_CreateTextureFromSurface(SDL_Renderer *r, SDL_Surface *s)
+{
+	SDL_Texture *t;
+	SDL_Rect re;
+
+	if((t = SDL_CreateTexture(r, s->format->format, 0, s->w, s->h)) != nil){
+		re.x = 0;
+		re.y = 0;
+		re.w = s->w;
+		re.h = s->h;
+		SDL_UpdateTexture(t, &re, s->pixels, s->pitch);
+	}
+
+	return t;
+}
+
+bool
+SDL_LockTexture(SDL_Texture *t, const 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->m->r;
+	*pitch = Dx(r)*(t->m->depth/8);
+	*pixels = t->m->data->bdata;
+	return true;
+}
+
+bool
+SDL_UnlockTexture(SDL_Texture *t)
+{
+	USED(t);
+	return true;
+}
+
+bool
+SDL_UpdateTexture(SDL_Texture *t, SDL_Rect *re, void *pixels, int pitch)
+{
+	Rectangle r;
+	u8int *pix;
+	int y, my;
+
+	r = re ? Rect(re->x, re->y, re->x+re->w, re->y+re->h) : t->m->r;
+	pix = pixels;
+	if(pitch == Dx(r)*4){
+		if(loadmemimage(t->m, r, pix, Dx(r)*Dy(r)*4) < 0){
+			werrstr("SDL_UpdateTexture: %r");
+			return false;
+		}
+	}else{
+		my = Dy(r);
+		for(y = 0; y < my; y++, pix += pitch, r.min.y += 1){
+			r.max.y = r.min.y + 1;
+			if(loadmemimage(t->m, r, pix, Dx(r)*4) < 0){
+				werrstr("SDL_UpdateTexture: %r");
+				return false;
+			}
+		}
+	}
+	return true;
+}
+
+/* FIXME: do this better */
+bool
+SDL_SetTextureAlphaMod(SDL_Texture *t, Uint8 a)
+{
+	int w, h;
+	u8int sa;
+	u32int c, *s, *e;
+	Point p;
+	Memimage *i;
+	Rectangle ir;
+
+	ir = t->m->r;
+	if(t->mod == nil){
+		if((t->mod = allocmemimage(ir, ARGB32)) == nil)	/* FIXME */
+			return false;
+	}
+	memimagedraw(t->mod, ir, t->m, ZP, nil, ZP, S);
+	i = t->mod;
+	w = Dx(ir);
+	h = Dy(ir);
+	p = ZP;
+	for(; p.y<h; p.y++){
+		s = (u32int *)byteaddr(i, p);
+		for(e=s+w; s<e; s++){
+			c = *s;
+			sa = (c >> 24 & 0xff) * (a / 255.0);
+			*s = sa << 24 | c & 0xffffff;
+		}
+	}
+	return true;
+}
+
+/* FIXME: do this better */
+bool
+SDL_SetTextureColorMod(SDL_Texture *t, Uint8 r, Uint8 g, Uint8 b)
+{
+	int w, h;
+	u8int u[4];
+	u32int c, *s, *e;
+	Point p;
+	Memimage *i;
+	Rectangle ir;
+
+	ir = t->m->r;
+	if(t->mod == nil){
+		if((t->mod = allocmemimage(ir, ARGB32)) == nil)	/* FIXME */
+			return false;
+	}
+	memimagedraw(t->mod, ir, t->m, ZP, nil, ZP, S);
+	i = t->mod;
+	w = Dx(ir);
+	h = Dy(ir);
+	p = ZP;
+	for(; p.y<h; p.y++){
+		s = (u32int *)byteaddr(i, p);
+		for(e=s+w; s<e; s++){
+			c = *s;
+			u[0] = c >> 24;
+			u[1] = (c >> 16 & 0xff) * (r / 255.0);
+			u[2] = (c >> 8 & 0xff) * (g / 255.0);
+			u[3] = (c & 0xff) * (b / 255.0);
+			*s = u[0] << 24 | u[1] << 16 | u[2] << 8 | u[3];
+		}
+	}
+	return true;
+}
+
+bool
+SDL_SetTextureBlendMode(SDL_Texture *t, SDL_BlendMode blendMode)
+{
+	if(blendMode != SDL_BLENDMODE_NONE && blendMode != SDL_BLENDMODE_BLEND){
+		werrstr("SDL_SetTextureBlendMode: unsupported blend mode %d", blendMode);
+		return false;
+	}
+	t->blend = blendMode;
+	return true;
+}
+
+void
+SDL_DestroyTexture(SDL_Texture *t)
+{
+	if(t == nil)
+		return;
+	freememimage(t->m);
+	free(t);
+}
+
+int
+npe_sdl_init_tex(void)
+{
+	npe_sdl.target = &backtex;
+	return 0;
+}
--- /dev/null
+++ b/libnpe_sdl3/threads.c
@@ -1,0 +1,99 @@
+#include "_sdl.h"
+
+struct SDL_Thread {
+	SDL_ThreadFunction f;
+	const char *name;
+	void *userdata;
+	Channel *wait;
+};
+
+static int prio[] = {
+	[SDL_THREAD_PRIORITY_LOW] = 5,
+	[SDL_THREAD_PRIORITY_NORMAL] = 10,
+	[SDL_THREAD_PRIORITY_HIGH] = 13,
+	[SDL_THREAD_PRIORITY_TIME_CRITICAL] = 19,
+};
+
+static void sdlthread(void *p);
+
+SDL_Thread *
+SDL_CreateThreadWithStackSize(SDL_ThreadFunction f, const char *name, size_t stacksz, void *data)
+{
+	SDL_Thread *t;
+
+	if((t = calloc(1, sizeof(*t))) == nil)
+		return nil;
+
+	t->f = f;
+	t->name = name;
+	t->userdata = data;
+	t->wait = chancreate(sizeof(int), 0);
+
+	if(t->wait == nil || proccreate(sdlthread, t, stacksz) < 0){
+		if(t->wait != nil)
+			chanfree(t->wait);
+		free(t);
+		t = nil;
+	}
+
+	return t;
+}
+
+SDL_Thread *
+SDL_CreateThread(SDL_ThreadFunction f, char *name, void *data)
+{
+	return SDL_CreateThreadWithStackSize(f, name, mainstacksize, data);
+}
+
+void
+SDL_DetachThread(SDL_Thread *t)
+{
+	if(t != nil)
+		chanclose(t->wait);
+}
+
+void
+SDL_WaitThread(SDL_Thread *t, int *status)
+{
+	int r;
+
+	if(t != nil){
+		recv(t->wait, &r);
+		if(status != nil)
+			*status = r;
+	}
+}
+
+void
+SDL_SetCurrentThreadPriority(int p)
+{
+	char t[32];
+	int f;
+
+	if(p < 0 || p >= nelem(prio))
+		return;
+
+	snprint(t, sizeof(t), "/proc/%d/ctl", getpid());
+	if((f = open(t, OWRITE)) >= 0){
+		fprint(f, "pri %d", prio[p]);
+		close(f);
+	}
+}
+
+static void
+sdlthread(void *p)
+{
+	SDL_Thread *t;
+	int res;
+
+	t = p;
+	if(t->name != nil)
+		threadsetname(t->name);
+
+	res = t->f(t->userdata);
+	send(t->wait, &res);
+	chanfree(t->wait);
+	free(t);
+
+	threadexits(res == 0 ? nil : "error");
+}
--- /dev/null
+++ b/libnpe_sdl3/window.c
@@ -1,0 +1,193 @@
+#include "_sdl.h"
+
+struct SDL_Window {
+	int dummy;
+};
+static SDL_Window onewin;
+
+void
+SDL_SetWindowIcon(SDL_Window *w, SDL_Surface *icon)
+{
+	USED(w); USED(icon);
+}
+
+void
+SDL_SetWindowBordered(SDL_Window *w, SDL_bool flag)
+{
+	USED(w); USED(flag);
+}
+
+Uint32
+SDL_GetWindowPixelFormat(SDL_Window *)
+{
+	if(screen != nil)
+		return chan2pixel(screen->chan);
+	return 0;
+}
+
+void
+SDL_GetWindowSize(SDL_Window *, int *w, int *h)
+{
+	/* no matter what rio decides */
+	*w = npe_sdl.physw;
+	*h = npe_sdl.physh;
+}
+
+bool
+SDL_GetWindowSizeInPixels(SDL_Window *, int *w, int *h)
+{
+	*w = Dx(screen->r);
+	*h = Dy(screen->r);
+	return true;
+}
+
+void
+SDL_GetWindowPosition(SDL_Window *, int *x, int *y)
+{
+	*x = screen->r.min.x;
+	*y = screen->r.min.y;
+}
+
+int
+SDL_GetWindowBordersSize(SDL_Window *, int *t, int *l, int *b, int *r)
+{
+	if(t != nil)
+		*t = Borderwidth;
+	if(l != nil)
+		*l = Borderwidth;
+	if(b != nil)
+		*b = Borderwidth;
+	if(r != nil)
+		*r = Borderwidth;
+	return 0;
+}
+
+void
+SDL_SetWindowSize(SDL_Window *, int w, int h)
+{
+	int f, n;
+
+	if(w == 0 || h == 0)
+		return;
+	if(npe_sdl.physw != w || npe_sdl.physh != h){
+		if((f = open("/dev/wctl", OWRITE|OCEXEC)) >= 0){
+			n = fprint(f, "resize -dx %d -dy %d", w+Borderwidth*2, h+Borderwidth*2);
+			if(n > 0){
+				while(getwindow(display, Refnone) != 1)
+					;
+				npe_sdl.physw = w;
+				npe_sdl.physh = h;
+				npe_sdl.fullredraw = 1;
+			}else{
+				fprint(2, "SDL_SetWindowSize: resize: %r\n");
+			}
+			close(f);
+		}else{
+			fprint(2, "SDL_SetWindowSize: open: %r\n");
+		}
+	}
+}
+
+void
+SDL_SetWindowMinimumSize(SDL_Window *window, int min_w, int min_h)
+{
+	SDL_SetWindowSize(window, min_w, min_h);
+}
+
+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){
+			while(getwindow(display, Refnone) != 1)
+				;
+			npe_sdl.fullredraw = 1;
+			npe_sdl.grabout = insetrect(screen->r, Dx(screen->r)/8);
+			npe_sdl.center = addpt(screen->r.min, Pt(Dx(screen->r)/2, Dy(screen->r)/2));
+		}
+	}
+}
+
+void
+SDL_RestoreWindow(SDL_Window *)
+{
+	/* nothing to do here */
+}
+
+void
+SDL_RaiseWindow(SDL_Window *)
+{
+	/* nothing to do here */
+}
+
+void
+SDL_ShowWindow(SDL_Window *)
+{
+	/* nothing to do here */
+}
+
+bool
+SDL_SyncWindow(SDL_Window *)
+{
+	return true;
+}
+
+bool
+SDL_CreateWindowAndRenderer(const char *title, int w, int h, SDL_WindowFlags f, SDL_Window **win, SDL_Renderer **rend)
+{
+	USED(f);
+	SDL_SetWindowSize(&onewin, w, h);
+	*win = &onewin;
+	*rend = SDL_CreateRenderer(&onewin, nil);
+	SDL_SetWindowTitle(&onewin, title);
+	return true;
+}
+
+void
+SDL_DestroyWindow(SDL_Window *)
+{
+}
+
+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 + (Dy(display->image->r) - h)/2;
+		SDL_SetWindowPosition(&onewin, x, y);
+	}
+
+	return &onewin;
+}
--- a/mkfile
+++ b/mkfile
@@ -5,6 +5,8 @@
 	libnpe_portmidi\
 	libnpe_pthread\
 	libnpe_sdl2\
+	libnpe_sdl3\
+	libnpe_rtmidi\
 
 default:VQ:
 	mk all
--