Path SDL3 GPU amb PostFX passthrough; Screen amb init/shutdown explicit (v0.4.0)
This commit is contained in:
1 parent
0348acb0eb
commit
6c8ef9ff44
18 files changed
+14210
-35
No files matched your search
@@ -79,6 +79,7 @@ if ! cppcheck \
|
||||
--suppress=missingIncludeSystem \
|
||||
--suppress=toomanyconfigs \
|
||||
--suppress='*:*source/external/*' \
|
||||
--suppress='*:*source/core/rendering/sdl3gpu/spv/*' \
|
||||
--suppress=normalCheckLevelMaxBranches \
|
||||
-D_DEBUG \
|
||||
-DLINUX_BUILD \
|
||||
|
||||
@@ -39,6 +39,7 @@ set(APP_SOURCES
|
||||
source/core/rendering/sprite/sprite.cpp
|
||||
source/core/rendering/sprite/moving_sprite.cpp
|
||||
source/core/rendering/sprite/animated_sprite.cpp
|
||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_cache.cpp
|
||||
source/utils/utils.cpp
|
||||
@@ -49,8 +50,57 @@ set(APP_SOURCES
|
||||
|
||||
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
||||
|
||||
# --- SHADER COMPILATION (Linux/Windows - macOS usa Metal inline) ---
|
||||
if(NOT APPLE)
|
||||
find_program(GLSLC_EXE NAMES glslc)
|
||||
|
||||
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
|
||||
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
|
||||
|
||||
set(ALL_SHADER_SOURCES
|
||||
"${SHADERS_DIR}/postfx.vert"
|
||||
"${SHADERS_DIR}/postfx.frag"
|
||||
)
|
||||
set(ALL_SHADER_HEADERS
|
||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||
)
|
||||
|
||||
if(GLSLC_EXE)
|
||||
add_custom_command(
|
||||
OUTPUT ${ALL_SHADER_HEADERS}
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-D GLSLC=${GLSLC_EXE}
|
||||
-D SHADERS_DIR=${SHADERS_DIR}
|
||||
-D HEADERS_DIR=${HEADERS_DIR}
|
||||
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
|
||||
DEPENDS ${ALL_SHADER_SOURCES}
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Compilant shaders SPIR-V..."
|
||||
)
|
||||
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
|
||||
message(STATUS "glslc trobat: els shaders es compilaran automaticament")
|
||||
else()
|
||||
# Dummy guards: sense glslc obligem que els headers pre-compilats existisquen.
|
||||
foreach(HDR ${ALL_SHADER_HEADERS})
|
||||
if(NOT EXISTS "${HDR}")
|
||||
message(FATAL_ERROR
|
||||
"glslc no trobat i el header SPIR-V no existeix: ${HDR}\n"
|
||||
" Instal·la glslc (Linux: sudo apt install glslang-tools) o\n"
|
||||
" comprova que el fitxer pre-compilat existeix al repositori."
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
message(STATUS "glslc no trobat - usant headers SPIR-V pre-compilats")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
||||
|
||||
if(NOT APPLE AND GLSLC_EXE)
|
||||
add_dependencies(${PROJECT_NAME} shaders)
|
||||
endif()
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
"${CMAKE_SOURCE_DIR}/source"
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
@@ -140,6 +190,7 @@ if(CPPCHECK_EXE)
|
||||
--suppress=missingIncludeSystem
|
||||
--suppress=toomanyconfigs
|
||||
--suppress=*:*source/external/*
|
||||
--suppress=*:*source/core/rendering/sdl3gpu/spv/*
|
||||
--suppress=normalCheckLevelMaxBranches
|
||||
-D_DEBUG
|
||||
-DLINUX_BUILD
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#version 450
|
||||
|
||||
// Vulkan GLSL fragment shader — PostFX effects
|
||||
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
|
||||
// Compile: glslc postfx.frag -o postfx.frag.spv
|
||||
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
||||
//
|
||||
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
|
||||
// (8 floats, 32 bytes, std140/scalar layout).
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||
|
||||
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
float gamma_strength;
|
||||
float curvature;
|
||||
float bleeding;
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16)
|
||||
} u;
|
||||
|
||||
// YCbCr helpers for NTSC bleeding
|
||||
vec3 rgb_to_ycc(vec3 rgb) {
|
||||
return vec3(
|
||||
0.299*rgb.r + 0.587*rgb.g + 0.114*rgb.b,
|
||||
-0.169*rgb.r - 0.331*rgb.g + 0.500*rgb.b + 0.5,
|
||||
0.500*rgb.r - 0.419*rgb.g - 0.081*rgb.b + 0.5
|
||||
);
|
||||
}
|
||||
vec3 ycc_to_rgb(vec3 ycc) {
|
||||
float y = ycc.x;
|
||||
float cb = ycc.y - 0.5;
|
||||
float cr = ycc.z - 0.5;
|
||||
return clamp(vec3(
|
||||
y + 1.402*cr,
|
||||
y - 0.344*cb - 0.714*cr,
|
||||
y + 1.772*cb
|
||||
), 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = v_uv;
|
||||
|
||||
// Curvatura barrel CRT
|
||||
if (u.curvature > 0.0) {
|
||||
vec2 c = uv - 0.5;
|
||||
float rsq = dot(c, c);
|
||||
vec2 dist = vec2(0.05, 0.1) * u.curvature;
|
||||
vec2 barrelScale = vec2(1.0) - 0.23 * dist;
|
||||
c += c * (dist * rsq);
|
||||
c *= barrelScale;
|
||||
if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) {
|
||||
out_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
return;
|
||||
}
|
||||
uv = c + 0.5;
|
||||
}
|
||||
|
||||
// Muestra base
|
||||
vec3 base = texture(scene, uv).rgb;
|
||||
|
||||
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||||
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample).
|
||||
vec3 colour;
|
||||
if (u.bleeding > 0.0) {
|
||||
float tw = float(textureSize(scene, 0).x);
|
||||
float step = u.oversample / tw; // 1 pixel lógico en UV
|
||||
vec3 ycc = rgb_to_ycc(base);
|
||||
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
|
||||
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
|
||||
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
|
||||
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
|
||||
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
|
||||
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||||
} else {
|
||||
colour = base;
|
||||
}
|
||||
|
||||
// Aberración cromática (drift animado con time para efecto NTSC real)
|
||||
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3));
|
||||
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
|
||||
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
|
||||
|
||||
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||
if (u.gamma_strength > 0.0) {
|
||||
vec3 lin = pow(colour, vec3(2.4));
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica.
|
||||
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50).
|
||||
// Constantes ajustables:
|
||||
const float SCAN_DARK_RATIO = 0.333; // fracción de subfilas oscuras (ps >= 3)
|
||||
const float SCAN_DARK_FLOOR = 0.42; // multiplicador de brillo de subfilas oscuras
|
||||
if (u.scanline_strength > 0.0) {
|
||||
float ps = max(1.0, round(u.pixel_scale));
|
||||
float frac_in_row = fract(uv.y * u.screen_height);
|
||||
float row_pos = floor(frac_in_row * ps);
|
||||
// bright_rows: cuántas subfilas son brillantes
|
||||
// ps==1 → ps (todo brillante → is_dark nunca se activa)
|
||||
// ps==2 → 1 brillante + 1 oscura
|
||||
// ps>=3 → floor(ps * (1 - DARK_RATIO)) brillantes
|
||||
float bright_rows = (ps < 2.0) ? ps : ((ps < 3.0) ? 1.0 : floor(ps * (1.0 - SCAN_DARK_RATIO)));
|
||||
float is_dark = step(bright_rows, row_pos);
|
||||
float scan = mix(1.0, SCAN_DARK_FLOOR, is_dark);
|
||||
colour *= mix(1.0, scan, u.scanline_strength);
|
||||
}
|
||||
|
||||
if (u.gamma_strength > 0.0) {
|
||||
vec3 enc = pow(colour, vec3(1.0 / 2.2));
|
||||
colour = mix(colour, enc, u.gamma_strength);
|
||||
}
|
||||
|
||||
// Viñeta
|
||||
vec2 d = uv - 0.5;
|
||||
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||||
colour *= clamp(vignette, 0.0, 1.0);
|
||||
|
||||
// Máscara de fósforo RGB — después de scanlines (orden original):
|
||||
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
|
||||
if (u.mask_strength > 0.0) {
|
||||
float whichMask = fract(gl_FragCoord.x * 0.3333333);
|
||||
vec3 mask = vec3(0.80);
|
||||
if (whichMask < 0.3333333)
|
||||
mask.x = 1.0;
|
||||
else if (whichMask < 0.6666666)
|
||||
mask.y = 1.0;
|
||||
else
|
||||
mask.z = 1.0;
|
||||
colour = mix(colour, colour * mask, u.mask_strength);
|
||||
}
|
||||
|
||||
// Parpadeo de fósforo CRT (~50 Hz)
|
||||
if (u.flicker > 0.0) {
|
||||
float flicker_wave = sin(u.time * 100.0) * 0.5 + 0.5;
|
||||
colour *= 1.0 - u.flicker * 0.04 * flicker_wave;
|
||||
}
|
||||
|
||||
out_color = vec4(colour, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#version 450
|
||||
|
||||
// Vulkan GLSL vertex shader — postfx full-screen triangle
|
||||
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
|
||||
// Compile: glslc postfx.vert -o postfx.vert.spv
|
||||
// xxd -i postfx.vert.spv > ../../source/core/rendering/sdl3gpu/postfx_vert_spv.h
|
||||
|
||||
layout(location = 0) out vec2 v_uv;
|
||||
|
||||
void main() {
|
||||
// Full-screen triangle (no vertex buffer needed)
|
||||
const vec2 positions[3] = vec2[3](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2( 3.0, -1.0),
|
||||
vec2(-1.0, 3.0)
|
||||
);
|
||||
const vec2 uvs[3] = vec2[3](
|
||||
vec2(0.0, 1.0),
|
||||
vec2(2.0, 1.0),
|
||||
vec2(0.0,-1.0)
|
||||
);
|
||||
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||
v_uv = uvs[gl_VertexIndex];
|
||||
}
|
||||
@@ -6,8 +6,11 @@
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
||||
#include "utils/defines.hpp"
|
||||
|
||||
namespace {
|
||||
@@ -39,29 +42,41 @@ auto Screen::get() -> Screen& {
|
||||
return instance_;
|
||||
}
|
||||
|
||||
Screen::Screen()
|
||||
: canvas_width_(GameCanvas::WIDTH),
|
||||
canvas_height_(GameCanvas::HEIGHT),
|
||||
canvas_(static_cast<std::size_t>(GameCanvas::WIDTH) * static_cast<std::size_t>(GameCanvas::HEIGHT), 0xFF000000U) {
|
||||
void Screen::configure(const Config& config) {
|
||||
// Inicialitza el subsistema de vídeo la primera volta; reconfiguracions
|
||||
// posteriors reutilitzen el subsistema i recreen els recursos SDL.
|
||||
if (!subsystem_initialized_) {
|
||||
if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) {
|
||||
std::fprintf(stderr, "SDL_InitSubSystem(VIDEO) failed: %s\n", SDL_GetError());
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
// Els recursos SDL (finestra, renderer, textura) es creen a configure().
|
||||
}
|
||||
|
||||
Screen::~Screen() {
|
||||
destroySdl();
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
void Screen::configure(const Config& config) {
|
||||
subsystem_initialized_ = true;
|
||||
canvas_width_ = GameCanvas::WIDTH;
|
||||
canvas_height_ = GameCanvas::HEIGHT;
|
||||
canvas_.assign(static_cast<std::size_t>(GameCanvas::WIDTH) * static_cast<std::size_t>(GameCanvas::HEIGHT), 0xFF000000U);
|
||||
}
|
||||
destroySdl();
|
||||
config_ = config;
|
||||
createSdl();
|
||||
}
|
||||
|
||||
void Screen::shutdown() {
|
||||
destroySdl();
|
||||
if (subsystem_initialized_) {
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
subsystem_initialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::destroySdl() {
|
||||
// Ordre per a evitar crashes a Wayland/Vulkan durant el teardown:
|
||||
// 1) Alliberar la finestra del GPU device (destrueix el VkSurface).
|
||||
// 2) Destruir el GPU device (vkDestroyInstance amb Wayland encara viu).
|
||||
// 3) Destruir la finestra SDL (tanca la surface de Wayland al final).
|
||||
if (shader_backend_ != nullptr) {
|
||||
static_cast<Rendering::SDL3GPUShader*>(shader_backend_.get())->releaseWindow();
|
||||
}
|
||||
shader_backend_.reset();
|
||||
if (game_texture_ != nullptr) {
|
||||
SDL_DestroyTexture(game_texture_);
|
||||
game_texture_ = nullptr;
|
||||
@@ -89,6 +104,19 @@ void Screen::createSdl() {
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// Primer intent: path SDL3 GPU. Si falla, fallback automàtic a SDL_Renderer.
|
||||
if (config_.gpu_acceleration) {
|
||||
auto backend = std::make_unique<Rendering::SDL3GPUShader>();
|
||||
backend->setPreferredDriver(config_.preferred_driver);
|
||||
backend->setVSync(config_.vsync);
|
||||
if (backend->init(window_, canvas_width_, canvas_height_)) {
|
||||
std::cout << "Screen: path GPU actiu (driver: " << backend->getDriverName() << ")\n";
|
||||
shader_backend_ = std::move(backend);
|
||||
return;
|
||||
}
|
||||
std::cerr << "Screen: GPU no disponible, fallback a SDL_Renderer\n";
|
||||
}
|
||||
|
||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||
if (renderer_ == nullptr) {
|
||||
std::fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
|
||||
@@ -114,8 +142,13 @@ void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
|
||||
}
|
||||
|
||||
void Screen::present() {
|
||||
SDL_UpdateTexture(game_texture_, nullptr, canvas_.data(), canvas_width_ * static_cast<int>(sizeof(std::uint32_t)));
|
||||
if (shader_backend_ != nullptr) {
|
||||
shader_backend_->uploadPixels(canvas_.data(), canvas_width_, canvas_height_);
|
||||
shader_backend_->render();
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_UpdateTexture(game_texture_, nullptr, canvas_.data(), canvas_width_ * static_cast<int>(sizeof(std::uint32_t)));
|
||||
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||
SDL_RenderClear(renderer_);
|
||||
SDL_RenderTexture(renderer_, game_texture_, nullptr, nullptr);
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/shader_backend.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
class Screen {
|
||||
@@ -17,12 +20,15 @@ class Screen {
|
||||
bool vsync{Defaults::Video::VSYNC};
|
||||
Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE};
|
||||
Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER};
|
||||
bool gpu_acceleration{Defaults::Video::GPU_ACCELERATION};
|
||||
std::string preferred_driver{Defaults::Video::GPU_PREFERRED_DRIVER};
|
||||
};
|
||||
|
||||
// Meyers singleton: la destrucció ocorre durant la termianció estàtica
|
||||
// (LIFO respecte a Resource::Cache). El SDL_Quit s'ha de registrar via
|
||||
// std::atexit ABANS de la primera crida a get(); així Screen::~Screen
|
||||
// (que crida SDL_Destroy*/SDL_QuitSubSystem) s'executa amb SDL encara viu.
|
||||
// Meyers singleton només per a l'accés (get()). El cicle de vida de SDL
|
||||
// el porta el Director: configure() i shutdown() s'han de cridar
|
||||
// explícitament, NO el destructor estàtic. El patró atexit + destructor
|
||||
// estàtic provoca segfaults a vkDestroyInstance en Wayland/Vulkan perquè
|
||||
// alguns recursos de SDL ja són inconsistents quan corre el destructor.
|
||||
static auto get() -> Screen&;
|
||||
|
||||
Screen(const Screen&) = delete;
|
||||
@@ -30,10 +36,14 @@ class Screen {
|
||||
auto operator=(const Screen&) -> Screen& = delete;
|
||||
auto operator=(Screen&&) -> Screen& = delete;
|
||||
|
||||
// Crea (o recrea) finestra, renderer i textura a partir de la config.
|
||||
// Imprescindible cridar-lo abans de qualsevol clear()/present().
|
||||
// Inicialitza SDL_VIDEO i crea finestra + renderer o shader backend GPU
|
||||
// segons la config. Cal cridar-lo una vegada abans de qualsevol clear()/present().
|
||||
void configure(const Config& config);
|
||||
|
||||
// Destrueix tots els recursos SDL (finestra, GPU device, renderer, textura)
|
||||
// i tanca el subsistema de vídeo. El Director el crida ABANS de SDL_Quit.
|
||||
void shutdown();
|
||||
|
||||
void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0);
|
||||
void present();
|
||||
|
||||
@@ -43,17 +53,22 @@ class Screen {
|
||||
[[nodiscard]] auto getCanvasHeight() const -> int { return canvas_height_; }
|
||||
|
||||
private:
|
||||
Screen();
|
||||
~Screen();
|
||||
Screen() = default;
|
||||
~Screen() = default;
|
||||
|
||||
void destroySdl();
|
||||
void createSdl();
|
||||
|
||||
bool subsystem_initialized_{false};
|
||||
|
||||
Config config_{};
|
||||
|
||||
SDL_Window* window_{nullptr};
|
||||
// Camí SDL_Renderer (fallback quan gpu_acceleration = false).
|
||||
SDL_Renderer* renderer_{nullptr};
|
||||
SDL_Texture* game_texture_{nullptr};
|
||||
// Camí SDL3 GPU (prioritari quan gpu_acceleration = true).
|
||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;
|
||||
|
||||
int canvas_width_{0};
|
||||
int canvas_height_{0};
|
||||
|
||||
@@ -0,0 +1,468 @@
|
||||
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#ifndef __APPLE__
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef __APPLE__
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
const char* POSTFX_VERT_MSL_ = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
|
||||
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
|
||||
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
|
||||
PostVOut out;
|
||||
out.pos = float4(positions[vid], 0.0, 1.0);
|
||||
out.uv = uvs[vid];
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
|
||||
const char* POSTFX_FRAG_MSL_ = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
float gamma_strength;
|
||||
float curvature;
|
||||
float bleeding;
|
||||
float pixel_scale;
|
||||
float time;
|
||||
float oversample;
|
||||
float flicker;
|
||||
};
|
||||
|
||||
static float3 rgb_to_ycc(float3 rgb) {
|
||||
return float3(
|
||||
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
|
||||
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
|
||||
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
|
||||
);
|
||||
}
|
||||
static float3 ycc_to_rgb(float3 ycc) {
|
||||
float y = ycc.x;
|
||||
float cb = ycc.y - 0.5f;
|
||||
float cr = ycc.z - 0.5f;
|
||||
return clamp(float3(
|
||||
y + 1.402f*cr,
|
||||
y - 0.344f*cb - 0.714f*cr,
|
||||
y + 1.772f*cb
|
||||
), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler samp [[sampler(0)]],
|
||||
constant PostFXUniforms& u [[buffer(0)]]) {
|
||||
float2 uv = in.uv;
|
||||
|
||||
if (u.curvature > 0.0f) {
|
||||
float2 c = uv - 0.5f;
|
||||
float rsq = dot(c, c);
|
||||
float2 dist = float2(0.05f, 0.1f) * u.curvature;
|
||||
float2 barrelScale = 1.0f - 0.23f * dist;
|
||||
c += c * (dist * rsq);
|
||||
c *= barrelScale;
|
||||
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
|
||||
return float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
uv = c + 0.5f;
|
||||
}
|
||||
|
||||
float3 base = scene.sample(samp, uv).rgb;
|
||||
|
||||
float3 colour;
|
||||
if (u.bleeding > 0.0f) {
|
||||
float tw = float(scene.get_width());
|
||||
float step = u.oversample / tw;
|
||||
float3 ycc = rgb_to_ycc(base);
|
||||
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
|
||||
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
|
||||
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
|
||||
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||||
} else {
|
||||
colour = base;
|
||||
}
|
||||
|
||||
float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f));
|
||||
colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r;
|
||||
colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b;
|
||||
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 lin = pow(colour, float3(2.4f));
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
const float SCAN_DARK_RATIO = 0.333f;
|
||||
const float SCAN_DARK_FLOOR = 0.42f;
|
||||
if (u.scanline_strength > 0.0f) {
|
||||
float ps = max(1.0f, round(u.pixel_scale));
|
||||
float frac_in_row = fract(uv.y * u.screen_height);
|
||||
float row_pos = floor(frac_in_row * ps);
|
||||
float bright_rows = (ps < 2.0f) ? ps : ((ps < 3.0f) ? 1.0f : floor(ps * (1.0f - SCAN_DARK_RATIO)));
|
||||
float is_dark = step(bright_rows, row_pos);
|
||||
float scan = mix(1.0f, SCAN_DARK_FLOOR, is_dark);
|
||||
colour *= mix(1.0f, scan, u.scanline_strength);
|
||||
}
|
||||
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 enc = pow(colour, float3(1.0f/2.2f));
|
||||
colour = mix(colour, enc, u.gamma_strength);
|
||||
}
|
||||
|
||||
float2 d = uv - 0.5f;
|
||||
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
|
||||
colour *= clamp(vignette, 0.0f, 1.0f);
|
||||
|
||||
if (u.mask_strength > 0.0f) {
|
||||
float whichMask = fract(in.pos.x * 0.3333333f);
|
||||
float3 mask = float3(0.80f);
|
||||
if (whichMask < 0.3333333f) mask.x = 1.0f;
|
||||
else if (whichMask < 0.6666667f) mask.y = 1.0f;
|
||||
else mask.z = 1.0f;
|
||||
colour = mix(colour, colour * mask, u.mask_strength);
|
||||
}
|
||||
|
||||
if (u.flicker > 0.0f) {
|
||||
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
|
||||
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
|
||||
}
|
||||
|
||||
return float4(colour, 1.0f);
|
||||
}
|
||||
)";
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
auto createShaderMSL(SDL_GPUDevice* device,
|
||||
const char* msl_source,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
std::uint32_t num_samplers,
|
||||
std::uint32_t num_uniform_buffers) -> SDL_GPUShader* {
|
||||
SDL_GPUShaderCreateInfo info{};
|
||||
info.code = reinterpret_cast<const Uint8*>(msl_source);
|
||||
info.code_size = std::strlen(msl_source) + 1;
|
||||
info.entrypoint = entrypoint;
|
||||
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
||||
info.stage = stage;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
return SDL_CreateGPUShader(device, &info);
|
||||
}
|
||||
#else
|
||||
auto createShaderSPIRV(SDL_GPUDevice* device,
|
||||
const std::uint8_t* spv_code,
|
||||
std::size_t spv_size,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
std::uint32_t num_samplers,
|
||||
std::uint32_t num_uniform_buffers) -> SDL_GPUShader* {
|
||||
SDL_GPUShaderCreateInfo info{};
|
||||
info.code = spv_code;
|
||||
info.code_size = spv_size;
|
||||
info.entrypoint = entrypoint;
|
||||
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
info.stage = stage;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
return SDL_CreateGPUShader(device, &info);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
SDL3GPUShader::~SDL3GPUShader() { cleanup(); }
|
||||
|
||||
auto SDL3GPUShader::init(SDL_Window* window, int game_width, int game_height) -> bool {
|
||||
window_ = window;
|
||||
game_width_ = game_width;
|
||||
game_height_ = game_height;
|
||||
uniforms_.screen_height = static_cast<float>(game_height);
|
||||
|
||||
if (!createDevice()) { return false; }
|
||||
if (!createShaders()) {
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
if (!createPipeline()) {
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
if (!createResources()) {
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
is_ready_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::createDevice() -> bool {
|
||||
#ifdef __APPLE__
|
||||
const SDL_GPUShaderFormat FORMAT = SDL_GPU_SHADERFORMAT_MSL;
|
||||
#else
|
||||
const SDL_GPUShaderFormat FORMAT = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
#endif
|
||||
const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
|
||||
device_ = SDL_CreateGPUDevice(FORMAT, false, driver);
|
||||
if (device_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: SDL_CreateGPUDevice ha fallat: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||
std::cerr << "SDL3GPUShader: SDL_ClaimWindowForGPUDevice ha fallat: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
||||
|
||||
const char* name = SDL_GetGPUDeviceDriver(device_);
|
||||
driver_name_ = (name != nullptr) ? name : "";
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::createShaders() -> bool {
|
||||
#ifdef __APPLE__
|
||||
vertex_shader_ = createShaderMSL(device_, POSTFX_VERT_MSL_, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
fragment_shader_ = createShaderMSL(device_, POSTFX_FRAG_MSL_, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
vertex_shader_ = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: SDL_CreateGPUShader ha fallat: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::createPipeline() -> bool {
|
||||
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_shader_;
|
||||
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
info.target_info.num_color_targets = 1;
|
||||
info.target_info.color_target_descriptions = &color_target;
|
||||
|
||||
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
||||
if (pipeline_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: SDL_CreateGPUGraphicsPipeline ha fallat: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::createResources() -> bool {
|
||||
SDL_GPUTextureCreateInfo tex_info{};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
tex_info.width = static_cast<Uint32>(game_width_);
|
||||
tex_info.height = static_cast<Uint32>(game_height_);
|
||||
tex_info.layer_count_or_depth = 1;
|
||||
tex_info.num_levels = 1;
|
||||
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
|
||||
|
||||
SDL_GPUTransferBufferCreateInfo tb_info{};
|
||||
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tb_info.size = static_cast<Uint32>(game_width_) * static_cast<Uint32>(game_height_) * 4U;
|
||||
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
|
||||
|
||||
SDL_GPUSamplerCreateInfo samp_info{};
|
||||
samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
|
||||
samp_info.mag_filter = SDL_GPU_FILTER_NEAREST;
|
||||
samp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||
samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
|
||||
|
||||
if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) {
|
||||
std::cerr << "SDL3GPUShader: creació de recursos GPU ha fallat: " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SDL3GPUShader::uploadPixels(const std::uint32_t* pixels, int width, int height) {
|
||||
if (!is_ready_ || upload_buffer_ == nullptr) { return; }
|
||||
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
|
||||
if (mapped == nullptr) { return; }
|
||||
std::memcpy(mapped, pixels, static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U);
|
||||
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
|
||||
}
|
||||
|
||||
void SDL3GPUShader::render() {
|
||||
if (!is_ready_) { return; }
|
||||
|
||||
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||
if (cmd == nullptr) { return; }
|
||||
|
||||
// Copy pass: transfer buffer -> scene texture.
|
||||
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||
if (copy != nullptr) {
|
||||
SDL_GPUTextureTransferInfo src{};
|
||||
src.transfer_buffer = upload_buffer_;
|
||||
src.pixels_per_row = static_cast<Uint32>(game_width_);
|
||||
src.rows_per_layer = static_cast<Uint32>(game_height_);
|
||||
|
||||
SDL_GPUTextureRegion dst{};
|
||||
dst.texture = scene_texture_;
|
||||
dst.w = static_cast<Uint32>(game_width_);
|
||||
dst.h = static_cast<Uint32>(game_height_);
|
||||
dst.d = 1;
|
||||
|
||||
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
}
|
||||
|
||||
// Agafa la textura del swapchain (pot fallar si la finestra està minimitzada).
|
||||
SDL_GPUTexture* swapchain = nullptr;
|
||||
Uint32 sw = 0;
|
||||
Uint32 sh = 0;
|
||||
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh) || swapchain == nullptr) {
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||
|
||||
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_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||
|
||||
SDL_GPUTextureSamplerBinding binding{};
|
||||
binding.texture = scene_texture_;
|
||||
binding.sampler = sampler_;
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setPostFXParams(const PostFXParams& params) {
|
||||
uniforms_.vignette_strength = params.vignette;
|
||||
uniforms_.scanline_strength = params.scanlines;
|
||||
uniforms_.chroma_strength = params.chroma;
|
||||
uniforms_.mask_strength = params.mask;
|
||||
uniforms_.gamma_strength = params.gamma;
|
||||
uniforms_.curvature = params.curvature;
|
||||
uniforms_.bleeding = params.bleeding;
|
||||
uniforms_.flicker = params.flicker;
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setVSync(bool vsync) {
|
||||
vsync_ = vsync;
|
||||
if (device_ != nullptr && window_ != nullptr) {
|
||||
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
||||
}
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::bestPresentMode(bool vsync) 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 SDL3GPUShader::releaseWindow() {
|
||||
if (device_ != nullptr && window_ != nullptr) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||
window_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3GPUShader::cleanup() {
|
||||
is_ready_ = false;
|
||||
if (device_ != nullptr) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
|
||||
if (pipeline_ != nullptr) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
||||
pipeline_ = nullptr;
|
||||
}
|
||||
if (vertex_shader_ != nullptr) {
|
||||
SDL_ReleaseGPUShader(device_, vertex_shader_);
|
||||
vertex_shader_ = nullptr;
|
||||
}
|
||||
if (fragment_shader_ != nullptr) {
|
||||
SDL_ReleaseGPUShader(device_, fragment_shader_);
|
||||
fragment_shader_ = nullptr;
|
||||
}
|
||||
if (scene_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||
scene_texture_ = nullptr;
|
||||
}
|
||||
if (upload_buffer_ != nullptr) {
|
||||
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||
upload_buffer_ = nullptr;
|
||||
}
|
||||
if (sampler_ != nullptr) {
|
||||
SDL_ReleaseGPUSampler(device_, sampler_);
|
||||
sampler_ = nullptr;
|
||||
}
|
||||
|
||||
// Si no s'ha cridat releaseWindow() abans, el desreclam és implícit dins
|
||||
// DestroyGPUDevice; però això pot bloquejar-se en drivers Wayland si la
|
||||
// Wayland surface ja no existeix. El camí recomanat és cridar releaseWindow()
|
||||
// des de Screen::destroySdl() ABANS de SDL_DestroyWindow.
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
}
|
||||
window_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace Rendering
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "core/rendering/shader_backend.hpp"
|
||||
|
||||
// Uniforms enviats al fragment stage cada frame.
|
||||
// Han de coincidir amb l'estructura MSL/GLSL (48 bytes, alineats a 16).
|
||||
struct PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
float gamma_strength;
|
||||
float curvature;
|
||||
float bleeding;
|
||||
float pixel_scale;
|
||||
float time;
|
||||
float oversample;
|
||||
float flicker;
|
||||
};
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
// Backend GPU basat en SDL3 GPU API (Vulkan/SPIR-V a Linux i Windows,
|
||||
// Metal/MSL a macOS). Pipeline únic: CPU framebuffer → transfer buffer →
|
||||
// scene texture → PostFX pass → swapchain. Sense supersampling, sense
|
||||
// CrtPi i sense Lanczos (reservats per a fases següents).
|
||||
//
|
||||
// Marcat `final` perquè el destructor crida cleanup() (que és virtual);
|
||||
// sense final, clang-tidy avisa de dispatch virtual durant destrucció.
|
||||
class SDL3GPUShader final : public ShaderBackend {
|
||||
public:
|
||||
SDL3GPUShader() = default;
|
||||
~SDL3GPUShader() override;
|
||||
|
||||
SDL3GPUShader(const SDL3GPUShader&) = delete;
|
||||
SDL3GPUShader(SDL3GPUShader&&) = delete;
|
||||
auto operator=(const SDL3GPUShader&) -> SDL3GPUShader& = delete;
|
||||
auto operator=(SDL3GPUShader&&) -> SDL3GPUShader& = delete;
|
||||
|
||||
auto init(SDL_Window* window, int game_width, int game_height) -> bool override;
|
||||
void uploadPixels(const std::uint32_t* pixels, int width, int height) override;
|
||||
void render() override;
|
||||
void setPostFXParams(const PostFXParams& params) override;
|
||||
void setVSync(bool vsync) override;
|
||||
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
||||
void cleanup() override;
|
||||
|
||||
// Allibera la finestra del device sense destruir el device. Cal cridar-ho
|
||||
// abans de SDL_DestroyWindow perquè el VkSurface tancat no sobrevisca la
|
||||
// finestra de Wayland (vkDestroyInstance cridaria Wayland amb surface invàlida).
|
||||
void releaseWindow();
|
||||
|
||||
[[nodiscard]] auto isReady() const -> bool override { return is_ready_; }
|
||||
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
|
||||
|
||||
private:
|
||||
auto createDevice() -> bool;
|
||||
auto createShaders() -> bool;
|
||||
auto createPipeline() -> bool;
|
||||
auto createResources() -> bool;
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) 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};
|
||||
SDL_GPUTexture* scene_texture_{nullptr};
|
||||
SDL_GPUTransferBuffer* upload_buffer_{nullptr};
|
||||
SDL_GPUSampler* sampler_{nullptr};
|
||||
|
||||
PostFXUniforms uniforms_{
|
||||
.vignette_strength = 0.0F,
|
||||
.chroma_strength = 0.0F,
|
||||
.scanline_strength = 0.0F,
|
||||
.screen_height = 0.0F,
|
||||
.mask_strength = 0.0F,
|
||||
.gamma_strength = 0.0F,
|
||||
.curvature = 0.0F,
|
||||
.bleeding = 0.0F,
|
||||
.pixel_scale = 1.0F,
|
||||
.time = 0.0F,
|
||||
.oversample = 1.0F,
|
||||
.flicker = 0.0F,
|
||||
};
|
||||
|
||||
int game_width_{0};
|
||||
int game_height_{0};
|
||||
std::string preferred_driver_;
|
||||
std::string driver_name_;
|
||||
bool vsync_{true};
|
||||
bool is_ready_{false};
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# Els headers SPIR-V són generats per glslc via tools/shaders/compile_spirv.cmake
|
||||
# i usen noms `kpostfx_*` com a convenció del pipeline de shaders (no és el nostre
|
||||
# naming). Desactivem les comprovacions de naming per a aquest directori.
|
||||
Checks: '-readability-identifier-naming,-readability-identifier-length'
|
||||
HeaderFilterRegex: '^$'
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
// Paràmetres d'intensitat dels efectes PostFX. Amb tot a 0 el shader
|
||||
// actua com un passthrough. Els presets els omplen en fases futures.
|
||||
struct PostFXParams {
|
||||
float vignette{0.0F};
|
||||
float scanlines{0.0F};
|
||||
float chroma{0.0F};
|
||||
float mask{0.0F};
|
||||
float gamma{0.0F};
|
||||
float curvature{0.0F};
|
||||
float bleeding{0.0F};
|
||||
float flicker{0.0F};
|
||||
};
|
||||
|
||||
// Interfície abstracta per a backends de renderitzat per GPU.
|
||||
// Per ara, l'única implementació és SDL3GPUShader. Un backend
|
||||
// alternatiu (p.ex. OpenGL o dummy) podria afegir-se més endavant.
|
||||
class ShaderBackend {
|
||||
public:
|
||||
ShaderBackend() = default;
|
||||
virtual ~ShaderBackend() = default;
|
||||
|
||||
ShaderBackend(const ShaderBackend&) = delete;
|
||||
ShaderBackend(ShaderBackend&&) = delete;
|
||||
auto operator=(const ShaderBackend&) -> ShaderBackend& = delete;
|
||||
auto operator=(ShaderBackend&&) -> ShaderBackend& = delete;
|
||||
|
||||
// Crea device, pipeline, textura i sampler a partir de la finestra
|
||||
// i la resolució del canvas del joc. Retorna false si la GPU no
|
||||
// està disponible (el Screen farà fallback al SDL_Renderer).
|
||||
virtual auto init(SDL_Window* window, int game_width, int game_height) -> bool = 0;
|
||||
|
||||
// Copia píxels ARGB8888 des de CPU cap al buffer de transferència.
|
||||
virtual void uploadPixels(const std::uint32_t* pixels, int width, int height) = 0;
|
||||
|
||||
// Executa el pas de renderitzat (copy pass + PostFX → swapchain + present).
|
||||
virtual void render() = 0;
|
||||
|
||||
// Actualitza els uniforms de PostFX per al pròxim render().
|
||||
virtual void setPostFXParams(const PostFXParams& params) = 0;
|
||||
|
||||
// Canvia el mode de presentació (VSYNC/IMMEDIATE/MAILBOX) en calent.
|
||||
virtual void setVSync(bool vsync) = 0;
|
||||
|
||||
// Fixa el driver preferit (buit = automàtic). S'ha de cridar abans d'init().
|
||||
virtual void setPreferredDriver(const std::string& driver) = 0;
|
||||
|
||||
// Destrueix tots els recursos GPU (pipeline, textura, buffer, sampler, device).
|
||||
virtual void cleanup() = 0;
|
||||
|
||||
[[nodiscard]] virtual auto isReady() const -> bool = 0;
|
||||
[[nodiscard]] virtual auto getDriverName() const -> std::string = 0;
|
||||
};
|
||||
|
||||
} // namespace Rendering
|
||||
@@ -22,13 +22,6 @@ Director::Director() {
|
||||
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
|
||||
}
|
||||
|
||||
// Registrem SDL_Quit ABANS de tocar cap singleton: així el handler atexit
|
||||
// s'executa DESPRÉS dels destructors estàtics de Screen i Cache (atexit
|
||||
// registrat abans de la construcció d'un objecte estàtic corre després
|
||||
// del seu destructor). Sense açò, Screen::~Screen cridaria SDL_Destroy*
|
||||
// amb SDL ja apagat → caos de destrucció clàssic.
|
||||
std::atexit(+[] { SDL_Quit(); });
|
||||
|
||||
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
|
||||
// les opcions; si no existeix config.yaml el crea amb els defaults.
|
||||
createSystemFolder();
|
||||
@@ -37,9 +30,8 @@ Director::Director() {
|
||||
std::cerr << "Director: no s'ha pogut carregar ni desar config.yaml a " << system_folder_ << '\n';
|
||||
}
|
||||
|
||||
// Forcem la construcció dels singletons en l'ordre desitjat: primer
|
||||
// Screen (necessari per a renderitzar), després Cache. La destrucció
|
||||
// estàtica anirà LIFO: Cache primer, Screen últim.
|
||||
// Screen es configura ara (subsystem + finestra + GPU/renderer); la seua
|
||||
// destrucció la fa aquest Director (veure ~Director). Res de static dtor.
|
||||
Screen::get().configure(Options::buildScreenConfig());
|
||||
(void)Resource::Cache::get();
|
||||
|
||||
@@ -48,8 +40,13 @@ Director::Director() {
|
||||
}
|
||||
|
||||
Director::~Director() {
|
||||
// Ordre imprescindible: primer l'escena (pot tenir recursos), després
|
||||
// Screen (GPU device, finestra, subsystem de vídeo), i finalment SDL_Quit.
|
||||
// Fer-ho així -- NO via atexit -- evita crashes a vkDestroyInstance quan
|
||||
// el runtime desfà les llibreries abans que corren els destructors estàtics.
|
||||
current_scene_.reset();
|
||||
// SDL_Quit corre via atexit; els singletons via destrucció estàtica.
|
||||
Screen::get().shutdown();
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void Director::createSystemFolder() {
|
||||
|
||||
+14
-1
@@ -64,6 +64,13 @@ namespace {
|
||||
if (node.contains("fullscreen")) { Options::window.fullscreen = node["fullscreen"].get_value<bool>(); }
|
||||
}
|
||||
|
||||
void parseGpu(const fkyaml::node& video_node) {
|
||||
if (!video_node.contains("gpu")) { return; }
|
||||
const auto& node = video_node["gpu"];
|
||||
if (node.contains("acceleration")) { Options::video.gpu.acceleration = node["acceleration"].get_value<bool>(); }
|
||||
if (node.contains("preferred_driver")) { Options::video.gpu.preferred_driver = node["preferred_driver"].get_value<std::string>(); }
|
||||
}
|
||||
|
||||
void parseVideo(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("video")) { return; }
|
||||
const auto& node = yaml["video"];
|
||||
@@ -74,6 +81,7 @@ namespace {
|
||||
if (node.contains("texture_filter")) {
|
||||
Options::video.texture_filter = filterFromString(node["texture_filter"].get_value<std::string>());
|
||||
}
|
||||
parseGpu(node);
|
||||
}
|
||||
|
||||
void parseMusic(const fkyaml::node& audio_node) {
|
||||
@@ -155,7 +163,10 @@ namespace Options {
|
||||
out << "video:\n";
|
||||
out << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
||||
out << " scaling_mode: \"" << scalingModeToString(video.scaling_mode) << "\"\n";
|
||||
out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n\n";
|
||||
out << " texture_filter: \"" << filterToString(video.texture_filter) << "\"\n";
|
||||
out << " gpu:\n";
|
||||
out << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << "\n";
|
||||
out << " preferred_driver: \"" << video.gpu.preferred_driver << "\"\n\n";
|
||||
|
||||
out << "audio:\n";
|
||||
out << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
|
||||
@@ -177,6 +188,8 @@ namespace Options {
|
||||
config.vsync = video.vsync;
|
||||
config.scaling_mode = video.scaling_mode;
|
||||
config.texture_filter = video.texture_filter;
|
||||
config.gpu_acceleration = video.gpu.acceleration;
|
||||
config.preferred_driver = video.gpu.preferred_driver;
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,16 @@ namespace Options {
|
||||
bool fullscreen{Defaults::Window::FULLSCREEN};
|
||||
};
|
||||
|
||||
struct Gpu {
|
||||
bool acceleration{Defaults::Video::GPU_ACCELERATION};
|
||||
std::string preferred_driver{Defaults::Video::GPU_PREFERRED_DRIVER};
|
||||
};
|
||||
|
||||
struct Video {
|
||||
bool vsync{Defaults::Video::VSYNC};
|
||||
Defaults::ScalingMode scaling_mode{Defaults::Video::SCALING_MODE};
|
||||
Defaults::TextureFilter texture_filter{Defaults::Video::TEXTURE_FILTER};
|
||||
Gpu gpu{};
|
||||
};
|
||||
|
||||
struct Music {
|
||||
|
||||
@@ -32,6 +32,8 @@ namespace Defaults {
|
||||
constexpr bool VSYNC = true;
|
||||
constexpr ScalingMode SCALING_MODE = ScalingMode::LETTERBOX;
|
||||
constexpr TextureFilter TEXTURE_FILTER = TextureFilter::NEAREST;
|
||||
constexpr bool GPU_ACCELERATION = true; // Path SDL3 GPU per defecte; fallback a SDL_Renderer si falla.
|
||||
constexpr const char* GPU_PREFERRED_DRIVER = ""; // Buit = auto. Aplica a l'arrencada.
|
||||
} // namespace Video
|
||||
|
||||
namespace Audio {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Texts {
|
||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
|
||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||
constexpr const char* VERSION = "0.3.0"; // Single source of truth — Makefile el grep
|
||||
constexpr const char* VERSION = "0.4.0"; // Single source of truth — Makefile el grep
|
||||
} // namespace Texts
|
||||
|
||||
namespace GameCanvas {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# compile_spirv.cmake
|
||||
# Compila shaders GLSL a SPIR-V i genera headers C++ embedibles.
|
||||
# Multiplataforma: Windows, macOS, Linux (no requereix bash ni xxd).
|
||||
#
|
||||
# Invocat per CMakeLists.txt amb:
|
||||
# cmake -D GLSLC=<path> -D SHADERS_DIR=<path> -D HEADERS_DIR=<path> -P compile_spirv.cmake
|
||||
#
|
||||
# També es pot executar manualment des de l'arrel del projecte:
|
||||
# cmake -D GLSLC=glslc -D SHADERS_DIR=data/shaders \
|
||||
# -D HEADERS_DIR=source/core/rendering/sdl3gpu/spv \
|
||||
# -P tools/shaders/compile_spirv.cmake
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
cmake_policy(SET CMP0007 NEW)
|
||||
|
||||
# Llista de shaders a compilar: font relativa a SHADERS_DIR
|
||||
set(SHADER_SOURCES
|
||||
"postfx.vert"
|
||||
"postfx.frag"
|
||||
)
|
||||
|
||||
# Nom de la variable C++ per a cada shader (mateix ordre)
|
||||
set(SHADER_VARS
|
||||
"kpostfx_vert_spv"
|
||||
"kpostfx_frag_spv"
|
||||
)
|
||||
|
||||
list(LENGTH SHADER_SOURCES NUM_SHADERS)
|
||||
math(EXPR LAST_IDX "${NUM_SHADERS} - 1")
|
||||
|
||||
foreach(IDX RANGE ${LAST_IDX})
|
||||
list(GET SHADER_SOURCES ${IDX} SRC_NAME)
|
||||
list(GET SHADER_VARS ${IDX} VAR)
|
||||
|
||||
# Derivem el nom del header a partir de la variable: kpostfx_vert_spv → postfx_vert_spv.h
|
||||
string(REGEX REPLACE "^k" "" HDR_BASE "${VAR}")
|
||||
set(SRC "${SHADERS_DIR}/${SRC_NAME}")
|
||||
set(SPV "${HEADERS_DIR}/${HDR_BASE}.spv")
|
||||
set(HDR "${HEADERS_DIR}/${HDR_BASE}.h")
|
||||
|
||||
message(STATUS "Compilant ${SRC} ...")
|
||||
|
||||
execute_process(
|
||||
COMMAND "${GLSLC}" "${SRC}" -o "${SPV}"
|
||||
RESULT_VARIABLE GLSLC_RESULT
|
||||
ERROR_VARIABLE GLSLC_ERROR
|
||||
)
|
||||
|
||||
if(NOT GLSLC_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "glslc ha fallat per a ${SRC}:\n${GLSLC_ERROR}")
|
||||
endif()
|
||||
|
||||
# Llegim el binari SPV com a hex (sense separadors) i el dividim en bytes.
|
||||
file(READ "${SPV}" HEX_DATA HEX)
|
||||
string(REGEX MATCHALL ".." BYTES "${HEX_DATA}")
|
||||
list(LENGTH BYTES NUM_BYTES)
|
||||
|
||||
set(ARRAY_BODY "")
|
||||
foreach(BYTE ${BYTES})
|
||||
string(APPEND ARRAY_BODY " 0x${BYTE},\n")
|
||||
endforeach()
|
||||
|
||||
file(WRITE "${HDR}"
|
||||
"#pragma once\n"
|
||||
"#include <cstddef>\n"
|
||||
"#include <cstdint>\n"
|
||||
"static const uint8_t ${VAR}[] = {\n"
|
||||
"${ARRAY_BODY}"
|
||||
"};\n"
|
||||
"static const size_t ${VAR}_size = ${NUM_BYTES};\n"
|
||||
)
|
||||
|
||||
file(REMOVE "${SPV}")
|
||||
message(STATUS " -> ${HDR} (${NUM_BYTES} bytes)")
|
||||
endforeach()
|
||||
|
||||
message(STATUS "Shaders SPIR-V compilats correctament.")
|
||||
Reference in new issue
Block a user