mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-10 01:11:45 -10:00
Compare commits
10
Commits
0.70-linux.4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d68ff3987e | ||
|
|
48fc69fb59 | ||
|
|
ce84be68dc | ||
|
|
61a3c3f4a7 | ||
|
|
23191f2a81 | ||
|
|
65e1a94db1 | ||
|
|
614ac83cb1 | ||
|
|
5f0580bd43 | ||
|
|
f16e9dec02 | ||
|
|
6f49316ca1 |
No files matched your search
+4
-1
@@ -49,7 +49,10 @@ 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.
|
||||
- **[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.
|
||||
- **[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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+42
-7
@@ -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",
|
||||
@@ -864,7 +866,10 @@ 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',
|
||||
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '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',
|
||||
@@ -1533,6 +1538,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",
|
||||
@@ -1954,7 +1961,10 @@ 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',
|
||||
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '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',
|
||||
@@ -2628,6 +2638,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",
|
||||
@@ -2959,7 +2971,10 @@ 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',
|
||||
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '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',
|
||||
@@ -3633,6 +3648,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",
|
||||
@@ -3964,7 +3981,10 @@ 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',
|
||||
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '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',
|
||||
@@ -4638,6 +4658,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",
|
||||
@@ -4969,7 +4991,10 @@ 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',
|
||||
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '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',
|
||||
@@ -5643,6 +5668,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",
|
||||
@@ -5974,7 +6001,10 @@ 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',
|
||||
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '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',
|
||||
@@ -6239,6 +6269,8 @@ ZH_CN = {
|
||||
'Бег примерно с 70% наклона стика вместо 90%': '摇杆推到约 70% 即可奔跑,而不是 90%',
|
||||
'Физика тел как в Dark Souls': '黑暗之魂式布娃娃物理',
|
||||
'Тела и предметы разлетаются от ударов': '尸体和物品被击中时会飞散',
|
||||
'L3: прыжок на бегу': '奔跑时按 L3 跳跃',
|
||||
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': '按住圆圈奔跑时,L3 会像再次按下圆圈一样跳跃。Kyo 的补丁(来自 bbhost)',
|
||||
'shadnet-p2p: на вашем ПК или VPS': 'shadnet-p2p:在你的电脑或 VPS 上',
|
||||
'Платиновый': '白金',
|
||||
'Золотой': '金',
|
||||
@@ -6948,7 +6980,10 @@ 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 и модель FSR 4 без шлейфов за персонажами': 'DLSS 支持的基础,以及角色身后无拖影的 FSR 4 模型',
|
||||
'Апскейлер FSR 3.1': 'FSR 3.1 超分辨率',
|
||||
|
||||
@@ -160,6 +160,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 +1036,12 @@ 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("Wozzardman", "Сервер для своего кооператива и бесшовный призыв BBSC"),
|
||||
_github("IFreemz", "Модель FSR 4 без шлейфов за персонажами"),
|
||||
_github("FireBurn", "Апскейлер FSR 3.1"),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 ---- */
|
||||
|
||||
@@ -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,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,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