From 35ced58cc9152632085bd5a5a9991b3d73327b29 Mon Sep 17 00:00:00 2001
From: "Zed A. Shaw" <zed.shaw@gmail.com>
Date: Sun, 13 Apr 2025 17:11:21 -0400
Subject: [PATCH] Shaders now are managed by a manger that can do hot reloading
 and it also will detect a bad shader and use an ERROR shader so you know it's
 busted visually.

---
 {shaders => assets/shaders}/modal.frag        |  0
 assets/shaders/ui_error.frag                  | 18 ++++++
 {shaders => assets/shaders}/ui_shader.frag    |  0
 .../shaders}/ui_shape_shader.frag             |  0
 constants.hpp                                 |  1 -
 guecs.cpp                                     | 17 +++---
 guecs.hpp                                     |  6 +-
 gui_fsm.cpp                                   |  1 +
 main.cpp                                      |  2 +
 meson.build                                   |  2 +
 raycaster.cpp                                 |  2 +-
 shaders.cpp                                   | 60 +++++++++++++++++++
 shaders.hpp                                   | 26 ++++++++
 tests/shaders.cpp                             | 22 +++++++
 14 files changed, 144 insertions(+), 13 deletions(-)
 rename {shaders => assets/shaders}/modal.frag (100%)
 create mode 100644 assets/shaders/ui_error.frag
 rename {shaders => assets/shaders}/ui_shader.frag (100%)
 rename {shaders => assets/shaders}/ui_shape_shader.frag (100%)
 create mode 100644 shaders.cpp
 create mode 100644 shaders.hpp
 create mode 100644 tests/shaders.cpp

diff --git a/shaders/modal.frag b/assets/shaders/modal.frag
similarity index 100%
rename from shaders/modal.frag
rename to assets/shaders/modal.frag
diff --git a/assets/shaders/ui_error.frag b/assets/shaders/ui_error.frag
new file mode 100644
index 0000000..29ccc8c
--- /dev/null
+++ b/assets/shaders/ui_error.frag
@@ -0,0 +1,18 @@
+uniform vec2 u_resolution;
+uniform vec2 u_mouse;
+uniform float u_duration;
+uniform float u_time;
+uniform float u_time_end;
+uniform sampler2D texture;
+uniform bool is_shape;
+
+void main() {
+  if(is_shape) {
+    vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
+    gl_FragColor = gl_Color * color;
+  } else {
+    vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
+    vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
+    gl_FragColor = gl_Color * color * pixel;
+  }
+}
diff --git a/shaders/ui_shader.frag b/assets/shaders/ui_shader.frag
similarity index 100%
rename from shaders/ui_shader.frag
rename to assets/shaders/ui_shader.frag
diff --git a/shaders/ui_shape_shader.frag b/assets/shaders/ui_shape_shader.frag
similarity index 100%
rename from shaders/ui_shape_shader.frag
rename to assets/shaders/ui_shape_shader.frag
diff --git a/constants.hpp b/constants.hpp
index 1a7d6ad..30d62a6 100644
--- a/constants.hpp
+++ b/constants.hpp
@@ -73,7 +73,6 @@ constexpr wchar_t BG_TILE = L'█';
 constexpr wchar_t UI_BASE_CHAR = L'█';
 constexpr int BG_BOX_OFFSET=5;
 constexpr const char *FONT_FILE_NAME="assets/text.otf";
