From 4ca429e3893f8aee16a31a7f4653898112f7d4f6 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Thu, 14 Aug 2025 17:11:37 +0200 Subject: [PATCH] impr: Allow post-processing shaders to be set dynamically --- .../include/hex/api/events/requests_gui.hpp | 7 ++ lib/libimhex/include/hex/api/imhex_api.hpp | 8 ++ lib/libimhex/include/hex/helpers/opengl.hpp | 1 + lib/libimhex/source/api/imhex_api.cpp | 4 + lib/libimhex/source/helpers/opengl.cpp | 15 ++- main/gui/include/window.hpp | 2 +- main/gui/source/init/tasks.cpp | 18 ++-- main/gui/source/window/window.cpp | 98 +++++++++---------- .../builtin/romfs/shaders/retro/fragment.glsl | 50 ++++++++++ .../builtin/romfs/shaders/retro/vertex.glsl | 10 ++ plugins/builtin/source/content/events.cpp | 29 ++++++ .../source/content/views/view_about.cpp | 12 +++ 12 files changed, 189 insertions(+), 65 deletions(-) create mode 100644 plugins/builtin/romfs/shaders/retro/fragment.glsl create mode 100644 plugins/builtin/romfs/shaders/retro/vertex.glsl diff --git a/lib/libimhex/include/hex/api/events/requests_gui.hpp b/lib/libimhex/include/hex/api/events/requests_gui.hpp index 5fdee10ee..8bb6ffd21 100644 --- a/lib/libimhex/include/hex/api/events/requests_gui.hpp +++ b/lib/libimhex/include/hex/api/events/requests_gui.hpp @@ -34,4 +34,11 @@ namespace hex { */ EVENT_DEF(RequestOpenPopup, std::string); + /** + * @brief Requests updating of the active post-processing shader + * + * @param vertexShader the vertex shader source code + * @param fragmentShader the fragment shader source code + */ + EVENT_DEF(RequestSetPostProcessingShader, std::string, std::string); } diff --git a/lib/libimhex/include/hex/api/imhex_api.hpp b/lib/libimhex/include/hex/api/imhex_api.hpp index eb64481af..ac222a6c0 100644 --- a/lib/libimhex/include/hex/api/imhex_api.hpp +++ b/lib/libimhex/include/hex/api/imhex_api.hpp @@ -747,6 +747,14 @@ EXPORT_MODULE namespace hex { * @brief Unlocks the frame rate temporarily, allowing animations to run smoothly */ void unlockFrameRate(); + + /** + * @brief Sets the current post-processing shader to use + * @param vertexShader The vertex shader to use + * @param fragmentShader The fragment shader to use + */ + void setPostProcessingShader(const std::string &vertexShader, const std::string &fragmentShader); + } /** diff --git a/lib/libimhex/include/hex/helpers/opengl.hpp b/lib/libimhex/include/hex/helpers/opengl.hpp index 8e8a931dd..37911e638 100644 --- a/lib/libimhex/include/hex/helpers/opengl.hpp +++ b/lib/libimhex/include/hex/helpers/opengl.hpp @@ -811,6 +811,7 @@ namespace hex::gl { void setUniform(std::string_view name, const int &value); void setUniform(std::string_view name, const float &value); + bool hasUniform(std::string_view name); template void setUniform(std::string_view name, const Vector &value) { diff --git a/lib/libimhex/source/api/imhex_api.cpp b/lib/libimhex/source/api/imhex_api.cpp index a62c22b78..65478bcd1 100644 --- a/lib/libimhex/source/api/imhex_api.cpp +++ b/lib/libimhex/source/api/imhex_api.cpp @@ -1074,6 +1074,10 @@ namespace hex { impl::s_frameRateUnlockRequested = true; } + void setPostProcessingShader(const std::string &vertexShader, const std::string &fragmentShader) { + RequestSetPostProcessingShader::post(vertexShader, fragmentShader); + } + } diff --git a/lib/libimhex/source/helpers/opengl.cpp b/lib/libimhex/source/helpers/opengl.cpp index 5dea0d7dd..909c7e265 100644 --- a/lib/libimhex/source/helpers/opengl.cpp +++ b/lib/libimhex/source/helpers/opengl.cpp @@ -61,6 +61,10 @@ namespace hex::gl { } Shader::Shader(std::string_view vertexSource, std::string_view fragmentSource) { + if (vertexSource.empty() || fragmentSource.empty()) { + return; + } + auto vertexShader = glCreateShader(GL_VERTEX_SHADER); this->compile(vertexShader, vertexSource); @@ -79,7 +83,7 @@ namespace hex::gl { glGetProgramiv(m_program, GL_LINK_STATUS, &result); if (!result) { std::vector log(512); - glGetShaderInfoLog(m_program, log.size(), nullptr, log.data()); + glGetProgramInfoLog(m_program, log.size(), nullptr, log.data()); log::error("Failed to link shader: {}", log.data()); glDeleteProgram(m_program); @@ -98,6 +102,9 @@ namespace hex::gl { } Shader& Shader::operator=(Shader &&other) noexcept { + if (m_program != 0) + glDeleteProgram(m_program); + m_program = other.m_program; other.m_program = 0; return *this; @@ -119,6 +126,11 @@ namespace hex::gl { glUniform1f(getUniformLocation(name), value); } + bool Shader::hasUniform(std::string_view name) { + return getUniformLocation(name) != -1; + } + + GLint Shader::getUniformLocation(std::string_view name) { auto uniform = m_uniforms.find(name.data()); @@ -126,6 +138,7 @@ namespace hex::gl { auto location = glGetUniformLocation(m_program, name.data()); if (location == -1) { log::warn("Uniform '{}' not found in shader", name); + m_uniforms[name.data()] = -1; return -1; } diff --git a/main/gui/include/window.hpp b/main/gui/include/window.hpp index 271ed3350..1d69335f2 100644 --- a/main/gui/include/window.hpp +++ b/main/gui/include/window.hpp @@ -52,7 +52,7 @@ namespace hex { void exitImGui(); void registerEventHandlers(); - void loadPostProcessingShader(); + void loadPostProcessingShader(const std::string &vertexShader, const std::string &fragmentShader); void setupEmergencyPopups(); void drawImGui(); diff --git a/main/gui/source/init/tasks.cpp b/main/gui/source/init/tasks.cpp index cdf8c907d..aefb5b2b0 100644 --- a/main/gui/source/init/tasks.cpp +++ b/main/gui/source/init/tasks.cpp @@ -133,16 +133,16 @@ namespace hex::init { // In debug builds, ignore all plugins that are not part of the executable directory #if !defined(DEBUG) return true; + #else + if (!executablePath.has_value()) + return true; + + if (!PluginManager::getPluginLoadPaths().empty()) + return true; + + // Check if the plugin is somewhere in the same directory tree as the executable + return !std::fs::relative(plugin.getPath(), executablePath->parent_path()).string().starts_with(".."); #endif - - if (!executablePath.has_value()) - return true; - - if (!PluginManager::getPluginLoadPaths().empty()) - return true; - - // Check if the plugin is somewhere in the same directory tree as the executable - return !std::fs::relative(plugin.getPath(), executablePath->parent_path()).string().starts_with(".."); }; u32 loadErrors = 0; diff --git a/main/gui/source/window/window.cpp b/main/gui/source/window/window.cpp index cb7b79f14..327b40794 100644 --- a/main/gui/source/window/window.cpp +++ b/main/gui/source/window/window.cpp @@ -66,13 +66,14 @@ namespace hex { this->setupEmergencyPopups(); #if !defined(OS_WEB) - this->loadPostProcessingShader(); + #endif } Window::~Window() { RequestCloseImHex::unsubscribe(this); EventDPIChanged::unsubscribe(this); + RequestSetPostProcessingShader::unsubscribe(this); EventWindowDeinitializing::post(m_window); @@ -106,6 +107,12 @@ namespace hex { glfwSetWindowSize(m_window, width, height); }); + RequestSetPostProcessingShader::subscribe(this, [this](const std::string &vertexShader, const std::string &fragmentShader) { + TaskManager::doLater([this, vertexShader, fragmentShader] { + this->loadPostProcessingShader(vertexShader, fragmentShader); + }); + }); + LayoutManager::registerLoadCallback([this](std::string_view line) { int width = 0, height = 0; @@ -143,33 +150,8 @@ namespace hex { } - void Window::loadPostProcessingShader() { - - for (const auto &folder : paths::Resources.all()) { - auto vertexShaderPath = folder / "shader.vert"; - auto fragmentShaderPath = folder / "shader.frag"; - - if (!wolv::io::fs::exists(vertexShaderPath)) - continue; - if (!wolv::io::fs::exists(fragmentShaderPath)) - continue; - - auto vertexShaderFile = wolv::io::File(vertexShaderPath, wolv::io::File::Mode::Read); - if (!vertexShaderFile.isValid()) - continue; - - auto fragmentShaderFile = wolv::io::File(fragmentShaderPath, wolv::io::File::Mode::Read); - if (!fragmentShaderFile.isValid()) - continue; - - const auto vertexShaderSource = vertexShaderFile.readString(); - const auto fragmentShaderSource = fragmentShaderFile.readString(); - m_postProcessingShader = gl::Shader(vertexShaderSource, fragmentShaderSource); - if (!m_postProcessingShader.isValid()) - continue; - - break; - } + void Window::loadPostProcessingShader(const std::string &vertexShader, const std::string &fragmentShader) { + m_postProcessingShader = gl::Shader(vertexShader, fragmentShader); } @@ -808,44 +790,49 @@ namespace hex { // If not, there's no point in sending the draw data off to the GPU and swapping buffers // NOTE: For anybody looking at this code and thinking "why not just hash the buffer and compare the hashes", // the reason is that hashing the buffer is significantly slower than just comparing the buffers directly. - // The buffer might become quite large if there's a lot of vertices on the screen but it's still usually less than + // The buffer might become quite large if there's a lot of vertices on the screen, but it's still usually less than // 10MB (out of which only the active portion needs to actually be compared) which is worth the ~60x speedup. - bool shouldRender = false; - { + bool shouldRender = [this] { + if (m_postProcessingShader.isValid() && m_postProcessingShader.hasUniform("Time")) + return true; + static std::vector previousVtxData; static size_t previousVtxDataSize = 0; + size_t totalVtxDataSize = 0; + + for (const auto *viewport : ImGui::GetPlatformIO().Viewports) { + const auto drawData = viewport->DrawData; + for (int n = 0; n < drawData->CmdListsCount; n++) { + totalVtxDataSize += drawData->CmdLists[n]->VtxBuffer.size() * sizeof(ImDrawVert); + } + } + + if (totalVtxDataSize != previousVtxDataSize) { + previousVtxDataSize = totalVtxDataSize; + previousVtxData.resize(totalVtxDataSize); + return true; + } + size_t offset = 0; - size_t vtxDataSize = 0; - - for (const auto viewPort : ImGui::GetPlatformIO().Viewports) { - auto drawData = viewPort->DrawData; + for (const auto *viewport : ImGui::GetPlatformIO().Viewports) { + const auto drawData = viewport->DrawData; for (int n = 0; n < drawData->CmdListsCount; n++) { - vtxDataSize += drawData->CmdLists[n]->VtxBuffer.size() * sizeof(ImDrawVert); - } - } - for (const auto viewPort : ImGui::GetPlatformIO().Viewports) { - auto drawData = viewPort->DrawData; - for (int n = 0; n < drawData->CmdListsCount; n++) { - const ImDrawList *cmdList = drawData->CmdLists[n]; + const auto& vtxBuffer = drawData->CmdLists[n]->VtxBuffer; + const std::size_t bufSize = vtxBuffer.size() * sizeof(ImDrawVert); - if (vtxDataSize == previousVtxDataSize) { - shouldRender = shouldRender || std::memcmp(previousVtxData.data() + offset, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.size() * sizeof(ImDrawVert)) != 0; - } else { - shouldRender = true; + if (std::memcmp(previousVtxData.data() + offset, vtxBuffer.Data, bufSize) != 0) { + std::memcpy(previousVtxData.data() + offset, vtxBuffer.Data, bufSize); + return true; } - if (previousVtxData.size() < offset + cmdList->VtxBuffer.size() * sizeof(ImDrawVert)) { - previousVtxData.resize(offset + cmdList->VtxBuffer.size() * sizeof(ImDrawVert)); - } - - std::memcpy(previousVtxData.data() + offset, cmdList->VtxBuffer.Data, cmdList->VtxBuffer.size() * sizeof(ImDrawVert)); - offset += cmdList->VtxBuffer.size() * sizeof(ImDrawVert); + offset += bufSize; } } - previousVtxDataSize = vtxDataSize; - } + return false; + }(); + GLFWwindow *backupContext = glfwGetCurrentContext(); ImGui::UpdatePlatformWindows(); @@ -948,6 +935,9 @@ namespace hex { m_postProcessingShader.bind(); + m_postProcessingShader.setUniform("Time", static_cast(glfwGetTime())); + m_postProcessingShader.setUniform("Resolution", gl::Vector{{ float(displayWidth), float(displayHeight) }}); + glBindVertexArray(quadVAO); glBindTexture(GL_TEXTURE_2D, texture); glClearColor(0.00F, 0.00F, 0.00F, 0.00F); diff --git a/plugins/builtin/romfs/shaders/retro/fragment.glsl b/plugins/builtin/romfs/shaders/retro/fragment.glsl new file mode 100644 index 000000000..75bfdd868 --- /dev/null +++ b/plugins/builtin/romfs/shaders/retro/fragment.glsl @@ -0,0 +1,50 @@ +#version 330 core +in vec2 TexCoords; +out vec4 FragColor; + +uniform sampler2D screenTexture; +uniform vec2 Resolution; // Output resolution (pixels) +uniform float Time; // Time in seconds + +// Number of color levels per channel +uniform int colorLevels = 16; // 4-bit look + +// Enable dithering (0 = off, 1 = on) +uniform int useDither = 1; + +// Bayer 4x4 matrix for ordered dithering +float bayerDither4x4(int x, int y) { + int index = (x & 3) + ((y & 3) << 2); + int ditherMatrix[16] = int[16]( + 0, 8, 2, 10, + 12, 4, 14, 6, + 3, 11, 1, 9, + 15, 7, 13, 5 + ); + return float(ditherMatrix[index]) / 16.0; +} + +void main() { + vec2 retroResolution = Resolution / 1.5; + // Calculate pixelated coordinates + vec2 pixelSize = 1.0 / retroResolution; + vec2 uv = floor(TexCoords * retroResolution) * pixelSize; + + // Sample the scene at pixelated coordinates + vec3 color = texture(screenTexture, uv).rgb; + + // Optional ordered dithering + if (useDither == 1) { + ivec2 pixelCoord = ivec2(floor(TexCoords * Resolution)); + float ditherValue = bayerDither4x4(pixelCoord.x, pixelCoord.y); + color += (ditherValue - 0.5) / float(colorLevels); // small shift + } + + // Quantize colors to limited palette + color = floor(color * float(colorLevels)) / float(colorLevels - 1); + + // Optional slight flicker for retro screens + color *= 1.0 + 0.02 * sin(Time * 60.0 + TexCoords.y * 200.0); + + FragColor = vec4(color, 1.0); +} diff --git a/plugins/builtin/romfs/shaders/retro/vertex.glsl b/plugins/builtin/romfs/shaders/retro/vertex.glsl new file mode 100644 index 000000000..19f06d5ab --- /dev/null +++ b/plugins/builtin/romfs/shaders/retro/vertex.glsl @@ -0,0 +1,10 @@ +#version 330 core +in vec2 position; +in vec2 texCoords; + +out vec2 TexCoords; + +void main() { + TexCoords = texCoords; + gl_Position = vec4(position, 0.0, 1.0); +} \ No newline at end of file diff --git a/plugins/builtin/source/content/events.cpp b/plugins/builtin/source/content/events.cpp index 2a41dd95d..6f59be5fc 100644 --- a/plugins/builtin/source/content/events.cpp +++ b/plugins/builtin/source/content/events.cpp @@ -34,6 +34,7 @@ #include #include +#include namespace hex::plugin::builtin { @@ -319,6 +320,34 @@ namespace hex::plugin::builtin { const auto &initArgs = ImHexApi::System::getInitArguments(); if (auto it = initArgs.find("language"); it != initArgs.end()) LocalizationManager::setLanguage(it->second); + + // Set the user-defined post-processing shader if one exists + #if !defined(OS_WEB) + for (const auto &folder : paths::Resources.all()) { + auto vertexShaderPath = folder / "shader.vert"; + auto fragmentShaderPath = folder / "shader.frag"; + + if (!wolv::io::fs::exists(vertexShaderPath)) + continue; + if (!wolv::io::fs::exists(fragmentShaderPath)) + continue; + + auto vertexShaderFile = wolv::io::File(vertexShaderPath, wolv::io::File::Mode::Read); + if (!vertexShaderFile.isValid()) + continue; + + auto fragmentShaderFile = wolv::io::File(fragmentShaderPath, wolv::io::File::Mode::Read); + if (!fragmentShaderFile.isValid()) + continue; + + const auto vertexShaderSource = vertexShaderFile.readString(); + const auto fragmentShaderSource = fragmentShaderFile.readString(); + + ImHexApi::System::setPostProcessingShader(vertexShaderSource, fragmentShaderSource); + + break; + } + #endif }); EventWindowFocused::subscribe([](bool focused) { diff --git a/plugins/builtin/source/content/views/view_about.cpp b/plugins/builtin/source/content/views/view_about.cpp index e5b4d1d04..9aa796f16 100644 --- a/plugins/builtin/source/content/views/view_about.cpp +++ b/plugins/builtin/source/content/views/view_about.cpp @@ -810,6 +810,18 @@ namespace hex::plugin::builtin { ImGui::Indent(indentation); ImGuiExt::TextFormattedWrapped("{}", romfs::get("licenses/LICENSE").string()); ImGui::Unindent(indentation); + + static bool enabled = false; + if (ImGuiExt::DimmedButtonToggle("N" "E" "R" "D", &enabled)) { + if (enabled) { + ImHexApi::System::setPostProcessingShader( + romfs::get("shaders/retro/vertex.glsl").data(), + romfs::get("shaders/retro/fragment.glsl").data() + ); + } else { + ImHexApi::System::setPostProcessingShader("", ""); + } + } } void ViewAbout::drawAboutPopup() {