// // xemu User Interface // // Copyright (C) 2020-2022 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 "common.hh" #include #include #include #include #include "gl-helpers.hh" #include "stb_image.h" #include "data/controller_mask.png.h" #include "data/logo_sdf.png.h" #include "ui/shader/xemu-logo-frag.h" #include "notifications.hh" Fbo *controller_fbo, *logo_fbo; GLuint g_controller_tex, g_logo_tex; enum class ShaderType { Blit, BlitGamma, // FIMXE: Move to nv2a_get_framebuffer_surface Mask, Logo, }; typedef struct DecalShader_ { int flip; float scale; uint32_t time; GLuint prog, vao, vbo, ebo; GLint flipy_loc; GLint tex_loc; GLint scale_offset_loc; GLint tex_scale_offset_loc; GLint color_primary_loc; GLint color_secondary_loc; GLint color_fill_loc; GLint time_loc; GLint scale_loc; GLint palette_loc[256]; } DecalShader; static DecalShader *g_decal_shader, *g_logo_shader, *g_framebuffer_shader; GLint Fbo::vp[4]; GLint Fbo::original_fbo; bool Fbo::blend; DecalShader *NewDecalShader(enum ShaderType type); void DeleteDecalShader(DecalShader *s); static GLint GetCurrentFbo() { GLint fbo; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, (GLint*)&fbo); return fbo; } Fbo::Fbo(int width, int height) { w = width; h = height; // Allocate the texture glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); GLint original = GetCurrentFbo(); // Allocate the framebuffer object glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); GLenum DrawBuffers[1] = { GL_COLOR_ATTACHMENT0 }; glDrawBuffers(1, DrawBuffers); glBindFramebuffer(GL_FRAMEBUFFER, original); } Fbo::~Fbo() { glDeleteTextures(1, &tex); glDeleteFramebuffers(1, &fbo); } void Fbo::Target() { GLint vp[4]; glGetIntegerv(GL_VIEWPORT, vp); original_fbo = GetCurrentFbo(); blend = glIsEnabled(GL_BLEND); if (!blend) { glEnable(GL_BLEND); } glBindFramebuffer(GL_FRAMEBUFFER, fbo); glViewport(0, 0, w, h); glClearColor(0, 0, 0, 0); glClear(GL_COLOR_BUFFER_BIT); } void Fbo::Restore() { if (!blend) { glDisable(GL_BLEND); } // Restore default framebuffer, viewport, blending function glBindFramebuffer(GL_FRAMEBUFFER, original_fbo); glViewport(vp[0], vp[1], vp[2], vp[3]); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } static GLuint InitTexture(unsigned char *data, int width, int height, int channels) { GLuint tex; glGenTextures(1, &tex); assert(tex != 0); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); return tex; } static GLuint LoadTextureFromMemory(const unsigned char *buf, unsigned int size) { // Flip vertically so textures are loaded according to GL convention. stbi_set_flip_vertically_on_load(1); int width, height, channels = 0; unsigned char *data = stbi_load_from_memory(buf, size, &width, &height, &channels, 4); assert(data != NULL); GLuint tex = InitTexture(data, width, height, channels); stbi_image_free(data); return tex; } static GLuint Shader(GLenum type, const char *src) { char err_buf[512]; GLuint shader = glCreateShader(type); assert(shader && "Failed to create shader"); glShaderSource(shader, 1, &src, NULL); glCompileShader(shader); GLint status; glGetShaderiv(shader, GL_COMPILE_STATUS, &status); if (status != GL_TRUE) { glGetShaderInfoLog(shader, sizeof(err_buf), NULL, err_buf); fprintf(stderr, "Shader compilation failed: %s\n\n" "[Shader Source]\n" "%s\n", err_buf, src); assert(0); } return shader; } DecalShader *NewDecalShader(enum ShaderType type) { // Allocate shader wrapper object DecalShader *s = new DecalShader; assert(s != NULL); s->flip = 0; s->scale = 1.4; s->time = 0; const char *vert_src = R"( #version 150 core uniform bool in_FlipY; uniform vec4 in_ScaleOffset; uniform vec4 in_TexScaleOffset; in vec2 in_Position; in vec2 in_Texcoord; out vec2 Texcoord; void main() { vec2 t = in_Texcoord; if (in_FlipY) t.y = 1-t.y; Texcoord = t*in_TexScaleOffset.xy + in_TexScaleOffset.zw; gl_Position = vec4(in_Position*in_ScaleOffset.xy+in_ScaleOffset.zw, 0.0, 1.0); } )"; GLuint vert = Shader(GL_VERTEX_SHADER, vert_src); assert(vert != 0); // const char *image_frag_src = R"( // #version 150 core // uniform sampler2D tex; // in vec2 Texcoord; // out vec4 out_Color; // void main() { // out_Color.rgba = texture(tex, Texcoord); // } // )"; const char *image_gamma_frag_src = R"( #version 400 core uniform sampler2D tex; uniform uint palette[256]; float gamma_ch(int ch, float col) { return float(bitfieldExtract(palette[uint(col * 255.0)], ch*8, 8)) / 255.0; } vec4 gamma(vec4 col) { return vec4(gamma_ch(0, col.r), gamma_ch(1, col.g), gamma_ch(2, col.b), col.a); } in vec2 Texcoord; out vec4 out_Color; void main() { out_Color.rgba = gamma(texture(tex, Texcoord)); } )"; // Simple 2-color decal shader // - in_ColorFill is first pass // - Red channel of the texture is used as primary color, mixed with 1-Red for // secondary color. // - Blue is a lazy alpha removal for now // - Alpha channel passed through const char *mask_frag_src = R"( #version 150 core uniform sampler2D tex; uniform vec4 in_ColorPrimary; uniform vec4 in_ColorSecondary; uniform vec4 in_ColorFill; in vec2 Texcoord; out vec4 out_Color; void main() { vec4 t = texture(tex, Texcoord); out_Color.rgba = in_ColorFill.rgba; out_Color.rgb += mix(in_ColorSecondary.rgb, in_ColorPrimary.rgb, t.r); out_Color.a += t.a - t.b; } )"; const char *frag_src = NULL; switch (type) { case ShaderType::Mask: frag_src = mask_frag_src; break; // case ShaderType::Blit: frag_src = image_frag_src; break; case ShaderType::BlitGamma: frag_src = image_gamma_frag_src; break; case ShaderType::Logo: frag_src = xemu_logo_frag_src; break; default: assert(0); } GLuint frag = Shader(GL_FRAGMENT_SHADER, frag_src); assert(frag != 0); // Link vertex and fragment shaders s->prog = glCreateProgram(); glAttachShader(s->prog, vert); glAttachShader(s->prog, frag); glBindFragDataLocation(s->prog, 0, "out_Color"); glLinkProgram(s->prog); glUseProgram(s->prog); // Flag shaders for deletion when program is deleted glDeleteShader(vert); glDeleteShader(frag); s->flipy_loc = glGetUniformLocation(s->prog, "in_FlipY"); s->scale_offset_loc = glGetUniformLocation(s->prog, "in_ScaleOffset"); s->tex_scale_offset_loc = glGetUniformLocation(s->prog, "in_TexScaleOffset"); s->tex_loc = glGetUniformLocation(s->prog, "tex"); s->color_primary_loc = glGetUniformLocation(s->prog, "in_ColorPrimary"); s->color_secondary_loc = glGetUniformLocation(s->prog, "in_ColorSecondary"); s->color_fill_loc = glGetUniformLocation(s->prog, "in_ColorFill"); s->time_loc = glGetUniformLocation(s->prog, "iTime"); s->scale_loc = glGetUniformLocation(s->prog, "scale"); for (int i = 0; i < 256; i++) { char name[64]; snprintf(name, sizeof(name), "palette[%d]", i); s->palette_loc[i] = glGetUniformLocation(s->prog, name); } const GLfloat verts[6][4] = { // x y s t { -1.0f, -1.0f, 0.0f, 0.0f }, // BL { -1.0f, 1.0f, 0.0f, 1.0f }, // TL { 1.0f, 1.0f, 1.0f, 1.0f }, // TR { 1.0f, -1.0f, 1.0f, 0.0f }, // BR }; const GLint indicies[] = { 0, 1, 2, 3 }; glGenVertexArrays(1, &s->vao); glBindVertexArray(s->vao); glGenBuffers(1, &s->vbo); glBindBuffer(GL_ARRAY_BUFFER, s->vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_COPY); glGenBuffers(1, &s->ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, s->ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indicies), indicies, GL_STATIC_DRAW); GLint loc = glGetAttribLocation(s->prog, "in_Position"); if (loc >= 0) { glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)0); glEnableVertexAttribArray(loc); } loc = glGetAttribLocation(s->prog, "in_Texcoord"); if (loc >= 0) { glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)(2*sizeof(GLfloat))); glEnableVertexAttribArray(loc); } return s; } void RenderDecal(DecalShader *s, float x, float y, float w, float h, float tex_x, float tex_y, float tex_w, float tex_h, uint32_t primary, uint32_t secondary, uint32_t fill) { GLint vp[4]; glGetIntegerv(GL_VIEWPORT, vp); float ww = vp[2], wh = vp[3]; x = (int)x; y = (int)y; w = (int)w; h = (int)h; tex_x = (int)tex_x; tex_y = (int)tex_y; tex_w = (int)tex_w; tex_h = (int)tex_h; int tw_i, th_i; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw_i); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th_i); float tw = tw_i, th = th_i; #define COL(color, c) (float)(((color)>>((c)*8)) & 0xff)/255.0 if (s->flipy_loc >= 0) { glUniform1i(s->flipy_loc, s->flip); } if (s->scale_offset_loc >= 0) { glUniform4f(s->scale_offset_loc, w / ww, h / wh, -1 + ((2 * x + w) / ww), -1 + ((2 * y + h) / wh)); } if (s->tex_scale_offset_loc >= 0) { glUniform4f(s->tex_scale_offset_loc, tex_w / tw, tex_h / th, tex_x / tw, tex_y / th); } if (s->tex_loc >= 0) { glUniform1i(s->tex_loc, 0); } if (s->color_primary_loc >= 0) { glUniform4f(s->color_primary_loc, COL(primary, 3), COL(primary, 2), COL(primary, 1), COL(primary, 0)); } if (s->color_secondary_loc >= 0) { glUniform4f(s->color_secondary_loc, COL(secondary, 3), COL(secondary, 2), COL(secondary, 1), COL(secondary, 0)); } if (s->color_fill_loc >= 0) { glUniform4f(s->color_fill_loc, COL(fill, 3), COL(fill, 2), COL(fill, 1), COL(fill, 0)); } if (s->time_loc >= 0) { glUniform1f(s->time_loc, s->time/1000.0f); } if (s->scale_loc >= 0) { glUniform1f(s->scale_loc, s->scale); } #undef COL glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL); } struct rect { int x, y, w, h; }; static 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 InitCustomRendering(void) { glActiveTexture(GL_TEXTURE0); g_controller_tex = LoadTextureFromMemory(controller_mask_data, controller_mask_size); g_decal_shader = NewDecalShader(ShaderType::Mask); controller_fbo = new Fbo(512, 512); g_logo_tex = LoadTextureFromMemory(logo_sdf_data, logo_sdf_size); g_logo_shader = NewDecalShader(ShaderType::Logo); logo_fbo = new Fbo(512, 512); g_framebuffer_shader = NewDecalShader(ShaderType::BlitGamma); } static void RenderMeter(DecalShader *s, float x, float y, float width, float height, float p, uint32_t color_bg, uint32_t color_fg) { RenderDecal(s, x, y, width, height, 0, 0, 1, 1, 0, 0, color_bg); RenderDecal(s, x, y, width * p, height, 0, 0, 1, 1, 0, 0, color_fg); } void RenderController(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, ControllerState *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(g_decal_shader->prog); glBindVertexArray(g_decal_shader->vao); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, g_controller_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 RenderDecal(g_decal_shader, 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 RenderDecal(g_decal_shader, 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++) { if (state->buttons & (1 << i)) { RenderDecal(g_decal_shader, frame_x + buttons[i].x, frame_y + buttons[i].y, buttons[i].w, buttons[i].h, 0, 0, 1, 1, 0, 0, primary_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; RenderDecal(g_decal_shader, (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; RenderDecal(g_decal_shader, (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); } RenderMeter(g_decal_shader, original_frame_x + 10, original_frame_y + tex_items[obj_controller].h + 20, 150, 5, ltrig, primary_color + alpha, primary_color + 0xff); RenderMeter(g_decal_shader, 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); glBindVertexArray(0); glUseProgram(0); } void RenderControllerPort(float frame_x, float frame_y, int i, uint32_t port_color) { glUseProgram(g_decal_shader->prog); glBindVertexArray(g_decal_shader->vao); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, g_controller_tex); glBlendFunc(GL_ONE, GL_ZERO); // Render port socket RenderDecal(g_decal_shader, 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 RenderDecal( g_decal_shader, 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 RenderLogo(uint32_t time) { uint32_t color = 0x62ca13ff; g_logo_shader->time = time; glUseProgram(g_logo_shader->prog); glBindVertexArray(g_decal_shader->vao); glBlendFunc(GL_ONE, GL_ZERO); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, g_logo_tex); RenderDecal(g_logo_shader, 0, 0, 512, 512, 0, 0, 128, 128, color, color, 0x00000000); glBindVertexArray(0); glUseProgram(0); } void RenderFramebuffer(GLint tex, int width, int height, bool flip) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex); int tw, th; glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &tw); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &th); // Calculate scaling factors float scale[2]; if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_STRETCH) { // Stretch to fit scale[0] = 1.0; scale[1] = 1.0; } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_CENTER) { // Centered scale[0] = (float)tw/(float)width; scale[1] = (float)th/(float)height; } else { float t_ratio; if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) { // Scale to fit window using a fixed 16:9 aspect ratio t_ratio = 16.0f/9.0f; } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) { t_ratio = 4.0f/3.0f; } else { // Scale to fit, preserving framebuffer aspect ratio t_ratio = (float)tw/(float)th; } float w_ratio = (float)width/(float)height; if (w_ratio >= t_ratio) { scale[0] = t_ratio/w_ratio; scale[1] = 1.0; } else { scale[0] = 1.0; scale[1] = w_ratio/t_ratio; } } DecalShader *s = g_framebuffer_shader; s->flip = flip; glViewport(0, 0, width, height); glUseProgram(s->prog); glBindVertexArray(s->vao); glUniform1i(s->flipy_loc, s->flip); glUniform4f(s->scale_offset_loc, scale[0], scale[1], 0, 0); glUniform4f(s->tex_scale_offset_loc, 1.0, 1.0, 0, 0); glUniform1i(s->tex_loc, 0); const uint8_t *palette = nv2a_get_dac_palette(); for (int i = 0; i < 256; i++) { uint32_t e = (palette[i * 3 + 2] << 16) | (palette[i * 3 + 1] << 8) | palette[i * 3]; glUniform1ui(s->palette_loc[i], e); } glClearColor(0, 0, 0, 0); glClear(GL_COLOR_BUFFER_BIT); glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL); } void SaveScreenshot(GLuint tex, bool flip) { int width, height; glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); glBindTexture(GL_TEXTURE_2D, 0); if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_16_9) { width = height * (16.0f / 9.0f); } else if (g_config.display.ui.fit == CONFIG_DISPLAY_UI_FIT_SCALE_4_3) { width = height * (4.0f / 3.0f); } std::vector pixels; pixels.resize(width * height * 3); Fbo fbo(width, height); fbo.Target(); bool blend = glIsEnabled(GL_BLEND); if (blend) glDisable(GL_BLEND); RenderFramebuffer(tex, width, height, !flip); if (blend) glEnable(GL_BLEND); glPixelStorei(GL_PACK_ROW_LENGTH, width); glPixelStorei(GL_PACK_IMAGE_HEIGHT, height); glPixelStorei(GL_PACK_ALIGNMENT, 1); glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); fbo.Restore(); char fname[128]; Error *err = NULL; std::vector png; if (fpng::fpng_encode_image_to_memory(pixels.data(), width, height, 3, png)) { time_t t = time(NULL); struct tm *tmp = localtime(&t); if (tmp) { strftime(fname, sizeof(fname), "xemu-%Y-%m-%d-%H-%M-%S.png", tmp); } else { strcpy(fname, "xemu.png"); } const char *output_dir = g_config.general.screenshot_dir; if (!strlen(output_dir)) { output_dir = "."; } // FIXME: Check for existing path char *path = g_strdup_printf("%s/%s", output_dir, fname); FILE *fd = qemu_fopen(path, "wb"); if (fd) { int s = fwrite(png.data(), png.size(), 1, fd); if (s != 1) { error_setg(&err, "Failed to write %s", path); } fclose(fd); } else { error_setg(&err, "Failed to open %s for writing", path); } g_free(path); } else { error_setg(&err, "Failed to encode PNG image"); } if (err) { xemu_queue_error_message(error_get_pretty(err)); error_report_err(err); } else { char *msg = g_strdup_printf("Screenshot Saved: %s", fname); xemu_queue_notification(msg); free(msg); } }