Reworked the gamescope to work better with scaling and maximize window.
This commit is contained in:
@@ -1,19 +1,114 @@
|
||||
"""Native X11 command construction for the managed game window."""
|
||||
"""Command construction for the managed game window."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from typing import Sequence
|
||||
from typing import Mapping, Sequence
|
||||
|
||||
from .settings import Settings
|
||||
|
||||
|
||||
MIN_GAME_CANVAS_DIMENSION = 320
|
||||
MAX_GAME_CANVAS_DIMENSION = 16384
|
||||
MAX_GAME_CONFIG_BYTES = 1024 * 1024
|
||||
|
||||
|
||||
class GameboxConfigurationError(ValueError):
|
||||
"""Raised when stable outer scaling cannot use the game configuration."""
|
||||
|
||||
|
||||
def find_gamescope(
|
||||
environment: Mapping[str, str] | None = None,
|
||||
) -> str | None:
|
||||
"""Find Gamescope using the environment inherited by the game launch."""
|
||||
|
||||
env = os.environ if environment is None else environment
|
||||
return shutil.which("gamescope", path=env.get("PATH"))
|
||||
|
||||
|
||||
def read_game_canvas_size(game_config: Path) -> tuple[int, int]:
|
||||
"""Read the exact fixed render size advertised in ``game.ini``."""
|
||||
|
||||
try:
|
||||
content = game_config.read_bytes()
|
||||
except OSError as exc:
|
||||
raise GameboxConfigurationError(
|
||||
f"could not read game window configuration {game_config}: {exc}"
|
||||
) from exc
|
||||
if len(content) > MAX_GAME_CONFIG_BYTES:
|
||||
raise GameboxConfigurationError(
|
||||
f"game window configuration is unexpectedly large: {game_config}"
|
||||
)
|
||||
if b"\x00" in content:
|
||||
raise GameboxConfigurationError(
|
||||
f"game window configuration uses an unsupported encoding: {game_config}"
|
||||
)
|
||||
try:
|
||||
text = content.decode("utf-8-sig")
|
||||
except UnicodeDecodeError as utf8_error:
|
||||
try:
|
||||
text = content.decode("cp1252")
|
||||
except UnicodeDecodeError:
|
||||
raise GameboxConfigurationError(
|
||||
f"game window configuration uses an unsupported encoding: {game_config}"
|
||||
) from utf8_error
|
||||
|
||||
values: dict[str, int] = {}
|
||||
section: str | None = None
|
||||
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
section = stripped[1:-1].strip().casefold()
|
||||
continue
|
||||
if section != "screen" or not stripped or stripped.startswith((";", "#")):
|
||||
continue
|
||||
left, separator, right = line.partition("=")
|
||||
if not separator:
|
||||
continue
|
||||
key = left.strip().casefold()
|
||||
if key not in {"width", "height"}:
|
||||
continue
|
||||
if key in values:
|
||||
raise GameboxConfigurationError(
|
||||
f"duplicate [Screen] {key.title()} in {game_config}"
|
||||
)
|
||||
try:
|
||||
value = int(right.strip(), 10)
|
||||
except ValueError as exc:
|
||||
raise GameboxConfigurationError(
|
||||
f"invalid [Screen] {key.title()} on line {line_number} of {game_config}"
|
||||
) from exc
|
||||
if value < MIN_GAME_CANVAS_DIMENSION or value > MAX_GAME_CANVAS_DIMENSION:
|
||||
raise GameboxConfigurationError(
|
||||
f"[Screen] {key.title()} must be between "
|
||||
f"{MIN_GAME_CANVAS_DIMENSION} and {MAX_GAME_CANVAS_DIMENSION}"
|
||||
)
|
||||
values[key] = value
|
||||
if set(values) != {"width", "height"}:
|
||||
raise GameboxConfigurationError(
|
||||
f"game window configuration has no complete [Screen] Width/Height pair: "
|
||||
f"{game_config}"
|
||||
)
|
||||
return values["width"], values["height"]
|
||||
|
||||
|
||||
def build_gamebox_command(
|
||||
game_command: Sequence[str],
|
||||
settings: Settings,
|
||||
*,
|
||||
game_size: tuple[int, int] | None = None,
|
||||
gamescope_path: str | None = None,
|
||||
) -> list[str]:
|
||||
if not game_command:
|
||||
raise ValueError("game command cannot be empty")
|
||||
use_gamescope = (
|
||||
settings.allow_window_resizing
|
||||
and gamescope_path is not None
|
||||
and game_size is not None
|
||||
)
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
@@ -23,7 +118,48 @@ def build_gamebox_command(
|
||||
]
|
||||
if settings.borderless:
|
||||
command.append("--borderless")
|
||||
if use_gamescope:
|
||||
command.append("--gamescope-scaling")
|
||||
if settings.suppress_unresponsive_warnings:
|
||||
command.append("--suppress-wm-ping")
|
||||
command.extend(("--", *[str(value) for value in game_command]))
|
||||
return command
|
||||
if not use_gamescope:
|
||||
return command
|
||||
|
||||
assert gamescope_path is not None
|
||||
assert game_size is not None
|
||||
width, height = game_size
|
||||
if not (
|
||||
MIN_GAME_CANVAS_DIMENSION <= width <= MAX_GAME_CANVAS_DIMENSION
|
||||
and MIN_GAME_CANVAS_DIMENSION <= height <= MAX_GAME_CANVAS_DIMENSION
|
||||
):
|
||||
raise ValueError("game size is outside the supported Gamescope range")
|
||||
gamescope_command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"ptlinux.gamescope_runtime",
|
||||
"--width",
|
||||
str(width),
|
||||
"--height",
|
||||
str(height),
|
||||
"--",
|
||||
gamescope_path,
|
||||
"-w",
|
||||
str(width),
|
||||
"-h",
|
||||
str(height),
|
||||
"-W",
|
||||
str(width),
|
||||
"-H",
|
||||
str(height),
|
||||
# Gamescope recalculates ``fit`` from each committed game buffer, so an
|
||||
# in-game resolution change keeps its own aspect ratio without cropping.
|
||||
"-S",
|
||||
"fit",
|
||||
"-F",
|
||||
"linear",
|
||||
]
|
||||
if settings.borderless:
|
||||
gamescope_command.append("--borderless")
|
||||
gamescope_command.extend(("--", *command))
|
||||
return gamescope_command
|
||||
|
||||
Reference in New Issue
Block a user