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

View File

@@ -100,9 +100,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// SDL ya inicializado arriba para validación
{
// Crear ventana principal (fullscreen si se especifica)
// NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4)
// El DPI se detectará manualmente con SDL_GetWindowSizeInPixels()
Uint32 window_flags = SDL_WINDOW_OPENGL;
// SDL_WINDOW_HIGH_PIXEL_DENSITY removido — DPI detectado con SDL_GetWindowSizeInPixels()
// SDL_WINDOW_OPENGL eliminado — SDL_GPU usa Metal/Vulkan/D3D12 directamente
Uint32 window_flags = 0;
if (fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
}
@@ -117,20 +117,24 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Crear renderizador
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl;
// Inicializar SDL_GPU (sustituye SDL_Renderer como backend principal)
gpu_ctx_ = std::make_unique<GpuContext>();
if (!gpu_ctx_->init(window_)) {
std::cout << "¡No se pudo inicializar SDL_GPU!" << std::endl;
success = false;
} else {
// Establecer color inicial del renderizador
SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);
gpu_ctx_->setVSync(vsync_enabled_);
// Establecer tamaño lógico para el renderizado (resolución interna)
SDL_SetRenderLogicalPresentation(renderer_, logical_width, logical_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Configurar V-Sync inicial
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
// Crear renderer de software para UI/texto (SDL3_ttf no es compatible con SDL_GPU)
// Renderiza a ui_surface_, que luego se sube como textura GPU overlay
ui_surface_ = SDL_CreateSurface(logical_width, logical_height, SDL_PIXELFORMAT_RGBA32);
if (ui_surface_) {
ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_);
}
if (!ui_renderer_) {
std::cout << "Advertencia: no se pudo crear el renderer de UI software" << std::endl;
// No es crítico — el juego funciona sin texto
}
}
}
}
@@ -143,7 +147,8 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
struct TextureInfo {
std::string name;
std::shared_ptr<Texture> texture;
std::shared_ptr<Texture> texture; // legacy (para physics sizing)
std::string path; // resource path para GPU upload
int width;
};
std::vector<TextureInfo> texture_files;
@@ -151,34 +156,32 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Buscar todas las texturas PNG en data/balls/
namespace fs = std::filesystem;
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
// Cargar todas las texturas desde disco
for (const auto& entry : fs::directory_iterator(balls_dir)) {
if (entry.is_regular_file() && entry.path().extension() == ".png") {
std::string filename = entry.path().stem().string();
std::string fullpath = entry.path().string();
// Cargar textura y obtener dimensiones
auto texture = std::make_shared<Texture>(renderer_, fullpath);
// Cargar textura legacy (usa ui_renderer_ en lugar del renderer_ eliminado)
auto texture = std::make_shared<Texture>(ui_renderer_, fullpath);
int width = texture->getWidth();
texture_files.push_back({filename, texture, width});
texture_files.push_back({filename, texture, fullpath, width});
}
}
} else {
// Fallback: cargar texturas desde pack usando la lista del ResourceManager
// Fallback: cargar texturas desde pack
if (ResourceManager::isPackLoaded()) {
auto pack_resources = ResourceManager::getResourceList();
// Filtrar solo los recursos en balls/ con extensión .png
for (const auto& resource : pack_resources) {
if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
std::string tex_name = resource.substr(6); // Quitar "balls/"
std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión
std::string tex_name = resource.substr(6);
std::string name = tex_name.substr(0, tex_name.find('.'));
auto texture = std::make_shared<Texture>(renderer_, resource);
auto texture = std::make_shared<Texture>(ui_renderer_, resource);
int width = texture->getWidth();
texture_files.push_back({name, texture, width});
texture_files.push_back({name, texture, resource, width});
}
}
}
@@ -186,13 +189,20 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
std::sort(texture_files.begin(), texture_files.end(), [](const TextureInfo& a, const TextureInfo& b) {
return a.width > b.width; // Descendente por tamaño
return a.width > b.width;
});
// Guardar texturas ya cargadas en orden (0=big, 1=normal, 2=small, 3=tiny)
// Guardar texturas en orden + crear texturas GPU
for (const auto& info : texture_files) {
textures_.push_back(info.texture);
texture_names_.push_back(info.name);
// Cargar textura GPU para renderizado de sprites
auto gpu_tex = std::make_unique<GpuTexture>();
if (gpu_ctx_ && !gpu_tex->fromFile(gpu_ctx_->device(), info.path)) {
std::cerr << "Advertencia: no se pudo cargar textura GPU: " << info.name << std::endl;
}
gpu_textures_.push_back(std::move(gpu_tex));
}
// Verificar que se cargaron texturas
@@ -201,16 +211,54 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
success = false;
}
// Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1)
current_texture_index_ = 0; // Fallback
// Buscar índice de "normal" para usarlo como textura inicial
current_texture_index_ = 0;
for (size_t i = 0; i < texture_names_.size(); i++) {
if (texture_names_[i] == "normal") {
current_texture_index_ = i; // Iniciar en "normal" (índice 1)
current_texture_index_ = i;
break;
}
}
texture_ = textures_[current_texture_index_];
current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente
current_ball_size_ = texture_->getWidth();
// Initialize GPU pipeline, sprite batch, and render textures
if (gpu_ctx_ && success) {
SDL_GPUTextureFormat offscreen_fmt = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
gpu_pipeline_ = std::make_unique<GpuPipeline>();
if (!gpu_pipeline_->init(gpu_ctx_->device(), gpu_ctx_->swapchainFormat(), offscreen_fmt)) {
std::cerr << "ERROR: No se pudo crear el pipeline GPU" << std::endl;
success = false;
}
sprite_batch_ = std::make_unique<GpuSpriteBatch>();
if (!sprite_batch_->init(gpu_ctx_->device())) {
std::cerr << "ERROR: No se pudo crear el sprite batch GPU" << std::endl;
success = false;
}
offscreen_tex_ = std::make_unique<GpuTexture>();
if (!offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_,
offscreen_fmt)) {
std::cerr << "ERROR: No se pudo crear render target offscreen" << std::endl;
success = false;
}
white_tex_ = std::make_unique<GpuTexture>();
if (!white_tex_->createWhite(gpu_ctx_->device())) {
std::cerr << "ERROR: No se pudo crear textura blanca" << std::endl;
success = false;
}
// Create UI overlay texture (render target usage so GPU can sample it)
ui_tex_ = std::make_unique<GpuTexture>();
if (!ui_tex_->createRenderTarget(gpu_ctx_->device(),
logical_width, logical_height,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)) {
std::cerr << "Advertencia: no se pudo crear textura UI GPU" << std::endl;
}
}
srand(static_cast<unsigned>(time(nullptr)));
@@ -240,7 +288,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Inicializar UIManager (HUD, FPS, notificaciones)
// NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager
ui_manager_ = std::make_unique<UIManager>();
ui_manager_->initialize(renderer_, theme_manager_.get(),
ui_manager_->initialize(ui_renderer_, theme_manager_.get(),
physical_window_width_, physical_window_height_,
current_screen_width_, current_screen_height_);
@@ -280,7 +328,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Inicializar AppLogo (logo periódico en pantalla)
app_logo_ = std::make_unique<AppLogo>();
if (!app_logo_->initialize(renderer_, current_screen_width_, current_screen_height_)) {
if (!app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_)) {
std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << std::endl;
// No es crítico, continuar sin logo
app_logo_.reset();
@@ -318,11 +366,28 @@ void Engine::run() {
}
void Engine::shutdown() {
// Limpiar recursos SDL
if (renderer_) {
SDL_DestroyRenderer(renderer_);
renderer_ = nullptr;
// Wait for GPU idle before releasing GPU resources
if (gpu_ctx_) SDL_WaitForGPUIdle(gpu_ctx_->device());
// Release GPU sprite textures
gpu_textures_.clear();
// Release GPU render targets and utility textures
if (gpu_ctx_) {
if (ui_tex_) { ui_tex_->destroy(gpu_ctx_->device()); ui_tex_.reset(); }
if (white_tex_) { white_tex_->destroy(gpu_ctx_->device()); white_tex_.reset(); }
if (offscreen_tex_) { offscreen_tex_->destroy(gpu_ctx_->device()); offscreen_tex_.reset(); }
if (sprite_batch_) { sprite_batch_->destroy(gpu_ctx_->device()); sprite_batch_.reset(); }
if (gpu_pipeline_) { gpu_pipeline_->destroy(gpu_ctx_->device()); gpu_pipeline_.reset(); }
}
// Destroy software UI renderer and surface
if (ui_renderer_) { SDL_DestroyRenderer(ui_renderer_); ui_renderer_ = nullptr; }
if (ui_surface_) { SDL_DestroySurface(ui_surface_); ui_surface_ = nullptr; }
// Destroy GPU context (releases device and window claim)
if (gpu_ctx_) { gpu_ctx_->destroy(); gpu_ctx_.reset(); }
if (window_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
@@ -636,140 +701,148 @@ void Engine::toggleLogoMode() {
}
void Engine::render() {
// Limpiar framebuffer completamente (evita artefactos en barras negras al cambiar modos)
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // Negro para barras de letterbox/integer
SDL_RenderClear(renderer_);
if (!gpu_ctx_ || !sprite_batch_ || !gpu_pipeline_) return;
// Renderizar fondo degradado (delegado a ThemeManager)
{
float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bottom_r, bottom_g, bottom_b);
// === Render UI text to software surface ===
renderUIToSurface();
// Crear quad de pantalla completa con degradado
SDL_Vertex bg_vertices[4];
// === Acquire command buffer ===
SDL_GPUCommandBuffer* cmd = gpu_ctx_->acquireCommandBuffer();
if (!cmd) return;
// Vértice superior izquierdo
bg_vertices[0].position = {0, 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f};
bg_vertices[0].color = {top_r, top_g, top_b, 1.0f};
// === Upload UI surface to GPU texture (inline copy pass) ===
uploadUISurface(cmd);
// Vértice superior derecho
bg_vertices[1].position = {static_cast<float>(current_screen_width_), 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f};
bg_vertices[1].color = {top_r, top_g, top_b, 1.0f};
// === Build sprite batch ===
sprite_batch_->beginFrame();
// Vértice inferior derecho
bg_vertices[2].position = {static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f};
bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Background gradient
float top_r = 0, top_g = 0, top_b = 0, bot_r = 0, bot_g = 0, bot_b = 0;
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bot_r, bot_g, bot_b);
sprite_batch_->addBackground(
static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_),
top_r, top_g, top_b, bot_r, bot_g, bot_b);
// Vértice inferior izquierdo
bg_vertices[3].position = {0, static_cast<float>(current_screen_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f};
bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Índices para 2 triángulos
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
// Renderizar sin textura (nullptr)
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
}
// Limpiar batches del frame anterior
batch_vertices_.clear();
batch_indices_.clear();
// Obtener referencia a las bolas desde SceneManager
// Sprites (balls)
const auto& balls = scene_manager_->getBalls();
if (current_mode_ == SimulationMode::SHAPE) {
// MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm)
// Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero
// Bucket sort per profunditat Z (O(N) vs O(N log N))
// Bucket sort by depth Z (Painter's Algorithm)
for (size_t i = 0; i < balls.size(); i++) {
int b = static_cast<int>(balls[i]->getDepthBrightness() * (DEPTH_SORT_BUCKETS - 1));
depth_buckets_[std::clamp(b, 0, DEPTH_SORT_BUCKETS - 1)].push_back(i);
}
// Renderizar en orden de profundidad (bucket 0 = fons, bucket 255 = davant)
for (int b = 0; b < DEPTH_SORT_BUCKETS; b++) {
for (size_t idx : depth_buckets_[b]) {
SDL_FRect pos = balls[idx]->getPosition();
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
Color color = theme_manager_->getInterpolatedColor(idx);
float brightness = balls[idx]->getDepthBrightness();
float depth_scale = balls[idx]->getDepthScale();
// Mapear brightness de 0-1 a rango MIN-MAX
float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
// Aplicar factor de brillo al color
int r_mod = static_cast<int>(color.r * brightness_factor);
int g_mod = static_cast<int>(color.g * brightness_factor);
int b_mod = static_cast<int>(color.b * brightness_factor);
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, r_mod, g_mod, b_mod, depth_scale);
float bf = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h,
color.r / 255.0f * bf,
color.g / 255.0f * bf,
color.b / 255.0f * bf,
1.0f, depth_scale,
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
}
depth_buckets_[b].clear(); // netejar per al proper frame
depth_buckets_[b].clear();
}
} else {
// MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
const auto& balls = scene_manager_->getBalls();
size_t idx = 0;
for (auto& ball : balls) {
for (const auto& ball : balls) {
SDL_FRect pos = ball->getPosition();
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
Color color = theme_manager_->getInterpolatedColor(idx);
sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h,
color.r / 255.0f, color.g / 255.0f, color.b / 255.0f,
1.0f, 1.0f,
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
idx++;
}
}
// Renderizar todas las bolas en una sola llamada
if (!batch_vertices_.empty()) {
SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast<int>(batch_vertices_.size()), batch_indices_.data(), static_cast<int>(batch_indices_.size()));
// UI overlay quad (drawn in Pass 2 over the postfx output)
sprite_batch_->addFullscreenOverlay();
// Upload batch to GPU buffers
if (!sprite_batch_->uploadBatch(gpu_ctx_->device(), cmd)) {
gpu_ctx_->submit(cmd);
return;
}
// SISTEMA DE TEXTO ANTIGUO DESHABILITADO
// Reemplazado completamente por el sistema de notificaciones (Notifier)
// El doble renderizado causaba que aparecieran textos duplicados detrás de las notificaciones
/*
if (show_text_) {
// Obtener datos del tema actual (delegado a ThemeManager)
int text_color_r, text_color_g, text_color_b;
theme_manager_->getCurrentThemeTextColor(text_color_r, text_color_g, text_color_b);
const char* theme_name_es = theme_manager_->getCurrentThemeNameES();
GpuTexture* sprite_tex = (!gpu_textures_.empty())
? gpu_textures_[current_texture_index_].get() : nullptr;
// Calcular espaciado dinámico
int line_height = text_renderer_.getTextHeight();
int margin = 8;
// === Pass 1: Render background + sprites to offscreen texture ===
if (offscreen_tex_ && offscreen_tex_->isValid() && sprite_tex && sprite_tex->isValid()) {
SDL_GPUColorTargetInfo ct = {};
ct.texture = offscreen_tex_->texture();
ct.load_op = SDL_GPU_LOADOP_CLEAR;
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
ct.store_op = SDL_GPU_STOREOP_STORE;
// Texto del número de pelotas con color del tema
text_renderer_.printPhysical(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->spritePipeline());
// Mostrar nombre del tema en castellano debajo del número de pelotas
// (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
if (theme_name_es != nullptr && text_ != theme_name_es) {
int theme_text_width = text_renderer_.getTextWidth(theme_name_es);
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
int theme_y = margin + line_height; // Espaciado dinámico
SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
SDL_BindGPUVertexBuffers(pass1, 0, &vb, 1);
SDL_BindGPUIndexBuffer(pass1, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
// Texto del nombre del tema con el mismo color
text_renderer_.printPhysical(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
// Background (white texture tinted by vertex color)
if (white_tex_ && white_tex_->isValid() && sprite_batch_->bgIndexCount() > 0) {
SDL_GPUTextureSamplerBinding tsb = {white_tex_->texture(), white_tex_->sampler()};
SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->bgIndexCount(), 1, 0, 0, 0);
}
}
*/
// Renderizar UI (debug HUD, texto obsoleto, notificaciones) - delegado a UIManager
ui_manager_->render(renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(),
shape_manager_->getActiveShape(), shape_manager_->getConvergence(),
physical_window_width_, physical_window_height_, current_screen_width_);
// Sprites
if (sprite_batch_->spriteIndexCount() > 0) {
SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()};
SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->spriteIndexCount(), 1,
sprite_batch_->spriteIndexOffset(), 0, 0);
}
// Renderizar AppLogo (logo periódico) - después de UI, antes de present
if (app_logo_) {
app_logo_->render();
SDL_EndGPURenderPass(pass1);
}
SDL_RenderPresent(renderer_);
// === Pass 2: PostFX (vignette) + UI overlay to swapchain ===
Uint32 sw_w = 0, sw_h = 0;
SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h);
if (swapchain && offscreen_tex_ && offscreen_tex_->isValid()) {
SDL_GPUColorTargetInfo ct = {};
ct.texture = swapchain;
ct.load_op = SDL_GPU_LOADOP_CLEAR;
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
ct.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
// PostFX: full-screen triangle via vertex_id (no vertex buffer needed)
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline());
SDL_GPUTextureSamplerBinding scene_tsb = {offscreen_tex_->texture(), offscreen_tex_->sampler()};
SDL_BindGPUFragmentSamplers(pass2, 0, &scene_tsb, 1);
SDL_DrawGPUPrimitives(pass2, 3, 1, 0, 0);
// UI overlay (alpha-blended, uses sprite pipeline)
if (ui_tex_ && ui_tex_->isValid() && sprite_batch_->overlayIndexCount() > 0) {
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->spritePipeline());
SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
SDL_BindGPUVertexBuffers(pass2, 0, &vb, 1);
SDL_BindGPUIndexBuffer(pass2, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
SDL_GPUTextureSamplerBinding ui_tsb = {ui_tex_->texture(), ui_tex_->sampler()};
SDL_BindGPUFragmentSamplers(pass2, 0, &ui_tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass2, sprite_batch_->overlayIndexCount(), 1,
sprite_batch_->overlayIndexOffset(), 0, 0);
}
SDL_EndGPURenderPass(pass2);
}
gpu_ctx_->submit(cmd);
}
void Engine::showNotificationForAction(const std::string& text) {
@@ -790,8 +863,8 @@ void Engine::toggleVSync() {
// Actualizar texto en UIManager
ui_manager_->updateVSyncText(vsync_enabled_);
// Aplicar el cambio de V-Sync al renderizador
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
// Aplicar el cambio de V-Sync al contexto GPU
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
}
void Engine::toggleFullscreen() {
@@ -837,8 +910,8 @@ void Engine::toggleRealFullscreen() {
SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
SDL_SetWindowFullscreen(window_, true);
// Actualizar presentación lógica del renderizador
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Recrear render target offscreen con nueva resolución
recreateOffscreenTexture();
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
@@ -873,8 +946,8 @@ void Engine::toggleRealFullscreen() {
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Restaurar presentación lógica base
SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Recrear render target offscreen con resolución base
recreateOffscreenTexture();
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
@@ -915,81 +988,25 @@ void Engine::toggleIntegerScaling() {
break;
}
// Aplicar el nuevo modo de escalado
SDL_RendererLogicalPresentation presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
// SDL_GPU stretches to fill swapchain by default; just show notification
const char* mode_name = "INTEGER";
switch (current_scaling_mode_) {
case ScalingMode::INTEGER:
presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
mode_name = "INTEGER";
break;
case ScalingMode::LETTERBOX:
presentation = SDL_LOGICAL_PRESENTATION_LETTERBOX;
mode_name = "LETTERBOX";
break;
case ScalingMode::STRETCH:
presentation = SDL_LOGICAL_PRESENTATION_STRETCH;
mode_name = "STRETCH";
break;
case ScalingMode::INTEGER: mode_name = "INTEGER"; break;
case ScalingMode::LETTERBOX: mode_name = "LETTERBOX"; break;
case ScalingMode::STRETCH: mode_name = "STRETCH"; break;
}
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation);
// Mostrar notificación del cambio
std::string notification = std::string("Escalado: ") + mode_name;
ui_manager_->showNotification(notification);
}
void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
int vertex_index = static_cast<int>(batch_vertices_.size());
// Crear 4 vértices para el quad (2 triángulos)
SDL_Vertex vertices[4];
// Convertir colores de int (0-255) a float (0.0-1.0)
float rf = r / 255.0f;
float gf = g / 255.0f;
float bf = b / 255.0f;
// Aplicar escala al tamaño (centrado en el punto x, y)
float scaled_w = w * scale;
float scaled_h = h * scale;
float offset_x = (w - scaled_w) / 2.0f; // Offset para centrar
float offset_y = (h - scaled_h) / 2.0f;
// Vértice superior izquierdo
vertices[0].position = {x + offset_x, y + offset_y};
vertices[0].tex_coord = {0.0f, 0.0f};
vertices[0].color = {rf, gf, bf, 1.0f};
// Vértice superior derecho
vertices[1].position = {x + offset_x + scaled_w, y + offset_y};
vertices[1].tex_coord = {1.0f, 0.0f};
vertices[1].color = {rf, gf, bf, 1.0f};
// Vértice inferior derecho
vertices[2].position = {x + offset_x + scaled_w, y + offset_y + scaled_h};
vertices[2].tex_coord = {1.0f, 1.0f};
vertices[2].color = {rf, gf, bf, 1.0f};
// Vértice inferior izquierdo
vertices[3].position = {x + offset_x, y + offset_y + scaled_h};
vertices[3].tex_coord = {0.0f, 1.0f};
vertices[3].color = {rf, gf, bf, 1.0f};
// Añadir vértices al batch
for (int i = 0; i < 4; i++) {
batch_vertices_.push_back(vertices[i]);
}
// Añadir índices para 2 triángulos
batch_indices_.push_back(vertex_index + 0);
batch_indices_.push_back(vertex_index + 1);
batch_indices_.push_back(vertex_index + 2);
batch_indices_.push_back(vertex_index + 2);
batch_indices_.push_back(vertex_index + 3);
batch_indices_.push_back(vertex_index + 0);
if (!sprite_batch_) return;
sprite_batch_->addSprite(x, y, w, h,
r / 255.0f, g / 255.0f, b / 255.0f, 1.0f,
scale,
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
}
// Sistema de zoom dinámico
@@ -1186,7 +1203,7 @@ void Engine::runPerformanceBenchmark() {
}
SDL_HideWindow(window_);
SDL_SetRenderVSync(renderer_, 0);
if (gpu_ctx_) gpu_ctx_->setVSync(false); // Disable VSync for benchmark
const int BENCH_DURATION_MS = 600;
const int WARMUP_FRAMES = 5;
@@ -1194,7 +1211,7 @@ void Engine::runPerformanceBenchmark() {
SimulationMode original_mode = current_mode_;
auto restore = [&]() {
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
SDL_ShowWindow(window_);
current_mode_ = original_mode;
if (shape_manager_->isShapeModeActive()) {
@@ -1261,4 +1278,104 @@ void Engine::runPerformanceBenchmark() {
max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO;
restore();
}
// ============================================================================
// GPU HELPERS
// ============================================================================
bool Engine::loadGpuSpriteTexture(size_t index) {
if (!gpu_ctx_ || index >= gpu_textures_.size()) return false;
return gpu_textures_[index] && gpu_textures_[index]->isValid();
}
void Engine::recreateOffscreenTexture() {
if (!gpu_ctx_ || !offscreen_tex_) return;
SDL_WaitForGPUIdle(gpu_ctx_->device());
offscreen_tex_->destroy(gpu_ctx_->device());
offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
// Recreate UI texture to match new screen size
if (ui_tex_) {
ui_tex_->destroy(gpu_ctx_->device());
ui_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
}
// Recreate software surface to match new screen size
if (ui_surface_) {
SDL_DestroySurface(ui_surface_);
ui_surface_ = SDL_CreateSurface(current_screen_width_, current_screen_height_,
SDL_PIXELFORMAT_RGBA32);
}
}
void Engine::renderUIToSurface() {
if (!ui_renderer_ || !ui_surface_) return;
// Clear surface (fully transparent)
SDL_SetRenderDrawColor(ui_renderer_, 0, 0, 0, 0);
SDL_RenderClear(ui_renderer_);
// Render UI (HUD, FPS counter, notifications)
ui_manager_->render(ui_renderer_, this, scene_manager_.get(), current_mode_,
state_manager_->getCurrentMode(),
shape_manager_->getActiveShape(), shape_manager_->getConvergence(),
physical_window_width_, physical_window_height_, current_screen_width_);
// Render periodic logo overlay
if (app_logo_) {
app_logo_->render();
}
SDL_RenderPresent(ui_renderer_); // Flush software renderer to surface
}
void Engine::uploadUISurface(SDL_GPUCommandBuffer* cmd_buf) {
if (!ui_tex_ || !ui_tex_->isValid() || !ui_surface_ || !gpu_ctx_) return;
int w = ui_surface_->w;
int h = ui_surface_->h;
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(gpu_ctx_->device(), &tb_info);
if (!transfer) return;
void* mapped = SDL_MapGPUTransferBuffer(gpu_ctx_->device(), transfer, true);
if (!mapped) {
SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer);
return;
}
memcpy(mapped, ui_surface_->pixels, data_size);
SDL_UnmapGPUTransferBuffer(gpu_ctx_->device(), transfer);
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);
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 = ui_tex_->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_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer);
}

View File

@@ -1,8 +1,9 @@
#pragma once
#include <SDL3/SDL_events.h> // for SDL_Event
#include <SDL3/SDL_render.h> // for SDL_Renderer
#include <SDL3/SDL_render.h> // for SDL_Renderer (ui_renderer_ software renderer)
#include <SDL3/SDL_stdinc.h> // for Uint64
#include <SDL3/SDL_surface.h> // for SDL_Surface (ui_surface_)
#include <SDL3/SDL_video.h> // for SDL_Window
#include <array> // for array
@@ -15,6 +16,10 @@
#include "boids_mgr/boid_manager.hpp" // for BoidManager
#include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType
#include "external/texture.hpp" // for Texture
#include "gpu/gpu_context.hpp" // for GpuContext
#include "gpu/gpu_pipeline.hpp" // for GpuPipeline
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
#include "gpu/gpu_texture.hpp" // for GpuTexture
#include "input/input_handler.hpp" // for InputHandler
#include "scene/scene_manager.hpp" // for SceneManager
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
@@ -124,14 +129,32 @@ class Engine {
std::unique_ptr<UIManager> ui_manager_; // Gestión de UI (HUD, FPS, notificaciones)
std::unique_ptr<AppLogo> app_logo_; // Gestión de logo periódico en pantalla
// Recursos SDL
// === SDL window ===
SDL_Window* window_ = nullptr;
SDL_Renderer* renderer_ = nullptr;
// === SDL_GPU rendering pipeline ===
std::unique_ptr<GpuContext> gpu_ctx_; // Device + swapchain
std::unique_ptr<GpuPipeline> gpu_pipeline_; // Sprite + postfx pipelines
std::unique_ptr<GpuSpriteBatch> sprite_batch_; // Per-frame vertex/index batch
std::unique_ptr<GpuTexture> offscreen_tex_; // Offscreen render target (Pass 1)
std::unique_ptr<GpuTexture> white_tex_; // 1×1 white (background gradient)
std::unique_ptr<GpuTexture> ui_tex_; // UI text overlay texture
// GPU sprite textures (one per ball skin, parallel to textures_/texture_names_)
std::unique_ptr<GpuTexture> gpu_texture_; // Active GPU sprite texture
std::vector<std::unique_ptr<GpuTexture>> gpu_textures_; // All GPU sprite textures
// === SDL_Renderer (software, for UI text via SDL3_ttf) ===
// Renders to ui_surface_, then uploaded as gpu texture overlay.
SDL_Renderer* ui_renderer_ = nullptr;
SDL_Surface* ui_surface_ = nullptr;
// Legacy Texture objects — kept for ball physics sizing and AppLogo
std::shared_ptr<Texture> texture_ = nullptr; // Textura activa actual
std::vector<std::shared_ptr<Texture>> textures_; // Todas las texturas disponibles
std::vector<std::string> texture_names_; // Nombres de texturas (sin extensión)
size_t current_texture_index_ = 0; // Índice de textura activa
int current_ball_size_ = 10; // Tamaño actual de pelotas (dinámico, se actualiza desde texture)
int current_ball_size_ = 10; // Tamaño actual de pelotas (dinámico)
// Estado del simulador
bool should_exit_ = false;
@@ -143,12 +166,12 @@ class Engine {
// Sistema de zoom dinámico
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
// V-Sync
// V-Sync y fullscreen
bool vsync_enabled_ = true;
bool fullscreen_enabled_ = false;
bool real_fullscreen_enabled_ = false;
bool kiosk_mode_ = false;
ScalingMode current_scaling_mode_ = ScalingMode::INTEGER; // Modo de escalado actual (F5)
ScalingMode current_scaling_mode_ = ScalingMode::INTEGER;
// Resolución base (configurada por CLI o default)
int base_screen_width_ = DEFAULT_SCREEN_WIDTH;
@@ -164,15 +187,13 @@ class Engine {
// Sistema de temas (delegado a ThemeManager)
std::unique_ptr<ThemeManager> theme_manager_;
int theme_page_ = 0; // Página actual de temas (0 o 1) para acceso por Numpad
int theme_page_ = 0;
// Modo de simulación actual (PHYSICS/SHAPE/BOIDS) — fuente de verdad para Engine
// El estado de figuras (active_shape_, scale, etc.) está en ShapeManager
// Modo de simulación actual (PHYSICS/SHAPE/BOIDS)
SimulationMode current_mode_ = SimulationMode::PHYSICS;
// Sistema de Modo DEMO (auto-play) y LOGO
// Toda la lógica DEMO/LOGO y su estado vive en StateManager
int max_auto_scenario_ = 5; // Índice máximo en modos auto (resultado del benchmark)
int max_auto_scenario_ = 5;
// Escenario custom (--custom-balls)
int custom_scenario_balls_ = 0;
@@ -180,19 +201,10 @@ class Engine {
bool custom_auto_available_ = false;
bool skip_benchmark_ = false;
// Batch rendering
std::vector<SDL_Vertex> batch_vertices_;
std::vector<int> batch_indices_;
// Bucket sort per z-ordering (SHAPE mode)
static constexpr int DEPTH_SORT_BUCKETS = 256;
std::array<std::vector<size_t>, DEPTH_SORT_BUCKETS> depth_buckets_;
// Configuración del sistema de texto (constantes configurables)
static constexpr const char* TEXT_FONT_PATH = "data/fonts/determination.ttf";
static constexpr int TEXT_BASE_SIZE = 24; // Tamaño base para 240p
static constexpr bool TEXT_ANTIALIASING = true; // true = suavizado, false = píxeles nítidos
// Métodos principales del loop
void calculateDeltaTime();
void update();
@@ -201,24 +213,30 @@ class Engine {
// Benchmark de rendimiento (determina max_auto_scenario_ al inicio)
void runPerformanceBenchmark();
// Métodos auxiliares privados (llamados por la interfaz pública)
// Métodos auxiliares privados
// Sistema de cambio de sprites dinámico - Métodos privados
void switchTextureInternal(bool show_notification); // Implementación interna del cambio de textura
// Sistema de cambio de sprites dinámico
void switchTextureInternal(bool show_notification);
// Sistema de zoom dinámico - Métodos privados
// Sistema de zoom dinámico
int calculateMaxWindowZoom() const;
void setWindowZoom(int new_zoom);
void zoomIn();
void zoomOut();
void updatePhysicalWindowSize(); // Actualizar tamaño físico real de ventana
void updatePhysicalWindowSize();
// Rendering
// Rendering (GPU path replaces addSpriteToBatch)
void addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale = 1.0f);
// Sistema de Figuras 3D - Métodos privados (thin wrappers a ShapeManager)
void toggleShapeModeInternal(bool force_gravity_on_exit = true); // Delega a ShapeManager + sincroniza current_mode_
void activateShapeInternal(ShapeType type); // Delega a ShapeManager + sets current_mode_ = SHAPE
void updateShape(); // Delega a ShapeManager::update()
void generateShape(); // Delega a ShapeManager::generateShape()
// Sistema de Figuras 3D
void toggleShapeModeInternal(bool force_gravity_on_exit = true);
void activateShapeInternal(ShapeType type);
void updateShape();
void generateShape();
// GPU helpers
bool loadGpuSpriteTexture(size_t index); // Upload one sprite texture to GPU
void recreateOffscreenTexture(); // Recreate when resolution changes
void renderUIToSurface(); // Render text/UI to ui_surface_
void uploadUISurface(SDL_GPUCommandBuffer* cmd_buf); // Upload ui_surface_ → ui_tex_
};

View File

@@ -0,0 +1,78 @@
#include "gpu_context.hpp"
#include <SDL3/SDL_log.h>
#include <iostream>
bool GpuContext::init(SDL_Window* window) {
window_ = window;
// Create GPU device — prefer Metal on macOS, Vulkan elsewhere
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_MSL
| SDL_GPU_SHADERFORMAT_METALLIB
| SDL_GPU_SHADERFORMAT_SPIRV;
device_ = SDL_CreateGPUDevice(preferred, false, nullptr);
if (!device_) {
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << std::endl;
return false;
}
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << std::endl;
// Claim the window so the GPU device owns its swapchain
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << std::endl;
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
}
// Query swapchain format (Metal: typically B8G8R8A8_UNORM or R8G8B8A8_UNORM)
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << std::endl;
// Default: VSync ON
SDL_SetGPUSwapchainParameters(device_, window_,
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
SDL_GPU_PRESENTMODE_VSYNC);
return true;
}
void GpuContext::destroy() {
if (device_) {
SDL_WaitForGPUIdle(device_);
SDL_ReleaseWindowFromGPUDevice(device_, window_);
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
}
window_ = nullptr;
}
SDL_GPUCommandBuffer* GpuContext::acquireCommandBuffer() {
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
if (!cmd) {
SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
}
return cmd;
}
SDL_GPUTexture* GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w, Uint32* out_h) {
SDL_GPUTexture* tex = nullptr;
if (!SDL_AcquireGPUSwapchainTexture(cmd_buf, window_, &tex, out_w, out_h)) {
SDL_Log("GpuContext: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
return nullptr;
}
// tex == nullptr when window is minimized — caller should skip rendering
return tex;
}
void GpuContext::submit(SDL_GPUCommandBuffer* cmd_buf) {
SDL_SubmitGPUCommandBuffer(cmd_buf);
}
bool GpuContext::setVSync(bool enabled) {
SDL_GPUPresentMode mode = enabled ? SDL_GPU_PRESENTMODE_VSYNC
: SDL_GPU_PRESENTMODE_IMMEDIATE;
return SDL_SetGPUSwapchainParameters(device_, window_,
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
mode);
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_video.h>
// ============================================================================
// GpuContext — SDL_GPU device + swapchain wrapper
// Replaces SDL_Renderer as the main rendering backend.
// ============================================================================
class GpuContext {
public:
bool init(SDL_Window* window);
void destroy();
SDL_GPUDevice* device() const { return device_; }
SDL_Window* window() const { return window_; }
SDL_GPUTextureFormat swapchainFormat() const { return swapchain_format_; }
// Per-frame helpers
SDL_GPUCommandBuffer* acquireCommandBuffer();
// Returns nullptr if window is minimized (swapchain not available).
SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w, Uint32* out_h);
void submit(SDL_GPUCommandBuffer* cmd_buf);
// VSync control (call after init)
bool setVSync(bool enabled);
private:
SDL_GPUDevice* device_ = nullptr;
SDL_Window* window_ = nullptr;
SDL_GPUTextureFormat swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
};

286
source/gpu/gpu_pipeline.cpp Normal file
View File

@@ -0,0 +1,286 @@
#include "gpu_pipeline.hpp"
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
#include <SDL3/SDL_log.h>
#include <cstddef> // offsetof
// ============================================================================
// MSL Shaders (Metal Shading Language, macOS)
// ============================================================================
// ---------------------------------------------------------------------------
// Sprite vertex shader
// Input: GpuVertex (pos=NDC float2, uv float2, col float4)
// Output: position, uv, col forwarded to fragment stage
// ---------------------------------------------------------------------------
static const char* kSpriteVertMSL = R"(
#include <metal_stdlib>
using namespace metal;
struct SpriteVIn {
float2 pos [[attribute(0)]];
float2 uv [[attribute(1)]];
float4 col [[attribute(2)]];
};
struct SpriteVOut {
float4 pos [[position]];
float2 uv;
float4 col;
};
vertex SpriteVOut sprite_vs(SpriteVIn in [[stage_in]]) {
SpriteVOut out;
out.pos = float4(in.pos, 0.0, 1.0);
out.uv = in.uv;
out.col = in.col;
return out;
}
)";
// ---------------------------------------------------------------------------
// Sprite fragment shader
// Samples a texture and multiplies by vertex color (for tinting + alpha).
// ---------------------------------------------------------------------------
static const char* kSpriteFragMSL = R"(
#include <metal_stdlib>
using namespace metal;
struct SpriteVOut {
float4 pos [[position]];
float2 uv;
float4 col;
};
fragment float4 sprite_fs(SpriteVOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]]) {
float4 t = tex.sample(samp, in.uv);
return float4(t.rgb * in.col.rgb, t.a * in.col.a);
}
)";
// ---------------------------------------------------------------------------
// PostFX vertex shader
// Generates a full-screen triangle from vertex_id (no vertex buffer needed).
// UV mapping: NDC(-1,-1)→UV(0,1) NDC(-1,3)→UV(0,-1) NDC(3,-1)→UV(2,1)
// ---------------------------------------------------------------------------
static const char* kPostFXVertMSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
// ---------------------------------------------------------------------------
// PostFX fragment shader
// Samples the offscreen scene texture and applies a subtle vignette.
// ---------------------------------------------------------------------------
static const char* kPostFXFragMSL = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]]) {
float4 color = scene.sample(samp, in.uv);
// Subtle vignette: darkens edges proportionally to distance from centre
float2 d = in.uv - float2(0.5, 0.5);
float vignette = 1.0 - dot(d, d) * 1.5;
color.rgb *= clamp(vignette, 0.0, 1.0);
return color;
}
)";
// ============================================================================
// GpuPipeline implementation
// ============================================================================
bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_GPUTextureFormat target_format,
SDL_GPUTextureFormat offscreen_format) {
SDL_GPUShaderFormat supported = SDL_GetGPUShaderFormats(device);
if (!(supported & SDL_GPU_SHADERFORMAT_MSL)) {
SDL_Log("GpuPipeline: device does not support MSL shaders (format mask=%u)", supported);
return false;
}
// ----------------------------------------------------------------
// Sprite pipeline
// ----------------------------------------------------------------
SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs",
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
if (!sprite_vert || !sprite_frag) {
SDL_Log("GpuPipeline: failed to create sprite shaders");
if (sprite_vert) SDL_ReleaseGPUShader(device, sprite_vert);
if (sprite_frag) SDL_ReleaseGPUShader(device, sprite_frag);
return false;
}
// Vertex input: GpuVertex layout
SDL_GPUVertexBufferDescription vb_desc = {};
vb_desc.slot = 0;
vb_desc.pitch = sizeof(GpuVertex);
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
vb_desc.instance_step_rate = 0;
SDL_GPUVertexAttribute attrs[3] = {};
attrs[0].location = 0;
attrs[0].buffer_slot = 0;
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attrs[0].offset = static_cast<Uint32>(offsetof(GpuVertex, x));
attrs[1].location = 1;
attrs[1].buffer_slot = 0;
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attrs[1].offset = static_cast<Uint32>(offsetof(GpuVertex, u));
attrs[2].location = 2;
attrs[2].buffer_slot = 0;
attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
attrs[2].offset = static_cast<Uint32>(offsetof(GpuVertex, r));
SDL_GPUVertexInputState vertex_input = {};
vertex_input.vertex_buffer_descriptions = &vb_desc;
vertex_input.num_vertex_buffers = 1;
vertex_input.vertex_attributes = attrs;
vertex_input.num_vertex_attributes = 3;
// Alpha blend state (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
SDL_GPUColorTargetBlendState blend = {};
blend.enable_blend = true;
blend.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
blend.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
blend.color_blend_op = SDL_GPU_BLENDOP_ADD;
blend.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
blend.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
blend.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
blend.enable_color_write_mask = false; // write all channels
SDL_GPUColorTargetDescription color_target_desc = {};
color_target_desc.format = offscreen_format;
color_target_desc.blend_state = blend;
SDL_GPUGraphicsPipelineCreateInfo sprite_pipe_info = {};
sprite_pipe_info.vertex_shader = sprite_vert;
sprite_pipe_info.fragment_shader = sprite_frag;
sprite_pipe_info.vertex_input_state = vertex_input;
sprite_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
sprite_pipe_info.target_info.num_color_targets = 1;
sprite_pipe_info.target_info.color_target_descriptions = &color_target_desc;
sprite_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &sprite_pipe_info);
SDL_ReleaseGPUShader(device, sprite_vert);
SDL_ReleaseGPUShader(device, sprite_frag);
if (!sprite_pipeline_) {
SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError());
return false;
}
// ----------------------------------------------------------------
// UI overlay pipeline (same as sprite but renders to swapchain format)
// Reuse sprite shaders with different target format.
// We create a second version of the sprite pipeline for swapchain.
// ----------------------------------------------------------------
// (postfx pipeline targets swapchain; UI overlay also targets swapchain
// but needs its own pipeline with swapchain format.)
// For simplicity, the sprite pipeline is used for the offscreen pass only.
// The UI overlay is composited via a separate postfx-like pass below.
// ----------------------------------------------------------------
// PostFX pipeline
// ----------------------------------------------------------------
SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs",
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
if (!postfx_vert || !postfx_frag) {
SDL_Log("GpuPipeline: failed to create postfx shaders");
if (postfx_vert) SDL_ReleaseGPUShader(device, postfx_vert);
if (postfx_frag) SDL_ReleaseGPUShader(device, postfx_frag);
return false;
}
// PostFX: no vertex input (uses vertex_id), no blend (replace output)
SDL_GPUColorTargetBlendState no_blend = {};
no_blend.enable_blend = false;
no_blend.enable_color_write_mask = false;
SDL_GPUColorTargetDescription postfx_target_desc = {};
postfx_target_desc.format = target_format;
postfx_target_desc.blend_state = no_blend;
SDL_GPUVertexInputState no_input = {};
SDL_GPUGraphicsPipelineCreateInfo postfx_pipe_info = {};
postfx_pipe_info.vertex_shader = postfx_vert;
postfx_pipe_info.fragment_shader = postfx_frag;
postfx_pipe_info.vertex_input_state = no_input;
postfx_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
postfx_pipe_info.target_info.num_color_targets = 1;
postfx_pipe_info.target_info.color_target_descriptions = &postfx_target_desc;
postfx_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &postfx_pipe_info);
SDL_ReleaseGPUShader(device, postfx_vert);
SDL_ReleaseGPUShader(device, postfx_frag);
if (!postfx_pipeline_) {
SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError());
return false;
}
SDL_Log("GpuPipeline: sprite and postfx pipelines created successfully");
return true;
}
void GpuPipeline::destroy(SDL_GPUDevice* device) {
if (sprite_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_); sprite_pipeline_ = nullptr; }
if (postfx_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_); postfx_pipeline_ = nullptr; }
}
SDL_GPUShader* GpuPipeline::createShader(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) {
SDL_GPUShaderCreateInfo info = {};
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = static_cast<size_t>(strlen(msl_source) + 1);
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_storage_textures = 0;
info.num_storage_buffers = 0;
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (!shader) {
SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <SDL3/SDL_gpu.h>
// ============================================================================
// GpuPipeline — Creates and owns the graphics pipelines used by the engine.
//
// sprite_pipeline_ : textured quads, alpha blending.
// Vertex layout: GpuVertex (pos float2, uv float2, col float4).
// postfx_pipeline_ : full-screen triangle, no vertex buffer, no blend.
// Reads offscreen texture, writes to swapchain.
// ============================================================================
class GpuPipeline {
public:
// target_format: pass SDL_GetGPUSwapchainTextureFormat() result.
// offscreen_format: format of the offscreen render target.
bool init(SDL_GPUDevice* device,
SDL_GPUTextureFormat target_format,
SDL_GPUTextureFormat offscreen_format);
void destroy(SDL_GPUDevice* device);
SDL_GPUGraphicsPipeline* spritePipeline() const { return sprite_pipeline_; }
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
private:
SDL_GPUShader* createShader(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers);
SDL_GPUGraphicsPipeline* sprite_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* postfx_pipeline_ = nullptr;
};

View File

@@ -0,0 +1,192 @@
#include "gpu_sprite_batch.hpp"
#include <SDL3/SDL_log.h>
#include <cstring> // memcpy
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
bool GpuSpriteBatch::init(SDL_GPUDevice* device) {
// Pre-allocate GPU buffers large enough for MAX_SPRITES quads.
Uint32 max_verts = static_cast<Uint32>(MAX_SPRITES) * 4;
Uint32 max_indices = static_cast<Uint32>(MAX_SPRITES) * 6;
Uint32 vb_size = max_verts * sizeof(GpuVertex);
Uint32 ib_size = max_indices * sizeof(uint32_t);
// Vertex buffer
SDL_GPUBufferCreateInfo vb_info = {};
vb_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
vb_info.size = vb_size;
vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info);
if (!vertex_buf_) {
SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError());
return false;
}
// Index buffer
SDL_GPUBufferCreateInfo ib_info = {};
ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
ib_info.size = ib_size;
index_buf_ = SDL_CreateGPUBuffer(device, &ib_info);
if (!index_buf_) {
SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError());
return false;
}
// Transfer buffers (reused every frame via cycle=true on upload)
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = vb_size;
vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!vertex_transfer_) {
SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError());
return false;
}
tb_info.size = ib_size;
index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!index_transfer_) {
SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError());
return false;
}
vertices_.reserve(MAX_SPRITES * 4);
indices_.reserve(MAX_SPRITES * 6);
return true;
}
void GpuSpriteBatch::destroy(SDL_GPUDevice* device) {
if (!device) return;
if (vertex_transfer_) { SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_); vertex_transfer_ = nullptr; }
if (index_transfer_) { SDL_ReleaseGPUTransferBuffer(device, index_transfer_); index_transfer_ = nullptr; }
if (vertex_buf_) { SDL_ReleaseGPUBuffer(device, vertex_buf_); vertex_buf_ = nullptr; }
if (index_buf_) { SDL_ReleaseGPUBuffer(device, index_buf_); index_buf_ = nullptr; }
}
void GpuSpriteBatch::beginFrame() {
vertices_.clear();
indices_.clear();
bg_index_count_ = 0;
sprite_index_offset_ = 0;
sprite_index_count_ = 0;
overlay_index_offset_ = 0;
overlay_index_count_ = 0;
}
void GpuSpriteBatch::addBackground(float screen_w, float screen_h,
float top_r, float top_g, float top_b,
float bot_r, float bot_g, float bot_b) {
// Background is the full screen quad, corners:
// TL(-1, 1) TR(1, 1) → top color
// BL(-1,-1) BR(1,-1) → bottom color
// We push it as 4 separate vertices (different colors per row).
uint32_t vi = static_cast<uint32_t>(vertices_.size());
// Top-left
vertices_.push_back({ -1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f });
// Top-right
vertices_.push_back({ 1.0f, 1.0f, 1.0f, 0.0f, top_r, top_g, top_b, 1.0f });
// Bottom-right
vertices_.push_back({ 1.0f, -1.0f, 1.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f });
// Bottom-left
vertices_.push_back({ -1.0f, -1.0f, 0.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f });
// Two triangles: TL-TR-BR, BR-BL-TL
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
bg_index_count_ = 6;
sprite_index_offset_ = 6;
(void)screen_w; (void)screen_h; // unused — bg always covers full NDC
}
void GpuSpriteBatch::addSprite(float x, float y, float w, float h,
float r, float g, float b, float a,
float scale,
float screen_w, float screen_h) {
// Apply scale around the sprite centre
float scaled_w = w * scale;
float scaled_h = h * scale;
float offset_x = (w - scaled_w) * 0.5f;
float offset_y = (h - scaled_h) * 0.5f;
float px0 = x + offset_x;
float py0 = y + offset_y;
float px1 = px0 + scaled_w;
float py1 = py0 + scaled_h;
float ndx0, ndy0, ndx1, ndy1;
toNDC(px0, py0, screen_w, screen_h, ndx0, ndy0);
toNDC(px1, py1, screen_w, screen_h, ndx1, ndy1);
pushQuad(ndx0, ndy0, ndx1, ndy1, 0.0f, 0.0f, 1.0f, 1.0f, r, g, b, a);
sprite_index_count_ += 6;
}
void GpuSpriteBatch::addFullscreenOverlay() {
overlay_index_offset_ = static_cast<int>(indices_.size());
pushQuad(-1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
overlay_index_count_ = 6;
}
bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) {
if (vertices_.empty()) return false;
Uint32 vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
Uint32 ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
// Map → write → unmap transfer buffers
void* vp = SDL_MapGPUTransferBuffer(device, vertex_transfer_, true /* cycle */);
if (!vp) { SDL_Log("GpuSpriteBatch: vertex map failed"); return false; }
memcpy(vp, vertices_.data(), vb_size);
SDL_UnmapGPUTransferBuffer(device, vertex_transfer_);
void* ip = SDL_MapGPUTransferBuffer(device, index_transfer_, true /* cycle */);
if (!ip) { SDL_Log("GpuSpriteBatch: index map failed"); return false; }
memcpy(ip, indices_.data(), ib_size);
SDL_UnmapGPUTransferBuffer(device, index_transfer_);
// Upload via copy pass
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);
SDL_GPUTransferBufferLocation v_src = { vertex_transfer_, 0 };
SDL_GPUBufferRegion v_dst = { vertex_buf_, 0, vb_size };
SDL_UploadToGPUBuffer(copy, &v_src, &v_dst, true /* cycle */);
SDL_GPUTransferBufferLocation i_src = { index_transfer_, 0 };
SDL_GPUBufferRegion i_dst = { index_buf_, 0, ib_size };
SDL_UploadToGPUBuffer(copy, &i_src, &i_dst, true /* cycle */);
SDL_EndGPUCopyPass(copy);
return true;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
void GpuSpriteBatch::toNDC(float px, float py,
float screen_w, float screen_h,
float& ndx, float& ndy) const {
ndx = (px / screen_w) * 2.0f - 1.0f;
ndy = 1.0f - (py / screen_h) * 2.0f;
}
void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
float u0, float v0, float u1, float v1,
float r, float g, float b, float a) {
uint32_t vi = static_cast<uint32_t>(vertices_.size());
// TL, TR, BR, BL
vertices_.push_back({ ndx0, ndy0, u0, v0, r, g, b, a });
vertices_.push_back({ ndx1, ndy0, u1, v0, r, g, b, a });
vertices_.push_back({ ndx1, ndy1, u1, v1, r, g, b, a });
vertices_.push_back({ ndx0, ndy1, u0, v1, r, g, b, a });
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include <SDL3/SDL_gpu.h>
#include <vector>
#include <cstdint>
// ---------------------------------------------------------------------------
// GpuVertex — 8-float vertex layout sent to the GPU.
// Position is in NDC (pre-transformed on CPU), UV in [0,1], color in [0,1].
// ---------------------------------------------------------------------------
struct GpuVertex {
float x, y; // NDC position (1..1)
float u, v; // Texture coords (0..1)
float r, g, b, a; // RGBA color (0..1)
};
// ============================================================================
// GpuSpriteBatch — Accumulates sprite quads, uploads them in one copy pass.
//
// Usage per frame:
// batch.beginFrame();
// batch.addBackground(...); // Must be first (bg indices = [0..5])
// batch.addSprite(...) × N;
// batch.uploadBatch(device, cmd); // Copy pass
// // Then in render pass: bind buffers, draw bg with white tex, draw sprites.
// ============================================================================
class GpuSpriteBatch {
public:
// Maximum sprites (background + UI overlay each count as one sprite)
static constexpr int MAX_SPRITES = 65536;
bool init(SDL_GPUDevice* device);
void destroy(SDL_GPUDevice* device);
void beginFrame();
// Add the full-screen background gradient quad.
// top_* and bot_* are RGB in [0,1].
void addBackground(float screen_w, float screen_h,
float top_r, float top_g, float top_b,
float bot_r, float bot_g, float bot_b);
// Add a sprite quad (pixel coordinates).
// scale: uniform scale around the quad centre.
void addSprite(float x, float y, float w, float h,
float r, float g, float b, float a,
float scale,
float screen_w, float screen_h);
// Add a full-screen overlay quad (e.g. UI surface, NDC 1..1).
void addFullscreenOverlay();
// Upload CPU vectors to GPU buffers via a copy pass.
// Returns false if the batch is empty.
bool uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf);
SDL_GPUBuffer* vertexBuffer() const { return vertex_buf_; }
SDL_GPUBuffer* indexBuffer() const { return index_buf_; }
int bgIndexCount() const { return bg_index_count_; }
int overlayIndexOffset() const { return overlay_index_offset_; }
int overlayIndexCount() const { return overlay_index_count_; }
int spriteIndexOffset() const { return sprite_index_offset_; }
int spriteIndexCount() const { return sprite_index_count_; }
bool isEmpty() const { return vertices_.empty(); }
private:
void toNDC(float px, float py, float screen_w, float screen_h,
float& ndx, float& ndy) const;
void pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
float u0, float v0, float u1, float v1,
float r, float g, float b, float a);
std::vector<GpuVertex> vertices_;
std::vector<uint32_t> indices_;
SDL_GPUBuffer* vertex_buf_ = nullptr;
SDL_GPUBuffer* index_buf_ = nullptr;
SDL_GPUTransferBuffer* vertex_transfer_ = nullptr;
SDL_GPUTransferBuffer* index_transfer_ = nullptr;
int bg_index_count_ = 0;
int sprite_index_offset_ = 0;
int sprite_index_count_ = 0;
int overlay_index_offset_ = 0;
int overlay_index_count_ = 0;
};

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;
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_surface.h>
#include <string>
// ============================================================================
// GpuTexture — SDL_GPU texture + sampler wrapper
// Handles sprite textures, render targets, and the 1×1 white utility texture.
// ============================================================================
class GpuTexture {
public:
GpuTexture() = default;
~GpuTexture() = default;
// Load from resource path (pack or disk) using stb_image.
bool fromFile(SDL_GPUDevice* device, const std::string& file_path);
// Upload pixel data from an SDL_Surface to a new GPU texture + sampler.
// Uses nearest-neighbor filter for sprite pixel-perfect look.
bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true);
// Create an offscreen render target (COLOR_TARGET | SAMPLER usage).
bool createRenderTarget(SDL_GPUDevice* device, int w, int h,
SDL_GPUTextureFormat format);
// Create a 1×1 opaque white texture (used for untextured geometry).
bool createWhite(SDL_GPUDevice* device);
// Release GPU resources.
void destroy(SDL_GPUDevice* device);
SDL_GPUTexture* texture() const { return texture_; }
SDL_GPUSampler* sampler() const { return sampler_; }
int width() const { return width_; }
int height() const { return height_; }
bool isValid() const { return texture_ != nullptr; }
private:
bool uploadPixels(SDL_GPUDevice* device, const void* pixels,
int w, int h, SDL_GPUTextureFormat format);
bool createSampler(SDL_GPUDevice* device, bool nearest);
SDL_GPUTexture* texture_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr;
int width_ = 0;
int height_ = 0;
};