FLTK: Modern GL renderer and shaders

This commit is contained in:
rdanbrook 2024-05-29 23:14:39 -06:00
parent a500d95bac
commit 0ab75febef
11 changed files with 1153 additions and 143 deletions

View file

@ -15,12 +15,14 @@ nestopia_CPPFLAGS = \
$(LIBARCHIVE_CFLAGS) \
$(SAMPLERATE_CFLAGS) \
$(SDL2_CFLAGS) \
$(LIBEPOXY_CFLAGS) \
$(FLTK_CFLAGS)
nestopia_LDADD = \
$(ZLIB_LIBS) \
$(LIBARCHIVE_LIBS) \
$(SAMPLERATE_LIBS) \
$(SDL2_LIBS) \
$(LIBEPOXY_LIBS) \
$(FLTK_LIBS)
################
@ -36,6 +38,9 @@ dist_palette_DATA = palettes/SONY_CXA2025AS_US.pal palettes/Royaltea.pal \
palettes/Magnum_FBX.pal palettes/PVM_Style_D93_FBX.pal \
palettes/Smooth_V2_FBX.pal
shaderdir = $(datadir)/nestopia/shaders
dist_shader_DATA = shaders/default.vs shaders/default.fs shaders/crtea.fs \
shaders/sharp-bilinear.fs
# freedesktop.org-specific files
desktopdir = $(datarootdir)/applications

View file

@ -78,6 +78,9 @@ PKG_CHECK_MODULES([SAMPLERATE], [samplerate])
dnl SDL2
PKG_CHECK_MODULES([SDL2], [sdl2])
dnl LibEpoxy
PKG_CHECK_MODULES([LIBEPOXY], [epoxy])
AC_CHECK_PROG(FLTKCONFIG,fltk-config,[fltk-config],[no])
test "$FLTKCONFIG" == "no" && AC_MSG_ERROR([Cannot find the fltk-config executable. Is FLTK installed?])

330
shaders/crtea.fs Normal file
View file

