#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" #include #include // std::min, std::max, std::floor #include // std::floor #include // memcpy, strlen #include // 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(fw); game_height_ = static_cast(fh); uniforms_.screen_height = static_cast(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(game_width_); tex_info.height = static_cast(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(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(width) * static_cast(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(game_width_); src.rows_per_layer = static_cast(game_height_); SDL_GPUTextureRegion dst = {}; dst.texture = scene_texture_; dst.w = static_cast(game_width_); dst.h = static_cast(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(static_cast(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(game_width_); const auto LOGICAL_H = static_cast(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(sw); vh = static_cast(sh); break; case Options::ScalingMode::LETTERBOX: { const float SCALE = std::min(static_cast(sw) / LOGICAL_W, static_cast(sh) / LOGICAL_H); vw = LOGICAL_W * SCALE; vh = LOGICAL_H * SCALE; break; } case Options::ScalingMode::OVERSCAN: { const float SCALE = std::max(static_cast(sw) / LOGICAL_W, static_cast(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(sw) / static_cast(LOGICAL_W), static_cast(sh) / static_cast(LOGICAL_H))); vw = LOGICAL_W * static_cast(SCALE); vh = LOGICAL_H * static_cast(SCALE); break; } } vx = std::floor((static_cast(sw) - vw) * 0.5F); vy = std::floor((static_cast(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(effective_height)) : 1.0F; uniforms_.screen_height = static_cast(effective_height); uniforms_.time = static_cast(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(game_width_); crtpi_uniforms_.texture_height = static_cast(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(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(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(game_width_); tex_info.height = static_cast(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(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(W); info.height = static_cast(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