mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-08 08:03:07 -10:00
Ultrawide: widen the view for outputs wider than 16:9, and keep custom sizes
- An output_res wider than 16:9 (5120x2160, 3440x1440, ...) filled the screen but kept the game's 16:9 projection, so the picture was stretched. patches.py now takes the community resolution patch for that size (Kyo's 21:9 and 32:9 ones in Bloodborne.xml, or one of the same aspect) and applies only its writes that widen the game's screen: not the scene and UI sizes or the heap patch, which bbport sets itself. bbport.ini aspect_patch=0 turns it off. - The in-game menu and the launcher listed four sizes: a custom output_res became 3840x2160 with the first saved setting, and 1280x720 in the launcher. Both keep it now until another size is chosen; tests/test_settings.cpp covers the menu side. From deadinside28/bloodborne_pc#183. Thanks: pmwork-personal, in 7 languages. Co-Authored-By: pmwork-personal <[email protected]>
This commit is contained in:
1 parent
16a08695c3
commit
8d1db30f9e
7 files changed
+127
-10
No files matched your search
@@ -378,13 +378,14 @@ void Menu() {
|
||||
for (int i = 0; i < BbSettings::PresetCount; ++i) {
|
||||
char label[64];
|
||||
const float scale = BbSettings::PresetScale(i);
|
||||
const int output = s.output_res;
|
||||
int output_width = BbSettings::OutputWidths[s.output_res];
|
||||
int output_height = BbSettings::OutputHeights[s.output_res];
|
||||
BbSettings::CustomOutput(output_width, output_height);
|
||||
std::snprintf(
|
||||
label, sizeof(label),
|
||||
BbSettings::MenuText("%s (x%.1f, render %dx%d)", "%s (x%.1f, рендер %dx%d)"),
|
||||
BbSettings::PresetName(i), scale,
|
||||
int(std::lround(BbSettings::OutputWidths[output] / scale / 2) * 2),
|
||||
int(std::lround(BbSettings::OutputHeights[output] / scale / 2) * 2));
|
||||
BbSettings::PresetName(i), scale, int(std::lround(output_width / scale / 2) * 2),
|
||||
int(std::lround(output_height / scale / 2) * 2));
|
||||
if (ImGui::Selectable(label, i == preset)) {
|
||||
Store(s.preset, i, true);
|
||||
}
|
||||
@@ -507,10 +508,19 @@ void Menu() {
|
||||
ImGui::SeparatorText(BbSettings::MenuText("Output resolution", "Разрешение вывода"));
|
||||
static const char* outputs[] = {"1280 x 720", "1920 x 1080", "2560 x 1440", "3840 x 2160"};
|
||||
int output = s.output_res;
|
||||
// A size from bbport.ini that the list does not have stays until another one is chosen.
|
||||
int custom_width = 0, custom_height = 0;
|
||||
const bool custom = BbSettings::CustomOutput(custom_width, custom_height);
|
||||
char custom_label[32];
|
||||
std::snprintf(custom_label, sizeof(custom_label), "%d x %d", custom_width, custom_height);
|
||||
if (ImGui::BeginCombo(BbSettings::MenuText("Output resolution", "Разрешение вывода"),
|
||||
outputs[output])) {
|
||||
custom ? custom_label : outputs[output])) {
|
||||
if (custom) {
|
||||
ImGui::Selectable(custom_label, true);
|
||||
}
|
||||
for (int i = 0; i < BbSettings::OutputCount; ++i) {
|
||||
if (ImGui::Selectable(outputs[i], i == output)) {
|
||||
if (ImGui::Selectable(outputs[i], !custom && i == output)) {
|
||||
s.custom_output_width = 0; // a listed size replaces the one from bbport.ini
|
||||
Store(s.output_res, i, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +86,19 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
} else if (key == "live_resolution") {
|
||||
v.live_resolution = value == "auto" ? -1 : std::clamp(i, 0, 1);
|
||||
} else if (key == "output_res") {
|
||||
bool listed = false;
|
||||
for (int r = 0; r < OutputCount; ++r) {
|
||||
if (value == std::to_string(OutputWidths[r]) + "x" + std::to_string(OutputHeights[r])) {
|
||||
v.output_res = r;
|
||||
listed = true;
|
||||
}
|
||||
}
|
||||
int width = 0, height = 0;
|
||||
if (!listed && std::sscanf(value.c_str(), "%dx%d", &width, &height) == 2 && width > 0 &&
|
||||
height > 0) {
|
||||
v.custom_output_width = width;
|
||||
v.custom_output_height = height;
|
||||
}
|
||||
} else {
|
||||
for (int e = 0; e < EffectCount; ++e) {
|
||||
if (key == Effects[e].key) {
|
||||
@@ -136,6 +144,7 @@ MenuLanguage MenuLanguageFromCode(const char* code) {
|
||||
|
||||
void Load() {
|
||||
auto& v = Get();
|
||||
v.custom_output_width = v.custom_output_height = 0;
|
||||
for (int e = 0; e < EffectCount; ++e) {
|
||||
v.effects[e] = Effects[e].default_on;
|
||||
}
|
||||
@@ -194,6 +203,7 @@ void Load() {
|
||||
}
|
||||
v.startup_model_lod = v.model_lod;
|
||||
v.startup_output_res = v.output_res;
|
||||
v.startup_custom_output_width = v.custom_output_width;
|
||||
v.startup_live_resolution = v.live_resolution;
|
||||
}
|
||||
|
||||
@@ -235,11 +245,22 @@ int RenderPreset() {
|
||||
v.upscaler == UpscalerTaa ? NativeAA : v.preset.load();
|
||||
}
|
||||
|
||||
bool CustomOutput(int& width, int& height) {
|
||||
const auto& v = Get();
|
||||
if (v.custom_output_width <= 0 || v.output_res != v.startup_output_res) {
|
||||
return false;
|
||||
}
|
||||
width = v.custom_output_width;
|
||||
height = v.custom_output_height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolutionNeedsRestart() {
|
||||
const auto& v = Get();
|
||||
// TAA needs the live path (native guest targets): run.sh selects it on restart.
|
||||
return FixedRenderSession() &&
|
||||
(v.preset != v.startup_preset || v.output_res != v.startup_output_res ||
|
||||
v.custom_output_width != v.startup_custom_output_width ||
|
||||
(v.upscaler == UpscalerOff) != (v.startup_upscaler == UpscalerOff) ||
|
||||
(v.upscaler == UpscalerTaa) != (v.startup_upscaler == UpscalerTaa));
|
||||
}
|
||||
@@ -282,8 +303,13 @@ void Save() {
|
||||
put(Effects[e].key, flag(v.effects[e]));
|
||||
}
|
||||
put("model_lod", std::to_string(v.model_lod.load()));
|
||||
put("output_res", std::to_string(OutputWidths[v.output_res]) + "x" +
|
||||
std::to_string(OutputHeights[v.output_res]));
|
||||
int custom_width = 0, custom_height = 0;
|
||||
if (CustomOutput(custom_width, custom_height)) {
|
||||
put("output_res", std::to_string(custom_width) + "x" + std::to_string(custom_height));
|
||||
} else {
|
||||
put("output_res", std::to_string(OutputWidths[v.output_res]) + "x" +
|
||||
std::to_string(OutputHeights[v.output_res]));
|
||||
}
|
||||
// Read by run.sh at start.
|
||||
put("live_resolution", v.live_resolution < 0 ? "auto" : flag(v.live_resolution != 0));
|
||||
if (v.menu_x >= 0.0f && v.menu_y >= 0.0f) {
|
||||
|
||||
@@ -97,6 +97,9 @@ struct Values {
|
||||
std::atomic<bool> effects[EffectCount]{};
|
||||
std::atomic<int> model_lod{0}; ///< -2 highest .. 2 lowest, 0 the game's
|
||||
std::atomic<int> output_res{OutputDefault}; ///< index into OutputWidths
|
||||
/// An output_res in bbport.ini that the list does not have (5120x2160, ...): run.sh and
|
||||
/// patches.py use it, and the menu keeps it until another size is chosen.
|
||||
int custom_output_width = 0, custom_output_height = 0;
|
||||
/// Live resolution and preset changes (run.sh): 0 off by default (startup patch, fastest
|
||||
/// on the Steam Deck and older GPUs), -1 auto (strong discrete GPUs), 1 on. On restart.
|
||||
std::atomic<int> live_resolution{0};
|
||||
@@ -114,6 +117,7 @@ struct Values {
|
||||
bool startup_effects[EffectCount]{};
|
||||
int startup_model_lod = 0;
|
||||
int startup_output_res = OutputDefault;
|
||||
int startup_custom_output_width = 0;
|
||||
int startup_live_resolution = 0;
|
||||
};
|
||||
|
||||
@@ -136,6 +140,8 @@ void ConfigureDlssSupport(bool available, const char* problem);
|
||||
bool FixedRenderSession();
|
||||
int RenderPreset();
|
||||
bool ResolutionNeedsRestart();
|
||||
/// The output size from bbport.ini that the menu does not list, while it is still the choice.
|
||||
bool CustomOutput(int& width, int& height);
|
||||
/// Writes the file (menu changes).
|
||||
void Save();
|
||||
|
||||
|
||||
@@ -600,6 +600,7 @@ EN = {
|
||||
'Вылет видеоплеера при его остановке': 'A video player crash when it stops',
|
||||
'Язык внутриигрового меню при запуске без окна лаунчера': "The in-game menu's language when starting without the launcher window",
|
||||
'DLSS в AppImage на NVIDIA': 'DLSS in the AppImage on NVIDIA',
|
||||
'Сверхширокие мониторы без растянутой картинки': 'Ultrawide monitors without a stretched picture',
|
||||
}
|
||||
|
||||
PT_BR = {
|
||||
@@ -1254,6 +1255,7 @@ PT_BR = {
|
||||
'Вылет видеоплеера при его остановке': 'Um travamento do reprodutor de vídeo ao parar',
|
||||
'Язык внутриигрового меню при запуске без окна лаунчера': 'O idioma do menu do jogo ao iniciar sem a janela do launcher',
|
||||
'DLSS в AppImage на NVIDIA': 'DLSS no AppImage em NVIDIA',
|
||||
'Сверхширокие мониторы без растянутой картинки': 'Monitores ultrawide sem imagem esticada',
|
||||
}
|
||||
|
||||
|
||||
@@ -1819,6 +1821,7 @@ ES = {
|
||||
'Вылет видеоплеера при его остановке': 'Un cierre del reproductor de vídeo al detenerse',
|
||||
'Язык внутриигрового меню при запуске без окна лаунчера': 'El idioma del menú del juego al arrancar sin la ventana del launcher',
|
||||
'DLSS в AppImage на NVIDIA': 'DLSS en la AppImage con NVIDIA',
|
||||
'Сверхширокие мониторы без растянутой картинки': 'Monitores ultrapanorámicos sin la imagen estirada',
|
||||
}
|
||||
|
||||
|
||||
@@ -2384,6 +2387,7 @@ IT = {
|
||||
'Вылет видеоплеера при его остановке': 'Un crash del lettore video quando si ferma',
|
||||
'Язык внутриигрового меню при запуске без окна лаунчера': 'La lingua del menu di gioco avviando senza la finestra del launcher',
|
||||
'DLSS в AppImage на NVIDIA': "DLSS nell'AppImage su NVIDIA",
|
||||
'Сверхширокие мониторы без растянутой картинки': 'Monitor ultrawide senza immagine stirata',
|
||||
}
|
||||
|
||||
|
||||
@@ -2949,6 +2953,7 @@ FR = {
|
||||
'Вылет видеоплеера при его остановке': 'Un plantage du lecteur vidéo à son arrêt',
|
||||
'Язык внутриигрового меню при запуске без окна лаунчера': 'La langue du menu en jeu au lancement sans la fenêtre du launcher',
|
||||
'DLSS в AppImage на NVIDIA': 'DLSS dans l’AppImage sur NVIDIA',
|
||||
'Сверхширокие мониторы без растянутой картинки': 'Les écrans ultra-larges sans image étirée',
|
||||
}
|
||||
|
||||
|
||||
@@ -3514,6 +3519,7 @@ DE = {
|
||||
'Вылет видеоплеера при его остановке': 'Ein Absturz des Videoplayers beim Anhalten',
|
||||
'Язык внутриигрового меню при запуске без окна лаунчера': 'Die Sprache des Spielmenüs beim Start ohne Launcher-Fenster',
|
||||
'DLSS в AppImage на NVIDIA': 'DLSS im AppImage auf NVIDIA',
|
||||
'Сверхширокие мониторы без растянутой картинки': 'Ultrawide-Monitore ohne gestrecktes Bild',
|
||||
}
|
||||
|
||||
|
||||
@@ -4022,6 +4028,7 @@ ZH_CN = {
|
||||
'Вылет видеоплеера при его остановке': '视频播放器停止时的崩溃',
|
||||
'Язык внутриигрового меню при запуске без окна лаунчера': '不经启动器窗口启动时游戏内菜单的语言',
|
||||
'DLSS в AppImage на NVIDIA': 'NVIDIA 上 AppImage 中的 DLSS',
|
||||
'Сверхширокие мониторы без растянутой картинки': '超宽屏显示器画面不再被拉伸',
|
||||
}
|
||||
|
||||
# Translations by language code; Russian is the key itself. Strings missing in a translation
|
||||
|
||||
@@ -631,6 +631,7 @@ THANKS = (
|
||||
_github("emurphy04", "Вылет видеоплеера при его остановке"),
|
||||
_github("AyrtonSanabio", "Язык внутриигрового меню при запуске без окна лаунчера"),
|
||||
_github("Aryxa", "DLSS в AppImage на NVIDIA"),
|
||||
_github("pmwork-personal", "Сверхширокие мониторы без растянутой картинки"),
|
||||
)),
|
||||
("Помощь и тесты", (
|
||||
_github("willthehuman", "Подробные отчёты с журналами и проверка исправлений"),
|
||||
@@ -838,9 +839,13 @@ class LauncherWindow(Adw.ApplicationWindow):
|
||||
page.add(self.patches_group)
|
||||
|
||||
screen = Adw.PreferencesGroup(title=tr("Экран"))
|
||||
# A size written by hand in bbport.ini (5120x2160, ...) stays a choice instead of 720p.
|
||||
output = self.ini.get("output_res", "1920x1080")
|
||||
outputs = OUTPUT_RES + ([(output.replace("x", "×"), output)]
|
||||
if output not in [value for _, value in OUTPUT_RES] else [])
|
||||
self.output_row = combo_row(tr("Разрешение вывода"),
|
||||
tr("Апскейлер дорисовывает кадр; Steam Deck — 720p"),
|
||||
OUTPUT_RES, self.ini.get("output_res", "1920x1080"))
|
||||
outputs, output)
|
||||
screen.add(self.output_row)
|
||||
self.live_row = combo_row(tr("Смена разрешения на лету"),
|
||||
tr("Без перезапуска, но медленнее на Steam Deck и старых GPU"),
|
||||
|
||||
@@ -7,6 +7,7 @@ image places eboot vaddr 0 at image offset 0. Only literal writes are supported
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -170,6 +171,14 @@ SCENE_WIDTH=0x02196A6B-EBOOT_BASE
|
||||
SCENE_HEIGHT=0x02196A7A-EBOOT_BASE
|
||||
UI_WIDTH=0x02358554-EBOOT_BASE
|
||||
UI_HEIGHT=0x0235855D-EBOOT_BASE
|
||||
# Outputs wider than 16:9: the community resolution patch for that size (Kyo's "Resolution Patch
|
||||
# WxH (21:9)" etc. in patches/Bloodborne.xml) also widens the game's logical screen -- the
|
||||
# projection aspect, the 2D layout width and the lock-on/HP-bar coordinates -- so the view
|
||||
# gets wider instead of stretched. bbport keeps its own scene/UI sizes (resolution_writes) and
|
||||
# heap patch, so only that patch's remaining writes are applied. bbport.ini aspect_patch=0
|
||||
# turns this off.
|
||||
HEAP_SITES={0x04B36DDA-EBOOT_BASE,0x04B36DE2-EBOOT_BASE}
|
||||
RESOLUTION_PATCH=re.compile(r'Resolution Patch (\d+)x(\d+) \(')
|
||||
|
||||
|
||||
def read_settings(path):
|
||||
@@ -242,6 +251,33 @@ def resolution_writes(xml,size,app_version,segments,ui=OUTPUT_SIZE):
|
||||
return out
|
||||
|
||||
|
||||
def aspect_patch_name(xml,app_version,output):
|
||||
"""The community resolution patch for an output wider than 16:9: the one for this exact
|
||||
size, else the closest-width one of the same aspect ratio. None for 16:9 and narrower
|
||||
outputs, and for an aspect ratio no patch covers (that output is still stretched)."""
|
||||
w,h=output
|
||||
if w*9<=h*16: return None
|
||||
same_aspect=[]
|
||||
for meta in ET.parse(xml).getroot().iter('Metadata'):
|
||||
m=RESOLUTION_PATCH.match(meta.get('Name',''))
|
||||
if not m or meta.get('AppVer')!=app_version: continue
|
||||
pw,ph=int(m[1]),int(m[2])
|
||||
if pw*9<=ph*16: continue
|
||||
if (pw,ph)==(w,h): return meta.get('Name')
|
||||
if abs(pw/ph-w/h)<0.005: same_aspect.append((abs(pw-w),meta.get('Name')))
|
||||
return min(same_aspect)[1] if same_aspect else None
|
||||
|
||||
|
||||
def aspect_writes(xml,output,app_version,segments):
|
||||
"""(patch name, writes) widening the game's view to the output's aspect, or (None, [])."""
|
||||
name=aspect_patch_name(xml,app_version,output)
|
||||
if not name: return None,[]
|
||||
template=dict(compile_patches(xml,[RESOLUTION_TEMPLATE],app_version,segments))
|
||||
skip={SCENE_WIDTH,SCENE_HEIGHT,UI_WIDTH,UI_HEIGHT}|HEAP_SITES
|
||||
return name,[(offset,data) for offset,data in compile_patches(xml,[name],app_version,segments)
|
||||
if offset not in skip and template.get(offset)!=data]
|
||||
|
||||
|
||||
def eboot_segments(elf):
|
||||
phoff,=struct.unpack_from('<Q',elf,0x20)
|
||||
phentsize,phnum=struct.unpack_from('<HH',elf,0x36)
|
||||
@@ -390,6 +426,13 @@ def main():
|
||||
writes+=compile_patches(a.xml,[heap],a.app_version,segments)
|
||||
names.append(heap)
|
||||
print(f'Patches: scene {size[0]}x{size[1]}; UI {ui[0]}x{ui[1]}')
|
||||
settings=read_settings(a.settings)
|
||||
output=tuple(int(v) for v in a.output_res.lower().split('x')) if a.output_res else output_size(settings)
|
||||
if settings.get('aspect_patch','1')!='0':
|
||||
aspect_name,aspect=aspect_writes(a.xml,output,a.app_version,segments)
|
||||
if aspect:
|
||||
writes+=aspect
|
||||
print(f'Patches: aspect {output[0]}x{output[1]} from {aspect_name!r} ({len(aspect)} writes)')
|
||||
if a.patches_dir:
|
||||
# After the built-in ones: an external patch of the same bytes wins.
|
||||
writes+=compile_external(external_selection(external_patches(a.patches_dir,a.app_version,a.xml),
|
||||
|
||||
+21
-1
@@ -66,6 +66,26 @@ int main() {
|
||||
s.show_fps = false;
|
||||
BbSettings::Save();
|
||||
assert(Read(path).find("menu_language=ru\n") != std::string::npos);
|
||||
|
||||
// An output size the menu does not list (deadinside28#183) survives a save...
|
||||
{
|
||||
FILE* f = std::fopen(path, "a");
|
||||
std::fputs("output_res=5120x2160\n", f);
|
||||
std::fclose(f);
|
||||
}
|
||||
BbSettings::Load();
|
||||
int width = 0, height = 0;
|
||||
assert(BbSettings::CustomOutput(width, height) && width == 5120 && height == 2160);
|
||||
s.show_fps = true;
|
||||
BbSettings::Save();
|
||||
assert(Read(path).find("output_res=5120x2160\n") != std::string::npos);
|
||||
// ...until a listed one is chosen.
|
||||
s.custom_output_width = 0;
|
||||
s.output_res = 2;
|
||||
BbSettings::Save();
|
||||
assert(Read(path).find("output_res=2560x1440\n") != std::string::npos);
|
||||
assert(!BbSettings::CustomOutput(width, height));
|
||||
unlink(path);
|
||||
std::puts("PASS: settings save keeps other keys, menu position, only a chosen menu language");
|
||||
std::puts("PASS: settings save keeps other keys, menu position, only a chosen menu language, "
|
||||
"a custom output size");
|
||||
}
|
||||
Reference in new issue
Block a user