Files
aee/source/core/rendering/screen.cpp
2026-04-16 21:40:14 +02:00

564 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "core/rendering/screen.hpp"
#include <cstdio>
#include <iostream>
#include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp"
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#endif
#include "game/defines.hpp"
#include "game/options.hpp"
#include "utils/utils.hpp"
Screen* Screen::instance_ = nullptr;
void Screen::init() {
instance_ = new Screen();
}
void Screen::destroy() {
delete instance_;
instance_ = nullptr;
}
auto Screen::get() -> Screen* {
return instance_;
}
Screen::Screen() {
// Carrega opcions guardades
zoom_ = Options::window.zoom;
fullscreen_ = Options::window.fullscreen;
calculateMaxZoom();
if (zoom_ < 1) zoom_ = 1;
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
// Clamp de la resolució interna a [1, max_zoom_]. Llegir del YAML i
// ajustar aquí és l'únic moment en què es fa — el menú re-clampa cada
// canvi. Si la pantalla és més petita que el valor desat (p.ex. canvi
// de monitor), baixem al màxim suportat.
if (Options::video.internal_resolution < 1) Options::video.internal_resolution = 1;
if (Options::video.internal_resolution > max_zoom_) Options::video.internal_resolution = max_zoom_;
int w = GAME_WIDTH * zoom_;
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0);
renderer_ = SDL_CreateRenderer(window_, nullptr);
texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT);
applyFallbackPresentation();
// Inicialitza backend GPU si l'acceleració està activada
initShaders();
std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n";
}
Screen::~Screen() {
// Guarda opcions abans de destruir
Options::window.zoom = zoom_;
Options::window.fullscreen = fullscreen_;
// Destrueix el backend GPU (només existeix si s'ha compilat amb shaders)
if (shader_backend_) {
#ifndef NO_SHADERS
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
if (gpu) gpu->destroy();
#endif
shader_backend_.reset();
}
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
if (texture_) SDL_DestroyTexture(texture_);
if (renderer_) SDL_DestroyRenderer(renderer_);
if (window_) SDL_DestroyWindow(window_);
}
void Screen::initShaders() {
#ifdef NO_SHADERS
// Build sense shaders (p.ex. emscripten/WebGL2, on SDL3 GPU no està
// disponible). Es salta tota la inicialització — shader_backend_ es
// queda nul·lptr i tots els `if (shader_backend_)` del render path
// curtcircuiten cap al fallback SDL_Renderer.
return;
#else
if (!Options::video.gpu_acceleration) return;
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
const std::string FALLBACK_DRIVER = "none";
shader_backend_->setPreferredDriver(
Options::video.gpu_acceleration ? "" : FALLBACK_DRIVER);
// init() rep la finestra i la textura (la textura s'usa com a referència, el GPU fa uploadPixels)
if (!shader_backend_->init(window_, texture_, "", "")) {
std::cerr << "GPU shader backend initialization failed, using SDL_Renderer fallback\n";
shader_backend_.reset();
return;
}
gpu_driver_ = shader_backend_->getDriverName();
std::cout << "GPU driver: " << gpu_driver_ << '\n';
// Aplica opcions de vídeo
shader_backend_->setScalingMode(Options::video.scaling_mode);
shader_backend_->setVSync(Options::video.vsync);
shader_backend_->setTextureFilter(Options::video.texture_filter);
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
if (Options::video.supersampling) {
shader_backend_->setOversample(3);
}
shader_backend_->setInternalResolution(Options::video.internal_resolution);
// Resol el shader actiu des del config
if (Options::video.current_shader == "crtpi") {
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
} else {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
// Resol presets per nom
for (int i = 0; i < static_cast<int>(Options::postfx_presets.size()); i++) {
if (Options::postfx_presets[i].name == Options::video.current_postfx_preset) {
Options::current_postfx_preset = i;
break;
}
}
for (int i = 0; i < static_cast<int>(Options::crtpi_presets.size()); i++) {
if (Options::crtpi_presets[i].name == Options::video.current_crtpi_preset) {
Options::current_crtpi_preset = i;
break;
}
}
applyCurrentPostFXPreset();
applyCurrentCrtPiPreset();
#endif
}
void Screen::present(Uint32* pixel_data) {
fps_.increment();
fps_.calculate(SDL_GetTicks());
updateRenderInfo();
Overlay::render(pixel_data);
if (shader_backend_ && shader_backend_->isHardwareAccelerated() && Options::video.shader_enabled) {
// Path GPU: puja els píxels i renderitza amb shaders
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render();
} else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// GPU activa però shaders desactivats: renderitza net (sense efectes).
// Força POSTFX amb params zerats — altrament, si l'actiu és CRTPI,
// els seus efectes (scanlines, curvatura) seguirien aplicant-se encara
// que shader_enabled sigui false. Restaurem l'actiu al final per a
// no trencar la selecció de l'usuari.
Rendering::PostFXParams clean{};
shader_backend_->setPostFXParams(clean);
const auto prev_shader = shader_backend_->getActiveShader();
if (prev_shader != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render();
if (prev_shader != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(prev_shader);
}
} else {
// Fallback SDL_Renderer. A mult=1, flux directe original: logical
// presentation (setada per applyFallbackPresentation) + scale mode de
// texture_ segons l'opció. A mult>1, la còpia intermèdia crea la
// font ampliada (NN via GPU), i es presenta via logical presentation
// a la mida de la font intermèdia.
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
const int mult = Options::video.internal_resolution;
if (mult > 1) {
ensureFallbackInternalTexture();
if (internal_texture_sdl_ != nullptr) {
// Còpia NN a la textura intermèdia (mult·game). Sampler NN
// per construcció: volem píxels grans i nets.
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
SDL_SetRenderTarget(renderer_, internal_texture_sdl_);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_SetRenderTarget(renderer_, nullptr);
// Filtre global al pas final → finestra (via logical presentation
// que applyFallbackPresentation ja configura amb mida game·mult).
SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
SDL_SetTextureScaleMode(internal_texture_sdl_, final_scale);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, internal_texture_sdl_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
return;
}
// Si la creació de la textura intermèdia ha fallat, caiem al path normal.
}
// mult=1 (o fallback-del-fallback): texture_ directament. El scale mode
// el manté applyFallbackPresentation — però el re-apliquem per si la
// ruta mult>1 el va sobreescriure anteriorment.
SDL_ScaleMode direct_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
SDL_SetTextureScaleMode(texture_, direct_scale);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
}
}
void Screen::toggleFullscreen() {
fullscreen_ = !fullscreen_;
SDL_SetWindowFullscreen(window_, fullscreen_);
if (!fullscreen_) {
adjustWindowSize();
}
}
void Screen::incZoom() {
if (fullscreen_ || zoom_ >= max_zoom_) return;
zoom_++;
adjustWindowSize();
}
void Screen::decZoom() {
if (fullscreen_ || zoom_ <= 1) return;
zoom_--;
adjustWindowSize();
}
void Screen::setZoom(int zoom) {
if (zoom < 1 || zoom > max_zoom_ || fullscreen_) return;
zoom_ = zoom;
adjustWindowSize();
}
void Screen::toggleShaders() {
Options::video.shader_enabled = !Options::video.shader_enabled;
if (Options::video.shader_enabled) {
applyCurrentPostFXPreset();
}
}
auto Screen::toggleSupersampling() -> bool {
// SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale
// i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi
// submostreig intern i no usa aquesta via).
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) return false;
Options::video.supersampling = !Options::video.supersampling;
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
return true;
}
void Screen::toggleAspectRatio() {
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
if (shader_backend_) {
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
} else {
applyFallbackPresentation();
}
if (!fullscreen_) {
adjustWindowSize();
}
}
void Screen::cycleScalingMode(int dir) {
constexpr int N = 5; // DISABLED, STRETCH, LETTERBOX, OVERSCAN, INTEGER
int cur = static_cast<int>(Options::video.scaling_mode);
int step = (dir >= 0) ? 1 : -1;
cur = ((cur + step) % N + N) % N;
Options::video.scaling_mode = static_cast<Options::ScalingMode>(cur);
if (shader_backend_) {
shader_backend_->setScalingMode(Options::video.scaling_mode);
} else {
applyFallbackPresentation();
}
}
void Screen::toggleVSync() {
Options::video.vsync = !Options::video.vsync;
if (shader_backend_) {
shader_backend_->setVSync(Options::video.vsync);
}
}
void Screen::cycleTextureFilter(int dir) {
// NEAREST <-> LINEAR (només 2 valors, dir no importa més enllà de canviar)
(void)dir;
Options::video.texture_filter =
(Options::video.texture_filter == Options::TextureFilter::LINEAR)
? Options::TextureFilter::NEAREST
: Options::TextureFilter::LINEAR;
if (shader_backend_) {
shader_backend_->setTextureFilter(Options::video.texture_filter);
} else {
applyFallbackPresentation();
}
}
void Screen::changeInternalResolution(int dir) {
int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
if (next < 1) next = 1;
if (next > max_zoom_) next = max_zoom_;
if (next == Options::video.internal_resolution) return;
Options::video.internal_resolution = next;
// Propaga al backend actiu. Al fallback path, la textura es recrea al
// pròxim present via ensureFallbackInternalTexture.
if (shader_backend_) {
shader_backend_->setInternalResolution(next);
} else {
applyFallbackPresentation();
}
}
auto Screen::nextShaderType() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
Options::video.current_shader = "crtpi";
applyCurrentCrtPiPreset();
} else {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
Options::video.current_shader = "postfx";
applyCurrentPostFXPreset();
}
return true;
}
auto Screen::nextPreset() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return false;
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset();
} else {
if (Options::crtpi_presets.empty()) return false;
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset();
}
return true;
}
auto Screen::prevShaderType() -> bool {
// Només dues opcions — prev == next
return nextShaderType();
}
auto Screen::prevPreset() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return false;
int n = static_cast<int>(Options::postfx_presets.size());
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset();
} else {
if (Options::crtpi_presets.empty()) return false;
int n = static_cast<int>(Options::crtpi_presets.size());
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset();
}
return true;
}
auto Screen::getCurrentPresetName() const -> const char* {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "---";
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size()))
return Options::postfx_presets[Options::current_postfx_preset].name.c_str();
} else {
if (Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size()))
return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
}
return "---";
}
void Screen::setActiveShader(Rendering::ShaderType type) {
if (shader_backend_) {
shader_backend_->setActiveShader(type);
}
}
void Screen::applyCurrentPostFXPreset() {
if (!shader_backend_ || Options::postfx_presets.empty()) return;
const auto& preset = Options::postfx_presets[Options::current_postfx_preset];
Rendering::PostFXParams p;
p.vignette = preset.vignette;
p.scanlines = preset.scanlines;
p.chroma = preset.chroma;
p.mask = preset.mask;
p.gamma = preset.gamma;
p.curvature = preset.curvature;
p.bleeding = preset.bleeding;
p.flicker = preset.flicker;
shader_backend_->setPostFXParams(p);
}
void Screen::applyCurrentCrtPiPreset() {
if (!shader_backend_ || Options::crtpi_presets.empty()) return;
const auto& preset = Options::crtpi_presets[Options::current_crtpi_preset];
Rendering::CrtPiParams p;
p.scanline_weight = preset.scanline_weight;
p.scanline_gap_brightness = preset.scanline_gap_brightness;
p.bloom_factor = preset.bloom_factor;
p.input_gamma = preset.input_gamma;
p.output_gamma = preset.output_gamma;
p.mask_brightness = preset.mask_brightness;
p.curvature_x = preset.curvature_x;
p.curvature_y = preset.curvature_y;
p.mask_type = preset.mask_type;
p.enable_scanlines = preset.enable_scanlines;
p.enable_multisample = preset.enable_multisample;
p.enable_gamma = preset.enable_gamma;
p.enable_curvature = preset.enable_curvature;
p.enable_sharper = preset.enable_sharper;
shader_backend_->setCrtPiParams(p);
}
auto Screen::isHardwareAccelerated() const -> bool {
return shader_backend_ && shader_backend_->isHardwareAccelerated();
}
auto Screen::getActiveShaderName() const -> const char* {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "SENSE GPU";
return shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX ? "POSTFX" : "CRT-PI";
}
void Screen::updateRenderInfo() {
static const Uint32 start_ticks = SDL_GetTicks();
std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);
// Segment 0: FPS + driver (sempre visible)
std::string fps_driver = std::to_string(fps_.last_value) + " fps - " + driver;
// Segment 1: shader + preset (només si shaders actius)
std::string shader_seg;
if (Options::video.shader_enabled) {
shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName());
}
// Segment 2: supersampling indicator
const char* ss_seg = (Options::video.shader_enabled && Options::video.supersampling) ? " (ss)" : nullptr;
// Segment 3: hora (només si show_time)
char time_buf[32] = {0};
if (Options::render_info.show_time) {
Uint32 elapsed = SDL_GetTicks() - start_ticks;
int minutes = elapsed / 60000;
int seconds = (elapsed / 1000) % 60;
int centis = (elapsed / 10) % 100;
snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis);
}
// Dígits en mono a FPS (segment 0) i TEMPS (segment 3): els dígits canvien
// contínuament mentre els símbols del voltant ("fps", ":", ".", " - ") no
Overlay::setRenderInfoSegments(
fps_driver.c_str(),
shader_seg.empty() ? nullptr : shader_seg.c_str(),
ss_seg,
time_buf[0] ? time_buf : nullptr,
0b1001);
}
void Screen::applyFallbackPresentation() {
// Fallback SDL_Renderer (p.ex. emscripten/WebGL2 sense shaders GPU).
// Filtre global (texture_filter) s'aplica sempre, independent de 4:3.
SDL_ScaleMode scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
if (texture_) SDL_SetTextureScaleMode(texture_, scale);
// Si 4:3 actiu, la finestra ja té aspect 4:3 (alçada × 1.2); STRETCH és
// l'única opció viable al path fallback (el GPU path fa l'upscale 4:3 abans
// d'escollir el mode de finestra; en fallback no tenim eixa capa intermèdia).
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
if (Options::video.aspect_ratio_4_3) {
mode = SDL_LOGICAL_PRESENTATION_STRETCH;
} else {
switch (Options::video.scaling_mode) {
case Options::ScalingMode::DISABLED: mode = SDL_LOGICAL_PRESENTATION_DISABLED; break;
case Options::ScalingMode::STRETCH: mode = SDL_LOGICAL_PRESENTATION_STRETCH; break;
case Options::ScalingMode::LETTERBOX: mode = SDL_LOGICAL_PRESENTATION_LETTERBOX; break;
case Options::ScalingMode::OVERSCAN: mode = SDL_LOGICAL_PRESENTATION_OVERSCAN; break;
case Options::ScalingMode::INTEGER: mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; break;
}
}
// Amb resolució interna N > 1, la mida lògica creix proporcionalment
// perquè SDL scale des de 320·N × 200·N a la finestra — menys aggressive linear.
const int mult = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * mult, GAME_HEIGHT * mult, mode);
}
void Screen::ensureFallbackInternalTexture() {
if (renderer_ == nullptr) return;
const int mult = Options::video.internal_resolution;
if (mult <= 1) {
// No cal textura intermèdia — recicla si la teníem.
if (internal_texture_sdl_ != nullptr) {
SDL_DestroyTexture(internal_texture_sdl_);
internal_texture_sdl_ = nullptr;
internal_texture_mult_ = 0;
}
return;
}
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == mult) return;
if (internal_texture_sdl_ != nullptr) {
SDL_DestroyTexture(internal_texture_sdl_);
internal_texture_sdl_ = nullptr;
}
internal_texture_sdl_ = SDL_CreateTexture(renderer_,
SDL_PIXELFORMAT_ABGR8888,
SDL_TEXTUREACCESS_TARGET,
GAME_WIDTH * mult,
GAME_HEIGHT * mult);
if (internal_texture_sdl_ == nullptr) {
std::cerr << "Screen: failed to create fallback internal texture (×" << mult << "): "
<< SDL_GetError() << '\n';
internal_texture_mult_ = 0;
return;
}
internal_texture_mult_ = mult;
}
void Screen::adjustWindowSize() {
int w = GAME_WIDTH * zoom_;
// Si 4:3 actiu, l'alçada visual és 240 per zoom (200 * 1.2)
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
SDL_SetWindowSize(window_, w, h);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
void Screen::calculateMaxZoom() {
SDL_DisplayID display = SDL_GetPrimaryDisplay();
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
if (mode) {
int max_w = mode->w / GAME_WIDTH;
int max_h = mode->h / GAME_HEIGHT;
max_zoom_ = (max_w < max_h) ? max_w : max_h;
if (max_zoom_ < 1) max_zoom_ = 1;
}
}