refactor(desktop): gate the WSLg D3D12 selection inside the helper and test the real launch env
Move the WSL / /dev/dxg / d3d12_dri.so probes into _prefer_wsl_d3d12 with the probed paths as module constants, so the launcher call site is a single line and a test can lay out a fake WSLg host without touching real /dev or /usr/lib. The two tests now run the real _desktop_launch_env end to end (selected under WSL+dxg+driver; untouched with an explicit Mesa override, off WSL, without /dev/dxg, or without the driver file) instead of unit-testing the helper with a precomputed boolean. Docs: one paragraph in the Desktop guide on the automatic selection and the env vars that keep an explicit choice authoritative. Follow-up to Xipong's fix for #106117 (salvaged from #106118).
This commit is contained in:
+21
-15
@@ -1403,29 +1403,35 @@ def _build_desktop_app(desktop_dir: Path, *, source_mode: bool, npm: str, env: d
|
||||
return packaged_executable
|
||||
|
||||
|
||||
def _prefer_wsl_d3d12(env: dict, *, available: bool) -> None:
|
||||
"""Select Mesa before exec: setting this inside Electron can miss GPU initialization."""
|
||||
overrides = ("GALLIUM_DRIVER", "MESA_LOADER_DRIVER_OVERRIDE", "LIBGL_ALWAYS_SOFTWARE", "LIBGL_DRIVERS_PATH")
|
||||
if available and not any(key in env for key in overrides):
|
||||
_WSL_DXG_DEVICE = Path("/dev/dxg")
|
||||
_WSL_D3D12_DRIVERS = (
|
||||
Path("/usr/lib/x86_64-linux-gnu/dri/d3d12_dri.so"),
|
||||
Path("/usr/lib/aarch64-linux-gnu/dri/d3d12_dri.so"),
|
||||
Path("/usr/lib64/dri/d3d12_dri.so"),
|
||||
Path("/usr/lib/dri/d3d12_dri.so"),
|
||||
)
|
||||
_MESA_DRIVER_OVERRIDES = ("GALLIUM_DRIVER", "MESA_LOADER_DRIVER_OVERRIDE", "LIBGL_ALWAYS_SOFTWARE", "LIBGL_DRIVERS_PATH")
|
||||
|
||||
|
||||
def _prefer_wsl_d3d12(env: dict) -> None:
|
||||
"""Under WSLg, /dev/dxg alone does not make Mesa pick the GPU: Chromium still lands on
|
||||
llvmpipe unless GALLIUM_DRIVER selects d3d12, and it must be set before Electron spawns
|
||||
its GPU process (setting it from JS is too late). Explicit Mesa choices win; hosts without
|
||||
the driver are left alone."""
|
||||
from hermes_constants import is_wsl
|
||||
if any(key in env for key in _MESA_DRIVER_OVERRIDES):
|
||||
return
|
||||
if is_wsl() and _WSL_DXG_DEVICE.exists() and any(driver.is_file() for driver in _WSL_D3D12_DRIVERS):
|
||||
env["GALLIUM_DRIVER"] = "d3d12"
|
||||
|
||||
|
||||
def _desktop_launch_env(args: argparse.Namespace) -> tuple[dict, list[str]]:
|
||||
"""Electron child env + config-supplied extra flags. ``desktop.*`` config is bridged to env vars
|
||||
Electron already reads; an explicit env var wins over config (and over keychain detection)."""
|
||||
from hermes_constants import is_wsl, with_hermes_node_path
|
||||
from hermes_constants import with_hermes_node_path
|
||||
# with_hermes_node_path() copies os.environ when called with no arg.
|
||||
env = with_hermes_node_path()
|
||||
# /dev/dxg alone does not select hardware Mesa: Chromium may still get
|
||||
# llvmpipe. Keep custom Mesa choices and systems without D3D12 unchanged.
|
||||
_prefer_wsl_d3d12(env, available=is_wsl() and Path("/dev/dxg").exists() and any(
|
||||
Path(driver).is_file() for driver in (
|
||||
"/usr/lib/x86_64-linux-gnu/dri/d3d12_dri.so",
|
||||
"/usr/lib/aarch64-linux-gnu/dri/d3d12_dri.so",
|
||||
"/usr/lib64/dri/d3d12_dri.so",
|
||||
"/usr/lib/dri/d3d12_dri.so",
|
||||
)
|
||||
))
|
||||
_prefer_wsl_d3d12(env)
|
||||
for attr, key in (
|
||||
("fake_boot", "HERMES_DESKTOP_BOOT_FAKE"), ("ignore_existing", "HERMES_DESKTOP_IGNORE_EXISTING")):
|
||||
if getattr(args, attr, False):
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
from hermes_cli.main_desktop import _prefer_wsl_d3d12
|
||||
"""``hermes gui`` under WSLg selects the installed Mesa D3D12 driver before Electron spawns
|
||||
its GPU process (#106117) — and never overrides an explicit Mesa choice or fires off-WSL."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import hermes_constants
|
||||
from hermes_cli import main_desktop
|
||||
|
||||
|
||||
def test_selects_available_wsl_driver_before_spawning_electron():
|
||||
env = {"PATH": "/bin"}
|
||||
_prefer_wsl_d3d12(env, available=True)
|
||||
assert env == {"PATH": "/bin", "GALLIUM_DRIVER": "d3d12"}
|
||||
def _launch_env(monkeypatch, tmp_path, *, wsl: bool, dxg: bool, driver: bool) -> dict:
|
||||
"""Run the real launcher env builder against a fake WSL host laid out under ``tmp_path``."""
|
||||
dxg_path = tmp_path / "dxg"
|
||||
driver_path = tmp_path / "d3d12_dri.so"
|
||||
dxg_path.unlink(missing_ok=True)
|
||||
driver_path.unlink(missing_ok=True)
|
||||
if dxg:
|
||||
dxg_path.touch()
|
||||
if driver:
|
||||
driver_path.write_bytes(b"\x7fELF")
|
||||
monkeypatch.setattr(hermes_constants, "_wsl_detected", wsl)
|
||||
monkeypatch.setattr(main_desktop, "_WSL_DXG_DEVICE", dxg_path)
|
||||
monkeypatch.setattr(main_desktop, "_WSL_D3D12_DRIVERS", (tmp_path / "missing_dri.so", driver_path))
|
||||
monkeypatch.setattr(main_desktop, "_desktop_launch_options", lambda: ([], "auto", "auto", "auto"))
|
||||
monkeypatch.setattr(main_desktop, "_detect_linux_password_store", lambda: None)
|
||||
env, _flags = main_desktop._desktop_launch_env(argparse.Namespace(cwd=str(tmp_path)))
|
||||
return env
|
||||
|
||||
|
||||
def test_missing_driver_and_explicit_mesa_choices_are_preserved():
|
||||
overrides = [
|
||||
{"GALLIUM_DRIVER": "llvmpipe"},
|
||||
{"GALLIUM_DRIVER": ""},
|
||||
{"MESA_LOADER_DRIVER_OVERRIDE": "zink"},
|
||||
{"LIBGL_ALWAYS_SOFTWARE": "1"},
|
||||
{"LIBGL_DRIVERS_PATH": "/custom/mesa"},
|
||||
]
|
||||
for original in overrides:
|
||||
env = dict(original)
|
||||
_prefer_wsl_d3d12(env, available=True)
|
||||
assert env == original
|
||||
env = {}
|
||||
_prefer_wsl_d3d12(env, available=False)
|
||||
assert env == {}
|
||||
def test_wslg_with_dxg_and_installed_d3d12_driver_selects_gpu_mesa_backend(monkeypatch, tmp_path):
|
||||
for var in main_desktop._MESA_DRIVER_OVERRIDES:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
env = _launch_env(monkeypatch, tmp_path, wsl=True, dxg=True, driver=True)
|
||||
|
||||
assert env["GALLIUM_DRIVER"] == "d3d12"
|
||||
assert Path(env["HERMES_DESKTOP_CWD"]) == tmp_path.resolve() # the rest of the env still builds
|
||||
|
||||
|
||||
def test_explicit_mesa_choice_and_non_wsl_hosts_are_left_alone(monkeypatch, tmp_path):
|
||||
for var in main_desktop._MESA_DRIVER_OVERRIDES:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
# WSL + GPU present, but the user pinned software rendering: their choice is authoritative.
|
||||
monkeypatch.setenv("LIBGL_ALWAYS_SOFTWARE", "1")
|
||||
env = _launch_env(monkeypatch, tmp_path, wsl=True, dxg=True, driver=True)
|
||||
assert "GALLIUM_DRIVER" not in env and env["LIBGL_ALWAYS_SOFTWARE"] == "1"
|
||||
monkeypatch.delenv("LIBGL_ALWAYS_SOFTWARE")
|
||||
|
||||
# Any one leg missing → untouched: plain Linux with a dxg-like node and driver installed,
|
||||
# WSL without /dev/dxg, WSL with /dev/dxg but no Mesa d3d12 driver on disk.
|
||||
for wsl, dxg, driver in ((False, True, True), (True, False, True), (True, True, False)):
|
||||
env = _launch_env(monkeypatch, tmp_path, wsl=wsl, dxg=dxg, driver=driver)
|
||||
assert "GALLIUM_DRIVER" not in env, (wsl, dxg, driver)
|
||||
|
||||
@@ -169,6 +169,10 @@ desktop:
|
||||
|
||||
That bridges to `ELECTRON_OZONE_PLATFORM_HINT` at launch (an explicit env var still wins). The trade: X11 cannot restore a window that has ignored the mouse, so the HUD stays a solid window instead of click-through. Some KDE setups also report keyboard breakage with the X11 ozone backend — leave the hint on `auto` unless you need always-on-top.
|
||||
|
||||
#### WSLg (Windows GPU from WSL2)
|
||||
|
||||
When `hermes gui` runs inside WSL2 with `/dev/dxg` present and Mesa's `d3d12_dri.so` installed, the launcher sets `GALLIUM_DRIVER=d3d12` for Electron so rendering uses the Windows GPU instead of the llvmpipe software rasterizer; an explicit `GALLIUM_DRIVER`, `MESA_LOADER_DRIVER_OVERRIDE`, `LIBGL_ALWAYS_SOFTWARE`, or `LIBGL_DRIVERS_PATH` in your environment is left untouched (for example `GALLIUM_DRIVER=llvmpipe hermes gui` keeps software rendering).
|
||||
|
||||
### Settings & onboarding
|
||||
|
||||
Manage providers, models, tools, and credentials from a real UI instead of editing YAML. First-run onboarding gets you to your first message in seconds. The settings panes cover providers/keys, model selection, toolset configuration, MCP servers, the gateway, and session management.
|
||||
|
||||
Reference in New Issue
Block a user