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:
@@ -25,7 +25,7 @@ if (NOT SDL3_ttf_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Archivos fuente (excluir main_old.cpp)
|
# 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")
|
list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp")
|
||||||
|
|
||||||
# Comprobar si se encontraron archivos fuente
|
# Comprobar si se encontraron archivos fuente
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
|
|||||||
// SDL ya inicializado arriba para validación
|
// SDL ya inicializado arriba para validación
|
||||||
{
|
{
|
||||||
// Crear ventana principal (fullscreen si se especifica)
|
// Crear ventana principal (fullscreen si se especifica)
|
||||||
// NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4)
|
// SDL_WINDOW_HIGH_PIXEL_DENSITY removido — DPI detectado con SDL_GetWindowSizeInPixels()
|
||||||
// El DPI se detectará manualmente con SDL_GetWindowSizeInPixels()
|
// SDL_WINDOW_OPENGL eliminado — SDL_GPU usa Metal/Vulkan/D3D12 directamente
|
||||||
Uint32 window_flags = SDL_WINDOW_OPENGL;
|
Uint32 window_flags = 0;
|
||||||
if (fullscreen) {
|
if (fullscreen) {
|
||||||
window_flags |= SDL_WINDOW_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);
|
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear renderizador
|
// Inicializar SDL_GPU (sustituye SDL_Renderer como backend principal)
|
||||||
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
gpu_ctx_ = std::make_unique<GpuContext>();
|
||||||
if (renderer_ == nullptr) {
|
if (!gpu_ctx_->init(window_)) {
|
||||||
std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl;
|
std::cout << "¡No se pudo inicializar SDL_GPU!" << std::endl;
|
||||||
success = false;
|
success = false;
|
||||||
} else {
|
} else {
|
||||||
// Establecer color inicial del renderizador
|
gpu_ctx_->setVSync(vsync_enabled_);
|
||||||
SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);
|
|
||||||
|
|
||||||
// Establecer tamaño lógico para el renderizado (resolución interna)
|
// Crear renderer de software para UI/texto (SDL3_ttf no es compatible con SDL_GPU)
|
||||||
SDL_SetRenderLogicalPresentation(renderer_, logical_width, logical_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
|
// Renderiza a ui_surface_, que luego se sube como textura GPU overlay
|
||||||
|
ui_surface_ = SDL_CreateSurface(logical_width, logical_height, SDL_PIXELFORMAT_RGBA32);
|
||||||
// Configurar V-Sync inicial
|
if (ui_surface_) {
|
||||||
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
|
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 {
|
struct TextureInfo {
|
||||||
std::string name;
|
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;
|
int width;
|
||||||
};
|
};
|
||||||
std::vector<TextureInfo> texture_files;
|
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/
|
// Buscar todas las texturas PNG en data/balls/
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
|
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)) {
|
for (const auto& entry : fs::directory_iterator(balls_dir)) {
|
||||||
if (entry.is_regular_file() && entry.path().extension() == ".png") {
|
if (entry.is_regular_file() && entry.path().extension() == ".png") {
|
||||||
std::string filename = entry.path().stem().string();
|
std::string filename = entry.path().stem().string();
|
||||||
std::string fullpath = entry.path().string();
|
std::string fullpath = entry.path().string();
|
||||||
|
|
||||||
// Cargar textura y obtener dimensiones
|
// Cargar textura legacy (usa ui_renderer_ en lugar del renderer_ eliminado)
|
||||||
auto texture = std::make_shared<Texture>(renderer_, fullpath);
|
auto texture = std::make_shared<Texture>(ui_renderer_, fullpath);
|
||||||
int width = texture->getWidth();
|
int width = texture->getWidth();
|
||||||
|
|
||||||
texture_files.push_back({filename, texture, width});
|
texture_files.push_back({filename, texture, fullpath, width});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: cargar texturas desde pack usando la lista del ResourceManager
|
// Fallback: cargar texturas desde pack
|
||||||
if (ResourceManager::isPackLoaded()) {
|
if (ResourceManager::isPackLoaded()) {
|
||||||
auto pack_resources = ResourceManager::getResourceList();
|
auto pack_resources = ResourceManager::getResourceList();
|
||||||
|
|
||||||
// Filtrar solo los recursos en balls/ con extensión .png
|
|
||||||
for (const auto& resource : pack_resources) {
|
for (const auto& resource : pack_resources) {
|
||||||
if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
|
if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
|
||||||
std::string tex_name = resource.substr(6); // Quitar "balls/"
|
std::string tex_name = resource.substr(6);
|
||||||
std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión
|
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();
|
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)
|
// 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) {
|
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) {
|
for (const auto& info : texture_files) {
|
||||||
textures_.push_back(info.texture);
|
textures_.push_back(info.texture);
|
||||||
texture_names_.push_back(info.name);
|
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
|
// Verificar que se cargaron texturas
|
||||||
@@ -201,16 +211,54 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
|
|||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1)
|
// Buscar índice de "normal" para usarlo como textura inicial
|
||||||
current_texture_index_ = 0; // Fallback
|
current_texture_index_ = 0;
|
||||||
for (size_t i = 0; i < texture_names_.size(); i++) {
|
for (size_t i = 0; i < texture_names_.size(); i++) {
|
||||||
if (texture_names_[i] == "normal") {
|
if (texture_names_[i] == "normal") {
|
||||||
current_texture_index_ = i; // Iniciar en "normal" (índice 1)
|
current_texture_index_ = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
texture_ = textures_[current_texture_index_];
|
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)));
|
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)
|
// Inicializar UIManager (HUD, FPS, notificaciones)
|
||||||
// NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager
|
// NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager
|
||||||
ui_manager_ = std::make_unique<UIManager>();
|
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_,
|
physical_window_width_, physical_window_height_,
|
||||||
current_screen_width_, current_screen_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)
|
// Inicializar AppLogo (logo periódico en pantalla)
|
||||||
app_logo_ = std::make_unique<AppLogo>();
|
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;
|
std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << std::endl;
|
||||||
// No es crítico, continuar sin logo
|
// No es crítico, continuar sin logo
|
||||||
app_logo_.reset();
|
app_logo_.reset();
|
||||||
@@ -318,11 +366,28 @@ void Engine::run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Engine::shutdown() {
|
void Engine::shutdown() {
|
||||||
// Limpiar recursos SDL
|
// Wait for GPU idle before releasing GPU resources
|
||||||
if (renderer_) {
|
if (gpu_ctx_) SDL_WaitForGPUIdle(gpu_ctx_->device());
|
||||||
SDL_DestroyRenderer(renderer_);
|
|
||||||
renderer_ = nullptr;
|
// 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_) {
|
if (window_) {
|
||||||
SDL_DestroyWindow(window_);
|
SDL_DestroyWindow(window_);
|
||||||
window_ = nullptr;
|
window_ = nullptr;
|
||||||
@@ -636,140 +701,148 @@ void Engine::toggleLogoMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Engine::render() {
|
void Engine::render() {
|
||||||
// Limpiar framebuffer completamente (evita artefactos en barras negras al cambiar modos)
|
if (!gpu_ctx_ || !sprite_batch_ || !gpu_pipeline_) return;
|
||||||
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // Negro para barras de letterbox/integer
|
|
||||||
SDL_RenderClear(renderer_);
|
|
||||||
|
|
||||||
// Renderizar fondo degradado (delegado a ThemeManager)
|
// === Render UI text to software surface ===
|
||||||
{
|
renderUIToSurface();
|
||||||
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);
|
|
||||||
|
|
||||||
// Crear quad de pantalla completa con degradado
|
// === Acquire command buffer ===
|
||||||
SDL_Vertex bg_vertices[4];
|
SDL_GPUCommandBuffer* cmd = gpu_ctx_->acquireCommandBuffer();
|
||||||
|
if (!cmd) return;
|
||||||
|
|
||||||
// Vértice superior izquierdo
|
// === Upload UI surface to GPU texture (inline copy pass) ===
|
||||||
bg_vertices[0].position = {0, 0};
|
uploadUISurface(cmd);
|
||||||
bg_vertices[0].tex_coord = {0.0f, 0.0f};
|
|
||||||
bg_vertices[0].color = {top_r, top_g, top_b, 1.0f};
|
|
||||||
|
|
||||||
// Vértice superior derecho
|
// === Build sprite batch ===
|
||||||
bg_vertices[1].position = {static_cast<float>(current_screen_width_), 0};
|
sprite_batch_->beginFrame();
|
||||||
bg_vertices[1].tex_coord = {1.0f, 0.0f};
|
|
||||||
bg_vertices[1].color = {top_r, top_g, top_b, 1.0f};
|
|
||||||
|
|
||||||
// Vértice inferior derecho
|
// Background gradient
|
||||||
bg_vertices[2].position = {static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_)};
|
float top_r = 0, top_g = 0, top_b = 0, bot_r = 0, bot_g = 0, bot_b = 0;
|
||||||
bg_vertices[2].tex_coord = {1.0f, 1.0f};
|
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bot_r, bot_g, bot_b);
|
||||||
bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f};
|
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
|
// Sprites (balls)
|
||||||
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
|
|
||||||
const auto& balls = scene_manager_->getBalls();
|
const auto& balls = scene_manager_->getBalls();
|
||||||
|
|
||||||
if (current_mode_ == SimulationMode::SHAPE) {
|
if (current_mode_ == SimulationMode::SHAPE) {
|
||||||
// MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm)
|
// Bucket sort by depth 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))
|
|
||||||
for (size_t i = 0; i < balls.size(); i++) {
|
for (size_t i = 0; i < balls.size(); i++) {
|
||||||
int b = static_cast<int>(balls[i]->getDepthBrightness() * (DEPTH_SORT_BUCKETS - 1));
|
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);
|
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 (int b = 0; b < DEPTH_SORT_BUCKETS; b++) {
|
||||||
for (size_t idx : depth_buckets_[b]) {
|
for (size_t idx : depth_buckets_[b]) {
|
||||||
SDL_FRect pos = balls[idx]->getPosition();
|
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 brightness = balls[idx]->getDepthBrightness();
|
||||||
float depth_scale = balls[idx]->getDepthScale();
|
float depth_scale = balls[idx]->getDepthScale();
|
||||||
|
float bf = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
|
||||||
// Mapear brightness de 0-1 a rango MIN-MAX
|
sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h,
|
||||||
float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
|
color.r / 255.0f * bf,
|
||||||
|
color.g / 255.0f * bf,
|
||||||
// Aplicar factor de brillo al color
|
color.b / 255.0f * bf,
|
||||||
int r_mod = static_cast<int>(color.r * brightness_factor);
|
1.0f, depth_scale,
|
||||||
int g_mod = static_cast<int>(color.g * brightness_factor);
|
static_cast<float>(current_screen_width_),
|
||||||
int b_mod = static_cast<int>(color.b * brightness_factor);
|
static_cast<float>(current_screen_height_));
|
||||||
|
|
||||||
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, r_mod, g_mod, b_mod, depth_scale);
|
|
||||||
}
|
}
|
||||||
depth_buckets_[b].clear(); // netejar per al proper frame
|
depth_buckets_[b].clear();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
|
|
||||||
const auto& balls = scene_manager_->getBalls();
|
|
||||||
size_t idx = 0;
|
size_t idx = 0;
|
||||||
for (auto& ball : balls) {
|
for (const auto& ball : balls) {
|
||||||
SDL_FRect pos = ball->getPosition();
|
SDL_FRect pos = ball->getPosition();
|
||||||
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
|
Color color = theme_manager_->getInterpolatedColor(idx);
|
||||||
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
|
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++;
|
idx++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderizar todas las bolas en una sola llamada
|
// UI overlay quad (drawn in Pass 2 over the postfx output)
|
||||||
if (!batch_vertices_.empty()) {
|
sprite_batch_->addFullscreenOverlay();
|
||||||
SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast<int>(batch_vertices_.size()), batch_indices_.data(), static_cast<int>(batch_indices_.size()));
|
|
||||||
|
// Upload batch to GPU buffers
|
||||||
|
if (!sprite_batch_->uploadBatch(gpu_ctx_->device(), cmd)) {
|
||||||
|
gpu_ctx_->submit(cmd);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SISTEMA DE TEXTO ANTIGUO DESHABILITADO
|
GpuTexture* sprite_tex = (!gpu_textures_.empty())
|
||||||
// Reemplazado completamente por el sistema de notificaciones (Notifier)
|
? gpu_textures_[current_texture_index_].get() : nullptr;
|
||||||
// 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();
|
|
||||||
|
|
||||||
// Calcular espaciado dinámico
|
// === Pass 1: Render background + sprites to offscreen texture ===
|
||||||
int line_height = text_renderer_.getTextHeight();
|
if (offscreen_tex_ && offscreen_tex_->isValid() && sprite_tex && sprite_tex->isValid()) {
|
||||||
int margin = 8;
|
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
|
SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
|
||||||
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_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->spritePipeline());
|
||||||
|
|
||||||
// Mostrar nombre del tema en castellano debajo del número de pelotas
|
SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
|
||||||
// (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
|
SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
|
||||||
if (theme_name_es != nullptr && text_ != theme_name_es) {
|
SDL_BindGPUVertexBuffers(pass1, 0, &vb, 1);
|
||||||
int theme_text_width = text_renderer_.getTextWidth(theme_name_es);
|
SDL_BindGPUIndexBuffer(pass1, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
|
||||||
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
|
|
||||||
int theme_y = margin + line_height; // Espaciado dinámico
|
|
||||||
|
|
||||||
// Texto del nombre del tema con el mismo color
|
// Background (white texture tinted by vertex 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);
|
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
|
// Sprites
|
||||||
ui_manager_->render(renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(),
|
if (sprite_batch_->spriteIndexCount() > 0) {
|
||||||
shape_manager_->getActiveShape(), shape_manager_->getConvergence(),
|
SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()};
|
||||||
physical_window_width_, physical_window_height_, current_screen_width_);
|
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
|
SDL_EndGPURenderPass(pass1);
|
||||||
if (app_logo_) {
|
|
||||||
app_logo_->render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
void Engine::showNotificationForAction(const std::string& text) {
|
||||||
@@ -790,8 +863,8 @@ void Engine::toggleVSync() {
|
|||||||
// Actualizar texto en UIManager
|
// Actualizar texto en UIManager
|
||||||
ui_manager_->updateVSyncText(vsync_enabled_);
|
ui_manager_->updateVSyncText(vsync_enabled_);
|
||||||
|
|
||||||
// Aplicar el cambio de V-Sync al renderizador
|
// Aplicar el cambio de V-Sync al contexto GPU
|
||||||
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
|
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::toggleFullscreen() {
|
void Engine::toggleFullscreen() {
|
||||||
@@ -837,8 +910,8 @@ void Engine::toggleRealFullscreen() {
|
|||||||
SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
|
SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
|
||||||
SDL_SetWindowFullscreen(window_, true);
|
SDL_SetWindowFullscreen(window_, true);
|
||||||
|
|
||||||
// Actualizar presentación lógica del renderizador
|
// Recrear render target offscreen con nueva resolución
|
||||||
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
|
recreateOffscreenTexture();
|
||||||
|
|
||||||
// Actualizar tamaño físico de ventana y fuentes
|
// Actualizar tamaño físico de ventana y fuentes
|
||||||
updatePhysicalWindowSize();
|
updatePhysicalWindowSize();
|
||||||
@@ -873,8 +946,8 @@ void Engine::toggleRealFullscreen() {
|
|||||||
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
|
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
|
||||||
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||||
|
|
||||||
// Restaurar presentación lógica base
|
// Recrear render target offscreen con resolución base
|
||||||
SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
|
recreateOffscreenTexture();
|
||||||
|
|
||||||
// Actualizar tamaño físico de ventana y fuentes
|
// Actualizar tamaño físico de ventana y fuentes
|
||||||
updatePhysicalWindowSize();
|
updatePhysicalWindowSize();
|
||||||
@@ -915,81 +988,25 @@ void Engine::toggleIntegerScaling() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplicar el nuevo modo de escalado
|
// SDL_GPU stretches to fill swapchain by default; just show notification
|
||||||
SDL_RendererLogicalPresentation presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
|
|
||||||
const char* mode_name = "INTEGER";
|
const char* mode_name = "INTEGER";
|
||||||
|
|
||||||
switch (current_scaling_mode_) {
|
switch (current_scaling_mode_) {
|
||||||
case ScalingMode::INTEGER:
|
case ScalingMode::INTEGER: mode_name = "INTEGER"; break;
|
||||||
presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
|
case ScalingMode::LETTERBOX: mode_name = "LETTERBOX"; break;
|
||||||
mode_name = "INTEGER";
|
case ScalingMode::STRETCH: mode_name = "STRETCH"; break;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation);
|
|
||||||
|
|
||||||
// Mostrar notificación del cambio
|
|
||||||
std::string notification = std::string("Escalado: ") + mode_name;
|
std::string notification = std::string("Escalado: ") + mode_name;
|
||||||
ui_manager_->showNotification(notification);
|
ui_manager_->showNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
|
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());
|
if (!sprite_batch_) return;
|
||||||
|
sprite_batch_->addSprite(x, y, w, h,
|
||||||
// Crear 4 vértices para el quad (2 triángulos)
|
r / 255.0f, g / 255.0f, b / 255.0f, 1.0f,
|
||||||
SDL_Vertex vertices[4];
|
scale,
|
||||||
|
static_cast<float>(current_screen_width_),
|
||||||
// Convertir colores de int (0-255) a float (0.0-1.0)
|
static_cast<float>(current_screen_height_));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sistema de zoom dinámico
|
// Sistema de zoom dinámico
|
||||||
@@ -1186,7 +1203,7 @@ void Engine::runPerformanceBenchmark() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SDL_HideWindow(window_);
|
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 BENCH_DURATION_MS = 600;
|
||||||
const int WARMUP_FRAMES = 5;
|
const int WARMUP_FRAMES = 5;
|
||||||
@@ -1194,7 +1211,7 @@ void Engine::runPerformanceBenchmark() {
|
|||||||
SimulationMode original_mode = current_mode_;
|
SimulationMode original_mode = current_mode_;
|
||||||
|
|
||||||
auto restore = [&]() {
|
auto restore = [&]() {
|
||||||
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
|
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
|
||||||
SDL_ShowWindow(window_);
|
SDL_ShowWindow(window_);
|
||||||
current_mode_ = original_mode;
|
current_mode_ = original_mode;
|
||||||
if (shape_manager_->isShapeModeActive()) {
|
if (shape_manager_->isShapeModeActive()) {
|
||||||
@@ -1262,3 +1279,103 @@ void Engine::runPerformanceBenchmark() {
|
|||||||
max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO;
|
max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO;
|
||||||
restore();
|
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);
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <SDL3/SDL_events.h> // for SDL_Event
|
#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_stdinc.h> // for Uint64
|
||||||
|
#include <SDL3/SDL_surface.h> // for SDL_Surface (ui_surface_)
|
||||||
#include <SDL3/SDL_video.h> // for SDL_Window
|
#include <SDL3/SDL_video.h> // for SDL_Window
|
||||||
|
|
||||||
#include <array> // for array
|
#include <array> // for array
|
||||||
@@ -15,6 +16,10 @@
|
|||||||
#include "boids_mgr/boid_manager.hpp" // for BoidManager
|
#include "boids_mgr/boid_manager.hpp" // for BoidManager
|
||||||
#include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType
|
#include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType
|
||||||
#include "external/texture.hpp" // for Texture
|
#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 "input/input_handler.hpp" // for InputHandler
|
||||||
#include "scene/scene_manager.hpp" // for SceneManager
|
#include "scene/scene_manager.hpp" // for SceneManager
|
||||||
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
|
#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<UIManager> ui_manager_; // Gestión de UI (HUD, FPS, notificaciones)
|
||||||
std::unique_ptr<AppLogo> app_logo_; // Gestión de logo periódico en pantalla
|
std::unique_ptr<AppLogo> app_logo_; // Gestión de logo periódico en pantalla
|
||||||
|
|
||||||
// Recursos SDL
|
// === SDL window ===
|
||||||
SDL_Window* window_ = nullptr;
|
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::shared_ptr<Texture> texture_ = nullptr; // Textura activa actual
|
||||||
std::vector<std::shared_ptr<Texture>> textures_; // Todas las texturas disponibles
|
std::vector<std::shared_ptr<Texture>> textures_; // Todas las texturas disponibles
|
||||||
std::vector<std::string> texture_names_; // Nombres de texturas (sin extensión)
|
std::vector<std::string> texture_names_; // Nombres de texturas (sin extensión)
|
||||||
size_t current_texture_index_ = 0; // Índice de textura activa
|
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
|
// Estado del simulador
|
||||||
bool should_exit_ = false;
|
bool should_exit_ = false;
|
||||||
@@ -143,12 +166,12 @@ class Engine {
|
|||||||
// Sistema de zoom dinámico
|
// Sistema de zoom dinámico
|
||||||
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
|
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
|
||||||
|
|
||||||
// V-Sync
|
// V-Sync y fullscreen
|
||||||
bool vsync_enabled_ = true;
|
bool vsync_enabled_ = true;
|
||||||
bool fullscreen_enabled_ = false;
|
bool fullscreen_enabled_ = false;
|
||||||
bool real_fullscreen_enabled_ = false;
|
bool real_fullscreen_enabled_ = false;
|
||||||
bool kiosk_mode_ = 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)
|
// Resolución base (configurada por CLI o default)
|
||||||
int base_screen_width_ = DEFAULT_SCREEN_WIDTH;
|
int base_screen_width_ = DEFAULT_SCREEN_WIDTH;
|
||||||
@@ -164,15 +187,13 @@ class Engine {
|
|||||||
|
|
||||||
// Sistema de temas (delegado a ThemeManager)
|
// Sistema de temas (delegado a ThemeManager)
|
||||||
std::unique_ptr<ThemeManager> theme_manager_;
|
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
|
// Modo de simulación actual (PHYSICS/SHAPE/BOIDS)
|
||||||
// El estado de figuras (active_shape_, scale, etc.) está en ShapeManager
|
|
||||||
SimulationMode current_mode_ = SimulationMode::PHYSICS;
|
SimulationMode current_mode_ = SimulationMode::PHYSICS;
|
||||||
|
|
||||||
// Sistema de Modo DEMO (auto-play) y LOGO
|
// 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;
|
||||||
int max_auto_scenario_ = 5; // Índice máximo en modos auto (resultado del benchmark)
|
|
||||||
|
|
||||||
// Escenario custom (--custom-balls)
|
// Escenario custom (--custom-balls)
|
||||||
int custom_scenario_balls_ = 0;
|
int custom_scenario_balls_ = 0;
|
||||||
@@ -180,19 +201,10 @@ class Engine {
|
|||||||
bool custom_auto_available_ = false;
|
bool custom_auto_available_ = false;
|
||||||
bool skip_benchmark_ = 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)
|
// Bucket sort per z-ordering (SHAPE mode)
|
||||||
static constexpr int DEPTH_SORT_BUCKETS = 256;
|
static constexpr int DEPTH_SORT_BUCKETS = 256;
|
||||||
std::array<std::vector<size_t>, DEPTH_SORT_BUCKETS> depth_buckets_;
|
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
|
// Métodos principales del loop
|
||||||
void calculateDeltaTime();
|
void calculateDeltaTime();
|
||||||
void update();
|
void update();
|
||||||
@@ -201,24 +213,30 @@ class Engine {
|
|||||||
// Benchmark de rendimiento (determina max_auto_scenario_ al inicio)
|
// Benchmark de rendimiento (determina max_auto_scenario_ al inicio)
|
||||||
void runPerformanceBenchmark();
|
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
|
// Sistema de cambio de sprites dinámico
|
||||||
void switchTextureInternal(bool show_notification); // Implementación interna del cambio de textura
|
void switchTextureInternal(bool show_notification);
|
||||||
|
|
||||||
// Sistema de zoom dinámico - Métodos privados
|
// Sistema de zoom dinámico
|
||||||
int calculateMaxWindowZoom() const;
|
int calculateMaxWindowZoom() const;
|
||||||
void setWindowZoom(int new_zoom);
|
void setWindowZoom(int new_zoom);
|
||||||
void zoomIn();
|
void zoomIn();
|
||||||
void zoomOut();
|
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);
|
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)
|
// Sistema de Figuras 3D
|
||||||
void toggleShapeModeInternal(bool force_gravity_on_exit = true); // Delega a ShapeManager + sincroniza current_mode_
|
void toggleShapeModeInternal(bool force_gravity_on_exit = true);
|
||||||
void activateShapeInternal(ShapeType type); // Delega a ShapeManager + sets current_mode_ = SHAPE
|
void activateShapeInternal(ShapeType type);
|
||||||
void updateShape(); // Delega a ShapeManager::update()
|
void updateShape();
|
||||||
void generateShape(); // Delega a ShapeManager::generateShape()
|
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_
|
||||||
};
|
};
|
||||||
|
|||||||
78
source/gpu/gpu_context.cpp
Normal file
78
source/gpu/gpu_context.cpp
Normal 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);
|
||||||
|
}
|
||||||
33
source/gpu/gpu_context.hpp
Normal file
33
source/gpu/gpu_context.hpp
Normal 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
286
source/gpu/gpu_pipeline.cpp
Normal 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;
|
||||||
|
}
|
||||||
35
source/gpu/gpu_pipeline.hpp
Normal file
35
source/gpu/gpu_pipeline.hpp
Normal 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;
|
||||||
|
};
|
||||||
192
source/gpu/gpu_sprite_batch.cpp
Normal file
192
source/gpu/gpu_sprite_batch.cpp
Normal 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);
|
||||||
|
}
|
||||||
86
source/gpu/gpu_sprite_batch.hpp
Normal file
86
source/gpu/gpu_sprite_batch.hpp
Normal 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
208
source/gpu/gpu_texture.cpp
Normal 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;
|
||||||
|
}
|
||||||
48
source/gpu/gpu_texture.hpp
Normal file
48
source/gpu/gpu_texture.hpp
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user