Skip to content

Commit a74e8dd

Browse files
committed
feat[closes #3977]: add lsfg-vk frame generation
1 parent b94e085 commit a74e8dd

18 files changed

Lines changed: 842 additions & 0 deletions

File tree

bottles/backend/globals.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,31 @@ def is_vkbasalt_available():
9090
return True
9191
return False
9292

93+
@staticmethod
94+
def get_lsfg_vk_version():
95+
layer_dirs = [
96+
path
97+
for path in os.environ.get("VK_ADD_LAYER_PATH", "").split(os.pathsep)
98+
if path
99+
]
100+
layer_dirs += [
101+
"/usr/lib/extensions/vulkan/lsfgvk/share/vulkan/implicit_layer.d",
102+
f"{Paths.xdg_data_home}/vulkan/implicit_layer.d",
103+
"/usr/local/share/vulkan/implicit_layer.d",
104+
"/usr/share/vulkan/implicit_layer.d",
105+
"/etc/vulkan/implicit_layer.d",
106+
]
107+
for version, manifest in (
108+
(2, "VkLayer_LSFGVK_frame_generation.json"),
109+
(1, "VkLayer_LS_frame_generation.json"),
110+
):
111+
if any(
112+
os.path.isfile(os.path.join(layer_dir, manifest))
113+
for layer_dir in layer_dirs
114+
):
115+
return version
116+
return 0
117+
93118

94119
class TrdyPaths:
95120
# External managers paths
@@ -123,6 +148,8 @@ def check_flatpak_extension(cmd: str, path: str):
123148
"/usr/lib/extensions/vulkan/HdrWsi/lib/libVkLayer_hdr_wsi.so"
124149
)
125150
vkbasalt_available = Paths.is_vkbasalt_available()
151+
lsfg_vk_version = Paths.get_lsfg_vk_version()
152+
lsfg_vk_available = bool(lsfg_vk_version)
126153
mangohud_available = check_flatpak_extension("mangohud", "/usr/lib/extensions/vulkan/MangoHud/bin/mangohud")
127154
obs_vkc_available = check_flatpak_extension("obs-vkcapture", "/usr/lib/extensions/vulkan/OBSVkCapture/bin/obs-vkcapture")
128155
vmtouch_available = shutil.which("vmtouch") or False

