fix(gamescope): ship the WSI layer built beside our compositor, instead of guessing at the distro's
A game nested under gamescope gets an HDR10 swapchain from the FROG WSI layer and from nothing else -- gamescope advertises no runtime colour-management protocol a Mesa/NVIDIA WSI could negotiate through. That layer talks `gamescope_swapchain` to the compositor, and when the two disagree the compositor rejects the client's swapchain_feedback and every Vulkan client dies on a black screen with sound and input and no error anywhere. We ship our own compositor and did NOT ship a layer, on the recorded grounds that the layer is "version-independent of the compositor binary". It is not, and wsi_layer_matches_our_gamescope() exists because it is not. So the host was left guessing from version triples, and that guess is wrong in both directions: a distro at the same upstream tag that patched the protocol compares EQUAL and keeps a layer that will kill every game, while a distro at a different tag with a byte-identical protocol compares unequal and loses HDR for nothing. Since we pin a rev, the second case is the normal one -- on essentially every box with a distro gamescope, the layer was disabled and no game could render HDR. Ship the layer instead. It is built from the same tree at the same rev as the compositor, so the two cannot drift, and the guess stops being load-bearing. It is installed under our own name (VK_LAYER_PUNKTFUNK_gamescope_wsi) at our own path with our own enable/disable variables, so it coexists with the distro's rather than colliding -- the Vulkan loader keys implicit layers on that name -- and the host switches the two independently in one session. WsiPlan makes the three states explicit and resolves them once per launch, since the fallback spawns `--version` probes: Ours our layer is installed: enable it, force the distro's off DistroKept no layer of ours, distro's looks compatible: touch nothing DistroDisabled no layer of ours, distro's untrusted: today's behaviour That last arm is the fail-safe. A host newer than its gamescope package behaves exactly as it does today rather than enabling a layer that is not there, so this can roll out one packaging surface at a time without a flag day. Only the Arch PKGBUILD carries the new files so far. The rpm path takes a CI-cached binary rather than the build script's stage dir, so it needs the cache, build-gamescope-rpm.sh and the spec moved together; the deb, both sysexts and gamescope.nix need the same two files added. Until each lands, those boxes take the DistroDisabled arm and are no worse off than before. Verified: 214 pf-vdisplay tests pass in a linux container (including a new one pinning that the Ours arm enables ours AND forces the distro's off together -- either half alone is a bug), clippy -D warnings and rustfmt clean, both shell files pass bash -n, and the manifest rewrite was run against a synthetic FROG manifest to confirm it renames/repoints/regates while preserving the `functions` block. NOT verified: an actual gamescope build, any package build, or a game taking an HDR swapchain on glass.
This commit is contained in:
@@ -1237,7 +1237,7 @@ fn write_session_plus_dropin(
|
||||
wrapper: &std::path::Path,
|
||||
mode: Mode,
|
||||
hdr: bool,
|
||||
wsi_ok: bool,
|
||||
wsi: WsiPlan,
|
||||
) -> Result<bool> {
|
||||
let Some(bind) = arm_session_bind(wrapper) else {
|
||||
remove_session_plus_dropin();
|
||||
@@ -1260,11 +1260,7 @@ fn write_session_plus_dropin(
|
||||
.chain(cursor_args())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
wsi = if wsi_ok {
|
||||
String::new()
|
||||
} else {
|
||||
wsi_off_unit_lines()
|
||||
},
|
||||
wsi = wsi.unit_lines(),
|
||||
);
|
||||
std::fs::write(&path, body).with_context(|| format!("write drop-in {}", path.display()))?;
|
||||
Ok(true)
|
||||
@@ -1579,7 +1575,7 @@ fn ensure_box_gamescope_mode(mode: Mode, hdr: bool) -> Result<u32> {
|
||||
// Same two fixes the transient path gets, but this unit is the BOX's own — they have to arrive
|
||||
// as a drop-in, and `daemon-reload` before the restart or systemd runs the old unit.
|
||||
let mut bound = match write_gamescope_bin_wrapper()
|
||||
.and_then(|w| write_session_plus_dropin(&w, mode, hdr, wsi_layer_matches_our_gamescope()))
|
||||
.and_then(|w| write_session_plus_dropin(&w, mode, hdr, WsiPlan::resolve()))
|
||||
{
|
||||
Ok(true) => {
|
||||
// Record it BEFORE the restart, and persist it: from this instant the box's OWN
|
||||
@@ -4215,25 +4211,89 @@ const WSI_OFF_ENV: [(&str, &str); 2] = [
|
||||
("ENABLE_GAMESCOPE_WSI", "0"),
|
||||
];
|
||||
|
||||
/// [`WSI_OFF_ENV`] as `systemd-run` arguments, for the transient unit.
|
||||
fn wsi_off_setenv_args() -> Vec<String> {
|
||||
WSI_OFF_ENV
|
||||
.iter()
|
||||
.map(|(name, value)| format!("--setenv={name}={value}"))
|
||||
.collect()
|
||||
/// Our own WSI layer's implicit-layer manifest, laid down beside the compositor by
|
||||
/// `packaging/gamescope/build-punktfunk-gamescope.sh`.
|
||||
///
|
||||
/// It is built from the SAME source tree at the SAME rev as `punktfunk-gamescope`, so the layer and
|
||||
/// the compositor cannot disagree about `gamescope_swapchain` — which is what makes every "is the
|
||||
/// distro's layer close enough to ours?" guess unnecessary. It carries its own layer name and its
|
||||
/// own `enable_environment`, so it coexists with the distro's rather than replacing it.
|
||||
const OUR_WSI_LAYER_DIR: &str = "/usr/lib/punktfunk/vulkan/implicit_layer.d";
|
||||
const OUR_WSI_LAYER_MANIFEST: &str =
|
||||
"/usr/lib/punktfunk/vulkan/implicit_layer.d/punktfunk_gamescope_wsi.json";
|
||||
|
||||
/// Which Vulkan WSI layer a session we spawn should run with. Three states, decided ONCE per
|
||||
/// launch because [`WsiPlan::resolve`] can spawn `--version` probes.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum WsiPlan {
|
||||
/// Our own matching layer is installed: enable it, suppress the distro's. Games get HDR.
|
||||
Ours,
|
||||
/// No layer of ours, and the distro's version triple matches the gamescope we run, so it is
|
||||
/// probably built against the same protocol. Leave the box exactly as it is.
|
||||
DistroKept,
|
||||
/// No layer of ours, and the distro's cannot be trusted. Disable it — a mismatched layer kills
|
||||
/// every Vulkan client — and accept that no game in this session can get an HDR10 swapchain.
|
||||
DistroDisabled,
|
||||
}
|
||||
|
||||
/// [`WSI_OFF_ENV`] as unit-file lines, for the box-session drop-in. Trailing newline included, so
|
||||
/// whatever the body puts after it still parses — same contract as [`SessionBind::unit_lines`].
|
||||
fn wsi_off_unit_lines() -> String {
|
||||
WSI_OFF_ENV
|
||||
.iter()
|
||||
.map(|(name, value)| format!("Environment={name}={value}\n"))
|
||||
.collect()
|
||||
impl WsiPlan {
|
||||
/// ⚠️ Spawns up to two `gamescope --version` probes in the fallback arms, so resolve once and
|
||||
/// pass the result around rather than calling this per use site.
|
||||
fn resolve() -> Self {
|
||||
if std::path::Path::new(OUR_WSI_LAYER_MANIFEST).is_file() {
|
||||
Self::Ours
|
||||
} else if wsi_layer_matches_our_gamescope() {
|
||||
Self::DistroKept
|
||||
} else {
|
||||
Self::DistroDisabled
|
||||
}
|
||||
}
|
||||
|
||||
/// The environment this plan needs, as `(name, value)` pairs.
|
||||
fn env(self) -> Vec<(&'static str, &'static str)> {
|
||||
match self {
|
||||
// `VK_ADD_IMPLICIT_LAYER_PATH` ADDS to the loader's implicit-layer search (loader
|
||||
// 1.3.234+), so the box's own layer directories keep working; the distro's gamescope
|
||||
// layer is then switched off by name through its own variables, leaving exactly one
|
||||
// gamescope WSI layer live — ours.
|
||||
Self::Ours => vec![
|
||||
("VK_ADD_IMPLICIT_LAYER_PATH", OUR_WSI_LAYER_DIR),
|
||||
("PUNKTFUNK_GAMESCOPE_WSI", "1"),
|
||||
("DISABLE_GAMESCOPE_WSI", "1"),
|
||||
("ENABLE_GAMESCOPE_WSI", "0"),
|
||||
],
|
||||
Self::DistroKept => Vec::new(),
|
||||
Self::DistroDisabled => WSI_OFF_ENV.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// As `systemd-run` arguments, for the transient unit.
|
||||
fn setenv_args(self) -> Vec<String> {
|
||||
self.env()
|
||||
.iter()
|
||||
.map(|(name, value)| format!("--setenv={name}={value}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// As unit-file lines, for the box-session drop-in. Trailing newline included, so whatever the
|
||||
/// body puts after it still parses — same contract as [`SessionBind::unit_lines`].
|
||||
fn unit_lines(self) -> String {
|
||||
self.env()
|
||||
.iter()
|
||||
.map(|(name, value)| format!("Environment={name}={value}\n"))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the box's `VkLayer_FROG_gamescope_wsi` can be trusted against the gamescope we run.
|
||||
///
|
||||
/// ⚠️ **Fallback only** — reached from [`WsiPlan::resolve`] just when our own layer is absent (a
|
||||
/// `punktfunk-gamescope` package older than the one that started shipping it). It is a guess, and a
|
||||
/// guess in BOTH directions: a distro at the same upstream tag that patched the protocol compares
|
||||
/// EQUAL and keeps a layer that will kill every Vulkan client, while a distro at a different tag
|
||||
/// with a byte-identical protocol compares unequal and loses HDR for nothing. Do not build anything
|
||||
/// new on it; ship the layer instead, which is what [`WsiPlan::Ours`] does.
|
||||
///
|
||||
/// The layer ships with the DISTRO's gamescope and speaks its `gamescope_swapchain` protocol; we
|
||||
/// run our own build. When the two disagree the compositor rejects the client's
|
||||
/// `swapchain_feedback` ("message too short") and **kills every Vulkan client** — Steam never
|
||||
@@ -4309,29 +4369,30 @@ fn launch_session(client: &str, unit_name: &str, mode: Mode, hdr: bool) -> Resul
|
||||
// The distro's Vulkan WSI layer speaks the distro gamescope's protocol; ours may differ, and a
|
||||
// mismatch kills every Vulkan client with no error but a black screen. Steam Big Picture is not
|
||||
// one of them, so the casualty is the GAMES — see [`WSI_OFF_ENV`] for why both variables go.
|
||||
let wsi_ok = wsi_layer_matches_our_gamescope();
|
||||
if !wsi_ok {
|
||||
let wsi = WsiPlan::resolve();
|
||||
if wsi == WsiPlan::DistroDisabled {
|
||||
tracing::warn!(
|
||||
"gamescope: this box's VkLayer_FROG_gamescope_wsi was built for a different gamescope \
|
||||
than the one we run — disabling it for this session (DISABLE_GAMESCOPE_WSI=1, which \
|
||||
the session script cannot clobber the way it clobbers ENABLE_GAMESCOPE_WSI). Left \
|
||||
enabled it rejects the client's swapchain_feedback and every Vulkan client dies; \
|
||||
Steam's own UI is not one, so what you see is a game that runs with sound and input \
|
||||
on a black screen, with no other symptom."
|
||||
);
|
||||
}
|
||||
// The two HDR decisions are made independently — `hdr_args` never consults the layer check — so
|
||||
// without this an HDR session launches advertising HDR while having made game HDR unreachable
|
||||
// in the same breath, and nothing anywhere says so.
|
||||
if !wsi_ok && hdr {
|
||||
tracing::warn!(
|
||||
"gamescope: this session negotiated HDR, but with the WSI layer disabled no game in it \
|
||||
can get an HDR10 swapchain — that layer is the only route to one. The stream itself \
|
||||
stays HDR (the capture really is PQ/BT.2020, and Steam's UI and the desktop ride the \
|
||||
same container), so what breaks is GAME HDR specifically: a title told to render HDR \
|
||||
renders it into an SDR swapchain and looks washed out. Fix by installing a \
|
||||
VkLayer_FROG_gamescope_wsi built for the gamescope we run."
|
||||
than the one we run, and no punktfunk layer is installed to use instead — disabling \
|
||||
it for this session (DISABLE_GAMESCOPE_WSI=1, which the session script cannot clobber \
|
||||
the way it clobbers ENABLE_GAMESCOPE_WSI). Left enabled it rejects the client's \
|
||||
swapchain_feedback and every Vulkan client dies; Steam's own UI is not one, so what \
|
||||
you see is a game that runs with sound and input on a black screen, with no other \
|
||||
symptom. Upgrading the punktfunk-gamescope package fixes this properly — it ships a \
|
||||
layer built from the same tree as the compositor."
|
||||
);
|
||||
// The HDR decisions are made independently — `hdr_args` never consults the layer plan — so
|
||||
// without this an HDR session launches advertising HDR while having made game HDR
|
||||
// unreachable in the same breath, and nothing anywhere says so.
|
||||
if hdr {
|
||||
tracing::warn!(
|
||||
"gamescope: this session negotiated HDR, but with the WSI layer disabled no game \
|
||||
in it can get an HDR10 swapchain — that layer is the only route to one. The \
|
||||
stream itself stays HDR (the capture really is PQ/BT.2020, and Steam's UI and the \
|
||||
desktop ride the same container), so what breaks is GAME HDR specifically: a \
|
||||
title told to render HDR renders it into an SDR swapchain and looks washed out."
|
||||
);
|
||||
}
|
||||
}
|
||||
let start_unit = |bind: Option<&SessionBind>| -> Result<()> {
|
||||
let mut cmd = Command::new("systemd-run");
|
||||
@@ -4339,10 +4400,8 @@ fn launch_session(client: &str, unit_name: &str, mode: Mode, hdr: bool) -> Resul
|
||||
for arg in bind.map(SessionBind::run_args).unwrap_or_default() {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
if !wsi_ok {
|
||||
for arg in wsi_off_setenv_args() {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
for arg in wsi.setenv_args() {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
// Same headless-must-not-attach rule as [`spawn`]: the transient unit inherits the
|
||||
// user manager env, which can carry a (possibly stale) desktop DISPLAY/WAYLAND_DISPLAY
|
||||
@@ -4814,9 +4873,9 @@ mod tests {
|
||||
mask_unit, missing_flags, mode_mismatch, nested_wrapper_script, plan_bind,
|
||||
release_autologin_mask, script_hardcodes_gamescope, sentinel_advanced,
|
||||
shape_dedicated_command, switch_ends_mask_window, takeover_state_is_live, unmask_unit,
|
||||
wsi_off_setenv_args, wsi_off_unit_lines, xwayland_refusal_marker, BindOff, BindPlan,
|
||||
BoxOutputSize, DmHelperError, SessionBind, TakeoverState, AUTOLOGIN_MASKED,
|
||||
DISTRO_GAMESCOPE_PATH, STOPPED_AUTOLOGIN, WSI_OFF_ENV, X11_SOCKET_DIR,
|
||||
xwayland_refusal_marker, BindOff, BindPlan, BoxOutputSize, DmHelperError, SessionBind,
|
||||
TakeoverState, WsiPlan, AUTOLOGIN_MASKED, DISTRO_GAMESCOPE_PATH, OUR_WSI_LAYER_DIR,
|
||||
STOPPED_AUTOLOGIN, WSI_OFF_ENV, X11_SOCKET_DIR,
|
||||
};
|
||||
|
||||
fn argv(s: &str) -> Vec<String> {
|
||||
@@ -5644,8 +5703,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// Both spellings reach both launch paths, and neither may lose the other.
|
||||
let args = wsi_off_setenv_args();
|
||||
let lines = wsi_off_unit_lines();
|
||||
let args = WsiPlan::DistroDisabled.setenv_args();
|
||||
let lines = WsiPlan::DistroDisabled.unit_lines();
|
||||
for (name, value) in WSI_OFF_ENV {
|
||||
assert!(args.contains(&format!("--setenv={name}={value}")), "{name}");
|
||||
assert!(
|
||||
@@ -5658,4 +5717,31 @@ mod tests {
|
||||
// lines above it rely on the same contract and the order has changed before.
|
||||
assert!(lines.ends_with('\n'));
|
||||
}
|
||||
|
||||
/// The whole point of shipping our own layer is that BOTH halves happen in one session: ours is
|
||||
/// switched on AND the distro's is forced off. Enabling ours while leaving theirs live would
|
||||
/// put two gamescope WSI layers in the loader's implicit set, and dropping ours while forcing
|
||||
/// theirs off is just the old no-game-HDR behaviour wearing a new name — so assert the pair,
|
||||
/// not either half.
|
||||
#[test]
|
||||
fn our_own_layer_is_enabled_and_the_distro_one_forced_off_together() {
|
||||
let env = WsiPlan::Ours.env();
|
||||
let get = |k: &str| {
|
||||
env.iter()
|
||||
.find(|(name, _)| *name == k)
|
||||
.map(|(_, v)| *v)
|
||||
.unwrap_or_else(|| panic!("{k} missing from the Ours plan"))
|
||||
};
|
||||
|
||||
assert_eq!(get("VK_ADD_IMPLICIT_LAYER_PATH"), OUR_WSI_LAYER_DIR);
|
||||
assert_eq!(get("PUNKTFUNK_GAMESCOPE_WSI"), "1");
|
||||
// The clobber-proof one, for exactly the reason the test above states.
|
||||
assert_eq!(get("DISABLE_GAMESCOPE_WSI"), "1");
|
||||
assert_eq!(get("ENABLE_GAMESCOPE_WSI"), "0");
|
||||
|
||||
// `DistroKept` must stay genuinely inert: it is the arm that runs on a box we decided not
|
||||
// to touch, so a stray variable there would change behaviour we promised not to change.
|
||||
assert!(WsiPlan::DistroKept.env().is_empty());
|
||||
assert!(WsiPlan::DistroKept.unit_lines().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +98,20 @@ build() {
|
||||
}
|
||||
|
||||
package() {
|
||||
# ONLY the compositor, under our own name: a full `meson install` would also lay down
|
||||
# gamescopectl / gamescopereaper / gamescopestream and the WSI layer, colliding file-for-file
|
||||
# with the distro's `gamescope` package. The host only ever execs the compositor.
|
||||
# The compositor, under our own name: a full `meson install` would also lay down
|
||||
# gamescopectl / gamescopereaper / gamescopestream, colliding file-for-file with the distro's
|
||||
# `gamescope` package. The host only ever execs the compositor.
|
||||
install -Dm755 "${srcdir}/stage/usr/bin/punktfunk-gamescope" \
|
||||
"${pkgdir}/usr/bin/punktfunk-gamescope"
|
||||
|
||||
# ...and the WSI layer built beside it. NOT a collision: the build script renamed the layer and
|
||||
# rehomed it under /usr/lib/punktfunk, so it sits alongside the distro's rather than over it, and
|
||||
# the host enables whichever it means to per session. Without this file no game in a punktfunk
|
||||
# gamescope session can get an HDR10 swapchain at all — that layer is the only route to one.
|
||||
install -Dm755 "${srcdir}/stage/usr/lib/punktfunk/libVkLayer_PUNKTFUNK_gamescope_wsi.so" \
|
||||
"${pkgdir}/usr/lib/punktfunk/libVkLayer_PUNKTFUNK_gamescope_wsi.so"
|
||||
install -Dm644 "${srcdir}/stage/usr/lib/punktfunk/vulkan/implicit_layer.d/punktfunk_gamescope_wsi.json" \
|
||||
"${pkgdir}/usr/lib/punktfunk/vulkan/implicit_layer.d/punktfunk_gamescope_wsi.json"
|
||||
|
||||
install -Dm644 gamescope/LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
|
||||
@@ -90,10 +90,14 @@ echo "==> configuring"
|
||||
# test suite is not our job either way.
|
||||
# -Denable_openvr_support the VR integration pulls the openvr submodule + its build for a
|
||||
# code path a headless capture session never enters.
|
||||
# -Denable_gamescope_wsi_layer the WSI layer is a SEPARATE artifact the distro's gamescope
|
||||
# package already installs; ours must not collide with it.
|
||||
# (The layer the nested games load is that one — it is version-
|
||||
# independent of the compositor binary.)
|
||||
# -Denable_gamescope_wsi_layer ON, and installed under our own name below. This used to be off,
|
||||
# on the grounds that the distro's gamescope package already ships a
|
||||
# layer and that the layer is "version-independent of the compositor
|
||||
# binary". That second half is FALSE: the layer and the compositor
|
||||
# speak `gamescope_swapchain` to each other, and when they disagree
|
||||
# the compositor rejects the client's `swapchain_feedback` and every
|
||||
# Vulkan client dies on a black screen. A compositor we ship needs
|
||||
# the layer we built beside it.
|
||||
#
|
||||
# `force_fallback_for` includes **wlroots** on purpose, and it is load-bearing for a binary we
|
||||
# SHIP: gamescope vendors a wlroots submodule, but meson prefers a system one when the build host
|
||||
@@ -124,7 +128,7 @@ meson setup "$BUILD" "$SRCDIR" \
|
||||
-Dpipewire=enabled \
|
||||
-Denable_tests=false \
|
||||
-Denable_openvr_support=false \
|
||||
-Denable_gamescope_wsi_layer=false
|
||||
-Denable_gamescope_wsi_layer=true
|
||||
|
||||
echo "==> building"
|
||||
ninja -C "$BUILD" ${JOBS:+-j "$JOBS"}
|
||||
@@ -149,6 +153,54 @@ DEST="${DESTDIR}${PREFIX}/bin/punktfunk-gamescope"
|
||||
echo "==> installing $DEST"
|
||||
install -Dm755 "$BIN" "$DEST"
|
||||
|
||||
# The WSI layer, under OUR name, at OUR path.
|
||||
#
|
||||
# A game nested under gamescope gets an HDR10 swapchain from this layer and from nothing else —
|
||||
# gamescope advertises no runtime colour-management protocol a Mesa/NVIDIA WSI could negotiate
|
||||
# through — so a compositor shipped WITHOUT a matching layer simply cannot do HDR for games. Built
|
||||
# from this same tree at this same rev, so the two can never drift apart; that is the whole point,
|
||||
# and it is what makes the host's old "compare version triples and hope" check unnecessary.
|
||||
#
|
||||
# It must not collide with the distro's layer and must be switchable independently of it, so the
|
||||
# generated manifest is rewritten to carry our layer name, our library path and our own
|
||||
# enable/disable variables. The Vulkan loader keys implicit layers on that NAME, so with a distinct
|
||||
# one both layers can sit installed side by side and the host picks per session.
|
||||
#
|
||||
# python3 rather than sed because meson is itself a Python program — it is guaranteed present on any
|
||||
# host that got this far — and a JSON edit belongs in a JSON parser.
|
||||
LAYER_SO=$(find "$BUILD" -type f -name 'libVkLayer_*gamescope_wsi*.so' | head -1)
|
||||
LAYER_SRC_JSON=$(find "$BUILD" -type f -name '*gamescope_wsi*.json' | head -1)
|
||||
[ -n "$LAYER_SO" ] && [ -n "$LAYER_SRC_JSON" ] || {
|
||||
echo "the WSI layer did not build (.so=${LAYER_SO:-none} .json=${LAYER_SRC_JSON:-none}) — without" >&2
|
||||
echo "it no game in a punktfunk gamescope session can get an HDR10 swapchain" >&2
|
||||
exit 1
|
||||
}
|
||||
LAYER_LIB_PATH="${PREFIX}/lib/punktfunk/libVkLayer_PUNKTFUNK_gamescope_wsi.so"
|
||||
LAYER_DEST_JSON="${DESTDIR}${PREFIX}/lib/punktfunk/vulkan/implicit_layer.d/punktfunk_gamescope_wsi.json"
|
||||
echo "==> installing ${DESTDIR}${LAYER_LIB_PATH}"
|
||||
install -Dm755 "$LAYER_SO" "${DESTDIR}${LAYER_LIB_PATH}"
|
||||
install -d "$(dirname "$LAYER_DEST_JSON")"
|
||||
python3 - "$LAYER_SRC_JSON" "$LAYER_DEST_JSON" "$LAYER_LIB_PATH" <<'PY'
|
||||
import json, sys
|
||||
|
||||
src, dst, lib = sys.argv[1:4]
|
||||
with open(src) as f:
|
||||
manifest = json.load(f)
|
||||
layer = manifest["layer"]
|
||||
# A distinct name is what lets our layer and the distro's coexist: the loader deduplicates implicit
|
||||
# layers by name, and with both called VK_LAYER_FROG_gamescope_wsi which one wins is unspecified.
|
||||
layer["name"] = "VK_LAYER_PUNKTFUNK_gamescope_wsi"
|
||||
# Absolute, so resolution never depends on where the loader happened to find the manifest.
|
||||
layer["library_path"] = lib
|
||||
# Our own gates. The distro layer's ENABLE_GAMESCOPE_WSI / DISABLE_GAMESCOPE_WSI keep working on the
|
||||
# distro layer alone, so the host can switch the two independently in one session.
|
||||
layer["enable_environment"] = {"PUNKTFUNK_GAMESCOPE_WSI": "1"}
|
||||
layer["disable_environment"] = {"PUNKTFUNK_GAMESCOPE_WSI_DISABLE": "1"}
|
||||
with open(dst, "w") as f:
|
||||
json.dump(manifest, f, indent=2)
|
||||
f.write("\n")
|
||||
PY
|
||||
|
||||
if [ "$SETCAP" = 1 ] && command -v setcap >/dev/null; then
|
||||
# gamescope raises its own scheduling priority; without CAP_SYS_NICE it still runs, just noisier
|
||||
# and with worse frame pacing. Best-effort — needs root, and a package sets it declaratively.
|
||||
|
||||
Reference in New Issue
Block a user