]> git.unchartedbackwaters.co.uk Git - francis/winuae.git/commitdiff
od-unix: add SDL runtime backend
authorStefan Reinauer <stefan.reinauer@coreboot.org>
Thu, 4 Jun 2026 14:57:54 +0000 (07:57 -0700)
committerStefan Reinauer <stefan.reinauer@coreboot.org>
Thu, 11 Jun 2026 21:08:30 +0000 (14:08 -0700)
Add the SDL-backed video, input, audio, sampler, and drive-click runtime
implementation for the Unix target.

This is the runtime path used by the standalone emulator window outside
the Qt configuration frontend.

od-unix/driveclick.cpp [new file with mode: 0644]
od-unix/graphics.cpp [new file with mode: 0644]
od-unix/input.cpp [new file with mode: 0644]
od-unix/input.h [new file with mode: 0644]
od-unix/sampler_sdl.cpp [new file with mode: 0644]
od-unix/sound.cpp [new file with mode: 0644]
od-unix/sound_unix.h [new file with mode: 0644]
od-unix/sounddep/sound.h [new file with mode: 0644]
od-unix/video.h [new file with mode: 0644]
od-unix/video_null.cpp [new file with mode: 0644]
od-unix/video_sdl.cpp [new file with mode: 0644]

diff --git a/od-unix/driveclick.cpp b/od-unix/driveclick.cpp
new file mode 100644 (file)
index 0000000..b99d5be
--- /dev/null
@@ -0,0 +1,188 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#ifdef DRIVESOUND
+
+#include "driveclick.h"
+#include "uae.h"
+#include "zfile.h"
+
+#include <string>
+#include <vector>
+
+#ifdef __APPLE__
+#include <mach-o/dyld.h>
+#endif
+
+#ifndef WINUAE_UNIX_INSTALL_DATA_DIR
+#define WINUAE_UNIX_INSTALL_DATA_DIR WINUAE_UNIX_SOURCE_DIR
+#endif
+
+#ifndef WINUAE_UNIX_INSTALL_DATADIR_RELATIVE
+#define WINUAE_UNIX_INSTALL_DATADIR_RELATIVE "share/winuae"
+#endif
+
+int driveclick_pcdrivemask;
+int driveclick_pcdrivenum;
+
+static const struct {
+       const TCHAR *name;
+       int slot;
+} builtin_samples[] = {
+       { _T("drive_click.wav"), DS_CLICK },
+       { _T("drive_spin.wav"), DS_SPIN },
+       { _T("drive_spinnd.wav"), DS_SPINND },
+       { _T("drive_startup.wav"), DS_START },
+       { _T("drive_snatch.wav"), DS_SNATCH },
+       { NULL, -1 }
+};
+
+static bool load_sample_file(const TCHAR *path, struct drvsample *sample)
+{
+       struct zfile *zf = zfile_fopen(path, _T("rb"), ZFD_NORMAL);
+       if (!zf) {
+               return false;
+       }
+       const uae_s64 size = zfile_size(zf);
+       if (size <= 0 || size > 16 * 1024 * 1024) {
+               zfile_fclose(zf);
+               return false;
+       }
+       uae_u8 *data = xmalloc(uae_u8, (size_t)size);
+       if (!data) {
+               zfile_fclose(zf);
+               return false;
+       }
+       const size_t got = zfile_fread(data, 1, (size_t)size, zf);
+       zfile_fclose(zf);
+       if (got != (size_t)size) {
+               xfree(data);
+               return false;
+       }
+       int decoded_len = (int)size;
+       sample->p = decodewav(data, &decoded_len);
+       sample->len = decoded_len;
+       xfree(data);
+       return sample->p != NULL && sample->len > 0;
+}
+
+static std::string dirname_copy(const std::string &path)
+{
+       size_t slash = path.find_last_of('/');
+       if (slash == std::string::npos) {
+               return ".";
+       }
+       if (slash == 0) {
+               return "/";
+       }
+       return path.substr(0, slash);
+}
+
+static std::string join_path(const std::string &dir, const std::string &name)
+{
+       if (dir.empty() || dir == ".") {
+               return name;
+       }
+       if (dir[dir.size() - 1] == '/') {
+               return dir + name;
+       }
+       return dir + "/" + name;
+}
+
+static std::string executable_dir()
+{
+#ifdef __APPLE__
+       char path[MAX_DPATH];
+       uint32_t size = sizeof path;
+       if (_NSGetExecutablePath(path, &size) == 0) {
+               return dirname_copy(path);
+       }
+#elif defined(__linux__)
+       char path[MAX_DPATH];
+       ssize_t len = readlink("/proc/self/exe", path, sizeof path - 1);
+       if (len > 0) {
+               path[len] = 0;
+               return dirname_copy(path);
+       }
+#endif
+       return std::string();
+}
+
+static bool load_builtin_sample(const TCHAR *name, struct drvsample *sample)
+{
+       TCHAR path[MAX_DPATH];
+       std::vector<std::string> dirs;
+       dirs.push_back(WINUAE_UNIX_SOURCE_DIR "/od-win32/resources/");
+       dirs.push_back(WINUAE_UNIX_SOURCE_DIR "/resources/");
+       dirs.push_back(WINUAE_UNIX_INSTALL_DATA_DIR "/od-win32/resources/");
+       if (start_path_data[0]) {
+               dirs.push_back(start_path_data);
+       }
+       if (start_path_data_exe[0]) {
+               dirs.push_back(start_path_data_exe);
+       }
+
+       const std::string exedir = executable_dir();
+       if (!exedir.empty()) {
+               dirs.push_back(join_path(exedir, "../Resources/od-win32/resources"));
+               dirs.push_back(join_path(exedir, "../" WINUAE_UNIX_INSTALL_DATADIR_RELATIVE "/od-win32/resources"));
+               dirs.push_back(join_path(exedir, "od-win32/resources"));
+       }
+
+       for (size_t i = 0; i < dirs.size(); i++) {
+               if (dirs[i].empty()) {
+                       continue;
+               }
+               const char *dir = dirs[i].c_str();
+               snprintf(path, sizeof path, "%s%s%s", dir, dir[strlen(dir) - 1] == '/' ? "" : "/", name);
+               if (load_sample_file(path, sample)) {
+                       return true;
+               }
+       }
+       return false;
+}
+
+int driveclick_loadresource(struct drvsample *sp, int)
+{
+       bool ok = true;
+       for (int i = 0; builtin_samples[i].name; i++) {
+               struct drvsample *sample = sp + builtin_samples[i].slot;
+               if (!load_builtin_sample(builtin_samples[i].name, sample)) {
+                       write_log(_T("Unix driveclick: missing built-in sample '%s'\n"), builtin_samples[i].name);
+                       ok = false;
+               }
+       }
+       if (ok) {
+               write_log(_T("Unix driveclick: loaded built-in A500 sample set\n"));
+       }
+       return ok ? 1 : 0;
+}
+
+void driveclick_fdrawcmd_seek(int, int)
+{
+}
+
+void driveclick_fdrawcmd_motor(int, int)
+{
+}
+
+void driveclick_fdrawcmd_vsync(void)
+{
+}
+
+void driveclick_fdrawcmd_close(int)
+{
+}
+
+int driveclick_fdrawcmd_open(int)
+{
+       return 0;
+}
+
+void driveclick_fdrawcmd_detect(void)
+{
+       driveclick_pcdrivemask = 0;
+       driveclick_pcdrivenum = 0;
+}
+
+#endif
diff --git a/od-unix/graphics.cpp b/od-unix/graphics.cpp
new file mode 100644 (file)
index 0000000..51e7316
--- /dev/null
@@ -0,0 +1,1186 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#include "custom.h"
+#ifdef AVIOUTPUT
+#include "avioutput.h"
+#endif
+#include "xwin.h"
+#include "drawing.h"
+#include "options.h"
+#include "memory.h"
+#include "picasso96.h"
+#include "uae.h"
+#include "video.h"
+#include "host.h"
+#include "devices.h"
+#include "gfxboard.h"
+
+#include <condition_variable>
+#include <mutex>
+#include <stdlib.h>
+#include <thread>
+#include <utility>
+#include <vector>
+
+extern int pause_emulation;
+extern void picasso_trigger_vblank(void);
+extern void unix_rtg_overlay_sprite(int monid, uae_u32 *dst, int width, int height, int rowpixels);
+
+uae_u32 p96_rgbx16[65536];
+bool gfx_hdr;
+int flashscreen;
+struct picasso96_state_struct picasso96_state[MAX_AMIGAMONITORS];
+struct picasso_vidbuf_description picasso_vidinfo[MAX_AMIGAMONITORS];
+
+static bool unix_graphics_initialized;
+static bool unix_video_debug;
+static bool unix_rtg_render_has_output[MAX_AMIGAMONITORS];
+static void unix_rtg_stop_render_thread(void);
+
+enum {
+    UNIX_PICASSO_STATE_SETDISPLAY = 1,
+    UNIX_PICASSO_STATE_SETPANNING = 2,
+    UNIX_PICASSO_STATE_SETGC = 4,
+    UNIX_PICASSO_STATE_SETDAC = 8,
+    UNIX_PICASSO_STATE_SETSWITCH = 16,
+    UNIX_PICASSO_STATE_SPRITE = 32
+};
+
+static int unix_picasso_bytes_per_pixel(RGBFTYPE rgbfmt)
+{
+    switch (rgbfmt) {
+    case RGBFB_CLUT:
+    case RGBFB_Y4U1V1:
+        return 1;
+    case RGBFB_R5G6B5:
+    case RGBFB_R5G5B5:
+    case RGBFB_R5G6B5PC:
+    case RGBFB_R5G5B5PC:
+    case RGBFB_B5G6R5PC:
+    case RGBFB_B5G5R5PC:
+    case RGBFB_Y4U2V2:
+        return 2;
+    case RGBFB_R8G8B8:
+    case RGBFB_B8G8R8:
+        return 3;
+    case RGBFB_A8R8G8B8:
+    case RGBFB_A8B8G8R8:
+    case RGBFB_R8G8B8A8:
+    case RGBFB_B8G8R8A8:
+        return 4;
+    default:
+        return 0;
+    }
+}
+
+static uae_u16 unix_picasso_load_host_u16(const uae_u8 *src)
+{
+    uae_u16 value;
+    memcpy(&value, src, sizeof value);
+    return value;
+}
+
+static void unix_init_colors(void)
+{
+    alloc_colors64k(0, 8, 8, 8, 16, 8, 0, 8, 24, 1, 0);
+    notice_new_xcolors();
+    alloc_colors_picasso(8, 8, 8, 16, 8, 0, RGBFB_R8G8B8A8, p96_rgbx16);
+}
+
+static void unix_alloc_buffer(int monid, struct vidbuffer *buffer, int width, int height)
+{
+    if (buffer->realbufmem && buffer->width_allocated >= width && buffer->height_allocated >= height &&
+        buffer->pixbytes == 4) {
+        return;
+    }
+
+    freevidbuffer(monid, buffer);
+    allocvidbuffer(monid, buffer, width, height, 32);
+    buffer->initialized = true;
+}
+
+static void unix_init_display_buffers(void)
+{
+    struct vidbuf_description *vidinfo = &adisplays[0].gfxvidinfo;
+
+    vidinfo->gfx_resolution_reserved = RES_MAX;
+    vidinfo->gfx_vresolution_reserved = VRES_MAX;
+    vidinfo->xchange = 1;
+    vidinfo->ychange = 1;
+
+    unix_alloc_buffer(0, &vidinfo->drawbuffer, 1920, 1280);
+    unix_alloc_buffer(0, &vidinfo->tempbuffer, 2048, 2048);
+
+    vidinfo->drawbuffer.monitor_id = 0;
+    vidinfo->tempbuffer.monitor_id = 0;
+    vidinfo->outbuffer = &vidinfo->drawbuffer;
+    vidinfo->inbuffer = &vidinfo->drawbuffer;
+}
+
+static int unix_apmode_index(int monid)
+{
+    if (monid >= 0 && monid < MAX_AMIGADISPLAYS &&
+        (adisplays[monid].picasso_on || picasso_vidinfo[monid].picasso_active)) {
+        return APMODE_RTG;
+    }
+    return APMODE_NATIVE;
+}
+
+static int unix_fullscreen_state(int fullscreen)
+{
+    if (fullscreen == GFX_FULLSCREEN) {
+        return 1;
+    }
+    if (fullscreen == GFX_FULLWINDOW) {
+        return -1;
+    }
+    return 0;
+}
+
+static enum unix_video_window_mode unix_video_mode_from_prefs(int fullscreen)
+{
+    if (fullscreen == GFX_FULLSCREEN) {
+        return UNIX_VIDEO_FULLSCREEN;
+    }
+    if (fullscreen == GFX_FULLWINDOW) {
+        return UNIX_VIDEO_FULLWINDOW;
+    }
+    return UNIX_VIDEO_WINDOWED;
+}
+
+static void unix_apply_video_mode_from_prefs(struct uae_prefs *prefs, int monid)
+{
+    if (monid < 0 || monid >= MAX_AMIGADISPLAYS) {
+        return;
+    }
+
+    fixup_prefs_dimensions(prefs);
+    int idx = unix_apmode_index(monid);
+    struct apmode *ap = &prefs->gfx_apmode[idx];
+    struct wh *size = ap->gfx_fullscreen == GFX_WINDOW
+        ? &prefs->gfx_monitor[monid].gfx_size_win
+        : &prefs->gfx_monitor[monid].gfx_size_fs;
+
+    prefs->gfx_monitor[monid].gfx_size = *size;
+    int width = size->special == WH_NATIVE ? 0 : size->width;
+    int height = size->special == WH_NATIVE ? 0 : size->height;
+    unix_video_set_window_mode(unix_video_mode_from_prefs(ap->gfx_fullscreen),
+        ap->gfx_display, width, height, ap->gfx_refreshrate);
+}
+
+static bool unix_runtime_graphics_prefs_changed(int monid)
+{
+    int idx = unix_apmode_index(monid);
+
+    return currprefs.gfx_apmode[APMODE_NATIVE].gfx_fullscreen != changed_prefs.gfx_apmode[APMODE_NATIVE].gfx_fullscreen ||
+        currprefs.gfx_apmode[APMODE_RTG].gfx_fullscreen != changed_prefs.gfx_apmode[APMODE_RTG].gfx_fullscreen ||
+        currprefs.gfx_apmode[APMODE_NATIVE].gfx_display != changed_prefs.gfx_apmode[APMODE_NATIVE].gfx_display ||
+        currprefs.gfx_apmode[APMODE_RTG].gfx_display != changed_prefs.gfx_apmode[APMODE_RTG].gfx_display ||
+        currprefs.gfx_apmode[APMODE_NATIVE].gfx_backbuffers != changed_prefs.gfx_apmode[APMODE_NATIVE].gfx_backbuffers ||
+        currprefs.gfx_apmode[APMODE_RTG].gfx_backbuffers != changed_prefs.gfx_apmode[APMODE_RTG].gfx_backbuffers ||
+        currprefs.gfx_apmode[idx].gfx_refreshrate != changed_prefs.gfx_apmode[idx].gfx_refreshrate ||
+        currprefs.gfx_monitor[monid].gfx_size_fs.width != changed_prefs.gfx_monitor[monid].gfx_size_fs.width ||
+        currprefs.gfx_monitor[monid].gfx_size_fs.height != changed_prefs.gfx_monitor[monid].gfx_size_fs.height ||
+        currprefs.gfx_monitor[monid].gfx_size_fs.special != changed_prefs.gfx_monitor[monid].gfx_size_fs.special ||
+        currprefs.gfx_monitor[monid].gfx_size_win.width != changed_prefs.gfx_monitor[monid].gfx_size_win.width ||
+        currprefs.gfx_monitor[monid].gfx_size_win.height != changed_prefs.gfx_monitor[monid].gfx_size_win.height ||
+        currprefs.gfx_monitor[monid].gfx_size_win.special != changed_prefs.gfx_monitor[monid].gfx_size_win.special;
+}
+
+static void unix_copy_runtime_graphics_prefs(int monid)
+{
+    currprefs.gfx_apmode[APMODE_NATIVE].gfx_fullscreen = changed_prefs.gfx_apmode[APMODE_NATIVE].gfx_fullscreen;
+    currprefs.gfx_apmode[APMODE_RTG].gfx_fullscreen = changed_prefs.gfx_apmode[APMODE_RTG].gfx_fullscreen;
+    currprefs.gfx_apmode[APMODE_NATIVE].gfx_display = changed_prefs.gfx_apmode[APMODE_NATIVE].gfx_display;
+    currprefs.gfx_apmode[APMODE_RTG].gfx_display = changed_prefs.gfx_apmode[APMODE_RTG].gfx_display;
+    currprefs.gfx_apmode[APMODE_NATIVE].gfx_backbuffers = changed_prefs.gfx_apmode[APMODE_NATIVE].gfx_backbuffers;
+    currprefs.gfx_apmode[APMODE_RTG].gfx_backbuffers = changed_prefs.gfx_apmode[APMODE_RTG].gfx_backbuffers;
+    currprefs.gfx_apmode[APMODE_NATIVE].gfx_refreshrate = changed_prefs.gfx_apmode[APMODE_NATIVE].gfx_refreshrate;
+    currprefs.gfx_apmode[APMODE_RTG].gfx_refreshrate = changed_prefs.gfx_apmode[APMODE_RTG].gfx_refreshrate;
+    currprefs.gfx_monitor[monid].gfx_size_fs = changed_prefs.gfx_monitor[monid].gfx_size_fs;
+    currprefs.gfx_monitor[monid].gfx_size_win = changed_prefs.gfx_monitor[monid].gfx_size_win;
+}
+
+int graphics_setup(void)
+{
+    unix_video_debug = getenv("WINUAE_UNIX_VIDEO_DEBUG") != NULL;
+    unix_init_colors();
+    if (unix_video_debug) {
+        write_log(_T("Unix video colors: direct_rgb=%d black=%08x white=%08x r255=%08x g255=%08x b255=%08x\n"),
+            direct_rgb ? 1 : 0, xcolors[0], xcolors[0xfff], xredcolors[255], xgreencolors[255], xbluecolors[255]);
+    }
+    InitPicasso96(0);
+    return 1;
+}
+
+int graphics_init(bool)
+{
+    unix_init_display_buffers();
+    struct vidbuffer *vb = &adisplays[0].gfxvidinfo.drawbuffer;
+    if (!unix_video_init(vb->outwidth, vb->outheight, vb->pixbytes)) {
+        write_log(_T("Unix video: no window presenter available, continuing headless\n"));
+    }
+    unix_apply_video_mode_from_prefs(&currprefs, 0);
+    unix_graphics_initialized = true;
+    return 1;
+}
+
+void graphics_leave(void)
+{
+#ifdef AVIOUTPUT
+    AVIOutput_Release();
+#endif
+    unix_rtg_stop_render_thread();
+    struct vidbuf_description *vidinfo = &adisplays[0].gfxvidinfo;
+    freevidbuffer(0, &vidinfo->drawbuffer);
+    freevidbuffer(0, &vidinfo->tempbuffer);
+    unix_video_shutdown();
+    unix_graphics_initialized = false;
+}
+
+void graphics_reset(bool) {}
+
+bool handle_events(void)
+{
+    handle_msgpump(false);
+    return pause_emulation != 0;
+}
+
+int handle_msgpump(bool)
+{
+    unix_host_check_quit();
+    bool quit_requested = false;
+    int got = unix_video_poll(&quit_requested);
+    if (quit_requested) {
+        uae_quit();
+    }
+    unix_host_check_quit();
+    return got;
+}
+
+int check_prefs_changed_gfx(void)
+{
+    int flags = config_changed_flags;
+    bool changed = unix_runtime_graphics_prefs_changed(0);
+
+    if (!unix_graphics_initialized || (!changed && !flags)) {
+        return 0;
+    }
+
+    config_changed_flags = 0;
+    if (changed) {
+        unix_copy_runtime_graphics_prefs(0);
+        unix_apply_video_mode_from_prefs(&currprefs, 0);
+    }
+    return 1;
+}
+
+int isfullscreen(void)
+{
+    int idx = unix_apmode_index(0);
+    return unix_fullscreen_state(currprefs.gfx_apmode[idx].gfx_fullscreen);
+}
+
+void toggle_fullscreen(int monid, int mode)
+{
+    if (monid < 0 || monid >= MAX_AMIGADISPLAYS) {
+        return;
+    }
+
+    int idx = unix_apmode_index(monid);
+    int v = changed_prefs.gfx_apmode[idx].gfx_fullscreen;
+    static int wasfs[2];
+
+    if (mode < 0) {
+        if (v == GFX_FULLWINDOW) {
+            wasfs[idx] = -1;
+            v = GFX_WINDOW;
+        } else if (v == GFX_WINDOW) {
+            v = wasfs[idx] >= 0 ? GFX_FULLSCREEN : GFX_FULLWINDOW;
+        } else if (v == GFX_FULLSCREEN) {
+            wasfs[idx] = 1;
+            v = GFX_WINDOW;
+        }
+    } else if (mode == 0) {
+        v = v == GFX_FULLSCREEN ? GFX_WINDOW : GFX_FULLSCREEN;
+    } else if (mode == 1) {
+        v = v == GFX_FULLSCREEN ? GFX_FULLWINDOW : GFX_FULLSCREEN;
+    } else if (mode == 2) {
+        v = v == GFX_FULLWINDOW ? GFX_WINDOW : GFX_FULLWINDOW;
+    } else if (mode == 10) {
+        v = GFX_WINDOW;
+    }
+
+    changed_prefs.gfx_apmode[idx].gfx_fullscreen = v;
+    devices_unsafeperiod();
+    set_config_changed();
+}
+bool toggle_rtg(int monid, int)
+{
+    return monid >= 0 && monid < MAX_AMIGAMONITORS && currprefs.rtgboards[0].rtgmem_size > 0;
+}
+void close_rtg(int, bool) {}
+void toggle_mousegrab(void) { unix_video_toggle_mouse_grab(); }
+void setmouseactivexy(int, int, int, int) {}
+
+void desktop_coords(int, int *dw, int *dh, int *x, int *y, int *w, int *h)
+{
+    unix_video_get_desktop(dw, dh, x, y, w, h);
+}
+
+bool vsync_switchmode(int, int) { return false; }
+void vsync_clear(void) {}
+int vsync_isdone(frame_time_t*) { return 1; }
+void doflashscreen(void) {}
+void updatedisplayarea(int) {}
+void flush_line(struct vidbuffer*, int) {}
+void flush_block(struct vidbuffer*, int, int) {}
+void flush_screen(struct vidbuffer*, int, int) {}
+void flush_clear_screen(struct vidbuffer*) {}
+bool render_screen(int, int, bool)
+{
+    set_custom_limits(-1, -1, -1, -1, false);
+    return true;
+}
+
+static void unix_log_video_frame(const struct vidbuffer *vb)
+{
+    static int frames;
+
+    if (!unix_video_debug || !vb || !vb->bufmem || vb->pixbytes != 4) {
+        return;
+    }
+
+    frames++;
+    if (frames > 1 && (frames % 50) != 0) {
+        return;
+    }
+
+    int nonblack = 0;
+    int firstx = -1;
+    int firsty = -1;
+    int lastx = -1;
+    int lasty = -1;
+    uae_u32 first = 0;
+    uae_u32 last = 0;
+
+    int scan_width = vb->width_allocated > 0 ? vb->width_allocated : vb->outwidth;
+    int scan_height = vb->height_allocated > 0 ? vb->height_allocated : vb->outheight;
+    for (int y = 0; y < scan_height; y++) {
+        const uae_u32 *row = (const uae_u32 *)(vb->bufmem + y * vb->rowbytes);
+        for (int x = 0; x < scan_width; x++) {
+            uae_u32 pixel = row[x];
+            if ((pixel & 0x00ffffff) != 0) {
+                if (!nonblack) {
+                    firstx = x;
+                    firsty = y;
+                    first = pixel;
+                }
+                lastx = x;
+                lasty = y;
+                last = pixel;
+                nonblack++;
+            }
+        }
+    }
+
+    write_log(_T("Unix video frame %d: out=%dx%d alloc=%dx%d pitch=%d xoff=%d yoff=%d nonblack=%d first=%d,%d:%08x last=%d,%d:%08x\n"),
+        frames, vb->outwidth, vb->outheight, vb->width_allocated, vb->height_allocated, vb->rowbytes, vb->xoffset, vb->yoffset,
+        nonblack, firstx, firsty, first, lastx, lasty, last);
+}
+
+void show_screen(int monid, int)
+{
+    if (!unix_graphics_initialized || monid < 0 || monid >= MAX_AMIGADISPLAYS) {
+        return;
+    }
+
+    struct vidbuf_description *vidinfo = &adisplays[monid].gfxvidinfo;
+    struct vidbuffer *vb = vidinfo->inbuffer ? vidinfo->inbuffer : &vidinfo->drawbuffer;
+    if (!vb->bufmem || vb->outwidth <= 0 || vb->outheight <= 0) {
+        return;
+    }
+
+    struct unix_video_frame frame;
+    frame.pixels = vb->bufmem;
+    frame.width = vb->outwidth;
+    frame.height = vb->outheight;
+    frame.rowbytes = vb->rowbytes;
+    frame.pixbytes = vb->pixbytes;
+    frame.filter_index = adisplays[monid].gf_index;
+    frame.monitor_id = monid;
+    frame.backbuffers = currprefs.gfx_apmode[unix_apmode_index(monid)].gfx_backbuffers;
+    unix_log_video_frame(vb);
+    unix_video_present(&frame);
+}
+
+bool show_screen_maybe(int monid, bool)
+{
+    show_screen(monid, 0);
+    return true;
+}
+
+int lockscr(struct vidbuffer *vb, bool, bool)
+{
+    if (!vb || !vb->bufmem) {
+        return 0;
+    }
+    vb->locked = true;
+    return 1;
+}
+
+void unlockscr(struct vidbuffer *vb, int, int)
+{
+    if (vb) {
+        vb->locked = false;
+    }
+}
+
+bool target_graphics_buffer_update(int, bool) { return true; }
+float target_adjust_vblank_hz(int, float hz) { return hz; }
+int target_get_display_scanline(int) { return -1; }
+void target_spin(int)
+{
+    static int spin_counter;
+    if ((spin_counter++ & 31) == 0 || unix_host_quit_requested()) {
+        handle_msgpump(false);
+    }
+}
+
+void getgfxoffset(int, float *dxp, float *dyp, float *mxp, float *myp)
+{
+    if (dxp) *dxp = 0;
+    if (dyp) *dyp = 0;
+    if (mxp) *mxp = 1;
+    if (myp) *myp = 1;
+}
+
+float target_getcurrentvblankrate(int monid)
+{
+    int idx = unix_apmode_index(monid);
+    float rate = unix_video_get_display_refresh_rate(currprefs.gfx_apmode[idx].gfx_display);
+    return rate > 0.0f ? rate : 60.0f;
+}
+int debuggable(void) { return 0; }
+
+void refreshtitle(void)
+{
+    unix_video_set_title(_T(WINUAE_UNIX_WINDOW_TITLE));
+}
+
+void InitPicasso96(int monid)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS) {
+        return;
+    }
+
+    memset(&picasso96_state[monid], 0, sizeof picasso96_state[monid]);
+    memset(&picasso_vidinfo[monid], 0, sizeof picasso_vidinfo[monid]);
+    unix_rtg_render_has_output[monid] = false;
+    picasso_vidinfo[monid].rgbformat = RGBFB_B8G8R8A8;
+    picasso_vidinfo[monid].selected_rgbformat = RGBFB_B8G8R8A8;
+    picasso_vidinfo[monid].host_mode = RGBFB_B8G8R8A8;
+    picasso_vidinfo[monid].pixbytes = 4;
+    for (int i = 0; i < 256; i++) {
+        picasso96_state[monid].CLUT[i].Red = i;
+        picasso96_state[monid].CLUT[i].Green = i;
+        picasso96_state[monid].CLUT[i].Blue = i;
+        picasso_vidinfo[monid].clut[i] = 0xff000000 | (i << 16) | (i << 8) | i;
+    }
+}
+
+static bool unix_picasso_ensure_buffer(int monid)
+{
+    struct picasso96_state_struct *state = &picasso96_state[monid];
+    struct picasso_vidbuf_description *pvidinfo = &picasso_vidinfo[monid];
+    struct vidbuf_description *vidinfo = &adisplays[monid].gfxvidinfo;
+    int width = state->Width > 0 ? state->Width : 640;
+    int height = state->Height > 0 ? state->Height : 480;
+
+    unix_alloc_buffer(monid, &vidinfo->drawbuffer, width, height);
+    vidinfo->drawbuffer.outwidth = width;
+    vidinfo->drawbuffer.outheight = height;
+    vidinfo->drawbuffer.pixbytes = 4;
+    vidinfo->drawbuffer.monitor_id = monid;
+    vidinfo->inbuffer = &vidinfo->drawbuffer;
+    vidinfo->outbuffer = &vidinfo->drawbuffer;
+
+    pvidinfo->width = width;
+    pvidinfo->height = height;
+    pvidinfo->depth = state->GC_Depth ? (state->GC_Depth + 7) / 8 : 0;
+    pvidinfo->pixbytes = 4;
+    pvidinfo->rowbytes = vidinfo->drawbuffer.rowbytes;
+    pvidinfo->maxwidth = vidinfo->drawbuffer.width_allocated;
+    pvidinfo->maxheight = vidinfo->drawbuffer.height_allocated;
+    pvidinfo->rgbformat = state->RGBFormat;
+    pvidinfo->selected_rgbformat = state->RGBFormat;
+    pvidinfo->host_mode = RGBFB_B8G8R8A8;
+
+    return vidinfo->drawbuffer.bufmem != NULL;
+}
+
+static uae_u32 unix_picasso_convert_pixel(const uae_u8 *src, RGBFTYPE fmt,
+    const uae_u32 *clut, const uae_u32 *rgbx16 = p96_rgbx16)
+{
+    switch (fmt) {
+    case RGBFB_CLUT:
+        return clut[src[0]];
+    case RGBFB_R8G8B8:
+        return 0xff000000 | ((uae_u32)src[0] << 16) | ((uae_u32)src[1] << 8) | src[2];
+    case RGBFB_B8G8R8:
+        return 0xff000000 | ((uae_u32)src[2] << 16) | ((uae_u32)src[1] << 8) | src[0];
+    case RGBFB_R8G8B8A8:
+        return ((uae_u32)src[3] << 24) | ((uae_u32)src[0] << 16) | ((uae_u32)src[1] << 8) | src[2];
+    case RGBFB_B8G8R8A8:
+        return ((uae_u32)src[3] << 24) | ((uae_u32)src[2] << 16) | ((uae_u32)src[1] << 8) | src[0];
+    case RGBFB_A8R8G8B8:
+        return ((uae_u32)src[0] << 24) | ((uae_u32)src[1] << 16) | ((uae_u32)src[2] << 8) | src[3];
+    case RGBFB_A8B8G8R8:
+        return ((uae_u32)src[0] << 24) | ((uae_u32)src[3] << 16) | ((uae_u32)src[2] << 8) | src[1];
+    case RGBFB_R5G6B5:
+    case RGBFB_R5G5B5:
+    case RGBFB_R5G6B5PC:
+    case RGBFB_R5G5B5PC:
+    case RGBFB_B5G6R5PC:
+    case RGBFB_B5G5R5PC:
+        return rgbx16[unix_picasso_load_host_u16(src)];
+    default:
+        return 0xff000000;
+    }
+}
+
+enum {
+    RGBFB_A8R8G8B8_32 = 1,
+    RGBFB_A8B8G8R8_32,
+    RGBFB_R8G8B8A8_32,
+    RGBFB_B8G8R8A8_32,
+    RGBFB_R8G8B8_32,
+    RGBFB_B8G8R8_32,
+    RGBFB_R5G6B5PC_32,
+    RGBFB_R5G5B5PC_32,
+    RGBFB_R5G6B5_32,
+    RGBFB_R5G5B5_32,
+    RGBFB_B5G6R5PC_32,
+    RGBFB_B5G5R5PC_32,
+    RGBFB_CLUT_RGBFB_32,
+    RGBFB_Y4U2V2_32,
+    RGBFB_Y4U1V1_32,
+};
+
+static void unix_picasso_render_pixels(const uae_u8 *srcbase, int width, int height,
+    int srcrowbytes, int srcpixbytes, RGBFTYPE rgbfmt, const uae_u32 *clut,
+    uae_u8 *dstbase, int dstrowbytes, const uae_u32 *rgbx16 = p96_rgbx16);
+
+int getconvert(int rgbformat)
+{
+    switch (rgbformat) {
+    case RGBFB_CLUT:
+        return RGBFB_CLUT_RGBFB_32;
+    case RGBFB_B5G6R5PC:
+        return RGBFB_B5G6R5PC_32;
+    case RGBFB_R5G6B5PC:
+        return RGBFB_R5G6B5PC_32;
+    case RGBFB_R5G5B5PC:
+        return RGBFB_R5G5B5PC_32;
+    case RGBFB_R5G6B5:
+        return RGBFB_R5G6B5_32;
+    case RGBFB_R5G5B5:
+        return RGBFB_R5G5B5_32;
+    case RGBFB_B5G5R5PC:
+        return RGBFB_B5G5R5PC_32;
+    case RGBFB_A8R8G8B8:
+        return RGBFB_A8R8G8B8_32;
+    case RGBFB_R8G8B8:
+        return RGBFB_R8G8B8_32;
+    case RGBFB_B8G8R8:
+        return RGBFB_B8G8R8_32;
+    case RGBFB_A8B8G8R8:
+        return RGBFB_A8B8G8R8_32;
+    case RGBFB_B8G8R8A8:
+        return RGBFB_B8G8R8A8_32;
+    case RGBFB_R8G8B8A8:
+        return RGBFB_R8G8B8A8_32;
+    case RGBFB_Y4U2V2:
+        return RGBFB_Y4U2V2_32;
+    case RGBFB_Y4U1V1:
+        return RGBFB_Y4U1V1_32;
+    default:
+        return 0;
+    }
+}
+
+static uae_u16 unix_yuv_to_rgb16(uae_u8 yx, uae_u8 ux, uae_u8 vx)
+{
+    int y = yx - 16;
+    int u = ux - 128;
+    int v = vx - 128;
+    int r = (298 * y + 409 * v + 128) >> (8 + 3);
+    int g = (298 * y - 100 * u - 208 * v + 128) >> (8 + 3);
+    int b = (298 * y + 516 * u + 128) >> (8 + 3);
+    if (r < 0) {
+        r = 0;
+    } else if (r > 31) {
+        r = 31;
+    }
+    if (g < 0) {
+        g = 0;
+    } else if (g > 31) {
+        g = 31;
+    }
+    if (b < 0) {
+        b = 0;
+    } else if (b > 31) {
+        b = 31;
+    }
+    return (r << 10) | (g << 5) | b;
+}
+
+static bool unix_picasso_colorkey_matches(const uae_u8 *screen, int dx, int screenpixbytes,
+    uae_u32 colorkey)
+{
+    if (!screen || dx < 0 || screenpixbytes <= 0) {
+        return false;
+    }
+    switch (screenpixbytes) {
+    case 1:
+        return screen[dx] == (uae_u8)colorkey;
+    case 2:
+        return do_get_mem_word((uae_u16 *)(screen + dx * 2)) == (uae_u16)colorkey;
+    case 3:
+        return screen[dx * 3 + 0] == (uae_u8)(colorkey >> 16) &&
+            screen[dx * 3 + 1] == (uae_u8)(colorkey >> 8) &&
+            screen[dx * 3 + 2] == (uae_u8)colorkey;
+    case 4:
+        return do_get_mem_long((uae_u32 *)(screen + dx * 4)) == colorkey;
+    default:
+        return false;
+    }
+}
+
+static void unix_picasso_store_scaled_pixel(uae_u8 *dst, int dx, int dstpixbytes, uae_u32 color)
+{
+    switch (dstpixbytes) {
+    case 1:
+        dst[dx] = (uae_u8)color;
+        break;
+    case 2:
+        do_put_mem_word((uae_u16 *)(dst + dx * 2), (uae_u16)color);
+        break;
+    case 3:
+        dst[dx * 3 + 0] = (uae_u8)(color >> 16);
+        dst[dx * 3 + 1] = (uae_u8)(color >> 8);
+        dst[dx * 3 + 2] = (uae_u8)color;
+        break;
+    case 4:
+        do_put_mem_long((uae_u32 *)(dst + dx * 4), color);
+        break;
+    }
+}
+
+static uae_u32 unix_picasso_convert_scaled_pixel(const uae_u8 *src, int x, int sxfrac,
+    int convert_mode, const uae_u32 *rgbx16, const uae_u32 *clut, bool yuv_swap)
+{
+    switch (convert_mode) {
+    case RGBFB_R8G8B8_32:
+        return 0xff000000 | ((uae_u32)src[x * 3 + 0] << 16) |
+            ((uae_u32)src[x * 3 + 1] << 8) | src[x * 3 + 2];
+    case RGBFB_B8G8R8_32:
+        return 0xff000000 | ((uae_u32)src[x * 3 + 2] << 16) |
+            ((uae_u32)src[x * 3 + 1] << 8) | src[x * 3 + 0];
+    case RGBFB_R8G8B8A8_32:
+        return 0xff000000 | ((uae_u32)src[x * 4 + 0] << 16) |
+            ((uae_u32)src[x * 4 + 1] << 8) | src[x * 4 + 2];
+    case RGBFB_A8R8G8B8_32:
+        return ((uae_u32)src[x * 4 + 0] << 24) | ((uae_u32)src[x * 4 + 1] << 16) |
+            ((uae_u32)src[x * 4 + 2] << 8) | src[x * 4 + 3];
+    case RGBFB_A8B8G8R8_32:
+        return ((uae_u32)src[x * 4 + 0] << 24) | ((uae_u32)src[x * 4 + 3] << 16) |
+            ((uae_u32)src[x * 4 + 2] << 8) | src[x * 4 + 1];
+    case RGBFB_B8G8R8A8_32:
+        return ((uae_u32)src[x * 4 + 3] << 24) | ((uae_u32)src[x * 4 + 2] << 16) |
+            ((uae_u32)src[x * 4 + 1] << 8) | src[x * 4 + 0];
+    case RGBFB_R5G6B5PC_32:
+    case RGBFB_R5G5B5PC_32:
+    case RGBFB_R5G6B5_32:
+    case RGBFB_R5G5B5_32:
+    case RGBFB_B5G6R5PC_32:
+    case RGBFB_B5G5R5PC_32:
+        return rgbx16 ? rgbx16[unix_picasso_load_host_u16(src + x * 2)] : 0xff000000;
+    case RGBFB_CLUT_RGBFB_32:
+        return clut ? clut[src[x]] : (0xff000000 | src[x] | ((uae_u32)src[x] << 8) |
+            ((uae_u32)src[x] << 16));
+    case RGBFB_Y4U2V2_32:
+    {
+        uae_u32 val = do_get_mem_long((uae_u32 *)(src + x * 4));
+        if (yuv_swap) {
+            val = ((val & 0xff00ff00) >> 8) | ((val & 0x00ff00ff) << 8);
+        }
+        uae_u8 y = (sxfrac & 0x80) ? (uae_u8)(val >> 24) : (uae_u8)(val >> 8);
+        uae_u8 u = (uae_u8)val;
+        uae_u8 v = (uae_u8)(val >> 16);
+        uae_u16 rgb = unix_yuv_to_rgb16(y, u, v);
+        return rgbx16 ? rgbx16[rgb] : 0xff000000;
+    }
+    case RGBFB_Y4U1V1_32:
+    {
+        uae_u32 val = do_get_mem_long((uae_u32 *)(src + x * 4));
+        if (yuv_swap) {
+            val = ((val & 0xff00ff00) >> 8) | ((val & 0x00ff00ff) << 8);
+        }
+        uae_u8 y[4] = {
+            (uae_u8)(val >> 24), (uae_u8)(val >> 18),
+            (uae_u8)(val >> 12), (uae_u8)(val >> 6)
+        };
+        uae_u8 u = (uae_u8)((val >> 3) & 7);
+        uae_u8 v = (uae_u8)(val & 7);
+        uae_u16 rgb = unix_yuv_to_rgb16(y[(sxfrac >> 6) & 3], u << 5, v << 5);
+        return rgbx16 ? rgbx16[rgb] : 0xff000000;
+    }
+    default:
+        return 0xff000000;
+    }
+}
+
+void copyrow_scale(int, uae_u8 *src, uae_u8 *src_screen, uae_u8 *dst,
+    int sx, int sy, int sxadd, int width, int srcbytesperrow, int,
+    int screenbytesperrow, int screenpixbytes,
+    int dx, int dy, int dstwidth, int dstheight, int dstbytesperrow, int dstpixbytes,
+    bool ck, uae_u32 colorkey, int convert_mode, uae_u32 *p96_rgbx16p,
+    uae_u32 *clut, bool yuv_swap)
+{
+    if (!src || !dst || sxadd <= 0 || width <= 0 || dx < 0 || dy < 0 ||
+        dx >= dstwidth || dy >= dstheight || dstpixbytes <= 0) {
+        return;
+    }
+
+    uae_u8 *srcrow = src + sy * srcbytesperrow;
+    uae_u8 *dstrow = dst + dy * dstbytesperrow;
+    uae_u8 *screenrow = src_screen ? src_screen + dy * screenbytesperrow : NULL;
+    int endx = (sx + width) << 8;
+
+    if (convert_mode == RGBFB_Y4U2V2_32) {
+        endx /= 2;
+        sxadd /= 2;
+    } else if (convert_mode == RGBFB_Y4U1V1_32) {
+        endx /= 4;
+        sxadd /= 4;
+    }
+
+    while (sx < endx && dx < dstwidth) {
+        int x = sx >> 8;
+        if (!ck || unix_picasso_colorkey_matches(screenrow, dx, screenpixbytes, colorkey)) {
+            uae_u32 color = unix_picasso_convert_scaled_pixel(srcrow, x, sx & 255,
+                convert_mode, p96_rgbx16p, clut, yuv_swap);
+            unix_picasso_store_scaled_pixel(dstrow, dx, dstpixbytes, color);
+        }
+        sx += sxadd;
+        dx++;
+    }
+}
+
+uae_u8 *uaegfx_getrtgbuffer(int monid, int *widthp, int *heightp, int *pitch, int *depth,
+    uae_u8 *palette)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS) {
+        monid = 0;
+    }
+    int bankid = monid < MAX_RTG_BOARDS && gfxmem_banks[monid] ? monid : 0;
+    addrbank *bank = gfxmem_banks[bankid];
+    struct picasso96_state_struct *state = &picasso96_state[monid];
+    struct picasso_vidbuf_description *pvidinfo = &picasso_vidinfo[monid];
+    int width = state->VirtualWidth ? state->VirtualWidth : state->Width;
+    int height = state->VirtualHeight ? state->VirtualHeight : state->Height;
+    int srcpixbytes = unix_picasso_bytes_per_pixel((RGBFTYPE)state->RGBFormat);
+    int dstpixbytes = state->BytesPerPixel == 1 && palette ? 1 : 4;
+
+    if (!bank || !bank->baseaddr || width <= 0 || height <= 0 || srcpixbytes <= 0 ||
+        state->BytesPerRow <= 0 || dstpixbytes <= 0 || (uae_u32)state->XYOffset < bank->start) {
+        return NULL;
+    }
+    uae_u32 offset = (uae_u32)state->XYOffset - bank->start;
+    uae_u32 needed = offset + (height - 1) * state->BytesPerRow + width * srcpixbytes;
+    if (needed > bank->allocated_size) {
+        return NULL;
+    }
+
+    uae_u8 *dst = xmalloc(uae_u8, (size_t)width * (size_t)height * (size_t)dstpixbytes);
+    if (!dst) {
+        return NULL;
+    }
+    if (dstpixbytes == 1) {
+        for (int y = 0; y < height; y++) {
+            memcpy(dst + (size_t)y * (size_t)width,
+                bank->baseaddr + offset + (size_t)y * (size_t)state->BytesPerRow,
+                (size_t)width);
+        }
+        for (int i = 0; i < 256; i++) {
+            palette[i * 3 + 0] = state->CLUT[i].Red;
+            palette[i * 3 + 1] = state->CLUT[i].Green;
+            palette[i * 3 + 2] = state->CLUT[i].Blue;
+        }
+    } else {
+        alloc_colors_picasso(8, 8, 8, 16, 8, 0, (RGBFTYPE)state->RGBFormat, p96_rgbx16);
+        unix_picasso_render_pixels(bank->baseaddr + offset, width, height, state->BytesPerRow,
+            srcpixbytes, (RGBFTYPE)state->RGBFormat, pvidinfo->clut, dst, width * dstpixbytes);
+    }
+
+    *widthp = width;
+    *heightp = height;
+    *pitch = width * dstpixbytes;
+    *depth = dstpixbytes * 8;
+    return dst;
+}
+
+void uaegfx_freertgbuffer(int, uae_u8 *dst)
+{
+    xfree(dst);
+}
+
+static void unix_picasso_render_pixels(const uae_u8 *srcbase, int width, int height,
+    int srcrowbytes, int srcpixbytes, RGBFTYPE rgbfmt, const uae_u32 *clut,
+    uae_u8 *dstbase, int dstrowbytes, const uae_u32 *rgbx16)
+{
+    for (int y = 0; y < height; y++) {
+        const uae_u8 *src = srcbase + y * srcrowbytes;
+        uae_u32 *dst = (uae_u32 *)(dstbase + y * dstrowbytes);
+        for (int x = 0; x < width; x++) {
+            dst[x] = unix_picasso_convert_pixel(src + x * srcpixbytes, rgbfmt, clut, rgbx16);
+        }
+    }
+}
+
+struct unix_rtg_render_job
+{
+    int monid;
+    int width;
+    int height;
+    int srcrowbytes;
+    int srcpixbytes;
+    RGBFTYPE rgbfmt;
+    uae_u32 clut[256];
+    std::vector<uae_u8> src;
+    std::vector<uae_u32> rgbx16;
+};
+
+struct unix_rtg_render_result
+{
+    int monid;
+    int width;
+    int height;
+    std::vector<uae_u8> pixels;
+};
+
+static std::thread unix_rtg_render_thread;
+static std::mutex unix_rtg_render_mutex;
+static std::condition_variable unix_rtg_render_cv;
+static bool unix_rtg_render_stop;
+static bool unix_rtg_render_has_job;
+static bool unix_rtg_render_busy;
+static bool unix_rtg_render_ready;
+static unix_rtg_render_job unix_rtg_pending_job;
+static unix_rtg_render_result unix_rtg_ready_result;
+
+static void unix_rtg_render_worker(void)
+{
+    for (;;) {
+        unix_rtg_render_job job;
+        {
+            std::unique_lock<std::mutex> lock(unix_rtg_render_mutex);
+            unix_rtg_render_cv.wait(lock, [] {
+                return unix_rtg_render_stop || unix_rtg_render_has_job;
+            });
+            if (unix_rtg_render_stop && !unix_rtg_render_has_job) {
+                return;
+            }
+            job = std::move(unix_rtg_pending_job);
+            unix_rtg_render_has_job = false;
+            unix_rtg_render_busy = true;
+        }
+
+        unix_rtg_render_result result;
+        result.monid = job.monid;
+        result.width = job.width;
+        result.height = job.height;
+        result.pixels.assign((size_t)job.width * (size_t)job.height * sizeof(uae_u32), 0);
+        unix_picasso_render_pixels(job.src.data(), job.width, job.height, job.srcrowbytes,
+            job.srcpixbytes, job.rgbfmt, job.clut, result.pixels.data(),
+            job.width * (int)sizeof(uae_u32),
+            job.rgbx16.empty() ? p96_rgbx16 : job.rgbx16.data());
+
+        {
+            std::lock_guard<std::mutex> lock(unix_rtg_render_mutex);
+            unix_rtg_ready_result = std::move(result);
+            unix_rtg_render_ready = true;
+            unix_rtg_render_busy = false;
+        }
+    }
+}
+
+static bool unix_rtg_start_render_thread_locked(void)
+{
+    if (unix_rtg_render_thread.joinable()) {
+        return true;
+    }
+    try {
+        unix_rtg_render_stop = false;
+        unix_rtg_render_thread = std::thread(unix_rtg_render_worker);
+    } catch (...) {
+        return false;
+    }
+    return true;
+}
+
+static void unix_rtg_stop_render_thread(void)
+{
+    {
+        std::lock_guard<std::mutex> lock(unix_rtg_render_mutex);
+        unix_rtg_render_stop = true;
+        unix_rtg_render_has_job = false;
+    }
+    unix_rtg_render_cv.notify_all();
+    if (unix_rtg_render_thread.joinable()) {
+        unix_rtg_render_thread.join();
+    }
+    {
+        std::lock_guard<std::mutex> lock(unix_rtg_render_mutex);
+        unix_rtg_pending_job = unix_rtg_render_job();
+        unix_rtg_ready_result = unix_rtg_render_result();
+        unix_rtg_render_ready = false;
+        unix_rtg_render_busy = false;
+    }
+    memset(unix_rtg_render_has_output, 0, sizeof unix_rtg_render_has_output);
+}
+
+static bool unix_rtg_collect_render_result(int monid, struct vidbuffer *vb)
+{
+    unix_rtg_render_result result;
+    {
+        std::lock_guard<std::mutex> lock(unix_rtg_render_mutex);
+        if (!unix_rtg_render_ready || unix_rtg_ready_result.monid != monid) {
+            return false;
+        }
+        result = std::move(unix_rtg_ready_result);
+        unix_rtg_render_ready = false;
+    }
+    if (!vb || !vb->bufmem || result.width != vb->outwidth || result.height != vb->outheight) {
+        return false;
+    }
+    for (int y = 0; y < result.height; y++) {
+        memcpy(vb->bufmem + y * vb->rowbytes,
+            result.pixels.data() + (size_t)y * (size_t)result.width * sizeof(uae_u32),
+            (size_t)result.width * sizeof(uae_u32));
+    }
+    unix_rtg_overlay_sprite(monid, (uae_u32 *)vb->bufmem, result.width, result.height,
+        vb->rowbytes / sizeof(uae_u32));
+    unix_rtg_render_has_output[monid] = true;
+    return true;
+}
+
+static bool unix_rtg_submit_render_job(int monid, const uae_u8 *srcbase, int width, int height,
+    int srcrowbytes, int srcpixbytes, RGBFTYPE rgbfmt, const uae_u32 *clut)
+{
+    if (!srcbase || width <= 0 || height <= 0 || srcrowbytes <= 0 || srcpixbytes <= 0) {
+        return false;
+    }
+
+    unix_rtg_render_job job;
+    job.monid = monid;
+    job.width = width;
+    job.height = height;
+    job.srcrowbytes = width * srcpixbytes;
+    job.srcpixbytes = srcpixbytes;
+    job.rgbfmt = rgbfmt;
+    memcpy(job.clut, clut, sizeof job.clut);
+    if (srcpixbytes == 2) {
+        job.rgbx16.assign(p96_rgbx16, p96_rgbx16 + 65536);
+    }
+    job.src.assign((size_t)job.srcrowbytes * (size_t)height, 0);
+    for (int y = 0; y < height; y++) {
+        memcpy(job.src.data() + (size_t)y * (size_t)job.srcrowbytes,
+            srcbase + (size_t)y * (size_t)srcrowbytes, (size_t)job.srcrowbytes);
+    }
+
+    {
+        std::lock_guard<std::mutex> lock(unix_rtg_render_mutex);
+        if (!unix_rtg_start_render_thread_locked() || unix_rtg_render_has_job ||
+            unix_rtg_render_busy || unix_rtg_render_ready) {
+            return false;
+        }
+        unix_rtg_pending_job = std::move(job);
+        unix_rtg_render_has_job = true;
+    }
+    unix_rtg_render_cv.notify_one();
+    return true;
+}
+
+static void unix_picasso_render(int monid)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS || !unix_picasso_ensure_buffer(monid)) {
+        return;
+    }
+
+    struct picasso96_state_struct *state = &picasso96_state[monid];
+    struct picasso_vidbuf_description *pvidinfo = &picasso_vidinfo[monid];
+    struct vidbuffer *vb = &adisplays[monid].gfxvidinfo.drawbuffer;
+    addrbank *bank = gfxmem_banks[0];
+    int srcpixbytes = unix_picasso_bytes_per_pixel((RGBFTYPE)state->RGBFormat);
+
+    if (!bank || !bank->baseaddr || !srcpixbytes || !state->Address || !state->BytesPerRow ||
+        !state->Width || !state->Height) {
+        return;
+    }
+    if ((uae_u32)state->XYOffset < bank->start) {
+        return;
+    }
+
+    uae_u32 offset = (uae_u32)state->XYOffset - bank->start;
+    uae_u32 needed = offset + (state->Height - 1) * state->BytesPerRow + state->Width * srcpixbytes;
+    if (needed > bank->allocated_size) {
+        return;
+    }
+
+    alloc_colors_picasso(8, 8, 8, 16, 8, 0, (RGBFTYPE)state->RGBFormat, p96_rgbx16);
+    const uae_u8 *srcbase = bank->baseaddr + offset;
+    if (currprefs.rtg_multithread) {
+        bool collected = unix_rtg_collect_render_result(monid, vb);
+        bool submitted = unix_rtg_submit_render_job(monid, srcbase, state->Width, state->Height,
+            state->BytesPerRow, srcpixbytes, (RGBFTYPE)state->RGBFormat, pvidinfo->clut);
+        if (collected || submitted) {
+            if (unix_rtg_render_has_output[monid]) {
+                return;
+            }
+        }
+    }
+    unix_picasso_render_pixels(srcbase, state->Width, state->Height, state->BytesPerRow,
+        srcpixbytes, (RGBFTYPE)state->RGBFormat, pvidinfo->clut, vb->bufmem, vb->rowbytes);
+    unix_rtg_overlay_sprite(monid, (uae_u32 *)vb->bufmem, state->Width, state->Height, vb->rowbytes / sizeof(uae_u32));
+    unix_rtg_render_has_output[monid] = true;
+}
+
+void picasso_enablescreen(int monid, int on)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS) {
+        return;
+    }
+    picasso_vidinfo[monid].picasso_active = on != 0;
+    unix_apply_video_mode_from_prefs(&currprefs, monid);
+    if (on) {
+        picasso_refresh(monid);
+    }
+}
+
+void picasso_refresh(int monid)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS) {
+        return;
+    }
+    if (currprefs.rtgboards[0].rtgmem_type >= GFXBOARD_HARDWARE) {
+        gfxboard_refresh(monid);
+        return;
+    }
+    unix_picasso_render(monid);
+    if (adisplays[monid].picasso_on || picasso_vidinfo[monid].picasso_active) {
+#ifdef AVIOUTPUT
+        frame_drawn(monid);
+#endif
+        show_screen(monid, 0);
+    }
+}
+void init_hz_p96(int) {}
+
+void gfx_set_picasso_modeinfo(int monid, RGBFTYPE rgbfmt)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS) {
+        return;
+    }
+    struct picasso96_state_struct *state = &picasso96_state[monid];
+    picasso_vidinfo[monid].rgbformat = rgbfmt;
+    picasso_vidinfo[monid].selected_rgbformat = rgbfmt;
+    picasso_vidinfo[monid].depth = state->GC_Depth ? (state->GC_Depth + 7) / 8 : 0;
+    picasso_vidinfo[monid].picasso_changed = true;
+    unix_picasso_ensure_buffer(monid);
+}
+
+void gfx_set_picasso_colors(int monid, RGBFTYPE rgbfmt)
+{
+    if (monid >= 0 && monid < MAX_AMIGAMONITORS) {
+        picasso_vidinfo[monid].rgbformat = rgbfmt;
+    }
+}
+
+void gfx_set_picasso_state(int monid, int on)
+{
+    if (monid >= 0 && monid < MAX_AMIGAMONITORS) {
+        picasso_vidinfo[monid].picasso_active = on != 0;
+    }
+}
+
+uae_u8 *gfx_lock_picasso(int monid, bool)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS || !unix_picasso_ensure_buffer(monid)) {
+        return NULL;
+    }
+    struct vidbuffer *vb = &adisplays[monid].gfxvidinfo.drawbuffer;
+    vb->locked = true;
+    return vb->bufmem;
+}
+
+void gfx_unlock_picasso(int monid, bool dorender)
+{
+    if (monid < 0 || monid >= MAX_AMIGAMONITORS) {
+        return;
+    }
+    struct vidbuffer *vb = &adisplays[monid].gfxvidinfo.drawbuffer;
+    vb->locked = false;
+    if (dorender) {
+        show_screen(monid, 0);
+    }
+}
+void picasso_invalidate(int, int, int, int, int) {}
+
+void picasso_handle_vsync(void)
+{
+    for (int monid = 0; monid < MAX_AMIGAMONITORS; monid++) {
+        struct amigadisplay *ad = &adisplays[monid];
+        struct picasso_vidbuf_description *vidinfo = &picasso_vidinfo[monid];
+        int state = vidinfo->picasso_state_change;
+        bool uaegfx = currprefs.rtgboards[0].rtgmem_type < GFXBOARD_HARDWARE;
+
+        if (state) {
+            atomic_and(&vidinfo->picasso_state_change, ~state);
+            if (state & UNIX_PICASSO_STATE_SETGC) {
+                gfx_set_picasso_modeinfo(monid, (RGBFTYPE)picasso96_state[monid].RGBFormat);
+            }
+            if (state & UNIX_PICASSO_STATE_SETSWITCH) {
+                vidinfo->picasso_active = ad->picasso_requested_on;
+            }
+            if (state & (UNIX_PICASSO_STATE_SETGC | UNIX_PICASSO_STATE_SETPANNING |
+                UNIX_PICASSO_STATE_SETDAC | UNIX_PICASSO_STATE_SETDISPLAY)) {
+                vidinfo->full_refresh = 1;
+            }
+        }
+        if (!uaegfx) {
+            continue;
+        }
+
+        if (ad->picasso_on || vidinfo->picasso_active) {
+            picasso_trigger_vblank();
+            picasso_refresh(monid);
+        }
+    }
+    gfxboard_vsync_handler(false, true);
+}
+
+void fb_copyrow(int monid, uae_u8 *src, uae_u8 *dst, int x, int, int width, int srcpixbytes, int dy)
+{
+    if (!src || !dst || monid < 0 || monid >= MAX_AMIGAMONITORS) {
+        return;
+    }
+    int rowbytes = picasso_vidinfo[monid].rowbytes;
+    int pixbytes = picasso_vidinfo[monid].pixbytes ? picasso_vidinfo[monid].pixbytes : srcpixbytes;
+    if (rowbytes <= 0 || pixbytes <= 0 || width <= 0) {
+        return;
+    }
+    memcpy(dst + dy * rowbytes + x * pixbytes, src, (size_t)width * (size_t)srcpixbytes);
+}
diff --git a/od-unix/input.cpp b/od-unix/input.cpp
new file mode 100644 (file)
index 0000000..71889a1
--- /dev/null
@@ -0,0 +1,1425 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#ifdef UAE_UNIX_WITH_SDL3
+#define SDL_MAIN_HANDLED
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#endif
+
+#include "options.h"
+#include "traps.h"
+#include "gui.h"
+#include "inputdevice.h"
+#include "input.h"
+
+int pause_emulation;
+int tablet_log;
+int key_swap_hack;
+
+static TCHAR empty_friendly[] = _T("Unix placeholder");
+static TCHAR empty_unique_name[] = _T("unix.placeholder");
+static TCHAR mouse_friendly[] = _T("Unix Mouse");
+static TCHAR mouse_unique_name[] = _T("unix.mouse");
+static TCHAR mouse_axis_names[][16] = {
+    _T("X Axis"),
+    _T("Y Axis"),
+    _T("Wheel")
+};
+static TCHAR mouse_button_names[][16] = {
+    _T("Button 1"),
+    _T("Button 2"),
+    _T("Button 3")
+};
+static TCHAR keyboard_friendly[] = _T("Unix Keyboard");
+static TCHAR keyboard_unique_name[] = _T("unix.keyboard");
+static bool mouse_active;
+static bool keyboard_state[512];
+static int capslockstate;
+static int host_capslockstate;
+static int host_numlockstate;
+static int host_scrolllockstate;
+
+#ifdef UAE_UNIX_WITH_SDL3
+enum {
+    UNIX_AXIS_SDL,
+    UNIX_AXIS_GAMEPAD_DPAD_X,
+    UNIX_AXIS_GAMEPAD_DPAD_Y,
+    UNIX_AXIS_HAT_X,
+    UNIX_AXIS_HAT_Y
+};
+
+struct unix_joystick_device {
+    SDL_JoystickID instance_id;
+    SDL_Gamepad *gamepad;
+    SDL_Joystick *joystick;
+    bool is_gamepad;
+    TCHAR friendly[128];
+    TCHAR unique[160];
+    int axis_count;
+    int button_count;
+    int axis_kind[ID_AXIS_TOTAL];
+    int axis_code[ID_AXIS_TOTAL];
+    int button_code[ID_BUTTON_TOTAL];
+    int axis_state[ID_AXIS_TOTAL];
+    bool button_state[ID_BUTTON_TOTAL];
+};
+
+static unix_joystick_device unix_joysticks[MAX_INPUT_DEVICES];
+static int unix_joystick_count;
+static bool unix_joystick_sdl_initialized;
+
+static const SDL_GamepadAxis unix_gamepad_axes[] = {
+    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
+};
+
+static const TCHAR *const unix_gamepad_axis_names[] = {
+    _T("Left X Axis"),
+    _T("Left Y Axis"),
+    _T("Right X Axis"),
+    _T("Right Y Axis"),
+    _T("Left Trigger"),
+    _T("Right Trigger")
+};
+
+static const SDL_GamepadButton unix_gamepad_buttons[] = {
+    SDL_GAMEPAD_BUTTON_SOUTH,
+    SDL_GAMEPAD_BUTTON_EAST,
+    SDL_GAMEPAD_BUTTON_WEST,
+    SDL_GAMEPAD_BUTTON_NORTH,
+    SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
+    SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER,
+    SDL_GAMEPAD_BUTTON_START,
+    SDL_GAMEPAD_BUTTON_BACK,
+    SDL_GAMEPAD_BUTTON_LEFT_STICK,
+    SDL_GAMEPAD_BUTTON_RIGHT_STICK,
+    SDL_GAMEPAD_BUTTON_GUIDE,
+    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
+};
+
+static const TCHAR *const unix_gamepad_button_names[] = {
+    _T("South Button"),
+    _T("East Button"),
+    _T("West Button"),
+    _T("North Button"),
+    _T("Left Shoulder"),
+    _T("Right Shoulder"),
+    _T("Start Button"),
+    _T("Back Button"),
+    _T("Left Stick Button"),
+    _T("Right Stick Button"),
+    _T("Guide Button"),
+    _T("Misc Button 1"),
+    _T("Right Paddle 1"),
+    _T("Left Paddle 1"),
+    _T("Right Paddle 2"),
+    _T("Left Paddle 2"),
+    _T("Touchpad Button"),
+    _T("Misc Button 2"),
+    _T("Misc Button 3"),
+    _T("Misc Button 4"),
+    _T("Misc Button 5"),
+    _T("Misc Button 6")
+};
+#endif
+
+enum {
+    UKEY_A = 4,
+    UKEY_B = 5,
+    UKEY_C = 6,
+    UKEY_D = 7,
+    UKEY_E = 8,
+    UKEY_F = 9,
+    UKEY_G = 10,
+    UKEY_H = 11,
+    UKEY_I = 12,
+    UKEY_J = 13,
+    UKEY_K = 14,
+    UKEY_L = 15,
+    UKEY_M = 16,
+    UKEY_N = 17,
+    UKEY_O = 18,
+    UKEY_P = 19,
+    UKEY_Q = 20,
+    UKEY_R = 21,
+    UKEY_S = 22,
+    UKEY_T = 23,
+    UKEY_U = 24,
+    UKEY_V = 25,
+    UKEY_W = 26,
+    UKEY_X = 27,
+    UKEY_Y = 28,
+    UKEY_Z = 29,
+    UKEY_1 = 30,
+    UKEY_2 = 31,
+    UKEY_3 = 32,
+    UKEY_4 = 33,
+    UKEY_5 = 34,
+    UKEY_6 = 35,
+    UKEY_7 = 36,
+    UKEY_8 = 37,
+    UKEY_9 = 38,
+    UKEY_0 = 39,
+    UKEY_RETURN = 40,
+    UKEY_ESCAPE = 41,
+    UKEY_BACKSPACE = 42,
+    UKEY_TAB = 43,
+    UKEY_SPACE = 44,
+    UKEY_MINUS = 45,
+    UKEY_EQUALS = 46,
+    UKEY_LEFTBRACKET = 47,
+    UKEY_RIGHTBRACKET = 48,
+    UKEY_BACKSLASH = 49,
+    UKEY_NONUSHASH = 50,
+    UKEY_SEMICOLON = 51,
+    UKEY_APOSTROPHE = 52,
+    UKEY_GRAVE = 53,
+    UKEY_COMMA = 54,
+    UKEY_PERIOD = 55,
+    UKEY_SLASH = 56,
+    UKEY_CAPSLOCK = 57,
+    UKEY_F1 = 58,
+    UKEY_F2 = 59,
+    UKEY_F3 = 60,
+    UKEY_F4 = 61,
+    UKEY_F5 = 62,
+    UKEY_F6 = 63,
+    UKEY_F7 = 64,
+    UKEY_F8 = 65,
+    UKEY_F9 = 66,
+    UKEY_F10 = 67,
+    UKEY_F11 = 68,
+    UKEY_F12 = 69,
+    UKEY_PRINTSCREEN = 70,
+    UKEY_SCROLLLOCK = 71,
+    UKEY_PAUSE = 72,
+    UKEY_INSERT = 73,
+    UKEY_HOME = 74,
+    UKEY_PAGEUP = 75,
+    UKEY_DELETE = 76,
+    UKEY_END = 77,
+    UKEY_PAGEDOWN = 78,
+    UKEY_RIGHT = 79,
+    UKEY_LEFT = 80,
+    UKEY_DOWN = 81,
+    UKEY_UP = 82,
+    UKEY_NUMLOCKCLEAR = 83,
+    UKEY_KP_DIVIDE = 84,
+    UKEY_KP_MULTIPLY = 85,
+    UKEY_KP_MINUS = 86,
+    UKEY_KP_PLUS = 87,
+    UKEY_KP_ENTER = 88,
+    UKEY_KP_1 = 89,
+    UKEY_KP_2 = 90,
+    UKEY_KP_3 = 91,
+    UKEY_KP_4 = 92,
+    UKEY_KP_5 = 93,
+    UKEY_KP_6 = 94,
+    UKEY_KP_7 = 95,
+    UKEY_KP_8 = 96,
+    UKEY_KP_9 = 97,
+    UKEY_KP_0 = 98,
+    UKEY_KP_PERIOD = 99,
+    UKEY_NONUSBACKSLASH = 100,
+    UKEY_APPLICATION = 101,
+    UKEY_F13 = 104,
+    UKEY_F14 = 105,
+    UKEY_F15 = 106,
+    UKEY_AUDIOSTOP = 260,
+    UKEY_AUDIOPLAY = 261,
+    UKEY_AUDIOPREV = 259,
+    UKEY_AUDIONEXT = 258,
+    UKEY_LCTRL = 224,
+    UKEY_LSHIFT = 225,
+    UKEY_LALT = 226,
+    UKEY_LGUI = 227,
+    UKEY_RCTRL = 228,
+    UKEY_RSHIFT = 229,
+    UKEY_RALT = 230,
+    UKEY_RGUI = 231
+};
+
+#define K(scancode, event) { scancode, { { event, 0 } } }
+#define KF(scancode, event, flags) { scancode, { { event, flags } } }
+#define K2(scancode, event1, flags1, event2, flags2) { scancode, { { event1, flags1 }, { event2, flags2 } } }
+#define K3(scancode, event1, flags1, event2, flags2, event3, flags3) { scancode, { { event1, flags1 }, { event2, flags2 }, { event3, flags3 } } }
+#define K4(scancode, event1, flags1, event2, flags2, event3, flags3, event4, flags4) { scancode, { { event1, flags1 }, { event2, flags2 }, { event3, flags3 }, { event4, flags4 } } }
+
+static uae_input_device_kbr_default keytrans_amiga[] = {
+    K(UKEY_ESCAPE, INPUTEVENT_KEY_ESC),
+
+    K3(UKEY_F1, INPUTEVENT_KEY_F1, 0, INPUTEVENT_SPC_FLOPPY0, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_EFLOPPY0, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K3(UKEY_F2, INPUTEVENT_KEY_F2, 0, INPUTEVENT_SPC_FLOPPY1, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_EFLOPPY1, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K3(UKEY_F3, INPUTEVENT_KEY_F3, 0, INPUTEVENT_SPC_FLOPPY2, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_EFLOPPY2, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K3(UKEY_F4, INPUTEVENT_KEY_F4, 0, INPUTEVENT_SPC_FLOPPY3, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_EFLOPPY3, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K3(UKEY_F5, INPUTEVENT_KEY_F5, 0, INPUTEVENT_SPC_CD0, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_ECD0, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K3(UKEY_F6, INPUTEVENT_KEY_F6, 0, INPUTEVENT_SPC_STATERESTOREDIALOG, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_STATESAVEDIALOG, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K(UKEY_F7, INPUTEVENT_KEY_F7),
+    K(UKEY_F8, INPUTEVENT_KEY_F8),
+    K2(UKEY_F9, INPUTEVENT_KEY_F9, 0, INPUTEVENT_SPC_TOGGLERTG, ID_FLAG_QUALIFIER_SPECIAL),
+    K(UKEY_F10, INPUTEVENT_KEY_F10),
+
+    K(UKEY_1, INPUTEVENT_KEY_1),
+    K(UKEY_2, INPUTEVENT_KEY_2),
+    K(UKEY_3, INPUTEVENT_KEY_3),
+    K(UKEY_4, INPUTEVENT_KEY_4),
+    K(UKEY_5, INPUTEVENT_KEY_5),
+    K(UKEY_6, INPUTEVENT_KEY_6),
+    K(UKEY_7, INPUTEVENT_KEY_7),
+    K(UKEY_8, INPUTEVENT_KEY_8),
+    K(UKEY_9, INPUTEVENT_KEY_9),
+    K(UKEY_0, INPUTEVENT_KEY_0),
+
+    K(UKEY_TAB, INPUTEVENT_KEY_TAB),
+    K(UKEY_A, INPUTEVENT_KEY_A),
+    K(UKEY_B, INPUTEVENT_KEY_B),
+    K(UKEY_C, INPUTEVENT_KEY_C),
+    K(UKEY_D, INPUTEVENT_KEY_D),
+    K(UKEY_E, INPUTEVENT_KEY_E),
+    K(UKEY_F, INPUTEVENT_KEY_F),
+    K(UKEY_G, INPUTEVENT_KEY_G),
+    K(UKEY_H, INPUTEVENT_KEY_H),
+    K(UKEY_I, INPUTEVENT_KEY_I),
+    K2(UKEY_J, INPUTEVENT_KEY_J, 0, INPUTEVENT_SPC_SWAPJOYPORTS, ID_FLAG_QUALIFIER_SPECIAL),
+    K(UKEY_K, INPUTEVENT_KEY_K),
+    K(UKEY_L, INPUTEVENT_KEY_L),
+    K(UKEY_M, INPUTEVENT_KEY_M),
+    K(UKEY_N, INPUTEVENT_KEY_N),
+    K(UKEY_O, INPUTEVENT_KEY_O),
+    K(UKEY_P, INPUTEVENT_KEY_P),
+    K(UKEY_Q, INPUTEVENT_KEY_Q),
+    K(UKEY_R, INPUTEVENT_KEY_R),
+    K(UKEY_S, INPUTEVENT_KEY_S),
+    K(UKEY_T, INPUTEVENT_KEY_T),
+    K(UKEY_U, INPUTEVENT_KEY_U),
+    K(UKEY_V, INPUTEVENT_KEY_V),
+    K(UKEY_W, INPUTEVENT_KEY_W),
+    K(UKEY_X, INPUTEVENT_KEY_X),
+    K(UKEY_Y, INPUTEVENT_KEY_Y),
+    K(UKEY_Z, INPUTEVENT_KEY_Z),
+
+    KF(UKEY_CAPSLOCK, INPUTEVENT_KEY_CAPS_LOCK, ID_FLAG_TOGGLE),
+
+    K(UKEY_KP_1, INPUTEVENT_KEY_NP_1),
+    K(UKEY_KP_2, INPUTEVENT_KEY_NP_2),
+    K(UKEY_KP_3, INPUTEVENT_KEY_NP_3),
+    K(UKEY_KP_4, INPUTEVENT_KEY_NP_4),
+    K(UKEY_KP_5, INPUTEVENT_KEY_NP_5),
+    K(UKEY_KP_6, INPUTEVENT_KEY_NP_6),
+    K(UKEY_KP_7, INPUTEVENT_KEY_NP_7),
+    K(UKEY_KP_8, INPUTEVENT_KEY_NP_8),
+    K(UKEY_KP_9, INPUTEVENT_KEY_NP_9),
+    K(UKEY_KP_0, INPUTEVENT_KEY_NP_0),
+    K(UKEY_KP_PERIOD, INPUTEVENT_KEY_NP_PERIOD),
+    K4(UKEY_KP_PLUS, INPUTEVENT_KEY_NP_ADD, 0, INPUTEVENT_SPC_VOLUME_UP, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_MASTER_VOLUME_UP, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_CONTROL, INPUTEVENT_SPC_INCREASE_REFRESHRATE, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K4(UKEY_KP_MINUS, INPUTEVENT_KEY_NP_SUB, 0, INPUTEVENT_SPC_VOLUME_DOWN, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_MASTER_VOLUME_DOWN, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_CONTROL, INPUTEVENT_SPC_DECREASE_REFRESHRATE, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT),
+    K3(UKEY_KP_MULTIPLY, INPUTEVENT_KEY_NP_MUL, 0, INPUTEVENT_SPC_VOLUME_MUTE, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_MASTER_VOLUME_MUTE, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_CONTROL),
+    K2(UKEY_KP_DIVIDE, INPUTEVENT_KEY_NP_DIV, 0, INPUTEVENT_SPC_STATEREWIND, ID_FLAG_QUALIFIER_SPECIAL),
+    K(UKEY_KP_ENTER, INPUTEVENT_KEY_ENTER),
+
+    K(UKEY_MINUS, INPUTEVENT_KEY_SUB),
+    K(UKEY_EQUALS, INPUTEVENT_KEY_EQUALS),
+    K(UKEY_BACKSPACE, INPUTEVENT_KEY_BACKSPACE),
+    K(UKEY_RETURN, INPUTEVENT_KEY_RETURN),
+    K(UKEY_SPACE, INPUTEVENT_KEY_SPACE),
+
+    K2(UKEY_LSHIFT, INPUTEVENT_KEY_SHIFT_LEFT, 0, INPUTEVENT_SPC_QUALIFIER_SHIFT, 0),
+    K2(UKEY_LCTRL, INPUTEVENT_KEY_CTRL, 0, INPUTEVENT_SPC_QUALIFIER_CONTROL, 0),
+    K2(UKEY_LGUI, INPUTEVENT_KEY_AMIGA_LEFT, 0, INPUTEVENT_SPC_QUALIFIER_WIN, 0),
+    K2(UKEY_LALT, INPUTEVENT_KEY_ALT_LEFT, 0, INPUTEVENT_SPC_QUALIFIER_ALT, 0),
+    K2(UKEY_RALT, INPUTEVENT_KEY_ALT_RIGHT, 0, INPUTEVENT_SPC_QUALIFIER_ALT, 0),
+    K2(UKEY_RGUI, INPUTEVENT_KEY_AMIGA_RIGHT, 0, INPUTEVENT_SPC_QUALIFIER_WIN, 0),
+    K2(UKEY_APPLICATION, INPUTEVENT_KEY_AMIGA_RIGHT, 0, INPUTEVENT_SPC_QUALIFIER_WIN, 0),
+    K2(UKEY_RCTRL, INPUTEVENT_KEY_CTRL, 0, INPUTEVENT_SPC_QUALIFIER_CONTROL, 0),
+    K2(UKEY_RSHIFT, INPUTEVENT_KEY_SHIFT_RIGHT, 0, INPUTEVENT_SPC_QUALIFIER_SHIFT, 0),
+
+    K(UKEY_UP, INPUTEVENT_KEY_CURSOR_UP),
+    K(UKEY_DOWN, INPUTEVENT_KEY_CURSOR_DOWN),
+    K2(UKEY_LEFT, INPUTEVENT_KEY_CURSOR_LEFT, 0, INPUTEVENT_SPC_PAUSE, ID_FLAG_QUALIFIER_SPECIAL),
+    K2(UKEY_RIGHT, INPUTEVENT_KEY_CURSOR_RIGHT, 0, INPUTEVENT_SPC_WARP, ID_FLAG_QUALIFIER_SPECIAL),
+
+    K2(UKEY_INSERT, INPUTEVENT_KEY_AMIGA_LEFT, 0, INPUTEVENT_SPC_PASTE, ID_FLAG_QUALIFIER_SPECIAL),
+    K(UKEY_DELETE, INPUTEVENT_KEY_DEL),
+    K(UKEY_HOME, INPUTEVENT_KEY_AMIGA_RIGHT),
+    K(UKEY_PAGEDOWN, INPUTEVENT_KEY_HELP),
+    K(UKEY_PAGEUP, INPUTEVENT_SPC_FREEZEBUTTON),
+
+    K(UKEY_LEFTBRACKET, INPUTEVENT_KEY_LEFTBRACKET),
+    K(UKEY_RIGHTBRACKET, INPUTEVENT_KEY_RIGHTBRACKET),
+    K(UKEY_SEMICOLON, INPUTEVENT_KEY_SEMICOLON),
+    K(UKEY_APOSTROPHE, INPUTEVENT_KEY_SINGLEQUOTE),
+    K(UKEY_GRAVE, INPUTEVENT_KEY_BACKQUOTE),
+    K(UKEY_BACKSLASH, INPUTEVENT_KEY_NUMBERSIGN),
+    K(UKEY_NONUSHASH, INPUTEVENT_KEY_BACKSLASH),
+    K(UKEY_NONUSBACKSLASH, INPUTEVENT_KEY_30),
+    K(UKEY_COMMA, INPUTEVENT_KEY_COMMA),
+    K(UKEY_PERIOD, INPUTEVENT_KEY_PERIOD),
+    K(UKEY_SLASH, INPUTEVENT_KEY_DIV),
+
+    K(UKEY_F11, INPUTEVENT_KEY_BACKSLASH),
+    K(UKEY_F13, INPUTEVENT_KEY_BACKSLASH),
+    K(UKEY_F14, INPUTEVENT_KEY_NP_LPAREN),
+    K(UKEY_F15, INPUTEVENT_KEY_NP_RPAREN),
+    K2(UKEY_PRINTSCREEN, INPUTEVENT_SPC_SCREENSHOT_CLIPBOARD, 0, INPUTEVENT_SPC_SCREENSHOT, ID_FLAG_QUALIFIER_SPECIAL),
+    K(UKEY_END, INPUTEVENT_SPC_QUALIFIER_SPECIAL),
+    K4(UKEY_PAUSE, INPUTEVENT_SPC_PAUSE, 0, INPUTEVENT_SPC_SINGLESTEP, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_CONTROL, INPUTEVENT_SPC_IRQ7, ID_FLAG_QUALIFIER_SPECIAL | ID_FLAG_QUALIFIER_SHIFT, INPUTEVENT_SPC_WARP, ID_FLAG_QUALIFIER_SPECIAL),
+    K4(UKEY_F12, INPUTEVENT_SPC_ENTERGUI, 0, INPUTEVENT_SPC_ENTERDEBUGGER, ID_FLAG_QUALIFIER_SPECIAL, INPUTEVENT_SPC_ENTERDEBUGGER, ID_FLAG_QUALIFIER_SHIFT, INPUTEVENT_SPC_TOGGLEDEFAULTSCREEN, ID_FLAG_QUALIFIER_CONTROL),
+
+    K(UKEY_AUDIOSTOP, INPUTEVENT_KEY_CDTV_STOP),
+    K(UKEY_AUDIOPLAY, INPUTEVENT_KEY_CDTV_PLAYPAUSE),
+    K(UKEY_AUDIOPREV, INPUTEVENT_KEY_CDTV_PREV),
+    K(UKEY_AUDIONEXT, INPUTEVENT_KEY_CDTV_NEXT),
+
+    { -1, { { 0, 0 } } }
+};
+
+static uae_input_device_kbr_default keytrans_pc[] = {
+    K(UKEY_ESCAPE, INPUTEVENT_KEY_ESC),
+
+    K(UKEY_F1, INPUTEVENT_KEY_F1),
+    K(UKEY_F2, INPUTEVENT_KEY_F2),
+    K(UKEY_F3, INPUTEVENT_KEY_F3),
+    K(UKEY_F4, INPUTEVENT_KEY_F4),
+    K(UKEY_F5, INPUTEVENT_KEY_F5),
+    K(UKEY_F6, INPUTEVENT_KEY_F6),
+    K(UKEY_F7, INPUTEVENT_KEY_F7),
+    K(UKEY_F8, INPUTEVENT_KEY_F8),
+    K(UKEY_F9, INPUTEVENT_KEY_F9),
+    K(UKEY_F10, INPUTEVENT_KEY_F10),
+    K(UKEY_F11, INPUTEVENT_KEY_F11),
+    K(UKEY_F12, INPUTEVENT_KEY_F12),
+
+    K(UKEY_1, INPUTEVENT_KEY_1),
+    K(UKEY_2, INPUTEVENT_KEY_2),
+    K(UKEY_3, INPUTEVENT_KEY_3),
+    K(UKEY_4, INPUTEVENT_KEY_4),
+    K(UKEY_5, INPUTEVENT_KEY_5),
+    K(UKEY_6, INPUTEVENT_KEY_6),
+    K(UKEY_7, INPUTEVENT_KEY_7),
+    K(UKEY_8, INPUTEVENT_KEY_8),
+    K(UKEY_9, INPUTEVENT_KEY_9),
+    K(UKEY_0, INPUTEVENT_KEY_0),
+
+    K(UKEY_TAB, INPUTEVENT_KEY_TAB),
+    K(UKEY_A, INPUTEVENT_KEY_A),
+    K(UKEY_B, INPUTEVENT_KEY_B),
+    K(UKEY_C, INPUTEVENT_KEY_C),
+    K(UKEY_D, INPUTEVENT_KEY_D),
+    K(UKEY_E, INPUTEVENT_KEY_E),
+    K(UKEY_F, INPUTEVENT_KEY_F),
+    K(UKEY_G, INPUTEVENT_KEY_G),
+    K(UKEY_H, INPUTEVENT_KEY_H),
+    K(UKEY_I, INPUTEVENT_KEY_I),
+    K(UKEY_J, INPUTEVENT_KEY_J),
+    K(UKEY_K, INPUTEVENT_KEY_K),
+    K(UKEY_L, INPUTEVENT_KEY_L),
+    K(UKEY_M, INPUTEVENT_KEY_M),
+    K(UKEY_N, INPUTEVENT_KEY_N),
+    K(UKEY_O, INPUTEVENT_KEY_O),
+    K(UKEY_P, INPUTEVENT_KEY_P),
+    K(UKEY_Q, INPUTEVENT_KEY_Q),
+    K(UKEY_R, INPUTEVENT_KEY_R),
+    K(UKEY_S, INPUTEVENT_KEY_S),
+    K(UKEY_T, INPUTEVENT_KEY_T),
+    K(UKEY_U, INPUTEVENT_KEY_U),
+    K(UKEY_V, INPUTEVENT_KEY_V),
+    K(UKEY_W, INPUTEVENT_KEY_W),
+    K(UKEY_X, INPUTEVENT_KEY_X),
+    K(UKEY_Y, INPUTEVENT_KEY_Y),
+    K(UKEY_Z, INPUTEVENT_KEY_Z),
+
+    KF(UKEY_CAPSLOCK, INPUTEVENT_KEY_CAPS_LOCK, ID_FLAG_TOGGLE),
+
+    K(UKEY_KP_1, INPUTEVENT_KEY_NP_1),
+    K(UKEY_KP_2, INPUTEVENT_KEY_NP_2),
+    K(UKEY_KP_3, INPUTEVENT_KEY_NP_3),
+    K(UKEY_KP_4, INPUTEVENT_KEY_NP_4),
+    K(UKEY_KP_5, INPUTEVENT_KEY_NP_5),
+    K(UKEY_KP_6, INPUTEVENT_KEY_NP_6),
+    K(UKEY_KP_7, INPUTEVENT_KEY_NP_7),
+    K(UKEY_KP_8, INPUTEVENT_KEY_NP_8),
+    K(UKEY_KP_9, INPUTEVENT_KEY_NP_9),
+    K(UKEY_KP_0, INPUTEVENT_KEY_NP_0),
+    K(UKEY_KP_PERIOD, INPUTEVENT_KEY_NP_PERIOD),
+    K(UKEY_KP_PLUS, INPUTEVENT_KEY_NP_ADD),
+    K(UKEY_KP_MINUS, INPUTEVENT_KEY_NP_SUB),
+    K(UKEY_KP_MULTIPLY, INPUTEVENT_KEY_NP_MUL),
+    K(UKEY_KP_DIVIDE, INPUTEVENT_KEY_NP_DIV),
+    K(UKEY_KP_ENTER, INPUTEVENT_KEY_ENTER),
+
+    K(UKEY_MINUS, INPUTEVENT_KEY_SUB),
+    K(UKEY_EQUALS, INPUTEVENT_KEY_EQUALS),
+    K(UKEY_BACKSPACE, INPUTEVENT_KEY_BACKSPACE),
+    K(UKEY_RETURN, INPUTEVENT_KEY_RETURN),
+    K(UKEY_SPACE, INPUTEVENT_KEY_SPACE),
+
+    K(UKEY_LSHIFT, INPUTEVENT_KEY_SHIFT_LEFT),
+    K(UKEY_LCTRL, INPUTEVENT_KEY_CTRL),
+    K(UKEY_LGUI, INPUTEVENT_KEY_AMIGA_LEFT),
+    K(UKEY_LALT, INPUTEVENT_KEY_ALT_LEFT),
+    K(UKEY_RALT, INPUTEVENT_KEY_ALT_RIGHT),
+    K(UKEY_RGUI, INPUTEVENT_KEY_AMIGA_RIGHT),
+    K(UKEY_APPLICATION, INPUTEVENT_KEY_APPS),
+    K(UKEY_RCTRL, INPUTEVENT_KEY_CTRL),
+    K(UKEY_RSHIFT, INPUTEVENT_KEY_SHIFT_RIGHT),
+
+    K(UKEY_UP, INPUTEVENT_KEY_CURSOR_UP),
+    K(UKEY_DOWN, INPUTEVENT_KEY_CURSOR_DOWN),
+    K(UKEY_LEFT, INPUTEVENT_KEY_CURSOR_LEFT),
+    K(UKEY_RIGHT, INPUTEVENT_KEY_CURSOR_RIGHT),
+
+    K(UKEY_LEFTBRACKET, INPUTEVENT_KEY_LEFTBRACKET),
+    K(UKEY_RIGHTBRACKET, INPUTEVENT_KEY_RIGHTBRACKET),
+    K(UKEY_SEMICOLON, INPUTEVENT_KEY_SEMICOLON),
+    K(UKEY_APOSTROPHE, INPUTEVENT_KEY_SINGLEQUOTE),
+    K(UKEY_GRAVE, INPUTEVENT_KEY_BACKQUOTE),
+    K(UKEY_BACKSLASH, INPUTEVENT_KEY_2B),
+    K(UKEY_NONUSHASH, INPUTEVENT_KEY_BACKSLASH),
+    K(UKEY_NONUSBACKSLASH, INPUTEVENT_KEY_30),
+    K(UKEY_COMMA, INPUTEVENT_KEY_COMMA),
+    K(UKEY_PERIOD, INPUTEVENT_KEY_PERIOD),
+    K(UKEY_SLASH, INPUTEVENT_KEY_DIV),
+
+    K(UKEY_INSERT, INPUTEVENT_KEY_INSERT),
+    K(UKEY_DELETE, INPUTEVENT_KEY_DEL),
+    K(UKEY_HOME, INPUTEVENT_KEY_HOME),
+    K(UKEY_END, INPUTEVENT_KEY_END),
+    K(UKEY_PAGEUP, INPUTEVENT_KEY_PAGEUP),
+    K(UKEY_PAGEDOWN, INPUTEVENT_KEY_PAGEDOWN),
+    K(UKEY_SCROLLLOCK, INPUTEVENT_KEY_HELP),
+    K(UKEY_PRINTSCREEN, INPUTEVENT_KEY_SYSRQ),
+    K(UKEY_PAUSE, INPUTEVENT_KEY_PAUSE),
+
+    K(UKEY_AUDIOSTOP, INPUTEVENT_KEY_CDTV_STOP),
+    K(UKEY_AUDIOPLAY, INPUTEVENT_KEY_CDTV_PLAYPAUSE),
+    K(UKEY_AUDIOPREV, INPUTEVENT_KEY_CDTV_PREV),
+    K(UKEY_AUDIONEXT, INPUTEVENT_KEY_CDTV_NEXT),
+
+    { -1, { { 0, 0 } } }
+};
+
+#undef K4
+#undef K3
+#undef K2
+#undef KF
+#undef K
+
+static uae_input_device_kbr_default *keytrans[] = {
+    keytrans_amiga,
+    keytrans_pc,
+    keytrans_pc
+};
+static int kb_np[] = { UKEY_KP_4, -1, UKEY_KP_6, -1, UKEY_KP_8, -1, UKEY_KP_2, -1, UKEY_KP_0, UKEY_KP_5, -1, UKEY_KP_PERIOD, -1, UKEY_KP_ENTER, -1, -1 };
+static int kb_ck[] = { UKEY_LEFT, -1, UKEY_RIGHT, -1, UKEY_UP, -1, UKEY_DOWN, -1, UKEY_RCTRL, UKEY_RALT, -1, UKEY_RSHIFT, -1, -1 };
+static int kb_se[] = { UKEY_A, -1, UKEY_D, -1, UKEY_W, -1, UKEY_S, -1, UKEY_LALT, -1, UKEY_LSHIFT, -1, -1 };
+static int kb_np3[] = { UKEY_KP_4, -1, UKEY_KP_6, -1, UKEY_KP_8, -1, UKEY_KP_2, -1, UKEY_KP_0, UKEY_KP_5, -1, UKEY_KP_PERIOD, -1, UKEY_KP_ENTER, -1, -1 };
+static int kb_ck3[] = { UKEY_LEFT, -1, UKEY_RIGHT, -1, UKEY_UP, -1, UKEY_DOWN, -1, UKEY_RCTRL, -1, UKEY_RSHIFT, -1, UKEY_RALT, -1, -1 };
+static int kb_se3[] = { UKEY_A, -1, UKEY_D, -1, UKEY_W, -1, UKEY_S, -1, UKEY_LALT, -1, UKEY_LSHIFT, -1, UKEY_LCTRL, -1, -1 };
+static int kb_cd32_np[] = { UKEY_KP_4, -1, UKEY_KP_6, -1, UKEY_KP_8, -1, UKEY_KP_2, -1, UKEY_KP_0, UKEY_KP_5, UKEY_KP_1, -1, UKEY_KP_PERIOD, UKEY_KP_3, -1, UKEY_KP_7, -1, UKEY_KP_9, -1, UKEY_KP_DIVIDE, -1, UKEY_KP_MINUS, -1, UKEY_KP_MULTIPLY, -1, -1 };
+static int kb_cd32_ck[] = { UKEY_LEFT, -1, UKEY_RIGHT, -1, UKEY_UP, -1, UKEY_DOWN, -1, UKEY_RCTRL, UKEY_RALT, UKEY_RSHIFT, -1, UKEY_KP_7, -1, UKEY_KP_9, -1, UKEY_KP_DIVIDE, -1, UKEY_KP_MINUS, -1, UKEY_KP_MULTIPLY, -1, -1 };
+static int kb_cd32_se[] = { UKEY_A, -1, UKEY_D, -1, UKEY_W, -1, UKEY_S, -1, -1, UKEY_LALT, -1, UKEY_LSHIFT, -1, UKEY_KP_7, -1, UKEY_KP_9, -1, UKEY_KP_DIVIDE, -1, UKEY_KP_MINUS, -1, UKEY_KP_MULTIPLY, -1, -1 };
+static int kb_arcadia[] = { UKEY_F2, -1, UKEY_1, -1, UKEY_2, -1, UKEY_5, -1, UKEY_6, -1, -1 };
+static int kb_cdtv[] = { UKEY_KP_1, -1, UKEY_KP_3, -1, UKEY_KP_7, -1, UKEY_KP_9, -1, -1 };
+static int *keymaps[] = {
+    kb_np, kb_ck, kb_se, kb_np3, kb_ck3, kb_se3,
+    kb_cd32_np, kb_cd32_ck, kb_cd32_se,
+    kb_arcadia, kb_cdtv
+};
+
+static const int keyboard_keycodes[] = {
+    UKEY_ESCAPE,
+    UKEY_F1, UKEY_F2, UKEY_F3, UKEY_F4, UKEY_F5, UKEY_F6, UKEY_F7, UKEY_F8, UKEY_F9, UKEY_F10, UKEY_F11, UKEY_F12,
+    UKEY_1, UKEY_2, UKEY_3, UKEY_4, UKEY_5, UKEY_6, UKEY_7, UKEY_8, UKEY_9, UKEY_0,
+    UKEY_TAB,
+    UKEY_A, UKEY_B, UKEY_C, UKEY_D, UKEY_E, UKEY_F, UKEY_G, UKEY_H, UKEY_I, UKEY_J, UKEY_K, UKEY_L, UKEY_M,
+    UKEY_N, UKEY_O, UKEY_P, UKEY_Q, UKEY_R, UKEY_S, UKEY_T, UKEY_U, UKEY_V, UKEY_W, UKEY_X, UKEY_Y, UKEY_Z,
+    UKEY_CAPSLOCK,
+    UKEY_KP_1, UKEY_KP_2, UKEY_KP_3, UKEY_KP_4, UKEY_KP_5, UKEY_KP_6, UKEY_KP_7, UKEY_KP_8, UKEY_KP_9, UKEY_KP_0,
+    UKEY_KP_PERIOD, UKEY_KP_PLUS, UKEY_KP_MINUS, UKEY_KP_MULTIPLY, UKEY_KP_DIVIDE, UKEY_KP_ENTER,
+    UKEY_MINUS, UKEY_EQUALS, UKEY_BACKSPACE, UKEY_RETURN, UKEY_SPACE,
+    UKEY_LSHIFT, UKEY_LCTRL, UKEY_LGUI, UKEY_LALT, UKEY_RALT, UKEY_RGUI, UKEY_APPLICATION, UKEY_RCTRL, UKEY_RSHIFT,
+    UKEY_UP, UKEY_DOWN, UKEY_LEFT, UKEY_RIGHT,
+    UKEY_INSERT, UKEY_DELETE, UKEY_HOME, UKEY_END, UKEY_PAGEUP, UKEY_PAGEDOWN, UKEY_SCROLLLOCK, UKEY_PRINTSCREEN, UKEY_PAUSE,
+    UKEY_LEFTBRACKET, UKEY_RIGHTBRACKET, UKEY_SEMICOLON, UKEY_APOSTROPHE, UKEY_GRAVE, UKEY_BACKSLASH, UKEY_NONUSHASH, UKEY_NONUSBACKSLASH,
+    UKEY_COMMA, UKEY_PERIOD, UKEY_SLASH,
+    UKEY_F13, UKEY_F14, UKEY_F15,
+    UKEY_AUDIOSTOP, UKEY_AUDIOPLAY, UKEY_AUDIOPREV, UKEY_AUDIONEXT
+};
+
+static int input_init(void)
+{
+    inputdevice_setkeytranslation(keytrans, keymaps);
+    return 1;
+}
+static void input_close(void) {}
+static int input_acquire(int, int) { return 1; }
+static void input_unacquire(int) {}
+static void input_read(void) {}
+static int empty_get_num(void) { return 0; }
+static TCHAR *empty_get_friendlyname(int) { return empty_friendly; }
+static TCHAR *empty_get_uniquename(int) { return empty_unique_name; }
+static int empty_get_widget_num(int) { return 0; }
+static int empty_get_widget_type(int, int, TCHAR *, uae_u32 *) { return IDEV_WIDGET_NONE; }
+static int empty_get_widget_first(int, int) { return -1; }
+static int empty_get_flags(int) { return 0; }
+
+static int mouse_get_num(void) { return 1; }
+static TCHAR *mouse_get_friendlyname(int) { return mouse_friendly; }
+static TCHAR *mouse_get_uniquename(int) { return mouse_unique_name; }
+static int mouse_get_widget_num(int) { return 6; }
+static int mouse_get_widget_type(int, int widget, TCHAR *name, uae_u32 *code)
+{
+    if (code) {
+        *code = widget;
+    }
+    if (widget >= 0 && widget < 3) {
+        if (name) {
+            _tcscpy(name, mouse_axis_names[widget]);
+        }
+        return IDEV_WIDGET_AXIS;
+    }
+    if (widget >= 3 && widget < 6) {
+        if (name) {
+            _tcscpy(name, mouse_button_names[widget - 3]);
+        }
+        return IDEV_WIDGET_BUTTON;
+    }
+    return IDEV_WIDGET_NONE;
+}
+static int mouse_get_widget_first(int, int type)
+{
+    if (type == IDEV_WIDGET_AXIS) {
+        return 0;
+    }
+    if (type == IDEV_WIDGET_BUTTON) {
+        return 3;
+    }
+    return -1;
+}
+static int mouse_get_flags(int) { return 0; }
+
+static int keyboard_get_num(void) { return 1; }
+static TCHAR *keyboard_get_friendlyname(int) { return keyboard_friendly; }
+static TCHAR *keyboard_get_uniquename(int) { return keyboard_unique_name; }
+static int keyboard_get_widget_num(int) { return sizeof keyboard_keycodes / sizeof keyboard_keycodes[0]; }
+static int keyboard_get_widget_type(int, int widget, TCHAR *name, uae_u32 *code)
+{
+    if (widget < 0 || widget >= keyboard_get_widget_num(0)) {
+        return IDEV_WIDGET_NONE;
+    }
+    int scancode = keyboard_keycodes[widget];
+    if (name) {
+        _sntprintf(name, 64, _T("Key %d"), scancode);
+        name[63] = 0;
+    }
+    if (code) {
+        *code = scancode;
+    }
+    return IDEV_WIDGET_KEY;
+}
+static int keyboard_get_widget_first(int, int type)
+{
+    return type == IDEV_WIDGET_KEY ? 0 : -1;
+}
+static int keyboard_get_flags(int) { return 0; }
+
+#ifdef UAE_UNIX_WITH_SDL3
+static void joystick_release_device(int index)
+{
+    if (index < 0 || index >= unix_joystick_count) {
+        return;
+    }
+    unix_joystick_device *dev = &unix_joysticks[index];
+    for (int i = 0; i < dev->axis_count && i < ID_AXIS_TOTAL; i++) {
+        if (dev->axis_state[i]) {
+            setjoystickstate(index, i, 0, 32767);
+            dev->axis_state[i] = 0;
+        }
+    }
+    for (int i = 0; i < dev->button_count && i < ID_BUTTON_TOTAL; i++) {
+        if (dev->button_state[i]) {
+            setjoybuttonstate(index, i, 0);
+            dev->button_state[i] = false;
+        }
+    }
+}
+
+static void joystick_close_devices(void)
+{
+    for (int i = 0; i < unix_joystick_count; i++) {
+        joystick_release_device(i);
+        if (unix_joysticks[i].gamepad) {
+            SDL_CloseGamepad(unix_joysticks[i].gamepad);
+        } else if (unix_joysticks[i].joystick) {
+            SDL_CloseJoystick(unix_joysticks[i].joystick);
+        }
+    }
+    memset(unix_joysticks, 0, sizeof unix_joysticks);
+    unix_joystick_count = 0;
+}
+
+static void joystick_copy_text(TCHAR *dst, int dstlen, const char *src, const TCHAR *fallback)
+{
+    if (!dst || dstlen <= 0) {
+        return;
+    }
+    if (src && src[0]) {
+        _sntprintf(dst, dstlen, _T("%s"), src);
+    } else {
+        _sntprintf(dst, dstlen, _T("%s"), fallback);
+    }
+    dst[dstlen - 1] = 0;
+}
+
+static void joystick_make_unique(TCHAR *dst, int dstlen, const TCHAR *kind, SDL_JoystickID instance_id, int ordinal)
+{
+    char guid[64];
+    SDL_GUIDToString(SDL_GetJoystickGUIDForID(instance_id), guid, sizeof guid);
+    _sntprintf(dst, dstlen, _T("unix.%s.%s.%d"), kind, guid, ordinal);
+    dst[dstlen - 1] = 0;
+}
+
+static void joystick_add_axis(unix_joystick_device *dev, int kind, int code)
+{
+    if (!dev || dev->axis_count >= ID_AXIS_TOTAL) {
+        return;
+    }
+    int axis = dev->axis_count++;
+    dev->axis_kind[axis] = kind;
+    dev->axis_code[axis] = code;
+}
+
+static void joystick_add_button(unix_joystick_device *dev, int code)
+{
+    if (!dev || dev->button_count >= ID_BUTTON_TOTAL) {
+        return;
+    }
+    dev->button_code[dev->button_count++] = code;
+}
+
+static void joystick_register_gamepad(SDL_JoystickID instance_id)
+{
+    if (unix_joystick_count >= MAX_INPUT_DEVICES) {
+        return;
+    }
+
+    SDL_Gamepad *gamepad = SDL_OpenGamepad(instance_id);
+    if (!gamepad) {
+        write_log(_T("SDL3: failed to open gamepad %d: %s\n"), (int)instance_id, SDL_GetError());
+        return;
+    }
+
+    unix_joystick_device *dev = &unix_joysticks[unix_joystick_count];
+    memset(dev, 0, sizeof *dev);
+    dev->instance_id = instance_id;
+    dev->gamepad = gamepad;
+    dev->is_gamepad = true;
+    joystick_copy_text(dev->friendly, sizeof dev->friendly / sizeof dev->friendly[0],
+        SDL_GetGamepadName(gamepad), _T("SDL Gamepad"));
+    joystick_make_unique(dev->unique, sizeof dev->unique / sizeof dev->unique[0],
+        _T("gamepad"), instance_id, unix_joystick_count);
+
+    for (int i = 0; i < (int)(sizeof unix_gamepad_axes / sizeof unix_gamepad_axes[0]); i++) {
+        if (SDL_GamepadHasAxis(gamepad, unix_gamepad_axes[i])) {
+            joystick_add_axis(dev, UNIX_AXIS_SDL, unix_gamepad_axes[i]);
+        }
+    }
+    joystick_add_axis(dev, UNIX_AXIS_GAMEPAD_DPAD_X, 0);
+    joystick_add_axis(dev, UNIX_AXIS_GAMEPAD_DPAD_Y, 0);
+
+    for (int i = 0; i < (int)(sizeof unix_gamepad_buttons / sizeof unix_gamepad_buttons[0]); i++) {
+        if (SDL_GamepadHasButton(gamepad, unix_gamepad_buttons[i])) {
+            joystick_add_button(dev, unix_gamepad_buttons[i]);
+        }
+    }
+
+    write_log(_T("SDL3: gamepad %d: '%s' (%s), %d axes, %d buttons\n"),
+        unix_joystick_count, dev->friendly, dev->unique, dev->axis_count, dev->button_count);
+    unix_joystick_count++;
+}
+
+static void joystick_register_joystick(SDL_JoystickID instance_id)
+{
+    if (unix_joystick_count >= MAX_INPUT_DEVICES || SDL_IsGamepad(instance_id)) {
+        return;
+    }
+
+    SDL_Joystick *joystick = SDL_OpenJoystick(instance_id);
+    if (!joystick) {
+        write_log(_T("SDL3: failed to open joystick %d: %s\n"), (int)instance_id, SDL_GetError());
+        return;
+    }
+
+    unix_joystick_device *dev = &unix_joysticks[unix_joystick_count];
+    memset(dev, 0, sizeof *dev);
+    dev->instance_id = instance_id;
+    dev->joystick = joystick;
+    joystick_copy_text(dev->friendly, sizeof dev->friendly / sizeof dev->friendly[0],
+        SDL_GetJoystickName(joystick), _T("SDL Joystick"));
+    joystick_make_unique(dev->unique, sizeof dev->unique / sizeof dev->unique[0],
+        _T("joystick"), instance_id, unix_joystick_count);
+
+    int axes = SDL_GetNumJoystickAxes(joystick);
+    int hats = SDL_GetNumJoystickHats(joystick);
+    int buttons = SDL_GetNumJoystickButtons(joystick);
+    if (axes < 0) {
+        axes = 0;
+    }
+    if (hats < 0) {
+        hats = 0;
+    }
+    if (buttons < 0) {
+        buttons = 0;
+    }
+    for (int i = 0; i < axes && dev->axis_count < ID_AXIS_TOTAL; i++) {
+        joystick_add_axis(dev, UNIX_AXIS_SDL, i);
+    }
+    for (int i = 0; i < hats && dev->axis_count + 1 < ID_AXIS_TOTAL; i++) {
+        joystick_add_axis(dev, UNIX_AXIS_HAT_X, i);
+        joystick_add_axis(dev, UNIX_AXIS_HAT_Y, i);
+    }
+    for (int i = 0; i < buttons && dev->button_count < ID_BUTTON_TOTAL; i++) {
+        joystick_add_button(dev, i);
+    }
+
+    write_log(_T("SDL3: joystick %d: '%s' (%s), %d axes, %d buttons\n"),
+        unix_joystick_count, dev->friendly, dev->unique, dev->axis_count, dev->button_count);
+    unix_joystick_count++;
+}
+
+static void joystick_open_devices(void)
+{
+    joystick_close_devices();
+
+    int count = 0;
+    SDL_JoystickID *ids = SDL_GetGamepads(&count);
+    if (ids) {
+        for (int i = 0; i < count; i++) {
+            joystick_register_gamepad(ids[i]);
+        }
+        SDL_free(ids);
+    }
+
+    count = 0;
+    ids = SDL_GetJoysticks(&count);
+    if (ids) {
+        for (int i = 0; i < count; i++) {
+            joystick_register_joystick(ids[i]);
+        }
+        SDL_free(ids);
+    }
+}
+
+static int joystick_init(void)
+{
+    input_init();
+    if (unix_joystick_sdl_initialized) {
+        return 1;
+    }
+
+    SDL_SetMainReady();
+    if (!SDL_InitSubSystem(SDL_INIT_EVENTS | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) {
+        write_log(_T("SDL3: joystick/gamepad unavailable: %s\n"), SDL_GetError());
+        return 0;
+    }
+    SDL_SetJoystickEventsEnabled(true);
+    SDL_SetGamepadEventsEnabled(true);
+    unix_joystick_sdl_initialized = true;
+    joystick_open_devices();
+    return 1;
+}
+
+static void joystick_close(void)
+{
+    if (!unix_joystick_sdl_initialized) {
+        return;
+    }
+    joystick_close_devices();
+    SDL_QuitSubSystem(SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK);
+    unix_joystick_sdl_initialized = false;
+}
+
+static int joystick_acquire(int num, int)
+{
+    return num < 0 || num < unix_joystick_count;
+}
+
+static void joystick_unacquire(int num)
+{
+    if (num >= 0 && num < unix_joystick_count) {
+        joystick_release_device(num);
+    }
+}
+
+static void joystick_set_axis(unix_joystick_device *dev, int joy, int axis, int value, int max)
+{
+    if (!dev || axis < 0 || axis >= dev->axis_count || axis >= ID_AXIS_TOTAL) {
+        return;
+    }
+    if (dev->axis_state[axis] == value) {
+        return;
+    }
+    dev->axis_state[axis] = value;
+    setjoystickstate(joy, axis, value, max);
+}
+
+static void joystick_set_button(unix_joystick_device *dev, int joy, int button, bool down)
+{
+    if (!dev || button < 0 || button >= dev->button_count || button >= ID_BUTTON_TOTAL) {
+        return;
+    }
+    if (dev->button_state[button] == down) {
+        return;
+    }
+    dev->button_state[button] = down;
+    setjoybuttonstate(joy, button, down ? 1 : 0);
+}
+
+static void joystick_read_gamepad(unix_joystick_device *dev, int joy)
+{
+    int dpad_x = 0;
+    int dpad_y = 0;
+
+    for (int axis = 0; axis < dev->axis_count; axis++) {
+        switch (dev->axis_kind[axis]) {
+        case UNIX_AXIS_SDL:
+            joystick_set_axis(dev, joy, axis, SDL_GetGamepadAxis(dev->gamepad, (SDL_GamepadAxis)dev->axis_code[axis]), 32767);
+            break;
+        case UNIX_AXIS_GAMEPAD_DPAD_X:
+            dpad_x = SDL_GetGamepadButton(dev->gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT) ? 1 : 0;
+            dpad_x -= SDL_GetGamepadButton(dev->gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT) ? 1 : 0;
+            joystick_set_axis(dev, joy, axis, dpad_x, 1);
+            break;
+        case UNIX_AXIS_GAMEPAD_DPAD_Y:
+            dpad_y = SDL_GetGamepadButton(dev->gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN) ? 1 : 0;
+            dpad_y -= SDL_GetGamepadButton(dev->gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP) ? 1 : 0;
+            joystick_set_axis(dev, joy, axis, dpad_y, 1);
+            break;
+        }
+    }
+
+    for (int button = 0; button < dev->button_count; button++) {
+        joystick_set_button(dev, joy, button,
+            SDL_GetGamepadButton(dev->gamepad, (SDL_GamepadButton)dev->button_code[button]));
+    }
+}
+
+static void joystick_read_joystick(unix_joystick_device *dev, int joy)
+{
+    for (int axis = 0; axis < dev->axis_count; axis++) {
+        switch (dev->axis_kind[axis]) {
+        case UNIX_AXIS_SDL:
+            joystick_set_axis(dev, joy, axis, SDL_GetJoystickAxis(dev->joystick, dev->axis_code[axis]), 32767);
+            break;
+        case UNIX_AXIS_HAT_X:
+        {
+            Uint8 hat = SDL_GetJoystickHat(dev->joystick, dev->axis_code[axis]);
+            int value = (hat & SDL_HAT_RIGHT) ? 1 : 0;
+            value -= (hat & SDL_HAT_LEFT) ? 1 : 0;
+            joystick_set_axis(dev, joy, axis, value, 1);
+            break;
+        }
+        case UNIX_AXIS_HAT_Y:
+        {
+            Uint8 hat = SDL_GetJoystickHat(dev->joystick, dev->axis_code[axis]);
+            int value = (hat & SDL_HAT_DOWN) ? 1 : 0;
+            value -= (hat & SDL_HAT_UP) ? 1 : 0;
+            joystick_set_axis(dev, joy, axis, value, 1);
+            break;
+        }
+        }
+    }
+
+    for (int button = 0; button < dev->button_count; button++) {
+        joystick_set_button(dev, joy, button, SDL_GetJoystickButton(dev->joystick, dev->button_code[button]));
+    }
+}
+
+static void joystick_read(void)
+{
+    if (!unix_joystick_sdl_initialized) {
+        return;
+    }
+    SDL_UpdateGamepads();
+    SDL_UpdateJoysticks();
+    for (int i = 0; i < unix_joystick_count; i++) {
+        unix_joystick_device *dev = &unix_joysticks[i];
+        if (dev->gamepad) {
+            joystick_read_gamepad(dev, i);
+        } else if (dev->joystick) {
+            joystick_read_joystick(dev, i);
+        }
+    }
+}
+
+static int joystick_get_num(void)
+{
+    return unix_joystick_count;
+}
+
+static TCHAR *joystick_get_friendlyname(int joy)
+{
+    return joy >= 0 && joy < unix_joystick_count ? unix_joysticks[joy].friendly : empty_friendly;
+}
+
+static TCHAR *joystick_get_uniquename(int joy)
+{
+    return joy >= 0 && joy < unix_joystick_count ? unix_joysticks[joy].unique : empty_unique_name;
+}
+
+static int joystick_get_widget_num(int joy)
+{
+    if (joy < 0 || joy >= unix_joystick_count) {
+        return 0;
+    }
+    return unix_joysticks[joy].axis_count + unix_joysticks[joy].button_count;
+}
+
+static void joystick_axis_name(unix_joystick_device *dev, int axis, TCHAR *name)
+{
+    if (!name) {
+        return;
+    }
+    switch (dev->axis_kind[axis]) {
+    case UNIX_AXIS_SDL:
+        if (dev->is_gamepad) {
+            for (int i = 0; i < (int)(sizeof unix_gamepad_axes / sizeof unix_gamepad_axes[0]); i++) {
+                if (dev->axis_code[axis] == unix_gamepad_axes[i]) {
+                    _tcscpy(name, unix_gamepad_axis_names[i]);
+                    return;
+                }
+            }
+        }
+        _sntprintf(name, 64, _T("Axis %d"), dev->axis_code[axis] + 1);
+        name[63] = 0;
+        return;
+    case UNIX_AXIS_GAMEPAD_DPAD_X:
+        _tcscpy(name, _T("DPad X Axis"));
+        return;
+    case UNIX_AXIS_GAMEPAD_DPAD_Y:
+        _tcscpy(name, _T("DPad Y Axis"));
+        return;
+    case UNIX_AXIS_HAT_X:
+        _sntprintf(name, 64, _T("Hat %d X Axis"), dev->axis_code[axis] + 1);
+        name[63] = 0;
+        return;
+    case UNIX_AXIS_HAT_Y:
+        _sntprintf(name, 64, _T("Hat %d Y Axis"), dev->axis_code[axis] + 1);
+        name[63] = 0;
+        return;
+    }
+    _tcscpy(name, _T("Axis"));
+}
+
+static void joystick_button_name(unix_joystick_device *dev, int button, TCHAR *name)
+{
+    if (!name) {
+        return;
+    }
+    if (dev->is_gamepad) {
+        for (int i = 0; i < (int)(sizeof unix_gamepad_buttons / sizeof unix_gamepad_buttons[0]); i++) {
+            if (dev->button_code[button] == unix_gamepad_buttons[i]) {
+                _tcscpy(name, unix_gamepad_button_names[i]);
+                return;
+            }
+        }
+    }
+    _sntprintf(name, 64, _T("Button %d"), dev->button_code[button] + 1);
+    name[63] = 0;
+}
+
+static int joystick_get_widget_type(int joy, int widget, TCHAR *name, uae_u32 *code)
+{
+    if (joy < 0 || joy >= unix_joystick_count) {
+        return IDEV_WIDGET_NONE;
+    }
+    unix_joystick_device *dev = &unix_joysticks[joy];
+    if (code) {
+        *code = widget;
+    }
+    if (widget >= 0 && widget < dev->axis_count) {
+        joystick_axis_name(dev, widget, name);
+        return IDEV_WIDGET_AXIS;
+    }
+    int button = widget - dev->axis_count;
+    if (button >= 0 && button < dev->button_count) {
+        joystick_button_name(dev, button, name);
+        return IDEV_WIDGET_BUTTON;
+    }
+    return IDEV_WIDGET_NONE;
+}
+
+static int joystick_get_widget_first(int joy, int type)
+{
+    if (joy < 0 || joy >= unix_joystick_count) {
+        return -1;
+    }
+    switch (type) {
+    case IDEV_WIDGET_AXIS:
+        return unix_joysticks[joy].axis_count > 0 ? 0 : -1;
+    case IDEV_WIDGET_BUTTON:
+        return unix_joysticks[joy].button_count > 0 ? unix_joysticks[joy].axis_count : -1;
+    }
+    return -1;
+}
+
+static int joystick_get_flags(int) { return 0; }
+
+static bool joystick_has_button(int joy, int button)
+{
+    return joy >= 0 && joy < unix_joystick_count && button >= 0 && button < unix_joysticks[joy].button_count;
+}
+
+static bool joystick_axis_is_dpad_or_hat(int joy, int axis)
+{
+    if (joy < 0 || joy >= unix_joystick_count || axis < 0 || axis >= unix_joysticks[joy].axis_count) {
+        return false;
+    }
+    int kind = unix_joysticks[joy].axis_kind[axis];
+    return kind == UNIX_AXIS_GAMEPAD_DPAD_X || kind == UNIX_AXIS_GAMEPAD_DPAD_Y ||
+        kind == UNIX_AXIS_HAT_X || kind == UNIX_AXIS_HAT_Y;
+}
+
+void unix_input_joystick_device_changed(void)
+{
+    if (unix_joystick_sdl_initialized) {
+        joystick_open_devices();
+    }
+}
+#else
+static int joystick_init(void) { return input_init(); }
+static void joystick_close(void) {}
+static int joystick_acquire(int, int) { return 1; }
+static void joystick_unacquire(int) {}
+static void joystick_read(void) {}
+static int joystick_get_num(void) { return 0; }
+static TCHAR *joystick_get_friendlyname(int) { return empty_friendly; }
+static TCHAR *joystick_get_uniquename(int) { return empty_unique_name; }
+static int joystick_get_widget_num(int) { return 0; }
+static int joystick_get_widget_type(int, int, TCHAR *, uae_u32 *) { return IDEV_WIDGET_NONE; }
+static int joystick_get_widget_first(int, int) { return -1; }
+static int joystick_get_flags(int) { return 0; }
+static bool joystick_has_button(int, int) { return false; }
+static bool joystick_axis_is_dpad_or_hat(int, int) { return false; }
+void unix_input_joystick_device_changed(void) {}
+#endif
+
+inputdevice_functions inputdevicefunc_joystick = {
+    joystick_init, joystick_close, joystick_acquire, joystick_unacquire, joystick_read,
+    joystick_get_num, joystick_get_friendlyname, joystick_get_uniquename,
+    joystick_get_widget_num, joystick_get_widget_type, joystick_get_widget_first,
+    joystick_get_flags
+};
+
+inputdevice_functions inputdevicefunc_mouse = {
+    input_init, input_close, input_acquire, input_unacquire, input_read,
+    mouse_get_num, mouse_get_friendlyname, mouse_get_uniquename,
+    mouse_get_widget_num, mouse_get_widget_type, mouse_get_widget_first,
+    mouse_get_flags
+};
+
+inputdevice_functions inputdevicefunc_keyboard = {
+    input_init, input_close, input_acquire, input_unacquire, input_read,
+    keyboard_get_num, keyboard_get_friendlyname, keyboard_get_uniquename,
+    keyboard_get_widget_num, keyboard_get_widget_type, keyboard_get_widget_first,
+    keyboard_get_flags
+};
+
+static int nextsub(struct uae_input_device *uid, int dev, int slot, int sub)
+{
+    if (currprefs.input_advancedmultiinput) {
+        while (uid[dev].eventid[slot][sub] > 0) {
+            sub++;
+            if (sub >= MAX_INPUT_SUB_EVENT) {
+                return -1;
+            }
+        }
+    }
+    return sub;
+}
+
+static void setid(struct uae_input_device *uid, int dev, int slot, int sub, int port, int evt, bool gp)
+{
+    sub = nextsub(uid, dev, slot, sub);
+    if (sub < 0 || evt <= 0) {
+        return;
+    }
+    if (gp && sub == 0) {
+        inputdevice_sparecopy(&uid[dev], slot, sub);
+    }
+    uid[dev].eventid[slot][sub] = evt;
+    uid[dev].port[slot][sub] = port + 1;
+}
+
+static void setid(struct uae_input_device *uid, int dev, int slot, int sub, int port, int evt, int af, bool gp)
+{
+    sub = nextsub(uid, dev, slot, sub);
+    if (sub < 0) {
+        return;
+    }
+    setid(uid, dev, slot, sub, port, evt, gp);
+    uid[dev].flags[slot][sub] &= ~ID_FLAG_AUTOFIRE_MASK;
+    if (af >= JPORT_AF_NORMAL) {
+        uid[dev].flags[slot][sub] |= ID_FLAG_AUTOFIRE;
+    }
+    if (af == JPORT_AF_TOGGLE) {
+        uid[dev].flags[slot][sub] |= ID_FLAG_TOGGLE;
+    }
+    if (af == JPORT_AF_ALWAYS) {
+        uid[dev].flags[slot][sub] |= ID_FLAG_INVERTTOGGLE;
+    }
+    if (af == JPORT_AF_TOGGLENOAF) {
+        uid[dev].flags[slot][sub] |= ID_FLAG_INVERT;
+    }
+}
+
+void unix_input_mouse_motion(int dx, int dy)
+{
+    if (dx) {
+        setmousestate(0, 0, dx, 0);
+    }
+    if (dy) {
+        setmousestate(0, 1, dy, 0);
+    }
+}
+
+void unix_input_mouse_button(int button, bool pressed)
+{
+    if (button >= 0 && button < 3) {
+        setmousebuttonstate(0, button, pressed ? 1 : 0);
+    }
+}
+
+void unix_input_mouse_wheel(int, int y)
+{
+    if (y) {
+        setmousestate(0, 2, y * 120, 0);
+    }
+}
+
+void unix_input_set_mouse_active(bool active)
+{
+    mouse_active = active;
+}
+
+bool unix_input_get_mouse_active(void)
+{
+    return mouse_active;
+}
+
+static void unix_input_update_lock_state(int lockstate)
+{
+    host_capslockstate = (lockstate & UNIX_INPUT_LOCK_CAPS) != 0;
+    host_numlockstate = (lockstate & UNIX_INPUT_LOCK_NUM) != 0;
+    host_scrolllockstate = (lockstate & UNIX_INPUT_LOCK_SCROLL) != 0;
+}
+
+void unix_input_keyboard_key(int scancode, bool pressed, int lockstate)
+{
+    if (scancode <= 0 || scancode >= (int)(sizeof keyboard_state / sizeof keyboard_state[0])) {
+        return;
+    }
+    unix_input_update_lock_state(lockstate);
+    if (keyboard_state[scancode] == pressed) {
+        return;
+    }
+
+    keyboard_state[scancode] = pressed;
+    inputdevice_translatekeycode(0, scancode, pressed ? 1 : 0, false);
+}
+
+void unix_input_release_keys(void)
+{
+    for (int scancode = 0; scancode < (int)(sizeof keyboard_state / sizeof keyboard_state[0]); scancode++) {
+        if (keyboard_state[scancode]) {
+            keyboard_state[scancode] = false;
+            inputdevice_translatekeycode(0, scancode, 0, true);
+        }
+    }
+    setmousebuttonstateall(0, 0, 7);
+}
+
+void release_keys(void) { unix_input_release_keys(); }
+int input_get_default_keyboard(int num)
+{
+    if (num < 0) {
+        return 0;
+    }
+    return num == 0 ? 1 : 0;
+}
+int input_get_default_mouse(uae_input_device *uid, int dev, int port, int af, bool gp, bool wheel, bool joymouseswap)
+{
+    if (joymouseswap || dev != 0) {
+        return 0;
+    }
+
+    setid(uid, dev, ID_AXIS_OFFSET + 0, 0, port, port ? INPUTEVENT_MOUSE2_HORIZ : INPUTEVENT_MOUSE1_HORIZ, gp);
+    setid(uid, dev, ID_AXIS_OFFSET + 1, 0, port, port ? INPUTEVENT_MOUSE2_VERT : INPUTEVENT_MOUSE1_VERT, gp);
+    if (wheel && port == 0) {
+        setid(uid, dev, ID_AXIS_OFFSET + 2, 0, port, INPUTEVENT_MOUSE1_WHEEL, gp);
+    }
+    setid(uid, dev, ID_BUTTON_OFFSET + 0, 0, port, port ? INPUTEVENT_JOY2_FIRE_BUTTON : INPUTEVENT_JOY1_FIRE_BUTTON, af, gp);
+    setid(uid, dev, ID_BUTTON_OFFSET + 1, 0, port, port ? INPUTEVENT_JOY2_2ND_BUTTON : INPUTEVENT_JOY1_2ND_BUTTON, gp);
+    setid(uid, dev, ID_BUTTON_OFFSET + 2, 0, port, port ? INPUTEVENT_JOY2_3RD_BUTTON : INPUTEVENT_JOY1_3RD_BUTTON, gp);
+
+    return 1;
+}
+int input_get_default_lightpen(uae_input_device *, int, int, int, bool, bool, int) { return 0; }
+int input_get_default_joystick(uae_input_device *uid, int dev, int port, int af, int mode, bool gp, bool joymouseswap, bool default_osk)
+{
+    if (joymouseswap || dev < 0 || dev >= joystick_get_num()) {
+        return 0;
+    }
+
+    int h;
+    int v;
+    if (mode == JSEM_MODE_MOUSE_CDTV) {
+        h = INPUTEVENT_MOUSE_CDTV_HORIZ;
+        v = INPUTEVENT_MOUSE_CDTV_VERT;
+    } else if (port >= 2) {
+        h = port == 3 ? INPUTEVENT_PAR_JOY2_HORIZ : INPUTEVENT_PAR_JOY1_HORIZ;
+        v = port == 3 ? INPUTEVENT_PAR_JOY2_VERT : INPUTEVENT_PAR_JOY1_VERT;
+    } else {
+        h = port ? INPUTEVENT_JOY2_HORIZ : INPUTEVENT_JOY1_HORIZ;
+        v = port ? INPUTEVENT_JOY2_VERT : INPUTEVENT_JOY1_VERT;
+    }
+
+    setid(uid, dev, ID_AXIS_OFFSET + 0, 0, port, h, gp);
+    setid(uid, dev, ID_AXIS_OFFSET + 1, 0, port, v, gp);
+    int first_button = joystick_get_widget_first(dev, IDEV_WIDGET_BUTTON);
+    if (first_button < 0) {
+        first_button = joystick_get_widget_num(dev);
+    }
+    for (int axis = 2; axis < first_button; axis++) {
+        if (!joystick_axis_is_dpad_or_hat(dev, axis) || axis + 1 >= first_button) {
+            continue;
+        }
+        if (joystick_axis_is_dpad_or_hat(dev, axis + 1)) {
+            setid(uid, dev, ID_AXIS_OFFSET + axis, 0, port, h, gp);
+            setid(uid, dev, ID_AXIS_OFFSET + axis + 1, 0, port, v, gp);
+            axis++;
+        }
+    }
+
+    if (port >= 2) {
+        setid(uid, dev, ID_BUTTON_OFFSET + 0, 0, port,
+            port == 3 ? INPUTEVENT_PAR_JOY2_FIRE_BUTTON : INPUTEVENT_PAR_JOY1_FIRE_BUTTON, af, gp);
+    } else {
+        setid(uid, dev, ID_BUTTON_OFFSET + 0, 0, port,
+            port ? INPUTEVENT_JOY2_FIRE_BUTTON : INPUTEVENT_JOY1_FIRE_BUTTON, af, gp);
+        if (joystick_has_button(dev, 1)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 1, 0, port,
+                port ? INPUTEVENT_JOY2_2ND_BUTTON : INPUTEVENT_JOY1_2ND_BUTTON, gp);
+        }
+        if (mode != JSEM_MODE_JOYSTICK && joystick_has_button(dev, 2)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 2, 0, port,
+                port ? INPUTEVENT_JOY2_3RD_BUTTON : INPUTEVENT_JOY1_3RD_BUTTON, gp);
+        }
+        if (default_osk && joystick_has_button(dev, 3)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 3, 0, port, INPUTEVENT_SPC_OSK, gp);
+        }
+    }
+
+    if (mode == JSEM_MODE_JOYSTICK_CD32) {
+        setid(uid, dev, ID_BUTTON_OFFSET + 0, 0, port,
+            port ? INPUTEVENT_JOY2_CD32_RED : INPUTEVENT_JOY1_CD32_RED, af, gp);
+        if (joystick_has_button(dev, 1)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 1, 0, port,
+                port ? INPUTEVENT_JOY2_CD32_BLUE : INPUTEVENT_JOY1_CD32_BLUE, gp);
+        }
+        if (joystick_has_button(dev, 2)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 2, 0, port,
+                port ? INPUTEVENT_JOY2_CD32_GREEN : INPUTEVENT_JOY1_CD32_GREEN, gp);
+        }
+        if (joystick_has_button(dev, 3)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 3, 0, port,
+                port ? INPUTEVENT_JOY2_CD32_YELLOW : INPUTEVENT_JOY1_CD32_YELLOW, gp);
+        }
+        if (joystick_has_button(dev, 4)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 4, 0, port,
+                port ? INPUTEVENT_JOY2_CD32_RWD : INPUTEVENT_JOY1_CD32_RWD, gp);
+        }
+        if (joystick_has_button(dev, 5)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 5, 0, port,
+                port ? INPUTEVENT_JOY2_CD32_FFW : INPUTEVENT_JOY1_CD32_FFW, gp);
+        }
+        if (joystick_has_button(dev, 6)) {
+            setid(uid, dev, ID_BUTTON_OFFSET + 6, 0, port,
+                port ? INPUTEVENT_JOY2_CD32_PLAY : INPUTEVENT_JOY1_CD32_PLAY, gp);
+        }
+    }
+
+    return dev == 0 ? 1 : 0;
+}
+
+int input_get_default_joystick_analog(uae_input_device *uid, int dev, int port, int af, bool gp, bool joymouseswap, bool default_osk)
+{
+    if (joymouseswap || dev < 0 || dev >= joystick_get_num()) {
+        return 0;
+    }
+
+    setid(uid, dev, ID_AXIS_OFFSET + 0, 0, port, port ? INPUTEVENT_JOY2_HORIZ_POT : INPUTEVENT_JOY1_HORIZ_POT, gp);
+    setid(uid, dev, ID_AXIS_OFFSET + 1, 0, port, port ? INPUTEVENT_JOY2_VERT_POT : INPUTEVENT_JOY1_VERT_POT, gp);
+    setid(uid, dev, ID_BUTTON_OFFSET + 0, 0, port, port ? INPUTEVENT_JOY2_LEFT : INPUTEVENT_JOY1_LEFT, af, gp);
+    if (joystick_has_button(dev, 1)) {
+        setid(uid, dev, ID_BUTTON_OFFSET + 1, 0, port, port ? INPUTEVENT_JOY2_RIGHT : INPUTEVENT_JOY1_RIGHT, gp);
+    }
+    if (joystick_has_button(dev, 2)) {
+        setid(uid, dev, ID_BUTTON_OFFSET + 2, 0, port, port ? INPUTEVENT_JOY2_UP : INPUTEVENT_JOY1_UP, gp);
+    }
+    if (joystick_has_button(dev, 3)) {
+        setid(uid, dev, ID_BUTTON_OFFSET + 3, 0, port, port ? INPUTEVENT_JOY2_DOWN : INPUTEVENT_JOY1_DOWN, gp);
+    }
+    if (default_osk && joystick_has_button(dev, 4)) {
+        setid(uid, dev, ID_BUTTON_OFFSET + 4, 0, port, INPUTEVENT_SPC_OSK, gp);
+    }
+
+    return dev == 0 ? 1 : 0;
+}
+int is_tablet(void) { return 0; }
+bool ismouseactive(void) { return unix_input_get_mouse_active(); }
+void setmouseactive(int, int active) { unix_input_set_mouse_active(active != 0); }
+bool target_can_autoswitchdevice(void) { return false; }
+void target_inputdevice_acquire(void) {}
+void target_inputdevice_unacquire(bool) {}
+int getcapslockstate(void) { return capslockstate; }
+void setcapslockstate(int state) { capslockstate = state; }
+int target_checkcapslock(int scancode, int *state)
+{
+    if (scancode != UKEY_CAPSLOCK && scancode != UKEY_NUMLOCKCLEAR && scancode != UKEY_SCROLLLOCK) {
+        return 0;
+    }
+    if (currprefs.keyboard_mode > 0) {
+        return 1;
+    }
+    if (*state == 0) {
+        return -1;
+    }
+    if (scancode == UKEY_CAPSLOCK) {
+        *state = host_capslockstate;
+        if (gui_data.capslock != (host_capslockstate != 0)) {
+            gui_data.capslock = host_capslockstate != 0;
+            gui_led(LED_CAPS, gui_data.capslock, -1);
+        }
+    } else if (scancode == UKEY_NUMLOCKCLEAR) {
+        *state = host_numlockstate;
+    } else if (scancode == UKEY_SCROLLLOCK) {
+        *state = host_scrolllockstate;
+    }
+    return 1;
+}
diff --git a/od-unix/input.h b/od-unix/input.h
new file mode 100644 (file)
index 0000000..89704c0
--- /dev/null
@@ -0,0 +1,18 @@
+#ifndef WINUAE_OD_UNIX_INPUT_H
+#define WINUAE_OD_UNIX_INPUT_H
+
+void unix_input_mouse_motion(int dx, int dy);
+void unix_input_mouse_button(int button, bool pressed);
+void unix_input_mouse_wheel(int x, int y);
+void unix_input_set_mouse_active(bool active);
+bool unix_input_get_mouse_active(void);
+enum {
+    UNIX_INPUT_LOCK_CAPS = 1 << 0,
+    UNIX_INPUT_LOCK_NUM = 1 << 1,
+    UNIX_INPUT_LOCK_SCROLL = 1 << 2
+};
+void unix_input_keyboard_key(int scancode, bool pressed, int lockstate);
+void unix_input_release_keys(void);
+void unix_input_joystick_device_changed(void);
+
+#endif /* WINUAE_OD_UNIX_INPUT_H */
diff --git a/od-unix/sampler_sdl.cpp b/od-unix/sampler_sdl.cpp
new file mode 100644 (file)
index 0000000..9b53100
--- /dev/null
@@ -0,0 +1,244 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#ifdef WINUAE_UNIX_WITH_SAMPLER
+
+#define SDL_MAIN_HANDLED
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+#include "options.h"
+#include "events.h"
+#include "sampler.h"
+#include "sound_unix.h"
+#include "uae.h"
+
+#define UNIX_SAMPLER_MAX_DEVICES 100
+#define UNIX_SAMPLER_FRAME_BYTES 4
+
+float sampler_evtime;
+
+struct unix_sampler_device {
+       SDL_AudioDeviceID id;
+       TCHAR name[256];
+       TCHAR config_name[320];
+};
+
+static unix_sampler_device sampler_devices[UNIX_SAMPLER_MAX_DEVICES];
+static int sampler_device_count;
+static bool sampler_devices_enumerated;
+static bool sampler_audio_initialized;
+static SDL_AudioStream *sampler_stream;
+static SDL_AudioSpec sampler_spec;
+static int sampler_inited;
+static uae_s16 sampler_last[2];
+
+static bool ensure_sampler_audio(void)
+{
+       if (sampler_audio_initialized) {
+               return true;
+       }
+       SDL_SetMainReady();
+       if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
+               write_log(_T("SDL3: sampler audio unavailable: %s\n"), SDL_GetError());
+               return false;
+       }
+       sampler_audio_initialized = true;
+       return true;
+}
+
+static void reset_sampler_devices(void)
+{
+       memset(sampler_devices, 0, sizeof sampler_devices);
+       sampler_device_count = 0;
+       sampler_devices_enumerated = false;
+}
+
+static void add_sampler_device(SDL_AudioDeviceID id, const char *name)
+{
+       if (sampler_device_count >= UNIX_SAMPLER_MAX_DEVICES) {
+               return;
+       }
+       unix_sampler_device *device = &sampler_devices[sampler_device_count++];
+       device->id = id;
+       const char *display_name = name && name[0] ? name : "Default Recording Device";
+       _tcsncpy(device->name, display_name, sizeof device->name / sizeof(TCHAR) - 1);
+       device->name[sizeof device->name / sizeof(TCHAR) - 1] = 0;
+       _sntprintf(device->config_name, sizeof device->config_name / sizeof(TCHAR),
+               _T("SDL:%s"), device->name);
+}
+
+static void enumerate_sdl_sampler_devices(void)
+{
+       int count = 0;
+       SDL_AudioDeviceID *devices;
+
+       if (sampler_devices_enumerated) {
+               return;
+       }
+       reset_sampler_devices();
+       add_sampler_device(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, "Default Recording Device");
+       devices = SDL_GetAudioRecordingDevices(&count);
+       if (devices) {
+               for (int i = 0; i < count; i++) {
+                       const char *name = SDL_GetAudioDeviceName(devices[i]);
+                       add_sampler_device(devices[i], name);
+               }
+               SDL_free(devices);
+       }
+       sampler_devices_enumerated = true;
+}
+
+static int enumerate_sampler_devices(void)
+{
+       if (!ensure_sampler_audio()) {
+               return 0;
+       }
+       enumerate_sdl_sampler_devices();
+       return sampler_device_count;
+}
+
+int unix_sampler_device_count(void)
+{
+       return enumerate_sampler_devices();
+}
+
+const TCHAR *unix_sampler_device_name(int index)
+{
+       if (index < 0 || index >= enumerate_sampler_devices()) {
+               return _T("");
+       }
+       return sampler_devices[index].name;
+}
+
+const TCHAR *unix_sampler_device_config_name(int index)
+{
+       if (index < 0 || index >= enumerate_sampler_devices()) {
+               return _T("");
+       }
+       return sampler_devices[index].config_name;
+}
+
+int unix_sampler_device_index_from_config_name(const TCHAR *name)
+{
+       TCHAR prefixed[320];
+
+       if (!name || !name[0]) {
+               return -1;
+       }
+       enumerate_sampler_devices();
+       for (int i = 0; i < sampler_device_count; i++) {
+               if (!_tcsicmp(sampler_devices[i].config_name, name) || !_tcsicmp(sampler_devices[i].name, name)) {
+                       return i;
+               }
+       }
+       if (_tcsncmp(name, _T("SDL:"), 4) != 0) {
+               _sntprintf(prefixed, sizeof prefixed / sizeof(TCHAR), _T("SDL:%s"), name);
+               for (int i = 0; i < sampler_device_count; i++) {
+                       if (!_tcsicmp(sampler_devices[i].config_name, prefixed)) {
+                               return i;
+                       }
+               }
+       }
+       return -1;
+}
+
+static bool open_sampler_stream(void)
+{
+       int device_index;
+       SDL_AudioDeviceID device_id;
+
+       if (!ensure_sampler_audio()) {
+               return false;
+       }
+       if (enumerate_sampler_devices() <= 0) {
+               write_log(_T("SDL3: no recording audio devices available\n"));
+               return false;
+       }
+       device_index = currprefs.win32_samplersoundcard;
+       if (device_index < 0 || device_index >= sampler_device_count) {
+               write_log(_T("SDL3: sampler input device is not selected\n"));
+               return false;
+       }
+       device_id = sampler_devices[device_index].id;
+
+       memset(&sampler_spec, 0, sizeof sampler_spec);
+       sampler_spec.freq = currprefs.sampler_freq > 0 ? currprefs.sampler_freq : 44100;
+       sampler_spec.format = SDL_AUDIO_S16;
+       sampler_spec.channels = 2;
+       sampler_stream = SDL_OpenAudioDeviceStream(device_id, &sampler_spec, NULL, NULL);
+       if (!sampler_stream) {
+               write_log(_T("SDL3: failed to open sampler device '%s': %s\n"),
+                       sampler_devices[device_index].config_name, SDL_GetError());
+               return false;
+       }
+       SDL_ResumeAudioStreamDevice(sampler_stream);
+       write_log(_T("SDL3: sampler input initialized, '%s'\n"), sampler_devices[device_index].config_name);
+       return true;
+}
+
+int sampler_init(void)
+{
+       return currprefs.win32_samplersoundcard >= 0 && enumerate_sampler_devices() > 0;
+}
+
+void sampler_free(void)
+{
+       if (sampler_stream) {
+               SDL_DestroyAudioStream(sampler_stream);
+               sampler_stream = NULL;
+       }
+       sampler_inited = 0;
+       sampler_last[0] = 0;
+       sampler_last[1] = 0;
+}
+
+static void update_sampler_frame(void)
+{
+       uae_s16 frame[2];
+       int available;
+
+       if (!sampler_inited) {
+               if (!open_sampler_stream()) {
+                       sampler_free();
+                       return;
+               }
+               sampler_inited = 1;
+       }
+       if (!sampler_stream) {
+               return;
+       }
+       available = SDL_GetAudioStreamAvailable(sampler_stream);
+       while (available >= UNIX_SAMPLER_FRAME_BYTES) {
+               if (SDL_GetAudioStreamData(sampler_stream, frame, sizeof frame) != sizeof frame) {
+                       break;
+               }
+               sampler_last[0] = frame[0];
+               sampler_last[1] = frame[1];
+               available -= UNIX_SAMPLER_FRAME_BYTES;
+       }
+}
+
+uae_u8 sampler_getsample(int channel)
+{
+       int sample;
+
+       if (!currprefs.sampler_stereo) {
+               channel = 0;
+       }
+       channel = channel ? 1 : 0;
+       update_sampler_frame();
+       sample = sampler_last[channel] / 128;
+       if (sample < -128) {
+               sample = 0;
+       } else if (sample > 127) {
+               sample = 127;
+       }
+       return (uae_u8)(sample - 128);
+}
+
+void sampler_vsync(void)
+{
+}
+
+#endif
diff --git a/od-unix/sound.cpp b/od-unix/sound.cpp
new file mode 100644 (file)
index 0000000..ce82090
--- /dev/null
@@ -0,0 +1,570 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#include "options.h"
+#include "audio.h"
+#ifdef AVIOUTPUT
+#include "avioutput.h"
+#endif
+#include "custom.h"
+#include "events.h"
+#include "gui.h"
+#include "sounddep/sound.h"
+#include "gensound.h"
+#include "sound_unix.h"
+#ifdef DRIVESOUND
+#include "driveclick.h"
+#endif
+
+#ifdef UAE_UNIX_WITH_SDL3
+#define SDL_MAIN_HANDLED
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#endif
+
+#define UNIX_SOUND_MAX_BUFFER_BYTES (65536 * 2 + 1024)
+#define UNIX_SOUND_MIN_FRAMES 128
+#define UNIX_SOUND_QUEUE_BUFFERS 4
+#define UNIX_SOUND_MAX_DEVICES 100
+
+int active_sound_stereo;
+uae_u16 paula_sndbuffer[UNIX_SOUND_MAX_BUFFER_BYTES / sizeof(uae_u16)];
+uae_u16 *paula_sndbufpt = paula_sndbuffer;
+int paula_sndbufsize = DEFAULT_SOUND_MAXB;
+
+static int have_sound;
+static int sound_muted;
+static int sound_softvolume = -1;
+static float sound_sync_multiplier = 1.0f;
+static float scaled_sample_evtime_orig;
+
+extern float sampler_evtime;
+
+#ifdef UAE_UNIX_WITH_SDL3
+static SDL_AudioStream *audio_stream;
+static SDL_AudioSpec audio_spec;
+static bool audio_subsystem_initialized;
+#endif
+
+struct unix_sound_device {
+#ifdef UAE_UNIX_WITH_SDL3
+    SDL_AudioDeviceID id;
+#endif
+    TCHAR name[256];
+    TCHAR config_name[320];
+};
+
+static unix_sound_device sound_devices[UNIX_SOUND_MAX_DEVICES];
+static int sound_device_count;
+static bool sound_devices_enumerated;
+
+static void reset_sound_devices(void)
+{
+    memset(sound_devices, 0, sizeof sound_devices);
+    sound_device_count = 0;
+    sound_devices_enumerated = false;
+}
+
+#ifdef UAE_UNIX_WITH_SDL3
+static void add_sound_device(SDL_AudioDeviceID id, const char *name, const TCHAR *config_prefix)
+{
+    if (sound_device_count >= UNIX_SOUND_MAX_DEVICES) {
+        return;
+    }
+
+    unix_sound_device *device = &sound_devices[sound_device_count++];
+    device->id = id;
+    const char *display_name = name && name[0] ? name : "Default Audio Device";
+    _tcsncpy(device->name, display_name, sizeof device->name / sizeof(TCHAR) - 1);
+    device->name[sizeof device->name / sizeof(TCHAR) - 1] = 0;
+    _sntprintf(device->config_name, sizeof device->config_name / sizeof(TCHAR),
+        _T("%s%s"), config_prefix, device->name);
+}
+
+static void enumerate_sdl_sound_devices(void)
+{
+    int count = 0;
+    SDL_AudioDeviceID *devices;
+
+    if (sound_devices_enumerated) {
+        return;
+    }
+    reset_sound_devices();
+    add_sound_device(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, "Default Audio Device", _T("SDL:"));
+
+    devices = SDL_GetAudioPlaybackDevices(&count);
+    if (devices) {
+        for (int i = 0; i < count; i++) {
+            const char *name = SDL_GetAudioDeviceName(devices[i]);
+            add_sound_device(devices[i], name, _T("SDL:"));
+        }
+        SDL_free(devices);
+    }
+    sound_devices_enumerated = true;
+}
+#endif
+
+static void clearbuffer(void)
+{
+    memset(paula_sndbuffer, 0, sizeof paula_sndbuffer);
+    paula_sndbufpt = paula_sndbuffer;
+#ifdef UAE_UNIX_WITH_SDL3
+    if (audio_stream) {
+        SDL_ClearAudioStream(audio_stream);
+    }
+#endif
+}
+
+static void channelswap(uae_s16 *sndbuffer, int len)
+{
+    for (int i = 0; i < len; i += 2) {
+        uae_s16 t = sndbuffer[i];
+        sndbuffer[i] = sndbuffer[i + 1];
+        sndbuffer[i + 1] = t;
+    }
+}
+
+static void channelswap6(uae_s16 *sndbuffer, int len)
+{
+    for (int i = 0; i < len; i += 6) {
+        uae_s16 t = sndbuffer[i + 0];
+        sndbuffer[i + 0] = sndbuffer[i + 1];
+        sndbuffer[i + 1] = t;
+        t = sndbuffer[i + 4];
+        sndbuffer[i + 4] = sndbuffer[i + 5];
+        sndbuffer[i + 5] = t;
+    }
+}
+
+static int get_sound_channels(void)
+{
+    int channels = get_audio_nativechannels(active_sound_stereo);
+    if (channels < 1) {
+        channels = 2;
+    }
+    return channels;
+}
+
+static int get_sound_buffer_frames(int channels)
+{
+    int frames = currprefs.sound_maxbsiz > 0 ? currprefs.sound_maxbsiz : DEFAULT_SOUND_MAXB;
+
+    frames >>= 2;
+    frames &= ~63;
+    if (frames < UNIX_SOUND_MIN_FRAMES) {
+        frames = UNIX_SOUND_MIN_FRAMES;
+    }
+    while (frames * channels * (int)sizeof(uae_s16) > UNIX_SOUND_MAX_BUFFER_BYTES) {
+        frames >>= 1;
+    }
+    if (frames < UNIX_SOUND_MIN_FRAMES) {
+        frames = UNIX_SOUND_MIN_FRAMES;
+    }
+    return frames;
+}
+
+static void update_softvolume(void)
+{
+    int volume = currprefs.sound_volume_master;
+
+    if (volume < 0) {
+        volume = 0;
+    } else if (volume > 100) {
+        volume = 100;
+    }
+
+    if (sound_muted || volume >= 100) {
+        sound_softvolume = 0;
+    } else {
+        sound_softvolume = (100 - volume) * 32768 / 100;
+        if (sound_softvolume >= 32768) {
+            sound_softvolume = -1;
+        }
+    }
+}
+
+int setup_sound(void)
+{
+#ifdef UAE_UNIX_WITH_SDL3
+    sound_available = 1;
+    return 1;
+#else
+    sound_available = 0;
+    return 0;
+#endif
+}
+
+void update_sound(float clk)
+{
+#ifdef UAE_UNIX_WITH_SDL3
+    if (!have_sound || audio_spec.freq <= 0) {
+        return;
+    }
+    scaled_sample_evtime_orig = clk * (float)CYCLE_UNIT * sound_sync_multiplier / audio_spec.freq;
+    scaled_sample_evtime = scaled_sample_evtime_orig;
+    sampler_evtime = clk * CYCLE_UNIT * sound_sync_multiplier;
+#endif
+}
+
+#ifdef UAE_UNIX_WITH_SDL3
+static bool ensure_audio_subsystem(void)
+{
+    if (audio_subsystem_initialized) {
+        return true;
+    }
+
+    SDL_SetMainReady();
+    if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
+        write_log(_T("SDL3: audio unavailable: %s\n"), SDL_GetError());
+        return false;
+    }
+    audio_subsystem_initialized = true;
+    return true;
+}
+
+static int selected_sound_device(void)
+{
+    int devices = enumerate_sound_devices();
+    if (devices <= 0) {
+        return -1;
+    }
+    if (currprefs.win32_soundcard < 0 || currprefs.win32_soundcard >= devices) {
+        currprefs.win32_soundcard = changed_prefs.win32_soundcard = 0;
+    }
+    return currprefs.win32_soundcard;
+}
+
+static bool open_sound_device(void)
+{
+    SDL_AudioSpec desired;
+    int channels = get_sound_channels();
+    int freq = currprefs.sound_freq > 0 ? currprefs.sound_freq : DEFAULT_SOUND_FREQ;
+    int device_index;
+    SDL_AudioDeviceID device_id;
+
+    if (!ensure_audio_subsystem()) {
+        return false;
+    }
+    device_index = selected_sound_device();
+    if (device_index < 0) {
+        write_log(_T("SDL3: no playback audio devices available\n"));
+        return false;
+    }
+    device_id = sound_devices[device_index].id;
+
+    memset(&desired, 0, sizeof desired);
+    desired.freq = freq;
+    desired.format = SDL_AUDIO_S16;
+    desired.channels = channels;
+
+    audio_stream = SDL_OpenAudioDeviceStream(device_id, &desired, NULL, NULL);
+    if (!audio_stream) {
+        write_log(_T("SDL3: failed to open audio device '%s': %s\n"),
+            sound_devices[device_index].config_name, SDL_GetError());
+        return false;
+    }
+    audio_spec = desired;
+    SDL_GetAudioStreamFormat(audio_stream, &audio_spec, NULL);
+
+    if (audio_spec.channels != channels) {
+        active_sound_stereo = get_audio_stereomode(audio_spec.channels);
+        channels = audio_spec.channels;
+    }
+
+    currprefs.sound_freq = changed_prefs.sound_freq = audio_spec.freq;
+    paula_sndbufsize = get_sound_buffer_frames(channels) * channels * (int)sizeof(uae_s16);
+    if (paula_sndbufsize > UNIX_SOUND_MAX_BUFFER_BYTES) {
+        paula_sndbufsize = UNIX_SOUND_MAX_BUFFER_BYTES;
+    }
+    paula_sndbufpt = paula_sndbuffer;
+
+    if (get_audio_amigachannels(active_sound_stereo) == 4) {
+        sample_handler = sample16ss_handler;
+    } else {
+        sample_handler = get_audio_ismono(active_sound_stereo) ? sample16_handler : sample16s_handler;
+    }
+
+    update_softvolume();
+    clearbuffer();
+    SDL_ResumeAudioStreamDevice(audio_stream);
+    gui_data.sndbuf_avail = true;
+    write_log(_T("SDL3: audio initialized: %s, %d Hz, %d channels, %d byte buffer\n"),
+        sound_devices[device_index].config_name, audio_spec.freq, audio_spec.channels, paula_sndbufsize);
+    return true;
+}
+#endif
+
+int init_sound(void)
+{
+#ifdef UAE_UNIX_WITH_SDL3
+    gui_data.sndbuf = 0;
+    gui_data.sndbuf_status = 3;
+    gui_data.sndbuf_avail = false;
+
+    if (!sound_available || currprefs.produce_sound <= 1) {
+        return 0;
+    }
+    if (have_sound) {
+        return 1;
+    }
+    if (!open_sound_device()) {
+        return 0;
+    }
+
+    have_sound = 1;
+#ifdef DRIVESOUND
+    driveclick_init();
+#endif
+    return 1;
+#else
+    return 0;
+#endif
+}
+
+void close_sound(void)
+{
+    gui_data.sndbuf = 0;
+    gui_data.sndbuf_status = 3;
+    gui_data.sndbuf_avail = false;
+    if (!have_sound) {
+        return;
+    }
+
+#ifdef UAE_UNIX_WITH_SDL3
+    if (audio_stream) {
+        SDL_PauseAudioStreamDevice(audio_stream);
+        SDL_ClearAudioStream(audio_stream);
+        SDL_DestroyAudioStream(audio_stream);
+        audio_stream = NULL;
+    }
+#endif
+    have_sound = 0;
+#ifdef DRIVESOUND
+    driveclick_reset();
+#endif
+    clearbuffer();
+}
+
+void finish_sound_buffer(void)
+{
+    int bufsize = (int)((uae_u8 *)paula_sndbufpt - (uae_u8 *)paula_sndbuffer);
+
+    if (currprefs.turbo_emulation) {
+        paula_sndbufpt = paula_sndbuffer;
+        return;
+    }
+
+    if (bufsize <= 0) {
+        paula_sndbufpt = paula_sndbuffer;
+        return;
+    }
+
+    if (currprefs.sound_stereo_swap_paula) {
+        int channels = get_audio_nativechannels(active_sound_stereo);
+        if (channels == 2 || channels == 4) {
+            channelswap((uae_s16 *)paula_sndbuffer, bufsize / (int)sizeof(uae_s16));
+        } else if (channels >= 6) {
+            channelswap6((uae_s16 *)paula_sndbuffer, bufsize / (int)sizeof(uae_s16));
+        }
+    }
+
+    paula_sndbufpt = paula_sndbuffer;
+
+#ifdef AVIOUTPUT
+    if (avioutput_audio) {
+        if (AVIOutput_WriteAudio((uae_u8 *)paula_sndbuffer, bufsize)) {
+            if (avioutput_nosoundsync) {
+                sound_setadjust(0);
+            }
+        }
+    }
+    if (avioutput_enabled && (!avioutput_framelimiter || avioutput_nosoundoutput)) {
+        return;
+    }
+#endif
+
+#ifdef UAE_UNIX_WITH_SDL3
+    if (!have_sound || !audio_stream) {
+        return;
+    }
+
+    if (sound_softvolume >= 0) {
+        uae_s16 *p = (uae_s16 *)paula_sndbuffer;
+        for (int i = 0; i < bufsize / (int)sizeof(uae_s16); i++) {
+            p[i] = (uae_s16)(p[i] * sound_softvolume / 32768);
+        }
+    }
+
+    if (!SDL_PutAudioStreamData(audio_stream, paula_sndbuffer, bufsize)) {
+        write_log(_T("SDL3: SDL_PutAudioStreamData failed: %s\n"), SDL_GetError());
+        gui_data.sndbuf_status = -1;
+        return;
+    }
+
+    int queued = SDL_GetAudioStreamQueued(audio_stream);
+    if (queued < 0) {
+        gui_data.sndbuf_status = -1;
+        return;
+    }
+    int target = paula_sndbufsize * UNIX_SOUND_QUEUE_BUFFERS;
+    if (queued > target * 3) {
+        SDL_ClearAudioStream(audio_stream);
+        gui_data.sndbuf_status = 2;
+    } else {
+        gui_data.sndbuf_status = 0;
+        gui_data.sndbuf = target ? (int)(queued * 1000 / target) : 0;
+    }
+#endif
+}
+
+void restart_sound_buffer(void)
+{
+    clearbuffer();
+}
+
+void pause_sound_buffer(void)
+{
+    reset_sound();
+}
+
+void resume_sound(void)
+{
+#ifdef UAE_UNIX_WITH_SDL3
+    if (have_sound && audio_stream) {
+        SDL_ResumeAudioStreamDevice(audio_stream);
+    }
+#endif
+}
+
+void pause_sound(void)
+{
+#ifdef UAE_UNIX_WITH_SDL3
+    if (have_sound && audio_stream) {
+        SDL_PauseAudioStreamDevice(audio_stream);
+    }
+#endif
+}
+
+void reset_sound(void)
+{
+    clearbuffer();
+}
+
+bool sound_paused(void)
+{
+#ifdef UAE_UNIX_WITH_SDL3
+    return !have_sound || !audio_stream || SDL_AudioStreamDevicePaused(audio_stream);
+#else
+    return true;
+#endif
+}
+
+void sound_setadjust(float v)
+{
+    if (v < -6.0f) {
+        v = -6.0f;
+    } else if (v > 6.0f) {
+        v = 6.0f;
+    }
+    if (scaled_sample_evtime_orig > 0) {
+        scaled_sample_evtime = scaled_sample_evtime_orig * (1000.0f + v) / 1000.0f;
+    }
+}
+
+int enumerate_sound_devices(void)
+{
+#ifdef UAE_UNIX_WITH_SDL3
+    if (!ensure_audio_subsystem()) {
+        return 0;
+    }
+    enumerate_sdl_sound_devices();
+    return sound_device_count;
+#else
+    return 0;
+#endif
+}
+
+int unix_sound_device_count(void)
+{
+    return enumerate_sound_devices();
+}
+
+const TCHAR *unix_sound_device_name(int index)
+{
+    if (index < 0 || index >= enumerate_sound_devices()) {
+        return _T("");
+    }
+    return sound_devices[index].name;
+}
+
+const TCHAR *unix_sound_device_config_name(int index)
+{
+    if (index < 0 || index >= enumerate_sound_devices()) {
+        return _T("");
+    }
+    return sound_devices[index].config_name;
+}
+
+int unix_sound_device_index_from_config_name(const TCHAR *name)
+{
+    TCHAR prefixed[320];
+
+    if (!name || !name[0]) {
+        return -1;
+    }
+    enumerate_sound_devices();
+    for (int i = 0; i < sound_device_count; i++) {
+        if (!_tcsicmp(sound_devices[i].config_name, name) || !_tcsicmp(sound_devices[i].name, name)) {
+            return i;
+        }
+    }
+    if (_tcsncmp(name, _T("SDL:"), 4) != 0) {
+        _sntprintf(prefixed, sizeof prefixed / sizeof(TCHAR), _T("SDL:%s"), name);
+        for (int i = 0; i < sound_device_count; i++) {
+            if (!_tcsicmp(sound_devices[i].config_name, prefixed)) {
+                return i;
+            }
+        }
+    }
+    return -1;
+}
+
+void sound_mute(int newmute)
+{
+    if (newmute < 0) {
+        sound_muted = !sound_muted;
+    } else {
+        sound_muted = newmute != 0;
+    }
+    update_softvolume();
+}
+
+void sound_volume(int dir)
+{
+    currprefs.sound_volume_master -= dir * 10;
+    if (currprefs.sound_volume_master < 0) {
+        currprefs.sound_volume_master = 0;
+    } else if (currprefs.sound_volume_master > 100) {
+        currprefs.sound_volume_master = 100;
+    }
+    changed_prefs.sound_volume_master = currprefs.sound_volume_master;
+    update_softvolume();
+    config_changed = 1;
+}
+
+void set_volume(int volume, int mute)
+{
+    currprefs.sound_volume_master = volume;
+    changed_prefs.sound_volume_master = volume;
+    sound_muted = mute != 0;
+    update_softvolume();
+}
+
+void master_sound_volume(int dir)
+{
+    if (dir == 0) {
+        sound_mute(-1);
+    } else {
+        sound_volume(dir);
+    }
+}
diff --git a/od-unix/sound_unix.h b/od-unix/sound_unix.h
new file mode 100644 (file)
index 0000000..5c63d99
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef WINUAE_OD_UNIX_SOUND_UNIX_H
+#define WINUAE_OD_UNIX_SOUND_UNIX_H
+
+#include "sysdeps.h"
+
+int unix_sound_device_count(void);
+const TCHAR *unix_sound_device_name(int index);
+const TCHAR *unix_sound_device_config_name(int index);
+int unix_sound_device_index_from_config_name(const TCHAR *name);
+int unix_sampler_device_count(void);
+const TCHAR *unix_sampler_device_name(int index);
+const TCHAR *unix_sampler_device_config_name(int index);
+int unix_sampler_device_index_from_config_name(const TCHAR *name);
+
+#endif /* WINUAE_OD_UNIX_SOUND_UNIX_H */
diff --git a/od-unix/sounddep/sound.h b/od-unix/sounddep/sound.h
new file mode 100644 (file)
index 0000000..4257fe8
--- /dev/null
@@ -0,0 +1,48 @@
+#ifndef WINUAE_OD_UNIX_SOUNDDEP_SOUND_H
+#define WINUAE_OD_UNIX_SOUNDDEP_SOUND_H
+
+#include "uae/types.h"
+
+#define SOUNDSTUFF 1
+#define SOUND_MODE_NG 0
+#define DEFAULT_SOUND_MAXB 16384
+#define DEFAULT_SOUND_MINB 16384
+#define DEFAULT_SOUND_BITS 16
+#define DEFAULT_SOUND_FREQ 44100
+#define HAVE_STEREO_SUPPORT 1
+
+#define FILTER_SOUND_OFF 0
+#define FILTER_SOUND_EMUL 1
+#define FILTER_SOUND_ON 2
+#define FILTER_SOUND_TYPE_A500 0
+#define FILTER_SOUND_TYPE_A1200 1
+#define FILTER_SOUND_TYPE_A500_FIXEDONLY 2
+
+extern uae_u16 paula_sndbuffer[];
+extern uae_u16 *paula_sndbufpt;
+extern int paula_sndbufsize;
+extern int active_sound_stereo;
+
+void finish_sound_buffer(void);
+void restart_sound_buffer(void);
+void pause_sound_buffer(void);
+int init_sound(void);
+void close_sound(void);
+int setup_sound(void);
+void resume_sound(void);
+void pause_sound(void);
+void reset_sound(void);
+bool sound_paused(void);
+void sound_setadjust(float);
+int enumerate_sound_devices(void);
+void sound_mute(int);
+void sound_volume(int);
+void set_volume(int, int);
+void master_sound_volume(int);
+
+#define PUT_SOUND_WORD(b) do { *(uae_u16 *)paula_sndbufpt = (b); paula_sndbufpt = (uae_u16 *)(((uae_u8 *)paula_sndbufpt) + 2); } while (0)
+#define PUT_SOUND_WORD_MONO(b) PUT_SOUND_WORD(b)
+#define SOUND16_BASE_VAL 0
+#define SOUND8_BASE_VAL 128
+
+#endif /* WINUAE_OD_UNIX_SOUNDDEP_SOUND_H */
diff --git a/od-unix/video.h b/od-unix/video.h
new file mode 100644 (file)
index 0000000..bd1b4a4
--- /dev/null
@@ -0,0 +1,48 @@
+#ifndef WINUAE_OD_UNIX_VIDEO_H
+#define WINUAE_OD_UNIX_VIDEO_H
+
+#include "sysdeps.h"
+
+struct unix_video_frame
+{
+    const uae_u8 *pixels;
+    int width;
+    int height;
+    int rowbytes;
+    int pixbytes;
+    int filter_index;
+    int monitor_id;
+    int backbuffers;
+};
+
+struct unix_video_display_mode
+{
+    int width;
+    int height;
+    int refresh_rate;
+};
+
+enum unix_video_window_mode
+{
+    UNIX_VIDEO_WINDOWED = 0,
+    UNIX_VIDEO_FULLSCREEN = 1,
+    UNIX_VIDEO_FULLWINDOW = 2
+};
+
+bool unix_video_setup(void);
+bool unix_video_init(int width, int height, int pixbytes);
+void unix_video_shutdown(void);
+int unix_video_poll(bool *quit_requested);
+int unix_video_poll_window_events(bool *quit_requested);
+void unix_video_present(const struct unix_video_frame *frame);
+void unix_video_set_title(const TCHAR *title);
+bool unix_video_set_window_mode(enum unix_video_window_mode mode, int display_index, int width, int height, int refresh_rate);
+enum unix_video_window_mode unix_video_get_window_mode(void);
+float unix_video_get_display_refresh_rate(int display_index);
+int unix_video_get_display_modes(int display_index, struct unix_video_display_mode *modes, int max_modes);
+void unix_video_get_desktop(int *dw, int *dh, int *x, int *y, int *w, int *h);
+void unix_video_set_mouse_grab(bool grab);
+bool unix_video_get_mouse_grab(void);
+void unix_video_toggle_mouse_grab(void);
+
+#endif /* WINUAE_OD_UNIX_VIDEO_H */
diff --git a/od-unix/video_null.cpp b/od-unix/video_null.cpp
new file mode 100644 (file)
index 0000000..267b4e1
--- /dev/null
@@ -0,0 +1,107 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#include "video.h"
+
+bool unix_video_setup(void)
+{
+    return true;
+}
+
+bool unix_video_init(int, int, int)
+{
+    return false;
+}
+
+void unix_video_shutdown(void)
+{
+}
+
+int unix_video_poll(bool *quit_requested)
+{
+    if (quit_requested) {
+        *quit_requested = false;
+    }
+    return 0;
+}
+
+int unix_video_poll_window_events(bool *quit_requested)
+{
+    if (quit_requested) {
+        *quit_requested = false;
+    }
+    return 0;
+}
+
+void unix_video_present(const struct unix_video_frame *)
+{
+}
+
+void unix_video_set_title(const TCHAR *)
+{
+}
+
+bool unix_video_set_window_mode(enum unix_video_window_mode, int, int, int, int)
+{
+    return false;
+}
+
+enum unix_video_window_mode unix_video_get_window_mode(void)
+{
+    return UNIX_VIDEO_WINDOWED;
+}
+
+float unix_video_get_display_refresh_rate(int)
+{
+    return 0.0f;
+}
+
+int unix_video_get_display_modes(int, struct unix_video_display_mode *, int)
+{
+    return 0;
+}
+
+void unix_video_set_mouse_grab(bool)
+{
+}
+
+bool unix_video_get_mouse_grab(void)
+{
+    return false;
+}
+
+void unix_video_toggle_mouse_grab(void)
+{
+}
+
+void unix_video_get_desktop(int *dw, int *dh, int *x, int *y, int *w, int *h)
+{
+    if (dw) {
+        *dw = 640;
+    }
+    if (dh) {
+        *dh = 480;
+    }
+    if (x) {
+        *x = 0;
+    }
+    if (y) {
+        *y = 0;
+    }
+    if (w) {
+        *w = 640;
+    }
+    if (h) {
+        *h = 480;
+    }
+}
+
+int target_get_display(const TCHAR *)
+{
+    return -1;
+}
+
+const TCHAR *target_get_display_name(int, bool)
+{
+    return NULL;
+}
diff --git a/od-unix/video_sdl.cpp b/od-unix/video_sdl.cpp
new file mode 100644 (file)
index 0000000..c8a0ee4
--- /dev/null
@@ -0,0 +1,2136 @@
+#include "sysconfig.h"
+#include "sysdeps.h"
+
+#define SDL_MAIN_HANDLED
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+#if defined(__APPLE__)
+#include <OpenGL/gl.h>
+#else
+#include <GL/gl.h>
+#endif
+#endif
+
+#include <algorithm>
+#include <atomic>
+#include <cmath>
+#include <cstring>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+#include "statusline.h"
+#include "traps.h"
+#include "clipboard.h"
+#include "disk.h"
+#include "gui.h"
+#include "input.h"
+#include "options.h"
+#include "threaddep/thread.h"
+#include "uae.h"
+#include "video.h"
+
+extern int pause_emulation;
+extern void pausemode(int mode);
+
+static SDL_Window *s_window;
+static SDL_Renderer *s_renderer;
+static SDL_Texture *s_texture;
+static std::vector<SDL_Texture *> s_textures;
+static SDL_Texture *s_status_texture;
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+static SDL_GLContext s_gl_context;
+static std::vector<GLuint> s_gl_textures;
+static GLuint s_gl_texture;
+static GLuint s_gl_status_texture;
+static GLuint s_gl_program;
+static bool s_gl_active;
+static bool s_gl_failed;
+static bool s_gl_functions_loaded;
+static int s_gl_uniform_texture = -1;
+static int s_gl_uniform_source_size = -1;
+static int s_gl_uniform_adjust = -1;
+static int s_gl_uniform_scanline = -1;
+static int s_gl_uniform_blur_noise = -1;
+static int s_gl_uniform_frame = -1;
+static unsigned int s_gl_frame_counter;
+#endif
+static bool s_setup_done;
+static bool s_available;
+static std::atomic<bool> s_event_thread_valid;
+static std::atomic<bool> s_wrong_event_thread_logged;
+static std::atomic<bool> s_queued_present_logged;
+static uae_thread_id s_event_thread;
+static bool s_mouse_grabbed;
+static enum unix_video_window_mode s_requested_window_mode = UNIX_VIDEO_WINDOWED;
+static enum unix_video_window_mode s_active_window_mode = UNIX_VIDEO_WINDOWED;
+static int s_requested_display_index;
+static int s_active_display_index = -1;
+static int s_requested_fullscreen_width;
+static int s_requested_fullscreen_height;
+static int s_requested_fullscreen_refresh;
+static int s_active_fullscreen_width = -1;
+static int s_active_fullscreen_height = -1;
+static int s_active_fullscreen_refresh = -1;
+static int s_auto_window_width;
+static int s_auto_window_height;
+static int s_texture_width;
+static int s_texture_height;
+static int s_texture_pixbytes;
+static int s_texture_backbuffers;
+static int s_texture_index;
+static int s_status_width;
+static int s_status_height;
+static std::vector<uae_u32> s_status_pixels;
+static uae_u32 s_status_rc[256];
+static uae_u32 s_status_gc[256];
+static uae_u32 s_status_bc[256];
+static bool s_status_colors_ready;
+static Uint8 s_status_click_button;
+static SDL_MouseButtonFlags s_suppressed_mouse_buttons;
+
+struct unix_pending_video_frame {
+    std::vector<uae_u8> pixels;
+    int width;
+    int height;
+    int rowbytes;
+    int pixbytes;
+    int filter_index;
+    int monitor_id;
+    int backbuffers;
+    bool valid;
+};
+
+struct unix_video_layout {
+    float pixel_scale_x;
+    float pixel_scale_y;
+    SDL_FRect frame_dst;
+    SDL_FRect status_dst;
+    SDL_Rect frame_clip;
+};
+
+static std::mutex s_pending_frame_mutex;
+static unix_pending_video_frame s_pending_frame;
+
+static constexpr int UnixStatusScale = 2;
+static TCHAR s_display_name[MAX_DPATH];
+
+static void unix_video_present_on_event_thread(const struct unix_video_frame *frame);
+
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+#ifndef APIENTRY
+#define APIENTRY
+#endif
+#ifndef APIENTRYP
+#define APIENTRYP APIENTRY *
+#endif
+#ifndef GL_FRAGMENT_SHADER
+#define GL_FRAGMENT_SHADER 0x8B30
+#endif
+#ifndef GL_VERTEX_SHADER
+#define GL_VERTEX_SHADER 0x8B31
+#endif
+#ifndef GL_COMPILE_STATUS
+#define GL_COMPILE_STATUS 0x8B81
+#endif
+#ifndef GL_LINK_STATUS
+#define GL_LINK_STATUS 0x8B82
+#endif
+#ifndef GL_INFO_LOG_LENGTH
+#define GL_INFO_LOG_LENGTH 0x8B84
+#endif
+#ifndef GL_CLAMP_TO_EDGE
+#define GL_CLAMP_TO_EDGE 0x812F
+#endif
+#ifndef GL_BGRA
+#define GL_BGRA 0x80E1
+#endif
+#ifndef GL_UNSIGNED_SHORT_5_6_5
+#define GL_UNSIGNED_SHORT_5_6_5 0x8363
+#endif
+
+typedef GLuint(APIENTRYP UnixGlCreateShaderProc)(GLenum);
+typedef void(APIENTRYP UnixGlShaderSourceProc)(GLuint, GLsizei, const GLchar **, const GLint *);
+typedef void(APIENTRYP UnixGlCompileShaderProc)(GLuint);
+typedef void(APIENTRYP UnixGlGetShaderivProc)(GLuint, GLenum, GLint *);
+typedef void(APIENTRYP UnixGlGetShaderInfoLogProc)(GLuint, GLsizei, GLsizei *, GLchar *);
+typedef GLuint(APIENTRYP UnixGlCreateProgramProc)(void);
+typedef void(APIENTRYP UnixGlAttachShaderProc)(GLuint, GLuint);
+typedef void(APIENTRYP UnixGlLinkProgramProc)(GLuint);
+typedef void(APIENTRYP UnixGlGetProgramivProc)(GLuint, GLenum, GLint *);
+typedef void(APIENTRYP UnixGlGetProgramInfoLogProc)(GLuint, GLsizei, GLsizei *, GLchar *);
+typedef void(APIENTRYP UnixGlDeleteShaderProc)(GLuint);
+typedef void(APIENTRYP UnixGlDeleteProgramProc)(GLuint);
+typedef void(APIENTRYP UnixGlUseProgramProc)(GLuint);
+typedef GLint(APIENTRYP UnixGlGetUniformLocationProc)(GLuint, const GLchar *);
+typedef void(APIENTRYP UnixGlUniform1iProc)(GLint, GLint);
+typedef void(APIENTRYP UnixGlUniform1fProc)(GLint, GLfloat);
+typedef void(APIENTRYP UnixGlUniform2fProc)(GLint, GLfloat, GLfloat);
+typedef void(APIENTRYP UnixGlUniform4fProc)(GLint, GLfloat, GLfloat, GLfloat, GLfloat);
+
+static UnixGlCreateShaderProc p_glCreateShader;
+static UnixGlShaderSourceProc p_glShaderSource;
+static UnixGlCompileShaderProc p_glCompileShader;
+static UnixGlGetShaderivProc p_glGetShaderiv;
+static UnixGlGetShaderInfoLogProc p_glGetShaderInfoLog;
+static UnixGlCreateProgramProc p_glCreateProgram;
+static UnixGlAttachShaderProc p_glAttachShader;
+static UnixGlLinkProgramProc p_glLinkProgram;
+static UnixGlGetProgramivProc p_glGetProgramiv;
+static UnixGlGetProgramInfoLogProc p_glGetProgramInfoLog;
+static UnixGlDeleteShaderProc p_glDeleteShader;
+static UnixGlDeleteProgramProc p_glDeleteProgram;
+static UnixGlUseProgramProc p_glUseProgram;
+static UnixGlGetUniformLocationProc p_glGetUniformLocation;
+static UnixGlUniform1iProc p_glUniform1i;
+static UnixGlUniform1fProc p_glUniform1f;
+static UnixGlUniform2fProc p_glUniform2f;
+static UnixGlUniform4fProc p_glUniform4f;
+
+static SDL_FunctionPointer unix_gl_get_proc(const char *name)
+{
+    SDL_FunctionPointer proc = SDL_GL_GetProcAddress(name);
+    if (!proc) {
+        write_log(_T("OpenGL shader pipeline: missing %s\n"), name);
+    }
+    return proc;
+}
+#endif
+
+static int clamp_window_dimension(int value, int fallback, int maxvalue)
+{
+    if (value <= 0) {
+        value = fallback;
+    }
+    if (value > maxvalue) {
+        value = maxvalue;
+    }
+    return value;
+}
+
+static SDL_PixelFormat texture_format_for_pixbytes(int pixbytes)
+{
+    if (pixbytes == 2) {
+        return SDL_PIXELFORMAT_RGB565;
+    }
+    return SDL_PIXELFORMAT_ARGB8888;
+}
+
+static int clamp_backbuffer_count(int backbuffers)
+{
+    if (backbuffers < 1) {
+        return 1;
+    }
+    if (backbuffers > 3) {
+        return 3;
+    }
+    return backbuffers;
+}
+
+static bool unix_video_on_event_thread(void)
+{
+    return !s_event_thread_valid.load() ||
+        pthread_equal(uae_thread_get_id(), s_event_thread);
+}
+
+static void queue_video_frame_for_event_thread(const struct unix_video_frame *frame)
+{
+    if (!frame || !frame->pixels || frame->width <= 0 || frame->height <= 0 ||
+        frame->rowbytes <= 0 || frame->pixbytes <= 0) {
+        return;
+    }
+
+    if (!s_queued_present_logged.exchange(true)) {
+        write_log(_T("SDL3: queueing video present from non-video thread\n"));
+    }
+
+    const int rowbytes = frame->width * frame->pixbytes;
+    std::vector<uae_u8> pixels((size_t)rowbytes * (size_t)frame->height);
+    for (int y = 0; y < frame->height; y++) {
+        memcpy(pixels.data() + (size_t)y * (size_t)rowbytes,
+            frame->pixels + (size_t)y * (size_t)frame->rowbytes,
+            (size_t)rowbytes);
+    }
+
+    std::lock_guard<std::mutex> lock(s_pending_frame_mutex);
+    s_pending_frame.pixels = std::move(pixels);
+    s_pending_frame.width = frame->width;
+    s_pending_frame.height = frame->height;
+    s_pending_frame.rowbytes = rowbytes;
+    s_pending_frame.pixbytes = frame->pixbytes;
+    s_pending_frame.filter_index = frame->filter_index;
+    s_pending_frame.monitor_id = frame->monitor_id;
+    s_pending_frame.backbuffers = frame->backbuffers;
+    s_pending_frame.valid = true;
+}
+
+static bool pop_queued_video_frame(unix_pending_video_frame *frame)
+{
+    std::lock_guard<std::mutex> lock(s_pending_frame_mutex);
+    if (!s_pending_frame.valid) {
+        return false;
+    }
+    *frame = std::move(s_pending_frame);
+    s_pending_frame = unix_pending_video_frame();
+    return true;
+}
+
+static void destroy_frame_textures(void)
+{
+    for (SDL_Texture *texture : s_textures) {
+        SDL_DestroyTexture(texture);
+    }
+    s_textures.clear();
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+    if (!s_gl_textures.empty()) {
+        glDeleteTextures((GLsizei)s_gl_textures.size(), s_gl_textures.data());
+        s_gl_textures.clear();
+    }
+    s_gl_texture = 0;
+#endif
+    s_texture = NULL;
+    s_texture_width = 0;
+    s_texture_height = 0;
+    s_texture_pixbytes = 0;
+    s_texture_backbuffers = 0;
+    s_texture_index = 0;
+}
+
+static SDL_DisplayID *get_sdl_displays(int *count)
+{
+    if (count) {
+        *count = 0;
+    }
+    if (!unix_video_setup()) {
+        return NULL;
+    }
+    return SDL_GetDisplays(count);
+}
+
+static SDL_DisplayID get_sdl_display_for_index(int display_index)
+{
+    int count = 0;
+    SDL_DisplayID *displays = get_sdl_displays(&count);
+    SDL_DisplayID display = 0;
+
+    if (displays && count > 0) {
+        if (display_index > 0 && display_index <= count) {
+            display = displays[display_index - 1];
+        } else {
+            display = displays[0];
+        }
+    }
+    if (displays) {
+        SDL_free(displays);
+    }
+    if (!display) {
+        display = SDL_GetPrimaryDisplay();
+    }
+    return display;
+}
+
+static void center_window_on_display(SDL_DisplayID display)
+{
+    if (!s_window || !display) {
+        return;
+    }
+    SDL_SetWindowPosition(s_window, SDL_WINDOWPOS_CENTERED_DISPLAY(display), SDL_WINDOWPOS_CENTERED_DISPLAY(display));
+}
+
+static float refresh_rate_from_mode(const SDL_DisplayMode *mode)
+{
+    if (!mode) {
+        return 0.0f;
+    }
+    if (mode->refresh_rate_numerator > 0 && mode->refresh_rate_denominator > 0) {
+        return (float)mode->refresh_rate_numerator / (float)mode->refresh_rate_denominator;
+    }
+    return mode->refresh_rate;
+}
+
+static int rounded_refresh_rate_from_mode(const SDL_DisplayMode *mode)
+{
+    float refresh = refresh_rate_from_mode(mode);
+    return refresh > 0.0f ? (int)(refresh + 0.5f) : 0;
+}
+
+static void add_display_mode(struct unix_video_display_mode *modes, int max_modes, int *count,
+    int width, int height, int refresh_rate)
+{
+    if (!modes || !count || width <= 0 || height <= 0) {
+        return;
+    }
+
+    for (int i = 0; i < *count; i++) {
+        if (modes[i].width == width && modes[i].height == height) {
+            if (!modes[i].refresh_rate && refresh_rate) {
+                modes[i].refresh_rate = refresh_rate;
+            }
+            return;
+        }
+    }
+    if (*count >= max_modes) {
+        return;
+    }
+
+    modes[*count].width = width;
+    modes[*count].height = height;
+    modes[*count].refresh_rate = refresh_rate;
+    (*count)++;
+}
+
+static void add_sdl_display_modes(SDL_DisplayID display, struct unix_video_display_mode *modes, int max_modes, int *count)
+{
+    if (!display) {
+        return;
+    }
+
+    int mode_count = 0;
+    SDL_DisplayMode **fullscreen_modes = SDL_GetFullscreenDisplayModes(display, &mode_count);
+    if (fullscreen_modes) {
+        for (int i = 0; i < mode_count; i++) {
+            const SDL_DisplayMode *mode = fullscreen_modes[i];
+            if (mode) {
+                add_display_mode(modes, max_modes, count, mode->w, mode->h, rounded_refresh_rate_from_mode(mode));
+            }
+        }
+        SDL_free(fullscreen_modes);
+    }
+
+    const SDL_DisplayMode *desktop = SDL_GetDesktopDisplayMode(display);
+    if (desktop) {
+        add_display_mode(modes, max_modes, count, desktop->w, desktop->h, rounded_refresh_rate_from_mode(desktop));
+    }
+
+    const SDL_DisplayMode *current = SDL_GetCurrentDisplayMode(display);
+    if (current) {
+        add_display_mode(modes, max_modes, count, current->w, current->h, rounded_refresh_rate_from_mode(current));
+    }
+}
+
+static bool copy_sdl_display_name(int index, bool friendlyname, TCHAR *dst, size_t dstsize)
+{
+    int count = 0;
+    SDL_DisplayID *displays = get_sdl_displays(&count);
+    if (!displays) {
+        return false;
+    }
+
+    bool found = false;
+    if (index >= 0 && index < count) {
+        if (friendlyname) {
+            const char *name = SDL_GetDisplayName(displays[index]);
+            if (name && name[0]) {
+                snprintf(dst, dstsize, "%s", name);
+                found = true;
+            }
+        } else {
+            snprintf(dst, dstsize, "SDL:%u", (unsigned int)displays[index]);
+            found = true;
+        }
+    }
+
+    SDL_free(displays);
+    return found;
+}
+
+int target_get_display(const TCHAR *name)
+{
+    if (!name || !name[0]) {
+        return -1;
+    }
+
+    int count = 0;
+    SDL_DisplayID *displays = get_sdl_displays(&count);
+    if (!displays) {
+        return -1;
+    }
+
+    int found = -1;
+    unsigned int displayid = 0;
+    if (sscanf(name, "SDL:%u", &displayid) == 1) {
+        for (int i = 0; i < count; i++) {
+            if (displays[i] == (SDL_DisplayID)displayid) {
+                found = i + 1;
+                break;
+            }
+        }
+    } else {
+        for (int i = 0; i < count; i++) {
+            const char *displayname = SDL_GetDisplayName(displays[i]);
+            if (displayname && !_tcsicmp(displayname, name)) {
+                found = i + 1;
+                break;
+            }
+        }
+    }
+
+    SDL_free(displays);
+    return found;
+}
+
+const TCHAR *target_get_display_name(int num, bool friendlyname)
+{
+    if (num <= 0) {
+        return NULL;
+    }
+    if (!copy_sdl_display_name(num - 1, friendlyname, s_display_name, sizeof s_display_name / sizeof s_display_name[0])) {
+        return NULL;
+    }
+    return s_display_name;
+}
+
+static int unix_input_lock_state_from_sdl(SDL_Keymod mod)
+{
+    int lockstate = 0;
+    if (mod & SDL_KMOD_CAPS) {
+        lockstate |= UNIX_INPUT_LOCK_CAPS;
+    }
+    if (mod & SDL_KMOD_NUM) {
+        lockstate |= UNIX_INPUT_LOCK_NUM;
+    }
+    if (mod & SDL_KMOD_SCROLL) {
+        lockstate |= UNIX_INPUT_LOCK_SCROLL;
+    }
+    return lockstate;
+}
+
+static int statusbar_source_height(void)
+{
+    return TD_TOTAL_HEIGHT;
+}
+
+static int statusbar_display_height(void)
+{
+    return statusbar_source_height() * UnixStatusScale;
+}
+
+static void init_status_colors(void)
+{
+    if (s_status_colors_ready) {
+        return;
+    }
+    for (int i = 0; i < 256; i++) {
+        s_status_rc[i] = 0xff000000u | (uae_u32(i) << 16);
+        s_status_gc[i] = uae_u32(i) << 8;
+        s_status_bc[i] = uae_u32(i);
+    }
+    s_status_colors_ready = true;
+}
+
+static bool ensure_texture(int width, int height, int pixbytes, int backbuffers)
+{
+    if (!s_renderer || width <= 0 || height <= 0) {
+        return false;
+    }
+    backbuffers = clamp_backbuffer_count(backbuffers);
+    if (!s_textures.empty() && s_texture_width == width && s_texture_height == height &&
+        s_texture_pixbytes == pixbytes && s_texture_backbuffers == backbuffers) {
+        return true;
+    }
+
+    destroy_frame_textures();
+
+    for (int i = 0; i < backbuffers; i++) {
+        SDL_Texture *texture = SDL_CreateTexture(s_renderer, texture_format_for_pixbytes(pixbytes),
+            SDL_TEXTUREACCESS_STREAMING, width, height);
+        if (!texture) {
+            write_log(_T("SDL3: failed to create %dx%d texture: %s\n"), width, height, SDL_GetError());
+            destroy_frame_textures();
+            return false;
+        }
+        SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE);
+        s_textures.push_back(texture);
+    }
+    s_texture = s_textures[0];
+    s_texture_width = width;
+    s_texture_height = height;
+    s_texture_pixbytes = pixbytes;
+    s_texture_backbuffers = backbuffers;
+    s_texture_index = 0;
+    return true;
+}
+
+static const struct gfx_filterdata *filterdata_for_frame(const struct unix_video_frame *frame)
+{
+    int index = frame ? frame->filter_index : GF_NORMAL;
+    if (index < 0 || index >= MAX_FILTERDATA) {
+        index = GF_NORMAL;
+    }
+    return &currprefs.gf[index];
+}
+
+static float bounded_scale(float value)
+{
+    if (!std::isfinite(value) || value < 0.01f) {
+        return 1.0f;
+    }
+    if (value > 64.0f) {
+        return 64.0f;
+    }
+    return value;
+}
+
+static SDL_FRect filtered_frame_rect(const struct unix_video_frame *frame, const struct gfx_filterdata *filter)
+{
+    float scale_x = 1.0f;
+    float scale_y = 1.0f;
+    float offset_x = 0.0f;
+    float offset_y = 0.0f;
+
+    if (filter) {
+        if (filter->gfx_filter_horiz_zoom_mult > 0.0f) {
+            scale_x = filter->gfx_filter_horiz_zoom_mult;
+        }
+        if (filter->gfx_filter_vert_zoom_mult > 0.0f) {
+            scale_y = filter->gfx_filter_vert_zoom_mult;
+        }
+        scale_x += scale_x * (filter->gfx_filter_horiz_zoom / 1000.0f) / 2.0f;
+        scale_y += scale_y * (filter->gfx_filter_vert_zoom / 1000.0f) / 2.0f;
+        offset_x = -(filter->gfx_filter_horiz_offset / 10000.0f) * frame->width;
+        offset_y = -(filter->gfx_filter_vert_offset / 10000.0f) * frame->height;
+    }
+
+    scale_x = bounded_scale(scale_x);
+    scale_y = bounded_scale(scale_y);
+
+    SDL_FRect rect;
+    rect.w = frame->width * scale_x;
+    rect.h = frame->height * scale_y;
+    rect.x = (frame->width - rect.w) / 2.0f + offset_x;
+    rect.y = (frame->height - rect.h) / 2.0f + offset_y;
+    return rect;
+}
+
+static void get_window_pixel_scale(int output_width, int output_height, float *scale_x, float *scale_y)
+{
+    int window_width = 0;
+    int window_height = 0;
+
+    if (scale_x) {
+        *scale_x = 1.0f;
+    }
+    if (scale_y) {
+        *scale_y = 1.0f;
+    }
+    if (!s_window || output_width <= 0 || output_height <= 0) {
+        return;
+    }
+
+    SDL_GetWindowSize(s_window, &window_width, &window_height);
+    if (window_width > 0 && scale_x) {
+        *scale_x = std::max(0.01f, (float)output_width / (float)window_width);
+    }
+    if (window_height > 0 && scale_y) {
+        *scale_y = std::max(0.01f, (float)output_height / (float)window_height);
+    }
+}
+
+static bool get_window_pixel_size(int *width, int *height)
+{
+    int window_width = 0;
+    int window_height = 0;
+
+    if (!s_window) {
+        return false;
+    }
+    SDL_GetWindowSizeInPixels(s_window, &window_width, &window_height);
+    if (window_width <= 0 || window_height <= 0) {
+        SDL_GetWindowSize(s_window, &window_width, &window_height);
+    }
+    if (window_width <= 0 || window_height <= 0) {
+        return false;
+    }
+
+    if (width) {
+        *width = window_width;
+    }
+    if (height) {
+        *height = window_height;
+    }
+    return true;
+}
+
+static bool get_renderer_output_size(int *width, int *height)
+{
+    int output_width = 0;
+    int output_height = 0;
+
+    if (s_renderer && SDL_GetRenderOutputSize(s_renderer, &output_width, &output_height) &&
+        output_width > 0 && output_height > 0) {
+        if (width) {
+            *width = output_width;
+        }
+        if (height) {
+            *height = output_height;
+        }
+        return true;
+    }
+    return get_window_pixel_size(width, height);
+}
+
+static bool make_video_layout(const struct unix_video_frame *frame, const struct gfx_filterdata *filter,
+    int output_width, int output_height, struct unix_video_layout *layout)
+{
+    if (!frame || !layout || frame->width <= 0 || frame->height <= 0 ||
+        output_width <= 0 || output_height <= 0) {
+        return false;
+    }
+
+    memset(layout, 0, sizeof(*layout));
+    get_window_pixel_scale(output_width, output_height, &layout->pixel_scale_x, &layout->pixel_scale_y);
+
+    int status_height = std::max(1, (int)(statusbar_display_height() * layout->pixel_scale_y + 0.5f));
+    if (status_height >= output_height) {
+        status_height = std::max(0, output_height - 1);
+    }
+    const int frame_area_height = std::max(1, output_height - status_height);
+
+    SDL_FRect source_rect = filtered_frame_rect(frame, filter);
+    SDL_FRect dst = {};
+    int mode = frame->filter_index == GF_RTG && filter ? filter->gfx_filter_autoscale : 0;
+
+    if (frame->filter_index == GF_RTG && mode == 2) { /* center */
+        dst.w = source_rect.w * layout->pixel_scale_x;
+        dst.h = source_rect.h * layout->pixel_scale_y;
+        dst.x = ((float)output_width - dst.w) / 2.0f + source_rect.x * layout->pixel_scale_x;
+        dst.y = ((float)frame_area_height - dst.h) / 2.0f + source_rect.y * layout->pixel_scale_y;
+    } else if (frame->filter_index == GF_RTG && mode == 3) { /* integer */
+        int ix = std::max(1, output_width / frame->width);
+        int iy = std::max(1, frame_area_height / frame->height);
+        int scale = std::max(1, std::min(ix, iy));
+        dst.w = source_rect.w * scale;
+        dst.h = source_rect.h * scale;
+        dst.x = ((float)output_width - (float)frame->width * scale) / 2.0f + source_rect.x * scale;
+        dst.y = ((float)frame_area_height - (float)frame->height * scale) / 2.0f + source_rect.y * scale;
+    } else {
+        const float sx = (float)output_width / (float)frame->width;
+        const float sy = (float)frame_area_height / (float)frame->height;
+        float draw_sx = sx;
+        float draw_sy = sy;
+
+        if (frame->filter_index == GF_RTG && mode == 1 && currprefs.win32_rtgscaleaspectratio) {
+            draw_sx = draw_sy = std::min(sx, sy);
+        }
+        dst.w = source_rect.w * draw_sx;
+        dst.h = source_rect.h * draw_sy;
+        dst.x = ((float)output_width - (float)frame->width * draw_sx) / 2.0f + source_rect.x * draw_sx;
+        dst.y = ((float)frame_area_height - (float)frame->height * draw_sy) / 2.0f + source_rect.y * draw_sy;
+    }
+
+    layout->frame_dst = dst;
+    layout->frame_clip = { 0, 0, output_width, frame_area_height };
+    layout->status_dst = {
+        0.0f,
+        (float)frame_area_height,
+        (float)output_width,
+        (float)status_height
+    };
+    return true;
+}
+
+static int valid_monitor_id(int monitor_id)
+{
+    if (monitor_id < 0 || monitor_id >= MAX_AMIGADISPLAYS) {
+        return 0;
+    }
+    return monitor_id;
+}
+
+static void configured_window_size(int monitor_id, int *width, int *height)
+{
+    monitor_id = valid_monitor_id(monitor_id);
+    int w = currprefs.gfx_monitor[monitor_id].gfx_size.width;
+    int h = currprefs.gfx_monitor[monitor_id].gfx_size.height;
+    if (w <= 0) {
+        w = currprefs.gfx_monitor[monitor_id].gfx_size_win.width;
+    }
+    if (h <= 0) {
+        h = currprefs.gfx_monitor[monitor_id].gfx_size_win.height;
+    }
+    if (width) {
+        *width = w;
+    }
+    if (height) {
+        *height = h;
+    }
+}
+
+static void auto_resize_window_for_rtg(const struct unix_video_frame *frame,
+    const struct gfx_filterdata *filter)
+{
+    if (!s_window || s_active_window_mode != UNIX_VIDEO_WINDOWED || !frame ||
+        frame->filter_index != GF_RTG || frame->width <= 0 || frame->height <= 0) {
+        s_auto_window_width = 0;
+        s_auto_window_height = 0;
+        return;
+    }
+
+    SDL_WindowFlags flags = SDL_GetWindowFlags(s_window);
+    if (flags & SDL_WINDOW_MAXIMIZED) {
+        return;
+    }
+
+    SDL_FRect rect = filtered_frame_rect(frame, filter);
+    int source_width = std::max(1, (int)std::ceil(rect.w));
+    int source_height = std::max(1, (int)std::ceil(rect.h));
+    int configured_width = 0;
+    int configured_height = 0;
+    int desired_width = source_width;
+    int desired_height = source_height;
+
+    configured_window_size(frame->monitor_id, &configured_width, &configured_height);
+    bool have_configured = configured_width > 0 && configured_height > 0;
+    int mode = filter ? filter->gfx_filter_autoscale : 0;
+
+    switch (mode) {
+    case 1: /* scale */
+        if (have_configured && currprefs.win32_rtgallowscaling) {
+            desired_width = configured_width;
+            desired_height = configured_height;
+        } else if (have_configured) {
+            desired_width = std::max(configured_width, source_width);
+            desired_height = std::max(configured_height, source_height);
+        }
+        break;
+    case 2: /* center */
+        if (have_configured &&
+            (currprefs.win32_rtgallowscaling ||
+             (configured_width >= source_width && configured_height >= source_height))) {
+            desired_width = configured_width;
+            desired_height = configured_height;
+        }
+        break;
+    case 3: /* integer */
+        if (have_configured) {
+            desired_width = configured_width;
+            desired_height = configured_height;
+        }
+        break;
+    default: /* resize */
+        break;
+    }
+
+    desired_height += statusbar_display_height();
+    if (desired_width == s_auto_window_width && desired_height == s_auto_window_height) {
+        return;
+    }
+
+    if (SDL_SetWindowSize(s_window, desired_width, desired_height)) {
+        s_auto_window_width = desired_width;
+        s_auto_window_height = desired_height;
+        write_log(_T("SDL3: RTG window resize %dx%d\n"), desired_width, desired_height);
+    } else {
+        write_log(_T("SDL3: failed to resize RTG window to %dx%d: %s\n"),
+            desired_width, desired_height, SDL_GetError());
+    }
+}
+
+static int clamp_filter_percent(int value)
+{
+    if (value < 0) {
+        return 0;
+    }
+    if (value > 100) {
+        return 100;
+    }
+    return value;
+}
+
+static void render_scanline_overlay(const struct gfx_filterdata *filter, const SDL_FRect *frame_dst)
+{
+    if (!filter || !frame_dst) {
+        return;
+    }
+
+    int opacity = clamp_filter_percent(filter->gfx_filter_scanlines);
+    int level = clamp_filter_percent(filter->gfx_filter_scanlinelevel);
+    if (!opacity && !level) {
+        return;
+    }
+
+    int lit_lines = filter->gfx_filter_scanlineratio & 15;
+    int shaded_lines = (filter->gfx_filter_scanlineratio >> 4) & 15;
+    int period = lit_lines + shaded_lines;
+    if (period <= 0 || shaded_lines <= 0) {
+        return;
+    }
+
+    int offset = filter->gfx_filter_scanlineoffset % (lit_lines + 1);
+    if (offset < 0) {
+        offset += lit_lines + 1;
+    }
+
+    Uint8 alpha = (Uint8)(opacity * 255 / 100);
+    Uint8 color = (Uint8)(level * 255 / 100);
+    int height = (int)(frame_dst->h + 0.5f);
+
+    SDL_SetRenderDrawBlendMode(s_renderer, SDL_BLENDMODE_BLEND);
+    SDL_SetRenderDrawColor(s_renderer, color, color, color, alpha);
+    for (int y = 0; y < height; y += period) {
+        int y2 = y + offset;
+        for (int yy = 0; yy < shaded_lines && y2 + yy < height; yy++) {
+            SDL_FRect line = {
+                frame_dst->x,
+                frame_dst->y + (float)(y2 + yy),
+                frame_dst->w,
+                1.0f
+            };
+            SDL_RenderFillRect(s_renderer, &line);
+        }
+    }
+    SDL_SetRenderDrawBlendMode(s_renderer, SDL_BLENDMODE_NONE);
+}
+
+static bool ensure_status_pixels(int width)
+{
+    const int height = statusbar_source_height();
+    if (width <= 0 || height <= 0) {
+        return false;
+    }
+    if (s_status_width == width && s_status_height == height) {
+        return true;
+    }
+
+    if (s_status_texture) {
+        SDL_DestroyTexture(s_status_texture);
+        s_status_texture = NULL;
+    }
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+    if (s_gl_status_texture) {
+        glDeleteTextures(1, &s_gl_status_texture);
+        s_gl_status_texture = 0;
+    }
+#endif
+    s_status_width = width;
+    s_status_height = height;
+    s_status_pixels.resize(size_t(width) * size_t(height));
+    return true;
+}
+
+static bool ensure_status_texture(int width)
+{
+    if (!s_renderer || !ensure_status_pixels(width)) {
+        return false;
+    }
+    if (s_status_texture) {
+        return true;
+    }
+
+    const int height = statusbar_source_height();
+    s_status_texture = SDL_CreateTexture(s_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height);
+    if (!s_status_texture) {
+        write_log(_T("SDL3: failed to create %dx%d status texture: %s\n"), width, height, SDL_GetError());
+        return false;
+    }
+    SDL_SetTextureBlendMode(s_status_texture, SDL_BLENDMODE_NONE);
+    return true;
+}
+
+static bool update_status_pixels(int width)
+{
+    if (!ensure_status_pixels(width)) {
+        return false;
+    }
+
+    init_status_colors();
+    std::fill(s_status_pixels.begin(), s_status_pixels.end(), 0xffd4d0c8u);
+    statusline_set_multiplier(0, width, statusbar_source_height());
+    for (int y = 0; y < statusbar_source_height(); y++) {
+        draw_status_line_single(
+            0,
+            reinterpret_cast<uae_u8 *>(s_status_pixels.data() + size_t(y) * size_t(width)),
+            y,
+            width,
+            s_status_rc,
+            s_status_gc,
+            s_status_bc,
+            NULL);
+    }
+    return true;
+}
+
+static bool update_status_texture(int width)
+{
+    if (!ensure_status_texture(width) || !update_status_pixels(width)) {
+        return false;
+    }
+
+    SDL_UpdateTexture(s_status_texture, NULL, s_status_pixels.data(), width * int(sizeof(uae_u32)));
+    return true;
+}
+
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+static bool unix_gl_load_functions(void)
+{
+    if (s_gl_functions_loaded) {
+        return true;
+    }
+
+    p_glCreateShader = reinterpret_cast<UnixGlCreateShaderProc>(unix_gl_get_proc("glCreateShader"));
+    p_glShaderSource = reinterpret_cast<UnixGlShaderSourceProc>(unix_gl_get_proc("glShaderSource"));
+    p_glCompileShader = reinterpret_cast<UnixGlCompileShaderProc>(unix_gl_get_proc("glCompileShader"));
+    p_glGetShaderiv = reinterpret_cast<UnixGlGetShaderivProc>(unix_gl_get_proc("glGetShaderiv"));
+    p_glGetShaderInfoLog = reinterpret_cast<UnixGlGetShaderInfoLogProc>(unix_gl_get_proc("glGetShaderInfoLog"));
+    p_glCreateProgram = reinterpret_cast<UnixGlCreateProgramProc>(unix_gl_get_proc("glCreateProgram"));
+    p_glAttachShader = reinterpret_cast<UnixGlAttachShaderProc>(unix_gl_get_proc("glAttachShader"));
+    p_glLinkProgram = reinterpret_cast<UnixGlLinkProgramProc>(unix_gl_get_proc("glLinkProgram"));
+    p_glGetProgramiv = reinterpret_cast<UnixGlGetProgramivProc>(unix_gl_get_proc("glGetProgramiv"));
+    p_glGetProgramInfoLog = reinterpret_cast<UnixGlGetProgramInfoLogProc>(unix_gl_get_proc("glGetProgramInfoLog"));
+    p_glDeleteShader = reinterpret_cast<UnixGlDeleteShaderProc>(unix_gl_get_proc("glDeleteShader"));
+    p_glDeleteProgram = reinterpret_cast<UnixGlDeleteProgramProc>(unix_gl_get_proc("glDeleteProgram"));
+    p_glUseProgram = reinterpret_cast<UnixGlUseProgramProc>(unix_gl_get_proc("glUseProgram"));
+    p_glGetUniformLocation = reinterpret_cast<UnixGlGetUniformLocationProc>(unix_gl_get_proc("glGetUniformLocation"));
+    p_glUniform1i = reinterpret_cast<UnixGlUniform1iProc>(unix_gl_get_proc("glUniform1i"));
+    p_glUniform1f = reinterpret_cast<UnixGlUniform1fProc>(unix_gl_get_proc("glUniform1f"));
+    p_glUniform2f = reinterpret_cast<UnixGlUniform2fProc>(unix_gl_get_proc("glUniform2f"));
+    p_glUniform4f = reinterpret_cast<UnixGlUniform4fProc>(unix_gl_get_proc("glUniform4f"));
+
+    s_gl_functions_loaded = p_glCreateShader && p_glShaderSource && p_glCompileShader &&
+        p_glGetShaderiv && p_glGetShaderInfoLog && p_glCreateProgram && p_glAttachShader &&
+        p_glLinkProgram && p_glGetProgramiv && p_glGetProgramInfoLog && p_glDeleteShader &&
+        p_glDeleteProgram && p_glUseProgram && p_glGetUniformLocation && p_glUniform1i &&
+        p_glUniform1f && p_glUniform2f && p_glUniform4f;
+    return s_gl_functions_loaded;
+}
+
+static bool unix_gl_shader_ok(GLuint shader, const char *label)
+{
+    GLint ok = 0;
+    p_glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
+    if (ok) {
+        return true;
+    }
+
+    GLint length = 0;
+    p_glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
+    std::vector<char> log(length > 1 ? length : 1, 0);
+    if (length > 1) {
+        p_glGetShaderInfoLog(shader, length, NULL, log.data());
+    }
+    write_log(_T("OpenGL shader pipeline: %s compile failed: %s\n"), label, log.data());
+    return false;
+}
+
+static bool unix_gl_program_ok(GLuint program)
+{
+    GLint ok = 0;
+    p_glGetProgramiv(program, GL_LINK_STATUS, &ok);
+    if (ok) {
+        return true;
+    }
+
+    GLint length = 0;
+    p_glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
+    std::vector<char> log(length > 1 ? length : 1, 0);
+    if (length > 1) {
+        p_glGetProgramInfoLog(program, length, NULL, log.data());
+    }
+    write_log(_T("OpenGL shader pipeline: program link failed: %s\n"), log.data());
+    return false;
+}
+
+static GLuint unix_gl_compile_shader(GLenum type, const char *source, const char *label)
+{
+    GLuint shader = p_glCreateShader(type);
+    if (!shader) {
+        return 0;
+    }
+    const GLchar *sources[] = { reinterpret_cast<const GLchar *>(source) };
+    p_glShaderSource(shader, 1, sources, NULL);
+    p_glCompileShader(shader);
+    if (!unix_gl_shader_ok(shader, label)) {
+        p_glDeleteShader(shader);
+        return 0;
+    }
+    return shader;
+}
+
+static bool unix_gl_build_program(void)
+{
+    if (s_gl_program) {
+        return true;
+    }
+    if (!unix_gl_load_functions()) {
+        return false;
+    }
+
+    static const char *vertex_shader =
+        "#version 120\n"
+        "varying vec2 v_tex;\n"
+        "void main(void) {\n"
+        "    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n"
+        "    v_tex = gl_MultiTexCoord0.xy;\n"
+        "}\n";
+    static const char *fragment_shader =
+        "#version 120\n"
+        "uniform sampler2D u_tex;\n"
+        "uniform vec2 u_source_size;\n"
+        "uniform vec4 u_adjust;\n"
+        "uniform vec4 u_scanline;\n"
+        "uniform vec4 u_blur_noise;\n"
+        "uniform float u_frame;\n"
+        "varying vec2 v_tex;\n"
+        "float rand(vec2 co) {\n"
+        "    return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);\n"
+        "}\n"
+        "void main(void) {\n"
+        "    vec2 texel = 1.0 / max(u_source_size, vec2(1.0));\n"
+        "    vec4 color = texture2D(u_tex, v_tex);\n"
+        "    float blur = clamp(u_blur_noise.x, 0.0, 1.0);\n"
+        "    if (blur > 0.001) {\n"
+        "        vec4 sum = color * 4.0;\n"
+        "        sum += texture2D(u_tex, v_tex + vec2(texel.x, 0.0));\n"
+        "        sum += texture2D(u_tex, v_tex - vec2(texel.x, 0.0));\n"
+        "        sum += texture2D(u_tex, v_tex + vec2(0.0, texel.y));\n"
+        "        sum += texture2D(u_tex, v_tex - vec2(0.0, texel.y));\n"
+        "        color = mix(color, sum / 8.0, blur);\n"
+        "    }\n"
+        "    float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n"
+        "    color.rgb = mix(vec3(luma), color.rgb, u_adjust.z);\n"
+        "    color.rgb = (color.rgb - vec3(0.5)) * u_adjust.y + vec3(0.5 + u_adjust.x);\n"
+        "    color.rgb = pow(max(color.rgb, vec3(0.0)), vec3(u_adjust.w));\n"
+        "    if (u_scanline.x > 0.001 && u_scanline.z > 0.5) {\n"
+        "        float y = mod(gl_FragCoord.y + u_scanline.w, u_scanline.z);\n"
+        "        if (y < u_scanline.y) {\n"
+        "            color.rgb *= 1.0 - u_scanline.x;\n"
+        "        }\n"
+        "    }\n"
+        "    float noise = clamp(u_blur_noise.y, 0.0, 1.0);\n"
+        "    if (noise > 0.001) {\n"
+        "        color.rgb += (rand(v_tex * u_source_size + vec2(u_frame, u_frame * 0.37)) - 0.5) * noise;\n"
+        "    }\n"
+        "    gl_FragColor = clamp(color, 0.0, 1.0);\n"
+        "}\n";
+
+    GLuint vs = unix_gl_compile_shader(GL_VERTEX_SHADER, vertex_shader, "vertex");
+    GLuint fs = unix_gl_compile_shader(GL_FRAGMENT_SHADER, fragment_shader, "fragment");
+    if (!vs || !fs) {
+        if (vs) {
+            p_glDeleteShader(vs);
+        }
+        if (fs) {
+            p_glDeleteShader(fs);
+        }
+        return false;
+    }
+
+    GLuint program = p_glCreateProgram();
+    p_glAttachShader(program, vs);
+    p_glAttachShader(program, fs);
+    p_glLinkProgram(program);
+    p_glDeleteShader(vs);
+    p_glDeleteShader(fs);
+    if (!unix_gl_program_ok(program)) {
+        p_glDeleteProgram(program);
+        return false;
+    }
+
+    s_gl_program = program;
+    s_gl_uniform_texture = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_tex"));
+    s_gl_uniform_source_size = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_source_size"));
+    s_gl_uniform_adjust = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_adjust"));
+    s_gl_uniform_scanline = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_scanline"));
+    s_gl_uniform_blur_noise = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_blur_noise"));
+    s_gl_uniform_frame = p_glGetUniformLocation(program, reinterpret_cast<const GLchar *>("u_frame"));
+    return true;
+}
+
+static bool unix_gl_ensure_context(int width, int height)
+{
+    if (s_gl_active) {
+        return true;
+    }
+    if (!s_window || s_gl_failed) {
+        return false;
+    }
+
+    s_gl_context = SDL_GL_CreateContext(s_window);
+    if (!s_gl_context) {
+        write_log(_T("OpenGL shader pipeline: context creation failed: %s\n"), SDL_GetError());
+        s_gl_failed = true;
+        return false;
+    }
+    SDL_GL_MakeCurrent(s_window, s_gl_context);
+    SDL_GL_SetSwapInterval(1);
+    if (!unix_gl_build_program()) {
+        SDL_GL_DestroyContext(s_gl_context);
+        s_gl_context = NULL;
+        s_gl_failed = true;
+        return false;
+    }
+
+    glDisable(GL_DEPTH_TEST);
+    glDisable(GL_CULL_FACE);
+    glEnable(GL_TEXTURE_2D);
+    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+    glViewport(0, 0, width, height);
+    s_gl_active = true;
+    write_log(_T("OpenGL shader pipeline enabled\n"));
+    return true;
+}
+
+static void unix_gl_destroy_status_texture(void)
+{
+    if (s_gl_status_texture) {
+        glDeleteTextures(1, &s_gl_status_texture);
+        s_gl_status_texture = 0;
+    }
+}
+
+static bool unix_gl_ensure_textures(int width, int height, int pixbytes, int backbuffers)
+{
+    if (!s_gl_active || width <= 0 || height <= 0) {
+        return false;
+    }
+    backbuffers = clamp_backbuffer_count(backbuffers);
+    if (!s_gl_textures.empty() && s_texture_width == width && s_texture_height == height &&
+        s_texture_pixbytes == pixbytes && s_texture_backbuffers == backbuffers) {
+        return true;
+    }
+
+    destroy_frame_textures();
+    s_gl_textures.resize(backbuffers);
+    glGenTextures(backbuffers, s_gl_textures.data());
+    for (GLuint texture : s_gl_textures) {
+        glBindTexture(GL_TEXTURE_2D, texture);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+        if (pixbytes == 2) {
+            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,
+                GL_RGB, GL_UNSIGNED_SHORT_5_6_5, NULL);
+        } else {
+            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
+                GL_BGRA, GL_UNSIGNED_BYTE, NULL);
+        }
+    }
+    s_gl_texture = s_gl_textures[0];
+    s_texture_width = width;
+    s_texture_height = height;
+    s_texture_pixbytes = pixbytes;
+    s_texture_backbuffers = backbuffers;
+    s_texture_index = 0;
+    return true;
+}
+
+static bool unix_gl_ensure_status_texture(int width)
+{
+    if (!s_gl_active || !update_status_pixels(width)) {
+        return false;
+    }
+    if (!s_gl_status_texture) {
+        glGenTextures(1, &s_gl_status_texture);
+        glBindTexture(GL_TEXTURE_2D, s_gl_status_texture);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, statusbar_source_height(), 0,
+            GL_BGRA, GL_UNSIGNED_BYTE, s_status_pixels.data());
+    } else {
+        glBindTexture(GL_TEXTURE_2D, s_gl_status_texture);
+        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, statusbar_source_height(),
+            GL_BGRA, GL_UNSIGNED_BYTE, s_status_pixels.data());
+    }
+    return true;
+}
+
+static void unix_gl_draw_texture(GLuint texture, const SDL_FRect &dst, bool shader,
+    const struct gfx_filterdata *filter, int source_width, int source_height)
+{
+    glBindTexture(GL_TEXTURE_2D, texture);
+    if (shader) {
+        p_glUseProgram(s_gl_program);
+        p_glUniform1i(s_gl_uniform_texture, 0);
+        p_glUniform2f(s_gl_uniform_source_size, (GLfloat)source_width, (GLfloat)source_height);
+
+        float luminance = filter ? filter->gfx_filter_luminance / 1000.0f : 0.0f;
+        float contrast = filter ? 1.0f + filter->gfx_filter_contrast / 1000.0f : 1.0f;
+        float saturation = filter ? 1.0f + filter->gfx_filter_saturation / 1000.0f : 1.0f;
+        float gamma = filter ? 1.0f + filter->gfx_filter_gamma / 1000.0f : 1.0f;
+        if (contrast < 0.0f) {
+            contrast = 0.0f;
+        }
+        if (saturation < 0.0f) {
+            saturation = 0.0f;
+        }
+        if (gamma <= 0.01f) {
+            gamma = 1.0f;
+        }
+        p_glUniform4f(s_gl_uniform_adjust, luminance, contrast, saturation, 1.0f / gamma);
+
+        int lit_lines = filter ? (filter->gfx_filter_scanlineratio & 15) : 0;
+        int shaded_lines = filter ? ((filter->gfx_filter_scanlineratio >> 4) & 15) : 0;
+        int period = lit_lines + shaded_lines;
+        float opacity = filter ? clamp_filter_percent(filter->gfx_filter_scanlines) / 100.0f : 0.0f;
+        if (period <= 0 || shaded_lines <= 0) {
+            opacity = 0.0f;
+            period = 0;
+            shaded_lines = 0;
+        }
+        float scan_offset = filter ? (float)filter->gfx_filter_scanlineoffset : 0.0f;
+        p_glUniform4f(s_gl_uniform_scanline, opacity, (GLfloat)shaded_lines,
+            (GLfloat)period, scan_offset);
+        float blur = filter ? std::min(1.0f, std::max(0.0f, filter->gfx_filter_blur / 1000.0f)) : 0.0f;
+        float noise = filter ? std::min(1.0f, std::max(0.0f, filter->gfx_filter_noise / 1000.0f)) : 0.0f;
+        p_glUniform4f(s_gl_uniform_blur_noise, blur, noise, 0.0f, 0.0f);
+        p_glUniform1f(s_gl_uniform_frame, (GLfloat)s_gl_frame_counter);
+    } else {
+        p_glUseProgram(0);
+    }
+
+    glBegin(GL_QUADS);
+    glTexCoord2f(0.0f, 0.0f);
+    glVertex2f(dst.x, dst.y);
+    glTexCoord2f(1.0f, 0.0f);
+    glVertex2f(dst.x + dst.w, dst.y);
+    glTexCoord2f(1.0f, 1.0f);
+    glVertex2f(dst.x + dst.w, dst.y + dst.h);
+    glTexCoord2f(0.0f, 1.0f);
+    glVertex2f(dst.x, dst.y + dst.h);
+    glEnd();
+    if (shader) {
+        p_glUseProgram(0);
+    }
+}
+
+static bool unix_gl_upload_frame(const struct unix_video_frame *frame)
+{
+    if (!unix_gl_ensure_textures(frame->width, frame->height, frame->pixbytes, frame->backbuffers)) {
+        return false;
+    }
+    if (!s_gl_textures.empty()) {
+        s_texture_index = (s_texture_index + 1) % (int)s_gl_textures.size();
+        s_gl_texture = s_gl_textures[s_texture_index];
+    }
+
+    glBindTexture(GL_TEXTURE_2D, s_gl_texture);
+    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+    if (frame->rowbytes == frame->width * frame->pixbytes) {
+        if (frame->pixbytes == 2) {
+            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->width, frame->height,
+                GL_RGB, GL_UNSIGNED_SHORT_5_6_5, frame->pixels);
+        } else {
+            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->width, frame->height,
+                GL_BGRA, GL_UNSIGNED_BYTE, frame->pixels);
+        }
+    } else {
+        std::vector<uae_u8> packed((size_t)frame->width * (size_t)frame->height * (size_t)frame->pixbytes);
+        for (int y = 0; y < frame->height; y++) {
+            memcpy(packed.data() + (size_t)y * (size_t)frame->width * (size_t)frame->pixbytes,
+                frame->pixels + (size_t)y * (size_t)frame->rowbytes,
+                (size_t)frame->width * (size_t)frame->pixbytes);
+        }
+        if (frame->pixbytes == 2) {
+            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->width, frame->height,
+                GL_RGB, GL_UNSIGNED_SHORT_5_6_5, packed.data());
+        } else {
+            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->width, frame->height,
+                GL_BGRA, GL_UNSIGNED_BYTE, packed.data());
+        }
+    }
+    return true;
+}
+
+static void unix_gl_present(const struct unix_video_frame *frame, const struct gfx_filterdata *filter)
+{
+    if (!unix_gl_upload_frame(frame)) {
+        return;
+    }
+    int output_width = 0;
+    int output_height = 0;
+    struct unix_video_layout layout;
+
+    if (!get_window_pixel_size(&output_width, &output_height) ||
+        !make_video_layout(frame, filter, output_width, output_height, &layout)) {
+        return;
+    }
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+        filter && filter->gfx_filter_bilinear ? GL_LINEAR : GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
+        filter && filter->gfx_filter_bilinear ? GL_LINEAR : GL_NEAREST);
+
+    glViewport(0, 0, output_width, output_height);
+    glMatrixMode(GL_PROJECTION);
+    glLoadIdentity();
+    glOrtho(0.0, output_width, output_height, 0.0, -1.0, 1.0);
+    glMatrixMode(GL_MODELVIEW);
+    glLoadIdentity();
+    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+
+    glEnable(GL_SCISSOR_TEST);
+    glScissor(layout.frame_clip.x,
+        output_height - layout.frame_clip.y - layout.frame_clip.h,
+        layout.frame_clip.w, layout.frame_clip.h);
+    unix_gl_draw_texture(s_gl_texture, layout.frame_dst, true, filter, frame->width, frame->height);
+    glDisable(GL_SCISSOR_TEST);
+
+    if (unix_gl_ensure_status_texture(frame->width)) {
+        unix_gl_draw_texture(s_gl_status_texture, layout.status_dst, true, NULL,
+            frame->width, statusbar_source_height());
+    }
+
+    SDL_GL_SwapWindow(s_window);
+    s_gl_frame_counter++;
+}
+#endif
+
+static int unix_mouse_button_from_sdl(Uint8 button)
+{
+    switch (button) {
+    case SDL_BUTTON_LEFT:
+        return 0;
+    case SDL_BUTTON_RIGHT:
+        return 1;
+    case SDL_BUTTON_MIDDLE:
+        return 2;
+    default:
+        return -1;
+    }
+}
+
+static SDL_MouseButtonFlags unix_mouse_button_mask(Uint8 button)
+{
+    if (button == 0 || button > 32) {
+        return 0;
+    }
+    return SDL_BUTTON_MASK(button);
+}
+
+static bool unix_event_for_window(SDL_WindowID window_id)
+{
+    return s_window && window_id != 0 && window_id == SDL_GetWindowID(s_window);
+}
+
+static bool unix_mouse_point_in_client(float x, float y)
+{
+    int width = 0;
+    int height = 0;
+
+    if (!s_window) {
+        return false;
+    }
+    SDL_GetWindowSize(s_window, &width, &height);
+    return width > 0 && height > 0 &&
+        x >= 0.0f && y >= 0.0f && x < (float)width && y < (float)height;
+}
+
+static bool unix_mouse_event_in_client(SDL_WindowID window_id, float x, float y)
+{
+    return unix_event_for_window(window_id) && unix_mouse_point_in_client(x, y);
+}
+
+static bool statusbar_logical_position(int window_x, int window_y, int *logical_x, int *logical_y)
+{
+    if (!s_window || s_texture_width <= 0 || s_texture_height <= 0) {
+        return false;
+    }
+
+    int window_width = 0;
+    int window_height = 0;
+    SDL_GetWindowSize(s_window, &window_width, &window_height);
+    if (window_width <= 0 || window_height <= 0) {
+        return false;
+    }
+
+    const int logical_width = s_texture_width;
+    const int logical_height = s_texture_height + statusbar_display_height();
+    const int lx = window_x * logical_width / window_width;
+    const int ly = window_y * logical_height / window_height;
+    if (ly < s_texture_height || ly >= logical_height) {
+        return false;
+    }
+    if (logical_x) {
+        *logical_x = lx;
+    }
+    if (logical_y) {
+        *logical_y = ly;
+    }
+    return true;
+}
+
+static int statusbar_hit_slot(int logical_x)
+{
+    if (s_status_width <= 0) {
+        return -1;
+    }
+
+    int mult = statusline_get_multiplier(0) / 100;
+    if (mult < 1) {
+        mult = 1;
+    }
+    const int x_start = (td_numbers_pos & TD_RIGHT)
+        ? s_status_width - (td_numbers_padx + VISIBLE_LEDS * td_width) * mult
+        : td_numbers_padx * mult;
+    const int slot_width = td_width * mult;
+    if (slot_width <= 0 || logical_x < x_start) {
+        return -1;
+    }
+    const int slot = (logical_x - x_start) / slot_width;
+    return slot >= 0 && slot < VISIBLE_LEDS ? slot : -1;
+}
+
+static bool handle_statusbar_click(int window_x, int window_y, Uint8 button)
+{
+    int logical_x = 0;
+    if (!statusbar_logical_position(window_x, window_y, &logical_x, NULL)) {
+        return false;
+    }
+
+    const int slot = statusbar_hit_slot(logical_x);
+    if (slot < 0) {
+        return true;
+    }
+
+    const bool right_click = button == SDL_BUTTON_RIGHT;
+    if (slot >= 8 && slot <= 11) {
+        const int drive = slot - 8;
+        if (right_click) {
+            disk_eject(drive);
+        } else if (changed_prefs.floppyslots[drive].dfxtype >= 0) {
+            gui_display(drive);
+        }
+        return true;
+    }
+    if (slot == 6) {
+        if (right_click) {
+            changed_prefs.cdslots[0].name[0] = 0;
+            changed_prefs.cdslots[0].inuse = false;
+            set_config_changed();
+        } else {
+            gui_display(6);
+        }
+        return true;
+    }
+    if (slot == 3) {
+        if (right_click) {
+            uae_reset(0, 1);
+        } else {
+            gui_display(-1);
+        }
+        return true;
+    }
+    if (slot == 2 && !right_click && pause_emulation) {
+        pausemode(0);
+        return true;
+    }
+
+    return true;
+}
+
+bool unix_video_setup(void)
+{
+    if (s_setup_done) {
+        return s_available;
+    }
+
+    SDL_SetMainReady();
+    if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
+        write_log(_T("SDL3: video unavailable: %s\n"), SDL_GetError());
+        s_setup_done = true;
+        s_available = false;
+        return false;
+    }
+
+    s_event_thread = uae_thread_get_id();
+    s_event_thread_valid = true;
+    s_wrong_event_thread_logged = false;
+    s_queued_present_logged = false;
+    s_setup_done = true;
+    s_available = true;
+    return true;
+}
+
+static bool apply_window_mode(void)
+{
+    if (!s_window || !s_setup_done || !s_available) {
+        return false;
+    }
+
+    if (s_requested_window_mode == s_active_window_mode &&
+        s_requested_display_index == s_active_display_index &&
+        s_requested_fullscreen_width == s_active_fullscreen_width &&
+        s_requested_fullscreen_height == s_active_fullscreen_height &&
+        s_requested_fullscreen_refresh == s_active_fullscreen_refresh) {
+        return true;
+    }
+
+    SDL_DisplayID display = get_sdl_display_for_index(s_requested_display_index);
+    center_window_on_display(display);
+
+    if (s_requested_window_mode == UNIX_VIDEO_WINDOWED) {
+        if (!SDL_SetWindowFullscreen(s_window, false)) {
+            write_log(_T("SDL3: failed to leave fullscreen: %s\n"), SDL_GetError());
+            return false;
+        }
+        s_active_window_mode = UNIX_VIDEO_WINDOWED;
+        s_active_display_index = s_requested_display_index;
+        s_active_fullscreen_width = s_requested_fullscreen_width;
+        s_active_fullscreen_height = s_requested_fullscreen_height;
+        s_active_fullscreen_refresh = s_requested_fullscreen_refresh;
+        return true;
+    }
+
+    SDL_DisplayMode fullscreen_mode;
+    const SDL_DisplayMode *mode = NULL;
+    if (s_requested_window_mode == UNIX_VIDEO_FULLSCREEN) {
+        if (s_requested_fullscreen_width > 0 && s_requested_fullscreen_height > 0) {
+            float refresh = s_requested_fullscreen_refresh > 0 ? (float)s_requested_fullscreen_refresh : 0.0f;
+            if (SDL_GetClosestFullscreenDisplayMode(display, s_requested_fullscreen_width,
+                s_requested_fullscreen_height, refresh, true, &fullscreen_mode)) {
+                mode = &fullscreen_mode;
+            } else {
+                write_log(_T("SDL3: no exclusive fullscreen mode %dx%d@%d on display %d, using desktop fullscreen\n"),
+                    s_requested_fullscreen_width, s_requested_fullscreen_height,
+                    s_requested_fullscreen_refresh, s_requested_display_index);
+            }
+        } else {
+            const SDL_DisplayMode *desktop = SDL_GetDesktopDisplayMode(display);
+            if (desktop) {
+                fullscreen_mode = *desktop;
+                mode = &fullscreen_mode;
+            }
+        }
+    }
+
+    if (!SDL_SetWindowFullscreenMode(s_window, mode)) {
+        write_log(_T("SDL3: failed to set fullscreen mode: %s\n"), SDL_GetError());
+        return false;
+    }
+    if (!SDL_SetWindowFullscreen(s_window, true)) {
+        write_log(_T("SDL3: failed to enter fullscreen: %s\n"), SDL_GetError());
+        return false;
+    }
+
+    s_active_window_mode = s_requested_window_mode;
+    s_active_display_index = s_requested_display_index;
+    s_active_fullscreen_width = s_requested_fullscreen_width;
+    s_active_fullscreen_height = s_requested_fullscreen_height;
+    s_active_fullscreen_refresh = s_requested_fullscreen_refresh;
+    return true;
+}
+
+bool unix_video_init(int width, int height, int pixbytes)
+{
+    if (!unix_video_setup()) {
+        return false;
+    }
+
+    width = width > 0 ? width : 768;
+    height = height > 0 ? height : 576;
+    pixbytes = pixbytes == 2 ? 2 : 4;
+
+    if (!s_window) {
+        int window_width = clamp_window_dimension(width, 768, 960);
+        int window_height = clamp_window_dimension(height + statusbar_display_height(), 576 + statusbar_display_height(), 720 + statusbar_display_height());
+        SDL_WindowFlags base_window_flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
+        SDL_WindowFlags window_flags = base_window_flags;
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+        bool tried_gl_window = false;
+        if (!s_gl_failed) {
+            window_flags = (SDL_WindowFlags)(window_flags | SDL_WINDOW_OPENGL);
+            tried_gl_window = true;
+        }
+#endif
+        s_window = SDL_CreateWindow(WINUAE_UNIX_WINDOW_TITLE, window_width, window_height,
+            window_flags);
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+        if (!s_window && tried_gl_window) {
+            write_log(_T("OpenGL shader pipeline: window creation failed: %s\n"), SDL_GetError());
+            s_gl_failed = true;
+            s_window = SDL_CreateWindow(WINUAE_UNIX_WINDOW_TITLE, window_width, window_height,
+                base_window_flags);
+        }
+#endif
+        if (!s_window) {
+            write_log(_T("SDL3: failed to create window: %s\n"), SDL_GetError());
+            s_available = false;
+            return false;
+        }
+        center_window_on_display(get_sdl_display_for_index(s_requested_display_index));
+    }
+
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+    if (!s_gl_failed) {
+        int window_width = 0;
+        int window_height = 0;
+        SDL_GetWindowSizeInPixels(s_window, &window_width, &window_height);
+        if (unix_gl_ensure_context(window_width, window_height)) {
+            apply_window_mode();
+            return unix_gl_ensure_textures(width, height, pixbytes, 1);
+        }
+        if (!s_renderer && s_window) {
+            SDL_DestroyWindow(s_window);
+            s_window = NULL;
+            return unix_video_init(width, height, pixbytes);
+        }
+    }
+    if (s_gl_active) {
+        apply_window_mode();
+        return unix_gl_ensure_textures(width, height, pixbytes, 1);
+    }
+#endif
+
+    if (!s_renderer) {
+        s_renderer = SDL_CreateRenderer(s_window, NULL);
+        if (!s_renderer) {
+            s_renderer = SDL_CreateRenderer(s_window, "software");
+        }
+        if (!s_renderer) {
+            write_log(_T("SDL3: failed to create renderer: %s\n"), SDL_GetError());
+            s_available = false;
+            return false;
+        }
+        SDL_SetRenderVSync(s_renderer, 1);
+        SDL_SetRenderDrawColor(s_renderer, 0, 0, 0, 255);
+        SDL_RenderClear(s_renderer);
+        SDL_RenderPresent(s_renderer);
+    }
+
+    apply_window_mode();
+
+    return ensure_texture(width, height, pixbytes, 1);
+}
+
+void unix_video_shutdown(void)
+{
+    unix_input_release_keys();
+    unix_video_set_mouse_grab(false);
+
+    destroy_frame_textures();
+    if (s_status_texture) {
+        SDL_DestroyTexture(s_status_texture);
+        s_status_texture = NULL;
+    }
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+    unix_gl_destroy_status_texture();
+    if (s_gl_program) {
+        p_glDeleteProgram(s_gl_program);
+        s_gl_program = 0;
+    }
+    if (s_gl_context) {
+        SDL_GL_DestroyContext(s_gl_context);
+        s_gl_context = NULL;
+    }
+    s_gl_active = false;
+#endif
+    s_status_pixels.clear();
+    if (s_renderer) {
+        SDL_DestroyRenderer(s_renderer);
+        s_renderer = NULL;
+    }
+    if (s_window) {
+        SDL_DestroyWindow(s_window);
+        s_window = NULL;
+    }
+    s_status_width = 0;
+    s_status_height = 0;
+    s_auto_window_width = 0;
+    s_auto_window_height = 0;
+    s_event_thread_valid = false;
+    s_wrong_event_thread_logged = false;
+    s_queued_present_logged = false;
+    s_suppressed_mouse_buttons = 0;
+    {
+        std::lock_guard<std::mutex> lock(s_pending_frame_mutex);
+        s_pending_frame = unix_pending_video_frame();
+    }
+
+    if (s_setup_done && s_available) {
+        SDL_QuitSubSystem(SDL_INIT_EVENTS | SDL_INIT_VIDEO);
+    }
+    s_setup_done = false;
+    s_available = false;
+}
+
+static int unix_video_poll_internal(bool *quit_requested, bool input_events)
+{
+    SDL_Event event;
+    int got = 0;
+
+    if (quit_requested) {
+        *quit_requested = false;
+    }
+    if (!s_setup_done || !s_available) {
+        return 0;
+    }
+    if (s_event_thread_valid.load() && !pthread_equal(uae_thread_get_id(), s_event_thread)) {
+        if (!s_wrong_event_thread_logged.exchange(true)) {
+            write_log(_T("SDL3: ignoring event poll from non-video thread\n"));
+        }
+        return 0;
+    }
+
+    unix_pending_video_frame pending;
+    if (pop_queued_video_frame(&pending)) {
+        struct unix_video_frame frame;
+        frame.pixels = pending.pixels.data();
+        frame.width = pending.width;
+        frame.height = pending.height;
+        frame.rowbytes = pending.rowbytes;
+        frame.pixbytes = pending.pixbytes;
+        frame.filter_index = pending.filter_index;
+        frame.monitor_id = pending.monitor_id;
+        frame.backbuffers = pending.backbuffers;
+        unix_video_present_on_event_thread(&frame);
+        got = 1;
+    }
+
+    while (SDL_PollEvent(&event)) {
+        got = 1;
+        switch (event.type) {
+        case SDL_EVENT_QUIT:
+            if (quit_requested) {
+                *quit_requested = true;
+            }
+            break;
+        case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
+            if (quit_requested) {
+                *quit_requested = true;
+            }
+            break;
+        case SDL_EVENT_WINDOW_MOVED:
+            if (s_mouse_grabbed && unix_event_for_window(event.window.windowID)) {
+                unix_input_release_keys();
+                unix_video_set_mouse_grab(false);
+                s_suppressed_mouse_buttons = 0;
+            }
+            break;
+        case SDL_EVENT_WINDOW_FOCUS_LOST:
+            unix_input_release_keys();
+            unix_video_set_mouse_grab(false);
+            s_suppressed_mouse_buttons = 0;
+            break;
+        case SDL_EVENT_KEY_DOWN:
+        case SDL_EVENT_KEY_UP:
+            if (!input_events) {
+                break;
+            }
+            if (event.key.repeat) {
+                break;
+            }
+            if (event.key.key == SDLK_Q && (event.key.mod & (SDL_KMOD_CTRL | SDL_KMOD_GUI))) {
+                if (quit_requested) {
+                    *quit_requested = true;
+                }
+                break;
+            }
+            if (event.key.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_G &&
+                (event.key.mod & (SDL_KMOD_CTRL | SDL_KMOD_GUI))) {
+                unix_video_set_mouse_grab(false);
+                break;
+            }
+            if (event.key.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE && s_mouse_grabbed) {
+                unix_video_set_mouse_grab(false);
+            }
+            unix_input_keyboard_key((int)event.key.scancode, event.key.type == SDL_EVENT_KEY_DOWN,
+                unix_input_lock_state_from_sdl(event.key.mod));
+            break;
+        case SDL_EVENT_MOUSE_MOTION:
+            if (!input_events) {
+                break;
+            }
+            if (s_suppressed_mouse_buttons) {
+                s_suppressed_mouse_buttons &= event.motion.state;
+                break;
+            }
+            if (s_mouse_grabbed) {
+                unix_input_mouse_motion((int)event.motion.xrel, (int)event.motion.yrel);
+            }
+            break;
+        case SDL_EVENT_MOUSE_BUTTON_DOWN:
+        case SDL_EVENT_MOUSE_BUTTON_UP:
+        {
+            if (!input_events) {
+                break;
+            }
+            SDL_MouseButtonFlags button_mask = unix_mouse_button_mask(event.button.button);
+            if (button_mask && (s_suppressed_mouse_buttons & button_mask)) {
+                if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
+                    s_suppressed_mouse_buttons &= ~button_mask;
+                }
+                break;
+            }
+            if (!s_mouse_grabbed &&
+                !unix_mouse_event_in_client(event.button.windowID, event.button.x, event.button.y)) {
+                if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && button_mask) {
+                    s_suppressed_mouse_buttons |= button_mask;
+                }
+                break;
+            }
+            int button = unix_mouse_button_from_sdl(event.button.button);
+            if (button >= 0) {
+                if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && handle_statusbar_click((int)event.button.x, (int)event.button.y, event.button.button)) {
+                    s_status_click_button = event.button.button;
+                    break;
+                }
+                if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && s_status_click_button == event.button.button) {
+                    s_status_click_button = 0;
+                    break;
+                }
+                if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && !s_mouse_grabbed) {
+                    unix_video_set_mouse_grab(true);
+                }
+                unix_input_mouse_button(button, event.type == SDL_EVENT_MOUSE_BUTTON_DOWN);
+            }
+            break;
+        }
+        case SDL_EVENT_MOUSE_WHEEL:
+            if (!input_events) {
+                break;
+            }
+            if (!s_mouse_grabbed &&
+                !unix_mouse_event_in_client(event.wheel.windowID, event.wheel.mouse_x, event.wheel.mouse_y)) {
+                break;
+            }
+            if (s_mouse_grabbed) {
+                unix_input_mouse_wheel(event.wheel.integer_x, event.wheel.integer_y);
+            }
+            break;
+        case SDL_EVENT_JOYSTICK_ADDED:
+        case SDL_EVENT_JOYSTICK_REMOVED:
+        case SDL_EVENT_GAMEPAD_ADDED:
+        case SDL_EVENT_GAMEPAD_REMOVED:
+        case SDL_EVENT_GAMEPAD_REMAPPED:
+            unix_input_joystick_device_changed();
+            break;
+        case SDL_EVENT_CLIPBOARD_UPDATE:
+            clipboard_host_changed();
+            break;
+        default:
+            break;
+        }
+    }
+
+    return got;
+}
+
+int unix_video_poll(bool *quit_requested)
+{
+    return unix_video_poll_internal(quit_requested, true);
+}
+
+int unix_video_poll_window_events(bool *quit_requested)
+{
+    return unix_video_poll_internal(quit_requested, false);
+}
+
+static void unix_video_present_on_event_thread(const struct unix_video_frame *frame)
+{
+    if (!frame || !frame->pixels || frame->width <= 0 || frame->height <= 0 || frame->rowbytes <= 0) {
+        return;
+    }
+    if (!unix_video_init(frame->width, frame->height, frame->pixbytes)) {
+        return;
+    }
+    const struct gfx_filterdata *filter = filterdata_for_frame(frame);
+    auto_resize_window_for_rtg(frame, filter);
+#ifdef WINUAE_UNIX_WITH_OPENGL_SHADER_PIPELINE
+    if (s_gl_active) {
+        unix_gl_present(frame, filter);
+        return;
+    }
+#endif
+    if (!ensure_texture(frame->width, frame->height, frame->pixbytes, frame->backbuffers)) {
+        return;
+    }
+    if (!s_textures.empty()) {
+        s_texture_index = (s_texture_index + 1) % (int)s_textures.size();
+        s_texture = s_textures[s_texture_index];
+    }
+
+    SDL_SetTextureScaleMode(s_texture,
+        filter && filter->gfx_filter_bilinear ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST);
+
+    SDL_UpdateTexture(s_texture, NULL, frame->pixels, frame->rowbytes);
+    SDL_SetRenderLogicalPresentation(s_renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
+    int output_width = 0;
+    int output_height = 0;
+    struct unix_video_layout layout;
+    if (!get_renderer_output_size(&output_width, &output_height) ||
+        !make_video_layout(frame, filter, output_width, output_height, &layout)) {
+        return;
+    }
+    SDL_SetRenderDrawColor(s_renderer, 0, 0, 0, 255);
+    SDL_RenderClear(s_renderer);
+
+    SDL_SetRenderClipRect(s_renderer, &layout.frame_clip);
+    SDL_RenderTexture(s_renderer, s_texture, NULL, &layout.frame_dst);
+    render_scanline_overlay(filter, &layout.frame_dst);
+    SDL_SetRenderClipRect(s_renderer, NULL);
+
+    if (update_status_texture(frame->width)) {
+        SDL_RenderTexture(s_renderer, s_status_texture, NULL, &layout.status_dst);
+    }
+    SDL_RenderPresent(s_renderer);
+}
+
+void unix_video_present(const struct unix_video_frame *frame)
+{
+    if (!unix_video_on_event_thread()) {
+        queue_video_frame_for_event_thread(frame);
+        return;
+    }
+    unix_video_present_on_event_thread(frame);
+}
+
+void unix_video_set_title(const TCHAR *title)
+{
+    if (s_window && title) {
+        SDL_SetWindowTitle(s_window, title);
+    }
+}
+
+bool unix_video_set_window_mode(enum unix_video_window_mode mode, int display_index, int width, int height, int refresh_rate)
+{
+    s_requested_window_mode = mode;
+    s_requested_display_index = display_index;
+    s_requested_fullscreen_width = width;
+    s_requested_fullscreen_height = height;
+    s_requested_fullscreen_refresh = refresh_rate;
+
+    if (!s_window) {
+        return true;
+    }
+    return apply_window_mode();
+}
+
+enum unix_video_window_mode unix_video_get_window_mode(void)
+{
+    return s_active_window_mode;
+}
+
+float unix_video_get_display_refresh_rate(int display_index)
+{
+    if (!unix_video_setup()) {
+        return 0.0f;
+    }
+
+    SDL_DisplayID display = 0;
+    if (display_index < 0 && s_window) {
+        display = SDL_GetDisplayForWindow(s_window);
+    }
+    if (!display) {
+        display = get_sdl_display_for_index(display_index);
+    }
+
+    const SDL_DisplayMode *mode = display ? SDL_GetCurrentDisplayMode(display) : NULL;
+    float refresh = refresh_rate_from_mode(mode);
+    if (refresh <= 0.0f && display) {
+        refresh = refresh_rate_from_mode(SDL_GetDesktopDisplayMode(display));
+    }
+    return refresh;
+}
+
+int unix_video_get_display_modes(int display_index, struct unix_video_display_mode *modes, int max_modes)
+{
+    if (!modes || max_modes <= 0 || !unix_video_setup()) {
+        return 0;
+    }
+
+    int count = 0;
+    int display_count = 0;
+    SDL_DisplayID *displays = get_sdl_displays(&display_count);
+    if (displays && display_count > 0) {
+        if (display_index > 0 && display_index <= display_count) {
+            add_sdl_display_modes(displays[display_index - 1], modes, max_modes, &count);
+        } else {
+            for (int i = 0; i < display_count; i++) {
+                add_sdl_display_modes(displays[i], modes, max_modes, &count);
+            }
+        }
+    }
+    if (displays) {
+        SDL_free(displays);
+    }
+    if (!count) {
+        add_sdl_display_modes(SDL_GetPrimaryDisplay(), modes, max_modes, &count);
+    }
+    return count;
+}
+
+void unix_video_set_mouse_grab(bool grab)
+{
+    s_mouse_grabbed = grab;
+    unix_input_set_mouse_active(grab);
+    if (grab) {
+        s_suppressed_mouse_buttons = 0;
+    }
+
+    if (!s_setup_done || !s_available) {
+        return;
+    }
+
+    if (s_window) {
+        SDL_SetWindowRelativeMouseMode(s_window, grab);
+        SDL_SetWindowMouseGrab(s_window, grab);
+        SDL_CaptureMouse(grab);
+    }
+}
+
+bool unix_video_get_mouse_grab(void)
+{
+    return s_mouse_grabbed;
+}
+
+void unix_video_toggle_mouse_grab(void)
+{
+    unix_video_set_mouse_grab(!unix_video_get_mouse_grab());
+}
+
+void unix_video_get_desktop(int *dw, int *dh, int *x, int *y, int *w, int *h)
+{
+    SDL_Rect usable;
+    SDL_DisplayID display = SDL_GetPrimaryDisplay();
+
+    if (x) {
+        *x = 0;
+    }
+    if (y) {
+        *y = 0;
+    }
+    const SDL_DisplayMode *mode = display ? SDL_GetCurrentDisplayMode(display) : NULL;
+    if (mode) {
+        if (dw) {
+            *dw = mode->w;
+        }
+        if (dh) {
+            *dh = mode->h;
+        }
+    } else {
+        if (dw) {
+            *dw = 640;
+        }
+        if (dh) {
+            *dh = 480;
+        }
+    }
+
+    if (display && SDL_GetDisplayUsableBounds(display, &usable)) {
+        if (x) {
+            *x = usable.x;
+        }
+        if (y) {
+            *y = usable.y;
+        }
+        if (w) {
+            *w = usable.w;
+        }
+        if (h) {
+            *h = usable.h;
+        }
+    } else {
+        if (w) {
+            *w = dw ? *dw : 640;
+        }
+        if (h) {
+            *h = dh ? *dh : 480;
+        }
+    }
+}