mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-12 18:08:46 -10:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1202bab21 | ||
|
|
c435733825 | ||
|
|
64acb09154 | ||
|
|
5f4ae0ac12 | ||
|
|
79b71e6b16 | ||
|
|
93a8f8e2a5 | ||
|
|
2433441711 | ||
|
|
726d7b3760 | ||
|
|
d68ff3987e | ||
|
|
48fc69fb59 | ||
|
|
ce84be68dc | ||
|
|
61a3c3f4a7 | ||
|
|
23191f2a81 | ||
|
|
65e1a94db1 | ||
|
|
614ac83cb1 | ||
|
|
5f0580bd43 | ||
|
|
f16e9dec02 | ||
|
|
6f49316ca1 |
No files matched your search
+7
-2
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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',
|
||||
'Страница трофеев, не находившая файлы дампа': '找不到转储文件的奖杯页面',
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -14,7 +14,7 @@ import re
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
VERSION = "0.70-linux.4"
|
||||
VERSION = "0.70-linux.6"
|
||||
|
||||
|
||||
def _build():
|
||||
|
||||
@@ -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}
|
||||
'';
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
}
|
||||
@@ -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
@@ -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 ---- */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user