Files
aee/source/core/rendering/screen.cpp

466 lines
18 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_;
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 (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);
}
// 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
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
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();
}
}
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;
}
}
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH, GAME_HEIGHT, mode);
}
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;
}
}