@ -0,0 +1,330 @@
/*
MIT License
CRTea - Configurable CRT Fragment Shader
Copyright (c) 2020-2022 Rupert Carmichael
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Based on Public Domain work by Timothy Lottes
precision highp float;
uniform sampler2D source;
uniform vec4 sourceSize;
uniform vec4 targetSize;
uniform int masktype; // Mask Type
uniform float maskstr; // Mask Strength
uniform float scanstr; // Scanline Strength
uniform float sharpness; // Pixel Sharpness
uniform float curve; // Screen Curvature
uniform float corner; // Screen Curvature
uniform float tcurve; // Trinitron Curve
in vec2 texCoord;
out vec4 FragColor;
//#define CRTS_DEBUG 1 // Define to see on/off split screen
//#define CRTS_2_TAP 1 // Faster very pixely 2-tap filter (off is 8)
#define CRTS_WARP 1 // Apply screen warp
#define INPUT_MASK (1.0 - maskstr)
#define INPUT_SCAN (0.5 + (0.5 * scanstr))
#define INPUT_SHRP (-1.0 * sharpness)
#define INPUT_X sourceSize.x
#define INPUT_Y sourceSize.y
#define CURVATURE curve
#define TRINITRON_CURVE tcurve
#define CORNER corner
#define CRT_GAMMA 2.4
#define CRTS_TONE 1 // Normalize mid-level and process color
#define CRTS_CONTRAST 0 // Process color - enable contrast control
#define CRTS_SATURATION 0 // Process color - enable saturation control
#define CrtsRcpF1(x) (1.0/(x))
#define CrtsSatF1(x) clamp((x),0.0,1.0)
// sRGB-Linear Conversions
float FromSrgb1(float c) {
return (c <= 0.04045) ? c * (1.0 / 12.92) :
pow(c * (1.0 / 1.055) + (0.055 / 1.055), CRT_GAMMA);
}
vec3 FromSrgb(vec3 c) {
return vec3(FromSrgb1(c.r), FromSrgb1(c.g), FromSrgb1(c.b));
}
float ToSrgb1(float c) {
return (c < 0.0031308 ? c * 12.92 : 1.055 * pow(c, 0.41666) - 0.055);
}
vec3 ToSrgb(vec3 c) {
return vec3(ToSrgb1(c.r), ToSrgb1(c.g), ToSrgb1(c.b));
}
// Fetch the input image colour
vec3 CrtsFetch(vec2 uv) {
// Scale to get native texels in the image
uv *= vec2(INPUT_X, INPUT_Y) / sourceSize.xy;
return FromSrgb(texture(source, uv.xy, -16.0).rgb);
}
// Get the maximum of 3 floats
float CrtsMax3F1(float a, float b, float c) {
return max(a, max(b, c));
}
// Tonal Control
vec4 CrtsTone(float contrast, float saturation, float thin, float mask) {
if (masktype == 0) mask = 1.0;
if (masktype == 1) {
// Normal R mask is {1.0,mask,mask}
// LITE R mask is {mask,1.0,1.0}
mask = 0.5 + mask * 0.5;
}
vec4 ret;
float midOut = 0.18 / ((1.5 - thin) * (0.5 * mask + 0.5));
float pMidIn = pow(0.18, contrast);
ret.x = contrast;
ret.y = ((-pMidIn) + midOut) / ((1.0 - pMidIn) * midOut);
ret.z = ((-pMidIn) * midOut + pMidIn) / (midOut * (-pMidIn) + midOut);
ret.w = contrast + saturation;
return ret;
}
// Apply the mask
vec3 CrtsMask(vec2 pos, float dark) {
if (masktype == 0) { // Scanlines (No Mask)
return vec3(1.0, 1.0, 1.0);
}
if (masktype == 1) { // Aperture Grille Lite
vec3 m = vec3(1.0, 1.0, 1.0);
float x = fract(pos.x * (1.0 / 3.0));
if (x < (1.0 / 3.0)) m.r = dark;
else if (x < (2.0 / 3.0)) m.g = dark;
else m.b = dark;
return m;
}
if (masktype == 2) { // Aperture Grille
vec3 m = vec3(dark, dark, dark);
float x = fract(pos.x * (1.0 / 3.0));
if (x < (1.0 / 3.0)) m.r = 1.0;
else if (x < (2.0 / 3.0)) m.g = 1.0;
else m.b = 1.0;
return m;
}
if (masktype == 3) { // Shadow Mask
pos.x += pos.y * 2.9999;
vec3 m = vec3(dark, dark, dark);
float x = fract(pos.x * (1.0 / 6.0));
if (x < (1.0 / 3.0)) m.r = 1.0;
else if (x < (2.0 / 3.0)) m.g = 1.0;
else m.b = 1.0;
return m;
}
}
// The filter function itself
// SV_POSITION, fragCoord.xy
// sourceSize / targetSize (in pixels)
// 0.5 * sourceSize (in pixels)
// 1.0 / sourceSize (in pixels)
// 1.0 / targetSize (in pixels)
// 2.0 / targetSize (in pixels)
// Warp scanlines but not phosphor mask
// Tonal curve parameters generated by CrtsTone()
vec3 CrtsFilter(vec2 ipos, vec2 inputSizeDivOutputSize, vec2 halfInputSize,
vec2 rcpInputSize, vec2 rcpOutputSize, vec2 twoDivOutputSize,
vec2 warp, vec4 tone) {
#ifdef CRTS_DEBUG
vec2 uv = ipos * rcpOutputSize;
// Show second half processed, and first half un-processed
if (uv.x < 0.5) {
// Force nearest to get squares
uv *= 1.0 / rcpInputSize;
uv = floor(uv) + vec2(0.5, 0.5);
uv *= rcpInputSize;
vec3 color = CrtsFetch(uv);
return color;
}
#endif
// Optional apply warp
vec2 pos;
#ifdef CRTS_WARP
// Convert to {-1 to 1} range
pos = ipos * twoDivOutputSize - vec2(1.0, 1.0);
// Distort pushes image outside {-1 to 1} range
pos *= vec2(1.0 + (pos.y * pos.y) * warp.x, 1.0 + (pos.x * pos.x) * warp.y);
// TODO: Vignette needs optimization
float vin = (1.0 -
((1.0 - CrtsSatF1(pos.x * pos.x)) * (1.0 - CrtsSatF1(pos.y * pos.y)))) *
(0.998 + (0.001 * CORNER));
vin = CrtsSatF1((-vin) * sourceSize.y + sourceSize.y);
// Leave in {0 to inputSize}
pos = pos * halfInputSize + halfInputSize;
#else
pos = ipos * inputSizeDivOutputSize;
#endif
// Snap to center of first scanline
float y0 = floor(pos.y - 0.5) + 0.5;
#ifdef CRTS_2_TAP
// Using Inigo's "Improved Texture Interpolation"
// http://iquilezles.org/www/articles/texture/texture.htm
pos.x += 0.5;
float xi = floor(pos.x);
float xf = pos.x - xi;
xf = xf * xf * xf * (xf * (xf * 6.0 - 15.0) + 10.0);
float x0 = xi + xf - 0.5;
vec2 p = vec2(x0 * rcpInputSize.x, y0 * rcpInputSize.y);
// Coordinate adjusted bilinear fetch from 2 nearest scanlines
vec3 colA = CrtsFetch(p);
p.y += rcpInputSize.y;
vec3 colB = CrtsFetch(p);
#else
// Snap to center of one of four pixels
float x0 = floor(pos.x - 1.5) + 0.5;
// Inital UV position
vec2 p = vec2(x0 * rcpInputSize.x, y0 * rcpInputSize.y);
// Fetch 4 nearest texels from 2 nearest scanlines
vec3 colA0 = CrtsFetch(p);
p.x += rcpInputSize.x;
vec3 colA1 = CrtsFetch(p);
p.x += rcpInputSize.x;
vec3 colA2 = CrtsFetch(p);
p.x += rcpInputSize.x;
vec3 colA3 = CrtsFetch(p);
p.y += rcpInputSize.y;
vec3 colB3 = CrtsFetch(p);
p.x -= rcpInputSize.x;
vec3 colB2 = CrtsFetch(p);
p.x -= rcpInputSize.x;
vec3 colB1 = CrtsFetch(p);
p.x -= rcpInputSize.x;
vec3 colB0 = CrtsFetch(p);
#endif
// Vertical filter
// Scanline intensity is using sine wave
// Easy filter window and integral used later in exposure
float off = pos.y - y0;
float pi2 = 6.28318530717958;
float hlf = 0.5;
float scanA = cos(min(0.5, off * INPUT_SCAN) * pi2) * hlf + hlf;
float scanB = cos(min(0.5, (-off) * INPUT_SCAN + INPUT_SCAN) * pi2) *
hlf + hlf;
#ifdef CRTS_2_TAP
#ifdef CRTS_WARP
// Get rid of wrong pixels on edge
scanA *= vin;
scanB *= vin;
#endif
// Apply vertical filter
vec3 color = (colA * scanA) + (colB * scanB);
#else
// Horizontal kernel is simple gaussian filter
float off0 = pos.x - x0;
float off1 = off0 - 1.0;
float off2 = off0 - 2.0;
float off3 = off0 - 3.0;
float pix0 = exp2(INPUT_SHRP * off0 * off0);
float pix1 = exp2(INPUT_SHRP * off1 * off1);
float pix2 = exp2(INPUT_SHRP * off2 * off2);
float pix3 = exp2(INPUT_SHRP * off3 * off3);
float pixT = CrtsRcpF1(pix0 + pix1 + pix2 + pix3);
#ifdef CRTS_WARP
// Get rid of wrong pixels on edge
pixT *= vin;
#endif
scanA *= pixT;
scanB *= pixT;
// Apply horizontal and vertical filters
vec3 color =
(colA0 * pix0 + colA1 * pix1 + colA2 * pix2 + colA3 * pix3) * scanA +
(colB0 * pix0 + colB1 * pix1 + colB2 * pix2 + colB3 * pix3) * scanB;
#endif
// Apply phosphor mask
color *= CrtsMask(gl_FragCoord.xy, INPUT_MASK);
// Optional color processing
#ifdef CRTS_TONE
// Tonal control, start by protecting from /0
float peak = max(1.0 / (256.0 * 65536.0),
CrtsMax3F1(color.r, color.g, color.b));
// Compute the ratios of {R,G,B}
vec3 ratio = color * CrtsRcpF1(peak);
// Apply tonal curve to peak value
#ifdef CRTS_CONTRAST
peak = pow(peak, tone.x);
#endif
peak = peak * CrtsRcpF1(peak * tone.y + tone.z);
// Apply saturation
#ifdef CRTS_SATURATION
ratio = pow(ratio, vec3(tone.w, tone.w, tone.w));
#endif
// Reconstruct color
return ratio * peak;
#else
return color;
#endif
}
void main() {
vec2 warp_factor;
warp_factor.x = CURVATURE;
warp_factor.y = (3.0 / 4.0) * warp_factor.x; // assume 4:3 aspect
warp_factor.x *= (1.0 - TRINITRON_CURVE);
FragColor.rgb = CrtsFilter(texCoord.xy * targetSize.xy,
sourceSize.xy * targetSize.zw,
sourceSize.xy * vec2(0.5, 0.5),
sourceSize.zw,
targetSize.zw,
2.0 * targetSize.zw,
warp_factor,
CrtsTone(1.0, 0.0, INPUT_SCAN, INPUT_MASK));
// Output non-linear color
FragColor.rgb = ToSrgb(FragColor.rgb);
}

35
shaders/default.fs Normal file
View file

@ -0,0 +1,35 @@
/*
MIT License
Copyright (c) 2020-2022 Rupert Carmichael
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
precision highp float;
in vec2 texCoord;
out vec4 fragColor;
uniform sampler2D source;
void main() {
fragColor = texture(source, texCoord);
}

33
shaders/default.vs Normal file
View file

@ -0,0 +1,33 @@
/*
MIT License
Copyright (c) 2020-2022 Rupert Carmichael
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
in vec2 position;
in vec2 vtxCoord;
out vec2 texCoord;
void main() {
texCoord = vtxCoord;
gl_Position = vec4(position, 0.0, 1.0);
}

54
shaders/sharp-bilinear.fs Normal file
View file

@ -0,0 +1,54 @@
/*
MIT License
Sharp Bilinear (Modified)
Copyright (c) 2020-2022 Rupert Carmichael
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Based on Public Domain work by Themaister
precision highp float;
uniform sampler2D source;
uniform vec4 sourceSize;
uniform vec4 targetSize;
in vec2 texCoord;
out vec4 fragColor;
void main() {
float prescale = floor(targetSize.y / sourceSize.y + 0.01);
if (prescale == 0.0) prescale = 1.0;
vec2 texel = texCoord.xy * sourceSize.xy;
vec2 texel_floored = floor(texel);
vec2 s = fract(texel);
float region_range = 0.5 - 0.5 / prescale;
/* Figure out where in the texel to sample to get correct pre-scaled
bilinear. Uses the hardware bilinear interpolator to avoid having to
sample 4 times manually.
*/
vec2 center_dist = s - 0.5;
vec2 f = (center_dist - clamp(center_dist, -region_range, region_range)) *
prescale + 0.5;
vec2 mod_texel = texel_floored + f;
fragColor = texture(source, mod_texel / sourceSize.xy);
}

