324 lines
13 KiB
C++
324 lines
13 KiB
C++
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
|
|
#include "core/rendering/sdl3gpu/shader_factory.hpp"
|
|
#include "core/rendering/sdl3gpu/viewport.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<float>(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<CrtPiPipeline>();
|
|
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) {
|
|
std::cerr << "SDL3GPUShader: SDL_CreateGPUDevice ha fallat: " << SDL_GetError() << '\n';
|
|
return false;
|
|
}
|
|
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
|
std::cerr << "SDL3GPUShader: SDL_ClaimWindowForGPUDevice ha fallat: " << SDL_GetError() << '\n';
|
|
return false;
|
|
}
|
|
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
|
|
|
const char* name = SDL_GetGPUDeviceDriver(device_);
|
|
driver_name_ = (name != nullptr) ? name : "";
|
|
return true;
|
|
}
|
|
|
|
auto SDL3GPUShader::createShaders() -> bool {
|
|
#ifdef __APPLE__
|
|
vertex_shader_ = createShaderMSL(device_, 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) {
|
|
std::cerr << "SDL3GPUShader: SDL_CreateGPUShader ha fallat: " << SDL_GetError() << '\n';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
auto SDL3GPUShader::createPipeline() -> bool {
|
|
const SDL_GPUTextureFormat SWAPCHAIN_FORMAT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
|
|
|
SDL_GPUColorTargetBlendState no_blend{};
|
|
SDL_GPUColorTargetDescription color_target{};
|
|
color_target.format = SWAPCHAIN_FORMAT;
|
|
color_target.blend_state = no_blend;
|
|
|
|
SDL_GPUGraphicsPipelineCreateInfo info{};
|
|
info.vertex_shader = vertex_shader_;
|
|
info.fragment_shader = fragment_shader_;
|
|
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
|
info.target_info.num_color_targets = 1;
|
|
info.target_info.color_target_descriptions = &color_target;
|
|
|
|
pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
|
if (pipeline_ == nullptr) {
|
|
std::cerr << "SDL3GPUShader: SDL_CreateGPUGraphicsPipeline ha fallat: " << SDL_GetError() << '\n';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
auto SDL3GPUShader::createResources() -> bool {
|
|
SDL_GPUTextureCreateInfo tex_info{};
|
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
|
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
|
tex_info.width = static_cast<Uint32>(game_width_);
|
|
tex_info.height = static_cast<Uint32>(game_height_);
|
|
tex_info.layer_count_or_depth = 1;
|
|
tex_info.num_levels = 1;
|
|
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
|
|
|
|
SDL_GPUTransferBufferCreateInfo tb_info{};
|
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
|
tb_info.size = static_cast<Uint32>(game_width_) * static_cast<Uint32>(game_height_) * 4U;
|
|
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
|
|
|
|
SDL_GPUSamplerCreateInfo samp_info{};
|
|
samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
|
|
samp_info.mag_filter = SDL_GPU_FILTER_NEAREST;
|
|
samp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
|
samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
|
samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
|
samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
|
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
|
|
|
|
if (scene_texture_ == nullptr || upload_buffer_ == nullptr || sampler_ == nullptr) {
|
|
std::cerr << "SDL3GPUShader: creació de recursos GPU ha fallat: " << SDL_GetError() << '\n';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void SDL3GPUShader::uploadPixels(const std::uint32_t* pixels, int width, int height) {
|
|
if (!is_ready_ || upload_buffer_ == nullptr) { return; }
|
|
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
|
|
if (mapped == nullptr) { return; }
|
|
std::memcpy(mapped, pixels, static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U);
|
|
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
|
|
}
|
|
|
|
void SDL3GPUShader::render() {
|
|
if (!is_ready_) { return; }
|
|
|
|
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
|
if (cmd == nullptr) { return; }
|
|
|
|
// Copy pass: transfer buffer -> scene texture.
|
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
|
if (copy != nullptr) {
|
|
SDL_GPUTextureTransferInfo src{};
|
|
src.transfer_buffer = upload_buffer_;
|
|
src.pixels_per_row = static_cast<Uint32>(game_width_);
|
|
src.rows_per_layer = static_cast<Uint32>(game_height_);
|
|
|
|
SDL_GPUTextureRegion dst{};
|
|
dst.texture = scene_texture_;
|
|
dst.w = static_cast<Uint32>(game_width_);
|
|
dst.h = static_cast<Uint32>(game_height_);
|
|
dst.d = 1;
|
|
|
|
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
|
SDL_EndGPUCopyPass(copy);
|
|
}
|
|
|
|
// Agafa la textura del swapchain (pot fallar si la finestra està minimitzada).
|
|
SDL_GPUTexture* swapchain = nullptr;
|
|
Uint32 sw = 0;
|
|
Uint32 sh = 0;
|
|
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh) || swapchain == nullptr) {
|
|
SDL_SubmitGPUCommandBuffer(cmd);
|
|
return;
|
|
}
|
|
|
|
// 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<float>(SDL_GetTicks()) / 1000.0F;
|
|
|
|
SDL_GPUColorTargetInfo color_target{};
|
|
color_target.texture = swapchain;
|
|
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
|
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
|
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
|
|
|
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
|
|
if (pass != nullptr) {
|
|
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<float>(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<ShaderBackend> {
|
|
return std::make_unique<SDL3GPUShader>();
|
|
}
|
|
|
|
} // namespace Rendering
|