impr: Allow post-processing shaders to be set dynamically

This commit is contained in:
WerWolv
2025-08-14 17:11:37 +02:00
parent 49c56e28b4
commit 4ca429e389
12 changed files with 189 additions and 65 deletions

View File

@@ -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);
}

View File

@@ -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);
}
/**

View File

@@ -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<size_t N>
void setUniform(std::string_view name, const Vector<float, N> &value) {

View File

@@ -1074,6 +1074,10 @@ namespace hex {
impl::s_frameRateUnlockRequested = true;
}
void setPostProcessingShader(const std::string &vertexShader, const std::string &fragmentShader) {
RequestSetPostProcessingShader::post(vertexShader, fragmentShader);
}
}

View File

@@ -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<char> 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;
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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<u8> 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<float>(glfwGetTime()));
m_postProcessingShader.setUniform("Resolution", gl::Vector<float, 2>{{ float(displayWidth), float(displayHeight) }});
glBindVertexArray(quadVAO);
glBindTexture(GL_TEXTURE_2D, texture);
glClearColor(0.00F, 0.00F, 0.00F, 0.00F);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -34,6 +34,7 @@
#include <GLFW/glfw3.h>
#include <hex/api/theme_manager.hpp>
#include <hex/helpers/default_paths.hpp>
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) {

View File

@@ -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<char>(),
romfs::get("shaders/retro/fragment.glsl").data<char>()
);
} else {
ImHexApi::System::setPostProcessingShader("", "");
}
}
}
void ViewAbout::drawAboutPopup() {