564 lines
22 KiB
C++
564 lines
22 KiB
C++
#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;
|
||
}
|
||
}
|