bottles/backend/managers/backup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,8 @@ def exclude_filter(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo | None:
328328
"""
329329
if "dosdevices" in tarinfo.name:
330330
return None
331+
if "lsfg-vk" in tarinfo.name.split("/"):
332+
return None
331333
return tarinfo
332334

333335
@staticmethod

bottles/backend/models/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ class BottleParams(DictCompatMixIn):
105105
mangohud_display_on_game_start: bool = True
106106
obsvkc: bool = False
107107
vkbasalt: bool = False
108+
lsfg_vk: bool = False
109+
lsfg_vk_multiplier: int = 2
110+
lsfg_vk_flow_scale: float = 1.0
111+
lsfg_vk_performance_mode: bool = False
108112
gamemode: bool = False
109113
gamescope: bool = False
110114
gamescope_game_width: int = 0

bottles/backend/utils/lsfgvk.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# lsfgvk.py
2+
#
3+
# Copyright 2026 Bottles Developers
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, in version 3 of the License.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import contextlib
18+
import os
19+
import shutil
20+
import tempfile
21+
22+
23+
def get_lsfg_vk_dll_path(bottle_path):
24+
return os.path.join(bottle_path, "lsfg-vk", "Lossless.dll")
25+
26+
27+
def store_lsfg_vk_dll(source, bottle_path):
28+
if os.path.basename(source).casefold() != "lossless.dll":
29+
raise ValueError("Unexpected DLL name")
30+
31+
with open(source, "rb") as file:
32+
if file.read(2) != b"MZ":
33+
raise ValueError("Invalid DLL")
34+
35+
target = get_lsfg_vk_dll_path(bottle_path)
36+
target_dir = os.path.dirname(target)
37+
os.makedirs(target_dir, exist_ok=True)
38+
if os.path.exists(target) and os.path.samefile(source, target):
39+
return target
40+
41+
fd, temporary = tempfile.mkstemp(dir=target_dir, prefix=".Lossless-", suffix=".dll")
42+
os.close(fd)
43+
try:
44+
shutil.copy2(source, temporary)
45+
os.replace(temporary, target)
46+
except BaseException:
47+
with contextlib.suppress(OSError):
48+
os.remove(temporary)
49+
raise
50+
51+
return target
52+
53+
54+
def remove_lsfg_vk_dll(bottle_path):
55+
target = get_lsfg_vk_dll_path(bottle_path)
56+
with contextlib.suppress(FileNotFoundError):
57+
os.remove(target)
58+
with contextlib.suppress(OSError):
59+
os.rmdir(os.path.dirname(target))

bottles/backend/utils/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ bottles_sources = [
2424
'connection.py',
2525
'gsettings_stub.py',
2626
'json.py',
27+
'lsfgvk.py',
2728
'singleton.py'
2829
]
2930

bottles/backend/wine/winecommand.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Paths,
1212
gamemode_available,
1313
gamescope_available,
14+
lsfg_vk_version,
1415
mangohud_available,
1516
obs_vkc_available,
1617
vmtouch_available,
@@ -23,6 +24,7 @@
2324
from bottles.backend.utils.display import DisplayUtils
2425
from bottles.backend.utils.generic import detect_encoding, is_ntsync_available
2526
from bottles.backend.utils.gpu import GPUUtils
27+
from bottles.backend.utils.lsfgvk import get_lsfg_vk_dll_path
2628
from bottles.backend.utils.manager import ManagerUtils
2729
from bottles.backend.utils.steam import SteamUtils
2830
from bottles.backend.utils.terminal import TerminalUtils
@@ -180,6 +182,57 @@ def apply_hdr_preferences(env: "WineEnv", params, gamescope_activated: bool) ->
180182
env.add("ENABLE_GAMESCOPE_WSI", "1", override=True)
181183

182184

185+
def apply_lsfg_vk_preferences(
186+
env: "WineEnv", params, bottle_path: str, version: Optional[int] = None
187+
) -> None:
188+
if version is None:
189+
version = lsfg_vk_version
190+
191+
if not getattr(params, "lsfg_vk", False):
192+
return
193+
194+
dll = get_lsfg_vk_dll_path(bottle_path)
195+
try:
196+
multiplier = int(getattr(params, "lsfg_vk_multiplier", 2))
197+
flow_scale = float(getattr(params, "lsfg_vk_flow_scale", 1.0))
198+
except (TypeError, ValueError):
199+
multiplier = 0
200+
flow_scale = 0
201+
enabled = (
202+
version in (1, 2)
203+
and os.path.isfile(dll)
204+
and os.access(dll, os.R_OK)
205+
and 2 <= multiplier <= 4
206+
and 0.25 <= flow_scale <= 1.0
207+
)
208+
if not enabled:
209+
env.add("DISABLE_LSFG", "1", override=True)
210+
env.add("DISABLE_LSFGVK", "1", override=True)
211+
return
212+
213+
multiplier = str(multiplier)
214+
flow_scale = str(flow_scale)
215+
performance = "1" if getattr(params, "lsfg_vk_performance_mode", False) else "0"
216+
217+
if version == 2:
218+
env.add("DISABLE_LSFG", "1", override=True)
219+
env.remove("DISABLE_LSFGVK")
220+
env.add("LSFGVK_ENV", "1", override=True)
221+
env.add("LSFGVK_DLL_PATH", dll, override=True)
222+
env.add("LSFGVK_MULTIPLIER", multiplier, override=True)
223+
env.add("LSFGVK_FLOW_SCALE", flow_scale, override=True)
224+
env.add("LSFGVK_PERFORMANCE_MODE", performance, override=True)
225+
return
226+
227+
env.remove("DISABLE_LSFG")
228+
env.add("DISABLE_LSFGVK", "1", override=True)
229+
env.add("LSFG_LEGACY", "1", override=True)
230+
env.add("LSFG_DLL_PATH", dll, override=True)
231+
env.add("LSFG_MULTIPLIER", multiplier, override=True)
232+
env.add("LSFG_FLOW_SCALE", flow_scale, override=True)
233+
env.add("LSFG_PERFORMANCE_MODE", performance, override=True)
234+
235+
183236
def _needs_steam_virtual_gamepad_workaround(runner_name: Optional[str]) -> bool:
184237
"""Return True if the runner should force SteamVirtualGamepadInfo."""
185238

@@ -505,6 +558,13 @@ def get_env(
505558
env.add("VKBASALT_CONFIG_FILE", vkbasalt_conf_path)
506559
env.add("ENABLE_VKBASALT", "1")
507560

561+
apply_lsfg_vk_preferences(
562+
env,
563+
params,
564+
bottle,
565+
version=lsfg_vk_version if arch != "win32" and not self.minimal else 0,
566+
)
567+
508568
# OBS Vulkan Capture environment variables
509569
if params.obsvkc and not self.minimal:
510570
env.add("OBS_VKCAPTURE", "1")

bottles/frontend/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,12 @@ def __show_about_dialog(self, *_args):
414414
"winetricks data: LGPL-2.1-or-later."
415415
),
416416
)
417+
about_dialog.add_legal_section(
418+
_("lsfg-vk"),
419+
_("Vulkan frame generation layer"),
420+
Gtk.License.CUSTOM,
421+
_("lsfg-vk 1.x: MIT.\nlsfg-vk 2.x: GPL-3.0-or-later."),
422+
)
417423
about_dialog.set_copyright(
418424
_("Copyright © 2017 {developer_name}").format(
419425
developer_name=about_dialog.get_developer_name()
@@ -432,6 +438,7 @@ def __show_about_dialog(self, *_args):
432438
"vkbasalt-cli https://gitlab.com/TheEvilSkeleton/vkbasalt-cli",
433439
"GameMode https://github.com/FeralInteractive/gamemode",
434440
"Gamescope https://github.com/Plagman/gamescope",
441+
"lsfg-vk https://github.com/PancakeTAS/lsfg-vk",
435442
"OBS Vulkan/OpenGL capture https://github.com/nowrep/obs-vkcapture",
436443
"Wine-TKG https://github.com/Frogging-Family/wine-tkg-git",
437444
"Proton https://github.com/ValveSoftware/proton",

bottles/frontend/ui/bottles.gresource.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<file preprocess="xml-stripblanks">importer.ui</file>
3939
<file preprocess="xml-stripblanks">library.ui</file>
4040
<file preprocess="xml-stripblanks">dialog-launch-options.ui</file>
41+
<file preprocess="xml-stripblanks">dialog-lsfg-vk.ui</file>
4142
<file preprocess="xml-stripblanks">dialog-playtime-graph.ui</file>
4243
<file preprocess="xml-stripblanks">dialog-dll-overrides.ui</file>
4344
<file preprocess="xml-stripblanks">dialog-env-vars.ui</file>

bottles/frontend/ui/details-preferences.blp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,26 @@ template $DetailsPreferences: Adw.PreferencesPage {
159159
}
160160
}
161161

162+
Adw.ActionRow row_lsfg_vk {
163+
activatable-widget: switch_lsfg_vk;
164+
title: _("Frame Generation (Experimental)");
165+
subtitle: _("Generate additional frames with lsfg-vk. Only works with 64-bit Vulkan applications.");
166+
167+
Button btn_manage_lsfg_vk {
168+
tooltip-text: _("Manage lsfg-vk settings");
169+
valign: center;
170+
icon-name: "applications-system-symbolic";
171+
172+
styles [
173+
"flat",
174+
]
175+
}
176+
177+
Switch switch_lsfg_vk {
178+
valign: center;
179+
}
180+
}
181+
162182
Adw.ActionRow row_gamescope {
163183
activatable-widget: switch_gamescope;
164184
title: "Gamescope";

0 commit comments

Comments
 (0)