From b42ab2fcb36d0d7119a21b6add09ea2906cf093a Mon Sep 17 00:00:00 2001 From: Tim Allen Date: Sun, 8 Nov 2015 19:54:42 +1100 Subject: [PATCH] Update to v095r02 release. byuu says: Aspect correction is fixed now. Works way better than in v095 official. It's still force-enabled in fullscreen mode. The idea of disabling it is that it looks bad at 2x scale. But when you're fullscreen with a minimum of 4x scale, there's no reason not to enable it. It won't turn on at all for GB/C/A anymore. And I dropped the cute attempt at making the aspect prettier on 2560x1600 monitors, so it'll be the stock 8:7 across the board now for S/NES. Also, the aspect correction will affect the window even when a game's not loaded now, so the size won't bounce around as you change games in windowed mode between GB/C/A and S/NES. ... I also enhanced the ruby/glx driver. It won't crash if OpenGL 3.2 isn't available anymore (fails safely ... had to capture the Xlib error handler to suppress that), and it defaults to the MESA glXSwapInterval before the SGI version. Because apparently the MESA version defines the SGI version, but makes it a no-op. What. The. Fuck. right? But whatever, reordering the enumerations fixes the ability to toggle Vsync on AMD GPUs now. ... Video shaders are back again. If you are using the OpenGL driver, you'll see a "Video Shaders" menu beneath the "Video Filters" menu (couldn't merge it with the filters due to hiro now constructing menu ordering inside the header files. This works fine though.) You want either "higan.exe" + "Video Shaders/" or "~/.local/bin/tomoko" + "~/.local/tomoko/Video Shaders/" --- emulator/emulator.hpp | 8 +- nall/beat/archive.hpp | 12 +-- ruby/video/glx.cpp | 47 ++++++--- shaders/Curvature.shader/curvature.fs | 21 ++++ shaders/Curvature.shader/manifest.bml | 4 + .../Edge Detection.shader/edge-detection.fs | 25 +++++ shaders/Edge Detection.shader/manifest.bml | 4 + shaders/Makefile | 5 + shaders/Scanline.shader/manifest.bml | 4 + shaders/Scanline.shader/scanline.fs | 20 ++++ target-tomoko/configuration/configuration.cpp | 1 + target-tomoko/configuration/configuration.hpp | 1 + target-tomoko/presentation/presentation.cpp | 97 +++++++++++-------- target-tomoko/presentation/presentation.hpp | 4 + target-tomoko/program/utility.cpp | 9 +- 15 files changed, 190 insertions(+), 72 deletions(-) create mode 100644 shaders/Curvature.shader/curvature.fs create mode 100644 shaders/Curvature.shader/manifest.bml create mode 100644 shaders/Edge Detection.shader/edge-detection.fs create mode 100644 shaders/Edge Detection.shader/manifest.bml create mode 100644 shaders/Makefile create mode 100644 shaders/Scanline.shader/manifest.bml create mode 100644 shaders/Scanline.shader/scanline.fs diff --git a/emulator/emulator.hpp b/emulator/emulator.hpp index 88801df3..6d06acf3 100644 --- a/emulator/emulator.hpp +++ b/emulator/emulator.hpp @@ -8,7 +8,7 @@ using namespace nall; namespace Emulator { static const string Name = "higan"; - static const string Version = "095.01"; + static const string Version = "095.02"; static const string Author = "byuu"; static const string License = "GPLv3"; static const string Website = "http://byuu.org/"; @@ -41,9 +41,9 @@ template struct hook { hook() {} hook(const hook& hook) { callback = hook.callback; } hook(void* function) { callback = function; } - hook(R (*function)(P...)) { callback = function; } - template hook(R (C::*function)(P...), C* object) { callback = {function, object}; } - template hook(R (C::*function)(P...) const, C* object) { callback = {function, object}; } + hook(auto (*function)(P...) -> R) { callback = function; } + template hook(auto (C::*function)(P...) -> R, C* object) { callback = {function, object}; } + template hook(auto (C::*function)(P...) const -> R, C* object) { callback = {function, object}; } template hook(const L& function) { callback = function; } auto operator=(const hook& source) -> hook& { callback = source.callback; return *this; } diff --git a/nall/beat/archive.hpp b/nall/beat/archive.hpp index 8b6d25a1..a515ef5d 100644 --- a/nall/beat/archive.hpp +++ b/nall/beat/archive.hpp @@ -30,10 +30,9 @@ auto Archive::create(const string& beatname, const string& pathname, const strin for(auto& name : contents) { string location{pathname, name}; bool directory = name.endsWith("/"); - bool readable = file_system_object::readable(location); bool writable = file_system_object::writable(location); bool executable = file_system_object::executable(location); - unsigned info = directory << 0 | readable << 1 | writable << 2 | executable << 3 | (name.rtrim("/").size() - 1) << 4; + unsigned info = directory << 0 | writable << 1 | executable << 2 | (name.rtrim("/").size() - 1) << 3; beat.writevu(info); beat.writes(name); @@ -68,14 +67,13 @@ auto Archive::unpack(const string& beatname, const string& pathname) -> bool { directory::create(pathname); while(beat.offset() < beat.size() - 4) { auto info = beat.readvu(); - auto name = beat.reads((info >> 4) + 1); + auto name = beat.reads((info >> 3) + 1); if(name.find("\\") || name.find("../")) return false; //block path exploits string location{pathname, name}; bool directory = info & 1; - bool readable = info & 2; - bool writable = info & 4; - bool executable = info & 8; + bool writable = info & 2; + bool executable = info & 4; if(directory) { if(!nall::directory::create(location)) return false; @@ -103,7 +101,7 @@ auto Archive::extract(const string& beatname, const string& filename) -> vector< while(beat.offset() < beat.size() - 4) { auto info = beat.readvu(); - auto name = beat.reads((info >> 4) + 1); + auto name = beat.reads((info >> 3) + 1); if(info & 1) continue; //ignore directories auto size = beat.readvu(); diff --git a/ruby/video/glx.cpp b/ruby/video/glx.cpp index 1f719470..81530c1f 100644 --- a/ruby/video/glx.cpp +++ b/ruby/video/glx.cpp @@ -3,10 +3,13 @@ #define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091 #define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092 +auto VideoGLX_X11ErrorHandler(Display*, XErrorEvent*) -> int { + return 0; //suppress errors +} + struct VideoGLX : Video, OpenGL { ~VideoGLX() { term(); } - auto (*glXCreateContextAttribs)(Display*, GLXFBConfig, GLXContext, signed, const signed*) -> GLXContext = nullptr; auto (*glXSwapInterval)(signed) -> signed = nullptr; Display* display = nullptr; @@ -17,8 +20,8 @@ struct VideoGLX : Video, OpenGL { GLXWindow glxwindow = 0; struct { - signed version_major = 0; - signed version_minor = 0; + signed versionMajor = 0; + signed versionMinor = 0; bool doubleBuffer = false; bool isDirect = false; } glx; @@ -127,12 +130,12 @@ struct VideoGLX : Video, OpenGL { display = XOpenDisplay(0); screen = DefaultScreen(display); - glXQueryVersion(display, &glx.version_major, &glx.version_minor); //require GLX 1.2+ API - if(glx.version_major < 1 || (glx.version_major == 1 && glx.version_minor < 2)) return false; + glXQueryVersion(display, &glx.versionMajor, &glx.versionMinor); + if(glx.versionMajor < 1 || (glx.versionMajor == 1 && glx.versionMinor < 2)) return false; - XWindowAttributes window_attributes; - XGetWindowAttributes(display, settings.handle, &window_attributes); + XWindowAttributes windowAttributes; + XGetWindowAttributes(display, settings.handle, &windowAttributes); //let GLX determine the best Visual to use for GL output; provide a few hints //note: some video drivers will override double buffering attribute @@ -146,7 +149,7 @@ struct VideoGLX : Video, OpenGL { None }; - signed fbCount; + signed fbCount = 0; GLXFBConfig* fbConfig = glXChooseFBConfig(display, screen, attributeList, &fbCount); if(fbCount == 0) return false; @@ -161,7 +164,7 @@ struct VideoGLX : Video, OpenGL { attributes.colormap = colormap; attributes.border_pixel = 0; xwindow = XCreateWindow(display, /* parent = */ settings.handle, - /* x = */ 0, /* y = */ 0, window_attributes.width, window_attributes.height, + /* x = */ 0, /* y = */ 0, windowAttributes.width, windowAttributes.height, /* border_width = */ 0, vi->depth, InputOutput, vi->visual, CWColormap | CWBorderPixel, &attributes); XSetWindowBackground(display, xwindow, /* color = */ 0); @@ -177,27 +180,39 @@ struct VideoGLX : Video, OpenGL { glxcontext = glXCreateContext(display, vi, /* sharelist = */ 0, /* direct = */ GL_TRUE); glXMakeCurrent(display, glxwindow = xwindow, glxcontext); - glXCreateContextAttribs = (GLXContext (*)(Display*, GLXFBConfig, GLXContext, signed, const signed*))glGetProcAddress("glXCreateContextAttribsARB"); - glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalSGI"); + //glXSwapInterval is used to toggle Vsync + //note that the ordering is very important! MESA declares SGI, but the SGI function does nothing + glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalEXT"); if(!glXSwapInterval) glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalMESA"); + if(!glXSwapInterval) glXSwapInterval = (signed (*)(signed))glGetProcAddress("glXSwapIntervalSGI"); - if(glXCreateContextAttribs) { + if(auto glXCreateContextAttribs = (auto (*)(Display*, GLXFBConfig, GLXContext, signed, const signed*) -> GLXContext)glGetProcAddress("glXCreateContextAttribsARB")) { signed attributes[] = { GLX_CONTEXT_MAJOR_VERSION_ARB, 3, GLX_CONTEXT_MINOR_VERSION_ARB, 2, None }; - GLXContext context = glXCreateContextAttribs(display, fbConfig[0], nullptr, true, attributes); + + //glXCreateContextAttribs tends to throw BadRequest errors instead of simply failing gracefully + auto originalHandler = XSetErrorHandler(VideoGLX_X11ErrorHandler); + auto context = glXCreateContextAttribs(display, fbConfig[0], nullptr, true, attributes); + XSync(display, False); + XSetErrorHandler(originalHandler); + if(context) { glXMakeCurrent(display, 0, nullptr); glXDestroyContext(display, glxcontext); glXMakeCurrent(display, glxwindow, glxcontext = context); + } else { + //OpenGL 3.2+ not supported (most likely OpenGL 2.x) + return false; } + } else { + //missing required glXCreateContextAtribs function + return false; } - if(glXSwapInterval) { - glXSwapInterval(settings.synchronize); - } + if(glXSwapInterval) glXSwapInterval(settings.synchronize); //read attributes of frame buffer for later use, as requested attributes from above are not always granted signed value = 0; diff --git a/shaders/Curvature.shader/curvature.fs b/shaders/Curvature.shader/curvature.fs new file mode 100644 index 00000000..d6db5b83 --- /dev/null +++ b/shaders/Curvature.shader/curvature.fs @@ -0,0 +1,21 @@ +#version 150 +#define distortion 0.2 + +uniform sampler2D source[]; +uniform vec4 sourceSize[]; + +in Vertex { + vec2 texCoord; +}; + +out vec4 fragColor; + +vec2 radialDistortion(vec2 coord) { + vec2 cc = coord - vec2(0.5); + float dist = dot(cc, cc) * distortion; + return coord + cc * (1.0 - dist) * dist; +} + +void main() { + fragColor = texture(source[0], radialDistortion(texCoord)); +} diff --git a/shaders/Curvature.shader/manifest.bml b/shaders/Curvature.shader/manifest.bml new file mode 100644 index 00000000..7bec4709 --- /dev/null +++ b/shaders/Curvature.shader/manifest.bml @@ -0,0 +1,4 @@ +program + filter: linear + wrap: border + fragment: curvature.fs diff --git a/shaders/Edge Detection.shader/edge-detection.fs b/shaders/Edge Detection.shader/edge-detection.fs new file mode 100644 index 00000000..bf00fc25 --- /dev/null +++ b/shaders/Edge Detection.shader/edge-detection.fs @@ -0,0 +1,25 @@ +#version 150 + +uniform sampler2D source[]; +uniform vec4 sourceSize[]; + +in Vertex { + vec2 texCoord; +}; + +out vec4 fragColor; + +vec3 grayscale(vec3 color) { + return vec3(dot(color, vec3(0.3, 0.59, 0.11))); +} + +void main() { + vec2 offset = fract(texCoord * sourceSize[0].xy) - 0.5; + offset /= sourceSize[0].xy; + + vec3 cx = texture(source[0], texCoord - offset).xyz; + vec3 cy = texture(source[0], texCoord).xyz; + vec3 cz = vec3(5.0 * grayscale(abs(cx - cy))); + + fragColor = vec4(clamp(cz, 0.0, 1.0), 1.0); +} diff --git a/shaders/Edge Detection.shader/manifest.bml b/shaders/Edge Detection.shader/manifest.bml new file mode 100644 index 00000000..166db6de --- /dev/null +++ b/shaders/Edge Detection.shader/manifest.bml @@ -0,0 +1,4 @@ +program + filter: linear + wrap: edge + fragment: edge-detection.fs diff --git a/shaders/Makefile b/shaders/Makefile new file mode 100644 index 00000000..80d315c8 --- /dev/null +++ b/shaders/Makefile @@ -0,0 +1,5 @@ +install: + if [ -d /usr/share/higan/Video\ Shaders ]; then sudo rm -r /usr/share/higan/Video\ Shaders; fi + sudo mkdir -p /usr/share/higan/Video\ Shaders + sudo cp -r *.shader /usr/share/higan/Video\ Shaders + sudo chmod -R 777 /usr/share/higan/Video\ Shaders diff --git a/shaders/Scanline.shader/manifest.bml b/shaders/Scanline.shader/manifest.bml new file mode 100644 index 00000000..a5db6351 --- /dev/null +++ b/shaders/Scanline.shader/manifest.bml @@ -0,0 +1,4 @@ +program + filter: linear + wrap: border + fragment: scanline.fs diff --git a/shaders/Scanline.shader/scanline.fs b/shaders/Scanline.shader/scanline.fs new file mode 100644 index 00000000..42a9603a --- /dev/null +++ b/shaders/Scanline.shader/scanline.fs @@ -0,0 +1,20 @@ +#version 150 + +uniform sampler2D source[]; + +in Vertex { + vec2 texCoord; +}; + +out vec4 fragColor; + +void main() { + vec4 rgba = texture(source[0], texCoord); + vec4 intensity; + if(fract(gl_FragCoord.y * (0.5 * 4.0 / 3.0)) > 0.5) { + intensity = vec4(0); + } else { + intensity = smoothstep(0.2, 0.8, rgba) + normalize(rgba); + } + fragColor = intensity * -0.25 + rgba * 1.1; +} diff --git a/target-tomoko/configuration/configuration.cpp b/target-tomoko/configuration/configuration.cpp index 92a7bc60..0be6bc01 100644 --- a/target-tomoko/configuration/configuration.cpp +++ b/target-tomoko/configuration/configuration.cpp @@ -15,6 +15,7 @@ ConfigurationManager::ConfigurationManager() { video.append(video.scale, "Scale"); video.append(video.aspectCorrection, "AspectCorrection"); video.append(video.filter, "Filter"); + video.append(video.shader, "Shader"); video.append(video.colorEmulation, "ColorEmulation"); video.append(video.saturation, "Saturation"); video.append(video.gamma, "Gamma"); diff --git a/target-tomoko/configuration/configuration.hpp b/target-tomoko/configuration/configuration.hpp index a7b4745f..7f506329 100644 --- a/target-tomoko/configuration/configuration.hpp +++ b/target-tomoko/configuration/configuration.hpp @@ -16,6 +16,7 @@ struct ConfigurationManager : Configuration::Document { string scale = "Small"; bool aspectCorrection = true; string filter = "Blur"; + string shader = "None"; bool colorEmulation = true; unsigned saturation = 100; unsigned gamma = 100; diff --git a/target-tomoko/presentation/presentation.cpp b/target-tomoko/presentation/presentation.cpp index 36b80d9d..295d11a1 100644 --- a/target-tomoko/presentation/presentation.cpp +++ b/target-tomoko/presentation/presentation.cpp @@ -62,6 +62,7 @@ Presentation::Presentation() { maskOverscan.setText("Mask Overscan").setChecked(config->video.overscan.mask).onToggle([&] { config->video.overscan.mask = maskOverscan.checked(); }); + loadShaders(); synchronizeVideo.setText("Synchronize Video").setChecked(config->video.synchronize).onToggle([&] { config->video.synchronize = synchronizeVideo.checked(); video->set(Video::Synchronize, config->video.synchronize); @@ -136,56 +137,35 @@ auto Presentation::updateEmulator() -> void { } auto Presentation::resizeViewport() -> void { - signed scale = 1; + signed width = emulator ? emulator->information.width : 256; + signed height = emulator ? emulator->information.height : 240; + double stretch = emulator ? emulator->information.aspectRatio : 1.0; + if(stretch != 1.0) { + //aspect correction is always enabled in fullscreen mode + if(!fullScreen() && !config->video.aspectCorrection) stretch = 1.0; + } + + signed scale = 2; if(config->video.scale == "Small" ) scale = 2; if(config->video.scale == "Medium") scale = 3; if(config->video.scale == "Large" ) scale = 4; - signed width = 256; - signed height = 240; - if(emulator) { - width = emulator->information.width; - height = emulator->information.height; - } - - bool arc = config->video.aspectCorrection; - - if(fullScreen() == false) { - signed windowWidth = 256 * scale; - signed windowHeight = 240 * scale; - if(arc) windowWidth = windowWidth * 8 / 7; - - double stretch = (arc && emulator && emulator->information.aspectRatio != 1.0) ? 8.0 / 7.0 : 1.0; - signed multiplier = min(windowWidth / (signed)(width * stretch), windowHeight / height); - width = width * multiplier * stretch; - height = height * multiplier; - - setSize({windowWidth, windowHeight}); - viewport.setGeometry({(windowWidth - width) / 2, (windowHeight - height) / 2, width, height}); + signed windowWidth = 0, windowHeight = 0; + if(!fullScreen()) { + windowWidth = 256 * scale * (config->video.aspectCorrection ? 8.0 / 7.0 : 1.0); + windowHeight = 240 * scale; } else { - signed windowWidth = geometry().width(); - signed windowHeight = geometry().height(); - - //aspect ratio correction is always enabled in fullscreen mode - //note that below algorithm yields 7:6 ratio on 2560x(1440,1600) monitors - //this is extremely close to the optimum 8:7 ratio - //it is used so that linear interpolation isn't required - //todo: we should handle other resolutions nicely as well - unsigned multiplier = windowHeight / height; - width *= 1 + multiplier; - height *= multiplier; - - signed x = (windowWidth - width) / 2; - signed y = (windowHeight - height) / 2; - - if(x < 0) x = 0; - if(y < 0) y = 0; - if(width > windowWidth) width = windowWidth; - if(height > windowHeight) height = windowHeight; - - viewport.setGeometry({x, y, width, height}); + windowWidth = geometry().width(); + windowHeight = geometry().height(); } + signed multiplier = min(windowWidth / (signed)(width * stretch), windowHeight / height); + width = width * multiplier * stretch; + height = height * multiplier; + + if(!fullScreen()) setSize({windowWidth, windowHeight}); + viewport.setGeometry({(windowWidth - width) / 2, (windowHeight - height) / 2, width, height}); + if(!emulator) drawSplashScreen(); } @@ -221,3 +201,34 @@ auto Presentation::drawSplashScreen() -> void { video->refresh(); } } + +auto Presentation::loadShaders() -> void { + if(config->video.driver != "OpenGL") { + videoShaderMenu.setVisible(false); + return; + } + + auto pathname = locate({localpath(), "tomoko/"}, "Video Shaders/"); + for(auto shader : directory::folders(pathname, "*.shader")) { + MenuRadioItem item{&videoShaderMenu}; + item.setText(string{shader}.rtrim(".shader/", 1L)).onActivate([=] { + config->video.shader = {pathname, shader}; + program->updateVideoFilter(); + }); + videoShaders.append(item); + } + + videoShaderMenu.setText("Video Shaders"); + videoShaderNone.setChecked().setText("None").onActivate([=] { + config->video.shader = "None"; + program->updateVideoFilter(); + }); + + for(auto object : videoShaders.objects()) { + if(auto radioItem = dynamic_cast(object.data())) { + if(config->video.shader == string{pathname, radioItem->text(), ".shader/"}) { + radioItem->setChecked(); + } + } + } +} diff --git a/target-tomoko/presentation/presentation.hpp b/target-tomoko/presentation/presentation.hpp index fe1ea41b..94a17fcd 100644 --- a/target-tomoko/presentation/presentation.hpp +++ b/target-tomoko/presentation/presentation.hpp @@ -4,6 +4,7 @@ struct Presentation : Window { auto resizeViewport() -> void; auto toggleFullScreen() -> void; auto drawSplashScreen() -> void; + auto loadShaders() -> void; MenuBar menuBar{this}; Menu libraryMenu{&menuBar}; @@ -31,6 +32,9 @@ struct Presentation : Window { MenuSeparator videoFilterSeparator{&videoFilterMenu}; MenuCheckItem colorEmulation{&videoFilterMenu}; MenuCheckItem maskOverscan{&videoFilterMenu}; + Menu videoShaderMenu{&settingsMenu}; + MenuRadioItem videoShaderNone{&videoShaderMenu}; + Group videoShaders{&videoShaderNone}; MenuSeparator settingsMenuSeparator1{&settingsMenu}; MenuCheckItem synchronizeVideo{&settingsMenu}; MenuCheckItem synchronizeAudio{&settingsMenu}; diff --git a/target-tomoko/program/utility.cpp b/target-tomoko/program/utility.cpp index 7caad7f0..2e765134 100644 --- a/target-tomoko/program/utility.cpp +++ b/target-tomoko/program/utility.cpp @@ -36,8 +36,13 @@ auto Program::updateStatusText() -> void { } auto Program::updateVideoFilter() -> void { - if(config->video.filter == "None") video->set(Video::Filter, Video::FilterNearest); - if(config->video.filter == "Blur") video->set(Video::Filter, Video::FilterLinear); + if(config->video.driver == "OpenGL" && config->video.shader != "None" && directory::exists(config->video.shader)) { + video->set(Video::Filter, Video::FilterNearest); + video->set(Video::Shader, (string)config->video.shader); + } else { + video->set(Video::Filter, config->video.filter == "Blur" ? Video::FilterLinear : Video::FilterNearest); + video->set(Video::Shader, (string)""); + } } auto Program::updateVideoPalette() -> void {