diff --git a/CMakeLists.txt b/CMakeLists.txt index 85a915e21..23c591c34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -457,11 +457,11 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp - src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp + src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp src/panda_qt/shader_editor.cpp ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp - include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp + include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp include/panda_qt/shader_editor.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 72725257b..831074a29 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -19,6 +19,7 @@ #include "panda_qt/config_window.hpp" #include "panda_qt/patch_window.hpp" #include "panda_qt/screen.hpp" +#include "panda_qt/shader_editor.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" @@ -48,6 +49,7 @@ class MainWindow : public QMainWindow { EditCheat, PressTouchscreen, ReleaseTouchscreen, + ReloadUbershader, }; // Tagged union representing our message queue messages @@ -99,6 +101,7 @@ class MainWindow : public QMainWindow { CheatsWindow* cheatsEditor; TextEditorWindow* luaEditor; PatchWindow* patchWindow; + ShaderEditorWindow* shaderEditor; // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible SDL_GameController* gameController = nullptr; @@ -110,9 +113,6 @@ class MainWindow : public QMainWindow { void selectROM(); void dumpDspFirmware(); void dumpRomFS(); - void openLuaEditor(); - void openCheatsEditor(); - void openPatchWindow(); void showAboutMenu(); void initControllers(); void pollControllers(); @@ -139,5 +139,6 @@ class MainWindow : public QMainWindow { void mouseReleaseEvent(QMouseEvent* event) override; void loadLuaScript(const std::string& code); + void reloadShader(const std::string& shader); void editCheat(u32 handle, const std::vector& cheat, const std::function& callback); }; diff --git a/include/panda_qt/shader_editor.hpp b/include/panda_qt/shader_editor.hpp new file mode 100644 index 000000000..86bc1149a --- /dev/null +++ b/include/panda_qt/shader_editor.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +#include "zep.h" +#include "zep/mode_repl.h" +#include "zep/regress.h" + +class ShaderEditorWindow : public QDialog { + Q_OBJECT + + private: + Zep::ZepWidget_Qt zepWidget; + Zep::IZepReplProvider replProvider; + static constexpr float fontSize = 14.0f; + + public: + // Whether this backend supports shader editor + bool supported = true; + + ShaderEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText); + void setText(const std::string& text) { zepWidget.GetEditor().GetMRUBuffer()->SetText(text); } + void setEnable(bool enable); +}; \ No newline at end of file diff --git a/include/renderer.hpp b/include/renderer.hpp index 8888b41e9..17812bcfe 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include "PICA/pica_vertex.hpp" @@ -66,6 +67,13 @@ class Renderer { // This function does things like write back or cache necessary state before we delete our context virtual void deinitGraphicsContext() = 0; + // Functions for hooking up the renderer core to the frontend's shader editor for editing ubershaders in real time + // SupportsShaderReload: Indicates whether the backend offers ubershader reload support or not + // GetUbershader/SetUbershader: Gets or sets the renderer's current ubershader + virtual bool supportsShaderReload() { return false; } + virtual std::string getUbershader() { return ""; } + virtual void setUbershader(const std::string& shader) {} + // Functions for initializing the graphics context for the Qt frontend, where we don't have the convenience of SDL_Window #ifdef PANDA3DS_FRONTEND_QT virtual void initGraphicsContext(GL::Context* context) { Helpers::panic("Tried to initialize incompatible renderer with GL context"); } diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 92f02662f..c947583e6 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -82,12 +82,17 @@ class RendererGL final : public Renderer { void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) override; void drawVertices(PICA::PrimType primType, std::span vertices) override; // Draw the given vertices void deinitGraphicsContext() override; - + + virtual bool supportsShaderReload() override { return true; } + virtual std::string getUbershader() override; + virtual void setUbershader(const std::string& shader) override; + std::optional getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true); // Note: The caller is responsible for deleting the currently bound FBO before calling this void setFBO(uint handle) { screenFramebuffer.m_handle = handle; } void resetStateManager() { gl.reset(); } + void initUbershader(OpenGL::Program& program); #ifdef PANDA3DS_FRONTEND_QT virtual void initGraphicsContext([[maybe_unused]] GL::Context* context) override { initGraphicsContextInternal(); } diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index a11a6ffa5..cfa32319c 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -57,24 +57,7 @@ void RendererGL::initGraphicsContextInternal() { OpenGL::Shader vert({vertexShaderSource.begin(), vertexShaderSource.size()}, OpenGL::Vertex); OpenGL::Shader frag({fragmentShaderSource.begin(), fragmentShaderSource.size()}, OpenGL::Fragment); triangleProgram.create({vert, frag}); - gl.useProgram(triangleProgram); - - textureEnvSourceLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvSource"); - textureEnvOperandLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvOperand"); - textureEnvCombinerLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvCombiner"); - textureEnvColorLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvColor"); - textureEnvScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvScale"); - - depthScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_depthScale"); - depthOffsetLoc = OpenGL::uniformLocation(triangleProgram, "u_depthOffset"); - depthmapEnableLoc = OpenGL::uniformLocation(triangleProgram, "u_depthmapEnable"); - picaRegLoc = OpenGL::uniformLocation(triangleProgram, "u_picaRegs"); - - // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex0"), 0); - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex1"), 1); - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex2"), 2); - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex_lighting_lut"), 3); + initUbershader(triangleProgram); auto displayVertexShaderSource = gl_resources.open("opengl_display.vert"); auto displayFragmentShaderSource = gl_resources.open("opengl_display.frag"); @@ -812,4 +795,43 @@ void RendererGL::deinitGraphicsContext() { // All other GL objects should be invalidated automatically and be recreated by the next call to initGraphicsContext // TODO: Make it so that depth and colour buffers get written back to 3DS memory printf("RendererGL::DeinitGraphicsContext called\n"); +} + +std::string RendererGL::getUbershader() { + auto gl_resources = cmrc::RendererGL::get_filesystem(); + auto fragmentShader = gl_resources.open("opengl_fragment_shader.frag"); + + return std::string(fragmentShader.begin(), fragmentShader.end()); +} + +void RendererGL::setUbershader(const std::string& shader) { + auto gl_resources = cmrc::RendererGL::get_filesystem(); + auto vertexShaderSource = gl_resources.open("opengl_vertex_shader.vert"); + + OpenGL::Shader vert({vertexShaderSource.begin(), vertexShaderSource.size()}, OpenGL::Vertex); + OpenGL::Shader frag(shader, OpenGL::Fragment); + triangleProgram.create({vert, frag}); + + initUbershader(triangleProgram); +} + +void RendererGL::initUbershader(OpenGL::Program& program) { + gl.useProgram(program); + + textureEnvSourceLoc = OpenGL::uniformLocation(program, "u_textureEnvSource"); + textureEnvOperandLoc = OpenGL::uniformLocation(program, "u_textureEnvOperand"); + textureEnvCombinerLoc = OpenGL::uniformLocation(program, "u_textureEnvCombiner"); + textureEnvColorLoc = OpenGL::uniformLocation(program, "u_textureEnvColor"); + textureEnvScaleLoc = OpenGL::uniformLocation(program, "u_textureEnvScale"); + + depthScaleLoc = OpenGL::uniformLocation(program, "u_depthScale"); + depthOffsetLoc = OpenGL::uniformLocation(program, "u_depthOffset"); + depthmapEnableLoc = OpenGL::uniformLocation(program, "u_depthmapEnable"); + picaRegLoc = OpenGL::uniformLocation(program, "u_picaRegs"); + + // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 + glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); + glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); + glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); + glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); } \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 54e4fabe0..cfa45e85c 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -55,12 +55,14 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); auto patchWindowAction = toolsMenu->addAction(tr("Open Patch Window")); + auto shaderEditorAction = toolsMenu->addAction(tr("Open Shader Editor")); auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); - connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); - connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); - connect(patchWindowAction, &QAction::triggered, this, &MainWindow::openPatchWindow); + connect(luaEditorAction, &QAction::triggered, this, [this]() { luaEditor->show(); }); + connect(shaderEditorAction, &QAction::triggered, this, [this]() { shaderEditor->show(); }); + connect(cheatsEditorAction, &QAction::triggered, this, [this]() { cheatsEditor->show(); }); + connect(patchWindowAction, &QAction::triggered, this, [this]() { patchWindow->show(); }); connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); @@ -75,6 +77,12 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) cheatsEditor = new CheatsWindow(emu, {}, this); patchWindow = new PatchWindow(this); luaEditor = new TextEditorWindow(this, "script.lua", ""); + shaderEditor = new ShaderEditorWindow(this, "shader.glsl", ""); + + shaderEditor->setEnable(emu->getRenderer()->supportsShaderReload()); + if (shaderEditor->supported) { + shaderEditor->setText(emu->getRenderer()->getUbershader()); + } auto args = QCoreApplication::arguments(); if (args.size() > 1) { @@ -294,10 +302,6 @@ void MainWindow::showAboutMenu() { about.exec(); } -void MainWindow::openLuaEditor() { luaEditor->show(); } -void MainWindow::openCheatsEditor() { cheatsEditor->show(); } -void MainWindow::openPatchWindow() { patchWindow->show(); } - void MainWindow::dispatchMessage(const EmulatorMessage& message) { switch (message.type) { case MessageType::LoadROM: @@ -351,6 +355,11 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { emu->getServiceManager().getHID().setTouchScreenPress(message.touchscreen.x, message.touchscreen.y); break; case MessageType::ReleaseTouchscreen: emu->getServiceManager().getHID().releaseTouchScreen(); break; + + case MessageType::ReloadUbershader: + emu->getRenderer()->setUbershader(*message.string.str); + delete message.string.str; + break; } } @@ -453,6 +462,14 @@ void MainWindow::loadLuaScript(const std::string& code) { sendMessage(message); } +void MainWindow::reloadShader(const std::string& shader) { + EmulatorMessage message{.type = MessageType::ReloadUbershader}; + + // Make a copy of the code on the heap to send via the message queue + message.string.str = new std::string(shader); + sendMessage(message); +} + void MainWindow::editCheat(u32 handle, const std::vector& cheat, const std::function& callback) { EmulatorMessage message{.type = MessageType::EditCheat}; diff --git a/src/panda_qt/shader_editor.cpp b/src/panda_qt/shader_editor.cpp new file mode 100644 index 000000000..122d841fd --- /dev/null +++ b/src/panda_qt/shader_editor.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include "panda_qt/main_window.hpp" +#include "panda_qt/shader_editor.hpp" + +using namespace Zep; + +ShaderEditorWindow::ShaderEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText) + : QDialog(parent), zepWidget(this, qApp->applicationDirPath().toStdString(), fontSize) { + resize(600, 600); + + // Register our extensions + ZepRegressExCommand::Register(zepWidget.GetEditor()); + ZepReplExCommand::Register(zepWidget.GetEditor(), &replProvider); + + // Default to standard mode instead of vim mode, initialize text box + zepWidget.GetEditor().InitWithText(filename, initialText); + zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName()); + + // Layout for widgets + QVBoxLayout* mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + + QPushButton* button = new QPushButton(tr("Reload shader"), this); + button->setFixedSize(100, 20); + + // When the Load Script button is pressed, send the current text to the MainWindow, which will upload it to the emulator's lua object + connect(button, &QPushButton::pressed, this, [this]() { + if (parentWidget()) { + auto buffer = zepWidget.GetEditor().GetMRUBuffer(); + const std::string text = buffer->GetBufferText(buffer->Begin(), buffer->End()); + + static_cast(parentWidget())->reloadShader(text); + } else { + // This should be unreachable, only here for safety purposes + printf("Text editor does not have any parent widget, click doesn't work :(\n"); + } + }); + + mainLayout->addWidget(button); + mainLayout->addWidget(&zepWidget); +} + +void ShaderEditorWindow::setEnable(bool enable) { + supported = enable; + + if (enable) { + setDisabled(false); + } else { + setDisabled(true); + setText("Shader editor window is not available for this renderer backend"); + } +}