diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index dcabe9809a..83975875fa 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,50 @@ +## 2026.08.04 (04-08-2026) +- Fix: reverted the GPU acceleration added in 2026.08.03. It did not just fail to help — it was + what disabled the GPU. `--use-gl=angle --use-angle=gl-egl` forces Mesa's EGL X11 platform, + which offers no window-capable EGLConfig under this Xvfb, so the GPU process logged + `gl_surface_egl.cc:262 No suitable EGL configs found`, abandoned GL, and was relaunched with + `--use-gl=disabled` while every renderer got `--disable-gpu-compositing`. + Chromium already renders on the GPU here with no flags at all, because LSIO's Xvfb runs with + `-vfbdevice /dev/dri/renderD128` and its GLX is therefore backed by the real render node — + the premise that Xvfb offers only a software path was wrong for this base image. Measured + three times, including at the production 15360x8640 screen: with the flags the GPU process + loads `libEGL_mesa` and holds 1 fd on the render node; without them it loads `libGLX_mesa`, + holds 8, and no renderer carries `--disable-gpu-compositing`. + Removes `claude-gpu-probe` and the `gpu_acceleration` option. The probe was not wrong about + the hardware, it was answering the wrong question: it exercised ANGLE's default GLX path, + which works, and so it passed while the flags it gated disabled the GPU. +- Fix: `max_resolution` never did anything; the option is renamed to `MAX_RES`. It wrote + `MAX_RES` into the s6 `container_environment`, but the base image's `svc-xorg` starts + `#!/usr/bin/env bashio` rather than `with-contenv` and never reads that directory, so Xvfb + kept starting at 15360x8640. Naming the option `MAX_RES` makes the add-on env layer inject + it directly into every service `run` script, which is how `DRINODE` already reaches Xvfb. + `MAX_RES` ships with no default, so nothing changes for anyone until it is set: the screen + stays at the base image's 15360x8640. The old `max_resolution` default of `1920x1080` is + deliberately not carried over — it had never taken effect, so shipping it now would have + silently shrunk every existing desktop, including on 4K displays. Home Assistant drops + options that are no longer in the schema (it logs a warning), so a saved `max_resolution` + is discarded rather than migrated. The schema bounds each axis to 100-9999, which keeps the + worst case (~400 MB of framebuffer) below the 15360x8640 default it replaces. + Removes `22-display_tuning.sh`. +- Removed the amd64 GPU driver install, which has never run in any release. The 2026.08.03 + build corrected its gate variable but the guard is `if [[ ... ]]` and there is no `SHELL` + directive, so it runs under dash, which has no `[[` — the condition was false and the `RUN` + still exited 0. Verified in the shipped 2026.08.03 image: no `vainfo`, no + `intel-media-va-driver-non-free`, and no matching install in its apt history. + It was deleted rather than repaired, because it had nothing to add. Hardware OpenGL already + works without it: the GPU process loads the Mesa gallium megadriver over DRI3 and holds 8 + fds on `/dev/dri/renderD128`, with no swrast/llvmpipe in the GL path. `libgl1-mesa-dri` and + `mesa-vulkan-drivers` already come from the LSIO base at a newer backports version (Mesa + 25.0.7) than reinstalling them would give. And `intel-media-va-driver-non-free`, the one + genuinely new payload, was installed live on the running add-on and changed nothing: VA-API + failed identically to the free driver (`iHD_drv_video.so init failed`) on both + `/dev/dri/renderD128` and `card0`. That failure is below the add-on, in the host i915 stack. + Repairing the guard would have turned dead code into an untested apt transaction that swaps + a base-image package for no measured benefit. +- A `[[` in the Dockerfile's legacy `/etc/services.d` shim is corrected to `[` for the same + dash reason; that directory does not exist on this s6-overlay v3 base, so it is + behaviour-neutral here. + ## 2026.08.03 (03-08-2026) - Performance: Claude Desktop now uses the GPU instead of rendering on the CPU. Under Xvfb, Chromium probed GLX, found only Xvfb's software path, and fell back to diff --git a/claude_desktop/Dockerfile b/claude_desktop/Dockerfile index 4d1cd34206..7156ca9825 100644 --- a/claude_desktop/Dockerfile +++ b/claude_desktop/Dockerfile @@ -74,7 +74,7 @@ VOLUME [ "/sys/fs/cgroup" ] # hadolint ignore=SC2015,DL4006,SC2013,SC2086 RUN \ usermod --home /data/data abc && \ - if [[ -d /etc/services.d ]] && ls /etc/services.d/*/run 1> /dev/null 2>&1; then sed -i "1a set +e" /etc/services.d/*/run; fi + if [ -d /etc/services.d ] && ls /etc/services.d/*/run 1> /dev/null 2>&1; then sed -i "1a set +e" /etc/services.d/*/run; fi ARG TEMPLATE_BASE_URL="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates" @@ -142,36 +142,29 @@ RUN install -d -m 0755 /etc/apt/keyrings && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -# The Intel N150/Twin Lake iGPU uses the host's i915 kernel driver through the mapped -# /dev/dri nodes. Explicitly install the amd64 userspace stack needed for accelerated -# OpenGL rendering, VA-API video encoding, and Vulkan, then fail the build if any driver -# payload is missing. Keep aarch64 unchanged because these Intel packages are amd64-only. +# No bespoke GPU driver install. The LSIO base image already provides the userspace stack that +# hardware rendering actually uses, and it does so at a newer version than adding it here would: +# libgl1-mesa-dri and mesa-vulkan-drivers come from bookworm-backports Mesa 25.0.7, where a +# plain `apt-get install` of the same names is at best a no-op. # -# Gate on BUILD_ARCH, not TARGETARCH. TARGETARCH is a BuildKit-provided platform ARG; the -# repo's builder (.github/workflows/onpush_builder.yaml) passes BUILD_ARCH explicitly and -# that is the contract this repo can rely on. This block previously used TARGETARCH and -# silently never ran: the shipped amd64 image has no `vainfo` and no -# `intel-media-va-driver-non-free`, and its apt history contains no matching install — so -# the "fail the build if a payload is missing" guarantee below had never once executed. +# There used to be an amd64 block here installing intel-media-va-driver-non-free, libgl1-mesa-dri, +# mesa-vulkan-drivers and vainfo. It never ran in any release: it was guarded by +# `if [[ ... ]]`, and with no SHELL directive RUN executes under /bin/sh — dash, which has no +# `[[`. dash printed `[[: not found`, the condition was false, and the RUN still exited 0, so +# every build silently skipped it and still passed. (An earlier fix corrected the gate variable +# from TARGETARCH to BUILD_ARCH but left the `[[`, so it stayed dead.) Confirmed in the shipped +# 2026.08.03 amd64 image: no vainfo, no intel-media-va-driver-non-free, no matching apt history. # -# The Vulkan ICD payload is checked via the driver directory rather than a single -# distro-specific filename: the previous `test -f intel_icd.x86_64.json` named a file Debian -# does not ship (it installs `intel_icd.json`), so restoring the guard without this change -# would have turned dead code straight into a failing build. -RUN if [[ "${BUILD_ARCH}" == "amd64" ]]; then \ - apt-get update && \ - apt-get install -y --no-install-recommends \ - intel-media-va-driver-non-free \ - libgl1-mesa-dri \ - mesa-vulkan-drivers \ - vainfo && \ - test -f /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so && \ - test -f /usr/lib/x86_64-linux-gnu/dri/iris_dri.so && \ - ls /usr/share/vulkan/icd.d/intel_icd*.json > /dev/null && \ - command -v vainfo > /dev/null; \ - fi && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* +# It was removed rather than repaired, because measurement showed it had nothing to add: +# - Hardware OpenGL already works without it. In the shipped image the Chromium GPU process +# loads the Mesa gallium megadriver over DRI3 and holds 8 fds on /dev/dri/renderD128, with +# no swrast/llvmpipe in the GL path. +# - The only genuinely new payload, intel-media-va-driver-non-free, does not fix anything +# here. Installed live alongside vainfo on the running add-on, VA-API failed identically to +# the free driver — `iHD_drv_video.so init failed` on both /dev/dri/renderD128 and card0, +# with both 23.1.1 builds. The failure is below the add-on, in the host i915/GuC stack. +# Repairing the guard would therefore have turned dead code into an untested apt transaction +# that swaps a base-image package for no measured benefit. # Install the current upstream hadolint and actionlint releases for both supported ARG HADOLINT_VERSION=v2.14.0 diff --git a/claude_desktop/README.md b/claude_desktop/README.md index 5d6665cede..ffcbf587c2 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -97,8 +97,7 @@ Git synchronization hooks. A repository is indexed only when it is listed in | `KEYBOARD` | | Optional Selkies keyboard layout. | | `PASSWORD` | | Optional password for direct Selkies ports. | | `DRINODE` | | Optional GPU device override for Selkies. | -| `gpu_acceleration` | `auto` | Whether Claude Desktop renders on the GPU. `auto` adds Chromium's ANGLE/EGL flags only when a probe confirms a hardware GL context is available, `on` forces them without probing, `off` keeps software rendering. Use `on` with care: it skips every safety check, so on a host that cannot actually drive those flags the desktop can come up black — set the option back to `auto` or `off` to recover. | -| `max_resolution` | `1920x1080` | Caps the virtual screen. Selkies still resizes dynamically below this; raise it only if you drive the desktop from a larger display. | +| `MAX_RES` | _(unset)_ | Optional cap on the virtual screen, as `WIDTHxHEIGHT` (100-9999 per axis). Unset means the base image default, 15360x8640 — Selkies resizes dynamically below whatever the cap is, so this only sets the ceiling. Named `MAX_RES` because that is the environment variable the base image's Xvfb service reads. Setting it lowers the area Xvfb and the Selkies capture loop track for damage; the framebuffer itself is lazily populated, so this is a CPU saving, not a memory one. | | `DNS_server` | `8.8.8.8` | DNS server used by the standard DNS module. | | `permission_mode` | `auto` | Claude Code permission policy: `strict`, `auto`, or `bypass`. | | `install_headroom` | `true` | Register Headroom MCP and run the supervised local proxy. | diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index 331d08889e..96b9e5f415 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -56,7 +56,6 @@ options: github_username: "" enable_tools_health_report: true expose_headroom_dashboard: false - gpu_acceleration: auto headroom_auto_compress: true headroom_wrap_claude_code: true install_caveman: false @@ -66,7 +65,6 @@ options: install_headroom: true install_rtk: true install_tokensave: true - max_resolution: 1920x1080 mcp_servers_desktop: - headroom - tokensave @@ -98,6 +96,7 @@ schema: data_location: str? DRINODE: list(/dev/dri/card0|/dev/dri/card1|/dev/dri/card2|/dev/dri/renderD128|/dev/dri/renderD129|)? KEYBOARD: list(da-dk-qwerty|de-de-qwertz|en-gb-qwerty|en-us-qwerty|es-es-qwerty|fr-ch-qwertz|fr-fr-azerty|it-it-qwerty|ja-jp-qwerty|pt-br-qwerty|sv-se-qwerty|tr-tr-qwerty)? + MAX_RES: match(^[1-9][0-9]{2,3}x[1-9][0-9]{2,3}$)? PASSWORD: str? PGID: int PUID: int @@ -118,7 +117,6 @@ schema: github_username: str? enable_tools_health_report: bool expose_headroom_dashboard: bool - gpu_acceleration: list(auto|on|off)? headroom_auto_compress: bool? headroom_wrap_claude_code: bool install_caveman: bool @@ -128,7 +126,6 @@ schema: install_headroom: bool install_rtk: bool install_tokensave: bool - max_resolution: str? mcp_servers_desktop: - list(headroom|tokensave|homeassistant|codex) mcp_servers_code: @@ -139,5 +136,5 @@ schema: slug: claude_desktop udev: true url: https://github.com/alexbelgium/hassio-addons -version: "2026.08.03" +version: "2026.08.04" video: true diff --git a/claude_desktop/rootfs/defaults/autostart b/claude_desktop/rootfs/defaults/autostart index 85ee09cfdc..aa1fab8596 100644 --- a/claude_desktop/rootfs/defaults/autostart +++ b/claude_desktop/rootfs/defaults/autostart @@ -21,48 +21,28 @@ # force-overrides ANTHROPIC_BASE_URL (headroom #869), so Desktop uses the registered Headroom # MCP tools instead. -# GPU acceleration. +# GPU acceleration: deliberately no flags. Do not add ANGLE/EGL flags here. # -# Under Xvfb, Chromium probes GLX, finds only Xvfb's indirect/software path, and falls back to -# rendering everything on the CPU (`--use-gl=disabled` on the GPU process, and -# `--disable-gpu-compositing` on the renderer). On a small Home Assistant host that is the -# add-on's single largest CPU consumer. +# Chromium already renders on the GPU on this base image, because LSIO's Xvfb is started with +# `-vfbdevice /dev/dri/renderD128` — so GLX here is backed by the real render node, not the +# indirect/software path. Left alone, Chromium picks Mesa's GLX (libGLX_mesa), initialises the +# GPU process, and composites on the GPU. # -# Routing Chromium through ANGLE's OpenGL backend over EGL uses the real render node instead. -# The flags are only added when /usr/local/bin/claude-gpu-probe confirms that Claude Desktop's -# own bundled ANGLE can create a hardware GL context here, because forcing them on a host with -# no render node (or one where Mesa falls back to llvmpipe) trades a working software desktop -# for a black window or a GPU-process crash loop. Any probe failure leaves the command line -# untouched, i.e. exactly the pre-existing software-rendering behaviour. +# Passing `--use-gl=angle --use-angle=gl-egl` actively *broke* that. It forces Mesa's EGL X11 +# platform, which offers no window-capable EGLConfig under this Xvfb, so the GPU process logged +# ui/gl/gl_surface_egl.cc:262 No suitable EGL configs found. +# gave up on GL entirely, and Chromium relaunched it with `--use-gl=disabled` while stamping +# `--disable-gpu-compositing` on every renderer — i.e. the flags caused the CPU rendering they +# were meant to remove. Measured three times, including at the production 15360x8640 screen: +# with the flags the GPU process loads libEGL_mesa and holds 1 fd on the render node; with no +# flags it loads libGLX_mesa and holds 8, and no renderer carries --disable-gpu-compositing. # -# `--use-angle=gles-egl` is deliberately not used: Mesa rejects it with "Intel or NVIDIA -# OpenGL ES drivers are not supported". +# A standalone ANGLE probe is not evidence for any of this: Claude Desktop's bundled ANGLE +# happily creates a hardware context here via its default (GLX) path, which is why the probe +# that used to gate these flags passed while the flags themselves disabled the GPU. # -# /run/claude-desktop-gpu-mode is written each boot by /etc/cont-init.d/85-openbox_autostart.sh -# from the `gpu_acceleration` add-on option (auto|on|off). -GPU_FLAGS="" -GPU_MODE="auto" -if [ -r /run/claude-desktop-gpu-mode ]; then - GPU_MODE="$(cat /run/claude-desktop-gpu-mode)" -fi -case "$GPU_MODE" in - off) - echo "claude-desktop: gpu_acceleration=off; using software rendering" >&2 - ;; - on) - # Escape hatch for hosts where the probe is wrong in either direction. - GPU_FLAGS="--ozone-platform=x11 --use-gl=angle --use-angle=gl-egl" - echo "claude-desktop: gpu_acceleration=on; forcing ANGLE/EGL without probing" >&2 - ;; - *) - # Bounded: this sits in the desktop's startup path, and a driver that wedges during - # EGL init must not leave the user staring at an empty screen. A timeout is treated - # exactly like a failed probe, i.e. software rendering. - if timeout 15 claude-gpu-probe; then - GPU_FLAGS="--ozone-platform=x11 --use-gl=angle --use-angle=gl-egl" - fi - ;; -esac +# `--use-angle=gl` also works, but it only reproduces what Chromium already chooses by itself, +# so it is not passed either. # Shared memory. # @@ -80,7 +60,6 @@ if [ -n "$SHM_KB" ] && [ "$SHM_KB" -ge 262144 ]; then SHM_FLAGS="" fi -# GPU_FLAGS and SHM_FLAGS must stay unquoted so they expand to separate arguments, or to -# nothing at all. +# SHM_FLAGS must stay unquoted so it expands to a separate argument, or to nothing at all. # shellcheck disable=SC2086 -exec claude-desktop --no-sandbox --password-store=basic $SHM_FLAGS $GPU_FLAGS +exec claude-desktop --no-sandbox --password-store=basic $SHM_FLAGS diff --git a/claude_desktop/rootfs/etc/cont-init.d/22-display_tuning.sh b/claude_desktop/rootfs/etc/cont-init.d/22-display_tuning.sh deleted file mode 100755 index 084ac5d028..0000000000 --- a/claude_desktop/rootfs/etc/cont-init.d/22-display_tuning.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/with-contenv bashio -# shellcheck shell=bash -set -e - -# Cap the virtual screen the desktop is drawn on. -# -# The Selkies base image starts Xvfb at DEFAULT_RES=15360x8640 so that a client on any monitor -# can resize into it. Nothing here needs a 133-megapixel screen: it enlarges the area Xvfb and -# the Selkies capture loop track for damage on every frame, which the add-on pays for -# continuously — measurably so even with no browser connected at all. -# -# MAX_RES is the base image's own knob for this (svc-xorg prefers it over DEFAULT_RES) and it -# only sets the *maximum*; Selkies still resizes dynamically underneath it, so a smaller cap -# costs nothing until a client actually asks for something larger. -# -# This is a CPU and address-space saving, not a memory one: the framebuffer is a lazily -# populated SysV shared segment, so the unused portion of the oversized screen was never -# resident to begin with. -# -# Note SELKIES_MANUAL_WIDTH/HEIGHT is a different knob that *pins* the resolution and disables -# dynamic resizing. It is deliberately not used here. -MAX_RESOLUTION="$(bashio::config 'max_resolution' '1920x1080')" - -if [ -z "$MAX_RESOLUTION" ]; then - bashio::log.info "max_resolution is empty; leaving the base image's default virtual screen size" - exit 0 -fi - -# Matched with bash's own =~ rather than grep: grep anchors per *line*, so a multi-line value -# such as "1920x1080\n640x480" satisfies ^...$ on its first line and would be passed through to -# Xvfb verbatim. Bash anchors the whole string, so an embedded newline is rejected. -if [[ ! "$MAX_RESOLUTION" =~ ^[0-9]{1,5}x[0-9]{1,5}$ ]]; then - bashio::log.warning "max_resolution '${MAX_RESOLUTION}' is not WIDTHxHEIGHT; leaving the base image default" - exit 0 -fi - -# A syntactically valid but nonsensical size (0x0, 99999x99999) would either stop Xvfb from -# starting at all or ask it for a framebuffer larger than the default this option exists to -# shrink. Bound it to something Xvfb and Selkies can actually serve; the upper bound is the -# base image's own default, so this option can only ever reduce the screen. -MAX_WIDTH="${MAX_RESOLUTION%%x*}" -MAX_HEIGHT="${MAX_RESOLUTION##*x}" -if [ "$MAX_WIDTH" -lt 640 ] || [ "$MAX_HEIGHT" -lt 480 ] || - [ "$MAX_WIDTH" -gt 15360 ] || [ "$MAX_HEIGHT" -gt 8640 ]; then - bashio::log.warning "max_resolution '${MAX_RESOLUTION}' is outside the supported range (640x480 to 15360x8640); leaving the base image default" - exit 0 -fi - -# cont-init.d completes before any s6-rc service starts, so svc-xorg picks this up on the same -# boot. Both paths are written because the base image's scripts read the legacy /var/run alias. -written=0 -for envdir in /var/run/s6/container_environment /run/s6/container_environment; do - if [ -d "$envdir" ]; then - printf '%s' "$MAX_RESOLUTION" > "${envdir}/MAX_RES" - written=1 - fi -done - -# Claiming success after writing nothing would send someone hunting for a cap that svc-xorg -# never saw. -if [ "$written" -eq 0 ]; then - bashio::log.warning "No s6 environment directory found; leaving the base image's default virtual screen size" - exit 0 -fi - -bashio::log.info "Virtual screen capped at ${MAX_RESOLUTION} (Selkies still resizes dynamically below this)" diff --git a/claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh b/claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh index 70960d5f98..feb6b60dd9 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/85-openbox_autostart.sh @@ -15,26 +15,6 @@ set -e # regardless, so every boot picks up the current /defaults/autostart content; ownership/mode # is left in the normal abc-writable state that init-selkies-config itself uses when # RESTART_APP is unset, and re-locked by that oneshot afterward if RESTART_APP is set. -# The autostart decides whether to hand Chromium the ANGLE/EGL flags, but it runs as abc under -# openbox where bashio is not available. Publish the resolved option to a file it can read. -# /run is tmpfs, so this is rewritten on every boot and never goes stale. -GPU_MODE="$(bashio::config 'gpu_acceleration' 'auto')" -case "$GPU_MODE" in - auto | on | off) ;; - *) - bashio::log.warning "Unknown gpu_acceleration '${GPU_MODE}'; falling back to auto" - GPU_MODE="auto" - ;; -esac -# Best-effort: the autostart falls back to "auto" when the file is absent, so a failure here -# must not abort this script under errexit and leave the openbox autostart unsynced. -if printf '%s\n' "$GPU_MODE" > /run/claude-desktop-gpu-mode 2> /dev/null; then - chmod 0644 /run/claude-desktop-gpu-mode - bashio::log.info "GPU acceleration mode: ${GPU_MODE}" -else - bashio::log.warning "Could not write /run/claude-desktop-gpu-mode; Claude Desktop will probe for GPU support (auto)" -fi - if [ -f /defaults/autostart ]; then mkdir -p "$HOME/.config/openbox" cp -f /defaults/autostart "$HOME/.config/openbox/autostart" diff --git a/claude_desktop/rootfs/usr/local/bin/claude-gpu-probe b/claude_desktop/rootfs/usr/local/bin/claude-gpu-probe deleted file mode 100755 index 95bb4f0b40..0000000000 --- a/claude_desktop/rootfs/usr/local/bin/claude-gpu-probe +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -"""Decide whether Claude Desktop can render on the GPU on this host. - -Claude Desktop is Electron/Chromium. Left to itself under Xvfb it probes GLX, finds -only the indirect/software path Xvfb offers, and gives up: the GPU process ends up -running `--use-gl=disabled` and the renderer `--disable-gpu-compositing`, so every -frame is rastered and composited on the CPU. On a small Home Assistant host that is -the single largest CPU consumer the add-on has. - -The fix is to point Chromium at ANGLE's OpenGL backend over EGL instead of GLX, but -those flags are only safe where they actually work — forcing them on a host with no -render node, or where Mesa falls back to a software rasterizer, trades a working -software desktop for a black window or a GPU-process crash loop. - -So rather than guessing from the presence of /dev/dri, this probe exercises the exact -code path Chromium will use: it loads Claude Desktop's *own bundled ANGLE* libEGL, -initializes the OpenGL backend, creates a real pbuffer context, and reads GL_RENDERER -back. Exit 0 means Chromium's GL stack is known-good here; any other exit means the -caller must leave Chromium alone and keep today's software rendering. - -Notes for future readers: - * ANGLE's OpenGL backend needs a reachable X display, so this must run after Xorg is - up (i.e. from the openbox autostart, not from cont-init.d). - * `gles-egl` is deliberately not attempted: Mesa reports "Intel or NVIDIA OpenGL ES - drivers are not supported" and ANGLE refuses to initialize. - * A renderer string naming SwiftShader/llvmpipe/softpipe is a *failure* here. That is - software rendering wearing a GL hat, and forcing the flags for it would add ANGLE - translation overhead on top of the CPU rasterization we are trying to avoid. -""" - -import ctypes -import os -import sys - -LIBDIR = "/usr/lib/claude-desktop" - -# EGL/ANGLE constants (see ANGLE's eglext.h); hardcoded to avoid a build dependency. -EGL_NONE = 0x3038 -EGL_PLATFORM_ANGLE_ANGLE = 0x3202 -EGL_PLATFORM_ANGLE_TYPE_ANGLE = 0x3203 -EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE = 0x320D -EGL_OPENGL_ES_API = 0x30A0 -EGL_SURFACE_TYPE = 0x3033 -EGL_PBUFFER_BIT = 0x0001 -EGL_RENDERABLE_TYPE = 0x3040 -EGL_OPENGL_ES2_BIT = 0x0004 -EGL_WIDTH = 0x3057 -EGL_HEIGHT = 0x3056 -EGL_CONTEXT_CLIENT_VERSION = 0x3098 -GL_VENDOR = 0x1F00 -GL_RENDERER = 0x1F01 - -SOFTWARE_MARKERS = ("swiftshader", "llvmpipe", "softpipe", "lavapipe", "software rasterizer") - - -class ProbeFailure(Exception): - """Raised when this host cannot give Chromium a hardware GL context.""" - - -def log(message): - """Write a probe diagnostic to stderr, where it lands in the add-on log.""" - sys.stderr.write(f"claude-gpu-probe: {message}\n") - - -def load_angle(): - """Load Claude Desktop's bundled ANGLE and declare the signatures we call.""" - egl_path = os.path.join(LIBDIR, "libEGL.so") - gles_path = os.path.join(LIBDIR, "libGLESv2.so") - if not (os.path.exists(egl_path) and os.path.exists(gles_path)): - raise ProbeFailure(f"bundled ANGLE libraries not found under {LIBDIR}") - - try: - egl = ctypes.CDLL(egl_path, mode=ctypes.RTLD_GLOBAL) - gles = ctypes.CDLL(gles_path, mode=ctypes.RTLD_GLOBAL) - except OSError as err: - raise ProbeFailure(f"could not load bundled ANGLE: {err}") from err - - egl.eglGetProcAddress.restype = ctypes.c_void_p - egl.eglGetError.restype = ctypes.c_int - egl.eglInitialize.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(ctypes.c_int), - ctypes.POINTER(ctypes.c_int), - ] - egl.eglCreatePbufferSurface.restype = ctypes.c_void_p - egl.eglCreateContext.restype = ctypes.c_void_p - gles.glGetString.restype = ctypes.c_char_p - gles.glGetString.argtypes = [ctypes.c_uint] - return egl, gles - - -def open_angle_display(egl): - """Initialize ANGLE's OpenGL backend and return its EGL display.""" - addr = egl.eglGetProcAddress(b"eglGetPlatformDisplayEXT") - if not addr: - raise ProbeFailure("bundled ANGLE has no eglGetPlatformDisplayEXT") - get_platform_display = ctypes.CFUNCTYPE( - ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int) - )(addr) - - attrs = (ctypes.c_int * 3)( - EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE, EGL_NONE - ) - display = get_platform_display(EGL_PLATFORM_ANGLE_ANGLE, None, attrs) - if not display: - raise ProbeFailure(f"no ANGLE OpenGL display (egl error 0x{egl.eglGetError():x})") - - major, minor = ctypes.c_int(), ctypes.c_int() - if not egl.eglInitialize(ctypes.c_void_p(display), ctypes.byref(major), ctypes.byref(minor)): - raise ProbeFailure( - f"ANGLE OpenGL backend failed to initialize (egl error 0x{egl.eglGetError():x})" - ) - return display - - -def make_current_context(egl, display): - """Bring up a real pbuffer context. - - Initialization alone is not proof of anything: only a current context makes - GL_RENDERER report the driver Chromium would actually be handed. - """ - egl.eglBindAPI(EGL_OPENGL_ES_API) - config = ctypes.c_void_p() - count = ctypes.c_int() - config_attrs = (ctypes.c_int * 5)( - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_NONE - ) - chosen = egl.eglChooseConfig( - ctypes.c_void_p(display), config_attrs, ctypes.byref(config), 1, ctypes.byref(count) - ) - if not chosen or count.value == 0: - raise ProbeFailure(f"no usable EGL config (egl error 0x{egl.eglGetError():x})") - - surface_attrs = (ctypes.c_int * 5)(EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE) - surface = egl.eglCreatePbufferSurface(ctypes.c_void_p(display), config, surface_attrs) - if not surface: - raise ProbeFailure(f"could not create pbuffer surface (egl error 0x{egl.eglGetError():x})") - - context_attrs = (ctypes.c_int * 3)(EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE) - context = egl.eglCreateContext(ctypes.c_void_p(display), config, None, context_attrs) - if not context: - raise ProbeFailure(f"could not create GL context (egl error 0x{egl.eglGetError():x})") - - if not egl.eglMakeCurrent( - ctypes.c_void_p(display), - ctypes.c_void_p(surface), - ctypes.c_void_p(surface), - ctypes.c_void_p(context), - ): - raise ProbeFailure( - f"could not make the GL context current (egl error 0x{egl.eglGetError():x})" - ) - - -def describe_renderer(gles): - """Return (renderer, vendor), rejecting software rasterizers.""" - renderer = (gles.glGetString(GL_RENDERER) or b"").decode(errors="replace") - vendor = (gles.glGetString(GL_VENDOR) or b"").decode(errors="replace") - if not renderer: - raise ProbeFailure("GL context reported no renderer") - - lowered = renderer.lower() - if any(marker in lowered for marker in SOFTWARE_MARKERS): - raise ProbeFailure( - f"software renderer ({renderer}); leaving Chromium on its own software path" - ) - return renderer, vendor - - -def main(): - """Exit 0 only when Chromium's GL stack is known-good on this host.""" - if not os.environ.get("DISPLAY"): - log("no DISPLAY; ANGLE's OpenGL backend needs an X server") - return 1 - - try: - egl, gles = load_angle() - display = open_angle_display(egl) - make_current_context(egl, display) - renderer, vendor = describe_renderer(gles) - except ProbeFailure as err: - log(str(err)) - return 1 - # A probe is advisory: whatever goes wrong in these native calls, the desktop must still - # start. Any unexpected failure is reported and treated as "no GPU". - # pylint: disable=broad-exception-caught - except Exception as err: - log(f"unexpected probe error: {err}") - return 1 - - log(f"hardware GL available: {renderer} | {vendor}") - return 0 - - -if __name__ == "__main__": - sys.exit(main())