mirror of
https://github.com/WerWolv/ImHex.git
synced 2026-03-27 23:37:05 -05:00
impr: Allow post-processing shaders to be set dynamically
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1074,6 +1074,10 @@ namespace hex {
|
||||
impl::s_frameRateUnlockRequested = true;
|
||||
}
|
||||
|
||||
void setPostProcessingShader(const std::string &vertexShader, const std::string &fragmentShader) {
|
||||
RequestSetPostProcessingShader::post(vertexShader, fragmentShader);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
50
plugins/builtin/romfs/shaders/retro/fragment.glsl
Normal file
50
plugins/builtin/romfs/shaders/retro/fragment.glsl
Normal 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);
|
||||
}
|
||||
10
plugins/builtin/romfs/shaders/retro/vertex.glsl
Normal file
10
plugins/builtin/romfs/shaders/retro/vertex.glsl
Normal 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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user