Compare commits

..
Author SHA1 Message Date
ayozetr c1202bab21 Launcher: version 0.70-linux.6 2026-11-12 00:39:59 +00:00
ayozetr c435733825 Launcher: a shortcut in the applications menu or on the desktop
The AppImage had no menu entry unless an AppImage manager (Gear Lever,
AppImageLauncher) made one, and a checkout had only
launcher/install-desktop.sh. The Setup page now has a Shortcut row, under
Add to Steam, with two buttons: Applications menu writes a desktop entry
to $XDG_DATA_HOME/applications, Desktop to the desktop folder
(xdg-user-dir DESKTOP), executable and marked trusted for GNOME's file
manager. The entry starts the AppImage itself, or the checkout's
bb-launcher.sh, with the icon kept in the data folder as Add to Steam
keeps it. In every launcher language.

bbport_shortcut.py holds the entry (Exec quoted as the Desktop Entry
Specification wants) and tests/test_shortcut.py covers it.
getting-started.md mentions the row.
2026-11-12 00:39:52 +00:00
ayozetr 64acb09154 Credits: the reporters of #50 and #51
In docs/CREDITS.md and the launcher's About page, in every language:
brenki1 for FSR 4 stopping after lamp travels (#50), and shaneo88's entry
gains the camera that did not follow the character on a controller (#51).
2026-11-12 00:38:21 +00:00
ayozetrandAsanilo 5f4ae0ac12 Upscaler: the preset file's upscaler with a fourth field too
From Asanilo's pull request to bbport (deadinside28/bloodborne_pc#438,
fixing #437). A BB_PRESET_FILE request with a fourth field (the aspect
ratio) kept the previous upscaler: the provider was taken only when
exactly three fields were read. It is now taken from three on, with the
same range check. tests/test_preset_file.py covers three- and four-field
requests, invalid and partial input and the polling cadence.

Credits (docs/CREDITS.md and the launcher's About page, in every
language): The-Fool-cmd for the NixOS shells, and Asanilo's entry gains
this fix.

Co-Authored-By: Asanilo <[email protected]>
2026-11-12 00:37:38 +00:00
ayozetrandThe-Fool-cmd 79b71e6b16 Nix shells: DLSS and the launcher's folder chooser on NixOS
From The-Fool-cmd's pull request to bbport (deadinside28/bloodborne_pc#452),
for running from source with nix-shell on NixOS:

- shell.nix: NixOS keeps the driver's libraries in /run/opengl-driver/lib,
  where the DLSS bridge's dlopen of libnvidia-ngx.so.1 did not look, so DLSS
  never turned on ("Feature requirement query failed: 0xbad00012"). The
  directory joins LD_LIBRARY_PATH; it holds only driver libraries.
- launcher/shell.nix: the launcher gets the XDG_DATA_DIRS the packaged one
  has (packaging/default.nix): the icons for hosts without an Adwaita theme,
  and GTK4's schemas, without which the folder chooser aborted.

The AppImage and the packaged launcher already handled both.

Co-Authored-By: The-Fool-cmd <[email protected]>
2026-11-12 00:36:51 +00:00
ayozetr 93a8f8e2a5 Pad: keep the camera following the character on a controller
With mouse look on, the game's own camera auto-rotation (the camera
swinging behind the character as it moves) is switched off while the
window holds the mouse, because it fights a mouse turning the camera
(#352). The window takes the mouse whenever it has the focus, so a
player on a controller lost it too, in the sessions where the mouse had
been taken (#51, a PS4 controller with mouse look on).

It is now switched off only while the keyboard and mouse are what is
being played with: the last of the two to be used, as the button prompts
already tell them apart. Back on the controller, the camera follows the
character again.
2026-11-12 00:35:26 +00:00
ayozetr 2433441711 FSR 4: keep registering images after many area loads
The FSR 4 provider (FSR-Vulkan's v07 backend) keeps the images the port
registers each frame (color, depth, motion, output) in a table of eight
records matched by view, and never let one go. The port makes new views
whenever it recreates those images, at an area load or a resolution
change, so after a few lamp travels every registration failed with
VK_ERROR_OUT_OF_POOL_MEMORY ("external image registration failed") and
the upscaler stopped until it was turned off and on again (#50, an RX
9060 XT with FSR 4).

gpu/patches/fsr-vulkan/0005 keeps the table in order of use, a
registration moving its record to the end, and a full table gives up the
record unused the longest: the images of the frame being recorded are the
ones registered last. The five patches apply in order to a clean
submodule.
2026-11-12 00:33:23 +00:00
ayozetr 726d7b3760 Launcher: version 0.70-linux.5 2026-11-12 00:22:55 +00:00
ayozetr d68ff3987e Atlas: remove runtime_atlas_build.h, which nothing includes
The header came with Hunter's Atlas (6621a1f0) and holds the build
description of the Windows release it was taken from (windows-v1.8.1).
No source, script or test includes it or reads BB_ATLAS_BUILD_JSON; the
build and the tests are the same without it.
2026-11-09 22:28:27 +00:00
ayozetr 48fc69fb59 Patches: L3 run-jump, from bbhost; credits for the latest fixes
Kyo's L3 run-jump patch for 1.09, as droogie's bbhost carries it
(patches/l3-run-jump.toml, byte for byte, each site checked against the
eboot first). While running with Circle held, L3 makes the Rolling
request a Circle press makes, and the player's SpEffect 390 counts as on
for three frames, so the script takes its own run-jump: L3 jumps as
letting go of Circle and pressing it again does, and Circle is
unchanged. Its code goes in the zero tail of the code segment
(0x054BF000 and 0x054BF080, PS4 addresses) and keeps its window in one
byte of the zeroed tail of .bss (0x05AD3D00); it is RIP- and
register-relative, so it runs wherever the eboot is loaded. Not for the
Nightreign Movement scripts, where L3 is their surge sprint.

Off by default: tweak_l3_run_jump in bbport.ini, the launcher's Cheats
page and the in-game menu, in every language.

Credits (docs/CREDITS.md and the launcher's About page, in every
language): droogie's entry gains the L3 run-jump, and Asanilo (the work
submission count and sampler fixes), AlexanderDzhoganov (BBSC's
spectating after a wipe) and nullsumlabs (unknown sysctl names) are in.
2026-11-09 22:11:46 +00:00
ayozetr ce84be68dc Window: keep keys and typed text for the name dialog on the Deck
With a Steam Deck or Steam Controller as the gamepad, keyboard and mouse
events are dropped as echoes of its own controls (Steam's desktop
layout), except while the port's menu is open. That also dropped them
while the game's text dialog was open: Steam's on-screen keyboard types
there as text input and a keyboard plugged into the Deck as keys, so in
Game Mode the character's name could not be typed and the dialog could
not be left with Escape (upstream issue #443, and a report on Reddit).
They now reach the dialog; the port's own on-screen keyboard still takes
the gamepad. BbOverlay::TextPromptOpen says when the dialog is open.
2026-11-09 22:07:00 +00:00
ayozetr 61a3c3f4a7 Docs: frame rate drops until a reload, and the mouse in menus
TROUBLESHOOTING.md gains two sections: the drops on NVIDIA cards that
lasted until a reload (#42), what 0.70-linux.4 changed, how to tell them
from a place that is simply heavy, BB_MIRROR_VRAM_ONLY and which log to
attach; and the keyboard and mouse in the game's menus: the left button
chooses, the others keep their bindings (the right one L2, for the
storage box), Escape goes back.
2026-11-09 22:04:38 +00:00
ayozetr 23191f2a81 Overlay: say how much VRAM other programs took, not "VRAM -552 MB"
The frame rate line adds how much less video memory the game has than
earlier in the session, when other programs (a browser, a video player)
take 150 MB or more. It read "VRAM -552 MB (other apps)", which looked
like a negative figure. It now reads "VRAM: 552 MB taken by other
apps", in every menu language.
2026-11-09 22:04:14 +00:00
ayozetr 65e1a94db1 Exit to desktop: notice the title through the game's menu flow
With online play on, the game came back to the title after Exit Game
without opening the intro logos or looking its title menu's texts up
again, the two things the port waited for, and closed only when a new
game was started from there (#46). The menu pointer already reads the
game's menu flow, whose +0xb11 is zero at the title menu: the change
from the world to the title now calls runtime_quit_at_title too, which
does nothing unless Exit to desktop is armed. The menu pointer's header
now says the right button keeps its binding in menus.
2026-11-09 22:04:14 +00:00
ayozetr 614ac83cb1 Kernel: answer an unknown sysctl name with ENOENT
A sysctl name the port does not know stopped the game (exit 21). It now
answers ENOENT, as FreeBSD does for a name it does not know, and logs the
first few with their mib. From the nullsumlabs fork
(bloodborne_pc-deck-optimized), which took it from Awakenchan's Windows
port.
2026-11-09 21:40:56 +00:00
ayozetrandAlexander Dzhoganov 5f0580bd43 BBSC: end a guest's spectating when the host reloads after a wipe
From Alexander Dzhoganov's pull request to bbport
(deadinside28/bloodborne_pc#418, refs CLSAMG22/BBSC#44). After a boss
wipe the host respawned while the guest stayed a spectator, its area
unloaded, seeing only the host's character moving in the air. The life
packets can miss a wipe when the host was already dead as the guest's
death began, or wakes in another map.

The host's position reports are now a second signal: one that arrives
by a load after the guest went down, with the boss not killed, means
the host reloaded. The guest reloads into the host's reported map when
it differs from its own, applying the host's world state first, as a
follow does.

Co-Authored-By: Alexander Dzhoganov <[email protected]>
2026-11-09 21:35:35 +00:00
ayozetrandAsanilo f16e9dec02 Texture cache: free evicted samplers after the frame that uses them
From Asanilo's pull request to bbport (deadinside28/bloodborne_pc#442,
fixing #441). The sampler cache's first collection pass could pick
entries just bound for the frame being recorded, and destroying them at
once left render commands pointing at a destroyed sampler. Evicted
samplers now go through Scheduler::DeferOperation and live until the
current render tick completes; their cache entries and LRU slots are
still removed at once.

tests/test_sampler_gc.py covers the first collection, entries used after
warm-up, cold entries and repeated use.

Co-Authored-By: Asanilo <[email protected]>
2026-11-09 21:35:34 +00:00
ayozetrandAsanilo 6f49316ca1 Command processor: publish submission counts under the queue lock
From Asanilo's pull request to bbport (deadinside28/bloodborne_pc#440,
fixing #439). SubmitGfx and SubmitAsc made a queued task visible before
updating its count and generation, so the decoder could finish the
previous task and see zero submissions while the new one was still
queued, and an idle callback in that window could accept the old
generation. The count, generation and their flags are now updated while
the queue lock is held; notifications stay outside the locks, and the
lock order (queue, then submit) is the consumer's.

tests/test_submission_publish.py covers the ordering.

Co-Authored-By: Asanilo <[email protected]>
2026-11-09 21:35:33 +00:00
33 changed files with 1344 additions and 60 deletions

No files matched your search

+7 -2
View File
@@ -49,7 +49,11 @@ Obrikash and noelmiller. What each one did:
motion vectors without NaN.
- **[Classmg22](https://github.com/Classmg22)** (CLS_AMG): BBSC, Seamless Co-op.
- **[Wozzardman](https://github.com/Wozzardman)**: the server for a group's own co-op, and BBSC's seamless summon.
- **[droogie](https://github.com/droogie)**: The Hunter's Dream server, and fast loading (bbhost).
- **[droogie](https://github.com/droogie)**: The Hunter's Dream server, fast loading, and the L3 run-jump patch (Kyo's) as bbhost carries it.
- **[Asanilo](https://github.com/Asanilo)**: two GPU fixes: work submission counts, and samplers freed after the frame that uses them; and the preset file's upscaler with four fields.
- **[AlexanderDzhoganov](https://github.com/AlexanderDzhoganov)**: BBSC: the guest no longer stays a spectator after a boss wipe.
- **[nullsumlabs](https://github.com/nullsumlabs)**: an unknown system value (sysctl) no longer stops the game.
- **[The-Fool-cmd](https://github.com/The-Fool-cmd)**: DLSS and the launcher's folder chooser when running from source on NixOS.
- **[fhody125-web](https://github.com/fhody125-web)**: NoDreamForHunter, the base of BBSC's game server.
- **[MikeTat1988](https://github.com/MikeTat1988)**: Hunter's Atlas, the fullscreen in-game map.
- **[IFreemz](https://github.com/IFreemz)**: the FSR 4 model without trails.
@@ -95,7 +99,8 @@ Obrikash and noelmiller. What each one did:
- **[bruxis](https://github.com/bruxis)**: the log that showed why Steam Input did not reach the game.
- **[shauryasagar](https://github.com/shauryasagar)** and **[ZeroSTF](https://github.com/ZeroSTF)**: the Hunter's Dream going black after long sessions.
- **[popcar2](https://github.com/popcar2)**: ideas for the launcher.
- **[shaneo88](https://github.com/shaneo88)**: shadPS4's patch file in the patches folder.
- **[shaneo88](https://github.com/shaneo88)**: shadPS4's patch file in the patches folder, and the camera not following the character on a controller.
- **[brenki1](https://github.com/brenki1)**: FSR 4 stopping after lamp travels.
- **[n3vrs0ft](https://github.com/n3vrs0ft)**: the "Transaction failed" crash on Seamless Hunt, the logs that stopped a BBSC guest from following a stranger's world, and testing BBSC 0.0.2 with a Windows player.
- **[cappp](https://github.com/cappp)**: the "Transaction failed" crash on Seamless Hunt.
- **[simo1730](https://github.com/simo1730)**: the Trophies page not finding the dump's files.
+21
View File
@@ -51,6 +51,27 @@ On a 6 GB card the texture cache can run out of video memory: the log then repea
a lower output resolution, lower **Model detail** in the game's *Display* settings, and close
other programs that use the GPU (browsers, video players, other games).
## The frame rate drops in some places and stays low until a reload
Seen mostly on NVIDIA cards (#42): the game runs at its usual rate, then in some places drops
(to 17-35 FPS) and stays there until a lamp travel or a restart. When video memory was full, a
copy of the game's memory could land in system memory and stay there, and the GPU read it over
the bus. From 0.70-linux.4, on cards with more than 4 GB those copies stay in video memory, and
one that does not fit is made again later; the log says so at start (`Layer memory: mirrors in
VRAM only`). `BB_MIRROR_VRAM_ONLY=0` under **Advanced → Developer → Extra variables** brings the
old behaviour back, and `=1` turns it on for a card of 4 GB or less.
A place that is simply heavy is different: it is slower from the moment you get there, and the
same after a reload. If a drop stays until a reload, turn on **Save the log and statistics to a
file** and attach the log of that whole session to the issue.
## Keyboard and mouse in the game's menus
In the game's menus the mouse points at items and the **left button** chooses them. The other
buttons keep what they do in play, as the game's own prompts show: the **right button** is L2
(the storage box switches to the storage with it), the side buttons R2 and L1. **Escape** goes
back.
## The picture is stretched on an ultrawide screen
Choose the screen's shape in **Graphics → Display → Aspect ratio** (*Auto* follows the output
+4
View File
@@ -18,6 +18,10 @@
3. In the launcher, choose your **game folder**: the one with `eboot.bin` in it.
4. Press **Play**.
The launcher can put itself in your applications menu or on your desktop: **Shortcut**, under
**Add to Steam** in its settings (an AppImage manager such as Gear Lever does the same for the
menu).
!!! tip "The first minutes"
The first time, the game builds its shaders as it meets them, so it can stutter for a few
minutes. The next sessions start with them ready.
+54 -4
View File
@@ -8535,7 +8535,31 @@ void TickBbscSpectate() {
LOG_INFO(Debug, "BBSC spectate: a partner said dead and then alive while we were down - "
"it reloaded after a wipe, the guest follows");
}
if (session_alive && LivingPartnerExists()) {
// Second wipe signal: the host's own position reports. It says nothing while it loads, so a
// report after a silence means it came out of a load while we were down, with the boss not
// killed: it reloaded after a wipe. The life packets above can miss this (the host was already
// dead when our death began, or it woke in another map), which left the guest spectating until
// the safety limit, with its world unloaded around it (BBSC issue #44).
BbscFollowMessage host_report{};
{
std::scoped_lock lock{bbsc_follow_mutex};
host_report = bbsc_follow_latest;
}
const bool host_report_fresh =
host_report.received_ms != 0 && now - host_report.received_ms < BbscFollowStaleMs;
const bool host_came_back = session_state == 6 && host_report_fresh &&
host_report.arrival_by_load &&
host_report.arrival_ms >= bbsc_spectate_since_ms &&
bbsc_boss_killed_ms < bbsc_spectate_since_ms &&
!BbscAllPartnersReportDead();
if (host_came_back && !bbsc_spectate_saw_wipe) {
bbsc_spectate_saw_wipe = true;
BBSC_TIMELINE("SPECTATE", "the host came out of a load in map {:#x} while we were down - "
"the guest follows",
host_report.packed_map);
}
// A host that woke in another map has no character here, so LivingPartnerExists can't see it.
if (session_alive && (LivingPartnerExists() || host_came_back)) {
std::string_view respawn_why;
const u64 chr = GetLocalPlayerCharacter();
if (bbsc_death_mode == BbscDeathMode::RespawnNow) {
@@ -8558,13 +8582,39 @@ void TickBbscSpectate() {
? real_host
: FindLivingPartnerCharacter();
const auto host_transform = ReadCharacterTransform(host);
if (host_transform.has_value()) {
SummonPlacementDescriptor to{};
to.packed_region = GetCurrentPackedMap();
const u32 here = GetCurrentPackedMap();
SummonPlacementDescriptor to{};
bool have_target = false;
if (host_report_fresh && host_report.packed_map != here) {
// The host's coordinates only mean something in its own map: load that map, with
// its world state, as a follow does.
to.packed_region = host_report.packed_map;
to.x = host_report.x + 1.0f;
to.y = host_report.y;
to.z = host_report.z;
to.heading = host_report.heading;
have_target = true;
if (BbscHostWorldFlagsReady(to.packed_region)) {
ApplyBbscHostWorldFlags(to.packed_region, "before the wipe load");
}
ApplyBbscHostTime("before the wipe load");
ApplyBbscGuestLootView(to.packed_region, "before the wipe load");
if (to.packed_region == BbscDreamStateMap) {
ApplyBbscHostDreamState("before the wipe load");
} else {
RestoreBbscOwnDreamState("following the host after the wipe");
}
} else if (host_transform.has_value()) {
to.packed_region = here;
to.x = host_transform->position[0] + 1.0f;
to.y = host_transform->position[1];
to.z = host_transform->position[2];
to.heading = host_transform->rotation[1];
have_target = true;
}
if (have_target) {
BBSC_TIMELINE("SPECTATE", "wipe: reloading beside the host in map {:#x}",
to.packed_region);
if (RequestBbscSessionWarp(GetGlobalState(), to, "wipe")) {
return; // the load builds a new character: "a load ended the boss fight"
}
+12 -10
View File
@@ -664,12 +664,14 @@ void CommandProcessor::SubmitGfx(std::span<const u32> dcb, std::span<const u32>
BbFreeCheck::NoteSubmit(work->seq, work->copy ? work->copy->guest_dcb : dcb.data(),
dcb.size_bytes());
queue.works.push_back(std::move(work));
// Visible only once counted: the decoder reads this queue under the same lock,
// so the count it tests must be published before the task can be seen.
std::scoped_lock submit_lock{submit_mutex};
++num_submits;
++submissions_total;
BbTimeline::Note(BbTimeline::GuestSubmit, 0, submissions_total);
work_retired = false;
}
std::scoped_lock lock{submit_mutex};
++num_submits;
++submissions_total;
BbTimeline::Note(BbTimeline::GuestSubmit, 0, submissions_total);
work_retired = false;
submit_cv.notify_one();
}
@@ -684,12 +686,12 @@ void CommandProcessor::SubmitAsc(u32 gnm_vqid, std::span<const u32> acb) {
std::scoped_lock lock{queue.mutex};
work->stamp = BbStats::guest_submit_stamp.fetch_add(1, std::memory_order_acq_rel) + 1;
queue.works.push_back(std::move(work));
std::scoped_lock submit_lock{submit_mutex};
num_mapped_queues = std::max(num_mapped_queues, gnm_vqid + 1);
++num_submits;
++submissions_total;
work_retired = false;
}
std::scoped_lock lock{submit_mutex};
num_mapped_queues = std::max(num_mapped_queues, gnm_vqid + 1);
++num_submits;
++submissions_total;
work_retired = false;
submit_cv.notify_one();
}
+20
View File
@@ -46,6 +46,9 @@
#include "bbport_toggles.h"
#include "common/types.h"
// bb-probe (runtime_services.c); weak: the GPU library's tests link without it.
extern "C" void runtime_quit_at_title(void) __attribute__((weak));
namespace BbMenuPointer {
namespace {
@@ -437,6 +440,12 @@ bool InputBlocked() {
return block && Get<std::uint32_t>(block + 0x2e4);
}
/// The game's title menu (the menu flow's +0xb11 zero), as InMenu reads it.
bool AtTitle() {
const Addr flow = Get<Addr>(At(MenuFlow));
return flow && !Get<std::uint8_t>(flow + 0xb11, 1);
}
bool InMenu() {
const Addr flow = Get<Addr>(At(MenuFlow));
const bool title = flow && !Get<std::uint8_t>(flow + 0xb11, 1);
@@ -705,6 +714,17 @@ void ReadTestFile() {
/// The pad state the game reads (pad thread): the pointer's presses, each held long enough for
/// the game to see it, with a gap after.
void OnPad(std::uint32_t* buttons, const std::uint8_t* sticks) {
// Exit to desktop (runtime_services.c): the game going from the world to its title menu is the
// title it waits for. Issue #46: with online play on, the game came back to the title without
// opening the intro logos or looking its title menu's texts up again, and closed only when a
// new game was started from there. Only the change counts (the flag is also set at start);
// runtime_quit_at_title does nothing unless Exit to desktop is armed.
static bool was_title = true;
const bool title = AtTitle();
if (title && !was_title && runtime_quit_at_title) {
runtime_quit_at_title();
}
was_title = title;
const auto off_centre = [](std::uint8_t v) { return v < 64 || v > 192; };
pad_steering.store((*buttons & (Up | Right | Down | Left)) || off_centre(sticks[0]) ||
off_centre(sticks[1]),
+2 -2
View File
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Bloodborne (1.09): the mouse as a pointer in the game's own menus (Scaleform lists: the title
// menu, the inventory, the shops, the options). The item under the pointer gets the game's cursor,
// a click presses the confirm button on it (or the slider's arrow it is on), the right button goes
// back, the wheel moves up and down. bloodborne_menu_pointer.cpp has the reverse engineering.
// a click presses the confirm button on it (or the slider's arrow it is on), the other buttons keep
// their bindings (the right one L2), the wheel moves up and down. bloodborne_menu_pointer.cpp has the reverse engineering.
#pragma once
namespace BbMenuPointer {
@@ -0,0 +1,46 @@
From: bbport
Subject: [PATCH] FSR 4 v07: a full external image table gives up its least used record
External image records are matched by view and were never removed, and the
table holds eight. The caller makes new views whenever it recreates its
images (an area load, a resolution change), so after a few of those every
ffxFsr4VkSetExternalImageState failed with VK_ERROR_OUT_OF_POOL_MEMORY and
the upscaler stopped (bbport Linux #50). The table is now kept in order of
use, a registration moving its record to the end, and a full table drops
the record unused the longest: the images of the frame being recorded are
the ones registered last.
---
diff --git a/src/ffx_vk_fsr4_v07.c b/src/ffx_vk_fsr4_v07.c
--- a/src/ffx_vk_fsr4_v07.c
+++ b/src/ffx_vk_fsr4_v07.c
@@ -2013,14 +2013,26 @@
return VK_ERROR_VALIDATION_FAILED_EXT;
FfxFsr4VkContext* c = (FfxFsr4VkContext*)iface->device;
+ /* bbport: the table is kept in order of use, the record registered last at
+ * its end, and a full table gives up the record unused the longest.
+ * Records are matched by view and were never removed, and the caller makes
+ * new views whenever it recreates its images (an area load, a resolution
+ * change): after a few of those every registration failed with
+ * VK_ERROR_OUT_OF_POOL_MEMORY and the upscaler stopped. The images of the
+ * frame being recorded are the ones registered last. */
for (uint32_t i = 0; i < c->externalImageCount; ++i) {
if (c->externalImages[i].state.view == state->view) {
- c->externalImages[i].state = *state;
+ memmove(&c->externalImages[i], &c->externalImages[i + 1],
+ (c->externalImageCount - i - 1) * sizeof(c->externalImages[0]));
+ c->externalImages[c->externalImageCount - 1].state = *state;
return VK_SUCCESS;
}
}
- if (c->externalImageCount >= MAX_EXTERNAL_IMAGES)
- return VK_ERROR_OUT_OF_POOL_MEMORY;
+ if (c->externalImageCount >= MAX_EXTERNAL_IMAGES) {
+ memmove(&c->externalImages[0], &c->externalImages[1],
+ (MAX_EXTERNAL_IMAGES - 1) * sizeof(c->externalImages[0]));
+ --c->externalImageCount;
+ }
c->externalImages[c->externalImageCount++].state = *state;
return VK_SUCCESS;
}
+4
View File
@@ -16,3 +16,7 @@ patches here to its working tree when they are not applied yet:
- `0004-...`: `aspectMask` in `FfxFsr4VkExternalImageState` (0 = color). The layout barriers of a
registered external image named the color aspect, which is invalid for a depth image (the
Linux fork's 0002).
- `0005-...`: the FSR 4 provider's table of external images (eight, matched by view) never let
a record go, and the port makes new views whenever it recreates its images: after a few area
loads every registration failed with `VK_ERROR_OUT_OF_POOL_MEMORY` and FSR 4 stopped (#50).
The table is kept in order of use and a full one drops the record unused the longest.
+14 -7
View File
@@ -759,6 +759,13 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
"Ragdoll-Physik wie in Dark Souls",
"Física ragdoll de Dark Souls",
"黑暗之魂的布娃娃物理"}},
{"L3 jumps while running",
{"L3 salta al correr",
"L3 salta durante la corsa",
"L3 saute en courant",
"L3 springt beim Rennen",
"L3 pula ao correr",
"奔跑时按 L3 跳跃"}},
// The game's System menu pages (bbport_game_menu.cpp).
{"Display",
{"Imagen",
@@ -1069,13 +1076,13 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
"%.0f FPS %.1f ms",
"%.0f FPS %.1f ms",
"%.0f FPS %.1f 毫秒"}},
{" VRAM -%u MB (other apps)",
{" VRAM -%u MB (otras aplicaciones)",
" VRAM -%u MB (altre app)",
" VRAM -%u Mo (autres applications)",
" VRAM -%u MB (andere Programme)",
" VRAM -%u MB (outros apps)",
" VRAM -%u MB(其他程序)"}},
{" VRAM: %u MB taken by other apps",
{" VRAM: %u MB ocupados por otras aplicaciones",
" VRAM: %u MB occupati da altre app",
" VRAM : %u Mo pris par d'autres applications",
" VRAM: %u MB von anderen Programmen belegt",
" VRAM: %u MB ocupados por outros apps",
" 显存:其他程序占用 %u MB"}},
{"Bloodborne - Graphics (Insert / hold L3+R3)###bbport_settings",
{"Bloodborne - Gráficos (Insert / mantener L3+R3)###bbport_settings",
"Bloodborne - Grafica (Insert / tieni premuto L3+R3)###bbport_settings",
+8 -2
View File
@@ -313,11 +313,13 @@ std::string FpsText() {
std::snprintf(text, sizeof(text), BbSettings::MenuText("%.0f FPS %.1f ms", "%.0f FPS %.1f мс"),
shown, frame_ms_avg);
}
// Other programs took video memory from the game (BufferCache::VramPromotionsPaused).
// Other programs took video memory from the game (BufferCache::VramPromotionsPaused): how much
// less the budget is than the most it was this session. It read "VRAM -552 MB", which looked
// like a negative figure.
if (const u32 lost = BbStats::vram_budget_lost_mb.load(std::memory_order_relaxed); lost >= 150) {
const size_t used = std::strlen(text);
std::snprintf(text + used, sizeof(text) - used,
BbSettings::MenuText(" VRAM -%u MB (other apps)", " VRAM −%u МБ (другие программы)"),
BbSettings::MenuText(" VRAM: %u MB taken by other apps", " VRAM: %u МБ заняты другими программами"),
lost);
}
return text;
@@ -1396,6 +1398,10 @@ bool MenuOpen() {
return menu_open || (AtlasOn() && initialized && BbAtlasMenu::Interacting());
}
bool TextPromptOpen() {
return prompt_active.load(std::memory_order_relaxed);
}
bool CapturesInput() {
// The text dialog too: keys typed into it (Backspace is the touchpad) stay out of the game.
// Hunter's Atlas: its open map and its controller shortcut while it is being held.
+2
View File
@@ -51,6 +51,8 @@ uint32_t RoutePad(uint32_t buttons, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t
/// The settings menu is open, or Hunter's Atlas's map or quick-menu tile uses the pointer: the
/// window lets the mouse go and shows the system cursor over it.
bool MenuOpen();
/// Any thread: the game's text dialog (ImeDialog, the character's name) is open.
bool TextPromptOpen();
/// Window thread: the game's text dialog (ImeDialog) state, drawn as a box over the frame.
void SetTextPrompt(bool active, const std::string& prompt, const std::string& text);
+1
View File
@@ -84,6 +84,7 @@ inline constexpr Effect Effects[] = {
{"tweak_no_camera_rotation", "No camera auto-rotation", "Без автоповорота камеры", false},
{"tweak_easy_run", "Run with less stick tilt", "Бег с меньшим наклоном стика", false},
{"tweak_ragdoll", "Dark Souls ragdoll physics", "Физика тел как в Dark Souls", false},
{"tweak_l3_run_jump", "L3 jumps while running", "L3: прыжок на бегу", false},
};
inline constexpr int EffectCount = int(sizeof(Effects) / sizeof(Effects[0]));
/// The game's own anti-aliasing effect (Effects index), derived from anti_aliasing.
+7 -2
View File
@@ -27,7 +27,11 @@ std::array<std::atomic<BbGpuEventFilter>, 4> event_filters{};
extern "C" int runtime_pad_steam_only(void) __attribute__((weak));
/// A keyboard or mouse event the game must not take (runtime_pad_steam_only): the gamepad's own
/// presses again. The port's menu (the overlay) keeps them, the trackpad as its pointer.
/// presses again. The port's menu (the overlay) keeps them, the trackpad as its pointer, and so
/// does the game's text dialog (the character's name): Steam's on-screen keyboard types into it
/// as text input and a keyboard plugged into the Deck as keys, and both were dropped there, so the
/// name could not be typed in Game Mode (upstream issue #443). The port's own on-screen keyboard
/// (bbport_overlay.cpp) still takes the gamepad.
bool SteamPadEcho(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_KEY_DOWN:
@@ -37,7 +41,8 @@ bool SteamPadEcho(const SDL_Event& event) {
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL:
return runtime_pad_steam_only && runtime_pad_steam_only() && !BbOverlay::MenuOpen();
return runtime_pad_steam_only && runtime_pad_steam_only() && !BbOverlay::MenuOpen() &&
!BbOverlay::TextPromptOpen();
default:
return false;
}
+6 -1
View File
@@ -426,8 +426,13 @@ void TextureCache::GarbageCollectSamplers() {
return true;
}
--deletions;
sampler_lru.Free(samplers.at(key).lru_id);
auto& sampler = samplers.at(key);
const auto lru_id = sampler.lru_id;
// LRU age can select a sampler still referenced by render work (including age zero).
// Retain its owner through the current scheduler tick before destroying the VkSampler.
scheduler.DeferOperation([retired = std::move(sampler)] { (void)retired; });
samplers.erase(key);
sampler_lru.Free(lru_id);
return false;
};
configure(false);
+1 -1
View File
@@ -210,7 +210,7 @@ bool TemporalUpscaler::OnFrameStart() {
if (fields >= 2 && output >= 0 && output < outputs.count) {
BbSettings::Get().output_res.store(BbSettings::PackSize(outputs.sizes[output]));
}
if (fields == 3 && provider >= 0 && provider < BbSettings::UpscalerCount) {
if (fields >= 3 && provider >= 0 && provider < BbSettings::UpscalerCount) {
BbSettings::Get().upscaler.store(provider);
}
std::fclose(f);
+112 -14
View File
@@ -619,6 +619,8 @@ EN = {
"Бег примерно с 70% наклона стика вместо 90%": "Run from about 70% stick tilt instead of 90%",
"Физика тел как в Dark Souls": "Dark Souls ragdoll physics",
"Тела и предметы разлетаются от ударов": "Corpses and objects fly around when hit",
'L3: прыжок на бегу': 'L3 jumps while running',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': "While running with Circle held, L3 jumps as pressing Circle again does. Kyo's patch (from bbhost)",
# Add to Steam
"Добавить в Steam": "Add to Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "From the Steam library the game starts straight away, with this launcher's settings",
@@ -631,6 +633,13 @@ EN = {
"Steam не закрылся. Закройте его сами и попробуйте ещё раз": "Steam did not close. Close it yourself and try again",
"Не удалось добавить игру в Steam: {}": "Couldn't add the game to Steam: {}",
"Bloodborne добавлен в библиотеку Steam": "Bloodborne was added to your Steam library",
'Ярлык': 'Shortcut',
'Лаунчер в меню приложений или на рабочем столе': 'The launcher in the applications menu or on the desktop',
'Меню приложений': 'Applications menu',
'Рабочий стол': 'Desktop',
'Не удалось создать ярлык: {}': 'Could not create the shortcut: {}',
'Ярлык создан на рабочем столе': 'Shortcut created on the desktop',
'Bloodborne добавлен в меню приложений': 'Bloodborne added to the applications menu',
# Online page
"Играть онлайн": "Play online",
"Игровой сервер (сообщения, пятна крови, призраки)": "Game server (messages, bloodstains, ghosts)",
@@ -864,7 +873,11 @@ EN = {
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'The interface vanishing outside 1080p, motion vectors for the large beasts and a memory fault',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Crashes when loading some maps, the gestures menu from the keyboard, and image changes for the upscaler',
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': "The on-screen keyboard for the character's name, the shader compiling notice and compacting the memory copies in VRAM",
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Fast loading after a death or travel (bbhost) and The Hunter's Dream server",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Fast loading after a death or travel, L3 run-jump (bbhost) and The Hunter's Dream server",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Two GPU fixes: work submission counts, and samplers freed after their frame',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: the guest no longer stays a spectator after a boss wipe',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'An unknown system value (sysctl) no longer stops the game',
'DLSS и выбор папки в лаунчере при запуске из исходников на NixOS': "DLSS and the launcher's folder chooser when running from source on NixOS",
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'The online play libraries and the server for your own co-op',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'The groundwork for DLSS, and the FSR 4 model without trails behind characters',
'Апскейлер FSR 3.1': 'The FSR 3.1 upscaler',
@@ -886,7 +899,8 @@ EN = {
'Журнал, показавший, почему Steam Input не доходил до игры': 'The log that showed why Steam Input did not reach the game',
'Чёрный Сон Охотника после долгой игры': "The Hunter's Dream going black after long sessions",
'«Выход на рабочий стол» при игре по сети и идеи для лаунчера': 'Quit to Desktop with online play on, and ideas for the launcher',
'Файл патчей shadPS4 в папке патчей': "shadPS4's patch file in the patches folder",
'Файл патчей shadPS4 в папке патчей и камера, которая не следовала за персонажем с геймпадом': "shadPS4's patch file in the patches folder, and the camera not following the character on a controller",
'FSR 4, переставший работать после перемещений у ламп': 'FSR 4 stopping after lamp travels',
'Вылет «Transaction failed» на Seamless Hunt': 'The "Transaction failed" crash on Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'The "Transaction failed" crash on Seamless Hunt, the logs that stopped a BBSC guest from following a stranger\'s world, and testing BBSC 0.0.2 with a Windows player',
'Страница трофеев, не находившая файлы дампа': "The Trophies page not finding the dump's files",
@@ -1533,6 +1547,8 @@ PT_BR = {
"Бег примерно с 70% наклона стика вместо 90%": "Corre com cerca de 70% de inclinação do analógico em vez de 90%",
"Физика тел как в Dark Souls": "Física de corpos como em Dark Souls",
"Тела и предметы разлетаются от ударов": "Corpos e objetos voam ao serem atingidos",
'L3: прыжок на бегу': 'L3 pula ao correr',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Correndo com o círculo pressionado, L3 pula como apertar o círculo de novo. Patch de Kyo (do bbhost)',
# Add to Steam
"Добавить в Steam": "Adicionar ao Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Na biblioteca do Steam o jogo inicia direto, com as configurações deste launcher",
@@ -1545,6 +1561,13 @@ PT_BR = {
"Steam не закрылся. Закройте его сами и попробуйте ещё раз": "O Steam não fechou. Feche-o você mesmo e tente de novo",
"Не удалось добавить игру в Steam: {}": "Não foi possível adicionar o jogo ao Steam: {}",
"Bloodborne добавлен в библиотеку Steam": "Bloodborne foi adicionado à sua biblioteca do Steam",
'Ярлык': 'Atalho',
'Лаунчер в меню приложений или на рабочем столе': 'O launcher no menu de aplicativos ou na área de trabalho',
'Меню приложений': 'Menu de aplicativos',
'Рабочий стол': 'Área de trabalho',
'Не удалось создать ярлык: {}': 'Não foi possível criar o atalho: {}',
'Ярлык создан на рабочем столе': 'Atalho criado na área de trabalho',
'Bloodborne добавлен в меню приложений': 'Bloodborne adicionado ao menu de aplicativos',
# Online page
"Играть онлайн": "Jogar online",
"Игровой сервер (сообщения, пятна крови, призраки)": "Servidor do jogo (mensagens, manchas de sangue, fantasmas)",
@@ -1954,7 +1977,11 @@ PT_BR = {
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'A interface que sumia fora de 1080p, vetores de movimento das grandes feras e uma falha de memória',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Travamentos ao carregar alguns mapas, o menu de gestos pelo teclado e mudanças de imagem para o upscaler',
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'O teclado na tela para o nome do personagem, o aviso de compilação de shaders e a compactação das cópias de memória na VRAM',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Carregamento rápido após morrer ou viajar (bbhost) e o servidor The Hunter's Dream",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Carregamento rápido após morrer ou viajar, pulo com L3 correndo (bbhost) e o servidor The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Duas correções da GPU: contagem do trabalho enviado e samplers liberados após o seu quadro',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: o convidado não fica mais como espectador após uma derrota no chefe',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Um valor de sistema desconhecido (sysctl) não para mais o jogo',
'DLSS и выбор папки в лаунчере при запуске из исходников на NixOS': 'DLSS e o seletor de pastas do launcher ao rodar do código-fonte no NixOS',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'As bibliotecas do jogo online e o servidor para o seu próprio cooperativo',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'A base do suporte a DLSS e o modelo do FSR 4 sem rastros atrás dos personagens',
'Апскейлер FSR 3.1': 'O upscaler FSR 3.1',
@@ -1976,7 +2003,8 @@ PT_BR = {
'Журнал, показавший, почему Steam Input не доходил до игры': 'O log que mostrou por que o Steam Input não chegava ao jogo',
'Чёрный Сон Охотника после долгой игры': 'O Sonho do Caçador ficando preto depois de sessões longas',
'«Выход на рабочий стол» при игре по сети и идеи для лаунчера': '"Sair para a área de trabalho" com o jogo online e ideias para o launcher',
'Файл патчей shadPS4 в папке патчей': 'O arquivo de patches do shadPS4 na pasta de patches',
'Файл патчей shadPS4 в папке патчей и камера, которая не следовала за персонажем с геймпадом': 'O arquivo de patches do shadPS4 na pasta de patches e a câmera que não seguia o personagem no controle',
'FSR 4, переставший работать после перемещений у ламп': 'O FSR 4 parando após viagens pelas lâmpadas',
'Вылет «Transaction failed» на Seamless Hunt': 'O travamento "Transaction failed" no Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'O crash "Transaction failed" no Seamless Hunt, os logs que fizeram o convidado do BBSC parar de seguir o mundo de um estranho e os testes do BBSC 0.0.2 com um jogador no Windows',
'Страница трофеев, не находившая файлы дампа': 'A página de troféus que não encontrava os arquivos do dump',
@@ -2628,6 +2656,8 @@ ES = {
"Бег примерно с 70% наклона стика вместо 90%": "Correr a partir de un 70 % de inclinación del stick en lugar del 90 %",
"Физика тел как в Dark Souls": "Física ragdoll de Dark Souls",
"Тела и предметы разлетаются от ударов": "Los cadáveres y los objetos salen volando al recibir golpes",
'L3: прыжок на бегу': 'L3 salta al correr',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Corriendo con el círculo pulsado, L3 salta como volver a pulsar el círculo. Parche de Kyo (de bbhost)',
# Add to Steam
"Добавить в Steam": "Añadir a Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Desde la biblioteca de Steam el juego arranca directamente, con los ajustes de este launcher",
@@ -2640,6 +2670,13 @@ ES = {
"Steam не закрылся. Закройте его сами и попробуйте ещё раз": "Steam no se ha cerrado. Ciérralo tú y vuelve a intentarlo",
"Не удалось добавить игру в Steam: {}": "No se pudo añadir el juego a Steam: {}",
"Bloodborne добавлен в библиотеку Steam": "Bloodborne se ha añadido a tu biblioteca de Steam",
'Ярлык': 'Acceso directo',
'Лаунчер в меню приложений или на рабочем столе': 'El launcher en el menú de aplicaciones o en el escritorio',
'Меню приложений': 'Menú de aplicaciones',
'Рабочий стол': 'Escritorio',
'Не удалось создать ярлык: {}': 'No se pudo crear el acceso directo: {}',
'Ярлык создан на рабочем столе': 'Acceso directo creado en el escritorio',
'Bloodborne добавлен в меню приложений': 'Bloodborne añadido al menú de aplicaciones',
# Online page
"Играть онлайн": "Jugar online",
"Игровой сервер (сообщения, пятна крови, призраки)": "Servidor del juego (mensajes, manchas de sangre, fantasmas)",
@@ -2959,7 +2996,11 @@ ES = {
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'La interfaz que desaparecía fuera de 1080p, los vectores de movimiento de las bestias grandes y un fallo de memoria',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Cierres al cargar algunos mapas, el menú de gestos desde el teclado y cambios de imagen para el escalador',
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'El teclado en pantalla para el nombre del personaje, el aviso de compilación de shaders y la compactación de las copias de memoria en la VRAM',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Cargas rápidas tras morir o viajar (bbhost) y el servidor The Hunter's Dream",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Cargas rápidas tras morir o viajar, salto con L3 al correr (bbhost) y el servidor The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Dos arreglos de la GPU: el recuento del trabajo enviado y los samplers liberados tras su fotograma',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: el invitado ya no se queda de espectador tras caer todos ante un jefe',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Un valor del sistema desconocido (sysctl) ya no detiene el juego',
'DLSS и выбор папки в лаунчере при запуске из исходников на NixOS': 'DLSS y el selector de carpetas del launcher al ejecutar desde el código en NixOS',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Las librerías del juego online y el servidor para el cooperativo propio',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base del soporte de DLSS y el modelo de FSR 4 sin estelas tras los personajes',
'Апскейлер FSR 3.1': 'El escalado FSR 3.1',
@@ -2981,7 +3022,8 @@ ES = {
'Журнал, показавший, почему Steam Input не доходил до игры': 'El log que mostró por qué Steam Input no llegaba al juego',
'Чёрный Сон Охотника после долгой игры': 'El Sueño del Cazador en negro tras sesiones largas',
'«Выход на рабочий стол» при игре по сети и идеи для лаунчера': '«Salir al escritorio» con el juego online e ideas para el launcher',
'Файл патчей shadPS4 в папке патчей': 'El archivo de parches de shadPS4 en la carpeta de parches',
'Файл патчей shadPS4 в папке патчей и камера, которая не следовала за персонажем с геймпадом': 'El archivo de parches de shadPS4 en la carpeta de parches y la cámara que no seguía al personaje con mando',
'FSR 4, переставший работать после перемещений у ламп': 'FSR 4 dejando de funcionar tras viajar entre lámparas',
'Вылет «Transaction failed» на Seamless Hunt': 'El cierre «Transaction failed» en Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'El cierre «Transaction failed» en Seamless Hunt, los registros que hicieron que el invitado de BBSC dejara de seguir el mundo de un desconocido y las pruebas de BBSC 0.0.2 con un jugador en Windows',
'Страница трофеев, не находившая файлы дампа': 'La página de trofeos que no encontraba los archivos del volcado',
@@ -3633,6 +3675,8 @@ IT = {
"Бег примерно с 70% наклона стика вместо 90%": "Corri inclinando lo stick di circa il 70% invece del 90%",
"Физика тел как в Dark Souls": "Fisica ragdoll di Dark Souls",
"Тела и предметы разлетаются от ударов": "Cadaveri e oggetti volano via quando vengono colpiti",
'L3: прыжок на бегу': 'L3 salta durante la corsa',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Correndo con il cerchio premuto, L3 salta come premere di nuovo il cerchio. Patch di Kyo (da bbhost)',
# Add to Steam
"Добавить в Steam": "Aggiungi a Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Dalla libreria di Steam il gioco si avvia subito, con le impostazioni di questo launcher",
@@ -3645,6 +3689,13 @@ IT = {
"Steam не закрылся. Закройте его сами и попробуйте ещё раз": "Steam non si è chiuso. Chiudilo tu e riprova",
"Не удалось добавить игру в Steam: {}": "Impossibile aggiungere il gioco a Steam: {}",
"Bloodborne добавлен в библиотеку Steam": "Bloodborne è stato aggiunto alla tua libreria di Steam",
'Ярлык': 'Collegamento',
'Лаунчер в меню приложений или на рабочем столе': 'Il launcher nel menu delle applicazioni o sul desktop',
'Меню приложений': 'Menu applicazioni',
'Рабочий стол': 'Desktop',
'Не удалось создать ярлык: {}': 'Impossibile creare il collegamento: {}',
'Ярлык создан на рабочем столе': 'Collegamento creato sul desktop',
'Bloodborne добавлен в меню приложений': 'Bloodborne aggiunto al menu delle applicazioni',
# Online page
"Играть онлайн": "Gioca online",
"Игровой сервер (сообщения, пятна крови, призраки)": "Server di gioco (messaggi, macchie di sangue, fantasmi)",
@@ -3964,7 +4015,11 @@ IT = {
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'L’interfaccia che spariva fuori da 1080p, i vettori di movimento delle grandi bestie e un errore di memoria',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': "Crash al caricamento di alcune mappe, il menu dei gesti da tastiera e modifiche all'immagine per l'upscaler",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'La tastiera su schermo per il nome del personaggio, l’avviso di compilazione degli shader e la compattazione delle copie di memoria nella VRAM',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Caricamenti rapidi dopo una morte o un viaggio (bbhost) e il server The Hunter's Dream",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Caricamenti rapidi dopo una morte o un viaggio, salto con L3 in corsa (bbhost) e il server The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Due correzioni della GPU: il conteggio del lavoro inviato e i sampler liberati dopo il loro fotogramma',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': "BBSC: l'ospite non resta più spettatore dopo una sconfitta contro un boss",
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Un valore di sistema sconosciuto (sysctl) non ferma più il gioco',
'DLSS и выбор папки в лаунчере при запуске из исходников на NixOS': 'DLSS e la scelta della cartella nel launcher eseguendo dai sorgenti su NixOS',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Le librerie del gioco online e il server per la propria cooperativa',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base del supporto DLSS e il modello FSR 4 senza scie dietro i personaggi',
'Апскейлер FSR 3.1': 'L’upscaler FSR 3.1',
@@ -3986,7 +4041,8 @@ IT = {
'Журнал, показавший, почему Steam Input не доходил до игры': 'Il log che ha mostrato perché Steam Input non arrivava al gioco',
'Чёрный Сон Охотника после долгой игры': 'Il Sogno del Cacciatore nero dopo sessioni lunghe',
'«Выход на рабочий стол» при игре по сети и идеи для лаунчера': '"Esci al desktop" con il gioco online e idee per il launcher',
'Файл патчей shadPS4 в папке патчей': 'Il file di patch di shadPS4 nella cartella delle patch',
'Файл патчей shadPS4 в папке патчей и камера, которая не следовала за персонажем с геймпадом': 'Il file delle patch di shadPS4 nella cartella delle patch e la telecamera che non seguiva il personaggio col controller',
'FSR 4, переставший работать после перемещений у ламп': 'FSR 4 che smetteva di funzionare dopo i viaggi tra le lanterne',
'Вылет «Transaction failed» на Seamless Hunt': 'La chiusura "Transaction failed" su Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': "Il crash «Transaction failed» su Seamless Hunt, i log che hanno fatto smettere all'ospite di BBSC di seguire il mondo di uno sconosciuto e i test di BBSC 0.0.2 con un giocatore su Windows",
'Страница трофеев, не находившая файлы дампа': 'La pagina dei trofei che non trovava i file del dump',
@@ -4638,6 +4694,8 @@ FR = {
"Бег примерно с 70% наклона стика вместо 90%": "Courir à partir d'environ 70 % d'inclinaison du stick au lieu de 90 %",
"Физика тел как в Dark Souls": "Physique ragdoll de Dark Souls",
"Тела и предметы разлетаются от ударов": "Les cadavres et les objets sont projetés quand on les frappe",
'L3: прыжок на бегу': 'L3 saute en courant',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'En courant avec rond maintenu, L3 saute comme un nouvel appui sur rond. Patch de Kyo (de bbhost)',
# Add to Steam
"Добавить в Steam": "Ajouter à Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Depuis la bibliothèque Steam, le jeu démarre directement avec les réglages de ce launcher",
@@ -4650,6 +4708,13 @@ FR = {
"Steam не закрылся. Закройте его сами и попробуйте ещё раз": "Steam ne s'est pas fermé. Fermez-le vous-même et réessayez",
"Не удалось добавить игру в Steam: {}": "Impossible d'ajouter le jeu à Steam : {}",
"Bloodborne добавлен в библиотеку Steam": "Bloodborne a été ajouté à votre bibliothèque Steam",
'Ярлык': 'Raccourci',
'Лаунчер в меню приложений или на рабочем столе': 'Le launcher dans le menu des applications ou sur le bureau',
'Меню приложений': 'Menu des applications',
'Рабочий стол': 'Bureau',
'Не удалось создать ярлык: {}': 'Impossible de créer le raccourci : {}',
'Ярлык создан на рабочем столе': 'Raccourci créé sur le bureau',
'Bloodborne добавлен в меню приложений': 'Bloodborne ajouté au menu des applications',
# Online page
"Играть онлайн": "Jouer en ligne",
"Игровой сервер (сообщения, пятна крови, призраки)": "Serveur de jeu (messages, taches de sang, fantômes)",
@@ -4969,7 +5034,11 @@ FR = {
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'L’interface qui disparaissait hors 1080p, les vecteurs de mouvement des grandes bêtes et une erreur mémoire',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': "Plantages au chargement de certaines cartes, le menu des gestes au clavier et des changements d'image pour l'upscaler",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'Le clavier à l’écran pour le nom du personnage, l’avis de compilation des shaders et le compactage des copies de mémoire en VRAM',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Chargements rapides après une mort ou un voyage (bbhost) et le serveur The Hunter's Dream",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Chargements rapides après une mort ou un voyage, saut avec L3 en courant (bbhost) et le serveur The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Deux correctifs GPU : le décompte du travail soumis et les samplers libérés après leur image',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': "BBSC : l'invité ne reste plus spectateur après une défaite face à un boss",
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Une valeur système inconnue (sysctl) n’arrête plus le jeu',
'DLSS и выбор папки в лаунчере при запуске из исходников на NixOS': 'DLSS et le sélecteur de dossier du launcher en lançant depuis les sources sous NixOS',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Les bibliothèques du jeu en ligne et le serveur pour votre propre coopération',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base de la prise en charge de DLSS et le modèle FSR 4 sans traînées derrière les personnages',
'Апскейлер FSR 3.1': 'L’upscaler FSR 3.1',
@@ -4991,7 +5060,8 @@ FR = {
'Журнал, показавший, почему Steam Input не доходил до игры': "Le journal qui a montré pourquoi Steam Input n'atteignait pas le jeu",
'Чёрный Сон Охотника после долгой игры': 'Le Rêve du chasseur noir après de longues sessions',
'«Выход на рабочий стол» при игре по сети и идеи для лаунчера': '« Quitter vers le bureau » en jeu en ligne, et des idées pour le lanceur',
'Файл патчей shadPS4 в папке патчей': 'Le fichier de patchs de shadPS4 dans le dossier des patchs',
'Файл патчей shadPS4 в папке патчей и камера, которая не следовала за персонажем с геймпадом': 'Le fichier de patchs de shadPS4 dans le dossier des patchs, et la caméra qui ne suivait pas le personnage à la manette',
'FSR 4, переставший работать после перемещений у ламп': 'FSR 4 qui cessait de fonctionner après des voyages entre lanternes',
'Вылет «Transaction failed» на Seamless Hunt': 'Le plantage « Transaction failed » sur Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': "Le plantage « Transaction failed » sur Seamless Hunt, les journaux qui ont empêché l'invité de BBSC de suivre le monde d'un inconnu, et les tests de BBSC 0.0.2 avec un joueur sous Windows",
'Страница трофеев, не находившая файлы дампа': 'La page des trophées qui ne trouvait pas les fichiers du dump',
@@ -5643,6 +5713,8 @@ DE = {
"Бег примерно с 70% наклона стика вместо 90%": "Rennen ab etwa 70 % Stick-Neigung statt 90 %",
"Физика тел как в Dark Souls": "Ragdoll-Physik wie in Dark Souls",
"Тела и предметы разлетаются от ударов": "Leichen und Objekte fliegen bei Treffern umher",
'L3: прыжок на бегу': 'L3 springt beim Rennen',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Beim Rennen mit gehaltenem Kreis springt L3 wie ein erneuter Druck auf Kreis. Patch von Kyo (aus bbhost)',
# Add to Steam
"Добавить в Steam": "Zu Steam hinzufügen",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Aus der Steam-Bibliothek startet das Spiel direkt, mit den Einstellungen dieses Launchers",
@@ -5655,6 +5727,13 @@ DE = {
"Steam не закрылся. Закройте его сами и попробуйте ещё раз": "Steam wurde nicht geschlossen. Schließe es selbst und versuche es erneut",
"Не удалось добавить игру в Steam: {}": "Spiel konnte nicht zu Steam hinzugefügt werden: {}",
"Bloodborne добавлен в библиотеку Steam": "Bloodborne wurde deiner Steam-Bibliothek hinzugefügt",
'Ярлык': 'Verknüpfung',
'Лаунчер в меню приложений или на рабочем столе': 'Der Launcher im Anwendungsmenü oder auf dem Desktop',
'Меню приложений': 'Anwendungsmenü',
'Рабочий стол': 'Desktop',
'Не удалось создать ярлык: {}': 'Die Verknüpfung konnte nicht erstellt werden: {}',
'Ярлык создан на рабочем столе': 'Verknüpfung auf dem Desktop erstellt',
'Bloodborne добавлен в меню приложений': 'Bloodborne zum Anwendungsmenü hinzugefügt',
# Online page
"Играть онлайн": "Online spielen",
"Игровой сервер (сообщения, пятна крови, призраки)": "Spielserver (Nachrichten, Blutflecken, Geister)",
@@ -5974,7 +6053,11 @@ DE = {
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'Die Oberfläche, die außerhalb von 1080p verschwand, Bewegungsvektoren der großen Bestien und ein Speicherfehler',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Abstürze beim Laden einiger Karten, das Gestenmenü per Tastatur und Bildänderungen für den Upscaler',
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'Die Bildschirmtastatur für den Charakternamen, der Hinweis zum Kompilieren der Shader und das Verdichten der Speicherkopien im VRAM',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Schnelles Laden nach Tod oder Reise (bbhost) und der Server The Hunter's Dream",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Schnelles Laden nach Tod oder Reise, Sprung mit L3 beim Rennen (bbhost) und der Server The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Zwei GPU-Fehlerbehebungen: die Zählung der übermittelten Arbeit und Sampler, die nach ihrem Frame freigegeben werden',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: der Gast bleibt nach einer Niederlage gegen einen Boss nicht mehr Zuschauer',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Ein unbekannter Systemwert (sysctl) hält das Spiel nicht mehr an',
'DLSS и выбор папки в лаунчере при запуске из исходников на NixOS': 'DLSS und die Ordnerauswahl des Launchers beim Start aus dem Quellcode unter NixOS',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Die Bibliotheken für das Online-Spiel und der Server für den eigenen Koop',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'Die Grundlage für DLSS und das FSR-4-Modell ohne Schlieren hinter Figuren',
'Апскейлер FSR 3.1': 'Der FSR-3.1-Upscaler',
@@ -5996,7 +6079,8 @@ DE = {
'Журнал, показавший, почему Steam Input не доходил до игры': 'Das Log, das zeigte, warum Steam Input das Spiel nicht erreichte',
'Чёрный Сон Охотника после долгой игры': 'Der schwarze Traum des Jägers nach langen Sitzungen',
'«Выход на рабочий стол» при игре по сети и идеи для лаунчера': '„Zum Desktop beenden“ beim Onlinespiel und Ideen für den Launcher',
'Файл патчей shadPS4 в папке патчей': 'Die Patch-Datei von shadPS4 im Patch-Ordner',
'Файл патчей shadPS4 в папке патчей и камера, которая не следовала за персонажем с геймпадом': 'Die Patch-Datei von shadPS4 im Patch-Ordner und die Kamera, die der Figur mit Controller nicht folgte',
'FSR 4, переставший работать после перемещений у ламп': 'FSR 4, das nach Laternenreisen ausfiel',
'Вылет «Transaction failed» на Seamless Hunt': 'Der Absturz „Transaction failed“ auf Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'Der Absturz „Transaction failed“ auf Seamless Hunt, die Logs, durch die ein BBSC-Gast nicht mehr der Welt eines Fremden folgt, und Tests von BBSC 0.0.2 mit einem Windows-Spieler',
'Страница трофеев, не находившая файлы дампа': 'Die Trophäenseite, die die Dateien des Dumps nicht fand',
@@ -6239,6 +6323,8 @@ ZH_CN = {
'Бег примерно с 70% наклона стика вместо 90%': '摇杆推到约 70% 即可奔跑,而不是 90%',
'Физика тел как в Dark Souls': '黑暗之魂式布娃娃物理',
'Тела и предметы разлетаются от ударов': '尸体和物品被击中时会飞散',
'L3: прыжок на бегу': '奔跑时按 L3 跳跃',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': '按住圆圈奔跑时,L3 会像再次按下圆圈一样跳跃。Kyo 的补丁(来自 bbhost)',
'shadnet-p2p: на вашем ПК или VPS': 'shadnet-p2p:在你的电脑或 VPS 上',
'Платиновый': '白金',
'Золотой': '金',
@@ -6811,6 +6897,13 @@ ZH_CN = {
", основной": ",主显示器",
"0: случайный при каждом запуске. Закрепите его, чтобы открыть в брандмауэре или на роутере вручную": "0:每次使用随机端口。如需在防火墙或路由器上手动开放,请固定一个端口",
"Bloodborne добавлен в библиотеку Steam": "Bloodborne 已添加到你的 Steam 库",
'Ярлык': '快捷方式',
'Лаунчер в меню приложений или на рабочем столе': '在应用菜单或桌面上放置启动器',
'Меню приложений': '应用菜单',
'Рабочий стол': '桌面',
'Не удалось создать ярлык: {}': '无法创建快捷方式:{}',
'Ярлык создан на рабочем столе': '已在桌面创建快捷方式',
'Bloodborne добавлен в меню приложений': '已将 Bloodborne 添加到应用菜单',
"Online ID — это имя учётной записи (NPID), зарегистрированное на сервере, а не адрес email": "Online ID 是你在服务器上注册的账号名(NPID),而不是邮箱地址",
"PlayStation (оригинал)": "PlayStation(原版)",
"Steam не закрылся. Закройте его сами и попробуйте ещё раз": "Steam 没有关闭。请手动关闭后重试",
@@ -6948,7 +7041,11 @@ ZH_CN = {
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': '非 1080p 下消失的界面、大型野兽的运动矢量与一处内存错误',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': '加载部分地图时的崩溃、用键盘打开的手势菜单,以及超分辨率的画面改动',
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': '角色名的屏幕键盘、着色器编译提示,以及显存中内存副本的压缩',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "死亡或传送后的快速加载(bbhost)与 The Hunter's Dream 服务器",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "死亡或传送后的快速加载、奔跑时 L3 跳跃(bbhost)与 The Hunter's Dream 服务器",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': '两项 GPU 修复:已提交工作的计数,以及在其帧结束后才释放的采样器',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC:首领战全灭后,客人不再一直处于观战状态',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': '未知的系统值(sysctl)不再导致游戏停止',
'DLSS и выбор папки в лаунчере при запуске из исходников на NixOS': '在 NixOS 上从源码运行时的 DLSS 与启动器文件夹选择',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '联机库与自建合作服务器',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'DLSS 支持的基础,以及角色身后无拖影的 FSR 4 模型',
'Апскейлер FSR 3.1': 'FSR 3.1 超分辨率',
@@ -6970,7 +7067,8 @@ ZH_CN = {
'Журнал, показавший, почему Steam Input не доходил до игры': '指出 Steam Input 为何无法传到游戏的日志',
'Чёрный Сон Охотника после долгой игры': '长时间游玩后猎人梦境变黑',
'«Выход на рабочий стол» при игре по сети и идеи для лаунчера': '联机时的“退出到桌面”以及对启动器的建议',
'Файл патчей shadPS4 в папке патчей': '补丁文件夹中的 shadPS4 补丁文件',
'Файл патчей shadPS4 в папке патчей и камера, которая не следовала за персонажем с геймпадом': '补丁文件夹中的 shadPS4 补丁文件,以及使用手柄时镜头不跟随角色的问题',
'FSR 4, переставший работать после перемещений у ламп': '在灯之间传送后 FSR 4 停止工作的问题',
'Вылет «Transaction failed» на Seamless Hunt': 'Seamless Hunt 上的“Transaction failed”崩溃',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'Seamless Hunt 上的“Transaction failed”崩溃、让 BBSC 访客不再跟随陌生人世界的日志,以及与 Windows 玩家一起测试 BBSC 0.0.2',
'Страница трофеев, не находившая файлы дампа': '找不到转储文件的奖杯页面',
+46 -3
View File
@@ -28,6 +28,7 @@ from bbport_i18n import language, set_language, system_game_language, tr
import bbport_cache
import bbport_atlas_maps
import bbport_coop
import bbport_shortcut
import bbport_steam
import bbport_update
@@ -160,6 +161,8 @@ TWEAKS = [
"Камера не поворачивается сама при движении; ручное управление и захват цели работают"),
("tweak_easy_run", "Бег с меньшим наклоном стика", "Бег примерно с 70% наклона стика вместо 90%"),
("tweak_ragdoll", "Физика тел как в Dark Souls", "Тела и предметы разлетаются от ударов"),
("tweak_l3_run_jump", "L3: прыжок на бегу",
"На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)"),
]
# Loading and smoothness (patches from bbhost, applied at start): bbport.ini key, title, subtitle,
# default.
@@ -1034,8 +1037,13 @@ THANKS = (
_github("SrShadowy", "Экранная клавиатура для имени персонажа, уведомление о компиляции "
"шейдеров и уплотнение копий памяти в видеопамяти"),
_github("Wooloomooloo2", "Запуск игры на встроенных видеокартах Intel Arc/Xe"),
_github("droogie", "Быстрая загрузка после смерти и перемещения (bbhost) и сервер "
"The Hunter's Dream"),
_github("droogie", "Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу "
"(bbhost) и сервер The Hunter's Dream"),
_github("Asanilo", "Два исправления GPU: счётчики отправленной работы и сэмплеры, "
"освобождаемые после их кадра"),
_github("AlexanderDzhoganov", "BBSC: гость больше не остаётся зрителем после гибели группы у босса"),
_github("nullsumlabs", "Неизвестный системный параметр (sysctl) больше не останавливает игру"),
_github("The-Fool-cmd", "DLSS и выбор папки в лаунчере при запуске из исходников на NixOS"),
_github("Wozzardman", "Сервер для своего кооператива и бесшовный призыв BBSC"),
_github("IFreemz", "Модель FSR 4 без шлейфов за персонажами"),
_github("FireBurn", "Апскейлер FSR 3.1"),
@@ -1072,7 +1080,9 @@ THANKS = (
_github("shauryasagar", "Чёрный Сон Охотника после долгой игры"),
_github("ZeroSTF", "Чёрный Сон Охотника после долгой игры"),
_github("popcar2", "Идеи для лаунчера"),
_github("shaneo88", "Файл патчей shadPS4 в папке патчей"),
_github("shaneo88", "Файл патчей shadPS4 в папке патчей и камера, которая не следовала за "
"персонажем с геймпадом"),
_github("brenki1", "FSR 4, переставший работать после перемещений у ламп"),
_github("n3vrs0ft", "Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость "
"BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с "
"игроком на Windows"),
@@ -1349,6 +1359,16 @@ class LauncherWindow(Adw.ApplicationWindow):
steam.add_prefix(themed_icon("steam-symbolic"))
steam.add_suffix(steam_button)
game.add(steam)
# A menu entry (and one on the desktop) for the AppImage or this checkout: an AppImage
# has none unless an AppImage manager makes it (bbport_shortcut.py).
shortcut = Adw.ActionRow(title=tr("Ярлык"),
subtitle=tr("Лаунчер в меню приложений или на рабочем столе"))
shortcut.add_prefix(themed_icon("application-x-executable-symbolic"))
for label, where in ((tr("Меню приложений"), "menu"), (tr("Рабочий стол"), "desktop")):
button = Gtk.Button(label=label, valign=Gtk.Align.CENTER)
button.connect("clicked", self.on_create_shortcut, where)
shortcut.add_suffix(button)
game.add(shortcut)
self.steam_overlay_row = Adw.SwitchRow(
title=tr("Оверлей Steam"),
subtitle=tr("Кнопка Steam, счётчик FPS и снимки экрана Steam, когда игра запущена из Steam"),
@@ -2624,6 +2644,29 @@ class LauncherWindow(Adw.ApplicationWindow):
return False
GLib.timeout_add(500, poll)
def on_create_shortcut(self, _button, where):
appimage = os.environ.get("APPIMAGE")
target = appimage or str(PORT_DIR / "launcher" / "bb-launcher.sh")
icon = PORT_DIR / "launcher" / "bloodborne.png"
if appimage:
# The AppImage's mount is gone once it closes: the icon is kept in the data folder.
try:
kept = DATA_DIR / "bloodborne.png"
kept.write_bytes(icon.read_bytes())
icon = kept
except OSError:
pass
desktop = where == "desktop"
folder = bbport_shortcut.desktop_dir() if desktop else bbport_shortcut.applications_dir()
try:
path = bbport_shortcut.write_entry(folder, target, str(icon), trusted=desktop)
except OSError as error:
self.alert(tr("Ярлык"), tr("Не удалось создать ярлык: {}").format(error))
return
message = tr("Ярлык создан на рабочем столе") if desktop else tr("Bloodborne добавлен в меню приложений")
print(f"Launcher: shortcut written to {path}")
self.toasts.add_toast(Adw.Toast(title=message, timeout=6))
def add_to_steam(self, steam, configs, restart):
# Packaged: the AppImage itself; from a checkout: the launcher script.
appimage = os.environ.get("APPIMAGE")
+72
View File
@@ -0,0 +1,72 @@
# SPDX-FileCopyrightText: Copyright 2026 bbport contributors
# SPDX-License-Identifier: GPL-3.0-or-later
"""A desktop entry for the launcher: in the applications menu and on the desktop.
The AppImage has no menu entry of its own unless an AppImage manager (Gear Lever, AppImageLauncher)
makes one, and a checkout has launcher/install-desktop.sh. The launcher's "Create shortcut" writes
one for whichever it is: the AppImage itself, or the checkout's bb-launcher.sh.
"""
import os
import subprocess
from pathlib import Path
ENTRY_NAME = "io.github.ayozetr.bloodborne_linux.desktop"
def exec_quote(path):
"""`path` as one Exec argument (Desktop Entry Specification: double quotes, with ", `, $ and \\
escaped)."""
escaped = "".join("\\" + c if c in '"`$\\' else c for c in str(path))
return f'"{escaped}"'
def entry_text(target, icon):
"""The desktop entry that starts `target` (the AppImage or bb-launcher.sh) with `icon`."""
return ("[Desktop Entry]\n"
"Type=Application\n"
"Name=Bloodborne (bbport)\n"
"Comment=Native Linux port launcher for your own copy of Bloodborne\n"
f"Exec={exec_quote(target)}\n"
f"Icon={icon}\n"
"Terminal=false\n"
"Categories=Game;\n"
"StartupWMClass=io.github.bbport.Launcher\n")
def applications_dir(env=os.environ):
"""Where the user's own menu entries go ($XDG_DATA_HOME/applications)."""
data = env.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
return Path(data) / "applications"
def desktop_dir():
"""The desktop folder (xdg-user-dir DESKTOP: "Escritorio", "Bureau"...), else ~/Desktop."""
try:
out = subprocess.run(["xdg-user-dir", "DESKTOP"], capture_output=True, text=True,
timeout=5).stdout.strip()
if out and Path(out).is_dir() and Path(out) != Path.home():
return Path(out)
except (OSError, subprocess.SubprocessError):
pass
return Path.home() / "Desktop"
def write_entry(folder, target, icon, trusted=False):
"""Writes the entry into `folder` and returns its path. On the desktop (`trusted`) it is made
executable and marked trusted for GNOME's file manager, which otherwise will not start it."""
folder.mkdir(parents=True, exist_ok=True)
path = folder / ENTRY_NAME
path.write_text(entry_text(target, icon), encoding="utf-8")
path.chmod(0o755 if trusted else 0o644)
if trusted:
try:
subprocess.run(["gio", "set", str(path), "metadata::trusted", "true"],
capture_output=True, timeout=5)
except (OSError, subprocess.SubprocessError):
pass
else:
try:
subprocess.run(["update-desktop-database", str(folder)], capture_output=True, timeout=10)
except (OSError, subprocess.SubprocessError):
pass
return path
+1 -1
View File
@@ -14,7 +14,7 @@ import re
import tempfile
import urllib.request
VERSION = "0.70-linux.4"
VERSION = "0.70-linux.6"
def _build():
+5
View File
@@ -5,4 +5,9 @@ pkgs.mkShell {
(python3.withPackages (ps: [ ps.pygobject3 ]))
gtk4 libadwaita gobject-introspection
];
# The same data directories packaging/default.nix gives the packaged launcher: icons for hosts
# without an Adwaita theme, and GTK4's schemas, which the file chooser aborts without.
shellHook = ''
export XDG_DATA_DIRS=${pkgs.adwaita-icon-theme}/share:${pkgs.hicolor-icon-theme}/share:${pkgs.gtk4}/share/gsettings-schemas/${pkgs.gtk4.name}''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}
'';
}
+12
View File
@@ -3492,4 +3492,16 @@
<Line Type="bytes" Address="0x018281A8" Value="C783500E000002000000"/>
</PatchList>
</Metadata>
<Metadata Title="Bloodborne" Name="L3 Run-Jump" Note="While running with Circle held, L3 jumps as letting go of Circle and pressing it again does; Circle is unchanged. Works with any c0000.hks that keeps the game's run-jump (vanilla, Hyperborne); not with the Nightreign Movement scripts. Uses one byte at 0x05AD3D00 in the zeroed tail of .bss as a 3-frame window." Author="Kyo (from droogie/bbhost)" PatchVer="1.0" AppVer="01.09" AppElf="eboot.bin" isEnabled="false">
<PatchList>
<!-- PadManipulator::Update: the call of ActionRequest::Update goes through the new code; was E8 BA 71 4E 00 -->
<Line Type="bytes" Address="0x01928751" Value="E8AA68B903"/>
<!-- the script's SpEffect query (env 56) goes through the new code; was 48 8B 8B C8 01 00 00 -->
<Line Type="bytes" Address="0x01E142F1" Value="E98AAD6A039090"/>
<!-- new code in the zero tail of the code segment: L3 pressed while running adds the Rolling request; was zeros -->
<Line Type="bytes" Address="0x054BF000" Value="803DF94C6100007406FE0DF14C6100F64711207444F6471920753EF64710207438488B47084885C0742F488B80C80100004885C07423488B40084885C0741A817840870100007406488B4058EBEC804F1202C605A74C610003E9B20895FC"/>
<!-- new code: SpEffect 390 counts as on for 3 frames after it; was zeros -->
<Line Type="bytes" Address="0x054BF080" Value="3D86010000752C803D724C6100007423488B0DE1F747004885C97417483B59607511488B3D374B3200BE01000000E9976695FC488B8BC8010000E9395295FC"/>
</PatchList>
</Metadata>
</Patch>
+1
View File
@@ -62,6 +62,7 @@ EFFECTS={
'tweak_no_camera_rotation':(None,'Disable Camera Auto Rotation via Movement'),
'tweak_easy_run':(None,'Sensitive Analog Input (easier to run)'),
'tweak_ragdoll':(None,'DS1-like physics'),
'tweak_l3_run_jump':(None,'L3 Run-Jump'),
# Loading and smoothness (from droogie/bbhost); the first two are on unless turned off.
'tweak_quick_reentry':(None,'Quick Re-entry'),
'tweak_fast_deflate':(None,'Fast Run-time Deflate'),
+6
View File
@@ -26,4 +26,10 @@ pkgs.mkShell {
# Online module (gpu/bbnet): shadNet client, HTTPS, UPnP
protobuf nlohmann_json openssl zlib miniupnpc
];
# NixOS keeps the driver's libraries in /run/opengl-driver/lib, where the DLSS bridge's dlopen of
# libnvidia-ngx.so.1 does not look (the AppImage links them in: launcher/bbport_vulkan.py).
# Only driver libraries live there, so nothing the shell provides is replaced.
shellHook = ''
export LD_LIBRARY_PATH=''${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}${pkgs.addDriverRunpath.driverLink}/lib
'';
}
-3
View File
@@ -1,3 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#define BB_ATLAS_BUILD_JSON "{\"adapter_protocol\":1,\"upstream_release\":\"windows-v1.8.1\",\"upstream_commit\":\"bee7ec16213022f15bf34084b5587f1ff09e0e65\",\"feature_schema\":1}"
+11 -4
View File
@@ -254,10 +254,17 @@ static ABI int32_t guest_sysctl(const int32_t *name,uint32_t namelen,void *old,u
if (old) { if (*oldlen<4) return fail_posix(ENOMEM); memcpy(old,&value,4); }
*oldlen=4; return 0;
}
fprintf(stderr,"STOP: unsupported sysctl mib");
for (uint32_t i=0;i<namelen && i<8;++i) fprintf(stderr," %d",name[i]);
fputc('\n',stderr);
exit(21);
/* Any other name: ENOENT, as FreeBSD answers a name it does not know, instead of stopping the
* game (exit 21). The first few are logged with their mib. From the nullsumlabs fork
* (bloodborne_pc-deck-optimized), after Awakenchan's Windows port. */
static int logged;
if (logged<8) {
++logged;
fprintf(stderr,"Runtime: sysctl mib");
for (uint32_t i=0;i<namelen && i<8;++i) fprintf(stderr," %d",name[i]);
fprintf(stderr," is not known: ENOENT\n");
}
return fail_posix(ENOENT);
}
/* ---- pthread_once and thread-specific data ---- */
+5 -2
View File
@@ -559,7 +559,10 @@ static uint8_t look_axis(float value, uint8_t stick, int deadzone) {
/* While the mouse turns the camera, the game's own camera auto-rotation (it swings the camera
* behind the character on strafes, made for a stick) fights it: its three stores (1.09) are NOPs
* then, put back when the mouse is let go (#352). Only where the bytes are the game's own.
* BB_MOUSE_AUTO_ROTATE=1: left as the game does it. */
* BB_MOUSE_AUTO_ROTATE=1: left as the game does it. Not while the gamepad is what is being played
* with (gamepad_playing, sample.c): the window holds the mouse whenever it has the focus, and a
* player on a controller lost the camera following the character (#51). */
static int gamepad_playing; /* the gamepad was used last, not the keyboard or the mouse */
static void mouse_auto_rotation_hold(int hold) {
static int held=-1, off=-1;
if (off<0) { const char *e=bb_env("BB_MOUSE_AUTO_ROTATE"); off=e && e[0]=='1'; }
@@ -588,7 +591,7 @@ static void mouse_auto_rotation_hold(int hold) {
held=hold;
}
static void apply_mouse_look(PadData *d, double dx, double dy, uint64_t now, int captured) {
mouse_auto_rotation_hold(mouse_look && captured && !bbgpu_free_camera());
mouse_auto_rotation_hold(mouse_look && captured && !gamepad_playing && !bbgpu_free_camera());
/* The free camera (patch) takes the mouse through the right stick below. */
if (captured && (dx!=0 || dy!=0) && !bbgpu_free_camera()) {
const float k=MOUSE_RAD_PER_PIXEL*mouse_sensitivity;
+2
View File
@@ -190,6 +190,8 @@ static void sample_host(PadData *d) {
(mouse_buttons!=0));
for (int i=0;i<IN_COUNT && !keyboard_active;++i) keyboard_active=key_down(k,i);
prompt_note(g,pad_active,keyboard_active);
if (keyboard_active) gamepad_playing=0;
else if (pad_active) gamepad_playing=1;
apply_keyboard(d,k); /* the bound mouse buttons too */
video_skip(d,k);
if (motion_on()) {
+4 -2
View File
@@ -135,8 +135,10 @@ static ABI int32_t hide_splash(void) { note("SystemService: splash screen hidden
static ABI int32_t launch_browser(void) { note("SystemService: web browser request ignored (offline)"); return 0; }
/* Exit to desktop in the world (gpu/shim/bbport_game_menu.cpp): the System menu's entry runs the
* game's Exit Game, which saves and leaves the world, and arms the quit here. The game closes
* once it is back at the title: when the intro logos start loading (a menu/logo_ file opens) or
* the title menu is built (its texts: runtime_quit_at_title), with no save data mounted, or after
* once it is back at the title: when the intro logos start loading (a menu/logo_ file opens), the
* title menu is built (its texts) or the game's menu flow says its title menu is up (online play
* comes back without either: gpu/games/bloodborne_menu_pointer.cpp, #46; all three call
* runtime_quit_at_title), with no save data mounted, or after
* the last save unmounts. The game's own Exit Game disarms it, and so do two minutes without
* getting there (the question answered No). Closing is the window's own close (SDL_EVENT_QUIT:
* the window thread ends the process with exit code 0). */
+123
View File
@@ -0,0 +1,123 @@
# SPDX-FileCopyrightText: Copyright 2026 bbport contributors
# SPDX-License-Identifier: GPL-2.0-or-later
"""Exercise the production preset-file reader without a Vulkan device.
Compile the unchanged OnFrameStart polling/reading block with the real settings
implementation. Only the remaining frame/GPU work is omitted from the harness.
"""
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
import unittest
from paths import ROOT
class PresetFileTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shlex.split(os.environ.get('CXX', 'c++'))
if not compiler or not shutil.which(compiler[0]):
raise unittest.SkipTest('a C++23 compiler is required')
cls.tmp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.tmp.cleanup)
cls.folder = Path(cls.tmp.name)
source = (ROOT / 'gpu/upscale/vk_temporal_upscaler.cpp').read_text(encoding='utf-8')
start = source.index('static const char* preset_file',
source.index('bool TemporalUpscaler::OnFrameStart() {'))
end = source.index('const auto& settings = BbSettings::Get();', start)
reader = source[start:end]
harness = cls.folder / 'preset_file.cpp'
harness.write_text('''#include <cstdio>
#include <cstdlib>
#include "shim/bb_env.h"
#include "shim/bbport_settings.h"
void Poll() {
static unsigned preset_file_frames = 0;
''' + reader + '''}
int main(int argc, char** argv) {
auto& s = BbSettings::Get();
s.preset = BbSettings::Balanced;
s.upscaler = BbSettings::UpscalerDlss;
s.aspect = BbSettings::Aspect16x9;
s.output_res = BbSettings::PackSize({1920, 1080});
const int frames = argc > 1 ? std::atoi(argv[1]) : 64;
for (int i = 0; i < frames; ++i) Poll();
const auto output = BbSettings::UnpackSize(s.output_res.load());
std::printf("%d %d %d %d %d\\n", s.preset.load(), s.upscaler.load(),
s.aspect.load(), output.width, output.height);
std::fprintf(stderr, "%d\\n", int(BbSettings::UpscalerCount));
}
''', encoding='utf-8')
cls.exe = cls.folder / 'preset-file-test'
result = subprocess.run(compiler + ['-std=c++23', '-Wall', '-Wextra', '-Werror',
'-I', str(ROOT / 'gpu'), str(harness),
str(ROOT / 'gpu/shim/bbport_settings.cpp'),
'-o', str(cls.exe)], capture_output=True, text=True, timeout=60)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
# The provider list is the real one, not a copy: a provider added later is covered too.
probe = subprocess.run([str(cls.exe), '0'], capture_output=True, text=True, check=True)
cls.providers = int(probe.stderr)
def read(self, text, frames=64):
request = self.folder / 'request.txt'
if text is None:
request.unlink(missing_ok=True)
else:
request.write_text(text, encoding='utf-8')
env = dict(os.environ, BB_PRESET_FILE=str(request))
result = subprocess.run([str(self.exe), str(frames)], env=env,
capture_output=True, text=True, timeout=5, check=True)
return tuple(map(int, result.stdout.split()))
def test_four_fields_apply_provider_and_aspect(self):
self.assertEqual(self.read('1 0 0 3'), (1, 0, 3, 640, 360))
def test_three_and_four_fields_accept_every_provider(self):
for provider in range(self.providers):
for suffix in ('', ' 3'):
with self.subTest(provider=provider, suffix=suffix):
self.assertEqual(self.read(f'1 0 {provider}{suffix}'),
(1, provider, 3, 640, 360))
def test_one_and_two_fields_keep_provider(self):
self.assertEqual(self.read('1'), (1, 5, 3, 1920, 1080))
self.assertEqual(self.read('1 0'), (1, 5, 3, 640, 360))
def test_invalid_provider_keeps_previous_provider(self):
for provider in (-1, self.providers, 999):
for suffix in ('', ' 2'):
with self.subTest(provider=provider, suffix=suffix):
aspect, size = (2, (640, 400)) if suffix else (3, (640, 360))
self.assertEqual(self.read(f'1 0 {provider}{suffix}'), (1, 5, aspect, *size))
def test_invalid_other_fields_do_not_block_valid_provider(self):
for text in ('-1 -1 0 -1', '5 999 0 6'):
with self.subTest(text=text):
self.assertEqual(self.read(text), (2, 0, 3, 1920, 1080))
def test_aspect_is_applied_before_output_lookup(self):
self.assertEqual(self.read('1 0 0 2'), (1, 0, 2, 640, 400))
def test_missing_empty_and_non_numeric_requests(self):
for text in (None, '', 'bad', 'bad 0 0 3'):
with self.subTest(text=text):
self.assertEqual(self.read(text), (2, 5, 3, 1920, 1080))
def test_partial_requests_apply_only_scanned_fields(self):
self.assertEqual(self.read('1 0 bad 2'), (1, 5, 3, 640, 360))
self.assertEqual(self.read('1 0 0 bad'), (1, 0, 3, 640, 360))
def test_polling_interval_is_unchanged(self):
self.assertEqual(self.read('1 0 0 3', frames=63), (2, 5, 3, 1920, 1080))
self.assertEqual(self.read('1 0 0 3', frames=64), (1, 0, 3, 640, 360))
if __name__ == '__main__':
unittest.main()
+287
View File
@@ -0,0 +1,287 @@
# SPDX-FileCopyrightText: Copyright 2026 bbport contributors
# SPDX-License-Identifier: GPL-2.0-or-later
"""CPU sampler-GC regression with production lookup, Touch and natural age advancement.
Compile the Sampler declaration, lookup/GC bodies, LRU, defer/pop and timeline
accessors. Only Vulkan allocation, image GC and completion progress are adapters.
The 4000-object allocation limit is modeled; no device or game is used.
"""
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
import unittest
from paths import ROOT
def body(text, anchor):
start = text.index('{', text.index(anchor))
depth = 0
for at in range(start, len(text)):
if text[at] == '{':
depth += 1
elif text[at] == '}':
depth -= 1
if depth == 0:
return text[start + 1:at]
raise ValueError('unbalanced source: ' + anchor)
VK_STUB = r'''#pragma once
#include <cstdint>
#include <memory>
#include <vector>
namespace vk {
using Sampler=std::uint64_t;
inline unsigned destroyed=0;
inline Sampler next_id=0;
inline std::vector<Sampler> destroyed_ids;
struct UniqueSampler {
std::unique_ptr<Sampler> value;
UniqueSampler()=default;
explicit UniqueSampler(Sampler id):value(std::make_unique<Sampler>(id)){}
UniqueSampler(UniqueSampler&&) noexcept=default;
UniqueSampler& operator=(UniqueSampler&&) noexcept=default;
~UniqueSampler(){if(value){++destroyed;destroyed_ids.push_back(*value);}}
Sampler operator*() const{return *value;}
};
}
'''
HARNESS = r'''#include "common/hash.h"
#include "common/lru_cache.h"
#include "common/scope_exit.h"
#include "common/unique_function.h"
#include "textures/sampler.h"
#include <algorithm>
#include <atomic>
#include <bit>
#include <cstdio>
#include <deque>
#include <mutex>
#include <stdexcept>
#include <string>
#include <tsl/robin_map.h>
#include <xxhash.h>
void check(bool condition, const char *message) {
if (!condition)
throw std::runtime_error(message);
}
namespace Vulkan {
class Instance {};
} // namespace Vulkan
namespace BbToggle {
constexpr int PendingPollLimit = 0;
bool Disabled(int) { return true; }
} // namespace BbToggle
VideoCore::Sampler::Sampler(const Vulkan::Instance &, const Gcn::Sampler &, Gcn::BorderColorBuffer,
bool, float) {
auto created = vk::UniqueSampler(++vk::next_id);
<<OWNER>>
}
VideoCore::Sampler::~Sampler() = default;
struct Timeline {
std::atomic<u64> current_tick{1}, gpu_tick{0};
u64 CurrentTick() const noexcept { <<CURRENT>> }
u64 KnownGpuTick() const noexcept { <<KNOWN>> }
bool IsFree(u64 tick) const noexcept { <<FREE>> }
u64 NextTick() noexcept { <<NEXT>> }
void Refresh() {}
};
struct Scheduler {
Timeline work_semaphore;
struct PendingOp {
Common::UniqueFunction<void> callback;
u64 gpu_tick;
};
std::deque<PendingOp> pending_ops;
std::recursive_mutex pending_ops_mutex;
std::atomic<u32> num_pending_ops{0};
u32 pending_polls = 0;
u64 CurrentTick() const { return work_semaphore.CurrentTick(); }
void DeferOperation(Common::UniqueFunction<void> &&func) { <<DEFER>> }
void PopPendingOperations() { <<POP>> }
void Complete(u64 tick) {
work_semaphore.gpu_tick = tick;
PopPendingOperations();
}
};
struct Cache {
Vulkan::Instance instance;
Scheduler scheduler;
std::mutex samplers_mutex;
tsl::robin_map<u64, VideoCore::Sampler> samplers;
Common::LeastRecentlyUsedCache<u64, u64> sampler_lru;
<<INITIAL_AGE>>
u64 trigger_gc_samplers, pressure_gc_samplers, critical_gc_samplers;
explicit Cache(u32 max_samplers) { <<THRESHOLDS>> }
vk::Sampler GetSampler(const Gcn::Sampler &sampler, Gcn::BorderColorBuffer border_color_base,
bool is_depth, float extra_lod_bias) {
<<GET>>
}
void GarbageCollectImages() {} // no images in this sampler-only fixture
void RunGarbageCollector() { <<RUN_GC>> }
void GarbageCollectSamplers() { <<GC>> }
// Only progress/completion is modeled. Age advances exclusively inside production RunGC.
void CompletedFrame() {
RunGarbageCollector();
scheduler.Complete(scheduler.work_semaphore.NextTick());
}
vk::Sampler Lookup(u64 key) {
Gcn::Sampler sampler{};
sampler.min_lod = u32(key);
sampler.max_lod = 4095;
return GetSampler(sampler, Gcn::BorderColorBuffer{}, false, 0.f);
}
void Fill() {
for (u64 key = 0; key < trigger_gc_samplers; ++key)
Lookup(key);
}
};
int main(int argc, char **argv) {
try {
check(argc == 3, "scenario max-samplers");
const std::string scenario = argv[1];
const auto limit = u32(std::stoul(argv[2]));
check(limit >= 32, "limit too small");
Cache cache(limit);
if (scenario != "startup")
for (int i = 0; i < 20; ++i)
cache.CompletedFrame();
const u64 inserted_age = cache.gc_tick;
cache.Fill();
const auto target = cache.Lookup(0); // calls production Touch even on repeat lookup
const auto initial_count = cache.samplers.size();
if (scenario == "startup" || scenario == "warm-fresh") {
cache.RunGarbageCollector();
const auto early = vk::destroyed;
const bool target_destroyed =
std::find(vk::destroyed_ids.begin(), vk::destroyed_ids.end(), target) !=
vk::destroyed_ids.end();
std::printf("scenario=%s limit=%u trigger=%llu inserted_age=%llu age_after=%llu "
"current_tick=%llu completed_tick=%llu early=%u target_destroyed=%d "
"remaining=%zu pending=%u\n",
argv[1], limit, (unsigned long long)cache.trigger_gc_samplers,
(unsigned long long)inserted_age, (unsigned long long)cache.gc_tick,
(unsigned long long)cache.scheduler.CurrentTick(),
(unsigned long long)cache.scheduler.work_semaphore.KnownGpuTick(), early,
target_destroyed, cache.samplers.size(),
cache.scheduler.num_pending_ops.load());
if (scenario == "startup") {
check(early == 0 && !target_destroyed &&
cache.samplers.size() == initial_count - 10 &&
cache.scheduler.num_pending_ops == 10,
"premature first-GC destruction");
cache.scheduler.Complete(cache.scheduler.work_semaphore.NextTick());
check(vk::destroyed == 10 && cache.scheduler.num_pending_ops == 0,
"startup final release");
} else
check(early == 0 && cache.samplers.size() == initial_count,
"fresh Touch did not protect");
} else {
// The first reference completes before later frames: the default pacing case.
cache.scheduler.Complete(cache.scheduler.work_semaphore.NextTick());
for (int frame = 0; frame < 18; ++frame) {
if (scenario == "touch-hot")
check(cache.Lookup(0) == target, "hot object replaced");
cache.CompletedFrame();
}
bool target_destroyed = std::find(vk::destroyed_ids.begin(), vk::destroyed_ids.end(),
target) != vk::destroyed_ids.end();
check(vk::destroyed == 10, "cold entries did not age out");
check(target_destroyed == (scenario == "paced-cold"), "Touch/expiry result wrong");
std::printf("scenario=%s inserted_age=%llu final_age=%llu destroyed=%u "
"target_destroyed=%d completed_before_expiry=1 pending=%u\n",
argv[1], (unsigned long long)inserted_age,
(unsigned long long)cache.gc_tick, vk::destroyed, target_destroyed,
cache.scheduler.num_pending_ops.load());
}
return 0;
} catch (const std::exception &e) {
std::fprintf(stderr, "FAIL %s\n", e.what());
return 1;
}
}
'''
class SamplerGcTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shlex.split(os.environ.get('CXX', 'c++'))
if not compiler or not shutil.which(compiler[0]):
raise unittest.SkipTest('a C++20 compiler is required')
flags = shlex.split(os.environ.get('CXXFLAGS', ''))
cls.tmp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.tmp.cleanup)
folder = Path(cls.tmp.name)
probe = folder / 'dependencies.cpp'
probe.write_text('#include <tsl/robin_map.h>\n#include <xxhash.h>\n'
'#include <fmt/format.h>\n'
'int main() { return int(XXH3_64bits("", 0) == 0); }\n')
result = subprocess.run(compiler + flags + ['-std=c++20', str(probe),
'-lxxhash', '-o', str(folder / 'dependencies')],
capture_output=True, text=True, timeout=60)
if result.returncode:
raise unittest.SkipTest('C++20, robin-map/fmt headers and libxxhash are required')
vk_header = folder / 'vulkan/vk_common.h'
vk_header.parent.mkdir()
vk_header.write_text(VK_STUB)
read = lambda name: (ROOT / 'gpu' / name).read_text(encoding='utf-8')
texture = read('textures/texture_cache.cpp')
memory = read('textures/texture_cache_memory.cpp')
timeline = read('vulkan/timeline.h')
replacements = {
'OWNER': next(line for line in read('textures/sampler.cpp').splitlines()
if 'handle =' in line),
'CURRENT': body(timeline, 'u64 CurrentTick()'),
'KNOWN': body(timeline, 'u64 KnownGpuTick()'),
'FREE': body(timeline, 'bool IsFree('),
'NEXT': body(timeline, 'u64 NextTick()'),
'DEFER': body(read('render/scheduler.h'), 'void DeferOperation('),
'POP': body(read('render/scheduler.cpp'), 'void Scheduler::PopPendingOperations('),
'INITIAL_AGE': next(line.strip() for line in read('textures/texture_cache.h').splitlines()
if 'u64 gc_tick =' in line),
'THRESHOLDS': '\n'.join(line for line in texture.splitlines()
if '_gc_samplers = max_samplers' in line),
'GET': body(texture, 'vk::Sampler TextureCache::GetSampler('),
'RUN_GC': body(memory, 'void TextureCache::RunGarbageCollector('),
'GC': body(memory, 'void TextureCache::GarbageCollectSamplers('),
}
code = HARNESS
for key, value in replacements.items():
code = code.replace('<<' + key + '>>', value)
source = folder / 'sampler-gc.cpp'
source.write_text(code)
cls.exe = folder / 'sampler-gc'
result = subprocess.run(compiler + flags + ['-std=c++20', '-Wall', '-Wextra', '-Werror',
'-I', str(folder), '-I', str(ROOT / 'gpu'), str(source),
'-lxxhash', '-pthread', '-o', str(cls.exe)],
capture_output=True, text=True, timeout=60)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
def run_case(self, mode):
result = subprocess.run([str(self.exe), mode, '4000'], capture_output=True,
text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
print(result.stdout, end='')
def test_first_gc_retains_fresh_samplers_until_render_completion(self):
self.run_case('startup')
def test_fresh_lookup_after_warmup_does_not_evict(self):
self.run_case('warm-fresh')
def test_completed_cold_samplers_age_out(self):
self.run_case('paced-cold')
def test_repeated_lookup_refreshes_the_hot_sampler(self):
self.run_case('touch-hot')
if __name__ == '__main__':
unittest.main()
+40
View File
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: Copyright 2026 bbport contributors
# SPDX-License-Identifier: GPL-3.0-or-later
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "launcher"))
import bbport_shortcut # noqa: E402
class ShortcutTest(unittest.TestCase):
def test_exec_quotes_spaces_and_specials(self):
self.assertEqual(bbport_shortcut.exec_quote("/a b/Bloodborne.AppImage"),
'"/a b/Bloodborne.AppImage"')
self.assertEqual(bbport_shortcut.exec_quote('/x/"$`\\'), '"/x/\\"\\$\\`\\\\"')
def test_entry_fields(self):
text = bbport_shortcut.entry_text("/games/Bloodborne.AppImage", "/data/bloodborne.png")
lines = text.splitlines()
self.assertEqual(lines[0], "[Desktop Entry]")
self.assertIn('Exec="/games/Bloodborne.AppImage"', lines)
self.assertIn("Icon=/data/bloodborne.png", lines)
self.assertIn("StartupWMClass=io.github.bbport.Launcher", lines)
def test_applications_dir_follows_xdg_data_home(self):
self.assertEqual(bbport_shortcut.applications_dir({"XDG_DATA_HOME": "/d"}),
Path("/d/applications"))
def test_write_entry_modes(self):
with tempfile.TemporaryDirectory() as tmp:
menu = bbport_shortcut.write_entry(Path(tmp) / "apps", "/a", "/i")
self.assertEqual(menu.stat().st_mode & 0o777, 0o644)
desk = bbport_shortcut.write_entry(Path(tmp) / "desk", "/a", "/i", trusted=True)
self.assertEqual(desk.stat().st_mode & 0o777, 0o755)
self.assertEqual(desk.read_text(), bbport_shortcut.entry_text("/a", "/i"))
if __name__ == "__main__":
unittest.main()
+408
View File
@@ -0,0 +1,408 @@
# SPDX-FileCopyrightText: Copyright 2026 bbport contributors
# SPDX-License-Identifier: GPL-2.0-or-later
"""A queued submission must not be visible to the decoder before its count is published.
Compiles the actual SubmitGfx/SubmitAsc publication blocks and the consumer's pop
block from gpu/cp/command_processor.cpp. The producer is paused at its publication
point (after the queue push, before the counter); the queue is then inspected the
way the decoder does it, under the queue mutex. If the task is visible there, the
count must already cover it.
Why the pause is injected instead of taking submit_mutex: the decoder takes the
same mutex inside its pop block, so parking the producer on it would park the
consumer too -- which is exactly why the window is short in production. The pause
sits on the boundary under test; the queue, both mutexes and the counter are real.
No Vulkan device and no command decoding: only the queue, the mutexes and the
counter take part, which is where the ordering lives.
Also executes the production SignalGpuIdle and WaitGpuIdle bodies. Rasterizer
and GPU execution are omitted: idle callbacks are explicitly invoked by the test.
"""
import os
from pathlib import Path
import re
import shlex
import shutil
import subprocess
import tempfile
import unittest
from paths import ROOT
SOURCE = ROOT / 'gpu/cp/command_processor.cpp'
PUSH = 'queue.works.push_back(std::move(work));'
def _block_start(text, pos):
"""Index of the line holding the lone '{' that opens the block around `pos`."""
opener = None
for match in re.finditer(r'\n([ \t]*)\{\n', text[:pos]):
opener = match
if opener is None:
raise ValueError('no block opener before offset %d' % pos)
return opener.start() + 1
def _match_brace(text, start):
depth = 0
for index in range(start, len(text)):
if text[index] == '{':
depth += 1
elif text[index] == '}':
depth -= 1
if depth == 0:
return index + 1
raise ValueError('unbalanced block at offset %d' % start)
def _producer_block(text, function):
"""One producer: the queue push under the queue mutex, then the count publication."""
entry = text.index('void CommandProcessor::%s' % function)
push = text.index(PUSH, entry)
start = _block_start(text, push)
end = text.index('submit_cv.notify_one();', push) + len('submit_cv.notify_one();')
block = text[start:end]
block = block.replace(PUSH, PUSH + '\n NotePushed();')
# Pause on the boundary under test: before the count's lock, wherever it is.
return re.sub(r'([ \t]*)(std::scoped_lock \w+\{submit_mutex\};)',
r'\1HoldPublication();\n\1\2', block)
def _pop_block(text):
entry = text.index('void CommandProcessor::Run(')
pop = text.index('queue.works.pop_front();', entry)
start = _block_start(text, pop)
return text[start:_match_brace(text, start)]
def _statement(text, start_anchor, statement):
entry = text.index(start_anchor)
at = text.index(statement, entry)
return text[at:at + len(statement)]
def _body(text, anchor):
entry = text.index(anchor)
start = text.index('{', entry)
return text[start + 1:_match_brace(text, start) - 1]
def _idle_snapshot(text):
entry = text.index('void CommandProcessor::AfterBatch(')
at = text.index('decoded = num_submits == 0;', entry)
start = _block_start(text, at)
return text[start:_match_brace(text, start)]
HARNESS = r'''#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <deque>
#include <latch>
#include <functional>
#include <string>
#include <memory>
#include <mutex>
#include <span>
#include <thread>
#include <utility>
using u32 = std::uint32_t;
using u64 = std::uint64_t;
constexpr u64 NoSeq = ~u64{0};
namespace BbStats {
std::atomic<u64> guest_submit_stamp{0};
}
namespace BbTimeline {
inline constexpr int GuestSubmit = 0;
inline void Note(int, u64 = 0, u64 = 0) {}
}
namespace BbFreeCheck {
inline void NoteSubmit(u64, const u32*, size_t) {}
}
struct SubmittedCopy {
const u32* guest_dcb = nullptr;
};
struct Prep {
void Enqueue(u64, void*) {}
void* Build(std::span<const u32>) { return nullptr; }
};
struct Work {
std::span<const u32> dcb, ccb;
u64 seq = NoSeq;
u64 stamp = 0;
std::shared_ptr<SubmittedCopy> copy;
};
namespace BbIrq {
enum class Line { GpuIdle };
std::atomic<unsigned> raises{0};
void Raise(Line) { ++raises; }
}
// A diagnostic wrapper around a real mutex. While the producer is paused, the
// flag lets the observer distinguish a held queue lock without try_lock's
// permitted spurious failure. Lock acquisition/release order is unchanged.
struct TrackedMutex {
std::mutex mutex;
std::atomic<bool> held{false};
void lock() { mutex.lock(); held.store(true); }
void unlock() { held.store(false); mutex.unlock(); }
};
struct State {
struct Queue {
TrackedMutex mutex;
std::deque<std::unique_ptr<Work>> works;
} queue;
std::mutex submit_mutex;
std::condition_variable submit_cv;
std::atomic<u32> num_submits{0};
std::atomic<u32> num_commands{0};
u64 submissions_total = 0;
u64 decoded_total = 0;
u32 num_mapped_queues = 1;
u32 gnm_vqid = 1;
u64 gfx_submit_seq = 0;
std::atomic<bool> work_retired{true};
Prep* draw_prep = nullptr;
std::span<const u32> dcb, ccb;
Work* last_decoded = nullptr;
std::deque<std::pair<u64, u64>> frame_ends;
bool frame_end = false;
std::atomic<bool> armed{false};
std::latch pushed{1};
std::latch pub{1};
std::latch pub_release{1};
void NotePushed() {
if (armed.load(std::memory_order_acquire)) {
pushed.count_down();
}
}
void HoldPublication() {
if (!armed.load(std::memory_order_acquire)) {
return;
}
pub.count_down();
pub_release.wait();
}
void Submit(bool gfx) {
auto work = std::make_unique<Work>();
auto* draw_prep = this->draw_prep;
[[maybe_unused]] void* prepared = nullptr;
if (gfx) {
<<GFX>>
} else {
<<ASC>>
}
}
void SignalGpuIdle(u64 generation) {
<<SIGNAL>>
}
std::function<void()> IdleCallback() {
u64 generation;
bool decoded;
<<SNAPSHOT>>
(void)decoded;
return <<CALLBACK>>;
}
std::latch waiting{1};
std::atomic<bool> wait_noted{false};
void NoteWaitPredicate() {
if (num_submits != 0 && !wait_noted.exchange(true)) waiting.count_down();
}
void WaitGpuIdle() {
<<WAIT>>
}
void PopOne() {
<<POP>>
}
bool Empty() {
std::scoped_lock lock{queue.mutex};
return queue.works.empty();
}
/// AfterBatch's own decision, on the same counter.
bool WouldBeIdle() {
bool decoded = false;
<<IDLE>>
return decoded;
}
/// Run()'s loop condition, on the same counters.
bool LoopContinues() {
return (<<LOOP>>);
}
};
struct Result {
bool visible = false;
u32 counted = 0;
u32 queued = 0;
bool premature = false;
u32 left = 0;
u64 total = 0;
unsigned early_idle = 0;
unsigned stale_idle = 0;
unsigned fresh_idle = 0;
};
Result Run(bool gfx) {
State s;
s.Submit(gfx); // the counted one
s.armed.store(true, std::memory_order_release);
std::thread producer([&] { s.Submit(gfx); }); // pushed, not yet counted
s.pushed.wait();
s.pub.wait();
Result r;
{
r.visible = !s.queue.mutex.held.load();
if (r.visible) {
std::scoped_lock q{s.queue.mutex};
r.counted = s.num_submits.load();
r.queued = u32(s.queue.works.size());
}
}
if (r.visible) {
s.PopOne();
r.premature = s.WouldBeIdle() && !s.LoopContinues();
}
auto idle = s.IdleCallback();
BbIrq::raises = 0;
idle();
r.early_idle = BbIrq::raises.exchange(0);
s.pub_release.count_down();
producer.join();
while (!s.Empty()) {
s.PopOne();
}
// The callback captured before the new publication must now be stale even
// after all work is drained: the generation guard must reject it.
idle();
r.stale_idle = BbIrq::raises.exchange(0);
s.IdleCallback()();
r.fresh_idle = BbIrq::raises.exchange(0);
r.left = s.num_submits.load();
r.total = s.submissions_total;
return r;
}
int main(int argc, char** argv) {
const std::string mode = argc > 1 ? argv[1] : "publication";
if (mode == "wait") {
State s;
s.Submit(true);
std::atomic<bool> returned{false};
std::thread waiter([&] { s.WaitGpuIdle(); returned = true; });
s.waiting.wait(); // actual wait predicate observed outstanding work
const bool early = returned.load();
s.PopOne(); // production queue -> submit pop, then notify_all
waiter.join();
std::printf("wait: early=%d returned=%d count=%u\n", int(early),
int(returned.load()), s.num_submits.load());
return early || !returned || s.num_submits != 0 ? 1 : 0;
}
int bad = 0;
for (int i = 0; i < 2; ++i) {
const bool gfx = i == 0;
const Result r = Run(gfx);
if (r.visible) {
std::printf("%s: snapshot=available counted=%u queued=%u premature_idle=%d left=%u total=%llu\n",
gfx ? "SubmitGfx" : "SubmitAsc", r.counted, r.queued,
int(r.premature), r.left, static_cast<unsigned long long>(r.total));
} else {
std::printf("%s: queue_locked snapshot=not_taken left=%u total=%llu\n",
gfx ? "SubmitGfx" : "SubmitAsc", r.left,
static_cast<unsigned long long>(r.total));
}
if (mode == "publication" && r.visible) {
std::printf(" a queued submission was visible before its count was published\n");
++bad;
}
if (r.premature) {
std::printf(" the decoder leaves the loop with a submission still queued\n");
}
std::printf(" idle callbacks: before_publication=%u stale=%u fresh=%u\n",
r.early_idle, r.stale_idle, r.fresh_idle);
if (mode == "early-idle" && r.early_idle != 0) ++bad;
if (mode == "generation" && (r.stale_idle != 0 || r.fresh_idle != 1)) ++bad;
if (r.left != 0 || r.total != 2) {
std::printf(" the count and the queue do not agree after the drain\n");
++bad;
}
}
return bad == 0 ? 0 : 1;
}
'''
class SubmissionPublishTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shlex.split(os.environ.get('CXX', 'c++'))
if not compiler or not shutil.which(compiler[0]):
raise unittest.SkipTest('a C++20 compiler is required')
text = SOURCE.read_text(encoding='utf-8')
callback = re.search(r'rasterizer->SignalAfterGpu\((\[this, generation\] \{ SignalGpuIdle\(generation\); \})\);', text)
if callback is None:
raise ValueError('idle callback anchor changed')
wait = _body(text, 'void CommandProcessor::WaitGpuIdle(')
wait = wait.replace('return num_submits == 0;',
'NoteWaitPredicate(); return num_submits == 0;')
code = (HARNESS
.replace('<<SIGNAL>>', _body(text, 'void CommandProcessor::SignalGpuIdle('))
.replace('<<SNAPSHOT>>', _idle_snapshot(text))
.replace('<<CALLBACK>>', callback.group(1))
.replace('<<WAIT>>', wait)
.replace('<<GFX>>', _producer_block(text, 'SubmitGfx'))
.replace('<<ASC>>', _producer_block(text, 'SubmitAsc'))
.replace('<<POP>>', _pop_block(text))
.replace('<<IDLE>>', _statement(text, 'void CommandProcessor::AfterBatch(',
'decoded = num_submits == 0;'))
.replace('<<LOOP>>', _statement(text, 'void CommandProcessor::Run(',
'while (num_submits || num_commands) {')
[len('while ('):]
[:-len(') {')]))
cls.tmp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.tmp.cleanup)
folder = Path(cls.tmp.name)
source = folder / 'publish.cpp'
source.write_text(code, encoding='utf-8')
cls.exe = folder / 'publish-test'
result = subprocess.run(compiler + ['-std=c++20', '-Wall', '-Wextra', '-Werror',
'-pthread', str(source), '-o', str(cls.exe)],
capture_output=True, text=True, timeout=120)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
def run_case(self, mode):
result = subprocess.run([str(self.exe), mode], capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
print(result.stdout, end='')
def test_count_is_published_before_the_task_is_visible(self):
self.run_case('publication')
def test_idle_callback_cannot_accept_unpublished_queued_work(self):
self.run_case('early-idle')
def test_generation_rejects_stale_callback_and_accepts_fresh_idle(self):
self.run_case('generation')
def test_wait_gpu_idle_blocks_then_wakes_after_the_real_pop(self):
self.run_case('wait')
if __name__ == '__main__':
unittest.main()