Add SDL3 GPU backend (Vulkan/Metal) with OpenGL fallback

This commit is contained in:
2026-05-04 11:45:43 +02:00
parent 0a03509323
commit f18d6143d1
42 changed files with 1273 additions and 738 deletions

View File

@@ -0,0 +1,297 @@
#include "rendering/opengl_shader_backend.hpp"
#include <iostream>
#include <vector>
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<GLADloadproc>(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<float> black(static_cast<std::size_t>(width) * static_cast<std::size_t>(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<float>(w), static_cast<float>(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<float>(w), static_cast<float>(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<float>(w), static_cast<float>(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<IShaderBackend> {
return std::make_unique<OpenGLShaderBackend>();
}
} // namespace Rendering

View File

@@ -0,0 +1,40 @@
#pragma once
#include <glad/glad.h>
#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

View File

@@ -0,0 +1,229 @@
#include "rendering/sdl3gpu/sdl3gpu_shader_backend.hpp"
#include <iostream>
#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<float>(sw);
vp.h = static_cast<float>(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<IShaderBackend> {
return std::make_unique<Sdl3GpuShaderBackend>();
}
} // namespace Rendering

View File

@@ -0,0 +1,44 @@
#pragma once
#include <SDL3/SDL.h>
#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

View File

@@ -0,0 +1,41 @@
#pragma once
#include <SDL3/SDL.h>
#include <filesystem>
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<Uint8*>(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

View File

@@ -0,0 +1,99 @@
#include "rendering/shader_backend.hpp"
#include <algorithm>
#include <fstream>
#include <sstream>
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<char>(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<char>(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

View File

@@ -0,0 +1,57 @@
#pragma once
#include <SDL3/SDL.h>
#include <filesystem>
#include <memory>
#include <string>
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<IShaderBackend>;
[[nodiscard]] auto makeSdl3GpuBackend() -> std::unique_ptr<IShaderBackend>;
[[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