nestopia/source/fltkui/videomanager.cpp
2024-12-14 20:29:32 -06:00

929 lines
32 KiB
C++

/*
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 <filesystem>
#include <sstream>
#include <fstream>
#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<uint8_t>& 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<uint8_t> pixels(sizeof(uint32_t) * w * h);
renderer->get_pixeldata(pixels);
lodepng_encode32_file(filename.c_str(), pixels.data(), w, h);
}