-constexpr const char *DEFAULT_UI_SHADER = "shaders/ui_shader.frag";
 
 constexpr std::array<std::wstring, 8> COMPASS{
     // L"E", L"SE", L"S", L"SW", L"W", L"NW", L"N", L"NE"
diff --git a/guecs.cpp b/guecs.cpp
index 24eff7c..1c5df73 100644
--- a/guecs.cpp
+++ b/guecs.cpp
@@ -1,4 +1,5 @@
 #include "guecs.hpp"
+#include "shaders.hpp"
 
 namespace guecs {
 
@@ -66,31 +67,31 @@ namespace guecs {
   }
 
   void Shader::init(lel::Cell &cell) {
-    ptr = std::make_shared<sf::Shader>();
-    bool good = ptr->loadFromFile(name, sf::Shader::Type::Fragment);
-    dbc::check(good, fmt::format("failed to load shader {}", name));
-    ptr->setUniform("u_resolution", sf::Vector2f({float(cell.w), float(cell.h)}));
-
+    auto shader = shaders::get(name);
+    shader->setUniform("u_resolution", sf::Vector2f({float(cell.w), float(cell.h)}));
     clock = std::make_shared<sf::Clock>();
   }
 
   void Shader::step() {
+    auto shader = shaders::get(name);
     sf::Time u_time = clock->getElapsedTime();
     float current_time = u_time.asSeconds();
 
     if(current_time < u_time_end) {
-      ptr->setUniform("u_time", current_time);
+      shader->setUniform("u_time", current_time);
     } else {
       active = false;
     }
   }
 
   void Shader::run() {
+    auto shader = shaders::get(name);
+
     active = true;
     sf::Time u_time = clock->getElapsedTime();
     u_time_end = u_time.asSeconds() + duration;
-    ptr->setUniform("u_duration", duration);
-    ptr->setUniform("u_time_end", u_time_end);
+    shader->setUniform("u_duration", duration);
+    shader->setUniform("u_time_end", u_time_end);
   }
 
   UI::UI() {
diff --git a/guecs.hpp b/guecs.hpp
index ebce9d1..4c1790d 100644
--- a/guecs.hpp
+++ b/guecs.hpp
@@ -11,6 +11,7 @@
 #include "constants.hpp"
 #include "components.hpp"
 #include <any>
+#include "shaders.hpp"
 
 namespace guecs {
   using std::shared_ptr, std::make_shared, std::wstring, std::string;
@@ -83,10 +84,9 @@ namespace guecs {
 
   struct Shader {
     float duration = 0.1f;
-    std::string name{DEFAULT_UI_SHADER};
+    std::string name{"ui_shader"};
     float u_time_end = 0.0;
     bool active = false;
-    std::shared_ptr<sf::Shader> ptr = nullptr;
     std::shared_ptr<sf::Clock> clock = nullptr;
 
     void init(lel::Cell &cell);
@@ -199,7 +199,7 @@ namespace guecs {
           auto& shader = $world.get<Shader>(ent);
 
           if(shader.active) {
-            shader_ptr = shader.ptr.get();
+            shader_ptr = shaders::get(shader.name);
             shader_ptr->setUniform("is_shape", is_shape);
           }
         }
diff --git a/gui_fsm.cpp b/gui_fsm.cpp
index f95cf07..ff5d316 100644
--- a/gui_fsm.cpp
+++ b/gui_fsm.cpp
@@ -286,6 +286,7 @@ namespace gui {
           case KEY::P:
             sound::mute(false);
             $debug_ui.debug();
+            shaders::reload();
             break;
           case KEY::O:
             autowalking = true;
diff --git a/main.cpp b/main.cpp
index 02913ce..d6d88e4 100644
--- a/main.cpp
+++ b/main.cpp
@@ -5,6 +5,7 @@
 #include "ai.hpp"
 #include "animation.hpp"
 #include <iostream>
+#include "shaders.hpp"
 
 int main(int argc, char* argv[]) {
   try {
@@ -12,6 +13,7 @@ int main(int argc, char* argv[]) {
     sound::init();
     ai::init("assets/ai.json");
     animation::init();
+    shaders::init();
 
     sound::mute(true);
     gui::FSM main;
diff --git a/meson.build b/meson.build
index d31d23c..38b26e5 100644
--- a/meson.build
+++ b/meson.build
@@ -114,6 +114,7 @@ sources = [
   'ritual_ui.cpp',
   'rituals.cpp',
   'save.cpp',
+  'shaders.cpp',
   'shiterator.hpp',
   'sound.cpp',
   'spatialmap.cpp',
@@ -145,6 +146,7 @@ executable('runtests', sources + [
   'tests/matrix.cpp',
   'tests/pathing.cpp',
   'tests/rituals.cpp',
+  'tests/shaders.cpp',
   'tests/sound.cpp',
   'tests/spatialmap.cpp',
   'tests/stats.cpp',
diff --git a/raycaster.cpp b/raycaster.cpp
index ab7e838..1a51795 100644
--- a/raycaster.cpp
+++ b/raycaster.cpp
@@ -394,7 +394,7 @@ void Raycaster::update_level(GameLevel level) {
 
 void Raycaster::init_shaders() {
   // dbc::check(sf::Shader::isAvailable(), "no shaders?!");
-  bool good = $brightness.loadFromFile("shaders/modal.frag", sf::Shader::Type::Fragment);
+  bool good = $brightness.loadFromFile("assets/shaders/modal.frag", sf::Shader::Type::Fragment);
   dbc::check(good, "shader could not be loaded");
   $brightness.setUniform("source", sf::Shader::CurrentTexture);
 }
diff --git a/shaders.cpp b/shaders.cpp
new file mode 100644
index 0000000..c464d68
--- /dev/null
+++ b/shaders.cpp
@@ -0,0 +1,60 @@
+#include "shaders.hpp"
+#include <SFML/Graphics/Image.hpp>
+#include "dbc.hpp"
+#include <fmt/core.h>
+#include "config.hpp"
+#include "constants.hpp"
+#include <memory>
+
+namespace shaders {
+  using std::shared_ptr, std::make_shared;
+
+  static ShaderManager SMGR;
+  static bool initialized = false;
+
+
+  bool load_shader(std::string name, nlohmann::json& settings) {
+    std::string file_name = settings["file_name"];
+    auto ptr = std::make_shared<sf::Shader>();
+    bool good = ptr->loadFromFile(file_name, sf::Shader::Type::Fragment);
+    if(good) SMGR.shaders.try_emplace(name, name, file_name, ptr);
+    return good;
+  }
+
+  void init() {
+    if(!initialized) {
+      initialized = true;
+      Config config("assets/shaders.json");
+      bool good = load_shader("ERROR", config["ERROR"]);
+      dbc::check(good, "Failed to load ERROR shader. Look in assets/shaders.json");
+
+      for(auto& [name, settings] : config.json().items()) {
+        if(name == "ERROR") continue;
+
+        dbc::check(!SMGR.shaders.contains(name),
+            fmt::format("shader name '{}' duplicated in assets/shaders.json", name));
+        good = load_shader(name, settings);
+
+        if(!good) {
+          dbc::log(fmt::format("failed to load shader {}", name));
+          SMGR.shaders.insert_or_assign(name, SMGR.shaders.at("ERROR"));
+        }
+      }
+    }
+  }
+
+  sf::Shader* get(std::string name) {
+    dbc::check(initialized, "you forgot to shaders::init()");
+    dbc::check(SMGR.shaders.contains(name),
+        fmt::format("shader name '{}' not in assets/shaders.json", name));
+
+    auto& rec = SMGR.shaders.at(name);
+    return rec.ptr.get();
+  }
+
+  void reload() {
+    initialized = false;
+    SMGR.shaders.clear();
+    init();
+  }
+};
diff --git a/shaders.hpp b/shaders.hpp
new file mode 100644
index 0000000..25db13b
--- /dev/null
+++ b/shaders.hpp
@@ -0,0 +1,26 @@
+#pragma once
+#include <cstdint>
+#include <vector>
+#include <string>
+#include <SFML/Graphics.hpp>
+#include <unordered_map>
+#include <memory>
+#include "matrix.hpp"
+#include <nlohmann/json.hpp>
+
+namespace shaders {
+  struct Record {
+    std::string name;
+    std::string file_name;
+    std::shared_ptr<sf::Shader> ptr = nullptr;
+  };
+
+  struct ShaderManager {
+    std::unordered_map<std::string, Record> shaders;
+  };
+
+  void init();
+  bool load_shader(std::string& name, nlohmann::json& settings);
+  sf::Shader* get(std::string name);
+  void reload();
+}
diff --git a/tests/shaders.cpp b/tests/shaders.cpp
new file mode 100644
index 0000000..52adb5e
--- /dev/null
+++ b/tests/shaders.cpp
@@ -0,0 +1,22 @@
+#include <catch2/catch_test_macros.hpp>
+#include <fmt/core.h>
+#include <string>
+#include "shaders.hpp"
+
+using namespace fmt;
+
+TEST_CASE("shader loading/init works", "[shaders]") {
+  shaders::init();
+
+  sf::Shader* ui_shader = shaders::get("ui_shader");
+  auto other_test = shaders::get("ui_shader");
+
+  REQUIRE(ui_shader != nullptr);
+  REQUIRE(ui_shader == other_test);
+
+  shaders::reload();
+
+  // auto after_reload = shaders::get("ui_shader");
+  // REQUIRE(ui_shader != after_reload);
+  // REQUIRE(other_test != after_reload);
+}