Files
vanilla/gui/ui/ui_sdl.c
T
itsmattkc c809ff73e7 implement support for display brightness
commit 54e685ba9fef2b5fc1505ddc932e4fa3fe87ed68
Author: itsmattkc <34096995+itsmattkc@users.noreply.github.com>
Date:   Wed Aug 26 17:46:15 2026 -0700

    improvements

commit b442a51a71
Author: itsmattkc <34096995+itsmattkc@users.noreply.github.com>
Date:   Wed Aug 26 14:49:31 2026 -0700

    docker: add new build deps for android

commit 1e6370391e
Merge: b21181c 0681459
Author: itsmattkc <34096995+itsmattkc@users.noreply.github.com>
Date:   Wed Aug 26 14:37:15 2026 -0700

    Merge branch 'master' into brightness

commit b21181c675
Author: itsmattkc <34096995+itsmattkc@users.noreply.github.com>
Date:   Sun Aug 23 16:58:09 2026 -0700

    update display brightness - initial implementation
2026-08-26 17:50:12 -07:00

2379 lines
83 KiB
C

#include "ui_sdl.h"
#include <stdatomic.h>
#include <math.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_hints.h>
#include <SDL2/SDL_syswm.h>
#include <SDL_image.h>
#include <SDL_power.h>
#include <SDL_ttf.h>
#include <pthread.h>
#include <time.h>
#include <vanilla.h>
#include <libavutil/hwcontext.h>
#include <unistd.h>
#ifdef ANDROID
#include <libavcodec/mediacodec.h>
#include "ui_sdl_android.h"
#endif
#ifdef VANILLA_HAS_EGL
#include <SDL2/SDL_egl.h>
#include <SDL2/SDL_opengl.h>
#include <SDL2/SDL_opengles2.h>
#endif // #ifdef VANILLA_HAS_EGL
#ifdef VANILLA_DRM_AVAILABLE
#include <libavutil/hwcontext_drm.h>
#include <drm_fourcc.h>
#include "ui_sdl_drm.h"
#endif // VANILLA_DRM_AVAILABLE
#ifdef VANILLA_CUDA_AVAILABLE
#include <libavutil/hwcontext_cuda.h>
#include <cuda_runtime.h>
#include <cuda_gl_interop.h>
#endif // VANILLA_CUDA_AVAILABLE
#include "config.h"
#include "menu/menu.h"
#include "menu/menu_game.h"
#include "platform.h"
#include "ui_priv.h"
#include "ui_util.h"
#ifdef VANILLA_NX_IMU
#include "platform_nx_imu.h"
#endif
#if defined(ANDROID) || defined(__ANDROID__)
// TODO
#elif defined(__linux__)
#include "ui_sdl_linux.h"
#endif
#define MIN(a,b) (((a)<(b))?(a):(b))
#define PW_CHAR_SIZE 20
#define PW_CHAR_PAD 2
#define AUDIO_BUFFER_SIZE 1664 // Wii U never deviates from this so we can hardcode it
#define AUDIO_BUFFER_COUNT 4
// Our tick rate is 180Hz, which matches the original gamepad input rate.
// Our render tick rate is 60Hz (or 1/3 the input tick rate).
#define VUI_INPUT_TICK_RATE 180
#define VUI_RENDER_TICK_DIVISOR 3
typedef struct {
SDL_Texture *texture;
int w;
int h;
char text[MAX_BUTTON_TEXT];
char icon[MAX_BUTTON_TEXT];
int32_t icon_mod;
int checked;
} vui_sdl_cached_texture_t;
typedef struct {
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Rect dst_rect;
SDL_Texture *background;
SDL_Texture *layer_data[MAX_BUTTON_COUNT];
SDL_Texture *label_data[MAX_BUTTON_COUNT];
vui_sdl_cached_texture_t button_cache[MAX_BUTTON_COUNT];
vui_sdl_cached_texture_t image_cache[MAX_BUTTON_COUNT];
vui_sdl_cached_texture_t textedit_cache[MAX_BUTTON_COUNT];
TTF_Font *sysfont;
TTF_Font *sysfont_small;
TTF_Font *sysfont_tiny;
SDL_AudioDeviceID audio;
SDL_AudioDeviceID mic;
SDL_AudioStream *mic_resampler;
SDL_GameController *controller;
SDL_GameController *controller_gyros;
SDL_Texture *game_tex;
#ifdef ANDROID
SDL_Texture *android_video_tex;
float android_video_transform[16];
#endif
int last_shown_toast;
SDL_Texture *toast_tex;
struct timeval toast_expiry;
AVFrame *frame;
AVFrame *held_frame; // keeps the displayed dmabuf alive until the next frame replaces it
uint64_t present_frame_sequence;
Uint64 update_tick_origin;
Uint64 update_tick_frequency;
uint64_t update_tick;
#ifdef VANILLA_DRM_AVAILABLE
int fast_drm_active;
#endif
#ifdef ANDROID
int android_video_ready;
#endif
SDL_Texture *pw_tex;
uint8_t audio_buffer[AUDIO_BUFFER_COUNT][AUDIO_BUFFER_SIZE];
_Atomic size_t audio_wseq;
_Atomic size_t audio_rseq;
Uint32 last_power_state_check;
vui_power_state_t last_power_state;
uint16_t last_vibration_state;
} vui_sdl_context_t;
#ifdef VANILLA_CUDA_AVAILABLE
typedef struct {
struct cudaGraphicsResource *resY;
struct cudaGraphicsResource *resUV;
} vui_cuda_context_t;
#endif
static int vibrate = 0;
void init_gamepad(vui_context_t *ctx)
{
vibrate = 0;
// Set up default keys/buttons/axes
ctx->default_button_map[SDL_CONTROLLER_BUTTON_A] = VANILLA_BTN_A;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_B] = VANILLA_BTN_B;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_X] = VANILLA_BTN_X;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_Y] = VANILLA_BTN_Y;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_BACK] = VANILLA_BTN_MINUS;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_GUIDE] = VANILLA_BTN_HOME;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_MISC1] = VANILLA_BTN_TV;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_START] = VANILLA_BTN_PLUS;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_LEFTSTICK] = VANILLA_BTN_L3;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_RIGHTSTICK] = VANILLA_BTN_R3;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_LEFTSHOULDER] = VANILLA_BTN_L;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_RIGHTSHOULDER] = VANILLA_BTN_R;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_DPAD_UP] = VANILLA_BTN_UP;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_DPAD_DOWN] = VANILLA_BTN_DOWN;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_DPAD_LEFT] = VANILLA_BTN_LEFT;
ctx->default_button_map[SDL_CONTROLLER_BUTTON_DPAD_RIGHT] = VANILLA_BTN_RIGHT;
ctx->default_axis_map[SDL_CONTROLLER_AXIS_LEFTX] = VANILLA_AXIS_L_X;
ctx->default_axis_map[SDL_CONTROLLER_AXIS_LEFTY] = VANILLA_AXIS_L_Y;
ctx->default_axis_map[SDL_CONTROLLER_AXIS_RIGHTX] = VANILLA_AXIS_R_X;
ctx->default_axis_map[SDL_CONTROLLER_AXIS_RIGHTY] = VANILLA_AXIS_R_Y;
ctx->default_axis_map[SDL_CONTROLLER_AXIS_TRIGGERLEFT] = VANILLA_BTN_ZL;
ctx->default_axis_map[SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = VANILLA_BTN_ZR;
ctx->default_key_map[SDL_SCANCODE_UP] = VANILLA_BTN_UP;
ctx->default_key_map[SDL_SCANCODE_DOWN] = VANILLA_BTN_DOWN;
ctx->default_key_map[SDL_SCANCODE_LEFT] = VANILLA_BTN_LEFT;
ctx->default_key_map[SDL_SCANCODE_RIGHT] = VANILLA_BTN_RIGHT;
ctx->default_key_map[SDL_SCANCODE_Z] = VANILLA_BTN_A;
ctx->default_key_map[SDL_SCANCODE_X] = VANILLA_BTN_B;
ctx->default_key_map[SDL_SCANCODE_C] = VANILLA_BTN_X;
ctx->default_key_map[SDL_SCANCODE_V] = VANILLA_BTN_Y;
ctx->default_key_map[SDL_SCANCODE_RETURN] = VANILLA_BTN_PLUS;
ctx->default_key_map[SDL_SCANCODE_LCTRL] = VANILLA_BTN_MINUS;
ctx->default_key_map[SDL_SCANCODE_H] = VANILLA_BTN_HOME;
ctx->default_key_map[SDL_SCANCODE_Y] = VANILLA_BTN_TV;
ctx->default_key_map[SDL_SCANCODE_W] = VANILLA_AXIS_L_UP;
ctx->default_key_map[SDL_SCANCODE_A] = VANILLA_AXIS_L_LEFT;
ctx->default_key_map[SDL_SCANCODE_S] = VANILLA_AXIS_L_DOWN;
ctx->default_key_map[SDL_SCANCODE_D] = VANILLA_AXIS_L_RIGHT;
ctx->default_key_map[SDL_SCANCODE_E] = VANILLA_BTN_L3;
ctx->default_key_map[SDL_SCANCODE_KP_8] = VANILLA_AXIS_R_UP;
ctx->default_key_map[SDL_SCANCODE_KP_4] = VANILLA_AXIS_R_LEFT;
ctx->default_key_map[SDL_SCANCODE_KP_2] = VANILLA_AXIS_R_DOWN;
ctx->default_key_map[SDL_SCANCODE_KP_6] = VANILLA_AXIS_R_RIGHT;
ctx->default_key_map[SDL_SCANCODE_KP_5] = VANILLA_BTN_R3;
ctx->default_key_map[SDL_SCANCODE_T] = VANILLA_BTN_L;
ctx->default_key_map[SDL_SCANCODE_G] = VANILLA_BTN_ZL;
ctx->default_key_map[SDL_SCANCODE_U] = VANILLA_BTN_R;
ctx->default_key_map[SDL_SCANCODE_J] = VANILLA_BTN_ZR;
ctx->default_key_map[SDL_SCANCODE_F5] = VPI_ACTION_TOGGLE_RECORDING;
ctx->default_key_map[SDL_SCANCODE_F12] = VPI_ACTION_SCREENSHOT;
ctx->default_key_map[SDL_SCANCODE_F11] = VPI_ACTION_TOGGLE_FULLSCREEN;
ctx->default_key_map[SDL_SCANCODE_ESCAPE] = VPI_ACTION_DISCONNECT;
ctx->default_key_map[SDL_SCANCODE_SLEEP] = VPI_ACTION_DISCONNECT; // Nintendo Switch power button
ctx->default_key_map[SDL_SCANCODE_VOLUMEUP] = VPI_ACTION_VOLUME_UP;
ctx->default_key_map[SDL_SCANCODE_VOLUMEDOWN] = VPI_ACTION_VOLUME_DOWN;
}
void find_valid_controller(vui_sdl_context_t *sdl_ctx)
{
// HACK: This is just for the Steam Deck. The Steam Deck provides a "virtual gamepad"
// that it expects games to use, however the "virtual gamepad" doesn't provide
// direct access to the Steam Deck's gyroscopes. We can bypass it, but then
// Steam Deck users lose usability features like remapping (and Vanilla will
// continue to receive inputs in the background, which are usually cut off on
// the virtual gamepad). To try to get the best of both worlds, we use a hybrid
// approach where we use the virtual gamepad for everything EXCEPT the gyros,
// and then use the direct hardware access JUST for the gyros.
int controller = -1;
int steam_virtual_gamepad_index = -1;
vpilog("Looking for game controllers...\n");
for (int i = 0; i < SDL_NumJoysticks(); i++) {
SDL_JoystickGUID guid = SDL_JoystickGetDeviceGUID(i);
char guid_str[64];
SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str));
// Ignore left and right joycon devices
if (!strcmp(guid_str, "0600c2f47e0500000620000000000000") || !strcmp(guid_str, "060091737e0500000720000000000000")) {
continue;
}
const char *ctrl_name = SDL_GameControllerNameForIndex(i);
vpilog(" Found %i: %s\n", i, ctrl_name);
if (ctrl_name != NULL && !strcmp(ctrl_name, "Steam Virtual Gamepad")) {
steam_virtual_gamepad_index = i;
} else if (controller == -1) {
controller = i;
}
}
SDL_GameController *steam_virtual_gamepad = NULL;
SDL_GameController *c = NULL;
if (steam_virtual_gamepad_index != -1) {
// Prefer the Steam Virtual Gamepad when present for everything EXCEPT gyros
steam_virtual_gamepad = SDL_GameControllerOpen(steam_virtual_gamepad_index);
vpilog("Using Steam Virtual Gamepad\n");
}
if (controller != -1) {
// Open regular controller
c = SDL_GameControllerOpen(controller);
if (c) {
// Enable gyro/accelerometer
const char *ctrl_name = SDL_GameControllerName(c);
SDL_GameControllerSetSensorEnabled(c, SDL_SENSOR_ACCEL, 1);
SDL_GameControllerSetSensorEnabled(c, SDL_SENSOR_GYRO, 1);
if (steam_virtual_gamepad) {
vpilog(" with \"%s\" for accelerometer/gyroscope\n", ctrl_name);
} else {
vpilog("Using game controller \"%s\"\n", ctrl_name);
}
}
}
if (steam_virtual_gamepad) {
sdl_ctx->controller = steam_virtual_gamepad;
sdl_ctx->controller_gyros = c;
} else {
sdl_ctx->controller = c;
}
}
void vui_sdl_audio_handler(const void *data, size_t size, void *userdata)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) userdata;
if (!sdl_ctx->audio) {
// Buffers will not have been initialized, so return before we try using them
return;
}
// static Uint32 last_call = 0;
// Uint32 now = SDL_GetTicks();
// vanilla_log("** PRODUCING AT %u delta %u", now, now - last_call);
// last_call = now;
size_t w = atomic_load_explicit(&sdl_ctx->audio_wseq, memory_order_relaxed);
uint8_t *buf = sdl_ctx->audio_buffer[w % AUDIO_BUFFER_COUNT];
memcpy(buf, data, size);
atomic_fetch_add_explicit(&sdl_ctx->audio_wseq, 1, memory_order_release);
}
void vui_sdl_vibrate_handler(uint8_t vibrate, void *userdata)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) userdata;
if (sdl_ctx->controller) {
uint16_t amount = vibrate ? 0xFFFF : 0;
if (sdl_ctx->last_vibration_state != amount) {
SDL_GameControllerRumble(sdl_ctx->controller, amount, amount, 0);
sdl_ctx->last_vibration_state = amount;
}
}
}
TTF_Font *get_font(vui_sdl_context_t *sdl_ctx, vui_font_size_t size)
{
switch (size) {
case VUI_FONT_SIZE_TINY:
return sdl_ctx->sysfont_tiny;
case VUI_FONT_SIZE_SMALL:
return sdl_ctx->sysfont_small;
case VUI_FONT_SIZE_NORMAL:
default:
return sdl_ctx->sysfont;
}
}
int vui_sdl_font_height_handler(vui_font_size_t size, void *userdata)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) userdata;
TTF_Font *font = get_font(sdl_ctx, size);
int h = 0;
if (font) {
TTF_SizeText(font, "Hj", 0, &h);
}
return h;
}
void vui_sdl_text_open_handler(vui_context_t *ctx, int textedit, int open, void *userdata)
{
if (open) {
vui_textedit_t *edit = &ctx->textedits[textedit];
SDL_Rect r;
r.x = edit->x;
r.y = edit->y;
r.w = edit->w;
r.h = edit->h;
SDL_SetTextInputRect(&r);
SDL_StartTextInput();
} else {
SDL_StopTextInput();
}
}
vui_power_state_t vui_sdl_power_state_handler(vui_context_t *ctx, int *percent)
{
#if 0
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) ctx->platform_data;
// Turns out checking the power state is an expensive operation. Let's
// limit checks to once per minute and cache the result.
Uint32 now = SDL_GetTicks();
if (now >= sdl_ctx->last_power_state_check + 60000) {
sdl_ctx->last_power_state = (vui_power_state_t) SDL_GetPowerInfo(NULL, percent);
sdl_ctx->last_power_state_check = now;
}
return sdl_ctx->last_power_state;
#else
return (vui_power_state_t) SDL_GetPowerInfo(NULL, percent);
#endif
}
void vui_sdl_fullscreen_enabled_handler(vui_context_t *ctx, int enabled, void *userdata)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) ctx->platform_data;
SDL_SetWindowFullscreen(sdl_ctx->window, enabled ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
SDL_ShowCursor(vpi_config.cursor_in_fullscreen || !enabled);
}
void vui_sdl_brightness_set_handler(vui_context_t *ctx, float brightness, void *userdata)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) ctx->platform_data;
vpilog("SETTING BRIGHTNESS TO %.1f\n", brightness);
#if defined(ANDROID) || defined(__ANDROID__)
if (vui_sdl_android_set_brightness(brightness) == 0) {
return;
}
#elif defined(__linux__)
if (vui_sdl_linux_set_brightness(brightness) == 0) {
return;
}
#endif
// Fallback
SDL_SetWindowBrightness(sdl_ctx->window, brightness);
}
void vui_sdl_get_key_mapping_handler(vui_context_t *ctx, int vanilla_btn, void *userdata)
{
}
void vui_sdl_set_key_mapping_handler(vui_context_t *ctx, int vanilla_btn, int key, void *userdata)
{
}
void vui_sdl_audio_set_enabled(vui_context_t *ctx, int enabled, void *userdata)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) userdata;
SDL_PauseAudioDevice(sdl_ctx->audio, !enabled);
}
void vui_sdl_mic_set_enabled(vui_context_t *ctx, int enabled, void *userdata)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) userdata;
SDL_PauseAudioDevice(sdl_ctx->mic, !enabled);
}
void audio_callback(void *userdata, Uint8 *stream, int len)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) userdata;
// static Uint32 last_call = 0;
// Uint32 now = SDL_GetTicks();
// vanilla_log("== CONSUMING AT %u delta %u", now, now - last_call);
// last_call = now;
size_t r = atomic_load_explicit(&sdl_ctx->audio_rseq, memory_order_relaxed);
size_t w = atomic_load_explicit(&sdl_ctx->audio_wseq, memory_order_acquire);
if (r != w) {
size_t min = w - AUDIO_BUFFER_COUNT + 1;
if (r < min) {
vanilla_log(" AUDIO SKIPPED FROM %zu TO %zu", r, min);
r = min;
}
memcpy(stream, sdl_ctx->audio_buffer[r % AUDIO_BUFFER_COUNT], len);
r++;
atomic_store_explicit(&sdl_ctx->audio_rseq, r, memory_order_release);
} else {
memset(stream, 0, len);
}
}
void mic_callback(void *userdata, Uint8 *stream, int len)
{
vui_context_t *ctx = (vui_context_t *) userdata;
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) ctx->platform_data;
if (!ctx->mic_callback) {
return;
}
if (sdl_ctx->mic_resampler) {
// We open the mic at the same rate as playback (48kHz) to avoid
// reconfiguring the shared Tegra audio clock domain, then resample
// down to the 16kHz the Wii U expects.
SDL_AudioStreamPut(sdl_ctx->mic_resampler, stream, len);
int avail;
while ((avail = SDL_AudioStreamAvailable(sdl_ctx->mic_resampler)) > 0) {
uint8_t buf[2048];
int chunk = avail < (int) sizeof(buf) ? avail : (int) sizeof(buf);
int got = SDL_AudioStreamGet(sdl_ctx->mic_resampler, buf, chunk);
if (got > 0) {
ctx->mic_callback(ctx->mic_callback_data, buf, got);
} else {
break;
}
}
} else {
ctx->mic_callback(ctx->mic_callback_data, stream, len);
}
}
int vui_sdl_event_thread(void *data)
{
vui_context_t *vui = (vui_context_t *) data;
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) vui->platform_data;
SDL_Event ev;
// while (!vui->quit) {
// while (SDL_WaitEventTimeout(&ev, 100)) {
while (SDL_PollEvent(&ev)) {
switch (ev.type) {
case SDL_QUIT:
vanilla_stop();
vui_quit(vui);
return 0;
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
{
// Ensure dst_rect is initialized
SDL_Rect *dst_rect = &sdl_ctx->dst_rect;
if (dst_rect->w > 0 && dst_rect->h > 0) {
// Translate screen coords to logical coords
int tr_x, tr_y;
tr_x = (ev.button.x - dst_rect->x) * vui->screen_width / dst_rect->w;
tr_y = (ev.button.y - dst_rect->y) * vui->screen_height / dst_rect->h;
if (vui->game_mode) {
// In game mode, pass clicks directly to Vanilla
int x, y;
if (ev.button.button == SDL_BUTTON_LEFT && (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEMOTION)) {
x = tr_x;
y = tr_y;
} else {
x = -1;
y = -1;
}
vanilla_set_touch(x, y);
} else {
// Otherwise, handle ourselves
if (ev.type == SDL_MOUSEBUTTONDOWN)
vui_process_mousedown(vui, tr_x, tr_y);
else if (ev.type == SDL_MOUSEBUTTONUP)
vui_process_mouseup(vui, tr_x, tr_y);
if (vui->active_textedit != -1 && (ev.type == SDL_MOUSEBUTTONDOWN || ev.type == SDL_MOUSEMOTION) && ev.button.button == SDL_BUTTON_LEFT) {
// Put cursor in correct position
vui_textedit_t *edit = &vui->textedits[vui->active_textedit];
TTF_Font *font = get_font(sdl_ctx, edit->size);
// Determine best location for new cursor
int rel_x = tr_x - edit->x;
char tmp[MAX_BUTTON_TEXT];
int new_cursor = 0;
int diff = rel_x;
size_t len = 0;
char *src = edit->text;
while (*src != 0) {
src = vui_utf8_advance(src);
size_t len = src - edit->text;
strncpy(tmp, edit->text, len);
tmp[len] = 0;
int tw;
TTF_SizeUTF8(font, tmp, &tw, 0);
int this_diff = abs(tw - rel_x);
if (this_diff < diff) {
diff = this_diff;
new_cursor++;
} else {
break;
}
}
vui_textedit_set_cursor(vui, vui->active_textedit, new_cursor);
}
}
}
break;
}
case SDL_CONTROLLERDEVICEADDED:
// Attempt to find controller if one doesn't exist already
if (!sdl_ctx->controller) {
find_valid_controller(sdl_ctx);
}
break;
case SDL_CONTROLLERDEVICEREMOVED:
// Handle current controller being removed
if ((sdl_ctx->controller && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(sdl_ctx->controller)))
|| (sdl_ctx->controller_gyros && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(sdl_ctx->controller_gyros)))) {
if (sdl_ctx->controller) {
SDL_GameControllerClose(sdl_ctx->controller);
sdl_ctx->controller = NULL;
}
if (sdl_ctx->controller_gyros) {
SDL_GameControllerClose(sdl_ctx->controller_gyros);
sdl_ctx->controller_gyros = NULL;
}
find_valid_controller(sdl_ctx);
}
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
if (sdl_ctx->controller && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(sdl_ctx->controller))) {
int btn_idx = ev.cbutton.button;
int vanilla_btn;
// First try to read from config. If unmapped, fallback to default.
vanilla_btn = vpi_config.buttonmap[btn_idx];
if (vanilla_btn == VPI_CONFIG_UNMAPPED) {
vanilla_btn = vui->default_button_map[btn_idx];
}
if (vanilla_btn != -1) {
if (vanilla_btn > VPI_ACTION_START_INDEX) {
if (ev.cbutton.state == SDL_PRESSED) {
vpi_menu_action(vui, (vpi_extra_action_t) vanilla_btn);
}
} else {
// Handle ABXY swap
if (vpi_config.swap_abxy) {
switch (vanilla_btn) {
case VANILLA_BTN_A: vanilla_btn = VANILLA_BTN_B; break;
case VANILLA_BTN_B: vanilla_btn = VANILLA_BTN_A; break;
case VANILLA_BTN_X: vanilla_btn = VANILLA_BTN_Y; break;
case VANILLA_BTN_Y: vanilla_btn = VANILLA_BTN_X; break;
}
}
if (vui->game_mode) {
vanilla_set_button(vanilla_btn, ev.cbutton.state == SDL_PRESSED ? INT16_MAX : 0);
} else if (ev.cbutton.state == SDL_PRESSED) {
vui_process_keydown(vui, vanilla_btn);
} else {
vui_process_keyup(vui, vanilla_btn);
}
}
}
}
break;
case SDL_CONTROLLERAXISMOTION:
if (sdl_ctx->controller && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(sdl_ctx->controller))) {
int axis_idx = ev.caxis.axis;
int vanilla_axis;
// First try to read from config. If unmapped, fallback to default.
vanilla_axis = vpi_config.axismap[axis_idx];
if (vanilla_axis == VPI_CONFIG_UNMAPPED) {
vanilla_axis = vui->default_axis_map[ev.caxis.axis];
}
Sint16 axis_value = ev.caxis.value;
if (vanilla_axis != -1) {
if (vui->game_mode) {
vanilla_set_button(vanilla_axis, axis_value);
} else if (vanilla_axis == SDL_CONTROLLER_AXIS_LEFTX) {
if (axis_value < 0)
vui_process_keydown(vui, VANILLA_AXIS_L_LEFT);
else if (axis_value > 0)
vui_process_keydown(vui, VANILLA_AXIS_L_RIGHT);
else {
vui_process_keyup(vui, VANILLA_AXIS_L_LEFT);
vui_process_keyup(vui, VANILLA_AXIS_L_RIGHT);
}
} else if (vanilla_axis == SDL_CONTROLLER_AXIS_LEFTY) {
if (axis_value < 0)
vui_process_keydown(vui, VANILLA_AXIS_L_UP);
else if (axis_value > 0)
vui_process_keydown(vui, VANILLA_AXIS_L_DOWN);
else {
vui_process_keyup(vui, VANILLA_AXIS_L_UP);
vui_process_keyup(vui, VANILLA_AXIS_L_DOWN);
}
}
}
}
break;
case SDL_CONTROLLERSENSORUPDATE:
if (ev.csensor.sensor == SDL_SENSOR_ACCEL) {
vanilla_set_button(VANILLA_SENSOR_ACCEL_X, pack_float(ev.csensor.data[0]));
vanilla_set_button(VANILLA_SENSOR_ACCEL_Y, pack_float(ev.csensor.data[1]));
vanilla_set_button(VANILLA_SENSOR_ACCEL_Z, pack_float(ev.csensor.data[2]));
} else if (ev.csensor.sensor == SDL_SENSOR_GYRO) {
vanilla_set_button(VANILLA_SENSOR_GYRO_PITCH, pack_float(ev.csensor.data[0]));
vanilla_set_button(VANILLA_SENSOR_GYRO_YAW, pack_float(ev.csensor.data[1]));
vanilla_set_button(VANILLA_SENSOR_GYRO_ROLL, pack_float(ev.csensor.data[2]));
}
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
{
if (vui->key_override_handler) {
if (ev.key.keysym.scancode == SDL_SCANCODE_ESCAPE) {
vui->key_override_cancel_handler(vui, vui->key_override_handler_data);
} else {
vui->key_override_handler(vui, ev.key.keysym.scancode, vui->key_override_handler_data);
}
} else if (vui->active_textedit == -1) {
int key_idx = ev.key.keysym.scancode;
int vanilla_btn;
// First try to read from config. If unmapped, fallback to default.
vanilla_btn = vpi_config.keymap[key_idx];
if (vanilla_btn == VPI_CONFIG_UNMAPPED) {
vanilla_btn = vui->default_key_map[key_idx];
}
if (vanilla_btn > VPI_ACTION_START_INDEX) {
if (ev.type == SDL_KEYDOWN)
vpi_menu_action(vui, (vpi_extra_action_t) vanilla_btn);
} else if (vanilla_btn != -1) {
if (vui->game_mode) {
vanilla_set_button(vanilla_btn, ev.type == SDL_KEYDOWN ? INT16_MAX : 0);
} else if (ev.type == SDL_KEYDOWN) {
vui_process_keydown(vui, vanilla_btn);
} else {
vui_process_keyup(vui, vanilla_btn);
}
}
} else if (ev.type == SDL_KEYDOWN) {
switch (ev.key.keysym.scancode) {
case SDL_SCANCODE_BACKSPACE:
vui_textedit_backspace(vui, vui->active_textedit);
break;
case SDL_SCANCODE_LEFT:
vui_textedit_move_cursor(vui, vui->active_textedit, -1);
break;
case SDL_SCANCODE_RIGHT:
vui_textedit_move_cursor(vui, vui->active_textedit, 1);
break;
case SDL_SCANCODE_HOME:
vui_textedit_move_cursor(vui, vui->active_textedit, -MAX_BUTTON_TEXT);
break;
case SDL_SCANCODE_END:
vui_textedit_move_cursor(vui, vui->active_textedit, MAX_BUTTON_TEXT);
break;
case SDL_SCANCODE_DELETE:
vui_textedit_del(vui, vui->active_textedit);
break;
}
}
break;
}
case SDL_TEXTINPUT:
vui_textedit_input(vui, vui->active_textedit, ev.text.text);
break;
case SDL_TEXTEDITING:
vpilog("text editing!\n");
break;
case SDL_APP_WILLENTERBACKGROUND:
// On mobile, if the user is about to switch away from us,
// disconnect from the console so it doesn't remain burning up
// resources in the background
if (vui_game_mode_get(vui)) {
// Send shutdown signal
vpi_game_shutdown();
}
break;
}
}
// }
return 0;
}
int vui_init_window(vui_context_t *ctx, vui_sdl_context_t *sdl_ctx, Uint32 window_flags)
{
int win_w, win_h;
if (window_flags & SDL_WINDOW_FULLSCREEN || window_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) {
// NOTE: Set window size to fullscreen. Theoretically, this shouldn't be
// necessary, but on the Nintendo Switch, if we set the window size
// to 854x480, everything gets cropped to that size on its 1280x720
// screen. Might be a bug with the specific SDL version we use in
// our Buildroot system, but still this is a cheap fix.
SDL_DisplayMode mode;
SDL_GetCurrentDisplayMode(0, &mode);
win_w = mode.w;
win_h = mode.h;
} else {
win_w = ctx->screen_width;
win_h = ctx->screen_height;
}
sdl_ctx->window = SDL_CreateWindow("Vanilla", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, win_w, win_h, window_flags);
if (!sdl_ctx->window) {
vpilog("Failed to CreateWindow: %s\n", SDL_GetError());
return -1;
}
sdl_ctx->renderer = SDL_CreateRenderer(sdl_ctx->window, -1, SDL_RENDERER_ACCELERATED);
if (!sdl_ctx->renderer) {
vpilog("Failed to CreateRenderer: %s\n", SDL_GetError());
return -1;
}
return 0;
}
int vui_init_sdl(vui_context_t *ctx, int fullscreen)
{
// Enable Steam Deck gyroscopes even while Steam is open and in gaming mode
// SDL_SetHintWithPriority("SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD", "0", SDL_HINT_OVERRIDE);
SDL_SetHintWithPriority(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, "", SDL_HINT_OVERRIDE);
#ifdef ANDROID
// Force SDL to use landscape orientations only
SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
// Force opengles2 for hardware decode interoperability
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");
#endif
// Force EGL when using X11 so that VAAPI works
SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1");
// Enable linear filtering
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) < 0) {
vpilog("Failed to initialize SDL: %s\n", SDL_GetError());
return -1;
}
vui_sdl_context_t *sdl_ctx = malloc(sizeof(vui_sdl_context_t));
memset(sdl_ctx, 0, sizeof(vui_sdl_context_t));
ctx->platform_data = sdl_ctx;
SDL_WindowFlags window_flags = SDL_WINDOW_RESIZABLE;
if (fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
SDL_ShowCursor(vpi_config.cursor_in_fullscreen);
}
#ifdef VANILLA_HAS_EGL
// Lookup ahead of time if we're on X11 + OpenGL (will be important for later check...)
const char *driver = SDL_GetCurrentVideoDriver();
if (!strcmp(driver, "x11") || !strcmp(driver, "wayland")) {
SDL_RendererInfo info;
SDL_GetRenderDriverInfo(0, &info);
if (!strcmp(info.name, "opengl")
|| !strcmp(info.name, "opengles")
|| !strcmp(info.name, "opengles2")) {
// This saves a small amount of time on the EGL check below because
// SDL will fail on the earlier CreateWindow call rather than the
// later CreateRenderer call.
window_flags |= SDL_WINDOW_OPENGL;
vpi_egl_available = 1;
}
}
#endif // VANILLA_HAS_EGL
if (vui_init_window(ctx, sdl_ctx, window_flags) != 0) {
// Re-attempt without EGL (X11/NVIDIA do not support this)
vpilog("Trying again without forcing EGL...\n");
SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "0");
vpi_egl_available = 0;
// Determine if we created a window, destroy it if so
if (sdl_ctx->window) {
SDL_DestroyWindow(sdl_ctx->window);
sdl_ctx->window = 0;
}
// SDL video subsystem must be reinitialized after changing hint
SDL_QuitSubSystem(SDL_INIT_VIDEO);
SDL_InitSubSystem(SDL_INIT_VIDEO);
if (vui_init_window(ctx, sdl_ctx, window_flags) != 0) {
// It's not because we forced EGL, so it's something else we're not prepared for
return -1;
}
}
if (!vpi_egl_available) {
vpilog("EGL unavailable, VAAPI will be disabled\n");
}
// Open audio output device
SDL_AudioSpec desired = {0}, obtained;
desired.freq = 48000;
desired.format = AUDIO_S16LSB;
desired.channels = 2;
desired.callback = audio_callback;
desired.userdata = sdl_ctx;
desired.samples = AUDIO_BUFFER_SIZE / 2 / 2; // Buffer size (bytes) divided by 2 channels (stereo) divided by 2 bytes per sample (16-bit)
sdl_ctx->audio = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
if (sdl_ctx->audio) {
// vpilog("obtained.samples = %u\n", obtained.samples);
// Set up buffers
atomic_store(&sdl_ctx->audio_wseq, 0);
atomic_store(&sdl_ctx->audio_rseq, 0);
// Set up handler for audio submitted from VPI to VUI
ctx->audio_handler = vui_sdl_audio_handler;
ctx->audio_handler_data = sdl_ctx;
// Set up handler for enabling audio
ctx->audio_enabled_handler = vui_sdl_audio_set_enabled;
ctx->audio_enabled_handler_data = sdl_ctx;
} else {
vpilog("Failed to open audio device\n");
}
// Open audio input device. We open at the same rate as playback (48kHz)
// because some shared-clock-domain audio hardware (e.g. Tegra ASoC) will
// reconfigure the codec/I2S rate on capture open, which then breaks any
// playback stream that was running at a different rate. We resample to
// 16kHz in software in mic_callback before forwarding to the Wii U.
SDL_AudioSpec mic_desired = {0};
mic_desired.freq = 48000;
mic_desired.format = AUDIO_S16LSB;
mic_desired.callback = mic_callback;
mic_desired.userdata = ctx;
mic_desired.channels = 1;
sdl_ctx->mic = SDL_OpenAudioDevice(NULL, 1, &mic_desired, NULL, 0);
if (sdl_ctx->mic) {
sdl_ctx->mic_resampler = SDL_NewAudioStream(
AUDIO_S16LSB, 1, 48000,
AUDIO_S16LSB, 1, 16000);
if (!sdl_ctx->mic_resampler) {
vpilog("Failed to create mic resampler: %s\n", SDL_GetError());
}
ctx->mic_enabled_handler = vui_sdl_mic_set_enabled;
ctx->mic_enabled_handler_data = sdl_ctx;
} else {
vpilog("Failed to open microphone device\n");
}
ctx->vibrate_handler = vui_sdl_vibrate_handler;
ctx->vibrate_handler_data = sdl_ctx;
sdl_ctx->last_vibration_state = 0;
ctx->font_height_handler = vui_sdl_font_height_handler;
ctx->font_height_handler_data = sdl_ctx;
ctx->text_open_handler = vui_sdl_text_open_handler;
ctx->power_state_handler = vui_sdl_power_state_handler;
sdl_ctx->last_power_state_check = 0;
sdl_ctx->last_power_state = VUI_POWERSTATE_UNKNOWN;
ctx->fullscreen_enabled_handler = vui_sdl_fullscreen_enabled_handler;
ctx->brightness_set_handler = vui_sdl_brightness_set_handler;
if (TTF_Init()) {
vpilog("Failed to TTF_Init\n");
return -1;
}
char font_filename[4096];
vpi_asset_filename(font_filename, sizeof(font_filename), "font", "system.ttf");
sdl_ctx->sysfont = TTF_OpenFont(font_filename, 36);
if (!sdl_ctx->sysfont) {
vpilog("Failed to TTF_OpenFont: %s\n", font_filename);
return -1;
}
sdl_ctx->sysfont_small = TTF_OpenFont(font_filename, 24);
if (!sdl_ctx->sysfont_small) {
vpilog("Failed to TTF_OpenFont: %s\n", font_filename);
return -1;
}
sdl_ctx->sysfont_tiny = TTF_OpenFont(font_filename, 16);
if (!sdl_ctx->sysfont_tiny) {
vpilog("Failed to TTF_OpenFont: %s\n", font_filename);
return -1;
}
TTF_SetFontWrappedAlign(sdl_ctx->sysfont, TTF_WRAPPED_ALIGN_CENTER);
TTF_SetFontWrappedAlign(sdl_ctx->sysfont_small, TTF_WRAPPED_ALIGN_CENTER);
TTF_SetFontWrappedAlign(sdl_ctx->sysfont_tiny, TTF_WRAPPED_ALIGN_CENTER);
sdl_ctx->game_tex = 0;
#ifdef ANDROID
sdl_ctx->android_video_tex = NULL;
if (vui_sdl_android_create_video_texture(sdl_ctx->renderer, ctx->screen_width, ctx->screen_height, &sdl_ctx->android_video_tex) < 0) {
vpilog("Android hardware decoding unavailable, falling back to software\n");
}
#endif
sdl_ctx->background = 0;
sdl_ctx->last_shown_toast = 0;
sdl_ctx->toast_tex = 0;
sdl_ctx->pw_tex = 0;
memset(sdl_ctx->layer_data, 0, sizeof(sdl_ctx->layer_data));
memset(sdl_ctx->label_data, 0, sizeof(sdl_ctx->label_data));
memset(sdl_ctx->button_cache, 0, sizeof(sdl_ctx->button_cache));
memset(sdl_ctx->image_cache, 0, sizeof(sdl_ctx->image_cache));
memset(sdl_ctx->textedit_cache, 0, sizeof(sdl_ctx->textedit_cache));
sdl_ctx->controller = NULL;
sdl_ctx->controller_gyros = NULL;
find_valid_controller(sdl_ctx);
#ifdef VANILLA_NX_IMU
vpi_nx_imu_init();
#endif
sdl_ctx->frame = av_frame_alloc();
sdl_ctx->held_frame = av_frame_alloc();
// Initialize gamepad lookup tables
init_gamepad(ctx);
return 0;
}
void vui_close_sdl(vui_context_t *ctx)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) ctx->platform_data;
if (!sdl_ctx) {
return;
}
av_frame_free(&sdl_ctx->frame);
av_frame_free(&sdl_ctx->held_frame);
#ifdef VANILLA_NX_IMU
vpi_nx_imu_quit();
#endif
if (sdl_ctx->controller) {
SDL_GameControllerClose(sdl_ctx->controller);
}
if (sdl_ctx->audio) {
SDL_CloseAudioDevice(sdl_ctx->audio);
}
if (sdl_ctx->mic) {
SDL_CloseAudioDevice(sdl_ctx->mic);
}
if (sdl_ctx->mic_resampler) {
SDL_FreeAudioStream(sdl_ctx->mic_resampler);
sdl_ctx->mic_resampler = NULL;
}
if (sdl_ctx->pw_tex) {
SDL_DestroyTexture(sdl_ctx->pw_tex);
sdl_ctx->pw_tex = 0;
}
SDL_DestroyTexture(sdl_ctx->toast_tex);
#ifdef ANDROID
vui_sdl_android_destroy_video_texture();
if (sdl_ctx->game_tex == sdl_ctx->android_video_tex) {
sdl_ctx->game_tex = 0;
}
SDL_DestroyTexture(sdl_ctx->android_video_tex);
#endif
if (sdl_ctx->game_tex) SDL_DestroyTexture(sdl_ctx->game_tex);
SDL_DestroyTexture(sdl_ctx->background);
for (int i = 0; i < MAX_BUTTON_COUNT; i++) {
if (sdl_ctx->layer_data[i]) {
SDL_DestroyTexture(sdl_ctx->layer_data[i]);
}
if (sdl_ctx->label_data[i]) {
SDL_DestroyTexture(sdl_ctx->label_data[i]);
}
if (sdl_ctx->button_cache[i].texture) {
SDL_DestroyTexture(sdl_ctx->button_cache[i].texture);
}
if (sdl_ctx->image_cache[i].texture) {
SDL_DestroyTexture(sdl_ctx->image_cache[i].texture);
}
if (sdl_ctx->textedit_cache[i].texture) {
SDL_DestroyTexture(sdl_ctx->textedit_cache[i].texture);
}
}
TTF_CloseFont(sdl_ctx->sysfont_tiny);
TTF_CloseFont(sdl_ctx->sysfont_small);
TTF_CloseFont(sdl_ctx->sysfont);
TTF_Quit();
SDL_CloseAudioDevice(sdl_ctx->audio);
SDL_DestroyRenderer(sdl_ctx->renderer);
SDL_DestroyWindow(sdl_ctx->window);
free(sdl_ctx);
SDL_Quit();
}
float lerp(float a, float b, float f)
{
return a * (1.0 - f) + (b * f);
}
void vui_sdl_set_render_color(SDL_Renderer *renderer, vui_color_t color)
{
SDL_SetRenderDrawColor(
renderer,
color.r * 0xFF,
color.g * 0xFF,
color.b * 0xFF,
color.a * 0xFF
);
}
SDL_Texture *vui_sdl_load_texture(SDL_Renderer *renderer, const char *filename)
{
char buf[1024];
vpi_asset_filename(buf, sizeof(buf), "tex", filename);
return IMG_LoadTexture(renderer, buf);
}
float calculate_x_inset(float y, float h, float radius)
{
float x_inset = 0;
if (y < radius) {
float yb = y - radius;
x_inset = radius - sqrtf(radius*radius - yb*yb);
} else {
if (y >= h - radius) {
float yb = (h - y) - radius;
x_inset = radius - sqrtf(radius*radius - yb*yb);
}
}
return x_inset;
}
void determine_color_for_gradient(int y, int h, uint8_t *r, uint8_t *g, uint8_t *b, const float *position, const uint8_t *colors, size_t count)
{
float yf = y / (float) h;
for (size_t j = 1; j < count; j++) {
float min = position[j - 1];
float max = position[j];
if (yf >= min && yf < max) {
yf -= min;
yf /= max - min;
const uint8_t *minrgb = colors + (j - 1) * 3;
const uint8_t *maxrgb = colors + j * 3;
*r = lerp(minrgb[0], maxrgb[0], yf);
*g = lerp(minrgb[1], maxrgb[1], yf);
*b = lerp(minrgb[2], maxrgb[2], yf);
break;
}
}
}
void vui_sdl_draw_button(vui_context_t *vui, vui_sdl_context_t *sdl_ctx, vui_button_t *btn, int msaa)
{
SDL_SetRenderDrawColor(sdl_ctx->renderer, 0, 0, 0, 0);
SDL_RenderClear(sdl_ctx->renderer);
SDL_Color text_color;
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.w = btn->w * msaa;
rect.h = btn->h * msaa;
// static const float button_x_gradient_positions[] = {
// 0.0f,
// 0.1f,
// 0.9f,
// 1.0f,
// };
// static const uint8_t button_x_gradient_colors[] = {
// 0xE8,
// 0xFF,
// 0xFF,
// 0xE8,
// };
static const float button_y_gradient_positions[] = {
0.0f,
0.1f,
0.66f,
0.875f,
1.0f
};
static const uint8_t button_y_gradient_colors[] = {
0xE8, 0xE8, 0xE8,
0xFF, 0xFF, 0xFF,
0xF8, 0xF8, 0xF8,
0xE0, 0xE0, 0xE0,
0xF0, 0xF0, 0xF0,
};
static const uint8_t button_checkable_y_gradient_colors[] = {
0xCA, 0xCA, 0xCA,
0xE5, 0xE5, 0xE5,
0xE4, 0xE4, 0xE4,
0xD6, 0xD6, 0xD6,
0xD2, 0xD2, 0xD2,
};
static const uint8_t button_checked_y_gradient_colors[] = {
0x50, 0xC4, 0x25,
0x71, 0xE5, 0x28,
0x7D, 0xE3, 0x06,
0x4D, 0xCF, 0x00,
0x64, 0xCD, 0x3D,
};
int scrw, scrh;
vui_get_screen_size(vui, &scrw, &scrh);
// Load icon if exists
int icon_x;
int icon_y;
int icon_size = btn->style == VUI_BUTTON_STYLE_CORNER ? rect.h * 1 / 3 : btn->font_size == VUI_FONT_SIZE_SMALL ? rect.h : rect.h * 2 / 4;
SDL_Texture *icon = 0;
if (btn->icon[0]) {
icon = vui_sdl_load_texture(sdl_ctx->renderer, btn->icon);
}
TTF_Font *text_font;
if (btn->style == VUI_BUTTON_STYLE_CORNER) {
if (!icon) {
text_font = sdl_ctx->sysfont_small;
} else {
text_font = sdl_ctx->sysfont_tiny;
}
} else {
switch (btn->font_size) {
case VUI_FONT_SIZE_SMALL:
text_font = sdl_ctx->sysfont_small;
break;
case VUI_FONT_SIZE_TINY:
text_font = sdl_ctx->sysfont_tiny;
break;
default:
text_font = sdl_ctx->sysfont;
break;
}
}
const int btn_radius = rect.h * 2 / 10;
for (int y = 0; y < rect.h; y++) {
int coord = rect.y + y;
// Determine color
uint8_t lr, lg, lb;
determine_color_for_gradient(
y, rect.h, &lr, &lg, &lb,
button_y_gradient_positions,
button_y_gradient_colors,
sizeof(button_y_gradient_positions)/sizeof(float)
);
// Handle rounded corners
float x_inset = 0;
float w_inset = 0;
if (btn->style != VUI_BUTTON_STYLE_CORNER) {
x_inset = w_inset = calculate_x_inset(y, rect.h, btn_radius);
} else {
float inset_y = y;
if (btn->y < scrh/2) {
inset_y += rect.h;
}
float hor_inset = calculate_x_inset(inset_y, rect.h*2, rect.h);
if (btn->x < scrw/2) {
w_inset = hor_inset;
} else {
x_inset = hor_inset;
}
}
// Make a cutoff section for the highlighted area
if (btn->checkable) {
uint8_t clr, clg, clb;
int lim = btn_radius + btn_radius;
determine_color_for_gradient(
y, rect.h, &clr, &clg, &clb,
button_y_gradient_positions,
btn->checked ? button_checked_y_gradient_colors : button_checkable_y_gradient_colors,
sizeof(button_y_gradient_positions)/sizeof(float)
);
SDL_SetRenderDrawColor(sdl_ctx->renderer, clr, clg, clb, 0xFF);
SDL_RenderDrawLineF(sdl_ctx->renderer, rect.x + x_inset, coord, rect.x + lim - 1, coord);
x_inset = lim;
}
SDL_SetRenderDrawColor(sdl_ctx->renderer, lr, lg, lb, 0xFF);
SDL_RenderDrawLineF(sdl_ctx->renderer, rect.x + x_inset, coord, rect.x + rect.w - w_inset, coord);
}
text_color.r = text_color.g = text_color.b = 0x40;
text_color.a = 0xFF;
// Draw text
if (btn->text[0]) {
SDL_Surface *surface = TTF_RenderUTF8_Blended(text_font, btn->text, text_color);
SDL_Texture *texture = SDL_CreateTextureFromSurface(sdl_ctx->renderer, surface);
SDL_Rect text_rect;
const int msaa_scaled_text_w = surface->w * msaa;
const int msaa_scaled_text_h = surface->h * msaa;
text_rect.w = msaa_scaled_text_w;
text_rect.h = msaa_scaled_text_h;
const int btn_padding = 8;
text_rect.x = rect.x + rect.w / 2 - msaa_scaled_text_w / 2;
text_rect.y = rect.y + rect.h / 2 - msaa_scaled_text_h / 2;
// Adjust text rect if icon exists
if (icon) {
if (btn->style == VUI_BUTTON_STYLE_CORNER) {
text_rect.y = rect.y + rect.h/2 - (msaa_scaled_text_h + icon_size + btn_padding)/2;
icon_x = rect.x + rect.w/2 - icon_size/2;
icon_y = text_rect.y + msaa_scaled_text_h + btn_padding;
} else {
const int total_width = icon_size + btn_padding + msaa_scaled_text_w;
icon_x = rect.x + rect.w/2 - total_width/2;
icon_y = rect.y + rect.h/2 - icon_size/2;
text_rect.x = icon_x + icon_size + btn_padding;
}
}
else {
if (btn->style == VUI_BUTTON_STYLE_CORNER) {
// we apply the formula of the circle quarter centroid
// https://www.efunda.com/math/areas/CircleQuarter.cfm
int scrw, scrh;
vui_get_screen_size(vui, &scrw, &scrh);
float fract = 4.0f / (3.0f * M_PI);
float off_h = fract * rect.h;
float off_w = fract * rect.w;
// we treat the x and y offsets differently
// to account for non-circular button shapes
// we also verify which corner on the screen the button is placed
// in order to adjust the offset
float cx = rect.x + (btn->x < scrw / 2 ? off_w : (rect.w - off_w));
float cy = rect.y + (btn->y < scrh / 2 ? off_h : (rect.h - off_h));
text_rect.x = (int)(cx - msaa_scaled_text_w * 0.5f);
text_rect.y = (int)(cy - msaa_scaled_text_h * 0.5f);
}
}
SDL_RenderCopy(sdl_ctx->renderer, texture, NULL, &text_rect);
SDL_DestroyTexture(texture);
SDL_FreeSurface(surface);
} else {
icon_x = rect.x + rect.w/2 - icon_size/2;
icon_y = rect.y + rect.h/2 - icon_size/2;
}
// Draw icon
if (icon) {
SDL_Rect icon_rect;
icon_rect.w = icon_rect.h = icon_size;
icon_rect.x = icon_x;
icon_rect.y = icon_y;
if(btn->icon_mod){
char r = (btn->icon_mod & 0xFF0000) >> sizeof(char) * 8 * 2;
char g = (btn->icon_mod & 0x00FF00) >> sizeof(char) * 8;
char b = (btn->icon_mod & 0x0000FF);
SDL_SetTextureColorMod(icon, r, g, b);
}
SDL_RenderCopy(sdl_ctx->renderer, icon, 0, &icon_rect);
SDL_DestroyTexture(icon);
}
}
void vui_sdl_draw_background(vui_context_t *ctx, SDL_Renderer *renderer)
{
const int bg_r1 = 0xB8;
const int bg_g1 = 0xB8;
const int bg_b1 = 0xC8;
const int bg_r2 = 0xE0;
const int bg_g2 = 0xF0;
const int bg_b2 = 0xF0;
for (int x = 0; x < ctx->screen_width; x++) {
float xf = x / (float) ctx->screen_width;
SDL_SetRenderDrawColor(renderer, lerp(bg_r1, bg_r2, xf), lerp(bg_g1, bg_g2, xf), lerp(bg_b1, bg_b2, xf), 0xFF);
SDL_RenderDrawLine(renderer, x, 0, x, ctx->screen_height);
}
}
static SDL_Texture *vui_sdl_get_layer_texture(vui_context_t *ctx,
SDL_Renderer *renderer,
int layer)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) ctx->platform_data;
if (sdl_ctx->layer_data[layer]) {
return sdl_ctx->layer_data[layer];
}
sdl_ctx->layer_data[layer] = SDL_CreateTexture(
renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET,
ctx->screen_width, ctx->screen_height);
if (!sdl_ctx->layer_data[layer]) {
return NULL;
}
SDL_BlendMode bm = SDL_ComposeCustomBlendMode(
SDL_BLENDFACTOR_ONE,
SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
SDL_BLENDOPERATION_ADD,
SDL_BLENDFACTOR_ONE,
SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
SDL_BLENDOPERATION_ADD
);
SDL_SetRenderDrawBlendMode(renderer, bm);
SDL_SetTextureBlendMode(sdl_ctx->layer_data[layer], bm);
return sdl_ctx->layer_data[layer];
}
void vui_draw_sdl(vui_context_t *ctx, SDL_Renderer *renderer)
{
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) ctx->platform_data;
for (int layer = 0; layer < ctx->layers; layer++) {
SDL_Texture *layer_texture = vui_sdl_get_layer_texture(ctx, renderer, layer);
SDL_SetRenderTarget(renderer, layer_texture);
if (layer == 0 && ctx->background_enabled) {
// Draw background
if (!sdl_ctx->background) {
sdl_ctx->background = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, ctx->screen_width, ctx->screen_height);
SDL_Texture *old_target = SDL_GetRenderTarget(renderer);
SDL_SetRenderTarget(renderer, sdl_ctx->background);
vui_sdl_draw_background(ctx, renderer);
SDL_SetRenderTarget(renderer, old_target);
}
SDL_RenderCopy(renderer, sdl_ctx->background, 0, 0);
} else {
vui_sdl_set_render_color(renderer, ctx->layer_color[layer]);
SDL_RenderClear(renderer);
}
}
// Draw rects
for (int i = 0; i < ctx->rect_count; i++) {
vui_rect_priv_t *rect = &ctx->rects[i];
SDL_SetRenderTarget(renderer, sdl_ctx->layer_data[rect->layer]);
SDL_Rect sr;
sr.x = rect->x;
sr.y = rect->y;
sr.w = rect->w;
sr.h = rect->h;
vui_sdl_set_render_color(renderer, rect->color);
if (rect->border_radius == 0) {
SDL_RenderFillRect(renderer, &sr);
} else {
for (int y = 0; y < sr.h; y++) {
float x_inset = calculate_x_inset(y, sr.h, rect->border_radius);
float draw_y = sr.y + y;
SDL_RenderDrawLineF(renderer, sr.x + x_inset, draw_y, sr.x + sr.w - x_inset, draw_y);
}
}
}
// Draw labels
for (int i = 0; i < ctx->label_count; i++) {
vui_label_t *lbl = &ctx->labels[i];
if (lbl->text[0] && lbl->visible) {
SDL_SetRenderTarget(renderer, sdl_ctx->layer_data[lbl->layer]);
SDL_Color c;
c.r = lbl->color.r * 0xFF;
c.g = lbl->color.g * 0xFF;
c.b = lbl->color.b * 0xFF;
c.a = lbl->color.a * 0xFF;
SDL_Surface *surface = TTF_RenderUTF8_Blended_Wrapped(get_font(sdl_ctx, lbl->size), lbl->text, c, lbl->w);
SDL_Rect sr;
sr.x = 0;
sr.y = 0;
sr.w = intmin(lbl->w, surface->w);
sr.h = intmin(lbl->h, surface->h);
SDL_Rect dr;
dr.x = lbl->x;
dr.y = lbl->y;
dr.w = sr.w;
dr.h = sr.h;
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_RenderCopy(renderer, texture, &sr, &dr);
SDL_DestroyTexture(texture);
SDL_FreeSurface(surface);
}
}
// Draw textedits
for (int i = 0; i < ctx->textedit_count; i++) {
vui_textedit_t *edit = &ctx->textedits[i];
if (!edit->visible) {
continue;
}
SDL_SetRenderTarget(renderer, sdl_ctx->layer_data[edit->layer]);
SDL_Rect rect;
rect.x = edit->x;
rect.y = edit->y;
rect.w = edit->w;
rect.h = edit->h;
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0x80);
const int bgrect_pad = 8;
SDL_Rect bgrect = rect;
bgrect.x -= bgrect_pad;
bgrect.y -= bgrect_pad;
bgrect.w += bgrect_pad + bgrect_pad;
bgrect.h += bgrect_pad + bgrect_pad;
for (int y = 0; y < bgrect.h; y++) {
int x_inset = calculate_x_inset(y, bgrect.h, bgrect_pad);
int ry = bgrect.y+y;
SDL_RenderDrawLine(renderer, bgrect.x + x_inset, ry, bgrect.x + bgrect.w - x_inset, ry);
}
SDL_Color c;
c.r = c.g = c.b = c.a = 0xFF;
TTF_Font *font = get_font(sdl_ctx, edit->size);
if (edit->text[0]) {
if (edit->password) {
if (!sdl_ctx->pw_tex) {
sdl_ctx->pw_tex = vui_sdl_load_texture(sdl_ctx->renderer, "circle_big.svg");
}
uint8_t f = (edit->enabled ? 1 : 0.5f) * 0xFF;
SDL_SetTextureAlphaMod(sdl_ctx->pw_tex, f);
SDL_SetTextureColorMod(sdl_ctx->pw_tex, f, f, f);
const char *cc = edit->text;
SDL_Rect pwr;
pwr.x = rect.x;
pwr.y = rect.y + rect.h/2 - PW_CHAR_SIZE/2;
pwr.w = PW_CHAR_SIZE;
pwr.h = PW_CHAR_SIZE;
while (*cc != 0) {
SDL_RenderCopy(renderer, sdl_ctx->pw_tex, 0, &pwr);
cc++;
pwr.x += PW_CHAR_SIZE + PW_CHAR_PAD;
}
} else {
SDL_Surface *surface = TTF_RenderUTF8_Blended(font, edit->text, c);
SDL_Texture *texture = SDL_CreateTextureFromSurface(sdl_ctx->renderer, surface);
SDL_Rect src;
src.x = src.y = 0;
src.w = surface->w;
src.h = surface->h;
if (src.w > rect.w) {
src.w = rect.w;
} else {
rect.w = src.w;
}
uint8_t f = (edit->enabled ? 1 : 0.5f) * 0xFF;
SDL_SetTextureAlphaMod(texture, f);
SDL_SetTextureColorMod(texture, f, f, f);
SDL_RenderCopy(renderer, texture, &src, &rect);
SDL_FreeSurface(surface);
SDL_DestroyTexture(texture);
}
}
if (ctx->active_textedit == i) {
struct timeval now;
gettimeofday(&now, 0);
time_t diff = (now.tv_sec - ctx->active_textedit_time.tv_sec) * 1000000 + (now.tv_usec - ctx->active_textedit_time.tv_usec);
if ((diff % 1000000) < 500000) {
char *copy_end = edit->text;
for (int i = 0; i < edit->cursor; i++) {
copy_end = vui_utf8_advance(copy_end);
}
size_t len = copy_end - edit->text;
int cursor_x;
if (edit->password) {
cursor_x = rect.x + (PW_CHAR_SIZE + PW_CHAR_PAD) * len - PW_CHAR_PAD/2;
} else {
char tmp_ate[MAX_BUTTON_TEXT];
strncpy(tmp_ate, edit->text, len);
tmp_ate[len] = 0;
int tw;
TTF_SizeUTF8(font, tmp_ate, &tw, 0);
cursor_x = rect.x + tw;
}
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
SDL_RenderDrawLine(renderer, cursor_x, rect.y, cursor_x, rect.y + rect.h);
}
}
}
// Draw buttons
for (int i = 0; i < ctx->button_count; i++) {
vui_button_t *btn = &ctx->buttons[i];
if (!btn->visible) {
continue;
}
SDL_SetRenderTarget(renderer, sdl_ctx->layer_data[btn->layer]);
// Check if button needs to be redrawn
vui_sdl_cached_texture_t *btn_tex = &sdl_ctx->button_cache[i];
if (!btn_tex->texture || btn_tex->w != btn->w || btn_tex->h != btn->h || strcmp(btn_tex->text, btn->text) || strcmp(btn_tex->icon, btn->icon) || btn_tex->checked != btn->checked || btn_tex->icon_mod != btn->icon_mod) {
// Must re-draw texture
if (btn_tex->texture && (btn_tex->w != btn->w || btn_tex->h != btn->h)) {
SDL_DestroyTexture(btn_tex->texture);
btn_tex->texture = 0;
}
const int MSAA = 2;
if (!btn_tex->texture) {
btn_tex->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, btn->w * MSAA, btn->h * MSAA);
}
SDL_Texture *old_target = SDL_GetRenderTarget(renderer);
SDL_SetRenderTarget(renderer, btn_tex->texture);
vui_sdl_draw_button(ctx, sdl_ctx, btn, MSAA);
SDL_SetRenderTarget(renderer, old_target);
btn_tex->w = btn->w;
btn_tex->h = btn->h;
btn_tex->icon_mod = btn->icon_mod;
strcpy(btn_tex->text, btn->text);
strcpy(btn_tex->icon, btn->icon);
btn_tex->checked = btn->checked;
}
// Draw cached texture onto screen
SDL_Rect rect;
rect.x = btn->sx;
rect.y = btn->sy;
rect.w = btn->sw;
rect.h = btn->sh;
uint8_t f = (btn->enabled ? 1 : 0.5f) * 0xFF;
SDL_SetTextureAlphaMod(btn_tex->texture, f);
SDL_SetTextureColorMod(btn_tex->texture, f, f, f);
SDL_RenderCopy(renderer, btn_tex->texture, 0, &rect);
if (i == ctx->selected_button) {
const int btn_select_short_sz = 4;
const int btn_select_long_sz = 24;
const int tlx = btn->sx;
const int tly = btn->sy;
const int trx = btn->sx + btn->sw;
const int try = tly;
const int blx = tlx;
const int bly = btn->sy + btn->sh;
const int brx = trx;
const int bry = bly;
SDL_SetRenderDrawColor(renderer, 0x17, 0xCB, 0xD1, 0xFF);
SDL_Rect r;
r.x = tlx;
r.y = tly;
r.w = btn_select_long_sz;
r.h = btn_select_short_sz;
SDL_RenderFillRect(renderer, &r);
r.x = trx - btn_select_long_sz;
SDL_RenderFillRect(renderer, &r);
r.y = bly - btn_select_short_sz;
SDL_RenderFillRect(renderer, &r);
r.x = tlx;
SDL_RenderFillRect(renderer, &r);
r.w = btn_select_short_sz;
r.h = btn_select_long_sz;
r.y = tly;
SDL_RenderFillRect(renderer, &r);
r.x = trx - btn_select_short_sz;
SDL_RenderFillRect(renderer, &r);
r.y = bry - btn_select_long_sz;
SDL_RenderFillRect(renderer, &r);
r.x = blx;
SDL_RenderFillRect(renderer, &r);
}
}
// Draw images
for (int i = 0; i < MAX_BUTTON_COUNT; i++) {
vui_image_t *img = &ctx->images[i];
if (!img->valid || img->image[0] == 0) {
continue;
}
SDL_SetRenderTarget(renderer, sdl_ctx->layer_data[img->layer]);
// Find image in cache
vui_sdl_cached_texture_t *img_tex = &sdl_ctx->image_cache[i];
if (!img_tex->texture || strcmp(img_tex->icon, img->image)) {
if (img_tex->texture) {
SDL_DestroyTexture(img_tex->texture);
}
img_tex->texture = vui_sdl_load_texture(renderer, img->image);
strcpy(img_tex->icon, img->image);
}
SDL_Rect dst;
dst.x = img->x;
dst.y = img->y;
dst.w = img->w;
dst.h = img->h;
SDL_RenderCopy(renderer, img_tex->texture, 0, &dst);
}
}
int get_texture_from_cpu_frame(vui_sdl_context_t *sdl_ctx, AVFrame *f)
{
#ifdef ANDROID
// If we were using the Android texture, but for some reason switched to
// software decoding, ensure our game_tex gets correctly recreated
if (sdl_ctx->game_tex == sdl_ctx->android_video_tex) {
sdl_ctx->game_tex = NULL;
sdl_ctx->android_video_ready = 0;
}
#endif
if (!sdl_ctx->game_tex) {
sdl_ctx->game_tex = SDL_CreateTexture(
sdl_ctx->renderer,
SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING,
f->width,
f->height
);
if (!sdl_ctx->game_tex) {
vpilog("Failed to create texture for CPU frame\n");
}
}
// SDL_SetRenderTarget(sdl_ctx->renderer, sdl_ctx->game_tex);
// SDL_SetRenderDrawColor(sdl_ctx->renderer, 0, 0, 0, 0);
// SDL_RenderClear(sdl_ctx->renderer);
int ret = SDL_UpdateYUVTexture(sdl_ctx->game_tex, NULL,
f->data[0], f->linesize[0],
f->data[1], f->linesize[1],
f->data[2], f->linesize[2]);
if (ret != 0) {
vpilog("Failed to update YUV texture for CPU frame: %s\n", SDL_GetError());
return 0;
}
return 1;
}
#ifdef VANILLA_HAS_EGL
int check_has_EGL_EXT_image_dma_buf_import()
{
// Determine if we have this EGL extension or not
const char *egl_extensions = eglQueryString(eglGetCurrentDisplay(), EGL_EXTENSIONS);
if (!egl_extensions) {
return 0;
}
char *extensions = SDL_strdup(egl_extensions);
if (!extensions) {
return 0;
}
char *saveptr, *token;
token = SDL_strtokr(extensions, " ", &saveptr);
if (!token) {
SDL_free(extensions);
return 0;
}
int ret = 0;
do {
if (SDL_strcmp(token, "EGL_EXT_image_dma_buf_import") == 0
|| SDL_strcmp(token, "EGL_EXT_image_dma_buf_import_modifiers") == 0) {
ret++;
}
} while ((token = SDL_strtokr(NULL, " ", &saveptr)) != NULL);
SDL_free(extensions);
return ret == 2;
// if (SDL_GL_ExtensionSupported("GL_OES_EGL_image")) {
// glEGLImageTargetTexture2DOESFunc = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
// }
// glActiveTextureARBFunc = (PFNGLACTIVETEXTUREARBPROC)SDL_GL_GetProcAddress("glActiveTextureARB");
// if (has_EGL_EXT_image_dma_buf_import &&
// glEGLImageTargetTexture2DOESFunc &&
// glActiveTextureARBFunc) {
// has_eglCreateImage = true;
// }
}
#endif // VANILLA_HAS_EGL
#ifdef VANILLA_CUDA_AVAILABLE
int get_texture_from_cuda_frame(vui_cuda_context_t *cuda_ctx, vui_sdl_context_t *sdl_ctx, AVFrame *f)
{
if (!sdl_ctx->game_tex) {
// Use SHADER_NV12_RA for CUDA frames
SDL_SetHint("SDL_RENDER_OPENGL_NV12_RG_SHADER", "0");
sdl_ctx->game_tex = SDL_CreateTexture(
sdl_ctx->renderer,
SDL_PIXELFORMAT_NV12,
SDL_TEXTUREACCESS_STATIC,
f->width,
f->height
);
if (!sdl_ctx->game_tex) {
vpilog("Failed to create texture for CUDA frame\n");
return 0;
}
GLuint texY, texUV;
static PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = NULL;
if (!glActiveTextureARB) {
glActiveTextureARB = SDL_GL_GetProcAddress("glActiveTextureARB");
}
static PFNGLGETINTEGERVPROC glGetIntegerv = NULL;
if (!glGetIntegerv) {
glGetIntegerv = SDL_GL_GetProcAddress("glGetIntegerv");
}
SDL_GL_BindTexture(sdl_ctx->game_tex, 0, 0);
glActiveTextureARB(GL_TEXTURE0_ARB + 0);
glGetIntegerv(GL_TEXTURE_BINDING_2D, &texY);
glActiveTextureARB(GL_TEXTURE0_ARB + 1);
glGetIntegerv(GL_TEXTURE_BINDING_2D, &texUV);
SDL_GL_UnbindTexture(sdl_ctx->game_tex);
cudaGraphicsGLRegisterImage(&cuda_ctx->resY, texY, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
cudaGraphicsGLRegisterImage(&cuda_ctx->resUV, texUV, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
}
cudaStream_t stream = 0;
cudaGraphicsMapResources(1, &cuda_ctx->resY, stream);
cudaGraphicsMapResources(1, &cuda_ctx->resUV, stream);
cudaArray_t arrY = 0;
cudaArray_t arrUV = 0;
cudaGraphicsSubResourceGetMappedArray(&arrY, cuda_ctx->resY, 0, 0);
cudaGraphicsSubResourceGetMappedArray(&arrUV, cuda_ctx->resUV, 0, 0);
cudaMemcpy2DToArrayAsync(arrY, 0, 0, f->data[0], f->linesize[0], f->width, f->height, cudaMemcpyDeviceToDevice, stream);
cudaMemcpy2DToArrayAsync(arrUV, 0, 0, f->data[1], f->linesize[1], f->width, f->height / 2, cudaMemcpyDeviceToDevice, stream);
cudaGraphicsUnmapResources(1, &cuda_ctx->resY, stream);
cudaGraphicsUnmapResources(1, &cuda_ctx->resUV, stream);
return 1;
}
#endif // VANILLA_CUDA_AVAILABLE
int get_texture_from_drm_prime_frame(vui_sdl_context_t *sdl_ctx, AVFrame *f)
{
#ifdef VANILLA_HAS_EGL
const AVDRMFrameDescriptor *desc = (const AVDRMFrameDescriptor *)f->data[0];
static PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = NULL;
if (!glActiveTextureARB) {
glActiveTextureARB = SDL_GL_GetProcAddress("glActiveTextureARB");
}
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES = NULL;
if (!glEGLImageTargetTexture2DOES) {
glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
}
int image_index = 0;
EGLDisplay display = eglGetCurrentDisplay();
for (int i = 0; i < desc->nb_layers; i++) {
const AVDRMLayerDescriptor *layer = &desc->layers[i];
for (int j = 0; j < layer->nb_planes; j++) {
const AVDRMPlaneDescriptor *plane = &layer->planes[j];
const AVDRMObjectDescriptor *object = &desc->objects[plane->object_index];
static int has_EGL_EXT_image_dma_buf_import = -1;
if (has_EGL_EXT_image_dma_buf_import == -1) {
has_EGL_EXT_image_dma_buf_import = check_has_EGL_EXT_image_dma_buf_import();
}
const EGLAttrib EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT_OR_NONE =
has_EGL_EXT_image_dma_buf_import && object->format_modifier != DRM_FORMAT_MOD_INVALID ?
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT : EGL_NONE;
if (!sdl_ctx->game_tex) {
// Use SHADER_NV12_RG for VAAPI NV12 frames
SDL_SetHint("SDL_RENDER_OPENGL_NV12_RG_SHADER", "1");
sdl_ctx->game_tex = SDL_CreateTexture(
sdl_ctx->renderer,
layer->format == DRM_FORMAT_YUV420 ? SDL_PIXELFORMAT_IYUV : SDL_PIXELFORMAT_NV12,
SDL_TEXTUREACCESS_STATIC,
f->width,
f->height
);
if (!sdl_ctx->game_tex) {
vpilog("Failed to create texture for DRM PRIME frame\n");
return 0;
}
}
EGLAttrib use_fmt;
if (layer->format == DRM_FORMAT_YUV420) {
use_fmt = DRM_FORMAT_R8;
} else if (layer->format == DRM_FORMAT_NV12) {
use_fmt = (image_index == 0) ? DRM_FORMAT_R8 : DRM_FORMAT_GR88;
} else {
use_fmt = layer->format;
}
int w = f->width;
int h = f->height;
if (image_index > 0) {
w /= 2;
h /= 2;
}
EGLAttrib attr[] = {
EGL_LINUX_DRM_FOURCC_EXT, use_fmt,
EGL_WIDTH, w,
EGL_HEIGHT, h,
EGL_DMA_BUF_PLANE0_FD_EXT, object->fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, plane->offset,
EGL_DMA_BUF_PLANE0_PITCH_EXT, plane->pitch,
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT_OR_NONE, (object->format_modifier >> 0) & 0xFFFFFFFF,
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, (object->format_modifier >> 32) & 0xFFFFFFFF,
EGL_NONE
};
EGLImage image = eglCreateImage(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, attr);
if (image == EGL_NO_IMAGE) {
vpilog("Failed to create EGLImage: 0x%x (display: %p, layer %i, plane %i)\n", eglGetError(), display, i, j);
return 0;
}
SDL_GL_BindTexture(sdl_ctx->game_tex, 0, 0);
glActiveTextureARB(GL_TEXTURE0_ARB + image_index);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image);
image_index++;
SDL_GL_UnbindTexture(sdl_ctx->game_tex);
eglDestroyImage(display, image);
}
}
return 1;
#else
vpilog("No EGL support to display VAAPI texture\n");
return 0;
#endif
}
#ifdef ANDROID
static SDL_FPoint transform_android_texcoord(const float matrix[16],
float s, float t)
{
// Flip Y
t = 1.0f - t;
SDL_FPoint point = {
matrix[0] * s + matrix[4] * t + matrix[12],
matrix[1] * s + matrix[5] * t + matrix[13]
};
return point;
}
static int render_android_video_texture(vui_sdl_context_t *sdl_ctx,
int width, int height)
{
const float *matrix = sdl_ctx->android_video_transform;
const SDL_Color white = { 255, 255, 255, 255 };
SDL_Vertex vertices[4] = {
{ { 0.0f, 0.0f }, white, transform_android_texcoord(matrix, 0.0f, 0.0f) },
{ { (float) width, 0.0f }, white, transform_android_texcoord(matrix, 1.0f, 0.0f) },
{ { (float) width, (float) height }, white, transform_android_texcoord(matrix, 1.0f, 1.0f) },
{ { 0.0f, (float) height }, white, transform_android_texcoord(matrix, 0.0f, 1.0f) }
};
static const int indices[] = { 0, 1, 2, 0, 2, 3 };
if (SDL_RenderGeometry(sdl_ctx->renderer, sdl_ctx->android_video_tex, vertices, 4, indices, 6) < 0) {
vpilog("Failed to render Android external texture: %s\n", SDL_GetError());
return 0;
}
return 1;
}
#endif
static Uint64 vui_sdl_tick_deadline(const vui_sdl_context_t *sdl_ctx,
uint64_t tick)
{
Uint64 whole_seconds = tick / VUI_INPUT_TICK_RATE;
Uint64 partial_ticks = tick % VUI_INPUT_TICK_RATE;
return sdl_ctx->update_tick_origin
+ whole_seconds * sdl_ctx->update_tick_frequency
+ partial_ticks * sdl_ctx->update_tick_frequency
/ VUI_INPUT_TICK_RATE;
}
static void vui_sdl_wait_for_next_tick(vui_sdl_context_t *sdl_ctx)
{
sdl_ctx->update_tick++;
Uint64 now = SDL_GetPerformanceCounter();
Uint64 deadline = vui_sdl_tick_deadline(sdl_ctx, sdl_ctx->update_tick);
if (now >= deadline) {
Uint64 elapsed = now - sdl_ctx->update_tick_origin;
uint64_t elapsed_ticks =
(elapsed / sdl_ctx->update_tick_frequency) * VUI_INPUT_TICK_RATE
+ (elapsed % sdl_ctx->update_tick_frequency)
* VUI_INPUT_TICK_RATE / sdl_ctx->update_tick_frequency;
sdl_ctx->update_tick = elapsed_ticks + 1;
deadline = vui_sdl_tick_deadline(sdl_ctx, sdl_ctx->update_tick);
}
pthread_mutex_lock(&vpi_present_frame_mutex);
while ((now = SDL_GetPerformanceCounter()) < deadline) {
Uint64 remaining = deadline - now;
uint64_t wait_ns = remaining * 1000000000ULL
/ sdl_ctx->update_tick_frequency;
if (!wait_ns)
wait_ns = 1;
struct timespec timeout;
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += wait_ns / 1000000000ULL;
timeout.tv_nsec += wait_ns % 1000000000ULL;
if (timeout.tv_nsec >= 1000000000L) {
timeout.tv_sec++;
timeout.tv_nsec -= 1000000000L;
}
pthread_cond_timedwait(&vpi_present_frame_cond, &vpi_present_frame_mutex, &timeout);
}
pthread_mutex_unlock(&vpi_present_frame_mutex);
}
// Rendering/main thread
int vui_update_sdl(vui_context_t *vui)
{
vui_sdl_event_thread(vui);
vui_sdl_context_t *sdl_ctx = (vui_sdl_context_t *) vui->platform_data;
if (!sdl_ctx->update_tick_frequency) {
sdl_ctx->update_tick_frequency = SDL_GetPerformanceFrequency();
sdl_ctx->update_tick_origin = SDL_GetPerformanceCounter();
}
int render_tick = sdl_ctx->update_tick % VUI_RENDER_TICK_DIVISOR == 0;
SDL_Renderer *renderer = sdl_ctx->renderer;
vui_update(vui);
SDL_Texture *main_tex = NULL;
#ifdef VANILLA_DRM_AVAILABLE
static vanilla_drm_ctx_t *drm_ctx = NULL;
#endif // VANILLA_DRM_AVAILABLE
#ifdef VANILLA_CUDA_AVAILABLE
static vui_cuda_context_t *cuda_ctx = NULL;
#endif // VANILLA_CUDA_AVAILABLE
int sdl_frame_ready = 0;
if (!vui->game_mode) {
#ifdef VANILLA_DRM_AVAILABLE
if (drm_ctx) {
vui_sdl_drm_free(&drm_ctx); // will set to null
}
sdl_ctx->fast_drm_active = 0;
#endif // VANILLA_DRM_AVAILABLE
#ifdef VANILLA_CUDA_AVAILABLE
if (cuda_ctx) {
if (cuda_ctx->resY) cudaGraphicsUnregisterResource(cuda_ctx->resY);
if (cuda_ctx->resUV) cudaGraphicsUnregisterResource(cuda_ctx->resUV);
free(cuda_ctx);
cuda_ctx = NULL;
}
#endif // VANILLA_CUDA_AVAILABLE
if (render_tick) {
// Draw vui to a custom texture
vui_draw_sdl(vui, renderer);
// Flatten layers
int el[MAX_BUTTON_COUNT];
int el_count = 0;
for (int i = 0; i < vui->layers; i++) {
if (vui->layer_enabled[i]) {
el[el_count] = i;
el_count++;
}
}
for (int i = el_count - 1; i > 0; i--) {
SDL_Texture *bg = sdl_ctx->layer_data[el[i-1]];
SDL_Texture *fg = sdl_ctx->layer_data[el[i]];
SDL_SetRenderTarget(renderer, bg);
SDL_SetTextureColorMod(fg, vui->layer_opacity[i] * 0xFF, vui->layer_opacity[i] * 0xFF, vui->layer_opacity[i] * 0xFF);
SDL_SetTextureAlphaMod(fg, vui->layer_opacity[i] * 0xFF);
SDL_RenderCopy(renderer, fg, NULL, NULL);
}
main_tex = sdl_ctx->layer_data[0];
sdl_frame_ready = main_tex != NULL;
}
} else {
pthread_mutex_lock(&vpi_present_frame_mutex);
if (vpi_present_frame
&& sdl_ctx->present_frame_sequence != vpi_present_frame_sequence
&& vpi_present_frame->format != -1) {
av_frame_move_ref(sdl_ctx->frame, vpi_present_frame);
}
sdl_ctx->present_frame_sequence = vpi_present_frame_sequence;
pthread_mutex_unlock(&vpi_present_frame_mutex);
if (sdl_ctx->frame->format != -1) {
switch (sdl_ctx->frame->format) {
case AV_PIX_FMT_DRM_PRIME:
{
#ifdef VANILLA_DRM_AVAILABLE
// For very low powered systems, we can save a little time by
// skipping SDL2 entirely and rendering straight to DRM. However,
// we lose features like "toast" notifications and upscaling, so
// this is provided as an option only.
if (vpi_config.fast_drm) {
if (!drm_ctx) {
if (vui_sdl_drm_initialize(&drm_ctx,
sdl_ctx->window)) {
// Blank screen so DRM doesn't appear to render over UI
SDL_SetRenderTarget(renderer, NULL);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
}
}
if (drm_ctx && vui_sdl_drm_present(drm_ctx, sdl_ctx->frame)) {
sdl_ctx->fast_drm_active = 1;
} else {
sdl_ctx->fast_drm_active = 0;
get_texture_from_drm_prime_frame(sdl_ctx,
sdl_ctx->frame);
}
} else {
sdl_ctx->fast_drm_active = 0;
get_texture_from_drm_prime_frame(sdl_ctx, sdl_ctx->frame);
}
#endif // VANILLA_DRM_AVAILABLE
break;
}
case AV_PIX_FMT_CUDA:
{
#ifdef VANILLA_CUDA_AVAILABLE
if (!cuda_ctx) {
cuda_ctx = malloc(sizeof(vui_cuda_context_t));
cuda_ctx->resY = 0;
cuda_ctx->resUV = 0;
}
get_texture_from_cuda_frame(cuda_ctx, sdl_ctx, sdl_ctx->frame);
#endif // VANILLA_CUDA_AVAILABLE
break;
}
case AV_PIX_FMT_VAAPI:
{
AVFrame *drm = av_frame_alloc();
if (drm) {
drm->format = AV_PIX_FMT_DRM_PRIME;
if (av_hwframe_map(drm, sdl_ctx->frame, 0) >= 0) {
get_texture_from_drm_prime_frame(sdl_ctx, drm);
} else {
vpilog("Failed to map DRM PRIME frame from VAAPI\n");
}
av_frame_free(&drm);
} else {
vpilog("Failed to allocate DRM PRIME frame from VAAPI\n");
}
break;
}
#ifdef ANDROID
case AV_PIX_FMT_MEDIACODEC:
{
AVMediaCodecBuffer *buffer = (AVMediaCodecBuffer *) sdl_ctx->frame->data[3];
if (!sdl_ctx->android_video_tex || !buffer) {
vpilog("MediaCodec produced a frame without an Android video surface\n");
break;
}
if (sdl_ctx->game_tex && sdl_ctx->game_tex != sdl_ctx->android_video_tex) {
SDL_DestroyTexture(sdl_ctx->game_tex);
}
sdl_ctx->game_tex = sdl_ctx->android_video_tex;
sdl_ctx->android_video_ready = 0;
if (av_mediacodec_release_buffer(buffer, 1) < 0) {
vpilog("Failed to render MediaCodec output buffer\n");
break;
}
break;
}
#endif
case AV_PIX_FMT_YUV420P:
get_texture_from_cpu_frame(sdl_ctx, sdl_ctx->frame);
break;
}
av_frame_unref(sdl_ctx->held_frame);
av_frame_move_ref(sdl_ctx->held_frame, sdl_ctx->frame);
}
#ifdef ANDROID
/* MediaCodec publishes into SurfaceTexture asynchronously. Poll it at
* the input rate, but only draw the retained image on render ticks. */
if (sdl_ctx->game_tex == sdl_ctx->android_video_tex
&& SDL_RenderFlush(renderer) == 0
&& vui_sdl_android_update_video_texture(
sdl_ctx->android_video_transform)) {
sdl_ctx->android_video_ready = 1;
}
#endif
int sdl_handles_final_blit = 1;
#ifdef VANILLA_DRM_AVAILABLE
sdl_handles_final_blit = !sdl_ctx->fast_drm_active;
#endif
if (render_tick && sdl_handles_final_blit) {
main_tex = vui_sdl_get_layer_texture(vui, renderer, 0);
if (main_tex) {
SDL_SetRenderTarget(renderer, main_tex);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
if (sdl_ctx->game_tex) {
#ifdef ANDROID
if (sdl_ctx->game_tex == sdl_ctx->android_video_tex) {
if (sdl_ctx->android_video_ready) {
render_android_video_texture(
sdl_ctx, vui->screen_width,
vui->screen_height);
}
} else
#endif
{
SDL_RenderCopy(renderer, sdl_ctx->game_tex, NULL,
NULL);
}
}
sdl_frame_ready = 1;
}
}
}
if (sdl_frame_ready) {
// Draw toast on screen if necessary
const int TOAST_PADDING = 12;
int cur_toast;
vpi_get_toast(&cur_toast, 0, 0, 0);
if (cur_toast != sdl_ctx->last_shown_toast) {
// Get toast information
char toast_str[VPI_TOAST_MAX_LEN];
vpi_get_toast(&cur_toast, toast_str, sizeof(toast_str), &sdl_ctx->toast_expiry);
sdl_ctx->last_shown_toast = cur_toast;
const int toast_w = vui->screen_width/2;
SDL_Color c;
c.r = c.g = c.b = 0x40;
c.a = 0xFF;
SDL_Surface *surface = TTF_RenderUTF8_Blended_Wrapped(sdl_ctx->sysfont_small, toast_str, c, toast_w - TOAST_PADDING - TOAST_PADDING);
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
const int toast_h = surface->h + TOAST_PADDING + TOAST_PADDING;
sdl_ctx->toast_tex = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_TARGET, toast_w, toast_h);
SDL_SetTextureBlendMode(sdl_ctx->toast_tex, SDL_BLENDMODE_BLEND);
SDL_SetRenderTarget(renderer, sdl_ctx->toast_tex);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xF0);
const int TOAST_BORDER_RADIUS = TOAST_PADDING;
for (int y = 0; y < toast_h; y++) {
const int x_inset = calculate_x_inset(y, toast_h, TOAST_BORDER_RADIUS);
SDL_RenderDrawLine(renderer, x_inset, y, toast_w - x_inset, y);
}
SDL_Rect dst_rect;
dst_rect.x = dst_rect.y = TOAST_PADDING;
dst_rect.w = surface->w;
dst_rect.h = surface->h;
SDL_RenderCopy(renderer, texture, 0, &dst_rect);
SDL_DestroyTexture(texture);
SDL_FreeSurface(surface);
}
if (sdl_ctx->toast_tex) {
// Draw toast to screen
SDL_SetRenderTarget(renderer, main_tex);
// Get texture size
int toast_w, toast_h;
SDL_QueryTexture(sdl_ctx->toast_tex, 0, 0, &toast_w, &toast_h);
// Calculate dst rect
SDL_Rect dst_rect;
dst_rect.w = toast_w;
dst_rect.h = toast_h;
dst_rect.y = vui->screen_height - dst_rect.h - TOAST_PADDING - TOAST_PADDING;
dst_rect.x = vui->screen_width/2 - toast_w/2;
SDL_RenderCopy(renderer, sdl_ctx->toast_tex, 0, &dst_rect);
// Handle expiry
struct timeval now;
gettimeofday(&now, 0);
if (now.tv_sec*1000000+now.tv_usec >= sdl_ctx->toast_expiry.tv_sec*1000000+sdl_ctx->toast_expiry.tv_usec) {
SDL_DestroyTexture(sdl_ctx->toast_tex);
sdl_ctx->toast_tex = 0;
}
}
// Calculate our destination rectangle
SDL_SetRenderTarget(renderer, NULL);
int out_w, out_h;
int tex_w = vui->screen_width, tex_h = vui->screen_height;
SDL_GetRendererOutputSize(renderer, &out_w, &out_h);
// Hold a reference to the dst_rect for later mouse operations
SDL_Rect *dst_rect = &sdl_ctx->dst_rect;
if (out_w == tex_w && out_h == tex_h) {
dst_rect->x = 0;
dst_rect->y = 0;
dst_rect->w = tex_w;
dst_rect->h = tex_h;
} else if ((uint64_t)out_w * tex_h > (uint64_t)out_h * tex_w) {
dst_rect->h = out_h;
dst_rect->y = 0;
dst_rect->w = (uint64_t)out_h * tex_w / tex_h;
dst_rect->x = (out_w - dst_rect->w) / 2;
} else {
dst_rect->w = out_w;
dst_rect->x = 0;
dst_rect->h = (uint64_t)out_w * tex_h / tex_w;
dst_rect->y = (out_h - dst_rect->h) / 2;
}
// Copy texture to window
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, main_tex, NULL, dst_rect);
// Flip surfaces
SDL_RenderPresent(renderer);
}
if (!vui->quit) {
vui_sdl_wait_for_next_tick(sdl_ctx);
}
return !vui->quit;
}