diff --git a/ui/xemu-custom-widgets.c b/ui/xemu-custom-widgets.c
new file mode 100644
index 0000000000..c6f823ae2d
--- /dev/null
+++ b/ui/xemu-custom-widgets.c
@@ -0,0 +1,307 @@
+/*
+ * xemu User Interface Rendering Helpers
+ *
+ * Copyright (C) 2020 Matt Borgerson
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include
+#include
+#include
+#include
+
+#include "xemu-shaders.h"
+#include "xemu-custom-widgets.h"
+
+static struct decal_shader *s = NULL;
+static struct decal_shader *s_logo = NULL;
+GLuint main_fb;
+struct fbo *controller_fbo;
+struct fbo *logo_fbo;
+GLint vp[4];
+GLuint g_ui_tex;
+GLuint g_logo_tex;
+
+struct rect {
+ int x, y, w, h;
+};
+
+const struct rect tex_items[] = {
+ { 0, 148, 467, 364 }, // obj_controller
+ { 0, 81, 67, 67 }, // obj_lstick
+ { 0, 14, 67, 67 }, // obj_rstick
+ { 67, 104, 68, 44 }, // obj_port_socket
+ { 67, 76, 28, 28 }, // obj_port_lbl_1
+ { 67, 48, 28, 28 }, // obj_port_lbl_2
+ { 67, 20, 28, 28 }, // obj_port_lbl_3
+ { 95, 76, 28, 28 }, // obj_port_lbl_4
+};
+
+enum tex_item_names {
+ obj_controller,
+ obj_lstick,
+ obj_rstick,
+ obj_port_socket,
+ obj_port_lbl_1,
+ obj_port_lbl_2,
+ obj_port_lbl_3,
+ obj_port_lbl_4,
+};
+
+void initialize_custom_ui_rendering(void)
+{
+ glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, (GLint*)&main_fb);
+ glGetIntegerv(GL_VIEWPORT, vp);
+
+ glActiveTexture(GL_TEXTURE0);
+ g_ui_tex = load_texture_from_file("data/controller-mask.png");
+ s = create_decal_shader(SHADER_TYPE_MASK);
+ g_logo_tex = load_texture_from_file("data/logo-sdf.png");
+ s_logo = create_decal_shader(SHADER_TYPE_LOGO);
+ controller_fbo = create_fbo(512, 512);
+ logo_fbo = create_fbo(512, 512);
+ render_to_default_fb();
+}
+
+void render_meter(
+ struct decal_shader *s,
+ float x, float y, float width, float height, float p,
+ uint32_t color_bg, uint32_t color_fg)
+{
+ render_decal(s, x, y, width, height,0, 0, 1, 1, 0, 0, color_bg);
+ render_decal(s, x, y, width*p, height ,0, 0, 1, 1, 0, 0, color_fg);
+}
+
+void render_controller(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, struct controller_state *state)
+{
+ // Location within the controller texture of masked button locations,
+ // relative to the origin of the controller
+ const struct rect jewel = { 177, 172, 113, 118 };
+ const struct rect lstick_ctr = { 93, 246, 0, 0 };
+ const struct rect rstick_ctr = { 342, 148, 0, 0 };
+ const struct rect buttons[12] = {
+ { 367, 187, 30, 38 }, // A
+ { 368, 229, 30, 38 }, // B
+ { 330, 204, 30, 38 }, // X
+ { 331, 247, 30, 38 }, // Y
+ { 82, 121, 31, 47 }, // D-Left
+ { 104, 160, 44, 25 }, // D-Up
+ { 141, 121, 31, 47 }, // D-Right
+ { 104, 105, 44, 25 }, // D-Down
+ { 187, 94, 34, 24 }, // Back
+ { 246, 94, 36, 26 }, // Start
+ { 348, 288, 30, 38 }, // White
+ { 386, 268, 30, 38 }, // Black
+ };
+
+ uint8_t alpha = 0;
+ uint32_t now = SDL_GetTicks();
+ float t;
+
+ glUseProgram(s->prog);
+ glBindVertexArray(s->vao);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, g_ui_tex);
+
+ // Add a 5 pixel space around the controller so we can wiggle the controller
+ // around to visualize rumble in action
+ frame_x += 5;
+ frame_y += 5;
+ float original_frame_x = frame_x;
+ float original_frame_y = frame_y;
+
+ // Floating point versions that will get scaled
+ float rumble_l = 0;
+ float rumble_r = 0;
+
+ glBlendEquation(GL_FUNC_ADD);
+ glBlendFunc(GL_ONE, GL_ZERO);
+
+ uint32_t jewel_color = secondary_color;
+
+ // Check to see if the guide button is pressed
+ const uint32_t animate_guide_button_duration = 2000;
+ if (state->buttons & CONTROLLER_BUTTON_GUIDE) {
+ state->animate_guide_button_end = now + animate_guide_button_duration;
+ }
+
+ if (now < state->animate_guide_button_end) {
+ t = 1.0f - (float)(state->animate_guide_button_end-now)/(float)animate_guide_button_duration;
+ float sin_wav = (1-sin(M_PI * t / 2.0f));
+
+ // Animate guide button by highlighting logo jewel and fading out over time
+ alpha = sin_wav * 255.0f;
+ jewel_color = primary_color + alpha;
+
+ // Add a little extra flare: wiggle the frame around while we rumble
+ frame_x += ((float)(rand() % 5)-2.5) * (1-t);
+ frame_y += ((float)(rand() % 5)-2.5) * (1-t);
+ rumble_l = rumble_r = sin_wav;
+ }
+
+ // Render controller texture
+ render_decal(s,
+ frame_x+0, frame_y+0, tex_items[obj_controller].w, tex_items[obj_controller].h,
+ tex_items[obj_controller].x, tex_items[obj_controller].y, tex_items[obj_controller].w, tex_items[obj_controller].h,
+ primary_color, secondary_color, 0);
+
+ glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ONE); // Blend with controller cutouts
+ render_decal(s, frame_x+jewel.x, frame_y+jewel.y, jewel.w, jewel.h, 0, 0, 1, 1, 0, 0, jewel_color);
+
+ // The controller has alpha cutouts where the buttons are. Draw a surface
+ // behind the buttons if they are activated
+ for (int i = 0; i < 12; i++) {
+ bool enabled = !!(state->buttons & (1 << i));
+ if (!enabled) continue;
+ render_decal(s,
+ frame_x+buttons[i].x, frame_y+buttons[i].y,
+ buttons[i].w, buttons[i].h,
+ 0, 0, 1, 1,
+ 0, 0, (enabled ? primary_color : secondary_color)+0xff);
+ }
+
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Blend with controller
+
+ // Render left thumbstick
+ float w = tex_items[obj_lstick].w;
+ float h = tex_items[obj_lstick].h;
+ float c_x = frame_x+lstick_ctr.x;
+ float c_y = frame_y+lstick_ctr.y;
+ float lstick_x = (float)state->axis[CONTROLLER_AXIS_LSTICK_X]/32768.0;
+ float lstick_y = (float)state->axis[CONTROLLER_AXIS_LSTICK_Y]/32768.0;
+ render_decal(s,
+ (int)(c_x-w/2.0f+10.0f*lstick_x),
+ (int)(c_y-h/2.0f+10.0f*lstick_y),
+ w, h,
+ tex_items[obj_lstick].x, tex_items[obj_lstick].y, w, h,
+ (state->buttons & CONTROLLER_BUTTON_LSTICK) ? secondary_color : primary_color,
+ (state->buttons & CONTROLLER_BUTTON_LSTICK) ? primary_color : secondary_color,
+ 0
+ );
+
+ // Render right thumbstick
+ w = tex_items[obj_rstick].w;
+ h = tex_items[obj_rstick].h;
+ c_x = frame_x+rstick_ctr.x;
+ c_y = frame_y+rstick_ctr.y;
+ float rstick_x = (float)state->axis[CONTROLLER_AXIS_RSTICK_X]/32768.0;
+ float rstick_y = (float)state->axis[CONTROLLER_AXIS_RSTICK_Y]/32768.0;
+ render_decal(s,
+ (int)(c_x-w/2.0f+10.0f*rstick_x),
+ (int)(c_y-h/2.0f+10.0f*rstick_y),
+ w, h,
+ tex_items[obj_rstick].x, tex_items[obj_rstick].y, w, h,
+ (state->buttons & CONTROLLER_BUTTON_RSTICK) ? secondary_color : primary_color,
+ (state->buttons & CONTROLLER_BUTTON_RSTICK) ? primary_color : secondary_color,
+ 0
+ );
+
+ glBlendFunc(GL_ONE, GL_ZERO); // Don't blend, just overwrite values in buffer
+
+ // Render trigger bars
+ float ltrig = state->axis[CONTROLLER_AXIS_LTRIG] / 32767.0;
+ float rtrig = state->axis[CONTROLLER_AXIS_RTRIG] / 32767.0;
+ const uint32_t animate_trigger_duration = 1000;
+ if ((ltrig > 0) || (rtrig > 0)) {
+ state->animate_trigger_end = now + animate_trigger_duration;
+ rumble_l = fmax(rumble_l, ltrig);
+ rumble_r = fmax(rumble_r, rtrig);
+ }
+
+ // Animate trigger alpha down after a period of inactivity
+ alpha = 0x80;
+ if (state->animate_trigger_end > now) {
+ t = 1.0f - (float)(state->animate_trigger_end-now)/(float)animate_trigger_duration;
+ float sin_wav = (1-sin(M_PI * t / 2.0f));
+ alpha += fmin(sin_wav * 0x40, 0x80);
+ }
+
+ render_meter(s,
+ original_frame_x+10,
+ original_frame_y+tex_items[obj_controller].h+20,
+ 150, 5,
+ ltrig,
+ primary_color + alpha,
+ primary_color + 0xff);
+ render_meter(s,
+ original_frame_x+tex_items[obj_controller].w-160,
+ original_frame_y+tex_items[obj_controller].h+20,
+ 150, 5,
+ rtrig,
+ primary_color + alpha,
+ primary_color + 0xff);
+
+ // Apply rumble updates
+ state->rumble_l = (int)(rumble_l * (float)0xffff);
+ state->rumble_r = (int)(rumble_r * (float)0xffff);
+ xemu_input_update_rumble(state);
+
+ glBindVertexArray(0);
+ glUseProgram(0);
+}
+
+void render_controller_port(float frame_x, float frame_y, int i, uint32_t port_color)
+{
+ glUseProgram(s->prog);
+ glBindVertexArray(s->vao);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, g_ui_tex);
+
+ glBlendFunc(GL_ONE, GL_ZERO);
+
+ // Render port socket
+ render_decal(s,
+ frame_x, frame_y,
+ tex_items[obj_port_socket].w, tex_items[obj_port_socket].h,
+ tex_items[obj_port_socket].x, tex_items[obj_port_socket].y,
+ tex_items[obj_port_socket].w, tex_items[obj_port_socket].h,
+ port_color, port_color, 0
+ );
+
+ frame_x += (tex_items[obj_port_socket].w-tex_items[obj_port_lbl_1].w)/2;
+ frame_y += tex_items[obj_port_socket].h + 8;
+
+ // Render port label
+ render_decal(s,
+ frame_x, frame_y,
+ tex_items[obj_port_lbl_1+i].w, tex_items[obj_port_lbl_1+i].h,
+ tex_items[obj_port_lbl_1+i].x, tex_items[obj_port_lbl_1+i].y,
+ tex_items[obj_port_lbl_1+i].w, tex_items[obj_port_lbl_1+i].h,
+ port_color, port_color, 0
+ );
+
+ glBindVertexArray(0);
+ glUseProgram(0);
+}
+
+void render_logo(uint32_t primary_color, uint32_t secondary_color, uint32_t fill_color)
+{
+ glUseProgram(s_logo->prog);
+ glBindVertexArray(s->vao);
+ glBlendFunc(GL_ONE, GL_ZERO);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, g_logo_tex);
+ render_decal(
+ s_logo,
+ 0, 0, 512, 512,
+ 0,
+ 0,
+ 128,
+ 128,
+ primary_color, secondary_color, fill_color
+ );
+ glBindVertexArray(0);
+ glUseProgram(0);
+}
diff --git a/ui/xemu-custom-widgets.h b/ui/xemu-custom-widgets.h
new file mode 100644
index 0000000000..4dbd7a0fb3
--- /dev/null
+++ b/ui/xemu-custom-widgets.h
@@ -0,0 +1,45 @@
+/*
+ * xemu User Interface Rendering Helpers
+ *
+ * Copyright (C) 2020 Matt Borgerson
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef XEMU_CUSTOM_WIDGETS
+#define XEMU_CUSTOM_WIDGETS
+
+#include
+#include "xemu-input.h"
+#include "xemu-shaders.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// FIXME: Cleanup
+extern struct fbo *controller_fbo;
+extern struct fbo *logo_fbo;
+
+void initialize_custom_ui_rendering(void);
+void render_meter(struct decal_shader *s, float x, float y, float width, float height, float p, uint32_t color_bg, uint32_t color_fg);
+void render_controller(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, struct controller_state *state);
+void render_controller_port(float frame_x, float frame_y, int i, uint32_t port_color);
+void render_logo(uint32_t primary_color, uint32_t secondary_color, uint32_t fill_color);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif