Compare commits
2 Commits
5cda8fc3f9
...
e8b0b12f98
| Author | SHA1 | Date | |
|---|---|---|---|
| e8b0b12f98 | |||
| 16a3f5b470 |
@@ -33,6 +33,7 @@ menu:
|
|||||||
texture_filter: "Filtre textura"
|
texture_filter: "Filtre textura"
|
||||||
render_info: "Render info"
|
render_info: "Render info"
|
||||||
uptime: "Temps de joc"
|
uptime: "Temps de joc"
|
||||||
|
internal_resolution: "Resolució interna"
|
||||||
master_enable: "Àudio"
|
master_enable: "Àudio"
|
||||||
master_volume: "Màster"
|
master_volume: "Màster"
|
||||||
music: "Música"
|
music: "Música"
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ namespace Menu {
|
|||||||
? Locale::get("menu.values.linear")
|
? Locale::get("menu.values.linear")
|
||||||
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
|
: 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)
|
// Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
|
||||||
#ifndef __EMSCRIPTEN__
|
#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});
|
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ Screen::Screen() {
|
|||||||
if (zoom_ < 1) zoom_ = 1;
|
if (zoom_ < 1) zoom_ = 1;
|
||||||
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
|
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 w = GAME_WIDTH * zoom_;
|
||||||
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * 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();
|
shader_backend_.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
|
||||||
if (texture_) SDL_DestroyTexture(texture_);
|
if (texture_) SDL_DestroyTexture(texture_);
|
||||||
if (renderer_) SDL_DestroyRenderer(renderer_);
|
if (renderer_) SDL_DestroyRenderer(renderer_);
|
||||||
if (window_) SDL_DestroyWindow(window_);
|
if (window_) SDL_DestroyWindow(window_);
|
||||||
@@ -108,6 +116,8 @@ void Screen::initShaders() {
|
|||||||
shader_backend_->setOversample(3);
|
shader_backend_->setOversample(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shader_backend_->setInternalResolution(Options::video.internal_resolution);
|
||||||
|
|
||||||
// Resol el shader actiu des del config
|
// Resol el shader actiu des del config
|
||||||
if (Options::video.current_shader == "crtpi") {
|
if (Options::video.current_shader == "crtpi") {
|
||||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||||
@@ -162,8 +172,45 @@ void Screen::present(Uint32* pixel_data) {
|
|||||||
shader_backend_->setActiveShader(prev_shader);
|
shader_backend_->setActiveShader(prev_shader);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback SDL_Renderer
|
// 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));
|
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_RenderClear(renderer_);
|
||||||
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
|
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
|
||||||
SDL_RenderPresent(renderer_);
|
SDL_RenderPresent(renderer_);
|
||||||
@@ -261,6 +308,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 {
|
auto Screen::nextShaderType() -> bool {
|
||||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||||
if (!Options::video.shader_enabled) return false;
|
if (!Options::video.shader_enabled) return false;
|
||||||
@@ -442,7 +505,42 @@ void Screen::applyFallbackPresentation() {
|
|||||||
case Options::ScalingMode::INTEGER: mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; break;
|
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() {
|
||||||
|
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() {
|
void Screen::adjustWindowSize() {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class Screen {
|
|||||||
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
|
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
|
||||||
void toggleVSync();
|
void toggleVSync();
|
||||||
void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
|
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 nextShaderType() -> bool; // false si GPU off / shaders off
|
||||||
auto prevShaderType() -> bool; // idem
|
auto prevShaderType() -> bool; // idem
|
||||||
auto nextPreset() -> bool; // false si GPU off / shaders off
|
auto nextPreset() -> bool; // false si GPU off / shaders off
|
||||||
@@ -45,6 +46,7 @@ class Screen {
|
|||||||
// Getters
|
// Getters
|
||||||
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
|
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
|
||||||
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
|
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
|
||||||
|
[[nodiscard]] auto getMaxZoom() const -> int { return max_zoom_; }
|
||||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool;
|
[[nodiscard]] auto isHardwareAccelerated() const -> bool;
|
||||||
[[nodiscard]] auto getActiveShaderName() const -> const char*;
|
[[nodiscard]] auto getActiveShaderName() const -> const char*;
|
||||||
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
|
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
|
||||||
@@ -58,12 +60,15 @@ class Screen {
|
|||||||
void calculateMaxZoom();
|
void calculateMaxZoom();
|
||||||
void initShaders();
|
void initShaders();
|
||||||
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
|
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
|
||||||
|
void ensureFallbackInternalTexture(); // Recrea internal_texture_sdl_ si cal (fallback path)
|
||||||
|
|
||||||
static Screen* instance_;
|
static Screen* instance_;
|
||||||
|
|
||||||
SDL_Window* window_{nullptr};
|
SDL_Window* window_{nullptr};
|
||||||
SDL_Renderer* renderer_{nullptr};
|
SDL_Renderer* renderer_{nullptr};
|
||||||
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer)
|
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)
|
// Backend GPU (nullptr si no disponible o desactivat)
|
||||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;
|
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;
|
||||||
|
|||||||
@@ -456,6 +456,11 @@ namespace Rendering {
|
|||||||
return false;
|
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
|
// scaled_texture_ se creará en el primer render() una vez conocido el zoom de ventana
|
||||||
ss_factor_ = 0;
|
ss_factor_ = 0;
|
||||||
|
|
||||||
@@ -812,14 +817,50 @@ namespace Rendering {
|
|||||||
SDL_EndGPUCopyPass(copy);
|
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)
|
// 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.
|
// El filtre s'aplica sempre (texture_filter_linear_), independent de 4:3.
|
||||||
// L'effective_scene/height reflecteix la textura real que veuen els shaders.
|
// L'effective_scene/height reflecteix la textura real que veuen els shaders.
|
||||||
// Sense SS ni stretch: scene_texture_ a game_height_.
|
// 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).
|
// 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_;
|
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) {
|
if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
|
||||||
SDL_GPUColorTargetInfo upscale_target = {};
|
SDL_GPUColorTargetInfo upscale_target = {};
|
||||||
@@ -834,7 +875,7 @@ namespace Rendering {
|
|||||||
if (upass != nullptr) {
|
if (upass != nullptr) {
|
||||||
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
|
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
|
||||||
SDL_GPUTextureSamplerBinding ubinding = {};
|
SDL_GPUTextureSamplerBinding ubinding = {};
|
||||||
ubinding.texture = scene_texture_;
|
ubinding.texture = source_texture;
|
||||||
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
||||||
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
|
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
|
||||||
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
|
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
|
||||||
@@ -846,6 +887,7 @@ namespace Rendering {
|
|||||||
// Sense SS: el viewport s'encarrega de l'estirament geomètric
|
// Sense SS: el viewport s'encarrega de l'estirament geomètric
|
||||||
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
|
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
|
||||||
}
|
}
|
||||||
|
(void)source_height;
|
||||||
|
|
||||||
// ---- Acquire swapchain texture ----
|
// ---- Acquire swapchain texture ----
|
||||||
SDL_GPUTexture* swapchain = nullptr;
|
SDL_GPUTexture* swapchain = nullptr;
|
||||||
@@ -935,9 +977,14 @@ namespace Rendering {
|
|||||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||||
SDL_SetGPUViewport(pass, &vp);
|
SDL_SetGPUViewport(pass, &vp);
|
||||||
|
|
||||||
|
// El shader CrtPi tradicionalment usa NEAREST per a fer el seu
|
||||||
|
// propi filtrat analític. Si l'usuari tria LINEAR explícitament,
|
||||||
|
// respectem la preferència (la mostra arribarà pre-suavitzada).
|
||||||
SDL_GPUTextureSamplerBinding binding = {};
|
SDL_GPUTextureSamplerBinding binding = {};
|
||||||
binding.texture = effective_scene;
|
binding.texture = effective_scene;
|
||||||
binding.sampler = sampler_; // NEAREST: el shader CrtPi fa el seu propi filtrat analític
|
binding.sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
|
||||||
|
? linear_sampler_
|
||||||
|
: sampler_;
|
||||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||||
|
|
||||||
// Injectar texture_width/height abans del push
|
// Injectar texture_width/height abans del push
|
||||||
@@ -1012,11 +1059,15 @@ namespace Rendering {
|
|||||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||||
SDL_SetGPUViewport(pass, &vp);
|
SDL_SetGPUViewport(pass, &vp);
|
||||||
|
|
||||||
// Amb SS: llegir de scaled_texture_ amb LINEAR; sense SS: effective_scene amb NEAREST.
|
// Font: amb SS scaled_texture_; sense SS, effective_scene (que ja
|
||||||
|
// és internal_texture_ si internal_res_>1, o scene_texture_ si no).
|
||||||
|
// Sampler: honora el filtre global que l'usuari tria al menú
|
||||||
|
// (texture_filter_linear_). Abans estava hardcoded a NEAREST
|
||||||
|
// quan SS era off — el menú no tenia efecte visible en aquest path.
|
||||||
SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr)
|
SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr)
|
||||||
? scaled_texture_
|
? scaled_texture_
|
||||||
: effective_scene;
|
: effective_scene;
|
||||||
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr)
|
SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
|
||||||
? linear_sampler_
|
? linear_sampler_
|
||||||
: sampler_;
|
: sampler_;
|
||||||
|
|
||||||
@@ -1068,6 +1119,10 @@ namespace Rendering {
|
|||||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
scene_texture_ = nullptr;
|
scene_texture_ = nullptr;
|
||||||
}
|
}
|
||||||
|
if (internal_texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||||
|
internal_texture_ = nullptr;
|
||||||
|
}
|
||||||
if (scaled_texture_ != nullptr) {
|
if (scaled_texture_ != nullptr) {
|
||||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||||
scaled_texture_ = nullptr;
|
scaled_texture_ = nullptr;
|
||||||
@@ -1218,6 +1273,18 @@ namespace Rendering {
|
|||||||
scaling_mode_ = mode;
|
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) {
|
void SDL3GPUShader::setStretch4_3(bool enabled) {
|
||||||
stretch_4_3_ = enabled;
|
stretch_4_3_ = enabled;
|
||||||
if (!is_initialized_ || device_ == nullptr) return;
|
if (!is_initialized_ || device_ == nullptr) return;
|
||||||
@@ -1263,6 +1330,10 @@ namespace Rendering {
|
|||||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
scene_texture_ = nullptr;
|
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
|
// scaled_texture_ se libera aquí; se recreará en el primer render() con el factor correcto
|
||||||
if (scaled_texture_ != nullptr) {
|
if (scaled_texture_ != nullptr) {
|
||||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||||
@@ -1305,10 +1376,15 @@ namespace Rendering {
|
|||||||
return false;
|
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_width_,
|
||||||
game_height_,
|
game_height_,
|
||||||
oversample_ > 1 ? "on" : "off");
|
oversample_ > 1 ? "on" : "off",
|
||||||
|
internal_res_);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1379,4 +1455,39 @@ namespace Rendering {
|
|||||||
return true;
|
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
|
} // namespace Rendering
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ namespace Rendering {
|
|||||||
texture_filter_linear_ = (filter == Options::TextureFilter::LINEAR);
|
texture_filter_linear_ = (filter == Options::TextureFilter::LINEAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multiplicador de resolució interna (1 = off).
|
||||||
|
void setInternalResolution(int multiplier) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static auto createShaderMSL(SDL_GPUDevice* device,
|
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||||
const char* msl_source,
|
const char* msl_source,
|
||||||
@@ -146,6 +149,7 @@ namespace Rendering {
|
|||||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
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)
|
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
[[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* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
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* 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* 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_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
|
||||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
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 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 oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
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 driver_name_;
|
||||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||||
bool is_initialized_ = false;
|
bool is_initialized_ = false;
|
||||||
|
|||||||
@@ -177,6 +177,13 @@ namespace Rendering {
|
|||||||
* @brief Filtre de textura global per a l'upscale final (sempre aplicat).
|
* @brief Filtre de textura global per a l'upscale final (sempre aplicat).
|
||||||
*/
|
*/
|
||||||
virtual void setTextureFilter(Options::TextureFilter /*filter*/) {}
|
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
|
} // namespace Rendering
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace Defaults::Video {
|
|||||||
constexpr bool VSYNC = true;
|
constexpr bool VSYNC = true;
|
||||||
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
|
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 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).
|
// TextureFilter i ScalingMode viuen a Options (requereixen #include, evitem dependència circular).
|
||||||
} // namespace Defaults::Video
|
} // namespace Defaults::Video
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ namespace Options {
|
|||||||
}
|
}
|
||||||
if (node.contains("downscale_algo"))
|
if (node.contains("downscale_algo"))
|
||||||
video.downscale_algo = node["downscale_algo"].get_value<int>();
|
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"))
|
if (node.contains("current_shader"))
|
||||||
video.current_shader = node["current_shader"].get_value<std::string>();
|
video.current_shader = node["current_shader"].get_value<std::string>();
|
||||||
if (node.contains("current_postfx_preset"))
|
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 << " 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 << " 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 << " 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_shader: " << video.current_shader << "\n";
|
||||||
file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
|
file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
|
||||||
file << " current_crtpi_preset: " << video.current_crtpi_preset << "\n";
|
file << " current_crtpi_preset: " << video.current_crtpi_preset << "\n";
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace Options {
|
|||||||
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
|
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
|
||||||
TextureFilter texture_filter{TextureFilter::NEAREST};
|
TextureFilter texture_filter{TextureFilter::NEAREST};
|
||||||
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
|
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_shader{"postfx"}; // "postfx" o "crtpi"
|
||||||
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
|
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
|
||||||
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu
|
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu
|
||||||
|
|||||||
Reference in New Issue
Block a user