Compare commits

..
7 Commits
Author SHA1 Message Date
ayozetr 98750d0687 AppImage: give appimagetool the version
appimagetool writes the desktop entry's X-AppImage-Version itself, from
the VERSION environment variable, and replaced the one the build had
added with "UNKNOWN" (#49). The build now exports VERSION instead.
2026-11-08 01:21:34 +00:00
ayozetr 0b3dac11d7 Launcher: version 0.70-linux.3 2026-11-08 01:15:44 +00:00
ayozetr 5041586f9a AppImage: version and description for AppImage managers
Gear Lever showed the AppImage's version as "UNKNOWN" (#49): its desktop
entry had no X-AppImage-Version and there was no AppStream metainfo.
The build now writes both: the version is taken from the launcher's
VERSION (bbport_update.py), so it is kept in one place, and the
metainfo carries the name, summary, description, licence, links and
that version. Both pass desktop-file-validate and appstreamcli validate.
2026-11-08 01:15:38 +00:00
ayozetr e77bce746f Menu pointer: clicks reach the quick menu while the Atlas tile shows
The pointer left a click alone while the port's overlay was open, and
that check was also true whenever Hunter's Atlas's Map tile showed in
the game's quick menu. Clicks on Status, System, Inventory and the rest
did nothing there; only the tile's Map opened.

The pointer now steps aside only when the overlay or the open map takes
the input, and leaves a click on the tile, with its release, to the
tile. With BB_MENU_POINTER_FILE set, each click is traced: whether the
pointer took it, why not, and the button it pressed.
2026-11-08 01:15:30 +00:00
ayozetr d1dea1b8e4 Menu pointer: the right mouse button is L2 in the game's menus
In a game menu the right button went back, and the mouse's other
buttons did not reach the game at all, so with keyboard and mouse
nothing could press L2 or R2 there. The storage box switches to the
storage with them (#44), and its prompt shows the right button for it:
pressing it closed the box instead.

Only the left button is the pointer's now. The others keep their
bindings in menus as well (the right one L2, the side ones R2 and L1),
as the game's prompts show them; Escape goes back. The test file's
"right" action is "back".
2026-11-08 01:15:30 +00:00
ayozetr 6796fbbd13 Window: ask for fullscreen again when Wayland drops it at start
On Wayland a program cannot place its window. With several monitors
KWin could map the game on the one SDL reports as primary, move it to
the monitor chosen in the launcher and leave it there as a plain
window (#39); asked again, fullscreen holds, as KDE's own shortcut
showed. During the first 10 seconds the port now asks again, at most
three times and a second apart, and logs each try. After that the
desktop decides, so its own shortcuts keep working.

TROUBLESHOOTING.md: choosing the monitor, the retry and
SDL_VIDEO_DRIVER=x11 as the fallback.
2026-11-07 12:57:45 +00:00
ayozetr badd460277 Texture cache: name the images when an overlap resolve fails
The warning said only "Image overlap resolve failed". It now gives the
image the game asked for and the one dropped for it (address, size,
dimensions, format, levels and layers), and whether the dropped image
held what the GPU drew: then its contents are lost and the new image is
read from guest memory. Issue #25 had two of these warnings around a
garbled texture that went away after a reload.
2026-11-07 11:56:21 +00:00
10 changed files with 150 additions and 21 deletions

No files matched your search

+11
View File
@@ -59,6 +59,17 @@ and the HUD keeps to the screen's edges; it changes live, in the game's *Display
**Output resolution** *Auto* is the monitor's own; any other size written as `output_res=WxH` in
bbport.ini works too.
## Fullscreen opens on the wrong monitor, or drops back to a window
With several monitors, choose the one to play on in **Display → Monitor**: the port's *Primary*
is the one the system reports first, which on Wayland is not always the one KDE or GNOME show as
primary. On Wayland a program cannot place its own window, so the compositor may open it on
another monitor first and then move it; when that leaves it as a plain window, the port asks for
fullscreen again during the first seconds (the log says
`Display: the compositor left the window out of fullscreen, asked again`). If it still ends up
windowed, add `SDL_VIDEO_DRIVER=x11` under **Advanced → Developer → Extra variables**: through
Xwayland the window places itself.
## Exit to desktop
**Exit to desktop** is in the game's System menu and on the title screen, from bbport 0.7. The
+42 -16
View File
@@ -41,6 +41,7 @@
#include "bbgpu.h"
#include "bbport_guest_code_hooks.h"
#include "bbport_atlas_menu.h"
#include "bbport_overlay.h"
#include "bbport_toggles.h"
#include "common/types.h"
@@ -520,24 +521,35 @@ void PS4_SYSV_ABI OnListUpdate(std::uint64_t, const Core::GuestRegisterSnapshot*
}
return;
}
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if ((!moved && !clicked) || pad_steering.load(std::memory_order_relaxed)) {
if (trace && clicked) {
std::printf("Menu pointer: list %p click waits (pad steering)\n",
static_cast<void*>(component));
}
return;
}
std::uint64_t slot = 0;
const int index = list.ItemAt(x, y, slot);
if (index < 0) {
if (trace && clicked) {
std::printf("Menu pointer: list %p click at %.0f,%.0f, on none of its items\n",
static_cast<void*>(component), x, y);
}
return;
}
if (clicked) {
click.store(false, std::memory_order_relaxed);
}
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if (trace) {
std::printf("Menu pointer: list %p item %d (cursor %d)%s\n", static_cast<void*>(component),
index, list.CursorIndex(), clicked ? " click" : "");
}
if (list.MoveTo(index, slot) && clicked) {
const std::uint32_t button = list.ClickButton(slot, x, y, ConfirmButton());
if (trace) {
std::printf("Menu pointer: list %p presses %#x\n", static_cast<void*>(component), button);
}
presses.fetch_or(button, std::memory_order_relaxed);
// A held click on a slider's arrow keeps moving it.
if (button == Left || button == Right) {
@@ -602,27 +614,41 @@ int OnEvent(const void* sdl_event, void* sdl_window) {
default:
break;
}
const bool ours = !bbgpu_mouse_captured() && !BbOverlay::MenuOpen() && InMenu();
// Not BbOverlay::MenuOpen(): that is also true while Hunter's Atlas's tile shows in the game's
// quick menu, and then no click reached the game's own items (only the tile's Map opened).
// A click on the tile itself is left to it (BbAtlasMenu::HandleMouse, after this filter),
// and so is its release, on which it opens the map.
const bool atlas_tile = BbAtlasMenu::Visible() && BbAtlasMenu::Hovered();
const bool ours =
!bbgpu_mouse_captured() && !BbOverlay::CapturesInput() && !atlas_tile && InMenu();
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if (trace && event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
std::printf("Menu pointer: button %d down (taken %d: captured %d, overlay %d, Atlas tile %d, "
"menu %d)\n",
int(event.button.button), ours ? 1 : 0, bbgpu_mouse_captured() ? 1 : 0,
BbOverlay::CapturesInput() ? 1 : 0, atlas_tile ? 1 : 0, InMenu() ? 1 : 0);
}
static bool left_taken = false; ///< the left button's press was the pointer's: so is its release
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
left_held.store(false, std::memory_order_relaxed);
repeat_press.store(0, std::memory_order_relaxed);
return ours ? 1 : 0;
const bool taken = left_taken;
left_taken = false;
return taken ? 1 : 0;
}
if (!ours) {
return 0;
}
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
// Only the left button is the pointer's. The others keep their bindings in a menu too
// (runtime_pad/sample.c), as the game's own prompts show them: the right button is L2, which
// switches the storage box's pages (#44); it used to be the way back, and with it nothing
// could press L2 in a menu. Escape is the way back.
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
SetPointer(window, event.button.x, event.button.y);
if (event.button.button == SDL_BUTTON_LEFT) {
left_held.store(true, std::memory_order_relaxed);
click_ms.store(NowMs(), std::memory_order_relaxed);
click.store(true, std::memory_order_relaxed);
} else if (event.button.button == SDL_BUTTON_RIGHT) {
presses.fetch_or(BackButton(), std::memory_order_relaxed);
}
return 1;
}
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
left_held.store(true, std::memory_order_relaxed);
click_ms.store(NowMs(), std::memory_order_relaxed);
click.store(true, std::memory_order_relaxed);
left_taken = true;
return 1;
}
if (event.type == SDL_EVENT_MOUSE_WHEEL) {
@@ -636,7 +662,7 @@ int OnEvent(const void* sdl_event, void* sdl_window) {
return 0;
}
/// BB_MENU_POINTER_FILE=<file> (tests): "x y [click|right|up|down]" in stage pixels, applied
/// BB_MENU_POINTER_FILE=<file> (tests): "x y [click|back|up|down]" in stage pixels, applied
/// when the file changes, as the mouse would.
void ReadTestFile() {
static const char* path = bb_env("BB_MENU_POINTER_FILE");
@@ -668,7 +694,7 @@ void ReadTestFile() {
if (!std::strcmp(action, "click")) {
click_ms.store(now, std::memory_order_relaxed);
click.store(true, std::memory_order_relaxed);
} else if (!std::strcmp(action, "right")) {
} else if (!std::strcmp(action, "back")) {
presses.fetch_or(BackButton(), std::memory_order_relaxed);
} else if (!std::strcmp(action, "up") || !std::strcmp(action, "down")) {
presses.fetch_or(action[0] == 'u' ? Up : Down, std::memory_order_relaxed);
+1
View File
@@ -71,6 +71,7 @@ bool Interacting(){return visible || BbMap::IsOpen() || runtime_map_menu_open();
bool UsesControllerGlyphs(){const char* qa=bb_env("BB_ATLAS_QA_DEVICE");return bb_env("BB_ATLAS_QA")&&qa?std::strcmp(qa,"controller")==0:bool(controller);}
bool UsesPlayStationGlyphs(){const auto type=SDL_GetGamepadTypeForID(last_controller.load());return type==SDL_GAMEPAD_TYPE_PS3||type==SDL_GAMEPAD_TYPE_PS4||type==SDL_GAMEPAD_TYPE_PS5;}
bool Focused(){return focused;}
bool Hovered(){return hovered;}
void Suppress(bool value){suppressed=value;if(value){visible=false;state.Reset();}}
void Reset(){state.Reset();focused=false;visible=false;hovered=false;mouse_held=false;map_previous=0;}
void Tick(){Refresh();if(BbMap::IsOpen()&&!runtime_map_menu_open()&&state.focused){BbMap::Close();Reset();}}
+2
View File
@@ -8,6 +8,8 @@ bool Interacting();
bool UsesControllerGlyphs();
bool UsesPlayStationGlyphs();
bool Focused();
/// The pointer is over the tile (the game's own menu pointer leaves its clicks to it).
bool Hovered();
void Suppress(bool value);
void Tick();
void Draw();
+5
View File
@@ -60,6 +60,11 @@ private:
u64 last_mouse_motion_ms{}; ///< SDL_GetTicks of the last mouse motion (UpdateCursor)
bool cursor_hidden{};
u64 capture_settle_ms{}; ///< mouse motion before this (SDL_GetTicks) is not looking around
/// Issue #39: fullscreen asked again on Wayland when the compositor did not keep it (FixFullscreen).
bool fullscreen_wanted{};
u64 fullscreen_since_ms{}, fullscreen_tried_ms{};
int fullscreen_tries{};
void FixFullscreen();
bool pointer_shown{}; ///< a game module's pointer menu is open (bbgpu_pointer_wanted)
bool released_for_pointer{}; ///< the mouse was let go for it: taken back after
void CaptureMouse(bool capture);
+30 -2
View File
@@ -223,8 +223,8 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
if (gamescope && !(fullscreen && fullscreen[0] == '1')) {
std::printf("Display: gamescope: fullscreen\n");
}
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN,
!placed && (gamescope || (fullscreen && fullscreen[0] == '1')));
fullscreen_wanted = !placed && (gamescope || (fullscreen && fullscreen[0] == '1'));
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, fullscreen_wanted);
// Issue #234: the swapchain at the display's pixels on a scaled desktop (Wayland 200%), not at
// the logical size stretched by the compositor. Sizes below are read in pixels already.
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
@@ -249,6 +249,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
window_info.render_surface = reinterpret_cast<void*>(SDL_GetNumberProperty(wp, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0));
} else if (driver && !std::strcmp(driver, "wayland")) {
window_info.type = WindowSystemType::Wayland;
fullscreen_since_ms = SDL_GetTicks();
window_info.display_connection = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
window_info.render_surface = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
} else {
@@ -519,9 +520,36 @@ bool WindowSDL::PollEvents() {
}
pointer_shown = pointer;
UpdateCursor();
FixFullscreen();
return is_open;
}
// Issue #39: on Wayland a program cannot place its window. With several monitors KWin can map
// it on another one first (the one SDL calls primary, not KDE's) and move it to the monitor
// asked for, and the window is left there as a plain window. Asked again, fullscreen holds (as
// KDE's own shortcut did). Only in the first seconds and a few times: there is no fullscreen
// toggle in the game, but the desktop's own shortcuts must keep working after that.
void WindowSDL::FixFullscreen() {
constexpr u64 FirstMs = 10000, BetweenMs = 1000;
constexpr int MaxTries = 3;
if (!fullscreen_wanted || !fullscreen_since_ms || fullscreen_tries >= MaxTries) {
return;
}
const u64 now = SDL_GetTicks();
if (now - fullscreen_since_ms > FirstMs) {
fullscreen_since_ms = 0; // past the start: the desktop decides from here
return;
}
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) || now - fullscreen_tried_ms < BetweenMs) {
return;
}
fullscreen_tried_ms = now;
++fullscreen_tries;
std::printf("Display: the compositor left the window out of fullscreen, asked again (%d of %d)\n",
fullscreen_tries, MaxTries);
SDL_SetWindowFullscreen(window, true);
}
// Issue #3: the OS cursor over the game. Hidden in fullscreen, and in a window after 3 s without
// moving the mouse; always shown while the settings menu or a game's pointer menu is open.
void WindowSDL::UpdateCursor() {
+15 -1
View File
@@ -251,9 +251,23 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
if (exact_fmt && info.pixel_format != resolved.guest_info.pixel_format) {
image_id = {}; // the very format is needed
} else if (resolved.guest_info.resources < info.resources) {
// bbport (Linux fork): which images, and whether the dropped one held what the GPU
// drew (then lost: the new image is read from guest memory). Issue #25: a texture
// garbled until the game was restarted.
const ImageInfo& had = resolved.guest_info;
LOG_WARNING(Render_Vulkan,
"Image overlap resolve failed: wanted {:#x}+{:#x} {}x{} {} levels {} layers {}; "
"dropped {:#x}+{:#x} {}x{} {} levels {} layers {}{}",
info.guest_address, info.guest_size, info.size.width, info.size.height,
vk::to_string(info.pixel_format), info.resources.levels, info.resources.layers,
had.guest_address, had.guest_size, had.size.width, had.size.height,
vk::to_string(had.pixel_format), had.resources.levels, had.resources.layers,
True(resolved.flags & ImageFlagBits::GpuModified)
? (resolved.SafeToDownload() ? ", drawn by the GPU (its contents are lost)"
: ", drawn by the GPU, then written")
: "");
FreeImage(image_id); // picked up wrongly
image_id = {};
LOG_WARNING(Render_Vulkan, "Image overlap resolve failed");
}
}
if (!image_id) {
+1 -1
View File
@@ -14,7 +14,7 @@ import re
import tempfile
import urllib.request
VERSION = "0.70-linux.2"
VERSION = "0.70-linux.3"
def _build():
+37
View File
@@ -89,6 +89,43 @@ EOF
chmod +x "$appdir/bin/bloodborne"
sed -e 's|@LAUNCHER@|bloodborne|' -e 's|@ICON@|bloodborne|' \
launcher/io.github.bbport.Launcher.desktop.in > "$work/bloodborne.desktop"
# The version and a description for AppImage managers (Gear Lever showed "UNKNOWN", #49): the
# desktop entry's X-AppImage-Version, which appimagetool writes from VERSION (it replaces one in
# the entry), and AppStream metainfo named after the desktop entry. The version is the
# launcher's own (bbport_update.py), so it is kept in one place.
version=$(sed -n 's/^VERSION = "\(.*\)"$/\1/p' launcher/bbport_update.py)
[[ -n $version ]] || { echo "No VERSION in launcher/bbport_update.py" >&2; exit 1; }
export VERSION=$version
mkdir -p "$appdir/share/metainfo"
cat > "$appdir/share/metainfo/bloodborne.appdata.xml" <<METAINFO
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>io.github.ayozetr.bloodborne_linux</id>
<name>Bloodborne (bbport)</name>
<developer id="io.github.ayozetr">
<name>bloodborne_linux contributors</name>
</developer>
<summary>Native Linux port launcher for your own copy of Bloodborne</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<description>
<p>Runs Bloodborne natively on Linux from your own decrypted dump of the PS4 game with update
1.09. No game files are included. Upscaling (DLSS, FSR), online play, mods and the community
patches are set up from the launcher, which also updates itself.</p>
</description>
<launchable type="desktop-id">bloodborne.desktop</launchable>
<url type="homepage">https://github.com/ayozetr/bloodborne_linux</url>
<url type="bugtracker">https://github.com/ayozetr/bloodborne_linux/issues</url>
<categories><category>Game</category></categories>
<content_rating type="oars-1.1">
<content_attribute id="violence-fantasy">intense</content_attribute>
<content_attribute id="violence-bloodshed">intense</content_attribute>
</content_rating>
<releases>
<release version="$version" date="$(date +%F)"/>
</releases>
</component>
METAINFO
# GTK, libadwaita and their typelibs are loaded by PyGObject at run time (not linked): named here.
# So are Python's ssl and hashlib modules, which quick-sharun leaves out by default: the launcher's
+6 -1
View File
@@ -101,7 +101,12 @@ static void sample_host(PadData *d) {
* it) and sample_atlas() gives the game the neutral input. */
if (bbgpu_overlay_captures_input() && !atlas_on()) { apply_mouse_look(d,0,0,d->timestamp,0); return; }
const bool *k=SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetKeyboardState(NULL) : NULL;
mouse_buttons=captured && SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetMouseState(NULL,NULL) : 0;
/* In a game's pointer menu (the mouse released) the buttons count too, but the left one, which
* is the pointer's click (gpu/games/bloodborne_menu_pointer.cpp): the right one is L2 there as
* the game's prompts show it (the storage box's pages, #44). */
const int pointer_menu=!captured && bbgpu_pointer_wanted();
mouse_buttons=(captured || pointer_menu) && SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetMouseState(NULL,NULL) : 0;
if (!captured) mouse_buttons&=~SDL_BUTTON_LMASK;
const int pad_only=steam_pad_only(g) || !keyboard_enabled();
atomic_store_explicit(&steam_only_now,pad_only,memory_order_relaxed);
if (pad_only) {