View file

@ -27,6 +27,8 @@
#include <iostream>
#include <set>
#include <epoxy/gl.h>
#include <FL/Fl.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Double_Window.H>
@ -125,7 +127,7 @@ static void fltkui_load_file(const char *filename) {
jgm->load_game(arcname.c_str(), game);
}
else {
VideoManager::text_print("No valid files in archive", 8, 212, 2, true);
LogDriver::log(LogLevel::OSD, "No valid files in archive");
}
}
else {
@ -367,7 +369,7 @@ void NstGlArea::resize(int x, int y, int w, int h) {
}
void FltkUi::rehash() {
videomgr->rehash();
videomgr->rehash(true);
audiomgr->rehash();
}
@ -443,6 +445,7 @@ static void fltkui_about(Fl_Widget* w, void* userdata) {
}
static void quit_cb(Fl_Widget* w, void* userdata) {
videomgr->renderer_deinit();
nstwin->hide();
}
@ -469,7 +472,7 @@ int NstWindow::handle(int e) {
}
void NstGlArea::draw() {
videomgr->ogl_render();
videomgr->render();
}
int NstGlArea::handle(int e) {
@ -630,11 +633,12 @@ int main(int argc, char *argv[]) {
nstwin->label("Nestopia UE");
nstwin->show();
menubar->show();
glarea->make_current();
glarea->show();
Fl::check();
videomgr->renderer_init();
videomgr->ogl_init();
Fl::check();
// Load a rom from the command line
if (argc > 1 && argv[argc - 1][0] != '-') {

View file

@ -28,7 +28,7 @@
void LogDriver::log(LogLevel level, std::string text) {
if (level == LogLevel::OSD) {
VideoManager::text_print(text.c_str(), 8, 212, 2, true);
VideoRenderer::text_print(text.c_str(), 8, 212, 2, true);
}
else if (level == LogLevel::Info) {
std::cout << text << std::endl;
@ -52,7 +52,7 @@ void LogDriver::jg_log(int level, const char *fmt, ...) {
FILE *fout = level == 1 ? stdout : stderr;
if (level == JG_LOG_SCR) {
VideoManager::text_print(buffer, 8, 212, 2, true);
VideoRenderer::text_print(buffer, 8, 212, 2, true);
return;
}
@ -60,6 +60,6 @@ void LogDriver::jg_log(int level, const char *fmt, ...) {
fflush(fout);
if (level == JG_LOG_ERR) {
VideoManager::text_print(buffer, 8, 212, 2, true);
VideoRenderer::text_print(buffer, 8, 212, 2, true);
}
}

View file

@ -37,21 +37,26 @@
namespace {
jg_setting_t fe_settings[] = {
{ "v_scale", "Initial Window Scale",
"N = Window scale factor at startup",
"Set the window's initial scale factor (multiple of NES resolution)",
2, 1, 16, FLAG_FRONTEND | JG_SETTING_RESTART
{ "v_renderer", "Video Renderer",
"0 = Modern, 1 = Legacy",
"Use Modern (Core Profile) or Legacy (Compatibility Profile) OpenGL",
0, 0, 1, FLAG_FRONTEND | JG_SETTING_RESTART
},
{ "v_linearfilter", "Linear Filter",
"0 = Disable, 1 = Enable",
"Use the GPU's built-in linear filter for video output",
1, 0, 1, FLAG_FRONTEND
{ "v_postproc", "Post-processing",
"0 = Nearest Neighbour, 1 = Linear, 2 = Sharp Bilinear, 3 = CRT",
"Select a video post-processing effect",
2, 0, 3, FLAG_FRONTEND
},
{ "v_aspect", "Aspect Ratio",
"0 = TV Correct, 1 = 1:1, 2 = 4:3",
"Set the aspect ratio to the correct TV aspect, 1:1 (square pixels), or 4:3",
0, 0, 2, FLAG_FRONTEND
},
{ "v_scale", "Initial Window Scale",
"N = Window scale factor at startup",
"Set the window's initial scale factor (multiple of NES resolution)",
2, 1, 16, FLAG_FRONTEND | JG_SETTING_RESTART
},
{ "a_rsqual", "Audio Resampler Quality",
"0 = Sinc (Best), 1 = Sinc (Medium), 2 = Sinc (Fast), 3 = Zero Order Hold, 4 = Linear",
"Set the frontend's audio resampling quality. Use Sinc unless you are on extremely weak hardware.",
@ -72,6 +77,42 @@ jg_setting_t fe_settings[] = {
"Hide the crosshair when a Zapper is present",
0, 0, 1, FLAG_FRONTEND
},
{ "s_crtmasktype", "CRT Mask Type",
"0 = No Mask, 1 = Aperture Grille Lite, 2 = Aperture Grille, "
"3 = Shadow Mask",
"",
1, 0, 3, FLAG_FRONTEND
},
{ "s_crtmaskstr", "CRT Mask Strength",
"N = CRT Mask Strength",
"",
5, 0, 10, FLAG_FRONTEND
},
{ "s_crtscanstr", "CRT Scanline Strength",
"N = CRT Scanline Strength",
"",
6, 0, 10, FLAG_FRONTEND
},
{ "s_crtsharp", "CRT Sharpness",
"N = CRT Sharpness",
"",
4, 0, 10, FLAG_FRONTEND
},
{ "s_crtcurve", "CRT Curve",
"N = CRT Curvature",
"",
3, 0, 10, FLAG_FRONTEND
},
{ "s_crtcorner", "CRT Corner",
"N = CRT Corner",
"",
3, 0, 10, FLAG_FRONTEND
},
{ "s_crttcurve", "CRT Trinitron Curve",
"N = CRT Trinitron Curvature",
"",
10, 0, 10, FLAG_FRONTEND
},
};
jg_setting_t nullsetting;

View file

@ -22,16 +22,20 @@
#include <cstdint>
#include <cstring>
#include <FL/gl.h>
#include <filesystem>
#include "videomanager.h"
#include "logdriver.h"
#include "font.h"
#include "png.h"
namespace {
jg_videoinfo_t *vidinfo;
uint32_t *videobuf;
struct osdtext {
int xpos;
int ypos;
@ -42,77 +46,22 @@ struct osdtext {
bool bg;
} osdtext;
jg_videoinfo_t *vidinfo;
uint32_t *videobuf;
// 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) {
}
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];
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;
}
VideoManager::~VideoManager() {
ogl_deinit();
if (videobuf) {
free(videobuf);
}
}
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;
default: break;
}
}
void VideoManager::rehash() {
GLuint filter = setmgr.get_setting("v_linearfilter")->val ? GL_LINEAR : GL_NEAREST;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
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::ogl_init() {
VideoRendererLegacy::VideoRendererLegacy(SettingManager& setmgr)
: VideoRenderer(setmgr) {
// Generate texture for raw game output
glEnable(GL_TEXTURE_2D);
glGenTextures(1, &gl_texture_id);
@ -124,7 +73,7 @@ void VideoManager::ogl_init() {
vidinfo->wmax, vidinfo->hmax, 0, GL_BGRA, GL_UNSIGNED_BYTE,
videobuf);
GLuint filter = setmgr.get_setting("v_linearfilter")->val ? GL_LINEAR : GL_NEAREST;
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);
@ -132,25 +81,103 @@ void VideoManager::ogl_init() {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
}
void VideoManager::ogl_deinit() {
// Deinitialize OpenGL
VideoRendererLegacy::~VideoRendererLegacy() {
if (gl_texture_id) {
glDeleteTextures(1, &gl_texture_id);
}
}
void VideoManager::ogl_render() {
// OSD Text
if (osdtext.drawtext) {
text_draw(osdtext.textbuf, osdtext.xpos, osdtext.ypos, osdtext.bg);
osdtext.drawtext--;
VideoRendererModern::VideoRendererModern(SettingManager& setmgr)
: VideoRenderer(setmgr) {
// 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);
shader_setup();
ogl_refresh();
}
VideoRendererModern::~VideoRendererModern() {
if (gl_texture_id) {
glDeleteTextures(1, &gl_texture_id);
}
if (osdtext.drawtime) {
text_draw(osdtext.timebuf, 208, 218, false);
if (framebuf) {
glDeleteFramebuffers(1, &framebuf);
}
//jgrf_video_gl_refresh(); // Check for changes
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);
@ -166,6 +193,20 @@ void VideoManager::ogl_render() {
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);
@ -205,25 +246,333 @@ void VideoManager::ogl_render() {
glEnd();
}
void VideoManager::get_dimensions(int *w, int *h) {
*w = dimensions.rw;
*h = dimensions.rh;
void VideoRendererModern::rehash(bool reset_shaders) {
if (reset_shaders) {
shader_setup();
}
}
// 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;
// Load a shader source file into memory
const char* VideoRendererModern::shader_load(const char *filename) {
FILE *file = fopen(filename, "rb");
//if (!file)
// jgrf_log(JG_LOG_ERR, "Could not open shader file, exiting...\n");
// Get the size of the shader source file
fseek(file, 0, SEEK_END);
size_t size = ftell(file);
rewind(file);
// Allocate memory to store the shader source including version string
GLchar *src = (GLchar*)calloc(size + SIZE_GLSLVER, sizeof(GLchar));
//if (!src)
// jgrf_log(JG_LOG_ERR, "Could not allocate memory, exiting...\n");
// Allocate memory for the shader source without version string
GLchar *shader = (GLchar*)calloc(size + 1, sizeof(GLchar));
// Write version string into the buffer for the full shader source
//snprintf(src, SIZE_GLSLVER, "%s", settings[VIDEO_API].val ?
// "#version 300 es\n" : "#version 330 core\n");
snprintf(src, SIZE_GLSLVER, "%s", "#version 330 core\n");
if (!shader || !fread(shader, size, sizeof(GLchar), file)) {
free(src);
fclose(file);
//jgrf_log(JG_LOG_ERR, "Could not open shader file, exiting...\n");
return NULL;
}
// Close file handle after reading
fclose(file);
// Append shader source to version string
src = strncat(src, shader, size + SIZE_GLSLVER);
// Free the shader source without version string
free(shader);
return src;
}
void VideoManager::resize(int w, int h) {
dimensions.ww = w;
dimensions.wh = h;
rehash();
GLuint VideoRendererModern::shader_create(const char *vs, const char *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/";
}
std::string vspath{shaderpath + std::string(vs)};
std::string fspath{shaderpath + std::string(fs)};
const GLchar *vsrc = shader_load(vspath.c_str());
const GLchar *fsrc = shader_load(fspath.c_str());
GLint err;
// Create and compile the vertex shader
GLuint vshader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vshader, 1, &vsrc, NULL);
glCompileShader(vshader);
// Test if the shader compiled
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, "Vertex shader: " + std::string(shaderlog));
}
// Free the allocated memory for shader sources
free((GLchar*)vsrc);
free((GLchar*)fsrc);
// 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;
}
}
// 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 video_screenshot_flip(unsigned char *pixels, int width, int height, int bytes) {
@ -266,7 +615,7 @@ void video_screenshot(const char* filename) {
free(pixels);
}*/
void VideoManager::text_print(const char *text, int xpos, int ypos, int seconds, bool bg) {
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;
@ -274,12 +623,12 @@ void VideoManager::text_print(const char *text, int xpos, int ypos, int seconds,
osdtext.bg = bg;
}
void VideoManager::text_print_time(const char *timebuf, bool drawtime) {
void VideoRenderer::text_print_time(const char *timebuf, bool drawtime) {
snprintf(osdtext.timebuf, sizeof(osdtext.timebuf), "%s", timebuf);
osdtext.drawtime = drawtime;
}
void VideoManager::text_draw(const char *text, int xpos, int ypos, bool bg) {
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
@ -327,7 +676,7 @@ void VideoManager::text_draw(const char *text, int xpos, int ypos, bool bg) {
}
}
void VideoManager::text_match(const char *text, int *xpos, int *ypos, int strpos) {
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;
@ -429,3 +778,108 @@ void VideoManager::text_match(const char *text, int *xpos, int *ypos, int strpos
default: *xpos = 0; *ypos = 0; break;
}
}
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];
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;
}
VideoManager::~VideoManager() {
if (videobuf) {
free(videobuf);
}
}
void VideoManager::renderer_init() {
if (setmgr.get_setting("v_renderer")->val) {
renderer = new VideoRendererLegacy(setmgr);
}
else {
renderer = new VideoRendererModern(setmgr);
}
}
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;
}
// 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) {
dimensions.ww = w;
dimensions.wh = h;
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;
default: break;
}
}

View file

@ -1,35 +1,31 @@
#pragma once
#include <epoxy/gl.h>
#include "setmanager.h"
#include "jgmanager.h"
class VideoManager {
class VideoRenderer {
public:
VideoManager() = delete;
VideoManager(JGManager& jgm, SettingManager& setmgr);
~VideoManager();
VideoRenderer() = delete;
VideoRenderer(SettingManager& setmgr);
virtual ~VideoRenderer() {};
void get_dimensions(int *w, int *h);
void get_scaled_coords(int x, int y, int *xcoord, int *ycoord);
virtual void ogl_render() = 0;
virtual void ogl_refresh() = 0;
void rehash();
void resize(int w, int h);
void set_aspect();
void ogl_init();
void ogl_deinit();
void ogl_render();
virtual void rehash(bool reset_shaders = false) = 0;
static void text_print(const char *text, int xpos, int ypos, int seconds, bool bg);
static void text_print_time(const char *timebuf, bool drawtime);
protected:
static void text_draw(const char *text, int xpos, int ypos, bool bg);
static void text_print_time(const char *timebuf, bool drawtime);
static void text_match(const char *text, int *xpos, int *ypos, int strpos);
private:
JGManager &jgm;
SettingManager &setmgr;
double aspect{1.0};
GLuint gl_texture_id{0};
// Triangle and Texture vertices
float vertices[16]{
@ -42,14 +38,69 @@ private:
1.0, 0.0, // Texture 4 (X, Y) Right Top
1.0, 1.0, // Texture 3 (X, Y) Right Bottom
};
unsigned int gl_texture_id{0};
// Dimensions
struct _dimensions {
int ww; int wh;
float rw; float rh;
float xo; float yo;
float dpiscale;
} dimensions;
};
class VideoRendererLegacy : public VideoRenderer {
public:
VideoRendererLegacy() = delete;
VideoRendererLegacy(SettingManager& setmgr);
~VideoRendererLegacy() override;
void ogl_render() override;
void ogl_refresh() override;
void rehash(bool reset_shaders = false) override;
};
class VideoRendererModern : public VideoRenderer {
public:
VideoRendererModern() = delete;
VideoRendererModern(SettingManager& setmgr);
~VideoRendererModern() override;
void ogl_render() override;
void ogl_refresh() override;
void rehash(bool reset_shaders = false) override;
private:
GLuint shader_create(const char *vs, const char *fs);
void shader_setup(void);
const char* shader_load(const char *filename);
static constexpr size_t NUMPASSES = 2;
#define SIZE_GLSLVER 20
GLuint vao[NUMPASSES];
GLuint vbo[NUMPASSES];
GLuint shaderprog[NUMPASSES];
GLuint tex[NUMPASSES];
GLuint texfilter[NUMPASSES];
GLuint framebuf; // Framebuffer for rendering offscreen
};
class VideoManager {
public:
VideoManager() = delete;
VideoManager(JGManager& jgm, SettingManager& setmgr);
~VideoManager();
void get_dimensions(int *w, int *h);
void get_scaled_coords(int x, int y, int *xcoord, int *ycoord);
void rehash(bool reset_shaders = false);
void resize(int w, int h);
void set_aspect();
void renderer_init();
void renderer_deinit();
void render();
private:
JGManager &jgm;
SettingManager &setmgr;
VideoRenderer *renderer{nullptr};
double aspect{1.0};
};