diff --git a/CMakeLists.txt b/CMakeLists.txt index 9579360..fa27fb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ if (NOT SDL3_ttf_FOUND) endif() # Archivos fuente (excluir main_old.cpp) -file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/boids_mgr/*.cpp source/input/*.cpp source/scene/*.cpp source/shapes/*.cpp source/shapes_mgr/*.cpp source/state/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp) +file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/boids_mgr/*.cpp source/gpu/*.cpp source/input/*.cpp source/scene/*.cpp source/shapes/*.cpp source/shapes_mgr/*.cpp source/state/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp) list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp") # Comprobar si se encontraron archivos fuente diff --git a/source/engine.cpp b/source/engine.cpp index 23da723..55dc2a7 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -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(); + 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; + std::shared_ptr texture; // legacy (para physics sizing) + std::string path; // resource path para GPU upload int width; }; std::vector 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(renderer_, fullpath); + // Cargar textura legacy (usa ui_renderer_ en lugar del renderer_ eliminado) + auto texture = std::make_shared(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(renderer_, resource); + auto texture = std::make_shared(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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(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(); - 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(); - 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(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(current_screen_width_), static_cast(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(current_screen_width_), static_cast(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(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(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(color.r * brightness_factor); - int g_mod = static_cast(color.g * brightness_factor); - int b_mod = static_cast(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(current_screen_width_), + static_cast(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(current_screen_width_), + static_cast(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(batch_vertices_.size()), batch_indices_.data(), static_cast(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(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(current_screen_width_), + static_cast(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(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(w); + src.rows_per_layer = static_cast(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(w); + dst.h = static_cast(h); + dst.d = 1; + + SDL_UploadToGPUTexture(copy, &src, &dst, false); + SDL_EndGPUCopyPass(copy); + + SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer); } \ No newline at end of file diff --git a/source/engine.hpp b/source/engine.hpp index cae8ba6..c4235e1 100644 --- a/source/engine.hpp +++ b/source/engine.hpp @@ -1,8 +1,9 @@ #pragma once #include // for SDL_Event -#include // for SDL_Renderer +#include // for SDL_Renderer (ui_renderer_ software renderer) #include // for Uint64 +#include // for SDL_Surface (ui_surface_) #include // for SDL_Window #include // 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 ui_manager_; // Gestión de UI (HUD, FPS, notificaciones) std::unique_ptr 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 gpu_ctx_; // Device + swapchain + std::unique_ptr gpu_pipeline_; // Sprite + postfx pipelines + std::unique_ptr sprite_batch_; // Per-frame vertex/index batch + std::unique_ptr offscreen_tex_; // Offscreen render target (Pass 1) + std::unique_ptr white_tex_; // 1×1 white (background gradient) + std::unique_ptr ui_tex_; // UI text overlay texture + + // GPU sprite textures (one per ball skin, parallel to textures_/texture_names_) + std::unique_ptr gpu_texture_; // Active GPU sprite texture + std::vector> 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_ = nullptr; // Textura activa actual std::vector> textures_; // Todas las texturas disponibles std::vector 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 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 batch_vertices_; - std::vector batch_indices_; - // Bucket sort per z-ordering (SHAPE mode) static constexpr int DEPTH_SORT_BUCKETS = 256; std::array, 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_ }; diff --git a/source/gpu/gpu_context.cpp b/source/gpu/gpu_context.cpp new file mode 100644 index 0000000..7b676c0 --- /dev/null +++ b/source/gpu/gpu_context.cpp @@ -0,0 +1,78 @@ +#include "gpu_context.hpp" + +#include +#include + +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(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); +} diff --git a/source/gpu/gpu_context.hpp b/source/gpu/gpu_context.hpp new file mode 100644 index 0000000..d26007e --- /dev/null +++ b/source/gpu/gpu_context.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +// ============================================================================ +// 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; +}; diff --git a/source/gpu/gpu_pipeline.cpp b/source/gpu/gpu_pipeline.cpp new file mode 100644 index 0000000..6d76008 --- /dev/null +++ b/source/gpu/gpu_pipeline.cpp @@ -0,0 +1,286 @@ +#include "gpu_pipeline.hpp" +#include "gpu_sprite_batch.hpp" // for GpuVertex layout + +#include +#include // 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 +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 +using namespace metal; + +struct SpriteVOut { + float4 pos [[position]]; + float2 uv; + float4 col; +}; + +fragment float4 sprite_fs(SpriteVOut in [[stage_in]], + texture2d 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 +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 +using namespace metal; + +struct PostVOut { + float4 pos [[position]]; + float2 uv; +}; + +fragment float4 postfx_fs(PostVOut in [[stage_in]], + texture2d 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(offsetof(GpuVertex, x)); + + attrs[1].location = 1; + attrs[1].buffer_slot = 0; + attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; + attrs[1].offset = static_cast(offsetof(GpuVertex, u)); + + attrs[2].location = 2; + attrs[2].buffer_slot = 0; + attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; + attrs[2].offset = static_cast(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(msl_source); + info.code_size = static_cast(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; +} diff --git a/source/gpu/gpu_pipeline.hpp b/source/gpu/gpu_pipeline.hpp new file mode 100644 index 0000000..f48bfd7 --- /dev/null +++ b/source/gpu/gpu_pipeline.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +// ============================================================================ +// 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; +}; diff --git a/source/gpu/gpu_sprite_batch.cpp b/source/gpu/gpu_sprite_batch.cpp new file mode 100644 index 0000000..fd7146a --- /dev/null +++ b/source/gpu/gpu_sprite_batch.cpp @@ -0,0 +1,192 @@ +#include "gpu_sprite_batch.hpp" + +#include +#include // memcpy + +// --------------------------------------------------------------------------- +// Public interface +// --------------------------------------------------------------------------- + +bool GpuSpriteBatch::init(SDL_GPUDevice* device) { + // Pre-allocate GPU buffers large enough for MAX_SPRITES quads. + Uint32 max_verts = static_cast(MAX_SPRITES) * 4; + Uint32 max_indices = static_cast(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(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(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(vertices_.size() * sizeof(GpuVertex)); + Uint32 ib_size = static_cast(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(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); +} diff --git a/source/gpu/gpu_sprite_batch.hpp b/source/gpu/gpu_sprite_batch.hpp new file mode 100644 index 0000000..0cd1a05 --- /dev/null +++ b/source/gpu/gpu_sprite_batch.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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 vertices_; + std::vector 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; +}; diff --git a/source/gpu/gpu_texture.cpp b/source/gpu/gpu_texture.cpp new file mode 100644 index 0000000..d84be95 --- /dev/null +++ b/source/gpu/gpu_texture.cpp @@ -0,0 +1,208 @@ +#include "gpu_texture.hpp" + +#include +#include +#include // memcpy +#include + +// 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(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(w); + info.height = static_cast(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(w); + tex_info.height = static_cast(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(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(w); + src.rows_per_layer = static_cast(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(w); + dst.h = static_cast(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; +} diff --git a/source/gpu/gpu_texture.hpp b/source/gpu/gpu_texture.hpp new file mode 100644 index 0000000..64145ab --- /dev/null +++ b/source/gpu/gpu_texture.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +// ============================================================================ +// 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; +};