diff --git a/qapi/ui.json b/qapi/ui.json index e16e98a060..f92b8bd2b8 100644 --- a/qapi/ui.json +++ b/qapi/ui.json @@ -1137,7 +1137,7 @@ { 'enum' : 'DisplayType', 'data' : [ 'default', 'none', 'gtk', 'sdl', 'egl-headless', 'curses', 'cocoa', - 'spice-app'] } + 'spice-app', 'xemu'] } ## # @DisplayOptions: diff --git a/ui/xemu.c b/ui/xemu.c new file mode 100644 index 0000000000..b50e97f275 --- /dev/null +++ b/ui/xemu.c @@ -0,0 +1,1325 @@ +/* + * xemu SDL display driver + * + * Copyright (c) 2020 Matt Borgerson + * + * Based on sdl2.c, sdl2-gl.c + * + * Copyright (c) 2003 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +/* Ported SDL 1.2 code to 2.0 by Dave Airlie. */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "qemu-version.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/sdl2.h" +#include "sysemu/runstate.h" +#include "sysemu/sysemu.h" +#include "xemu-hud.h" +#include "xemu-input.h" +#include "xemu-settings.h" +#include "xemu-shaders.h" + +// void sdl2_window_create(struct sdl2_console *scon); +// void sdl2_window_destroy(struct sdl2_console *scon); +// void sdl2_window_resize(struct sdl2_console *scon); +// void sdl2_poll_events(struct sdl2_console *scon); + +// void sdl2_gl_update(DisplayChangeListener *dcl, +// int x, int y, int w, int h); +// void sdl2_gl_switch(DisplayChangeListener *dcl, +// DisplaySurface *new_surface); +// void sdl2_gl_refresh(DisplayChangeListener *dcl); +// void sdl2_gl_redraw(struct sdl2_console *scon); + +// QEMUGLContext sdl2_gl_create_context(DisplayChangeListener *dcl, +// QEMUGLParams *params); +// void sdl2_gl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx); +// int sdl2_gl_make_context_current(DisplayChangeListener *dcl, +// QEMUGLContext ctx); +// QEMUGLContext sdl2_gl_get_current_context(DisplayChangeListener *dcl); + +void xb_surface_gl_create_texture(QemuGLShader *gls, + DisplaySurface *surface); +void xb_surface_gl_update_texture(QemuGLShader *gls, + DisplaySurface *surface, + int x, int y, int w, int h); +void xb_surface_gl_destroy_texture(QemuGLShader *gls, + DisplaySurface *surface); +// void sdl2_gl_scanout_disable(DisplayChangeListener *dcl); +// void sdl2_gl_scanout_texture(DisplayChangeListener *dcl, +// uint32_t backing_id, +// bool backing_y_0_top, +// uint32_t backing_width, +// uint32_t backing_height, +// uint32_t x, uint32_t y, +// uint32_t w, uint32_t h); +// void sdl2_gl_scanout_flush(DisplayChangeListener *dcl, +// uint32_t x, uint32_t y, uint32_t w, uint32_t h); + +// FIXME: Move this to a better location +void pre_swap(void); +void post_swap(void); + +static int sdl2_num_outputs; +static struct sdl2_console *sdl2_console; + +static SDL_Surface *guest_sprite_surface; +static int gui_grab; /* if true, all keyboard/mouse events are grabbed */ + +static int gui_saved_grab; +static int gui_fullscreen; +static int gui_grab_code = KMOD_LALT | KMOD_LCTRL; +static SDL_Cursor *sdl_cursor_normal; +static SDL_Cursor *sdl_cursor_hidden; +static int absolute_enabled; +static int guest_cursor; +static int guest_x, guest_y; +static SDL_Cursor *guest_sprite; +static Notifier mouse_mode_notifier; + +int scaling_mode = 1; +struct decal_shader *blit; + +static void toggle_full_screen(struct sdl2_console *scon); + +int xemu_is_fullscreen(void) +{ + return gui_fullscreen; +} + +void xemu_toggle_fullscreen(void) +{ + toggle_full_screen(&sdl2_console[0]); +} + +#define SDL2_REFRESH_INTERVAL_BUSY 16 +#define SDL2_MAX_IDLE_COUNT (2 * GUI_REFRESH_INTERVAL_DEFAULT \ + / SDL2_REFRESH_INTERVAL_BUSY + 1) + +// static void sdl_update_caption(struct sdl2_console *scon); + +static struct sdl2_console *get_scon_from_window(uint32_t window_id) +{ + int i; + for (i = 0; i < sdl2_num_outputs; i++) { + if (sdl2_console[i].real_window == SDL_GetWindowFromID(window_id)) { + return &sdl2_console[i]; + } + } + return NULL; +} + +#if 0 +void sdl2_window_create(struct sdl2_console *scon) +{ + int flags = 0; + + if (!scon->surface) { + return; + } + assert(!scon->real_window); + + if (gui_fullscreen) { + flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + } else { + flags |= SDL_WINDOW_RESIZABLE; + } + if (scon->hidden) { + flags |= SDL_WINDOW_HIDDEN; + } + + scon->real_window = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + surface_width(scon->surface), + surface_height(scon->surface), + flags); + scon->real_renderer = SDL_CreateRenderer(scon->real_window, -1, 0); + if (scon->opengl) { + scon->winctx = SDL_GL_GetCurrentContext(); + } + sdl_update_caption(scon); +} + +void sdl2_window_destroy(struct sdl2_console *scon) +{ + if (!scon->real_window) { + return; + } + + SDL_DestroyRenderer(scon->real_renderer); + scon->real_renderer = NULL; + SDL_DestroyWindow(scon->real_window); + scon->real_window = NULL; +} +#endif + +void sdl2_window_resize(struct sdl2_console *scon) +{ + if (!scon->real_window) { + return; + } + + SDL_SetWindowSize(scon->real_window, + surface_width(scon->surface), + surface_height(scon->surface)); +} + +static void sdl2_redraw(struct sdl2_console *scon) +{ + if (scon->opengl) { + sdl2_gl_redraw(scon); + } +} + +static void sdl_update_caption(struct sdl2_console *scon) +{ +#if 0 + char win_title[1024]; + char icon_title[1024]; + const char *status = ""; + + if (!runstate_is_running()) { + status = " [Stopped]"; + } else if (gui_grab) { + if (alt_grab) { + status = " - Press Ctrl-Alt-Shift-G to exit grab"; + } else if (ctrl_grab) { + status = " - Press Right-Ctrl-G to exit grab"; + } else { + status = " - Press Ctrl-Alt-G to exit grab"; + } + } + + if (qemu_name) { + snprintf(win_title, sizeof(win_title), "QEMU (%s-%d)%s", qemu_name, + scon->idx, status); + snprintf(icon_title, sizeof(icon_title), "QEMU (%s)", qemu_name); + } else { + snprintf(win_title, sizeof(win_title), "QEMU%s", status); + snprintf(icon_title, sizeof(icon_title), "QEMU"); + } + + if (scon->real_window) { + SDL_SetWindowTitle(scon->real_window, win_title); + } +#endif +} + +static void sdl_hide_cursor(void) +{ + if (!cursor_hide) { + return; + } + + SDL_ShowCursor(SDL_DISABLE); + SDL_SetCursor(sdl_cursor_hidden); + + if (!qemu_input_is_absolute()) { + SDL_SetRelativeMouseMode(SDL_TRUE); + } +} + +static void sdl_show_cursor(void) +{ + if (!cursor_hide) { + return; + } + + if (!qemu_input_is_absolute()) { + SDL_SetRelativeMouseMode(SDL_FALSE); + } + + if (guest_cursor && + (gui_grab || qemu_input_is_absolute() || absolute_enabled)) { + SDL_SetCursor(guest_sprite); + } else { + SDL_SetCursor(sdl_cursor_normal); + } + + SDL_ShowCursor(SDL_ENABLE); +} + +static void sdl_grab_start(struct sdl2_console *scon) +{ +#if 0 + QemuConsole *con = scon ? scon->dcl.con : NULL; + + if (!con || !qemu_console_is_graphic(con)) { + return; + } + /* + * If the application is not active, do not try to enter grab state. This + * prevents 'SDL_WM_GrabInput(SDL_GRAB_ON)' from blocking all the + * application (SDL bug). + */ + if (!(SDL_GetWindowFlags(scon->real_window) & SDL_WINDOW_INPUT_FOCUS)) { + return; + } + if (guest_cursor) { + SDL_SetCursor(guest_sprite); + if (!qemu_input_is_absolute() && !absolute_enabled) { + SDL_WarpMouseInWindow(scon->real_window, guest_x, guest_y); + } + } else { + sdl_hide_cursor(); + } + SDL_SetWindowGrab(scon->real_window, SDL_TRUE); + gui_grab = 1; + sdl_update_caption(scon); +#endif +} + +static void sdl_grab_end(struct sdl2_console *scon) +{ + SDL_SetWindowGrab(scon->real_window, SDL_FALSE); + gui_grab = 0; + sdl_show_cursor(); + sdl_update_caption(scon); +} + +static void absolute_mouse_grab(struct sdl2_console *scon) +{ + int mouse_x, mouse_y; + int scr_w, scr_h; + SDL_GetMouseState(&mouse_x, &mouse_y); + SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h); + if (mouse_x > 0 && mouse_x < scr_w - 1 && + mouse_y > 0 && mouse_y < scr_h - 1) { + sdl_grab_start(scon); + } +} + +static void sdl_mouse_mode_change(Notifier *notify, void *data) +{ + if (qemu_input_is_absolute()) { + if (!absolute_enabled) { + absolute_enabled = 1; + SDL_SetRelativeMouseMode(SDL_FALSE); + absolute_mouse_grab(&sdl2_console[0]); + } + } else if (absolute_enabled) { + if (!gui_fullscreen) { + sdl_grab_end(&sdl2_console[0]); + } + absolute_enabled = 0; + } +} + +static void sdl_send_mouse_event(struct sdl2_console *scon, int dx, int dy, + int x, int y, int state) +{ + static uint32_t bmap[INPUT_BUTTON__MAX] = { + [INPUT_BUTTON_LEFT] = SDL_BUTTON(SDL_BUTTON_LEFT), + [INPUT_BUTTON_MIDDLE] = SDL_BUTTON(SDL_BUTTON_MIDDLE), + [INPUT_BUTTON_RIGHT] = SDL_BUTTON(SDL_BUTTON_RIGHT), + }; + static uint32_t prev_state; + + if (prev_state != state) { + qemu_input_update_buttons(scon->dcl.con, bmap, prev_state, state); + prev_state = state; + } + + if (qemu_input_is_absolute()) { + qemu_input_queue_abs(scon->dcl.con, INPUT_AXIS_X, + x, 0, surface_width(scon->surface)); + qemu_input_queue_abs(scon->dcl.con, INPUT_AXIS_Y, + y, 0, surface_height(scon->surface)); + } else { + if (guest_cursor) { + x -= guest_x; + y -= guest_y; + guest_x += x; + guest_y += y; + dx = x; + dy = y; + } + qemu_input_queue_rel(scon->dcl.con, INPUT_AXIS_X, dx); + qemu_input_queue_rel(scon->dcl.con, INPUT_AXIS_Y, dy); + } + qemu_input_event_sync(); +} + +static void toggle_full_screen(struct sdl2_console *scon) +{ + gui_fullscreen = !gui_fullscreen; + if (gui_fullscreen) { + SDL_SetWindowFullscreen(scon->real_window, + SDL_WINDOW_FULLSCREEN_DESKTOP); + gui_saved_grab = gui_grab; + sdl_grab_start(scon); + } else { + if (!gui_saved_grab) { + sdl_grab_end(scon); + } + SDL_SetWindowFullscreen(scon->real_window, 0); + } + + // Note: If this gets called while rendering HUD, we will draw twice. Just + // wait for next refresh. +#if 0 + sdl2_redraw(scon); +#endif +} + +static int get_mod_state(void) +{ + SDL_Keymod mod = SDL_GetModState(); + + if (alt_grab) { + return (mod & (gui_grab_code | KMOD_LSHIFT)) == + (gui_grab_code | KMOD_LSHIFT); + } else if (ctrl_grab) { + return (mod & KMOD_RCTRL) == KMOD_RCTRL; + } else { + return (mod & gui_grab_code) == gui_grab_code; + } +} + +static void handle_keydown(SDL_Event *ev) +{ + int win; + struct sdl2_console *scon = get_scon_from_window(ev->key.windowID); + int gui_key_modifier_pressed = get_mod_state(); + int gui_keysym = 0; + + if (!scon->ignore_hotkeys && gui_key_modifier_pressed && !ev->key.repeat) { + switch (ev->key.keysym.scancode) { + case SDL_SCANCODE_2: + case SDL_SCANCODE_3: + case SDL_SCANCODE_4: + case SDL_SCANCODE_5: + case SDL_SCANCODE_6: + case SDL_SCANCODE_7: + case SDL_SCANCODE_8: + case SDL_SCANCODE_9: + if (gui_grab) { + sdl_grab_end(scon); + } + + win = ev->key.keysym.scancode - SDL_SCANCODE_1; + if (win < sdl2_num_outputs) { + sdl2_console[win].hidden = !sdl2_console[win].hidden; + if (sdl2_console[win].real_window) { + if (sdl2_console[win].hidden) { + SDL_HideWindow(sdl2_console[win].real_window); + } else { + SDL_ShowWindow(sdl2_console[win].real_window); + } + } + gui_keysym = 1; + } + break; + case SDL_SCANCODE_F: + toggle_full_screen(scon); + gui_keysym = 1; + break; + case SDL_SCANCODE_G: + gui_keysym = 1; + if (!gui_grab) { + sdl_grab_start(scon); + } else if (!gui_fullscreen) { + sdl_grab_end(scon); + } + break; + case SDL_SCANCODE_U: + sdl2_window_resize(scon); + gui_keysym = 1; + break; + default: + break; + } + } + if (!gui_keysym) { + sdl2_process_key(scon, &ev->key); + } +} + +static void handle_keyup(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->key.windowID); + + scon->ignore_hotkeys = false; + sdl2_process_key(scon, &ev->key); +} + +static void handle_textinput(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->text.windowID); + QemuConsole *con = scon ? scon->dcl.con : NULL; + + if (qemu_console_is_graphic(con)) { + return; + } + kbd_put_string_console(con, ev->text.text, strlen(ev->text.text)); +} + +static void handle_mousemotion(SDL_Event *ev) +{ + int max_x, max_y; + struct sdl2_console *scon = get_scon_from_window(ev->motion.windowID); + + if (!scon || !qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + if (qemu_input_is_absolute() || absolute_enabled) { + int scr_w, scr_h; + SDL_GetWindowSize(scon->real_window, &scr_w, &scr_h); + max_x = scr_w - 1; + max_y = scr_h - 1; + if (gui_grab && !gui_fullscreen + && (ev->motion.x == 0 || ev->motion.y == 0 || + ev->motion.x == max_x || ev->motion.y == max_y)) { + sdl_grab_end(scon); + } + if (!gui_grab && + (ev->motion.x > 0 && ev->motion.x < max_x && + ev->motion.y > 0 && ev->motion.y < max_y)) { + sdl_grab_start(scon); + } + } + if (gui_grab || qemu_input_is_absolute() || absolute_enabled) { + sdl_send_mouse_event(scon, ev->motion.xrel, ev->motion.yrel, + ev->motion.x, ev->motion.y, ev->motion.state); + } +} + +static void handle_mousebutton(SDL_Event *ev) +{ + int buttonstate = SDL_GetMouseState(NULL, NULL); + SDL_MouseButtonEvent *bev; + struct sdl2_console *scon = get_scon_from_window(ev->button.windowID); + + if (!scon || !qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + bev = &ev->button; + if (!gui_grab && !qemu_input_is_absolute()) { + if (ev->type == SDL_MOUSEBUTTONUP && bev->button == SDL_BUTTON_LEFT) { + /* start grabbing all events */ + sdl_grab_start(scon); + } + } else { + if (ev->type == SDL_MOUSEBUTTONDOWN) { + buttonstate |= SDL_BUTTON(bev->button); + } else { + buttonstate &= ~SDL_BUTTON(bev->button); + } + sdl_send_mouse_event(scon, 0, 0, bev->x, bev->y, buttonstate); + } +} + +static void handle_mousewheel(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->wheel.windowID); + SDL_MouseWheelEvent *wev = &ev->wheel; + InputButton btn; + + if (!scon || !qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + if (wev->y > 0) { + btn = INPUT_BUTTON_WHEEL_UP; + } else if (wev->y < 0) { + btn = INPUT_BUTTON_WHEEL_DOWN; + } else { + return; + } + + qemu_input_queue_btn(scon->dcl.con, btn, true); + qemu_input_event_sync(); + qemu_input_queue_btn(scon->dcl.con, btn, false); + qemu_input_event_sync(); +} + +static void handle_windowevent(SDL_Event *ev) +{ + struct sdl2_console *scon = get_scon_from_window(ev->window.windowID); + bool allow_close = true; + + if (!scon) { + return; + } + + switch (ev->window.event) { + case SDL_WINDOWEVENT_RESIZED: + { + QemuUIInfo info; + memset(&info, 0, sizeof(info)); + info.width = ev->window.data1; + info.height = ev->window.data2; + dpy_set_ui_info(scon->dcl.con, &info); + } + sdl2_redraw(scon); + break; + case SDL_WINDOWEVENT_EXPOSED: + sdl2_redraw(scon); + break; + case SDL_WINDOWEVENT_FOCUS_GAINED: + case SDL_WINDOWEVENT_ENTER: + if (!gui_grab && (qemu_input_is_absolute() || absolute_enabled)) { + absolute_mouse_grab(scon); + } + /* If a new console window opened using a hotkey receives the + * focus, SDL sends another KEYDOWN event to the new window, + * closing the console window immediately after. + * + * Work around this by ignoring further hotkey events until a + * key is released. + */ + scon->ignore_hotkeys = get_mod_state(); + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + if (gui_grab && !gui_fullscreen) { + sdl_grab_end(scon); + } + break; + case SDL_WINDOWEVENT_RESTORED: + update_displaychangelistener(&scon->dcl, GUI_REFRESH_INTERVAL_DEFAULT); + break; + case SDL_WINDOWEVENT_MINIMIZED: + update_displaychangelistener(&scon->dcl, 500); + break; + case SDL_WINDOWEVENT_CLOSE: + if (qemu_console_is_graphic(scon->dcl.con)) { + if (scon->opts->has_window_close && !scon->opts->window_close) { + allow_close = false; + } + if (allow_close) { + no_shutdown = 0; + qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); + } + } else { + SDL_HideWindow(scon->real_window); + scon->hidden = true; + } + break; + case SDL_WINDOWEVENT_SHOWN: + scon->hidden = false; + break; + case SDL_WINDOWEVENT_HIDDEN: + scon->hidden = true; + break; + } +} + +void sdl2_poll_events(struct sdl2_console *scon) +{ + SDL_Event ev1, *ev = &ev1; + bool allow_close = true; + + if (scon->last_vm_running != runstate_is_running()) { + scon->last_vm_running = runstate_is_running(); + sdl_update_caption(scon); + } + + while (SDL_PollEvent(ev)) { + int kbd = 0, mouse = 0; + xemu_input_process_sdl_events(ev); + xemu_hud_process_sdl_events(ev); + xemu_input_update_controllers(); + xemu_hud_should_capture_kbd_mouse(&kbd, &mouse); + + switch (ev->type) { + case SDL_KEYDOWN: + if (kbd) break; + handle_keydown(ev); + break; + case SDL_KEYUP: + if (kbd) break; + handle_keyup(ev); + break; + case SDL_TEXTINPUT: + if (kbd) break; + handle_textinput(ev); + break; + case SDL_QUIT: + if (scon->opts->has_window_close && !scon->opts->window_close) { + allow_close = false; + } + if (allow_close) { + no_shutdown = 0; + qemu_system_shutdown_request(SHUTDOWN_CAUSE_HOST_UI); + } + break; + case SDL_MOUSEMOTION: + if (mouse) break; + handle_mousemotion(ev); + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + if (mouse) break; + handle_mousebutton(ev); + break; + case SDL_MOUSEWHEEL: + if (mouse) break; + handle_mousewheel(ev); + break; + case SDL_WINDOWEVENT: + handle_windowevent(ev); + break; + default: + break; + } + } + + scon->idle_counter = 0; + scon->dcl.update_interval = 16; // Ignored +} + +static void sdl_mouse_warp(DisplayChangeListener *dcl, + int x, int y, int on) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + if (!qemu_console_is_graphic(scon->dcl.con)) { + return; + } + + if (on) { + if (!guest_cursor) { + sdl_show_cursor(); + } + if (gui_grab || qemu_input_is_absolute() || absolute_enabled) { + SDL_SetCursor(guest_sprite); + if (!qemu_input_is_absolute() && !absolute_enabled) { + SDL_WarpMouseInWindow(scon->real_window, x, y); + } + } + } else if (gui_grab) { + sdl_hide_cursor(); + } + guest_cursor = on; + guest_x = x, guest_y = y; +} + +static void sdl_mouse_define(DisplayChangeListener *dcl, + QEMUCursor *c) +{ + + if (guest_sprite) { + SDL_FreeCursor(guest_sprite); + } + + if (guest_sprite_surface) { + SDL_FreeSurface(guest_sprite_surface); + } + + guest_sprite_surface = + SDL_CreateRGBSurfaceFrom(c->data, c->width, c->height, 32, c->width * 4, + 0xff0000, 0x00ff00, 0xff, 0xff000000); + + if (!guest_sprite_surface) { + fprintf(stderr, "Failed to make rgb surface from %p\n", c); + return; + } + guest_sprite = SDL_CreateColorCursor(guest_sprite_surface, + c->hot_x, c->hot_y); + if (!guest_sprite) { + fprintf(stderr, "Failed to make color cursor from %p\n", c); + return; + } + if (guest_cursor && + (gui_grab || qemu_input_is_absolute() || absolute_enabled)) { + SDL_SetCursor(guest_sprite); + } +} + +static void sdl_cleanup(void) +{ + if (guest_sprite) { + SDL_FreeCursor(guest_sprite); + } + SDL_QuitSubSystem(SDL_INIT_VIDEO); +} + +static const DisplayChangeListenerOps dcl_gl_ops = { + .dpy_name = "sdl2-gl", + .dpy_gfx_update = sdl2_gl_update, + .dpy_gfx_switch = sdl2_gl_switch, + .dpy_gfx_check_format = console_gl_check_format, + .dpy_refresh = sdl2_gl_refresh, + .dpy_mouse_set = sdl_mouse_warp, + .dpy_cursor_define = sdl_mouse_define, + + .dpy_gl_ctx_create = sdl2_gl_create_context, + .dpy_gl_ctx_destroy = sdl2_gl_destroy_context, + .dpy_gl_ctx_make_current = sdl2_gl_make_context_current, + .dpy_gl_ctx_get_current = sdl2_gl_get_current_context, + .dpy_gl_scanout_disable = sdl2_gl_scanout_disable, + .dpy_gl_scanout_texture = sdl2_gl_scanout_texture, + .dpy_gl_update = sdl2_gl_scanout_flush, +}; + +SDL_Window *m_window; +SDL_GLContext m_context; + +static void sdl2_display_early_init(DisplayOptions *o) +{ + assert(o->type == DISPLAY_TYPE_XEMU); + display_opengl = 1; + + fprintf(stderr, "%s\n", __func__); + +#ifdef __linux__ + /* on Linux, SDL may use fbcon|directfb|svgalib when run without + * accessible $DISPLAY to open X11 window. This is often the case + * when qemu is run using sudo. But in this case, and when actually + * run in X11 environment, SDL fights with X11 for the video card, + * making current display unavailable, often until reboot. + * So make x11 the default SDL video driver if this variable is unset. + * This is a bit hackish but saves us from bigger problem. + * Maybe it's a good idea to fix this in SDL instead. + */ + setenv("SDL_VIDEODRIVER", "x11", 0); +#endif + + if (SDL_Init(SDL_INIT_VIDEO)) { + fprintf(stderr, "Could not initialize SDL(%s) - exiting\n", + SDL_GetError()); + exit(1); + } +#ifdef SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR /* only available since SDL 2.0.8 */ + SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); +#endif + SDL_SetHint(SDL_HINT_GRAB_KEYBOARD, "1"); + + // Initialize rendering context + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + SDL_GL_SetAttribute( + SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetSwapInterval(1); + + // Create main window + m_window = SDL_CreateWindow( + "xemu", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + 1024, 768, + SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + if (m_window == NULL) { + fprintf(stderr, "Failed to create main window\n"); + SDL_Quit(); + exit(1); + } + + m_context = SDL_GL_CreateContext(m_window); + assert(m_context != NULL); + if (m_context == NULL) { + fprintf(stderr, "%s: Failed to create GL context\n", __func__); + SDL_DestroyWindow(m_window); + SDL_Quit(); + exit(1); + } + + SDL_GL_MakeCurrent(m_window, m_context); + xemu_hud_init(m_window, m_context); + blit = create_decal_shader(SHADER_TYPE_BLIT); +} + +static void sdl2_display_init(DisplayState *ds, DisplayOptions *o) +{ + uint8_t data = 0; + int i; + SDL_SysWMinfo info; + SDL_Surface *icon = NULL; + + assert(o->type == DISPLAY_TYPE_XEMU); + + xemu_input_init(); + xemu_settings_get_enum(XEMU_SETTINGS_DISPLAY_SCALE, &scaling_mode); + + memset(&info, 0, sizeof(info)); + SDL_VERSION(&info.version); + + gui_fullscreen = o->has_full_screen && o->full_screen; + +#if 1 + // Explicitly set number of outputs to 1 for a single screen. We don't need + // multiple for now, but maybe in the future debug stuff can go on a second + // screen. + sdl2_num_outputs = 1; +#else + for (i = 0;; i++) { + QemuConsole *con = qemu_console_lookup_by_index(i); + if (!con) { + break; + } + } + sdl2_num_outputs = i; + if (sdl2_num_outputs == 0) { + return; + } +#endif + + sdl2_console = g_new0(struct sdl2_console, sdl2_num_outputs); + for (i = 0; i < sdl2_num_outputs; i++) { + QemuConsole *con = qemu_console_lookup_by_index(i); + assert(con != NULL); + if (!qemu_console_is_graphic(con) && + qemu_console_get_index(con) != 0) { + sdl2_console[i].hidden = true; + } + sdl2_console[i].idx = i; + sdl2_console[i].opts = o; + sdl2_console[i].opengl = 1; + sdl2_console[i].dcl.ops = &dcl_gl_ops; + sdl2_console[i].dcl.con = con; + sdl2_console[i].kbd = qkbd_state_init(con); + register_displaychangelistener(&sdl2_console[i].dcl); + +#if defined(SDL_VIDEO_DRIVER_WINDOWS) || defined(SDL_VIDEO_DRIVER_X11) + if (SDL_GetWindowWMInfo(sdl2_console[i].real_window, &info)) { +#if defined(SDL_VIDEO_DRIVER_WINDOWS) + qemu_console_set_window_id(con, (uintptr_t)info.info.win.window); +#elif defined(SDL_VIDEO_DRIVER_X11) + qemu_console_set_window_id(con, info.info.x11.window); +#endif + } +#endif + } + + int width, height, channels = 0; + stbi_set_flip_vertically_on_load(0); + unsigned char *icon_data = stbi_load("./data/xemu_64x64.png", &width, &height, &channels, 4); + if (icon_data) { + icon = SDL_CreateRGBSurfaceFrom(icon_data, width, height, 32, width*4, + 0x000000ff, + 0x0000ff00, + 0x00ff0000, + 0xff000000 + ); + // Note: Retaining the memory allocated by stbi_load. It's used in-place + // by the SDL surface. + } + if (icon) { + SDL_SetWindowIcon(sdl2_console[0].real_window, icon); + } + + gui_grab = 0; + if (gui_fullscreen) { + sdl_grab_start(0); + } + + mouse_mode_notifier.notify = sdl_mouse_mode_change; + qemu_add_mouse_mode_change_notifier(&mouse_mode_notifier); + + sdl_cursor_hidden = SDL_CreateCursor(&data, &data, 8, 1, 0, 0); + sdl_cursor_normal = SDL_GetCursor(); + + atexit(sdl_cleanup); +} + +static QemuDisplay qemu_display_sdl2 = { + .type = DISPLAY_TYPE_XEMU, + .early_init = sdl2_display_early_init, + .init = sdl2_display_init, +}; + +static void register_sdl1(void) +{ + qemu_display_register(&qemu_display_sdl2); +} + +type_init(register_sdl1); + +#include "qemu/osdep.h" +#include "ui/console.h" +#include "ui/input.h" +#include "ui/sdl2.h" + +void xb_surface_gl_create_texture(QemuGLShader *gls, + DisplaySurface *surface) +{ + // assert(gls); + assert(QEMU_IS_ALIGNED(surface_stride(surface), surface_bytes_per_pixel(surface))); + + switch (surface->format) { + case PIXMAN_BE_b8g8r8x8: + case PIXMAN_BE_b8g8r8a8: + surface->glformat = GL_BGRA_EXT; + surface->gltype = GL_UNSIGNED_BYTE; + break; + case PIXMAN_BE_x8r8g8b8: + case PIXMAN_BE_a8r8g8b8: + surface->glformat = GL_RGBA; + surface->gltype = GL_UNSIGNED_BYTE; + break; + case PIXMAN_r5g6b5: + surface->glformat = GL_RGB; + surface->gltype = GL_UNSIGNED_SHORT_5_6_5; + break; + default: + g_assert_not_reached(); + } + + glGenTextures(1, &surface->texture); + // glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, surface->texture); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, + surface_stride(surface) / surface_bytes_per_pixel(surface)); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, + surface_width(surface), + surface_height(surface), + 0, surface->glformat, surface->gltype, + surface_data(surface)); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, 0); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +} + +void xb_surface_gl_update_texture(QemuGLShader *gls, + DisplaySurface *surface, + int x, int y, int w, int h) +{ + uint8_t *data = (void *)surface_data(surface); + + // assert(gls); + + if (surface->texture) { + glBindTexture(GL_TEXTURE_2D, surface->texture); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, + surface_stride(surface) + / surface_bytes_per_pixel(surface)); + glTexSubImage2D(GL_TEXTURE_2D, 0, + x, y, w, h, + surface->glformat, surface->gltype, + data + surface_stride(surface) * y + + surface_bytes_per_pixel(surface) * x); + glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, 0); + } +} + +void xb_surface_gl_destroy_texture(QemuGLShader *gls, + DisplaySurface *surface) +{ + if (!surface || !surface->texture) { + return; + } + glDeleteTextures(1, &surface->texture); + surface->texture = 0; +} + +#if 0 +static void sdl2_set_scanout_mode(struct sdl2_console *scon, bool scanout) +{ + if (scon->scanout_mode == scanout) { + return; + } + + scon->scanout_mode = scanout; + if (!scon->scanout_mode) { + egl_fb_destroy(&scon->guest_fb); + if (scon->surface) { + surface_gl_destroy_texture(scon->gls, scon->surface); + surface_gl_create_texture(scon->gls, scon->surface); + } + } +} +#endif + +static void xemu_sdl2_gl_render_surface(struct sdl2_console *scon) +{ + int ww, wh; + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + // sdl2_set_scanout_mode(scon, false); + SDL_GL_GetDrawableSize(scon->real_window, &ww, &wh); + + // Get texture dimensions + int tw, th; + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, scon->surface->texture); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th); + + // Calculate scaling factors + float scale[2]; + + if (scaling_mode == DISPLAY_SCALE_STRETCH) { + // Stretch to fit + scale[0] = 1.0; + scale[1] = 1.0; + } else if (scaling_mode == DISPLAY_SCALE_CENTER) { + // Centered + scale[0] = (float)tw/(float)ww; + scale[1] = (float)th/(float)wh; + } else { + // Scale to fit + float t_ratio = (float)tw/(float)th; + float w_ratio = (float)ww/(float)wh; + if (w_ratio >= t_ratio) { + scale[0] = t_ratio/w_ratio; + scale[1] = 1.0; + } else { + scale[0] = 1.0; + scale[1] = w_ratio/t_ratio; + } + } + + struct decal_shader *s = blit; + s->flip = 1; + + glViewport(0, 0, ww, wh); + glUseProgram(s->prog); + glBindVertexArray(s->vao); + glUniform1i(s->FlipY_loc, s->flip); + glUniform4f(s->ScaleOffset_loc, scale[0], scale[1], 0, 0); + glUniform4f(s->TexScaleOffset_loc, 1.0, 1.0, 0, 0); + glUniform1i(s->tex_loc, 0); + + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL); + + xemu_hud_render(scon->real_window); + + // xb_surface_gl_render_texture(scon->gls, scon->surface); + pre_swap(); + SDL_GL_SwapWindow(scon->real_window); + post_swap(); +} + +void sdl2_gl_update(DisplayChangeListener *dcl, + int x, int y, int w, int h) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + assert(scon->opengl); +#if 1 + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + if (scon->surface) { + // glDeleteTextures(1, &scon->surface->texture); + xb_surface_gl_destroy_texture(scon->gls, scon->surface); + // assert(glGetError() == GL_NO_ERROR); + } + xb_surface_gl_create_texture(scon->gls, scon->surface); +#endif + scon->updates++; +} + +void sdl2_gl_switch(DisplayChangeListener *dcl, + DisplaySurface *new_surface) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + DisplaySurface *old_surface = scon->surface; + + assert(scon->opengl); + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + xb_surface_gl_destroy_texture(scon->gls, scon->surface); + + scon->surface = new_surface; + + if (!new_surface) { + // qemu_gl_fini_shader(scon->gls); + scon->gls = NULL; + // sdl2_window_destroy(scon); + return; + } + + if (!scon->real_window) { + // sdl2_window_create(scon); + scon->real_window = m_window; + scon->winctx = m_context; + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + + // scon->gls = qemu_gl_init_shader(); + } else if (old_surface && + ((surface_width(old_surface) != surface_width(new_surface)) || + (surface_height(old_surface) != surface_height(new_surface)))) { + // sdl2_window_resize(scon); + } + + xb_surface_gl_create_texture(scon->gls, scon->surface); +} + +void sdl2_gl_refresh(DisplayChangeListener *dcl) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + assert(scon->opengl); + + graphic_hw_update(dcl->con); + if (scon->updates && scon->surface) { + scon->updates = 0; + } + xemu_sdl2_gl_render_surface(scon); + sdl2_poll_events(scon); +} + +void sdl2_gl_redraw(struct sdl2_console *scon) +{ + assert(scon->opengl); + + if (scon->scanout_mode) { + assert(0); + /* sdl2_gl_scanout_flush actually only care about + * the first argument. */ + // return sdl2_gl_scanout_flush(&scon->dcl, 0, 0, 0, 0); + } + if (scon->surface) { + xemu_sdl2_gl_render_surface(scon); + } +} + +QEMUGLContext sdl2_gl_create_context(DisplayChangeListener *dcl, + QEMUGLParams *params) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + SDL_GLContext ctx; + + assert(0); + + assert(scon->opengl); + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + + SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1); + if (scon->opts->gl == DISPLAYGL_MODE_ON || + scon->opts->gl == DISPLAYGL_MODE_CORE) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_CORE); + } else if (scon->opts->gl == DISPLAYGL_MODE_ES) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_ES); + } + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, params->major_ver); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, params->minor_ver); + + ctx = SDL_GL_CreateContext(scon->real_window); + + /* If SDL fail to create a GL context and we use the "on" flag, + * then try to fallback to GLES. + */ + if (!ctx && scon->opts->gl == DISPLAYGL_MODE_ON) { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_ES); + ctx = SDL_GL_CreateContext(scon->real_window); + } + return (QEMUGLContext)ctx; +} + +void sdl2_gl_destroy_context(DisplayChangeListener *dcl, QEMUGLContext ctx) +{ + SDL_GLContext sdlctx = (SDL_GLContext)ctx; + + SDL_GL_DeleteContext(sdlctx); +} + +int sdl2_gl_make_context_current(DisplayChangeListener *dcl, + QEMUGLContext ctx) +{ + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + SDL_GLContext sdlctx = (SDL_GLContext)ctx; + + assert(scon->opengl); + + return SDL_GL_MakeCurrent(scon->real_window, sdlctx); +} + +QEMUGLContext sdl2_gl_get_current_context(DisplayChangeListener *dcl) +{ + SDL_GLContext sdlctx; + + sdlctx = SDL_GL_GetCurrentContext(); + return (QEMUGLContext)sdlctx; +} + +void sdl2_gl_scanout_disable(DisplayChangeListener *dcl) +{ + assert(0); +#if 0 + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + assert(scon->opengl); + scon->w = 0; + scon->h = 0; + sdl2_set_scanout_mode(scon, false); +#endif +} + +void sdl2_gl_scanout_texture(DisplayChangeListener *dcl, + uint32_t backing_id, + bool backing_y_0_top, + uint32_t backing_width, + uint32_t backing_height, + uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + assert(0); +#if 0 + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + + assert(scon->opengl); + scon->x = x; + scon->y = y; + scon->w = w; + scon->h = h; + scon->y0_top = backing_y_0_top; + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + + sdl2_set_scanout_mode(scon, true); + egl_fb_setup_for_tex(&scon->guest_fb, backing_width, backing_height, + backing_id, false); +#endif +} + +void sdl2_gl_scanout_flush(DisplayChangeListener *dcl, + uint32_t x, uint32_t y, uint32_t w, uint32_t h) +{ + assert(0); +#if 0 + struct sdl2_console *scon = container_of(dcl, struct sdl2_console, dcl); + int ww, wh; + + assert(scon->opengl); + if (!scon->scanout_mode) { + return; + } + if (!scon->guest_fb.framebuffer) { + return; + } + + SDL_GL_MakeCurrent(scon->real_window, scon->winctx); + + SDL_GetWindowSize(scon->real_window, &ww, &wh); + egl_fb_setup_default(&scon->win_fb, ww, wh); + egl_fb_blit(&scon->win_fb, &scon->guest_fb, !scon->y0_top); + + SDL_GL_SwapWindow(scon->real_window); +#endif +}