feat(gpu): migrar a SDL3_GPU amb 2-pass rendering i post-processat

- Infraestructura GPU: GpuContext, GpuPipeline, GpuSpriteBatch, GpuTexture
- Engine::render() migrat a 2-pass: sprites → offscreen R8G8B8A8 → swapchain + vignette
- UI/text via software renderer (SDL3_ttf) + upload com a textura overlay GPU
- CMakeLists.txt actualitzat per incloure subsistema gpu/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 22:08:12 +01:00
parent 736db8cf41
commit 00a5875c92
11 changed files with 1341 additions and 240 deletions

208
source/gpu/gpu_texture.cpp Normal file
View File

@@ -0,0 +1,208 @@
#include "gpu_texture.hpp"
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_pixels.h>
#include <cstring> // memcpy
#include <string>
// stb_image is compiled in texture.cpp (STB_IMAGE_IMPLEMENTATION defined there)
#include "external/stb_image.h"
#include "resource_manager.hpp"
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
unsigned char* resource_data = nullptr;
size_t resource_size = 0;
if (!ResourceManager::loadResource(file_path, resource_data, resource_size)) {
SDL_Log("GpuTexture: can't load resource '%s'", file_path.c_str());
return false;
}
int w = 0, h = 0, orig = 0;
unsigned char* pixels = stbi_load_from_memory(
resource_data, static_cast<int>(resource_size),
&w, &h, &orig, STBI_rgb_alpha);
delete[] resource_data;
if (!pixels) {
SDL_Log("GpuTexture: stbi decode failed for '%s': %s",
file_path.c_str(), stbi_failure_reason());
return false;
}
destroy(device);
bool ok = uploadPixels(device, pixels, w, h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
stbi_image_free(pixels);
if (ok) {
ok = createSampler(device, true /*nearest = pixel-perfect sprites*/);
}
return ok;
}
bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) {
if (!surface) return false;
// Ensure RGBA32 format
SDL_Surface* rgba = surface;
bool need_free = false;
if (surface->format != SDL_PIXELFORMAT_RGBA32) {
rgba = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
if (!rgba) {
SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError());
return false;
}
need_free = true;
}
destroy(device);
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
if (ok) ok = createSampler(device, nearest);
if (need_free) SDL_DestroySurface(rgba);
return ok;
}
bool GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h,
SDL_GPUTextureFormat format) {
destroy(device);
SDL_GPUTextureCreateInfo info = {};
info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = format;
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET
| SDL_GPU_TEXTUREUSAGE_SAMPLER;
info.width = static_cast<Uint32>(w);
info.height = static_cast<Uint32>(h);
info.layer_count_or_depth = 1;
info.num_levels = 1;
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
texture_ = SDL_CreateGPUTexture(device, &info);
if (!texture_) {
SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError());
return false;
}
width_ = w;
height_ = h;
// Render targets are sampled with linear filter (postfx reads them)
return createSampler(device, false);
}
bool GpuTexture::createWhite(SDL_GPUDevice* device) {
destroy(device);
// 1×1 white RGBA pixel
const Uint8 white[4] = {255, 255, 255, 255};
bool ok = uploadPixels(device, white, 1, 1,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
if (ok) ok = createSampler(device, true);
return ok;
}
void GpuTexture::destroy(SDL_GPUDevice* device) {
if (!device) return;
if (sampler_) { SDL_ReleaseGPUSampler(device, sampler_); sampler_ = nullptr; }
if (texture_) { SDL_ReleaseGPUTexture(device, texture_); texture_ = nullptr; }
width_ = height_ = 0;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
int w, int h, SDL_GPUTextureFormat format) {
// Create GPU texture
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = format;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(w);
tex_info.height = static_cast<Uint32>(h);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
texture_ = SDL_CreateGPUTexture(device, &tex_info);
if (!texture_) {
SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError());
return false;
}
// Create transfer buffer and upload pixels
Uint32 data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = data_size;
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!transfer) {
SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError());
SDL_ReleaseGPUTexture(device, texture_);
texture_ = nullptr;
return false;
}
void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
if (!mapped) {
SDL_Log("GpuTexture: map failed: %s", SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device, transfer);
SDL_ReleaseGPUTexture(device, texture_);
texture_ = nullptr;
return false;
}
memcpy(mapped, pixels, data_size);
SDL_UnmapGPUTransferBuffer(device, transfer);
// Upload via command buffer
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device);
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = transfer;
src.offset = 0;
src.pixels_per_row = static_cast<Uint32>(w);
src.rows_per_layer = static_cast<Uint32>(h);
SDL_GPUTextureRegion dst = {};
dst.texture = texture_;
dst.mip_level = 0;
dst.layer = 0;
dst.x = dst.y = dst.z = 0;
dst.w = static_cast<Uint32>(w);
dst.h = static_cast<Uint32>(h);
dst.d = 1;
SDL_UploadToGPUTexture(copy, &src, &dst, false);
SDL_EndGPUCopyPass(copy);
SDL_SubmitGPUCommandBuffer(cmd);
SDL_ReleaseGPUTransferBuffer(device, transfer);
width_ = w;
height_ = h;
return true;
}
bool GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) {
SDL_GPUSamplerCreateInfo info = {};
info.min_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
info.mag_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sampler_ = SDL_CreateGPUSampler(device, &info);
if (!sampler_) {
SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError());
return false;
}
return true;
}