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

430 lines
15 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_->setScaleMode(Options::video.integer_scale);
shader_backend_->setVSync(Options::video.vsync);
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear);
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
shader_backend_->setLinearUpscale(Options::video.linear_upscale);
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)
Rendering::PostFXParams clean{};
shader_backend_->setPostFXParams(clean);
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render();
} 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();
}
}
void Screen::toggleSupersampling() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
Options::video.supersampling = !Options::video.supersampling;
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
}
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::toggleIntegerScale() {
Options::video.integer_scale = !Options::video.integer_scale;
if (shader_backend_) {
shader_backend_->setScaleMode(Options::video.integer_scale);
} else {
applyFallbackPresentation();
}
}
void Screen::toggleVSync() {
Options::video.vsync = !Options::video.vsync;
if (shader_backend_) {
shader_backend_->setVSync(Options::video.vsync);
}
}
void Screen::toggleStretchFilter() {
Options::video.stretch_filter_linear = !Options::video.stretch_filter_linear;
if (shader_backend_) {
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear);
} else {
applyFallbackPresentation();
}
}
void Screen::nextShaderType() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
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();
}
}
void Screen::nextPreset() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return;
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;
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();
}
}
void Screen::prevShaderType() {
// Només dues opcions — prev == next
nextShaderType();
}
void Screen::prevPreset() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return;
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;
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();
}
}
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): tria
// el mode de presentació lògica segons 4:3 i integer_scale, i aplica el
// filtre de la textura segons stretch_filter_linear. Sense açò, el path
// fallback mostrava sempre LETTERBOX i ignorava les tres flags.
SDL_ScaleMode scale = Options::video.stretch_filter_linear ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST;
if (texture_) SDL_SetTextureScaleMode(texture_, scale);
SDL_RendererLogicalPresentation mode;
if (Options::video.aspect_ratio_4_3) {
// La finestra ja té aspect 4:3 (alçada × 1.2); STRETCH estira la
// textura 320×200 fins a omplir-la exactament.
mode = SDL_LOGICAL_PRESENTATION_STRETCH;
} else if (Options::video.integer_scale) {
mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
} else {
mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
}
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;
}
}