diff --git a/10bpc-gl.diff b/10bpc-gl.diff deleted file mode 100644 index 17e1878b103..00000000000 --- a/10bpc-gl.diff +++ /dev/null @@ -1,96 +0,0 @@ -diff --git a/gfx/drivers_context/wgl_ctx.c b/gfx/drivers_context/wgl_ctx.c -index b90a8e40c3..e57c4df194 100644 ---- a/gfx/drivers_context/wgl_ctx.c -+++ b/gfx/drivers_context/wgl_ctx.c -@@ -78,6 +78,47 @@ - #ifndef WGL_CONTEXT_DEBUG_BIT_ARB - #define WGL_CONTEXT_DEBUG_BIT_ARB 0x0001 - #endif -+ -+#ifndef WGL_ACCELERATION_ARB -+#define WGL_ACCELERATION_ARB 0x2003 -+#endif -+ -+#ifndef WGL_FULL_ACCELERATION_ARB -+#define WGL_FULL_ACCELERATION_ARB 0x2027 -+#endif -+ -+#ifndef WGL_DRAW_TO_WINDOW_ARB -+#define WGL_DRAW_TO_WINDOW_ARB 0x2001 -+#endif -+ -+#ifndef WGL_DOUBLE_BUFFER_ARB -+#define WGL_DOUBLE_BUFFER_ARB 0x2011 -+#endif -+ -+#ifndef WGL_RED_BITS_ARB -+#define WGL_RED_BITS_ARB 0x2015 -+#endif -+ -+#ifndef WGL_GREEN_BITS_ARB -+#define WGL_GREEN_BITS_ARB 0x2017 -+#endif -+ -+#ifndef WGL_BLUE_BITS_ARB -+#define WGL_BLUE_BITS_ARB 0x2019 -+#endif -+ -+#ifndef WGL_ALPHA_BITS_ARB -+#define WGL_ALPHA_BITS_ARB 0x201B -+#endif -+ -+#ifndef WGL_PIXEL_TYPE_ARB -+#define WGL_PIXEL_TYPE_ARB 0x2013 -+#endif -+ -+#ifndef WGL_TYPE_RGBA_ARB -+#define WGL_TYPE_RGBA_ARB 0x202B -+#endif -+ - #endif - - #if defined(HAVE_OPENGL) -@@ -313,6 +354,43 @@ static void create_gl_context(HWND hwnd, bool *quit) - RARCH_LOG("[WGL]: Adaptive VSync supported.\n"); - wgl_adaptive_vsync = true; - } -+ if (wgl_has_extension("WGL_ARB_pixel_format", extensions)) -+ { -+ BOOL (WINAPI * wglChoosePixelFormatARB) -+ (HDC hdc, -+ const int *piAttribIList, -+ const FLOAT *pfAttribFList, -+ UINT nMaxFormats, -+ int *piFormats, -+ UINT *nNumFormats); -+ UINT nMatchingFormats; -+ int index = 0; -+ int attribsDesired[] = { -+ WGL_DRAW_TO_WINDOW_ARB, 1, -+ WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, -+ WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, -+ WGL_RED_BITS_ARB, 10, -+ WGL_GREEN_BITS_ARB, 10, -+ WGL_BLUE_BITS_ARB, 10, -+ WGL_ALPHA_BITS_ARB, 2, -+ WGL_DOUBLE_BUFFER_ARB, 1, -+ 0,0 -+ }; -+ wglChoosePixelFormatARB = (BOOL (WINAPI *) (HDC, const int *, -+ const FLOAT*, UINT, int*, UINT*)) -+ gfx_ctx_wgl_get_proc_address("wglChoosePixelFormatARB"); -+ -+ RARCH_LOG("[WGL]: ARB pixel format supported.\n"); -+ -+ if (wglChoosePixelFormatARB(win32_hdc, attribsDesired, -+ NULL, 1, &index, &nMatchingFormats)) -+ { -+ if (nMatchingFormats == 0) -+ { -+ RARCH_WARN("No 10bpc WGL_ARB_pixel_formats found!\n"); -+ } -+ } -+ } - } - } - #endif diff --git a/audio/drivers/coreaudio_mic_ios.m b/audio/drivers/coreaudio_mic_ios.m index 9cc6d792781..af2683d3d70 100644 --- a/audio/drivers/coreaudio_mic_ios.m +++ b/audio/drivers/coreaudio_mic_ios.m @@ -87,15 +87,19 @@ static OSStatus coreaudio_input_callback( if (FIFO_WRITE_AVAIL(microphone->sample_buffer) >= actual_bytes) fifo_write(microphone->sample_buffer, bufferList.mBuffers[0].mData, actual_bytes); +#ifdef DEBUG else if (microphone->drop_count++ % 1000 == 0) RARCH_WARN("[CoreAudio] FIFO full, dropping %u bytes.\n", (unsigned)actual_bytes); +#endif scond_signal(microphone->fifo_cond); slock_unlock(microphone->fifo_lock); } } +#ifdef DEBUG else if (status != noErr) RARCH_ERR("[CoreAudio] Failed to render audio: %d.\n", (int)status); +#endif return noErr; } @@ -105,10 +109,7 @@ static OSStatus coreaudio_input_callback( { coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)calloc(1, sizeof(*microphone)); if (!microphone) - { - RARCH_ERR("[CoreAudio] Failed to allocate microphone driver.\n"); return NULL; - } microphone->sample_rate = 0; microphone->nonblock = false; @@ -150,14 +151,11 @@ static void coreaudio_microphone_free(void *driver_context) static int coreaudio_microphone_read(void *driver_context, void *microphone_context, void *buf, size_t size) { - coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)driver_context; size_t avail, read_amt; + coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)driver_context; if (!microphone || !buf) - { - RARCH_ERR("[CoreAudio] Invalid parameters in read.\n"); return -1; - } slock_lock(microphone->fifo_lock); @@ -219,10 +217,7 @@ static void coreaudio_microphone_set_format(coreaudio_microphone_t *microphone, { coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)driver_context; if (!microphone) - { - RARCH_ERR("[CoreAudio] Invalid driver context.\n"); return NULL; - } /* Guard against calling open_mic twice without close_mic */ if (microphone->audio_unit) @@ -300,10 +295,7 @@ static void coreaudio_microphone_set_format(coreaudio_microphone_t *microphone, } microphone->sample_buffer = fifo_new(fifoBufferSize); if (!microphone->sample_buffer) - { - RARCH_ERR("[CoreAudio] Failed to create sample buffer.\n"); return NULL; - } } /* Initialize audio unit */ @@ -436,10 +428,7 @@ static void coreaudio_microphone_close_mic(void *driver_context, void *microphon { coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context; if (!microphone) - { - RARCH_ERR("[CoreAudio] Failed to close microphone.\n"); return; - } if (microphone->is_running) { @@ -472,10 +461,7 @@ static bool coreaudio_microphone_start_mic(void *driver_context, void *microphon { coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context; if (!microphone) - { - RARCH_ERR("[CoreAudio] Failed to start microphone.\n"); return false; - } if (microphone->sample_buffer) { @@ -501,10 +487,7 @@ static bool coreaudio_microphone_stop_mic(void *driver_context, void *microphone { coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context; if (!microphone) - { - RARCH_ERR("[CoreAudio] Failed to stop microphone.\n"); return false; - } if (microphone->is_running) { diff --git a/audio/drivers/coreaudio_mic_macos.m b/audio/drivers/coreaudio_mic_macos.m index e7cc7dccb27..5ffe516b97a 100644 --- a/audio/drivers/coreaudio_mic_macos.m +++ b/audio/drivers/coreaudio_mic_macos.m @@ -228,16 +228,20 @@ static OSStatus coreaudio_macos_input_callback(void *inRefCon, if (FIFO_WRITE_AVAIL(mic->fifo) >= actual_bytes) fifo_write(mic->fifo, buffer_list.mBuffers[0].mData, actual_bytes); +#ifdef DEBUG else if (mic->drop_count++ % 1000 == 0) RARCH_WARN("[CoreAudio macOS Mic] FIFO full, dropping %u bytes\n", (unsigned)actual_bytes); +#endif scond_signal(mic->fifo_cond); slock_unlock(mic->fifo_lock); } } +#ifdef DEBUG else if (status != noErr) RARCH_ERR("[CoreAudio macOS Mic] Failed to render audio: %d\n", (int)status); +#endif /* Always return noErr — returning errors may cause CoreAudio to * stop invoking the callback entirely. */ @@ -335,10 +339,7 @@ void coreaudio_macos_microphone_free(void *data) { struct string_list *list = string_list_new(); if (!list) - { - RARCH_ERR("[CoreAudio macOS Mic] Failed to create string_list.\n"); return NULL; - } AudioObjectPropertyAddress prop_addr_devices = { kAudioHardwarePropertyDevices, @@ -350,7 +351,6 @@ void coreaudio_macos_microphone_free(void *data) OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop_addr_devices, 0, NULL, &propsize); if (status != noErr || propsize == 0) { - RARCH_ERR("[CoreAudio macOS Mic] Error getting size of device list: %d\n", (int)status); string_list_free(list); return NULL; } @@ -359,7 +359,6 @@ void coreaudio_macos_microphone_free(void *data) AudioDeviceID *all_devices = (AudioDeviceID *)malloc(propsize); if (!all_devices) { - RARCH_ERR("[CoreAudio macOS Mic] Failed to allocate memory for device list.\n"); string_list_free(list); return NULL; } @@ -367,7 +366,6 @@ void coreaudio_macos_microphone_free(void *data) status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop_addr_devices, 0, NULL, &propsize, all_devices); if (status != noErr) { - RARCH_ERR("[CoreAudio macOS Mic] Error getting device list: %d\n", (int)status); free(all_devices); string_list_free(list); return NULL; @@ -484,13 +482,9 @@ static void coreaudio_macos_microphone_device_list_free(const void *data, struct /* For this driver, init() returns a placeholder (void*)1, so we don't use 'data' to allocate the mic instance. */ /* The actual mic instance (coreaudio_macos_microphone_t) is allocated below. */ - coreaudio_macos_microphone_t *mic = (coreaudio_macos_microphone_t *)calloc(1, sizeof(coreaudio_macos_microphone_t)); if (!mic) - { - RARCH_ERR("[CoreAudio macOS Mic] Failed to allocate memory for microphone context.\n"); return NULL; - } atomic_init(&mic->is_running, false); atomic_init(&mic->is_initialized, false); @@ -684,10 +678,7 @@ static void coreaudio_macos_microphone_device_list_free(const void *data, struct mic->callback_buffer_size = max_frames * mic->format.mBytesPerFrame; mic->callback_buffer = calloc(1, mic->callback_buffer_size); if (!mic->callback_buffer) - { - RARCH_ERR("[CoreAudio macOS Mic] Failed to allocate callback buffer\n"); goto error; - } } /* Initialize FIFO — size based on latency parameter */ @@ -699,10 +690,7 @@ static void coreaudio_macos_microphone_device_list_free(const void *data, struct * mic->format.mBytesPerFrame / 10); /* 100ms fallback */ mic->fifo = fifo_new(fifo_size); if (!mic->fifo) - { - RARCH_ERR("[CoreAudio macOS Mic] Failed to allocate FIFO buffer\n"); goto error; - } fifo_clear(mic->fifo); RARCH_LOG("[CoreAudio macOS Mic] FIFO buffer: %u bytes (%.0f ms)\n", (unsigned)fifo_size, @@ -932,10 +920,7 @@ static AudioDeviceID get_macos_device_id_for_uid_or_name(const char *uid_or_name UInt32 num_devices = propsize / sizeof(AudioDeviceID); AudioDeviceID *all_devices = (AudioDeviceID *)malloc(propsize); if (!all_devices) - { - RARCH_ERR("[CoreAudio macOS Mic] Failed to allocate memory for device list for UID lookup.\n"); return kAudioObjectUnknown; - } status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop_addr_devices, 0, NULL, &propsize, all_devices); if (status != noErr) diff --git a/configuration.c b/configuration.c index 00b86224e61..11128b1abaa 100644 --- a/configuration.c +++ b/configuration.c @@ -3599,6 +3599,15 @@ static bool check_menu_driver_compatibility(settings_t *settings) return (memcmp(video_driver, "metal", 5) == 0 && video_driver[5] == '\0'); case 'r': return (memcmp(video_driver, "rsx", 3) == 0 && video_driver[3] == '\0'); + case 's': + /* sdl2 supports the full menu set (XMB/Ozone/MaterialUI/RGUI) + * via gfx_display_ctx_sdl2 + sdl2_raster_font, gated on + * SDL_RenderGeometry (>= 2.0.18). On older SDL builds the + * driver self-disables the gfx_display backend, and only + * RGUI's bitmap path is functional - which already returned + * true via the rgui early-out above, so allowing sdl2 here + * is safe regardless of the runtime SDL version. */ + return (memcmp(video_driver, "sdl2", 4) == 0 && video_driver[4] == '\0'); case 'c': return (memcmp(video_driver, "ctr", 3) == 0 && video_driver[3] == '\0'); default: diff --git a/gfx/common/sdl2_common.h b/gfx/common/sdl2_common.h index ccaf537c8ab..d6dc63e435e 100644 --- a/gfx/common/sdl2_common.h +++ b/gfx/common/sdl2_common.h @@ -64,6 +64,31 @@ typedef struct _sdl2_video uint8_t font_b; uint8_t flags; + +#ifdef HAVE_OVERLAY + /* On-screen input overlay state. Each entry holds an SDL_Texture + * uploaded as ARGB8888 (matching the supports_rgba=false byte + * layout RetroArch hands us in load()) and rendered via + * SDL_RenderCopy with per-texture alpha modulation each frame. + * + * vert_coords / tex_coords are 4-float (x, y, w, h) tuples in + * 0..1 normalised space. vertex_geom flips y to (1.0f - y) and + * negates h, matching d3d8 / gl / d3d9_common; vertex_geom + * comments document the rationale - we undo the flip at render + * time in sdl2_overlays_render. */ + struct sdl2_overlay + { + SDL_Texture *tex; + unsigned tex_w; + unsigned tex_h; + float tex_coords[4]; + float vert_coords[4]; + float alpha_mod; + bool fullscreen; + } *overlays; + unsigned overlays_size; + bool overlays_enabled; +#endif } sdl2_video_t; void sdl2_set_handles(void *data, enum rarch_display_type diff --git a/gfx/drivers/gdi_gfx.c b/gfx/drivers/gdi_gfx.c index 0cdd7475843..7b9549fe885 100644 --- a/gfx/drivers/gdi_gfx.c +++ b/gfx/drivers/gdi_gfx.c @@ -2612,11 +2612,9 @@ static bool gdi_frame(void *data, const void *frame, unsigned surface_width; unsigned surface_height; - /* GDI does not implement these effects. XMB still calls into the - * shader-pipeline draw path, so we force-disable the bits that - * would route through it; this is preserved verbatim from the - * legacy driver. */ - video_info->xmb_shadows_enable = false; + /* GDI has no programmable shader pipeline, so the animated XMB + * backgrounds (Ribbon / Snow / Bokeh / etc.) can't run — force + * that off so XMB falls back to the static gradient. */ video_info->menu_shader_pipeline = 0; if (!frame || !frame_width || !frame_height) diff --git a/gfx/drivers/gl1.c b/gfx/drivers/gl1.c index 5c97b986554..a6a603f0c3e 100644 --- a/gfx/drivers/gl1.c +++ b/gfx/drivers/gl1.c @@ -1641,8 +1641,9 @@ static bool gl1_frame(void *data, const void *frame, &video_info->osd_stat_params; bool overlay_behind_menu = video_info->overlay_behind_menu; - /* FIXME: Force these settings off as they interfere with the rendering */ - video_info->xmb_shadows_enable = false; + /* gl1 fixed-function has no programmable pipeline, so the + * animated XMB backgrounds (Ribbon / Snow / Bokeh / etc.) can't + * run -- force that off so XMB falls back to the static gradient. */ video_info->menu_shader_pipeline = 0; if (gl1->flags & GL1_FLAG_SHOULD_RESIZE) diff --git a/gfx/drivers/sdl2_gfx.c b/gfx/drivers/sdl2_gfx.c index be0396313b5..c5101c9968c 100644 --- a/gfx/drivers/sdl2_gfx.c +++ b/gfx/drivers/sdl2_gfx.c @@ -17,9 +17,11 @@ #include #include +#include #include #include +#include #ifdef HAVE_CONFIG_H #include "../../config.h" @@ -33,6 +35,10 @@ #include "../../menu/menu_driver.h" #endif +#ifdef HAVE_GFX_WIDGETS +#include "../gfx_widgets.h" +#endif + #include "SDL.h" #include "SDL_syswm.h" #include "../common/sdl2_common.h" @@ -48,6 +54,10 @@ */ static void sdl2_gfx_free(void *data); +#ifdef HAVE_OVERLAY +static void sdl2_overlay_free(sdl2_video_t *vid); +static void sdl2_overlays_render(sdl2_video_t *vid); +#endif static INLINE void sdl_tex_zero(sdl2_tex_t *t) { @@ -234,6 +244,16 @@ static void sdl_refresh_viewport(sdl2_video_t *vid) vid->vp.full_height = win_h; video_driver_update_viewport(&vid->vp, false, vid->video.force_aspect, true); + /* Tell the rest of the engine about our actual window dimensions. + * Without this, video_st->width/height retain whatever value was + * computed from core geometry at init time (eg 320x240 for an + * NES-like core), so menu_driver_frame and gfx_widgets_frame + * receive a tiny video_height in video_info, position widgets + * relative to that tiny coordinate space, and end up drawing into + * the top-left corner of the actual SDL framebuffer. Most other + * drivers (vga, gx2, d3d8, d3d9 common) make this call too. */ + video_driver_set_size(win_w, win_h); + vid->flags &= ~SDL2_FLAG_SHOULD_RESIZE; sdl_refresh_renderer(vid); @@ -393,6 +413,23 @@ static void *sdl2_gfx_init(const video_info_t *video, sdl_refresh_viewport(vid); +#if SDL_VERSION_ATLEAST(2, 0, 18) + /* Set up the global OSD font (video_font_driver) using our + * sdl2_raster_font. Required for the "Display Statistics" + * overlay - the central video_driver_frame path renders that + * via font_driver_render_msg(driver_data, stat_text, params, NULL), + * and font_driver_render_msg falls through to video_font_driver + * when font_data is NULL. Without this, video_font_driver stays + * NULL on SDL2 and statistics never render. + * + * We also gain a working render_msg path for any other RA + * subsystem that calls font_driver_render_msg with NULL font - + * this is the same wiring every other modern driver does. */ + if (video->font_enable) + font_driver_init_osd(vid, video, false, video->is_threaded, + FONT_DRIVER_RENDER_SDL2); +#endif + *input = NULL; *input_data = NULL; @@ -450,12 +487,155 @@ static bool sdl2_gfx_frame(void *data, const void *frame, unsigned width, SDL_RenderCopyEx(vid->renderer, vid->frame.tex, NULL, NULL, vid->rotation, NULL, SDL_FLIP_NONE); #ifdef HAVE_MENU - menu_driver_frame(menu_is_alive, video_info); + { +#if SDL_VERSION_ATLEAST(2, 0, 18) + /* Non-RGUI menus (XMB / Ozone / MaterialUI) issue gfx_display + * draw calls during menu_driver_frame and need the full-window + * viewport for the same reason widgets do (see widget block + * below). RGUI doesn't care - it renders into vid->menu.tex + * and is composited via SDL_RenderCopy after - but setting the + * viewport unconditionally is harmless for RGUI and saves a + * branch on whether the active menu is RGUI or not. */ + SDL_Rect screen_vp_menu; + SDL_Rect saved_vp_menu; + SDL_RenderGetViewport(vid->renderer, &saved_vp_menu); + screen_vp_menu.x = 0; + screen_vp_menu.y = 0; + screen_vp_menu.w = (int)vid->vp.full_width; + screen_vp_menu.h = (int)vid->vp.full_height; + SDL_RenderSetViewport(vid->renderer, &screen_vp_menu); + + menu_driver_frame(menu_is_alive, video_info); + + SDL_RenderSetViewport(vid->renderer, &saved_vp_menu); +#else + menu_driver_frame(menu_is_alive, video_info); +#endif + } #endif if (vid->menu.active) SDL_RenderCopy(vid->renderer, vid->menu.tex, NULL, NULL); +#if SDL_VERSION_ATLEAST(2, 0, 18) + /* "Display Statistics" overlay (Settings -> Onscreen Notifications + * -> Display Statistics). Rendered between the menu composite + * and widgets, matching gdi/d3d8/d3d9 order so widget toasts can + * still paint over the stats block in the rare case they overlap. + * + * Suppressed while the menu is alive - the menu drivers + * (XMB/Ozone/MaterialUI) own the screen and the overlay would + * just bleed under their UI. RGUI is software-rendered into + * vid->menu.tex which is composited above, so it would actually + * cover the stats; suppressing here keeps the behaviour + * symmetric across menu drivers. + * + * Needs the full-window viewport for the same reason widgets do + * - OSD font math is done against video_info->width/height. */ + { + const char *stat_text = video_info->stat_text; + struct font_params *osd_params = (struct font_params*) + &video_info->osd_stat_params; + bool show_stats = video_info->statistics_show + && stat_text && stat_text[0] != '\0' +#ifdef HAVE_MENU + && !menu_is_alive +#endif + ; + if (show_stats) + { + SDL_Rect screen_vp_stats; + SDL_Rect saved_vp_stats; + SDL_RenderGetViewport(vid->renderer, &saved_vp_stats); + screen_vp_stats.x = 0; + screen_vp_stats.y = 0; + screen_vp_stats.w = (int)vid->vp.full_width; + screen_vp_stats.h = (int)vid->vp.full_height; + SDL_RenderSetViewport(vid->renderer, &screen_vp_stats); + + font_driver_render_msg(vid, stat_text, osd_params, NULL); + + SDL_RenderSetViewport(vid->renderer, &saved_vp_stats); + } + } +#endif + +#ifdef HAVE_OVERLAY + /* Input overlay (touch / virtual gamepad images). Rendered + * between stats and widgets so a widget toast can paint on top + * of a button if they overlap (rare in practice — widgets land + * in their own corner — but keeping the order consistent across + * backends means an overlay-on-d3d9 config carries over here + * unchanged). + * + * Needs the full-window viewport (the same one widgets use): + * fullscreen overlays span the whole window including + * letterbox bars, and even non-fullscreen overlays compute + * against vid->vp.x/y/width/height which are also window-space + * pixel coordinates. Going through the game viewport (the + * default sdl_refresh_renderer state) would clip overlay + * buttons drawn on the pillar/letter bars. */ + if (vid->overlays_enabled && vid->overlays && vid->overlays_size) + { + SDL_Rect screen_vp_ov; + SDL_Rect saved_vp_ov; + SDL_RenderGetViewport(vid->renderer, &saved_vp_ov); + screen_vp_ov.x = 0; + screen_vp_ov.y = 0; + screen_vp_ov.w = (int)vid->vp.full_width; + screen_vp_ov.h = (int)vid->vp.full_height; + SDL_RenderSetViewport(vid->renderer, &screen_vp_ov); + + sdl2_overlays_render(vid); + + SDL_RenderSetViewport(vid->renderer, &saved_vp_ov); + } +#endif + +#if defined(HAVE_GFX_WIDGETS) && SDL_VERSION_ATLEAST(2, 0, 18) + /* Widgets composite over the framebuffer (and over the menu, if + * the menu is active). gfx_widgets_frame ultimately calls back + * into gfx_display_sdl2_draw and sdl2_raster_font_render_msg, + * so the SDL_Renderer must already be in a state where its + * draw color/blend won't poison the widget submissions. The + * blend_begin/blend_end callbacks in our gfx_display backend + * handle that on a per-call basis. + * + * Critically, sdl_refresh_renderer set a viewport equal to + * vid->vp (the aspect-corrected GAME area, not the window). + * Widgets compute coords against video_info->width/height which + * are the full window dimensions, so without a full-window + * viewport reset here the widget would draw in the wrong place + * (or be clipped entirely - at 4K with a 4:3 game, the widget's + * bottom-of-screen position lands outside vid->vp's bottom edge + * and SDL clips it to nothing). + * + * Same hazard exists for menu_driver_frame above when XMB / Ozone + * etc. issue gfx_display draws, but the existing sdl2 driver only + * ever supported RGUI (drawn into vid->menu.tex via SDL_RenderCopy + * which doesn't care about viewport), so that path was implicitly + * fine. Now that we accept arbitrary menus, set the viewport for + * the menu pass too. */ + if (video_info->widgets_active) + { + SDL_Rect screen_vp; + SDL_Rect saved_vp; + SDL_RenderGetViewport(vid->renderer, &saved_vp); + + screen_vp.x = 0; + screen_vp.y = 0; + screen_vp.w = (int)vid->vp.full_width; + screen_vp.h = (int)vid->vp.full_height; + SDL_RenderSetViewport(vid->renderer, &screen_vp); + + gfx_widgets_frame(video_info); + + /* Restore the game viewport so the next frame's core blit + * lands in the right place. */ + SDL_RenderSetViewport(vid->renderer, &saved_vp); + } +#endif + if (msg) sdl2_render_msg(vid, msg); @@ -508,6 +688,21 @@ static void sdl2_gfx_free(void *data) if (!vid) return; +#if SDL_VERSION_ATLEAST(2, 0, 18) + /* Mirror font_driver_init_osd in sdl2_gfx_init. Must run before + * SDL_DestroyRenderer because the OSD font owns SDL_Textures + * created against vid->renderer; tearing the renderer down first + * would leave the font driver holding dangling texture pointers + * for the next free() call. */ + font_driver_free_osd(); +#endif + +#ifdef HAVE_OVERLAY + /* Same constraint - overlay textures are owned by vid->renderer. + * Drop them before SDL_DestroyRenderer below. */ + sdl2_overlay_free(vid); +#endif + if (vid->renderer) SDL_DestroyRenderer(vid->renderer); @@ -612,6 +807,35 @@ static void sdl2_poke_set_osd_msg(void *data, const char *msg, const struct font_params *params, void *font) { sdl2_video_t *vid = (sdl2_video_t*)data; + + /* The poke->set_osd_msg API is dual-purpose: + * + * 1. Real OSD path (called from video_driver_frame with msg arg). + * `font` is NULL and we render via sdl2_render_msg's bitmap + * font - this is the legacy yellow-text OSD output. + * + * 2. gfx_display_draw_text path (used by all menu drivers and + * by widgets via gfx_widgets_draw_text). `font` is a valid + * font_data_t* whose renderer was selected via + * FONT_DRIVER_RENDER_SDL2 - i.e. our sdl2_raster_font. We + * must dispatch to that font driver's render_msg, otherwise + * every menu/widget text call falls through to the OSD font + * and lands in the wrong place with the wrong glyphs. + * + * The original sdl2 driver only supported RGUI (which renders + * text into vid->menu.tex itself, never via gfx_display_draw_text) + * so this branch never had a non-NULL font_data and the bug was + * masked. */ + if (font && params) + { + const font_data_t *fd = (const font_data_t*)font; + if (fd->renderer && fd->renderer->render_msg) + { + fd->renderer->render_msg(vid, fd->renderer_data, msg, params); + return; + } + } + sdl2_render_msg(vid, msg); } @@ -623,10 +847,72 @@ static void sdl2_grab_mouse_toggle(void *data) } static uint32_t sdl2_get_flags(void *data) { return 0; } +#if SDL_VERSION_ATLEAST(2, 0, 18) +/* Texture upload hook for menu icons, the gfx_display white texture, + * and any other gfx_display-driven texture loads. Without this hook, + * gfx_display_init_white_texture is a no-op (gfx_white_texture stays + * 0), which means every menu/widget quad that doesn't bind its own + * texture passes NULL to SDL_RenderGeometry - and widgets in + * particular rely heavily on the white texture for their tinted + * backgrounds and panels. So the menu/widgets are visibly broken + * until this is wired up. + * + * Pixel format: sdl2_get_flags returns 0 (no VIDEO_FLAG_USE_RGBA), + * so the image task gives us pixels in BGRA byte order packed into + * uint32. On little-endian that maps to native-uint32 0xAARRGGBB, + * which is SDL_PIXELFORMAT_ARGB8888. On big-endian SDL_BYTEORDER + * matches and the same format constant resolves correctly. */ +static uintptr_t sdl2_load_texture(void *video_data, void *data, + bool threaded, enum texture_filter_type filter_type) +{ + sdl2_video_t *vid = (sdl2_video_t*)video_data; + struct texture_image *ti = (struct texture_image*)data; + SDL_Texture *tex = NULL; + + if (!vid || !vid->renderer || !ti || !ti->pixels || !ti->width || !ti->height) + return 0; + + tex = SDL_CreateTexture(vid->renderer, + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STATIC, + ti->width, ti->height); + if (!tex) + return 0; + + SDL_UpdateTexture(tex, NULL, ti->pixels, ti->width * sizeof(uint32_t)); + SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); + + /* filter_type maps loosely; SDL_Renderer doesn't expose mipmaps, + * and the only meaningful distinction is nearest vs linear. */ + if (filter_type == TEXTURE_FILTER_NEAREST + || filter_type == TEXTURE_FILTER_MIPMAP_NEAREST) + SDL_SetTextureScaleMode(tex, SDL_ScaleModeNearest); + else + SDL_SetTextureScaleMode(tex, SDL_ScaleModeLinear); + + return (uintptr_t)tex; +} + +static void sdl2_unload_texture(void *data, + bool threaded, uintptr_t id) +{ + SDL_Texture *tex = (SDL_Texture*)id; + (void)data; + (void)threaded; + if (tex) + SDL_DestroyTexture(tex); +} +#endif + static video_poke_interface_t sdl2_video_poke_interface = { sdl2_get_flags, - NULL, /* load_texture */ +#if SDL_VERSION_ATLEAST(2, 0, 18) + sdl2_load_texture, + sdl2_unload_texture, +#else + NULL, /* load_texture - menus/widgets need SDL_RenderGeometry */ NULL, /* unload_texture */ +#endif NULL, /* set_video_mode */ NULL, /* get_refresh_rate */ sdl2_poke_set_filtering, @@ -668,6 +954,1157 @@ static bool sdl2_gfx_set_shader(void *data, return false; } +#ifdef HAVE_GFX_WIDGETS +static bool sdl2_gfx_widgets_enabled(void *data) { (void)data; return true; } +#endif + +/* + * gfx_display BACKEND + * + * Hardware-accelerated menu/widget rendering for SDL2. Built on + * SDL_RenderGeometry (added in SDL 2.0.18) so the menu and widget + * quads run on the GPU through SDL_Renderer's underlying backend + * (D3D9/11, Metal, GL/GLES depending on platform). + * + * The whole gfx_display backend, font driver, and the + * gfx_widgets_enabled hook are gated on SDL >= 2.0.18. Older SDL + * builds fall back to the existing rgui-only path (rgui renders + * its own software framebuffer that we composite via + * SDL_RenderCopy), which check_menu_driver_compatibility allows + * unconditionally. The runtime guard is needed because the qb + * configure script only requires SDL 2.0.0. + */ + +#if SDL_VERSION_ATLEAST(2, 0, 18) + +static void *gfx_display_sdl2_get_default_mvp(void *data) +{ + /* SDL_Renderer has no MVP concept; transforms go through + * SDL_RenderSetScale / SDL_RenderSetViewport. The menu code + * tolerates a NULL here for fixed-function drivers. */ + (void)data; + return NULL; +} + +static void gfx_display_sdl2_blend_begin(void *data) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid) + return; + SDL_SetRenderDrawBlendMode(vid->renderer, SDL_BLENDMODE_BLEND); +} + +static void gfx_display_sdl2_blend_end(void *data) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid) + return; + SDL_SetRenderDrawBlendMode(vid->renderer, SDL_BLENDMODE_NONE); +} + +static void gfx_display_sdl2_scissor_begin(void *data, + unsigned video_width, unsigned video_height, + int x, int y, unsigned width, unsigned height) +{ + SDL_Rect rect; + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid) + return; + + /* gfx_display passes scissor rects in GL convention (origin + * bottom-left). SDL_RenderSetClipRect is top-left, so flip. */ + rect.x = x; + rect.y = (int)video_height - y - (int)height; + rect.w = (int)width; + rect.h = (int)height; + + if (rect.x < 0) { rect.w += rect.x; rect.x = 0; } + if (rect.y < 0) { rect.h += rect.y; rect.y = 0; } + if (rect.w <= 0 || rect.h <= 0) + return; + + SDL_RenderSetClipRect(vid->renderer, &rect); +} + +static void gfx_display_sdl2_scissor_end(void *data, + unsigned video_width, unsigned video_height) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + (void)video_width; + (void)video_height; + if (!vid) + return; + SDL_RenderSetClipRect(vid->renderer, NULL); +} + +/* All gfx_display draws are triangle strips (see gl1's draw, which + * does GL_TRIANGLE_STRIP unconditionally). SDL_RenderGeometry only + * does triangle lists, so we expand the strip into indices on the + * fly. Most draws are the 4-vertex quad case; we special-case it. + * + * Two distinct call paths converge here: + * + * 1. gfx_display_draw_quad - used by widgets and most menu chrome. + * Sets coords->vertex = NULL and coords->tex_coord = NULL, and + * encodes the quad rectangle in draw->x / draw->y / draw->width / + * draw->height (pixel coords, Y already flipped to top-left + * origin by the caller). gl1 handles this by substituting a + * static 0..1 vertex array and calling glViewport with the rect, + * but per-quad viewport changes don't make sense for SDL_Renderer + * so we synthesize the four corners directly in pixel space. + * + * 2. The general path - menu drivers that build their own vertex + * arrays in 0..1 normalized coords (origin bottom-left). We + * scale these to video_width / video_height and flip Y to match + * SDL's top-left origin. + * + * Without case 1, every widget call to gfx_display_draw_quad gets + * silently dropped (vertex pointer is NULL) and the entire widget + * system renders as nothing. */ +static void gfx_display_sdl2_draw(gfx_display_ctx_draw_t *draw, + void *data, unsigned video_width, unsigned video_height) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + SDL_Vertex *verts = NULL; + int *indices = NULL; + int quad_idx[6] = { 0, 1, 2, 2, 1, 3 }; + SDL_Texture *tex = NULL; + const float *vtx; + const float *tc; + const float *col; + unsigned n; + unsigned i; + unsigned num_idx; + + if (!vid || !draw || !draw->coords) + return; + + n = draw->coords->vertices; + if (n < 3) + return; + + vtx = draw->coords->vertex; + tc = draw->coords->tex_coord; + col = draw->coords->color; + + /* The texture handle is a uintptr_t cast of an SDL_Texture* + * registered via sdl2_load_texture (poke->load_texture). For + * gfx_display_draw_quad calls without an explicit texture the + * caller substitutes gfx_white_texture, so passing this through + * directly is safe; if SDL_RenderGeometry receives NULL we get + * flat-shaded geometry, which is a reasonable degraded path. */ + tex = (SDL_Texture*)(uintptr_t)draw->texture; + + verts = (SDL_Vertex*)alloca(sizeof(SDL_Vertex) * n); + + /* Path 1: gfx_display_draw_quad - vtx is NULL, geometry comes + * from draw->x/y/width/height with y bottom-up. n is always 4. + * + * COORDINATE CONVENTIONS (cribbed from gdi_gfx.c, the canonical + * reference for a top-down-pixel target): + * + * - draw->x / y / width / height: pixel coords, y bottom-up + * (gfx_display_draw_quad pre-flips: draw.y = height - y - h). + * To put the rect at the right spot in SDL's top-down pixel + * space, re-flip: + * dst_y = video_height - draw->height - draw->y + * + * - coords->tex_coord (when non-NULL): 0..1 normalised, TOP-DOWN + * (yes, opposite to the bottom-up vertex convention; this is + * documented in gfx_display.c). Used directly without flip. + * When NULL we synthesise (0,0)..(1,1). + * + * Without honouring those conventions, widgets render at the top + * of the screen instead of the bottom (Y not flipped), and icons + * with sub-rect texcoords - 9-patch slices, achievement badge + * sprites - sample from the wrong part of the atlas. */ + if (!vtx && n == 4) + { + float x0, x1, y0, y1; + + /* Defensive clamp: gfx_widgets_draw_icon's coordinate math + * depends on widget layout values that can underflow during + * the first few frames after icon load, producing + * draw->y == INT_MIN. Float-converting that and feeding it + * to SDL_RenderGeometry produces NaN vertex positions and a + * spurious SDL error that pollutes the renderer state for + * subsequent draws, making the entire widget invisible. + * Reject any rect whose origin / extent can't fit in a + * reasonable floating point coord. */ + if ( draw->x < -65536 || draw->x > 65536 + || draw->y < -65536 || draw->y > 65536 + || draw->width > 65536 + || draw->height > 65536) + return; + + x0 = (float)draw->x; + x1 = (float)draw->x + (float)draw->width; + /* Re-flip Y from bottom-up to SDL top-down. */ + y0 = (float)video_height - (float)draw->height - (float)draw->y; + y1 = y0 + (float)draw->height; + + /* Apply draw->scale_factor (centred scaling around the quad's + * midpoint). XMB sets this on icon draws (node->zoom) to grow + * the active tab icon ~2x; without honouring it every icon + * renders at base size and the active tab is visually + * indistinguishable. Only the plain-quad path uses + * scale_factor; the slice path encodes scaling in + * coords->vertex. */ + if (draw->scale_factor > 0.0f && draw->scale_factor != 1.0f) + { + float cx = (x0 + x1) * 0.5f; + float cy = (y0 + y1) * 0.5f; + float hw = (x1 - x0) * 0.5f * draw->scale_factor; + float hh = (y1 - y0) * 0.5f * draw->scale_factor; + x0 = cx - hw; x1 = cx + hw; + y0 = cy - hh; y1 = cy + hh; + } + + /* Default texcoords cover the whole texture; tex_coord is + * top-down so (0,0)..(1,1) means the upper-left source pixel + * maps to the upper-left of the quad. */ + { + float u0 = 0.0f, v0 = 0.0f, u1 = 1.0f, v1 = 1.0f; + if (tc) + { + /* tex_coord is 4 (u,v) pairs. For axis-aligned quads + * (everything that hits this path) the bbox IS the + * slice, so we don't need to track per-vertex order. */ + float min_u = tc[0], max_u = tc[0]; + float min_v = tc[1], max_v = tc[1]; + unsigned k; + for (k = 1; k < 4; k++) + { + float u = tc[k * 2 + 0]; + float v = tc[k * 2 + 1]; + if (u < min_u) min_u = u; + if (u > max_u) max_u = u; + if (v < min_v) min_v = v; + if (v > max_v) max_v = v; + } + u0 = min_u; v0 = min_v; u1 = max_u; v1 = max_v; + } + + /* Vertex order matches gl1_menu_vertexes (the canonical + * triangle-strip layout used by every gfx_display caller): + * + * vertex 0 -> (x=0, y=0) bottom-left in GL bottom-up coords + * vertex 1 -> (x=1, y=0) bottom-right + * vertex 2 -> (x=0, y=1) top-left + * vertex 3 -> (x=1, y=1) top-right + * + * After Y is flipped to SDL's top-down screen space, vertex + * indices stay the same but the geometric meaning becomes: + * + * vertex 0 -> bottom-left in screen space (high pixel Y) + * vertex 1 -> bottom-right (high pixel Y) + * vertex 2 -> top-left (low pixel Y) + * vertex 3 -> top-right (low pixel Y) + * + * This matters for per-vertex colour interpolation - + * gfx_widget_load_content_animation puts an opaque alpha on + * vertices 0/1 of its top-shadow gradient, expecting that + * pair to be the edge touching the main bar. If we map + * indices 0/1 to the top edge in screen space the gradient + * inverts and a visible hard line appears at the bar/shadow + * boundary. */ + verts[0].position.x = x0; verts[0].position.y = y1; + verts[0].tex_coord.x = u0; verts[0].tex_coord.y = v1; + verts[1].position.x = x1; verts[1].position.y = y1; + verts[1].tex_coord.x = u1; verts[1].tex_coord.y = v1; + verts[2].position.x = x0; verts[2].position.y = y0; + verts[2].tex_coord.x = u0; verts[2].tex_coord.y = v0; + verts[3].position.x = x1; verts[3].position.y = y0; + verts[3].tex_coord.x = u1; verts[3].tex_coord.y = v0; + + /* Apply 2D rotation around the rect's centre. Used by + * gfx_widgets_draw_icon for the spinning hourglass on + * pending tasks - draw->rotation is the angle in radians, + * draw->matrix_data also encodes it but rotating the four + * corners directly is simpler than multiplying every vertex + * by a 4x4. SDL_RenderGeometry happily takes non-axis- + * aligned quads. + * + * Y axis flips because SDL is top-down: a positive radians + * value should rotate clockwise on screen (matching what + * gl/d3d produce after their viewport transform), which + * means negating sine. */ + if (draw->rotation != 0.0f) + { + float cx = (x0 + x1) * 0.5f; + float cy = (y0 + y1) * 0.5f; + float c = cosf(draw->rotation); + float s = sinf(draw->rotation); + unsigned k; + for (k = 0; k < 4; k++) + { + float dx = verts[k].position.x - cx; + float dy = verts[k].position.y - cy; + verts[k].position.x = cx + dx * c + dy * s; + verts[k].position.y = cy - dx * s + dy * c; + } + } + } + + for (i = 0; i < 4; i++) + { + if (col) + { + verts[i].color.r = (Uint8)(col[i * 4 + 0] * 255.0f); + verts[i].color.g = (Uint8)(col[i * 4 + 1] * 255.0f); + verts[i].color.b = (Uint8)(col[i * 4 + 2] * 255.0f); + verts[i].color.a = (Uint8)(col[i * 4 + 3] * 255.0f); + } + else + { + verts[i].color.r = verts[i].color.g + = verts[i].color.b + = verts[i].color.a = 255; + } + } + + SDL_RenderGeometry(vid->renderer, tex, verts, 4, quad_idx, 6); + return; + } + + /* Path 2: caller-supplied vertex array in 0..1 normalised coords + * (origin bottom-up). Convert to SDL pixel coords (top-down) by + * scaling and flipping Y. Texcoords are top-down already - no + * flip. */ + if (!vtx) + return; + + for (i = 0; i < n; i++) + { + float vx = vtx[i * 2 + 0]; + float vy = vtx[i * 2 + 1]; + + verts[i].position.x = vx * (float)video_width; + verts[i].position.y = (1.0f - vy) * (float)video_height; + + if (tc) + { + /* tex_coord is top-down per gfx_display.c convention; no + * flip. Vertex Y was flipped above (bottom-up to top-down) + * but tex_coord is already in the same orientation as our + * SDL pixel target. */ + verts[i].tex_coord.x = tc[i * 2 + 0]; + verts[i].tex_coord.y = tc[i * 2 + 1]; + } + else + { + verts[i].tex_coord.x = 0.0f; + verts[i].tex_coord.y = 0.0f; + } + + if (col) + { + verts[i].color.r = (Uint8)(col[i * 4 + 0] * 255.0f); + verts[i].color.g = (Uint8)(col[i * 4 + 1] * 255.0f); + verts[i].color.b = (Uint8)(col[i * 4 + 2] * 255.0f); + verts[i].color.a = (Uint8)(col[i * 4 + 3] * 255.0f); + } + else + { + verts[i].color.r = verts[i].color.g + = verts[i].color.b + = verts[i].color.a = 255; + } + } + + /* Quad fast path - the common case for menu items. */ + if (n == 4) + { + SDL_RenderGeometry(vid->renderer, tex, verts, 4, quad_idx, 6); + return; + } + + /* General triangle-strip expansion: n verts -> (n-2) triangles. */ + num_idx = (n - 2) * 3; + indices = (int*)alloca(sizeof(int) * num_idx); + for (i = 0; i < n - 2; i++) + { + if ((i & 1) == 0) + { + indices[i * 3 + 0] = (int)i; + indices[i * 3 + 1] = (int)i + 1; + indices[i * 3 + 2] = (int)i + 2; + } + else + { + /* Flip winding on odd triangles to keep the strip + * consistent (matches GL_TRIANGLE_STRIP semantics). */ + indices[i * 3 + 0] = (int)i + 1; + indices[i * 3 + 1] = (int)i; + indices[i * 3 + 2] = (int)i + 2; + } + } + + SDL_RenderGeometry(vid->renderer, tex, verts, n, indices, num_idx); +} + +/* Pipeline draws (XMB ribbon, snow, snowflake, bokeh) require a + * programmable pipeline. SDL_Renderer does not expose one, so we + * leave this as a no-op - the menu still renders, just without the + * animated background. Documented as a feature gap. */ +static void gfx_display_sdl2_draw_pipeline( + gfx_display_ctx_draw_t *draw, + gfx_display_t *p_disp, + void *data, + unsigned video_width, unsigned video_height) +{ + (void)draw; + (void)p_disp; + (void)data; + (void)video_width; + (void)video_height; +} + +gfx_display_ctx_driver_t gfx_display_ctx_sdl2 = { + gfx_display_sdl2_draw, + gfx_display_sdl2_draw_pipeline, + gfx_display_sdl2_blend_begin, + gfx_display_sdl2_blend_end, + gfx_display_sdl2_get_default_mvp, + NULL, /* get_default_vertices */ + NULL, /* get_default_tex_coords */ + FONT_DRIVER_RENDER_SDL2, + GFX_VIDEO_DRIVER_SDL2, + "sdl2", + false, + gfx_display_sdl2_scissor_begin, + gfx_display_sdl2_scissor_end +}; + +/* + * FONT DRIVER + * + * Submits glyph quads through SDL_RenderGeometry so menu/widget text + * shares the exact same vertex pipeline (and therefore the same + * batched blend state, scissor state, and z-ordering) as the menu + * quads emitted by gfx_display_sdl2_draw. The atlas is uploaded as a + * single RGBA SDL_Texture; we expand the 8-bit alpha buffer that + * font_renderer produces into RGBA so vertex colors can tint it. + * + * This is independent of the OSD font path (sdl2_init_font / + * sdl2_render_msg) which is used for on-screen messages drawn over + * the emulated framebuffer. Both can coexist. + */ + +typedef struct +{ + sdl2_video_t *vid; + SDL_Texture *tex; + const font_renderer_driver_t *font_driver; + void *font_data; + struct font_atlas *atlas; + int tex_width; + int tex_height; + bool atlas_dirty; +} sdl2_raster_t; + +static void sdl2_raster_font_upload_atlas(sdl2_raster_t *font) +{ + uint32_t *rgba; + int i, total; + const uint8_t *src; + + if (!font || !font->atlas) + return; + + if (font->tex) + { + SDL_DestroyTexture(font->tex); + font->tex = NULL; + } + + font->tex_width = (int)font->atlas->width; + font->tex_height = (int)font->atlas->height; + + font->tex = SDL_CreateTexture(font->vid->renderer, + SDL_PIXELFORMAT_ABGR8888, + SDL_TEXTUREACCESS_STATIC, + font->tex_width, font->tex_height); + if (!font->tex) + return; + + total = font->tex_width * font->tex_height; + rgba = (uint32_t*)malloc(total * sizeof(uint32_t)); + if (!rgba) + { + SDL_DestroyTexture(font->tex); + font->tex = NULL; + return; + } + + /* Atlas buffer is 8-bit alpha. Expand to white-RGB plus the alpha + * value so vertex color modulation produces correctly-tinted + * glyphs. SDL_PIXELFORMAT_ABGR8888 is byte order R,G,B,A on + * little-endian, packed as 0xAABBGGRR in a uint32_t. */ + src = font->atlas->buffer; + for (i = 0; i < total; i++) + { + uint32_t a = src[i]; + rgba[i] = (a << 24) | 0x00FFFFFFu; + } + + SDL_UpdateTexture(font->tex, NULL, rgba, font->tex_width * sizeof(uint32_t)); + SDL_SetTextureBlendMode(font->tex, SDL_BLENDMODE_BLEND); + + free(rgba); + font->atlas->dirty = false; + font->atlas_dirty = false; +} + +static void *sdl2_raster_font_init(void *data, const char *font_path, + float font_size, bool is_threaded) +{ + sdl2_raster_t *font; + sdl2_video_t *vid = (sdl2_video_t*)data; + + if (!vid || !vid->renderer) + { + RARCH_WARN("[SDL2] sdl2_raster_font_init: no video data or renderer " + "(vid=%p, renderer=%p) - widget/menu fonts will be unavailable\n", + (void*)vid, vid ? (void*)vid->renderer : NULL); + return NULL; + } + + font = (sdl2_raster_t*)calloc(1, sizeof(*font)); + if (!font) + return NULL; + + font->vid = vid; + + if (!font_renderer_create_default( + &font->font_driver, &font->font_data, + font_path, font_size)) + { + RARCH_WARN("[SDL2] sdl2_raster_font_init: font_renderer_create_default " + "failed for path '%s' size %.1f\n", + font_path ? font_path : "(default)", font_size); + free(font); + return NULL; + } + + font->atlas = font->font_driver->get_atlas(font->font_data); + sdl2_raster_font_upload_atlas(font); + + if (!font->tex) + { + RARCH_WARN("[SDL2] sdl2_raster_font_init: atlas upload failed: %s\n", + SDL_GetError()); + font->font_driver->free(font->font_data); + free(font); + return NULL; + } + + return font; +} + +static void sdl2_raster_font_free(void *data, bool is_threaded) +{ + sdl2_raster_t *font = (sdl2_raster_t*)data; + if (!font) + return; + if (font->tex) + SDL_DestroyTexture(font->tex); + if (font->font_driver && font->font_data) + font->font_driver->free(font->font_data); + free(font); +} + +static int sdl2_raster_font_get_message_width(void *data, const char *msg, + size_t msg_len, float scale) +{ + sdl2_raster_t *font = (sdl2_raster_t*)data; + const char *cur = msg; + size_t i = 0; + int width = 0; + + if (!font || !msg) + return 0; + + while (i < msg_len && *cur) + { + const struct font_glyph *glyph = + font->font_driver->get_glyph(font->font_data, (uint8_t)*cur); + if (!glyph) + glyph = font->font_driver->get_glyph(font->font_data, '?'); + if (glyph) + width += glyph->advance_x; + i++; + cur++; + } + + return (int)((float)width * scale); +} + +/* Render a single line into one SDL_RenderGeometry batch. Up to + * MAX_GLYPHS per submitted batch; we flush mid-line for longer runs. + * + * Coordinates: render_msg gives us params->x/y in 0..1 normalized + * space. We convert to pixel coords against the full window, with + * a top-left origin (SDL convention). */ +static void sdl2_raster_font_render_line( + sdl2_raster_t *font, + const char *msg, size_t msg_len, + float scale, + const SDL_Color col, + float pos_x, float pos_y, + enum text_alignment align, + unsigned width, unsigned height) +{ +#define SDL2_FONT_MAX_GLYPHS 256 + SDL_Vertex verts[SDL2_FONT_MAX_GLYPHS * 4]; + int idx[SDL2_FONT_MAX_GLYPHS * 6]; + int n_glyphs = 0; + const char *cur = msg; + size_t ci = 0; + float x = pos_x; + float y = pos_y; + float inv_w; + float inv_h; + + if (!font || !font->tex) + return; + + if (font->atlas_dirty || font->atlas->dirty) + sdl2_raster_font_upload_atlas(font); + + /* gfx_display_draw_text gives us params->x/y in normalized 0..1 + * coords (origin bottom-left to match GL). Convert to pixels + * with a top-left origin. */ + x = pos_x * (float)width; + y = (1.0f - pos_y) * (float)height; + + if (align == TEXT_ALIGN_RIGHT) + x -= sdl2_raster_font_get_message_width(font, msg, msg_len, scale); + else if (align == TEXT_ALIGN_CENTER) + x -= sdl2_raster_font_get_message_width(font, msg, msg_len, scale) + * 0.5f; + + inv_w = 1.0f / (float)font->tex_width; + inv_h = 1.0f / (float)font->tex_height; + + /* gfx_display draw_text passes 8-bit-clean strings; we walk byte + * by byte. UTF-8 multi-byte sequences are handled by the upstream + * font renderer's glyph lookup (which keys on uint32_t codepoints + * but tolerates byte-by-byte queries for the ASCII-only RA UI). */ + while (ci < msg_len && *cur) + { + const struct font_glyph *glyph = + font->font_driver->get_glyph(font->font_data, (uint8_t)*cur); + float gx, gy, gw, gh; + float u0, v0, u1, v1; + int base; + + if (!glyph) + glyph = font->font_driver->get_glyph(font->font_data, '?'); + if (!glyph) + { + ci++; + cur++; + continue; + } + + gx = x + glyph->draw_offset_x * scale; + gy = y + glyph->draw_offset_y * scale; + gw = glyph->width * scale; + gh = glyph->height * scale; + + u0 = (float)glyph->atlas_offset_x * inv_w; + v0 = (float)glyph->atlas_offset_y * inv_h; + u1 = u0 + (float)glyph->width * inv_w; + v1 = v0 + (float)glyph->height * inv_h; + + base = n_glyphs * 4; + + verts[base + 0].position.x = gx; + verts[base + 0].position.y = gy; + verts[base + 0].tex_coord.x = u0; + verts[base + 0].tex_coord.y = v0; + verts[base + 0].color = col; + + verts[base + 1].position.x = gx + gw; + verts[base + 1].position.y = gy; + verts[base + 1].tex_coord.x = u1; + verts[base + 1].tex_coord.y = v0; + verts[base + 1].color = col; + + verts[base + 2].position.x = gx; + verts[base + 2].position.y = gy + gh; + verts[base + 2].tex_coord.x = u0; + verts[base + 2].tex_coord.y = v1; + verts[base + 2].color = col; + + verts[base + 3].position.x = gx + gw; + verts[base + 3].position.y = gy + gh; + verts[base + 3].tex_coord.x = u1; + verts[base + 3].tex_coord.y = v1; + verts[base + 3].color = col; + + idx[n_glyphs * 6 + 0] = base + 0; + idx[n_glyphs * 6 + 1] = base + 1; + idx[n_glyphs * 6 + 2] = base + 2; + idx[n_glyphs * 6 + 3] = base + 2; + idx[n_glyphs * 6 + 4] = base + 1; + idx[n_glyphs * 6 + 5] = base + 3; + + x += glyph->advance_x * scale; + n_glyphs++; + ci++; + cur++; + + if (n_glyphs >= SDL2_FONT_MAX_GLYPHS) + { + SDL_RenderGeometry(font->vid->renderer, font->tex, + verts, n_glyphs * 4, idx, n_glyphs * 6); + n_glyphs = 0; + } + } + + if (n_glyphs > 0) + SDL_RenderGeometry(font->vid->renderer, font->tex, + verts, n_glyphs * 4, idx, n_glyphs * 6); +#undef SDL2_FONT_MAX_GLYPHS +} + +/* Walk a (possibly multi-line) string and call render_line once per + * line segment, dropping each subsequent line by one line-height in + * GL-convention (params->y increases upward, so we subtract). + * + * Required because callers like XMB sublabels embed real '\n' bytes + * into their wrapped text — gfx_display_draw_text doesn't pre-split + * for us, and feeding the newline straight to render_line just looks + * up '\n' in the glyph atlas, gets a tofu placeholder, and renders it + * as garbage (visible as a small box between words). Mirrors gl1's + * gl1_raster_font_render_message wrapper. */ +static void sdl2_raster_font_render_message( + sdl2_raster_t *font, const char *msg, float scale, + const SDL_Color col, float pos_x, float pos_y, + enum text_alignment align, unsigned width, unsigned height) +{ + struct font_line_metrics *line_metrics = NULL; + float line_height_norm = 0.0f; + int lines = 0; + + if (font->font_driver && font->font_driver->get_line_metrics) + { + font->font_driver->get_line_metrics(font->font_data, &line_metrics); + if (line_metrics && height > 0) + line_height_norm = (float)line_metrics->height * scale + / (float)height; + } + + for (;;) + { + const char *p = msg; + size_t len; + + while (*p && *p != '\n') + p++; + len = (size_t)(p - msg); + + if (len > 0) + sdl2_raster_font_render_line(font, msg, len, scale, col, + pos_x, + pos_y - (float)lines * line_height_norm, + align, width, height); + + if (!*p) + break; + msg = p + 1; + lines++; + } +} + +static void sdl2_raster_font_render_msg( + void *userdata, + void *data, + const char *msg, + const struct font_params *params) +{ + sdl2_raster_t *font = (sdl2_raster_t*)data; + sdl2_video_t *vid = (sdl2_video_t*)userdata; + SDL_Color col; + SDL_Color col_drop; + float x, y, scale; + int drop_x, drop_y; + float drop_mod, drop_alpha; + enum text_alignment align = TEXT_ALIGN_LEFT; + unsigned width; + unsigned height; + + if (!font || !msg || !*msg || !vid) + return; + + width = vid->vp.full_width ? vid->vp.full_width : vid->video.width; + height = vid->vp.full_height ? vid->vp.full_height : vid->video.height; + if (!width || !height) + { + /* viewport not set up yet (very early frames) - skip rather + * than divide by zero downstream. */ + return; + } + + if (params) + { + x = params->x; + y = params->y; + scale = params->scale; + align = params->text_align; + drop_x = params->drop_x; + drop_y = params->drop_y; + drop_mod = params->drop_mod; + drop_alpha = params->drop_alpha; + + col.r = FONT_COLOR_GET_RED(params->color); + col.g = FONT_COLOR_GET_GREEN(params->color); + col.b = FONT_COLOR_GET_BLUE(params->color); + col.a = FONT_COLOR_GET_ALPHA(params->color); + if (col.a == 0) + col.a = 255; + } + else + { + x = 0.0f; + y = 0.0f; + scale = 1.0f; + drop_x = 0; + drop_y = 0; + drop_mod = 0.0f; + drop_alpha = 0.0f; + col.r = col.g = col.b = col.a = 255; + } + + if (drop_x || drop_y) + { + col_drop.r = (Uint8)(col.r * drop_mod); + col_drop.g = (Uint8)(col.g * drop_mod); + col_drop.b = (Uint8)(col.b * drop_mod); + col_drop.a = (Uint8)(col.a * drop_alpha); + + sdl2_raster_font_render_message(font, msg, scale, col_drop, + x + scale * drop_x / (float)width, + y + scale * drop_y / (float)height, + align, width, height); + } + + sdl2_raster_font_render_message(font, msg, scale, col, + x, y, align, width, height); +} + +static const struct font_glyph *sdl2_raster_font_get_glyph( + void *data, uint32_t code) +{ + sdl2_raster_t *font = (sdl2_raster_t*)data; + if (font && font->font_driver) + return font->font_driver->get_glyph(font->font_data, code); + return NULL; +} + +static bool sdl2_raster_font_get_line_metrics(void *data, + struct font_line_metrics **metrics) +{ + sdl2_raster_t *font = (sdl2_raster_t*)data; + if (font && font->font_driver && font->font_data) + { + font->font_driver->get_line_metrics(font->font_data, metrics); + return true; + } + return false; +} + +font_renderer_t sdl2_raster_font = { + sdl2_raster_font_init, + sdl2_raster_font_free, + sdl2_raster_font_render_msg, + "sdl2", + sdl2_raster_font_get_glyph, + NULL, /* bind_block - no batched render-block support yet */ + NULL, /* flush_block - widgets/menu work without it */ + sdl2_raster_font_get_message_width, + sdl2_raster_font_get_line_metrics +}; + +#endif /* SDL_VERSION_ATLEAST(2, 0, 18) */ + +#ifdef HAVE_OVERLAY +/* + * INPUT OVERLAY DRIVER + * + * Implements video_overlay_interface_t for SDL2. The overlay + * subsystem (input/input_overlay.c) hands us BGRA32 RGBA images + * via load(), tells us where they go in 0..1 normalised space via + * vertex_geom() / tex_geom(), and we draw them on top of the game + * frame each frame using SDL_RenderCopy with per-texture alpha + * modulation. + * + * Storage: each loaded overlay is uploaded once into a streaming + * SDL_Texture at load time, then redrawn every frame from that + * texture. Mirrors the per-load static-texture pattern d3d8 / d3d9 + * use. + * + * Coordinate conventions (matching d3d8/d3d9/gl): + * - vert_coords[0..3] = (x, y, w, h) in 0..1 normalised space. + * The setter flips y to (1.0f - y) and negates h, the same + * adjustment d3d8 makes for D3D's y-up convention. We undo + * that flip at render time so the overlay lands the right way + * up in SDL's y-down space. + * - tex_coords[0..3] = (x, y, w, h) in 0..1 texture space. + * Used to sub-rect the source texture - typical touch overlays + * pack many buttons into one atlas, then split via tex_geom(). + * - fullscreen=true: vert_coords span the entire window, including + * letterbox/pillarbox bars. Touch overlays use this so the + * buttons keep working when the game is letterboxed. + * - fullscreen=false: vert_coords span only the game viewport. + */ +static void sdl2_overlay_free(sdl2_video_t *vid) +{ + unsigned i; + if (!vid || !vid->overlays) + return; + for (i = 0; i < vid->overlays_size; i++) + { + if (vid->overlays[i].tex) + SDL_DestroyTexture(vid->overlays[i].tex); + } + free(vid->overlays); + vid->overlays = NULL; + vid->overlays_size = 0; +} + +static bool sdl2_overlay_load(void *data, + const void *image_data, unsigned num_images) +{ + unsigned i; + sdl2_video_t *vid = (sdl2_video_t*)data; + const struct texture_image *imgs = (const struct texture_image*)image_data; + + if (!vid) + return false; + + /* Drop any prior overlay set first. load() is the install + * point - input_overlay.c calls it once per overlay activation + * with the full image array, never incrementally. */ + sdl2_overlay_free(vid); + + if (num_images == 0 || !imgs) + return true; + + vid->overlays = (struct sdl2_overlay*)calloc(num_images, + sizeof(*vid->overlays)); + if (!vid->overlays) + return false; + vid->overlays_size = num_images; + + for (i = 0; i < num_images; i++) + { + SDL_Texture *tex; + struct sdl2_overlay *o = &vid->overlays[i]; + unsigned w = imgs[i].width; + unsigned h = imgs[i].height; + + if (w == 0 || h == 0 || !imgs[i].pixels) + continue; + + /* Static so we can SDL_UpdateTexture once at load time; + * source pixels are 0xAARRGGBB in memory order, which on + * little-endian maps to SDL_PIXELFORMAT_ARGB8888. */ + tex = SDL_CreateTexture(vid->renderer, + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STATIC, + (int)w, (int)h); + if (!tex) + continue; + + SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); + SDL_UpdateTexture(tex, NULL, imgs[i].pixels, (int)(w * 4)); + + o->tex = tex; + o->tex_w = w; + o->tex_h = h; + o->alpha_mod = 1.0f; + o->fullscreen = false; + /* Default to whole-texture / whole-target until vertex_geom + * and tex_geom set the real values. */ + o->tex_coords[0] = 0.0f; + o->tex_coords[1] = 0.0f; + o->tex_coords[2] = 1.0f; + o->tex_coords[3] = 1.0f; + o->vert_coords[0] = 0.0f; + o->vert_coords[1] = 0.0f; + o->vert_coords[2] = 1.0f; + o->vert_coords[3] = 1.0f; + } + + return true; +} + +static void sdl2_overlay_tex_geom(void *data, unsigned index, + float x, float y, float w, float h) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid || index >= vid->overlays_size) + return; + vid->overlays[index].tex_coords[0] = x; + vid->overlays[index].tex_coords[1] = y; + vid->overlays[index].tex_coords[2] = w; + vid->overlays[index].tex_coords[3] = h; +} + +static void sdl2_overlay_vertex_geom(void *data, unsigned index, + float x, float y, float w, float h) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid || index >= vid->overlays_size) + return; + /* Overlay descriptor coordinates from input_overlay are already + * y-down (y=0 top of screen, y=1 bottom — same convention as + * RETRO_DEVICE_POINTER which the hit-test path uses) and (x, y) + * names the top-left corner of the rect. SDL2's render space + * (SDL_RenderCopy with y-down SDL_Rect) is also y-down with the + * same origin, so we store the values verbatim and the render + * path does a direct multiply. + * + * d3d8 / d3d9 / gl all flip y here (y = 1.0f - y; h = -h;) + * because their pipelines emit vertices in y-up clip space and + * rely on the viewport transform to flip back to screen. SDL's + * high-level render API has no such pipeline - SDL_RenderCopy + * blits straight to pixel space - so the flip is a bug for us, + * not a compatibility shim. Mirrors gdi_overlay_vertex_geom for + * the same reason. */ + vid->overlays[index].vert_coords[0] = x; + vid->overlays[index].vert_coords[1] = y; + vid->overlays[index].vert_coords[2] = w; + vid->overlays[index].vert_coords[3] = h; +} + +static void sdl2_overlay_enable(void *data, bool state) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid) + return; + vid->overlays_enabled = state; +} + +static void sdl2_overlay_full_screen(void *data, bool enable) +{ + unsigned i; + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid || !vid->overlays) + return; + for (i = 0; i < vid->overlays_size; i++) + vid->overlays[i].fullscreen = enable; +} + +static void sdl2_overlay_set_alpha(void *data, unsigned index, float mod) +{ + sdl2_video_t *vid = (sdl2_video_t*)data; + if (!vid || index >= vid->overlays_size) + return; + vid->overlays[index].alpha_mod = mod; +} + +/* Render every enabled overlay with the current SDL_Renderer. + * Caller is responsible for setting an appropriate viewport before + * calling - typically the full-window viewport so fullscreen + * overlays cover the entire output, with non-fullscreen overlays + * placed in window-relative coords by the caller's choice of + * base_x/base_y/base_w/base_h. */ +static void sdl2_overlays_render(sdl2_video_t *vid) +{ + unsigned i; + + if (!vid || !vid->overlays || !vid->overlays_enabled) + return; + + for (i = 0; i < vid->overlays_size; i++) + { + SDL_Rect dst; + struct sdl2_overlay *o = &vid->overlays[i]; + float vx, vy, vw, vh; + int base_x, base_y; + unsigned base_w, base_h; + Uint8 alpha_byte; + + if (!o->tex || o->tex_w == 0 || o->tex_h == 0) + continue; + if (o->alpha_mod <= 0.0f) + continue; + + /* vert_coords are stored verbatim in y-down 0..1 space with + * (x, y) = top-left corner of the rect, so a straight multiply + * by base_w/base_h gives the destination rect in pixel coords + * (no flip). See sdl2_overlay_vertex_geom for the rationale. */ + vx = o->vert_coords[0]; + vy = o->vert_coords[1]; + vw = o->vert_coords[2]; + vh = o->vert_coords[3]; + + if (o->fullscreen) + { + base_x = 0; + base_y = 0; + base_w = vid->vp.full_width; + base_h = vid->vp.full_height; + } + else + { + base_x = (int)vid->vp.x; + base_y = (int)vid->vp.y; + base_w = vid->vp.width; + base_h = vid->vp.height; + } + + dst.x = base_x + (int)(vx * (float)base_w); + dst.y = base_y + (int)(vy * (float)base_h); + dst.w = (int)(vw * (float)base_w); + dst.h = (int)(vh * (float)base_h); + + if (dst.w <= 0 || dst.h <= 0) + continue; + + alpha_byte = (Uint8)(o->alpha_mod * 255.0f); + SDL_SetTextureAlphaMod(o->tex, alpha_byte); + + /* tex_coords sub-rect into the source texture. Most touch + * overlays use the full image (0,0,1,1) but atlases need this + * to pick the right button. */ + { + SDL_Rect src; + src.x = (int)(o->tex_coords[0] * (float)o->tex_w); + src.y = (int)(o->tex_coords[1] * (float)o->tex_h); + src.w = (int)(o->tex_coords[2] * (float)o->tex_w); + src.h = (int)(o->tex_coords[3] * (float)o->tex_h); + if (src.w <= 0 || src.h <= 0) + { + src.x = 0; src.y = 0; + src.w = (int)o->tex_w; src.h = (int)o->tex_h; + } + SDL_RenderCopy(vid->renderer, o->tex, &src, &dst); + } + } +} + +static const video_overlay_interface_t sdl2_overlay_iface = { + sdl2_overlay_enable, + sdl2_overlay_load, + sdl2_overlay_tex_geom, + sdl2_overlay_vertex_geom, + sdl2_overlay_full_screen, + sdl2_overlay_set_alpha +}; + +static void sdl2_get_overlay_interface(void *data, + const video_overlay_interface_t **iface) +{ + (void)data; + *iface = &sdl2_overlay_iface; +} +#endif /* HAVE_OVERLAY */ + video_driver_t video_sdl2 = { sdl2_gfx_init, sdl2_gfx_frame, @@ -689,13 +2126,17 @@ video_driver_t video_sdl2 = { sdl2_gfx_read_viewport, NULL, /* read_frame_raw */ #ifdef HAVE_OVERLAY - NULL, /* get_overlay_interface */ + sdl2_get_overlay_interface, #endif sdl2_gfx_poke_interface, NULL, /* wrap_type_to_enum */ NULL, /* shader_load_begin */ NULL, /* shader_load_step */ #ifdef HAVE_GFX_WIDGETS - NULL /* gfx_widgets_enabled */ +#if SDL_VERSION_ATLEAST(2, 0, 18) + sdl2_gfx_widgets_enabled +#else + NULL /* widgets need SDL_RenderGeometry */ +#endif #endif }; diff --git a/gfx/font_driver.c b/gfx/font_driver.c index 70f65b157c5..a6bfd1e0392 100644 --- a/gfx/font_driver.c +++ b/gfx/font_driver.c @@ -22,6 +22,13 @@ #include "../config.h" #endif +#ifdef HAVE_SDL2 +/* For the SDL_VERSION_ATLEAST gate around the SDL2 font case in + * font_driver_init_first - sdl2_raster_font is only emitted by + * sdl2_gfx.c when SDL >= 2.0.18 (SDL_RenderGeometry availability). */ +#include +#endif + #include "font_driver.h" #include "video_thread_wrapper.h" @@ -153,6 +160,20 @@ static bool font_init_first( } } #endif +#if defined(HAVE_SDL2) && SDL_VERSION_ATLEAST(2, 0, 18) + case FONT_DRIVER_RENDER_SDL2: + { + void *data = sdl2_raster_font.init(video_data, + font_path, font_size, is_threaded); + if (data) + { + *font_driver = &sdl2_raster_font; + *font_handle = data; + return true; + } + } + break; +#endif #ifdef HAVE_D3D8 case FONT_DRIVER_RENDER_D3D8_API: { diff --git a/gfx/font_driver.h b/gfx/font_driver.h index 1a43a2ae62f..fb4592ac53e 100644 --- a/gfx/font_driver.h +++ b/gfx/font_driver.h @@ -161,6 +161,7 @@ extern font_renderer_t vga_font; extern font_renderer_t sixel_font; extern font_renderer_t switch_font; extern font_renderer_t rsx_font; +extern font_renderer_t sdl2_raster_font; extern font_renderer_driver_t stb_font_renderer; extern font_renderer_driver_t stb_unicode_font_renderer; diff --git a/gfx/gfx_display.c b/gfx/gfx_display.c index d175f2db492..8e8a159f2c4 100644 --- a/gfx/gfx_display.c +++ b/gfx/gfx_display.c @@ -18,6 +18,14 @@ #include "gfx_display.h" +#ifdef HAVE_SDL2 +/* SDL_version.h is needed for the SDL_VERSION_ATLEAST gate on the + * gfx_display_ctx_sdl2 table entry. The driver itself requires + * SDL_RenderGeometry (>= 2.0.18); on older SDL the symbol is not + * defined in sdl2_gfx.c, so the table entry must be elided too. */ +#include +#endif + #include "../configuration.h" #include "../tasks/tasks_internal.h" #include "../verbosity.h" @@ -102,6 +110,9 @@ static gfx_display_ctx_driver_t *gfx_display_ctx_drivers[] = { #ifdef HAVE_GDI &gfx_display_ctx_gdi, #endif +#endif +#if defined(HAVE_SDL2) && SDL_VERSION_ATLEAST(2, 0, 18) + &gfx_display_ctx_sdl2, #endif NULL, }; diff --git a/gfx/gfx_display.h b/gfx/gfx_display.h index cafd9db61c4..311a535e964 100644 --- a/gfx/gfx_display.h +++ b/gfx/gfx_display.h @@ -102,7 +102,8 @@ enum gfx_display_driver_type GFX_VIDEO_DRIVER_WIIU, GFX_VIDEO_DRIVER_GDI, GFX_VIDEO_DRIVER_SWITCH, - GFX_VIDEO_DRIVER_RSX + GFX_VIDEO_DRIVER_RSX, + GFX_VIDEO_DRIVER_SDL2 }; typedef struct gfx_display_ctx_draw gfx_display_ctx_draw_t; @@ -353,6 +354,7 @@ extern gfx_display_ctx_driver_t gfx_display_ctx_wiiu; extern gfx_display_ctx_driver_t gfx_display_ctx_gdi; extern gfx_display_ctx_driver_t gfx_display_ctx_switch; extern gfx_display_ctx_driver_t gfx_display_ctx_rsx; +extern gfx_display_ctx_driver_t gfx_display_ctx_sdl2; RETRO_END_DECLS diff --git a/gfx/video_defines.h b/gfx/video_defines.h index 55bed0c49ee..4f98837ea98 100644 --- a/gfx/video_defines.h +++ b/gfx/video_defines.h @@ -142,6 +142,7 @@ enum font_driver_render_api FONT_DRIVER_RENDER_WIIU, FONT_DRIVER_RENDER_VULKAN_API, FONT_DRIVER_RENDER_METAL_API, + FONT_DRIVER_RENDER_SDL2, FONT_DRIVER_RENDER_CACA, FONT_DRIVER_RENDER_SIXEL, FONT_DRIVER_RENDER_NETWORK_VIDEO,