Files
aee/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
T

840 lines
38 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#include <SDL3/SDL_log.h>
#include <algorithm> // std::min, std::max, std::floor
#include <cmath> // std::floor
#include <cstring> // memcpy, strlen
#include <iostream> // std::cout
#ifndef __APPLE__
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
#endif
#ifdef __APPLE__
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
#include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h"
#include "core/rendering/sdl3gpu/msl/upscale_frag.msl.h"
#endif
namespace Rendering {
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
SDL3GPUShader::~SDL3GPUShader() {
destroy();
}
// ---------------------------------------------------------------------------
// init
// ---------------------------------------------------------------------------
auto SDL3GPUShader::init(SDL_Window* window,
SDL_Texture* texture,
const std::string& /*vertex_source*/,
const std::string& /*fragment_source*/) -> bool {
// Si ya estaba inicializado (p.ej. al cambiar borde), liberar recursos
// de textura/pipeline pero mantener el device vivo para evitar conflictos
// con SDL_Renderer en Windows/Vulkan.
if (is_initialized_) {
cleanup();
}
window_ = window;
// Dimensions from the SDL_Texture placeholder
float fw = 0.0F;
float fh = 0.0F;
SDL_GetTextureSize(texture, &fw, &fh);
game_width_ = static_cast<int>(fw);
game_height_ = static_cast<int>(fh);
uniforms_.screen_height = static_cast<float>(game_height_);
// ----------------------------------------------------------------
// 1. Create GPU device (solo si no existe ya)
// ----------------------------------------------------------------
if (preferred_driver_ == "none") {
SDL_Log("SDL3GPUShader: GPU disabled by config, using SDL_Renderer fallback");
driver_name_ = ""; // vacío → RenderInfo mostrará "sdl"
return false;
}
if (device_ == nullptr) {
#ifdef __APPLE__
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB;
#else
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_SPIRV;
#endif
const char* preferred = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
device_ = SDL_CreateGPUDevice(PREFERRED, false, preferred);
if (device_ == nullptr && preferred != nullptr) {
SDL_Log("SDL3GPUShader: driver '%s' not available, falling back to auto", preferred);
device_ = SDL_CreateGPUDevice(PREFERRED, false, nullptr);
}
if (device_ == nullptr) {
SDL_Log("SDL3GPUShader: SDL_CreateGPUDevice failed: %s", SDL_GetError());
return false;
}
driver_name_ = SDL_GetGPUDeviceDriver(device_);
std::cout << "GPU Driver : " << driver_name_ << '\n';
// ----------------------------------------------------------------
// 2. Claim window (una sola vez — no liberar hasta destroy())
// ----------------------------------------------------------------
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
SDL_Log("SDL3GPUShader: SDL_ClaimWindowForGPUDevice failed: %s", SDL_GetError());
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
}
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
}
// ----------------------------------------------------------------
// 3. Create scene texture (upload target, always game resolution)
// Format: B8G8R8A8_UNORM matches SDL ARGB8888 byte layout on LE
// ----------------------------------------------------------------
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_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);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create scene texture: %s", SDL_GetError());
cleanup();
return false;
}
// internal_texture_: si el multiplicador és > 1, es crea ací amb les
// dimensions game·N × game·N. No bloqueja si falla — només deixa la
// textura a nullptr i el pipeline ometrà la còpia.
recreateInternalTexture();
// ----------------------------------------------------------------
// 4. Create upload transfer buffer (CPU → GPU, always game resolution)
// ----------------------------------------------------------------
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create upload buffer: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 5. Create samplers: NEAREST (pixel art) + LINEAR (supersampling)
// ----------------------------------------------------------------
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 (sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create sampler: %s", SDL_GetError());
cleanup();
return false;
}
SDL_GPUSamplerCreateInfo lsamp_info = {};
lsamp_info.min_filter = SDL_GPU_FILTER_LINEAR;
lsamp_info.mag_filter = SDL_GPU_FILTER_LINEAR;
lsamp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
lsamp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
lsamp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
lsamp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
linear_sampler_ = SDL_CreateGPUSampler(device_, &lsamp_info);
if (linear_sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create linear sampler: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 6. Create PostFX graphics pipeline
// ----------------------------------------------------------------
if (!createPipeline()) {
cleanup();
return false;
}
// ----------------------------------------------------------------
// 7. Create CrtPi graphics pipeline
// ----------------------------------------------------------------
if (!createCrtPiPipeline()) {
cleanup();
return false;
}
is_initialized_ = true;
std::cout << "GPU Shader : initialized OK — game " << game_width_ << 'x' << game_height_ << '\n';
return true;
}
// ---------------------------------------------------------------------------
// createPipeline
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createPipeline() -> bool { // NOLINT(readability-function-cognitive-complexity)
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
// ---- PostFX pipeline (scene/scaled → swapchain) ----
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
SDL_Log("SDL3GPUShader: failed to compile PostFX shaders");
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false;
}
SDL_GPUColorTargetBlendState no_blend = {};
no_blend.enable_blend = false;
no_blend.enable_color_write_mask = false;
SDL_GPUColorTargetDescription color_target = {};
color_target.format = SWAPCHAIN_FMT;
color_target.blend_state = no_blend;
SDL_GPUVertexInputState no_input = {};
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
pipe_info.vertex_shader = vert;
pipe_info.fragment_shader = frag;
pipe_info.vertex_input_state = no_input;
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
pipe_info.target_info.num_color_targets = 1;
pipe_info.target_info.color_target_descriptions = &color_target;
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info);
SDL_ReleaseGPUShader(device_, vert);
SDL_ReleaseGPUShader(device_, frag);
if (pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: PostFX pipeline creation failed: %s", SDL_GetError());
return false;
}
// ---- Upscale pipeline (scene → scaled_texture_, nearest) ----
#ifdef __APPLE__
SDL_GPUShader* uvert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* ufrag = createShaderMSL(device_, Rendering::Msl::kUpscaleFrag, "upscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#else
SDL_GPUShader* uvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* ufrag = createShaderSPIRV(device_, kupscale_frag_spv, kupscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#endif
if ((uvert == nullptr) || (ufrag == nullptr)) {
SDL_Log("SDL3GPUShader: failed to compile upscale shaders");
if (uvert != nullptr) { SDL_ReleaseGPUShader(device_, uvert); }
if (ufrag != nullptr) { SDL_ReleaseGPUShader(device_, ufrag); }
return false;
}
SDL_GPUColorTargetDescription upscale_color_target = {};
upscale_color_target.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
upscale_color_target.blend_state = no_blend;
SDL_GPUGraphicsPipelineCreateInfo upscale_pipe_info = {};
upscale_pipe_info.vertex_shader = uvert;
upscale_pipe_info.fragment_shader = ufrag;
upscale_pipe_info.vertex_input_state = no_input;
upscale_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
upscale_pipe_info.target_info.num_color_targets = 1;
upscale_pipe_info.target_info.color_target_descriptions = &upscale_color_target;
upscale_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &upscale_pipe_info);
SDL_ReleaseGPUShader(device_, uvert);
SDL_ReleaseGPUShader(device_, ufrag);
if (upscale_pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: upscale pipeline creation failed: %s", SDL_GetError());
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// createCrtPiPipeline — pipeline dedicado para el shader CRT-Pi.
// Usa el mismo vertex shader que postfx (fullscreen-triangle genérico).
// El fragment shader es específico para el algoritmo CRT-Pi.
// Sin supersampling ni Lanczos: va siempre directo al swapchain.
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createCrtPiPipeline() -> bool {
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
#ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif
if ((vert == nullptr) || (frag == nullptr)) {
SDL_Log("SDL3GPUShader: failed to compile CrtPi shaders");
if (vert != nullptr) { SDL_ReleaseGPUShader(device_, vert); }
if (frag != nullptr) { SDL_ReleaseGPUShader(device_, frag); }
return false;
}
SDL_GPUColorTargetBlendState no_blend = {};
no_blend.enable_blend = false;
no_blend.enable_color_write_mask = false;
SDL_GPUColorTargetDescription color_target = {};
color_target.format = SWAPCHAIN_FMT;
color_target.blend_state = no_blend;
SDL_GPUVertexInputState no_input = {};
SDL_GPUGraphicsPipelineCreateInfo pipe_info = {};
pipe_info.vertex_shader = vert;
pipe_info.fragment_shader = frag;
pipe_info.vertex_input_state = no_input;
pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
pipe_info.target_info.num_color_targets = 1;
pipe_info.target_info.color_target_descriptions = &color_target;
crtpi_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &pipe_info);
SDL_ReleaseGPUShader(device_, vert);
SDL_ReleaseGPUShader(device_, frag);
if (crtpi_pipeline_ == nullptr) {
SDL_Log("SDL3GPUShader: CrtPi pipeline creation failed: %s", SDL_GetError());
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// uploadPixels — copies ARGB8888 CPU pixels into the GPU transfer buffer.
// Con supersampling (oversample_ > 1) expande cada pixel del juego a un bloque
// oversample × oversample y hornea la scanline oscura en la última fila del bloque.
// ---------------------------------------------------------------------------
void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) {
if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; }
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
if (mapped == nullptr) {
SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError());
return;
}
// Copia directa — el upscale lo hace la GPU en el primer render pass
std::memcpy(mapped, pixels, static_cast<size_t>(width) * static_cast<size_t>(height) * 4);
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
}
// ---------------------------------------------------------------------------
// render — upload scene texture + PostFX pass → swapchain
// ---------------------------------------------------------------------------
void SDL3GPUShader::render() { // NOLINT(readability-function-cognitive-complexity)
if (!is_initialized_) { return; }
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
if (cmd == nullptr) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
return;
}
// ---- Copy pass: transfer buffer → scene texture (siempre a resolución del juego) ----
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
if (copy != nullptr) {
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = upload_buffer_;
src.offset = 0;
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);
}
// ---- Internal resolution NN upscale: scene_texture_ → internal_texture_ ----
// Multiplicador enter. Si > 1, tot el pipeline downstream veu internal_texture_
// com a "scene" (mida game·N × game·N) i els passos següents (SS, PostFX,
// Lanczos, letterbox) operen sobre aquesta font més gran. L'objectiu: quan el
// filtre final LINEAR estira a finestra, parteix d'una base més gran i es veu
// menys borrós. Amb internal_res_ == 1, s'omet el pas (zero overhead).
SDL_GPUTexture* source_texture = scene_texture_;
int source_width = game_width_;
int source_height = game_height_;
if (internal_res_ > 1 && internal_texture_ != nullptr && upscale_pipeline_ != nullptr) {
SDL_GPUColorTargetInfo internal_target = {};
internal_target.texture = internal_texture_;
internal_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
internal_target.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* ipass = SDL_BeginGPURenderPass(cmd, &internal_target, 1, nullptr);
if (ipass != nullptr) {
SDL_BindGPUGraphicsPipeline(ipass, upscale_pipeline_);
SDL_GPUTextureSamplerBinding ibinding = {};
ibinding.texture = scene_texture_;
ibinding.sampler = sampler_; // sempre NEAREST per a la còpia de resolució interna
SDL_BindGPUFragmentSamplers(ipass, 0, &ibinding, 1);
SDL_DrawGPUPrimitives(ipass, 3, 1, 0, 0);
SDL_EndGPURenderPass(ipass);
}
source_texture = internal_texture_;
source_width = game_width_ * internal_res_;
source_height = game_height_ * internal_res_;
}
// L'effective_scene és la textura font definitiva (internal_texture_
// si internal_res_ > 1, altrament scene_texture_). effective_height
// reflecteix l'alçada lògica del frame: game_height_ * 1.2 si 4:3 actiu.
SDL_GPUTexture* effective_scene = source_texture;
int effective_height = stretch_4_3_ ? static_cast<int>(static_cast<float>(game_height_) * 1.2F) : game_height_;
(void)source_width;
(void)source_height;
// ---- Acquire swapchain texture ----
SDL_GPUTexture* swapchain = nullptr;
Uint32 sw = 0;
Uint32 sh = 0;
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
if (swapchain == nullptr) {
// Window minimized — skip frame
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
// ---- Calcular viewport (dimensions lògiques del canvas) ----
// Si 4:3 actiu, effective_height ja és 240 (la textura estirada)
const auto LOGICAL_W = static_cast<float>(game_width_);
const auto LOGICAL_H = static_cast<float>(effective_height);
float vx = 0.0F;
float vy = 0.0F;
float vw = 0.0F;
float vh = 0.0F;
switch (scaling_mode_) {
case Options::ScalingMode::DISABLED:
// 1:1, sense escala (pot ser diminut en finestres grans)
vw = LOGICAL_W;
vh = LOGICAL_H;
break;
case Options::ScalingMode::STRETCH:
// Omple tota la finestra, escala no uniforme
vw = static_cast<float>(sw);
vh = static_cast<float>(sh);
break;
case Options::ScalingMode::LETTERBOX: {
const float SCALE = std::min(static_cast<float>(sw) / LOGICAL_W,
static_cast<float>(sh) / LOGICAL_H);
vw = LOGICAL_W * SCALE;
vh = LOGICAL_H * SCALE;
break;
}
case Options::ScalingMode::OVERSCAN: {
const float SCALE = std::max(static_cast<float>(sw) / LOGICAL_W,
static_cast<float>(sh) / LOGICAL_H);
vw = LOGICAL_W * SCALE;
vh = LOGICAL_H * SCALE;
break;
}
case Options::ScalingMode::INTEGER: {
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / static_cast<int>(LOGICAL_W), static_cast<int>(sh) / static_cast<int>(LOGICAL_H)));
vw = LOGICAL_W * static_cast<float>(SCALE);
vh = LOGICAL_H * static_cast<float>(SCALE);
break;
}
}
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
// pixel_scale: subpíxels per píxel lògic (zoom de finestra).
uniforms_.pixel_scale = (effective_height > 0) ? (vh / static_cast<float>(effective_height)) : 1.0F;
uniforms_.screen_height = static_cast<float>(effective_height);
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
// ---- Path CrtPi: directo scene_texture_ → swapchain, sin SS ni Lanczos ----
if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) {
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, crtpi_pipeline_);
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &vp);
// El shader CrtPi tradicionalment usa NEAREST per a fer el seu
// propi filtrat analític. Si l'usuari tria LINEAR explícitament,
// respectem la preferència (la mostra arribarà pre-suavitzada).
SDL_GPUTextureSamplerBinding binding = {};
binding.texture = effective_scene;
binding.sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
? linear_sampler_
: sampler_;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
// Injectar texture_width/height abans del push
crtpi_uniforms_.texture_width = static_cast<float>(game_width_);
crtpi_uniforms_.texture_height = static_cast<float>(effective_height);
SDL_PushGPUFragmentUniformData(cmd, 0, &crtpi_uniforms_, sizeof(CrtPiUniforms));
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
// ---- Render pass: PostFX → swapchain ----
// Font: effective_scene (que ja és internal_texture_ si internal_res_ > 1, o
// scene_texture_ altrament). Sampler honora el filtre global de l'usuari.
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_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &vp);
SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
? linear_sampler_
: sampler_;
SDL_GPUTextureSamplerBinding binding = {};
binding.texture = effective_scene;
binding.sampler = active_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);
}
// ---------------------------------------------------------------------------
// cleanup — libera pipeline/texturas/buffer pero mantiene device + swapchain
// ---------------------------------------------------------------------------
void SDL3GPUShader::cleanup() {
is_initialized_ = false;
if (device_ != nullptr) {
SDL_WaitForGPUIdle(device_);
if (pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
pipeline_ = nullptr;
}
if (crtpi_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device_, crtpi_pipeline_);
crtpi_pipeline_ = nullptr;
}
if (upscale_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device_, upscale_pipeline_);
upscale_pipeline_ = nullptr;
}
if (scene_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (upload_buffer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
upload_buffer_ = nullptr;
}
if (sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device_, sampler_);
sampler_ = nullptr;
}
if (linear_sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device_, linear_sampler_);
linear_sampler_ = nullptr;
}
// device_ y el claim de la ventana se mantienen vivos
}
}
// ---------------------------------------------------------------------------
// destroy — limpieza completa incluyendo device y swapchain (solo al cerrar)
// ---------------------------------------------------------------------------
void SDL3GPUShader::destroy() {
cleanup();
if (device_ != nullptr) {
if (window_ != nullptr) {
SDL_ReleaseWindowFromGPUDevice(device_, window_);
}
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
}
window_ = nullptr;
}
// ---------------------------------------------------------------------------
// Shader creation helpers
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 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;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: MSL shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device, // NOLINT(readability-convert-member-functions-to-static)
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 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;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
uniforms_.vignette_strength = p.vignette;
uniforms_.chroma_min = p.chroma_min;
uniforms_.chroma_max = p.chroma_max;
uniforms_.mask_strength = p.mask;
uniforms_.gamma_strength = p.gamma;
uniforms_.curvature = p.curvature;
uniforms_.bleeding = p.bleeding;
uniforms_.flicker = p.flicker;
uniforms_.scanline_strength = p.scanlines;
uniforms_.scan_dark_ratio = p.scan_dark_ratio;
uniforms_.scan_dark_floor = p.scan_dark_floor;
uniforms_.scan_edge_soft = p.scan_edge_soft;
}
void SDL3GPUShader::setCrtPiParams(const CrtPiParams& p) {
crtpi_uniforms_.scanline_weight = p.scanline_weight;
crtpi_uniforms_.scanline_gap_brightness = p.scanline_gap_brightness;
crtpi_uniforms_.bloom_factor = p.bloom_factor;
crtpi_uniforms_.input_gamma = p.input_gamma;
crtpi_uniforms_.output_gamma = p.output_gamma;
crtpi_uniforms_.mask_brightness = p.mask_brightness;
crtpi_uniforms_.curvature_x = p.curvature_x;
crtpi_uniforms_.curvature_y = p.curvature_y;
crtpi_uniforms_.mask_type = p.mask_type;
crtpi_uniforms_.enable_scanlines = p.enable_scanlines ? 1 : 0;
crtpi_uniforms_.enable_multisample = p.enable_multisample ? 1 : 0;
crtpi_uniforms_.enable_gamma = p.enable_gamma ? 1 : 0;
crtpi_uniforms_.enable_curvature = p.enable_curvature ? 1 : 0;
crtpi_uniforms_.enable_sharper = p.enable_sharper ? 1 : 0;
// texture_width/height se inyectan en render() cada frame
}
void SDL3GPUShader::setActiveShader(ShaderType type) {
active_shader_ = type;
}
auto SDL3GPUShader::bestPresentMode(bool vsync) const -> SDL_GPUPresentMode {
if (vsync) {
return SDL_GPU_PRESENTMODE_VSYNC;
}
// IMMEDIATE: sin sincronización — el driver puede no soportarlo en Wayland/compositing
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_IMMEDIATE)) {
return SDL_GPU_PRESENTMODE_IMMEDIATE;
}
// MAILBOX: presenta en el siguiente VBlank pero sin bloquear el hilo (triple buffer)
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_MAILBOX)) {
SDL_Log("SDL3GPUShader: IMMEDIATE no soportado, usando MAILBOX para VSync desactivado");
return SDL_GPU_PRESENTMODE_MAILBOX;
}
SDL_Log("SDL3GPUShader: IMMEDIATE y MAILBOX no soportados, forzando VSYNC");
return SDL_GPU_PRESENTMODE_VSYNC;
}
void SDL3GPUShader::setVSync(bool vsync) {
vsync_ = vsync;
if (device_ != nullptr && window_ != nullptr) {
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
}
}
void SDL3GPUShader::setScalingMode(Options::ScalingMode mode) {
scaling_mode_ = mode;
}
// setInternalResolution — canvia el multiplicador de resolució interna.
// Recrea la textura intermèdia amb les noves dimensions (320·N × 200·N).
void SDL3GPUShader::setInternalResolution(int multiplier) {
const int NEW = std::max(1, multiplier);
if (NEW == internal_res_) {
return;
}
internal_res_ = NEW;
if (is_initialized_ && device_ != nullptr) {
SDL_WaitForGPUIdle(device_);
recreateInternalTexture();
}
}
void SDL3GPUShader::setStretch43(bool enabled) {
stretch_4_3_ = enabled;
}
// ---------------------------------------------------------------------------
// reinitTexturesAndBuffer — recrea scene_texture_, internal_texture_ i
// upload_buffer_. No toca pipelines ni samplers.
// ---------------------------------------------------------------------------
auto SDL3GPUShader::reinitTexturesAndBuffer() -> bool {
if (device_ == nullptr) { return false; }
SDL_WaitForGPUIdle(device_);
if (scene_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (upload_buffer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
upload_buffer_ = nullptr;
}
uniforms_.screen_height = static_cast<float>(game_height_);
// scene_texture_: sempre a resolució del joc
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_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);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create scene texture: %s", SDL_GetError());
return false;
}
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create upload buffer: %s", SDL_GetError());
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
return false;
}
recreateInternalTexture();
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, internal ×%d", game_width_, game_height_, internal_res_);
return true;
}
// ---------------------------------------------------------------------------
// recreateInternalTexture — libera y recrea internal_texture_ para el
// multiplicador internal_res_ actual. Si val 1, allibera i queda a nullptr
// (el pipeline ometrà la còpia al següent render).
// ---------------------------------------------------------------------------
auto SDL3GPUShader::recreateInternalTexture() -> bool {
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (internal_res_ <= 1 || device_ == nullptr) {
return true;
}
const int W = game_width_ * internal_res_;
const int H = game_height_ * internal_res_;
SDL_GPUTextureCreateInfo info = {};
info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
info.width = static_cast<Uint32>(W);
info.height = static_cast<Uint32>(H);
info.layer_count_or_depth = 1;
info.num_levels = 1;
internal_texture_ = SDL_CreateGPUTexture(device_, &info);
if (internal_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create internal texture %dx%d (×%d): %s",
W,
H,
internal_res_,
SDL_GetError());
return false;
}
SDL_Log("SDL3GPUShader: internal texture %dx%d (×%d)", W, H, internal_res_);
return true;
}
} // namespace Rendering