mirror of
https://github.com/0ldsk00l/nestopia.git
synced 2025-04-02 10:31:51 -04:00
929 lines
32 KiB
C++
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);
|
|
}
|