#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" #include #include #include "core/rendering/sdl3gpu/shader_factory.hpp" #include "core/rendering/sdl3gpu/viewport.hpp" #include "utils/log.hpp" #ifdef __APPLE__ #include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h" #include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h" #else #include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h" #endif namespace Rendering { SDL3GPUShader::~SDL3GPUShader() { cleanup(); } auto SDL3GPUShader::init(SDL_Window* window, int game_width, int game_height) -> bool { window_ = window; game_width_ = game_width; game_height_ = game_height; uniforms_.screen_height = static_cast(game_height); if (!createDevice()) { return false; } if (!createShaders()) { cleanup(); return false; } if (!createPipeline()) { cleanup(); return false; } if (!createResources()) { cleanup(); return false; } // El pipeline CrtPi es crea sempre — l'usuari pot alternar en calent // via setActiveShader() sense tornar a init'ar la GPU. crtpi_pipeline_ = std::make_unique(); if (!crtpi_pipeline_->init(device_, window_)) { cleanup(); return false; } is_ready_ = true; return true; } auto SDL3GPUShader::createDevice() -> bool { #ifdef __APPLE__ const SDL_GPUShaderFormat FORMAT = SDL_GPU_SHADERFORMAT_MSL; #else const SDL_GPUShaderFormat FORMAT = SDL_GPU_SHADERFORMAT_SPIRV; #endif const char* driver = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str(); device_ = SDL_CreateGPUDevice(FORMAT, false, driver); if (device_ == nullptr) { Log::error("SDL3GPUShader", "SDL_CreateGPUDevice ha fallat: {}", SDL_GetError()); return false; } if (!SDL_ClaimWindowForGPUDevice(device_, window_)) { Log::error("SDL3GPUShader", "SDL_ClaimWindowForGPUDevice ha fallat: {}", SDL_GetError()); return false; } SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_)); const char* name = SDL_GetGPUDeviceDriver(device_); driver_name_ = (name != nullptr) ? name : ""; return true; } auto SDL3GPUShader::createShaders() -> bool { #ifdef __APPLE__ vertex_shader_ = createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); fragment_shader_ = createShaderMSL(device_, Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #else vertex_shader_ = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #endif if (vertex_shader_ == nullptr || fragment_shader_ == nullptr) { Log::error("SDL3GPUShader", "SDL_CreateGPUShader ha fallat: {}", SDL_GetError()); return false; } return true; } auto SDL3GPUShader::createPipeline() -> bool { const SDL_GPUTextureFormat SWAPCHAIN_FORMAT = SDL_GetGPUSwapchainTextureFormat(device_, window_); SDL_GPUColorTargetBlendState no_blend{}; SDL_GPUColorTargetDescription color_target{}; color_target.format = SWAPCHAIN_FORMAT; color_target.blend_state = no_blend; SDL_GPUGraphicsPipelineCreateInfo info{}; info.vertex_shader = vertex_shader_; info.fragment_shader = fragment_shader_; info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; info.target_info.num_color_targets = 1; info.target_info.color_target_descriptions = &color_target; pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info); if (pipeline_ == nullptr) { Log::error("SDL3GPUShader", "SDL_CreateGPUGraphicsPipeline ha fallat: {}", SDL_GetError()); return false; } return true; } auto SDL3GPUShader::createResources() -> bool { SDL_GPUTextureCreateInfo tex_info{}; tex_info.type = SDL_GPU_TEXTURETYPE_2D; tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM; tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER; tex_info.width = static_cast(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); SDL_GPUTransferBufferCreateInfo tb_info{}; tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; tb_info.size = static_cast(game_width_) * static_cast(game_height_) * 4U; upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info); SDL_GPUSamplerCreateInfo samp_info{}; samp_info.min_filter = SDL_GPU_FILTER_NEAREST; samp_info.mag_filter = SDL_GPU_FILTER_NEAREST; samp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST; samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; sampler_ = SDL_CreateGPUSampler(device_, &samp_info); if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) { Log::error("SDL3GPUShader", "creació de recursos GPU ha fallat: {}", SDL_GetError()); return false; } return true; } void SDL3GPUShader::uploadPixels(const std::uint32_t* pixels, int width, int height) { if (!is_ready_ || upload_buffer_ == nullptr) { return; } void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false); if (mapped == nullptr) { return; } std::memcpy(mapped, pixels, static_cast(width) * static_cast(height) * 4U); SDL_UnmapGPUTransferBuffer(device_, upload_buffer_); } void SDL3GPUShader::render() { if (!is_ready_) { return; } SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_); if (cmd == nullptr) { return; } // Copy pass: transfer buffer -> scene texture. SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd); if (copy != nullptr) { SDL_GPUTextureTransferInfo src{}; src.transfer_buffer = upload_buffer_; src.pixels_per_row = static_cast(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); } // Agafa la textura del swapchain (pot fallar si la finestra està minimitzada). SDL_GPUTexture* swapchain = nullptr; Uint32 sw = 0; Uint32 sh = 0; if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh) || swapchain == nullptr) { SDL_SubmitGPUCommandBuffer(cmd); return; } // Camí CrtPi: delega tot el render pass al pipeline dedicat. if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) { crtpi_pipeline_->render(cmd, swapchain, scene_texture_, sampler_, game_width_, game_height_, scaling_mode_, sw, sh); SDL_SubmitGPUCommandBuffer(cmd); return; } uniforms_.time = static_cast(SDL_GetTicks()) / 1000.0F; SDL_GPUColorTargetInfo color_target{}; color_target.texture = swapchain; color_target.load_op = SDL_GPU_LOADOP_CLEAR; color_target.store_op = SDL_GPU_STOREOP_STORE; color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); if (pass != nullptr) { const SDL_GPUViewport VP = computeViewport(scaling_mode_, sw, sh, game_width_, game_height_); SDL_SetGPUViewport(pass, &VP); // pixel_scale = píxels físics per fila lògica al viewport. El shader l'usa // per ajustar l'ample del smoothstep de scanlines a 1 píxel físic (estil crtpi). uniforms_.pixel_scale = (game_height_ > 0) ? VP.h / static_cast(game_height_) : 1.0F; SDL_BindGPUGraphicsPipeline(pass, pipeline_); SDL_GPUTextureSamplerBinding binding{}; binding.texture = scene_texture_; binding.sampler = sampler_; SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms)); SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0); SDL_EndGPURenderPass(pass); } SDL_SubmitGPUCommandBuffer(cmd); } void SDL3GPUShader::setCrtPiParams(const CrtPiParams& params) { if (crtpi_pipeline_ != nullptr) { crtpi_pipeline_->setParams(params); } } void SDL3GPUShader::setPostFXParams(const PostFXParams& params) { uniforms_.vignette_strength = params.vignette; uniforms_.scanline_strength = params.scanlines; uniforms_.chroma_strength = params.chroma; uniforms_.mask_strength = params.mask; uniforms_.gamma_strength = params.gamma; uniforms_.curvature = params.curvature; uniforms_.bleeding = params.bleeding; uniforms_.flicker = params.flicker; uniforms_.scan_dark_ratio = params.scan_dark_ratio; uniforms_.scan_dark_floor = params.scan_dark_floor; uniforms_.scan_edge_soft = params.scan_edge_soft; } void SDL3GPUShader::setVSync(bool vsync) { vsync_ = vsync; if (device_ != nullptr && window_ != nullptr) { SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_)); } } auto SDL3GPUShader::bestPresentMode(bool vsync) const -> SDL_GPUPresentMode { if (vsync) { return SDL_GPU_PRESENTMODE_VSYNC; } if (device_ != nullptr && window_ != nullptr) { if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_IMMEDIATE)) { return SDL_GPU_PRESENTMODE_IMMEDIATE; } if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_MAILBOX)) { return SDL_GPU_PRESENTMODE_MAILBOX; } } return SDL_GPU_PRESENTMODE_VSYNC; } void SDL3GPUShader::releaseWindow() { if (device_ != nullptr && window_ != nullptr) { SDL_WaitForGPUIdle(device_); SDL_ReleaseWindowFromGPUDevice(device_, window_); window_ = nullptr; } } void SDL3GPUShader::cleanup() { is_ready_ = false; if (device_ != nullptr) { SDL_WaitForGPUIdle(device_); // El pipeline CrtPi ha d'alliberar-se mentre el device segueix viu. crtpi_pipeline_.reset(); if (pipeline_ != nullptr) { SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_); pipeline_ = nullptr; } if (vertex_shader_ != nullptr) { SDL_ReleaseGPUShader(device_, vertex_shader_); vertex_shader_ = nullptr; } if (fragment_shader_ != nullptr) { SDL_ReleaseGPUShader(device_, fragment_shader_); fragment_shader_ = nullptr; } if (scene_texture_ != nullptr) { SDL_ReleaseGPUTexture(device_, scene_texture_); scene_texture_ = nullptr; } if (upload_buffer_ != nullptr) { SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_); upload_buffer_ = nullptr; } if (sampler_ != nullptr) { SDL_ReleaseGPUSampler(device_, sampler_); sampler_ = nullptr; } // Si no s'ha cridat releaseWindow() abans, el desreclam és implícit dins // DestroyGPUDevice; però això pot bloquejar-se en drivers Wayland si la // Wayland surface ja no existeix. El camí recomanat és cridar releaseWindow() // des de Screen::destroySdl() ABANS de SDL_DestroyWindow. SDL_DestroyGPUDevice(device_); device_ = nullptr; } window_ = nullptr; } auto makeDefaultShaderBackend() -> std::unique_ptr { return std::make_unique(); } } // namespace Rendering