treballant en internal resolution

This commit is contained in:
2026-04-16 20:53:13 +02:00
parent 5cda8fc3f9
commit 16a3f5b470
10 changed files with 227 additions and 7 deletions

View File

@@ -33,6 +33,7 @@ menu:
texture_filter: "Filtre textura"
render_info: "Render info"
uptime: "Temps de joc"
internal_resolution: "Resolució interna"
master_enable: "Àudio"
master_volume: "Màster"
music: "Música"

View File

@@ -176,6 +176,11 @@ namespace Menu {
? Locale::get("menu.values.linear")
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::IntRange, [] {
char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Options::video.internal_resolution);
return std::string(buf); }, [](int dir) { Screen::get()->changeInternalResolution(dir); }, nullptr, nullptr, nullptr});
// Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});

View File

@@ -37,6 +37,13 @@ Screen::Screen() {
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_;
@@ -66,6 +73,7 @@ Screen::~Screen() {
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_);
@@ -108,6 +116,8 @@ void Screen::initShaders() {
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);
@@ -164,6 +174,33 @@ void Screen::present(Uint32* pixel_data) {
} else {
// Fallback SDL_Renderer
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
const int mult = Options::video.internal_resolution;
if (mult > 1) {
// Resolució interna: còpia NN de texture_ → internal_texture_sdl_
// (la fa SDL/GPU, no CPU). Tota la presentació downstream llegirà
// d'aquesta textura més gran — el filtre LINEAR final parteix d'una
// font més fina i l'estirament a finestra queda menys difús.
ensureFallbackInternalTexture();
if (internal_texture_sdl_ != nullptr) {
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);
// El filtre global (LINEAR/NEAREST) s'aplica a l'estirament final
// cap a la finestra; per això l'aplicam a la textura intermèdia.
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 ha fallat, caiem al path normal.
}
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
@@ -261,6 +298,22 @@ void Screen::cycleTextureFilter(int dir) {
}
}
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;
@@ -442,7 +495,41 @@ void Screen::applyFallbackPresentation() {
case Options::ScalingMode::INTEGER: mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; break;
}
}
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH, GAME_HEIGHT, mode);
// 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() {
const int mult = Options::video.internal_resolution;
if (mult <= 1 || renderer_ == nullptr) {
// No cal textura intermèdia. Si la teníem, la reciclem.
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() {

View File

@@ -33,6 +33,7 @@ class Screen {
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
void toggleVSync();
void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
void changeInternalResolution(int dir); // +/1, clampat a [1, max_zoom_]
auto nextShaderType() -> bool; // false si GPU off / shaders off
auto prevShaderType() -> bool; // idem
auto nextPreset() -> bool; // false si GPU off / shaders off
@@ -45,6 +46,7 @@ class Screen {
// Getters
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
[[nodiscard]] auto getMaxZoom() const -> int { return max_zoom_; }
[[nodiscard]] auto isHardwareAccelerated() const -> bool;
[[nodiscard]] auto getActiveShaderName() const -> const char*;
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
@@ -58,12 +60,15 @@ class Screen {
void calculateMaxZoom();
void initShaders();
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
void ensureFallbackInternalTexture(); // Recrea internal_texture_sdl_ si cal (fallback path)
static Screen* instance_;
SDL_Window* window_{nullptr};
SDL_Renderer* renderer_{nullptr};
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer)
SDL_Texture* internal_texture_sdl_{nullptr}; // 320·N x 200·N TARGET (fallback path, només si N>1)
int internal_texture_mult_{0}; // Multiplicador amb què es va crear internal_texture_sdl_
// Backend GPU (nullptr si no disponible o desactivat)
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;

View File

@@ -456,6 +456,11 @@ namespace Rendering {
return false;
}
// internal_texture_: si el multiplicador és > 1, es crea ací amb les
// dimensions game·N × game·N. No bloqueja si falla — només deixa la
// textura a nullptr i el pipeline ometrà la còpia.
recreateInternalTexture();
// scaled_texture_ se creará en el primer render() una vez conocido el zoom de ventana
ss_factor_ = 0;
@@ -812,14 +817,50 @@ namespace Rendering {
SDL_EndGPUCopyPass(copy);
}
// ---- Upscale pass: scene_texture_ → scaled_texture_ ----
// ---- Internal resolution NN upscale: scene_texture_ → internal_texture_ ----
// Multiplicador enter. Si > 1, tot el pipeline downstream veu internal_texture_
// com a "scene" (mida game·N × game·N) i els passos següents (SS, PostFX,
// Lanczos, letterbox) operen sobre aquesta font més gran. L'objectiu: quan el
// filtre final LINEAR estira a finestra, parteix d'una base més gran i es veu
// menys borrós. Amb internal_res_ == 1, s'omet el pas (zero overhead).
SDL_GPUTexture* source_texture = scene_texture_;
int source_width = game_width_;
int source_height = game_height_;
if (internal_res_ > 1 && internal_texture_ != nullptr && upscale_pipeline_ != nullptr) {
SDL_GPUColorTargetInfo internal_target = {};
internal_target.texture = internal_texture_;
internal_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
internal_target.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* ipass = SDL_BeginGPURenderPass(cmd, &internal_target, 1, nullptr);
if (ipass != nullptr) {
SDL_BindGPUGraphicsPipeline(ipass, upscale_pipeline_);
SDL_GPUTextureSamplerBinding ibinding = {};
ibinding.texture = scene_texture_;
ibinding.sampler = sampler_; // sempre NEAREST per a la còpia de resolució interna
SDL_BindGPUFragmentSamplers(ipass, 0, &ibinding, 1);
SDL_DrawGPUPrimitives(ipass, 3, 1, 0, 0);
SDL_EndGPURenderPass(ipass);
}
source_texture = internal_texture_;
source_width = game_width_ * internal_res_;
source_height = game_height_ * internal_res_;
}
// ---- Upscale pass: source_texture → scaled_texture_ ----
// Si 4:3 actiu, l'estirament s'aplica ací directament (320x200 → W*factor × H*factor*1.2)
// El filtre s'aplica sempre (texture_filter_linear_), independent de 4:3.
// L'effective_scene/height reflecteix la textura real que veuen els shaders.
// Sense SS ni stretch: scene_texture_ a game_height_.
// Amb SS o stretch: scaled_texture_ a l'alçada escalada (amb o sense 4:3).
SDL_GPUTexture* effective_scene = scene_texture_;
SDL_GPUTexture* effective_scene = source_texture;
// `effective_height` reflecteix l'alçada lògica del frame (per a
// scanlines i viewport), no la mida real de la textura. Es manté
// a `game_height_` encara que internal_res_ > 1 — el multiplicador
// només afecta la resolució física de la font, no l'aspect ni el
// nombre de scanlines visibles.
int effective_height = game_height_;
(void)source_width; // només es fa servir com a context informatiu
if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
SDL_GPUColorTargetInfo upscale_target = {};
@@ -834,7 +875,7 @@ namespace Rendering {
if (upass != nullptr) {
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
SDL_GPUTextureSamplerBinding ubinding = {};
ubinding.texture = scene_texture_;
ubinding.texture = source_texture;
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
@@ -846,6 +887,7 @@ namespace Rendering {
// Sense SS: el viewport s'encarrega de l'estirament geomètric
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
}
(void)source_height;
// ---- Acquire swapchain texture ----
SDL_GPUTexture* swapchain = nullptr;
@@ -1068,6 +1110,10 @@ namespace Rendering {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (scaled_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scaled_texture_);
scaled_texture_ = nullptr;
@@ -1218,6 +1264,18 @@ namespace Rendering {
scaling_mode_ = mode;
}
// setInternalResolution — canvia el multiplicador de resolució interna.
// Recrea la textura intermèdia amb les noves dimensions (320·N × 200·N).
void SDL3GPUShader::setInternalResolution(int multiplier) {
const int NEW = std::max(1, multiplier);
if (NEW == internal_res_) return;
internal_res_ = NEW;
if (is_initialized_ && device_ != nullptr) {
SDL_WaitForGPUIdle(device_);
recreateInternalTexture();
}
}
void SDL3GPUShader::setStretch4_3(bool enabled) {
stretch_4_3_ = enabled;
if (!is_initialized_ || device_ == nullptr) return;
@@ -1263,6 +1321,10 @@ namespace Rendering {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
// scaled_texture_ se libera aquí; se recreará en el primer render() con el factor correcto
if (scaled_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scaled_texture_);
@@ -1305,10 +1367,15 @@ namespace Rendering {
return false;
}
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s (scaled se creará en render)",
// Recrea la textura interna si internal_res_ > 1 — manté coherència
// en canvis d'SS que passen per reinitTexturesAndBuffer().
recreateInternalTexture();
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s, internal ×%d (scaled se creará en render)",
game_width_,
game_height_,
oversample_ > 1 ? "on" : "off");
oversample_ > 1 ? "on" : "off",
internal_res_);
return true;
}
@@ -1379,4 +1446,39 @@ namespace Rendering {
return true;
}
// ---------------------------------------------------------------------------
// recreateInternalTexture — libera y recrea internal_texture_ para el
// multiplicador internal_res_ actual. Si val 1, allibera i queda a nullptr
// (el pipeline ometrà la còpia al següent render).
// ---------------------------------------------------------------------------
auto SDL3GPUShader::recreateInternalTexture() -> bool {
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (internal_res_ <= 1 || device_ == nullptr) return true;
const int W = game_width_ * internal_res_;
const int H = game_height_ * internal_res_;
SDL_GPUTextureCreateInfo info = {};
info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
info.width = static_cast<Uint32>(W);
info.height = static_cast<Uint32>(H);
info.layer_count_or_depth = 1;
info.num_levels = 1;
internal_texture_ = SDL_CreateGPUTexture(device_, &info);
if (internal_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create internal texture %dx%d (×%d): %s",
W, H, internal_res_, SDL_GetError());
return false;
}
SDL_Log("SDL3GPUShader: internal texture %dx%d (×%d)", W, H, internal_res_);
return true;
}
} // namespace Rendering

View File

@@ -126,6 +126,9 @@ namespace Rendering {
texture_filter_linear_ = (filter == Options::TextureFilter::LINEAR);
}
// Multiplicador de resolució interna (1 = off).
void setInternalResolution(int multiplier) override;
private:
static auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
@@ -146,6 +149,7 @@ namespace Rendering {
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
auto recreateInternalTexture() -> bool; // Recrea internal_texture_ (res interna × N)
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
@@ -158,6 +162,7 @@ namespace Rendering {
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor, amb 4:3 si actiu)
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
@@ -173,6 +178,7 @@ namespace Rendering {
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
std::string driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false;

View File

@@ -177,6 +177,13 @@ namespace Rendering {
* @brief Filtre de textura global per a l'upscale final (sempre aplicat).
*/
virtual void setTextureFilter(Options::TextureFilter /*filter*/) {}
/**
* @brief Multiplicador enter de la "resolució interna": fa un NN upscale
* de scene (320×200) a 320·N × 200·N i la pipeline downstream
* parteix d'aquesta textura. 1 = off (sense còpia addicional).
*/
virtual void setInternalResolution(int /*multiplier*/) {}
};
} // namespace Rendering

View File

@@ -20,6 +20,7 @@ namespace Defaults::Video {
constexpr bool VSYNC = true;
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
constexpr int INTERNAL_RESOLUTION = 1; // Multiplicador enter de la textura font abans del pipeline
// TextureFilter i ScalingMode viuen a Options (requereixen #include, evitem dependència circular).
} // namespace Defaults::Video

View File

@@ -141,6 +141,10 @@ namespace Options {
}
if (node.contains("downscale_algo"))
video.downscale_algo = node["downscale_algo"].get_value<int>();
if (node.contains("internal_resolution")) {
video.internal_resolution = node["internal_resolution"].get_value<int>();
if (video.internal_resolution < 1) video.internal_resolution = 1;
}
if (node.contains("current_shader"))
video.current_shader = node["current_shader"].get_value<std::string>();
if (node.contains("current_postfx_preset"))
@@ -298,6 +302,7 @@ namespace Options {
file << " aspect_ratio_4_3: " << (video.aspect_ratio_4_3 ? "true" : "false") << "\n";
file << " texture_filter: " << (video.texture_filter == TextureFilter::LINEAR ? "linear" : "nearest") << " # nearest|linear\n";
file << " downscale_algo: " << video.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
file << " internal_resolution: " << video.internal_resolution << " # multiplicador enter font, clampat a max_zoom\n";
file << " current_shader: " << video.current_shader << "\n";
file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
file << " current_crtpi_preset: " << video.current_crtpi_preset << "\n";

View File

@@ -45,6 +45,7 @@ namespace Options {
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
TextureFilter texture_filter{TextureFilter::NEAREST};
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
int internal_resolution{Defaults::Video::INTERNAL_RESOLUTION}; // Multiplicador enter ≥ 1, clampat a max_zoom
std::string current_shader{"postfx"}; // "postfx" o "crtpi"
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu