840 lines
38 KiB
C++
840 lines
38 KiB
C++
#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
|