/* Copyright (c) 2012-2024 R. Danbrook Copyright (c) 2020-2024 Rupert Carmichael All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include "videomanager.h" #include "logdriver.h" #include "font.h" #include "lodepng.h" namespace { jg_videoinfo_t *vidinfo{nullptr}; uint32_t *videobuf{nullptr}; struct osdtext { int xpos; int ypos; char textbuf[32]; char timebuf[6]; int drawtext; bool drawtime; bool bg; } osdtext; // Dimensions struct _dimensions { int ww; int wh; float rw; float rh; float xo; float yo; float dpiscale; } dimensions; } // namespace VideoRenderer::VideoRenderer(SettingManager& setmgr) : setmgr(setmgr) { } VideoRendererLegacy::VideoRendererLegacy(SettingManager& setmgr) : VideoRenderer(setmgr) { // Generate texture for raw game output glEnable(GL_TEXTURE_2D); glGenTextures(1, &gl_texture_id); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gl_texture_id); // The full sized source image before any clipping glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, vidinfo->wmax, vidinfo->hmax, 0, GL_BGRA, GL_UNSIGNED_BYTE, videobuf); GLuint filter = setmgr.get_setting("v_postproc")->val ? GL_LINEAR : GL_NEAREST; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); // Ignore the empty alpha channel glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); } VideoRendererLegacy::~VideoRendererLegacy() { if (gl_texture_id) { glDeleteTextures(1, &gl_texture_id); } } VideoRendererModern::VideoRendererModern(SettingManager& setmgr, const std::string ver) : VideoRenderer(setmgr), glslver(ver) { // Create Vertex Array Objects glGenVertexArrays(1, &vao[0]); glGenVertexArrays(1, &vao[1]); // Create Vertex Buffer Objects glGenBuffers(1, &vbo[0]); glGenBuffers(1, &vbo[1]); // Bind buffers for vertex buffer objects glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); GLfloat vertices_out[] = { -1.0, -1.0, // Vertex 1 (X, Y) Left Bottom -1.0, 1.0, // Vertex 2 (X, Y) Left Top 1.0, -1.0, // Vertex 3 (X, Y) Right Bottom 1.0, 1.0, // Vertex 4 (X, Y) Right Top 0.0, 1.0, // Texture 1 (X, Y) Left Bottom 0.0, 0.0, // Texture 2 (X, Y) Left Top 1.0, 1.0, // Texture 3 (X, Y) Right Bottom 1.0, 0.0, // Texture 4 (X, Y) Right Top }; glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_out), vertices_out, GL_STATIC_DRAW); // Bind vertex array and specify layout for first pass glBindVertexArray(vao[0]); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // Generate texture for raw game output glGenTextures(1, &tex[0]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex[0]); // The full sized source image before any clipping glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, vidinfo->wmax, vidinfo->hmax, 0, GL_BGRA, GL_UNSIGNED_BYTE, videobuf); // Create framebuffer glGenFramebuffers(1, &framebuf); glBindFramebuffer(GL_FRAMEBUFFER, framebuf); // Create texture to hold colour buffer glGenTextures(1, &tex[1]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex[1]); // The framebuffer texture that is being rendered to offscreen, after clip glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, vidinfo->w, vidinfo->h, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex[1], 0); // Ignore the empty alpha channel glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); shader_setup(); ogl_refresh(); } VideoRendererModern::~VideoRendererModern() { if (gl_texture_id) { glDeleteTextures(1, &gl_texture_id); } if (framebuf) { glDeleteFramebuffers(1, &framebuf); } for (size_t i = 0; i < NUMPASSES; ++i) { if (shaderprog[i]) glDeleteProgram(shaderprog[i]); if (tex[i]) glDeleteTextures(1, &tex[i]); if (vao[i]) glDeleteVertexArrays(1, &vao[i]); if (vbo[i]) glDeleteBuffers(1, &vbo[i]); } } void VideoRendererLegacy::rehash(bool reset_shaders) { if (reset_shaders) { GLuint filter = setmgr.get_setting("v_postproc")->val ? GL_LINEAR : GL_NEAREST; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); } } void VideoRendererLegacy::ogl_refresh() { float top = (float)vidinfo->y / vidinfo->hmax; float bottom = 1.0 + top - ((vidinfo->hmax - (float)vidinfo->h) / vidinfo->hmax); float left = (float)vidinfo->x / vidinfo->wmax; float right = 1.0 + left - ((vidinfo->wmax -(float)vidinfo->w) / vidinfo->wmax); // Check if any vertices have changed since last time if (vertices[9] != top || vertices[11] != bottom || vertices[8] != left || vertices[12] != right) { vertices[9] = vertices[13] = top; vertices[11] = vertices[15] = bottom; vertices[8] = vertices[10] = left; vertices[12] = vertices[14] = right; } } void VideoRendererLegacy::ogl_render() { // OSD Text if (osdtext.drawtext) { text_draw(osdtext.textbuf, osdtext.xpos, osdtext.ypos, osdtext.bg); osdtext.drawtext--; } if (osdtext.drawtime) { text_draw(osdtext.timebuf, 208, 218, false); } ogl_refresh(); // Check for changes glPixelStorei(GL_UNPACK_ROW_LENGTH, vidinfo->p); // Viewport set to size of the output glViewport(dimensions.xo, dimensions.yo, dimensions.rw, dimensions.rh); // Clear the screen to black glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gl_texture_id); // Render if there is new pixel data, do Black Frame Insertion otherwise glTexSubImage2D(GL_TEXTURE_2D, 0, 0, // xoffset 0, // yoffset vidinfo->w + vidinfo->x, // width vidinfo->h + vidinfo->y, // height GL_BGRA, // format GL_UNSIGNED_BYTE, // type videobuf); glBegin(GL_QUADS); glTexCoord2f(vertices[10], vertices[11]); glVertex2f(vertices[0], vertices[1]); // Bottom Left glTexCoord2f(vertices[8], vertices[9]); glVertex2f(vertices[2], vertices[3]); // Top Left glTexCoord2f(vertices[12], vertices[13]); glVertex2f(vertices[6], vertices[7]); // Top Right glTexCoord2f(vertices[14], vertices[15]); glVertex2f(vertices[4], vertices[5]); // Bottom Right glEnd(); } void VideoRendererModern::rehash(bool reset_shaders) { if (reset_shaders) { shader_setup(); } // Update uniforms for post-processing glUniform4f(glGetUniformLocation(shaderprog[1], "sourceSize"), (float)vidinfo->w, (float)vidinfo->h, 1.0/(float)vidinfo->w, 1.0/(float)vidinfo->h); glUniform4f(glGetUniformLocation(shaderprog[1], "targetSize"), dimensions.rw, dimensions.rh, 1.0/dimensions.rw, 1.0/dimensions.rh); } GLuint VideoRendererModern::shader_create(const std::string& vs, const std::string& fs) { // If the binary is run from the source directory, shader path is PWD std::string shaderpath{}; if (std::filesystem::exists(std::filesystem::path{"shaders/default.vs"})) { shaderpath = std::string(std::getenv("PWD")) + "/shaders/"; } else { shaderpath = std::string(NST_DATADIR) + "/shaders/"; } auto shader_load = [this](const std::string filename) -> std::string { std::ifstream shader_file(filename); if (shader_file.is_open()) { std::stringstream buffer; buffer << shader_file.rdbuf(); shader_file.close(); return glslver + buffer.str(); } return {}; }; std::string vssrc = shader_load(shaderpath + vs); const GLchar *vsrc = vssrc.c_str(); std::string fssrc = shader_load(shaderpath + fs); const GLchar *fsrc = fssrc.c_str(); // Create and compile the vertex shader GLuint vshader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vshader, 1, &vsrc, NULL); glCompileShader(vshader); // Test if the shader compiled GLint err; glGetShaderiv(vshader, GL_COMPILE_STATUS, &err); if (err == GL_FALSE) { char shaderlog[1024]; glGetShaderInfoLog(vshader, 1024, NULL, shaderlog); LogDriver::log(LogLevel::Warn, "Vertex shader: " + std::string(shaderlog)); } // Create and compile the fragment shader GLuint fshader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fshader, 1, &fsrc, NULL); glCompileShader(fshader); // Test if the fragment shader compiled glGetShaderiv(fshader, GL_COMPILE_STATUS, &err); if (err == GL_FALSE) { char shaderlog[1024]; glGetShaderInfoLog(fshader, 1024, NULL, shaderlog); LogDriver::log(LogLevel::Warn, "Fragment shader: " + std::string(shaderlog)); } // Create the shader program GLuint prog = glCreateProgram(); glAttachShader(prog, vshader); glAttachShader(prog, fshader); glLinkProgram(prog); // Clean up fragment and vertex shaders glDeleteShader(vshader); glDeleteShader(fshader); // Return the successfully linked shader program return prog; } void VideoRendererModern::shader_setup(void) { for (size_t i = 0; i < NUMPASSES; ++i) { if (shaderprog[i]) { glDeleteProgram(shaderprog[i]); } texfilter[i] = GL_NEAREST; } // Create the shader program for the first pass (clipping) shaderprog[0] = shader_create("default.vs", "default.fs"); GLint posattrib = glGetAttribLocation(shaderprog[0], "position"); glEnableVertexAttribArray(posattrib); glVertexAttribPointer(posattrib, 2, GL_FLOAT, GL_FALSE, 0, 0); GLint texattrib = glGetAttribLocation(shaderprog[0], "vtxCoord"); glEnableVertexAttribArray(texattrib); glVertexAttribPointer(texattrib, 2, GL_FLOAT, GL_FALSE, 0, (void*)(8 * sizeof(GLfloat))); // Set up uniform for input texture glUseProgram(shaderprog[0]); glUniform1i(glGetUniformLocation(shaderprog[0], "source"), 0); // Set up the post-processing shader switch (setmgr.get_setting("v_postproc")->val) { default: case 0: { // Nearest Neighbour shaderprog[1] = shader_create("default.vs", "default.fs"); break; } case 1: { // Linear shaderprog[1] = shader_create("default.vs", "default.fs"); texfilter[1] = GL_LINEAR; break; } case 2: { // Sharp Bilinear shaderprog[1] = shader_create("default.vs", "sharp-bilinear.fs"); texfilter[0] = GL_LINEAR; texfilter[1] = GL_LINEAR; break; } case 3: { // CRTea shaderprog[1] = shader_create("default.vs", "crtea.fs"); break; } case 4: { // MMPX shaderprog[1] = shader_create("default.vs", "mmpx.fs"); break; } case 5: { // Omniscale shaderprog[1] = shader_create("default.vs", "omniscale.fs"); break; } } // Set texture parameters for input texture glBindTexture(GL_TEXTURE_2D, tex[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, texfilter[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, texfilter[0]); // Bind vertex array and specify layout for second pass glBindVertexArray(vao[1]); glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); GLint posattrib_out = glGetAttribLocation(shaderprog[1], "position"); glEnableVertexAttribArray(posattrib_out); glVertexAttribPointer(posattrib_out, 2, GL_FLOAT, GL_FALSE, 0, 0); GLint texattrib_out = glGetAttribLocation(shaderprog[1], "vtxCoord"); glEnableVertexAttribArray(texattrib_out); glVertexAttribPointer(texattrib_out, 2, GL_FLOAT, GL_FALSE, 0, (void*)(8 * sizeof(GLfloat))); // Set up uniforms for post-processing texture glUseProgram(shaderprog[1]); glUniform1i(glGetUniformLocation(shaderprog[1], "source"), 0); glUniform4f(glGetUniformLocation(shaderprog[1], "sourceSize"), (float)vidinfo->w, (float)vidinfo->h, 1.0/(float)vidinfo->w, 1.0/(float)vidinfo->h); glUniform4f(glGetUniformLocation(shaderprog[1], "targetSize"), dimensions.rw, dimensions.rh, 1.0/dimensions.rw, 1.0/dimensions.rh); // Settings for CRT glUniform1i(glGetUniformLocation(shaderprog[1], "masktype"), setmgr.get_setting("s_crtmasktype")->val); glUniform1f(glGetUniformLocation(shaderprog[1], "maskstr"), setmgr.get_setting("s_crtmaskstr")->val / 10.0); glUniform1f(glGetUniformLocation(shaderprog[1], "scanstr"), setmgr.get_setting("s_crtscanstr")->val / 10.0); glUniform1f(glGetUniformLocation(shaderprog[1], "sharpness"), float(setmgr.get_setting("s_crtsharp")->val)); glUniform1f(glGetUniformLocation(shaderprog[1], "curve"), setmgr.get_setting("s_crtcurve")->val / 100.0); glUniform1f(glGetUniformLocation(shaderprog[1], "corner"), setmgr.get_setting("s_crtcorner")->val ? float(setmgr.get_setting("s_crtcorner")->val) : -3.0); glUniform1f(glGetUniformLocation(shaderprog[1], "tcurve"), setmgr.get_setting("s_crttcurve")->val / 10.0); // Set parameters for output texture glBindTexture(GL_TEXTURE_2D, tex[1]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, texfilter[1]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, texfilter[1]); } void VideoRendererModern::ogl_render() { // OSD Text if (osdtext.drawtext) { text_draw(osdtext.textbuf, osdtext.xpos, osdtext.ypos, osdtext.bg); osdtext.drawtext--; } if (osdtext.drawtime) { text_draw(osdtext.timebuf, 208, 218, false); } ogl_refresh(); // Check for changes // Viewport set to size of the input pixel array glViewport(0, 0, vidinfo->w, vidinfo->h); // Make sure first pass shader program is active glUseProgram(shaderprog[0]); // Bind user-created framebuffer and draw scene onto it glBindFramebuffer(GL_FRAMEBUFFER, framebuf); glBindVertexArray(vao[0]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex[0]); // Render if there is new pixel data, do Black Frame Insertion otherwise glTexSubImage2D(GL_TEXTURE_2D, 0, 0, // xoffset 0, // yoffset vidinfo->w + vidinfo->x, // width vidinfo->h + vidinfo->y, // height GL_BGRA, // format GL_UNSIGNED_BYTE, // type videobuf); // Clear the screen to black glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Draw a rectangle from the 2 triangles glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // Now deal with the actual image to be output // Viewport adjusted for output glViewport(dimensions.xo, dimensions.yo, dimensions.rw, dimensions.rh); // Make sure second pass shader program is active glUseProgram(shaderprog[1]); // Bind default framebuffer and draw contents of user framebuffer glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindVertexArray(vao[1]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex[1]); // Clear the screen to black again glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // Draw framebuffer contents glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } void VideoRendererModern::ogl_refresh() { float top = (float)vidinfo->y / vidinfo->hmax; float bottom = 1.0 + top - ((vidinfo->hmax - (float)vidinfo->h) / vidinfo->hmax); float left = (float)vidinfo->x / vidinfo->wmax; float right = 1.0 + left - ((vidinfo->wmax -(float)vidinfo->w) / vidinfo->wmax); // Check if any vertices have changed since last time if (vertices[9] != top || vertices[11] != bottom || vertices[8] != left || vertices[12] != right) { vertices[9] = vertices[13] = top; vertices[11] = vertices[15] = bottom; vertices[8] = vertices[10] = left; vertices[12] = vertices[14] = right; } else { return; } // Bind the VAO/VBO for the offscreen texture, update with new vertex data glBindVertexArray(vao[0]); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // Resize the offscreen texture glBindTexture(GL_TEXTURE_2D, tex[0]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, vidinfo->wmax, vidinfo->hmax, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); // Set row length glUseProgram(shaderprog[0]); glPixelStorei(GL_UNPACK_ROW_LENGTH, vidinfo->p); // Resize the output texture glUseProgram(shaderprog[1]); glBindTexture(GL_TEXTURE_2D, tex[1]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, vidinfo->w, vidinfo->h, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); // Update uniforms for post-processing glUniform4f(glGetUniformLocation(shaderprog[1], "sourceSize"), (float)vidinfo->w, (float)vidinfo->h, 1.0/(float)vidinfo->w, 1.0/(float)vidinfo->h); glUniform4f(glGetUniformLocation(shaderprog[1], "targetSize"), dimensions.rw, dimensions.rh, 1.0/dimensions.rw, 1.0/dimensions.rh); } void VideoRenderer::text_print(const char *text, int xpos, int ypos, int seconds, bool bg) { snprintf(osdtext.textbuf, sizeof(osdtext.textbuf), "%s", text); osdtext.xpos = xpos; osdtext.ypos = ypos; osdtext.drawtext = seconds * 60; // FIXME frametime osdtext.bg = bg; } void VideoRenderer::text_print_time(const char *timebuf, bool drawtime) { snprintf(osdtext.timebuf, sizeof(osdtext.timebuf), "%s", timebuf); osdtext.drawtime = drawtime; } void VideoRenderer::text_draw(const char *text, int xpos, int ypos, bool bg) { // Draw text on screen uint32_t w = 0xc0c0c0c0; // "White", actually Grey uint32_t b = 0x00000000; // Black uint32_t g = 0x00358570; // Nestopia UE Green uint32_t d = 0x00255f65; // Nestopia UE Dark Green int numchars = strlen(text); int letterypos; int letterxpos; int letternum = 0; if (bg) { // Draw background borders for (int i = 0; i < numchars * 8; i++) { // Rows above and below videobuf[(xpos + i) + ((ypos - 1) * vidinfo->wmax)] = g; videobuf[(xpos + i) + ((ypos + 8) * vidinfo->wmax)] = g; } for (int i = 0; i < 8; i++) { // Columns on both sides videobuf[(xpos - 1) + ((ypos + i) * vidinfo->wmax)] = g; videobuf[(xpos + (numchars * 8)) + ((ypos + i) * vidinfo->wmax)] = g; } } // FIXME this code is terrible for (int tpos = 0; tpos < (8 * numchars); tpos+=8) { text_match(text, &letterxpos, &letterypos, letternum); for (int row = 0; row < 8; row++) { // Draw Rows for (int col = 0; col < 8; col++) { // Draw Columns switch (nesfont[row + letterypos][col + letterxpos]) { case '.': videobuf[xpos + ((ypos + row) * vidinfo->wmax) + (col + tpos)] = w; break; case '+': videobuf[xpos + ((ypos + row) * vidinfo->wmax) + (col + tpos)] = g; break; default: if (bg) { videobuf[xpos + ((ypos + row) * vidinfo->wmax) + (col + tpos)] = d; } break; } } } letternum++; } } void VideoRenderer::text_match(const char *text, int *xpos, int *ypos, int strpos) { // Match letters to draw on screen switch (text[strpos]) { case ' ': *xpos = 0; *ypos = 0; break; case '!': *xpos = 8; *ypos = 0; break; case '"': *xpos = 16; *ypos = 0; break; case '#': *xpos = 24; *ypos = 0; break; case '$': *xpos = 32; *ypos = 0; break; case '%': *xpos = 40; *ypos = 0; break; case '&': *xpos = 48; *ypos = 0; break; case '\'': *xpos = 56; *ypos = 0; break; case '(': *xpos = 64; *ypos = 0; break; case ')': *xpos = 72; *ypos = 0; break; case '*': *xpos = 80; *ypos = 0; break; case '+': *xpos = 88; *ypos = 0; break; case ',': *xpos = 96; *ypos = 0; break; case '-': *xpos = 104; *ypos = 0; break; case '.': *xpos = 112; *ypos = 0; break; case '/': *xpos = 120; *ypos = 0; break; case '0': *xpos = 0; *ypos = 8; break; case '1': *xpos = 8; *ypos = 8; break; case '2': *xpos = 16; *ypos = 8; break; case '3': *xpos = 24; *ypos = 8; break; case '4': *xpos = 32; *ypos = 8; break; case '5': *xpos = 40; *ypos = 8; break; case '6': *xpos = 48; *ypos = 8; break; case '7': *xpos = 56; *ypos = 8; break; case '8': *xpos = 64; *ypos = 8; break; case '9': *xpos = 72; *ypos = 8; break; case ':': *xpos = 80; *ypos = 8; break; case ';': *xpos = 88; *ypos = 8; break; case '<': *xpos = 96; *ypos = 8; break; case '=': *xpos = 104; *ypos = 8; break; case '>': *xpos = 112; *ypos = 8; break; case '?': *xpos = 120; *ypos = 8; break; case '@': *xpos = 0; *ypos = 16; break; case 'A': *xpos = 8; *ypos = 16; break; case 'B': *xpos = 16; *ypos = 16; break; case 'C': *xpos = 24; *ypos = 16; break; case 'D': *xpos = 32; *ypos = 16; break; case 'E': *xpos = 40; *ypos = 16; break; case 'F': *xpos = 48; *ypos = 16; break; case 'G': *xpos = 56; *ypos = 16; break; case 'H': *xpos = 64; *ypos = 16; break; case 'I': *xpos = 72; *ypos = 16; break; case 'J': *xpos = 80; *ypos = 16; break; case 'K': *xpos = 88; *ypos = 16; break; case 'L': *xpos = 96; *ypos = 16; break; case 'M': *xpos = 104; *ypos = 16; break; case 'N': *xpos = 112; *ypos = 16; break; case 'O': *xpos = 120; *ypos = 16; break; case 'P': *xpos = 0; *ypos = 24; break; case 'Q': *xpos = 8; *ypos = 24; break; case 'R': *xpos = 16; *ypos = 24; break; case 'S': *xpos = 24; *ypos = 24; break; case 'T': *xpos = 32; *ypos = 24; break; case 'U': *xpos = 40; *ypos = 24; break; case 'V': *xpos = 48; *ypos = 24; break; case 'W': *xpos = 56; *ypos = 24; break; case 'X': *xpos = 64; *ypos = 24; break; case 'Y': *xpos = 72; *ypos = 24; break; case 'Z': *xpos = 80; *ypos = 24; break; case '[': *xpos = 88; *ypos = 24; break; case '\\': *xpos = 96; *ypos = 24; break; case ']': *xpos = 104; *ypos = 24; break; case '^': *xpos = 112; *ypos = 24; break; case '_': *xpos = 120; *ypos = 24; break; case '`': *xpos = 0; *ypos = 32; break; case 'a': *xpos = 8; *ypos = 32; break; case 'b': *xpos = 16; *ypos = 32; break; case 'c': *xpos = 24; *ypos = 32; break; case 'd': *xpos = 32; *ypos = 32; break; case 'e': *xpos = 40; *ypos = 32; break; case 'f': *xpos = 48; *ypos = 32; break; case 'g': *xpos = 56; *ypos = 32; break; case 'h': *xpos = 64; *ypos = 32; break; case 'i': *xpos = 72; *ypos = 32; break; case 'j': *xpos = 80; *ypos = 32; break; case 'k': *xpos = 88; *ypos = 32; break; case 'l': *xpos = 96; *ypos = 32; break; case 'm': *xpos = 104; *ypos = 32; break; case 'n': *xpos = 112; *ypos = 32; break; case 'o': *xpos = 120; *ypos = 32; break; case 'p': *xpos = 0; *ypos = 40; break; case 'q': *xpos = 8; *ypos = 40; break; case 'r': *xpos = 16; *ypos = 40; break; case 's': *xpos = 24; *ypos = 40; break; case 't': *xpos = 32; *ypos = 40; break; case 'u': *xpos = 40; *ypos = 40; break; case 'v': *xpos = 48; *ypos = 40; break; case 'w': *xpos = 56; *ypos = 40; break; case 'x': *xpos = 64; *ypos = 40; break; case 'y': *xpos = 72; *ypos = 40; break; case 'z': *xpos = 80; *ypos = 40; break; case '{': *xpos = 88; *ypos = 40; break; case '|': *xpos = 96; *ypos = 40; break; case '}': *xpos = 104; *ypos = 40; break; case '~': *xpos = 112; *ypos = 40; break; //case ' ': *xpos = 120; *ypos = 40; break; // Triangle default: *xpos = 0; *ypos = 0; break; } } void VideoRenderer::get_pixeldata(std::vector& pixels) { int w = dimensions.rw; int h = dimensions.rh; // Remove any on-screen text before grabbing pixel data int drawtext = osdtext.drawtext; bool drawtime = osdtext.drawtime; osdtext.drawtext = osdtext.drawtime = 0; ogl_render(); glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); // Put any text back on screen osdtext.drawtext = drawtext; osdtext.drawtime = drawtime; // Flip the image for (int line = 0; line != h / 2; ++line) { std::swap_ranges(pixels.begin() + sizeof(uint32_t) * w * line, pixels.begin() + sizeof(uint32_t) * w * (line + 1), pixels.begin() + sizeof(uint32_t) * w * (h - line - 1)); } } VideoManager::VideoManager(JGManager& jgm, SettingManager& setmgr) : jgm(jgm), setmgr(setmgr) { // Initialize video vidinfo = jg_get_videoinfo(); videobuf = (uint32_t*)calloc(1, vidinfo->hmax * vidinfo->wmax * sizeof(uint32_t)); vidinfo->buf = (void*)&videobuf[0]; } VideoManager::~VideoManager() { if (videobuf) { free(videobuf); } } void VideoManager::renderer_init() { if (setmgr.get_setting("v_renderer")->val) { renderer = new VideoRendererLegacy(setmgr); LogDriver::log(LogLevel::Debug, "Renderer: Legacy OpenGL"); } else { const std::string ver_core{"#version 140\n"}; const std::string ver_es{"#version 300 es\n"}; // Build a test vertex shader to check version compatibility auto shadertest = [](const std::string ver) -> bool { GLuint nullshader = glCreateShader(GL_VERTEX_SHADER); std::string nullsrc = ver + "void main() {}\n"; const char *c_str = nullsrc.c_str(); glShaderSource(nullshader, 1, &c_str, NULL); glCompileShader(nullshader); GLint err; glGetShaderiv(nullshader, GL_COMPILE_STATUS, &err); glDeleteShader(nullshader); return err != GL_FALSE; }; if (shadertest(ver_core)) { renderer = new VideoRendererModern(setmgr, ver_core); LogDriver::log(LogLevel::Debug, "Renderer: OpenGL 3.1"); } else if (shadertest(ver_es)) { renderer = new VideoRendererModern(setmgr, ver_es); LogDriver::log(LogLevel::Debug, "Renderer: OpenGL ES 3.0"); } else { renderer = new VideoRendererLegacy(setmgr); LogDriver::log(LogLevel::Warn, "Renderer: Legacy OpenGL (Fallback)"); } } } void VideoManager::renderer_deinit() { if (renderer) { delete renderer; renderer = nullptr; } } void VideoManager::render() { renderer->ogl_render(); } void VideoManager::get_dimensions(int *w, int *h) { *w = dimensions.rw; *h = dimensions.rh; } void VideoManager::set_dimensions() { // Try to guess the correct video parameters if no game is loaded if (!jgm.is_loaded()) { int t = jgm.get_setting("overscan_t")->val; int b = jgm.get_setting("overscan_b")->val; int l = jgm.get_setting("overscan_l")->val; int r = jgm.get_setting("overscan_r")->val; vidinfo->w = vidinfo->w - (l + r); vidinfo->h = vidinfo->h - (t + b); vidinfo->x = l; vidinfo->y = t; vidinfo->aspect = (vidinfo->w * 8.0 / 7.0) / vidinfo->h; // NTSC } set_aspect(); int scale = setmgr.get_setting("v_scale")->val; dimensions.ww = (aspect * vidinfo->h * scale) + 0.5; dimensions.wh = (vidinfo->h * scale) + 0.5; dimensions.rw = dimensions.ww; dimensions.rh = dimensions.wh; dimensions.dpiscale = 1.0; } void VideoManager::set_dpiscale(float dpiscale) { dimensions.dpiscale = dpiscale; } // FIXME maybe use std::tuple here void VideoManager::get_scaled_coords(int x, int y, int *xcoord, int *ycoord) { float xscale = dimensions.rw / (vidinfo->aspect * vidinfo->h) / dimensions.dpiscale; float yscale = dimensions.rh / vidinfo->h / dimensions.dpiscale; float xo = dimensions.xo / dimensions.dpiscale; float yo = dimensions.yo / dimensions.dpiscale; *xcoord = (x - xo) / ((vidinfo->aspect * vidinfo->h * xscale)/(float)vidinfo->w); *ycoord = ((y - yo) / yscale) + vidinfo->y; } void VideoManager::rehash(bool reset_shaders) { renderer->rehash(reset_shaders); set_aspect(); dimensions.rw = dimensions.ww; dimensions.rh = dimensions.wh; // Check which dimension to optimize if (dimensions.rh * aspect > dimensions.rw) { dimensions.rh = dimensions.rw / aspect + 0.5; } else if (dimensions.rw / aspect > dimensions.rh) { dimensions.rw = dimensions.rh * aspect + 0.5; } // Store X and Y offsets dimensions.xo = (dimensions.ww - dimensions.rw) / 2; dimensions.yo = (dimensions.wh - dimensions.rh) / 2; } void VideoManager::resize(int w, int h) { if (!renderer) { return; } dimensions.ww = w * dimensions.dpiscale; dimensions.wh = h * dimensions.dpiscale; rehash(); } void VideoManager::set_aspect() { switch (setmgr.get_setting("v_aspect")->val) { case 0: aspect = vidinfo->aspect; break; case 1: if (jgm.get_setting("ntsc_filter")->val) { aspect = 301/(double)vidinfo->h; } else { aspect = vidinfo->w/(double)vidinfo->h; } break; case 2: aspect = 4.0/3.0; break; case 3: aspect = 5.0/4.0; break; default: break; } } void VideoManager::screenshot(std::string& filename) { int w = dimensions.rw; int h = dimensions.rh; std::vector pixels(sizeof(uint32_t) * w * h); renderer->get_pixeldata(pixels); lodepng_encode32_file(filename.c_str(), pixels.data(), w, h); }