diff --git a/CLAUDE.md b/CLAUDE.md index eecfeb9..8fd5e04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a cross-platform Shadertoy-like fragment shader viewer built with SDL3 + OpenGL 3.3. The application loads and displays GLSL shaders from the `shaders/` directory with runtime switching capabilities. +This is a cross-platform Shadertoy-like fragment shader viewer built with SDL3. It supports two rendering backends: +- **SDL3 GPU** (default): renders via Vulkan on Linux/Windows and Metal on macOS. +- **OpenGL 3.3** (fallback): the original backend, used automatically if SDL3 GPU init fails. + +Selection at launch: +```bash +./shadertoy --backend=gpu # force SDL3 GPU +./shadertoy --backend=opengl # force OpenGL +./shadertoy --backend=auto # default: GPU first, fall back to OpenGL on failure +``` ## Build and Development Commands @@ -30,13 +39,21 @@ make macos_debug make linux_debug ``` +### Compiling Shaders to SPIR-V +The SDL3 GPU backend reads compiled SPIR-V (`.frag.spv`) at runtime. Run this after touching any `.vk.glsl` file: +```bash +cmake --build build --target compile_shaders +``` +Requires `glslc` (Debian/Ubuntu: `apt install glslang-tools`; macOS: `brew install glslang`). + ### Running the Application ```bash -./shadertoy [SHADER_PATH] [-F|--fullscreen] +./shadertoy [SHADER_NAME_OR_PATH] [-F|--fullscreen] [--backend=auto|gpu|opengl] # Examples: -./shadertoy shaders/test.frag.glsl -./shadertoy -F shaders/fractal_pyramid.frag.glsl +./shadertoy test # auto backend, shaders/test/ +./shadertoy --backend=gpu seascape # force SDL3 GPU (Vulkan/Metal) +./shadertoy -F shaders/fractal_pyramid # explicit folder path, fullscreen ``` **Runtime Controls:** @@ -46,28 +63,49 @@ make linux_debug ## Architecture -### Core Design -All application logic resides in `src/main.cpp` (~469 lines) - a monolithic design that's straightforward to understand. Key components: +### Layered design +- `src/main.cpp` — SDL3 window, event loop, shader-list scanning, dispatch to a backend. +- `src/rendering/shader_backend.hpp` — abstract `IShaderBackend` interface + `ShaderMetadata` / `ShaderUniforms` / `ShaderProgramSpec` shared types. Two factories: `makeOpenGLBackend()`, `makeSdl3GpuBackend()`. +- `src/rendering/opengl_shader_backend.{hpp,cpp}` — OpenGL 3.3 implementation. Loads `/.gl.glsl` at runtime. +- `src/rendering/sdl3gpu/sdl3gpu_shader_backend.{hpp,cpp}` — SDL3 GPU implementation. Loads `/.frag.spv` (Linux/Windows) or `/.frag.msl` (macOS); shares one passthrough vertex shader from `shaders/_common/passthrough.vert.{spv,msl}`. +- `src/rendering/sdl3gpu/shader_factory.hpp` — small helper that loads a shader binary/source from disk and creates an `SDL_GPUShader`. -1. **Shader Loading System** - Automatic directory scanning of `.glsl` files, sorted alphabetically -2. **OpenGL Rendering** - Single fullscreen quad with fragment shader using GL_TRIANGLE_STRIP -3. **Event Loop** - SDL3-based with vsync (SDL_GL_SwapWindow + 1ms delay) -4. **Resource Path Resolution** - Multi-path fallback system for executable, relative, and macOS bundle paths +### Shader folder layout +One folder per shader, under `shaders/`: +``` +shaders// + .gl.glsl # OpenGL GLSL 3.30 (handwritten) + .vk.glsl # Vulkan GLSL 4.50 (handwritten) + .frag.msl # Metal Shading Language (handwritten) + .frag.spv # generated by `make compile_shaders` from .vk.glsl + meta.txt # Name / Author / iChannelN +shaders/_common/ + passthrough.vk.glsl # shared fullscreen-triangle vertex (Vulkan) + passthrough.vert.spv # generated + passthrough.vert.msl # handwritten Metal +``` +Folders starting with `_` or `.` are skipped by the scanner. The `meta.txt` parser is in `Rendering::parseMetaFile`. + +### Backend selection logic (`main.cpp`) +1. Parse `--backend=...` flag (default `auto`). +2. If not OpenGL: create a window with no flags, instantiate `Sdl3GpuShaderBackend`. If `init()` fails AND choice was `auto`, destroy the window and continue. If choice was `gpu`, exit with error. +3. Otherwise: create a window with `SDL_WINDOW_OPENGL` and instantiate `OpenGLShaderBackend`. ### Global State (main.cpp) ```cpp -shader_list_ // Vector of discovered .glsl shader paths +shader_list_ // Vector current_shader_index_ // Active shader in rotation -current_program_ // OpenGL shader program handle shader_start_ticks_ // Base time for iTime uniform calculation window_ // SDL3 window pointer -shaders_directory_ // Shader directory path (resolved at startup) +backend_ // unique_ptr +shaders_directory_ // Shaders root path (resolved at startup) ``` ### Dependencies -- **SDL3** - Window/input management, OpenGL context -- **GLAD** - OpenGL 3.3 loader (statically linked via `third_party/glad/`) -- **C++17 stdlib** - filesystem, fstream, vector, algorithms +- **SDL3** ≥ 3.2 — windowing, events, GPU API (`SDL_gpu.h`) +- **GLAD** — OpenGL 3.3 loader (used only by the OpenGL backend, statically linked via `third_party/glad/`) +- **C++17 stdlib** — filesystem, fstream, vector, algorithms +- Build-time tool: `glslc` (only required to regenerate `.frag.spv`) ### Platform-Specific Code Uses preprocessor defines (`WINDOWS_BUILD`, `MACOS_BUILD`, `LINUX_BUILD`) for: @@ -77,6 +115,22 @@ Uses preprocessor defines (`WINDOWS_BUILD`, `MACOS_BUILD`, `LINUX_BUILD`) for: ## Shader System +### Authoring a shader (manual steps) +For each new shader, three handwritten files live in `shaders//`: +1. **`.gl.glsl`** — OpenGL GLSL 3.30 (the original Shadertoy-style format described below). +2. **`.vk.glsl`** — Vulkan GLSL 4.50 with explicit `layout(set=3, binding=0) uniform ShadertoyUBO { float iTime; vec2 iResolution; } u;` and `layout(set=2, binding=N) uniform sampler2D` for any texture channel. +3. **`.frag.msl`** — Metal Shading Language. Entry point must be named `_fs`. Uniforms come in via `[[buffer(0)]]`; samplers via `[[texture(N)]] [[sampler(N)]]`. + +After editing any `.vk.glsl`, rebuild SPIR-V: `cmake --build build --target compile_shaders`. + +The MSL is **not** auto-generated — write it by hand following the same convention as `aee_2026/source/core/rendering/sdl3gpu/msl/*.msl.h`. + +### Shadertoy → Vulkan/Metal porting cheatsheet +- `iResolution` is `vec2` here (no `.xy`). Reconstruct vec3 manually if needed. +- Replace `texture(iChannel0, ...)` calls — no texture channels currently wired through the GPU backend (planned). +- The shared vertex stage emits `vUV` already in Shadertoy convention `(0,0)` bottom-left → `(1,1)` top-right; multiply by `iResolution` to get `fragCoord`. +- The Y axis is flipped in NDC inside the shared vertex shader so Vulkan/Metal render right-side-up like OpenGL. + ### Shader Format (GLSL 3.3 Core) All shaders must follow this structure: diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b02ba8..ec98df8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,9 @@ set(OpenGL_GL_PREFERENCE GLVND) # --- LISTA EXPLÍCITA DE FUENTES --- set(APP_SOURCES src/main.cpp + src/rendering/shader_backend.cpp + src/rendering/opengl_shader_backend.cpp + src/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp ) # Fuentes de librerías de terceros @@ -29,6 +32,21 @@ set(EXTERNAL_SOURCES find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}") +# --- COMPILACIÓN DE SHADERS (Vulkan SPIR-V) --- +find_program(GLSLC_EXE NAMES glslc) +if(GLSLC_EXE) + message(STATUS "glslc encontrado: ${GLSLC_EXE}") + add_custom_target(compile_shaders + COMMAND ${CMAKE_COMMAND} + -D GLSLC=${GLSLC_EXE} + -D SHADERS_DIR=${CMAKE_SOURCE_DIR}/shaders + -P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_shaders.cmake + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Compiling .vk.glsl shaders to SPIR-V") +else() + message(STATUS "glslc no encontrado — el target compile_shaders no estará disponible") +endif() + # --- AÑADIR EJECUTABLE --- add_executable(${PROJECT_NAME} ${APP_SOURCES} ${EXTERNAL_SOURCES}) diff --git a/Makefile b/Makefile index 9339505..9a3afa6 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,9 @@ LINUX_RELEASE := $(TARGET_NAME)-$(VERSION)-linux.tar.gz # Lista completa de archivos fuente APP_SOURCES := \ src/main.cpp \ + src/rendering/shader_backend.cpp \ + src/rendering/opengl_shader_backend.cpp \ + src/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp \ third_party/glad/src/glad.c \ third_party/jail_audio.cpp diff --git a/shaders/_common/passthrough.vert.msl b/shaders/_common/passthrough.vert.msl new file mode 100644 index 0000000..f596d75 --- /dev/null +++ b/shaders/_common/passthrough.vert.msl @@ -0,0 +1,19 @@ +#include +using namespace metal; + +// Shared fullscreen-triangle vertex shader for the SDL3 GPU backend. +// Emits uv in Shadertoy convention: (0,0) bottom-left, (1,1) top-right. + +struct PassthroughVOut { + float4 pos [[position]]; + float2 uv; +}; + +vertex PassthroughVOut passthrough_vs(uint vid [[vertex_id]]) { + const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} }; + PassthroughVOut out; + float2 pos = positions[vid]; + out.uv = pos * 0.5 + 0.5; + out.pos = float4(pos.x, -pos.y, 0.0, 1.0); + return out; +} diff --git a/shaders/_common/passthrough.vert.spv b/shaders/_common/passthrough.vert.spv new file mode 100644 index 0000000..6c18908 Binary files /dev/null and b/shaders/_common/passthrough.vert.spv differ diff --git a/shaders/_common/passthrough.vk.glsl b/shaders/_common/passthrough.vk.glsl new file mode 100644 index 0000000..cf192c2 --- /dev/null +++ b/shaders/_common/passthrough.vk.glsl @@ -0,0 +1,18 @@ +#version 450 + +// Shared fullscreen-triangle vertex shader for the SDL3 GPU backend. +// Emits vUV in Shadertoy convention: (0,0) bottom-left, (1,1) top-right. +// Y flipped in NDC because Vulkan/Metal point Y down by default. + +layout(location = 0) out vec2 vUV; + +void main() { + const vec2 positions[3] = vec2[3]( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0) + ); + vec2 pos = positions[gl_VertexIndex]; + vUV = pos * 0.5 + 0.5; + gl_Position = vec4(pos.x, -pos.y, 0.0, 1.0); +} diff --git a/shaders/cineshader_lava.frag.glsl b/shaders/cineshader_lava/cineshader_lava.gl.glsl similarity index 100% rename from shaders/cineshader_lava.frag.glsl rename to shaders/cineshader_lava/cineshader_lava.gl.glsl diff --git a/shaders/cineshader_lava/meta.txt b/shaders/cineshader_lava/meta.txt new file mode 100644 index 0000000..e6330c1 --- /dev/null +++ b/shaders/cineshader_lava/meta.txt @@ -0,0 +1,2 @@ +Name: Cineshader Lava +Author: edankwan diff --git a/shaders/creation.frac.glsl b/shaders/creation/creation.gl.glsl similarity index 100% rename from shaders/creation.frac.glsl rename to shaders/creation/creation.gl.glsl diff --git a/shaders/creation/meta.txt b/shaders/creation/meta.txt new file mode 100644 index 0000000..38a0cd5 --- /dev/null +++ b/shaders/creation/meta.txt @@ -0,0 +1,2 @@ +Name: Creation by Silexars +Author: Danguafer diff --git a/shaders/cube_lines.frag.glsl b/shaders/cube_lines/cube_lines.gl.glsl similarity index 100% rename from shaders/cube_lines.frag.glsl rename to shaders/cube_lines/cube_lines.gl.glsl diff --git a/shaders/cube_lines/meta.txt b/shaders/cube_lines/meta.txt new file mode 100644 index 0000000..75e3001 --- /dev/null +++ b/shaders/cube_lines/meta.txt @@ -0,0 +1,2 @@ +Name: Cube lines +Author: Danil diff --git a/shaders/dbz.frag.glsl b/shaders/dbz/dbz.gl.glsl similarity index 100% rename from shaders/dbz.frag.glsl rename to shaders/dbz/dbz.gl.glsl diff --git a/shaders/dbz/meta.txt b/shaders/dbz/meta.txt new file mode 100644 index 0000000..5c44a90 --- /dev/null +++ b/shaders/dbz/meta.txt @@ -0,0 +1,2 @@ +Name: New Leaked 3I/Atlas NASA Footage +Author: msm01 diff --git a/shaders/fractal_pyramid.frag.glsl b/shaders/fractal_pyramid/fractal_pyramid.gl.glsl similarity index 100% rename from shaders/fractal_pyramid.frag.glsl rename to shaders/fractal_pyramid/fractal_pyramid.gl.glsl diff --git a/shaders/fractal_pyramid/meta.txt b/shaders/fractal_pyramid/meta.txt new file mode 100644 index 0000000..53a46e7 --- /dev/null +++ b/shaders/fractal_pyramid/meta.txt @@ -0,0 +1,2 @@ +Name: Fractal Pyramid +Author: bradjamesgrant diff --git a/shaders/just_another_cube.frag.glsl b/shaders/just_another_cube/just_another_cube.gl.glsl similarity index 100% rename from shaders/just_another_cube.frag.glsl rename to shaders/just_another_cube/just_another_cube.gl.glsl diff --git a/shaders/just_another_cube/meta.txt b/shaders/just_another_cube/meta.txt new file mode 100644 index 0000000..e61d440 --- /dev/null +++ b/shaders/just_another_cube/meta.txt @@ -0,0 +1,2 @@ +Name: Just Another Cube +Author: mrange diff --git a/shaders/octograms/meta.txt b/shaders/octograms/meta.txt new file mode 100644 index 0000000..2f5ca97 --- /dev/null +++ b/shaders/octograms/meta.txt @@ -0,0 +1,2 @@ +Name: Octograms +Author: whisky_shusuky diff --git a/shaders/octograms.frag.glsl b/shaders/octograms/octograms.gl.glsl similarity index 100% rename from shaders/octograms.frag.glsl rename to shaders/octograms/octograms.gl.glsl diff --git a/shaders/remember/meta.txt b/shaders/remember/meta.txt new file mode 100644 index 0000000..a7384d5 --- /dev/null +++ b/shaders/remember/meta.txt @@ -0,0 +1,2 @@ +Name: Remember +Author: diatribes diff --git a/shaders/remember.frag.glsl b/shaders/remember/remember.gl.glsl similarity index 100% rename from shaders/remember.frag.glsl rename to shaders/remember/remember.gl.glsl diff --git a/shaders/seascape/meta.txt b/shaders/seascape/meta.txt new file mode 100644 index 0000000..3f73ba4 --- /dev/null +++ b/shaders/seascape/meta.txt @@ -0,0 +1,2 @@ +Name: Seascape +Author: Alexander Alekseev diff --git a/shaders/seascape.frag.glsl b/shaders/seascape/seascape.gl.glsl similarity index 100% rename from shaders/seascape.frag.glsl rename to shaders/seascape/seascape.gl.glsl diff --git a/shaders/shader_art_coding_introduction/meta.txt b/shaders/shader_art_coding_introduction/meta.txt new file mode 100644 index 0000000..91fe62b --- /dev/null +++ b/shaders/shader_art_coding_introduction/meta.txt @@ -0,0 +1,2 @@ +Name: Shader Art Coding Introduction +Author: kishimisu diff --git a/shaders/shader_art_coding_introduction.frag.glsl b/shaders/shader_art_coding_introduction/shader_art_coding_introduction.gl.glsl similarity index 100% rename from shaders/shader_art_coding_introduction.frag.glsl rename to shaders/shader_art_coding_introduction/shader_art_coding_introduction.gl.glsl diff --git a/shaders/test/meta.txt b/shaders/test/meta.txt new file mode 100644 index 0000000..9fc5454 --- /dev/null +++ b/shaders/test/meta.txt @@ -0,0 +1,2 @@ +Name: Test +Author: JailDesigner diff --git a/shaders/test/test.frag.msl b/shaders/test/test.frag.msl new file mode 100644 index 0000000..a3b7e0b --- /dev/null +++ b/shaders/test/test.frag.msl @@ -0,0 +1,36 @@ +#include +using namespace metal; + +// Test shader (Metal Shading Language port of test.vk.glsl). +// Author: JailDesigner + +struct ShadertoyUBO { + float iTime; + float2 iResolution; +}; + +struct PassthroughVOut { + float4 pos [[position]]; + float2 uv; +}; + +static float3 palette(float t) { + float3 a = float3(1.0, 0.5, 0.5); + float3 b = float3(1.0, 0.5, 0.5); + float3 c = float3(1.0, 1.0, 1.0); + float3 d = float3(0.263, 0.416, 0.557); + return a + b * cos(6.28318 * (c * t * d)); +} + +fragment float4 test_fs(PassthroughVOut in [[stage_in]], + constant ShadertoyUBO& u [[buffer(0)]]) { + float2 fragCoord = in.uv * u.iResolution; + float2 uv = (fragCoord * 2.0 - u.iResolution) / u.iResolution.y; + float d = length(uv); + float3 col = palette(d); + d = sin(d * 8.0 + u.iTime) / 8.0; + d = abs(d); + d = 0.02 / d; + col *= d; + return float4(col, 1.0); +} diff --git a/shaders/test/test.frag.spv b/shaders/test/test.frag.spv new file mode 100644 index 0000000..e887b4a Binary files /dev/null and b/shaders/test/test.frag.spv differ diff --git a/shaders/test.frag.glsl b/shaders/test/test.gl.glsl similarity index 100% rename from shaders/test.frag.glsl rename to shaders/test/test.gl.glsl diff --git a/shaders/test/test.vk.glsl b/shaders/test/test.vk.glsl new file mode 100644 index 0000000..819ab8e --- /dev/null +++ b/shaders/test/test.vk.glsl @@ -0,0 +1,41 @@ +#version 450 + +// Name: Test +// Author: JailDesigner +// +// Vulkan port of test.gl.glsl. Bindings follow the SDL3 GPU convention: +// set=3, binding=0 — fragment uniform buffer (ShadertoyUniforms in C++). + +layout(location = 0) in vec2 vUV; +layout(location = 0) out vec4 FragColor; + +layout(set = 3, binding = 0) uniform ShadertoyUBO { + float iTime; + vec2 iResolution; +} u; + +vec3 palette(float t) { + vec3 a = vec3(1.0, 0.5, 0.5); + vec3 b = vec3(1.0, 0.5, 0.5); + vec3 c = vec3(1.0, 1.0, 1.0); + vec3 d = vec3(0.263, 0.416, 0.557); + return a + b * cos(6.28318 * (c * t * d)); +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = (fragCoord * 2.0 - u.iResolution) / u.iResolution.y; + float d = length(uv); + vec3 col = palette(d); + d = sin(d * 8.0 + u.iTime) / 8.0; + d = abs(d); + d = 0.02 / d; + col *= d; + fragColor = vec4(col, 1.0); +} + +void main() { + vec2 fragCoordPixels = vUV * u.iResolution; + vec4 outColor; + mainImage(outColor, fragCoordPixels); + FragColor = outColor; +} diff --git a/shaders/water/meta.txt b/shaders/water/meta.txt new file mode 100644 index 0000000..d34a8a0 --- /dev/null +++ b/shaders/water/meta.txt @@ -0,0 +1,2 @@ +Name: Water +Author: diatribes diff --git a/shaders/water.glsl b/shaders/water/water.gl.glsl similarity index 100% rename from shaders/water.glsl rename to shaders/water/water.gl.glsl diff --git a/src/main.cpp b/src/main.cpp index 5c49ce9..eba209b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,32 +1,29 @@ // src/main.cpp -#include -#include -#include -#include -#include #include -#include -#include #include +#include +#include +#include +#include +#include +#include + #include -#include -#include "jail_audio.h" #include "defines.hpp" +#include "jail_audio.h" +#include "rendering/shader_backend.hpp" -// Simple logger compatible con el estilo que usas struct Logger { static void info(const std::string& s) { std::cout << "[INFO] " << s << '\n'; } static void error(const std::string& s) { std::cerr << "[ERROR] " << s << '\n'; } }; -// Opciones mínimas parecidas a las tuyas struct VideoOptions { bool fullscreen = false; bool vsync = true; } Options_video; -// Estructura para guardar info del display struct DisplayMonitor { std::string name; int width = 0; @@ -34,147 +31,32 @@ struct DisplayMonitor { int refresh_rate = 0; }; -// Forward declarations of structs -struct ShaderMetadata { - std::string name; - std::string author; - std::string iChannel0; // "BufferA", "BufferB", "none", etc. - std::string iChannel1; - std::string iChannel2; - std::string iChannel3; -}; - -struct ShaderBuffer { - GLuint program = 0; // Shader program for this buffer - GLuint fbo = 0; // Framebuffer object - GLuint texture = 0; // Output texture - std::string name; // "BufferA", "BufferB", etc. -}; - -struct ShaderPass { - std::string shaderName; // Base name (e.g., "water") - std::string displayName; // Custom name from metadata - std::string author; // Author from metadata - GLuint imageProgram = 0; // Main image shader program - std::vector buffers; // BufferA, BufferB, etc. - ShaderMetadata metadata; // iChannel configuration -}; - -// Globales simplificados (tu proyecto puede integrarlo en clases) static DisplayMonitor display_monitor_; static SDL_Window* window_ = nullptr; +static std::unique_ptr backend_; -// Sistema de shaders (legacy - kept for backward compatibility with single-pass shaders) -static std::vector shader_list_; -static std::vector shader_names_; // Custom names from "// Name: XXX" comments -static std::vector shader_authors_; // Custom authors from "// Author: XXX" comments +struct ShaderEntry { + std::filesystem::path folder; + std::string base_name; +}; + +static std::vector shader_list_; +static std::vector shader_names_; +static std::vector shader_authors_; static size_t current_shader_index_ = 0; static std::filesystem::path shaders_directory_; -static GLuint current_program_ = 0; static Uint32 shader_start_ticks_ = 0; -// Multi-pass shader system -static std::vector shader_passes_; -static int current_window_width_ = 0; -static int current_window_height_ = 0; - -// Self-feedback system (for shaders that use their own output as input) -static GLuint feedback_fbo_ = 0; -static GLuint feedback_texture_ = 0; -static bool current_shader_uses_feedback_ = false; -static int feedback_channel_ = -1; // Which iChannel (0-3) is used for feedback - -// FPS tracking static Uint32 fps_frame_count_ = 0; static Uint32 fps_last_update_ticks_ = 0; static float current_fps_ = 0.0f; -// Sistema de música static std::vector music_list_; static size_t current_music_index_ = 0; static JA_Music_t* current_music_ = nullptr; -// Vertex shader embebido -static const char* vertexShaderSrc = R"glsl( -#version 330 core -layout(location = 0) in vec2 aPos; -out vec2 vUV; -void main() { - vUV = aPos * 0.5 + 0.5; - gl_Position = vec4(aPos, 0.0, 1.0); -} -)glsl"; - -// Helpers de carga -static bool loadFileToString(const std::filesystem::path& path, std::string& out) { - std::ifstream ifs(path, std::ios::in | std::ios::binary); - if (!ifs) return false; - std::ostringstream ss; - ss << ifs.rdbuf(); - out = ss.str(); - return true; -} - -static std::string trimString(const std::string& str) { - size_t start = str.find_first_not_of(" \t\r\n"); - size_t end = str.find_last_not_of(" \t\r\n"); - if (start != std::string::npos && end != std::string::npos) { - return str.substr(start, end - start + 1); - } - return ""; -} - -static ShaderMetadata extractShaderMetadata(const std::string& shaderSource) { - ShaderMetadata metadata; - metadata.iChannel0 = "none"; - metadata.iChannel1 = "none"; - metadata.iChannel2 = "none"; - metadata.iChannel3 = "none"; - - std::istringstream stream(shaderSource); - std::string line; - int lineCount = 0; - const int maxLinesToCheck = 30; - - while (std::getline(stream, line) && lineCount < maxLinesToCheck) { - lineCount++; - - // Look for "// XXX: YYY" patterns (case-insensitive) - size_t pos = line.find("//"); - if (pos != std::string::npos) { - std::string comment = line.substr(pos + 2); - std::string commentLower = comment; - std::transform(commentLower.begin(), commentLower.end(), commentLower.begin(), ::tolower); - - // Check for Name: - if (commentLower.find("name:") != std::string::npos) { - metadata.name = trimString(comment.substr(comment.find(":") + 1)); - } - // Check for Author: - else if (commentLower.find("author:") != std::string::npos) { - metadata.author = trimString(comment.substr(comment.find(":") + 1)); - } - // Check for iChannel0-3: - else if (commentLower.find("ichannel0:") != std::string::npos) { - metadata.iChannel0 = trimString(comment.substr(comment.find(":") + 1)); - } - else if (commentLower.find("ichannel1:") != std::string::npos) { - metadata.iChannel1 = trimString(comment.substr(comment.find(":") + 1)); - } - else if (commentLower.find("ichannel2:") != std::string::npos) { - metadata.iChannel2 = trimString(comment.substr(comment.find(":") + 1)); - } - else if (commentLower.find("ichannel3:") != std::string::npos) { - metadata.iChannel3 = trimString(comment.substr(comment.find(":") + 1)); - } - } - } - - return metadata; -} - -static std::vector scanShaderDirectory(const std::filesystem::path& directory) { - std::vector shaders; +static std::vector scanShaderDirectory(const std::filesystem::path& directory) { + std::vector shaders; if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) { Logger::error("Shader directory does not exist: " + directory.string()); @@ -182,20 +64,25 @@ static std::vector scanShaderDirectory(const std::filesys } for (const auto& entry : std::filesystem::directory_iterator(directory)) { - if (entry.is_regular_file()) { - auto ext = entry.path().extension().string(); - if (ext == ".glsl") { - shaders.push_back(entry.path()); - } + if (!entry.is_directory()) { continue; } + + const std::string folder_name = entry.path().filename().string(); + if (folder_name.empty() || folder_name[0] == '_' || folder_name[0] == '.') { continue; } + + const std::filesystem::path gl_source = entry.path() / (folder_name + ".gl.glsl"); + if (!std::filesystem::exists(gl_source)) { + Logger::info("Skipping " + folder_name + ": missing " + gl_source.filename().string()); + continue; } + + shaders.push_back(ShaderEntry{entry.path(), folder_name}); } - // Ordenar alfabéticamente - std::sort(shaders.begin(), shaders.end()); + std::sort(shaders.begin(), shaders.end(), + [](const ShaderEntry& a, const ShaderEntry& b) { return a.base_name < b.base_name; }); Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string()); - // Initialize shader metadata vectors with empty strings (will be filled when shaders are loaded) shader_names_.resize(shaders.size(), ""); shader_authors_.resize(shaders.size(), ""); @@ -212,14 +99,13 @@ static std::vector scanMusicDirectory(const std::filesyst for (const auto& entry : std::filesystem::directory_iterator(directory)) { if (entry.is_regular_file()) { - auto ext = entry.path().extension().string(); + const auto ext = entry.path().extension().string(); if (ext == ".ogg") { music_files.push_back(entry.path()); } } } - // Ordenar alfabéticamente std::sort(music_files.begin(), music_files.end()); Logger::info("Found " + std::to_string(music_files.size()) + " music file(s) in " + directory.string()); @@ -227,288 +113,36 @@ static std::vector scanMusicDirectory(const std::filesyst } static void playRandomMusic() { - if (music_list_.empty()) return; + if (music_list_.empty()) { return; } - // Liberar música anterior si existe - if (current_music_) { + if (current_music_ != nullptr) { JA_DeleteMusic(current_music_); current_music_ = nullptr; } - // Elegir índice aleatorio - current_music_index_ = rand() % music_list_.size(); + current_music_index_ = static_cast(rand()) % music_list_.size(); - // Cargar y reproducir música (sin loop, loop=0) const auto& music_path = music_list_[current_music_index_]; current_music_ = JA_LoadMusic(music_path.string().c_str()); - if (current_music_) { - JA_PlayMusic(current_music_, 0); // 0 = no loop, se reproduce una vez + if (current_music_ != nullptr) { + JA_PlayMusic(current_music_, 0); Logger::info("Now playing: " + music_path.filename().string()); } else { Logger::error("Failed to load music: " + music_path.string()); } } -// ===== Multi-pass FBO/Texture Management ===== - -static bool createBufferFBO(ShaderBuffer& buffer, int width, int height) { - // Create texture - glGenTextures(1, &buffer.texture); - glBindTexture(GL_TEXTURE_2D, buffer.texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glBindTexture(GL_TEXTURE_2D, 0); - - // Create FBO - glGenFramebuffers(1, &buffer.fbo); - glBindFramebuffer(GL_FRAMEBUFFER, buffer.fbo); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, buffer.texture, 0); - - // Check FBO completeness - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - if (status != GL_FRAMEBUFFER_COMPLETE) { - Logger::error("FBO creation failed for " + buffer.name + ": " + std::to_string(status)); - return false; - } - - Logger::info("Created FBO for " + buffer.name + " (" + std::to_string(width) + "x" + std::to_string(height) + ")"); - return true; -} - -static void destroyBuffer(ShaderBuffer& buffer) { - if (buffer.fbo != 0) { - glDeleteFramebuffers(1, &buffer.fbo); - buffer.fbo = 0; - } - if (buffer.texture != 0) { - glDeleteTextures(1, &buffer.texture); - buffer.texture = 0; - } - if (buffer.program != 0) { - glDeleteProgram(buffer.program); - buffer.program = 0; - } -} - -static void destroyShaderPass(ShaderPass& pass) { - if (pass.imageProgram != 0) { - glDeleteProgram(pass.imageProgram); - pass.imageProgram = 0; - } - for (auto& buffer : pass.buffers) { - destroyBuffer(buffer); - } - pass.buffers.clear(); -} - -static bool resizeBuffersIfNeeded(ShaderPass& pass, int width, int height) { - if (current_window_width_ == width && current_window_height_ == height) { - return false; // No resize needed - } - - Logger::info("Resizing buffers: " + std::to_string(width) + "x" + std::to_string(height)); - - // Destroy and recreate all buffers with new size - for (auto& buffer : pass.buffers) { - // Keep program, destroy FBO/texture only - if (buffer.fbo != 0) glDeleteFramebuffers(1, &buffer.fbo); - if (buffer.texture != 0) glDeleteTextures(1, &buffer.texture); - buffer.fbo = 0; - buffer.texture = 0; - - if (!createBufferFBO(buffer, width, height)) { - return false; - } - } - - current_window_width_ = width; - current_window_height_ = height; - return true; -} - -// ===== Self-Feedback System ===== - -static bool createFeedbackFBO(int width, int height) { - // Delete existing if any - if (feedback_fbo_ != 0) glDeleteFramebuffers(1, &feedback_fbo_); - if (feedback_texture_ != 0) glDeleteTextures(1, &feedback_texture_); - - // Create texture - glGenTextures(1, &feedback_texture_); - glBindTexture(GL_TEXTURE_2D, feedback_texture_); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - // Clear to black initially - std::vector black(width * height * 4, 0.0f); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, black.data()); - glBindTexture(GL_TEXTURE_2D, 0); - - // Create FBO - glGenFramebuffers(1, &feedback_fbo_); - glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, feedback_texture_, 0); - - GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - - if (status != GL_FRAMEBUFFER_COMPLETE) { - Logger::error("Feedback FBO creation failed: " + std::to_string(status)); - return false; - } - - Logger::info("Created feedback FBO (" + std::to_string(width) + "x" + std::to_string(height) + ")"); - return true; -} - -static void destroyFeedbackFBO() { - if (feedback_fbo_ != 0) { - glDeleteFramebuffers(1, &feedback_fbo_); - feedback_fbo_ = 0; - } - if (feedback_texture_ != 0) { - glDeleteTextures(1, &feedback_texture_); - feedback_texture_ = 0; - } - current_shader_uses_feedback_ = false; - feedback_channel_ = -1; -} - -static int detectFeedbackChannel(const ShaderMetadata& metadata) { - // Check which iChannel uses "self" feedback - if (metadata.iChannel0 == "self") return 0; - if (metadata.iChannel1 == "self") return 1; - if (metadata.iChannel2 == "self") return 2; - if (metadata.iChannel3 == "self") return 3; - return -1; // No feedback -} - -// ===== Multi-pass Shader Loading ===== - -// Forward declarations -static GLuint compileShader(GLenum type, const char* src); -static GLuint linkProgram(GLuint vs, GLuint fs); - -static std::vector findBufferFiles(const std::filesystem::path& imagePath) { - std::vector buffers; - std::filesystem::path dir = imagePath.parent_path(); - std::string basename = imagePath.stem().stem().string(); // Remove .image.glsl -> get base name - - // Check for BufferA, BufferB, BufferC, BufferD - std::vector bufferNames = {"bufferA", "bufferB", "bufferC", "bufferD"}; - for (const auto& bufName : bufferNames) { - std::filesystem::path bufferPath = dir / (basename + "." + bufName + ".glsl"); - if (std::filesystem::exists(bufferPath)) { - buffers.push_back(bufName); - Logger::info("Found buffer: " + bufferPath.string()); - } - } - - return buffers; -} - -static GLuint loadMultiPassShader(const std::filesystem::path& imagePath, ShaderPass& outPass, int width, int height) { - std::string basename = imagePath.stem().stem().string(); - outPass.shaderName = basename; - - // Load and compile Image shader - std::string imageSrc; - if (!loadFileToString(imagePath, imageSrc)) { - Logger::error("Failed to load image shader: " + imagePath.string()); - return 0; - } - - // Extract metadata from Image shader - outPass.metadata = extractShaderMetadata(imageSrc); - outPass.displayName = outPass.metadata.name.empty() ? basename : outPass.metadata.name; - outPass.author = outPass.metadata.author; - - GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc); - GLuint fs = compileShader(GL_FRAGMENT_SHADER, imageSrc.c_str()); - - if (!vs || !fs) { - if (vs) glDeleteShader(vs); - if (fs) glDeleteShader(fs); - return 0; - } - - outPass.imageProgram = linkProgram(vs, fs); - glDeleteShader(vs); - glDeleteShader(fs); - - if (!outPass.imageProgram) { - return 0; - } - - // Find and load buffer shaders - std::vector bufferNames = findBufferFiles(imagePath); - std::filesystem::path dir = imagePath.parent_path(); - - for (const auto& bufName : bufferNames) { - ShaderBuffer buffer; - buffer.name = bufName; - - // Load buffer shader source - std::filesystem::path bufferPath = dir / (basename + "." + bufName + ".glsl"); - std::string bufferSrc; - if (!loadFileToString(bufferPath, bufferSrc)) { - Logger::error("Failed to load buffer: " + bufferPath.string()); - continue; - } - - // Compile buffer shader - GLuint bufVs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc); - GLuint bufFs = compileShader(GL_FRAGMENT_SHADER, bufferSrc.c_str()); - - if (!bufVs || !bufFs) { - if (bufVs) glDeleteShader(bufVs); - if (bufFs) glDeleteShader(bufFs); - continue; - } - - buffer.program = linkProgram(bufVs, bufFs); - glDeleteShader(bufVs); - glDeleteShader(bufFs); - - if (!buffer.program) { - continue; - } - - // Create FBO and texture for this buffer - if (!createBufferFBO(buffer, width, height)) { - glDeleteProgram(buffer.program); - continue; - } - - outPass.buffers.push_back(buffer); - Logger::info("Loaded buffer: " + bufName); - } - - Logger::info("Multi-pass shader loaded: " + outPass.displayName + " (" + std::to_string(outPass.buffers.size()) + " buffers)"); - return outPass.imageProgram; -} - static void updateWindowTitle() { - if (!window_ || shader_list_.empty()) return; + if (window_ == nullptr || shader_list_.empty()) { return; } - // Use custom shader name if available, otherwise fallback to filename std::string shaderName; if (!shader_names_.empty() && !shader_names_[current_shader_index_].empty()) { shaderName = shader_names_[current_shader_index_]; } else { - shaderName = shader_list_[current_shader_index_].filename().string(); + shaderName = shader_list_[current_shader_index_].base_name; } - // Add author if available if (!shader_authors_.empty() && !shader_authors_[current_shader_index_].empty()) { shaderName += " by " + shader_authors_[current_shader_index_]; } @@ -524,107 +158,48 @@ static void updateWindowTitle() { SDL_SetWindowTitle(window_, title.c_str()); } -static GLuint compileShader(GLenum type, const char* src) { - GLuint s = glCreateShader(type); - glShaderSource(s, 1, &src, nullptr); - glCompileShader(s); - GLint ok = 0; - glGetShaderiv(s, GL_COMPILE_STATUS, &ok); - if (!ok) { - GLint len = 0; - glGetShaderiv(s, GL_INFO_LOG_LENGTH, &len); - std::string log(len > 0 ? len : 1, ' '); - glGetShaderInfoLog(s, len, nullptr, &log[0]); - Logger::error("Shader compile error: " + log); - glDeleteShader(s); - return 0; - } - return s; -} - -static GLuint linkProgram(GLuint vs, GLuint fs) { - GLuint p = glCreateProgram(); - glAttachShader(p, vs); - glAttachShader(p, fs); - glLinkProgram(p); - GLint ok = 0; - glGetProgramiv(p, GL_LINK_STATUS, &ok); - if (!ok) { - GLint len = 0; - glGetProgramiv(p, GL_INFO_LOG_LENGTH, &len); - std::string log(len > 0 ? len : 1, ' '); - glGetProgramInfoLog(p, len, nullptr, &log[0]); - Logger::error("Program link error: " + log); - glDeleteProgram(p); - return 0; - } - return p; -} - -static GLuint loadAndCompileShader(size_t index) { +static bool loadShaderAtIndex(size_t index) { if (index >= shader_list_.size()) { Logger::error("Invalid shader index: " + std::to_string(index)); - return 0; + return false; } - const auto& shaderPath = shader_list_[index]; - Logger::info("Loading shader: " + shaderPath.string()); + const auto& entry = shader_list_[index]; + Logger::info("Loading shader: " + entry.folder.string()); - std::string fragSrc; - if (!loadFileToString(shaderPath, fragSrc)) { - Logger::error("Failed to load shader file: " + shaderPath.string()); - return 0; + Rendering::ShaderProgramSpec spec; + spec.folder = entry.folder; + spec.base_name = entry.base_name; + + const std::filesystem::path meta_path = entry.folder / "meta.txt"; + if (std::filesystem::exists(meta_path)) { + spec.metadata = Rendering::parseMetaFile(meta_path); + } else { + std::string source; + const std::filesystem::path gl_source = entry.folder / (entry.base_name + ".gl.glsl"); + if (Rendering::loadFileToString(gl_source, source)) { + spec.metadata = Rendering::extractShaderMetadata(source); + } } - // Extract custom shader metadata (name, author, iChannels) from source code - ShaderMetadata metadata = extractShaderMetadata(fragSrc); - if (!metadata.name.empty()) { - shader_names_[index] = metadata.name; - Logger::info("Shader name: " + metadata.name); + if (!spec.metadata.name.empty()) { + shader_names_[index] = spec.metadata.name; + Logger::info("Shader name: " + spec.metadata.name); } - if (!metadata.author.empty()) { - shader_authors_[index] = metadata.author; - Logger::info("Shader author: " + metadata.author); + if (!spec.metadata.author.empty()) { + shader_authors_[index] = spec.metadata.author; + Logger::info("Shader author: " + spec.metadata.author); } - // Detect self-feedback - feedback_channel_ = detectFeedbackChannel(metadata); - current_shader_uses_feedback_ = (feedback_channel_ >= 0); - - if (current_shader_uses_feedback_) { - Logger::info("Shader uses self-feedback on iChannel" + std::to_string(feedback_channel_)); - } - - GLuint vs = compileShader(GL_VERTEX_SHADER, vertexShaderSrc); - GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragSrc.c_str()); - - if (!vs || !fs) { - if (vs) glDeleteShader(vs); - if (fs) glDeleteShader(fs); - Logger::error("Shader compilation failed for: " + shaderPath.string()); - return 0; - } - - GLuint program = linkProgram(vs, fs); - glDeleteShader(vs); - glDeleteShader(fs); - - if (!program) { - Logger::error("Program linking failed for: " + shaderPath.string()); - return 0; - } - - Logger::info("Shader loaded successfully: " + shaderPath.filename().string()); - return program; + return backend_->loadShader(spec); } -// --- Funciones basadas en tu código --- void getDisplayInfo() { int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); if (displays != nullptr && num_displays > 0) { for (int i = 0; i < num_displays; ++i) { - SDL_DisplayID instance_id = displays[i]; + const SDL_DisplayID instance_id = displays[i]; const char* name = SDL_GetDisplayName(instance_id); Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown")); } @@ -632,7 +207,7 @@ void getDisplayInfo() { const SDL_DisplayMode* dm = SDL_GetCurrentDisplayMode(displays[0]); const char* first_display_name = SDL_GetDisplayName(displays[0]); display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown"; - if (dm) { + if (dm != nullptr) { display_monitor_.width = static_cast(dm->w); display_monitor_.height = static_cast(dm->h); display_monitor_.refresh_rate = static_cast(dm->refresh_rate); @@ -645,26 +220,23 @@ void getDisplayInfo() { } void setFullscreenMode() { - if (!window_) return; + if (window_ == nullptr) { return; } if (Options_video.fullscreen) { - // Intentar fullscreen if (!SDL_SetWindowFullscreen(window_, true)) { - // Fallback: volver a modo ventana si falla Logger::error(std::string("Failed to set fullscreen: ") + SDL_GetError()); Logger::info("Fallback to windowed mode 800x800"); SDL_SetWindowFullscreen(window_, false); SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT); Options_video.fullscreen = false; - SDL_ShowCursor(); // Show cursor on fallback to windowed + SDL_ShowCursor(); } else { - SDL_HideCursor(); // Hide cursor in fullscreen + SDL_HideCursor(); } } else { - // Volver a modo ventana 800x800 SDL_SetWindowFullscreen(window_, false); SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT); - SDL_ShowCursor(); // Show cursor in windowed mode + SDL_ShowCursor(); } } @@ -675,19 +247,14 @@ void toggleFullscreen() { void toggleVSync() { Options_video.vsync = !Options_video.vsync; - int result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0); - - if (result == 0) { - Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled"); - } else { - Logger::error(std::string("Failed to set VSync: ") + SDL_GetError()); + if (backend_) { + backend_->setVSync(Options_video.vsync); } } void switchShader(int direction) { - if (shader_list_.empty()) return; + if (shader_list_.empty()) { return; } - // Calcular nuevo índice con wrap-around cíclico size_t new_index = current_shader_index_; if (direction > 0) { new_index = (current_shader_index_ + 1) % shader_list_.size(); @@ -695,68 +262,56 @@ void switchShader(int direction) { new_index = (current_shader_index_ == 0) ? shader_list_.size() - 1 : current_shader_index_ - 1; } - // Intentar cargar el nuevo shader - GLuint new_program = loadAndCompileShader(new_index); - if (new_program == 0) { + if (!loadShaderAtIndex(new_index)) { Logger::error("Failed to switch shader, keeping current one"); return; } - // Éxito: eliminar programa anterior y actualizar - if (current_program_ != 0) { - glDeleteProgram(current_program_); - } - - // Destroy feedback FBO from previous shader - destroyFeedbackFBO(); - - current_program_ = new_program; current_shader_index_ = new_index; shader_start_ticks_ = SDL_GetTicks(); - updateWindowTitle(); } -// Manejo de teclas void handleDebugEvents(const SDL_Event& event) { - // evitar repetición de teclas: event.key.repeat disponible en SDL3 if (event.type == SDL_EVENT_KEY_DOWN && static_cast(event.key.repeat) == 0) { switch (event.key.key) { - case SDLK_F3: { - toggleFullscreen(); - break; - } - case SDLK_F4: { - toggleVSync(); - break; - } - case SDLK_LEFT: { - switchShader(-1); - break; - } - case SDLK_RIGHT: { - switchShader(+1); - break; - } - default: - break; + case SDLK_F3: { toggleFullscreen(); break; } + case SDLK_F4: { toggleVSync(); break; } + case SDLK_LEFT: { switchShader(-1); break; } + case SDLK_RIGHT: { switchShader(+1); break; } + default: break; } } } -// --- main (integra todo y soporta -F y argv[1] para shader path) --- +enum class BackendChoice { Auto, Gpu, OpenGL }; + +static auto createWindowForBackend(BackendChoice choice) -> SDL_Window* { + SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE; + if (choice == BackendChoice::OpenGL) { + flags |= SDL_WINDOW_OPENGL; + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + } + return SDL_CreateWindow(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT, flags); +} + int main(int argc, char** argv) { std::string shaderPath; bool fullscreenFlag = false; + BackendChoice backend_choice = BackendChoice::Auto; for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; + const std::string a = argv[i]; if (a == "-F" || a == "--fullscreen") { fullscreenFlag = true; continue; } - if (shaderPath.empty()) shaderPath = a; + if (a == "--backend=gpu") { backend_choice = BackendChoice::Gpu; continue; } + if (a == "--backend=opengl") { backend_choice = BackendChoice::OpenGL; continue; } + if (a == "--backend=auto") { backend_choice = BackendChoice::Auto; continue; } + if (shaderPath.empty()) { shaderPath = a; } } - if (shaderPath.empty()) shaderPath = "test.frag.glsl"; + if (shaderPath.empty()) { shaderPath = "test"; } Options_video.fullscreen = fullscreenFlag; - // Inicializar SDL3 auto initResult = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); if constexpr (std::is_same_v) { if (!initResult) { Logger::error(SDL_GetError()); return -1; } @@ -764,103 +319,93 @@ int main(int argc, char** argv) { if (initResult != 0) { Logger::error(SDL_GetError()); return -1; } } - // Obtener información del display antes de crear ventana getDisplayInfo(); - // Atributos GL - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + if (backend_choice != BackendChoice::OpenGL) { + window_ = createWindowForBackend(BackendChoice::Gpu); + if (window_ != nullptr) { + backend_ = Rendering::makeSdl3GpuBackend(); + if (!backend_->init(window_)) { + Logger::info("SDL3 GPU backend init failed, falling back to OpenGL"); + backend_.reset(); + SDL_DestroyWindow(window_); + window_ = nullptr; + if (backend_choice == BackendChoice::Gpu) { + SDL_Quit(); + return -1; + } + } + } + } - // Crear ventana - window_ = SDL_CreateWindow(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); - if (!window_) { Logger::error(std::string("SDL_CreateWindow error: ") + SDL_GetError()); SDL_Quit(); return -1; } + if (backend_ == nullptr) { + window_ = createWindowForBackend(BackendChoice::OpenGL); + if (window_ == nullptr) { + Logger::error(std::string("SDL_CreateWindow error: ") + SDL_GetError()); + SDL_Quit(); + return -1; + } + backend_ = Rendering::makeOpenGLBackend(); + if (!backend_->init(window_)) { + Logger::error("Failed to initialize shader backend"); + SDL_DestroyWindow(window_); + SDL_Quit(); + return -1; + } + } - // Aplicar fullscreen si el flag estaba activado setFullscreenMode(); + backend_->setVSync(Options_video.vsync); - // Crear contexto GL - SDL_GLContext glContext = SDL_GL_CreateContext(window_); - if (!glContext) { - Logger::error(std::string("SDL_GL_CreateContext error: ") + SDL_GetError()); - SDL_DestroyWindow(window_); - SDL_Quit(); - return -1; - } - - if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)) { - Logger::error("Failed to initialize GL loader"); - SDL_GL_DestroyContext(glContext); - SDL_DestroyWindow(window_); - SDL_Quit(); - return -1; - } - - // Set initial vsync state - int vsync_result = SDL_GL_SetSwapInterval(Options_video.vsync ? 1 : 0); - if (vsync_result == 0) { - Logger::info(Options_video.vsync ? "VSync enabled" : "VSync disabled"); - } else { - Logger::error(std::string("Failed to set initial VSync: ") + SDL_GetError()); - } - - // Inicializar jail_audio JA_Init(48000, SDL_AUDIO_S16LE, 2); - // Obtener directorio de recursos - std::string resources_dir = getResourcesDirectory(); + const std::string resources_dir = getResourcesDirectory(); - // Inicializar generador de números aleatorios srand(static_cast(time(nullptr))); - // Escanear directorio de música - std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music"; + const std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music"; music_list_ = scanMusicDirectory(music_directory); - // Reproducir primera canción aleatoria if (!music_list_.empty()) { playRandomMusic(); } else { Logger::info("No music files found in " + music_directory.string()); } - // Determinar carpeta de shaders - std::filesystem::path shaderFile(shaderPath); - if (shaderFile.has_parent_path()) { - shaders_directory_ = shaderFile.parent_path(); + const std::filesystem::path arg_path(shaderPath); + std::filesystem::path target_folder; + if (arg_path.has_parent_path()) { + target_folder = arg_path; + shaders_directory_ = arg_path.parent_path(); } else { shaders_directory_ = std::filesystem::path(resources_dir) / "shaders"; + target_folder = shaders_directory_ / shaderPath; } - // Escanear carpeta de shaders shader_list_ = scanShaderDirectory(shaders_directory_); if (shader_list_.empty()) { Logger::error("No shaders found in directory: " + shaders_directory_.string()); - SDL_GL_DestroyContext(glContext); + backend_->cleanup(); SDL_DestroyWindow(window_); SDL_Quit(); return -1; } - // Determinar shader inicial size_t initial_index = 0; bool found_shader = false; - // Intentar encontrar el shader especificado - std::filesystem::path target_shader = shaderFile.has_parent_path() ? shaderFile : (shaders_directory_ / shaderFile.filename()); for (size_t i = 0; i < shader_list_.size(); ++i) { - if (shader_list_[i] == target_shader) { + if (shader_list_[i].folder == target_folder) { initial_index = i; found_shader = true; break; } } - // Si no se encuentra, intentar con shader por defecto (test.frag.glsl) if (!found_shader) { - std::filesystem::path default_path = std::filesystem::path(resources_dir) / "shaders" / "test.frag.glsl"; + const std::filesystem::path default_folder = std::filesystem::path(resources_dir) / "shaders" / "test"; for (size_t i = 0; i < shader_list_.size(); ++i) { - if (shader_list_[i] == default_path) { + if (shader_list_[i].folder == default_folder) { initial_index = i; found_shader = true; break; @@ -868,18 +413,15 @@ int main(int argc, char** argv) { } } - // Si aún no se encuentra, usar el primer shader de la lista if (!found_shader) { Logger::info("Specified shader not found, using first shader in directory"); initial_index = 0; } - // Cargar shader inicial current_shader_index_ = initial_index; - current_program_ = loadAndCompileShader(current_shader_index_); - if (current_program_ == 0) { + if (!loadShaderAtIndex(current_shader_index_)) { Logger::error("Failed to load initial shader"); - SDL_GL_DestroyContext(glContext); + backend_->cleanup(); SDL_DestroyWindow(window_); SDL_Quit(); return -1; @@ -889,160 +431,63 @@ int main(int argc, char** argv) { fps_last_update_ticks_ = SDL_GetTicks(); updateWindowTitle(); - // Quad setup - float quadVertices[] = { - -1.0f, -1.0f, - 1.0f, -1.0f, - -1.0f, 1.0f, - 1.0f, 1.0f, - }; - - GLuint vao = 0, vbo = 0; - glGenVertexArrays(1, &vao); - glGenBuffers(1, &vbo); - glBindVertexArray(vao); - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); - bool running = true; while (running) { - // Update FPS counter fps_frame_count_++; - Uint32 current_ticks = SDL_GetTicks(); + const Uint32 current_ticks = SDL_GetTicks(); - // Update FPS display every 500ms if (current_ticks - fps_last_update_ticks_ >= 500) { - float elapsed_seconds = (current_ticks - fps_last_update_ticks_) / 1000.0f; - current_fps_ = fps_frame_count_ / elapsed_seconds; + const float elapsed_seconds = static_cast(current_ticks - fps_last_update_ticks_) / 1000.0f; + current_fps_ = static_cast(fps_frame_count_) / elapsed_seconds; fps_frame_count_ = 0; fps_last_update_ticks_ = current_ticks; updateWindowTitle(); } - // Actualizar audio (necesario para streaming y loops) JA_Update(); - // Verificar si la música actual terminó y reproducir siguiente aleatoria if (!music_list_.empty() && JA_GetMusicState() == JA_MUSIC_STOPPED) { playRandomMusic(); } SDL_Event e; while (SDL_PollEvent(&e)) { - if (e.type == SDL_EVENT_QUIT) running = false; - else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) running = false; - else if (e.type == SDL_EVENT_KEY_DOWN) { - // Escape cierra la app - if (e.key.key == SDLK_ESCAPE) running = false; - // handle your debug keys + if (e.type == SDL_EVENT_QUIT) { + running = false; + } else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + running = false; + } else if (e.type == SDL_EVENT_KEY_DOWN) { + if (e.key.key == SDLK_ESCAPE) { running = false; } handleDebugEvents(e); - } else if (e.type == SDL_EVENT_WINDOW_RESIZED) { - // opcional: podrías actualizar algo con new size } } - int w, h; + Rendering::ShaderUniforms uniforms; + uniforms.iTime = static_cast(SDL_GetTicks() - shader_start_ticks_) / 1000.0f; + + int w = 0; + int h = 0; SDL_GetWindowSize(window_, &w, &h); + uniforms.iResolutionX = static_cast(w); + uniforms.iResolutionY = static_cast(h); - // Create/resize feedback FBO if needed - if (current_shader_uses_feedback_) { - if (feedback_fbo_ == 0 || current_window_width_ != w || current_window_height_ != h) { - createFeedbackFBO(w, h); - current_window_width_ = w; - current_window_height_ = h; - } - } + backend_->render(uniforms); - glUseProgram(current_program_); - - // Obtener uniform locations - GLint locRes = glGetUniformLocation(current_program_, "iResolution"); - GLint locTime = glGetUniformLocation(current_program_, "iTime"); - - float t = (SDL_GetTicks() - shader_start_ticks_) / 1000.0f; - - // === FEEDBACK RENDERING === - if (current_shader_uses_feedback_) { - // Step 1: Bind feedback texture to iChannel - std::string channelName = "iChannel" + std::to_string(feedback_channel_); - GLint locChannel = glGetUniformLocation(current_program_, channelName.c_str()); - - if (locChannel >= 0) { - glActiveTexture(GL_TEXTURE0 + feedback_channel_); - glBindTexture(GL_TEXTURE_2D, feedback_texture_); - glUniform1i(locChannel, feedback_channel_); - } - - // Step 2: Render to feedback FBO - glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_); - glViewport(0, 0, w, h); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - if (locRes >= 0) glUniform2f(locRes, float(w), float(h)); - if (locTime >= 0) glUniform1f(locTime, t); - - glBindVertexArray(vao); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glBindVertexArray(0); - - // Step 3: Render to screen (using the same FBO texture) - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glViewport(0, 0, w, h); - - if (locRes >= 0) glUniform2f(locRes, float(w), float(h)); - if (locTime >= 0) glUniform1f(locTime, t); - - glBindVertexArray(vao); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glBindVertexArray(0); - - // Unbind texture - glActiveTexture(GL_TEXTURE0 + feedback_channel_); - glBindTexture(GL_TEXTURE_2D, 0); - } else { - // === NORMAL RENDERING (no feedback) === - glViewport(0, 0, w, h); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - if (locRes >= 0) glUniform2f(locRes, float(w), float(h)); - if (locTime >= 0) glUniform1f(locTime, t); - - glBindVertexArray(vao); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - glBindVertexArray(0); - } - - SDL_GL_SwapWindow(window_); if (!Options_video.vsync) { - SDL_Delay(1); // Prevent CPU spinning when vsync is off + SDL_Delay(1); } } - // Cleanup - glDeleteBuffers(1, &vbo); - glDeleteVertexArrays(1, &vao); - if (current_program_ != 0) { - glDeleteProgram(current_program_); - } + backend_->cleanup(); + backend_.reset(); - // Cleanup feedback FBO - destroyFeedbackFBO(); - - // Cleanup audio - if (current_music_) { + if (current_music_ != nullptr) { JA_DeleteMusic(current_music_); current_music_ = nullptr; } JA_Quit(); - SDL_GL_DestroyContext(glContext); SDL_DestroyWindow(window_); SDL_Quit(); return 0; diff --git a/src/rendering/opengl_shader_backend.cpp b/src/rendering/opengl_shader_backend.cpp new file mode 100644 index 0000000..b602e6e --- /dev/null +++ b/src/rendering/opengl_shader_backend.cpp @@ -0,0 +1,297 @@ +#include "rendering/opengl_shader_backend.hpp" + +#include +#include + +namespace Rendering { + + namespace { + + constexpr const char* VERTEX_SHADER_SRC = R"glsl( +#version 330 core +layout(location = 0) in vec2 aPos; +out vec2 vUV; +void main() { + vUV = aPos * 0.5 + 0.5; + gl_Position = vec4(aPos, 0.0, 1.0); +} +)glsl"; + + void logInfo(const std::string& msg) { std::cout << "[INFO] " << msg << '\n'; } + void logError(const std::string& msg) { std::cerr << "[ERROR] " << msg << '\n'; } + + auto compileShader(GLenum type, const char* src) -> GLuint { + const GLuint s = glCreateShader(type); + glShaderSource(s, 1, &src, nullptr); + glCompileShader(s); + GLint ok = 0; + glGetShaderiv(s, GL_COMPILE_STATUS, &ok); + if (ok == 0) { + GLint len = 0; + glGetShaderiv(s, GL_INFO_LOG_LENGTH, &len); + std::string log(len > 0 ? len : 1, ' '); + glGetShaderInfoLog(s, len, nullptr, log.data()); + logError("Shader compile error: " + log); + glDeleteShader(s); + return 0; + } + return s; + } + + auto linkProgram(GLuint vs, GLuint fs) -> GLuint { + const GLuint p = glCreateProgram(); + glAttachShader(p, vs); + glAttachShader(p, fs); + glLinkProgram(p); + GLint ok = 0; + glGetProgramiv(p, GL_LINK_STATUS, &ok); + if (ok == 0) { + GLint len = 0; + glGetProgramiv(p, GL_INFO_LOG_LENGTH, &len); + std::string log(len > 0 ? len : 1, ' '); + glGetProgramInfoLog(p, len, nullptr, log.data()); + logError("Program link error: " + log); + glDeleteProgram(p); + return 0; + } + return p; + } + + auto detectFeedbackChannel(const ShaderMetadata& metadata) -> int { + if (metadata.iChannel0 == "self") { return 0; } + if (metadata.iChannel1 == "self") { return 1; } + if (metadata.iChannel2 == "self") { return 2; } + if (metadata.iChannel3 == "self") { return 3; } + return -1; + } + + } // namespace + + OpenGLShaderBackend::~OpenGLShaderBackend() { cleanup(); } + + auto OpenGLShaderBackend::init(SDL_Window* window) -> bool { + window_ = window; + + gl_context_ = SDL_GL_CreateContext(window_); + if (gl_context_ == nullptr) { + logError(std::string("SDL_GL_CreateContext error: ") + SDL_GetError()); + return false; + } + + if (gladLoadGLLoader(reinterpret_cast(SDL_GL_GetProcAddress)) == 0) { + logError("Failed to initialize GL loader"); + SDL_GL_DestroyContext(gl_context_); + gl_context_ = nullptr; + return false; + } + + constexpr float QUAD_VERTICES[] = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + 1.0f, 1.0f, + }; + + glGenVertexArrays(1, &vao_); + glGenBuffers(1, &vbo_); + glBindVertexArray(vao_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); + glBufferData(GL_ARRAY_BUFFER, sizeof(QUAD_VERTICES), QUAD_VERTICES, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + return true; + } + + auto OpenGLShaderBackend::loadShader(const ShaderProgramSpec& spec) -> bool { + const std::filesystem::path source_path = spec.folder / (spec.base_name + ".gl.glsl"); + + std::string fragSrc; + if (!loadFileToString(source_path, fragSrc)) { + logError("Failed to load shader file: " + source_path.string()); + return false; + } + + const int feedback = detectFeedbackChannel(spec.metadata); + + const GLuint vs = compileShader(GL_VERTEX_SHADER, VERTEX_SHADER_SRC); + const GLuint fs = compileShader(GL_FRAGMENT_SHADER, fragSrc.c_str()); + + if (vs == 0 || fs == 0) { + if (vs != 0) { glDeleteShader(vs); } + if (fs != 0) { glDeleteShader(fs); } + logError("Shader compilation failed for: " + source_path.string()); + return false; + } + + const GLuint program = linkProgram(vs, fs); + glDeleteShader(vs); + glDeleteShader(fs); + + if (program == 0) { + logError("Program linking failed for: " + source_path.string()); + return false; + } + + if (current_program_ != 0) { + glDeleteProgram(current_program_); + } + current_program_ = program; + + destroyFeedbackFBO(); + feedback_channel_ = feedback; + current_shader_uses_feedback_ = (feedback >= 0); + + if (current_shader_uses_feedback_) { + logInfo("Shader uses self-feedback on iChannel" + std::to_string(feedback_channel_)); + } + + logInfo("Shader loaded successfully: " + spec.base_name); + return true; + } + + auto OpenGLShaderBackend::createFeedbackFBO(int width, int height) -> bool { + destroyFeedbackFBO(); + + glGenTextures(1, &feedback_texture_); + glBindTexture(GL_TEXTURE_2D, feedback_texture_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + std::vector black(static_cast(width) * static_cast(height) * 4U, 0.0f); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, black.data()); + glBindTexture(GL_TEXTURE_2D, 0); + + glGenFramebuffers(1, &feedback_fbo_); + glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, feedback_texture_, 0); + + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + logError("Feedback FBO creation failed: " + std::to_string(status)); + destroyFeedbackFBO(); + return false; + } + + feedback_width_ = width; + feedback_height_ = height; + logInfo("Created feedback FBO (" + std::to_string(width) + "x" + std::to_string(height) + ")"); + return true; + } + + void OpenGLShaderBackend::destroyFeedbackFBO() { + if (feedback_fbo_ != 0) { + glDeleteFramebuffers(1, &feedback_fbo_); + feedback_fbo_ = 0; + } + if (feedback_texture_ != 0) { + glDeleteTextures(1, &feedback_texture_); + feedback_texture_ = 0; + } + feedback_width_ = 0; + feedback_height_ = 0; + } + + void OpenGLShaderBackend::render(const ShaderUniforms& uniforms) { + if (current_program_ == 0 || window_ == nullptr) { return; } + + int w = 0; + int h = 0; + SDL_GetWindowSize(window_, &w, &h); + + if (current_shader_uses_feedback_) { + if (feedback_fbo_ == 0 || feedback_width_ != w || feedback_height_ != h) { + createFeedbackFBO(w, h); + } + } + + glUseProgram(current_program_); + + const GLint locRes = glGetUniformLocation(current_program_, "iResolution"); + const GLint locTime = glGetUniformLocation(current_program_, "iTime"); + + if (current_shader_uses_feedback_) { + const std::string channel_name = "iChannel" + std::to_string(feedback_channel_); + const GLint locChannel = glGetUniformLocation(current_program_, channel_name.c_str()); + + if (locChannel >= 0) { + glActiveTexture(GL_TEXTURE0 + feedback_channel_); + glBindTexture(GL_TEXTURE_2D, feedback_texture_); + glUniform1i(locChannel, feedback_channel_); + } + + glBindFramebuffer(GL_FRAMEBUFFER, feedback_fbo_); + glViewport(0, 0, w, h); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + if (locRes >= 0) { glUniform2f(locRes, static_cast(w), static_cast(h)); } + if (locTime >= 0) { glUniform1f(locTime, uniforms.iTime); } + + glBindVertexArray(vao_); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, w, h); + + if (locRes >= 0) { glUniform2f(locRes, static_cast(w), static_cast(h)); } + if (locTime >= 0) { glUniform1f(locTime, uniforms.iTime); } + + glBindVertexArray(vao_); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + + glActiveTexture(GL_TEXTURE0 + feedback_channel_); + glBindTexture(GL_TEXTURE_2D, 0); + } else { + glViewport(0, 0, w, h); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + if (locRes >= 0) { glUniform2f(locRes, static_cast(w), static_cast(h)); } + if (locTime >= 0) { glUniform1f(locTime, uniforms.iTime); } + + glBindVertexArray(vao_); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + } + + SDL_GL_SwapWindow(window_); + } + + void OpenGLShaderBackend::setVSync(bool vsync) { + const int result = SDL_GL_SetSwapInterval(vsync ? 1 : 0); + if (result == 0) { + logInfo(vsync ? "VSync enabled" : "VSync disabled"); + } else { + logError(std::string("Failed to set VSync: ") + SDL_GetError()); + } + } + + void OpenGLShaderBackend::cleanup() { + if (gl_context_ == nullptr) { return; } + + if (vbo_ != 0) { glDeleteBuffers(1, &vbo_); vbo_ = 0; } + if (vao_ != 0) { glDeleteVertexArrays(1, &vao_); vao_ = 0; } + if (current_program_ != 0) { glDeleteProgram(current_program_); current_program_ = 0; } + destroyFeedbackFBO(); + current_shader_uses_feedback_ = false; + feedback_channel_ = -1; + + SDL_GL_DestroyContext(gl_context_); + gl_context_ = nullptr; + window_ = nullptr; + } + + auto makeOpenGLBackend() -> std::unique_ptr { + return std::make_unique(); + } + +} // namespace Rendering diff --git a/src/rendering/opengl_shader_backend.hpp b/src/rendering/opengl_shader_backend.hpp new file mode 100644 index 0000000..975763a --- /dev/null +++ b/src/rendering/opengl_shader_backend.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "rendering/shader_backend.hpp" + +namespace Rendering { + + class OpenGLShaderBackend final : public IShaderBackend { + public: + OpenGLShaderBackend() = default; + ~OpenGLShaderBackend() override; + + auto init(SDL_Window* window) -> bool override; + auto loadShader(const ShaderProgramSpec& spec) -> bool override; + void render(const ShaderUniforms& uniforms) override; + void setVSync(bool vsync) override; + void cleanup() override; + [[nodiscard]] auto driverName() const -> std::string override { return "OpenGL 3.3"; } + + private: + auto createFeedbackFBO(int width, int height) -> bool; + void destroyFeedbackFBO(); + + SDL_Window* window_{nullptr}; + SDL_GLContext gl_context_{nullptr}; + + GLuint vao_{0}; + GLuint vbo_{0}; + GLuint current_program_{0}; + + GLuint feedback_fbo_{0}; + GLuint feedback_texture_{0}; + bool current_shader_uses_feedback_{false}; + int feedback_channel_{-1}; + int feedback_width_{0}; + int feedback_height_{0}; + }; + +} // namespace Rendering diff --git a/src/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp b/src/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp new file mode 100644 index 0000000..748c54e --- /dev/null +++ b/src/rendering/sdl3gpu/sdl3gpu_shader_backend.cpp @@ -0,0 +1,229 @@ +#include "rendering/sdl3gpu/sdl3gpu_shader_backend.hpp" + +#include + +#include "rendering/sdl3gpu/shader_factory.hpp" + +namespace Rendering { + + namespace { + + void logInfo(const std::string& msg) { std::cout << "[INFO] " << msg << '\n'; } + void logError(const std::string& msg) { std::cerr << "[ERROR] " << msg << '\n'; } + +#ifdef __APPLE__ + constexpr SDL_GPUShaderFormat SHADER_FORMAT = SDL_GPU_SHADERFORMAT_MSL; + constexpr const char* VERTEX_ENTRY = "passthrough_vs"; + constexpr const char* FRAGMENT_ENTRY = "test_fs"; // overridden per-shader (see loadShader) + constexpr const char* VERTEX_SUFFIX = ".vert.msl"; + constexpr const char* FRAGMENT_SUFFIX = ".frag.msl"; +#else + constexpr SDL_GPUShaderFormat SHADER_FORMAT = SDL_GPU_SHADERFORMAT_SPIRV; + constexpr const char* VERTEX_ENTRY = "main"; + constexpr const char* FRAGMENT_ENTRY = "main"; + constexpr const char* VERTEX_SUFFIX = ".vert.spv"; + constexpr const char* FRAGMENT_SUFFIX = ".frag.spv"; +#endif + + } // namespace + + Sdl3GpuShaderBackend::~Sdl3GpuShaderBackend() { cleanup(); } + + auto Sdl3GpuShaderBackend::init(SDL_Window* window) -> bool { + window_ = window; + return createDevice(); + } + + auto Sdl3GpuShaderBackend::createDevice() -> bool { + device_ = SDL_CreateGPUDevice(SHADER_FORMAT, false, nullptr); + if (device_ == nullptr) { + logError(std::string("SDL_CreateGPUDevice failed: ") + SDL_GetError()); + return false; + } + if (!SDL_ClaimWindowForGPUDevice(device_, window_)) { + logError(std::string("SDL_ClaimWindowForGPUDevice failed: ") + SDL_GetError()); + SDL_DestroyGPUDevice(device_); + device_ = nullptr; + return false; + } + SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode()); + + const char* name = SDL_GetGPUDeviceDriver(device_); + driver_name_ = (name != nullptr) ? std::string("SDL3 GPU/") + name : "SDL3 GPU"; + logInfo("GPU driver: " + driver_name_); + return true; + } + + auto Sdl3GpuShaderBackend::loadVertexShaderFor(const ShaderProgramSpec& spec) -> bool { + if (vertex_shader_ != nullptr) { return true; } + + const std::filesystem::path common_dir = spec.folder.parent_path() / "_common"; + const std::filesystem::path vertex_path = common_dir / (std::string("passthrough") + VERTEX_SUFFIX); + + vertex_shader_ = Sdl3Gpu::loadShaderFromFile(device_, vertex_path, SHADER_FORMAT, + VERTEX_ENTRY, SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); + if (vertex_shader_ == nullptr) { + logError("Failed to load shared vertex shader: " + vertex_path.string() + " (" + SDL_GetError() + ")"); + return false; + } + logInfo("Loaded shared vertex shader: " + vertex_path.filename().string()); + return true; + } + + auto Sdl3GpuShaderBackend::buildPipeline(SDL_GPUShader* fragment) -> SDL_GPUGraphicsPipeline* { + const SDL_GPUTextureFormat SWAPCHAIN_FORMAT = SDL_GetGPUSwapchainTextureFormat(device_, window_); + + SDL_GPUColorTargetBlendState no_blend{}; + SDL_GPUColorTargetDescription color_target{}; + color_target.format = SWAPCHAIN_FORMAT; + color_target.blend_state = no_blend; + + SDL_GPUGraphicsPipelineCreateInfo info{}; + info.vertex_shader = vertex_shader_; + info.fragment_shader = fragment; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.target_info.num_color_targets = 1; + info.target_info.color_target_descriptions = &color_target; + + SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device_, &info); + if (pipeline == nullptr) { + logError(std::string("SDL_CreateGPUGraphicsPipeline failed: ") + SDL_GetError()); + } + return pipeline; + } + + auto Sdl3GpuShaderBackend::loadShader(const ShaderProgramSpec& spec) -> bool { + if (device_ == nullptr) { return false; } + + if (!loadVertexShaderFor(spec)) { return false; } + + const std::filesystem::path frag_path = spec.folder / (spec.base_name + FRAGMENT_SUFFIX); + +#ifdef __APPLE__ + const std::string entry = spec.base_name + "_fs"; + const char* fragment_entry = entry.c_str(); +#else + const char* fragment_entry = FRAGMENT_ENTRY; +#endif + + SDL_GPUShader* new_fragment = Sdl3Gpu::loadShaderFromFile(device_, frag_path, SHADER_FORMAT, + fragment_entry, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 1); + if (new_fragment == nullptr) { + logError("Failed to load fragment shader: " + frag_path.string() + " (" + SDL_GetError() + ")"); + return false; + } + + SDL_GPUGraphicsPipeline* new_pipeline = buildPipeline(new_fragment); + if (new_pipeline == nullptr) { + SDL_ReleaseGPUShader(device_, new_fragment); + return false; + } + + SDL_WaitForGPUIdle(device_); + if (pipeline_ != nullptr) { SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_); } + if (fragment_shader_ != nullptr) { SDL_ReleaseGPUShader(device_, fragment_shader_); } + + pipeline_ = new_pipeline; + fragment_shader_ = new_fragment; + logInfo("Shader loaded successfully: " + spec.base_name); + return true; + } + + void Sdl3GpuShaderBackend::render(const ShaderUniforms& uniforms) { + if (device_ == nullptr || pipeline_ == nullptr) { return; } + + SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_); + if (cmd == nullptr) { return; } + + SDL_GPUTexture* swapchain = nullptr; + Uint32 sw = 0; + Uint32 sh = 0; + if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh) || swapchain == nullptr) { + SDL_SubmitGPUCommandBuffer(cmd); + return; + } + + SDL_GPUColorTargetInfo color_target{}; + color_target.texture = swapchain; + color_target.load_op = SDL_GPU_LOADOP_CLEAR; + color_target.store_op = SDL_GPU_STOREOP_STORE; + color_target.clear_color = {.r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f}; + + SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); + if (pass != nullptr) { + SDL_GPUViewport vp{}; + vp.x = 0.0f; + vp.y = 0.0f; + vp.w = static_cast(sw); + vp.h = static_cast(sh); + vp.min_depth = 0.0f; + vp.max_depth = 1.0f; + SDL_SetGPUViewport(pass, &vp); + + SDL_BindGPUGraphicsPipeline(pass, pipeline_); + + UniformsStd140 ubo{}; + ubo.iTime = uniforms.iTime; + ubo.iResolutionX = uniforms.iResolutionX; + ubo.iResolutionY = uniforms.iResolutionY; + SDL_PushGPUFragmentUniformData(cmd, 0, &ubo, sizeof(ubo)); + + SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); + SDL_EndGPURenderPass(pass); + } + + SDL_SubmitGPUCommandBuffer(cmd); + } + + void Sdl3GpuShaderBackend::setVSync(bool vsync) { + vsync_ = vsync; + if (device_ != nullptr && window_ != nullptr) { + SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode()); + logInfo(vsync ? "VSync enabled" : "VSync disabled"); + } + } + + auto Sdl3GpuShaderBackend::bestPresentMode() const -> SDL_GPUPresentMode { + if (vsync_) { return SDL_GPU_PRESENTMODE_VSYNC; } + if (device_ != nullptr && window_ != nullptr) { + if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_IMMEDIATE)) { + return SDL_GPU_PRESENTMODE_IMMEDIATE; + } + if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_MAILBOX)) { + return SDL_GPU_PRESENTMODE_MAILBOX; + } + } + return SDL_GPU_PRESENTMODE_VSYNC; + } + + void Sdl3GpuShaderBackend::cleanup() { + if (device_ == nullptr) { return; } + + SDL_WaitForGPUIdle(device_); + + if (pipeline_ != nullptr) { + SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_); + pipeline_ = nullptr; + } + if (fragment_shader_ != nullptr) { + SDL_ReleaseGPUShader(device_, fragment_shader_); + fragment_shader_ = nullptr; + } + if (vertex_shader_ != nullptr) { + SDL_ReleaseGPUShader(device_, vertex_shader_); + vertex_shader_ = nullptr; + } + + if (window_ != nullptr) { + SDL_ReleaseWindowFromGPUDevice(device_, window_); + } + SDL_DestroyGPUDevice(device_); + device_ = nullptr; + window_ = nullptr; + } + + auto makeSdl3GpuBackend() -> std::unique_ptr { + return std::make_unique(); + } + +} // namespace Rendering diff --git a/src/rendering/sdl3gpu/sdl3gpu_shader_backend.hpp b/src/rendering/sdl3gpu/sdl3gpu_shader_backend.hpp new file mode 100644 index 0000000..829f841 --- /dev/null +++ b/src/rendering/sdl3gpu/sdl3gpu_shader_backend.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include "rendering/shader_backend.hpp" + +namespace Rendering { + + class Sdl3GpuShaderBackend final : public IShaderBackend { + public: + Sdl3GpuShaderBackend() = default; + ~Sdl3GpuShaderBackend() override; + + auto init(SDL_Window* window) -> bool override; + auto loadShader(const ShaderProgramSpec& spec) -> bool override; + void render(const ShaderUniforms& uniforms) override; + void setVSync(bool vsync) override; + void cleanup() override; + [[nodiscard]] auto driverName() const -> std::string override { return driver_name_; } + + private: + struct UniformsStd140 { + float iTime{0.0f}; + float pad0{0.0f}; + float iResolutionX{0.0f}; + float iResolutionY{0.0f}; + }; + + auto createDevice() -> bool; + auto loadVertexShaderFor(const ShaderProgramSpec& spec) -> bool; + auto buildPipeline(SDL_GPUShader* fragment) -> SDL_GPUGraphicsPipeline*; + [[nodiscard]] auto bestPresentMode() const -> SDL_GPUPresentMode; + + SDL_Window* window_{nullptr}; + SDL_GPUDevice* device_{nullptr}; + SDL_GPUShader* vertex_shader_{nullptr}; + SDL_GPUShader* fragment_shader_{nullptr}; + SDL_GPUGraphicsPipeline* pipeline_{nullptr}; + + bool vsync_{true}; + std::string driver_name_; + }; + +} // namespace Rendering diff --git a/src/rendering/sdl3gpu/shader_factory.hpp b/src/rendering/sdl3gpu/shader_factory.hpp new file mode 100644 index 0000000..a8f637d --- /dev/null +++ b/src/rendering/sdl3gpu/shader_factory.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +namespace Rendering::Sdl3Gpu { + + // Loads a compiled shader binary or source from disk and creates an SDL_GPUShader. + // For SPIR-V: pass the .spv path with format = SDL_GPU_SHADERFORMAT_SPIRV. + // For MSL: pass the .msl text path with format = SDL_GPU_SHADERFORMAT_MSL. + inline auto loadShaderFromFile(SDL_GPUDevice* device, + const std::filesystem::path& path, + SDL_GPUShaderFormat format, + const char* entrypoint, + SDL_GPUShaderStage stage, + Uint32 num_samplers, + Uint32 num_uniform_buffers) -> SDL_GPUShader* { + std::size_t size = 0; + void* data = SDL_LoadFile(path.string().c_str(), &size); + if (data == nullptr) { + return nullptr; + } + + SDL_GPUShaderCreateInfo info{}; + info.code_size = size; + info.code = static_cast(data); + info.entrypoint = entrypoint; + info.format = format; + info.stage = stage; + info.num_samplers = num_samplers; + info.num_storage_textures = 0; + info.num_storage_buffers = 0; + info.num_uniform_buffers = num_uniform_buffers; + + SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info); + SDL_free(data); + return shader; + } + +} // namespace Rendering::Sdl3Gpu diff --git a/src/rendering/shader_backend.cpp b/src/rendering/shader_backend.cpp new file mode 100644 index 0000000..9f16e61 --- /dev/null +++ b/src/rendering/shader_backend.cpp @@ -0,0 +1,99 @@ +#include "rendering/shader_backend.hpp" + +#include +#include +#include + +namespace Rendering { + + namespace { + + auto trimString(const std::string& str) -> std::string { + const std::size_t start = str.find_first_not_of(" \t\r\n"); + const std::size_t end = str.find_last_not_of(" \t\r\n"); + if (start != std::string::npos && end != std::string::npos) { + return str.substr(start, end - start + 1); + } + return ""; + } + + } // namespace + + auto loadFileToString(const std::filesystem::path& path, std::string& out) -> bool { + std::ifstream ifs(path, std::ios::in | std::ios::binary); + if (!ifs) { return false; } + std::ostringstream ss; + ss << ifs.rdbuf(); + out = ss.str(); + return true; + } + + auto parseMetaFile(const std::filesystem::path& meta_path) -> ShaderMetadata { + ShaderMetadata metadata; + std::ifstream ifs(meta_path); + if (!ifs) { return metadata; } + + std::string line; + while (std::getline(ifs, line)) { + const std::size_t colon = line.find(':'); + if (colon == std::string::npos) { continue; } + + std::string key = line.substr(0, colon); + std::string value = trimString(line.substr(colon + 1)); + + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + key = trimString(key); + + if (key == "name") { metadata.name = value; } + else if (key == "author") { metadata.author = value; } + else if (key == "ichannel0") { metadata.iChannel0 = value; } + else if (key == "ichannel1") { metadata.iChannel1 = value; } + else if (key == "ichannel2") { metadata.iChannel2 = value; } + else if (key == "ichannel3") { metadata.iChannel3 = value; } + } + return metadata; + } + + auto extractShaderMetadata(const std::string& source) -> ShaderMetadata { + ShaderMetadata metadata; + + std::istringstream stream(source); + std::string line; + int line_count = 0; + constexpr int MAX_LINES_TO_CHECK = 30; + + while (std::getline(stream, line) && line_count < MAX_LINES_TO_CHECK) { + line_count++; + + const std::size_t pos = line.find("//"); + if (pos == std::string::npos) { continue; } + + const std::string comment = line.substr(pos + 2); + std::string lower = comment; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + auto valueAfterColon = [&]() { + return trimString(comment.substr(comment.find(':') + 1)); + }; + + if (lower.find("name:") != std::string::npos) { + metadata.name = valueAfterColon(); + } else if (lower.find("author:") != std::string::npos) { + metadata.author = valueAfterColon(); + } else if (lower.find("ichannel0:") != std::string::npos) { + metadata.iChannel0 = valueAfterColon(); + } else if (lower.find("ichannel1:") != std::string::npos) { + metadata.iChannel1 = valueAfterColon(); + } else if (lower.find("ichannel2:") != std::string::npos) { + metadata.iChannel2 = valueAfterColon(); + } else if (lower.find("ichannel3:") != std::string::npos) { + metadata.iChannel3 = valueAfterColon(); + } + } + + return metadata; + } + +} // namespace Rendering diff --git a/src/rendering/shader_backend.hpp b/src/rendering/shader_backend.hpp new file mode 100644 index 0000000..c4a445d --- /dev/null +++ b/src/rendering/shader_backend.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include +#include + +namespace Rendering { + + struct ShaderMetadata { + std::string name; + std::string author; + std::string iChannel0{"none"}; + std::string iChannel1{"none"}; + std::string iChannel2{"none"}; + std::string iChannel3{"none"}; + }; + + struct ShaderUniforms { + float iTime{0.0f}; + float iResolutionX{0.0f}; + float iResolutionY{0.0f}; + }; + + struct ShaderProgramSpec { + std::filesystem::path folder; + std::string base_name; + ShaderMetadata metadata; + }; + + class IShaderBackend { + public: + IShaderBackend() = default; + virtual ~IShaderBackend() = default; + + IShaderBackend(const IShaderBackend&) = delete; + IShaderBackend(IShaderBackend&&) = delete; + auto operator=(const IShaderBackend&) -> IShaderBackend& = delete; + auto operator=(IShaderBackend&&) -> IShaderBackend& = delete; + + virtual auto init(SDL_Window* window) -> bool = 0; + virtual auto loadShader(const ShaderProgramSpec& spec) -> bool = 0; + virtual void render(const ShaderUniforms& uniforms) = 0; + virtual void setVSync(bool vsync) = 0; + virtual void cleanup() = 0; + [[nodiscard]] virtual auto driverName() const -> std::string = 0; + }; + + [[nodiscard]] auto makeOpenGLBackend() -> std::unique_ptr; + [[nodiscard]] auto makeSdl3GpuBackend() -> std::unique_ptr; + + [[nodiscard]] auto extractShaderMetadata(const std::string& source) -> ShaderMetadata; + [[nodiscard]] auto loadFileToString(const std::filesystem::path& path, std::string& out) -> bool; + [[nodiscard]] auto parseMetaFile(const std::filesystem::path& meta_path) -> ShaderMetadata; + +} // namespace Rendering diff --git a/tools/shaders/compile_shaders.cmake b/tools/shaders/compile_shaders.cmake new file mode 100644 index 0000000..4231631 --- /dev/null +++ b/tools/shaders/compile_shaders.cmake @@ -0,0 +1,70 @@ +# compile_shaders.cmake — invoked via `cmake -P`. +# +# Required cache vars: +# GLSLC — path to the glslc executable. +# SHADERS_DIR — path to the shaders/ directory. +# +# Walks SHADERS_DIR for: +# _common/passthrough.vk.glsl -> _common/passthrough.vert.spv +# /.vk.glsl -> /.frag.spv +# +# Shaders are recompiled only if the .spv is missing or older than its source. + +if(NOT GLSLC) + message(FATAL_ERROR "compile_shaders.cmake: GLSLC not provided") +endif() +if(NOT SHADERS_DIR) + message(FATAL_ERROR "compile_shaders.cmake: SHADERS_DIR not provided") +endif() +if(NOT EXISTS "${SHADERS_DIR}") + message(FATAL_ERROR "compile_shaders.cmake: SHADERS_DIR does not exist: ${SHADERS_DIR}") +endif() + +function(_compile_to_spv SOURCE STAGE OUTPUT) + if(EXISTS "${OUTPUT}") + file(TIMESTAMP "${SOURCE}" SRC_T "%s") + file(TIMESTAMP "${OUTPUT}" OUT_T "%s") + if(NOT "${SRC_T}" STREQUAL "" AND NOT "${OUT_T}" STREQUAL "") + if(SRC_T LESS_EQUAL OUT_T) + return() + endif() + endif() + endif() + message(STATUS "glslc (${STAGE}) ${SOURCE} -> ${OUTPUT}") + execute_process( + COMMAND "${GLSLC}" "-fshader-stage=${STAGE}" "${SOURCE}" -o "${OUTPUT}" + RESULT_VARIABLE RC + OUTPUT_VARIABLE STDOUT + ERROR_VARIABLE STDERR + ) + if(NOT RC EQUAL 0) + message(FATAL_ERROR "glslc failed for ${SOURCE}:\n${STDERR}") + endif() +endfunction() + +set(VERT_SOURCE "${SHADERS_DIR}/_common/passthrough.vk.glsl") +set(VERT_OUTPUT "${SHADERS_DIR}/_common/passthrough.vert.spv") +if(EXISTS "${VERT_SOURCE}") + _compile_to_spv("${VERT_SOURCE}" "vert" "${VERT_OUTPUT}") +else() + message(WARNING "Missing shared vertex shader: ${VERT_SOURCE}") +endif() + +file(GLOB SHADER_DIRS LIST_DIRECTORIES true RELATIVE "${SHADERS_DIR}" "${SHADERS_DIR}/*") +foreach(DIR ${SHADER_DIRS}) + set(ABS_DIR "${SHADERS_DIR}/${DIR}") + if(NOT IS_DIRECTORY "${ABS_DIR}") + continue() + endif() + string(SUBSTRING "${DIR}" 0 1 FIRST_CHAR) + if(FIRST_CHAR STREQUAL "_" OR FIRST_CHAR STREQUAL ".") + continue() + endif() + + set(VK_SOURCE "${ABS_DIR}/${DIR}.vk.glsl") + set(SPV_OUTPUT "${ABS_DIR}/${DIR}.frag.spv") + + if(EXISTS "${VK_SOURCE}") + _compile_to_spv("${VK_SOURCE}" "frag" "${SPV_OUTPUT}") + endif() +endforeach()