mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-07 10:46:06 -10:00
feat: non-Adreno graphics stack (Venus, open driver pairs, gamescope patches) (#2029)
* Retain compositor resources when GPU completion is unconfirmed * Stop quarantined GPU callers before retries and mapped writes * Support Android hardware-buffer presentation over Wayland * Support PowerVR swapchains and linear image imports in Gamescope * Bundle verified PowerVR graphics components through existing build preparation * Detect and select the system Vulkan backend on PowerVR * Apply PowerVR Steam and game compatibility during normal launches * Show PowerVR support through existing driver and component controls * Update existing hardware guidance for system Vulkan support * Import the shared system Vulkan readiness check * Discover and propagate the actual PowerVR render node * Prune obsolete PowerVR runtimes after guest processes exit * Retain external DMA-BUF ownership after GPU quarantine * Name the PowerVR HAL selection explicitly * Isolate component UI tests from application startup * Cover the inherited FEX preset in PowerVR profile tests * Log test worker failures for CI diagnosis * Remove temporary test logging after diagnostic CI * Prefer system dependencies for the Android Vulkan loader * Capture raw stderr before test worker termination * Remove the temporary test worker crash probe * Import pinned DXVK recipe prerequisite from PowerVR PR570 Copy only the standalone build recipe, source pin, BC-format prerequisite and presentation-sync patch/check from wallentx/DroidDeck atd0f7264ea8. Source: https://github.com/Droid-Deck/DroidDeck/pull/570 Preserve that proposal attribution; no Android app, compositor, managed pack, private data or fork development history is imported. * Add opt-in low32 GPU mapping adapter and integrity probes * Add opt-in D3D9 single-sample workaround and fix HUD texel format * Document verified production game workaround and publication scope * Restrict reset diagnostics and create private output atomically * Target official runtime review and document PR570 prerequisite provenance * Fix runtime tool output isolation, mmap64 chaining and DST ambiguity * Normalize swapchain backbuffers for the opt-in D3D9 single-sample path * Experimental Samsung Xclipse support on the system Vulkan path Detect Samsung's Vulkan HAL (vulkan.samsung.so) and its sgpu render node alongside PowerVR, and run Xclipse through the same libhybris bridge: - VK_LAYER_DROIDDECK_xclipse (tools/linuxfs/xclipse-layer), built with the glibc preloads and enabled only on Xclipse: sets bionic's environ for the driver's shader compiler, avoids immutable YCbCr samplers in compute layouts, forwards the KHR calibrated-timestamp entry points to the EXT ones, and reports textureCompressionBC and BC4/BC5 filtering over the compat layer's BC emulation. - Upstream DXVK 2.6.2, pinned as a tar.gz component and selected on Xclipse: the driver has 128 bytes of push constants, DXVK 3 needs 256. - android_wlegl accepts a zero stride and passes it through unchanged: Samsung's gralloc records none for GPU-only buffers. Tested on a Galaxy A56 (Xclipse 540, driver 24.0.560): Steam Big Picture and PICO PARK (D3D9). * Xclipse layer: copy the device chain and track samplers per device - Clear textureCompressionBC in a copy of the device-create chain (every structure up to the VkPhysicalDeviceFeatures2 that enables it) instead of writing the caller's structure; a chain led by a structure of unknown size is passed on unchanged. - Track YCbCr samplers per device in a growable list, drop them on vkDestroySampler and device teardown, and fail sampler creation rather than leave one untracked when the list cannot grow. - Fail descriptor set layout creation with VK_ERROR_OUT_OF_HOST_MEMORY when a replacement sampler array cannot be allocated, instead of passing the original YCbCr samplers to the driver. * Xclipse layer: copy any device-chain structure the headers define The chain copy that clears the emulated textureCompressionBC only knew a handful of structure sizes, so a valid chain such as VkPhysicalDeviceTimelineSemaphoreFeatures -> VkPhysicalDeviceFeatures2 was passed on unchanged and the compat layer refused the bit. Size every structure that may extend VkDeviceCreateInfo from a table generated from the Vulkan registry (gen_device_chain.py, Vulkan-Headers 1.3.275, the headers the build compiles with), and copy onto the heap instead of a fixed buffer. Only a structure newer than those headers still passes the chain unchanged. Add a regression test that runs the layer between a simulated loader and a mock driver: the timeline-semaphore chain, pEnabledFeatures, an unknown structure, the caller's structures left untouched, per-device YCbCr sampler tracking with handle reuse, and the KHR calibrated-timestamp forwarding. CI installs libvulkan-dev for it. * Mali bridge (#517, by ASEbrahim) on the non-Adreno test branch Squash of Droid-Deck/DroidDeck#517 (ASEbrahim/DroidDeck mali-bridge @6b823341): the Vulkan bridge (tools/vkbridge) that runs the guest's Vulkan on the phone's own driver, with BC1-BC7, clip/cull distance, vertex-divisor and depth-clip emulation and dma-heap memory sharing for Mali. All of the bridge is ASEbrahim's work; squashed only to carry it on this test branch. Combined with #570 on this branch, a device draws through exactly one path: - Adreno: Turnip, unchanged (checked first; the bridge setting is ignored there). - PowerVR / Xclipse with the system HAL and a render node: the system-Vulkan path (#570). - Anything else with the Setup toggle on (Mali): the bridge. * Test branch: keep #570's DXVK build recipe so its pinned PowerVR components verify #602 adds output checks to tools/dxvk/build.sh; tools/powervr/fetch.py pins the exact recipe the published PowerVR DXVK was built from, so the branch carries #570's copy. * Test branch: make #570, #602 and #560/#561 pass each other's tests - tools/dxvk/build.sh: #602's copy (its output checks) and the PowerVR pin in tools/powervr/release.json moved to that file's hash; the recipe's outputs are unchanged. - droiddeck-session: the presentation block sets _bl_cef for #570's PowerVR path, and the UI block keeps main's order, defaulting to -cef-use-angle=vulkan when the block has not run. - Compositor test harnesses: AHardwareBuffer stubs for #570's AHB import, and a stub for vkp_effects_forget_image (main #597) that #561's caller harness predates. * Auto drivers for Mali and Xclipse on the non-Adreno test branch Auto now picks an open driver pair for the GPU, the way it does for Adreno: - Mali (no KGSL, /dev/mali0): the GPU's name ("Mali-G720-Immortalis MC12", from the vendor Vulkan driver or kbase's gpuinfo) picks the PanVK build: G52-G78 (Job Manager), G610/G710, G615 or G720. A Mali none of them covers (G925...), or one whose pair is not published yet, draws through the Mali bridge, switched on by Auto. - Xclipse keeps the system-driver path as Auto; the RADV pair is offered as a manual choice. - The pairs come from The412Banner/droiddeck-nonadreno-drivers (one pre-release per driver, each a DriverBundle zip). Only a device without an Adreno reads that repo. - The importers accept PanVK and RADV libraries (libvulkan_panfrost / libvulkan_radeon). - An open driver pair and the Mali bridge are alternatives: a selected pair turns the bridge off. - TU_DEBUG is only set for Turnip, on an Adreno. Every new branch is reached only when the device has no Adreno; an Adreno's detection, release sources, Auto choice and session environment are unchanged. * Auto: the one-binary PanVK kbase build for Mali v9-v13 on Android 15+ The412Banner/droiddeck-nonadreno-drivers adds panvk-mali-kbase: one PanVK build for G57/G68/G77/G78, G610/G710, G615/G715, G720/G725 and G925, whose display half needs Android 15. On Android 15+ Auto prefers it for every Mali it covers; below 15, and whenever its release is not listed, Auto falls back to the GPU generation's own pair. G52/G76 keep the Job Manager build; a G715/G725/G925 below Android 15, or any Mali no pair covers, draws through the bridge. Every pair stays selectable by hand. * Test: the G925 is in the kbase build's family now * Offer faux123's device PanVK builds (RP4 Pro, Ace, RG556) as manual pairs for their Mali * Detailed graphics logs for non-Adreno test reports - graphics-report.txt in every session folder: the graphics path the session draws with and why (Turnip, system Vulkan #570, Mali bridge #517, or an open driver pair with its release), the driver mode and choices, what Auto would pick, the GPU (family, Vulkan device name, kbase gpuinfo, Android, SoC, kernel), the GPU nodes the app can see (access, device numbers, SELinux label; nothing is opened) and the guest's graphics environment. At the end it adds the compositor's driver lines from app.log and the game launches that were logged. - vulkan-summary.txt: the session's own vulkaninfo --summary with the driver as exported, in the background with a 10 s timeout. - Per-game logs under games/<time>-<appid>/: Proton's log, DXVK's and vkd3d-proton's, Mesa's and PanVK's startup messages and the launch environment, from droiddeck-game-env. Each file is cut to 20 MB and the folder to 120 MB at the session's end. - The per-game logs and the Vulkan summary are on for a GPU that is not an Adreno, and on an Adreno only with Setup's new "Detailed graphics logs" switch. Share logs already takes the whole session folder, so all of it is in the zip, scrubbed as before. * Mali pairs: a stand-in render node, the driver's real library name, and a Mali known by its chip - Auto on Jim's Dimensity 9300+ chose the bridge: the GPU had no name (the app had not asked Vulkan yet and kbase's gpuinfo is unreadable under SELinux), so a Mali-G720 counted as a Mali no build covers. The name now falls back to the GPU the chip is known to carry (Dimensity, Helio, Kompanio, Tensor); and when nothing names it, Auto asks the vendor Vulkan driver off the main thread before it settles for the bridge. Auto logs what it decided and why. - An open Mali driver pair gets /dev/mali0 as the guest's render node, as the bridge does (bindGpuNode, BL_DRM_STANDIN): gamescope needs the node the driver reports. The session shim only answers libdrm's PRIME and GEM-close calls on it, which PanVK on kbase never makes. - A PanVK or RADV runtime half is installed as libvulkan_panfrost.so / libvulkan_radeon.so rather than under Turnip's name; Turnip keeps libvulkan_freedreno.so. - graphics-report.txt names the render node the guest got, adds the session script's driver lines and flags a failed Mali-bridge self-test at the top. * Auto takes the RADV pair on a Samsung Xclipse, with the system route as its fallback A tester's Galaxy S24 FE (Xclipse 940) boots to Steam with the RADV-Xclipse pair, where the system-driver route fails at Steam's OpenGL (glamor and GLX cannot load a driver). Auto now downloads and selects RADV on an Xclipse like the Mali pairs, and falls back to the system route only when RADV is not published. #570's reset of both drivers at each start no longer undoes an open driver pair. PowerVR (vulkan.powervr.so) keeps the system driver as before. * An Exynos with a Mali is a Mali, not an Xclipse The system route took vulkan.samsung.so as Samsung's Xclipse, but an Exynos 1280/1380 (Galaxy Tab S9 FE: Mali-G68) may name its HAL the same; with /dev/mali0 present that HAL is not the system route's, and the device is detected as the Mali it is. The chip table adds the Exynos 1380 and 1280 (s5e8835, s5e8825: Mali-G68). * Auto fetches the open drivers itself; Xclipse named as such; sound and frame-gen in the logs - Auto looks online again when its saved release list predates this device's open drivers, so a Samsung or Mali phone gets its pair without pressing refresh first. - A Samsung Xclipse is labelled "Samsung Xclipse" (not PowerVR), with its own support line. - Per-game logs add audio-streams.txt: PulseAudio's sink inputs 20 s and 60 s into the launch, so a silent game shows whether its stream reached Pulse. - graphics-report.txt states the frame-generation setting and keeps the LSFG gate, FrameGen and audio lines from app.log. * Client sound retries a briefly disconnected AAudio; kbase ioctl refusals explained in the report A Galaxy S24 FE's session had no sound at all: the DirectAudio sink's AAudio stream opened with AAUDIO_ERROR_DISCONNECTED while the output route was still changing at session start, and the plain AAudio fallback failed the same moment. The sink guard now retries DirectAudio, then the plain sink, five times over ten seconds and says which one it got. graphics-report.txt keeps the client-sink and kbase lines, and explains a KBASE_IOCTL_MEM_EXEC_INIT EPERM as the kbase driver's own answer rather than the session's sandbox. * Mali pairs draw the display on the phone's own Mali driver; Xwayland falls back without glamor - Testers' Mali pairs got the guest drawing (gamescope on the stand-in node, Zink on PanVK) but the compositor's PanVK Android half failed on Mali gralloc: it probes AHB formats 0x38/0x3b that mali_gralloc does not have, and "vkCreateImage w/ native buffer failed". Under Auto a Mali pair now leaves the display on the phone's own Vulkan driver (dma-buf, modifiers and AHB all pass the testers' checks) and runs the pair's PanVK in the guest; the Android half stays installed for a manual pick. "Pair in use" now follows the runtime half. - A session after one whose Xwayland crashed (segfault, then gamescope's X11 I/O error) starts Xwayland with XWAYLAND_NO_GLAMOR=1 on a non-Adreno device, and says so in app.log. - graphics-report.txt names the display driver the compositor used, and keeps gralloc, dma-buf, modifier and import lines from app.log and Xwayland's own lines from the session log. * Non-Adreno sound: the daemon falls back on its own; the build's commit in device.txt and the report The S24 FE's session still had no sound: its AAudio refuses DirectAudio's low-latency stream with AAUDIO_ERROR_DISCONNECTED, and the sink guard never got to retry because pactl did not reach the daemon in time ("the daemon did not answer in 15 s"). On a GPU that is not an Adreno the daemon's own start-up script now asks DirectAudio again without low latency and loads the plain AAudio sink underneath it, so a default sink exists whatever pactl does. device.txt and graphics-report.txt name the build label and source commit, so a log says which build it came from. * Non-Adreno sound: no second DirectAudio load (it hung the daemon); the plain AAudio sink underneath On the S24 FE the daemon stopped after DirectAudio's failed low-latency load: the second DirectAudio load in the same daemon never returned, so nothing after it ran and pactl never connected. The daemon now loads only the plain AAudio sink (no low latency) after DirectAudio, inside .nofail, and sets whichever exists as the default. * gamescope: explicit sync off where the kernel or the stand-in node cannot signal its timelines Adds tools/gamescope/patches/0126 (GAMESCOPE_DISABLE_EXPLICIT_SYNC=1 for the nested backends) and sets it on a GPU that is not an Adreno when the kernel is older than 6.6 (no DRM_IOCTL_SYNCOBJ_EVENTFD) or the guest's render node is Mali's stand-in. The graphics report lists it with the rest of the guest's graphics environment. * Test branch: gamescope with the explicit-sync switch (0126), pinned by sha Built by build-gamescope.yml run 38335744351 from0136fb61and published as a component release of The412Banner/droiddeck-nonadreno-drivers; the apk build fetches it by tag and checks the sha. * Sound: nothing that can block in the daemon's start-up; Steam's web compositing in software on open pairs - The S24 FE (US) daemon hung loading the plain AAudio sink from default.pa (its AAudio open blocks rather than fails), so it never finished starting. default.pa is back to DirectAudio alone inside .nofail; the fallbacks are loaded from the app once the daemon answers, each pactl bounded by its timeout (DirectAudio, then the plain sink with and without low latency). The guard waits 30 s and logs, with timestamps, how long the daemon took, whether its socket and pactl exist, and what it finally got. - With an open driver pair on a GPU that is not an Adreno, Steam's CEF composites in software (-cef-disable-gpu-compositing) with GPU raster kept: RADV on Xclipse showed stale, half-written tiles, as #570 saw on PowerVR. The session log says so. * Mali under Auto: the display always on Android's own Vulkan loader; a reboot hint for refused audio Axel's Tab S9 FE still drew its display with the PanVK Android half, set by an earlier build, and stalled at "Waiting for Steam's first frame" on Mali gralloc's format probes. Under Auto a Mali's display now goes to Android's own loader whatever was set before (DriverMenus resets it, and the compositor's driver choice ignores a pair's Android half), with no HAL path to find first. A pair counts as in use when its runtime half is. A manual pick of the PanVK display is kept. When audio.log shows AAUDIO_ERROR_DISCONNECTED, the report and the client-sink line say Android refused the output and that rebooting the phone usually fixes it. * Fix: call SessionPrefs.gpuDriverMode through its instance from Java * Compositor: log each new dma-buf format/modifier/size it imports (for colour and swizzle reports) Steam's software web compositing already covers every open driver pair, Mali included. * Open driver pairs: vkd3d-proton reports the driver's caps; buffer attach/release tracing; Mesa log - With a PanVK or RADV pair (BL_OPEN_DRIVER=1), droiddeck-game-env drops the app-wide VKD3D_FEATURE_LEVEL=12_2 and VKD3D_SHADER_MODEL=6_9 so vkd3d-proton reports what the driver has (an Unreal DX12 game crashed at start on PanVK with the overrides); a game's own setting still wins, and launch.txt records DROIDDECK_VKD3D_CAPS=driver. - The compositor traces, on a GPU that is not an Adreno, the first 400 dma-buf attaches and buffer releases by wl_buffer id and sending line, and logs any release of a buffer already released and not attached again (gamescope's "Compositor released us but we were not acquired" on G925). - Per-game logs no longer set MESA_DEBUG=silent, which kept mesa.log empty. * Tests: the dma-buf quarantine harness follows send_buffer_release's tracing macro * Mali (Venus): David Cimo's Venus route sources and recipes From DavidCimo/droiddeck-mali, branch mali-venus @ 4f838891 (prototype 13; his 12 commits on top of DroidDeck 0.3.1, GPL-3.0), by David Cimo: - tools/venus: Mesa Venus vtest patch (incl. the per-queue submit_mutex fix and the cached-memory frame readback), virglrenderer Android patch, the Mali compatibility layer (the six features DXVK needs: fill modes, multi-viewport, robustness2, clip/cull distance, BC1-BC7) and his tools. - tools/linuxfs/preload/venus.c: keeps Zink's kopper under Venus. - tools/gamescope/patches/0127 (his 0114): shm buffers uploaded on every commit. - docs/development/mali-venus.md: his design notes. - tools/venus/ci-build.sh and build-venus.yml: his build recipes as one CI job that packs the component (vtest server, ICD, layer) as venus.tzst for the apk. Co-authored-by: Dersim Abbas <[email protected]> * Mali (Venus) as a selectable route, by David Cimo Ports David Cimo's Venus route (DavidCimo/droiddeck-mali, mali-venus @ 4f838891) onto this branch as "Mali (Venus)", one tap in Setup next to the Mali bridge: - VenusComponent (his): stages the Venus ICD and runs virglrenderer's vtest server over the phone's own Vulkan driver; the guest gets VK_ICD_FILENAMES, VN_DEBUG=vtest, VTEST_SOCKET_NAME and MESA_VK_WSI_DEBUG=sw. - Selection rule: Venus when Setup's "Mali (Venus)" is on, on a GPU that is neither an Adreno nor a system-driver one (PowerVR, Xclipse), and the apk carries it. It then replaces the Mali bridge and any open driver pair for the session, and the display stays on the phone's own driver. Auto is unchanged (the PanVK pair). Adreno never takes any of it. - The compositor enables only the dma-buf extensions the driver has, logs a vendor driver's extension list, and gives a zero row pitch a packed row (his vk_present.c changes). - The Mali compatibility layer is staged into the runtime on non-Adreno devices only. - The component (vtest server, ICD, layer) comes from build-venus.yml as venus.tzst, published on The412Banner/droiddeck-nonadreno-drivers and pinned by sha in tools/venus/release.env. - gamescope 3.16.29-p15 adds his shm upload patch (0127) next to 0126; pinned by sha. - device.txt and graphics-report.txt name the route. Co-authored-by: Dersim Abbas <[email protected]> * Mali: Venus gamescope without robustness2, the bridge switch honoured and kept, clean driver switches - gamescope 0128: VK_EXT_robustness2 is optional. A Mali driver through Venus has none, and gamescope stopped at "Missing required extension: VK_EXT_robustness2" (the wayland_surface error after it was only the knock-on). Without it, unused descriptor slots point at a 1x1 texture instead of VK_NULL_HANDLE. (David Cimo's Pixel 9 driver has robustness2, so his build never needed this.) gamescope component rebuilt and pinned by sha. - Open-files limit: on a GPU that is not an Adreno the session script raises the soft limit to the hard limit for the whole session tree (a PanVK-kbase session froze on "Too many open files" after ten minutes) and graphics-report.txt shows before and after. A stopgap; the driver leak is fixed separately. - The Mali bridge switch is honoured: when on and usable it wins over a PanVK pair for the session (Venus still wins over it). Selecting a pair no longer switches the bridge off, which is why the switch did not survive an app restart (Auto re-selected the pair at start). Setup says when this phone cannot use the bridge at all. - Driver switching: picking a Mali pair by hand keeps the display on the phone's own driver, as Auto does (a PanVK Android display stalls on Mali gralloc at "Waiting for Steam's first frame", and stayed set after switching back); installing a bundle again replaces it instead of adding a "-2" copy beside the old one. - graphics-report.txt names the runtime driver's folder and libraries, and the previous session's drivers, flagged when they changed. * gamescope p16 (0128 robustness2 optional), pinned by sha; graphics-report says who chose the display driver * Bundle replace only for open non-Adreno pairs; Adreno bundles keep their numbered copies * Rebuilt open drivers reach phones: re-downloaded when their digest changes, release list hourly on non-Adreno The non-Adreno driver releases replace their assets in place (the PanVK kbase sync-file fd leak fix). A download is now remembered with its GitHub sha256, and an asset whose digest changed counts as not installed, so Auto fetches it again and the bundle replaces the old one; the release list is re-read after an hour instead of a day on a GPU that is not an Adreno. * Mali compatibility layer: multiDrawIndirect, depthClipEnable and robustness2 for DXVK on Venus A Galaxy A15 (Mali-G57) on Venus got gamescope and Steam up, but DXVK refused the GPU: "Device does not support required feature 'multiDrawIndirect'". DXVK 3.1 stops at the first missing feature, so the rest of its required list (dxvk_device_info.cpp) was checked against what the testers' Mali drivers cannot have, and covered in the same pass, on top of David Cimo's six: - multiDrawIndirect: reported, taken off the device, and an indirect draw with drawCount > 1 is recorded as single draws at offset + i * stride; maxDrawIndirectCount reported as 2^30. (drawIndirectCount is optional for DXVK and stays as the driver says.) - VK_EXT_depth_clip_enable (required): offered when the driver lacks it; depthClipEnable off in a pipeline becomes depthClampEnable, and the extension's structures never reach the driver. - VK_EXT_robustness2 missing entirely (gamescope saw the same on these phones): offered with robustBufferAccess2 (as robustBufferAccess, as before) and nullDescriptor, emulated by small stand-ins (a zeroed buffer, a 1x1 image) for VK_NULL_HANDLE buffers, image views and vertex buffers in plain, template and push descriptor writes and vertex-buffer binds. A null texel-buffer view stays null (its format is not known). The layer logs every feature it emulates per device. * Venus component22694a38(layer: multiDrawIndirect, depthClipEnable, robustness2), pinned by sha * Non-Adreno round 3: bridge images, Venus GL, shared buffers, Steam GPU drawing - vkbridge: the memory model skips lazily allocated types. Mali-G615 lists DEVICE_LOCAL|LAZILY_ALLOCATED first; taken as the device-local type it left images with no memory type (self-test "image clear/draw: setup" FAIL, gamescope "No compatible image memory type"). - Session: a bridge self-test that fails images or draws falls back to the runtime's driver pair for that session (BL_VKBRIDGE_FALLBACK_ICD); Setup and the graphics report say so. - Venus: the Mali compatibility layer runs in every process but gamescope (DROIDDECK_MALI_COMPAT=1), so Zink in Steam and Xwayland gets nullDescriptor; null texel-buffer views get stand-ins too. Without nullDescriptor the session's GL goes to llvmpipe. - gamescope 0129: a refused compute pipeline is named with its SPIR-V capabilities. - Compositor: a buffer on two surfaces at once (gamescope's planes while a notification is up) is released only when the last one lets go. - Open driver pairs: processes.txt timeline for the first three minutes; Setup "Steam interface: GPU drawing" turns off software web compositing. * Pin gamescope p17 (0129) and Venus55b30fbf* Non-Adreno round 4: Mali GPU-drawing block, gamescope YCbCr retry, GL probe, bridge draw sync - Setup's "Steam interface: GPU drawing" is hidden on Mali and a saved "on" is cleared: with PanVK it rebooted an Edge 60 (G615) as steamwebhelper started. Kept for Xclipse. - gamescope 0130: a composite pipeline refused at start-up is retried with a layout without immutable YCbCr samplers (G615 through Venus refused every composite shader; their SPIR-V has no Float16, so fp16 was not the cause). - Venus: droiddeck-glprobe opens GL through GLX the way the Steam client does, with the loader's reasons logged; when it fails the client's GL goes to llvmpipe. - vkbridge self-test: the draw check waits for the render pass's stores before copying out (a tiler read zeros without the dependency). * Pin gamescope p18 (0130) * Non-Adreno round 5: gamescope memory fallback, Venus destroy guard, OpenAL depot - gamescope 0131: DEVICE_LOCAL|HOST_VISIBLE requests fall back to host-visible memory (the bridge's unified-memory model lists none with both), with the request, type bits and memory types logged on failure; 0130's probe pipeline is kept and skipped when forced. - Venus: GAMESCOPE_NO_YCBCR_SAMPLERS=1 from the start; virglrenderer ignores the destroy of a pipeline it never created instead of marking the context fatal (vtest server exit 137 on a Mali-G615). - Steam's OpenAL depot (229020) maps to oalinst_dll, so games that list it (Hotline Miami) get OpenAL in their prefix. * Pin gamescope p19 (0131) and Venus6199a93f* Non-Adreno round 6: PowerVR PRIME on old kernels, thermal, hang-up names, bridge flippable images, Venus Zink variants - drm.c: PR #570's current PRIME ownership check (F_DUPFD_QUERY, then kcmp, then the F_SETOWN_EX owner marker): on a kernel before 6.10 with kcmp blocked, drmPrimeFDToHandle answered EOPNOTSUPP for every PowerVR import (Pixel 10, Android 17: Zink "failed drmPrimeFDToHandle", Xwayland error 7, gamescope X11 I/O error). The Mali stand-in node is kept. - A /sys/class/thermal the kernel will not list gets an empty directory, so mangoapp no longer aborts on it. - gamescope 0132: a hung-up waitable is named before the abort; 0133: the bridge's flippable images use supported-but-unexportable modifiers instead of asserting. - Venus: when Zink does not start, the loader's view is logged and Zink is retried without ZINK_DEBUG=compact, with ZINK_DESCRIPTORS=auto and without kopper before falling back to llvmpipe. * Pin gamescope p20 (0132, 0133) * Non-Adreno round 7: bridge command-buffer guard, Venus Zink trace, PowerVR mangoapp, Xclipse label - vkbridge server: every command buffer a device hands out is tracked; Begin, End (stream replay) and QueueSubmit refuse one it never allocated or has freed (VK_ERROR_DEVICE_LOST, logged) instead of passing it to the driver, where Mali's vkBeginCommandBuffer crashed the app (SIGSEGV 0x10, Steam's Zink on an Edge 60 and a Tab S11). - vkbridge server: a fault inside the driver while serving a request cuts that client off (SIGSEGV/SIGBUS guard per request) and leaves its objects alone; the app and the other clients carry on. Other faults go to the previous handler. - vkbridge server: the pipeline cache directory is copied; the JNI string it pointed at was freed (garbage in the cache path). - Venus: DROIDDECK_MALI_COMPAT_TRACE=1 makes the Mali compatibility layer log what an application queries before vkCreateDevice; a failed glprobe adds a traced run and vulkaninfo --summary under the same environment. - PowerVR: mangoapp draws with llvmpipe (Zink could not make it a swapchain and it crashed at every start). - Setup: an Xclipse GPU reads "Experimental: RADV or the system Xclipse driver", not the PowerVR line. * Pin Venuse40cd361(layer trace) * Non-Adreno round 8: bridge sharing fallback and crash detail, Venus diagnostics and 1.2 gate, compositor disconnect reasons, RADV upstream - Mali bridge: the self-test fails "memory sharing" when the server offers no host-visible memory type (no sharing method passed, RP4 Pro G77); the session then falls back to the PanVK pair like a failed draw, and Setup says the bridge cannot share memory with that driver. Without a pair to fall back on, the log says so. - Mali bridge: a guarded driver crash now logs the sub-command it was in, the fault address and pc (library + offset via dladdr), the command buffer's level, begin flags, pool flags and state, and the last 32 replayed commands. Begin on an executable buffer from a pool without RESET_COMMAND_BUFFER_BIT is logged once (implicit reset). - Venus: vulkan-summary.txt gets the device extension list and key features (full vulkaninfo in vulkan-full.txt); a failed GL probe runs once with VN_DEBUG=init,result, ZINK_DEBUG=mem, MESA_LOG_LEVEL=debug and the layer trace into glprobe-trace.txt. The Mali compatibility layer's trace covers every pre-device query (devices, memory, queue families, formats, image formats, external handles) and names the extensions. - Venus: off when the phone's driver reports Vulkan below 1.2 (gamescope skips such a device); Setup and the graphics report say why and the session uses the PanVK pair. - Compositor: libwayland's own disconnect reasons go to wayland.log, and a client's buffer may grow to 1 MB before a slow reader is dropped. A session that ended without teardown records Android's exit reasons. - Drivers: RADV-Xclipse upstream (radv-xclipse-upstream) is a manual choice for Xclipse; Auto keeps radv-xclipse. * Tests: the terminal-callers harness stubs the Quest layer and Cardboard modules and Android's system properties * Pin gamescope p21 (main's patches + 0120-0133) and Venusa4d47cf3(layer trace) * Display driver Auto: Quest's patched Turnip first; the system driver only for a Mali, other GPUs keep the generic fallback * Open driver pairs: the installed build's version, its upload date, and an Update when the release is rebuilt in place - The release check records each asset's GitHub id and updated_at; a download records them (with the digest) for an open non-Adreno pair. - Components > GPU drivers: an installed open pair shows its manifest version ("android <tag> + linux <commit>"), one not yet installed shows "built YYYY-MM-DD". A different digest, asset id or upload time on the same tag marks the installed pair "Update available" with an Update action: the pair is downloaded again and replaced in place, and stays selected only if it was. Adreno and DD-Turnip pairs are unchanged. - graphics-report.txt: the pair release line gives the installed and the listed upload dates and flags a newer build; an exact bundle id match is preferred (RADV-Xclipse-upstream is not RADV-Xclipse). * Session preload: drop oom_score_adj raises so lmkd does not treat games as cached apps steam-launch-wrapper starts every game with --oom-score-adjust 900; on Android 900 is CACHED_APP_MIN_ADJ, so lmkd kills the running game first when memory runs short (and on Linux 6.1 the raise becomes the process's permanent minimum). A process in the app cannot go below the score it inherits, so the writes that succeed from inside are raises: every write-open of /proc/<pid|self|thread-self>[/task/<tid>]/oom_score_adj (or oom_adj), through open/openat or fopen, goes to /dev/null, logged once. Based on the oom_score_adj redirect in the preload's opens.c by jdmg28 (https://github.com/jdmg28/DroidDeck-Mali/commit/6be00e00) * Display: why presenting failed on the session-ended screen, and the display driver that really loaded - The compositor records why it cannot present (no Vulkan driver loaded, vkCreateInstance/vkCreateDevice failed, no GPU or graphics queue, no swapchain on the screen with the driver's reason, device lost, GPU work unconfirmed). Each is a DD_PRESENT_FAULT line in wayland.log, read back over JNI or from wayland.log; the session-ended screen shows it instead of a black screen, and graphics-report.txt gets "!! DISPLAY FAILED". A first frame or a working swapchain clears it. - After the display device is created, the driver that answered is logged from VkPhysicalDeviceDriverProperties (device, driverName, driverInfo, driver id, Vulkan version) and reported in graphics-report.txt. When a driver pair's display half was requested but a vendor driver answered (adrenotools falls back to the system driver when the requested one does not load), it says so. Based on DD_PRESENT_FAULT in vk_present.c / waylandcomp_jni.c / SessionActivity by jdmg28 (https://github.com/jdmg28/DroidDeck-Mali/blob/arena/cd8ab18a-droiddeck-mali/app/src/main/cpp/waylandcomp/src/vk_present.c) Based on the adrenotools silent-fallback finding by ryanistr (https://github.com/ryanistr/DroidDeck-Mali/blob/main/docs/development/panvk-mali-g77-bringup.md) * tests: steam presentation test sets 0.5.0's steam_client * Non-Adreno: find every driver pair, Venus before the bridge, fix the bridge's begin info From the logs testers sent sincec9ebb247: - The app read the drivers repo's newest 15 releases only; the component releases (gamescope, Venus) pushed every PanVK pair and RADV-Xclipse past them, so Auto found no pair on any Mali and switched the Mali bridge on. The non-Adreno repo is now read in one page of 100. - Auto's stand-in for a missing pair: Mali (Venus) where the apk carries it (Steam runs on it from the G57 to the G1-Ultra), the bridge otherwise, nothing on a driver below Vulkan 1.2. It no longer clears the Linux driver, does nothing when the list could not be read, records that it chose the route, and turns it off again once a pair is installed. A bridge that earlier builds left on under Auto is taken as Auto's. - Mali bridge: vkBeginCommandBuffer sent its begin info without the count the server decodes, so Mali got a zeroed begin info with stale flags and crashed at 0x10 (Pixel 8 Pro G715, Lenovo G615, Xiaomi G925, Tab S11). - Mali-G1 series (MT6993, arch v14) known by its chip; its label says Venus, not the bridge. - A session Android killed for memory is said so on the next start (the S24 FE "crashes" were low-memory kills, not driver faults). - The in-app updater never installs an official (Adreno-only) build over this test build. * Venus: destroy the device's object mutex after the objects that lock it Bionic aborts on locking a destroyed mutex ("pthread_mutex_lock called on a destroyed mutex", Tab S9 FE G68, Pixel 9a), so a client that exited holding objects took the render worker down. From BattleTempest's DroidDeck-Pixel (kevinbryant551-ux/DroidDeck-Pixel, GPL-3.0). * release check: a session helper may be extracted to another runtime path droiddeck-steam-taskset (#570) goes to usr/local/libexec/droiddeck-steam/taskset, where PATH puts it in front of Steam's own taskset for the client only; 0.5.0's check wanted every droiddeck-* helper at its own path. * Venus component93038b7d(destroyed-mutex fix) * Update MaliBridge to v0.5.1 (#1135) * Mali Auto: never leave a session on the runtime's Turnip Logs from a Retroid Pocket 4 Pro (G77), Galaxy S21 Ultra (G78), Tab S11 Ultra (G925) and Xiaomi G610 showed Auto ending on the runtime's Turnip, where gamescope finds no GPU ("failed to find physical device"): after a pair's trial rolled back, when the pair is on the bad list, and when the driver list could not be read. In each case Auto now switches on its stand-in (Venus, or the bridge) when no open driver, Venus or bridge is set. Under Auto the Mali display also leaves any imported driver, not only a pair's PanVK half (a lone PanVK_G720 import gave a black screen). * Mali bridge: MaliBridge 224b6e36 (v0.5.3 source, by ASEbrahim) From c65062b6 (v0.5.1). v0.5.2/v0.5.3: Steam mode on older Mali drivers (G77/G710/G610/G57) gets past "starting display and audio", OpenGL 4.6 on most Malis through dynamic-rendering and vertex-format emulation, Dimensity 9500 texture fixes, async pipeline compile, RE2 flicker on G925, DirectX on the Pixel 8 family. JNI entry and session env names are unchanged. * gamescope 0134: linear when the backend offers no modifier; Venus on Mesa 26.2.5 0134: CVulkanTexture::BInit asserted !modifiers.empty() when the nested Wayland backend offered no modifier for a flippable image's format (NieR Replicant on a Pixel 9a under Venus, session exit 134). It now tries linear and logs it. Venus ICD: mesa-26.2.5 (venus TLS teardown from an unloaded driver, virtio-gpu devices without 3D skipped); virglrenderer stays on 1.3.0. * gamescope p22-nonadreno (0134 + main's 0116 cursor fix; build run 40159674290) * CI: build the non-Adreno additions before the preload check * build.gradle: drop verifyHandModel (main moved the hand models into the tracking pack) * Venus component venus-proto13-e24ed8ab (Mesa 26.2.5; build run 40161447324) * CI: fetch the Mali (Venus) component again (lost when build.yml took main's jobs) Builda7a1ebf1(run 40161734101) had no assets/venus or libvirgl_test_server.so, so VenusComponent.available() was false and Auto's stand-in would have been the bridge. * Auto: the Mali bridge stands in on a Vulkan 1.1 Mali driver Auto refused both Venus and the bridge when the phone's driver reports Vulkan below 1.2, so on the G77, G78 and G610 (r32p1/r38, Vulkan 1.1) a rolled-back or missing pair left the runtime's Turnip: "failed to find physical device" (Retroid Pocket 4 Pro after the JM pair's trial, Xiaomi 13T). Only Venus needs 1.2; the bridge raises the driver for gamescope and runs on these phones. A Venus switch that cannot start on such a driver no longer counts as a route. * Tests: stub main's g_release_unpaced in the DMA-BUF quarantine probe The probe copies release_buffer out of compositor.c; since the merge it reads g_release_unpaced, which the harness did not declare (setUpClass failed to compile). * Update MaliBridge to v0.5.4 (ASEbrahim) 48477fe6 (tag v0.5.4): Dimensity 9500 green squares and red-tinted textures, Balatro and other OpenGL text, fewer crashes when memory runs low, one screen update per game frame, picture kept after a long background pause (SIGHUP no longer stops the server), wrong colours after changing graphics settings on the G720, more in the session report. The app-side changes of his DroidDeck Mali app (Steam menus setting, per-game setup, Send logs, updates) are not part of the MaliBridge repository and are not in this build. * gamescope 0135: linear when the driver rejects every offered modifier An r32p1 Mali-G610 (Vulkan 1.1) behind the Mali bridge answered both of the backend's modifiers for AR24 with VK_ERROR_FORMAT_NOT_SUPPORTED, so the first flippable image failed and gamescope asserted in vulkan_create_flat_texture (Xiaomi 13T). Linear, when offered, is tried anyway; the bridge makes such images without the handle type and exports them through a dma-heap. If the image cannot be made, BInit fails as before. * gamescope: pin the non-Adreno p23 build (0135) * Tests: the terminal-callers harness stubs take_shm's game-frame detection Main's take_shm now reads surface damage, parent/role/mapped and the HUD state to spot game frames; the harness declares them so the copied function compiles (setUpClass failed in Build APK run 40505286127). No damage, so it stays off. * gamescope: number the test branch's patches 0920-0935 Main now has its own 0121-0125 (window override walk, host-event wake, commit fences, done at import, HUD frame times), which sorted in between this branch's 0120-0135. The branch's sixteen patches move to 0920-0935 with the same last two digits, so they apply after main's set and stay clear of its next numbers. Applying all 38 to gamescope 3.16.29 in the old order and the new one gives the same tree (checked with patch -p1, as build-in-arch.sh does). PATCHES.md and the one code comment that named 0130 follow the new numbers. * check_session_assets: helpers count in usr/local/bin or the Steam taskset folder only The merge kept this branch's pattern (any usr/ target), which main's new test rejects: a helper extracted into a subfolder of usr/local/bin must not count. The pattern now takes usr/local/bin under any name, as on main, plus usr/local/libexec/droiddeck-steam for droiddeck-steam-taskset. Every helper in the overlay is still found extracted in SessionFiles.kt. * gamescope: pin 3.16.29 p24 (non-Adreno) Built from this branch @d13b9d88 (run 41119284267): main a5b035b2's patches plus this branch's, now 0920-0935. All 38 applied, gamescope's tests 2/2. sha256 1d1088d4. * Update MaliBridge to v0.5.4.2 (ASEbrahim) 95a1e600 (tag v0.5.4.2), was v0.5.4 (48477fe6). For the bridge: memory sharing without the handle type where that is all the self-test passed (unflagged buffers, client/memory.c, server_memory.c), the older-Mali (G52-G77) game-mode start fixes, the r38p1 beta geometry-shader erratum (gs_hide), present timeouts and an API cap per process. Client, server and the in-app library all build from the submodule with MaliBridge's own scripts, which did not change; the protocol only gained fields an older server sends as 0. * Session end: the compositor fault query no longer fails the hint when the native library is absent presentationFault() (6778a224) called WaylandCompositor.compositorFault() directly. Main's new SessionEndHintTest runs sessionEndHint() in Robolectric, where libdroiddeckwayland cannot load, so all six cases ended in UnsatisfiedLinkError (Build APK run 41125749537). The call is now wrapped like runtimeDamageHint()'s, and without an answer the hint reads wayland.log as before. * test: stub the GPU busy reader in the caller harness * fix: release build is not debuggable, Mali uses the system display driver, staging failure ends its own session * fix: address non-Adreno review findings * fix: preserve current gamescope and GPU warning behavior * fix: retain graphics failure hints and launch diagnostics * fix: identify non-Adreno GPUs in driver failure hints * test: harnesses follow the external client barrier and effects upscaling changes * fix: client sources return to foreign ownership after the overlay draws --------- Co-authored-by: Leask Wong <[email protected]> Co-authored-by: wallentx <[email protected]> Co-authored-by: Stefan Zijlstra <[email protected]> Co-authored-by: William Allen <[email protected]> Co-authored-by: The412Banner <[email protected]> Co-authored-by: Dersim Abbas <[email protected]> Co-authored-by: root <[email protected]> Co-authored-by: ASEbrahim <[email protected]> Co-authored-by: The412Banner <[email protected]> Co-authored-by: The412Banner <[email protected]>
This commit is contained in:
215 files changed
+19024
-428
No files matched your search
@@ -0,0 +1,31 @@
|
||||
name: Build Venus (Mali)
|
||||
# The Mali (Venus) route's binaries from tools/venus (David Cimo's recipes): Mesa's Venus ICD and the
|
||||
# Mali compatibility layer (glibc, for the session) and virglrenderer's vtest server (bionic, run by
|
||||
# the app over the phone's own Vulkan driver), packed as venus.tzst rooted at app/src/main. The apk
|
||||
# build fetches it by tools/venus/release.env (tag + sha256); nothing compiled is kept in this repo.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [test/non-adreno-all]
|
||||
paths: ['tools/venus/**', '.github/workflows/build-venus.yml']
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq ninja-build pkg-config zstd python3-pip
|
||||
pip3 install --break-system-packages 'meson>=1.5'
|
||||
- name: Build
|
||||
run: ANDROID_NDK_HOME="$ANDROID_NDK_LATEST_HOME" WORK=$PWD/venus-work tools/venus/ci-build.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: venus
|
||||
path: |
|
||||
venus-work/venus.tzst
|
||||
venus-work/venus.tzst.sha256
|
||||
@@ -275,6 +275,19 @@ jobs:
|
||||
if: steps.proot.outputs.cache-hit != 'true'
|
||||
run: python3 tools/components/prepare.py proot
|
||||
|
||||
- name: Build the non-Adreno test additions (Mali bridge client, Venus GL probe, Xclipse layer)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash tools/ci/install-ubuntu-packages.sh gcc-aarch64-linux-gnu libvulkan-dev
|
||||
D=app/src/main/assets/linuxfs
|
||||
mkdir -p "$D/usr/local/bin" "$D/usr/local/lib/droiddeck-xclipse"
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \
|
||||
-o "$D/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so" tools/linuxfs/xclipse-layer/xclipse_layer.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so"
|
||||
install -m644 tools/linuxfs/xclipse-layer/VkLayer_droiddeck_xclipse.json "$D/usr/local/lib/droiddeck-xclipse/"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -o "$D/usr/local/bin/droiddeck-glprobe" tools/venus/glprobe.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-glprobe"
|
||||
|
||||
- name: Check the preloads
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -573,6 +586,18 @@ jobs:
|
||||
|
||||
- name: Check out MaliBridge
|
||||
run: git submodule update --init external/MaliBridge
|
||||
- name: Fetch the Mali (Venus) component (tools/venus/release.env)
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. tools/venus/release.env
|
||||
gh release download "${VENUS_TAG}" -R "${VENUS_REPO}" -p venus.tzst -O venus.tzst
|
||||
echo "${VENUS_SHA256} venus.tzst" | sha256sum -c -
|
||||
tar --use-compress-program=unzstd -xf venus.tzst -C app/src/main
|
||||
test -f app/src/main/jniLibs/arm64-v8a/libvirgl_test_server.so
|
||||
test -f app/src/main/assets/venus/libvulkan_virtio.so
|
||||
rm venus.tzst
|
||||
|
||||
- name: Build
|
||||
id: build
|
||||
@@ -581,6 +606,7 @@ jobs:
|
||||
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 tools/powervr/fetch.py
|
||||
python3 tools/release/runtime_inputs.py record
|
||||
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
|
||||
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
@@ -34,4 +34,5 @@ __pycache__/
|
||||
|
||||
.signing.env
|
||||
app/src/main/assets/directaudio/
|
||||
app/src/main/assets/hybris/
|
||||
/tools/vr/out/
|
||||
@@ -11,6 +11,7 @@ add_library(droiddeckwayland SHARED
|
||||
src/gpu_clock_pin.c
|
||||
src/droiddeck_ext.c
|
||||
src/wl_dmabuf.c
|
||||
src/wl_android_buffer.c
|
||||
src/wl_clipboard.c
|
||||
src/wl_text_input.c
|
||||
src/wl_toplevel_icon.c
|
||||
@@ -22,6 +23,7 @@ add_library(droiddeckwayland SHARED
|
||||
generated/color-management-v1-protocol.c
|
||||
generated/xdg-shell-protocol.c
|
||||
generated/linux-dmabuf-v1-protocol.c
|
||||
generated/wayland-android-protocol.c
|
||||
generated/viewporter-protocol.c
|
||||
generated/banner-desktop-v1-protocol.c
|
||||
generated/presentation-time-protocol.c
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/* Generated by wayland-scanner 1.26.0 */
|
||||
|
||||
/*
|
||||
* Copyright © 2012 Collabora, Ltd.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this
|
||||
* software and its documentation for any purpose is hereby granted
|
||||
* without fee, provided that the above copyright notice appear in
|
||||
* all copies and that both that copyright notice and this permission
|
||||
* notice appear in supporting documentation, and that the name of
|
||||
* the copyright holders not be used in advertising or publicity
|
||||
* pertaining to distribution of the software without specific,
|
||||
* written prior permission. The copyright holders make no
|
||||
* representations about the suitability of this software for any
|
||||
* purpose. It is provided "as is" without express or implied
|
||||
* warranty.
|
||||
*
|
||||
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
* THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include "wayland-util.h"
|
||||
|
||||
#ifndef __has_attribute
|
||||
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
|
||||
#endif
|
||||
|
||||
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
|
||||
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
|
||||
#else
|
||||
#define WL_PRIVATE
|
||||
#endif
|
||||
|
||||
extern const struct wl_interface android_wlegl_handle_interface;
|
||||
extern const struct wl_interface android_wlegl_server_buffer_handle_interface;
|
||||
extern const struct wl_interface wl_buffer_interface;
|
||||
|
||||
static const struct wl_interface *android_types[] = {
|
||||
NULL,
|
||||
NULL,
|
||||
&android_wlegl_handle_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
&wl_buffer_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&android_wlegl_handle_interface,
|
||||
&android_wlegl_server_buffer_handle_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&wl_buffer_interface,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const struct wl_message android_wlegl_requests[] = {
|
||||
{ "create_handle", "nia", android_types + 2 },
|
||||
{ "create_buffer", "niiiiio", android_types + 5 },
|
||||
{ "get_server_buffer_handle", "2niiii", android_types + 12 },
|
||||
};
|
||||
|
||||
WL_PRIVATE const struct wl_interface android_wlegl_interface = {
|
||||
"android_wlegl", 3,
|
||||
3, android_wlegl_requests,
|
||||
0, NULL,
|
||||
};
|
||||
|
||||
static const struct wl_message android_wlegl_handle_requests[] = {
|
||||
{ "add_fd", "h", android_types + 0 },
|
||||
{ "destroy", "", android_types + 0 },
|
||||
};
|
||||
|
||||
WL_PRIVATE const struct wl_interface android_wlegl_handle_interface = {
|
||||
"android_wlegl_handle", 1,
|
||||
2, android_wlegl_handle_requests,
|
||||
0, NULL,
|
||||
};
|
||||
|
||||
static const struct wl_message android_wlegl_server_buffer_handle_events[] = {
|
||||
{ "buffer_fd", "h", android_types + 0 },
|
||||
{ "buffer_ints", "a", android_types + 0 },
|
||||
{ "buffer", "nii", android_types + 17 },
|
||||
{ "linear_layout", "3uu", android_types + 0 },
|
||||
};
|
||||
|
||||
WL_PRIVATE const struct wl_interface android_wlegl_server_buffer_handle_interface = {
|
||||
"android_wlegl_server_buffer_handle", 3,
|
||||
0, NULL,
|
||||
4, android_wlegl_server_buffer_handle_events,
|
||||
};
|
||||
@@ -0,0 +1,341 @@
|
||||
/* Generated by wayland-scanner 1.26.0 */
|
||||
|
||||
#ifndef ANDROID_SERVER_PROTOCOL_H
|
||||
#define ANDROID_SERVER_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "wayland-server.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct wl_client;
|
||||
struct wl_resource;
|
||||
|
||||
/**
|
||||
* @page page_android The android protocol
|
||||
* @section page_ifaces_android Interfaces
|
||||
* - @subpage page_iface_android_wlegl - Android EGL graphics buffer support
|
||||
* - @subpage page_iface_android_wlegl_handle - An Android native_handle_t object
|
||||
* - @subpage page_iface_android_wlegl_server_buffer_handle - a server allocated buffer
|
||||
* @section page_copyright_android Copyright
|
||||
* <pre>
|
||||
*
|
||||
* Copyright © 2012 Collabora, Ltd.
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this
|
||||
* software and its documentation for any purpose is hereby granted
|
||||
* without fee, provided that the above copyright notice appear in
|
||||
* all copies and that both that copyright notice and this permission
|
||||
* notice appear in supporting documentation, and that the name of
|
||||
* the copyright holders not be used in advertising or publicity
|
||||
* pertaining to distribution of the software without specific,
|
||||
* written prior permission. The copyright holders make no
|
||||
* representations about the suitability of this software for any
|
||||
* purpose. It is provided "as is" without express or implied
|
||||
* warranty.
|
||||
*
|
||||
* THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
* ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
* THIS SOFTWARE.
|
||||
* </pre>
|
||||
*/
|
||||
struct android_wlegl;
|
||||
struct android_wlegl_handle;
|
||||
struct android_wlegl_server_buffer_handle;
|
||||
struct wl_buffer;
|
||||
|
||||
#ifndef ANDROID_WLEGL_INTERFACE
|
||||
#define ANDROID_WLEGL_INTERFACE
|
||||
/**
|
||||
* @page page_iface_android_wlegl android_wlegl
|
||||
* @section page_iface_android_wlegl_desc Description
|
||||
*
|
||||
* Interface used in the Android wrapper libEGL to share
|
||||
* graphics buffers between the server and the client.
|
||||
* @section page_iface_android_wlegl_api API
|
||||
* See @ref iface_android_wlegl.
|
||||
*/
|
||||
/**
|
||||
* @defgroup iface_android_wlegl The android_wlegl interface
|
||||
*
|
||||
* Interface used in the Android wrapper libEGL to share
|
||||
* graphics buffers between the server and the client.
|
||||
*/
|
||||
extern const struct wl_interface android_wlegl_interface;
|
||||
#endif
|
||||
#ifndef ANDROID_WLEGL_HANDLE_INTERFACE
|
||||
#define ANDROID_WLEGL_HANDLE_INTERFACE
|
||||
/**
|
||||
* @page page_iface_android_wlegl_handle android_wlegl_handle
|
||||
* @section page_iface_android_wlegl_handle_desc Description
|
||||
*
|
||||
* The Android native_handle_t is a semi-opaque object, that
|
||||
* contains an EGL implementation specific number of int32
|
||||
* values and file descriptors.
|
||||
*
|
||||
* We cannot send a variable size array of file descriptors
|
||||
* over the Wayland protocol, so we send them one by one.
|
||||
* @section page_iface_android_wlegl_handle_api API
|
||||
* See @ref iface_android_wlegl_handle.
|
||||
*/
|
||||
/**
|
||||
* @defgroup iface_android_wlegl_handle The android_wlegl_handle interface
|
||||
*
|
||||
* The Android native_handle_t is a semi-opaque object, that
|
||||
* contains an EGL implementation specific number of int32
|
||||
* values and file descriptors.
|
||||
*
|
||||
* We cannot send a variable size array of file descriptors
|
||||
* over the Wayland protocol, so we send them one by one.
|
||||
*/
|
||||
extern const struct wl_interface android_wlegl_handle_interface;
|
||||
#endif
|
||||
#ifndef ANDROID_WLEGL_SERVER_BUFFER_HANDLE_INTERFACE
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_INTERFACE
|
||||
/**
|
||||
* @page page_iface_android_wlegl_server_buffer_handle android_wlegl_server_buffer_handle
|
||||
* @section page_iface_android_wlegl_server_buffer_handle_desc Description
|
||||
*
|
||||
* On creation a server_side_buffer object will immediately send
|
||||
* the "buffer_fd" and "buffer_ints" events needed by the client to
|
||||
* reference the gralloc buffer, followed by the "buffer" event carrying
|
||||
* the wl_buffer object.
|
||||
* @section page_iface_android_wlegl_server_buffer_handle_api API
|
||||
* See @ref iface_android_wlegl_server_buffer_handle.
|
||||
*/
|
||||
/**
|
||||
* @defgroup iface_android_wlegl_server_buffer_handle The android_wlegl_server_buffer_handle interface
|
||||
*
|
||||
* On creation a server_side_buffer object will immediately send
|
||||
* the "buffer_fd" and "buffer_ints" events needed by the client to
|
||||
* reference the gralloc buffer, followed by the "buffer" event carrying
|
||||
* the wl_buffer object.
|
||||
*/
|
||||
extern const struct wl_interface android_wlegl_server_buffer_handle_interface;
|
||||
#endif
|
||||
|
||||
#ifndef ANDROID_WLEGL_ERROR_ENUM
|
||||
#define ANDROID_WLEGL_ERROR_ENUM
|
||||
enum android_wlegl_error {
|
||||
ANDROID_WLEGL_ERROR_BAD_HANDLE = 0,
|
||||
ANDROID_WLEGL_ERROR_BAD_VALUE = 1,
|
||||
};
|
||||
#endif /* ANDROID_WLEGL_ERROR_ENUM */
|
||||
|
||||
#ifndef ANDROID_WLEGL_ERROR_ENUM_IS_VALID
|
||||
#define ANDROID_WLEGL_ERROR_ENUM_IS_VALID
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
* Validate a android_wlegl error value.
|
||||
*
|
||||
* @return true on success, false on error.
|
||||
* @ref android_wlegl_error
|
||||
*/
|
||||
static inline bool
|
||||
android_wlegl_error_is_valid(uint32_t value, uint32_t version) {
|
||||
switch (value) {
|
||||
case ANDROID_WLEGL_ERROR_BAD_HANDLE:
|
||||
return version >= 1;
|
||||
case ANDROID_WLEGL_ERROR_BAD_VALUE:
|
||||
return version >= 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif /* ANDROID_WLEGL_ERROR_ENUM_IS_VALID */
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
* @struct android_wlegl_interface
|
||||
*/
|
||||
struct android_wlegl_interface {
|
||||
/**
|
||||
* Create an Android native_handle_t object
|
||||
*
|
||||
* This creator method initialises the native_handle_t object
|
||||
* with everything except the file descriptors, which have to be
|
||||
* submitted separately.
|
||||
* @param ints an array of int32_t
|
||||
*/
|
||||
void (*create_handle)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
uint32_t id,
|
||||
int32_t num_fds,
|
||||
struct wl_array *ints);
|
||||
/**
|
||||
* Create a wl_buffer from the native handle
|
||||
*
|
||||
* Pass the Android native_handle_t to the server and attach it
|
||||
* to the new wl_buffer object.
|
||||
*
|
||||
* The android_wlegl_handle object must be destroyed immediately
|
||||
* after this request.
|
||||
*/
|
||||
void (*create_buffer)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
uint32_t id,
|
||||
int32_t width,
|
||||
int32_t height,
|
||||
int32_t stride,
|
||||
int32_t format,
|
||||
int32_t usage,
|
||||
struct wl_resource *native_handle);
|
||||
/**
|
||||
* @since 2
|
||||
*/
|
||||
void (*get_server_buffer_handle)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
uint32_t id,
|
||||
int32_t width,
|
||||
int32_t height,
|
||||
int32_t format,
|
||||
int32_t usage);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
*/
|
||||
#define ANDROID_WLEGL_CREATE_HANDLE_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
*/
|
||||
#define ANDROID_WLEGL_CREATE_BUFFER_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl
|
||||
*/
|
||||
#define ANDROID_WLEGL_GET_SERVER_BUFFER_HANDLE_SINCE_VERSION 2
|
||||
|
||||
#ifndef ANDROID_WLEGL_HANDLE_ERROR_ENUM
|
||||
#define ANDROID_WLEGL_HANDLE_ERROR_ENUM
|
||||
enum android_wlegl_handle_error {
|
||||
ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS = 0,
|
||||
};
|
||||
#endif /* ANDROID_WLEGL_HANDLE_ERROR_ENUM */
|
||||
|
||||
#ifndef ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID
|
||||
#define ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
* Validate a android_wlegl_handle error value.
|
||||
*
|
||||
* @return true on success, false on error.
|
||||
* @ref android_wlegl_handle_error
|
||||
*/
|
||||
static inline bool
|
||||
android_wlegl_handle_error_is_valid(uint32_t value, uint32_t version) {
|
||||
switch (value) {
|
||||
case ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS:
|
||||
return version >= 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif /* ANDROID_WLEGL_HANDLE_ERROR_ENUM_IS_VALID */
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
* @struct android_wlegl_handle_interface
|
||||
*/
|
||||
struct android_wlegl_handle_interface {
|
||||
/**
|
||||
*/
|
||||
void (*add_fd)(struct wl_client *client,
|
||||
struct wl_resource *resource,
|
||||
int32_t fd);
|
||||
/**
|
||||
*/
|
||||
void (*destroy)(struct wl_client *client,
|
||||
struct wl_resource *resource);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_HANDLE_ADD_FD_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_HANDLE_DESTROY_SINCE_VERSION 1
|
||||
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD 0
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS 1
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER 2
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT 3
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_SINCE_VERSION 1
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
*/
|
||||
#define ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT_SINCE_VERSION 3
|
||||
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an buffer_fd event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_buffer_fd(struct wl_resource *resource_, int32_t fd)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_FD, fd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an buffer_ints event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_buffer_ints(struct wl_resource *resource_, struct wl_array *ints)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER_INTS, ints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an buffer event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_buffer(struct wl_resource *resource_, struct wl_resource *buffer, int32_t format, int32_t stride)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_BUFFER, buffer, format, stride);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ingroup iface_android_wlegl_server_buffer_handle
|
||||
* Sends an linear_layout event to the client owning the resource.
|
||||
* @param resource_ The client's resource
|
||||
*/
|
||||
static inline void
|
||||
android_wlegl_server_buffer_handle_send_linear_layout(struct wl_resource *resource_, uint32_t drm_format, uint32_t stride)
|
||||
{
|
||||
wl_resource_post_event(resource_, ANDROID_WLEGL_SERVER_BUFFER_HANDLE_LINEAR_LAYOUT, drm_format, stride);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="android">
|
||||
|
||||
<copyright>
|
||||
Copyright © 2012 Collabora, Ltd.
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this
|
||||
software and its documentation for any purpose is hereby granted
|
||||
without fee, provided that the above copyright notice appear in
|
||||
all copies and that both that copyright notice and this permission
|
||||
notice appear in supporting documentation, and that the name of
|
||||
the copyright holders not be used in advertising or publicity
|
||||
pertaining to distribution of the software without specific,
|
||||
written prior permission. The copyright holders make no
|
||||
representations about the suitability of this software for any
|
||||
purpose. It is provided "as is" without express or implied
|
||||
warranty.
|
||||
|
||||
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="android_wlegl" version="3">
|
||||
<description summary="Android EGL graphics buffer support">
|
||||
Interface used in the Android wrapper libEGL to share
|
||||
graphics buffers between the server and the client.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="bad_handle" value="0" />
|
||||
<entry name="bad_value" value="1" />
|
||||
</enum>
|
||||
|
||||
<request name="create_handle">
|
||||
<description summary="Create an Android native_handle_t object">
|
||||
This creator method initialises the native_handle_t object
|
||||
with everything except the file descriptors, which have to be
|
||||
submitted separately.
|
||||
</description>
|
||||
|
||||
<arg name="id" type="new_id" interface="android_wlegl_handle" />
|
||||
<arg name="num_fds" type="int" />
|
||||
<arg name="ints" type="array" summary="an array of int32_t" />
|
||||
</request>
|
||||
|
||||
<request name="create_buffer">
|
||||
<description summary="Create a wl_buffer from the native handle">
|
||||
Pass the Android native_handle_t to the server and attach it
|
||||
to the new wl_buffer object.
|
||||
|
||||
The android_wlegl_handle object must be destroyed immediately
|
||||
after this request.
|
||||
</description>
|
||||
|
||||
<arg name="id" type="new_id" interface="wl_buffer" />
|
||||
<arg name="width" type="int" />
|
||||
<arg name="height" type="int" />
|
||||
<arg name="stride" type="int" />
|
||||
<arg name="format" type="int" />
|
||||
<arg name="usage" type="int" />
|
||||
<arg name="native_handle" type="object" interface="android_wlegl_handle" />
|
||||
</request>
|
||||
|
||||
|
||||
<request name="get_server_buffer_handle" since="2">
|
||||
<arg name="id" type="new_id" interface="android_wlegl_server_buffer_handle"/>
|
||||
<arg name="width" type="int"/>
|
||||
<arg name="height" type="int"/>
|
||||
<arg name="format" type="int"/>
|
||||
<arg name="usage" type="int"/>
|
||||
</request>
|
||||
|
||||
</interface>
|
||||
|
||||
<interface name="android_wlegl_handle" version="1">
|
||||
<description summary="An Android native_handle_t object">
|
||||
The Android native_handle_t is a semi-opaque object, that
|
||||
contains an EGL implementation specific number of int32
|
||||
values and file descriptors.
|
||||
|
||||
We cannot send a variable size array of file descriptors
|
||||
over the Wayland protocol, so we send them one by one.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="too_many_fds" value="0" />
|
||||
</enum>
|
||||
|
||||
<request name="add_fd">
|
||||
<arg name="fd" type="fd" />
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor" />
|
||||
|
||||
</interface>
|
||||
|
||||
|
||||
<interface name="android_wlegl_server_buffer_handle" version="3">
|
||||
<description summary="a server allocated buffer">
|
||||
On creation a server_side_buffer object will immediately send
|
||||
the "buffer_fd" and "buffer_ints" events needed by the client to
|
||||
reference the gralloc buffer, followed by the "buffer" event carrying
|
||||
the wl_buffer object.
|
||||
</description>
|
||||
<event name="buffer_fd">
|
||||
<arg name="fd" type="fd"/>
|
||||
</event>
|
||||
<event name="buffer_ints">
|
||||
<arg name="ints" type="array"/>
|
||||
</event>
|
||||
<event name="buffer">
|
||||
<description summary="the wl_buffer">
|
||||
This event will be sent after the ints and all the fds have been sent
|
||||
</description>
|
||||
<arg name="buffer" type="new_id" interface="wl_buffer"/>
|
||||
<arg name="format" type="int"/>
|
||||
<arg name="stride" type="int"/>
|
||||
</event>
|
||||
<event name="linear_layout" since="3">
|
||||
<description summary="verified single-plane linear allocation">
|
||||
Optional metadata sent before the buffer event. The first buffer_fd
|
||||
contains one uncompressed 32-bit RGB plane at byte offset zero.
|
||||
drm_format is its DRM FourCC and stride is its row pitch in bytes.
|
||||
The server has verified the layout and allocation bounds using
|
||||
Android mapper metadata, not inferred them from opaque handle ints.
|
||||
Absence of this event means the layout is unspecified.
|
||||
</description>
|
||||
<arg name="drm_format" type="uint"/>
|
||||
<arg name="stride" type="uint"/>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
</protocol>
|
||||
@@ -79,6 +79,9 @@ static struct ahb_buf *find_buf(uint64_t id) {
|
||||
}
|
||||
|
||||
static void buf_free(struct ahb_buf *ab) {
|
||||
/* A display release does not prove the compositor's Vulkan reads finished.
|
||||
* Keep the AHB reference and its DMA-BUF record even if the client is gone. */
|
||||
if (vkp_completion_unknown()) return;
|
||||
if (ab->resource) { wl_list_remove(&ab->resource_destroy.link); ab->resource = NULL; }
|
||||
if (ab->fence_src) { wl_event_source_remove(ab->fence_src); ab->fence_src = NULL; }
|
||||
if (ab->fence_fd >= 0) { close(ab->fence_fd); ab->fence_fd = -1; }
|
||||
@@ -103,6 +106,7 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) {
|
||||
|
||||
|
||||
static void send_deferred_release(struct ahb_buf *ab) {
|
||||
if (vkp_completion_unknown()) return;
|
||||
if (ab->release_pending && ab->resource)
|
||||
droiddeck_release_buffer(ab->surface, ab->resource, ab->surface != NULL, ab->deferred_ns);
|
||||
ab->release_pending = 0;
|
||||
|
||||
@@ -154,6 +154,16 @@ static void on_client_destroyed(struct wl_listener *l, void *data) {
|
||||
free(ci);
|
||||
}
|
||||
|
||||
/* libwayland's own reasons for dropping a client (a failed read or flush, an invalid object, a
|
||||
* malformed request) go through wl_log, which on Android writes to a closed stderr. */
|
||||
static void wl_log_to_session(const char *fmt, va_list ap) {
|
||||
char line[512];
|
||||
vsnprintf(line, sizeof(line), fmt, ap);
|
||||
size_t n = strlen(line);
|
||||
while (n && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = 0;
|
||||
droiddeck_log("program", "libwayland: %s", line);
|
||||
}
|
||||
|
||||
static void on_client_created(struct wl_listener *l, void *data) {
|
||||
struct wl_client *client = data;
|
||||
struct client_info *ci = calloc(1, sizeof(*ci));
|
||||
@@ -349,6 +359,62 @@ static void perf_note_release(int64_t since_ns) {
|
||||
}
|
||||
|
||||
|
||||
/* Completion-unknown is terminal for externally owned DMA-BUF contents too.
|
||||
* Keeping a VkImage alone cannot stop the client from overwriting its backing
|
||||
* memory after wl_buffer.release. All paths (including cursors and idle roles)
|
||||
* use this gate; copied SHM buffers retain their normal release behaviour. */
|
||||
static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer);
|
||||
static int buffer_release_blocked(struct wl_resource *buffer) {
|
||||
return vkp_completion_unknown() && get_dmabuf(buffer) != NULL;
|
||||
}
|
||||
/* Buffer tracing on a GPU that is not an Adreno (no KGSL node): every attach and release of the
|
||||
* first 400 buffer events, by wl_buffer id and the line that sent it, and any release of a buffer
|
||||
* already released and not attached again since (gamescope: "Compositor released us but we were
|
||||
* not acquired"). Off on an Adreno. */
|
||||
static int g_buf_trace = -1;
|
||||
static int g_buf_trace_left = 400;
|
||||
static uint32_t g_released_ids[128];
|
||||
static int buf_trace_on(void) {
|
||||
if (g_buf_trace < 0) g_buf_trace = access("/dev/kgsl-3d0", F_OK) != 0;
|
||||
return g_buf_trace;
|
||||
}
|
||||
static void buf_trace_attach(struct wl_resource *buffer) {
|
||||
if (!buffer || !buf_trace_on()) return;
|
||||
uint32_t id = wl_resource_get_id(buffer);
|
||||
for (int i = 0; i < 128; i++) if (g_released_ids[i] == id) g_released_ids[i] = 0;
|
||||
if (g_buf_trace_left > 0) { g_buf_trace_left--; droiddeck_log("buffer", "attach id %u", id); }
|
||||
}
|
||||
static void buf_trace_release(struct wl_resource *buffer, int line) {
|
||||
if (!buffer || !buf_trace_on()) return;
|
||||
uint32_t id = wl_resource_get_id(buffer);
|
||||
int twice = 0, slot = -1;
|
||||
for (int i = 0; i < 128; i++) {
|
||||
if (g_released_ids[i] == id) twice = 1;
|
||||
if (slot < 0 && g_released_ids[i] == 0) slot = i;
|
||||
}
|
||||
if (twice) droiddeck_log("buffer", "DOUBLE release id %u (line %d): released again without an attach", id, line);
|
||||
else if (slot >= 0) g_released_ids[slot] = id;
|
||||
if (g_buf_trace_left > 0) { g_buf_trace_left--; droiddeck_log("buffer", "release id %u (line %d)", id, line); }
|
||||
}
|
||||
|
||||
#define send_buffer_release(b) send_buffer_release_at((b), __LINE__)
|
||||
static void send_buffer_release_at(struct wl_resource *buffer, int line) {
|
||||
if (!buffer_release_blocked(buffer)) { buf_trace_release(buffer, line); wl_buffer_send_release(buffer); }
|
||||
}
|
||||
|
||||
/* Whether a surface other than `except` is showing this buffer right now. gamescope's Wayland
|
||||
* backend moves one buffer between its planes (the game's frame goes from the window to a
|
||||
* subsurface above a black backing while a Steam notification is up): releasing it when the first
|
||||
* surface lets go handed gamescope a buffer still on screen ("Compositor released us but we were
|
||||
* not acquired"). Only the last surface to let go releases it. */
|
||||
static int buffer_shown_elsewhere(struct wl_resource *buffer, const struct surface *except) {
|
||||
struct surface *o;
|
||||
if (!buffer) return 0;
|
||||
wl_list_for_each(o, &g_surfaces, link)
|
||||
if (o != except && o->dmabuf == buffer) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void pending_release_free(struct pending_release *pr) {
|
||||
if (pr->surface && pr->surface->releases_pending > 0) pr->surface->releases_pending--;
|
||||
wl_list_remove(&pr->destroy.link);
|
||||
@@ -372,7 +438,8 @@ static void arm_release_timer(void) {
|
||||
int64_t earliest = 0;
|
||||
if (g_release_timer_fd < 0) return;
|
||||
wl_list_for_each(pr, &g_pending_releases, link)
|
||||
if (!earliest || pr->at_ns < earliest) earliest = pr->at_ns;
|
||||
if (!buffer_release_blocked(pr->buffer) && (!earliest || pr->at_ns < earliest))
|
||||
earliest = pr->at_ns;
|
||||
struct itimerspec its = {0};
|
||||
if (earliest) { its.it_value.tv_sec = earliest / 1000000000LL; its.it_value.tv_nsec = earliest % 1000000000LL; }
|
||||
timerfd_settime(g_release_timer_fd, TFD_TIMER_ABSTIME, &its, NULL);
|
||||
@@ -384,8 +451,13 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
|
||||
int64_t now = now_ns() + 300000;
|
||||
if (read(fd, &expirations, sizeof(expirations)) < 0 && errno != EAGAIN) return 0;
|
||||
wl_list_for_each_safe(pr, tmp, &g_pending_releases, link) {
|
||||
if (pr->at_ns <= now) {
|
||||
wl_buffer_send_release(pr->buffer);
|
||||
if (pr->at_ns <= now && !buffer_release_blocked(pr->buffer)) {
|
||||
/* Attached again since (to its surface or another): that surface releases it later. */
|
||||
if (buffer_shown_elsewhere(pr->buffer, NULL)) {
|
||||
pending_release_free(pr);
|
||||
continue;
|
||||
}
|
||||
send_buffer_release(pr->buffer);
|
||||
perf_note_release(pr->since_ns);
|
||||
pending_release_free(pr);
|
||||
}
|
||||
@@ -396,9 +468,12 @@ static int on_release_timer(int fd, uint32_t mask, void *data) {
|
||||
}
|
||||
|
||||
static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) {
|
||||
/* No allocation or immediate-release fallback is allowed for a quarantined
|
||||
* DMA-BUF. Existing queued releases stay held; their timer is disarmed. */
|
||||
if (buffer_release_blocked(buffer)) return;
|
||||
const int limit = g_fps_limit;
|
||||
const int64_t interval = limit > 0 ? 1000000000LL / limit : g_refresh_ns;
|
||||
if (g_release_unpaced || interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
|
||||
if (g_release_unpaced || interval <= 0 || g_release_timer_fd < 0) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
|
||||
int64_t now = now_ns();
|
||||
int64_t at = s->next_release_ns + interval;
|
||||
int64_t latest = now + interval * (int64_t)(s->releases_pending + 1);
|
||||
@@ -406,7 +481,7 @@ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_
|
||||
if (at > latest) at = latest;
|
||||
s->next_release_ns = at;
|
||||
struct pending_release *pr = calloc(1, sizeof(*pr));
|
||||
if (!pr) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; }
|
||||
if (!pr) { send_buffer_release(buffer); perf_note_release(since_ns); return; }
|
||||
pr->buffer = buffer;
|
||||
pr->surface = s;
|
||||
pr->at_ns = at;
|
||||
@@ -523,7 +598,10 @@ static void constraints_focus_entered(struct wl_resource *target, struct wl_clie
|
||||
|
||||
|
||||
void dmabuf_buffer_unref(struct dmabuf_buffer *b) {
|
||||
if (!b || --b->refs > 0) return;
|
||||
/* Surface/client destruction must not close the backing FDs or free the
|
||||
* imported-buffer record while a GPU read may still be pending. Retain the
|
||||
* reference without allocating a quarantine node; process exit reclaims it. */
|
||||
if (!b || vkp_completion_unknown() || --b->refs > 0) return;
|
||||
vkp_image_destroy(b->img);
|
||||
for (int i = 0; i < b->n_planes; i++)
|
||||
if (b->fd[i] >= 0) close(b->fd[i]);
|
||||
@@ -622,9 +700,10 @@ static void unmap_toplevel(struct surface *s) {
|
||||
static void drop_dmabuf(struct surface *s, int paced) {
|
||||
if (s->dmabuf) {
|
||||
wl_list_remove(&s->dmabuf_destroy.link);
|
||||
if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
|
||||
if (buffer_shown_elsewhere(s->dmabuf, s)) {
|
||||
} else if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) {
|
||||
if (paced) release_buffer(s, s->dmabuf, now_ns());
|
||||
else wl_buffer_send_release(s->dmabuf);
|
||||
else send_buffer_release(s->dmabuf);
|
||||
}
|
||||
s->dmabuf = NULL;
|
||||
}
|
||||
@@ -733,7 +812,7 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res
|
||||
vkp_image_upload_shm(s->shm_img, wl_shm_buffer_get_data(shm), stride);
|
||||
wl_shm_buffer_end_access(shm);
|
||||
}
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
s->buf_w = w;
|
||||
s->buf_h = h;
|
||||
s->buf_alpha = wl_shm_buffer_get_format(shm) == WL_SHM_FORMAT_ARGB8888;
|
||||
@@ -777,7 +856,7 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou
|
||||
}
|
||||
if (!s->shm_img) s->shm_img = vkp_image_create_shm(1, 1);
|
||||
if (s->shm_img) vkp_image_upload_shm(s->shm_img, bgra, 4);
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
s->buf_w = 1;
|
||||
s->buf_h = 1;
|
||||
s->buf_alpha = bgra[3] != 0xff;
|
||||
@@ -880,8 +959,9 @@ void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b) { return &b->ahb_state
|
||||
void droiddeck_dmabuf_ref(struct dmabuf_buffer *b) { b->refs++; }
|
||||
void droiddeck_dmabuf_unref(struct dmabuf_buffer *b) { dmabuf_buffer_unref(b); }
|
||||
void droiddeck_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns) {
|
||||
if (buffer_release_blocked(buffer)) return;
|
||||
if (paced && s) release_buffer(s, buffer, since_ns);
|
||||
else { wl_buffer_send_release(buffer); perf_note_release(since_ns); }
|
||||
else { send_buffer_release(buffer); perf_note_release(since_ns); }
|
||||
}
|
||||
void droiddeck_surface_describe(const struct surface *s, char *out, size_t size) { describe(s, out, size); }
|
||||
const struct droiddeck_color *droiddeck_surface_color(const struct surface *s) {
|
||||
@@ -1101,7 +1181,7 @@ static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) {
|
||||
struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer);
|
||||
int64_t px = db ? (int64_t)db->width * db->height
|
||||
: shm ? (int64_t)wl_shm_buffer_get_width(shm) * wl_shm_buffer_get_height(shm) : 0;
|
||||
if (px <= 0 || px > CURSOR_MAX_PX) { wl_buffer_send_release(buffer); return; }
|
||||
if (px <= 0 || px > CURSOR_MAX_PX) { send_buffer_release(buffer); return; }
|
||||
s->idle_buffer = buffer;
|
||||
s->idle_buffer_destroy.notify = on_idle_buffer_destroyed;
|
||||
wl_resource_add_destroy_listener(buffer, &s->idle_buffer_destroy);
|
||||
@@ -1119,7 +1199,7 @@ static struct wl_resource *surface_take_idle(struct surface *s) {
|
||||
static void surface_drop_idle(struct surface *s) {
|
||||
surface_forget_idle_px(s);
|
||||
struct wl_resource *buffer = surface_take_idle(s);
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
if (buffer) send_buffer_release(buffer);
|
||||
}
|
||||
|
||||
static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
@@ -1172,11 +1252,12 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
cursor_publish_hidden();
|
||||
} else {
|
||||
cursor_publish_buffer(s, buffer);
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
}
|
||||
} else if (s->role == ROLE_NONE) {
|
||||
if (buffer) surface_hold_idle(s, buffer);
|
||||
} else if (db) {
|
||||
buf_trace_attach(buffer);
|
||||
take_dmabuf(s, db, buffer);
|
||||
} else if (shm) {
|
||||
drop_dmabuf(s, 1);
|
||||
@@ -1187,7 +1268,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) {
|
||||
} else {
|
||||
drop_dmabuf(s, 1);
|
||||
s->has_content = 0;
|
||||
if (buffer) wl_buffer_send_release(buffer);
|
||||
if (buffer) send_buffer_release(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2437,7 +2518,7 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3
|
||||
struct wl_resource *buffer = surface_take_idle(s);
|
||||
if (buffer) {
|
||||
cursor_publish_buffer(s, buffer);
|
||||
wl_buffer_send_release(buffer);
|
||||
send_buffer_release(buffer);
|
||||
} else if (g_cursor_shown == s) {
|
||||
cursor_publish_shown();
|
||||
} else if (s->idle_px) {
|
||||
@@ -3836,6 +3917,10 @@ int droiddeck_wayland_run(void) {
|
||||
return 1;
|
||||
}
|
||||
droiddeck_log("display", "Wayland compositor listening on %s/wayland-0", rt ? rt : "?");
|
||||
wl_log_set_handler_server(wl_log_to_session);
|
||||
/* A client that falls behind reading (gamescope at a few fps) must not be dropped for a full
|
||||
* event buffer: libwayland's default is 4 KB per direction. */
|
||||
wl_display_set_default_max_buffer_size(display, 1u << 20);
|
||||
wl_display_add_client_created_listener(display, &g_client_created);
|
||||
|
||||
wl_global_create(display, &wl_compositor_interface, 6, NULL, bind_compositor);
|
||||
@@ -3846,6 +3931,7 @@ int droiddeck_wayland_run(void) {
|
||||
wl_global_create(display, &wl_output_interface, 4, NULL, bind_output);
|
||||
wl_global_create(display, &xdg_wm_base_interface, 1, NULL, bind_xdg_wm_base);
|
||||
wl_global_create(display, &zwp_linux_dmabuf_v1_interface, 4, NULL, bind_dmabuf);
|
||||
droiddeck_android_wlegl_init(display);
|
||||
wl_global_create(display, &wl_seat_interface, 9, NULL, bind_seat);
|
||||
wl_global_create(display, &banner_desktop_v1_interface, 1, NULL, bind_desktop);
|
||||
wl_global_create(display, &wp_presentation_interface, 2, NULL, bind_presentation);
|
||||
|
||||
@@ -61,3 +61,6 @@ extern volatile int g_no_render_node;
|
||||
|
||||
COMPOSITOR_INTERNAL extern dev_t g_main_device;
|
||||
COMPOSITOR_INTERNAL void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id);
|
||||
|
||||
/* ---- wl_android_buffer.c */
|
||||
COMPOSITOR_INTERNAL void droiddeck_android_wlegl_init(struct wl_display *display);
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef VK_EXTERNAL_IMAGE_SYNC_H
|
||||
#define VK_EXTERNAL_IMAGE_SYNC_H
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
struct vkp_external_client_barrier_pair {
|
||||
VkImageMemoryBarrier acquire;
|
||||
VkImageMemoryBarrier release;
|
||||
};
|
||||
|
||||
static inline int vkp_is_external_client_image(int dmabuf, int blit_dst) {
|
||||
return dmabuf && !blit_dst;
|
||||
}
|
||||
|
||||
/* Client-owned dma-bufs, including AHardwareBuffers, live in GENERAL while
|
||||
* FOREIGN owns them. Layer pool dma-bufs are destinations with a separate
|
||||
* acquire/release lifecycle and must never take this source-image path. */
|
||||
static inline int vkp_external_client_barrier_pair(
|
||||
VkImage image, uint32_t queue_family, int dmabuf, int blit_dst,
|
||||
struct vkp_external_client_barrier_pair *pair) {
|
||||
if (!vkp_is_external_client_image(dmabuf, blit_dst) || !pair) return 0;
|
||||
|
||||
const VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
pair->acquire = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT,
|
||||
.dstQueueFamilyIndex = queue_family,
|
||||
.image = image,
|
||||
.subresourceRange = range,
|
||||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
|
||||
};
|
||||
pair->release = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = queue_family,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT,
|
||||
.image = image,
|
||||
.subresourceRange = range,
|
||||
.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT,
|
||||
};
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,7 +12,7 @@
|
||||
X(CreateAndroidSurfaceKHR) X(DestroySurfaceKHR) X(CreateDevice) \
|
||||
X(GetDeviceProcAddr) X(EnumerateDeviceExtensionProperties) \
|
||||
X(GetPhysicalDeviceMemoryProperties) X(GetPhysicalDeviceFormatProperties2) \
|
||||
X(GetPhysicalDeviceImageFormatProperties2)
|
||||
X(GetPhysicalDeviceImageFormatProperties2) X(GetPhysicalDeviceProperties2)
|
||||
|
||||
#define VK_DEVICE_FUNCS(X) \
|
||||
X(GetDeviceQueue) X(CreateSwapchainKHR) X(DestroySwapchainKHR) X(GetSwapchainImagesKHR) \
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "vk_present.h"
|
||||
#include "vk_loader.h"
|
||||
#include "vk_external_image_sync.h"
|
||||
#include "sc_layer.h"
|
||||
#include "effects_chain.h"
|
||||
#include "cardboard.h"
|
||||
@@ -19,6 +20,7 @@
|
||||
#include <poll.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <android/log.h>
|
||||
#include <android/hardware_buffer.h>
|
||||
|
||||
@@ -38,16 +40,59 @@ struct vkp_image {
|
||||
int in_general;
|
||||
VkFormat fmt;
|
||||
int sampled;
|
||||
AHardwareBuffer *android_buffer;
|
||||
};
|
||||
|
||||
static ANativeWindow *g_window;
|
||||
static int g_ahb_import;
|
||||
static char *g_driver_path, *g_library_name, *g_native_lib_dir;
|
||||
static int g_dev_state;
|
||||
static void device_lost(const char *where);
|
||||
static const char *vk_result_name(VkResult r);
|
||||
|
||||
/* Why the compositor cannot present ("" while it can). Logged as a DD_PRESENT_FAULT line, read
|
||||
* back over JNI, and shown on the session-ended screen instead of a silent black one. */
|
||||
static char g_fault[256];
|
||||
static char g_swap_reason[160];
|
||||
static pthread_mutex_t g_fault_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static void set_fault(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
static void set_fault(const char *fmt, ...) {
|
||||
char msg[sizeof(g_fault)];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(msg, sizeof(msg), fmt, ap);
|
||||
va_end(ap);
|
||||
pthread_mutex_lock(&g_fault_lock);
|
||||
int same = !strcmp(msg, g_fault);
|
||||
if (!same) snprintf(g_fault, sizeof(g_fault), "%s", msg);
|
||||
pthread_mutex_unlock(&g_fault_lock);
|
||||
if (!same) droiddeck_log("present", "DD_PRESENT_FAULT: %s", msg);
|
||||
}
|
||||
static void clear_fault(void) {
|
||||
pthread_mutex_lock(&g_fault_lock);
|
||||
int had = g_fault[0] != 0;
|
||||
g_fault[0] = 0;
|
||||
pthread_mutex_unlock(&g_fault_lock);
|
||||
if (had) droiddeck_log("present", "presentation recovered: fault cleared");
|
||||
}
|
||||
const char *vkp_fault(void) {
|
||||
static __thread char copy[sizeof(g_fault)];
|
||||
pthread_mutex_lock(&g_fault_lock);
|
||||
snprintf(copy, sizeof(copy), "%s", g_fault);
|
||||
pthread_mutex_unlock(&g_fault_lock);
|
||||
if (!copy[0] && g_dev_state == -3)
|
||||
snprintf(copy, sizeof(copy), "the GPU did not confirm the compositor's work finished; presenting stopped");
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* The driver that answered for the display, as the driver itself reports it. */
|
||||
static char g_display_driver[320];
|
||||
const char *vkp_display_driver(void) { return g_display_driver; }
|
||||
static VkInstance g_inst;
|
||||
static VkPhysicalDevice g_pd;
|
||||
static VkDevice g_dev;
|
||||
static VkQueue g_queue;
|
||||
static PFN_vkGetAndroidHardwareBufferPropertiesANDROID g_ahb_properties;
|
||||
static uint32_t g_qfam;
|
||||
static VkCommandPool g_pool;
|
||||
#define MAX_PRESENTS (1 + VKP_FG_MAX_GENERATIONS)
|
||||
@@ -108,10 +153,22 @@ static void perf_add(int64_t *sum, int64_t *max, unsigned *count, int64_t d) {
|
||||
if (d > *max) *max = d;
|
||||
(*count)++;
|
||||
}
|
||||
static void quarantine_pending_work(VkResult result, const char *where) {
|
||||
if (g_dev_state != 1) return;
|
||||
g_dev_state = -3;
|
||||
g_base_black = 0;
|
||||
LOGE("GPU completion unconfirmed (%d in %s): presenting stopped; pending resources retained; "
|
||||
"restart DroidDeck", (int)result, where);
|
||||
}
|
||||
|
||||
static VkResult timed_wait(VkFence fence, uint64_t timeout) {
|
||||
if (g_dev_state != 1)
|
||||
return g_dev_state == -2 ? VK_ERROR_DEVICE_LOST : VK_ERROR_UNKNOWN;
|
||||
int64_t t0 = perf_now();
|
||||
VkResult r = g_vk.WaitForFences(g_dev, 1, &fence, VK_TRUE, timeout);
|
||||
perf_add(&g_perf.wait_ns, &g_perf.wait_max_ns, &g_perf.waits, perf_now() - t0);
|
||||
if (r != VK_SUCCESS && r != VK_ERROR_DEVICE_LOST)
|
||||
quarantine_pending_work(r, "fence wait");
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -206,7 +263,12 @@ static void reset_sync(void) {
|
||||
static void destroy_swapchain(void) {
|
||||
g_base_black = 0;
|
||||
if (g_dev_state != 1) return;
|
||||
g_vk.DeviceWaitIdle(g_dev);
|
||||
VkResult idle = g_vk.DeviceWaitIdle(g_dev);
|
||||
if (idle != VK_SUCCESS) {
|
||||
if (idle == VK_ERROR_DEVICE_LOST) device_lost("swapchain idle");
|
||||
else quarantine_pending_work(idle, "swapchain idle");
|
||||
return;
|
||||
}
|
||||
reset_sync();
|
||||
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
|
||||
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
|
||||
@@ -296,6 +358,7 @@ int vkp_has_window(void) {
|
||||
}
|
||||
|
||||
int vkp_device_lost(void) { return g_dev_state == -2; }
|
||||
int vkp_completion_unknown(void) { return g_dev_state == -3; }
|
||||
|
||||
void vk_present_set_scale_mode(int mode, int alignment) {
|
||||
if (mode < VKP_MODE_OFF || mode > VKP_MODE_INTEGER) mode = VKP_MODE_FIT;
|
||||
@@ -406,11 +469,43 @@ static int has_ext(VkExtensionProperties *e, uint32_t n, const char *name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_proprietary(VkDriverId id) {
|
||||
return id == VK_DRIVER_ID_ARM_PROPRIETARY || id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY ||
|
||||
id == VK_DRIVER_ID_IMAGINATION_PROPRIETARY || id == VK_DRIVER_ID_SAMSUNG_PROPRIETARY ||
|
||||
id == VK_DRIVER_ID_BROADCOM_PROPRIETARY;
|
||||
}
|
||||
|
||||
/* What really loaded: adrenotools falls back to the vendor driver when the requested one fails
|
||||
* to load (hook_impl.cpp), so the name asked for is not proof of the driver in use. */
|
||||
static void log_display_driver(void) {
|
||||
PFN_vkGetPhysicalDeviceProperties2 props2 = g_vk.GetPhysicalDeviceProperties2;
|
||||
VkPhysicalDeviceDriverProperties drv = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 p2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &drv};
|
||||
if (!props2) {
|
||||
snprintf(g_display_driver, sizeof(g_display_driver), "%.200s (driver properties not available)", g_gpu_name);
|
||||
droiddeck_log("gpu", "display driver: %s", g_display_driver);
|
||||
return;
|
||||
}
|
||||
props2(g_pd, &p2);
|
||||
int fell_back = g_library_name && is_proprietary(drv.driverID);
|
||||
snprintf(g_display_driver, sizeof(g_display_driver), "%.80s: %.64s %.96s (driver id %d, Vulkan %u.%u.%u)%s", p2.properties.deviceName,
|
||||
drv.driverName, drv.driverInfo, (int)drv.driverID, VK_API_VERSION_MAJOR(p2.properties.apiVersion),
|
||||
VK_API_VERSION_MINOR(p2.properties.apiVersion), VK_API_VERSION_PATCH(p2.properties.apiVersion),
|
||||
fell_back ? " - FELL BACK to the phone's own driver" : "");
|
||||
droiddeck_log("gpu", "display driver loaded: %s", g_display_driver);
|
||||
if (fell_back)
|
||||
droiddeck_log("gpu", "display driver: %s was requested but the phone's own driver answered; the requested "
|
||||
"driver did not load (a missing library beside it, or a rejected ABI - see the adrenotools lines)",
|
||||
g_library_name);
|
||||
}
|
||||
|
||||
static int dev_init(void) {
|
||||
if (g_dev_state != 0) return g_dev_state == 1 ? 0 : -1;
|
||||
|
||||
if (vk_loader_open(g_driver_path, g_library_name, g_native_lib_dir) != 0) {
|
||||
LOGE("present: vk_loader_open failed"); g_dev_state = -1; return -1;
|
||||
LOGE("present: vk_loader_open failed"); g_dev_state = -1;
|
||||
set_fault("no Vulkan driver could be loaded for the display (%s)", g_library_name ? g_library_name : "libvulkan.so");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *inst_exts[3] = {VK_KHR_SURFACE_EXTENSION_NAME,
|
||||
@@ -441,13 +536,19 @@ static int dev_init(void) {
|
||||
.enabledExtensionCount = n_inst_exts,
|
||||
.ppEnabledExtensionNames = inst_exts};
|
||||
if (g_vk.CreateInstance(&ici, NULL, &g_inst) != VK_SUCCESS) {
|
||||
LOGE("present: vkCreateInstance failed"); g_dev_state = -1; return -1;
|
||||
LOGE("present: vkCreateInstance failed"); g_dev_state = -1;
|
||||
set_fault("vkCreateInstance failed on the display driver (%s)", g_library_name ? g_library_name : "the phone's own");
|
||||
return -1;
|
||||
}
|
||||
vk_loader_load_instance(g_inst);
|
||||
|
||||
uint32_t npd = 0;
|
||||
g_vk.EnumeratePhysicalDevices(g_inst, &npd, NULL);
|
||||
if (!npd) { LOGE("present: no physical devices"); g_dev_state = -1; return -1; }
|
||||
if (!npd) {
|
||||
LOGE("present: no physical devices"); g_dev_state = -1;
|
||||
set_fault("the display driver exposed no GPU to the app");
|
||||
return -1;
|
||||
}
|
||||
VkPhysicalDevice pds[8]; if (npd > 8) npd = 8;
|
||||
g_vk.EnumeratePhysicalDevices(g_inst, &npd, pds);
|
||||
g_pd = VK_NULL_HANDLE;
|
||||
@@ -459,7 +560,11 @@ static int dev_init(void) {
|
||||
for (uint32_t q = 0; q < nq; q++)
|
||||
if (qs[q].queueFlags & VK_QUEUE_GRAPHICS_BIT) { g_pd = pds[i]; g_qfam = q; break; }
|
||||
}
|
||||
if (g_pd == VK_NULL_HANDLE) { LOGE("present: no graphics queue"); g_dev_state = -1; return -1; }
|
||||
if (g_pd == VK_NULL_HANDLE) {
|
||||
LOGE("present: no graphics queue"); g_dev_state = -1;
|
||||
set_fault("the display GPU offers no graphics queue");
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
VkPhysicalDeviceProperties props;
|
||||
g_vk.GetPhysicalDeviceProperties(g_pd, &props);
|
||||
@@ -479,6 +584,21 @@ static int dev_init(void) {
|
||||
g_vk.EnumerateDeviceExtensionProperties(g_pd, NULL, &ne, NULL);
|
||||
VkExtensionProperties *exts = calloc(ne ? ne : 1, sizeof(*exts));
|
||||
g_vk.EnumerateDeviceExtensionProperties(g_pd, NULL, &ne, exts);
|
||||
if (!g_library_name || !strstr(g_library_name, "freedreno")) {
|
||||
char names[440];
|
||||
size_t len = 0;
|
||||
names[0] = 0;
|
||||
droiddeck_log("gpu", "device extensions: %u", ne);
|
||||
for (uint32_t i = 0; i < ne; i++) {
|
||||
if (len + strlen(exts[i].extensionName) + 2 > sizeof(names)) {
|
||||
droiddeck_log("gpu", " %s", names);
|
||||
len = 0;
|
||||
names[0] = 0;
|
||||
}
|
||||
len += (size_t)snprintf(names + len, sizeof(names) - len, "%s%s", len ? " " : "", exts[i].extensionName);
|
||||
}
|
||||
if (len) droiddeck_log("gpu", " %s", names);
|
||||
}
|
||||
for (unsigned i = 0; i < 4; i++) {
|
||||
if (has_ext(exts, ne, import_exts[i])) dev_exts[n_dev_exts++] = import_exts[i];
|
||||
else LOGE("present: driver MISSING %s (dmabuf import will fail)", import_exts[i]);
|
||||
@@ -492,6 +612,8 @@ static int dev_init(void) {
|
||||
if (want_sem_fd) dev_exts[n_dev_exts++] = VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME;
|
||||
g_ahb_import = has_ext(exts, ne, VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME) &&
|
||||
has_ext(exts, ne, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME);
|
||||
/* The present path's own AHB import (#561) stays on the phone's own driver. */
|
||||
const int want_ahb = !g_library_name && g_ahb_import;
|
||||
if (g_ahb_import) {
|
||||
dev_exts[n_dev_exts++] = VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME;
|
||||
dev_exts[n_dev_exts++] = VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME;
|
||||
@@ -514,11 +636,17 @@ static int dev_init(void) {
|
||||
dr = g_vk.CreateDevice(g_pd, &dci, NULL, &g_dev);
|
||||
}
|
||||
if (dr != VK_SUCCESS) {
|
||||
LOGE("present: vkCreateDevice failed"); g_dev_state = -1; return -1;
|
||||
LOGE("present: vkCreateDevice failed"); g_dev_state = -1;
|
||||
set_fault("vkCreateDevice failed on %s (%s)", g_gpu_name[0] ? g_gpu_name : "the display GPU", vk_result_name(dr));
|
||||
return -1;
|
||||
}
|
||||
vk_loader_load_device(g_dev);
|
||||
log_display_driver();
|
||||
g_vk.GetDeviceQueue(g_dev, g_qfam, 0, &g_queue);
|
||||
vkp_gpu_busy_device(g_pd, g_dev, g_qfam);
|
||||
if (want_ahb)
|
||||
g_ahb_properties = (PFN_vkGetAndroidHardwareBufferPropertiesANDROID)
|
||||
g_vk.GetDeviceProcAddr(g_dev, "vkGetAndroidHardwareBufferPropertiesANDROID");
|
||||
if (want_sem_fd) {
|
||||
g_import_sem_fd = (PFN_vkImportSemaphoreFdKHR)g_vk.GetDeviceProcAddr(g_dev, "vkImportSemaphoreFdKHR");
|
||||
VkSemaphoreCreateInfo wsci = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||
@@ -555,6 +683,7 @@ static int dev_init(void) {
|
||||
g_dev_state = 1;
|
||||
reset_sync();
|
||||
init_black_image();
|
||||
if (g_dev_state != 1) return -1;
|
||||
vkp_framegen_device_ready(g_dev, g_queue, g_qfam, fg_features != NULL);
|
||||
return 0;
|
||||
}
|
||||
@@ -566,13 +695,15 @@ static int swap_init_locked(void);
|
||||
static int swap_init(void) {
|
||||
struct timespec ts;
|
||||
int64_t now;
|
||||
if (!g_window) return -1;
|
||||
if (!g_window || g_dev_state != 1) return -1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
now = (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec;
|
||||
if (now < g_swap_retry_at_ns) return -1;
|
||||
g_swap_reason[0] = 0;
|
||||
if (swap_init_locked() == 0) {
|
||||
g_swap_retry_at_ns = 0;
|
||||
g_swap_fail_logged = 0;
|
||||
clear_fault();
|
||||
return 0;
|
||||
}
|
||||
destroy_swapchain();
|
||||
@@ -580,11 +711,12 @@ static int swap_init(void) {
|
||||
if (!g_swap_fail_logged) {
|
||||
g_swap_fail_logged = 1;
|
||||
LOGE("present: no swapchain on the screen surface; retrying every 0.5 s");
|
||||
set_fault("no swapchain on the session screen (%s)", g_swap_reason[0] ? g_swap_reason : "unknown reason");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
#define SWLOGE(...) do { if (!g_swap_fail_logged) LOGE(__VA_ARGS__); } while (0)
|
||||
#define SWLOGE(...) do { snprintf(g_swap_reason, sizeof(g_swap_reason), __VA_ARGS__); if (!g_swap_fail_logged) LOGE(__VA_ARGS__); } while (0)
|
||||
|
||||
static void vk_format_short(VkFormat f, char *out, size_t n) {
|
||||
const char *s = NULL;
|
||||
@@ -824,10 +956,15 @@ static void init_black_image(void) {
|
||||
g_vk.EndCommandBuffer(cmd);
|
||||
VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd};
|
||||
g_vk.ResetFences(g_dev, 1, &g_fence);
|
||||
if (g_vk.QueueSubmit(g_queue, 1, &si, g_fence) == VK_SUCCESS &&
|
||||
g_vk.WaitForFences(g_dev, 1, &g_fence, VK_TRUE, 1000000000ULL) == VK_SUCCESS)
|
||||
g_black_state = 1;
|
||||
else
|
||||
VkResult qr = g_vk.QueueSubmit(g_queue, 1, &si, g_fence);
|
||||
if (qr == VK_SUCCESS) {
|
||||
VkResult fr = timed_wait(g_fence, 1000000000ULL);
|
||||
if (fr == VK_SUCCESS) g_black_state = 1;
|
||||
else if (fr == VK_ERROR_DEVICE_LOST) device_lost("black image");
|
||||
} else if (qr == VK_ERROR_DEVICE_LOST) {
|
||||
device_lost("black image submit");
|
||||
}
|
||||
if (g_black_state != 1)
|
||||
droiddeck_log("gpu", "letterbox black image unavailable: the whole output is cleared every copy-path frame");
|
||||
}
|
||||
|
||||
@@ -926,7 +1063,98 @@ struct vkp_image *vkp_image_from_dmabuf(int fd, uint32_t drm_format, uint64_t mo
|
||||
return vkp_image_import_dmabuf(fd, drm_format, modifier, w, h, stride, offset, 0);
|
||||
}
|
||||
|
||||
int vkp_image_is_dmabuf(const struct vkp_image *img) { return img && img->dmabuf && !img->blit_dst; }
|
||||
/* Return client sources to FOREIGN after their final read. The consuming paths
|
||||
* wait for our fence before Wayland can release an ordinary dma-buf or an
|
||||
* android_wlegl AHardwareBuffer. Producer-fence synchronization is separate from
|
||||
* these image-layout and ownership transfers. */
|
||||
static void release_external_client_image(VkCommandBuffer cmd, struct vkp_image *img) {
|
||||
struct vkp_external_client_barrier_pair pair;
|
||||
if (!img || !vkp_external_client_barrier_pair(
|
||||
img->image, g_qfam, img->dmabuf, img->blit_dst, &pair))
|
||||
return;
|
||||
g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &pair.release);
|
||||
}
|
||||
|
||||
static void release_external_client_draws(VkCommandBuffer cmd, const struct vkp_draw *draws, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int seen = 0;
|
||||
for (int j = 0; j < i; j++) if (draws[j].img == draws[i].img) { seen = 1; break; }
|
||||
if (!seen) release_external_client_image(cmd, draws[i].img);
|
||||
}
|
||||
}
|
||||
|
||||
int vkp_image_is_dmabuf(const struct vkp_image *img) {
|
||||
return img && vkp_is_external_client_image(img->dmabuf, img->blit_dst);
|
||||
}
|
||||
|
||||
int vkp_android_buffer_supported(void) {
|
||||
return dev_init() == 0 && g_ahb_properties != NULL;
|
||||
}
|
||||
|
||||
struct vkp_image *vkp_image_from_android_buffer(AHardwareBuffer *buffer) {
|
||||
if (!buffer || !vkp_android_buffer_supported()) return NULL;
|
||||
AHardwareBuffer_Desc desc;
|
||||
AHardwareBuffer_describe(buffer, &desc);
|
||||
if (!desc.width || !desc.height || desc.layers != 1 ||
|
||||
(desc.usage & (AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT | AHARDWAREBUFFER_USAGE_GPU_MIPMAP_COMPLETE)) ||
|
||||
!(desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE)) return NULL;
|
||||
VkAndroidHardwareBufferFormatPropertiesANDROID format = {
|
||||
.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID};
|
||||
VkAndroidHardwareBufferPropertiesANDROID props = {
|
||||
.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID, .pNext = &format};
|
||||
VkResult result = g_ahb_properties(g_dev, buffer, &props);
|
||||
if (result != VK_SUCCESS ||
|
||||
(format.format != VK_FORMAT_R8G8B8A8_UNORM && format.format != VK_FORMAT_R8G8B8A8_SRGB &&
|
||||
format.format != VK_FORMAT_B8G8R8A8_UNORM && format.format != VK_FORMAT_B8G8R8A8_SRGB) ||
|
||||
!props.memoryTypeBits ||
|
||||
!(format.formatFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT)) {
|
||||
LOGE("android_wlegl: unsupported Android buffer import (%s, format %d)",
|
||||
vk_result_name(result), format.format);
|
||||
return NULL;
|
||||
}
|
||||
struct vkp_image *img = calloc(1, sizeof(*img));
|
||||
if (!img) return NULL;
|
||||
img->w = (int)desc.width; img->h = (int)desc.height;
|
||||
img->fmt = format.format; img->dmabuf = 1;
|
||||
const VkFormatFeatureFlags sampled = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
||||
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
|
||||
img->sampled = (format.formatFeatures & sampled) == sampled;
|
||||
VkExternalMemoryImageCreateInfo external = {
|
||||
.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
|
||||
.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID};
|
||||
VkImageCreateInfo create = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .pNext = &external,
|
||||
.imageType = VK_IMAGE_TYPE_2D, .format = img->fmt,
|
||||
.extent = {desc.width, desc.height, 1}, .mipLevels = 1, .arrayLayers = 1,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
|
||||
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | (img->sampled ? VK_IMAGE_USAGE_SAMPLED_BIT : 0),
|
||||
.sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
result = g_vk.CreateImage(g_dev, &create, NULL, &img->image);
|
||||
if (result != VK_SUCCESS) goto fail;
|
||||
/* AHB image requirements are not queryable before binding. The AHB query supplies them. */
|
||||
uint32_t memory_type = 0;
|
||||
while (!(props.memoryTypeBits & (1u << memory_type))) memory_type++;
|
||||
VkImportAndroidHardwareBufferInfoANDROID import = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID, .buffer = buffer};
|
||||
VkMemoryDedicatedAllocateInfo dedicated = {
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO,
|
||||
.pNext = &import, .image = img->image};
|
||||
VkMemoryAllocateInfo allocate = {
|
||||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .pNext = &dedicated,
|
||||
.allocationSize = props.allocationSize, .memoryTypeIndex = memory_type};
|
||||
result = g_vk.AllocateMemory(g_dev, &allocate, NULL, &img->mem);
|
||||
if (result != VK_SUCCESS) goto fail;
|
||||
result = g_vk.BindImageMemory(g_dev, img->image, img->mem, 0);
|
||||
if (result != VK_SUCCESS) goto fail;
|
||||
AHardwareBuffer_acquire(buffer);
|
||||
img->android_buffer = buffer;
|
||||
return img;
|
||||
fail:
|
||||
LOGE("android_wlegl: Vulkan buffer import failed: %s", vk_result_name(result));
|
||||
vkp_image_destroy(img);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t modifier, int w, int h,
|
||||
uint32_t stride, uint32_t offset, int as_blit_dst) {
|
||||
@@ -940,7 +1168,27 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t
|
||||
const VkImageUsageFlags sampled_usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
img->sampled = !as_blit_dst && g_vk.GetPhysicalDeviceImageFormatProperties2 &&
|
||||
modifier_importable(img->fmt, modifier, sampled_usage);
|
||||
/* Each new format/modifier/size a client hands over, once, for the graphics report: a colour
|
||||
* or swizzle mismatch on a non-Adreno driver shows up as the pair it arrived as. */
|
||||
{
|
||||
static struct { uint32_t fmt; uint64_t mod; int w, h; } seen[32];
|
||||
static int nseen;
|
||||
int known = 0;
|
||||
for (int i = 0; i < nseen; i++)
|
||||
if (seen[i].fmt == drm_format && seen[i].mod == modifier && seen[i].w == w && seen[i].h == h) known = 1;
|
||||
if (!known && nseen < 32) {
|
||||
seen[nseen].fmt = drm_format; seen[nseen].mod = modifier; seen[nseen].w = w; seen[nseen].h = h; nseen++;
|
||||
droiddeck_log("gpu", "dmabuf import: %c%c%c%c %s %dx%d stride %u offset %u -> vk format %d, %s%s",
|
||||
drm_format & 0xff, (drm_format >> 8) & 0xff, (drm_format >> 16) & 0xff, (drm_format >> 24) & 0xff,
|
||||
vkp_modifier_name(modifier), w, h, stride, offset, (int)img->fmt,
|
||||
img->sampled ? "sampled" : "copied", as_blit_dst ? ", blit target" : "");
|
||||
}
|
||||
}
|
||||
|
||||
/* Venus reports a row pitch of 0 for gamescope's 1x1 root buffer, and a zero pitch fails
|
||||
* vkCreateImage, which left the whole window unshown (David Cimo). One packed, 64-byte aligned
|
||||
* row is what a linear buffer that small holds. */
|
||||
if (stride == 0 && modifier == 0) stride = ((uint32_t)w * 4 + 63) & ~63u;
|
||||
VkSubresourceLayout plane = {.offset = offset, .rowPitch = stride};
|
||||
VkImageDrmFormatModifierExplicitCreateInfoEXT modInfo = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT,
|
||||
@@ -1211,7 +1459,7 @@ struct vkp_image *vkp_image_create_shm(int w, int h) {
|
||||
}
|
||||
|
||||
void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride) {
|
||||
if (!img || !img->map || !data) return;
|
||||
if (g_dev_state != 1 || !img || !img->map || !data) return;
|
||||
size_t rowbytes = (size_t)img->w * 4;
|
||||
if ((size_t)stride < rowbytes) rowbytes = (size_t)stride;
|
||||
for (int y = 0; y < img->h; y++)
|
||||
@@ -1224,12 +1472,14 @@ int vkp_image_height(const struct vkp_image *img) { return img ? img->h : 0; }
|
||||
|
||||
void vkp_image_destroy(struct vkp_image *img) {
|
||||
if (!img) return;
|
||||
if (g_dev_state == -3) return; /* completion unknown: retain GPU ownership */
|
||||
if (g_dev) {
|
||||
blendp_forget_image(img->image);
|
||||
if (img->map) g_vk.UnmapMemory(g_dev, img->mem);
|
||||
if (img->image) g_vk.DestroyImage(g_dev, img->image, NULL);
|
||||
if (img->mem) g_vk.FreeMemory(g_dev, img->mem, NULL);
|
||||
}
|
||||
if (img->android_buffer) AHardwareBuffer_release(img->android_buffer);
|
||||
free(img);
|
||||
}
|
||||
|
||||
@@ -1325,11 +1575,14 @@ static void effects_source_note(int scene_w, int scene_h, const struct vkp_draw
|
||||
static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkImageBlit *blit, VkImage target,
|
||||
VkFormat target_fmt, int target_w, int target_h, VkFilter filter) {
|
||||
const struct vkp_image *im = d->img;
|
||||
const int external_client = vkp_image_is_dmabuf(im);
|
||||
if (d->blend && im->sampled &&
|
||||
blendp_draw(cmd, im->image, im->fmt, im->w, im->h, !im->dmabuf, target, target_fmt, target_w, target_h,
|
||||
blendp_draw(cmd, im->image, im->fmt, im->w, im->h, !external_client,
|
||||
target, target_fmt, target_w, target_h,
|
||||
blit) == 0)
|
||||
return;
|
||||
g_vk.CmdBlitImage(cmd, im->image, im->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
g_vk.CmdBlitImage(cmd, im->image,
|
||||
external_client ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, filter);
|
||||
}
|
||||
|
||||
@@ -1491,6 +1744,7 @@ static void device_lost(const char *where) {
|
||||
vkp_gpu_busy_device_lost();
|
||||
droiddeck_log("error", "GPU device lost (VK_ERROR_DEVICE_LOST in %s): the compositor has stopped presenting; "
|
||||
"restart the session", where);
|
||||
set_fault("the display GPU was lost (VK_ERROR_DEVICE_LOST in %s)", where);
|
||||
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
|
||||
if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL);
|
||||
g_swapchain = VK_NULL_HANDLE;
|
||||
@@ -1588,6 +1842,7 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int
|
||||
|
||||
static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) {
|
||||
for (int attempt = 0; ; attempt++) {
|
||||
if (g_dev_state != 1) return -1;
|
||||
if (!g_swapchain && (!first || swap_init() != 0)) return -1;
|
||||
const int64_t t_acq = perf_now();
|
||||
VkResult ar = g_vk.AcquireNextImageKHR(g_dev, g_swapchain, 1000000000ULL, g_acqs[k], VK_NULL_HANDLE, img);
|
||||
@@ -1597,7 +1852,7 @@ static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) {
|
||||
if (ar == VK_ERROR_OUT_OF_DATE_KHR || ar == VK_ERROR_SURFACE_LOST_KHR) {
|
||||
droiddeck_log("gpu", "screen surface %s on acquire: rebuilding the swapchain", vk_result_name(ar));
|
||||
destroy_swapchain();
|
||||
if (first && attempt == 0) continue;
|
||||
if (first && attempt == 0 && g_dev_state == 1) continue;
|
||||
return -1;
|
||||
}
|
||||
droiddeck_log("error", "present: acquire %d failed (%s %d)", k, vk_result_name(ar), (int)ar);
|
||||
@@ -1714,6 +1969,7 @@ static void send_hdr_metadata(const struct droiddeck_color *c) {
|
||||
static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, int n,
|
||||
const struct vkp_hdr_frame *hf, int *how) {
|
||||
if (how) *how = 0;
|
||||
if (g_dev_state < 0) return -1;
|
||||
vkp_apply_window_request();
|
||||
sc_layer_hide_base();
|
||||
check_surface_changed();
|
||||
@@ -1732,6 +1988,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
: "no HDR frames through the screen swapchain any more: rebuilding it as SDR");
|
||||
destroy_swapchain();
|
||||
}
|
||||
if (g_dev_state != 1) return -1;
|
||||
g_swap_hdr_want = want_hdr;
|
||||
|
||||
uint32_t img = 0;
|
||||
@@ -1815,13 +2072,11 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
int seen = 0;
|
||||
for (int j = 0; j < i; j++) if (draws[j].img == im) { seen = 1; break; }
|
||||
if (seen || !im) continue;
|
||||
if (im->dmabuf) {
|
||||
bars[nb++] = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, .dstQueueFamilyIndex = g_qfam,
|
||||
.image = im->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT};
|
||||
} else if (!im->in_general) {
|
||||
struct vkp_external_client_barrier_pair pair;
|
||||
if (vkp_external_client_barrier_pair(
|
||||
im->image, g_qfam, im->dmabuf, im->blit_dst, &pair)) {
|
||||
bars[nb++] = pair.acquire;
|
||||
} else if (!im->blit_dst && !im->in_general) {
|
||||
bars[nb++] = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
@@ -1880,6 +2135,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
drawn++;
|
||||
}
|
||||
|
||||
if (!(pass && fx)) release_external_client_draws(cmd, draws, n);
|
||||
int ngen = 0;
|
||||
VkImage gens[VKP_FG_MAX_GENERATIONS] = {VK_NULL_HANDLE};
|
||||
VkImage result = scene_img;
|
||||
@@ -1901,6 +2157,7 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
result = vkp_effects_run(cmd, scene_img, input_w, input_h, mapped_w, mapped_h, &rw, &rh);
|
||||
effects_overlay_draws(cmd, result, rw, rh, scene_w, scene_h,
|
||||
draws ? draws + overlay_start : NULL, n - overlay_start);
|
||||
release_external_client_draws(cmd, draws, n);
|
||||
result_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
}
|
||||
VkImageMemoryBarrier b_general = {
|
||||
@@ -1932,6 +2189,10 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
if (k > 0) {
|
||||
VkResult ark;
|
||||
if (acquire_image(k, 0, &img, &ark) != 0) {
|
||||
/* Teardown may have retained this frame's pending work, or
|
||||
* acquire may have reported real device loss. Neither permits
|
||||
* a completion-marker submit or another fence wait. */
|
||||
if (g_dev_state != 1) return -1;
|
||||
droiddeck_log("framegen", "present %d/%d: no swapchain image; the frame ends early", k + 1, presents);
|
||||
pr = VK_ERROR_OUT_OF_DATE_KHR;
|
||||
break;
|
||||
@@ -1963,15 +2224,22 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i
|
||||
pr = g_vk.QueuePresentKHR(g_queue, &pi);
|
||||
perf_add(&g_perf.present_ns, &g_perf.present_max_ns, &g_perf.presents, perf_now() - t_pres);
|
||||
if (k < ngen) presented_gen++;
|
||||
if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR || pr == VK_ERROR_DEVICE_LOST) break;
|
||||
if (pr == VK_ERROR_DEVICE_LOST) { device_lost("present"); return -1; }
|
||||
if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR) break;
|
||||
}
|
||||
if (!fence_submitted) {
|
||||
VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
g_vk.QueueSubmit(g_queue, 1, &si, g_fence);
|
||||
VkResult marker = g_vk.QueueSubmit(g_queue, 1, &si, g_fence);
|
||||
if (marker != VK_SUCCESS) {
|
||||
if (marker == VK_ERROR_DEVICE_LOST) device_lost("frame completion submit");
|
||||
else quarantine_pending_work(marker, "frame completion submit");
|
||||
return -1; /* no fence was submitted; never wait on its unsignalled state */
|
||||
}
|
||||
}
|
||||
VkResult fr = timed_wait(g_fence, UINT64_MAX);
|
||||
if (fg) vkp_framegen_presented(presented_gen);
|
||||
if (fr == VK_ERROR_DEVICE_LOST || pr == VK_ERROR_DEVICE_LOST) { device_lost("present"); return -1; }
|
||||
if (fr != VK_SUCCESS) return -1;
|
||||
if (fg) vkp_framegen_presented(presented_gen);
|
||||
if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR) {
|
||||
droiddeck_log("gpu", "screen surface %s on present: rebuilding the swapchain", vk_result_name(pr));
|
||||
destroy_swapchain();
|
||||
@@ -2038,6 +2306,7 @@ void vkp_reset_first_frame(void) {
|
||||
void vkp_signal_first_frame(void) {
|
||||
if (g_first_frame_done) return;
|
||||
g_first_frame_done = 1;
|
||||
clear_fault();
|
||||
droiddeck_on_first_frame();
|
||||
}
|
||||
|
||||
@@ -2051,6 +2320,7 @@ int vkp_update_map(int scene_w, int scene_h) {
|
||||
return g_map.valid ? 0 : -1;
|
||||
}
|
||||
check_surface_changed();
|
||||
if (g_dev_state != 1) return -1;
|
||||
if (!g_swapchain && swap_init() != 0) return -1;
|
||||
update_map(scene_w, scene_h);
|
||||
return g_map.valid ? 0 : -1;
|
||||
@@ -2073,7 +2343,7 @@ int vkp_map_draw(const struct vkp_draw *d, int out[8]) {
|
||||
}
|
||||
|
||||
int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) {
|
||||
if (!src || !dst || !dst->blit_dst || g_dev_state == -2 || dev_init() != 0) {
|
||||
if (!src || src->blit_dst || !dst || !dst->blit_dst || g_dev_state == -2 || dev_init() != 0) {
|
||||
if (wait_fd >= 0) close(wait_fd);
|
||||
return -1;
|
||||
}
|
||||
@@ -2086,17 +2356,21 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) {
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
g_vk.BeginCommandBuffer(g_cmd, &bi);
|
||||
struct vkp_external_client_barrier_pair src_pair;
|
||||
const int src_external = vkp_external_client_barrier_pair(
|
||||
src->image, g_qfam, src->dmabuf, src->blit_dst, &src_pair);
|
||||
VkImageMemoryBarrier acq[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
src_external ? src_pair.acquire : (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = src->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT},
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, .dstQueueFamilyIndex = g_qfam,
|
||||
.image = dst->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}};
|
||||
if (!src->dmabuf) {
|
||||
if (!src_external) {
|
||||
acq[0].oldLayout = src->in_general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_PREINITIALIZED;
|
||||
acq[0].newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
acq[0].srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
|
||||
@@ -2110,8 +2384,9 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) {
|
||||
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
||||
.dstOffsets = {{0, 0, 0}, {bw, bh, 1}}};
|
||||
g_vk.CmdBlitImage(g_cmd, src->image,
|
||||
src->dmabuf ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
src_external ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL,
|
||||
dst->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_NEAREST);
|
||||
release_external_client_image(g_cmd, src);
|
||||
VkImageMemoryBarrier rel = {
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = g_qfam,
|
||||
@@ -2146,7 +2421,7 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) {
|
||||
static struct vkp_image *g_rb;
|
||||
|
||||
int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) {
|
||||
if (!src || !out || src->w <= 0 || src->h <= 0 || src->w * src->h > max_px ||
|
||||
if (!src || src->blit_dst || !out || src->w <= 0 || src->h <= 0 || src->w * src->h > max_px ||
|
||||
g_dev_state == -2 || dev_init() != 0)
|
||||
return -1;
|
||||
if (g_rb && (g_rb->w != src->w || g_rb->h != src->h)) { vkp_image_destroy(g_rb); g_rb = NULL; }
|
||||
@@ -2187,11 +2462,15 @@ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) {
|
||||
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
|
||||
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
|
||||
g_vk.BeginCommandBuffer(cmd, &bi);
|
||||
struct vkp_external_client_barrier_pair src_pair;
|
||||
const int src_external = vkp_external_client_barrier_pair(
|
||||
src->image, g_qfam, src->dmabuf, src->blit_dst, &src_pair);
|
||||
VkImageMemoryBarrier acq[2] = {
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
src_external ? src_pair.acquire : (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = src->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT},
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
@@ -2209,15 +2488,20 @@ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) {
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range,
|
||||
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT},
|
||||
{.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT}};
|
||||
uint32_t done_count = 1;
|
||||
if (!src_external) {
|
||||
done[done_count++] = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
.srcQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = src->dmabuf ? VK_QUEUE_FAMILY_FOREIGN_EXT : VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = src->image, .subresourceRange = range, .srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT}};
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = src->image, .subresourceRange = range, .srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT};
|
||||
}
|
||||
g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
|
||||
0, 0, NULL, 0, NULL, 2, done);
|
||||
0, 0, NULL, 0, NULL, done_count, done);
|
||||
release_external_client_image(cmd, src);
|
||||
g_vk.EndCommandBuffer(cmd);
|
||||
VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd};
|
||||
g_vk.ResetFences(g_dev, 1, &g_fence);
|
||||
@@ -2307,13 +2591,11 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
|
||||
int seen = 0;
|
||||
for (int j = 0; j < i; j++) if (draws[j].img == im) { seen = 1; break; }
|
||||
if (seen || !im) continue;
|
||||
if (im->dmabuf) {
|
||||
bars[nb++] = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, .dstQueueFamilyIndex = g_qfam,
|
||||
.image = im->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT};
|
||||
} else if (!im->in_general) {
|
||||
struct vkp_external_client_barrier_pair pair;
|
||||
if (vkp_external_client_barrier_pair(
|
||||
im->image, g_qfam, im->dmabuf, im->blit_dst, &pair)) {
|
||||
bars[nb++] = pair.acquire;
|
||||
} else if (!im->blit_dst && !im->in_general) {
|
||||
bars[nb++] = (VkImageMemoryBarrier){
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED,
|
||||
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
@@ -2358,6 +2640,7 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
|
||||
&g_pass.rw, &g_pass.rh);
|
||||
effects_overlay_draws(cmd, g_pass.result, g_pass.rw, g_pass.rh, scene_w, scene_h,
|
||||
draws ? draws + overlay_start : NULL, n - overlay_start);
|
||||
release_external_client_draws(cmd, draws, n);
|
||||
g_pass.active = 1;
|
||||
if (rw) *rw = g_pass.rw;
|
||||
if (rh) *rh = g_pass.rh;
|
||||
@@ -2367,8 +2650,9 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw
|
||||
int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) {
|
||||
if (!g_pass.active) { if (wait_fd >= 0) close(wait_fd); return -1; }
|
||||
g_pass.active = 0;
|
||||
if (g_dev_state != 1) { if (wait_fd >= 0) close(wait_fd); return -1; }
|
||||
VkCommandBuffer cmd = g_cmds[0];
|
||||
if (!dst || !dst->blit_dst || g_dev_state == -2) {
|
||||
if (!dst || !dst->blit_dst) {
|
||||
if (wait_fd >= 0) close(wait_fd);
|
||||
g_vk.EndCommandBuffer(cmd);
|
||||
return -1;
|
||||
@@ -2420,5 +2704,6 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) {
|
||||
void vkp_pass_abort(void) {
|
||||
if (!g_pass.active) return;
|
||||
g_pass.active = 0;
|
||||
g_vk.EndCommandBuffer(g_cmds[0]);
|
||||
if (g_dev_state == 1)
|
||||
g_vk.EndCommandBuffer(g_cmds[0]);
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/hardware_buffer.h>
|
||||
|
||||
void vk_present_set_driver(const char *driver_path, const char *library_name,
|
||||
const char *native_lib_dir);
|
||||
|
||||
@@ -22,6 +24,9 @@ void vkp_output_size(int *w, int *h);
|
||||
|
||||
struct vkp_image;
|
||||
|
||||
int vkp_android_buffer_supported(void);
|
||||
struct vkp_image *vkp_image_from_android_buffer(AHardwareBuffer *buffer);
|
||||
|
||||
#define VKP_MOD_LINEAR 0x0000000000000000ULL
|
||||
#define VKP_MOD_QCOM_COMPRESSED 0x0500000000000001ULL
|
||||
#define VKP_MOD_INVALID 0x00ffffffffffffffULL
|
||||
@@ -61,6 +66,10 @@ int vkp_ready(void);
|
||||
int vkp_has_window(void);
|
||||
int vkp_wait_window_request(int timeout_ms);
|
||||
int vkp_device_lost(void);
|
||||
/* Compositor thread only. Submitted reads may still be pending: retain imported
|
||||
* buffer references and withhold DMA-BUF releases until the app process exits.
|
||||
* This terminal state is distinct from genuine device loss. */
|
||||
int vkp_completion_unknown(void);
|
||||
|
||||
int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n);
|
||||
int vkp_render_plain(int scene_w, int scene_h);
|
||||
|
||||
@@ -216,6 +216,24 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv
|
||||
return (jint)g_comp_tid;
|
||||
}
|
||||
|
||||
/* Why presenting failed ("" while it works), for the session-ended screen and the report. */
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorFault(JNIEnv *env, jclass clazz) {
|
||||
(void)clazz;
|
||||
extern const char *vkp_fault(void);
|
||||
const char *f = vkp_fault();
|
||||
return (*env)->NewStringUTF(env, f ? f : "");
|
||||
}
|
||||
|
||||
/* The display driver as the driver itself reports it ("" before the compositor's device exists). */
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeDisplayDriver(JNIEnv *env, jclass clazz) {
|
||||
(void)clazz;
|
||||
extern const char *vkp_display_driver(void);
|
||||
const char *d = vkp_display_driver();
|
||||
return (*env)->NewStringUTF(env, d ? d : "");
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStart(JNIEnv *env, jclass clazz,
|
||||
jstring xdgRuntimeDir) {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/* android_wlegl buffer import for the existing libhybris Wayland WSI.
|
||||
* Protocol: Collabora/libhybris, vendored with its license in protocols/wayland-android.xml.
|
||||
* Native handles are cloned through Android; opaque vendor integers are never interpreted. */
|
||||
#include "compositor_internal.h"
|
||||
#include "droiddeck_ext.h"
|
||||
#include "wayland-android-server-protocol.h"
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Stable native_handle_t ABI, unavailable in the public NDK. */
|
||||
struct android_native_handle {
|
||||
int version, num_fds, num_ints;
|
||||
int data[];
|
||||
};
|
||||
struct wire_handle {
|
||||
struct android_native_handle *handle;
|
||||
int received;
|
||||
};
|
||||
enum { MAX_HANDLE_FDS = 64, MAX_HANDLE_INTS = 256, MAX_BUFFER_SIDE = 8192 };
|
||||
/* VNDK ABI (not exposed by the NDK): frameworks/native/libs/nativewindow/include/
|
||||
* vndk/hardware_buffer.h, AHARDWAREBUFFER_CREATE_FROM_HANDLE_METHOD_CLONE = 3.
|
||||
* Method 1 is not accepted by AHardwareBuffer_createFromHandle. */
|
||||
enum { CREATE_FROM_HANDLE_CLONE = 3 };
|
||||
static int (*import_handle)(const AHardwareBuffer_Desc *, const struct android_native_handle *,
|
||||
int32_t, AHardwareBuffer **);
|
||||
static const struct android_native_handle *(*native_handle)(const AHardwareBuffer *);
|
||||
|
||||
static int valid_description(int32_t width, int32_t height, int32_t stride, int32_t format) {
|
||||
return width > 0 && height > 0 && width <= MAX_BUFFER_SIDE && height <= MAX_BUFFER_SIDE &&
|
||||
stride >= width && stride <= MAX_BUFFER_SIDE * 4 &&
|
||||
(format == 1 || format == 2 || format == 5);
|
||||
}
|
||||
|
||||
static void handle_resource_destroy(struct wl_resource *resource) {
|
||||
struct wire_handle *wire = wl_resource_get_user_data(resource);
|
||||
if (!wire) return;
|
||||
for (int i = 0; i < wire->received; i++) close(wire->handle->data[i]);
|
||||
free(wire->handle);
|
||||
free(wire);
|
||||
}
|
||||
|
||||
static void handle_add_fd(struct wl_client *client, struct wl_resource *resource, int32_t fd) {
|
||||
struct wire_handle *wire = wl_resource_get_user_data(resource);
|
||||
if (wire->received >= wire->handle->num_fds) {
|
||||
close(fd);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_HANDLE_ERROR_TOO_MANY_FDS,
|
||||
"more native handle descriptors than declared");
|
||||
return;
|
||||
}
|
||||
wire->handle->data[wire->received++] = fd;
|
||||
}
|
||||
|
||||
static void handle_destroy(struct wl_client *client, struct wl_resource *resource) {
|
||||
wl_resource_destroy(resource);
|
||||
}
|
||||
|
||||
static const struct android_wlegl_handle_interface handle_impl = {
|
||||
.add_fd = handle_add_fd, .destroy = handle_destroy,
|
||||
};
|
||||
|
||||
static void create_handle(struct wl_client *client, struct wl_resource *resource,
|
||||
uint32_t id, int32_t fds, struct wl_array *ints) {
|
||||
if (fds < 0 || fds > MAX_HANDLE_FDS || ints->size % sizeof(int32_t) ||
|
||||
ints->size > MAX_HANDLE_INTS * sizeof(int32_t)) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid native handle size");
|
||||
return;
|
||||
}
|
||||
struct wire_handle *wire = calloc(1, sizeof(*wire));
|
||||
if (!wire) { wl_client_post_no_memory(client); return; }
|
||||
wire->handle = calloc(1, sizeof(*wire->handle) + (size_t)fds * sizeof(int32_t) + ints->size);
|
||||
if (!wire->handle) { free(wire); wl_client_post_no_memory(client); return; }
|
||||
wire->handle->version = sizeof(*wire->handle);
|
||||
wire->handle->num_fds = fds;
|
||||
wire->handle->num_ints = (int)(ints->size / sizeof(int32_t));
|
||||
if (ints->size) memcpy(wire->handle->data + fds, ints->data, ints->size);
|
||||
struct wl_resource *handle = wl_resource_create(client, &android_wlegl_handle_interface, 1, id);
|
||||
if (!handle) {
|
||||
free(wire->handle); free(wire); wl_client_post_no_memory(client); return;
|
||||
}
|
||||
wl_resource_set_implementation(handle, &handle_impl, wire, handle_resource_destroy);
|
||||
}
|
||||
|
||||
static void buffer_resource_destroy(struct wl_resource *resource) {
|
||||
dmabuf_buffer_unref(wl_resource_get_user_data(resource));
|
||||
}
|
||||
|
||||
static struct wl_resource *create_android_buffer(struct wl_client *client, uint32_t id,
|
||||
AHardwareBuffer *hardware) {
|
||||
AHardwareBuffer_Desc desc;
|
||||
AHardwareBuffer_describe(hardware, &desc);
|
||||
struct vkp_image *image = vkp_image_from_android_buffer(hardware);
|
||||
if (!image) return NULL;
|
||||
struct dmabuf_buffer *buffer = calloc(1, sizeof(*buffer));
|
||||
if (!buffer) { vkp_image_destroy(image); wl_client_post_no_memory(client); return NULL; }
|
||||
buffer->refs = 1;
|
||||
buffer->width = (int32_t)desc.width; buffer->height = (int32_t)desc.height;
|
||||
buffer->format = desc.format == 5 ? DRM_ARGB8888 : desc.format == 2 ? DRM_XBGR8888 : DRM_ABGR8888;
|
||||
buffer->modifier = MOD_INVALID; /* No DMA-BUF layout was inferred from the native handle. */
|
||||
buffer->img = image;
|
||||
for (int i = 0; i < MAX_PLANES; i++) buffer->fd[i] = -1;
|
||||
struct wl_resource *result = wl_resource_create(client, &wl_buffer_interface, 1, id);
|
||||
if (!result) { dmabuf_buffer_unref(buffer); wl_client_post_no_memory(client); return NULL; }
|
||||
wl_resource_set_implementation(result, &dbuf_buffer_impl, buffer, buffer_resource_destroy);
|
||||
struct client_info *info = client_info_of(client);
|
||||
if (info) info->dmabuf_buffers++;
|
||||
return result;
|
||||
}
|
||||
|
||||
static void create_buffer(struct wl_client *client, struct wl_resource *resource, uint32_t id,
|
||||
int32_t width, int32_t height, int32_t stride, int32_t format,
|
||||
int32_t usage, struct wl_resource *handle_resource) {
|
||||
/* Some vendor allocators (Samsung Xclipse) record no stride for GPU-only buffers. The import
|
||||
* must pass that 0 back unchanged: gralloc validates it against the handle's own value. */
|
||||
if (!valid_description(width, height, stride ? stride : width, format) ||
|
||||
!wl_resource_instance_of(handle_resource, &android_wlegl_handle_interface, &handle_impl)) {
|
||||
droiddeck_log("display", "android_wlegl rejected: %dx%d stride=%d format=%d usage=0x%x",
|
||||
width, height, stride, format, (uint32_t)usage);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid Android buffer description");
|
||||
return;
|
||||
}
|
||||
struct wire_handle *wire = wl_resource_get_user_data(handle_resource);
|
||||
if (wire->received != wire->handle->num_fds || !wire->received) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "incomplete native handle");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_Desc desc = {
|
||||
.width = (uint32_t)width, .height = (uint32_t)height, .layers = 1,
|
||||
.format = (uint32_t)format, .usage = (uint32_t)usage, .stride = (uint32_t)stride};
|
||||
AHardwareBuffer *hardware = NULL;
|
||||
int status = import_handle(&desc, wire->handle, CREATE_FROM_HANDLE_CLONE, &hardware);
|
||||
if (status != 0 || !hardware) {
|
||||
droiddeck_log("display", "android_wlegl import failed: status=%d %dx%d stride=%d format=%d usage=0x%x fds=%d ints=%d",
|
||||
status, width, height, stride, format, (uint32_t)usage,
|
||||
wire->handle->num_fds, wire->handle->num_ints);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE,
|
||||
"Android rejected the native handle (status %d)", status);
|
||||
return;
|
||||
}
|
||||
struct wl_resource *buffer = create_android_buffer(client, id, hardware);
|
||||
AHardwareBuffer_release(hardware);
|
||||
if (!buffer) wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "Vulkan cannot import this buffer");
|
||||
}
|
||||
|
||||
static void get_server_buffer(struct wl_client *client, struct wl_resource *resource, uint32_t id,
|
||||
int32_t width, int32_t height, int32_t format, int32_t usage) {
|
||||
if (!format) format = 1;
|
||||
if (!valid_description(width, height, width, format)) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "invalid buffer allocation request");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_Desc desc = {
|
||||
.width = (uint32_t)width, .height = (uint32_t)height, .layers = 1,
|
||||
.format = (uint32_t)format,
|
||||
.usage = (uint32_t)usage | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT};
|
||||
AHardwareBuffer *hardware = NULL;
|
||||
if (AHardwareBuffer_allocate(&desc, &hardware) != 0 || !hardware) {
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_VALUE, "Android buffer allocation failed");
|
||||
return;
|
||||
}
|
||||
AHardwareBuffer_describe(hardware, &desc);
|
||||
const struct android_native_handle *handle = native_handle(hardware);
|
||||
if (!handle || handle->num_fds < 1 || handle->num_fds > MAX_HANDLE_FDS ||
|
||||
handle->num_ints < 0 || handle->num_ints > MAX_HANDLE_INTS) {
|
||||
AHardwareBuffer_release(hardware);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "invalid allocated native handle");
|
||||
return;
|
||||
}
|
||||
struct wl_resource *buffer = create_android_buffer(client, 0, hardware);
|
||||
if (!buffer) {
|
||||
AHardwareBuffer_release(hardware);
|
||||
wl_resource_post_error(resource, ANDROID_WLEGL_ERROR_BAD_HANDLE, "Vulkan cannot import allocated buffer");
|
||||
return;
|
||||
}
|
||||
struct wl_resource *reply = wl_resource_create(client, &android_wlegl_server_buffer_handle_interface,
|
||||
wl_resource_get_version(resource), id);
|
||||
if (!reply) {
|
||||
wl_resource_destroy(buffer); AHardwareBuffer_release(hardware);
|
||||
wl_client_post_no_memory(client); return;
|
||||
}
|
||||
struct wl_array ints = {.size = (size_t)handle->num_ints * sizeof(int32_t),
|
||||
.alloc = 0, .data = (void *)(handle->data + handle->num_fds)};
|
||||
android_wlegl_server_buffer_handle_send_buffer_ints(reply, &ints);
|
||||
for (int i = 0; i < handle->num_fds; i++)
|
||||
android_wlegl_server_buffer_handle_send_buffer_fd(reply, handle->data[i]);
|
||||
/* No linear_layout event: Android's opaque allocation need not be linear. */
|
||||
android_wlegl_server_buffer_handle_send_buffer(reply, buffer, (int32_t)desc.format, (int32_t)desc.stride);
|
||||
wl_resource_destroy(reply);
|
||||
AHardwareBuffer_release(hardware);
|
||||
}
|
||||
|
||||
static const struct android_wlegl_interface wlegl_impl = {
|
||||
.create_handle = create_handle, .create_buffer = create_buffer,
|
||||
.get_server_buffer_handle = get_server_buffer,
|
||||
};
|
||||
|
||||
static void bind_android_wlegl(struct wl_client *client, void *data, uint32_t version, uint32_t id) {
|
||||
struct wl_resource *resource = wl_resource_create(client, &android_wlegl_interface,
|
||||
version < 3 ? version : 3, id);
|
||||
if (!resource) { wl_client_post_no_memory(client); return; }
|
||||
wl_resource_set_implementation(resource, &wlegl_impl, NULL, NULL);
|
||||
}
|
||||
|
||||
void droiddeck_android_wlegl_init(struct wl_display *display) {
|
||||
if (!vkp_android_buffer_supported()) return;
|
||||
void *library = dlopen("libnativewindow.so", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!library) return;
|
||||
import_handle = dlsym(library, "AHardwareBuffer_createFromHandle");
|
||||
native_handle = dlsym(library, "AHardwareBuffer_getNativeHandle");
|
||||
if (!import_handle || !native_handle) { dlclose(library); return; }
|
||||
/* Keep the platform entry points valid for the compositor's lifetime. */
|
||||
if (wl_global_create(display, &android_wlegl_interface, 3, NULL, bind_android_wlegl))
|
||||
droiddeck_log("display", "android_wlegl v3: Android buffer import available");
|
||||
}
|
||||
@@ -14,10 +14,12 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import com.droiddeck.launcher.gpu.AutoDriverTrial
|
||||
import com.droiddeck.launcher.gpu.AutoPicks
|
||||
import com.droiddeck.launcher.gpu.DriverBundle
|
||||
import com.droiddeck.launcher.core.DeviceSupport
|
||||
import com.droiddeck.launcher.gpu.DriverPairs
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import com.droiddeck.launcher.gpu.TurnipReleases
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
@@ -42,7 +44,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
var canRestoreBundled by mutableStateOf(false)
|
||||
private val releaseProgress = HashMap<String, Int>()
|
||||
var linuxSelected by mutableStateOf("")
|
||||
val gpu: GpuInfo = GpuInfo.detect()
|
||||
var gpu: GpuInfo = GpuInfo.detect()
|
||||
private set
|
||||
var mode by mutableStateOf(SessionPrefs.GPU_DRIVERS_AUTO)
|
||||
var pairRows by mutableStateOf<List<PairRow>>(emptyList())
|
||||
var pairBusy by mutableStateOf<String?>(null)
|
||||
@@ -65,13 +68,14 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}
|
||||
|
||||
fun state() = GpuDriversState(
|
||||
gpuName = gpu.displayName(activity), gpuFamily = gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)),
|
||||
gpuName = gpu.displayName(activity), gpuFamily = if (gpu.name.contains("Xclipse", ignoreCase = true)) gpu.family.label(gpu.name) else gpu.family.label(activity), soc = gpu.soc, supportText = gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)),
|
||||
supported = gpu.support == GpuInfo.Support.TESTED, unsupported = gpu.support == GpuInfo.Support.UNSUPPORTED,
|
||||
auto = mode == SessionPrefs.GPU_DRIVERS_AUTO, pairs = pairRows, busy = pairBusy, percent = pairPercent,
|
||||
autoStatus = autoStatus, releaseStatus = releaseStatus.ifEmpty { activity.getString(R.string.drivers_not_checked) }, checking = releaseChecking,
|
||||
linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected,
|
||||
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled,
|
||||
activeBundle = activeBundle,
|
||||
activeBundle = activeBundle, powerVr = gpu.family == GpuInfo.Family.POWERVR,
|
||||
systemPowerVr = SystemVulkanDriver.usesDefault(linuxSelected),
|
||||
mode = uiMode, display = displaySlot(), runtime = runtimeSlot(),
|
||||
canRestart = !sessionRunning,
|
||||
checkedAt = TurnipReleases.cached(activity)?.checkedAt ?: 0L,
|
||||
@@ -142,7 +146,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
DriverRow(id, it.label, activity.getString(if (linux) R.string.drivers_bundle_sets_display else R.string.drivers_bundle_sets_runtime), true, DriverRow.BUNDLE)
|
||||
}
|
||||
linuxRows = LinuxVulkanDriver.optionValues(activity).map { id ->
|
||||
if (id.isEmpty()) DriverRow("", activity.getString(R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
|
||||
if (id.isEmpty()) DriverRow("", activity.getString(if (gpu.family == GpuInfo.Family.POWERVR) R.string.gpu_system_powervr else R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
|
||||
else bundleRow(id, linux = true) ?: DriverRow(
|
||||
id, lm.getDriverName(id),
|
||||
listOfNotNull(
|
||||
@@ -177,6 +181,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
|
||||
fun summary(): String {
|
||||
val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle
|
||||
?: if (SystemVulkanDriver.usesDefault(linuxSelected) && androidSelected == TurnipDriver.AUTO) activity.getString(R.string.gpu_system_powervr) else null
|
||||
val display = displaySlot().name
|
||||
val base = when {
|
||||
uiMode == GPU_MODE_AUTO -> activity.getString(R.string.drivers_summary, activity.getString(R.string.common_auto), active ?: display)
|
||||
@@ -193,17 +198,28 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
|
||||
val recommended = if (QuestExperimental.isSupported()) null else recommendedKey(pairs)
|
||||
pairRows = pairs.map { p ->
|
||||
val bundle = p.bundle?.let { installedBundle(it) }
|
||||
val nad = DriverPairs.isNonAdreno(p.key)
|
||||
val bundle = p.bundle?.let { if (nad) installedBundleAnyBuild(it) else installedBundle(it) }
|
||||
val displayId = if (p.bundle != null) bundle?.androidId else p.display?.let { TurnipReleases.installedId(activity, it, td::isInstalled) }
|
||||
val linuxId = if (p.bundle != null) bundle?.linuxId else p.linux?.let { TurnipReleases.installedId(activity, it, lm::isInstalled) }
|
||||
val mb = p.assets.sumOf { it.size } / 1_048_576.0
|
||||
val update = nad && bundle != null && p.bundle?.let { TurnipReleases.updateAvailable(activity, it) } == true
|
||||
val builtOn = if (nad) p.bundle?.builtOn.orEmpty() else ""
|
||||
val version = when {
|
||||
!nad -> p.version
|
||||
bundle != null -> bundle.version.ifEmpty { p.version }
|
||||
builtOn.isNotEmpty() -> activity.getString(R.string.drivers_built_on, builtOn)
|
||||
else -> p.version
|
||||
}
|
||||
PairRow(
|
||||
key = p.key, name = p.name, version = p.version,
|
||||
key = p.key, name = p.name, version = version, update = if (update) builtOn.takeIf { it.isNotEmpty() }?.let { activity.getString(R.string.drivers_built_on, it) } ?: p.version else null,
|
||||
detail = if (!p.complete) activity.getString(R.string.drivers_half_published) else activity.getString(R.string.drivers_pair_size, mb),
|
||||
recommended = p.key == recommended, suits = p.suits(gpu), complete = p.complete,
|
||||
installed = displayId != null && linuxId != null,
|
||||
active = displayId != null && linuxId != null &&
|
||||
SessionPrefs.androidDriver(activity) == displayId && SessionPrefs.linuxDriver(activity) == linuxId,
|
||||
active = displayId != null && linuxId != null && SessionPrefs.linuxDriver(activity) == linuxId &&
|
||||
(SessionPrefs.androidDriver(activity) == displayId ||
|
||||
// A Mali pair under Auto: the display is the phone's own driver.
|
||||
(gpu.family.mali && DriverPairs.isNonAdreno(p.key) && SessionPrefs.androidDriver(activity) == TurnipDriver.AUTO)),
|
||||
)
|
||||
}.sortedByDescending { it.recommended }
|
||||
}
|
||||
@@ -237,7 +253,9 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
private fun releasesStale(): Boolean {
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
return cached == null || System.currentTimeMillis() - cached.checkedAt > RELEASE_CHECK_INTERVAL_MS ||
|
||||
cached.assets.none { it.pair.isNotEmpty() }
|
||||
cached.assets.none { it.pair.isNotEmpty() } ||
|
||||
// A list checked before this device's open non-Adreno drivers were offered has none of them.
|
||||
(!DeviceSupport.adreno() && cached.assets.none { DriverPairs.isNonAdreno(it.pair) })
|
||||
}
|
||||
|
||||
fun ensureAuto(force: Boolean) {
|
||||
@@ -247,9 +265,33 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (force) autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return
|
||||
}
|
||||
if (gpu.family == GpuInfo.Family.POWERVR && !SystemVulkanDriver.isXclipse()) {
|
||||
// Auto owns both choices. Manual imports remain available when Manual is selected.
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(activity)
|
||||
autoStatus = activity.getString(R.string.drivers_powervr_builtin)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
// A Mali pair set by an earlier build put its PanVK Android half on the display, which fails
|
||||
// on Mali gralloc. Under Auto the display is the phone's own driver (Android's loader).
|
||||
if (gpu.family.mali) {
|
||||
// Builds before 0.5.0-nad1 turned the bridge on under Auto without recording it, and
|
||||
// left it on for good (it wins over the pair). Under Auto that switch is taken as Auto's.
|
||||
if (!SessionPrefs.maliFallbackKnown(activity))
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, if (SessionPrefs.vkBridge(activity)) "bridge" else "")
|
||||
// A lone imported PanVK Android driver set by hand stalls the display the same way
|
||||
// ("black screen with the start-up logo"), so under Auto any imported display goes.
|
||||
val display = SessionPrefs.androidDriver(activity)
|
||||
if (display != TurnipDriver.AUTO) {
|
||||
SessionPrefs.setAndroidDriver(activity, TurnipDriver.AUTO)
|
||||
Log.i(TAG, "auto: Mali display back on the phone's own driver (was $display)")
|
||||
}
|
||||
}
|
||||
AutoDriverTrial.rollbackDue(activity)?.let { trial ->
|
||||
AutoDriverTrial.rollBack(activity, trial)
|
||||
autoStatus = activity.getString(R.string.drivers_rolled_back, trial.name)
|
||||
// The drivers before the trial on a Mali are the runtime's Turnip, which finds no GPU there.
|
||||
if (gpu.family.mali && maliHasNoRoute()) useMaliFallback(trial.name)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
@@ -263,6 +305,29 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
if (gpu.family.mali && DriverPairs.recommendedKey(gpu, emptyList()) == null &&
|
||||
com.droiddeck.launcher.gpu.VulkanInfo.cachedOrNull() == null) {
|
||||
releaseChecking = true
|
||||
autoStatus = activity.getString(R.string.drivers_checking_latest)
|
||||
Thread({
|
||||
com.droiddeck.launcher.gpu.VulkanInfo.query()
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
gpu = GpuInfo.detect()
|
||||
Log.i(TAG, "auto: Mali named by its driver: ${gpu.name} -> ${gpu.family}")
|
||||
lastAutoCheck = 0L
|
||||
ensureAuto(force)
|
||||
}
|
||||
}, "gpu-name").start()
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "auto: ${gpu.name} (${gpu.family}) -> " + (DriverPairs.recommendedKey(gpu, emptyList()) ?: "no pair"))
|
||||
if (gpu.family.mali && DriverPairs.recommendedKey(gpu, emptyList()) == null) {
|
||||
// No PanVK build covers this Mali (e.g. the G1 series, arch v14): the phone's own driver.
|
||||
autoStatus = useMaliFallback(null)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
if (DriverPairs.recommendedKey(gpu, emptyList()) == null) {
|
||||
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText(activity, com.droiddeck.launcher.core.DeviceSupport.vkBridge(activity)).lowercase())
|
||||
return
|
||||
@@ -284,10 +349,37 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
when {
|
||||
row?.active == true -> autoStatus = problem?.let { activity.getString(R.string.drivers_up_to_date_check_failed, it) }
|
||||
?: activity.getString(R.string.upd_current_status)
|
||||
gpu.family == GpuInfo.Family.POWERVR && (pair == null || !pair.complete) -> {
|
||||
// RADV is not published right now: the Xclipse falls back to the system route.
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(activity)
|
||||
autoStatus = activity.getString(R.string.drivers_powervr_builtin)
|
||||
refreshDrivers()
|
||||
}
|
||||
gpu.family.mali && (pair == null || !pair.complete) &&
|
||||
(problem != null || TurnipReleases.cached(activity)?.failed?.contains(TurnipReleases.NON_ADRENO_LABEL) == true) -> {
|
||||
// The list could not be read: nothing is known to be missing, so a working route stays.
|
||||
// With no open driver, Venus or bridge the session would run the runtime's Turnip,
|
||||
// which finds no GPU on a Mali, so the stand-in goes on anyway.
|
||||
autoStatus = activity.getString(R.string.upd_couldnt_check, problem ?: TurnipReleases.NON_ADRENO_LABEL)
|
||||
if (maliHasNoRoute()) {
|
||||
useMaliFallback(null)
|
||||
refreshDrivers()
|
||||
}
|
||||
}
|
||||
gpu.family.mali && (pair == null || !pair.complete) -> {
|
||||
// No open driver for this Mali published yet: the phone's own driver until it is.
|
||||
autoStatus = useMaliFallback(key?.let { DriverPairs.nonAdrenoName(it) } ?: gpu.name)
|
||||
refreshDrivers()
|
||||
}
|
||||
pair == null || !pair.complete ->
|
||||
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
|
||||
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) ->
|
||||
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) -> {
|
||||
autoStatus = activity.getString(R.string.drivers_skipping_bad, pair.name, pair.version)
|
||||
if (gpu.family.mali && maliHasNoRoute()) {
|
||||
useMaliFallback(pair.name)
|
||||
refreshDrivers()
|
||||
}
|
||||
}
|
||||
SessionState.running -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
holdBack && row?.installed != true -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_network, pair.name, pair.version)
|
||||
else -> installPair(pair, auto = true)
|
||||
@@ -296,6 +388,53 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}, "gpu-driver-auto").start()
|
||||
}
|
||||
|
||||
/**
|
||||
* A Mali with no open driver pair to install ([missing] names the unpublished pair, or null when
|
||||
* no PanVK build covers this GPU) runs through the phone's own driver: Mali (Venus) where this
|
||||
* apk carries it (Steam runs on it from the G57 to the G1 series), the Mali bridge otherwise.
|
||||
* Venus needs Vulkan 1.2 from the phone's driver; the bridge raises a Vulkan 1.1 driver
|
||||
* (r32p1 on the G77, G78 and G610) for gamescope itself, so there the bridge stands in.
|
||||
* The Linux driver choice is left alone (it is the bridge's fallback), and Auto
|
||||
* switches only its own choice off again once a pair is installed ([endMaliFallback]).
|
||||
*/
|
||||
private fun useMaliFallback(missing: String?): String {
|
||||
val oldVulkan = com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(activity)
|
||||
val venus = oldVulkan == null && com.droiddeck.launcher.session.VenusComponent.available(activity)
|
||||
if (oldVulkan != null) Log.i(TAG, "auto: Mali driver reports Vulkan $oldVulkan; Venus needs 1.2, the bridge stands in")
|
||||
if (venus && !SessionPrefs.venus(activity)) {
|
||||
SessionPrefs.setVenus(activity, true)
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, "venus")
|
||||
} else if (!venus && !SessionPrefs.vkBridge(activity)) {
|
||||
SessionPrefs.setVkBridge(activity, true)
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, "bridge")
|
||||
}
|
||||
val route = activity.getString(if (venus) R.string.drivers_route_venus else R.string.drivers_route_bridge)
|
||||
Log.i(TAG, "auto: Mali stand-in route $route (missing pair: ${missing ?: "none covers ${gpu.name}"})")
|
||||
if (oldVulkan != null) return activity.getString(R.string.drivers_mali_old_vulkan_bridge, missing ?: gpu.name, oldVulkan)
|
||||
return if (missing == null) activity.getString(R.string.drivers_mali_no_pair_auto, route)
|
||||
else activity.getString(R.string.drivers_mali_unpublished_auto, missing, route)
|
||||
}
|
||||
|
||||
/** Neither an installed open Linux driver nor Venus or the bridge: the runtime's Turnip would run. */
|
||||
private fun maliHasNoRoute(): Boolean {
|
||||
if (SessionPrefs.vkBridge(activity)) return false
|
||||
// Venus on a Vulkan 1.1 driver does not start (VenusComponent.wanted), so it is no route.
|
||||
if (SessionPrefs.venus(activity) && com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(activity) == null) return false
|
||||
val linux = SessionPrefs.linuxDriver(activity)
|
||||
return linux.isEmpty() || !LinuxVulkanDriverManager(activity).isInstalled(linux)
|
||||
}
|
||||
|
||||
/** A pair is installed for this Mali: the route Auto switched on in its place goes off again. */
|
||||
private fun endMaliFallback() {
|
||||
when (SessionPrefs.maliFallbackByAuto(activity)) {
|
||||
"venus" -> SessionPrefs.setVenus(activity, false)
|
||||
"bridge" -> SessionPrefs.setVkBridge(activity, false)
|
||||
else -> return
|
||||
}
|
||||
Log.i(TAG, "auto: pair installed; ${SessionPrefs.maliFallbackByAuto(activity)} (Auto's stand-in) off")
|
||||
SessionPrefs.setMaliFallbackByAuto(activity, "")
|
||||
}
|
||||
|
||||
fun selectPair(key: String) {
|
||||
if (pairBusy != null) return
|
||||
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
|
||||
@@ -304,7 +443,15 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
installPair(pair, auto = false)
|
||||
}
|
||||
|
||||
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean) {
|
||||
/** A newer build of an installed open non-Adreno pair: replaced in place, still selected if it was. */
|
||||
fun updatePair(key: String) {
|
||||
if (pairBusy != null) return
|
||||
val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return
|
||||
val wasActive = pairRows.firstOrNull { it.key == key }?.active == true
|
||||
installPair(pair, auto = wasActive && mode == SessionPrefs.GPU_DRIVERS_AUTO, force = true, select = wasActive)
|
||||
}
|
||||
|
||||
private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean, force: Boolean = false, select: Boolean = true) {
|
||||
if (!pair.complete) return
|
||||
pairBusy = pair.key
|
||||
pairPercent = 0
|
||||
@@ -314,7 +461,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val result = runCatching {
|
||||
val zips = pair.assets
|
||||
val ids = zips.mapIndexed { i, asset ->
|
||||
installedIdOf(asset)
|
||||
(if (force) null else installedIdOf(asset))
|
||||
?: installAsset(asset) { pct -> ui.post { pairPercent = (i * 100 + pct) / zips.size } }.also { downloaded = true }
|
||||
}
|
||||
if (pair.bundle != null) DriverBundle.get(activity, ids[0])!!.let { listOf(it.androidId, it.linuxId) } else ids
|
||||
@@ -322,23 +469,39 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
ui.post {
|
||||
pairBusy = null
|
||||
pairPercent = -1
|
||||
result.onSuccess { (displayId, linuxId) ->
|
||||
result.onSuccess { (pairDisplayId, linuxId) ->
|
||||
if (!select) {
|
||||
android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_pair_updated, pair.name), android.widget.Toast.LENGTH_LONG).show()
|
||||
return@onSuccess
|
||||
}
|
||||
if (auto && SessionState.running) {
|
||||
lastAutoCheck = 0L
|
||||
autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
|
||||
return@onSuccess
|
||||
}
|
||||
// A Mali pair under Auto: the compositor draws on the phone's own Mali driver
|
||||
// (dma-buf, modifiers and AHB all work there) and the guest runs the pair's
|
||||
// PanVK. Its Android half stays installed for a manual pick.
|
||||
// The same when the pair is picked by hand: a PanVK Android half on the display
|
||||
// stalls Mali gralloc ("Waiting for Steam's first frame"); it stays a manual pick
|
||||
// under "Runtime and display drivers separately".
|
||||
val displayId = if (gpu.family.mali && DriverPairs.isNonAdreno(pair.key)) TurnipDriver.AUTO else pairDisplayId
|
||||
val previousDisplay = SessionPrefs.androidDriver(activity)
|
||||
val previousLinux = SessionPrefs.linuxDriver(activity)
|
||||
val displayChanged = previousDisplay != displayId
|
||||
SessionPrefs.setAndroidDriver(activity, displayId)
|
||||
SessionPrefs.setLinuxDriver(activity, linuxId)
|
||||
// A route Auto switched on in place of a missing pair goes off now that one is
|
||||
// here. A Mali bridge or Venus switch the user set stays: it wins for the session
|
||||
// (DeviceSupport.vkBridge, VenusComponent.wanted) and the pair waits.
|
||||
if (gpu.family.mali && DriverPairs.isNonAdreno(pair.key)) endMaliFallback()
|
||||
val restart = displayChanged && CompositorHost.isStarted
|
||||
if (auto) {
|
||||
val trial = AutoDriverTrial.current(activity)
|
||||
val lastGood = trial?.let { setOf(it.previousDisplay, it.previousLinux) } ?: setOf(previousDisplay, previousLinux)
|
||||
val changed = previousDisplay != displayId || previousLinux != linuxId
|
||||
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId))
|
||||
// A Mali pair's display is the phone's own driver (AUTO): not a download to own or prune.
|
||||
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId).filter { it.isNotEmpty() && it != TurnipDriver.AUTO })
|
||||
if (changed) AutoDriverTrial.begin(
|
||||
activity, AutoDriverTrial.release(pair), "${pair.name} ${pair.version}", displayId, linuxId,
|
||||
trial?.previousDisplay ?: previousDisplay, trial?.previousLinux ?: previousLinux,
|
||||
@@ -366,6 +529,9 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
private fun installedBundle(asset: TurnipReleases.Asset): DriverBundle.Bundle? =
|
||||
TurnipReleases.installedId(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) }
|
||||
|
||||
private fun installedBundleAnyBuild(asset: TurnipReleases.Asset): DriverBundle.Bundle? =
|
||||
TurnipReleases.installedIdAnyBuild(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) }
|
||||
|
||||
private fun installedIdOf(asset: TurnipReleases.Asset): String? = when {
|
||||
asset.bundle -> installedBundle(asset)?.id
|
||||
asset.linux -> TurnipReleases.installedId(activity, asset, LinuxVulkanDriverManager(activity)::isInstalled)
|
||||
|
||||
@@ -164,6 +164,8 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var tuSysmem by mutableStateOf(false)
|
||||
private var zinkLazy by mutableStateOf(false)
|
||||
private var vkBridge by mutableStateOf(false)
|
||||
private var venus by mutableStateOf(false)
|
||||
private var venusHostVulkan by mutableStateOf<String?>(null)
|
||||
private var vkBridgeSafe by mutableStateOf(true)
|
||||
private var bridgeProtonDxvk by mutableStateOf(false)
|
||||
private val adrenoGpu by lazy { com.droiddeck.launcher.core.DeviceSupport.adreno() }
|
||||
@@ -582,6 +584,10 @@ class FrontEndActivity : ComponentActivity() {
|
||||
private var gameDebugLogsNeedsRestart by mutableStateOf(false)
|
||||
private var vacDiagnostics by mutableStateOf(false)
|
||||
private var agentCommands by mutableStateOf(false)
|
||||
private var graphicsLogs by mutableStateOf(false)
|
||||
private var steamGpuDrawing by mutableStateOf(false)
|
||||
private var bridgeFellBack by mutableStateOf(false)
|
||||
private var bridgeFellBackMemory by mutableStateOf(false)
|
||||
private var clearCachedApps by mutableStateOf(false)
|
||||
private var adbPaired by mutableStateOf(false)
|
||||
private var memoryHud by mutableStateOf(SessionPrefs.MEMORY_HUD_OFF)
|
||||
@@ -1129,7 +1135,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
FrontEndScreen(
|
||||
FrontEndState(
|
||||
installed = installed, ready = ready, available = available?.version, removalPending = removalPending, runtimeActionsBlocked = runtimeChangesBlocked(),
|
||||
vkBridge = vkBridge, vkBridgeSafe = vkBridgeSafe, bridgeProtonDxvk = bridgeProtonDxvk, adreno = adrenoGpu,
|
||||
vkBridge = vkBridge, venus = venus, venusHostVulkan = venusHostVulkan, vkBridgeSafe = vkBridgeSafe, bridgeProtonDxvk = bridgeProtonDxvk, adreno = adrenoGpu,
|
||||
shortcutPicker = shortcutPicker,
|
||||
shortcutLibraryScanning = shortcutLibraryScanning,
|
||||
gameSyncFolder = gameSyncFolder,
|
||||
@@ -1140,6 +1146,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
enhancedDiagnosticsArmed = enhancedDiagnosticsArmed,
|
||||
gameDebugLogsEnabled = gameDebugLogsEnabled, gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart,
|
||||
vacDiagnostics = vacDiagnostics, agentCommands = agentCommands,
|
||||
graphicsLogs = graphicsLogs, steamGpuDrawing = steamGpuDrawing, bridgeFellBack = bridgeFellBack, bridgeFellBackMemory = bridgeFellBackMemory,
|
||||
clearCachedApps = clearCachedApps, adbPaired = adbPaired, memoryHud = memoryHud, backgroundAppLimit = backgroundAppLimit,
|
||||
steamGames = steamGames, steamGamesX64 = steamGamesX64, emulators = emulatorList, running = runningLabel,
|
||||
frameGen = FrameGen.mode(this),
|
||||
@@ -1285,9 +1292,18 @@ class FrontEndActivity : ComponentActivity() {
|
||||
onRemovePackage = { id -> removePackage(id) },
|
||||
onRuntime = { onRuntimeButton() },
|
||||
onVkBridge = { on ->
|
||||
SessionPrefs.setVkBridge(this, on); vkBridge = on
|
||||
SessionPrefs.setVkBridge(this, on); SessionPrefs.setMaliFallbackByAuto(this, ""); vkBridge = on
|
||||
if (!on) com.droiddeck.launcher.session.ComponentsManager.settleAutoDxvk(this)
|
||||
},
|
||||
onVenus = { on ->
|
||||
SessionPrefs.setVenus(this, on)
|
||||
SessionPrefs.setMaliFallbackByAuto(this, "")
|
||||
venus = on
|
||||
if (on && !com.droiddeck.launcher.core.DeviceSupport.adreno()) Thread({
|
||||
com.droiddeck.launcher.gpu.VulkanInfo.query()["api"]?.let { SessionPrefs.setHostVulkanApi(this, it) }
|
||||
runOnUiThread { venusHostVulkan = com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(this) }
|
||||
}, "venus-host-vulkan").start()
|
||||
},
|
||||
onVkBridgeSafe = { on -> SessionPrefs.setVkBridgeSafe(this, on); vkBridgeSafe = on },
|
||||
onBridgeProtonDxvk = { on ->
|
||||
SessionPrefs.setBridgeProtonDxvk(this, on); bridgeProtonDxvk = on
|
||||
@@ -1346,6 +1362,14 @@ class FrontEndActivity : ComponentActivity() {
|
||||
gameDebugLogsEnabled = SessionPrefs.gameDebugLogsEnabled(this)
|
||||
gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart()
|
||||
},
|
||||
onSteamGpuDrawing = {
|
||||
SessionPrefs.setSteamGpuCompositing(this, !SessionPrefs.steamGpuCompositing(this))
|
||||
steamGpuDrawing = SessionPrefs.steamGpuCompositing(this)
|
||||
},
|
||||
onGraphicsLogs = {
|
||||
SessionPrefs.setDetailedGraphicsLogs(this, !SessionPrefs.detailedGraphicsLogs(this))
|
||||
graphicsLogs = SessionPrefs.detailedGraphicsLogs(this)
|
||||
},
|
||||
onVacDiagnostics = {
|
||||
SessionPrefs.setVacDiagnostics(this, !SessionPrefs.vacDiagnostics(this))
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
@@ -1684,6 +1708,9 @@ class FrontEndActivity : ComponentActivity() {
|
||||
com.droiddeck.launcher.frontend.GamesFolderMigration.run(this)
|
||||
SessionLogShare.clear(this)
|
||||
SessionArtifacts.finishAbandoned(this)
|
||||
if (SessionPrefs.takeMemoryKillNotice(this)) runOnUiThread {
|
||||
android.widget.Toast.makeText(this, R.string.session_killed_for_memory, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
SessionArtifacts.scrubOlder(this)
|
||||
SessionArtifacts.prune(this)
|
||||
}, "finish-abandoned").start()
|
||||
@@ -2137,6 +2164,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
gpuActions = com.droiddeck.launcher.ui.GpuDriversActions(
|
||||
onAuto = { on -> drivers.setMode(on) },
|
||||
onPair = { key -> drivers.selectPair(key) },
|
||||
onPairUpdate = { key -> drivers.updatePair(key) },
|
||||
onRefresh = { if (drivers.mode == SessionPrefs.GPU_DRIVERS_AUTO) drivers.ensureAuto(force = true) else drivers.checkLatestTurnip() },
|
||||
onSelectLinux = { id -> drivers.selectDriver(id, linux = true) },
|
||||
onSelectAndroid = { id -> drivers.selectDriver(id, linux = false) },
|
||||
@@ -2687,6 +2715,8 @@ class FrontEndActivity : ComponentActivity() {
|
||||
tuSysmem = SessionPrefs.tuSysmem(this)
|
||||
zinkLazy = SessionPrefs.zinkLazy(this)
|
||||
vkBridge = SessionPrefs.vkBridge(this)
|
||||
venus = SessionPrefs.venus(this)
|
||||
venusHostVulkan = com.droiddeck.launcher.session.VenusComponent.hostVulkanTooOld(this)
|
||||
vkBridgeSafe = SessionPrefs.vkBridgeSafe(this)
|
||||
bridgeProtonDxvk = SessionPrefs.bridgeProtonDxvk(this)
|
||||
noXalia = SessionPrefs.noXalia(this)
|
||||
@@ -2764,6 +2794,10 @@ class FrontEndActivity : ComponentActivity() {
|
||||
gameDebugLogsNeedsRestart = gameDebugLogsNeedsRestart()
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
|
||||
graphicsLogs = SessionPrefs.detailedGraphicsLogs(this)
|
||||
steamGpuDrawing = SessionPrefs.steamGpuCompositing(this)
|
||||
bridgeFellBack = SessionPrefs.bridgeFellBack(this)
|
||||
bridgeFellBackMemory = SessionPrefs.bridgeFellBackForMemory(this)
|
||||
clearCachedApps = SessionPrefs.clearCachedApps(this)
|
||||
adbPaired = WirelessAdbFix.hasSavedPairing(this)
|
||||
memoryHud = SessionPrefs.memoryHud(this)
|
||||
|
||||
@@ -1214,6 +1214,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionDrawer(drawerOpen || (docked && drawerPinned), drawerPage, drawerControllerActive, onPageChange = { drawerPage = it }, docked = docked,
|
||||
backRequest = drawerBackRequests, onLayerChange = { drawerLayerOpen = it }, a = DrawerActions(
|
||||
steam = SessionState.mode == SessionService.MODE_STEAM,
|
||||
systemPowerVr = com.droiddeck.launcher.gpu.SystemVulkanDriver.usesDefault(
|
||||
SessionPrefs.linuxDriver(this@SessionActivity)),
|
||||
direct = noSteamSession(),
|
||||
title = if (SessionState.mode == SessionService.MODE_RUN)
|
||||
com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_title_game) else null,
|
||||
@@ -2476,10 +2478,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
closeSecondScreen(reset = true)
|
||||
updateGlassesScreen()
|
||||
updateHeadTracker()
|
||||
if (status == 0) {
|
||||
Thread({
|
||||
val hint = sessionExitHint(status)
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
SessionState.relaunch?.let { next ->
|
||||
if (status == 0) SessionState.relaunch?.let { next ->
|
||||
SessionState.relaunch = null
|
||||
next.putExtra(EXTRA_RETURN_HOME, intent.getBooleanExtra(EXTRA_RETURN_HOME, false))
|
||||
next.putExtra(EXTRA_LINK_TASK, intent.getIntExtra(EXTRA_LINK_TASK, -1))
|
||||
@@ -2487,14 +2490,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
recreate()
|
||||
return@runOnUiThread
|
||||
}
|
||||
finish()
|
||||
}
|
||||
return
|
||||
}
|
||||
Thread({
|
||||
val hint = runtimeDamageHint() ?: sessionEndHint(SessionState.logFile) ?: prootFailureHint()
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
showEnded(status, hint)
|
||||
}
|
||||
}, "session-end-hint").start()
|
||||
@@ -2557,13 +2552,44 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return EndHint(resources.getQuantityString(R.plurals.session_end_runtime_damaged, broken.size, broken.size, broken.first()), repair = true)
|
||||
}
|
||||
|
||||
private fun presentationFault(): String? {
|
||||
runCatching { com.droiddeck.launcher.wayland.WaylandCompositor.compositorFault() }.getOrNull()
|
||||
?.takeIf { it.isNotEmpty() }?.let { return it }
|
||||
val waylandLog = SessionState.logDirectory?.let { File(it, "wayland.log") }?.takeIf { it.isFile } ?: return null
|
||||
return try {
|
||||
java.io.RandomAccessFile(waylandLog, "r").use { f ->
|
||||
val start = maxOf(0L, waylandLog.length() - 256L * 1024)
|
||||
f.seek(start)
|
||||
val bytes = ByteArray((waylandLog.length() - start).toInt())
|
||||
f.readFully(bytes)
|
||||
var fault: String? = null
|
||||
String(bytes, Charsets.ISO_8859_1).lineSequence().forEach { line ->
|
||||
val i = line.indexOf("DD_PRESENT_FAULT: ")
|
||||
if (i >= 0) fault = line.substring(i + 18).trim().ifEmpty { null }
|
||||
else if (line.contains("presentation recovered: fault cleared")) fault = null
|
||||
}
|
||||
fault
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun prootFailureHint(): EndHint? {
|
||||
if (SessionState.failureCode != "PROOT_MISSING") return null
|
||||
val message = SessionState.failureMessage ?: return null
|
||||
return EndHint(message)
|
||||
}
|
||||
|
||||
private fun presentationFaultHint(): EndHint? = presentationFault()?.let {
|
||||
EndHint(getString(R.string.session_end_present_fault, it), evenOnSuccess = true)
|
||||
}
|
||||
|
||||
private fun sessionExitHint(status: Int): EndHint? = if (status == 0) presentationFaultHint()
|
||||
else runtimeDamageHint() ?: sessionEndHint(SessionState.logFile) ?: prootFailureHint()
|
||||
|
||||
private fun sessionEndHint(log: File?): EndHint? {
|
||||
presentationFaultHint()?.let { return it }
|
||||
if (log == null || !log.isFile) return null
|
||||
return try {
|
||||
var enosys = 0
|
||||
@@ -2590,8 +2616,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val fallback = if (SessionState.steamClientArch == "arm64") getString(R.string.session_end_steam_try_x64) else null
|
||||
EndHint(listOfNotNull(message, fallback).joinToString("\n"))
|
||||
} else if (launchFailure == com.droiddeck.launcher.session.SessionLaunchFailure.VULKAN_DEVICE_MISSING) {
|
||||
val adreno = com.droiddeck.launcher.gpu.GpuInfo.detect().family != com.droiddeck.launcher.gpu.GpuInfo.Family.NOT_ADRENO
|
||||
EndHint(getString(if (adreno) R.string.session_end_vulkan_device else R.string.session_end_gpu_unsupported))
|
||||
val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect()
|
||||
val family = gpu.family
|
||||
val adreno = family != com.droiddeck.launcher.gpu.GpuInfo.Family.NOT_ADRENO &&
|
||||
family != com.droiddeck.launcher.gpu.GpuInfo.Family.POWERVR && !family.mali
|
||||
val message = getString(R.string.session_end_vulkan_device)
|
||||
EndHint(if (adreno) message else "${gpu.name} (${family.label(gpu.name)})\n$message")
|
||||
} else if (fexMissing) {
|
||||
val what = com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_end_program)
|
||||
EndHint(getString(R.string.session_end_fex_missing, what), evenOnSuccess = true)
|
||||
|
||||
@@ -130,22 +130,46 @@ public class PulseAudioComponent extends SessionPart {
|
||||
|
||||
private void ensureClientSink(java.io.PrintWriter out) {
|
||||
try {
|
||||
long deadline = System.currentTimeMillis() + 15000L;
|
||||
// Nothing that may block (a sink whose AAudio open hangs: a Galaxy S24 FE (US) hung the
|
||||
// daemon's start-up loading one from default.pa) is loaded by the daemon itself; the
|
||||
// fallbacks are loaded here, after it is up, each pactl call bounded by its timeout.
|
||||
long started = System.currentTimeMillis();
|
||||
long deadline = started + 30000L;
|
||||
String sinks = null;
|
||||
boolean socketSeen = false;
|
||||
while (System.currentTimeMillis() < deadline && pid > 1) {
|
||||
if (socket().exists() && (sinks = pactl("list", "short", "sinks")) != null) break;
|
||||
socketSeen |= socket().exists();
|
||||
if (socketSeen && (sinks = pactl("list", "short", "sinks")) != null) break;
|
||||
try { Thread.sleep(250L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
|
||||
}
|
||||
note(out, "client sink: daemon " + (sinks != null ? "answered" : "did not answer") + " after "
|
||||
+ (System.currentTimeMillis() - started) + " ms (socket " + (socketSeen ? "present" : "never appeared") + ", pactl "
|
||||
+ (new File(workingDir, "pactl").isFile() ? "present" : "MISSING") + ", daemon pid " + pid + ")");
|
||||
String line;
|
||||
if (sinks == null) {
|
||||
line = "== client sink: the daemon did not answer in 15 s; no fallback attempted";
|
||||
line = "== client sink: the daemon did not answer in 30 s; no fallback attempted";
|
||||
} else if (sinks.contains("\tDirectAudio\t") || sinks.startsWith("DirectAudio\t") || sinks.contains("\tDirectAudio\n") || sinks.matches("(?s).*\\bDirectAudio\\b.*")) {
|
||||
line = "== client sink: DirectAudio (module-directaudio-native-sink)";
|
||||
} else {
|
||||
boolean loaded = pactl("load-module", "module-aaudio-sink", "sink_name=AAudioSink", "performance_mode=1", "adaptive=1", "volume=1.0", "control_dir=" + deviceControls) != null
|
||||
&& pactl("set-default-sink", "AAudioSink") != null;
|
||||
line = loaded ? "== client sink: DirectAudio did not come up; FALLBACK to the plain AAudio sink (AAudioSink)"
|
||||
: "== client sink: DirectAudio did not come up and the AAudio fallback failed too - no client sound";
|
||||
// AAudio can refuse the first open with AAUDIO_ERROR_DISCONNECTED while Android is
|
||||
// still switching the output route as the session starts (seen on a Galaxy S24 FE):
|
||||
// try DirectAudio again, then the plain sink, a few times over ten seconds.
|
||||
String loadedAs = null;
|
||||
for (int attempt = 1; attempt <= 5 && loadedAs == null && pid > 1; attempt++) {
|
||||
try { Thread.sleep(2000L); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; }
|
||||
if (pactl("load-module", "module-directaudio-native-sink", "sink_name=DirectAudio", "performance_mode=1",
|
||||
"adaptive=1", "decay=0", "limiter=1", "watchdog=1", "volume=1.0", "control_dir=" + deviceControls) != null
|
||||
&& pactl("set-default-sink", "DirectAudio") != null) {
|
||||
loadedAs = "DirectAudio on attempt " + attempt;
|
||||
} else if (pactl("load-module", "module-aaudio-sink", "sink_name=AAudioSink",
|
||||
"performance_mode=" + (attempt == 1 ? "1" : "0"), "adaptive=1", "volume=1.0", "control_dir=" + deviceControls) != null
|
||||
&& pactl("set-default-sink", "AAudioSink") != null) {
|
||||
loadedAs = "the plain AAudio sink (AAudioSink) on attempt " + attempt;
|
||||
}
|
||||
}
|
||||
line = loadedAs != null ? "== client sink: DirectAudio did not come up at start; retried and got " + loadedAs
|
||||
: "== client sink: DirectAudio did not come up and the AAudio fallback failed too after 5 retries - no client sound."
|
||||
+ " If audio.log shows AAUDIO_ERROR_DISCONNECTED, Android refused the audio output: rebooting the phone usually fixes this";
|
||||
Log.w(TAG, line);
|
||||
}
|
||||
Log.i(TAG, line);
|
||||
@@ -155,6 +179,14 @@ public class PulseAudioComponent extends SessionPart {
|
||||
}
|
||||
}
|
||||
|
||||
/** A timestamped line in the session's audio log. */
|
||||
private void note(java.io.PrintWriter out, String what) {
|
||||
String line = "== " + new java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.US).format(new java.util.Date()) + " " + what;
|
||||
Log.i(TAG, line);
|
||||
if (out != null) synchronized (out) { out.println(line); out.flush(); }
|
||||
}
|
||||
|
||||
/** Runs pactl against this daemon; its stdout, or null when it failed or timed out. */
|
||||
private String pactl(String... args) {
|
||||
File pactl = new File(workingDir, "pactl");
|
||||
if (!pactl.isFile() || pid <= 1) return null;
|
||||
|
||||
@@ -10,6 +10,8 @@ import android.os.PowerManager
|
||||
import android.os.StatFs
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.gpu.DeviceInfo
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import android.view.Display
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
@@ -110,6 +112,7 @@ object DeviceReport {
|
||||
runCatching {
|
||||
val info = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
k("Version", "${info.versionName} (${info.longVersionCode})")
|
||||
k("App build", "${com.droiddeck.launcher.BuildConfig.BUILD_LABEL} (source ${com.droiddeck.launcher.BuildConfig.SOURCE_COMMIT})")
|
||||
}
|
||||
k("Package", context.packageName)
|
||||
k("targetSdk", context.applicationInfo.targetSdkVersion)
|
||||
@@ -186,7 +189,11 @@ object DeviceReport {
|
||||
h("GPU")
|
||||
k("KGSL gpu_model", readSys("/sys/class/kgsl/kgsl-3d0/gpu_model"))
|
||||
k("KGSL chip id", readSys("/sys/class/kgsl/kgsl-3d0/gpu_chipid"))
|
||||
k("System Vulkan ICD", if (File("/vendor/lib64/hw/vulkan.adreno.so").exists()) "/vendor/lib64/hw/vulkan.adreno.so" else "not at the usual path")
|
||||
k("System Vulkan ICD", when {
|
||||
SystemVulkanDriver.isPowerVr() -> SystemVulkanDriver.hal()
|
||||
File("/vendor/lib64/hw/vulkan.adreno.so").exists() -> "/vendor/lib64/hw/vulkan.adreno.so"
|
||||
else -> "not at the usual path"
|
||||
})
|
||||
|
||||
h("Display")
|
||||
k("Session output", SessionState.outputSize?.let { "${it.first}x${it.second}" })
|
||||
@@ -208,7 +215,7 @@ object DeviceReport {
|
||||
|
||||
h("Drivers")
|
||||
val gpu = com.droiddeck.launcher.gpu.GpuInfo.detect()
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText(SessionPrefs.vkBridge(context))}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
k("GPU", "${gpu.name} · ${gpu.family.label(gpu.name)} · ${gpu.supportText(SessionPrefs.vkBridge(context))}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
|
||||
if (gpu.model == 740) {
|
||||
val hint = com.droiddeck.launcher.gpu.TpUbwcHint
|
||||
k("TP UBWC hint", hint.forThisDevice(context) ?: "driver default")
|
||||
@@ -221,19 +228,25 @@ object DeviceReport {
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.bad(context))?.let { " (recommended pair: $it)" } ?: ""))
|
||||
val turnip = TurnipDriver(context)
|
||||
val androidChoice = SessionPrefs.androidDriver(context)
|
||||
k("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> ${turnip.autoId()}" else androidChoice)
|
||||
k(" name / version", "${turnip.displayName(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)} ${turnip.driverVersion(if (androidChoice.isEmpty()) turnip.autoId() else androidChoice)}".trim())
|
||||
val displayId = androidChoice.ifEmpty { turnip.autoId() }
|
||||
val displayLabel = if (displayId == "system") DeviceInfo.builtinDriverLabel(SystemVulkanDriver.isPowerVr(), runtime = false)
|
||||
else "${turnip.displayName(displayId)} ${turnip.driverVersion(displayId)}".trim()
|
||||
k("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> $displayLabel" else androidChoice)
|
||||
k(" name / version", displayLabel)
|
||||
k(" imported available", turnip.enumerateImported().ifEmpty { listOf("none") }.joinToString(", "))
|
||||
val lm = LinuxVulkanDriverManager(context)
|
||||
run {
|
||||
val id = SessionPrefs.linuxDriver(context)
|
||||
val label = if (id.isEmpty()) "runtime default (the Turnip built into the runtime)"
|
||||
val label = if (id.isEmpty()) DeviceInfo.builtinDriverLabel(SystemVulkanDriver.isPowerVr(), runtime = true)
|
||||
else "${lm.getDriverName(id)} ${lm.getDriverVersion(id)}".trim() +
|
||||
lm.getMinGlibc(id).let { if (it.isEmpty()) "" else " (glibc $it+)" } +
|
||||
if (lm.isInstalled(id)) "" else " [MISSING - falls back to the runtime's own]"
|
||||
if (lm.isInstalled(id)) "" else if (SystemVulkanDriver.isPowerVr()) " [MISSING - select an installed driver or Auto]"
|
||||
else " [MISSING - falls back to the runtime's own]"
|
||||
k("Linux driver", label)
|
||||
}
|
||||
k("Runtime's own ICD", LinuxRuntime.vulkanIcd(context)?.path)
|
||||
k("Mali (Venus)", if (com.droiddeck.launcher.session.VenusComponent.wanted(context)) "in use"
|
||||
else if (com.droiddeck.launcher.session.VenusComponent.available(context)) "available, off" else "not in this build")
|
||||
|
||||
h("Runtime")
|
||||
k("Installed version", LinuxRuntimeInstaller.installedVersion(context))
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.core
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import java.io.File
|
||||
|
||||
object DeviceSupport {
|
||||
@@ -10,16 +11,35 @@ object DeviceSupport {
|
||||
File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists()
|
||||
}
|
||||
|
||||
private val maliGpu by lazy { !adrenoGpu && File("/dev/mali0").exists() }
|
||||
|
||||
fun adreno(): Boolean = adrenoGpu
|
||||
|
||||
fun mali(): Boolean = maliGpu
|
||||
fun graphicsAvailable(): Boolean = graphicsAvailable(adreno(), SystemVulkanDriver.isAvailable())
|
||||
|
||||
internal fun graphicsAvailable(adreno: Boolean, powerVrReady: Boolean): Boolean = adreno || powerVrReady
|
||||
|
||||
fun vkBridge(context: Context): Boolean =
|
||||
!adreno() && com.droiddeck.launcher.session.SessionPrefs.vkBridge(context)
|
||||
bridgeUsable() && !com.droiddeck.launcher.session.VenusComponent.wanted(context) &&
|
||||
com.droiddeck.launcher.session.SessionPrefs.vkBridge(context)
|
||||
|
||||
fun canDraw(context: Context): Boolean = adreno() || vkBridge(context)
|
||||
fun mali(): Boolean = isMali
|
||||
|
||||
private val isMali by lazy {
|
||||
!adreno() && (File("/dev/mali0").exists() ||
|
||||
com.droiddeck.launcher.gpu.GpuInfo.detect().family.name.startsWith("MALI"))
|
||||
}
|
||||
|
||||
fun bridgeUsable(): Boolean = !adreno() && !SystemVulkanDriver.isAvailable() && File("/dev/mali0").exists()
|
||||
|
||||
fun openDriverActive(context: Context): Boolean {
|
||||
if (adreno()) return false
|
||||
if (com.droiddeck.launcher.session.VenusComponent.wanted(context)) return false
|
||||
if (bridgeUsable() && com.droiddeck.launcher.session.SessionPrefs.vkBridge(context)) return false
|
||||
val linux = com.droiddeck.launcher.session.SessionPrefs.linuxDriver(context).ifEmpty { return false }
|
||||
return com.droiddeck.launcher.gpu.DriverPairs.isNonAdreno(com.droiddeck.launcher.gpu.DriverBundle.containing(context, linux, true)?.id)
|
||||
}
|
||||
|
||||
fun canDraw(context: Context): Boolean = graphicsAvailable() || openDriverActive(context) || vkBridge(context) ||
|
||||
com.droiddeck.launcher.session.VenusComponent.wanted(context)
|
||||
|
||||
fun gpuName(context: Context): String {
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN } else null
|
||||
|
||||
@@ -14,6 +14,8 @@ object SteamRedists {
|
||||
"228988" to ("vcredist2019_dll" to "VC++ 2019"),
|
||||
"228989" to ("vcredist2022_dll" to "VC++ 2022"),
|
||||
"228990" to ("d3dx9" to "DirectX (June 2010)"),
|
||||
// OpenAL 2.0.7.0 (Hotline Miami lists it; without it HotlineGL.exe fails to load, c0000135).
|
||||
"229020" to ("oalinst_dll" to "OpenAL"),
|
||||
)
|
||||
val SCRIPTED = mapOf(
|
||||
72850 to listOf("xact" to "DirectX XACT audio"),
|
||||
|
||||
@@ -53,7 +53,7 @@ object DeviceInfo {
|
||||
|
||||
private fun gpu(context: Context, gpu: GpuInfo) = Section("GPU", buildList<Pair<String, String>> {
|
||||
add("GPU", gpu.name)
|
||||
add("Family", gpu.family.label)
|
||||
add("Family", gpu.family.label(gpu.name))
|
||||
add("Model found from", when (gpu.modelSource) {
|
||||
"kernel" -> "the kernel's GPU name"
|
||||
"vulkan" -> "the Vulkan driver (the kernel's name has no model)"
|
||||
@@ -146,10 +146,20 @@ object DeviceInfo {
|
||||
add("Driver mode", SessionPrefs.gpuDriverMode(context))
|
||||
val turnip = TurnipDriver(context)
|
||||
val choice = SessionPrefs.androidDriver(context).ifEmpty { turnip.autoId() }
|
||||
add("Display driver", "${turnip.displayName(choice)} ${turnip.driverVersion(choice)}".trim())
|
||||
val powerVr = gpu.family == GpuInfo.Family.POWERVR
|
||||
add("Display driver", if (choice == "system") builtinDriverLabel(powerVr, runtime = false)
|
||||
else "${turnip.displayName(choice)} ${turnip.driverVersion(choice)}".trim())
|
||||
val linux = LinuxVulkanDriverManager(context)
|
||||
val id = SessionPrefs.linuxDriver(context)
|
||||
add("Game driver", if (id.isEmpty()) "the runtime's own Turnip" else "${linux.getDriverName(id)} ${linux.getDriverVersion(id)}".trim())
|
||||
add("Game driver", if (id.isEmpty()) builtinDriverLabel(powerVr, runtime = true) else "${linux.getDriverName(id)} ${linux.getDriverVersion(id)}".trim())
|
||||
if (gpu.oneUi8Gen2) add("Note", "One UI on an 8 Gen 2: needs the OneUI Turnip build")
|
||||
})
|
||||
|
||||
/** Shared by the device card and the session report, which are written in English. */
|
||||
internal fun builtinDriverLabel(powerVr: Boolean, runtime: Boolean): String = when {
|
||||
powerVr -> "System PowerVR"
|
||||
runtime -> "the runtime's own Turnip"
|
||||
else -> "System Vulkan"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -75,8 +75,8 @@ object DriverBundle {
|
||||
|
||||
val linuxStage = lm.newStagingDir()
|
||||
val linuxId = try {
|
||||
val library = linuxDir.listFiles()?.firstOrNull { it.isFile && it.name.startsWith("libvulkan_freedreno") && it.name.endsWith(".so") }
|
||||
library?.copyTo(File(linuxStage, LinuxVulkanDriverManager.LIB_NAME))
|
||||
val library = linuxDir.listFiles()?.firstOrNull { it.isFile && LinuxVulkanDriverManager.isDriverLibrary(it.name) }
|
||||
library?.copyTo(File(linuxStage, LinuxVulkanDriverManager.storedName(library.name)))
|
||||
val meta = File(linuxDir, LinuxVulkanDriverManager.META_NAME).takeIf { it.isFile }
|
||||
?.let { runCatching { JSONObject(FileUtils.readString(it)) }.getOrNull() }
|
||||
lm.adopt(linuxStage, library?.name, meta, manifest.name)
|
||||
@@ -86,6 +86,10 @@ object DriverBundle {
|
||||
}
|
||||
|
||||
val base = LinuxVulkanDriverManager.sanitizeId(manifest.tag.ifEmpty { "${manifest.name}-${manifest.version}" })
|
||||
get(context, base)?.takeIf { DriverPairs.isNonAdreno(it.id) }?.let { old ->
|
||||
Log.i(TAG, "replacing bundle ${old.id} (display ${old.androidId}, runtime ${old.linuxId})")
|
||||
remove(context, old)
|
||||
}
|
||||
val saved = registry(context)
|
||||
var id = base
|
||||
var n = 2
|
||||
|
||||
@@ -12,6 +12,47 @@ object DriverPairs {
|
||||
const val DD_TURNIP = "dd-turnip"
|
||||
fun ddTurnip(tag: String) = "$DD_TURNIP:$tag"
|
||||
|
||||
/** The open non-Adreno drivers (The412Banner/droiddeck-nonadreno-drivers), one pair per driver. */
|
||||
const val NON_ADRENO = "nad"
|
||||
const val NAD_RADV_XCLIPSE = "nad:radv-xclipse"
|
||||
/** RADV-Xclipse rebased on JimVulkan's upstream: a manual choice, never Auto. */
|
||||
const val NAD_RADV_XCLIPSE_UPSTREAM = "nad:radv-xclipse-upstream"
|
||||
const val NAD_MALI_JM = "nad:panvk-mali-jm"
|
||||
const val NAD_MALI_G710 = "nad:panvk-mali-g710"
|
||||
const val NAD_MALI_G615 = "nad:panvk-mali-g615"
|
||||
const val NAD_MALI_G720 = "nad:panvk-mali-g720"
|
||||
/** One PanVK kbase binary for Mali v9-v13; its display half needs Android 15. */
|
||||
const val NAD_MALI_KBASE = "nad:panvk-mali-kbase"
|
||||
const val MALI_KBASE_MIN_SDK = 35
|
||||
/** faux123's PanVK builds for three handhelds: manual choices, never Auto. */
|
||||
const val NAD_FAUX_RP4PRO = "nad:panvk-faux123-rp4pro"
|
||||
const val NAD_FAUX_ACE = "nad:panvk-faux123-ace"
|
||||
const val NAD_FAUX_RG556 = "nad:panvk-faux123-rg556"
|
||||
/** The one Mali a device-specific build is for, by the number in the GPU's name. */
|
||||
private val DEVICE_BUILDS = mapOf(NAD_FAUX_RP4PRO to 77, NAD_FAUX_ACE to 610, NAD_FAUX_RG556 to 57)
|
||||
/** "PanVK-Mali-G720" (the release asset's id) to its pair key, "nad:panvk-mali-g720". */
|
||||
fun nonAdreno(id: String) = "$NON_ADRENO:" + id.lowercase()
|
||||
/** True for a non-Adreno pair's key, or a bundle installed from one ("PanVK-Mali-G720"). */
|
||||
fun isNonAdreno(keyOrBundleId: String?): Boolean = keyOrBundleId != null &&
|
||||
(keyOrBundleId.startsWith("$NON_ADRENO:") || Regex("""^(?i)(RADV-Xclipse|PanVK-[A-Za-z0-9]+)(-[A-Za-z0-9]+)*$""").matches(keyOrBundleId))
|
||||
|
||||
fun nonAdrenoName(key: String): String = describeNonAdreno(key).first
|
||||
|
||||
private fun describeNonAdreno(key: String): Pair<String, Set<Family>> = when (key) {
|
||||
NAD_RADV_XCLIPSE -> "RADV · Samsung Xclipse" to setOf(Family.POWERVR)
|
||||
NAD_RADV_XCLIPSE_UPSTREAM -> "RADV upstream · Samsung Xclipse" to setOf(Family.POWERVR)
|
||||
NAD_MALI_JM -> "PanVK · Mali G52-G78" to setOf(Family.MALI_JM)
|
||||
NAD_MALI_G710 -> "PanVK · Mali-G610/G710" to setOf(Family.MALI_G610_G710)
|
||||
NAD_MALI_G615 -> "PanVK · Mali-G615" to setOf(Family.MALI_G615)
|
||||
NAD_MALI_G720 -> "PanVK · Mali-G720" to setOf(Family.MALI_G720)
|
||||
NAD_FAUX_RP4PRO -> "PanVK faux123 · Retroid Pocket 4 Pro (Mali-G77)" to setOf(Family.MALI_JM)
|
||||
NAD_FAUX_ACE -> "PanVK faux123 · GameForce Ace (Mali-G610)" to setOf(Family.MALI_G610_G710)
|
||||
NAD_FAUX_RG556 -> "PanVK faux123 · Anbernic RG556 (Mali-G57)" to setOf(Family.MALI_JM)
|
||||
NAD_MALI_KBASE -> "PanVK kbase · Mali G57-G925" to
|
||||
setOf(Family.MALI_JM, Family.MALI_G610_G710, Family.MALI_G615, Family.MALI_G720, Family.MALI_LATEST)
|
||||
else -> key.removePrefix("$NON_ADRENO:") to emptySet<Family>()
|
||||
}
|
||||
|
||||
class DriverPair(
|
||||
val key: String,
|
||||
val name: String,
|
||||
@@ -24,14 +65,23 @@ object DriverPairs {
|
||||
val assets: List<TurnipReleases.Asset> get() = listOfNotNull(bundle ?: display, if (bundle == null) linux else null)
|
||||
val version: String
|
||||
get() = listOfNotNull(bundle?.tag, display?.tag, linux?.tag).distinct().joinToString(" + ") { it.removePrefix("linux-").removePrefix("DD-Turnip-") }
|
||||
fun suits(gpu: GpuInfo) = gpu.family in families ||
|
||||
(gpu.family == Family.ADRENO_UNKNOWN && (Family.A7XX in families || Family.A8XX in families))
|
||||
fun suits(gpu: GpuInfo) = when {
|
||||
// The system-driver family covers PowerVR and Xclipse; RADV is only for the Xclipse.
|
||||
key == NAD_RADV_XCLIPSE || key == NAD_RADV_XCLIPSE_UPSTREAM ->
|
||||
gpu.family == Family.POWERVR && gpu.name.contains("Xclipse", ignoreCase = true)
|
||||
// One binary for Mali v9-v13, not the older G52/G76 in the Job Manager family.
|
||||
key == NAD_MALI_KBASE -> gpu.family.mali && GpuInfo.maliKbaseCovered(gpu.name)
|
||||
key in DEVICE_BUILDS -> gpu.family.mali && GpuInfo.maliNumber(gpu.name) == DEVICE_BUILDS[key]
|
||||
else -> gpu.family in families ||
|
||||
(gpu.family == Family.ADRENO_UNKNOWN && (Family.A7XX in families || Family.A8XX in families))
|
||||
}
|
||||
}
|
||||
|
||||
private val ALL_ADRENO = setOf(Family.A6XX, Family.A7XX, Family.A7XX_LOW, Family.A8XX, Family.ADRENO_UNKNOWN)
|
||||
|
||||
private fun describe(key: String): Pair<String, Set<Family>> = when {
|
||||
key.startsWith(DD_TURNIP) -> "DroidDeck · DD-Turnip" to ALL_ADRENO
|
||||
key.startsWith("$NON_ADRENO:") -> describeNonAdreno(key)
|
||||
else -> describeFixed(key)
|
||||
}
|
||||
|
||||
@@ -47,7 +97,7 @@ object DriverPairs {
|
||||
|
||||
fun from(check: TurnipReleases.Check?): List<DriverPair> {
|
||||
val assets = check?.assets.orEmpty().filter { it.pair.isNotEmpty() }
|
||||
val order = listOf(DD_TURNIP, BANNER, BANNER_A8XX, BANNER_ONEUI, BANNER_710, WN_BALANCED, WN_PERFORMANCE)
|
||||
val order = listOf(DD_TURNIP, BANNER, BANNER_A8XX, BANNER_ONEUI, BANNER_710, WN_BALANCED, WN_PERFORMANCE, NON_ADRENO)
|
||||
return assets.map { it.pair }.distinct()
|
||||
.sortedBy { key -> order.indexOf(key.substringBefore(':')).let { i -> if (i < 0) order.size else i } }
|
||||
.map { key ->
|
||||
@@ -66,7 +116,13 @@ object DriverPairs {
|
||||
rules: List<AutoPicks.Rule> = AutoPicks.BUILT_IN, bad: Set<String> = emptySet(),
|
||||
sdk: Int = android.os.Build.VERSION.SDK_INT, blob: Int? = null,
|
||||
): String? {
|
||||
if (gpu.family == Family.NOT_ADRENO) return null
|
||||
when (gpu.family) {
|
||||
Family.POWERVR -> return NAD_RADV_XCLIPSE.takeIf { gpu.name.contains("Xclipse", ignoreCase = true) }
|
||||
Family.MALI_OTHER, Family.NOT_ADRENO -> return null
|
||||
Family.MALI_JM, Family.MALI_G610_G710, Family.MALI_G615, Family.MALI_G720, Family.MALI_LATEST ->
|
||||
return maliKey(gpu, pairs, sdk)
|
||||
else -> Unit
|
||||
}
|
||||
val keys = AutoPicks.preferences(gpu, rules, sdk, blob)
|
||||
.ifEmpty { AutoPicks.preferences(gpu, AutoPicks.BUILT_IN, sdk, blob) }
|
||||
.mapNotNull { if (it == AutoPicks.NEWEST_DD_TURNIP) newestDdTurnip(pairs) else it }
|
||||
@@ -74,6 +130,20 @@ object DriverPairs {
|
||||
?: keys.firstOrNull()
|
||||
}
|
||||
|
||||
private fun maliKey(gpu: GpuInfo, pairs: List<DriverPair>, sdk: Int): String? {
|
||||
val family = when (gpu.family) {
|
||||
Family.MALI_JM -> NAD_MALI_JM
|
||||
Family.MALI_G610_G710 -> NAD_MALI_G710
|
||||
Family.MALI_G615 -> NAD_MALI_G615
|
||||
Family.MALI_G720 -> NAD_MALI_G720
|
||||
else -> null
|
||||
}
|
||||
val kbase = NAD_MALI_KBASE.takeIf { sdk >= MALI_KBASE_MIN_SDK && GpuInfo.maliKbaseCovered(gpu.name) }
|
||||
if (pairs.isEmpty()) return kbase ?: family
|
||||
fun listed(key: String?) = key?.takeIf { k -> pairs.any { it.key == k && it.complete } }
|
||||
return listed(kbase) ?: listed(family) ?: kbase ?: family
|
||||
}
|
||||
|
||||
fun newestDdTurnip(pairs: List<DriverPair>): String? = pairs
|
||||
.filter { it.key.startsWith("$DD_TURNIP:") && it.complete }
|
||||
.maxByOrNull { p ->
|
||||
|
||||
@@ -24,8 +24,22 @@ data class GpuInfo(
|
||||
A7XX("Adreno 7xx"),
|
||||
A6XX("Adreno 6xx"),
|
||||
ADRENO_UNKNOWN("Adreno"),
|
||||
POWERVR("PowerVR"),
|
||||
/** Mali on Arm's kbase driver, by the open PanVK build each generation needs. */
|
||||
MALI_JM("Mali (G52-G78)"),
|
||||
MALI_G610_G710("Mali-G610/G710"),
|
||||
MALI_G615("Mali-G615"),
|
||||
MALI_G720("Mali-G720"),
|
||||
/** Mali-G715/G725/G925: only the one-binary PanVK kbase build covers them (Android 15+). */
|
||||
MALI_LATEST("Mali-G715/G725/G925"),
|
||||
/** A Mali no open driver build covers: the Vulkan bridge is its only path. */
|
||||
MALI_OTHER("Mali"),
|
||||
NOT_ADRENO("Not an Adreno GPU");
|
||||
|
||||
val mali: Boolean get() = name.startsWith("MALI_")
|
||||
|
||||
fun label(gpuName: String): String = if (this == POWERVR && gpuName.contains("Xclipse", ignoreCase = true)) "Samsung Xclipse" else label
|
||||
|
||||
fun label(context: Context): String = if (this == NOT_ADRENO) context.getString(R.string.gpuinfo_not_adreno) else label
|
||||
}
|
||||
|
||||
@@ -47,10 +61,16 @@ data class GpuInfo(
|
||||
val supportText: String
|
||||
get() = when (support) {
|
||||
Support.TESTED -> "Supported"
|
||||
Support.UNTESTED -> if (isA850) "Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device"
|
||||
else if (family == Family.A7XX_LOW) "Experimental: its drivers are test builds"
|
||||
else "Outside tested hardware (Adreno 650, 725 and newer): it may not run"
|
||||
Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU"
|
||||
Support.UNTESTED -> when {
|
||||
family == Family.POWERVR -> if (name.contains("Xclipse", ignoreCase = true)) "Experimental: RADV or the system Xclipse driver"
|
||||
else "System PowerVR driver: compatibility varies by device"
|
||||
family == Family.MALI_OTHER -> "Experimental: Mali through the phone's own driver (Venus)"
|
||||
family.mali -> "Experimental: open Mali driver (test builds)"
|
||||
isA850 -> "Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device"
|
||||
family == Family.A7XX_LOW -> "Experimental: its drivers are test builds"
|
||||
else -> "Outside tested hardware (Adreno 650, 725 and newer): it may not run"
|
||||
}
|
||||
Support.UNSUPPORTED -> "Not supported: no compatible graphics backend is available for this GPU"
|
||||
}
|
||||
|
||||
fun supportText(bridge: Boolean): String = when {
|
||||
@@ -68,8 +88,9 @@ data class GpuInfo(
|
||||
fun supportText(context: Context): String = context.getString(when (support) {
|
||||
Support.TESTED -> R.string.gpuinfo_supported
|
||||
Support.UNTESTED -> when {
|
||||
family == Family.POWERVR -> if (name.contains("Xclipse", ignoreCase = true)) R.string.gpuinfo_xclipse else R.string.gpuinfo_powervr
|
||||
isA850 -> R.string.gpuinfo_a850_experimental
|
||||
family == Family.A7XX_LOW -> R.string.gpuinfo_experimental
|
||||
family == Family.A7XX_LOW || family.mali -> R.string.gpuinfo_experimental
|
||||
else -> R.string.gpuinfo_below_tested
|
||||
}
|
||||
Support.UNSUPPORTED -> R.string.gpuinfo_unsupported
|
||||
@@ -91,10 +112,20 @@ data class GpuInfo(
|
||||
{ VulkanInfo.cachedOrNull()?.get("device") },
|
||||
{ platformGpuModel },
|
||||
) else 0 to ""
|
||||
val family = familyOf(adreno, model)
|
||||
// Mali, named by the vendor's Vulkan driver ("Mali-G720-Immortalis MC12") or by kbase.
|
||||
val mali = if (adreno || !File("/dev/mali0").exists()) null else maliName()
|
||||
val family = familyOf(adreno, model, SystemVulkanDriver.isPowerVr(), mali)
|
||||
val samsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
return GpuInfo(
|
||||
name = if (!adreno) Build.HARDWARE.ifBlank { UNNAMED } else if (model > 0) "Adreno $model" else "Adreno",
|
||||
name = when {
|
||||
family == Family.POWERVR -> SystemVulkanDriver.gpuName().let { gpu ->
|
||||
VulkanInfo.cachedOrNull()?.get("device")?.takeIf { it.contains(gpu, ignoreCase = true) } ?: gpu
|
||||
}
|
||||
mali != null && mali.isNotEmpty() -> mali
|
||||
!adreno -> Build.HARDWARE.ifBlank { UNNAMED }
|
||||
model > 0 -> "Adreno $model"
|
||||
else -> "Adreno"
|
||||
},
|
||||
model = model, family = family, soc = soc,
|
||||
oneUi8Gen2 = samsung && model == 740,
|
||||
kgslName = raw.orEmpty(), modelSource = source, kgslChipId = chipId,
|
||||
@@ -136,7 +167,68 @@ data class GpuInfo(
|
||||
""
|
||||
}
|
||||
|
||||
internal fun familyOf(adreno: Boolean, model: Int): Family = when {
|
||||
/**
|
||||
* The Mali's name, "" when nothing names it: the vendor Vulkan driver's own name when this
|
||||
* process has asked it, kbase's gpuinfo where SELinux lets the app read it (most phones do
|
||||
* not), else the GPU the chip is known to carry ([MALI_SOCS]).
|
||||
*/
|
||||
private fun maliName(): String =
|
||||
VulkanInfo.cachedOrNull()?.get("device")?.takeIf { it.contains("Mali", ignoreCase = true) }
|
||||
?: FileUtils.readString(File("/sys/class/misc/mali0/device/gpuinfo"))?.trim()?.takeIf { it.contains("Mali", ignoreCase = true) }
|
||||
?.substringBefore(" ")
|
||||
?: maliOfSoc(socIds())
|
||||
?: ""
|
||||
|
||||
/** The chip's names as the platform gives them: "mt6989", "MT6989", "gs201". */
|
||||
private fun socIds(): List<String> = listOfNotNull(
|
||||
if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL else null,
|
||||
systemProperty("ro.board.platform"), systemProperty("ro.soc.model"), Build.HARDWARE, Build.BOARD,
|
||||
).filter { it.isNotBlank() }
|
||||
|
||||
/** Chips whose Mali is known for certain (MediaTek Dimensity / Helio / Kompanio, Google Tensor). */
|
||||
private val MALI_SOCS = mapOf(
|
||||
"mt6993" to "Mali-G1-Ultra", "mt6991" to "Mali-G925", "mt6989" to "Mali-G720", "mt6899" to "Mali-G720", "mt6985" to "Mali-G715",
|
||||
"mt6983" to "Mali-G710", "mt6897" to "Mali-G615", "mt6878" to "Mali-G615", "mt6895" to "Mali-G610",
|
||||
"mt6896" to "Mali-G610", "mt6886" to "Mali-G610", "mt6893" to "Mali-G77", "mt6891" to "Mali-G77",
|
||||
"mt6889" to "Mali-G77", "mt6885" to "Mali-G77", "mt6877" to "Mali-G68", "mt6853" to "Mali-G57",
|
||||
"mt6833" to "Mali-G57", "mt6835" to "Mali-G57", "mt6789" to "Mali-G57", "mt6785" to "Mali-G76",
|
||||
"mt6781" to "Mali-G52", "mt6769" to "Mali-G52", "mt8192" to "Mali-G57", "mt8195" to "Mali-G57",
|
||||
"gs101" to "Mali-G78", "gs201" to "Mali-G710", "zuma" to "Mali-G715", "zumapro" to "Mali-G715",
|
||||
"s5e8835" to "Mali-G68", "s5e8825" to "Mali-G68",
|
||||
)
|
||||
|
||||
/** The Mali [ids] name a chip of, "Mali-G720 (by chip mt6989)", or null for an unknown chip. */
|
||||
internal fun maliOfSoc(ids: List<String>): String? = ids.firstNotNullOfOrNull { id ->
|
||||
MALI_SOCS[id.trim().lowercase()]?.let { "$it (by chip ${id.trim().lowercase()})" }
|
||||
}
|
||||
|
||||
/** The number in a Mali's name: 720 for "Mali-G720-Immortalis MC12". */
|
||||
fun maliNumber(name: String): Int? =
|
||||
Regex("""G(\d{2,3})""", RegexOption.IGNORE_CASE).find(name)?.groupValues?.get(1)?.toIntOrNull()
|
||||
|
||||
/**
|
||||
* The Malis the one-binary PanVK kbase build covers (arch v9-v13): G57/G68/G77/G78,
|
||||
* G610/G710, G615/G715, G720/G725 and G925. Not the older Bifrost G52/G76.
|
||||
*/
|
||||
fun maliKbaseCovered(name: String): Boolean =
|
||||
maliNumber(name) in setOf(57, 68, 77, 78, 610, 710, 615, 715, 720, 725, 925)
|
||||
|
||||
/** The PanVK generation a Mali name belongs to; MALI_OTHER when none of the builds covers it. */
|
||||
internal fun maliFamily(name: String): Family {
|
||||
val number = maliNumber(name) ?: return Family.MALI_OTHER
|
||||
return when (number) {
|
||||
31, 51, 52, 57, 68, 71, 72, 76, 77, 78 -> Family.MALI_JM
|
||||
610, 710 -> Family.MALI_G610_G710
|
||||
615 -> Family.MALI_G615
|
||||
720 -> Family.MALI_G720
|
||||
715, 725, 925 -> Family.MALI_LATEST
|
||||
else -> Family.MALI_OTHER
|
||||
}
|
||||
}
|
||||
|
||||
internal fun familyOf(adreno: Boolean, model: Int, powerVr: Boolean = false, mali: String? = null): Family = when {
|
||||
!adreno && powerVr -> Family.POWERVR
|
||||
!adreno && mali != null -> maliFamily(mali)
|
||||
!adreno -> Family.NOT_ADRENO
|
||||
model == 0 -> Family.ADRENO_UNKNOWN
|
||||
model >= 800 -> Family.A8XX
|
||||
|
||||
@@ -3,8 +3,12 @@ package com.droiddeck.launcher.gpu;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.droiddeck.launcher.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class LinuxVulkanDriver {
|
||||
private static final String TAG = "LinuxVulkanDriver";
|
||||
@@ -25,15 +29,36 @@ public final class LinuxVulkanDriver {
|
||||
return values;
|
||||
}
|
||||
|
||||
public static String resolveIcdPath(Context context, String value) {
|
||||
if (value == null || value.isEmpty()) return null;
|
||||
public static String optionLabel(Context context, String value) {
|
||||
if (value == null || value.isEmpty()) return SystemVulkanDriver.isPowerVr()
|
||||
? context.getString(R.string.gpu_system_powervr) : "Runtime default (built into the runtime)";
|
||||
LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context);
|
||||
String icd = m.getIcdPath(value);
|
||||
if (icd == null) {
|
||||
Log.w(TAG, "imported Linux driver \"" + value + "\" is gone; using the runtime's own driver");
|
||||
return null;
|
||||
}
|
||||
Log.i(TAG, "Linux session draws with imported driver " + value + " (" + icd + ")");
|
||||
if (!m.isInstalled(value)) return value + (SystemVulkanDriver.isPowerVr()
|
||||
? " (imported, missing)" : " (imported, missing - uses the runtime default)");
|
||||
String ver = m.getDriverVersion(value);
|
||||
return m.getDriverName(value) + (ver.isEmpty() ? "" : " " + ver) + " (imported)";
|
||||
}
|
||||
|
||||
public static String resolveIcdPath(Context context, String value) {
|
||||
boolean powerVr = SystemVulkanDriver.isPowerVr();
|
||||
return resolveIcdPath(value, powerVr,
|
||||
() -> SystemVulkanDriver.icdPath(context), id -> {
|
||||
String icd = new LinuxVulkanDriverManager(context).getIcdPath(id);
|
||||
if (icd == null) Log.w(TAG, "imported Linux driver \"" + id + "\" is gone"
|
||||
+ (powerVr ? "" : "; using the runtime's own driver"));
|
||||
else Log.i(TAG, "Linux session draws with imported driver " + id + " (" + icd + ")");
|
||||
return icd;
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve the selected backend before looking up an imported driver. */
|
||||
static String resolveIcdPath(String value, boolean powerVr, Supplier<String> systemIcd,
|
||||
Function<String, String> importedIcd) {
|
||||
if (SystemVulkanDriver.usesDefault(powerVr, value)) return systemIcd.get();
|
||||
if (value == null || value.isEmpty()) return null;
|
||||
String icd = importedIcd.apply(value);
|
||||
if (powerVr && icd == null) throw new IllegalStateException(
|
||||
"Selected Linux Vulkan driver \"" + value + "\" is unavailable. Select an installed driver or use Auto.");
|
||||
return icd;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,9 @@ public class LinuxVulkanDriverManager {
|
||||
public boolean isInstalled(String id) {
|
||||
if (id == null || id.isEmpty() || id.contains("/") || id.contains("..")) return false;
|
||||
File dir = getDriverDir(id);
|
||||
return new File(dir, LIB_NAME).isFile() && new File(dir, ICD_NAME).isFile();
|
||||
if (!new File(dir, ICD_NAME).isFile()) return false;
|
||||
for (String name : STORED_NAMES) if (new File(dir, name).isFile()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getIcdPath(String id) {
|
||||
@@ -107,10 +109,10 @@ public class LinuxVulkanDriverManager {
|
||||
if (entry.isDirectory()) continue;
|
||||
String base = new File(entry.getName()).getName();
|
||||
if (base.isEmpty()) continue;
|
||||
if (base.startsWith("libvulkan_freedreno") && base.endsWith(".so")) {
|
||||
if (soName != null) Log.w(TAG, "zip has several libvulkan_freedreno*.so; using the first (" + soName + ")");
|
||||
if (isDriverLibrary(base)) {
|
||||
if (soName != null) Log.w(TAG, "zip has several driver libraries; using the first (" + soName + ")");
|
||||
else {
|
||||
Files.copy(zis, new File(tmpDir, LIB_NAME).toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(zis, new File(tmpDir, storedName(base)).toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
soName = base;
|
||||
}
|
||||
} else if (base.equals(META_NAME)) {
|
||||
@@ -142,7 +144,8 @@ public class LinuxVulkanDriverManager {
|
||||
if (soName == null) {
|
||||
throw new IllegalArgumentException(context.getString(R.string.lvd_no_library));
|
||||
}
|
||||
File so = new File(tmpDir, LIB_NAME);
|
||||
String stored = storedName(soName);
|
||||
File so = new File(tmpDir, stored);
|
||||
if (!isAarch64Elf(so)) {
|
||||
throw new IllegalArgumentException(context.getString(R.string.lvd_not_aarch64, soName));
|
||||
}
|
||||
@@ -167,7 +170,7 @@ public class LinuxVulkanDriverManager {
|
||||
JSONObject icd = new JSONObject();
|
||||
icd.put("file_format_version", "1.0.0");
|
||||
JSONObject icdBody = new JSONObject();
|
||||
icdBody.put("library_path", new File(dir, LIB_NAME).getAbsolutePath());
|
||||
icdBody.put("library_path", new File(dir, stored).getAbsolutePath());
|
||||
icdBody.put("api_version", "1.1.274");
|
||||
icd.put("ICD", icdBody);
|
||||
if (!FileUtils.writeString(new File(tmpDir, ICD_NAME), icd.toString(2))) throw new IOException("cannot write icd.json");
|
||||
@@ -180,6 +183,7 @@ public class LinuxVulkanDriverManager {
|
||||
meta.put("libc", "glibc");
|
||||
meta.put("minGlibc", minGlibc);
|
||||
meta.put("sourceLibraryName", soName);
|
||||
meta.put("libraryName", stored);
|
||||
meta.put("importedAt", System.currentTimeMillis());
|
||||
if (!FileUtils.writeString(new File(tmpDir, META_NAME), meta.toString(2))) throw new IOException("cannot write meta.json");
|
||||
|
||||
@@ -198,6 +202,24 @@ public class LinuxVulkanDriverManager {
|
||||
return id;
|
||||
}
|
||||
|
||||
/** The names an installed driver's library is kept under; Turnip keeps {@link #LIB_NAME}. */
|
||||
private static final String[] STORED_NAMES = {LIB_NAME, "libvulkan_panfrost.so", "libvulkan_radeon.so"};
|
||||
|
||||
/**
|
||||
* The name a driver library is installed under: its own driver's (PanVK, RADV), so the session
|
||||
* log and the graphics report say which driver it is; a Turnip build keeps {@link #LIB_NAME}.
|
||||
*/
|
||||
public static String storedName(String soName) {
|
||||
if (soName != null && soName.startsWith("libvulkan_panfrost")) return "libvulkan_panfrost.so";
|
||||
if (soName != null && soName.startsWith("libvulkan_radeon")) return "libvulkan_radeon.so";
|
||||
return LIB_NAME;
|
||||
}
|
||||
|
||||
public static boolean isDriverLibrary(String base) {
|
||||
return base.endsWith(".so") && (base.startsWith("libvulkan_freedreno")
|
||||
|| base.startsWith("libvulkan_panfrost") || base.startsWith("libvulkan_radeon"));
|
||||
}
|
||||
|
||||
static String sanitizeId(String name) {
|
||||
String s = name.trim().replaceAll("[^A-Za-z0-9._-]+", "_").replaceAll("^[._]+", "");
|
||||
if (s.length() > 64) s = s.substring(0, 64);
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package com.droiddeck.launcher.gpu;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
import com.droiddeck.launcher.core.FileUtils;
|
||||
import com.droiddeck.launcher.core.TarZst;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** Isolated glibc-to-Android Vulkan bridge. It does not replace the rootfs's Mesa libraries. */
|
||||
public final class SystemVulkanDriver {
|
||||
private static final String ASSET = "hybris/runtime.tzst";
|
||||
/** Vendor HALs the bridge loads. Samsung Xclipse (Exynos, sgpu) is experimental. */
|
||||
private static final String[] HALS = {
|
||||
"/vendor/lib64/hw/vulkan.powervr.so",
|
||||
"/vendor/lib64/hw/vulkan.samsung.so"
|
||||
};
|
||||
private static final String[] REQUIRED = {
|
||||
"lib/libhybris-vulkan-icd.so.0", "lib/libhybris-common.so.1",
|
||||
"lib/libhybris/linker/q.so", "lib/libVkLayer_hybris_compat.so",
|
||||
"run-proton.py", "wsi/libVkLayer_window_system_integration.so",
|
||||
"wsi/VkLayer_window_system_integration.json",
|
||||
"dxvk/x32/d3d8.dll", "dxvk/x32/d3d9.dll", "dxvk/x32/d3d10core.dll",
|
||||
"dxvk/x32/d3d11.dll", "dxvk/x32/dxgi.dll",
|
||||
"dxvk/x64/d3d8.dll", "dxvk/x64/d3d9.dll", "dxvk/x64/d3d10core.dll",
|
||||
"dxvk/x64/d3d11.dll", "dxvk/x64/dxgi.dll"
|
||||
};
|
||||
|
||||
/** Rootfs directory of the Xclipse layer the apk stages (SessionFiles). */
|
||||
private static final String XCLIPSE_LAYER_DIR = "/usr/local/lib/droiddeck-xclipse";
|
||||
|
||||
private SystemVulkanDriver() {}
|
||||
|
||||
/**
|
||||
* The first vendor HAL present, or null. An Exynos with a Mali (Exynos 1280/1380: /dev/mali0)
|
||||
* may name its HAL vulkan.samsung.so too; that is a Mali, not an Xclipse, and is not taken.
|
||||
*/
|
||||
public static String hal() {
|
||||
for (String path : HALS) {
|
||||
if (!new File(path).isFile()) continue;
|
||||
if (path.endsWith("/vulkan.samsung.so") && new File("/dev/mali0").exists()) continue;
|
||||
return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Samsung's Xclipse (Exynos, sgpu) rather than PowerVR. */
|
||||
public static boolean isXclipse() {
|
||||
String hal = hal();
|
||||
return hal != null && hal.endsWith("/vulkan.samsung.so");
|
||||
}
|
||||
|
||||
/** The system-driver GPU family, for labels. */
|
||||
public static String gpuName() { return isXclipse() ? "Xclipse" : "PowerVR"; }
|
||||
|
||||
/** Whether a system-driver GPU (PowerVR or Xclipse) is present; the name predates Xclipse. */
|
||||
public static boolean isPowerVr() { return hal() != null; }
|
||||
|
||||
/** HAL detection names the GPU; launching also needs the real DRM render node. */
|
||||
public static boolean isAvailable() { return isPowerVr() && renderNodeAvailable(); }
|
||||
|
||||
public static boolean renderNodeAvailable() {
|
||||
return renderNodePath() != null;
|
||||
}
|
||||
|
||||
/** Select the actual PowerVR node, including devices where another GPU owns renderD128. */
|
||||
public static String renderNodePath() {
|
||||
String[] nodes = new File("/dev/dri").list();
|
||||
if (nodes == null) nodes = new File("/sys/class/drm").list();
|
||||
return findRenderNode(nodes, SystemVulkanDriver::renderNodeAvailable);
|
||||
}
|
||||
|
||||
static String findRenderNode(String[] nodes, Predicate<String> available) {
|
||||
TreeSet<String> candidates = new TreeSet<>();
|
||||
// Android can deny directory listing while allowing direct access. Probe the kernel's
|
||||
// reserved render range too; directory entries also cover dynamically allocated minors.
|
||||
for (int minor = 128; minor < 192; minor++) candidates.add("renderD" + minor);
|
||||
if (nodes != null) for (String node : nodes)
|
||||
if (node.matches("renderD[0-9]+")) candidates.add(node);
|
||||
for (String node : candidates) {
|
||||
String path = "/dev/dri/" + node;
|
||||
if (available.test(path)) return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean renderNodeAvailable(String node) {
|
||||
try {
|
||||
if (!renderNodeAvailable(OsConstants.S_ISCHR(Os.stat(node).st_mode),
|
||||
Os.access(node, OsConstants.R_OK), Os.access(node, OsConstants.W_OK))) return false;
|
||||
return powerVrDevice(new File("/sys/class/drm/" + new File(node).getName() + "/device"));
|
||||
} catch (ErrnoException error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean powerVrDriver(String driver) {
|
||||
return "pvrsrvkm".equals(driver) || "pvr".equals(driver) || "powervr".equals(driver)
|
||||
|| "sgpu".equals(driver);
|
||||
}
|
||||
|
||||
static boolean powerVrDevice(File device) {
|
||||
try {
|
||||
return powerVrDriver(Files.readSymbolicLink(new File(device, "driver").toPath())
|
||||
.getFileName().toString());
|
||||
} catch (IOException error) {
|
||||
// Some Android policies expose uevent but deny reading the driver symlink.
|
||||
try (BufferedReader input = Files.newBufferedReader(
|
||||
new File(device, "uevent").toPath(), StandardCharsets.US_ASCII)) {
|
||||
String line;
|
||||
while ((line = input.readLine()) != null)
|
||||
if (line.startsWith("DRIVER=")) return powerVrDriver(line.substring(7));
|
||||
} catch (IOException ignored) { }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static boolean renderNodeAvailable(boolean characterDevice, boolean readable, boolean writable) {
|
||||
return characterDevice && readable && writable;
|
||||
}
|
||||
|
||||
public static boolean usesDefault(String choice) {
|
||||
return usesDefault(isPowerVr(), choice);
|
||||
}
|
||||
|
||||
static boolean usesDefault(boolean powerVr, String choice) {
|
||||
return powerVr && (choice == null || choice.isEmpty());
|
||||
}
|
||||
|
||||
/** APK-versioned directories stay intact while an older session still has libraries mapped. */
|
||||
public static synchronized File prepare(Context context) throws IOException {
|
||||
if (!isPowerVr()) throw new IOException("System PowerVR Vulkan driver is unavailable");
|
||||
if (!renderNodeAvailable()) throw new IOException(
|
||||
"System PowerVR requires an identifiable, readable and writable DRM render node");
|
||||
File target = runtimeDirectory(context);
|
||||
File base = target.getParentFile();
|
||||
if (complete(target)) return target;
|
||||
if (!base.isDirectory() && !base.mkdirs()) throw new IOException("Cannot create system Vulkan directory");
|
||||
File staged = new File(base, ".stage-" + UUID.randomUUID());
|
||||
try {
|
||||
if (!TarZst.extractAsset(context, ASSET, staged)) throw new IOException("Cannot unpack system Vulkan runtime");
|
||||
for (String name : REQUIRED)
|
||||
if (!present(staged, name)) throw new IOException("Missing system Vulkan component: " + name);
|
||||
if (!new File(staged, "run-proton.py").canExecute())
|
||||
throw new IOException("System Vulkan Proton wrapper is not executable");
|
||||
JSONObject library = new JSONObject();
|
||||
library.put("library_path", new File(target, REQUIRED[0]).getPath());
|
||||
library.put("api_version", "1.3.0");
|
||||
JSONObject manifest = new JSONObject();
|
||||
manifest.put("file_format_version", "1.0.0");
|
||||
manifest.put("ICD", library);
|
||||
Files.write(new File(staged, "icd.json").toPath(), manifest.toString().getBytes(StandardCharsets.UTF_8));
|
||||
if (target.exists()) throw new IOException("Incomplete system Vulkan runtime directory: " + target.getName());
|
||||
if (!staged.renameTo(target)) throw new IOException("Cannot publish system Vulkan runtime");
|
||||
return target;
|
||||
} catch (org.json.JSONException error) {
|
||||
throw new IOException("Cannot write system Vulkan manifest", error);
|
||||
} finally {
|
||||
if (staged.exists()) FileUtils.delete(staged);
|
||||
}
|
||||
}
|
||||
|
||||
/** The current APK's versioned runtime path, without extracting or modifying it. */
|
||||
public static File runtimeDirectory(Context context) throws IOException {
|
||||
String version;
|
||||
try (BufferedReader input = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open("hybris/version.txt"), StandardCharsets.US_ASCII))) {
|
||||
version = input.readLine();
|
||||
}
|
||||
if (version == null || !version.matches("[0-9a-f]{64}")) throw new IOException("Invalid system Vulkan runtime version");
|
||||
return new File(new File(context.getFilesDir(), "system_vulkan"), version);
|
||||
}
|
||||
|
||||
/** Collect abandoned stages, and obsolete versions once older guests can no longer load them. */
|
||||
public static synchronized void pruneUnused(Context context, boolean guestsStopped) throws IOException {
|
||||
pruneUnused(runtimeDirectory(context), guestsStopped);
|
||||
}
|
||||
|
||||
static synchronized void pruneUnused(File current, boolean guestsStopped) {
|
||||
File base = current.getParentFile();
|
||||
if (base == null || Files.isSymbolicLink(base.toPath()) || Files.isSymbolicLink(current.toPath())) return;
|
||||
File[] entries = base.listFiles();
|
||||
if (entries == null) return;
|
||||
// Retain the previous good version if publishing the replacement failed.
|
||||
boolean pruneVersions = guestsStopped && complete(current);
|
||||
for (File entry : entries) {
|
||||
if (entry.equals(current)) continue;
|
||||
String name = entry.getName();
|
||||
boolean oldVersion = pruneVersions && name.matches("[0-9a-f]{64}");
|
||||
boolean abandonedStage = name.matches("\\.stage-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
|
||||
// The prepare monitor excludes any extraction still writing a .stage directory.
|
||||
if (oldVersion || abandonedStage) FileUtils.delete(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean complete(File directory) {
|
||||
if (!new File(directory, "icd.json").isFile()) return false;
|
||||
for (String name : REQUIRED) if (!present(directory, name)) return false;
|
||||
return new File(directory, "run-proton.py").canExecute();
|
||||
}
|
||||
|
||||
private static boolean present(File directory, String name) {
|
||||
File file = new File(directory, name);
|
||||
return file.isFile() && file.length() > 0;
|
||||
}
|
||||
|
||||
public static String icdPath(Context context) {
|
||||
try {
|
||||
return new File(prepare(context), "icd.json").getPath();
|
||||
} catch (IOException error) {
|
||||
throw new IllegalStateException("System Vulkan runtime is unavailable", error);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addEnvironment(Context context, List<String> environment) {
|
||||
File directory;
|
||||
try { directory = prepare(context); }
|
||||
catch (IOException error) { throw new IllegalStateException("System Vulkan runtime is unavailable", error); }
|
||||
String node = renderNodePath();
|
||||
if (node == null) throw new IllegalStateException("System PowerVR render node is unavailable");
|
||||
environment.add("HYBRIS_ANDROID_SDK_VERSION=" + Build.VERSION.SDK_INT);
|
||||
environment.add("HYBRIS_VULKAN_HAL=" + hal());
|
||||
environment.add("HYBRIS_LINKER_DIR=" + new File(directory, "lib/libhybris/linker").getPath());
|
||||
// Prefer framework dependencies for the system Vulkan loader, including libbase.so.
|
||||
environment.add("HYBRIS_LD_LIBRARY_PATH=/system/lib64:/system_ext/lib64:/vendor/lib64/egl:/vendor/lib64/hw:/vendor/lib64");
|
||||
boolean xclipse = isXclipse();
|
||||
// Xclipse gets its own layer (tools/linuxfs/xclipse-layer) in front of everything: it sets
|
||||
// bionic's environ for the driver's shader compiler, among other driver workarounds. The
|
||||
// session's own clients need nothing else; games keep the compat layer's BC emulation.
|
||||
String layers = xclipse ? "VK_LAYER_DROIDDECK_xclipse" : "VK_LAYER_HYBRIS_compat";
|
||||
environment.add("VK_LAYER_PATH=" + new File(directory, "lib").getPath() + (xclipse ? ":" + XCLIPSE_LAYER_DIR : ""));
|
||||
environment.add("VK_INSTANCE_LAYERS=" + layers);
|
||||
// Proton game launches add their WSI layer in front of these (droiddeck-game-env).
|
||||
environment.add("DROIDDECK_HYBRIS_LAYERS=" + (xclipse ? layers + ":VK_LAYER_HYBRIS_compat" : layers));
|
||||
if (xclipse) environment.add("DROIDDECK_HYBRIS_LAYER_PATH=" + XCLIPSE_LAYER_DIR);
|
||||
// DXVK 3 needs 256 bytes of push constants; this driver has 128.
|
||||
if (xclipse) environment.add("DROIDDECK_HYBRIS_DXVK=dxvk-2.6.2");
|
||||
// The Android driver has a render node, but no KMS primary node. SDL presents
|
||||
// through the wrapper's Wayland Vulkan swapchain instead of a KMS-style path.
|
||||
environment.add("BL_GAMESCOPE_BACKEND=sdl");
|
||||
// The Android pvr kernel accepts Vulkan DMA-BUF imports but rejects PRIME GEM handles.
|
||||
environment.add("BL_POWERVR_DRM_HANDLES=1");
|
||||
environment.add("BL_POWERVR_RENDER_NODE=" + node);
|
||||
// Inner Gamescope/Xwayland lacks the bridge's Android-buffer WSI protocols.
|
||||
environment.add("BL_STEAM_GL_PRESENT=1");
|
||||
// Only actual Proton game launches use the bundled DXVK and X11 WSI layer.
|
||||
environment.add("DROIDDECK_POWERVR_RUNTIME=" + directory.getPath());
|
||||
}
|
||||
}
|
||||
@@ -263,7 +263,24 @@ public final class TurnipDriver {
|
||||
}
|
||||
|
||||
private String choose() {
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(context);
|
||||
String chosen = SessionPrefs.androidDriver(context);
|
||||
// Mali (Venus): the phone's own driver draws the screen, as Venus replays on it.
|
||||
if (com.droiddeck.launcher.session.VenusComponent.wanted(context)) {
|
||||
Log.i(TAG, "Mali (Venus): display on the system Vulkan loader");
|
||||
return null;
|
||||
}
|
||||
// A Mali under Auto draws its display on Android's own Vulkan loader, whatever an earlier
|
||||
// build set: a PanVK Android half fails on Mali gralloc. A manual pick is respected.
|
||||
if (chosen != null && !chosen.isEmpty() && new File("/dev/mali0").exists()
|
||||
&& !com.droiddeck.launcher.core.DeviceSupport.INSTANCE.adreno()
|
||||
&& SessionPrefs.GPU_DRIVERS_AUTO.equals(SessionPrefs.INSTANCE.gpuDriverMode(context))) {
|
||||
DriverBundle.Bundle bundle = DriverBundle.INSTANCE.containing(context, chosen, false);
|
||||
if (bundle != null && DriverPairs.INSTANCE.isNonAdreno(bundle.getId())) {
|
||||
Log.i(TAG, "Mali under Auto: display on the system Vulkan loader, not " + chosen);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (chosen != null && !chosen.isEmpty()) {
|
||||
if (BUNDLED.contains(chosen)) {
|
||||
Log.i(TAG, "driver pinned by the user: " + chosen);
|
||||
@@ -289,6 +306,7 @@ public final class TurnipDriver {
|
||||
if (forced.startsWith("a7")) return DRIVER_A7XX;
|
||||
if (forced.startsWith("a6")) return DRIVER_A6XX;
|
||||
}
|
||||
if (SystemVulkanDriver.isPowerVr()) return null;
|
||||
String quest = QuestExperimental.INSTANCE.defaultAndroidDriver();
|
||||
if (quest != null) return quest;
|
||||
if (com.droiddeck.launcher.core.DeviceSupport.INSTANCE.vkBridge(context)) {
|
||||
@@ -296,7 +314,7 @@ public final class TurnipDriver {
|
||||
return null;
|
||||
}
|
||||
GpuInfo gpu = gpuDetector.get();
|
||||
if (gpu.getFamily() == GpuInfo.Family.NOT_ADRENO) {
|
||||
if (gpu.getFamily() == GpuInfo.Family.NOT_ADRENO || gpu.getFamily().getMali()) {
|
||||
Log.i(TAG, "not an Adreno GPU; leaving the display driver to the system");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,17 @@ object TurnipReleases {
|
||||
private const val KEY_PICKS = "autoPicks"
|
||||
|
||||
class Asset(val source: String, val tag: String, val name: String, val url: String, val size: Long, val linux: Boolean, val label: String,
|
||||
val sha256: String = "", val pair: String = "", val bundle: Boolean = false)
|
||||
val sha256: String = "", val pair: String = "", val bundle: Boolean = false,
|
||||
val assetId: Long = 0, val updatedAt: String = "") {
|
||||
/** "2026-10-15" from GitHub's updated_at; empty when the check did not record it. */
|
||||
val builtOn: String get() = updatedAt.take(10)
|
||||
}
|
||||
class Check(val assets: List<Asset>, val latest: List<Pair<String, String>>, val failed: List<String>, val checkedAt: Long)
|
||||
|
||||
private class Kind(val linux: Boolean, val label: String, val pair: String, val bundle: Boolean = false)
|
||||
|
||||
private class Source(val label: String, val repo: String, val classify: (name: String, tag: String) -> Kind?)
|
||||
/** [prerelease]: a source whose bundles are all test builds offers them from pre-releases too. */
|
||||
private class Source(val label: String, val repo: String, val prerelease: Boolean = false, val classify: (name: String, tag: String) -> Kind?)
|
||||
|
||||
private val SOURCES = listOf(
|
||||
Source("Banners-Turnip", "The412Banner/Banners-Turnip") { name, tag ->
|
||||
@@ -56,8 +61,22 @@ object TurnipReleases {
|
||||
if (!Regex("""^DD-Turnip-v\d+\.\d+\.\d+\.zip$""").matches(name)) return@Source null
|
||||
Kind(false, "Android + Linux · all Adreno", DriverPairs.ddTurnip(tag), bundle = true)
|
||||
},
|
||||
// <Id>-Pair.zip: open drivers for GPUs that are not Adreno (PanVK for Mali, RADV for
|
||||
// Xclipse), both halves in one zip. One release per driver, replaced in place on each
|
||||
// rebuild; every one is a test build, published as a pre-release.
|
||||
Source(NON_ADRENO_LABEL, NON_ADRENO_REPO, prerelease = true) { name, _ ->
|
||||
val m = Regex("""^(RADV-Xclipse(?:-[A-Za-z0-9]+)*?|PanVK-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*?)-Pair\.zip$""").find(name) ?: return@Source null
|
||||
val key = DriverPairs.nonAdreno(m.groupValues[1])
|
||||
Kind(false, "Android + Linux · " + DriverPairs.nonAdrenoName(key), key, bundle = true)
|
||||
},
|
||||
)
|
||||
|
||||
private const val NON_ADRENO_REPO = "The412Banner/droiddeck-nonadreno-drivers"
|
||||
const val NON_ADRENO_LABEL = "Non-Adreno"
|
||||
|
||||
private fun sources(): List<Source> =
|
||||
if (com.droiddeck.launcher.core.DeviceSupport.adreno()) SOURCES.filter { it.repo != NON_ADRENO_REPO } else SOURCES
|
||||
|
||||
fun autoPicks(context: Context): List<AutoPicks.Rule> =
|
||||
AutoPicks.parse(prefs(context).getString(KEY_PICKS, null)) ?: AutoPicks.BUILT_IN
|
||||
|
||||
@@ -71,9 +90,13 @@ object TurnipReleases {
|
||||
val latest = JSONArray()
|
||||
val failed = ArrayList<String>()
|
||||
var lastError: Exception? = null
|
||||
for (src in SOURCES) {
|
||||
val sources = sources()
|
||||
for (src in sources) {
|
||||
try {
|
||||
val releases = JSONArray(get(context, "https://api.github.com/repos/${src.repo}/releases?per_page=15"))
|
||||
// The non-Adreno repo keeps one release per driver plus many component builds (gamescope,
|
||||
// Venus): its driver pairs fall past the first 15, so it is read in one full page.
|
||||
val perPage = if (src.repo == NON_ADRENO_REPO) 100 else 15
|
||||
val releases = JSONArray(get(context, "https://api.github.com/repos/${src.repo}/releases?per_page=$perPage"))
|
||||
val seen = HashSet<String>()
|
||||
var newest: String? = null
|
||||
for (i in 0 until releases.length()) {
|
||||
@@ -86,7 +109,7 @@ object TurnipReleases {
|
||||
val a = list.getJSONObject(j)
|
||||
val name = a.getString("name")
|
||||
val kind = src.classify(name, tag) ?: continue
|
||||
if (kind.bundle && r.optBoolean("prerelease")) continue
|
||||
if (kind.bundle && r.optBoolean("prerelease") && !src.prerelease) continue
|
||||
val linux = kind.linux
|
||||
val label = kind.label
|
||||
val sha = Hashes.githubSha256(a.optString("digest")) ?: continue
|
||||
@@ -94,7 +117,7 @@ object TurnipReleases {
|
||||
assets.put(JSONObject().put("source", src.label).put("tag", tag).put("name", name)
|
||||
.put("url", a.getString("browser_download_url")).put("size", a.optLong("size"))
|
||||
.put("linux", linux).put("label", label).put("sha256", sha).put("pair", kind.pair)
|
||||
.put("bundle", kind.bundle))
|
||||
.put("bundle", kind.bundle).put("id", a.optLong("id")).put("updated_at", a.optString("updated_at")))
|
||||
}
|
||||
}
|
||||
if (newest != null) latest.put(JSONObject().put("source", src.label).put("tag", newest))
|
||||
@@ -103,7 +126,7 @@ object TurnipReleases {
|
||||
lastError = e
|
||||
}
|
||||
}
|
||||
if (failed.size == SOURCES.size) throw lastError ?: IOException("no source answered")
|
||||
if (failed.size == sources.size) throw lastError ?: IOException("no source answered")
|
||||
runCatching { get(context, AutoPicks.URL, AutoPicks.URL) }
|
||||
.onFailure { Log.i(TAG, "auto picks: ${it.message}") }
|
||||
.getOrNull()?.takeIf { AutoPicks.parse(it) != null }
|
||||
@@ -154,7 +177,7 @@ object TurnipReleases {
|
||||
val a = list.getJSONObject(i)
|
||||
out.add(Asset(a.getString("source"), a.getString("tag"), a.getString("name"), a.getString("url"),
|
||||
a.optLong("size"), a.getBoolean("linux"), a.getString("label"), a.optString("sha256", ""), a.optString("pair", ""),
|
||||
a.optBoolean("bundle", false)))
|
||||
a.optBoolean("bundle", false), a.optLong("id"), a.optString("updated_at", "")))
|
||||
}
|
||||
val latest = ArrayList<Pair<String, String>>()
|
||||
val l = json.optJSONArray("latest") ?: JSONArray()
|
||||
@@ -165,16 +188,53 @@ object TurnipReleases {
|
||||
return Check(out, latest, failed, json.optLong("checkedAt"))
|
||||
}
|
||||
|
||||
fun installedId(context: Context, asset: Asset, isInstalled: (String) -> Boolean): String? =
|
||||
fun installedId(context: Context, asset: Asset, isInstalled: (String) -> Boolean): String? {
|
||||
val d = downloads(context)
|
||||
if (DriverPairs.isNonAdreno(asset.pair) && asset.sha256.isNotEmpty() &&
|
||||
!asset.sha256.equals(d.optString(SHA_PREFIX + asset.name, asset.sha256), ignoreCase = true)) return null
|
||||
return d.optString(asset.name, "").takeIf { it.isNotEmpty() && isInstalled(it) }
|
||||
}
|
||||
|
||||
private const val SHA_PREFIX = "sha256:"
|
||||
private const val ASSET_ID_PREFIX = "asset:"
|
||||
private const val UPDATED_PREFIX = "updated:"
|
||||
|
||||
/** The id an open non-Adreno pair's asset was installed as, whichever build of it that was. */
|
||||
fun installedIdAnyBuild(context: Context, asset: Asset, isInstalled: (String) -> Boolean): String? =
|
||||
downloads(context).optString(asset.name, "").takeIf { it.isNotEmpty() && isInstalled(it) }
|
||||
|
||||
/**
|
||||
* An open non-Adreno pair is rebuilt in place under the same tag: a different digest, asset id
|
||||
* or upload time than the one installed means a newer build.
|
||||
*/
|
||||
fun updateAvailable(context: Context, asset: Asset): Boolean {
|
||||
if (!DriverPairs.isNonAdreno(asset.pair)) return false
|
||||
val d = downloads(context)
|
||||
if (d.optString(asset.name, "").isEmpty()) return false
|
||||
val sha = d.optString(SHA_PREFIX + asset.name, "")
|
||||
val id = d.optLong(ASSET_ID_PREFIX + asset.name, 0)
|
||||
val updated = d.optString(UPDATED_PREFIX + asset.name, "")
|
||||
return (sha.isNotEmpty() && asset.sha256.isNotEmpty() && !sha.equals(asset.sha256, ignoreCase = true)) ||
|
||||
(id != 0L && asset.assetId != 0L && id != asset.assetId) ||
|
||||
(updated.isNotEmpty() && asset.updatedAt.isNotEmpty() && updated != asset.updatedAt)
|
||||
}
|
||||
|
||||
/** When the installed build of [asset] was uploaded, as recorded at download ("" if unknown). */
|
||||
fun installedBuiltOn(context: Context, asset: Asset): String = downloads(context).optString(UPDATED_PREFIX + asset.name, "").take(10)
|
||||
|
||||
fun isDownloaded(context: Context, id: String): Boolean {
|
||||
val d = downloads(context)
|
||||
return d.keys().asSequence().any { d.optString(it) == id }
|
||||
}
|
||||
|
||||
fun recordDownload(context: Context, asset: Asset, id: String) {
|
||||
prefs(context).edit().putString(KEY_DOWNLOADS, downloads(context).put(asset.name, id).toString()).apply()
|
||||
val d = downloads(context).put(asset.name, id)
|
||||
if (DriverPairs.isNonAdreno(asset.pair)) {
|
||||
if (asset.sha256.isNotEmpty()) d.put(SHA_PREFIX + asset.name, asset.sha256)
|
||||
if (asset.assetId != 0L) d.put(ASSET_ID_PREFIX + asset.name, asset.assetId)
|
||||
if (asset.updatedAt.isNotEmpty()) d.put(UPDATED_PREFIX + asset.name, asset.updatedAt)
|
||||
}
|
||||
prefs(context).edit().putString(KEY_DOWNLOADS, d.toString()).apply()
|
||||
}
|
||||
|
||||
fun forget(context: Context, id: String) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.StructStat;
|
||||
import android.system.StructUtsname;
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver;
|
||||
import com.droiddeck.launcher.session.SessionPrefs;
|
||||
|
||||
|
||||
@@ -191,6 +192,13 @@ public final class LinuxRuntime {
|
||||
bind(cmd, "/dev");
|
||||
bind(cmd, "/proc");
|
||||
bind(cmd, "/sys");
|
||||
if (com.droiddeck.launcher.gpu.SystemVulkanDriver.isPowerVr()) {
|
||||
// The glibc bridge loads the device's existing Android HAL and its dependencies.
|
||||
// These read-only Android partitions remain subject to the app's normal permissions.
|
||||
for (String path : new String[]{"/system", "/vendor", "/apex", "/system_ext", "/product", "/odm"})
|
||||
if (new File(path).isDirectory()) bind(cmd, path);
|
||||
if (new File("/linkerconfig/ld.config.txt").isFile()) bind(cmd, "/linkerconfig/ld.config.txt");
|
||||
}
|
||||
bind(cmd, DeviceControlsComponent.prepareBacklight(context, root).getPath() + ":/sys/class/backlight");
|
||||
bind(cmd, "/dev/urandom:/dev/random");
|
||||
bind(cmd, "/proc/self/fd:/dev/fd");
|
||||
@@ -198,6 +206,10 @@ public final class LinuxRuntime {
|
||||
bind(cmd, "/proc/self/fd/1:/dev/stdout");
|
||||
bind(cmd, "/proc/self/fd/2:/dev/stderr");
|
||||
bind(cmd, new File(root, "etc/droiddeck/empty").getPath() + ":/sys/fs/selinux");
|
||||
// A kernel that refuses to list /sys/class/thermal (Pixel 10, Android 17) made mangoapp abort
|
||||
// at start ("filesystem_error ... Permission denied: '/sys/class/thermal/'") until its
|
||||
// wrapper gave up: it lists the directory for CPU temperatures. Empty there instead.
|
||||
if (new File("/sys/class/thermal").list() == null) bind(cmd, new File(root, "etc/droiddeck/empty").getPath() + ":/sys/class/thermal");
|
||||
bind(cmd, context.getFilesDir().getPath());
|
||||
bind(cmd, context.getCacheDir().getPath());
|
||||
bind(cmd, liveDir(context).getPath());
|
||||
@@ -259,7 +271,11 @@ public final class LinuxRuntime {
|
||||
if (unreadablePciDevice()) {
|
||||
bind(cmd, new File(root, "etc/droiddeck/empty").getPath() + ":/sys/bus/pci/devices");
|
||||
}
|
||||
bindGpuNode(context, cmd);
|
||||
if (com.droiddeck.launcher.gpu.SystemVulkanDriver.isPowerVr()) {
|
||||
bindPowerVrGpuNode(context, cmd);
|
||||
} else {
|
||||
bindGpuNode(context, cmd);
|
||||
}
|
||||
bindAdrenoStats(cmd, extraBinds);
|
||||
bindCpuTemps(cmd, root);
|
||||
if (extraBinds != null) {
|
||||
@@ -270,6 +286,24 @@ public final class LinuxRuntime {
|
||||
return specs;
|
||||
}
|
||||
|
||||
private static void bindPowerVrGpuNode(Context context, List<String> cmd) {
|
||||
if (new File("/dev/dri").list() != null) return;
|
||||
String node = SystemVulkanDriver.renderNodePath();
|
||||
if (node == null) return;
|
||||
String name = new File(node).getName();
|
||||
File dri = new File(context.getCacheDir(), "powervr-drm/" + name + "/dri");
|
||||
try {
|
||||
if (!dri.isDirectory() && !dri.mkdirs()) return;
|
||||
new File(dri, name).createNewFile();
|
||||
} catch (IOException e) {
|
||||
android.util.Log.w("LinuxRuntime", "Cannot expose the PowerVR render node", e);
|
||||
return;
|
||||
}
|
||||
bind(cmd, dri.getPath() + ":/dev/dri");
|
||||
bind(cmd, node + ":" + node);
|
||||
android.util.Log.i("LinuxRuntime", "PowerVR: exposing real " + name + " through a listable directory");
|
||||
}
|
||||
|
||||
private static boolean unreadablePciDevice() {
|
||||
File[] devices = new File("/sys/bus/pci/devices").listFiles();
|
||||
if (devices == null) return false;
|
||||
@@ -279,7 +313,7 @@ public final class LinuxRuntime {
|
||||
return false;
|
||||
}
|
||||
|
||||
static final String MALI_DEVICE = "/dev/mali0";
|
||||
public static final String MALI_DEVICE = "/dev/mali0";
|
||||
|
||||
static final String[] DRM_RENDER_NODES = {
|
||||
"/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/renderD130", "/dev/dri/renderD131",
|
||||
@@ -287,6 +321,7 @@ public final class LinuxRuntime {
|
||||
|
||||
public static String gpuStandIn(Context context) {
|
||||
if (new File(KGSL_DEVICE).exists()) return KGSL_DEVICE;
|
||||
if (com.droiddeck.launcher.core.DeviceSupport.INSTANCE.openDriverActive(context) && new File(MALI_DEVICE).exists()) return MALI_DEVICE;
|
||||
if (!com.droiddeck.launcher.core.DeviceSupport.INSTANCE.vkBridge(context)) return null;
|
||||
return bridgeStandIn(path -> new File(path).exists());
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.core.GameEnvironment
|
||||
import com.droiddeck.launcher.gpu.AppVulkan
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.github.luben.zstd.ZstdInputStream
|
||||
import com.github.luben.zstd.ZstdOutputStream
|
||||
@@ -199,9 +200,13 @@ object ComponentsManager {
|
||||
COMPONENTS.mapNotNull { comp -> profile.optString(comp).takeIf(String::isNotEmpty)?.let { comp to it } }.toMap()
|
||||
}
|
||||
|
||||
fun setGameComponent(context: Context, scope: String, comp: String, file: String?) = synchronized(lock) {
|
||||
fun setGameComponent(context: Context, scope: String, comp: String, file: String?) =
|
||||
setGameComponent(context, scope, comp, file) { runtimeManaged(context, it) }
|
||||
|
||||
internal fun setGameComponent(context: Context, scope: String, comp: String, file: String?, managed: (String) -> Boolean) = synchronized(lock) {
|
||||
require(scope.matches(Regex("[1-9][0-9]{0,9}")) && scope.toLong() <= 4294967295L)
|
||||
require(comp in COMPONENTS)
|
||||
if (file != null) requireSelectable(context, comp, managed)
|
||||
val state = loadState(context)
|
||||
val profiles = state.sub("profiles")
|
||||
val profile = profiles.optJSONObject(scope) ?: JSONObject().also { profiles.put(scope, it) }
|
||||
@@ -638,11 +643,21 @@ object ComponentsManager {
|
||||
return PreparedLaunchState(lines.toString(), keep)
|
||||
}
|
||||
|
||||
internal fun runtimeManaged(comp: String, systemPowerVr: Boolean): Boolean = comp == "dxvk" && systemPowerVr
|
||||
|
||||
private fun runtimeManaged(context: Context, comp: String): Boolean =
|
||||
runtimeManaged(comp, SystemVulkanDriver.usesDefault(SessionPrefs.linuxDriver(context)))
|
||||
|
||||
private fun requireSelectable(context: Context, comp: String, managed: (String) -> Boolean = { runtimeManaged(context, it) }) {
|
||||
check(!managed(comp)) { context.getString(R.string.cmgr_powervr_dxvk_managed) }
|
||||
}
|
||||
|
||||
|
||||
fun swap(context: Context, protonId: String, file: String, auto: Boolean = false): String = synchronized(lock) {
|
||||
val p = proton(context, protonId)
|
||||
val wcp = File(packagesDir(context), safeName(file))
|
||||
val info = packageInfo(context, wcp)
|
||||
requireSelectable(context, info.comp)
|
||||
val state = loadState(context)
|
||||
if (inUse(context, p)) {
|
||||
state.sub("queued").sub(p.id).put(info.comp, JSONObject().put("kind", "package").put("file", wcp.name).put("label", info.version)
|
||||
@@ -668,6 +683,7 @@ object ComponentsManager {
|
||||
}
|
||||
|
||||
fun restore(context: Context, protonId: String, comp: String, protonVersion: String): String = synchronized(lock) {
|
||||
requireSelectable(context, comp)
|
||||
val p = proton(context, protonId)
|
||||
val wcp = File(originalsDir(context), "$protonId/${safeName(protonVersion)}/$comp.wcp")
|
||||
check(wcp.isFile) { context.getString(R.string.cmgr_original_missing) }
|
||||
@@ -693,7 +709,10 @@ object ComponentsManager {
|
||||
.put("protonVersion", p.version).put("files", JSONObject(fingerprint(p.dir, comp) as Map<*, *>)).put("at", System.currentTimeMillis() / 1000))
|
||||
}
|
||||
|
||||
fun applyQueued(context: Context): List<String> = synchronized(lock) {
|
||||
/** Runs the swaps that waited for a game to close. Cheap when nothing is queued. */
|
||||
fun applyQueued(context: Context): List<String> = applyQueued(context) { runtimeManaged(context, it) }
|
||||
|
||||
internal fun applyQueued(context: Context, managed: (String) -> Boolean): List<String> = synchronized(lock) {
|
||||
val state = loadState(context)
|
||||
val queued = state.sub("queued")
|
||||
if (queued.length() == 0) return emptyList()
|
||||
@@ -705,6 +724,7 @@ object ComponentsManager {
|
||||
val comps = queued.optJSONObject(pid) ?: continue
|
||||
for (comp in comps.keys().asSequence().toList()) {
|
||||
val q = comps.optJSONObject(comp) ?: continue
|
||||
if (managed(comp)) continue
|
||||
runCatching {
|
||||
if (q.optString("kind") == "original") {
|
||||
val v = q.optString("protonVersion")
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.system.Os
|
||||
import com.droiddeck.launcher.core.DeviceSupport
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.gpu.DriverBundle
|
||||
import com.droiddeck.launcher.gpu.DriverPairs
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.TurnipReleases
|
||||
import com.droiddeck.launcher.gpu.VulkanInfo
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* graphics-report.txt in each session's folder: which graphics path the session drew with and why,
|
||||
* the GPU and its device nodes, the environment the guest got, and at the end what the compositor
|
||||
* said about its driver. Written for a non-Adreno test report, where a black screen otherwise comes
|
||||
* with nothing to read; the Share logs zip takes the whole folder, so it travels with the logs.
|
||||
* Read-only: nothing here opens a GPU node or changes a setting.
|
||||
*/
|
||||
object GraphicsReport {
|
||||
const val FILE = "graphics-report.txt"
|
||||
const val GAMES_DIR = "games"
|
||||
|
||||
/** One file in games/ past this is cut to its first part at the session's end. */
|
||||
private const val GAME_FILE_MAX = 20L * 1024 * 1024
|
||||
/** All of games/ past this loses its oldest launches. */
|
||||
private const val GAMES_TOTAL_MAX = 120L * 1024 * 1024
|
||||
|
||||
/** The guest keys the report repeats: everything that decides which Vulkan driver loads. */
|
||||
private val ENV_PREFIXES = listOf(
|
||||
"VK_", "BL_VK", "BL_DRM", "BL_VKBRIDGE", "VKBRIDGE", "MESA_", "GALLIUM_", "ZINK", "TU_", "PANVK",
|
||||
"DROIDDECK_POWERVR", "DROIDDECK_HYBRIS", "BL_GAME_LOGS", "LIBGL_", "GAMESCOPE_DISABLE", "XWAYLAND",
|
||||
)
|
||||
|
||||
/** Per-game graphics logs and the guest's Vulkan summary: on for a non-Adreno GPU, or by Setup's switch. */
|
||||
fun detailed(context: Context): Boolean = !DeviceSupport.adreno() || SessionPrefs.detailedGraphicsLogs(context)
|
||||
|
||||
/** For finish(), which runs from the session's end without a context of its own. */
|
||||
@Volatile private var appContext: Context? = null
|
||||
|
||||
/** Writes the start of the report; never throws. */
|
||||
fun begin(context: Context, dir: File, guest: List<String>) {
|
||||
appContext = context.applicationContext
|
||||
runCatching { File(dir, FILE).writeText(start(context, guest)) }
|
||||
.onFailure { android.util.Log.w("GraphicsReport", "could not write $FILE", it) }
|
||||
}
|
||||
|
||||
private fun start(context: Context, guest: List<String>): String = buildString {
|
||||
val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
||||
appendLine("== graphics report, session start $time")
|
||||
appendLine("build: ${com.droiddeck.launcher.BuildConfig.BUILD_LABEL} (source ${com.droiddeck.launcher.BuildConfig.SOURCE_COMMIT})")
|
||||
appendLine()
|
||||
val gpu = runCatching { GpuInfo.detect() }.getOrNull()
|
||||
appendLine("-- path")
|
||||
appendLine("path: " + path(context))
|
||||
appendLine("driver mode: " + SessionPrefs.gpuDriverMode(context))
|
||||
appendLine("display driver (compositor): " + SessionPrefs.androidDriver(context).ifEmpty {
|
||||
if (DeviceSupport.adreno()) "(auto: bundled Turnip)" else "(auto: the phone's own Vulkan driver, ${SystemVulkanDriver.hal() ?: "vendor HAL"})"
|
||||
})
|
||||
val display = SessionPrefs.androidDriver(context)
|
||||
appendLine("display driver chosen by: " + when {
|
||||
display.isEmpty() -> "auto (or a pair on a non-Adreno GPU: the phone's own driver)"
|
||||
DriverBundle.containing(context, display, false) != null -> "pair (its display half)"
|
||||
else -> "user (picked on its own)"
|
||||
})
|
||||
val linux = SessionPrefs.linuxDriver(context)
|
||||
appendLine("runtime driver: " + linux.ifEmpty { "(runtime default)" })
|
||||
if (linux.isNotEmpty()) {
|
||||
val dir = com.droiddeck.launcher.gpu.LinuxVulkanDriverManager(context).getDriverDir(linux)
|
||||
appendLine("runtime driver dir: ${dir.path} (" + (dir.listFiles()?.filter { it.name.endsWith(".so") }?.joinToString { it.name } ?: "missing") + ")")
|
||||
}
|
||||
val prefs = context.getSharedPreferences("graphics_report", Context.MODE_PRIVATE)
|
||||
val previous = prefs.getString("lastDrivers", null)
|
||||
val now = "display ${SessionPrefs.androidDriver(context).ifEmpty { "(auto)" }}, runtime ${linux.ifEmpty { "(default)" }}, bridge ${DeviceSupport.vkBridge(context)}, venus ${VenusComponent.wanted(context)}"
|
||||
appendLine("previous session's drivers: " + (previous ?: "(none recorded)") + if (previous != null && previous != now) " << CHANGED" else "")
|
||||
prefs.edit().putString("lastDrivers", now).apply()
|
||||
DriverBundle.active(context)?.let { b ->
|
||||
appendLine("pair: ${b.name} ${b.version} (bundle ${b.id})")
|
||||
release(context, b.id)?.let { appendLine("pair release: $it") }
|
||||
}
|
||||
if (gpu != null) {
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(context))
|
||||
appendLine("auto would pick: " + (DriverPairs.recommendedKey(gpu, pairs) ?: "(no pair: system driver or bridge)"))
|
||||
}
|
||||
appendLine("frame generation: " + runCatching { com.droiddeck.launcher.gpu.FrameGen.mode(context).toString() }.getOrDefault("?") +
|
||||
" (engine=off means it is switched off in Play settings; a driver that fails LSFG's gates logs \"device gates\" below)")
|
||||
appendLine("mali bridge setting: " + SessionPrefs.vkBridge(context) + ", in use: " + DeviceSupport.vkBridge(context))
|
||||
val standIn = runCatching { com.droiddeck.launcher.runtime.LinuxRuntime.gpuStandIn(context) }.getOrNull()
|
||||
appendLine("render node for the guest: " + (standIn?.let { node ->
|
||||
com.droiddeck.launcher.runtime.LinuxRuntime.deviceNumbers(node)?.let { n -> "/dev/dri/renderD${n.substringAfter(':')} -> $node ($n)" } ?: node
|
||||
} ?: if (SystemVulkanDriver.isPowerVr()) "the system GPU's own /dev/dri node (#570)" else "none"))
|
||||
appendLine()
|
||||
appendLine("-- gpu")
|
||||
if (gpu != null) {
|
||||
appendLine("family: ${gpu.family} (${gpu.supportText})")
|
||||
appendLine("name: ${gpu.name}" + if (gpu.model > 0) ", model ${gpu.model} (${gpu.modelSource})" else "")
|
||||
}
|
||||
appendLine("vulkan device (app): " + (VulkanInfo.cachedOrNull()?.get("device") ?: "(not probed)"))
|
||||
appendLine("system vulkan hal: " + (SystemVulkanDriver.hal() ?: "none"))
|
||||
read("/sys/class/misc/mali0/device/gpuinfo")?.let { appendLine("mali gpuinfo: $it") }
|
||||
appendLine("android: ${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT}), ${Build.MANUFACTURER} ${Build.MODEL}")
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) "${Build.SOC_MANUFACTURER} ${Build.SOC_MODEL}" else Build.HARDWARE
|
||||
appendLine("soc: $soc, board ${Build.BOARD}, hardware ${Build.HARDWARE}")
|
||||
appendLine("kernel: " + (System.getProperty("os.version") ?: "?"))
|
||||
appendLine()
|
||||
appendLine("-- gpu nodes (from the app; not opened)")
|
||||
val dri = File("/dev/dri").list()?.sorted()?.map { "/dev/dri/$it" }.orEmpty()
|
||||
for (path in listOf("/dev/kgsl-3d0", "/dev/mali0") + dri) appendLine(node(path))
|
||||
if (dri.isEmpty()) appendLine("/dev/dri: " + if (File("/dev/dri").exists()) "not listable" else "absent")
|
||||
appendLine()
|
||||
appendLine("-- guest environment (graphics)")
|
||||
guest.filter { e -> ENV_PREFIXES.any { e.startsWith(it) } }.sorted().forEach { appendLine(it) }
|
||||
appendLine()
|
||||
}
|
||||
|
||||
/** Which backend this session's Vulkan goes through, as the session decides it. */
|
||||
private fun path(context: Context): String = when {
|
||||
DeviceSupport.adreno() -> "Turnip (Adreno, KGSL)"
|
||||
VenusComponent.wanted(context) -> "Mali (Venus): Mesa Venus over vtest to the phone's own Vulkan driver, with the Mali compatibility layer"
|
||||
DeviceSupport.vkBridge(context) -> "Mali bridge (#517): the phone's own Vulkan driver through the bridge"
|
||||
DeviceSupport.openDriverActive(context) -> "open driver pair (PanVK / RADV)"
|
||||
SystemVulkanDriver.usesDefault(SessionPrefs.linuxDriver(context)) && SystemVulkanDriver.isAvailable() ->
|
||||
"system Vulkan (#570): ${SystemVulkanDriver.gpuName()} driver through libhybris"
|
||||
SessionPrefs.linuxDriver(context).isNotEmpty() -> "imported runtime driver ${SessionPrefs.linuxDriver(context)}"
|
||||
else -> "none for this GPU (no Adreno, no system HAL, no pair, bridge off)"
|
||||
}
|
||||
|
||||
/** The release tag and download a pair bundle came from, when the last release check knows it. */
|
||||
private fun release(context: Context, bundleId: String): String? {
|
||||
val bundles = TurnipReleases.cached(context)?.assets?.filter { it.bundle }.orEmpty()
|
||||
val asset = bundles.firstOrNull { it.name.removeSuffix("-Pair.zip") == bundleId }
|
||||
?: bundles.firstOrNull { bundleId.startsWith(it.name.removeSuffix("-Pair.zip") + "-") }
|
||||
?: return null
|
||||
val installed = TurnipReleases.installedBuiltOn(context, asset)
|
||||
return "${asset.tag} ${asset.url}" +
|
||||
(if (installed.isNotEmpty()) ", installed build uploaded $installed" else "") +
|
||||
(if (asset.builtOn.isNotEmpty()) ", listed build uploaded ${asset.builtOn}" else "") +
|
||||
(if (TurnipReleases.updateAvailable(context, asset)) " << NEWER BUILD AVAILABLE" else "")
|
||||
}
|
||||
|
||||
private fun node(path: String): String {
|
||||
val f = File(path)
|
||||
if (!f.exists()) return "$path: absent"
|
||||
val access = (if (f.canRead()) "r" else "-") + (if (f.canWrite()) "w" else "-")
|
||||
val rdev = runCatching { Os.stat(path).st_rdev }.getOrNull()
|
||||
val numbers = rdev?.let { " ${major(it)}:${minor(it)}" }.orEmpty()
|
||||
val label = runCatching { String(Os.getxattr(path, "security.selinux")).trimEnd('\u0000') }.getOrNull()
|
||||
return "$path: $access$numbers" + (label?.let { " $it" } ?: "")
|
||||
}
|
||||
|
||||
/** Linux's dev_t split (glibc's gnu_dev_major / gnu_dev_minor). */
|
||||
internal fun major(dev: Long): Long = ((dev ushr 8) and 0xfff) or ((dev ushr 32) and 0xfffff000L)
|
||||
internal fun minor(dev: Long): Long = (dev and 0xff) or ((dev ushr 12) and 0xffffff00L)
|
||||
|
||||
private fun read(path: String): String? = FileUtils.readString(File(path))?.trim()?.takeIf { it.isNotEmpty() }
|
||||
|
||||
/** The compositor's driver lines from the app's own log. */
|
||||
private val COMPOSITOR = Regex(
|
||||
"""vk_present|waylandcomp|vk_loader|compositor|vkCreateDevice|extension|adrenotools|TurnipDriver|""" +
|
||||
"""graphics driver|display driver|DD_PRESENT_FAULT|SystemVulkan|vkbridge|VkBridge|mali_gralloc|panvk|PanVK|radv|MESA|native buffer|first frame|present|""" +
|
||||
"""lsfg|FrameGen|device gates|AAudio|directaudio|pulse|gralloc|dma.?buf|modifier|import""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
/** The session script's driver lines: what it exported, the bridge's self-test, vulkaninfo, gamescope's Vulkan errors. */
|
||||
private val GUEST = Regex("""== vulkan driver|== vkbridge|== vulkaninfo|vulkan:|panvk|PanVK|radv|primary node|No compatible|DRM format|kbase|AAUDIO|client sink|Xwayland|\(EE\)|glamor|XWAYLAND_NO_GLAMOR""", RegexOption.IGNORE_CASE)
|
||||
|
||||
/**
|
||||
* The end of the report, from the finished folder: the compositor's driver lines out of
|
||||
* app.log, and games/ held to its size limits. Never throws.
|
||||
*/
|
||||
fun finish(dir: File) {
|
||||
runCatching {
|
||||
val report = File(dir, FILE)
|
||||
if (!report.isFile) return
|
||||
val lines = File(dir, "app.log").takeIf { it.isFile }?.useLines { seq -> seq.filter { COMPOSITOR.containsMatchIn(it) }.take(400).toList() }.orEmpty()
|
||||
val games = File(dir, GAMES_DIR).listFiles()?.filter { it.isDirectory }?.sortedBy { it.name }.orEmpty()
|
||||
val guestLines = File(dir, "session.log").takeIf { it.isFile }?.useLines { seq ->
|
||||
seq.filter { GUEST.containsMatchIn(it) }.take(200).toList()
|
||||
}.orEmpty()
|
||||
val fallbackLines = File(dir, "vkbridge-fallback").takeIf { it.isFile }?.readLines()?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||
val fellBack = fallbackLines?.firstOrNull()
|
||||
val fallbackReason = fallbackLines?.getOrNull(1).orEmpty()
|
||||
runCatching { SessionPrefs.setBridgeFellBack(appContext ?: return@runCatching, fellBack != null, fallbackReason) }
|
||||
val limits = File(dir, "limits.txt").takeIf { it.isFile }?.readText()?.trim()
|
||||
val audio = File(dir, "audio.log").takeIf { it.isFile }?.readText().orEmpty()
|
||||
report.appendText(buildString {
|
||||
limits?.let { appendLine("open files limit: $it") }
|
||||
fellBack?.let {
|
||||
if (fallbackReason == "memory") appendLine("!! MALI BRIDGE FELL BACK: memory sharing = none (no method passed the server's self-test); the session ran on $it")
|
||||
else appendLine("!! MALI BRIDGE FELL BACK: the self-test failed images or draws; the session ran on $it")
|
||||
}
|
||||
com.droiddeck.launcher.wayland.WaylandCompositor.compositorFault().takeIf { it.isNotEmpty() }?.let {
|
||||
appendLine("!! DISPLAY FAILED: $it")
|
||||
}
|
||||
com.droiddeck.launcher.wayland.WaylandCompositor.displayDriver().takeIf { it.isNotEmpty() }?.let {
|
||||
appendLine("display driver (as loaded): $it")
|
||||
}
|
||||
appContext?.let { ctx ->
|
||||
if (SessionPrefs.venus(ctx)) VenusComponent.hostVulkanTooOld(ctx)?.let {
|
||||
appendLine("!! VENUS OFF: the phone's driver reports Vulkan $it; gamescope needs 1.2 or newer, so the session did not use Venus")
|
||||
}
|
||||
}
|
||||
if (audio.contains("AAUDIO_ERROR_DISCONNECTED")) {
|
||||
appendLine("!! SOUND: Android refused the audio output (AAUDIO_ERROR_DISCONNECTED) - rebooting the phone usually fixes this")
|
||||
}
|
||||
if (guestLines.any { it.contains("vkbridge", ignoreCase = true) && it.contains("FAIL") }) {
|
||||
appendLine("!! MALI BRIDGE SELF-TEST FAILED: this GPU's driver did not pass the bridge's checks (see below)")
|
||||
}
|
||||
if (guestLines.any { it.contains("MEM_EXEC_INIT") }) {
|
||||
appendLine("note: kbase refused KBASE_IOCTL_MEM_EXEC_INIT with EPERM. That answer comes from the kbase driver")
|
||||
appendLine(" itself (proot does not filter ioctls; Android's seccomp would kill or return ENOSYS; SELinux")
|
||||
appendLine(" would return EACCES), so it is the kernel's policy for this context, not the session's.")
|
||||
}
|
||||
appendLine("-- driver lines from the session log (${guestLines.size})")
|
||||
guestLines.forEach { appendLine(it) }
|
||||
appendLine()
|
||||
appendLine("-- compositor and driver lines from app.log (${lines.size})")
|
||||
lines.forEach { appendLine(it) }
|
||||
appendLine()
|
||||
appendLine("-- game launches with detailed logs: ${games.size}")
|
||||
games.forEach { appendLine(it.name) }
|
||||
appendLine("== session end " + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date()))
|
||||
})
|
||||
capGames(File(dir, GAMES_DIR))
|
||||
}.onFailure { android.util.Log.w("GraphicsReport", "could not finish $FILE", it) }
|
||||
}
|
||||
|
||||
/** Cuts each file in [games] to [GAME_FILE_MAX] and drops the oldest launches past [GAMES_TOTAL_MAX]. */
|
||||
internal fun capGames(games: File, fileMax: Long = GAME_FILE_MAX, totalMax: Long = GAMES_TOTAL_MAX) {
|
||||
if (!games.isDirectory) return
|
||||
games.walkTopDown().filter { it.isFile && it.length() > fileMax }.forEach { f ->
|
||||
runCatching { java.io.RandomAccessFile(f, "rw").use { it.setLength(fileMax) } }
|
||||
}
|
||||
val launches = games.listFiles()?.filter { it.isDirectory }?.sortedBy { it.name }.orEmpty().toMutableList()
|
||||
fun size(d: File) = d.walkTopDown().filter { it.isFile }.sumOf { it.length() }
|
||||
var total = launches.sumOf { size(it) }
|
||||
while (total > totalMax && launches.size > 1) {
|
||||
val oldest = launches.removeAt(0)
|
||||
total -= size(oldest)
|
||||
FileUtils.delete(oldest)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,22 @@ object OrphanReaper {
|
||||
|
||||
fun release(pid: Int) { kept.remove(pid) }
|
||||
|
||||
fun hasOtherProcesses(): Boolean = hasOtherProcesses(
|
||||
File("/proc").list()?.mapNotNull { it.toIntOrNull() }, android.os.Process.myPid(),
|
||||
) { pid ->
|
||||
try {
|
||||
android.system.Os.kill(pid, 0)
|
||||
true
|
||||
} catch (error: android.system.ErrnoException) {
|
||||
error.errno != android.system.OsConstants.ESRCH && error.errno != android.system.OsConstants.EPERM
|
||||
} catch (error: SecurityException) {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hasOtherProcesses(pids: List<Int>?, ownPid: Int, signalable: (Int) -> Boolean): Boolean =
|
||||
pids == null || pids.any { it > 1 && it != ownPid && signalable(it) }
|
||||
|
||||
fun reap(reason: String, matching: (String) -> Boolean = { true }): Int {
|
||||
val me = android.os.Process.myPid()
|
||||
val uid = android.os.Process.myUid()
|
||||
|
||||
@@ -502,14 +502,20 @@ object SessionArtifacts {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the folders of sessions whose process died without its teardown. Returns true when
|
||||
* the newest of them ended because Android killed the app for memory, so the app can say so
|
||||
* (a game "crashing" on a phone with little free RAM is usually this, not a driver fault).
|
||||
*/
|
||||
@Synchronized
|
||||
fun finishAbandoned(context: Context, beforeGuestStart: Boolean = false) {
|
||||
fun finishAbandoned(context: Context, beforeGuestStart: Boolean = false): Boolean {
|
||||
val stopWhenSessionStarts = !beforeGuestStart
|
||||
val parent = LinuxRuntime.logDir(context)
|
||||
val abandoned = parent.listFiles { f ->
|
||||
SessionPaths.isSessionFolder(f) && !File(f, COMPLETE_MARKER).exists() && !inFlight(f)
|
||||
}?.sortedWith(SessionPaths.chronological) ?: return
|
||||
if (abandoned.isEmpty()) return
|
||||
}?.sortedWith(SessionPaths.chronological) ?: return false
|
||||
if (abandoned.isEmpty()) return false
|
||||
var killedForMemory = false
|
||||
val current = SessionPaths.current()
|
||||
val latestPreviousSession = SessionPaths.sessionFolders(context).lastOrNull { it != current }
|
||||
LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context))
|
||||
@@ -534,8 +540,10 @@ object SessionArtifacts {
|
||||
"audio.log, device.txt) were on disk already and are as they were left.\n" +
|
||||
(if (newest) "" else "Steam's logs are not included: a later session has overwritten them.\n") +
|
||||
"crash.log holds Android's crash buffer as of the next app start - if this\n" +
|
||||
"session died of a crash, the entry is in there unless the device rebooted.\n"
|
||||
"session died of a crash, the entry is in there unless the device rebooted.\n" +
|
||||
(if (newest) exitReasons(context) else "")
|
||||
)
|
||||
if (newest && lastExitForMemory(context)) killedForMemory = true
|
||||
}
|
||||
if (recoveryPreviouslyAttempted && !runtimeCaptured.exists() &&
|
||||
(runtimeExpired.exists() || !beforeGuestStart) &&
|
||||
@@ -593,7 +601,7 @@ object SessionArtifacts {
|
||||
break
|
||||
}
|
||||
SessionEvents.record("session.artifacts_recovered", mapOf("newest" to newest), dir)
|
||||
if (beforeGuestStart) return
|
||||
if (beforeGuestStart) break
|
||||
val scrubbed = scrubTree(record) { stopWhenSessionStarts && SessionState.running }
|
||||
val marked = markScrubbed(scrubbed)
|
||||
if (scrubbed.deferred) break
|
||||
@@ -604,6 +612,54 @@ object SessionArtifacts {
|
||||
Log.w(TAG, "could not finish $dir", e)
|
||||
}
|
||||
}
|
||||
// Whoever finishes the folder first (the agent provider starts before the activity) leaves
|
||||
// the notice for the launcher to show.
|
||||
if (killedForMemory) SessionPrefs.setMemoryKillNotice(context, true)
|
||||
return killedForMemory
|
||||
}
|
||||
|
||||
/** Android's newest record for this app's main process says it was killed for memory. */
|
||||
private fun lastExitForMemory(context: Context): Boolean {
|
||||
if (android.os.Build.VERSION.SDK_INT < 30) return false
|
||||
return runCatching {
|
||||
val am = context.getSystemService(android.app.ActivityManager::class.java) ?: return false
|
||||
am.getHistoricalProcessExitReasons(context.packageName, 0, 3)
|
||||
.firstOrNull { it.processName == context.packageName }
|
||||
?.reason == android.app.ApplicationExitInfo.REASON_LOW_MEMORY
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
private fun exitReasons(context: Context): String {
|
||||
if (android.os.Build.VERSION.SDK_INT < 30) return ""
|
||||
return runCatching {
|
||||
val am = context.getSystemService(android.app.ActivityManager::class.java) ?: return ""
|
||||
val reasons = am.getHistoricalProcessExitReasons(context.packageName, 0, 3)
|
||||
if (reasons.isEmpty()) return ""
|
||||
val fmt = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US)
|
||||
buildString {
|
||||
append("\nAndroid's record of how this app's recent processes ended (newest first):\n")
|
||||
reasons.forEach { r ->
|
||||
append(" ${fmt.format(java.util.Date(r.timestamp))} ${r.processName} pid ${r.pid}: reason ${reasonName(r.reason)}, " +
|
||||
"status ${r.status}, importance ${r.importance}, pss ${r.pss / 1024} MB, rss ${r.rss / 1024} MB" +
|
||||
(r.description?.let { " - $it" } ?: "") + "\n")
|
||||
}
|
||||
}
|
||||
}.getOrDefault("")
|
||||
}
|
||||
|
||||
private fun reasonName(reason: Int): String = when (reason) {
|
||||
android.app.ApplicationExitInfo.REASON_LOW_MEMORY -> "LOW_MEMORY (killed for memory)"
|
||||
android.app.ApplicationExitInfo.REASON_SIGNALED -> "SIGNALED"
|
||||
android.app.ApplicationExitInfo.REASON_CRASH -> "CRASH"
|
||||
android.app.ApplicationExitInfo.REASON_CRASH_NATIVE -> "CRASH_NATIVE"
|
||||
android.app.ApplicationExitInfo.REASON_ANR -> "ANR"
|
||||
android.app.ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> "EXCESSIVE_RESOURCE_USAGE"
|
||||
android.app.ApplicationExitInfo.REASON_USER_REQUESTED -> "USER_REQUESTED"
|
||||
android.app.ApplicationExitInfo.REASON_USER_STOPPED -> "USER_STOPPED"
|
||||
android.app.ApplicationExitInfo.REASON_EXIT_SELF -> "EXIT_SELF"
|
||||
android.app.ApplicationExitInfo.REASON_DEPENDENCY_DIED -> "DEPENDENCY_DIED"
|
||||
android.app.ApplicationExitInfo.REASON_OTHER -> "OTHER"
|
||||
else -> reason.toString()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
||||
@@ -45,6 +45,7 @@ object SessionFiles {
|
||||
"usr/local/bin/droiddeck-msi-install" to "usr/local/bin/droiddeck-msi-install",
|
||||
"usr/local/bin/droiddeck-esync" to "usr/local/bin/droiddeck-esync",
|
||||
"usr/local/bin/droiddeck-steam-compat" to "usr/local/bin/droiddeck-steam-compat",
|
||||
"usr/local/bin/droiddeck-steam-taskset" to "usr/local/libexec/droiddeck-steam/taskset",
|
||||
"usr/local/bin/droiddeck-fex" to "usr/local/bin/droiddeck-fex",
|
||||
"libblsession.so" to "usr/local/lib/libblsession.so",
|
||||
"libfakeinput.so" to "usr/local/lib/libfakeinput.so",
|
||||
@@ -143,6 +144,8 @@ object SessionFiles {
|
||||
"usr/local/lib/mangoapp/libtraceevent.so.1",
|
||||
"usr/local/lib/mangoapp/libtracefs.so.1",
|
||||
).map { it to it }
|
||||
val xclipse = listOf("libVkLayer_droiddeck_xclipse.so", "VkLayer_droiddeck_xclipse.json")
|
||||
.map { "usr/local/lib/droiddeck-xclipse/$it" }
|
||||
val msitools = listOf("msiinfo", "cabextract", "7z", "7z.so", "libmsi-1.0.so.0", "libgsf-1.so.114", "libgcab-1.0.so.0")
|
||||
.map { "usr/local/lib/droiddeck-msitools/$it" } +
|
||||
listOf("NOTICE", "GPL-2", "GPL-3", "LGPL-2.1", "7zip-License", "7zip-unRarLicense").map { "usr/local/share/licenses/droiddeck-msitools/$it" }
|
||||
@@ -172,8 +175,16 @@ object SessionFiles {
|
||||
val fexPreloads = listOf("x86_64", "i386").flatMap { arch ->
|
||||
listOf("libblsession.so", "libfakeinput.so", "libfaultreport.so").map { "$arch/$it" to "usr/local/lib/droiddeck-fex/$arch/$it" }
|
||||
} + listOf("libvulkan-thunk.so", "libwebkitinput.so").map { "x86_64/$it" to "usr/local/lib/droiddeck-fex/x86_64/$it" }
|
||||
// The Mali compatibility layer (tools/venus/layer, David Cimo): an implicit Vulkan layer that
|
||||
// fills in the features DXVK requires and Mali lacks. Idle outside Wine and on GPUs that have them.
|
||||
// Never staged on an Adreno: an implicit layer is loaded into every Vulkan program there.
|
||||
val maliCompat = if (com.droiddeck.launcher.core.DeviceSupport.adreno()) emptyList() else listOf(
|
||||
"usr/local/lib/droiddeck-mali/libVkLayer_droiddeck_mali_compat.so",
|
||||
"usr/local/share/vulkan/implicit_layer.d/VkLayer_droiddeck_mali_compat.json",
|
||||
).map { it to it }
|
||||
val optional = (arrayOf(
|
||||
"usr/local/bin/gamescope" to "usr/local/bin/gamescope",
|
||||
"usr/local/bin/droiddeck-glprobe" to "usr/local/bin/droiddeck-glprobe",
|
||||
"usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so" to "usr/lib/libVkLayer_FROG_gamescope_wsi_aarch64.so",
|
||||
"usr/local/lib/vkbridge/libvulkan_droidbridge.so" to "usr/local/lib/vkbridge/libvulkan_droidbridge.so",
|
||||
"usr/local/share/vkbridge/droidbridge_icd.json" to "usr/local/share/vkbridge/droidbridge_icd.json",
|
||||
@@ -182,7 +193,7 @@ object SessionFiles {
|
||||
"usr/local/lib/droiddeck-pipewire.tzst" to "usr/local/lib/droiddeck-pipewire.tzst",
|
||||
"usr/local/share/licenses/uruntime/LICENSE" to "usr/local/share/licenses/uruntime/LICENSE",
|
||||
"usr/local/lib/droiddeck-dirt/ws2_32.dll" to "usr/local/lib/droiddeck-dirt/ws2_32.dll",
|
||||
) + mangoapp + msitools.map { it to it } + vr + gpuLoad + fexPreloads).filter { (asset, _) ->
|
||||
) + mangoapp + xclipse.map { it to it } + maliCompat + msitools.map { it to it } + vr + gpuLoad + fexPreloads).filter { (asset, _) ->
|
||||
val dir = asset.substringBeforeLast('/')
|
||||
runCatching { context.assets.list("linuxfs/$dir")?.contains(asset.substringAfterLast('/')) == true }.getOrDefault(false)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.droiddeck.launcher.core.GameEnvironment
|
||||
import com.droiddeck.launcher.core.TextureFiltering
|
||||
import com.droiddeck.launcher.gpu.Cardboard
|
||||
import com.droiddeck.launcher.gpu.ScreenEffects
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
@@ -504,6 +506,17 @@ object SessionPrefs {
|
||||
p.edit().putString("gpuDriverMode", if (chosen) GPU_DRIVERS_MANUAL else GPU_DRIVERS_AUTO).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ensurePowerVrAutoDrivers(context: Context) {
|
||||
if (com.droiddeck.launcher.core.DeviceSupport.openDriverActive(context)) return
|
||||
ensurePowerVrAutoDrivers(context, SystemVulkanDriver.isPowerVr())
|
||||
}
|
||||
|
||||
internal fun ensurePowerVrAutoDrivers(context: Context, powerVr: Boolean) {
|
||||
if (!powerVr || gpuDriverMode(context) != GPU_DRIVERS_AUTO) return
|
||||
prefs(context).edit().putString("linuxDriver", "").putString("androidDriver", TurnipDriver.AUTO).apply()
|
||||
}
|
||||
|
||||
fun gpuAutoInstalled(context: Context): Set<String> =
|
||||
prefs(context).getStringSet("gpuAutoInstalled", emptySet()).orEmpty()
|
||||
|
||||
@@ -775,8 +788,62 @@ object SessionPrefs {
|
||||
@JvmStatic
|
||||
fun vkBridge(context: Context): Boolean = prefs(context).getBoolean("vkBridge", com.droiddeck.launcher.core.DeviceSupport.mali())
|
||||
|
||||
fun venus(context: Context): Boolean = prefs(context).getBoolean("venus", false)
|
||||
|
||||
fun hostVulkanApi(context: Context): String = prefs(context).getString("hostVulkanApi", "").orEmpty()
|
||||
|
||||
fun setHostVulkanApi(context: Context, api: String) {
|
||||
if (api != hostVulkanApi(context)) prefs(context).edit().putString("hostVulkanApi", api).apply()
|
||||
}
|
||||
|
||||
fun bridgeFellBack(context: Context): Boolean = prefs(context).getBoolean("bridgeFellBack", false)
|
||||
|
||||
fun bridgeFellBackForMemory(context: Context): Boolean = prefs(context).getString("bridgeFallbackReason", "") == "memory"
|
||||
|
||||
fun setBridgeFellBack(context: Context, on: Boolean, reason: String = "") {
|
||||
prefs(context).edit().putBoolean("bridgeFellBack", on).putString("bridgeFallbackReason", if (on) reason else "").apply()
|
||||
}
|
||||
|
||||
fun steamGpuCompositing(context: Context): Boolean {
|
||||
if (!steamGpuCompositingAllowed()) {
|
||||
if (prefs(context).getBoolean("steamGpuCompositing", false))
|
||||
prefs(context).edit().putBoolean("steamGpuCompositing", false).apply()
|
||||
return false
|
||||
}
|
||||
return prefs(context).getBoolean("steamGpuCompositing", false)
|
||||
}
|
||||
|
||||
fun steamGpuCompositingAllowed(): Boolean =
|
||||
!com.droiddeck.launcher.core.DeviceSupport.adreno() && !com.droiddeck.launcher.core.DeviceSupport.mali()
|
||||
|
||||
fun setSteamGpuCompositing(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("steamGpuCompositing", on && steamGpuCompositingAllowed()).apply()
|
||||
}
|
||||
|
||||
fun takeMemoryKillNotice(context: Context): Boolean {
|
||||
val on = prefs(context).getBoolean("memoryKillNotice", false)
|
||||
if (on) prefs(context).edit().putBoolean("memoryKillNotice", false).apply()
|
||||
return on
|
||||
}
|
||||
|
||||
fun setMemoryKillNotice(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("memoryKillNotice", on).apply()
|
||||
}
|
||||
|
||||
fun maliFallbackByAuto(context: Context): String = prefs(context).getString("maliFallbackByAuto", "").orEmpty()
|
||||
|
||||
fun maliFallbackKnown(context: Context): Boolean = prefs(context).contains("maliFallbackByAuto")
|
||||
|
||||
fun setMaliFallbackByAuto(context: Context, route: String) {
|
||||
prefs(context).edit().putString("maliFallbackByAuto", route).apply()
|
||||
}
|
||||
|
||||
fun setVenus(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("venus", on).apply()
|
||||
}
|
||||
|
||||
fun setVkBridge(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("vkBridge", on).apply()
|
||||
prefs(context).edit().putBoolean("vkBridge", on).putBoolean("bridgeFellBack", false).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -786,6 +853,7 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("bridgeProtonDxvk", on).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun vkBridgeSafe(context: Context): Boolean = prefs(context).getBoolean("vkBridgeSafe", true)
|
||||
|
||||
fun setVkBridgeSafe(context: Context, on: Boolean) {
|
||||
@@ -886,6 +954,13 @@ object SessionPrefs {
|
||||
}
|
||||
}
|
||||
|
||||
fun detailedGraphicsLogs(context: Context): Boolean =
|
||||
prefs(context).getBoolean("detailedGraphicsLogs", false)
|
||||
|
||||
fun setDetailedGraphicsLogs(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("detailedGraphicsLogs", on).apply()
|
||||
}
|
||||
|
||||
internal fun gameDebugLogsEffective(
|
||||
context: Context,
|
||||
logs: Boolean = logsEnabled(context),
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.droiddeck.launcher.session
|
||||
import com.droiddeck.launcher.core.AddonPack
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -405,6 +406,7 @@ class SessionService : Service() {
|
||||
private fun collectSessionArtifacts(dir: File, steamArch: String?) {
|
||||
SessionLogCapture.stopFor(dir)
|
||||
try {
|
||||
GraphicsReport.finish(dir)
|
||||
SessionArtifacts.collect(this, dir, "session stopped", steamArch)
|
||||
} finally {
|
||||
SessionPaths.release(this, dir)
|
||||
@@ -424,6 +426,21 @@ class SessionService : Service() {
|
||||
return lines
|
||||
}
|
||||
|
||||
/** Whether a kernel release ("6.1.157-android14-11") is older than [major].[minor]; false if unreadable. */
|
||||
internal fun kernelBefore(release: String, major: Int, minor: Int): Boolean {
|
||||
val m = Regex("""^(\d+)\.(\d+)""").find(release) ?: return false
|
||||
val (a, b) = m.destructured
|
||||
return a.toInt() < major || (a.toInt() == major && b.toInt() < minor)
|
||||
}
|
||||
|
||||
/** Whether the newest finished session's log shows Xwayland itself crashing. */
|
||||
private fun lastXwaylandCrashed(): Boolean = runCatching {
|
||||
val last = SessionPaths.sessionFolders(this).filter { it != SessionPaths.current() }.lastOrNull() ?: return false
|
||||
val log = File(last, "session.log").takeIf { it.isFile } ?: return false
|
||||
log.useLines { lines -> lines.any { it.contains("(EE) Segmentation fault") } } &&
|
||||
log.useLines { lines -> lines.any { it.contains("X11 I/O error") || it.contains("Xwayland", ignoreCase = true) } }
|
||||
}.getOrDefault(false)
|
||||
|
||||
private fun tuDebugOverride(): String? = File(Environment.getExternalStorageDirectory(), TU_DEBUG_SWITCH)
|
||||
.takeIf { it.isFile }?.let { FileUtils.readString(it)?.trim() }?.takeIf { it.isNotEmpty() }
|
||||
|
||||
@@ -435,6 +452,13 @@ class SessionService : Service() {
|
||||
|
||||
|
||||
private fun runSession(gen: Int) {
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(this)
|
||||
if (!com.droiddeck.launcher.core.DeviceSupport.adreno()) {
|
||||
com.droiddeck.launcher.gpu.VulkanInfo.query()["api"]?.let { SessionPrefs.setHostVulkanApi(this, it) }
|
||||
if (SessionPrefs.venus(this)) VenusComponent.hostVulkanTooOld(this)?.let {
|
||||
Log.w(TAG, "venus: off for this session - the phone's driver reports Vulkan $it and gamescope needs 1.2 or newer; the session uses the PanVK pair or the bridge")
|
||||
}
|
||||
}
|
||||
SessionTerminal.clear()
|
||||
com.droiddeck.launcher.gpu.TpUbwcHint.forThisDevice(this)
|
||||
try {
|
||||
@@ -476,7 +500,17 @@ class SessionService : Service() {
|
||||
val agentEnvironment = runCatching { com.droiddeck.launcher.agent.AgentEnv.beginSession(this) }
|
||||
.onFailure { Log.w(TAG, "agent environment skipped", it) }
|
||||
.getOrDefault(emptyList())
|
||||
SessionFiles.stage(this, root, startsSession = true)
|
||||
try {
|
||||
if (SystemVulkanDriver.usesDefault(SessionPrefs.linuxDriver(this))) {
|
||||
SystemVulkanDriver.prepare(this)
|
||||
pruneSystemVulkanRuntime()
|
||||
}
|
||||
SessionFiles.stage(this, root, startsSession = true)
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "Could not prepare the session graphics runtime", error)
|
||||
stopSession(-1, gen)
|
||||
return
|
||||
}
|
||||
SessionArtifacts.awaitCollections()
|
||||
SessionArtifacts.finishAbandoned(this, beforeGuestStart = true)
|
||||
VacDiagnostics.arm(this)
|
||||
@@ -512,18 +546,28 @@ class SessionService : Service() {
|
||||
.onSuccess { if (it != null) Log.i(TAG, it) }
|
||||
.onFailure { Log.w(TAG, "forced components", it) }
|
||||
}
|
||||
addClientEnvironment(guest, steamHere, agentEnvironment)
|
||||
try {
|
||||
addClientEnvironment(guest, steamHere, agentEnvironment)
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Could not resolve the selected Linux Vulkan driver", e)
|
||||
failSession(gen, "VULKAN_DRIVER_UNAVAILABLE", e.message ?: "Selected Linux Vulkan driver is unavailable", -1)
|
||||
stopSession(-1, gen)
|
||||
return
|
||||
}
|
||||
if (steamHere) guest.add("BL_EPIC_APP_ID=" + com.droiddeck.launcher.stores.epic.EpicClient.shortcutAppId)
|
||||
val fastPathAt = guest.size
|
||||
|
||||
val pulse = startAudio(guest, sessionDir, directAudioPack)
|
||||
startVkBridge(guest, sessionDir)
|
||||
if (VenusComponent.wanted(this)) {
|
||||
components.add(VenusComponent(File(sessionDir, "venus.log")).also { it.attach(this) })
|
||||
}
|
||||
if (steamHere) {
|
||||
val encoder = VideoEncodeBridge(File(filesDir, "venc/venc.sock"))
|
||||
encoder.attach(this)
|
||||
components.add(encoder)
|
||||
guest.add("BL_VENC_SOCKET=" + encoder.socket.absolutePath)
|
||||
}
|
||||
startVkBridge(guest, sessionDir)
|
||||
com.droiddeck.launcher.gpu.ClockLogComponent(File(sessionDir, "clocks.log")).let {
|
||||
it.attach(this)
|
||||
components.add(it)
|
||||
@@ -574,6 +618,10 @@ class SessionService : Service() {
|
||||
guest.add("BL_LAUNCH_DIR=" + sessionRoot.path)
|
||||
GameStorage.effective(this)?.let { guest.add("BL_LIBRARY_LABEL=" + it.label.replace('"', ' ')) }
|
||||
if (SessionState.mode == MODE_STEAM && SessionState.steamUi == "desktop") guest.add("BL_STEAM_UI=desktop")
|
||||
// Detailed graphics logs: per-game logs and the guest's Vulkan summary (a GPU that is not
|
||||
// an Adreno, or Setup's switch), and graphics-report.txt for every session.
|
||||
if (GraphicsReport.detailed(this)) guest.add("BL_GAME_LOGS=1")
|
||||
GraphicsReport.begin(this, sessionDir, guest)
|
||||
val shellGuest = ArrayList(guest).apply {
|
||||
add("SHELL=/bin/bash")
|
||||
add("TERM=xterm-256color")
|
||||
@@ -673,7 +721,14 @@ class SessionService : Service() {
|
||||
if (gen == sessionGen) components.clear()
|
||||
return
|
||||
}
|
||||
components.forEach { it.start() }
|
||||
try {
|
||||
components.forEach { it.start() }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Could not start the session components", e)
|
||||
failSession(gen, "COMPONENT_START_FAILED", e.message ?: "Session component could not be started", -1)
|
||||
stopSession(-1, gen)
|
||||
return
|
||||
}
|
||||
|
||||
val line = command.joinToString(" ") {
|
||||
it.replace("\\", "\\\\").replace(" ", "\\ ")
|
||||
@@ -816,7 +871,12 @@ class SessionService : Service() {
|
||||
guest.add("MESA_LOADER_DRIVER_OVERRIDE=zink")
|
||||
guest.add("GALLIUM_DRIVER=zink")
|
||||
guest.add("LIBGL_KOPPER_DRI2=true")
|
||||
LinuxRuntime.vulkanIcd(this)?.let { guest.add("VK_ICD_FILENAMES=" + it.path) }
|
||||
val venus = VenusComponent.wanted(this)
|
||||
if (venus) {
|
||||
VenusComponent.guestEnv(this).forEach { guest.add(it) }
|
||||
} else {
|
||||
LinuxRuntime.vulkanIcd(this)?.let { guest.add("VK_ICD_FILENAMES=" + it.path) }
|
||||
}
|
||||
val linuxDriverId = SessionPrefs.linuxDriver(this)
|
||||
SessionState.runtimeDriver = linuxDriverId
|
||||
com.droiddeck.launcher.gpu.AutoDriverTrial.sessionStarting(
|
||||
@@ -825,11 +885,39 @@ class SessionService : Service() {
|
||||
?: SessionPrefs.androidDriver(this),
|
||||
linuxDriverId,
|
||||
)
|
||||
if (!com.droiddeck.launcher.core.DeviceSupport.vkBridge(this)) LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId)
|
||||
val bridge = com.droiddeck.launcher.core.DeviceSupport.vkBridge(this)
|
||||
if (!bridge && !venus && SystemVulkanDriver.usesDefault(linuxDriverId)) {
|
||||
SystemVulkanDriver.addEnvironment(this, guest)
|
||||
}
|
||||
(if (bridge || venus) null else LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId))
|
||||
?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) }
|
||||
if (!com.droiddeck.launcher.core.DeviceSupport.adreno() && lastXwaylandCrashed()) {
|
||||
guest.add("XWAYLAND_NO_GLAMOR=1")
|
||||
Log.w(TAG, "xwayland: the last session's Xwayland crashed; glamor off for this one (XWAYLAND_NO_GLAMOR=1)")
|
||||
}
|
||||
if (!com.droiddeck.launcher.core.DeviceSupport.adreno()) guest.add("BL_RAISE_NOFILE=1")
|
||||
if (!com.droiddeck.launcher.core.DeviceSupport.adreno()) {
|
||||
val kernel = System.getProperty("os.version").orEmpty()
|
||||
val old = kernelBefore(kernel, 6, 6)
|
||||
val maliNode = LinuxRuntime.gpuStandIn(this) == LinuxRuntime.MALI_DEVICE
|
||||
if (old || maliNode) {
|
||||
guest.add("GAMESCOPE_DISABLE_EXPLICIT_SYNC=1")
|
||||
Log.i(TAG, "gamescope: explicit sync off (" + (if (old) "kernel $kernel has no SYNCOBJ_EVENTFD" else "Mali stand-in node has no syncobjs") + ")")
|
||||
}
|
||||
}
|
||||
if (!bridge && com.droiddeck.launcher.core.DeviceSupport.openDriverActive(this)) {
|
||||
if (SessionPrefs.steamGpuCompositing(this)) {
|
||||
Log.i(TAG, "steam: GPU web compositing (Setup's \"Steam interface: GPU drawing\")")
|
||||
} else {
|
||||
guest.add("BL_STEAM_SOFT_COMPOSITING=1")
|
||||
Log.i(TAG, "steam: software web compositing for the open driver pair (GPU raster kept)")
|
||||
}
|
||||
guest.add("BL_OPEN_DRIVER=1")
|
||||
LinuxRuntime.gpuStandIn(this)?.takeIf { it == LinuxRuntime.MALI_DEVICE }?.let { guest.add("BL_DRM_STANDIN=$it") }
|
||||
}
|
||||
val tuOverride = tuDebugOverride()
|
||||
val tuDriver = sysmemDriver(linuxDriverId)
|
||||
(tuOverride ?: "sysmem".takeIf { tuDriver || SessionPrefs.tuSysmem(this) })?.let { guest.add("TU_DEBUG=$it") }
|
||||
if (!venus && com.droiddeck.launcher.core.DeviceSupport.adreno()) (tuOverride ?: "sysmem".takeIf { tuDriver || SessionPrefs.tuSysmem(this) })?.let { guest.add("TU_DEBUG=$it") }
|
||||
if (SessionPrefs.zinkLazy(this)) {
|
||||
guest.add("ZINK_DESCRIPTORS=lazy")
|
||||
guest.add("ZINK_DEBUG=compact")
|
||||
@@ -878,9 +966,6 @@ class SessionService : Service() {
|
||||
.takeIf { it.isNotEmpty() }?.let { guest.add("BL_GAME_CPUS=$it") }
|
||||
}
|
||||
|
||||
private fun noSteamGame(): Boolean =
|
||||
SessionState.mode == MODE_RUN && SessionState.program in setOf(NoSteamLaunch.PROTON_RUN, NoSteamLaunch.EPIC, NoSteamLaunch.BATTLENET)
|
||||
|
||||
private fun startVkBridge(guest: MutableList<String>, sessionDir: File) {
|
||||
if (!com.droiddeck.launcher.core.DeviceSupport.vkBridge(this)) return
|
||||
val socket = File(filesDir, "vkbridge/server.sock")
|
||||
@@ -888,6 +973,10 @@ class SessionService : Service() {
|
||||
bridge.attach(this)
|
||||
components.add(bridge)
|
||||
guest.add("BL_VKBRIDGE=1")
|
||||
if (SessionPrefs.steamGpuCompositing(this)) guest.add("BL_STEAM_GPU_COMPOSITING=1")
|
||||
SessionPrefs.linuxDriver(this).takeIf { it.isNotEmpty() }
|
||||
?.let { runCatching { LinuxVulkanDriver.resolveIcdPath(this, it) }.getOrNull() }
|
||||
?.let { guest.add("BL_VKBRIDGE_FALLBACK_ICD=$it") }
|
||||
guest.add("VKBRIDGE_SOCKET=" + socket.path)
|
||||
guest.add("VKBRIDGE_CLIENT_LOG=" + File(sessionDir, "vkbridge-client.log").path)
|
||||
guest.add("VKBRIDGE_MODE=" + SessionPrefs.vkBridgeMode(this))
|
||||
@@ -897,6 +986,8 @@ class SessionService : Service() {
|
||||
}
|
||||
Log.i(TAG, "vkbridge: on (${com.droiddeck.launcher.core.DeviceSupport.gpuName(this)}, ${SessionPrefs.vkBridgeMode(this)} mode), socket $socket")
|
||||
}
|
||||
private fun noSteamGame(): Boolean =
|
||||
SessionState.mode == MODE_RUN && SessionState.program in setOf(NoSteamLaunch.PROTON_RUN, NoSteamLaunch.EPIC, NoSteamLaunch.BATTLENET)
|
||||
|
||||
private fun startAudio(guest: MutableList<String>, sessionDir: File, directAudioPack: Boolean): PulseAudioComponent {
|
||||
val wantsDirectAudio = directAudioPack &&
|
||||
@@ -1834,6 +1925,12 @@ class SessionService : Service() {
|
||||
}.onFailure { Log.w(TAG, "could not request Steam wake", it) }
|
||||
}
|
||||
|
||||
/** Background-only: preserve old runtimes until all earlier guest processes are gone. */
|
||||
private fun pruneSystemVulkanRuntime() {
|
||||
runCatching { SystemVulkanDriver.pruneUnused(this, !OrphanReaper.hasOtherProcesses()) }
|
||||
.onFailure { Log.w(TAG, "could not prune unused system Vulkan runtimes", it) }
|
||||
}
|
||||
|
||||
private fun finishSessionStop(status: Int, stoppedGen: Int, afterStopped: () -> Unit = {}) {
|
||||
mainHandler.post {
|
||||
try {
|
||||
@@ -1973,10 +2070,12 @@ class SessionService : Service() {
|
||||
auxiliary.forEach { (pid, started) -> teardown(pid, started) }
|
||||
if (prootPid > 1) teardown(prootPid)
|
||||
killGuestLeftovers()
|
||||
pruneSystemVulkanRuntime()
|
||||
finishSessionStop(status, stoppedGen, collectEnded)
|
||||
if (launcherEnded) importLauncherGames()
|
||||
}, "session-teardown").start()
|
||||
} else {
|
||||
Thread({ pruneSystemVulkanRuntime() }, "system-vulkan-prune").start()
|
||||
Thread({
|
||||
com.droiddeck.launcher.stores.gog.GogComet.end(stoppedSession)
|
||||
finishSessionStop(status, stoppedGen, collectEnded)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.DeviceSupport
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.core.HostProcess
|
||||
import com.droiddeck.launcher.core.SessionPart
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
|
||||
/**
|
||||
* Vulkan for the session on a Mali, through Venus (the "Mali (Venus)" route; by David Cimo,
|
||||
* DavidCimo/droiddeck-mali, ported to this branch).
|
||||
*
|
||||
* The guest's processes are glibc and cannot load the vendor's bionic driver. Instead they load
|
||||
* Mesa's Venus ICD (libvulkan_virtio.so), which serializes every Vulkan call over a unix socket to
|
||||
* virglrenderer's vtest server. The server runs here, on the Android side, and replays the calls
|
||||
* on the device's own Vulkan driver. Both ends of the socket are under the app's files directory,
|
||||
* which the session binds at its own path, so the same path works inside and out.
|
||||
*/
|
||||
class VenusComponent(private val logFile: File?) : SessionPart() {
|
||||
@Volatile private var pid = -1
|
||||
|
||||
override fun start() {
|
||||
val context = app()
|
||||
stageIcd(context)
|
||||
val libDir = context.applicationInfo.nativeLibraryDir
|
||||
val socket = socket(context)
|
||||
socket.delete()
|
||||
val env = arrayOf(
|
||||
"LD_LIBRARY_PATH=$libDir",
|
||||
// vtest forks a render server per client; this is where its executable is.
|
||||
"RENDER_SERVER_EXEC_PATH=$libDir/libvirgl_render_server.so",
|
||||
"TMPDIR=" + context.cacheDir.path,
|
||||
)
|
||||
val command = "$libDir/libvirgl_test_server.so --venus --no-virgl --socket-path " + socket.path
|
||||
val out = logFile?.let { runCatching { PrintWriter(java.io.FileWriter(it, true)) }.getOrNull() }
|
||||
pid = HostProcess.start(command, env, context.filesDir, { status ->
|
||||
Log.w(TAG, "vtest server exited: $status")
|
||||
out?.let { synchronized(it) { it.println("vtest server exited: $status"); it.flush() } }
|
||||
}, { line ->
|
||||
Log.i(TAG, line)
|
||||
out?.let { synchronized(it) { it.println(line); it.flush() } }
|
||||
})
|
||||
Log.i(TAG, "vtest server pid $pid on $socket")
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (pid > 1) Process.killProcess(pid)
|
||||
pid = -1
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Venus"
|
||||
private const val ASSET = "venus/libvulkan_virtio.so"
|
||||
private const val DIR = "venus"
|
||||
private const val LIB = "libvulkan_virtio.so"
|
||||
private const val ICD = "virtio_icd.json"
|
||||
|
||||
/**
|
||||
* The selection rule: Venus when Setup's "Mali (Venus)" is on, on a GPU that is neither an
|
||||
* Adreno nor one the system-driver route covers (PowerVR, Xclipse), and the apk carries it.
|
||||
* It then replaces the Mali bridge and any open driver pair for the session.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun wanted(context: Context): Boolean =
|
||||
!DeviceSupport.adreno() && !com.droiddeck.launcher.gpu.SystemVulkanDriver.isAvailable() &&
|
||||
SessionPrefs.venus(context) && available(context) && hostVulkanTooOld(context) == null
|
||||
|
||||
/** The phone driver's Vulkan version when it is below 1.2: gamescope skips such a device, so Venus cannot run. */
|
||||
@JvmStatic
|
||||
fun hostVulkanTooOld(context: Context): String? {
|
||||
val api = com.droiddeck.launcher.gpu.VulkanInfo.cachedOrNull()?.get("api") ?: SessionPrefs.hostVulkanApi(context)
|
||||
val parts = api.split('.').mapNotNull { it.trim().toIntOrNull() }
|
||||
if (parts.size < 2) return null
|
||||
return api.takeIf { parts[0] < 1 || (parts[0] == 1 && parts[1] < 2) }
|
||||
}
|
||||
|
||||
/** Whether this apk carries the Venus component (tools/venus, venus.tzst). */
|
||||
@JvmStatic
|
||||
fun available(context: Context): Boolean = runCatching {
|
||||
context.assets.list(DIR)?.contains(LIB) == true &&
|
||||
File(context.applicationInfo.nativeLibraryDir, "libvirgl_test_server.so").isFile
|
||||
}.getOrDefault(false)
|
||||
|
||||
@JvmStatic
|
||||
fun socket(context: Context): File = File(File(context.filesDir, DIR), "vtest.sock")
|
||||
|
||||
/** What the guest needs to find the ICD and the server. */
|
||||
@JvmStatic
|
||||
fun guestEnv(context: Context): List<String> {
|
||||
val dir = File(context.filesDir, DIR)
|
||||
return listOf(
|
||||
"VK_ICD_FILENAMES=" + File(dir, ICD).path,
|
||||
"VN_DEBUG=vtest",
|
||||
"VTEST_SOCKET_NAME=" + socket(context).path,
|
||||
// Xwayland has no DRI3 without a DRM node, so swapchains present through shared memory.
|
||||
"MESA_VK_WSI_DEBUG=sw",
|
||||
// The Mali compatibility layer in every process but gamescope, not only Wine: Zink
|
||||
// (Steam's GL, Xwayland's glamor) requires robustness2's nullDescriptor, which the
|
||||
// layer stands in for. The session script checks it and falls back to llvmpipe.
|
||||
"DROIDDECK_MALI_COMPAT=1",
|
||||
"BL_VENUS=1",
|
||||
// gamescope's composite layout without immutable YCbCr samplers from the start
|
||||
// (patch 0930): a Mali-G615 refused every composite pipeline with them, and testing
|
||||
// that through Venus took the render server down.
|
||||
"GAMESCOPE_NO_YCBCR_SAMPLERS=1",
|
||||
)
|
||||
}
|
||||
|
||||
/** Copies the ICD out of the APK when it changed, and writes its manifest with an absolute path. */
|
||||
private fun stageIcd(context: Context) {
|
||||
val dir = File(context.filesDir, DIR).apply { mkdirs() }
|
||||
val lib = File(dir, LIB)
|
||||
// Copied again after every app update: the asset is compressed, so its size is unknown up front.
|
||||
val installed = context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
|
||||
if (!lib.isFile || lib.lastModified() < installed) {
|
||||
val tmp = File(dir, "$LIB.tmp")
|
||||
context.assets.open(ASSET).use { input -> tmp.outputStream().use { input.copyTo(it) } }
|
||||
if (!tmp.renameTo(lib)) throw java.io.IOException("could not move the Venus ICD into place")
|
||||
Log.i(TAG, "staged $lib (${lib.length()} bytes)")
|
||||
}
|
||||
if (!FileUtils.writeString(
|
||||
File(dir, ICD),
|
||||
"""{"file_format_version":"1.0.1","ICD":{"library_path":"${lib.path}","api_version":"1.4.354"}}""",
|
||||
)) throw java.io.IOException("could not write the Venus ICD manifest")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -483,17 +483,27 @@ fun ComponentsPage(
|
||||
)
|
||||
else -> {
|
||||
val st = compState(l)
|
||||
val managedDxvk = ComponentsManager.runtimeManaged(l, gpu.systemPowerVr)
|
||||
val label = ComponentsManager.LABEL[l] ?: l
|
||||
if (st == null) InUseBar(false, stringResource(R.string.comp_no_proton), "")
|
||||
else {
|
||||
val reappliedAt = view?.reappliedAt?.takeIf { it > 0 }?.let { DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(it * 1000)) }
|
||||
InUseBar(
|
||||
queued = st.queued != null,
|
||||
sentence = (if (st.queued != null) stringResource(R.string.comp_next_queued, st.queued) else stringResource(R.string.comp_in_use, label, st.inUse))
|
||||
.let { if (reappliedAt != null) stringResource(R.string.comp_reapplied, it, reappliedAt) else it },
|
||||
note = if (view?.inUseByGame == true) stringResource(R.string.comp_waits) else stringResource(R.string.comp_applies_next_game),
|
||||
onCancel = if (st.queued != null) onCancelQueued else null,
|
||||
)
|
||||
Column {
|
||||
InUseBar(
|
||||
queued = st.queued != null,
|
||||
sentence = (if (managedDxvk) stringResource(R.string.comp_in_use, label, stringResource(R.string.gpu_system_powervr)) +
|
||||
(st.queued?.let { "\n" + stringResource(R.string.cmgr_powervr_dxvk_queued, it) } ?: "")
|
||||
else if (st.queued != null) stringResource(R.string.comp_next_queued, st.queued) else stringResource(R.string.comp_in_use, label, st.inUse))
|
||||
.let { if (reappliedAt != null && !managedDxvk) stringResource(R.string.comp_reapplied, it, reappliedAt) else it },
|
||||
note = if (managedDxvk) "" else if (view?.inUseByGame == true) stringResource(R.string.comp_waits) else stringResource(R.string.comp_applies_next_game),
|
||||
onCancel = if (st.queued != null) onCancelQueued else null,
|
||||
maxLines = if (managedDxvk && st.queued != null) 3 else 2,
|
||||
)
|
||||
if (managedDxvk) Text(
|
||||
stringResource(R.string.cmgr_powervr_dxvk_managed),
|
||||
fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,16 +547,17 @@ fun ComponentsPage(
|
||||
}
|
||||
else -> {
|
||||
val label = ComponentsManager.LABEL[l] ?: l
|
||||
val rows = installedFor(l, view, snapshot)
|
||||
val managedDxvk = ComponentsManager.runtimeManaged(l, gpu.systemPowerVr)
|
||||
val rows = installedFor(l, view, snapshot, managedDxvk)
|
||||
if (rows.isEmpty()) EmptyLine(stringResource(R.string.comp_nothing_stored, label))
|
||||
val p = view?.proton
|
||||
rows.forEachIndexed { i, item ->
|
||||
val key = "row:$l:${item.file ?: item.protonVersion}"
|
||||
ChoiceLine(
|
||||
item.name, item.detail, item.selected, item.removable, tag = item.tag,
|
||||
modifier = (if (i == 0) Modifier.focusRequester(firstRow) else Modifier).trackRow(key, rowsAt),
|
||||
item.name, item.detail, item.selected, item.removable, tag = item.tag, enabled = !managedDxvk,
|
||||
modifier = (if (i == 0 && !managedDxvk) Modifier.focusRequester(firstRow) else Modifier).trackRow(key, rowsAt),
|
||||
onSelect = {
|
||||
if (item.selected || p == null) return@ChoiceLine
|
||||
if (managedDxvk || item.selected || p == null) return@ChoiceLine
|
||||
val waits = if (view.inUseByGame) ctx.getString(R.string.comp_waits_long) else ctx.getString(R.string.comp_applies_long)
|
||||
if (item.file != null) ask(key, right, ctx.getString(R.string.comp_swap_title, label), ctx.getString(R.string.comp_swap_body, item.name, p.name, waits), ctx.getString(R.string.comp_swap), false, item.name) {
|
||||
onSwap(item.file); flyHome(key, l)
|
||||
@@ -617,14 +628,19 @@ fun ComponentsPage(
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
SmallButton(stringResource(R.string.comp_import_wcp), onClick = onImport)
|
||||
val managedDxvk = ComponentsManager.runtimeManaged(l, gpu.systemPowerVr)
|
||||
SmallButton(
|
||||
stringResource(R.string.comp_import_wcp), onClick = onImport,
|
||||
modifier = if (managedDxvk) Modifier.focusRequester(firstRow) else Modifier,
|
||||
)
|
||||
val st = compState(l)
|
||||
val p = view?.proton
|
||||
val build = p?.let { ComponentsManager.safeName(it.version) }
|
||||
if (st != null && p != null && st.activeFile != null && view.originals.any { it.comp == l && it.protonVersion == build }) {
|
||||
val label = ComponentsManager.LABEL[l] ?: l
|
||||
Box(Modifier.trackRow("restore:$l", rowsAt)) {
|
||||
FocusText(stringResource(R.string.comp_restore_original), pal.signal) {
|
||||
if (managedDxvk) SmallButton(stringResource(R.string.comp_restore_original), enabled = false) {}
|
||||
else FocusText(stringResource(R.string.comp_restore_original), pal.signal) {
|
||||
ask("restore:$l", false, ctx.getString(R.string.comp_restore_title, label), ctx.getString(R.string.comp_restore_original_body, p.name, label), ctx.getString(R.string.common_restore), false, ctx.getString(R.string.comp_restore_original)) {
|
||||
onRestore(build!!); flyHome("restore:$l", l)
|
||||
}
|
||||
@@ -770,6 +786,7 @@ fun ComponentsPage(
|
||||
protonChoice = gameProtonChoice,
|
||||
packages = snapshot?.packages.orEmpty(),
|
||||
componentChoices = gameComponents,
|
||||
systemPowerVr = gpu.systemPowerVr,
|
||||
componentMenu = gameComponentMenu,
|
||||
protonMenu = gameProtonMenu,
|
||||
fexMenu = fexMenu,
|
||||
@@ -909,11 +926,11 @@ fun ComponentsPage(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapshot: Snapshot?): List<InstalledItem> {
|
||||
private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapshot: Snapshot?, runtimeManaged: Boolean = false): List<InstalledItem> {
|
||||
view ?: return emptyList()
|
||||
val st = view.components[l] ?: return emptyList()
|
||||
val build = ComponentsManager.safeName(view.proton.version)
|
||||
val activeIsOriginal = st.activeFile == null
|
||||
val activeIsOriginal = !runtimeManaged && st.activeFile == null
|
||||
return view.originals.filter { it.comp == l }.map { o ->
|
||||
val isCurrent = o.protonVersion == build
|
||||
InstalledItem(
|
||||
@@ -922,7 +939,7 @@ private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapsho
|
||||
"ORIGINAL", activeIsOriginal && isCurrent, !isCurrent, null, o.protonVersion,
|
||||
)
|
||||
} + (snapshot?.packages ?: emptyList()).filter { it.comp == l }.map { s ->
|
||||
InstalledItem(s.version, stringResource(R.string.common_size_mb, s.size / 1048576.0), "STORED", s.file == st.activeFile, true, s.file, null)
|
||||
InstalledItem(s.version, stringResource(R.string.common_size_mb, s.size / 1048576.0), "STORED", !runtimeManaged && s.file == st.activeFile, true, s.file, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,11 +978,12 @@ private fun Stack(
|
||||
LayerCard(
|
||||
title = LayerTitle(l),
|
||||
value = when (l) {
|
||||
GPU_TAB -> gpuCardValue(gpu, auto)
|
||||
GPU_TAB -> if (gpu.systemPowerVr) stringResource(R.string.gpu_system_powervr) else gpuCardValue(gpu, auto)
|
||||
PROTON_TAB -> (view?.proton?.name ?: "") + " ▾"
|
||||
else -> st?.let { if (it.activeFile == null) original else it.inUse } ?: ""
|
||||
else -> if (ComponentsManager.runtimeManaged(l, gpu.systemPowerVr)) stringResource(R.string.gpu_system_powervr)
|
||||
else st?.let { if (it.activeFile == null) original else it.inUse } ?: ""
|
||||
},
|
||||
swapped = l in ComponentsManager.COMPONENTS && st?.activeFile != null,
|
||||
swapped = l in ComponentsManager.COMPONENTS && st?.activeFile != null && !ComponentsManager.runtimeManaged(l, gpu.systemPowerVr),
|
||||
selected = l == layer,
|
||||
landed = landed[l] ?: 0, ring = rings[l] ?: 0,
|
||||
modifier = Modifier.focusRequester(focus.getValue(l)).onGloballyPositioned { onPlaced(l, it.boundsInRoot()) },
|
||||
@@ -1032,7 +1050,7 @@ private fun LayerCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InUseBar(queued: Boolean, sentence: String, note: String, onCancel: (() -> Unit)? = null) {
|
||||
private fun InUseBar(queued: Boolean, sentence: String, note: String, onCancel: (() -> Unit)? = null, maxLines: Int = 2) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Row(
|
||||
@@ -1041,7 +1059,7 @@ private fun InUseBar(queued: Boolean, sentence: String, note: String, onCancel:
|
||||
.border(1.dp, pal.line, RoundedCornerShape(12.dp)).padding(horizontal = 14.dp, vertical = 8.dp),
|
||||
) {
|
||||
Box(Modifier.size(8.dp).background(if (queued) QUEUED else pal.good, CircleShape))
|
||||
Text(sentence, fontSize = 14.sp, color = colors.onBackground, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
|
||||
Text(sentence, fontSize = 14.sp, color = colors.onBackground, maxLines = maxLines, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
|
||||
if (onCancel != null) FocusText(stringResource(R.string.common_cancel), pal.signal, onClick = onCancel)
|
||||
else if (note.isNotEmpty() && !LocalNarrowPane.current) Text(note, fontSize = 13.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
@@ -1076,6 +1094,7 @@ private fun GameComponentOverrides(
|
||||
protonChoice: ProtonDefault.GameChoice?,
|
||||
packages: List<ComponentsManager.Package>,
|
||||
componentChoices: Map<String, String>,
|
||||
systemPowerVr: Boolean,
|
||||
componentMenu: String?,
|
||||
protonMenu: Boolean,
|
||||
fexMenu: Boolean,
|
||||
@@ -1167,6 +1186,7 @@ private fun GameComponentOverrides(
|
||||
}
|
||||
GamePackageRow(
|
||||
comp = "dxvk",
|
||||
runtimeManaged = ComponentsManager.runtimeManaged("dxvk", systemPowerVr),
|
||||
packages = packages,
|
||||
selectedFile = componentChoices["dxvk"],
|
||||
open = componentMenu == "dxvk",
|
||||
@@ -1231,13 +1251,16 @@ private fun GamePackageRow(
|
||||
onMenu: (String?) -> Unit,
|
||||
onPick: (String, String?) -> Unit,
|
||||
onRuntimeOptions: (() -> Unit)? = null,
|
||||
runtimeManaged: Boolean = false,
|
||||
) {
|
||||
val choices = packages.filter { it.comp == comp }
|
||||
val selected = choices.firstOrNull { it.file == selectedFile }
|
||||
val title = ComponentsManager.LABEL[comp] ?: comp
|
||||
val enabled = !busy && (!runtimeManaged || selectedFile != null)
|
||||
SettingsRow(
|
||||
title,
|
||||
if (selected == null) {
|
||||
if (runtimeManaged) stringResource(R.string.cmgr_powervr_dxvk_managed)
|
||||
else if (selected == null) {
|
||||
if (proton == null) stringResource(R.string.comp_game_component_general)
|
||||
else stringResource(R.string.comp_game_component_runtime, proton.name)
|
||||
} else stringResource(R.string.comp_game_component_override, selected.version),
|
||||
@@ -1245,18 +1268,18 @@ private fun GamePackageRow(
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box {
|
||||
ValueChip(
|
||||
selected?.version ?: stringResource(R.string.comp_game_use_general),
|
||||
open,
|
||||
enabled = !busy,
|
||||
if (runtimeManaged) stringResource(R.string.gpu_system_powervr) else selected?.version ?: stringResource(R.string.comp_game_use_general),
|
||||
open && enabled,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.widthIn(max = 240.dp),
|
||||
) { onMenu(if (open) null else comp) }
|
||||
AnchoredMenu(open, onDismiss = { onMenu(null) }, title = title) { first ->
|
||||
AnchoredMenu(open && enabled, onDismiss = { onMenu(null) }, title = title) { first ->
|
||||
MenuItem(
|
||||
stringResource(R.string.comp_game_use_general),
|
||||
checked = selected == null,
|
||||
focusRequester = first,
|
||||
) { onPick(comp, null); onMenu(null) }
|
||||
choices.forEach { option ->
|
||||
if (!runtimeManaged) choices.forEach { option ->
|
||||
MenuItem(option.version, checked = option.file == selectedFile, detail = option.description) {
|
||||
onPick(comp, option.file)
|
||||
onMenu(null)
|
||||
@@ -1378,13 +1401,13 @@ private fun Tag(text: String) {
|
||||
@Composable
|
||||
private fun ChoiceLine(
|
||||
name: String, detail: String, selected: Boolean, removable: Boolean,
|
||||
modifier: Modifier = Modifier, tag: String? = null, badge: String? = null, progress: Int? = null,
|
||||
modifier: Modifier = Modifier, tag: String? = null, badge: String? = null, progress: Int? = null, enabled: Boolean = true,
|
||||
onSelect: () -> Unit, onDelete: () -> Unit = {},
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = rememberHot(src)
|
||||
val hot = rememberHot(src) && enabled
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -1394,8 +1417,8 @@ private fun ChoiceLine(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.weight(1f).heightIn(min = 56.dp).then(modifier)
|
||||
.glideBorder(hot, ROW_SHAPE, pal.signal)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = null, role = Role.RadioButton, onClick = onSelect)
|
||||
.controllerConfirm(onClick = onSelect)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = null, enabled = enabled, role = Role.RadioButton, onClick = onSelect)
|
||||
.controllerConfirm(enabled = enabled, onClick = onSelect)
|
||||
.padding(start = 14.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(20.dp).border(2.dp, if (selected) pal.signal else colors.onSurfaceVariant, CircleShape)) {
|
||||
@@ -1405,7 +1428,7 @@ private fun ChoiceLine(
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
name, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, maxLines = 2, overflow = TextOverflow.Ellipsis,
|
||||
color = if (selected) pal.signal else colors.onBackground, modifier = Modifier.weight(1f, fill = false),
|
||||
color = if (!enabled) colors.onSurfaceVariant else if (selected) pal.signal else colors.onBackground, modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (tag != null) Tag(tag)
|
||||
}
|
||||
|
||||
@@ -107,6 +107,10 @@ class FrontEndState(
|
||||
val gameDebugLogsNeedsRestart: Boolean = false,
|
||||
val vacDiagnostics: Boolean = false,
|
||||
val agentCommands: Boolean = false,
|
||||
val graphicsLogs: Boolean = false,
|
||||
val steamGpuDrawing: Boolean = false,
|
||||
val bridgeFellBack: Boolean = false,
|
||||
val bridgeFellBackMemory: Boolean = false,
|
||||
val clearCachedApps: Boolean = false,
|
||||
val adbPaired: Boolean = false,
|
||||
val backgroundAppLimit: Int = -1,
|
||||
@@ -152,6 +156,8 @@ class FrontEndState(
|
||||
val phantomProcessStatus: PhantomProcessStatus = PhantomProcessStatus.NOT_APPLICABLE,
|
||||
val adreno: Boolean = true,
|
||||
val vkBridge: Boolean = false,
|
||||
val venus: Boolean = false,
|
||||
val venusHostVulkan: String? = null,
|
||||
val vkBridgeSafe: Boolean = true,
|
||||
val bridgeProtonDxvk: Boolean = false,
|
||||
val showPhantomGate: Boolean = false,
|
||||
@@ -211,6 +217,7 @@ class FrontEndActions(
|
||||
val onRemovePackage: (String) -> Unit,
|
||||
val onRuntime: () -> Unit,
|
||||
val onVkBridge: (Boolean) -> Unit = {},
|
||||
val onVenus: (Boolean) -> Unit = {},
|
||||
val onVkBridgeSafe: (Boolean) -> Unit = {},
|
||||
val onBridgeProtonDxvk: (Boolean) -> Unit = {},
|
||||
val onFrameGenPick: (FrameGen.Mode) -> Unit,
|
||||
@@ -232,6 +239,8 @@ class FrontEndActions(
|
||||
val onGameDebugLogs: () -> Unit = {},
|
||||
val onVacDiagnostics: () -> Unit = {},
|
||||
val onAgentCommands: () -> Unit = {},
|
||||
val onGraphicsLogs: () -> Unit = {},
|
||||
val onSteamGpuDrawing: () -> Unit = {},
|
||||
val onClearCachedApps: (Boolean) -> Unit = {},
|
||||
val onMemoryHud: (String) -> Unit = {},
|
||||
val onBackgroundAppLimit: (Int) -> Unit = {},
|
||||
|
||||
@@ -248,6 +248,8 @@ internal fun SetupPanel(
|
||||
Column(modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) {
|
||||
when (tab) {
|
||||
0 -> {
|
||||
// Setup's "Safe mode" row, under the GPU row while the bridge is on and can be used here.
|
||||
val bridgeModeRow = !s.adreno && s.vkBridge && com.droiddeck.launcher.core.DeviceSupport.bridgeUsable()
|
||||
val gpuCheck: @Composable (Boolean) -> Unit = { divider ->
|
||||
CheckRow(
|
||||
if (gpuOk) CheckState.OK else CheckState.WARN,
|
||||
@@ -259,19 +261,32 @@ internal fun SetupPanel(
|
||||
},
|
||||
when (gpu.support) {
|
||||
com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> stringResource(R.string.setup_gpu_detail, gpu.displayName(ctx), gpuName)
|
||||
com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> stringResource(R.string.setup_gpu_untested_detail, gpu.displayName(ctx), gpu.supportText(ctx).replaceFirstChar { it.lowercase() })
|
||||
else -> if (s.vkBridge) stringResource(R.string.setup_gpu_bridge_detail, gpuName)
|
||||
com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED ->
|
||||
if (!s.adreno && s.venus && s.venusHostVulkan != null) stringResource(R.string.setup_gpu_venus_host_too_old, gpu.displayName(ctx), s.venusHostVulkan)
|
||||
else if (!s.adreno && s.vkBridge && !com.droiddeck.launcher.core.DeviceSupport.bridgeUsable()) stringResource(R.string.setup_gpu_bridge_unusable, gpu.displayName(ctx))
|
||||
else if (!s.adreno && s.vkBridge && s.bridgeFellBack) stringResource(if (s.bridgeFellBackMemory) R.string.setup_gpu_bridge_no_memory_sharing else R.string.setup_gpu_bridge_fell_back, gpu.displayName(ctx))
|
||||
else stringResource(R.string.setup_gpu_untested_detail, gpu.displayName(ctx), gpu.supportText(ctx).replaceFirstChar { it.lowercase() })
|
||||
else -> if (s.venus && s.venusHostVulkan != null) stringResource(R.string.setup_gpu_venus_host_too_old, gpuName, s.venusHostVulkan)
|
||||
else if (s.vkBridge && !com.droiddeck.launcher.core.DeviceSupport.bridgeUsable()) stringResource(R.string.setup_gpu_bridge_unusable, gpuName)
|
||||
else if (s.vkBridge && s.bridgeFellBack) stringResource(if (s.bridgeFellBackMemory) R.string.setup_gpu_bridge_no_memory_sharing else R.string.setup_gpu_bridge_fell_back, gpuName)
|
||||
else if (s.vkBridge) stringResource(R.string.setup_gpu_bridge_detail, gpuName)
|
||||
else stringResource(R.string.setup_gpu_unsupported_detail, gpuName)
|
||||
},
|
||||
divider = divider || (!s.adreno && s.vkBridge),
|
||||
divider = divider || bridgeModeRow,
|
||||
action = if (s.adreno) null else { {
|
||||
SecondaryButton(
|
||||
stringResource(if (s.vkBridge) R.string.setup_gpu_bridge_on else R.string.setup_gpu_bridge_off),
|
||||
enabled = !s.sessionRunning, compact = true, onClick = { a.onVkBridge(!s.vkBridge) },
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SecondaryButton(
|
||||
stringResource(if (s.venus) R.string.setup_gpu_venus_on else R.string.setup_gpu_venus_off),
|
||||
enabled = !s.sessionRunning, compact = true, onClick = { a.onVenus(!s.venus) },
|
||||
)
|
||||
SecondaryButton(
|
||||
stringResource(if (s.vkBridge) R.string.setup_gpu_bridge_on else R.string.setup_gpu_bridge_off),
|
||||
enabled = !s.sessionRunning, compact = true, onClick = { a.onVkBridge(!s.vkBridge) },
|
||||
)
|
||||
}
|
||||
} },
|
||||
)
|
||||
if (!s.adreno && s.vkBridge) CheckRow(
|
||||
if (bridgeModeRow) CheckRow(
|
||||
CheckState.OK,
|
||||
stringResource(R.string.setup_bridge_safe),
|
||||
stringResource(if (s.vkBridgeSafe) R.string.setup_bridge_safe_on_detail else R.string.setup_bridge_safe_off_detail),
|
||||
@@ -558,6 +573,8 @@ internal fun SetupPanel(
|
||||
) { a.onGameDebugLogs() }
|
||||
ToggleRow(host, "vacdiag", stringResource(R.string.setup_vac_diag), stringResource(R.string.setup_vac_diag_hint), s.vacDiagnostics) { a.onVacDiagnostics() }
|
||||
ToggleRow(host, "agent-commands", stringResource(R.string.setup_agent_commands), null, s.agentCommands) { a.onAgentCommands() }
|
||||
ToggleRow(host, "graphics-logs", stringResource(R.string.setup_graphics_logs), stringResource(R.string.setup_graphics_logs_hint), s.graphicsLogs) { a.onGraphicsLogs() }
|
||||
if (!s.adreno && com.droiddeck.launcher.session.SessionPrefs.steamGpuCompositingAllowed()) ToggleRow(host, "steam-gpu-drawing", stringResource(R.string.setup_steam_gpu_drawing), stringResource(R.string.setup_steam_gpu_drawing_hint), s.steamGpuDrawing) { a.onSteamGpuDrawing() }
|
||||
ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs,
|
||||
progress = com.droiddeck.launcher.session.SessionLogShare.progress)
|
||||
androidx.compose.material3.HorizontalDivider()
|
||||
|
||||
@@ -89,6 +89,8 @@ class PairRow(
|
||||
val complete: Boolean,
|
||||
val installed: Boolean,
|
||||
val active: Boolean,
|
||||
/** A newer build of this installed pair (its build date), or null. */
|
||||
val update: String? = null,
|
||||
)
|
||||
|
||||
const val GPU_MODE_AUTO = "auto"
|
||||
@@ -110,6 +112,9 @@ class GpuDriversState(
|
||||
val supported: Boolean = true,
|
||||
val unsupported: Boolean = false,
|
||||
val auto: Boolean = true,
|
||||
val powerVr: Boolean = false,
|
||||
/** The selected runtime supplies DXVK for PowerVR game launches. */
|
||||
val systemPowerVr: Boolean = false,
|
||||
val pairs: List<PairRow> = emptyList(),
|
||||
val busy: String? = null,
|
||||
val percent: Int = -1,
|
||||
@@ -138,6 +143,7 @@ class GpuDriversState(
|
||||
class GpuDriversActions(
|
||||
val onAuto: (Boolean) -> Unit = {},
|
||||
val onPair: (String) -> Unit = {},
|
||||
val onPairUpdate: (String) -> Unit = {},
|
||||
val onRefresh: () -> Unit = {},
|
||||
val onSelectLinux: (String) -> Unit = {},
|
||||
val onSelectAndroid: (String) -> Unit = {},
|
||||
@@ -258,7 +264,7 @@ internal fun GpuDriversBody(
|
||||
modes.indices.forEach { choice(it, Modifier.weight(1f).fillMaxHeight()) }
|
||||
}
|
||||
when (gpu.mode) {
|
||||
GPU_MODE_AUTO -> AutoBody(gpu)
|
||||
GPU_MODE_AUTO -> AutoBody(gpu, actions)
|
||||
GPU_MODE_PAIR -> PairBody(gpu, actions)
|
||||
else -> MixBody(single, actions, slotFocus, onOpenSlot)
|
||||
}
|
||||
@@ -351,14 +357,26 @@ private fun RestartBanner(target: String, canRestart: Boolean, afterLater: Focus
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AutoBody(gpu: GpuDriversState) {
|
||||
private fun AutoBody(gpu: GpuDriversState, actions: GpuDriversActions) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
PanelTitle(
|
||||
stringResource(if (gpu.activePair != null) R.string.gpu_auto_title else R.string.gpu_auto_builtin_title, gpu.gpuName),
|
||||
stringResource(R.string.gpu_auto_reason, gpu.gpuFamily),
|
||||
)
|
||||
gpu.activePair?.let { p -> PanelRow(p.name, "${p.version} · ${p.detail}", selected = true, onClick = null) }
|
||||
gpu.activePair?.let { p ->
|
||||
// An open non-Adreno pair is rebuilt in place while it is tested: offer the newer build.
|
||||
PanelRow(
|
||||
p.name, "${p.version} · " + (p.update?.let { stringResource(R.string.proton_update_available, it) } ?: p.detail), selected = true,
|
||||
trailing = when {
|
||||
gpu.busy == p.key -> "${gpu.percent.coerceAtLeast(0)}%"
|
||||
p.update != null -> stringResource(R.string.common_update)
|
||||
else -> null
|
||||
},
|
||||
enabled = gpu.busy == null,
|
||||
onClick = p.update?.let { { actions.onPairUpdate(p.key) } },
|
||||
)
|
||||
}
|
||||
if (gpu.autoStatus.isNotEmpty()) Text(gpu.autoStatus, fontSize = 13.sp, color = colors.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
@@ -376,15 +394,17 @@ private fun PairBody(gpu: GpuDriversState, actions: GpuDriversActions) {
|
||||
fontSize = 13.sp, color = colors.onSurfaceVariant,
|
||||
)
|
||||
for (p in rows) PanelRow(
|
||||
p.name, "${p.version} · ${p.detail}", selected = p.active,
|
||||
p.name, "${p.version} · " + (p.update?.let { stringResource(R.string.proton_update_available, it) } ?: p.detail), selected = p.active,
|
||||
badge = if (p.recommended) stringResource(R.string.gpu_autos_pick) else null,
|
||||
trailing = when {
|
||||
gpu.busy == p.key -> "${gpu.percent.coerceAtLeast(0)}%"
|
||||
p.update != null -> stringResource(R.string.common_update)
|
||||
p.installed -> stringResource(R.string.gpu_installed)
|
||||
else -> stringResource(R.string.common_download)
|
||||
},
|
||||
enabled = gpu.busy == null,
|
||||
onClick = { if (!p.active) actions.onPair(p.key) },
|
||||
// A rebuilt open non-Adreno pair (same release, newer build) is updated in place.
|
||||
onClick = { if (p.update != null) actions.onPairUpdate(p.key) else if (!p.active) actions.onPair(p.key) },
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (gpu.pairs.any { !it.suits }) FocusText(stringResource(if (showAll) R.string.gpu_hide_other else R.string.gpu_show_other), pal.signal) { showAll = !showAll }
|
||||
|
||||
@@ -324,6 +324,7 @@ class DrawerActions(
|
||||
val onClose: () -> Unit,
|
||||
val onDismissScreen: (() -> Unit)? = null,
|
||||
val components: ComponentsManager.Snapshot? = null,
|
||||
val systemPowerVr: Boolean = false,
|
||||
val onComponentsRefresh: () -> Unit = {},
|
||||
val componentCatalog: List<ComponentsManager.CatalogItem> = emptyList(),
|
||||
val componentCatalogAt: Long = 0,
|
||||
@@ -1341,7 +1342,11 @@ private fun SelectedGameProfileGroup(host: MenuHost, a: DrawerActions, track: (S
|
||||
for (component in ComponentsManager.COMPONENTS) {
|
||||
val value = profile.components.getValue(component)
|
||||
if (!value.inherited) {
|
||||
ProfileSummaryRow(ComponentsManager.LABEL.getValue(component), profileValueLabel(value))
|
||||
ProfileSummaryRow(
|
||||
ComponentsManager.LABEL.getValue(component),
|
||||
if (ComponentsManager.runtimeManaged(component, a.systemPowerVr)) stringResource(R.string.gpu_system_powervr)
|
||||
else profileValueLabel(value),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (profile.startWithoutSteam) {
|
||||
@@ -1450,20 +1455,23 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin
|
||||
)
|
||||
for (comp in ComponentsManager.COMPONENTS) {
|
||||
val value = gameProfile.components.getValue(comp)
|
||||
val managedDxvk = ComponentsManager.runtimeManaged(comp, a.systemPowerVr)
|
||||
val options: List<Pair<String?, String>> =
|
||||
listOf(null to stringResource(R.string.comp_game_use_general)) +
|
||||
snap.packages.filter { it.comp == comp }.map { it.file to it.version }
|
||||
if (managedDxvk) emptyList() else snap.packages.filter { it.comp == comp }.map { it.file to it.version }
|
||||
DrawerStackedChoice(
|
||||
host = host,
|
||||
key = "game-cmp-$comp",
|
||||
label = ComponentsManager.LABEL.getValue(comp),
|
||||
hint = if (value.inherited) {
|
||||
hint = if (managedDxvk) stringResource(R.string.cmgr_powervr_dxvk_managed) else if (value.inherited) {
|
||||
stringResource(R.string.drawer_component_inherited, value.value)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
options = options,
|
||||
selected = gameProfile.componentFiles[comp],
|
||||
enabled = !managedDxvk || gameProfile.componentFiles[comp] != null,
|
||||
valueLabel = if (managedDxvk) stringResource(R.string.gpu_system_powervr) else null,
|
||||
chipModifier = track("cmp-$comp"),
|
||||
onPick = { file -> a.onGameComponent(gameProfile.appId, comp, file) },
|
||||
)
|
||||
@@ -1498,7 +1506,11 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin
|
||||
for (comp in ComponentsManager.COMPONENTS) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(top = 3.dp)) {
|
||||
Text(ComponentsManager.LABEL.getValue(comp), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.width(96.dp))
|
||||
Text(r.components.getValue(comp).inUse, fontSize = 12.sp, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(
|
||||
if (ComponentsManager.runtimeManaged(comp, a.systemPowerVr)) stringResource(R.string.gpu_system_powervr)
|
||||
else r.components.getValue(comp).inUse,
|
||||
fontSize = 12.sp, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1526,16 +1538,21 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin
|
||||
val build = ComponentsManager.safeName(p.version)
|
||||
for (comp in ComponentsManager.COMPONENTS) {
|
||||
val st = view.components.getValue(comp)
|
||||
val managedDxvk = ComponentsManager.runtimeManaged(comp, a.systemPowerVr)
|
||||
val originals = view.originals.filter { it.comp == comp }
|
||||
val options = originals.map { "orig:${it.protonVersion}" to (if (it.protonVersion == build) originalLabel else stringResource(R.string.comp_original, it.protonVersion)) } +
|
||||
snap.packages.filter { it.comp == comp }.map { it.file to it.version }
|
||||
if (options.isEmpty()) continue
|
||||
if (options.isEmpty() && !managedDxvk) continue
|
||||
val current = st.activeFile ?: "orig:$build"
|
||||
val shown = options.firstOrNull { it.first == current }?.second
|
||||
DrawerStackedChoice(
|
||||
host, "cmp-$comp", ComponentsManager.LABEL.getValue(comp),
|
||||
st.queued?.let { stringResource(R.string.comp_next_queued, it) } ?: st.inUse.takeIf { it != shown },
|
||||
if (managedDxvk) stringResource(R.string.cmgr_powervr_dxvk_managed) +
|
||||
(st.queued?.let { "\n" + stringResource(R.string.cmgr_powervr_dxvk_queued, it) } ?: "")
|
||||
else st.queued?.let { stringResource(R.string.comp_next_queued, it) } ?: st.inUse.takeIf { it != shown },
|
||||
options, current,
|
||||
enabled = !managedDxvk,
|
||||
valueLabel = if (managedDxvk) stringResource(R.string.gpu_system_powervr) else null,
|
||||
note = if (view.inUseByGame) stringResource(R.string.comp_waits_long) else stringResource(R.string.comp_applies_long),
|
||||
chipModifier = track("cmp-$comp"),
|
||||
onPick = { v -> if (v != current || st.queued != null) a.onComponentSwap(p.id, comp, v) },
|
||||
@@ -1648,11 +1665,11 @@ internal fun ComponentDownloadsDrawerGroup(
|
||||
private fun <T> DrawerStackedChoice(
|
||||
host: MenuHost, key: String, label: String, hint: String?,
|
||||
options: List<Pair<T, String>>, selected: T, note: String? = null,
|
||||
chipModifier: Modifier = Modifier, onPick: (T) -> Unit,
|
||||
chipModifier: Modifier = Modifier, enabled: Boolean = true, valueLabel: String? = null, onPick: (T) -> Unit,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val open = host.open == key
|
||||
val open = host.open == key && enabled
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.background(if (open) pal.signal.copy(alpha = 0.10f) else Color.Transparent)
|
||||
@@ -1660,7 +1677,7 @@ private fun <T> DrawerStackedChoice(
|
||||
) {
|
||||
Text(label, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground)
|
||||
Box(modifier = Modifier.fillMaxWidth().padding(top = 6.dp)) {
|
||||
ValueChip(options.firstOrNull { it.first == selected }?.second ?: "-", open, modifier = chipModifier.fillMaxWidth()) {
|
||||
ValueChip(valueLabel ?: options.firstOrNull { it.first == selected }?.second ?: "-", open, enabled = enabled, modifier = chipModifier.fillMaxWidth()) {
|
||||
host.open = if (open) null else key
|
||||
}
|
||||
AnchoredMenu(open, onDismiss = { if (host.open == key) host.open = null }, title = label, note = note) { firstItemFocus ->
|
||||
|
||||
@@ -74,6 +74,12 @@ object AppUpdates {
|
||||
val updatable: Boolean,
|
||||
val committedAt: Long = 0L,
|
||||
val ciBuild: Boolean = BuildConfig.CI_BUILD,
|
||||
/**
|
||||
* A build of the non-Adreno test branch (Mali / Xclipse / PowerVR routes). The official
|
||||
* channels carry the Adreno-only app: installing one over it drops those routes, so none is
|
||||
* offered. Its updates are published on The412Banner/droiddeck-nonadreno-drivers.
|
||||
*/
|
||||
val nonAdrenoTest: Boolean = false,
|
||||
)
|
||||
|
||||
enum class PreviewHistoryKind { CURRENT, BEHIND, RECENT }
|
||||
@@ -125,6 +131,7 @@ object AppUpdates {
|
||||
BuildConfig.VERSION_CODE,
|
||||
releaseSigned,
|
||||
BuildConfig.COMMIT_TIME,
|
||||
nonAdrenoTest = BuildConfig.BUILD_LABEL.contains("non-adreno"),
|
||||
)
|
||||
|
||||
fun release(catalog: Catalog, follow: Follow): Release? = when (follow.channel) {
|
||||
@@ -191,6 +198,7 @@ object AppUpdates {
|
||||
}
|
||||
|
||||
fun installBlock(r: Release, me: Installed = installed()): InstallBlock? = when {
|
||||
me.nonAdrenoTest -> InstallBlock(R.string.upd_block_non_adreno_test)
|
||||
r.apk == null -> InstallBlock(R.string.upd_block_no_apk)
|
||||
r.apk.versionCode < me.versionCode -> InstallBlock(R.string.upd_block_version_code, r.apk.versionCode, me.versionCode)
|
||||
else -> null
|
||||
|
||||
@@ -180,6 +180,28 @@ public final class WaylandCompositor {
|
||||
|
||||
public static native int nativeCompositorTid();
|
||||
|
||||
private static native String nativeCompositorFault();
|
||||
|
||||
private static native String nativeDisplayDriver();
|
||||
|
||||
/** Why the compositor cannot present, or "" while it can (or before the library is loaded). */
|
||||
public static String compositorFault() {
|
||||
try {
|
||||
return nativeCompositorFault();
|
||||
} catch (Throwable t) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** The driver that answered for the display, as it reports itself; "" when not known yet. */
|
||||
public static String displayDriver() {
|
||||
try {
|
||||
return nativeDisplayDriver();
|
||||
} catch (Throwable t) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static native void nativeSetHideShell(boolean hide);
|
||||
|
||||
public static native void nativeSetOutputRefreshRate(float hz);
|
||||
|
||||
@@ -825,7 +825,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU غير مدعوم</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">يحتاج Steam إلى برنامج تشغيل Adreno؛ قد يعرض %1$s شاشة سوداء</string>
|
||||
<string name="setup_gpu_unsupported_detail">لا تتوفر واجهة رسومات متوافقة مع %1$s. قد يعرض Steam شاشة سوداء.</string>
|
||||
<string name="setup_runtime">بيئة تشغيل Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">غير مثبّت · نحو 3 GB، يُثبَّت عند أول تشغيل</string>
|
||||
@@ -1450,8 +1450,8 @@
|
||||
<string name="main_wadb_unavailable">تصحيح الأخطاء اللاسلكي غير متاح</string>
|
||||
<string name="main_adb_copied">تم نسخ أمر ADB</string>
|
||||
<string name="main_pick_roms">اختر مجلد ROMs</string>
|
||||
<string name="main_non_adreno_title">وحدة GPU ليست من نوع Adreno</string>
|
||||
<string name="main_non_adreno_text">يدعم Turnip وحدات GPU من نوع Adreno. على %1$s، قد يعرض Steam شاشة سوداء. حجم التنزيل: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">وحدة معالجة رسومات غير مدعومة</string>
|
||||
<string name="main_non_adreno_text">لا تتوفر واجهة رسومات متوافقة مع %1$s. قد يعرض Steam شاشة سوداء. التنزيل: %2$.0f ميغابايت.</string>
|
||||
<string name="main_install_anyway">التثبيت على أي حال</string>
|
||||
<string name="main_remove_runtime_title">إزالة بيئة تشغيل Linux</string>
|
||||
<string name="main_remove_runtime_text">سيؤدي هذا إلى حذف بيئة التشغيل وعميل Steam بداخلها وكل لعبة مثبَّتة فيها.</string>
|
||||
@@ -2217,7 +2217,7 @@
|
||||
<string name="gpuinfo_experimental">تجريبي: برامج تشغيله إصدارات اختبارية</string>
|
||||
<string name="gpuinfo_a850_experimental">دعم تجريبي لـ Adreno 850: يتطلب برامج تشغيل تجريبية متوافقة للعرض والألعاب، ولم يُتحقَّق منه على هذا الجهاز</string>
|
||||
<string name="gpuinfo_below_tested">أقل من العتاد المُختبَر (Adreno 725 وأحدث): قد لا يعمل</string>
|
||||
<string name="gpuinfo_unsupported">غير مدعوم: يحتاج DroidDeck إلى وحدة GPU من نوع Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">غير مدعوم: لا تتوفر واجهة رسومات متوافقة مع وحدة معالجة الرسومات هذه</string>
|
||||
|
||||
<string name="turnip_rate_limited">تم الوصول إلى حدّ الطلبات في GitHub - حاوِل مرة أخرى لاحقًا</string>
|
||||
<string name="turnip_github_http">استجاب GitHub بـ HTTP %1$d</string>
|
||||
|
||||
@@ -843,7 +843,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU no compatible</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam necesita un controlador Adreno; %1$s puede mostrar una pantalla negra</string>
|
||||
<string name="setup_gpu_unsupported_detail">No hay un motor gráfico compatible disponible para %1$s. Steam puede mostrar una pantalla negra.</string>
|
||||
<string name="setup_runtime">Runtime de Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">No instalado · unos 3 GB, se instala al jugar por primera vez</string>
|
||||
@@ -1483,8 +1483,8 @@
|
||||
<string name="main_wadb_unavailable">La Depuración inalámbrica no está disponible</string>
|
||||
<string name="main_adb_copied">Comando ADB copiado</string>
|
||||
<string name="main_pick_roms">Elige la carpeta de ROMs</string>
|
||||
<string name="main_non_adreno_title">La GPU no es Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU no compatible</string>
|
||||
<string name="main_non_adreno_text">No hay un motor gráfico compatible disponible para %1$s. Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Instalar de todos modos</string>
|
||||
<string name="main_remove_runtime_title">Eliminar el runtime de Linux</string>
|
||||
<string name="main_remove_runtime_text">Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él.</string>
|
||||
@@ -2192,7 +2192,7 @@
|
||||
<string name="gpuinfo_experimental">Experimental: sus controladores son versiones de prueba</string>
|
||||
<string name="gpuinfo_a850_experimental">Compatibilidad experimental con Adreno 850: requiere controladores experimentales de pantalla y de juegos compatibles; no verificada en este dispositivo</string>
|
||||
<string name="gpuinfo_below_tested">Por debajo del hardware probado (Adreno 725 y posteriores): puede que no funcione</string>
|
||||
<string name="gpuinfo_unsupported">No compatible: DroidDeck necesita una GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">No compatible: no hay un motor gráfico compatible disponible para esta GPU</string>
|
||||
<string name="turnip_rate_limited">Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde</string>
|
||||
<string name="turnip_github_http">GitHub respondió HTTP %1$d</string>
|
||||
<string name="turnip_download_failed">Error en la descarga</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU не се поддържа</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam изисква драйвер за Adreno; %1$s може да покаже черен екран</string>
|
||||
<string name="setup_gpu_unsupported_detail">Няма съвместим графичен бекенд за %1$s. Steam може да показва черен екран.</string>
|
||||
<string name="setup_runtime">Linux среда за изпълнение</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Не е инсталирано · около 3 GB, инсталира се при първото Play</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">Безжичното отстраняване на грешки не е достъпно</string>
|
||||
<string name="main_adb_copied">Командата за ADB е копирана</string>
|
||||
<string name="main_pick_roms">Избор на папката ROMs</string>
|
||||
<string name="main_non_adreno_title">Не е Adreno GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip поддържа Adreno GPU-та. На %1$s Steam може да покаже черен екран. Изтегляне: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Неподдържан GPU</string>
|
||||
<string name="main_non_adreno_text">Няма съвместим графичен бекенд за %1$s. Steam може да показва черен екран. Изтегляне: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Инсталиране въпреки това</string>
|
||||
<string name="main_remove_runtime_title">Премахване на средата на Linux</string>
|
||||
<string name="main_remove_runtime_text">Това изтрива средата, клиента на Steam в нея и всяка инсталирана в нея игра.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Експериментален: драйверите му са тестови версии</string>
|
||||
<string name="gpuinfo_a850_experimental">Експериментална поддръжка на Adreno 850: изисква съвместими експериментални драйвери за дисплея и игрите; не е проверена на това устройство</string>
|
||||
<string name="gpuinfo_below_tested">Под тестваните устройства (Adreno 725 и по-нови): може да не работи</string>
|
||||
<string name="gpuinfo_unsupported">Не се поддържа: DroidDeck изисква GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Не се поддържа: няма съвместим графичен бекенд за този GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">Достигнато е ограничението на GitHub за заявки - опитай отново по-късно</string>
|
||||
<string name="turnip_github_http">GitHub отговори с HTTP %1$d</string>
|
||||
|
||||
@@ -819,7 +819,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU nepodporováno</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam vyžaduje ovladač Adreno; %1$s může zobrazit černou obrazovku</string>
|
||||
<string name="setup_gpu_unsupported_detail">Pro %1$s není k dispozici kompatibilní grafický backend. Steam může zobrazit černou obrazovku.</string>
|
||||
<string name="setup_runtime">Běhové prostředí Linuxu</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d %%</string>
|
||||
<string name="setup_runtime_missing">Nenainstalováno · přibližně 3 GB, instaluje se při prvním Play</string>
|
||||
@@ -1426,8 +1426,8 @@
|
||||
<string name="main_wadb_unavailable">Bezdrátové ladění není k dispozici</string>
|
||||
<string name="main_adb_copied">Příkaz ADB zkopírován</string>
|
||||
<string name="main_pick_roms">Vybrat složku ROMs</string>
|
||||
<string name="main_non_adreno_title">Není GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip podporuje GPU Adreno. Na %1$s může Steam zobrazit černou obrazovku. Stažení: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Nepodporovaný GPU</string>
|
||||
<string name="main_non_adreno_text">Pro %1$s není k dispozici kompatibilní grafický backend. Steam může zobrazit černou obrazovku. Ke stažení: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Přesto instalovat</string>
|
||||
<string name="main_remove_runtime_title">Odstranit runtime Linuxu</string>
|
||||
<string name="main_remove_runtime_text">Tím se odstraní runtime, klient Steam v něm a všechny hry v něm nainstalované.</string>
|
||||
@@ -2167,7 +2167,7 @@
|
||||
<string name="gpuinfo_experimental">Experimentální: jeho ovladače jsou testovací sestavení</string>
|
||||
<string name="gpuinfo_a850_experimental">Experimentální podpora Adreno 850: vyžaduje kompatibilní experimentální ovladače displeje a her; na tomto zařízení neověřeno</string>
|
||||
<string name="gpuinfo_below_tested">Pod testovaným hardwarem (Adreno 725 a novější): nemusí fungovat</string>
|
||||
<string name="gpuinfo_unsupported">Není podporováno: DroidDeck potřebuje GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Nepodporováno: pro tento GPU není k dispozici kompatibilní grafický backend</string>
|
||||
|
||||
<string name="turnip_rate_limited">Byl dosažen limit požadavků GitHubu - zkuste to později</string>
|
||||
<string name="turnip_github_http">GitHub odpověděl HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU ikke understøttet</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam kræver en Adreno-driver; %1$s kan vise en sort skærm</string>
|
||||
<string name="setup_gpu_unsupported_detail">Ingen kompatibel grafikbackend er tilgængelig for %1$s. Steam kan vise en sort skærm.</string>
|
||||
<string name="setup_runtime">Linux-runtime</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Ikke installeret · omkring 3 GB, installeres ved første Spil</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">Trådløs fejlretning er ikke tilgængelig</string>
|
||||
<string name="main_adb_copied">ADB-kommando kopieret</string>
|
||||
<string name="main_pick_roms">Vælg ROMs-mappen</string>
|
||||
<string name="main_non_adreno_title">Ikke en Adreno-GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip understøtter Adreno-GPU\'er. På %1$s kan Steam vise en sort skærm. Download: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU understøttes ikke</string>
|
||||
<string name="main_non_adreno_text">Ingen kompatibel grafikbackend er tilgængelig for %1$s. Steam kan vise en sort skærm. Download: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Installer alligevel</string>
|
||||
<string name="main_remove_runtime_title">Fjern Linux-runtime</string>
|
||||
<string name="main_remove_runtime_text">Dette sletter runtime\'en, Steam-klienten i den og alle spil installeret der.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Eksperimentel: dens drivere er testversioner</string>
|
||||
<string name="gpuinfo_a850_experimental">Eksperimentel Adreno 850-understøttelse: kræver kompatible eksperimentelle skærm- og spildrivere; ikke bekræftet på denne enhed</string>
|
||||
<string name="gpuinfo_below_tested">Under testet hardware (Adreno 725 og nyere): kører muligvis ikke</string>
|
||||
<string name="gpuinfo_unsupported">Ikke understøttet: DroidDeck kræver en Adreno (Snapdragon) GPU</string>
|
||||
<string name="gpuinfo_unsupported">Understøttes ikke: ingen kompatibel grafikbackend er tilgængelig for denne GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHubs hastighedsgrænse blev nået - forsøg igen senere</string>
|
||||
<string name="turnip_github_http">GitHub svarede HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU nicht unterstützt</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam benötigt einen Adreno-Treiber; %1$s zeigt möglicherweise einen schwarzen Bildschirm</string>
|
||||
<string name="setup_gpu_unsupported_detail">Für %1$s ist kein kompatibles Grafik-Backend verfügbar. Steam zeigt möglicherweise einen schwarzen Bildschirm.</string>
|
||||
<string name="setup_runtime">Linux-Laufzeit</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d %%</string>
|
||||
<string name="setup_runtime_missing">Nicht installiert · ca. 3 GB, wird beim ersten „Spielen“ installiert</string>
|
||||
@@ -1394,8 +1394,8 @@
|
||||
<string name="main_wadb_unavailable">Debugging über WLAN ist nicht verfügbar</string>
|
||||
<string name="main_adb_copied">ADB-Befehl kopiert</string>
|
||||
<string name="main_pick_roms">ROMs-Ordner auswählen</string>
|
||||
<string name="main_non_adreno_title">Keine Adreno-GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip unterstützt Adreno-GPUs. Auf %1$s zeigt Steam eventuell einen schwarzen Bildschirm. Download: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Nicht unterstützte GPU</string>
|
||||
<string name="main_non_adreno_text">Für %1$s ist kein kompatibles Grafik-Backend verfügbar. Steam zeigt möglicherweise einen schwarzen Bildschirm. Download: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Trotzdem installieren</string>
|
||||
<string name="main_remove_runtime_title">Linux-Laufzeit entfernen</string>
|
||||
<string name="main_remove_runtime_text">Dadurch werden die Laufzeit, der Steam-Client darin und alle dort installierten Spiele gelöscht.</string>
|
||||
@@ -2109,7 +2109,7 @@
|
||||
<string name="gpuinfo_experimental">Experimentell: Die Treiber sind Test-Builds</string>
|
||||
<string name="gpuinfo_a850_experimental">Experimentelle Adreno-850-Unterstützung: erfordert kompatible experimentelle Anzeige- und Spieltreiber; auf diesem Gerät nicht geprüft</string>
|
||||
<string name="gpuinfo_below_tested">Unter der getesteten Hardware (Adreno 725 und neuer): läuft eventuell nicht</string>
|
||||
<string name="gpuinfo_unsupported">Nicht unterstützt: DroidDeck braucht eine Adreno-GPU (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Nicht unterstützt: Für diese GPU ist kein kompatibles Grafik-Backend verfügbar</string>
|
||||
|
||||
<string name="turnip_rate_limited">Das Rate-Limit von GitHub wurde erreicht - versuche es später erneut</string>
|
||||
<string name="turnip_github_http">GitHub antwortete mit HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">Η GPU δεν υποστηρίζεται</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Το Steam απαιτεί πρόγραμμα οδήγησης Adreno· το %1$s μπορεί να εμφανίσει μαύρη οθόνη</string>
|
||||
<string name="setup_gpu_unsupported_detail">Δεν υπάρχει συμβατό υποσύστημα γραφικών για %1$s. Το Steam μπορεί να εμφανίσει μαύρη οθόνη.</string>
|
||||
<string name="setup_runtime">Runtime Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Δεν έχει εγκατασταθεί · περίπου 3 GB, εγκαθίσταται στο πρώτο Play</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">Ο ασύρματος εντοπισμός σφαλμάτων δεν είναι διαθέσιμος</string>
|
||||
<string name="main_adb_copied">Η εντολή ADB αντιγράφηκε</string>
|
||||
<string name="main_pick_roms">Επιλέξτε τον φάκελο ROMs</string>
|
||||
<string name="main_non_adreno_title">Δεν είναι GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Το Turnip υποστηρίζει GPU Adreno. Σε %1$s, το Steam ίσως εμφανίσει μαύρη οθόνη. Λήψη: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Μη υποστηριζόμενη GPU</string>
|
||||
<string name="main_non_adreno_text">Δεν υπάρχει συμβατό υποσύστημα γραφικών για %1$s. Το Steam μπορεί να εμφανίσει μαύρη οθόνη. Λήψη: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Εγκατάσταση παρόλα αυτά</string>
|
||||
<string name="main_remove_runtime_title">Κατάργηση του Linux runtime</string>
|
||||
<string name="main_remove_runtime_text">Αυτό διαγράφει το runtime, τον πελάτη Steam μέσα σε αυτό και κάθε παιχνίδι που είναι εγκατεστημένο εκεί.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Πειραματικό: τα προγράμματα οδήγησής του είναι δοκιμαστικές εκδόσεις</string>
|
||||
<string name="gpuinfo_a850_experimental">Πειραματική υποστήριξη Adreno 850: απαιτεί συμβατά πειραματικά προγράμματα οδήγησης οθόνης και παιχνιδιών· δεν έχει επαληθευτεί σε αυτή τη συσκευή</string>
|
||||
<string name="gpuinfo_below_tested">Κάτω από το δοκιμασμένο υλικό (Adreno 725 και νεότερα): ίσως να μην εκτελεστεί</string>
|
||||
<string name="gpuinfo_unsupported">Δεν υποστηρίζεται: το DroidDeck χρειάζεται GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Δεν υποστηρίζεται: δεν υπάρχει συμβατό υποσύστημα γραφικών για αυτήν την GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">Το όριο ρυθμού του GitHub ξεπεράστηκε - δοκιμάστε ξανά αργότερα</string>
|
||||
<string name="turnip_github_http">Το GitHub απάντησε με HTTP %1$d</string>
|
||||
|
||||
@@ -816,7 +816,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU no compatible</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam necesita un controlador Adreno; %1$s puede mostrar una pantalla negra</string>
|
||||
<string name="setup_gpu_unsupported_detail">No hay un motor gráfico compatible disponible; %1$s puede mostrar una pantalla negra</string>
|
||||
<string name="setup_runtime">Runtime de Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">No instalado · unos 3 GB, se instala al jugar por primera vez</string>
|
||||
@@ -1414,8 +1414,8 @@
|
||||
<string name="main_wadb_unavailable">La Depuración inalámbrica no está disponible</string>
|
||||
<string name="main_adb_copied">Comando ADB copiado</string>
|
||||
<string name="main_pick_roms">Elige la carpeta de ROMs</string>
|
||||
<string name="main_non_adreno_title">La GPU no es Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU no compatible</string>
|
||||
<string name="main_non_adreno_text">No hay un motor gráfico compatible disponible para %1$s. Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Instalar de todos modos</string>
|
||||
<string name="main_remove_runtime_title">Eliminar el runtime de Linux</string>
|
||||
<string name="main_remove_runtime_text">Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él.</string>
|
||||
@@ -2142,7 +2142,7 @@
|
||||
<string name="gpuinfo_experimental">Experimental: sus controladores son versiones de prueba</string>
|
||||
<string name="gpuinfo_a850_experimental">Compatibilidad experimental con Adreno 850: requiere controladores experimentales de pantalla y de juegos compatibles; no verificada en este dispositivo</string>
|
||||
<string name="gpuinfo_below_tested">Por debajo del hardware probado (Adreno 725 y posteriores): puede que no funcione</string>
|
||||
<string name="gpuinfo_unsupported">No compatible: DroidDeck necesita una GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">No compatible: no hay un motor gráfico compatible disponible para esta GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde</string>
|
||||
<string name="turnip_github_http">GitHub respondió HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU ei ole tuettu</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam tarvitsee Adreno-ajurin; %1$s saattaa näyttää mustan ruudun</string>
|
||||
<string name="setup_gpu_unsupported_detail">GPU:lle %1$s ei ole saatavilla yhteensopivaa grafiikkataustajärjestelmää. Steam saattaa näyttää mustan ruudun.</string>
|
||||
<string name="setup_runtime">Linux-ajoympäristö</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Ei asennettu · noin 3 GB, asennetaan ensimmäisellä Pelaa-painalluksella</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">Langaton virheenkorjaus ei ole käytettävissä</string>
|
||||
<string name="main_adb_copied">ADB-komento kopioitu</string>
|
||||
<string name="main_pick_roms">Valitse ROMs-kansio</string>
|
||||
<string name="main_non_adreno_title">Ei Adreno-grafiikkapiiri</string>
|
||||
<string name="main_non_adreno_text">Turnip tukee Adreno-grafiikkapiirejä. Laitteella %1$s Steam voi näyttää mustan ruudun. Latauksen koko: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU:ta ei tueta</string>
|
||||
<string name="main_non_adreno_text">GPU:lle %1$s ei ole saatavilla yhteensopivaa grafiikkataustajärjestelmää. Steam saattaa näyttää mustan ruudun. Lataus: %2$.0f Mt.</string>
|
||||
<string name="main_install_anyway">Asenna silti</string>
|
||||
<string name="main_remove_runtime_title">Poista Linux-ajoympäristö</string>
|
||||
<string name="main_remove_runtime_text">Tämä poistaa ajoympäristön, siihen asennetun Steam-asiakasohjelman ja kaikki sinne asennetut pelit.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Kokeellinen: sen ajurit ovat testiversioita</string>
|
||||
<string name="gpuinfo_a850_experimental">Kokeellinen Adreno 850 -tuki: vaatii yhteensopivat kokeelliset näyttö- ja peliajurit; ei vahvistettu tällä laitteella</string>
|
||||
<string name="gpuinfo_below_tested">Testatun laitteiston alapuolella (Adreno 725 ja uudemmat): ei välttämättä toimi</string>
|
||||
<string name="gpuinfo_unsupported">Ei tuettu: DroidDeck vaatii Adreno (Snapdragon) -grafiikkapiirin</string>
|
||||
<string name="gpuinfo_unsupported">Ei tuettu: tälle GPU:lle ei ole saatavilla yhteensopivaa grafiikkataustajärjestelmää</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHubin käyttörajoitus tuli vastaan - yritä myöhemmin uudelleen</string>
|
||||
<string name="turnip_github_http">GitHub vastasi HTTP %1$d</string>
|
||||
|
||||
@@ -816,7 +816,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU non pris en charge</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s : %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam nécessite un pilote Adreno ; %1$s peut afficher un écran noir.</string>
|
||||
<string name="setup_gpu_unsupported_detail">Aucun moteur graphique compatible n’est disponible ; %1$s peut afficher un écran noir.</string>
|
||||
<string name="setup_runtime">Environnement d\'exécution Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Non installé · Environ 3 Go, installé lors du premier lancement d\'un jeu</string>
|
||||
@@ -1414,8 +1414,8 @@
|
||||
<string name="main_wadb_unavailable">Le débogage sans fil n\'est pas disponible</string>
|
||||
<string name="main_adb_copied">Commande ADB copiée</string>
|
||||
<string name="main_pick_roms">Choisir le dossier ROM</string>
|
||||
<string name="main_non_adreno_title">GPU non Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip prend en charge les GPU Adreno. Sur %1$s, Steam peut afficher un écran noir. Téléchargement : %2$.0f Mo.</string>
|
||||
<string name="main_non_adreno_title">GPU non pris en charge</string>
|
||||
<string name="main_non_adreno_text">Aucun moteur graphique compatible n’est disponible pour %1$s. Steam peut afficher un écran noir. Téléchargement : %2$.0f Mo.</string>
|
||||
<string name="main_install_anyway">Installer quand même</string>
|
||||
<string name="main_remove_runtime_title">Supprimer l\'environnement d\'exécution Linux</string>
|
||||
<string name="main_remove_runtime_text">Cela supprime l\'environnement d\'exécution, le client Steam qu\'il contient et tous les jeux qui y sont installés.</string>
|
||||
@@ -2142,7 +2142,7 @@
|
||||
<string name="gpuinfo_experimental">Expérimental : ses pilotes sont des versions de test</string>
|
||||
<string name="gpuinfo_a850_experimental">Prise en charge expérimentale de l\'Adreno 850 : nécessite des pilotes d\'affichage et de jeu expérimentaux compatibles ; non vérifiée sur cet appareil</string>
|
||||
<string name="gpuinfo_below_tested">En dessous du matériel testé (Adreno 725 et plus récent) : risque de ne pas fonctionner</string>
|
||||
<string name="gpuinfo_unsupported">Non pris en charge : DroidDeck nécessite un GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Non pris en charge : aucun moteur graphique compatible n’est disponible pour ce GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">Limite de requêtes GitHub atteinte - réessayez plus tard</string>
|
||||
<string name="turnip_github_http">GitHub a répondu HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU समर्थित नहीं</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam को Adreno ड्राइवर चाहिए; %1$s में काली स्क्रीन दिख सकती है</string>
|
||||
<string name="setup_gpu_unsupported_detail">%1$s के लिए कोई संगत ग्राफ़िक्स बैकएंड उपलब्ध नहीं है। Steam काली स्क्रीन दिखा सकता है।</string>
|
||||
<string name="setup_runtime">Linux रनटाइम</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">इंस्टॉल नहीं है · लगभग 3 GB, पहली बार Play पर इंस्टॉल होगा</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">वायरलेस डीबगिंग उपलब्ध नहीं है</string>
|
||||
<string name="main_adb_copied">ADB कमांड कॉपी हो गया</string>
|
||||
<string name="main_pick_roms">ROM फ़ोल्डर चुनें</string>
|
||||
<string name="main_non_adreno_title">Adreno GPU नहीं है</string>
|
||||
<string name="main_non_adreno_text">Turnip Adreno GPU को सपोर्ट करता है। %1$s पर, Steam में काली स्क्रीन दिख सकती है। डाउनलोड: %2$.0f MB।</string>
|
||||
<string name="main_non_adreno_title">असमर्थित GPU</string>
|
||||
<string name="main_non_adreno_text">%1$s के लिए कोई संगत ग्राफ़िक्स बैकएंड उपलब्ध नहीं है। Steam काली स्क्रीन दिखा सकता है। डाउनलोड: %2$.0f MB।</string>
|
||||
<string name="main_install_anyway">फिर भी इंस्टॉल करें</string>
|
||||
<string name="main_remove_runtime_title">Linux रनटाइम हटाएं</string>
|
||||
<string name="main_remove_runtime_text">यह रनटाइम, इसके भीतर का Steam क्लाइंट, और वहां इंस्टॉल हर गेम हटा देता है।</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">एक्सपेरिमेंटल: इसके ड्राइवर टेस्ट बिल्ड हैं</string>
|
||||
<string name="gpuinfo_a850_experimental">प्रयोगात्मक Adreno 850 समर्थन: कम्पैटिबल प्रयोगात्मक डिस्प्ले और गेम ड्राइवर चाहिए; इस डिवाइस पर जांचा नहीं गया</string>
|
||||
<string name="gpuinfo_below_tested">जांचे गए हार्डवेयर से नीचे (Adreno 725 और नए): यह शायद न चले</string>
|
||||
<string name="gpuinfo_unsupported">समर्थित नहीं: DroidDeck को Adreno (Snapdragon) GPU चाहिए</string>
|
||||
<string name="gpuinfo_unsupported">समर्थित नहीं: इस GPU के लिए कोई संगत ग्राफ़िक्स बैकएंड उपलब्ध नहीं है</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHub की रेट लिमिट पार हो गई - बाद में फिर कोशिश करें</string>
|
||||
<string name="turnip_github_http">GitHub ने HTTP %1$d जवाब दिया</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">A GPU nem támogatott</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">A Steamnek Adreno-illesztőprogram szükséges; a %1$s fekete képernyőt jeleníthet meg</string>
|
||||
<string name="setup_gpu_unsupported_detail">Nincs kompatibilis grafikus háttérrendszer ehhez: %1$s. A Steam fekete képernyőt jeleníthet meg.</string>
|
||||
<string name="setup_runtime">Linux futtatókörnyezet</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Nincs telepítve · kb. 3 GB, az első Játék gombnyomáskor települ</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">A vezeték nélküli hibakeresés nem áll rendelkezésre</string>
|
||||
<string name="main_adb_copied">ADB-parancs másolva</string>
|
||||
<string name="main_pick_roms">ROMs mappa kiválasztása</string>
|
||||
<string name="main_non_adreno_title">Nem Adreno GPU</string>
|
||||
<string name="main_non_adreno_text">A Turnip Adreno GPU-kat támogat. %1$s esetén a Steam fekete képernyőt jeleníthet meg. Letöltés: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Nem támogatott GPU</string>
|
||||
<string name="main_non_adreno_text">Nincs kompatibilis grafikus háttérrendszer ehhez: %1$s. A Steam fekete képernyőt jeleníthet meg. Letöltés: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Telepítés mindenképpen</string>
|
||||
<string name="main_remove_runtime_title">Linux futtatókörnyezet eltávolítása</string>
|
||||
<string name="main_remove_runtime_text">Ez törli a futtatókörnyezetet, a benne lévő Steam-klienst és az összes ott telepített játékot.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Kísérleti: az illesztőprogramjai teszt buildek</string>
|
||||
<string name="gpuinfo_a850_experimental">Kísérleti Adreno 850-támogatás: kompatibilis kísérleti kijelző- és játék-illesztőprogramokat igényel; ezen az eszközön nincs ellenőrizve</string>
|
||||
<string name="gpuinfo_below_tested">A tesztelt hardver alatt (Adreno 725 és újabb): lehet, hogy nem fut</string>
|
||||
<string name="gpuinfo_unsupported">Nem támogatott: a DroidDecknek Adreno (Snapdragon) GPU szükséges</string>
|
||||
<string name="gpuinfo_unsupported">Nem támogatott: nincs kompatibilis grafikus háttérrendszer ehhez a GPU-hoz</string>
|
||||
|
||||
<string name="turnip_rate_limited">A GitHub korlátja elérve – próbálja újra később</string>
|
||||
<string name="turnip_github_http">A GitHub HTTP %1$d választ adott</string>
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU tidak didukung</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam memerlukan driver Adreno; %1$s mungkin menampilkan layar hitam</string>
|
||||
<string name="setup_gpu_unsupported_detail">Tidak ada backend grafis yang kompatibel untuk %1$s. Steam mungkin menampilkan layar hitam.</string>
|
||||
<string name="setup_runtime">Runtime Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Belum terinstal · sekitar 3 GB, terinstal saat Play pertama kali</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">Debugging nirkabel tidak tersedia</string>
|
||||
<string name="main_adb_copied">Perintah ADB disalin</string>
|
||||
<string name="main_pick_roms">Pilih folder ROMs</string>
|
||||
<string name="main_non_adreno_title">Bukan GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip mendukung GPU Adreno. Di %1$s, Steam mungkin menampilkan layar hitam. Unduhan: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU tidak didukung</string>
|
||||
<string name="main_non_adreno_text">Tidak ada backend grafis yang kompatibel untuk %1$s. Steam mungkin menampilkan layar hitam. Unduhan: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Tetap instal</string>
|
||||
<string name="main_remove_runtime_title">Hapus runtime Linux</string>
|
||||
<string name="main_remove_runtime_text">Ini menghapus runtime, klien Steam di dalamnya, dan setiap game yang terinstal di sana.</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">Eksperimental: drivernya adalah build uji</string>
|
||||
<string name="gpuinfo_a850_experimental">Dukungan eksperimental Adreno 850: memerlukan driver tampilan dan driver gim eksperimental yang kompatibel; belum diverifikasi di perangkat ini</string>
|
||||
<string name="gpuinfo_below_tested">Di bawah perangkat keras yang diuji (Adreno 725 dan lebih baru): mungkin tidak dapat berjalan</string>
|
||||
<string name="gpuinfo_unsupported">Tidak didukung: DroidDeck memerlukan GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Tidak didukung: tidak ada backend grafis yang kompatibel untuk GPU ini</string>
|
||||
|
||||
<string name="turnip_rate_limited">Batas kecepatan GitHub tercapai - coba lagi nanti</string>
|
||||
<string name="turnip_github_http">GitHub merespons HTTP %1$d</string>
|
||||
|
||||
@@ -816,7 +816,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU non supportata</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam richiede un driver Adreno; %1$s potrebbe mostrare una schermata nera</string>
|
||||
<string name="setup_gpu_unsupported_detail">Nessun backend grafico compatibile disponibile per %1$s. Steam potrebbe mostrare una schermata nera.</string>
|
||||
<string name="setup_runtime">Runtime Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Non installato · circa 3 GB, si installa la prima volta che premi Gioca</string>
|
||||
@@ -1406,8 +1406,8 @@
|
||||
<string name="main_wadb_unavailable">Il debug wireless non è disponibile</string>
|
||||
<string name="main_adb_copied">Comando ADB copiato</string>
|
||||
<string name="main_pick_roms">Scegli la cartella delle ROM</string>
|
||||
<string name="main_non_adreno_title">Non è una GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip supporta le GPU Adreno. Su %1$s, Steam potrebbe mostrare una schermata nera. Download: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU non supportata</string>
|
||||
<string name="main_non_adreno_text">Nessun backend grafico compatibile disponibile per %1$s. Steam potrebbe mostrare una schermata nera. Download: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Installa comunque</string>
|
||||
<string name="main_remove_runtime_title">Rimuovi runtime Linux</string>
|
||||
<string name="main_remove_runtime_text">Verranno eliminati il runtime, il client Steam al suo interno e tutti i giochi installati lì.</string>
|
||||
@@ -2134,7 +2134,7 @@
|
||||
<string name="gpuinfo_experimental">Sperimentale: i suoi driver sono build di test</string>
|
||||
<string name="gpuinfo_a850_experimental">Supporto sperimentale per Adreno 850: richiede driver di visualizzazione e di gioco sperimentali compatibili; non verificato su questo dispositivo</string>
|
||||
<string name="gpuinfo_below_tested">Inferiore all\'hardware testato (Adreno 725 e successive): potrebbe non funzionare</string>
|
||||
<string name="gpuinfo_unsupported">Non supportata: DroidDeck richiede una GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Non supportata: nessun backend grafico compatibile disponibile per questa GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">Raggiunto il limite di richieste di GitHub - riprova più tardi</string>
|
||||
<string name="turnip_github_http">GitHub ha risposto HTTP %1$d</string>
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">GPUはサポートされていません</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s:%2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">SteamにはAdrenoドライバーが必要です。%1$sでは画面が真っ暗になる可能性があります。</string>
|
||||
<string name="setup_gpu_unsupported_detail">利用できる互換グラフィックスバックエンドがありません。%1$sでは画面が真っ暗になる可能性があります。</string>
|
||||
<string name="setup_runtime">Linux実行環境</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">未インストール · 約3GB、初回ゲーム起動時にインストール</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">ワイヤレスデバッグは利用できません</string>
|
||||
<string name="main_adb_copied">ADBコマンドをコピーしました</string>
|
||||
<string name="main_pick_roms">ROMフォルダを選択</string>
|
||||
<string name="main_non_adreno_title">Adreno GPUではありません</string>
|
||||
<string name="main_non_adreno_text">TurnipはAdreno GPUをサポートしています。%1$sでは、Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。</string>
|
||||
<string name="main_non_adreno_title">サポート対象外のGPU</string>
|
||||
<string name="main_non_adreno_text">%1$sで利用できる互換グラフィックスバックエンドがありません。Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。</string>
|
||||
<string name="main_install_anyway">インストールする</string>
|
||||
<string name="main_remove_runtime_title">Linux実行環境を削除</string>
|
||||
<string name="main_remove_runtime_text">実行環境、その中のSteamクライアント、そこにインストールされたすべてのゲームが削除されます。</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">実験的:ドライバーはテストビルドです</string>
|
||||
<string name="gpuinfo_a850_experimental">Adreno 850の実験的サポート:互換性のある実験版のディスプレイドライバーとゲームドライバーが必要です。このデバイスでは未検証です</string>
|
||||
<string name="gpuinfo_below_tested">テスト済みハードウェア(Adreno 725以降)未満:動作しない可能性があります</string>
|
||||
<string name="gpuinfo_unsupported">サポート対象外:DroidDeckにはAdreno(Snapdragon)GPUが必要です</string>
|
||||
<string name="gpuinfo_unsupported">サポート対象外:このGPUで利用できる互換グラフィックスバックエンドがありません</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHubのレート制限に達しました。後でもう一度お試しください</string>
|
||||
<string name="turnip_github_http">GitHubがHTTP %1$dを返しました</string>
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU 지원되지 않음</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam에는 Adreno 드라이버가 필요합니다. %1$s에서 화면이 검게 변할 수 있습니다.</string>
|
||||
<string name="setup_gpu_unsupported_detail">사용할 수 있는 호환 그래픽 백엔드가 없습니다. %1$s에서 화면이 검게 표시될 수 있습니다.</string>
|
||||
<string name="setup_runtime">Linux 런타임</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">설치되지 않음 · 약 3GB, 처음 \"Steam 실행\" 시 설치</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">무선 디버깅을 사용할 수 없습니다</string>
|
||||
<string name="main_adb_copied">ADB 명령이 복사되었습니다</string>
|
||||
<string name="main_pick_roms">ROM 폴더 선택</string>
|
||||
<string name="main_non_adreno_title">Adreno GPU가 아님</string>
|
||||
<string name="main_non_adreno_text">Turnip은 Adreno GPU를 지원합니다. %1$s에서는 Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">지원되지 않는 GPU</string>
|
||||
<string name="main_non_adreno_text">%1$s에서 사용할 수 있는 호환 그래픽 백엔드가 없습니다. Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">그래도 설치</string>
|
||||
<string name="main_remove_runtime_title">Linux 런타임 제거</string>
|
||||
<string name="main_remove_runtime_text">런타임과 그 안의 Steam 클라이언트, 그리고 런타임에 설치된 모든 게임이 삭제됩니다.</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">실험적: 드라이버가 테스트 빌드입니다</string>
|
||||
<string name="gpuinfo_a850_experimental">실험적 Adreno 850 지원: 호환되는 실험용 디스플레이 및 게임 드라이버가 필요하며, 이 기기에서 검증되지 않았습니다</string>
|
||||
<string name="gpuinfo_below_tested">테스트된 하드웨어(Adreno 725 이상)보다 낮음: 실행되지 않을 수 있습니다</string>
|
||||
<string name="gpuinfo_unsupported">지원되지 않음: DroidDeck에는 Adreno(Snapdragon) GPU가 필요합니다</string>
|
||||
<string name="gpuinfo_unsupported">지원되지 않음: 이 GPU에서 사용할 수 있는 호환 그래픽 백엔드가 없습니다</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHub 요청 한도에 도달했습니다. 나중에 다시 시도하세요</string>
|
||||
<string name="turnip_github_http">GitHub 응답: HTTP %1$d</string>
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU tidak disokong</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam memerlukan pemacu Adreno; %1$s mungkin memaparkan skrin hitam</string>
|
||||
<string name="setup_gpu_unsupported_detail">Tiada backend grafik yang serasi tersedia untuk %1$s. Steam mungkin memaparkan skrin hitam.</string>
|
||||
<string name="setup_runtime">Masa jalan Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Belum dipasang · kira-kira 3 GB, dipasang pada Main pertama</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">Debugging Wayarles tidak tersedia</string>
|
||||
<string name="main_adb_copied">Arahan ADB disalin</string>
|
||||
<string name="main_pick_roms">Pilih folder ROM</string>
|
||||
<string name="main_non_adreno_title">Bukan GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip menyokong GPU Adreno. Pada %1$s, Steam mungkin memaparkan skrin hitam. Muat turun: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU tidak disokong</string>
|
||||
<string name="main_non_adreno_text">Tiada backend grafik yang serasi tersedia untuk %1$s. Steam mungkin memaparkan skrin hitam. Muat turun: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Pasang juga</string>
|
||||
<string name="main_remove_runtime_title">Alih keluar masa jalan Linux</string>
|
||||
<string name="main_remove_runtime_text">Ini memadam masa jalan, klien Steam di dalamnya, dan setiap permainan yang dipasang di sana.</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">Eksperimen: pemacunya ialah binaan ujian</string>
|
||||
<string name="gpuinfo_a850_experimental">Sokongan eksperimen Adreno 850: memerlukan pemacu paparan dan permainan eksperimen yang serasi; belum disahkan pada peranti ini</string>
|
||||
<string name="gpuinfo_below_tested">Di bawah perkakasan yang diuji (Adreno 725 dan lebih baharu): mungkin tidak dapat berjalan</string>
|
||||
<string name="gpuinfo_unsupported">Tidak disokong: DroidDeck memerlukan GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Tidak disokong: tiada backend grafik yang serasi tersedia untuk GPU ini</string>
|
||||
|
||||
<string name="turnip_rate_limited">Had kadar GitHub dicapai - cuba lagi kemudian</string>
|
||||
<string name="turnip_github_http">GitHub menjawab HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU ikke støttet</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam krever en Adreno-driver; %1$s kan vise en svart skjerm</string>
|
||||
<string name="setup_gpu_unsupported_detail">Ingen kompatibel grafikkbackend er tilgjengelig for %1$s. Steam kan vise en svart skjerm.</string>
|
||||
<string name="setup_runtime">Linux-kjøretid</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Ikke installert · omtrent 3 GB, installeres ved første Spill</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">Trådløs feilsøking er ikke tilgjengelig</string>
|
||||
<string name="main_adb_copied">ADB-kommando kopiert</string>
|
||||
<string name="main_pick_roms">Velg ROM-mappen</string>
|
||||
<string name="main_non_adreno_title">Ikke en Adreno-GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip støtter Adreno-GPU-er. På %1$s kan Steam vise en svart skjerm. Nedlasting: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU støttes ikke</string>
|
||||
<string name="main_non_adreno_text">Ingen kompatibel grafikkbackend er tilgjengelig for %1$s. Steam kan vise en svart skjerm. Nedlasting: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Installer likevel</string>
|
||||
<string name="main_remove_runtime_title">Fjern Linux-kjøretid</string>
|
||||
<string name="main_remove_runtime_text">Dette sletter kjøretiden, Steam-klienten inni den, og hvert spill installert der.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Eksperimentell: driverne er testbuilder</string>
|
||||
<string name="gpuinfo_a850_experimental">Eksperimentell støtte for Adreno 850: krever kompatible eksperimentelle skjerm- og spilldrivere; ikke bekreftet på denne enheten</string>
|
||||
<string name="gpuinfo_below_tested">Under testet maskinvare (Adreno 725 og nyere): den kan ikke kjøre</string>
|
||||
<string name="gpuinfo_unsupported">Ikke støttet: DroidDeck krever en Adreno (Snapdragon) GPU</string>
|
||||
<string name="gpuinfo_unsupported">Støttes ikke: ingen kompatibel grafikkbackend er tilgjengelig for denne GPU-en</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHubs hastighetsgrense ble nådd – prøv igjen senere</string>
|
||||
<string name="turnip_github_http">GitHub svarte HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU niet ondersteund</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam heeft een Adreno-driver nodig; %1$s kan een zwart scherm tonen</string>
|
||||
<string name="setup_gpu_unsupported_detail">Er is geen compatibele grafische backend beschikbaar voor %1$s. Steam kan een zwart scherm tonen.</string>
|
||||
<string name="setup_runtime">Linux-runtime</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Niet geïnstalleerd · ongeveer 3 GB, geïnstalleerd bij de eerste keer Spelen</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">Draadloze foutopsporing is niet beschikbaar</string>
|
||||
<string name="main_adb_copied">ADB-opdracht gekopieerd</string>
|
||||
<string name="main_pick_roms">Kies de ROM-map</string>
|
||||
<string name="main_non_adreno_title">Geen Adreno-GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip ondersteunt Adreno-GPU\'s. Op %1$s kan Steam een zwart scherm tonen. Download: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Niet-ondersteunde GPU</string>
|
||||
<string name="main_non_adreno_text">Er is geen compatibele grafische backend beschikbaar voor %1$s. Steam kan een zwart scherm tonen. Download: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Toch installeren</string>
|
||||
<string name="main_remove_runtime_title">Linux-runtime verwijderen</string>
|
||||
<string name="main_remove_runtime_text">Hiermee verwijder je de runtime, de Steam-client erin en alle games die daar zijn geïnstalleerd.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Experimenteel: de drivers zijn testbuilds</string>
|
||||
<string name="gpuinfo_a850_experimental">Experimentele ondersteuning voor Adreno 850: vereist compatibele experimentele scherm- en gamedrivers; niet geverifieerd op dit apparaat</string>
|
||||
<string name="gpuinfo_below_tested">Onder de geteste hardware (Adreno 725 en nieuwer): werkt mogelijk niet</string>
|
||||
<string name="gpuinfo_unsupported">Niet ondersteund: DroidDeck heeft een Adreno-GPU (Snapdragon) nodig</string>
|
||||
<string name="gpuinfo_unsupported">Niet ondersteund: er is geen compatibele grafische backend beschikbaar voor deze GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">De ratelimiet van GitHub is bereikt - probeer het later opnieuw</string>
|
||||
<string name="turnip_github_http">GitHub antwoordde met HTTP %1$d</string>
|
||||
|
||||
@@ -819,7 +819,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU nieobsługiwany</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam wymaga sterownika Adreno; %1$s może wyświetlać czarny ekran</string>
|
||||
<string name="setup_gpu_unsupported_detail">Brak zgodnego zaplecza graficznego dla %1$s. Steam może wyświetlać czarny ekran.</string>
|
||||
<string name="setup_runtime">Środowisko Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Nie zainstalowano · ok. 3 GB, instalowane przy pierwszym „Graj”</string>
|
||||
@@ -1426,8 +1426,8 @@
|
||||
<string name="main_wadb_unavailable">Debugowanie bezprzewodowe jest niedostępne</string>
|
||||
<string name="main_adb_copied">Skopiowano polecenie ADB</string>
|
||||
<string name="main_pick_roms">Wybierz folder ROM-ów</string>
|
||||
<string name="main_non_adreno_title">To nie jest GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip obsługuje GPU Adreno. Na tym urządzeniu (%1$s) Steam może wyświetlać czarny ekran. Pobieranie: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Nieobsługiwany GPU</string>
|
||||
<string name="main_non_adreno_text">Brak zgodnego zaplecza graficznego dla %1$s. Steam może wyświetlać czarny ekran. Do pobrania: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Zainstaluj mimo to</string>
|
||||
<string name="main_remove_runtime_title">Usuń środowisko Linux</string>
|
||||
<string name="main_remove_runtime_text">Spowoduje to usunięcie środowiska, klienta Steam w nim i wszystkich zainstalowanych tam gier.</string>
|
||||
@@ -2167,7 +2167,7 @@
|
||||
<string name="gpuinfo_experimental">Eksperymentalny: jego sterowniki to wersje testowe</string>
|
||||
<string name="gpuinfo_a850_experimental">Eksperymentalna obsługa Adreno 850: wymaga zgodnych eksperymentalnych sterowników ekranu i gier; nie zweryfikowano na tym urządzeniu</string>
|
||||
<string name="gpuinfo_below_tested">Poniżej testowanego sprzętu (Adreno 725 i nowsze): może nie działać</string>
|
||||
<string name="gpuinfo_unsupported">Nieobsługiwany: DroidDeck wymaga GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Nieobsługiwany: brak zgodnego zaplecza graficznego dla tego GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">Osiągnięto limit zapytań GitHub – spróbuj ponownie później</string>
|
||||
<string name="turnip_github_http">GitHub odpowiedział HTTP %1$d</string>
|
||||
|
||||
@@ -816,7 +816,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU não suportada</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">O Steam requer um controlador Adreno; %1$s pode mostrar um ecrã preto</string>
|
||||
<string name="setup_gpu_unsupported_detail">Não está disponível um backend gráfico compatível para %1$s. O Steam pode apresentar um ecrã preto.</string>
|
||||
<string name="setup_runtime">Ambiente de execução Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Não instalado · cerca de 3 GB, instalado ao tocar em Jogar pela primeira vez</string>
|
||||
@@ -1406,8 +1406,8 @@
|
||||
<string name="main_wadb_unavailable">A depuração sem fios não está disponível</string>
|
||||
<string name="main_adb_copied">Comando ADB copiado</string>
|
||||
<string name="main_pick_roms">Escolha a pasta de ROMs</string>
|
||||
<string name="main_non_adreno_title">Não é uma GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">O Turnip suporta GPUs Adreno. Com %1$s, o Steam pode mostrar um ecrã preto. Transferência: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU não suportada</string>
|
||||
<string name="main_non_adreno_text">Não está disponível um backend gráfico compatível para %1$s. O Steam pode apresentar um ecrã preto. Transferência: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Instalar mesmo assim</string>
|
||||
<string name="main_remove_runtime_title">Remover runtime Linux</string>
|
||||
<string name="main_remove_runtime_text">Isto elimina o runtime, o cliente Steam que nele existe e todos os jogos lá instalados.</string>
|
||||
@@ -2134,7 +2134,7 @@
|
||||
<string name="gpuinfo_experimental">Experimental: os controladores são versões de teste</string>
|
||||
<string name="gpuinfo_a850_experimental">Suporte experimental para Adreno 850: requer controladores de ecrã e de jogos experimentais compatíveis; não verificado neste dispositivo</string>
|
||||
<string name="gpuinfo_below_tested">Abaixo do hardware testado (Adreno 725 e mais recentes): pode não funcionar</string>
|
||||
<string name="gpuinfo_unsupported">Não suportada: o DroidDeck precisa de uma GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Não suportada: não está disponível um backend gráfico compatível para esta GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">Foi atingido o limite de pedidos do GitHub - tente novamente mais tarde</string>
|
||||
<string name="turnip_github_http">O GitHub respondeu HTTP %1$d</string>
|
||||
|
||||
@@ -816,7 +816,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU sem suporte</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">A Steam requer um driver Adreno; %1$s pode mostrar uma tela preta</string>
|
||||
<string name="setup_gpu_unsupported_detail">Nenhum backend gráfico compatível está disponível para %1$s. O Steam pode exibir uma tela preta.</string>
|
||||
<string name="setup_runtime">Runtime do Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Não instalado · cerca de 3 GB, instalado no primeiro Jogar</string>
|
||||
@@ -1406,8 +1406,8 @@
|
||||
<string name="main_wadb_unavailable">A depuração por Wi-Fi não está disponível</string>
|
||||
<string name="main_adb_copied">Comando ADB copiado</string>
|
||||
<string name="main_pick_roms">Escolha a pasta de ROMs</string>
|
||||
<string name="main_non_adreno_title">Não é uma GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">O Turnip é compatível com GPUs Adreno. Com %1$s, o Steam pode mostrar uma tela preta. Download: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU não suportada</string>
|
||||
<string name="main_non_adreno_text">Nenhum backend gráfico compatível está disponível para %1$s. O Steam pode exibir uma tela preta. Download: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Instalar mesmo assim</string>
|
||||
<string name="main_remove_runtime_title">Remover runtime Linux</string>
|
||||
<string name="main_remove_runtime_text">Isso exclui o runtime, o cliente Steam dentro dele e todos os jogos instalados nele.</string>
|
||||
@@ -2134,7 +2134,7 @@
|
||||
<string name="gpuinfo_experimental">Experimental: os drivers são versões de teste</string>
|
||||
<string name="gpuinfo_a850_experimental">Suporte experimental à Adreno 850: requer drivers de tela e de jogos experimentais compatíveis; não verificado neste dispositivo</string>
|
||||
<string name="gpuinfo_below_tested">Abaixo do hardware testado (Adreno 725 ou mais recente): pode não funcionar</string>
|
||||
<string name="gpuinfo_unsupported">Incompatível: o DroidDeck precisa de uma GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Não suportada: nenhum backend gráfico compatível está disponível para esta GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">O limite de requisições do GitHub foi atingido - tente mais tarde</string>
|
||||
<string name="turnip_github_http">O GitHub respondeu HTTP %1$d</string>
|
||||
|
||||
@@ -816,7 +816,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU neacceptat</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam are nevoie de un driver Adreno; %1$s poate arăta un ecran negru</string>
|
||||
<string name="setup_gpu_unsupported_detail">Nu este disponibil un backend grafic compatibil pentru %1$s. Steam poate afișa un ecran negru.</string>
|
||||
<string name="setup_runtime">Mediul de execuție Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Neinstalat · circa 3 GB, instalat la prima apăsare pe Play</string>
|
||||
@@ -1414,8 +1414,8 @@
|
||||
<string name="main_wadb_unavailable">Depanarea wireless nu este disponibilă</string>
|
||||
<string name="main_adb_copied">Comanda ADB copiată</string>
|
||||
<string name="main_pick_roms">Alege folderul ROMs</string>
|
||||
<string name="main_non_adreno_title">Nu este un GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip acceptă GPU-uri Adreno. Pe %1$s, Steam poate arăta un ecran negru. Descărcare: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU neacceptat</string>
|
||||
<string name="main_non_adreno_text">Nu este disponibil un backend grafic compatibil pentru %1$s. Steam poate afișa un ecran negru. Descărcare: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Instalează oricum</string>
|
||||
<string name="main_remove_runtime_title">Elimină mediul de execuție Linux</string>
|
||||
<string name="main_remove_runtime_text">Aceasta șterge mediul de execuție, clientul Steam din interiorul lui și fiecare joc instalat acolo.</string>
|
||||
@@ -2142,7 +2142,7 @@
|
||||
<string name="gpuinfo_experimental">Experimental: driverele sale sunt versiuni de test</string>
|
||||
<string name="gpuinfo_a850_experimental">Suport experimental pentru Adreno 850: necesită drivere experimentale compatibile de afișaj și de jocuri; neverificat pe acest dispozitiv</string>
|
||||
<string name="gpuinfo_below_tested">Sub hardware-ul testat (Adreno 725 și mai nou): poate să nu ruleze</string>
|
||||
<string name="gpuinfo_unsupported">Neacceptat: DroidDeck are nevoie de un GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Neacceptat: nu este disponibil un backend grafic compatibil pentru acest GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">S-a atins limita de solicitări a GitHub - încearcă din nou mai târziu</string>
|
||||
<string name="turnip_github_http">GitHub a răspuns cu HTTP %1$d</string>
|
||||
|
||||
@@ -839,7 +839,7 @@
|
||||
<string name="setup_gpu_bridged">GPU через мост Mali</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam требует драйвер Adreno; на %1$s возможен черный экран.</string>
|
||||
<string name="setup_gpu_unsupported_detail">Совместимый графический бэкенд недоступен; на %1$s возможен черный экран.</string>
|
||||
<string name="setup_runtime">Среда выполнения Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Не установлено · около 3 ГБ, устанавливается при первом запуске игры</string>
|
||||
@@ -1472,8 +1472,8 @@
|
||||
<string name="main_wadb_unavailable">Отладка по Wi-Fi недоступна</string>
|
||||
<string name="main_adb_copied">Команда ADB скопирована</string>
|
||||
<string name="main_pick_roms">Выберите папку ROM</string>
|
||||
<string name="main_non_adreno_title">GPU не Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip поддерживает GPU Adreno. На %1$s в Steam может быть черный экран. Загрузка: %2$.0f МБ.</string>
|
||||
<string name="main_non_adreno_title">Неподдерживаемый GPU</string>
|
||||
<string name="main_non_adreno_text">Для %1$s нет совместимого графического бэкенда. В Steam может отображаться чёрный экран. Загрузка: %2$.0f МБ.</string>
|
||||
<string name="main_install_anyway">Все равно установить</string>
|
||||
<string name="main_remove_runtime_title">Удалить среду выполнения Linux</string>
|
||||
<string name="main_remove_runtime_text">Будут удалены среда выполнения, клиент Steam в ней и все установленные туда игры.</string>
|
||||
@@ -2248,7 +2248,7 @@
|
||||
<string name="gpuinfo_experimental">Экспериментально: его драйверы — тестовые сборки</string>
|
||||
<string name="gpuinfo_a850_experimental">Экспериментальная поддержка Adreno 850: требуются совместимые экспериментальные драйверы дисплея и игр; не проверено на этом устройстве</string>
|
||||
<string name="gpuinfo_below_tested">Ниже проверенного оборудования (Adreno 725 и новее): может не работать</string>
|
||||
<string name="gpuinfo_unsupported">Не поддерживается: DroidDeck требует GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Не поддерживается: для этого GPU нет совместимого графического бэкенда</string>
|
||||
<string name="gpuinfo_bridge">Работает через мост Mali на собственном драйвере Vulkan телефона</string>
|
||||
<string name="gpuinfo_bridge_off">Не поддерживается без моста Mali: Turnip нужен GPU Adreno (Snapdragon)</string>
|
||||
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU stöds inte</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam behöver en Adreno-drivrutin; %1$s kan visa en svart skärm</string>
|
||||
<string name="setup_gpu_unsupported_detail">Ingen kompatibel grafikbackend är tillgänglig för %1$s. Steam kan visa en svart skärm.</string>
|
||||
<string name="setup_runtime">Linux-körmiljö</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d %%</string>
|
||||
<string name="setup_runtime_missing">Inte installerad · cirka 3 GB, installeras första gången du trycker på Play</string>
|
||||
@@ -1402,8 +1402,8 @@
|
||||
<string name="main_wadb_unavailable">Trådlös felsökning är inte tillgänglig</string>
|
||||
<string name="main_adb_copied">ADB-kommandot har kopierats</string>
|
||||
<string name="main_pick_roms">Välj ROM-mappen</string>
|
||||
<string name="main_non_adreno_title">Inte en Adreno-GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip stöder Adreno-GPU:er. På %1$s kan Steam visa en svart skärm. Nedladdning: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU stöds inte</string>
|
||||
<string name="main_non_adreno_text">Ingen kompatibel grafikbackend är tillgänglig för %1$s. Steam kan visa en svart skärm. Hämtning: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Installera trots det</string>
|
||||
<string name="main_remove_runtime_title">Ta bort Linux-körmiljön</string>
|
||||
<string name="main_remove_runtime_text">Det här tar bort körmiljön, Steam-klienten i den och alla spel som är installerade där.</string>
|
||||
@@ -2117,7 +2117,7 @@
|
||||
<string name="gpuinfo_experimental">Experimentell: dess drivrutiner är testversioner</string>
|
||||
<string name="gpuinfo_a850_experimental">Experimentellt stöd för Adreno 850: kräver kompatibla experimentella skärm- och speldrivrutiner; inte verifierat på den här enheten</string>
|
||||
<string name="gpuinfo_below_tested">Under testad maskinvara (Adreno 725 och nyare): det kan hända att det inte fungerar</string>
|
||||
<string name="gpuinfo_unsupported">Stöds inte: DroidDeck behöver en Adreno-GPU (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Stöds inte: ingen kompatibel grafikbackend är tillgänglig för denna GPU</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHubs hastighetsgräns nåddes - försök igen senare</string>
|
||||
<string name="turnip_github_http">GitHub svarade HTTP %1$d</string>
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">ไม่รองรับ GPU นี้</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam ต้องใช้ไดรเวอร์ Adreno; %1$s อาจแสดงจอดำ</string>
|
||||
<string name="setup_gpu_unsupported_detail">ไม่มีแบ็กเอนด์กราฟิกที่เข้ากันได้สำหรับ %1$s Steam อาจแสดงหน้าจอสีดำ</string>
|
||||
<string name="setup_runtime">รันไทม์ Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">ยังไม่ติดตั้ง · ประมาณ 3 GB จะติดตั้งเมื่อกด Play ครั้งแรก</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">Wireless debugging ไม่พร้อมใช้งาน</string>
|
||||
<string name="main_adb_copied">คัดลอกคำสั่ง ADB แล้ว</string>
|
||||
<string name="main_pick_roms">เลือกโฟลเดอร์ ROM</string>
|
||||
<string name="main_non_adreno_title">ไม่ใช่ GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip รองรับ GPU Adreno บน %1$s Steam อาจแสดงจอดำ ดาวน์โหลด: %2$.0f MB</string>
|
||||
<string name="main_non_adreno_title">ไม่รองรับ GPU</string>
|
||||
<string name="main_non_adreno_text">ไม่มีแบ็กเอนด์กราฟิกที่เข้ากันได้สำหรับ %1$s Steam อาจแสดงหน้าจอสีดำ ดาวน์โหลด: %2$.0f MB</string>
|
||||
<string name="main_install_anyway">ติดตั้งอยู่ดี</string>
|
||||
<string name="main_remove_runtime_title">ลบรันไทม์ Linux</string>
|
||||
<string name="main_remove_runtime_text">การลบนี้จะลบรันไทม์ ไคลเอนต์ Steam ภายใน และเกมทุกเกมที่ติดตั้งอยู่ในนั้น</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">ทดลอง: ไดรเวอร์เป็นบิลด์ทดสอบ</string>
|
||||
<string name="gpuinfo_a850_experimental">รองรับ Adreno 850 แบบทดลอง: ต้องใช้ไดรเวอร์จอแสดงผลและไดรเวอร์เกมแบบทดลองที่เข้ากันได้ ยังไม่ได้ตรวจสอบบนอุปกรณ์นี้</string>
|
||||
<string name="gpuinfo_below_tested">ต่ำกว่าฮาร์ดแวร์ที่ทดสอบแล้ว (Adreno 725 และรุ่นใหม่กว่า): อาจรันไม่ได้</string>
|
||||
<string name="gpuinfo_unsupported">ไม่รองรับ: DroidDeck ต้องใช้ GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">ไม่รองรับ: ไม่มีแบ็กเอนด์กราฟิกที่เข้ากันได้สำหรับ GPU นี้</string>
|
||||
|
||||
<string name="turnip_rate_limited">ติดขีดจำกัดอัตราของ GitHub - ลองใหม่ในหลัง</string>
|
||||
<string name="turnip_github_http">GitHub ตอบกลับด้วย HTTP %1$d</string>
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU desteklenmiyor</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam için Adreno sürücüsü gerekir; %1$s siyah ekran gösterebilir</string>
|
||||
<string name="setup_gpu_unsupported_detail">%1$s için uyumlu bir grafik arka ucu yok. Steam siyah ekran gösterebilir.</string>
|
||||
<string name="setup_runtime">Linux çalışma zamanı</string>
|
||||
<string name="setup_runtime_progress">%1$s · %%%2$d</string>
|
||||
<string name="setup_runtime_missing">Yüklü değil · yaklaşık 3 GB, ilk kez Oyna\'ya basınca yüklenir</string>
|
||||
@@ -1394,8 +1394,8 @@
|
||||
<string name="main_wadb_unavailable">Kablosuz hata ayıklama kullanılamıyor</string>
|
||||
<string name="main_adb_copied">ADB komutu kopyalandı</string>
|
||||
<string name="main_pick_roms">ROM klasörünü seçin</string>
|
||||
<string name="main_non_adreno_title">Adreno GPU değil</string>
|
||||
<string name="main_non_adreno_text">Turnip, Adreno GPU\'larını destekler. %1$s üzerinde Steam siyah ekran gösterebilir. İndirme: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Desteklenmeyen GPU</string>
|
||||
<string name="main_non_adreno_text">%1$s için uyumlu bir grafik arka ucu yok. Steam siyah ekran gösterebilir. İndirme: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Yine de yükle</string>
|
||||
<string name="main_remove_runtime_title">Linux çalışma ortamını kaldır</string>
|
||||
<string name="main_remove_runtime_text">Bu işlem çalışma ortamını, içindeki Steam istemcisini ve oraya yüklenen tüm oyunları siler.</string>
|
||||
@@ -2109,7 +2109,7 @@
|
||||
<string name="gpuinfo_experimental">Deneysel: sürücüleri test derlemeleri</string>
|
||||
<string name="gpuinfo_a850_experimental">Deneysel Adreno 850 desteği: uyumlu deneysel ekran ve oyun sürücüleri gerekir; bu cihazda doğrulanmadı</string>
|
||||
<string name="gpuinfo_below_tested">Test edilen donanımın altında (Adreno 725 ve daha yenisi): çalışmayabilir</string>
|
||||
<string name="gpuinfo_unsupported">Desteklenmiyor: DroidDeck bir Adreno (Snapdragon) GPU\'su gerektirir</string>
|
||||
<string name="gpuinfo_unsupported">Desteklenmiyor: bu GPU için uyumlu bir grafik arka ucu yok</string>
|
||||
|
||||
<string name="turnip_rate_limited">GitHub\'ın hız sınırına ulaşıldı - daha sonra yeniden deneyin</string>
|
||||
<string name="turnip_github_http">GitHub HTTP %1$d yanıtı verdi</string>
|
||||
|
||||
@@ -819,7 +819,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU не підтримується</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam потребує драйвера Adreno; %1$s може показувати чорний екран</string>
|
||||
<string name="setup_gpu_unsupported_detail">Для %1$s немає сумісного графічного бекенда. Steam може показувати чорний екран.</string>
|
||||
<string name="setup_runtime">Середовище Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Не встановлено · приблизно 3 GB, встановлюється під час першого натискання «Грати»</string>
|
||||
@@ -1426,8 +1426,8 @@
|
||||
<string name="main_wadb_unavailable">Бездротове налагодження недоступне</string>
|
||||
<string name="main_adb_copied">Команду ADB скопійовано</string>
|
||||
<string name="main_pick_roms">Виберіть папку ROMs</string>
|
||||
<string name="main_non_adreno_title">GPU не Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip підтримує GPU Adreno. На %1$s Steam може показувати чорний екран. Завантаження: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Непідтримуваний GPU</string>
|
||||
<string name="main_non_adreno_text">Для %1$s немає сумісного графічного бекенда. Steam може показувати чорний екран. Завантаження: %2$.0f МБ.</string>
|
||||
<string name="main_install_anyway">Усе одно встановити</string>
|
||||
<string name="main_remove_runtime_title">Видалити середовище Linux</string>
|
||||
<string name="main_remove_runtime_text">Буде видалено середовище, клієнт Steam у ньому та всі встановлені там ігри.</string>
|
||||
@@ -2167,7 +2167,7 @@
|
||||
<string name="gpuinfo_experimental">Експериментально: його драйвери - тестові збірки</string>
|
||||
<string name="gpuinfo_a850_experimental">Експериментальна підтримка Adreno 850: потрібні сумісні експериментальні драйвери дисплея та ігор; не перевірено на цьому пристрої</string>
|
||||
<string name="gpuinfo_below_tested">Нижче за протестоване обладнання (Adreno 725 і новіші): може не запрацювати</string>
|
||||
<string name="gpuinfo_unsupported">Не підтримується: DroidDeck потребує GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Не підтримується: для цього GPU немає сумісного графічного бекенда</string>
|
||||
|
||||
<string name="turnip_rate_limited">Перевищено ліміт запитів GitHub - спробуйте пізніше</string>
|
||||
<string name="turnip_github_http">GitHub відповів HTTP %1$d</string>
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU không được hỗ trợ</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam cần driver Adreno; %1$s có thể hiện màn hình đen</string>
|
||||
<string name="setup_gpu_unsupported_detail">Không có backend đồ họa tương thích cho %1$s. Steam có thể hiển thị màn hình đen.</string>
|
||||
<string name="setup_runtime">Runtime Linux</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Chưa cài · khoảng 3 GB, được cài vào lần Play đầu tiên</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">Gỡ lỗi qua mạng không dây không khả dụng</string>
|
||||
<string name="main_adb_copied">Đã sao chép lệnh ADB</string>
|
||||
<string name="main_pick_roms">Chọn thư mục ROM</string>
|
||||
<string name="main_non_adreno_title">Không phải GPU Adreno</string>
|
||||
<string name="main_non_adreno_text">Turnip hỗ trợ GPU Adreno. Trên %1$s, Steam có thể hiện màn hình đen. Tải xuống: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">GPU không được hỗ trợ</string>
|
||||
<string name="main_non_adreno_text">Không có backend đồ họa tương thích cho %1$s. Steam có thể hiển thị màn hình đen. Tải xuống: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Vẫn cài</string>
|
||||
<string name="main_remove_runtime_title">Gỡ runtime Linux</string>
|
||||
<string name="main_remove_runtime_text">Việc này xóa runtime, client Steam bên trong nó, và mọi game đã cài ở đó.</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">Thử nghiệm: driver của nó là bản build thử nghiệm</string>
|
||||
<string name="gpuinfo_a850_experimental">Hỗ trợ thử nghiệm Adreno 850: cần driver hiển thị và driver game thử nghiệm tương thích; chưa được xác minh trên thiết bị này</string>
|
||||
<string name="gpuinfo_below_tested">Dưới mức phần cứng đã kiểm thử (Adreno 725 và mới hơn): có thể không chạy được</string>
|
||||
<string name="gpuinfo_unsupported">Không được hỗ trợ: DroidDeck cần GPU Adreno (Snapdragon)</string>
|
||||
<string name="gpuinfo_unsupported">Không được hỗ trợ: không có backend đồ họa tương thích cho GPU này</string>
|
||||
|
||||
<string name="turnip_rate_limited">Đã đạt giới hạn tốc độ của GitHub - hãy thử lại sau</string>
|
||||
<string name="turnip_github_http">GitHub trả về HTTP %1$d</string>
|
||||
|
||||
@@ -811,7 +811,7 @@
|
||||
<string name="setup_gpu_bridged">GPU 通过 Mali 桥接</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s:%2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam 需要 Adreno 驱动;%1$s 可能显示黑屏</string>
|
||||
<string name="setup_gpu_unsupported_detail">没有可用的兼容图形后端;%1$s 可能显示黑屏</string>
|
||||
<string name="setup_runtime">Linux 运行时</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">未安装 · 约 3 GB,首次点“游玩”时安装</string>
|
||||
@@ -1474,8 +1474,8 @@
|
||||
<string name="main_wadb_unavailable">无线调试不可用</string>
|
||||
<string name="main_adb_copied">已复制 ADB 命令</string>
|
||||
<string name="main_pick_roms">选择 ROM 文件夹</string>
|
||||
<string name="main_non_adreno_title">不是 Adreno GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip 支持 Adreno GPU。在 %1$s 上,Steam 可能显示黑屏。需下载:%2$.0f MB。</string>
|
||||
<string name="main_non_adreno_title">不受支持的 GPU</string>
|
||||
<string name="main_non_adreno_text">没有适用于 %1$s 的兼容图形后端。Steam 可能显示黑屏。需下载:%2$.0f MB。</string>
|
||||
<string name="main_install_anyway">仍要安装</string>
|
||||
<string name="main_remove_runtime_title">移除 Linux 运行时</string>
|
||||
<string name="main_remove_runtime_text">这会删除运行时、其中的 Steam 客户端,以及安装在其中的所有游戏。</string>
|
||||
@@ -2171,7 +2171,7 @@
|
||||
<string name="gpuinfo_experimental">实验性:其驱动为测试构建</string>
|
||||
<string name="gpuinfo_a850_experimental">Adreno 850 实验支持:需要配套的实验版显示驱动和游戏驱动;尚未在此设备上验证</string>
|
||||
<string name="gpuinfo_below_tested">低于已测试的硬件(Adreno 725 及更新):可能无法运行</string>
|
||||
<string name="gpuinfo_unsupported">不受支持:DroidDeck 需要 Adreno(骁龙)GPU</string>
|
||||
<string name="gpuinfo_unsupported">不受支持:没有适用于此 GPU 的兼容图形后端</string>
|
||||
<string name="gpuinfo_bridge">通过 Mali 桥接运行,使用手机自带的 Vulkan 驱动</string>
|
||||
<string name="gpuinfo_bridge_off">关闭 Mali 桥接时不受支持:Turnip 需要 Adreno(骁龙)GPU</string>
|
||||
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU 不受支援</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s:%2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam 需要 Adreno 驅動程式;%1$s 可能會出現黑畫面</string>
|
||||
<string name="setup_gpu_unsupported_detail">沒有可用的兼容圖形後端;%1$s 可能會出現黑畫面</string>
|
||||
<string name="setup_runtime">Linux 運行環境</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">未安裝 · 約 3 GB,首次開啟 Steam 時安裝</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">無法使用無線偵錯</string>
|
||||
<string name="main_adb_copied">已複製 ADB 指令</string>
|
||||
<string name="main_pick_roms">選擇 ROM 資料夾</string>
|
||||
<string name="main_non_adreno_title">不是 Adreno GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
|
||||
<string name="main_non_adreno_title">不受支援的 GPU</string>
|
||||
<string name="main_non_adreno_text">沒有適用於 %1$s 的兼容圖形後端。Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
|
||||
<string name="main_install_anyway">仍要安裝</string>
|
||||
<string name="main_remove_runtime_title">移除 Linux 運行環境</string>
|
||||
<string name="main_remove_runtime_text">此操作會刪除運行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string>
|
||||
<string name="gpuinfo_a850_experimental">Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證</string>
|
||||
<string name="gpuinfo_below_tested">低於已測試的硬件(Adreno 725 或更新):可能無法執行</string>
|
||||
<string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno (Snapdragon) GPU</string>
|
||||
<string name="gpuinfo_unsupported">不受支援:沒有適用於此 GPU 的兼容圖形後端</string>
|
||||
|
||||
<string name="turnip_rate_limited">已達 GitHub 的速率限制 - 請稍後再試</string>
|
||||
<string name="turnip_github_http">GitHub 回應 HTTP %1$d</string>
|
||||
|
||||
@@ -810,7 +810,7 @@
|
||||
<string name="setup_gpu_unsupported">GPU 不受支援</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s:%2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam 需要 Adreno 驅動程式;%1$s 可能出現黑畫面。</string>
|
||||
<string name="setup_gpu_unsupported_detail">沒有可用的相容圖形後端;%1$s 可能出現黑畫面。</string>
|
||||
<string name="setup_runtime">Linux 執行環境</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">未安裝 · 約 3 GB,首次點擊「執行」時安裝</string>
|
||||
@@ -1390,8 +1390,8 @@
|
||||
<string name="main_wadb_unavailable">無法使用無線偵錯</string>
|
||||
<string name="main_adb_copied">已複製 ADB 指令</string>
|
||||
<string name="main_pick_roms">選擇 ROM 資料夾</string>
|
||||
<string name="main_non_adreno_title">不是 Adreno GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
|
||||
<string name="main_non_adreno_title">不受支援的 GPU</string>
|
||||
<string name="main_non_adreno_text">沒有適用於 %1$s 的相容圖形後端。Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
|
||||
<string name="main_install_anyway">仍要安裝</string>
|
||||
<string name="main_remove_runtime_title">移除 Linux 執行環境</string>
|
||||
<string name="main_remove_runtime_text">這會刪除執行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string>
|
||||
@@ -2092,7 +2092,7 @@
|
||||
<string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string>
|
||||
<string name="gpuinfo_a850_experimental">Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證</string>
|
||||
<string name="gpuinfo_below_tested">低於已測試的硬體(Adreno 725 及更新機型):可能無法執行</string>
|
||||
<string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno(Snapdragon)GPU</string>
|
||||
<string name="gpuinfo_unsupported">不受支援:沒有適用於此 GPU 的相容圖形後端</string>
|
||||
|
||||
<string name="turnip_rate_limited">已達 GitHub 速率限制 - 請稍後再試</string>
|
||||
<string name="turnip_github_http">GitHub 回應 HTTP %1$d</string>
|
||||
|
||||
@@ -210,6 +210,7 @@
|
||||
<string name="session_proot_missing">DroidDeck\'s proot could not be downloaded. Check the connection and try again. The session was not started, because the fallback proot is too slow to play on.</string>
|
||||
<string name="session_directaudio_missing">DirectAudio could not be downloaded, so game audio uses the slower path this session.</string>
|
||||
<string name="session_end_program">This program</string>
|
||||
<string name="session_end_present_fault">The session could not draw to the screen: %1$s. This screen shows why instead of a black one; the session logs have the details.</string>
|
||||
<string name="session_end_steam_download">Couldn\'t download the Steam client. Your network or VPN may be blocking Steam\'s servers; check the connection and try again.</string>
|
||||
<string name="session_end_steam_setup">Couldn\'t set up the Steam client. Check your connection and free storage, then try again.</string>
|
||||
<string name="session_end_fex_missing">%1$s is built for x86 PCs and runs through FEX, which is not installed yet. Start Steam once while signed in and let it download FEX and the Steam Linux Runtime, then start this again.</string>
|
||||
@@ -888,7 +889,7 @@
|
||||
<string name="setup_gpu_bridged">GPU via Mali bridge</string>
|
||||
<string name="setup_gpu_detail">%1$s · %2$s</string>
|
||||
<string name="setup_gpu_untested_detail">%1$s: %2$s</string>
|
||||
<string name="setup_gpu_unsupported_detail">Steam needs an Adreno driver; %1$s may show a black screen</string>
|
||||
<string name="setup_gpu_unsupported_detail">No compatible graphics backend is available; %1$s may show a black screen</string>
|
||||
<string name="setup_runtime">Linux runtime</string>
|
||||
<string name="setup_runtime_progress">%1$s · %2$d%%</string>
|
||||
<string name="setup_runtime_missing">Not installed · about 3 GB, installed on first Play</string>
|
||||
@@ -927,6 +928,14 @@
|
||||
<string name="setup_game_debug_logs_restart">Restart this session to save game debug logs</string>
|
||||
<string name="setup_logging_update_failed">Couldn\'t update logging. Check the switch and try again.</string>
|
||||
<string name="setup_agent_commands">Debugging tools</string>
|
||||
<string name="setup_graphics_logs">Detailed graphics logs</string>
|
||||
<string name="setup_steam_gpu_drawing">Steam interface: GPU drawing</string>
|
||||
<string name="setup_steam_gpu_drawing_hint">With the Xclipse driver pair, Steam draws its pages in software (GPU drawing showed broken tiles). Turn on for a faster Steam interface if it looks right on this phone.</string>
|
||||
<string name="drivers_built_on">built %1$s</string>
|
||||
<string name="drivers_pair_updated">%1$s updated to the newest build</string>
|
||||
<string name="setup_gpu_bridge_fell_back">The Mali bridge failed its self-test on %1$s last session (images or draws), so the session used the PanVK driver pair instead.</string>
|
||||
<string name="setup_gpu_bridge_no_memory_sharing">The Mali bridge found no way to share memory with the driver on %1$s last session, so the session used the PanVK driver pair instead. The bridge cannot work on this phone; turning it off keeps the PanVK pair.</string>
|
||||
<string name="setup_graphics_logs_hint">Each game\'s Proton, DXVK and driver logs in the session\'s logs. Always on when the GPU is not an Adreno.</string>
|
||||
<string name="setup_latest_logs">Latest session logs</string>
|
||||
<string name="setup_saved_logs">Saved session logs</string>
|
||||
<string name="setup_saved_logs_hint">Keeps the newest %1$d</string>
|
||||
@@ -1552,8 +1561,8 @@
|
||||
<string name="main_wadb_unavailable">Wireless debugging is unavailable</string>
|
||||
<string name="main_adb_copied">ADB command copied</string>
|
||||
<string name="main_pick_roms">Choose the ROMs folder</string>
|
||||
<string name="main_non_adreno_title">Not an Adreno GPU</string>
|
||||
<string name="main_non_adreno_text">Turnip supports Adreno GPUs. On %1$s, Steam may show a black screen. Download: %2$.0f MB.</string>
|
||||
<string name="main_non_adreno_title">Unsupported GPU</string>
|
||||
<string name="main_non_adreno_text">No compatible graphics backend is available for %1$s. Steam may show a black screen. Download: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Install anyway</string>
|
||||
<string name="main_remove_runtime_title">Remove Linux runtime</string>
|
||||
<string name="main_remove_runtime_text">This deletes the runtime, the Steam client inside it, and every game installed there.</string>
|
||||
@@ -2310,11 +2319,27 @@
|
||||
<string name="look_anime_edge_desc">Toon outlines plus sharpening.</string>
|
||||
|
||||
<string name="gpuinfo_not_adreno">Not an Adreno GPU</string>
|
||||
<string name="gpuinfo_powervr">System PowerVR driver: compatibility varies by device</string>
|
||||
<string name="gpuinfo_xclipse">Experimental: RADV or the system Xclipse driver</string>
|
||||
<string name="gpu_system_powervr">System PowerVR</string>
|
||||
<string name="gpu_auto_system_hint">Uses the built-in drivers for this GPU</string>
|
||||
<string name="drivers_powervr_builtin">The system PowerVR driver is selected for the runtime and display.</string>
|
||||
<string name="drivers_mali_bridge_auto">No open driver covers this Mali yet: the Mali bridge is on.</string>
|
||||
<string name="upd_block_non_adreno_test">This is the non-Snapdragon test build: the official builds here only run on Adreno, so they are not installed over it. New test builds are on github.com/The412Banner/droiddeck-nonadreno-drivers.</string>
|
||||
<string name="session_killed_for_memory">The last session ended because Android closed DroidDeck to free memory, not a crash. Close other apps or lower the game\'s texture settings, then try again.</string>
|
||||
<string name="drivers_route_venus">Mali (Venus)</string>
|
||||
<string name="drivers_route_bridge">the Mali bridge</string>
|
||||
<string name="drivers_mali_no_pair_auto">No open driver covers this Mali yet: %1$s is used, on the phone\'s own driver.</string>
|
||||
<string name="drivers_mali_unpublished_auto">%1$s isn\'t published yet: %2$s is used until it is.</string>
|
||||
<string name="drivers_mali_old_vulkan_bridge">%1$s isn\'t available, and this phone\'s driver reports Vulkan %2$s (Venus needs 1.2): the Mali bridge is used.</string>
|
||||
<string name="drivers_mali_bridge_fallback">%1$s isn\'t published yet: the Mali bridge is on until it is.</string>
|
||||
<string name="cmgr_powervr_dxvk_queued">Queued: %1$s (waiting for a different runtime driver)</string>
|
||||
<string name="cmgr_powervr_dxvk_managed">DXVK is provided by the system PowerVR runtime. Other DXVK packages can be stored but are not applied with this driver.</string>
|
||||
<string name="gpuinfo_supported">Supported</string>
|
||||
<string name="gpuinfo_experimental">Experimental: its drivers are test builds</string>
|
||||
<string name="gpuinfo_a850_experimental">Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device</string>
|
||||
<string name="gpuinfo_below_tested">Below tested hardware (Adreno 725 and newer): it may not run</string>
|
||||
<string name="gpuinfo_unsupported">Not supported: DroidDeck needs an Adreno (Snapdragon) GPU</string>
|
||||
<string name="gpuinfo_unsupported">Not supported: no compatible graphics backend is available for this GPU</string>
|
||||
<string name="gpuinfo_bridge">Runs through the Mali bridge, on the phone\'s own Vulkan driver</string>
|
||||
<string name="gpuinfo_bridge_off">Not supported with the Mali bridge off: Turnip needs an Adreno (Snapdragon) GPU</string>
|
||||
|
||||
@@ -2869,6 +2894,13 @@
|
||||
<string name="gog_achievements_enable_store">Enable GOG achievements in Stores settings</string>
|
||||
<string name="gog_achievements_unavailable">Achievements unavailable; check session logs</string>
|
||||
<string name="achievement_unlocked">Achievement unlocked</string>
|
||||
<string name="setup_gpu_venus_off">Mali (Venus): off</string>
|
||||
<string name="setup_gpu_venus_on">Mali (Venus): on</string>
|
||||
<string name="setup_gpu_venus_host_too_old">The driver on %1$s reports Vulkan %2$s. Mali (Venus) needs Vulkan 1.2 or newer (gamescope does not start on older), so sessions skip Venus and use the PanVK driver pair instead.</string>
|
||||
<string name="setup_gpu_bridge_off">Mali bridge: off</string>
|
||||
<string name="setup_gpu_bridge_unusable">%1$s cannot use the Mali bridge (it needs a Mali GPU without a system-driver route), so this switch has no effect here.</string>
|
||||
<string name="setup_gpu_bridge_on">Mali bridge: on</string>
|
||||
<string name="setup_gpu_bridge_detail">%1$s is not an Adreno. The experimental Vulkan bridge is on: the session draws through the phone\'s own driver. Expect bugs; send the session logs.</string>
|
||||
<string name="touch_paste">Paste</string>
|
||||
<string name="touch_dpad">D-pad</string>
|
||||
<string name="touch_left_stick">Left stick</string>
|
||||
@@ -3644,6 +3676,11 @@
|
||||
<string name="sync_web_title">Syncthing</string>
|
||||
<string name="sync_web_reload">Reload</string>
|
||||
<string name="sync_not_running">Syncthing isn\'t running.</string>
|
||||
<string name="setup_bridge_safe">Safe mode</string>
|
||||
<string name="setup_bridge_safe_on">Safe mode: on</string>
|
||||
<string name="setup_bridge_safe_off">Safe mode: off</string>
|
||||
<string name="setup_bridge_safe_on_detail">Most compatible. Recommended for a new device.</string>
|
||||
<string name="setup_bridge_safe_off_detail">Faster where this GPU passed its checks.</string>
|
||||
<string name="session_end_steam_sigill">Steam stopped with an illegal instruction (SIGILL). This can be a CPU instruction compatibility problem or a client crash. Share the logs with your device details.</string>
|
||||
<string name="session_end_steam_try_x64">You can try Steam settings → Steam client → x86-64 (experimental). It needs an additional download and may run more slowly.</string>
|
||||
<string name="session_end_vulkan_device">The Linux graphics driver could not find a usable Vulkan device. Check that the selected Linux GPU driver supports this GPU.</string>
|
||||
@@ -3723,19 +3760,11 @@
|
||||
<string name="stores_xbox_cloud_settings">Cloud settings</string>
|
||||
<string name="xbox_cloud_gaming" translatable="false">Xbox Cloud Gaming</string>
|
||||
<string name="xbox_filter_game_pass" translatable="false">Game Pass</string>
|
||||
<string name="setup_gpu_bridge_off">Mali bridge: off</string>
|
||||
<string name="setup_gpu_bridge_on">Mali bridge: on</string>
|
||||
<string name="setup_bridge_safe">Safe mode</string>
|
||||
<string name="setup_bridge_safe_on">Safe mode: on</string>
|
||||
<string name="setup_bridge_safe_off">Safe mode: off</string>
|
||||
<string name="setup_bridge_safe_on_detail">Most compatible. Recommended for a new device.</string>
|
||||
<string name="setup_bridge_safe_off_detail">Faster where this GPU passed its checks.</string>
|
||||
<string name="setup_bridge_dxvk">DXVK</string>
|
||||
<string name="setup_bridge_dxvk_auto">DXVK: automatic</string>
|
||||
<string name="setup_bridge_dxvk_proton">DXVK: Proton\'s own</string>
|
||||
<string name="setup_bridge_dxvk_auto_detail">Uses an older DXVK where this GPU\'s Vulkan is too old for Proton\'s.</string>
|
||||
<string name="setup_bridge_dxvk_proton_detail">Every Proton keeps its own DXVK, even where this GPU\'s Vulkan may be too old for it.</string>
|
||||
<string name="setup_gpu_bridge_detail">%1$s is not an Adreno. The experimental Vulkan bridge is on: the session draws through the phone\'s own driver. Expect bugs; send the session logs.</string>
|
||||
<string name="integration_title">Steam integration</string>
|
||||
<string name="integration_count">%1$d of %2$d on</string>
|
||||
<string name="integration_set_up">Set up</string>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class DeviceSupportTest {
|
||||
@Test fun runtimeDownloadAcceptsOnlyRecognizedGraphicsBackends() {
|
||||
assertTrue(DeviceSupport.graphicsAvailable(adreno = true, powerVrReady = false))
|
||||
assertTrue(DeviceSupport.graphicsAvailable(adreno = false, powerVrReady = true))
|
||||
// A PowerVR HAL without usable DRM access also reaches this unavailable case.
|
||||
assertFalse(DeviceSupport.graphicsAvailable(adreno = false, powerVrReady = false))
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,8 @@ class WinCompSourcesTest {
|
||||
|
||||
@Test fun steamworksSharedDepotsMapToComponents() {
|
||||
assertEquals(listOf("vcredist2015_dll" to "VC++ 2015", "d3dx9" to "DirectX (June 2010)"), WinCompSources.steamDepots(listOf("228986", "228990", "999")))
|
||||
// Hotline Miami's OpenAL depot: the DLL-copy entry, which installs without running anything.
|
||||
assertEquals(listOf("oalinst_dll" to "OpenAL"), WinCompSources.steamDepots(listOf("229020")))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class DeviceInfoTest {
|
||||
@Test fun builtinLabelsDistinguishPowerVrFromTurnipAndGenericSystemVulkan() {
|
||||
assertEquals("System PowerVR", DeviceInfo.builtinDriverLabel(powerVr = true, runtime = true))
|
||||
assertEquals("System PowerVR", DeviceInfo.builtinDriverLabel(powerVr = true, runtime = false))
|
||||
assertEquals("the runtime's own Turnip", DeviceInfo.builtinDriverLabel(powerVr = false, runtime = true))
|
||||
assertEquals("System Vulkan", DeviceInfo.builtinDriverLabel(powerVr = false, runtime = false))
|
||||
}
|
||||
}
|
||||
@@ -129,4 +129,19 @@ class DriverBundleTest {
|
||||
assertEquals(2, DriverBundle.all(context).size)
|
||||
assertFalse(first.androidId == second.androidId)
|
||||
}
|
||||
|
||||
@Test fun failedNonAdrenoReplacementKeepsTheInstalledPairAndSelection() {
|
||||
manifest.put("tag", "PanVK-G610-v1")
|
||||
val first = DriverBundle.install(context, bundle())
|
||||
DriverBundle.select(context, first)
|
||||
try {
|
||||
DriverBundle.install(context, bundle(linuxLibc = "libc.so"))
|
||||
fail("Accepted a bionic Linux driver")
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
assertEquals(first.id, DriverBundle.active(context)?.id)
|
||||
assertTrue(TurnipDriver(context).isInstalled(first.androidId))
|
||||
assertTrue(LinuxVulkanDriverManager(context).isInstalled(first.linuxId))
|
||||
assertEquals(1, DriverBundle.all(context).size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class DriverPairUpdateTest {
|
||||
private fun pair(sha: String, id: Long, updated: String, key: String = DriverPairs.nonAdreno("PanVK-Mali-G615")) =
|
||||
TurnipReleases.Asset("Non-Adreno", "panvk-mali-g615", "PanVK-Mali-G615-Pair.zip", "https://example/p", 9_000_000, false,
|
||||
"label", sha, key, bundle = true, assetId = id, updatedAt = updated)
|
||||
|
||||
@Test fun aRebuildUnderTheSameTagIsAnUpdate() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val installed = pair("a".repeat(64), 1, "2026-10-13T10:00:00Z")
|
||||
assertFalse("nothing installed yet", TurnipReleases.updateAvailable(context, installed))
|
||||
TurnipReleases.recordDownload(context, installed, "PanVK-Mali-G615")
|
||||
assertFalse(TurnipReleases.updateAvailable(context, installed))
|
||||
assertEquals("2026-10-13", TurnipReleases.installedBuiltOn(context, installed))
|
||||
assertTrue(TurnipReleases.updateAvailable(context, pair("b".repeat(64), 2, "2026-10-15T10:00:00Z")))
|
||||
assertTrue("a new asset id alone", TurnipReleases.updateAvailable(context, pair("a".repeat(64), 2, "2026-10-13T10:00:00Z")))
|
||||
assertEquals("2026-10-15", pair("b".repeat(64), 2, "2026-10-15T10:00:00Z").builtOn)
|
||||
}
|
||||
|
||||
@Test fun adrenoPairsAreNeverFlagged() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val turnip = pair("a".repeat(64), 1, "2026-10-13T10:00:00Z", key = DriverPairs.BANNER)
|
||||
TurnipReleases.recordDownload(context, turnip, "Turnip-x")
|
||||
assertFalse(TurnipReleases.updateAvailable(context, pair("b".repeat(64), 2, "2026-10-15T10:00:00Z", key = DriverPairs.BANNER)))
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,23 @@ class DriverPairsTest {
|
||||
assertEquals(GpuInfo.Support.UNSUPPORTED, gpu(0, adreno = false).support)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun powerVrUsesItsOwnFamilyWithoutClaimingAllModelsAreTested() {
|
||||
assertEquals(Family.POWERVR, GpuInfo.familyOf(false, 0, powerVr = true))
|
||||
assertEquals(Family.NOT_ADRENO, GpuInfo.familyOf(false, 0, powerVr = false))
|
||||
assertEquals(Family.A7XX, GpuInfo.familyOf(true, 740, powerVr = true))
|
||||
val powerVr = GpuInfo("PowerVR", 0, Family.POWERVR, "", false)
|
||||
assertEquals(GpuInfo.Support.UNTESTED, powerVr.support)
|
||||
assertNull("PowerVR never downloads an Adreno driver pair", DriverPairs.recommendedKey(powerVr, emptyList()))
|
||||
val pairs = DriverPairs.from(TurnipReleases.Check(
|
||||
listOf(asset("Turnip-r4.zip", "r4", false, DriverPairs.BANNER),
|
||||
asset("Turnip-r4-Linux.zip", "r4", true, DriverPairs.BANNER)),
|
||||
emptyList(), emptyList(), 0L,
|
||||
))
|
||||
assertFalse(pairs.single().suits(powerVr))
|
||||
assertNull(DriverPairs.recommendedKey(powerVr, pairs))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun autoPicksWinNativeBalancedOn8xxAndBannersElsewhere() {
|
||||
assertEquals(DriverPairs.WN_BALANCED, DriverPairs.recommendedKey(gpu(830), emptyList()))
|
||||
@@ -116,4 +133,134 @@ class DriverPairsTest {
|
||||
assertEquals(DriverPairs.BANNER_ONEUI, DriverPairs.recommendedKey(gpu(740, oneUi = true), pairs))
|
||||
assertEquals(DriverPairs.BANNER, DriverPairs.recommendedKey(gpu(650), pairs))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun maliFamiliesFollowTheOpenDriverBuilds() {
|
||||
assertEquals(Family.MALI_JM, GpuInfo.familyOf(false, 0, mali = "Mali-G57 MC2"))
|
||||
assertEquals(Family.MALI_JM, GpuInfo.familyOf(false, 0, mali = "Mali-G77"))
|
||||
assertEquals(Family.MALI_G610_G710, GpuInfo.familyOf(false, 0, mali = "Mali-G710"))
|
||||
assertEquals(Family.MALI_G615, GpuInfo.familyOf(false, 0, mali = "Mali-G615 MC6"))
|
||||
assertEquals(Family.MALI_G720, GpuInfo.familyOf(false, 0, mali = "Mali-G720-Immortalis MC12"))
|
||||
assertEquals(Family.MALI_LATEST, GpuInfo.familyOf(false, 0, mali = "Mali-G925-Immortalis MC12"))
|
||||
assertEquals(Family.MALI_OTHER, GpuInfo.familyOf(false, 0, mali = "Mali-T880"))
|
||||
assertEquals(Family.MALI_OTHER, GpuInfo.familyOf(false, 0, mali = ""))
|
||||
// An Adreno is an Adreno whatever else the device reports, and the system HAL keeps priority.
|
||||
assertEquals(Family.A7XX, GpuInfo.familyOf(true, 740, mali = "Mali-G720"))
|
||||
assertEquals(Family.POWERVR, GpuInfo.familyOf(false, 0, powerVr = true, mali = "Mali-G720"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun autoPicksTheOpenDriverForEachMaliAndNothingForTheRest() {
|
||||
fun mali(name: String) = GpuInfo(name, 0, GpuInfo.familyOf(false, 0, mali = name), "", false)
|
||||
// Android 14: the per-generation builds; the newest Malis have none and use the bridge.
|
||||
assertEquals(DriverPairs.NAD_MALI_JM, DriverPairs.recommendedKey(mali("Mali-G57"), emptyList(), sdk = 34))
|
||||
assertEquals(DriverPairs.NAD_MALI_G710, DriverPairs.recommendedKey(mali("Mali-G710"), emptyList(), sdk = 34))
|
||||
assertEquals(DriverPairs.NAD_MALI_G615, DriverPairs.recommendedKey(mali("Mali-G615"), emptyList(), sdk = 34))
|
||||
assertEquals(DriverPairs.NAD_MALI_G720, DriverPairs.recommendedKey(mali("Mali-G720"), emptyList(), sdk = 34))
|
||||
assertNull(DriverPairs.recommendedKey(mali("Mali-G925-Immortalis MC12"), emptyList(), sdk = 34))
|
||||
// Android 15+: the one-binary kbase build for every Mali it covers, not the Bifrost G52/G76.
|
||||
assertEquals(DriverPairs.NAD_MALI_KBASE, DriverPairs.recommendedKey(mali("Mali-G57"), emptyList(), sdk = 35))
|
||||
assertEquals(DriverPairs.NAD_MALI_KBASE, DriverPairs.recommendedKey(mali("Mali-G720"), emptyList(), sdk = 36))
|
||||
assertEquals(DriverPairs.NAD_MALI_KBASE, DriverPairs.recommendedKey(mali("Mali-G925-Immortalis MC12"), emptyList(), sdk = 36))
|
||||
assertEquals(DriverPairs.NAD_MALI_JM, DriverPairs.recommendedKey(mali("Mali-G52 MC2"), emptyList(), sdk = 36))
|
||||
assertNull("a Mali no build covers uses the bridge", DriverPairs.recommendedKey(mali("Mali-T880"), emptyList(), sdk = 36))
|
||||
assertEquals(DriverPairs.BANNER, DriverPairs.recommendedKey(gpu(740), emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aMissingKbaseReleaseFallsBackToTheGenerationsPair() {
|
||||
val g720 = GpuInfo("Mali-G720", 0, Family.MALI_G720, "", false)
|
||||
fun bundle(id: String) = TurnipReleases.Asset("Non-Adreno", id.lowercase(), "$id-Pair.zip", "https://example/$id", 9_000_000,
|
||||
false, "label", "0".repeat(64), DriverPairs.nonAdreno(id), bundle = true)
|
||||
val onlyG720 = DriverPairs.from(TurnipReleases.Check(listOf(bundle("PanVK-Mali-G720")), emptyList(), emptyList(), 0L))
|
||||
assertEquals(DriverPairs.NAD_MALI_G720, DriverPairs.recommendedKey(g720, onlyG720, sdk = 36))
|
||||
val both = DriverPairs.from(TurnipReleases.Check(listOf(bundle("PanVK-Mali-G720"), bundle("PanVK-Mali-kbase")), emptyList(), emptyList(), 0L))
|
||||
assertEquals(DriverPairs.NAD_MALI_KBASE, DriverPairs.recommendedKey(g720, both, sdk = 36))
|
||||
assertTrue(both.single { it.key == DriverPairs.NAD_MALI_KBASE }.suits(g720))
|
||||
assertFalse(both.single { it.key == DriverPairs.NAD_MALI_KBASE }.suits(GpuInfo("Mali-G52", 0, Family.MALI_JM, "", false)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonAdrenoPairsComeFromTheirBundlesAndSuitOnlyTheirGpu() {
|
||||
val pairs = DriverPairs.from(TurnipReleases.Check(
|
||||
listOf(
|
||||
TurnipReleases.Asset("Non-Adreno", "panvk-mali-g720", "PanVK-Mali-G720-Pair.zip", "https://example/a", 9_000_000,
|
||||
false, "label", "0".repeat(64), DriverPairs.nonAdreno("PanVK-Mali-G720"), bundle = true),
|
||||
TurnipReleases.Asset("Non-Adreno", "radv-xclipse", "RADV-Xclipse-Pair.zip", "https://example/b", 9_000_000,
|
||||
false, "label", "0".repeat(64), DriverPairs.nonAdreno("RADV-Xclipse"), bundle = true),
|
||||
),
|
||||
emptyList(), emptyList(), 0L,
|
||||
))
|
||||
val g720 = pairs.single { it.key == DriverPairs.NAD_MALI_G720 }
|
||||
val radv = pairs.single { it.key == DriverPairs.NAD_RADV_XCLIPSE }
|
||||
assertTrue(g720.complete)
|
||||
assertTrue(g720.suits(GpuInfo("Mali-G720", 0, Family.MALI_G720, "", false)))
|
||||
assertFalse(g720.suits(gpu(740)))
|
||||
assertTrue(radv.suits(GpuInfo("Xclipse", 0, Family.POWERVR, "", false)))
|
||||
assertFalse("RADV is not for a PowerVR Pixel", radv.suits(GpuInfo("PowerVR", 0, Family.POWERVR, "", false)))
|
||||
assertTrue(DriverPairs.isNonAdreno("PanVK-Mali-G720"))
|
||||
assertTrue(DriverPairs.isNonAdreno("RADV-Xclipse-2"))
|
||||
assertFalse(DriverPairs.isNonAdreno("DD-Turnip-v0.1.0"))
|
||||
assertFalse(DriverPairs.isNonAdreno(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun upstreamRadvIsAManualXclipseChoiceAndAutoKeepsOurs() {
|
||||
val asset = { id: String -> TurnipReleases.Asset("Non-Adreno", id.lowercase(), "$id-Pair.zip", "https://example/$id", 9_000_000,
|
||||
false, "label", "0".repeat(64), DriverPairs.nonAdreno(id), bundle = true) }
|
||||
val pairs = DriverPairs.from(TurnipReleases.Check(listOf(asset("RADV-Xclipse"), asset("RADV-Xclipse-upstream")), emptyList(), emptyList(), 0L))
|
||||
val xclipse = GpuInfo("Samsung Xclipse 940", 0, Family.POWERVR, "", false)
|
||||
val upstream = pairs.single { it.key == DriverPairs.NAD_RADV_XCLIPSE_UPSTREAM }
|
||||
assertEquals("nad:radv-xclipse-upstream", DriverPairs.nonAdreno("RADV-Xclipse-upstream"))
|
||||
assertTrue(upstream.complete)
|
||||
assertTrue(upstream.suits(xclipse))
|
||||
assertFalse(upstream.suits(GpuInfo("PowerVR", 0, Family.POWERVR, "", false)))
|
||||
assertEquals("RADV upstream · Samsung Xclipse", upstream.name)
|
||||
assertEquals(DriverPairs.NAD_RADV_XCLIPSE, DriverPairs.recommendedKey(xclipse, pairs))
|
||||
assertTrue(DriverPairs.isNonAdreno("RADV-Xclipse-upstream"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deviceBuildsAreOfferedOnlyForTheirMaliAndNeverByAuto() {
|
||||
fun bundle(id: String) = TurnipReleases.Asset("Non-Adreno", id.lowercase(), "$id-Pair.zip", "https://example/$id", 9_000_000,
|
||||
false, "label", "0".repeat(64), DriverPairs.nonAdreno(id), bundle = true)
|
||||
val pairs = DriverPairs.from(TurnipReleases.Check(
|
||||
listOf(bundle("PanVK-faux123-RP4Pro"), bundle("PanVK-faux123-Ace"), bundle("PanVK-faux123-RG556")),
|
||||
emptyList(), emptyList(), 0L))
|
||||
val g77 = GpuInfo("Mali-G77 MC9", 0, Family.MALI_JM, "", false)
|
||||
val g57 = GpuInfo("Mali-G57 MC2", 0, Family.MALI_JM, "", false)
|
||||
val g610 = GpuInfo("Mali-G610 MC6", 0, Family.MALI_G610_G710, "", false)
|
||||
assertTrue(pairs.single { it.key == DriverPairs.NAD_FAUX_RP4PRO }.suits(g77))
|
||||
assertFalse(pairs.single { it.key == DriverPairs.NAD_FAUX_RP4PRO }.suits(g57))
|
||||
assertTrue(pairs.single { it.key == DriverPairs.NAD_FAUX_RG556 }.suits(g57))
|
||||
assertTrue(pairs.single { it.key == DriverPairs.NAD_FAUX_ACE }.suits(g610))
|
||||
assertEquals(DriverPairs.NAD_MALI_JM, DriverPairs.recommendedKey(g77, pairs, sdk = 34))
|
||||
assertTrue(DriverPairs.isNonAdreno("PanVK-faux123-RP4Pro"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aMaliTheDeviceDoesNotNameIsKnownByItsChip() {
|
||||
assertEquals("Mali-G720 (by chip mt6989)", GpuInfo.maliOfSoc(listOf("", "mt6989")))
|
||||
assertEquals("Mali-G710 (by chip gs201)", GpuInfo.maliOfSoc(listOf("GS201")))
|
||||
assertEquals(null, GpuInfo.maliOfSoc(listOf("unknown-chip")))
|
||||
// An Exynos with a Mali, and a Helio G99, by board name.
|
||||
assertEquals(Family.MALI_JM, GpuInfo.familyOf(false, 0, mali = GpuInfo.maliOfSoc(listOf("s5e8835"))))
|
||||
assertEquals(Family.MALI_JM, GpuInfo.familyOf(false, 0, mali = GpuInfo.maliOfSoc(listOf("MT6789"))))
|
||||
assertEquals(Family.MALI_G720, GpuInfo.familyOf(false, 0, mali = "Mali-G720 (by chip mt6989)"))
|
||||
assertEquals(Family.MALI_LATEST, GpuInfo.familyOf(false, 0, mali = "Mali-G925 (by chip mt6991)"))
|
||||
assertTrue(GpuInfo.maliKbaseCovered("Mali-G720 (by chip mt6989)"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theMaliG1SeriesHasNoPanVkPairAndIsKnownByItsChip() {
|
||||
// Arch v14 (Dimensity 9500): no PanVK build is proven there, so Auto uses the phone's own driver.
|
||||
assertEquals("Mali-G1-Ultra (by chip mt6993)", GpuInfo.maliOfSoc(listOf("MT6993")))
|
||||
assertEquals(Family.MALI_OTHER, GpuInfo.familyOf(false, 0, mali = "Mali-G1-Ultra MC12"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun autoTakesRadvOnAnXclipseAndLeavesPowerVrOnTheSystemDriver() {
|
||||
assertEquals(DriverPairs.NAD_RADV_XCLIPSE, DriverPairs.recommendedKey(GpuInfo("Samsung Xclipse 940", 0, Family.POWERVR, "", false), emptyList()))
|
||||
assertNull(DriverPairs.recommendedKey(GpuInfo("PowerVR", 0, Family.POWERVR, "", false), emptyList()))
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class GpuDetectionTest {
|
||||
assertEquals("Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device", info(true, 850).supportText)
|
||||
assertEquals("Experimental: its drivers are test builds", info(true, 720).supportText)
|
||||
assertEquals("Outside tested hardware (Adreno 650, 725 and newer): it may not run", info(true, 618).supportText)
|
||||
assertEquals("Not supported: DroidDeck needs an Adreno (Snapdragon) GPU", info(false, 0).supportText)
|
||||
assertEquals("Not supported: no compatible graphics backend is available for this GPU", info(false, 0).supportText)
|
||||
assertEquals("Not an Adreno GPU", Family.NOT_ADRENO.label)
|
||||
assertEquals("Adreno 8xx", Family.A8XX.label)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
|
||||
class SystemVulkanPruningTest {
|
||||
@get:Rule val temporary = TemporaryFolder()
|
||||
|
||||
@Test fun completedReplacementCollectsOnlyManagedOldDirectories() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = completeRuntime(base, "a".repeat(64))
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val stage = abandonedStage(base)
|
||||
val unrelated = File(base, "notes").apply { mkdir(); File(this, "keep").writeText("keep") }
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
|
||||
assertEquals("current ICD", File(current, "icd.json").readText())
|
||||
assertTrue(File(current, "run-proton.py").canExecute())
|
||||
assertFalse(old.exists())
|
||||
assertFalse(stage.exists())
|
||||
assertEquals("keep", File(unrelated, "keep").readText())
|
||||
}
|
||||
|
||||
@Test fun aSurvivingGuestKeepsOldVersionsButNotAbandonedStages() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = completeRuntime(base, "a".repeat(64))
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val stage = abandonedStage(base)
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, false)
|
||||
|
||||
assertTrue(current.isDirectory)
|
||||
assertTrue(old.isDirectory)
|
||||
assertFalse(stage.exists())
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertFalse("collect the old version once guests stop", old.exists())
|
||||
}
|
||||
|
||||
@Test fun failedReplacementPreservesThePreviousGoodRuntime() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = File(base, "a".repeat(64))
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val stage = abandonedStage(base)
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertTrue("missing replacement", old.isDirectory)
|
||||
assertFalse(stage.exists())
|
||||
|
||||
current.mkdir()
|
||||
File(current, "icd.json").writeText("incomplete")
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertEquals("current ICD", File(old, "icd.json").readText())
|
||||
}
|
||||
|
||||
@Test fun deletingOldRuntimeDoesNotFollowItsSymlinks() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = completeRuntime(base, "a".repeat(64))
|
||||
val old = File(base, "b".repeat(64)).apply { mkdir() }
|
||||
val outside = temporary.newFolder("outside")
|
||||
val sentinel = File(outside, "keep").apply { writeText("keep") }
|
||||
Files.createSymbolicLink(File(old, "outside").toPath(), outside.toPath())
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
|
||||
assertFalse(old.exists())
|
||||
assertEquals("keep", sentinel.readText())
|
||||
}
|
||||
|
||||
@Test fun linkedBaseOrCurrentRuntimePreventsCleanup() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val current = File(base, "a".repeat(64))
|
||||
Files.createSymbolicLink(current.toPath(), old.toPath())
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertTrue(old.isDirectory)
|
||||
|
||||
Files.delete(current.toPath())
|
||||
completeRuntime(base, current.name)
|
||||
val alias = File(temporary.root, "alias")
|
||||
Files.createSymbolicLink(alias.toPath(), base.toPath())
|
||||
SystemVulkanDriver.pruneUnused(File(alias, current.name), true)
|
||||
assertTrue(old.isDirectory)
|
||||
}
|
||||
|
||||
private fun abandonedStage(base: File) =
|
||||
File(base, ".stage-12345678-1234-1234-1234-123456789abc").apply { mkdir(); File(this, "partial").writeText("partial") }
|
||||
|
||||
private fun completeRuntime(base: File, version: String): File {
|
||||
val directory = File(base, version).apply { mkdir() }
|
||||
val components = listOf(
|
||||
"lib/libhybris-vulkan-icd.so.0", "lib/libhybris-common.so.1", "lib/libhybris/linker/q.so",
|
||||
"lib/libVkLayer_hybris_compat.so", "run-proton.py",
|
||||
"wsi/libVkLayer_window_system_integration.so", "wsi/VkLayer_window_system_integration.json",
|
||||
) + listOf("x32", "x64").flatMap { arch ->
|
||||
listOf("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi").map { "dxvk/$arch/$it.dll" }
|
||||
}
|
||||
components.forEach { path -> File(directory, path).apply { parentFile!!.mkdirs(); writeText("component") } }
|
||||
File(directory, "run-proton.py").setExecutable(true)
|
||||
File(directory, "icd.json").writeText("current ICD")
|
||||
return directory
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
|
||||
class SystemVulkanSelectionTest {
|
||||
@Test fun defaultPowerVrUsesTheSystemBridge() {
|
||||
assertTrue(SystemVulkanDriver.usesDefault(true, ""))
|
||||
assertTrue(SystemVulkanDriver.usesDefault(true, null))
|
||||
}
|
||||
|
||||
@Test fun otherGpusAndExplicitImportsKeepTheirDriver() {
|
||||
assertFalse(SystemVulkanDriver.usesDefault(false, ""))
|
||||
assertFalse(SystemVulkanDriver.usesDefault(false, null))
|
||||
assertFalse(SystemVulkanDriver.usesDefault(true, "custom-driver"))
|
||||
}
|
||||
@Test fun renderNodeRequiresARealCharacterDeviceAndBothAccessModes() {
|
||||
assertTrue(SystemVulkanDriver.renderNodeAvailable(true, true, true))
|
||||
assertFalse("a regular placeholder is not a DRM device", SystemVulkanDriver.renderNodeAvailable(false, true, true))
|
||||
assertFalse("read access is required", SystemVulkanDriver.renderNodeAvailable(true, false, true))
|
||||
assertFalse("write access is required", SystemVulkanDriver.renderNodeAvailable(true, true, false))
|
||||
assertFalse("a missing node is unavailable", SystemVulkanDriver.renderNodeAvailable(false, false, false))
|
||||
}
|
||||
|
||||
@Test fun renderNodeDriverMustIdentifyPowerVrExactly() {
|
||||
for (name in listOf("pvrsrvkm", "pvr", "powervr"))
|
||||
assertTrue(name, SystemVulkanDriver.powerVrDriver(name))
|
||||
for (name in listOf(null, "", "msm", "panfrost", "not-pvr"))
|
||||
assertFalse(name, SystemVulkanDriver.powerVrDriver(name))
|
||||
}
|
||||
|
||||
@Test fun samsungXclipseGpuNodeIsSelectedOverItsDisplayNode() {
|
||||
assertTrue(SystemVulkanDriver.powerVrDriver("sgpu"))
|
||||
assertFalse("the display controller is not the GPU", SystemVulkanDriver.powerVrDriver("exynos-drm"))
|
||||
// Galaxy A56 (Exynos 1580): the GPU owns renderD128, the display controller renderD129.
|
||||
val drivers = mapOf("/dev/dri/renderD128" to "sgpu", "/dev/dri/renderD129" to "exynos-drm")
|
||||
assertEquals("/dev/dri/renderD128", SystemVulkanDriver.findRenderNode(null) { path ->
|
||||
SystemVulkanDriver.powerVrDriver(drivers[path])
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun renderNodeIdentityUsesTheDriverLinkOrUevent() {
|
||||
val device = Files.createTempDirectory("powervr-device").toFile()
|
||||
try {
|
||||
assertFalse("missing identity is unavailable", SystemVulkanDriver.powerVrDevice(device))
|
||||
val uevent = File(device, "uevent")
|
||||
uevent.writeText("DRIVER=pvrsrvkm\nOF_NAME=gpu0\n")
|
||||
assertTrue(SystemVulkanDriver.powerVrDevice(device))
|
||||
uevent.writeText("DRIVER=msm\nOF_COMPATIBLE_0=not-pvr\n")
|
||||
assertFalse(SystemVulkanDriver.powerVrDevice(device))
|
||||
val driver = File(device, "driver").toPath()
|
||||
Files.createSymbolicLink(driver, File("../../drivers/pvrsrvkm").toPath())
|
||||
assertTrue(SystemVulkanDriver.powerVrDevice(device))
|
||||
Files.delete(driver)
|
||||
Files.createSymbolicLink(driver, File("../../drivers/msm").toPath())
|
||||
uevent.writeText("DRIVER=pvrsrvkm\n")
|
||||
assertFalse("an unrelated driver link must not fall back", SystemVulkanDriver.powerVrDevice(device))
|
||||
} finally {
|
||||
device.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun renderNodeDiscoverySkipsOtherDriversAndDeniedNodes() {
|
||||
val drivers = mapOf(
|
||||
"/dev/dri/renderD128" to "msm",
|
||||
"/dev/dri/renderD129" to "pvr",
|
||||
"/dev/dri/renderD130" to "pvrsrvkm",
|
||||
)
|
||||
assertEquals("/dev/dri/renderD129", SystemVulkanDriver.findRenderNode(null) { path ->
|
||||
SystemVulkanDriver.powerVrDriver(drivers[path])
|
||||
})
|
||||
assertEquals("/dev/dri/renderD130", SystemVulkanDriver.findRenderNode(null) { path ->
|
||||
SystemVulkanDriver.powerVrDriver(drivers[path]) &&
|
||||
SystemVulkanDriver.renderNodeAvailable(true, path != "/dev/dri/renderD129", true)
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun renderNodesAreDiscoveredWithoutDirectoryListing() {
|
||||
for (minor in listOf(128, 129, 191)) {
|
||||
val expected = "/dev/dri/renderD$minor"
|
||||
assertEquals(expected, SystemVulkanDriver.findRenderNode(null) { it == expected })
|
||||
}
|
||||
assertNull(SystemVulkanDriver.findRenderNode(null) { false })
|
||||
}
|
||||
|
||||
@Test fun listedRenderNodesCoverExtendedMinorNumbers() {
|
||||
assertEquals("/dev/dri/renderD256", SystemVulkanDriver.findRenderNode(
|
||||
arrayOf("card0", "renderD256", "renderD128/device", "../renderD129"),
|
||||
) { path ->
|
||||
assertTrue(path, path.matches(Regex("/dev/dri/renderD[0-9]+")))
|
||||
path == "/dev/dri/renderD256"
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun defaultPowerVrResolvesToTheSystemIcd() {
|
||||
assertEquals("system/icd.json", LinuxVulkanDriver.resolveIcdPath("", true,
|
||||
{ "system/icd.json" }, { error("default must not resolve an import") }))
|
||||
}
|
||||
|
||||
@Test fun defaultAdrenoLeavesTheRuntimeIcdUnchanged() {
|
||||
assertNull(LinuxVulkanDriver.resolveIcdPath("", false,
|
||||
{ error("Adreno must not prepare the system bridge") }, { error("default must not resolve an import") }))
|
||||
}
|
||||
|
||||
@Test fun missingManualPowerVrImportFailsWithoutSwitchingBackends() {
|
||||
val failure = assertThrows(IllegalStateException::class.java) {
|
||||
LinuxVulkanDriver.resolveIcdPath("missing-driver", true,
|
||||
{ error("manual preference must not silently switch to the system backend") }, { null })
|
||||
}
|
||||
assertTrue(failure.message!!.contains("missing-driver"))
|
||||
assertTrue(failure.message!!.contains("use Auto"))
|
||||
}
|
||||
|
||||
@Test fun missingAdrenoImportKeepsTheExistingRuntimeFallback() {
|
||||
assertNull(LinuxVulkanDriver.resolveIcdPath("missing-driver", false,
|
||||
{ error("Adreno must not prepare the system bridge") }, { null }))
|
||||
}
|
||||
|
||||
@Test fun manualPowerVrResolvesTheChosenImport() {
|
||||
assertEquals("imports/chosen/icd.json", LinuxVulkanDriver.resolveIcdPath("chosen", true,
|
||||
{ error("manual must not prepare the system bridge") }, { "imports/$it/icd.json" }))
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,34 @@ class ForcedPackageTest {
|
||||
assertDefaultDxvkRestoreRow(protonName)
|
||||
}
|
||||
|
||||
@Test fun systemPowerVrRejectsNewGameDxvkSelectionsWithoutChangingSavedProfiles() {
|
||||
writeGameProfile("42", "dxvk", "custom-dxvk.wcp")
|
||||
val state = File(context.filesDir, "components/state.json")
|
||||
val saved = state.readText()
|
||||
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
ComponentsManager.setGameComponent(context, "42", "dxvk", "replacement.wcp") {
|
||||
ComponentsManager.runtimeManaged(it, true)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(saved, state.readText())
|
||||
assertEquals("custom-dxvk.wcp", ComponentsManager.gameComponents(context, "42")["dxvk"])
|
||||
assertFalse(launchState().exists())
|
||||
}
|
||||
|
||||
@Test fun systemPowerVrAllowsClearingASavedGameDxvkSelection() {
|
||||
proton("proton-exp")
|
||||
writeGameProfile("42", "dxvk", "custom-dxvk.wcp")
|
||||
|
||||
ComponentsManager.setGameComponent(context, "42", "dxvk", null) {
|
||||
ComponentsManager.runtimeManaged(it, true)
|
||||
}
|
||||
|
||||
assertTrue(ComponentsManager.gameComponents(context, "42").isEmpty())
|
||||
assertDefaultDxvkRestoreRow("proton-exp")
|
||||
}
|
||||
|
||||
@Test fun aNewProtonIsCapturedWhenTheLastOverrideIsReset() {
|
||||
proton("existing")
|
||||
writeGameProfile("42", "dxvk", "custom-dxvk.wcp")
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
|
||||
class GraphicsReportTest {
|
||||
@get:Rule val tmp = TemporaryFolder()
|
||||
|
||||
@Test fun deviceNumbersSplitAsLinuxDoes() {
|
||||
// /dev/mali0 on the testers' phones: 10:103 (misc); a DRM render node: 226:128.
|
||||
assertEquals(10L, GraphicsReport.major(10L shl 8 or 103L))
|
||||
assertEquals(103L, GraphicsReport.minor(10L shl 8 or 103L))
|
||||
assertEquals(226L, GraphicsReport.major(226L shl 8 or 128L))
|
||||
assertEquals(128L, GraphicsReport.minor(226L shl 8 or 128L))
|
||||
}
|
||||
|
||||
@Test fun gameLogsAreCutPerFileAndOldestLaunchesGoFirst() {
|
||||
val games = tmp.newFolder("games")
|
||||
fun launch(name: String, bytes: Int) = File(games, name).apply { mkdirs() }.also { File(it, "steam-1.log").writeBytes(ByteArray(bytes)) }
|
||||
val old = launch("20261012-100000-1", 60)
|
||||
val mid = launch("20261012-110000-2", 60)
|
||||
val new = launch("20261012-120000-3", 200)
|
||||
GraphicsReport.capGames(games, fileMax = 100, totalMax = 170)
|
||||
assertEquals(100L, File(new, "steam-1.log").length())
|
||||
assertFalse(old.exists())
|
||||
assertTrue(mid.exists())
|
||||
assertTrue(new.exists())
|
||||
}
|
||||
|
||||
@Test fun aMissingGamesFolderIsFine() {
|
||||
GraphicsReport.capGames(File(tmp.root, "none"))
|
||||
GraphicsReport.finish(tmp.newFolder("session"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class OrphanReaperProcessTest {
|
||||
@Test fun unavailableProcessListDefersCleanup() {
|
||||
assertTrue(OrphanReaper.hasOtherProcesses(null, 100) { false })
|
||||
}
|
||||
|
||||
@Test fun theAppAndForeignProcessesDoNotKeepOldRuntimes() {
|
||||
assertFalse(OrphanReaper.hasOtherProcesses(listOf(0, 1, 100, 200), 100) { pid ->
|
||||
check(pid == 200) { "do not probe self or process groups" }
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun anyRemainingAppProcessDefersCleanup() {
|
||||
assertTrue(OrphanReaper.hasOtherProcesses(listOf(100, 200, 300), 100) { it == 300 })
|
||||
assertFalse(OrphanReaper.hasOtherProcesses(emptyList(), 100) { true })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class PowerVrAutoDriversTest {
|
||||
private val context: Context = RuntimeEnvironment.getApplication()
|
||||
|
||||
@Before fun reset() {
|
||||
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
SessionPrefs.setLinuxDriver(context, "imported-linux")
|
||||
SessionPrefs.setAndroidDriver(context, "imported-display")
|
||||
}
|
||||
|
||||
@Test fun autoRepairsStaleAdrenoSelectionsOnPowerVr() {
|
||||
SessionPrefs.setGpuDriverMode(context, SessionPrefs.GPU_DRIVERS_AUTO)
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(context, powerVr = true)
|
||||
assertEquals("", SessionPrefs.linuxDriver(context))
|
||||
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(context))
|
||||
}
|
||||
|
||||
@Test fun explicitManualChoicesRemainOnPowerVr() {
|
||||
SessionPrefs.setGpuDriverMode(context, SessionPrefs.GPU_DRIVERS_MANUAL)
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(context, powerVr = true)
|
||||
assertEquals("imported-linux", SessionPrefs.linuxDriver(context))
|
||||
assertEquals("imported-display", SessionPrefs.androidDriver(context))
|
||||
}
|
||||
|
||||
@Test fun adrenoAutoChoicesRemain() {
|
||||
SessionPrefs.setGpuDriverMode(context, SessionPrefs.GPU_DRIVERS_AUTO)
|
||||
SessionPrefs.ensurePowerVrAutoDrivers(context, powerVr = false)
|
||||
assertEquals("imported-linux", SessionPrefs.linuxDriver(context))
|
||||
assertEquals("imported-display", SessionPrefs.androidDriver(context))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PowerVrComponentsTest {
|
||||
@Test fun onlyDxvkIsManagedWithTheSystemPowerVrRuntime() {
|
||||
assertTrue(ComponentsManager.runtimeManaged("dxvk", systemPowerVr = true))
|
||||
assertFalse(ComponentsManager.runtimeManaged("dxvk", systemPowerVr = false))
|
||||
assertFalse(ComponentsManager.runtimeManaged("fex", systemPowerVr = true))
|
||||
assertFalse(ComponentsManager.runtimeManaged("vkd3d", systemPowerVr = true))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class PowerVrQueuedComponentsTest {
|
||||
@Test fun managedDxvkQueueRemainsVisibleAndCanBeCancelled() {
|
||||
val context: Context = RuntimeEnvironment.getApplication()
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
root.deleteRecursively()
|
||||
val data = File(context.filesDir, "components")
|
||||
data.deleteRecursively()
|
||||
data.mkdirs()
|
||||
val proton = File(root, "root/.local/share/Steam/compatibilitytools.d/test-proton")
|
||||
File(proton, "files/lib/wine").mkdirs()
|
||||
File(proton, "proton").writeText("# test Proton\n")
|
||||
File(proton, "version").writeText("1 test-build\n")
|
||||
File(data, "state.json").writeText(JSONObject().put("queued", JSONObject().put("test-proton",
|
||||
JSONObject().put("dxvk", JSONObject().put("kind", "package").put("file", "chosen.wcp").put("label", "chosen DXVK"))
|
||||
)).toString())
|
||||
|
||||
assertTrue(ComponentsManager.applyQueued(context) { ComponentsManager.runtimeManaged(it, true) }.isEmpty())
|
||||
val deferred = ComponentsManager.snapshot(context).protons.single().components.getValue("dxvk")
|
||||
assertEquals("chosen DXVK", deferred.queued)
|
||||
|
||||
ComponentsManager.cancelQueued(context, "test-proton", "dxvk")
|
||||
assertNull(ComponentsManager.snapshot(context).protons.single().components.getValue("dxvk").queued)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -31,11 +32,18 @@ class SessionEndHintTest {
|
||||
@get:Rule val temporary = TemporaryFolder()
|
||||
private lateinit var activity: SessionActivity
|
||||
private var previousArchitecture: String? = null
|
||||
private var previousLog: File? = null
|
||||
private var previousDirectory: File? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
previousArchitecture = SessionState.steamClientArch
|
||||
previousLog = SessionState.logFile
|
||||
previousDirectory = SessionState.logDirectory
|
||||
SessionState.logFile = null
|
||||
SessionState.logDirectory = null
|
||||
DetectedGpu.family = GpuInfo.Family.NOT_ADRENO
|
||||
DetectedGpu.name = "test GPU"
|
||||
activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
SessionState.steamClientArch = "arm64"
|
||||
}
|
||||
@@ -43,7 +51,10 @@ class SessionEndHintTest {
|
||||
@After
|
||||
fun tearDown() {
|
||||
SessionState.steamClientArch = previousArchitecture
|
||||
SessionState.logFile = previousLog
|
||||
SessionState.logDirectory = previousDirectory
|
||||
DetectedGpu.family = GpuInfo.Family.NOT_ADRENO
|
||||
DetectedGpu.name = "test GPU"
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -80,10 +91,35 @@ class SessionEndHintTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun vulkanFailureOnNonAdrenoExplainsTheMissingLinuxDriver() {
|
||||
DetectedGpu.family = GpuInfo.Family.NOT_ADRENO
|
||||
fun vulkanFailureOnNonAdrenoIdentifiesTheGpuAndOffersDriverGuidance() {
|
||||
for (family in GpuInfo.Family.entries.filter { it.mali || it == GpuInfo.Family.POWERVR || it == GpuInfo.Family.NOT_ADRENO }) {
|
||||
DetectedGpu.family = family
|
||||
val expected = "test GPU (${family.label("test GPU")})\n" + activity.getString(R.string.session_end_vulkan_device)
|
||||
assertEquals(family.name, expected, hintText(vulkanLog()))
|
||||
}
|
||||
DetectedGpu.family = GpuInfo.Family.POWERVR
|
||||
DetectedGpu.name = "Samsung Xclipse 940"
|
||||
assertEquals("Samsung Xclipse 940 (Samsung Xclipse)\n" + activity.getString(R.string.session_end_vulkan_device),
|
||||
hintText(vulkanLog()))
|
||||
}
|
||||
|
||||
assertEquals(activity.getString(R.string.session_end_gpu_unsupported), hintText(vulkanLog()))
|
||||
@Test
|
||||
fun cleanExitRetainsAnUnrecoveredPresentationFault() {
|
||||
SessionState.logDirectory = temporary.root
|
||||
File(temporary.root, "wayland.log").writeText("DD_PRESENT_FAULT: GPU completion is unknown\n")
|
||||
val hint = exitHint(0)
|
||||
assertNotNull(hint)
|
||||
assertEquals(activity.getString(R.string.session_end_present_fault, "GPU completion is unknown"),
|
||||
ReflectionHelpers.getField<String>(hint, "text"))
|
||||
assertTrue(ReflectionHelpers.getField(hint, "evenOnSuccess"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanExitIgnoresRecoveredFaultsAndUnrelatedSessionWarnings() {
|
||||
SessionState.logDirectory = temporary.root
|
||||
File(temporary.root, "wayland.log").writeText("DD_PRESENT_FAULT: old fault\npresentation recovered: fault cleared\n")
|
||||
SessionState.logFile = sigillLog()
|
||||
assertNull(exitHint(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,6 +139,9 @@ class SessionEndHintTest {
|
||||
private fun hint(log: File?): Any? = SessionActivity::class.java
|
||||
.getDeclaredMethod("sessionEndHint", File::class.java).apply { isAccessible = true }.invoke(activity, log)
|
||||
|
||||
private fun exitHint(status: Int): Any? = SessionActivity::class.java
|
||||
.getDeclaredMethod("sessionExitHint", Int::class.javaPrimitiveType).apply { isAccessible = true }.invoke(activity, status)
|
||||
|
||||
private fun hintText(log: File): String {
|
||||
val result = hint(log)
|
||||
assertNotNull(result)
|
||||
@@ -112,10 +151,11 @@ class SessionEndHintTest {
|
||||
@Implements(value = GpuInfo.Companion::class, isInAndroidSdk = false)
|
||||
class DetectedGpu {
|
||||
@Implementation
|
||||
fun detect(): GpuInfo = GpuInfo("test GPU", if (family == GpuInfo.Family.A6XX) 610 else 0, family, "", false)
|
||||
fun detect(): GpuInfo = GpuInfo(name, if (family == GpuInfo.Family.A6XX) 610 else 0, family, "", false)
|
||||
|
||||
companion object {
|
||||
var family = GpuInfo.Family.NOT_ADRENO
|
||||
var name = "test GPU"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,24 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onLast
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.session.ComponentsManager
|
||||
import java.io.File
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -12,8 +26,9 @@ import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
// Exercise the component UI without starting App's process-wide servers or crash handler.
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28])
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
class ComponentsPageProfileTest {
|
||||
@get:Rule val compose = createRobolectricComposeRule()
|
||||
private val gameA = Library.SteamGame(42, "Game A", null, "internal")
|
||||
@@ -98,4 +113,143 @@ class ComponentsPageProfileTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Config(qualifiers = "w1280dp-h1000dp")
|
||||
fun systemPowerVrKeepsStoredDxvkDisabledAndQueuedDxvkCancellable() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
var cancelled = 0
|
||||
var imported = 0
|
||||
var swapped = 0
|
||||
var restored = 0
|
||||
showDxvk(
|
||||
systemPowerVr = true,
|
||||
onCancel = { cancelled++ },
|
||||
onImport = { imported++ },
|
||||
onSwap = { swapped++ },
|
||||
onRestore = { restored++ },
|
||||
)
|
||||
|
||||
compose.onNodeWithText(context.getString(R.string.cmgr_powervr_dxvk_managed)).assertExists()
|
||||
compose.onNodeWithText(context.getString(R.string.cmgr_powervr_dxvk_queued, "Next DXVK"), substring = true).assertExists()
|
||||
compose.onNodeWithText("Stored DXVK").assertIsNotEnabled()
|
||||
compose.onNodeWithText(context.getString(R.string.comp_original, "1.0")).assertIsNotEnabled()
|
||||
compose.onNodeWithText(context.getString(R.string.comp_restore_original)).assertIsNotEnabled()
|
||||
compose.onNodeWithText(context.getString(R.string.common_cancel)).performClick()
|
||||
compose.onNodeWithText(context.getString(R.string.comp_import_wcp)).performScrollTo().assertIsEnabled().performClick()
|
||||
compose.runOnIdle {
|
||||
assertEquals(1, cancelled)
|
||||
assertEquals(1, imported)
|
||||
assertEquals(0, swapped)
|
||||
assertEquals(0, restored)
|
||||
}
|
||||
}
|
||||
|
||||
@Test @Config(qualifiers = "w1280dp-h1000dp")
|
||||
fun manualRuntimeKeepsDxvkSelectionAvailable() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
showDxvk(systemPowerVr = false)
|
||||
|
||||
compose.onNode(hasText("Stored DXVK") and SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.RadioButton)).assertIsEnabled()
|
||||
compose.onNodeWithText(context.getString(R.string.comp_original, "1.0")).assertIsEnabled()
|
||||
compose.onNodeWithText(context.getString(R.string.comp_restore_original)).assertIsEnabled()
|
||||
compose.onNodeWithText(context.getString(R.string.cmgr_powervr_dxvk_managed)).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test @Config(qualifiers = "w1280dp-h1000dp")
|
||||
fun systemPowerVrGameProfileKeepsOtherComponentChoicesAndRuntimeOptionsAvailable() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
showDxvk(systemPowerVr = true)
|
||||
compose.onNodeWithText(context.getString(R.string.comp_scope_general)).performClick()
|
||||
compose.onNodeWithText("Game A").performClick()
|
||||
compose.waitUntil(5_000) {
|
||||
compose.onAllNodesWithText(context.getString(R.string.comp_game_reset)).fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
|
||||
compose.onNodeWithText(context.getString(R.string.gpu_system_powervr)).assertIsNotEnabled()
|
||||
compose.onNodeWithText(context.getString(R.string.cmgr_powervr_dxvk_managed)).assertExists()
|
||||
val inheritedChoices = compose.onAllNodesWithText(context.getString(R.string.comp_game_use_general))
|
||||
inheritedChoices.assertCountEquals(4) // FEX preset, FEX package, Proton and VKD3D remain selectable.
|
||||
repeat(4) { inheritedChoices[it].assertIsEnabled() }
|
||||
val runtimeOptions = compose.onAllNodesWithText(context.getString(R.string.comp_game_runtime_options))
|
||||
runtimeOptions.assertCountEquals(2)
|
||||
repeat(2) { runtimeOptions[it].assertIsEnabled() }
|
||||
compose.onNodeWithText(context.getString(R.string.comp_this_proton).uppercase()).assertDoesNotExist()
|
||||
|
||||
compose.onNodeWithText("Game A").performClick()
|
||||
compose.onNodeWithText(context.getString(R.string.comp_scope_general)).performClick()
|
||||
compose.onNodeWithText(context.getString(R.string.comp_this_proton).uppercase()).assertExists()
|
||||
}
|
||||
|
||||
@Test @Config(qualifiers = "w1280dp-h1000dp")
|
||||
fun systemPowerVrGameProfileCanClearAnExistingDxvkOverrideWithoutSelectingAnother() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
showDxvk(systemPowerVr = true, gameDxvkOverride = true)
|
||||
compose.onNodeWithText(context.getString(R.string.comp_scope_general)).performClick()
|
||||
compose.onNodeWithText("Game A").performClick()
|
||||
compose.waitUntil(5_000) {
|
||||
compose.onAllNodesWithText(context.getString(R.string.comp_game_reset)).fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
|
||||
compose.onNodeWithText(context.getString(R.string.gpu_system_powervr)).performScrollTo().assertIsEnabled().performClick()
|
||||
compose.onNodeWithText("Stored DXVK").assertDoesNotExist()
|
||||
compose.onAllNodesWithText(context.getString(R.string.comp_game_use_general)).onLast().performClick()
|
||||
compose.waitUntil(5_000) { "dxvk" !in ComponentsManager.gameComponents(context, "42") }
|
||||
compose.onNodeWithText(context.getString(R.string.gpu_system_powervr)).assertIsNotEnabled()
|
||||
}
|
||||
|
||||
private fun showDxvk(
|
||||
systemPowerVr: Boolean,
|
||||
gameDxvkOverride: Boolean = false,
|
||||
onCancel: () -> Unit = {},
|
||||
onImport: () -> Unit = {},
|
||||
onSwap: (String) -> Unit = {},
|
||||
onRestore: (String) -> Unit = {},
|
||||
) {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
File(context.filesDir, "components/state.json").apply {
|
||||
parentFile!!.mkdirs()
|
||||
writeText(if (gameDxvkOverride) """{"profiles":{"42":{"dxvk":"stored.wcp"}}}""" else "{}")
|
||||
}
|
||||
val proton = ComponentsManager.Proton(
|
||||
"test-proton", "Test Proton", File(context.filesDir, "test-proton"), "/test-proton", "1.0", false,
|
||||
)
|
||||
val snapshot = ComponentsManager.Snapshot(
|
||||
protons = listOf(ComponentsManager.ProtonView(
|
||||
proton = proton,
|
||||
components = mapOf("dxvk" to ComponentsManager.Component("1.0", "", "Stored DXVK", "", "stored.wcp", "Next DXVK")),
|
||||
originals = listOf(ComponentsManager.Original("dxvk", "1.0", "Original DXVK", 1)),
|
||||
inUseByGame = false,
|
||||
)),
|
||||
packages = listOf(ComponentsManager.Package("stored.wcp", "dxvk", "Stored DXVK", "", 1)),
|
||||
)
|
||||
compose.setContent {
|
||||
DroidDeckTheme {
|
||||
ComponentsPage(
|
||||
snapshot = snapshot,
|
||||
catalog = emptyList(),
|
||||
catalogAt = 0,
|
||||
protonId = proton.id,
|
||||
comp = "dxvk",
|
||||
checking = false,
|
||||
busy = null,
|
||||
downloads = emptyMap(),
|
||||
onProton = {},
|
||||
onComp = {},
|
||||
onSwap = onSwap,
|
||||
onRestore = onRestore,
|
||||
onCancelQueued = onCancel,
|
||||
onDeletePackage = {},
|
||||
onDeleteOriginal = {},
|
||||
onDownload = {},
|
||||
onRefresh = {},
|
||||
onImport = onImport,
|
||||
onBack = {},
|
||||
gpu = GpuDriversState(powerVr = true, systemPowerVr = systemPowerVr),
|
||||
games = listOf(Library.SteamGame(42, "Game A", null, "internal")),
|
||||
requestInitialFocus = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -130,6 +130,12 @@ class AppUpdatesTest {
|
||||
assertNull(AppUpdates.installBlock(stable, me("eeeeeee", versionCode = 9)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theNonAdrenoTestBuildIsNeverReplacedByAnOfficialBuild() {
|
||||
val testBuild = AppUpdates.Installed("eeeeeee", 0, "0.2.0", 9, updatable = true, nonAdrenoTest = true)
|
||||
assertNotNull(AppUpdates.installBlock(stable, testBuild))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aMissingVariantIsBlockedBeforeDownload() {
|
||||
val missing = release("ddddddd", withApk = false)
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# American Nightmare on Pixel: rendering workaround and remaining limits
|
||||
|
||||
Local engineering results from 2026-10-11/12. Steam game 202750 was tested on a
|
||||
Pixel 11 Pro Fold using the Pixel Lab runtime. These observations do not establish
|
||||
compatibility across the Pixel family or a fix for the system resets.
|
||||
|
||||
## Rendering result
|
||||
|
||||
The bright dotted triangle edges over textured rocks, boards and the player are
|
||||
not an application wireframe setting: traced input and driver pipelines all used
|
||||
`VK_POLYGON_MODE_FILL`, with no dynamic polygon mode. The original affected path
|
||||
used 2x MSAA, RGBA16F color and D24S8 depth. The game's minimum UI AA setting and
|
||||
`d3d11.disableMsaa` did not disable its D3D9 multisampling.
|
||||
|
||||
A real single-sample D3D9 comparison removed the visible bright triangle edges.
|
||||
Trial 16 verified 114 input and 114 driver pipelines, all single-sample. Trial 17
|
||||
retained correct meshes/textures through movement and turning, with 119 pipelines
|
||||
in each phase, all single-sample. This isolates a useful workaround to the MSAA
|
||||
path; it does not identify the precise faulty operation or prove a vendor bug.
|
||||
The override also changes effective surface descriptors, so the application may
|
||||
select different AA-aware shader/rendering paths.
|
||||
|
||||
`tools/dxvk/patches/0004-d3d9-force-single-sample.patch` adds an explicit per-game
|
||||
`d3d9.forceSingleSample` option, default **False**. After validating the original
|
||||
sample request, D3D9 surface normalization selects `D3DMULTISAMPLE_NONE` and quality
|
||||
0. GetDesc, image creation, resolves and sample-dependent state use this effective
|
||||
description. This intentionally disables MSAA, per-sample masks and
|
||||
alpha-to-coverage. It is a compatibility override, not transparent MSAA emulation
|
||||
or a universal PowerVR default. Existing low32, BC-format, present synchronization
|
||||
and tiler workarounds must remain intact.
|
||||
|
||||
Swapchain backbuffers have a separate creation path. With the override enabled,
|
||||
that path now validates a copy of the original requested descriptor and transfers
|
||||
the effective sample type/quality before allocation, so automatic depth surfaces
|
||||
and backbuffers agree. Validation uses the application's lockable-backbuffer flag;
|
||||
the internal lockability required for GDI fallback is retained on the allocated
|
||||
backbuffer. With the override disabled the original backbuffer path is unchanged.
|
||||
This correction addresses official PR602 review 5486295088. Its CPU harness
|
||||
compiles the actual DXVK sample decoder, surface normalizer and backbuffer method,
|
||||
plus the automatic-depth descriptor and framebuffer depth-binding excerpts.
|
||||
Fifteen cases cover on/off, NONMASKABLE quality, matching depth, no-depth operation,
|
||||
unsupported/invalid/lockable requests, allocation cleanup and recreation. Backend
|
||||
images and adapter capabilities are mocked; these are not GPU or gameplay tests.
|
||||
|
||||
## Independent HUD correction
|
||||
|
||||
The diagnostic frametimes HUD bound an R8_UINT storage texel buffer to a shader
|
||||
whose SPIR-V image format was Unknown. This required formatless buffer writes
|
||||
which the device did not report for that format, producing VUID 07029.
|
||||
`0003-hud-explicit-r8-texel-format.patch` declares `r8ui` to match the actual host
|
||||
view. No capabilities are fabricated. The build now compiles and validates the
|
||||
actual shader, and checks binding 3 for DimBuffer/storage/R8ui. The original shader
|
||||
fails that contract; the patched shader passes. Real-game trial 15 used the new
|
||||
DLLs, removed 07029, and retained the original triangle edges. This HUD bug is
|
||||
separate from the rendering workaround, and is not a proven reset cause.
|
||||
|
||||
VUID 06479 for color-format comparison descriptors remains under investigation.
|
||||
The D3D9 shader uses a conditional comparison path; static validation output alone
|
||||
does not prove that the offending branch executes. No warning suppression or
|
||||
false format-feature claims are proposed. Trial 14's LOG+FAIL with a duplicate
|
||||
limit blocked only the first message of each kind, not every invalid API call;
|
||||
subsequent trials used logging only. Do not describe these runs as fully blocked
|
||||
validation or GPU-assisted validation.
|
||||
|
||||
## Responsiveness, performance and stability
|
||||
|
||||
Paired screenshots prove movement and camera changes, rather than relying on an
|
||||
input receipt. Trial 17 completed eight samples without a black viewport. Its
|
||||
cold sample was 3.2 FPS; later recognized samples were 19.1-29.9 FPS, with roughly
|
||||
2.54 CPU-core equivalents. Camera/workload, capture overhead and thermals were
|
||||
not controlled; this is not a reliable A/B benchmark.
|
||||
|
||||
The planned eight-minute trial 17 stopped at a focus/session guard after the last
|
||||
confirmed sample at 315.6 seconds. Steam owned focus; the target game was still
|
||||
available for a single client quit. Its temporary settings were successfully
|
||||
restored, and the phone boot counter remained 12. The focus change's cause is
|
||||
unknown. The run is **incomplete**, not an eight-minute pass or a demonstrated
|
||||
game crash. Controls, focus ownership and sustained stability still need broader
|
||||
coverage beyond these short tests.
|
||||
|
||||
### Final production configuration and user confirmation
|
||||
|
||||
The clean production configuration was persisted only for game 202750 through
|
||||
the application's per-game environment UI. It selects the tested DXVK component
|
||||
and sets `d3d9.forceSingleSample = True`, retaining the existing low32, BC,
|
||||
presentation-sync and `dxvk.tilerMode = False` workarounds. Shared configuration
|
||||
and other games were unchanged. Shader audit layers, validation and the
|
||||
frametimes HUD are not enabled in this production configuration.
|
||||
|
||||
A final automated production trial loaded the existing saved game, verified
|
||||
turning/movement with five paired non-black screenshots, and observed 115 seconds
|
||||
without the bright triangle edges or another phone reset. It then quit normally;
|
||||
the saved per-game environment remained unchanged. This is bounded observation,
|
||||
not a long-duration stability or controlled performance benchmark.
|
||||
|
||||
On 2026-10-11 (America/Toronto), the device owner separately confirmed that this
|
||||
stage worked and authorized publication. No duration, frame rate or full-game
|
||||
completion was supplied with that confirmation. It is additional user validation
|
||||
of the stage, not a claim that all Pixel devices or the GNOC resets are fixed.
|
||||
|
||||
## Native guards, resets and diagnostic controls
|
||||
|
||||
The Pixel base is e589e3ae21959c9c74c490d917cebf256acc8ac8. PR561's 9e6fe314 and
|
||||
3c363910 guards were integrated in an isolated worktree; 26 selected production-C
|
||||
regressions and a full APK/runtime verification passed. Pixel AHB support remains
|
||||
present. The existing Lab app was updated without uninstalling or resetting its
|
||||
Steam account. This APK preceded a second system reset, so the native guards are
|
||||
not a verified reset fix.
|
||||
|
||||
Two resets changed the boot counter 10 -> 11 -> 12. After personal unlock, narrow
|
||||
kernel/firmware projections confirmed GNOC/access-control details for the first,
|
||||
and `reset,warm,gnoc_error` with a fresh GNOC numeric record for the second. The
|
||||
specific faulty component/operation and any relation to rendering are unknown.
|
||||
`tools/pixel_reset_diagnostics.py` exports only fixed fault categories/numeric
|
||||
fields, lock state, boot counter and entry times; never arbitrary log text or
|
||||
account data. Boot-property and kernel last-boot reasons use the same exact
|
||||
allowlist of canonical categories and the observed `reset,warm,gnoc_error` value;
|
||||
unknown reasons and custom suffixes produce `unknown`, never their original text.
|
||||
Primary-user states are likewise limited to recognized lifecycle categories.
|
||||
Output files are created exclusively with mode `0600` before any bytes are
|
||||
written (POSIX), and existing files/symlinks are refused. Host regressions cover
|
||||
these boundaries as well as freshness, unlock gating and narrow output.
|
||||
Dropbox's offset-free local timestamp is not assigned a guessed instant during
|
||||
a DST transition: repeated hours report `ambiguous_device_local_time`, skipped
|
||||
hours report `nonexistent_device_local_time`. Neither is attributed to the trial
|
||||
or used to project kernel-body categories until its time can be established.
|
||||
|
||||
Private opt-in diagnostics captured maintenance5 inline modules, which the older
|
||||
shim path missed. Materialization passed nine actual-C ASan/UBSan cases; benign
|
||||
pipeline-chain planning passed twelve. Real-game NoContraction, scaled vertex
|
||||
conversion and force-clip rewrites were actually applied and did not remove the
|
||||
edges. None is a production default. Sixty-two valid bounded, owned offscreen GPU
|
||||
cases passed exact pixel readback, fences and validation, covering interpolation,
|
||||
seams, perspective meshes, interface widening, scaled fetch, HDR/depth prepasses,
|
||||
real clipping and nonconstant depth. Invalid initial fixtures are excluded. These
|
||||
passing controls do not exonerate every driver/rendering path.
|
||||
|
||||
## Reproducibility and recovery
|
||||
|
||||
The runtime artifact supplies both architectures, exact DLL hashes, public patched
|
||||
DXVK sources, all four patches, wrapper and build recipe. It contains no game
|
||||
assets, bytecode, saves, account files, screenshots or raw device logs. Source
|
||||
patches apply cleanly to pinned DXVK revision 25ca63f17f34bdc05a39873ee63907d3cbbfa030.
|
||||
All five runtime DLL targets were built for both architectures.
|
||||
|
||||
Stop further load tests after a full reset. Wait for personal unlock and inspect a
|
||||
fresh narrow projection before another launch. Temporary profile restoration checks
|
||||
baseline/current hashes and unchanged shared/other-game settings; it never rolls
|
||||
back saved game progress. Persistent selection must affect only 202750 and leave
|
||||
private shader audit layers, validation settings and frametimes HUD disabled.
|
||||
Routine Heartbeat must not launch games or automatically resume this engineering.
|
||||
|
||||
## Submission scope
|
||||
|
||||
These opt-in runtime tools are submitted directly against official DroidDeck
|
||||
`main`. The standalone DXVK build recipe, source pin, BC-format prerequisite and
|
||||
presentation-semaphore patch/check are imported verbatim from the PowerVR proposal
|
||||
<https://github.com/Droid-Deck/DroidDeck/pull/570> at
|
||||
`d0f7264ea8e402d30c868779198dc2fd74e7431c`. That proposal's attribution and existing
|
||||
source/license provenance are retained. The semaphore correction originated in
|
||||
<https://github.com/wallentx/DroidDeck/pull/3>. No Android app changes, managed pack
|
||||
or the experimental fork's development history are imported with those recipes.
|
||||
|
||||
The source tools can be reviewed and built independently. Automatic PowerVR
|
||||
integration and delivery in DroidDeck require the separately reviewed #570
|
||||
foundation and a new managed-runtime release. This submission does not change
|
||||
managed-pack manifests/checksums or globally enable any override. The earlier
|
||||
fork-only stage <https://github.com/wallentx/DroidDeck/pull/4> is superseded by the
|
||||
official submission; its review history is preserved.
|
||||
|
||||
For standalone verification, run
|
||||
`python3 -m unittest discover -s tools/tests -p test_pixel_reset_diagnostics.py`
|
||||
on the host. On native 64-bit Linux/glibc,
|
||||
`tools/hybris/low32/build.sh /absolute/output-dir` builds and runs the CPU integrity
|
||||
and independent mmap/mmap64 preload-chain regressions (both library orders in
|
||||
off, trace and low32 modes). Optional Vulkan headers produce `gpu-copy-native`
|
||||
for the selected native host architecture, not a hardcoded ARM64 filename.
|
||||
`python3 -m unittest discover -s tools/tests -p test_dxvk_build.py` verifies output
|
||||
isolation with a synthetic toolchain, without fetching or compiling DXVK.
|
||||
The DXVK component recipe is `tools/dxvk/build.sh /absolute/output-dir`;
|
||||
it requires the pinned DXVK build prerequisites, including Meson/Ninja, MinGW
|
||||
cross compilers, glslangValidator and SPIR-V Tools. It retains both architectures,
|
||||
source/license provenance and downstream patches, without editing an installed
|
||||
runtime or APK. Its output directory must be new or empty (including hidden
|
||||
entries). Prior or partially built outputs are refused before fetching sources,
|
||||
left untouched and never included in a new DLL manifest. Use a fresh directory
|
||||
after a failed build.
|
||||
The recipe also requires a native C++17 compiler for
|
||||
`python3 tools/dxvk/check_d3d9_single_sample.py /path/to/patched-dxvk` before DLL
|
||||
compilation. Add `--sanitize` on supported hosts to run its ASan/UBSan controls.
|
||||
|
||||
The compositor ownership safeguards are independently proposed in
|
||||
<https://github.com/Droid-Deck/DroidDeck/pull/561>. Those native changes were present
|
||||
in the integrated Pixel Lab APK used for the game experiments, but are not part
|
||||
of this runtime PR's diff. The original BC emulation is a prerequisite supplied by
|
||||
the separately proposed PowerVR runtime. This submission supplies source and reproducibility tools,
|
||||
not an APK, proprietary driver, game data or private diagnostic archive.
|
||||
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user