diff --git a/.github/workflows/build-venus.yml b/.github/workflows/build-venus.yml new file mode 100644 index 000000000..d355bdcd7 --- /dev/null +++ b/.github/workflows/build-venus.yml @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7418c5bfb..083c05250 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore index bfeee7641..0a404ef66 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ __pycache__/ .signing.env app/src/main/assets/directaudio/ +app/src/main/assets/hybris/ /tools/vr/out/ diff --git a/app/src/main/cpp/waylandcomp/CMakeLists.txt b/app/src/main/cpp/waylandcomp/CMakeLists.txt index fd0b2f20a..61baf0d37 100644 --- a/app/src/main/cpp/waylandcomp/CMakeLists.txt +++ b/app/src/main/cpp/waylandcomp/CMakeLists.txt @@ -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 diff --git a/app/src/main/cpp/waylandcomp/generated/wayland-android-protocol.c b/app/src/main/cpp/waylandcomp/generated/wayland-android-protocol.c new file mode 100644 index 000000000..79e171faa --- /dev/null +++ b/app/src/main/cpp/waylandcomp/generated/wayland-android-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 +#include +#include +#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, +}; diff --git a/app/src/main/cpp/waylandcomp/generated/wayland-android-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/wayland-android-server-protocol.h new file mode 100644 index 000000000..0fd3b6d68 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/generated/wayland-android-server-protocol.h @@ -0,0 +1,341 @@ +/* Generated by wayland-scanner 1.26.0 */ + +#ifndef ANDROID_SERVER_PROTOCOL_H +#define ANDROID_SERVER_PROTOCOL_H + +#include +#include +#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 + *
+ *
+ * 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.
+ * 
+ */ +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 diff --git a/app/src/main/cpp/waylandcomp/protocols/wayland-android.xml b/app/src/main/cpp/waylandcomp/protocols/wayland-android.xml new file mode 100644 index 000000000..de8a78b66 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/protocols/wayland-android.xml @@ -0,0 +1,139 @@ + + + + + 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. + + + + + Interface used in the Android wrapper libEGL to share + graphics buffers between the server and the client. + + + + + + + + + + This creator method initialises the native_handle_t object + with everything except the file descriptors, which have to be + submitted separately. + + + + + + + + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + 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. + + + + + + + + + + + + + + + + + + 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. + + + + + + + + + + This event will be sent after the ints and all the fds have been sent + + + + + + + + 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. + + + + + + + diff --git a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c index cb4447835..e33b0c77b 100644 --- a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c +++ b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c @@ -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; diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index 471748a6d..21ef1d23f 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -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); diff --git a/app/src/main/cpp/waylandcomp/src/compositor_internal.h b/app/src/main/cpp/waylandcomp/src/compositor_internal.h index 0e4229d0a..c70f9dd19 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor_internal.h +++ b/app/src/main/cpp/waylandcomp/src/compositor_internal.h @@ -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); diff --git a/app/src/main/cpp/waylandcomp/src/vk_external_image_sync.h b/app/src/main/cpp/waylandcomp/src/vk_external_image_sync.h new file mode 100644 index 000000000..676c36a14 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/vk_external_image_sync.h @@ -0,0 +1,47 @@ +#ifndef VK_EXTERNAL_IMAGE_SYNC_H +#define VK_EXTERNAL_IMAGE_SYNC_H + +#include + +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 diff --git a/app/src/main/cpp/waylandcomp/src/vk_loader.h b/app/src/main/cpp/waylandcomp/src/vk_loader.h index 54cb88881..2f5701dd6 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_loader.h +++ b/app/src/main/cpp/waylandcomp/src/vk_loader.h @@ -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) \ diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index 9e1d1f7d6..8d0267cc1 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -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 #include #include +#include #include #include @@ -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]); } diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.h b/app/src/main/cpp/waylandcomp/src/vk_present.h index ac975c93d..3a76b4c0b 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.h +++ b/app/src/main/cpp/waylandcomp/src/vk_present.h @@ -3,6 +3,8 @@ #include #include #include +#include + 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); diff --git a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c index 5d69098a6..ffbfdfd93 100644 --- a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c +++ b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c @@ -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) { diff --git a/app/src/main/cpp/waylandcomp/src/wl_android_buffer.c b/app/src/main/cpp/waylandcomp/src/wl_android_buffer.c new file mode 100644 index 000000000..c7b436f46 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/wl_android_buffer.c @@ -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 +#include +#include +#include +#include + +/* 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"); +} diff --git a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt index 200deb55a..5fa92865f 100644 --- a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt +++ b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt @@ -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() 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>(emptyList()) var pairBusy by mutableStateOf(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) diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index eee09c03b..919d02cab 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -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(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) diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index f8e11d289..2363b8beb 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -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) diff --git a/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java b/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java index 32d5f5189..08c463dfd 100644 --- a/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java +++ b/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java @@ -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; diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt index 309716d3f..cb0b9487b 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt @@ -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)) diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt index ee214fedf..c847dbcdf 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt @@ -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 diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt index a287c2299..17a2e8b6c 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt @@ -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"), diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt index 2d9aab902..148555c5c 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt @@ -53,7 +53,7 @@ object DeviceInfo { private fun gpu(context: Context, gpu: GpuInfo) = Section("GPU", buildList> { 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" + } + } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt index db8dc86c4..594dbebb2 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt @@ -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 diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt index c99bfb5f1..367a747b3 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt @@ -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> = 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() + } + class DriverPair( val key: String, val name: String, @@ -24,14 +65,23 @@ object DriverPairs { val assets: List 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> = 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 { 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.BUILT_IN, bad: Set = 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, 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): String? = pairs .filter { it.key.startsWith("$DD_TURNIP:") && it.complete } .maxByOrNull { p -> diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt index 9b0a1897e..7645d3326 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt @@ -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 = 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? = 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 diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java index 42dce3d64..25dd68b4d 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java @@ -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 systemIcd, + Function 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; } } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java index 3973297cd..4f0b62edd 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java @@ -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); diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SystemVulkanDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/SystemVulkanDriver.java new file mode 100644 index 000000000..07ba47cd1 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SystemVulkanDriver.java @@ -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 available) { + TreeSet 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 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()); + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java index ba96c3eca..294f5a95b 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -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; } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt index 43540c0e2..1936779cb 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt @@ -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, val latest: List>, val failed: List, 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) }, + // -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 = + if (com.droiddeck.launcher.core.DeviceSupport.adreno()) SOURCES.filter { it.repo != NON_ADRENO_REPO } else SOURCES + fun autoPicks(context: Context): List = AutoPicks.parse(prefs(context).getString(KEY_PICKS, null)) ?: AutoPicks.BUILT_IN @@ -71,9 +90,13 @@ object TurnipReleases { val latest = JSONArray() val failed = ArrayList() 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() 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>() 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) { diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java index e2cc349aa..3e5b0a29c 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -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 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()); } diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index 8255209d1..df5db5eb6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -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 = synchronized(lock) { + /** Runs the swaps that waited for a game to close. Cheap when nothing is queued. */ + fun applyQueued(context: Context): List = applyQueued(context) { runtimeManaged(context, it) } + + internal fun applyQueued(context: Context, managed: (String) -> Boolean): List = 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") diff --git a/app/src/main/java/com/droiddeck/launcher/session/GraphicsReport.kt b/app/src/main/java/com/droiddeck/launcher/session/GraphicsReport.kt new file mode 100644 index 000000000..4d1ee473e --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/session/GraphicsReport.kt @@ -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) { + 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 = 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) + } + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt b/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt index 300306f8c..cbcd904bc 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt @@ -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?, 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() diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt index 4fc004304..cb766a2de 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt @@ -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 diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt index 3b0172136..cb9b5391f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -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) } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 2e7f3b6c5..a7541e261 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -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 = 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), diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 1a6e00bcf..72e5543d3 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -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, 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, 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) diff --git a/app/src/main/java/com/droiddeck/launcher/session/VenusComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/VenusComponent.kt new file mode 100644 index 000000000..a1856a6a9 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/session/VenusComponent.kt @@ -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 { + 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") + } + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt index 6c15923b6..43f613486 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt @@ -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 { +private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapshot: Snapshot?, runtimeManaged: Boolean = false): List { 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, componentChoices: Map, + 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) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 5881ad715..fda8c6fed 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -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 = {}, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 830b68ef3..66dd4e0e1 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -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() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt index 69936548c..f0df5a220 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt @@ -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 = 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 } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index b11b0640b..92074c314 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -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 = 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> = 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 DrawerStackedChoice( host: MenuHost, key: String, label: String, hint: String?, options: List>, 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 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 -> diff --git a/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt b/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt index 1bbeb18e2..51b5fc3d3 100644 --- a/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt +++ b/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt @@ -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 diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java index 8fd329ce2..a2ac2171d 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java +++ b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java @@ -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); diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d0222536c..c891154b5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -825,7 +825,7 @@ GPU غير مدعوم %1$s · %2$s %1$s: %2$s - يحتاج Steam إلى برنامج تشغيل Adreno؛ قد يعرض %1$s شاشة سوداء + لا تتوفر واجهة رسومات متوافقة مع %1$s. قد يعرض Steam شاشة سوداء. بيئة تشغيل Linux %1$s · %2$d%% غير مثبّت · نحو 3 GB، يُثبَّت عند أول تشغيل @@ -1450,8 +1450,8 @@ تصحيح الأخطاء اللاسلكي غير متاح تم نسخ أمر ADB اختر مجلد ROMs - وحدة GPU ليست من نوع Adreno - يدعم Turnip وحدات GPU من نوع Adreno. على %1$s، قد يعرض Steam شاشة سوداء. حجم التنزيل: %2$.0f MB. + وحدة معالجة رسومات غير مدعومة + لا تتوفر واجهة رسومات متوافقة مع %1$s. قد يعرض Steam شاشة سوداء. التنزيل: %2$.0f ميغابايت. التثبيت على أي حال إزالة بيئة تشغيل Linux سيؤدي هذا إلى حذف بيئة التشغيل وعميل Steam بداخلها وكل لعبة مثبَّتة فيها. @@ -2217,7 +2217,7 @@ تجريبي: برامج تشغيله إصدارات اختبارية دعم تجريبي لـ Adreno 850: يتطلب برامج تشغيل تجريبية متوافقة للعرض والألعاب، ولم يُتحقَّق منه على هذا الجهاز أقل من العتاد المُختبَر (Adreno 725 وأحدث): قد لا يعمل - غير مدعوم: يحتاج DroidDeck إلى وحدة GPU من نوع Adreno (Snapdragon) + غير مدعوم: لا تتوفر واجهة رسومات متوافقة مع وحدة معالجة الرسومات هذه تم الوصول إلى حدّ الطلبات في GitHub - حاوِل مرة أخرى لاحقًا استجاب GitHub بـ HTTP %1$d diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index e89919b32..df6965507 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -843,7 +843,7 @@ GPU no compatible %1$s · %2$s %1$s: %2$s - Steam necesita un controlador Adreno; %1$s puede mostrar una pantalla negra + No hay un motor gráfico compatible disponible para %1$s. Steam puede mostrar una pantalla negra. Runtime de Linux %1$s · %2$d%% No instalado · unos 3 GB, se instala al jugar por primera vez @@ -1483,8 +1483,8 @@ La Depuración inalámbrica no está disponible Comando ADB copiado Elige la carpeta de ROMs - La GPU no es Adreno - Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB. + GPU no compatible + No hay un motor gráfico compatible disponible para %1$s. Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB. Instalar de todos modos Eliminar el runtime de Linux Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él. @@ -2192,7 +2192,7 @@ Experimental: sus controladores son versiones de prueba Compatibilidad experimental con Adreno 850: requiere controladores experimentales de pantalla y de juegos compatibles; no verificada en este dispositivo Por debajo del hardware probado (Adreno 725 y posteriores): puede que no funcione - No compatible: DroidDeck necesita una GPU Adreno (Snapdragon) + No compatible: no hay un motor gráfico compatible disponible para esta GPU Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde GitHub respondió HTTP %1$d Error en la descarga diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 3087a2cca..a12a74bc8 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -813,7 +813,7 @@ GPU не се поддържа %1$s · %2$s %1$s: %2$s - Steam изисква драйвер за Adreno; %1$s може да покаже черен екран + Няма съвместим графичен бекенд за %1$s. Steam може да показва черен екран. Linux среда за изпълнение %1$s · %2$d%% Не е инсталирано · около 3 GB, инсталира се при първото Play @@ -1402,8 +1402,8 @@ Безжичното отстраняване на грешки не е достъпно Командата за ADB е копирана Избор на папката ROMs - Не е Adreno GPU - Turnip поддържа Adreno GPU-та. На %1$s Steam може да покаже черен екран. Изтегляне: %2$.0f MB. + Неподдържан GPU + Няма съвместим графичен бекенд за %1$s. Steam може да показва черен екран. Изтегляне: %2$.0f MB. Инсталиране въпреки това Премахване на средата на Linux Това изтрива средата, клиента на Steam в нея и всяка инсталирана в нея игра. @@ -2117,7 +2117,7 @@ Експериментален: драйверите му са тестови версии Експериментална поддръжка на Adreno 850: изисква съвместими експериментални драйвери за дисплея и игрите; не е проверена на това устройство Под тестваните устройства (Adreno 725 и по-нови): може да не работи - Не се поддържа: DroidDeck изисква GPU Adreno (Snapdragon) + Не се поддържа: няма съвместим графичен бекенд за този GPU Достигнато е ограничението на GitHub за заявки - опитай отново по-късно GitHub отговори с HTTP %1$d diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 6d6058813..f66fb31fb 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -819,7 +819,7 @@ GPU nepodporováno %1$s · %2$s %1$s: %2$s - Steam vyžaduje ovladač Adreno; %1$s může zobrazit černou obrazovku + Pro %1$s není k dispozici kompatibilní grafický backend. Steam může zobrazit černou obrazovku. Běhové prostředí Linuxu %1$s · %2$d %% Nenainstalováno · přibližně 3 GB, instaluje se při prvním Play @@ -1426,8 +1426,8 @@ Bezdrátové ladění není k dispozici Příkaz ADB zkopírován Vybrat složku ROMs - Není GPU Adreno - Turnip podporuje GPU Adreno. Na %1$s může Steam zobrazit černou obrazovku. Stažení: %2$.0f MB. + Nepodporovaný GPU + Pro %1$s není k dispozici kompatibilní grafický backend. Steam může zobrazit černou obrazovku. Ke stažení: %2$.0f MB. Přesto instalovat Odstranit runtime Linuxu Tím se odstraní runtime, klient Steam v něm a všechny hry v něm nainstalované. @@ -2167,7 +2167,7 @@ Experimentální: jeho ovladače jsou testovací sestavení Experimentální podpora Adreno 850: vyžaduje kompatibilní experimentální ovladače displeje a her; na tomto zařízení neověřeno Pod testovaným hardwarem (Adreno 725 a novější): nemusí fungovat - Není podporováno: DroidDeck potřebuje GPU Adreno (Snapdragon) + Nepodporováno: pro tento GPU není k dispozici kompatibilní grafický backend Byl dosažen limit požadavků GitHubu - zkuste to později GitHub odpověděl HTTP %1$d diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 191c15c7b..ab5f0859c 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -813,7 +813,7 @@ GPU ikke understøttet %1$s · %2$s %1$s: %2$s - Steam kræver en Adreno-driver; %1$s kan vise en sort skærm + Ingen kompatibel grafikbackend er tilgængelig for %1$s. Steam kan vise en sort skærm. Linux-runtime %1$s · %2$d%% Ikke installeret · omkring 3 GB, installeres ved første Spil @@ -1402,8 +1402,8 @@ Trådløs fejlretning er ikke tilgængelig ADB-kommando kopieret Vælg ROMs-mappen - Ikke en Adreno-GPU - Turnip understøtter Adreno-GPU\'er. På %1$s kan Steam vise en sort skærm. Download: %2$.0f MB. + GPU understøttes ikke + Ingen kompatibel grafikbackend er tilgængelig for %1$s. Steam kan vise en sort skærm. Download: %2$.0f MB. Installer alligevel Fjern Linux-runtime Dette sletter runtime\'en, Steam-klienten i den og alle spil installeret der. @@ -2117,7 +2117,7 @@ Eksperimentel: dens drivere er testversioner Eksperimentel Adreno 850-understøttelse: kræver kompatible eksperimentelle skærm- og spildrivere; ikke bekræftet på denne enhed Under testet hardware (Adreno 725 og nyere): kører muligvis ikke - Ikke understøttet: DroidDeck kræver en Adreno (Snapdragon) GPU + Understøttes ikke: ingen kompatibel grafikbackend er tilgængelig for denne GPU GitHubs hastighedsgrænse blev nået - forsøg igen senere GitHub svarede HTTP %1$d diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 200e8e242..e4bfaa32b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -813,7 +813,7 @@ GPU nicht unterstützt %1$s · %2$s %1$s: %2$s - Steam benötigt einen Adreno-Treiber; %1$s zeigt möglicherweise einen schwarzen Bildschirm + Für %1$s ist kein kompatibles Grafik-Backend verfügbar. Steam zeigt möglicherweise einen schwarzen Bildschirm. Linux-Laufzeit %1$s · %2$d %% Nicht installiert · ca. 3 GB, wird beim ersten „Spielen“ installiert @@ -1394,8 +1394,8 @@ Debugging über WLAN ist nicht verfügbar ADB-Befehl kopiert ROMs-Ordner auswählen - Keine Adreno-GPU - Turnip unterstützt Adreno-GPUs. Auf %1$s zeigt Steam eventuell einen schwarzen Bildschirm. Download: %2$.0f MB. + Nicht unterstützte GPU + Für %1$s ist kein kompatibles Grafik-Backend verfügbar. Steam zeigt möglicherweise einen schwarzen Bildschirm. Download: %2$.0f MB. Trotzdem installieren Linux-Laufzeit entfernen Dadurch werden die Laufzeit, der Steam-Client darin und alle dort installierten Spiele gelöscht. @@ -2109,7 +2109,7 @@ Experimentell: Die Treiber sind Test-Builds Experimentelle Adreno-850-Unterstützung: erfordert kompatible experimentelle Anzeige- und Spieltreiber; auf diesem Gerät nicht geprüft Unter der getesteten Hardware (Adreno 725 und neuer): läuft eventuell nicht - Nicht unterstützt: DroidDeck braucht eine Adreno-GPU (Snapdragon) + Nicht unterstützt: Für diese GPU ist kein kompatibles Grafik-Backend verfügbar Das Rate-Limit von GitHub wurde erreicht - versuche es später erneut GitHub antwortete mit HTTP %1$d diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index d8a9e8d13..806ba0972 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -813,7 +813,7 @@ Η GPU δεν υποστηρίζεται %1$s · %2$s %1$s: %2$s - Το Steam απαιτεί πρόγραμμα οδήγησης Adreno· το %1$s μπορεί να εμφανίσει μαύρη οθόνη + Δεν υπάρχει συμβατό υποσύστημα γραφικών για %1$s. Το Steam μπορεί να εμφανίσει μαύρη οθόνη. Runtime Linux %1$s · %2$d%% Δεν έχει εγκατασταθεί · περίπου 3 GB, εγκαθίσταται στο πρώτο Play @@ -1402,8 +1402,8 @@ Ο ασύρματος εντοπισμός σφαλμάτων δεν είναι διαθέσιμος Η εντολή ADB αντιγράφηκε Επιλέξτε τον φάκελο ROMs - Δεν είναι GPU Adreno - Το Turnip υποστηρίζει GPU Adreno. Σε %1$s, το Steam ίσως εμφανίσει μαύρη οθόνη. Λήψη: %2$.0f MB. + Μη υποστηριζόμενη GPU + Δεν υπάρχει συμβατό υποσύστημα γραφικών για %1$s. Το Steam μπορεί να εμφανίσει μαύρη οθόνη. Λήψη: %2$.0f MB. Εγκατάσταση παρόλα αυτά Κατάργηση του Linux runtime Αυτό διαγράφει το runtime, τον πελάτη Steam μέσα σε αυτό και κάθε παιχνίδι που είναι εγκατεστημένο εκεί. @@ -2117,7 +2117,7 @@ Πειραματικό: τα προγράμματα οδήγησής του είναι δοκιμαστικές εκδόσεις Πειραματική υποστήριξη Adreno 850: απαιτεί συμβατά πειραματικά προγράμματα οδήγησης οθόνης και παιχνιδιών· δεν έχει επαληθευτεί σε αυτή τη συσκευή Κάτω από το δοκιμασμένο υλικό (Adreno 725 και νεότερα): ίσως να μην εκτελεστεί - Δεν υποστηρίζεται: το DroidDeck χρειάζεται GPU Adreno (Snapdragon) + Δεν υποστηρίζεται: δεν υπάρχει συμβατό υποσύστημα γραφικών για αυτήν την GPU Το όριο ρυθμού του GitHub ξεπεράστηκε - δοκιμάστε ξανά αργότερα Το GitHub απάντησε με HTTP %1$d diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ae6cda557..25fe78e32 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -816,7 +816,7 @@ GPU no compatible %1$s · %2$s %1$s: %2$s - Steam necesita un controlador Adreno; %1$s puede mostrar una pantalla negra + No hay un motor gráfico compatible disponible; %1$s puede mostrar una pantalla negra Runtime de Linux %1$s · %2$d%% No instalado · unos 3 GB, se instala al jugar por primera vez @@ -1414,8 +1414,8 @@ La Depuración inalámbrica no está disponible Comando ADB copiado Elige la carpeta de ROMs - La GPU no es Adreno - Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB. + GPU no compatible + No hay un motor gráfico compatible disponible para %1$s. Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB. Instalar de todos modos Eliminar el runtime de Linux Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él. @@ -2142,7 +2142,7 @@ Experimental: sus controladores son versiones de prueba Compatibilidad experimental con Adreno 850: requiere controladores experimentales de pantalla y de juegos compatibles; no verificada en este dispositivo Por debajo del hardware probado (Adreno 725 y posteriores): puede que no funcione - No compatible: DroidDeck necesita una GPU Adreno (Snapdragon) + No compatible: no hay un motor gráfico compatible disponible para esta GPU Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde GitHub respondió HTTP %1$d diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index b96591b1c..22732a673 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -813,7 +813,7 @@ GPU ei ole tuettu %1$s · %2$s %1$s: %2$s - Steam tarvitsee Adreno-ajurin; %1$s saattaa näyttää mustan ruudun + GPU:lle %1$s ei ole saatavilla yhteensopivaa grafiikkataustajärjestelmää. Steam saattaa näyttää mustan ruudun. Linux-ajoympäristö %1$s · %2$d%% Ei asennettu · noin 3 GB, asennetaan ensimmäisellä Pelaa-painalluksella @@ -1402,8 +1402,8 @@ Langaton virheenkorjaus ei ole käytettävissä ADB-komento kopioitu Valitse ROMs-kansio - Ei Adreno-grafiikkapiiri - Turnip tukee Adreno-grafiikkapiirejä. Laitteella %1$s Steam voi näyttää mustan ruudun. Latauksen koko: %2$.0f MB. + GPU:ta ei tueta + GPU:lle %1$s ei ole saatavilla yhteensopivaa grafiikkataustajärjestelmää. Steam saattaa näyttää mustan ruudun. Lataus: %2$.0f Mt. Asenna silti Poista Linux-ajoympäristö Tämä poistaa ajoympäristön, siihen asennetun Steam-asiakasohjelman ja kaikki sinne asennetut pelit. @@ -2117,7 +2117,7 @@ Kokeellinen: sen ajurit ovat testiversioita Kokeellinen Adreno 850 -tuki: vaatii yhteensopivat kokeelliset näyttö- ja peliajurit; ei vahvistettu tällä laitteella Testatun laitteiston alapuolella (Adreno 725 ja uudemmat): ei välttämättä toimi - Ei tuettu: DroidDeck vaatii Adreno (Snapdragon) -grafiikkapiirin + Ei tuettu: tälle GPU:lle ei ole saatavilla yhteensopivaa grafiikkataustajärjestelmää GitHubin käyttörajoitus tuli vastaan - yritä myöhemmin uudelleen GitHub vastasi HTTP %1$d diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2b71aeadd..f7cf22942 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -816,7 +816,7 @@ GPU non pris en charge %1$s · %2$s %1$s : %2$s - Steam nécessite un pilote Adreno ; %1$s peut afficher un écran noir. + Aucun moteur graphique compatible n’est disponible ; %1$s peut afficher un écran noir. Environnement d\'exécution Linux %1$s · %2$d%% Non installé · Environ 3 Go, installé lors du premier lancement d\'un jeu @@ -1414,8 +1414,8 @@ Le débogage sans fil n\'est pas disponible Commande ADB copiée Choisir le dossier ROM - GPU non Adreno - Turnip prend en charge les GPU Adreno. Sur %1$s, Steam peut afficher un écran noir. Téléchargement : %2$.0f Mo. + GPU non pris en charge + Aucun moteur graphique compatible n’est disponible pour %1$s. Steam peut afficher un écran noir. Téléchargement : %2$.0f Mo. Installer quand même Supprimer l\'environnement d\'exécution Linux Cela supprime l\'environnement d\'exécution, le client Steam qu\'il contient et tous les jeux qui y sont installés. @@ -2142,7 +2142,7 @@ Expérimental : ses pilotes sont des versions de test 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 En dessous du matériel testé (Adreno 725 et plus récent) : risque de ne pas fonctionner - Non pris en charge : DroidDeck nécessite un GPU Adreno (Snapdragon) + Non pris en charge : aucun moteur graphique compatible n’est disponible pour ce GPU Limite de requêtes GitHub atteinte - réessayez plus tard GitHub a répondu HTTP %1$d diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f039dd022..97a501f42 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -813,7 +813,7 @@ GPU समर्थित नहीं %1$s · %2$s %1$s: %2$s - Steam को Adreno ड्राइवर चाहिए; %1$s में काली स्क्रीन दिख सकती है + %1$s के लिए कोई संगत ग्राफ़िक्स बैकएंड उपलब्ध नहीं है। Steam काली स्क्रीन दिखा सकता है। Linux रनटाइम %1$s · %2$d%% इंस्टॉल नहीं है · लगभग 3 GB, पहली बार Play पर इंस्टॉल होगा @@ -1402,8 +1402,8 @@ वायरलेस डीबगिंग उपलब्ध नहीं है ADB कमांड कॉपी हो गया ROM फ़ोल्डर चुनें - Adreno GPU नहीं है - Turnip Adreno GPU को सपोर्ट करता है। %1$s पर, Steam में काली स्क्रीन दिख सकती है। डाउनलोड: %2$.0f MB। + असमर्थित GPU + %1$s के लिए कोई संगत ग्राफ़िक्स बैकएंड उपलब्ध नहीं है। Steam काली स्क्रीन दिखा सकता है। डाउनलोड: %2$.0f MB। फिर भी इंस्टॉल करें Linux रनटाइम हटाएं यह रनटाइम, इसके भीतर का Steam क्लाइंट, और वहां इंस्टॉल हर गेम हटा देता है। @@ -2117,7 +2117,7 @@ एक्सपेरिमेंटल: इसके ड्राइवर टेस्ट बिल्ड हैं प्रयोगात्मक Adreno 850 समर्थन: कम्पैटिबल प्रयोगात्मक डिस्प्ले और गेम ड्राइवर चाहिए; इस डिवाइस पर जांचा नहीं गया जांचे गए हार्डवेयर से नीचे (Adreno 725 और नए): यह शायद न चले - समर्थित नहीं: DroidDeck को Adreno (Snapdragon) GPU चाहिए + समर्थित नहीं: इस GPU के लिए कोई संगत ग्राफ़िक्स बैकएंड उपलब्ध नहीं है GitHub की रेट लिमिट पार हो गई - बाद में फिर कोशिश करें GitHub ने HTTP %1$d जवाब दिया diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index bfa3f5f5a..9106d8a27 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -813,7 +813,7 @@ A GPU nem támogatott %1$s · %2$s %1$s: %2$s - A Steamnek Adreno-illesztőprogram szükséges; a %1$s fekete képernyőt jeleníthet meg + Nincs kompatibilis grafikus háttérrendszer ehhez: %1$s. A Steam fekete képernyőt jeleníthet meg. Linux futtatókörnyezet %1$s · %2$d%% Nincs telepítve · kb. 3 GB, az első Játék gombnyomáskor települ @@ -1402,8 +1402,8 @@ A vezeték nélküli hibakeresés nem áll rendelkezésre ADB-parancs másolva ROMs mappa kiválasztása - Nem Adreno GPU - 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. + Nem támogatott GPU + Nincs kompatibilis grafikus háttérrendszer ehhez: %1$s. A Steam fekete képernyőt jeleníthet meg. Letöltés: %2$.0f MB. Telepítés mindenképpen Linux futtatókörnyezet eltávolítása Ez törli a futtatókörnyezetet, a benne lévő Steam-klienst és az összes ott telepített játékot. @@ -2117,7 +2117,7 @@ Kísérleti: az illesztőprogramjai teszt buildek 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 A tesztelt hardver alatt (Adreno 725 és újabb): lehet, hogy nem fut - Nem támogatott: a DroidDecknek Adreno (Snapdragon) GPU szükséges + Nem támogatott: nincs kompatibilis grafikus háttérrendszer ehhez a GPU-hoz A GitHub korlátja elérve – próbálja újra később A GitHub HTTP %1$d választ adott diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 902720023..5746a1ed6 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -810,7 +810,7 @@ GPU tidak didukung %1$s · %2$s %1$s: %2$s - Steam memerlukan driver Adreno; %1$s mungkin menampilkan layar hitam + Tidak ada backend grafis yang kompatibel untuk %1$s. Steam mungkin menampilkan layar hitam. Runtime Linux %1$s · %2$d%% Belum terinstal · sekitar 3 GB, terinstal saat Play pertama kali @@ -1390,8 +1390,8 @@ Debugging nirkabel tidak tersedia Perintah ADB disalin Pilih folder ROMs - Bukan GPU Adreno - Turnip mendukung GPU Adreno. Di %1$s, Steam mungkin menampilkan layar hitam. Unduhan: %2$.0f MB. + GPU tidak didukung + Tidak ada backend grafis yang kompatibel untuk %1$s. Steam mungkin menampilkan layar hitam. Unduhan: %2$.0f MB. Tetap instal Hapus runtime Linux Ini menghapus runtime, klien Steam di dalamnya, dan setiap game yang terinstal di sana. @@ -2092,7 +2092,7 @@ Eksperimental: drivernya adalah build uji Dukungan eksperimental Adreno 850: memerlukan driver tampilan dan driver gim eksperimental yang kompatibel; belum diverifikasi di perangkat ini Di bawah perangkat keras yang diuji (Adreno 725 dan lebih baru): mungkin tidak dapat berjalan - Tidak didukung: DroidDeck memerlukan GPU Adreno (Snapdragon) + Tidak didukung: tidak ada backend grafis yang kompatibel untuk GPU ini Batas kecepatan GitHub tercapai - coba lagi nanti GitHub merespons HTTP %1$d diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0b9fcdd67..64ef99818 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -816,7 +816,7 @@ GPU non supportata %1$s · %2$s %1$s: %2$s - Steam richiede un driver Adreno; %1$s potrebbe mostrare una schermata nera + Nessun backend grafico compatibile disponibile per %1$s. Steam potrebbe mostrare una schermata nera. Runtime Linux %1$s · %2$d%% Non installato · circa 3 GB, si installa la prima volta che premi Gioca @@ -1406,8 +1406,8 @@ Il debug wireless non è disponibile Comando ADB copiato Scegli la cartella delle ROM - Non è una GPU Adreno - Turnip supporta le GPU Adreno. Su %1$s, Steam potrebbe mostrare una schermata nera. Download: %2$.0f MB. + GPU non supportata + Nessun backend grafico compatibile disponibile per %1$s. Steam potrebbe mostrare una schermata nera. Download: %2$.0f MB. Installa comunque Rimuovi runtime Linux Verranno eliminati il runtime, il client Steam al suo interno e tutti i giochi installati lì. @@ -2134,7 +2134,7 @@ Sperimentale: i suoi driver sono build di test Supporto sperimentale per Adreno 850: richiede driver di visualizzazione e di gioco sperimentali compatibili; non verificato su questo dispositivo Inferiore all\'hardware testato (Adreno 725 e successive): potrebbe non funzionare - Non supportata: DroidDeck richiede una GPU Adreno (Snapdragon) + Non supportata: nessun backend grafico compatibile disponibile per questa GPU Raggiunto il limite di richieste di GitHub - riprova più tardi GitHub ha risposto HTTP %1$d diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1015207fe..a2bb315e0 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -810,7 +810,7 @@ GPUはサポートされていません %1$s · %2$s %1$s:%2$s - SteamにはAdrenoドライバーが必要です。%1$sでは画面が真っ暗になる可能性があります。 + 利用できる互換グラフィックスバックエンドがありません。%1$sでは画面が真っ暗になる可能性があります。 Linux実行環境 %1$s · %2$d%% 未インストール · 約3GB、初回ゲーム起動時にインストール @@ -1390,8 +1390,8 @@ ワイヤレスデバッグは利用できません ADBコマンドをコピーしました ROMフォルダを選択 - Adreno GPUではありません - TurnipはAdreno GPUをサポートしています。%1$sでは、Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。 + サポート対象外のGPU + %1$sで利用できる互換グラフィックスバックエンドがありません。Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。 インストールする Linux実行環境を削除 実行環境、その中のSteamクライアント、そこにインストールされたすべてのゲームが削除されます。 @@ -2092,7 +2092,7 @@ 実験的:ドライバーはテストビルドです Adreno 850の実験的サポート:互換性のある実験版のディスプレイドライバーとゲームドライバーが必要です。このデバイスでは未検証です テスト済みハードウェア(Adreno 725以降)未満:動作しない可能性があります - サポート対象外:DroidDeckにはAdreno(Snapdragon)GPUが必要です + サポート対象外:このGPUで利用できる互換グラフィックスバックエンドがありません GitHubのレート制限に達しました。後でもう一度お試しください GitHubがHTTP %1$dを返しました diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 09b427356..2772a9ce1 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -810,7 +810,7 @@ GPU 지원되지 않음 %1$s · %2$s %1$s: %2$s - Steam에는 Adreno 드라이버가 필요합니다. %1$s에서 화면이 검게 변할 수 있습니다. + 사용할 수 있는 호환 그래픽 백엔드가 없습니다. %1$s에서 화면이 검게 표시될 수 있습니다. Linux 런타임 %1$s · %2$d%% 설치되지 않음 · 약 3GB, 처음 \"Steam 실행\" 시 설치 @@ -1390,8 +1390,8 @@ 무선 디버깅을 사용할 수 없습니다 ADB 명령이 복사되었습니다 ROM 폴더 선택 - Adreno GPU가 아님 - Turnip은 Adreno GPU를 지원합니다. %1$s에서는 Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB. + 지원되지 않는 GPU + %1$s에서 사용할 수 있는 호환 그래픽 백엔드가 없습니다. Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB. 그래도 설치 Linux 런타임 제거 런타임과 그 안의 Steam 클라이언트, 그리고 런타임에 설치된 모든 게임이 삭제됩니다. @@ -2092,7 +2092,7 @@ 실험적: 드라이버가 테스트 빌드입니다 실험적 Adreno 850 지원: 호환되는 실험용 디스플레이 및 게임 드라이버가 필요하며, 이 기기에서 검증되지 않았습니다 테스트된 하드웨어(Adreno 725 이상)보다 낮음: 실행되지 않을 수 있습니다 - 지원되지 않음: DroidDeck에는 Adreno(Snapdragon) GPU가 필요합니다 + 지원되지 않음: 이 GPU에서 사용할 수 있는 호환 그래픽 백엔드가 없습니다 GitHub 요청 한도에 도달했습니다. 나중에 다시 시도하세요 GitHub 응답: HTTP %1$d diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index f39b73212..604346d7f 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -810,7 +810,7 @@ GPU tidak disokong %1$s · %2$s %1$s: %2$s - Steam memerlukan pemacu Adreno; %1$s mungkin memaparkan skrin hitam + Tiada backend grafik yang serasi tersedia untuk %1$s. Steam mungkin memaparkan skrin hitam. Masa jalan Linux %1$s · %2$d%% Belum dipasang · kira-kira 3 GB, dipasang pada Main pertama @@ -1390,8 +1390,8 @@ Debugging Wayarles tidak tersedia Arahan ADB disalin Pilih folder ROM - Bukan GPU Adreno - Turnip menyokong GPU Adreno. Pada %1$s, Steam mungkin memaparkan skrin hitam. Muat turun: %2$.0f MB. + GPU tidak disokong + Tiada backend grafik yang serasi tersedia untuk %1$s. Steam mungkin memaparkan skrin hitam. Muat turun: %2$.0f MB. Pasang juga Alih keluar masa jalan Linux Ini memadam masa jalan, klien Steam di dalamnya, dan setiap permainan yang dipasang di sana. @@ -2092,7 +2092,7 @@ Eksperimen: pemacunya ialah binaan ujian Sokongan eksperimen Adreno 850: memerlukan pemacu paparan dan permainan eksperimen yang serasi; belum disahkan pada peranti ini Di bawah perkakasan yang diuji (Adreno 725 dan lebih baharu): mungkin tidak dapat berjalan - Tidak disokong: DroidDeck memerlukan GPU Adreno (Snapdragon) + Tidak disokong: tiada backend grafik yang serasi tersedia untuk GPU ini Had kadar GitHub dicapai - cuba lagi kemudian GitHub menjawab HTTP %1$d diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 6efdff992..91921ebca 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -813,7 +813,7 @@ GPU ikke støttet %1$s · %2$s %1$s: %2$s - Steam krever en Adreno-driver; %1$s kan vise en svart skjerm + Ingen kompatibel grafikkbackend er tilgjengelig for %1$s. Steam kan vise en svart skjerm. Linux-kjøretid %1$s · %2$d%% Ikke installert · omtrent 3 GB, installeres ved første Spill @@ -1402,8 +1402,8 @@ Trådløs feilsøking er ikke tilgjengelig ADB-kommando kopiert Velg ROM-mappen - Ikke en Adreno-GPU - Turnip støtter Adreno-GPU-er. På %1$s kan Steam vise en svart skjerm. Nedlasting: %2$.0f MB. + GPU støttes ikke + Ingen kompatibel grafikkbackend er tilgjengelig for %1$s. Steam kan vise en svart skjerm. Nedlasting: %2$.0f MB. Installer likevel Fjern Linux-kjøretid Dette sletter kjøretiden, Steam-klienten inni den, og hvert spill installert der. @@ -2117,7 +2117,7 @@ Eksperimentell: driverne er testbuilder Eksperimentell støtte for Adreno 850: krever kompatible eksperimentelle skjerm- og spilldrivere; ikke bekreftet på denne enheten Under testet maskinvare (Adreno 725 og nyere): den kan ikke kjøre - Ikke støttet: DroidDeck krever en Adreno (Snapdragon) GPU + Støttes ikke: ingen kompatibel grafikkbackend er tilgjengelig for denne GPU-en GitHubs hastighetsgrense ble nådd – prøv igjen senere GitHub svarte HTTP %1$d diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 40e6503ca..cc6cafe92 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -813,7 +813,7 @@ GPU niet ondersteund %1$s · %2$s %1$s: %2$s - Steam heeft een Adreno-driver nodig; %1$s kan een zwart scherm tonen + Er is geen compatibele grafische backend beschikbaar voor %1$s. Steam kan een zwart scherm tonen. Linux-runtime %1$s · %2$d%% Niet geïnstalleerd · ongeveer 3 GB, geïnstalleerd bij de eerste keer Spelen @@ -1402,8 +1402,8 @@ Draadloze foutopsporing is niet beschikbaar ADB-opdracht gekopieerd Kies de ROM-map - Geen Adreno-GPU - Turnip ondersteunt Adreno-GPU\'s. Op %1$s kan Steam een zwart scherm tonen. Download: %2$.0f MB. + Niet-ondersteunde GPU + Er is geen compatibele grafische backend beschikbaar voor %1$s. Steam kan een zwart scherm tonen. Download: %2$.0f MB. Toch installeren Linux-runtime verwijderen Hiermee verwijder je de runtime, de Steam-client erin en alle games die daar zijn geïnstalleerd. @@ -2117,7 +2117,7 @@ Experimenteel: de drivers zijn testbuilds Experimentele ondersteuning voor Adreno 850: vereist compatibele experimentele scherm- en gamedrivers; niet geverifieerd op dit apparaat Onder de geteste hardware (Adreno 725 en nieuwer): werkt mogelijk niet - Niet ondersteund: DroidDeck heeft een Adreno-GPU (Snapdragon) nodig + Niet ondersteund: er is geen compatibele grafische backend beschikbaar voor deze GPU De ratelimiet van GitHub is bereikt - probeer het later opnieuw GitHub antwoordde met HTTP %1$d diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 0265a3ab8..a7f13dc4e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -819,7 +819,7 @@ GPU nieobsługiwany %1$s · %2$s %1$s: %2$s - Steam wymaga sterownika Adreno; %1$s może wyświetlać czarny ekran + Brak zgodnego zaplecza graficznego dla %1$s. Steam może wyświetlać czarny ekran. Środowisko Linux %1$s · %2$d%% Nie zainstalowano · ok. 3 GB, instalowane przy pierwszym „Graj” @@ -1426,8 +1426,8 @@ Debugowanie bezprzewodowe jest niedostępne Skopiowano polecenie ADB Wybierz folder ROM-ów - To nie jest GPU Adreno - Turnip obsługuje GPU Adreno. Na tym urządzeniu (%1$s) Steam może wyświetlać czarny ekran. Pobieranie: %2$.0f MB. + Nieobsługiwany GPU + Brak zgodnego zaplecza graficznego dla %1$s. Steam może wyświetlać czarny ekran. Do pobrania: %2$.0f MB. Zainstaluj mimo to Usuń środowisko Linux Spowoduje to usunięcie środowiska, klienta Steam w nim i wszystkich zainstalowanych tam gier. @@ -2167,7 +2167,7 @@ Eksperymentalny: jego sterowniki to wersje testowe Eksperymentalna obsługa Adreno 850: wymaga zgodnych eksperymentalnych sterowników ekranu i gier; nie zweryfikowano na tym urządzeniu Poniżej testowanego sprzętu (Adreno 725 i nowsze): może nie działać - Nieobsługiwany: DroidDeck wymaga GPU Adreno (Snapdragon) + Nieobsługiwany: brak zgodnego zaplecza graficznego dla tego GPU Osiągnięto limit zapytań GitHub – spróbuj ponownie później GitHub odpowiedział HTTP %1$d diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index aaff56fc0..d546126ad 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -816,7 +816,7 @@ GPU não suportada %1$s · %2$s %1$s: %2$s - O Steam requer um controlador Adreno; %1$s pode mostrar um ecrã preto + Não está disponível um backend gráfico compatível para %1$s. O Steam pode apresentar um ecrã preto. Ambiente de execução Linux %1$s · %2$d%% Não instalado · cerca de 3 GB, instalado ao tocar em Jogar pela primeira vez @@ -1406,8 +1406,8 @@ A depuração sem fios não está disponível Comando ADB copiado Escolha a pasta de ROMs - Não é uma GPU Adreno - O Turnip suporta GPUs Adreno. Com %1$s, o Steam pode mostrar um ecrã preto. Transferência: %2$.0f MB. + GPU não suportada + 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. Instalar mesmo assim Remover runtime Linux Isto elimina o runtime, o cliente Steam que nele existe e todos os jogos lá instalados. @@ -2134,7 +2134,7 @@ Experimental: os controladores são versões de teste Suporte experimental para Adreno 850: requer controladores de ecrã e de jogos experimentais compatíveis; não verificado neste dispositivo Abaixo do hardware testado (Adreno 725 e mais recentes): pode não funcionar - Não suportada: o DroidDeck precisa de uma GPU Adreno (Snapdragon) + Não suportada: não está disponível um backend gráfico compatível para esta GPU Foi atingido o limite de pedidos do GitHub - tente novamente mais tarde O GitHub respondeu HTTP %1$d diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 31ea84d9d..2d1651790 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -816,7 +816,7 @@ GPU sem suporte %1$s · %2$s %1$s: %2$s - A Steam requer um driver Adreno; %1$s pode mostrar uma tela preta + Nenhum backend gráfico compatível está disponível para %1$s. O Steam pode exibir uma tela preta. Runtime do Linux %1$s · %2$d%% Não instalado · cerca de 3 GB, instalado no primeiro Jogar @@ -1406,8 +1406,8 @@ A depuração por Wi-Fi não está disponível Comando ADB copiado Escolha a pasta de ROMs - Não é uma GPU Adreno - O Turnip é compatível com GPUs Adreno. Com %1$s, o Steam pode mostrar uma tela preta. Download: %2$.0f MB. + GPU não suportada + Nenhum backend gráfico compatível está disponível para %1$s. O Steam pode exibir uma tela preta. Download: %2$.0f MB. Instalar mesmo assim Remover runtime Linux Isso exclui o runtime, o cliente Steam dentro dele e todos os jogos instalados nele. @@ -2134,7 +2134,7 @@ Experimental: os drivers são versões de teste Suporte experimental à Adreno 850: requer drivers de tela e de jogos experimentais compatíveis; não verificado neste dispositivo Abaixo do hardware testado (Adreno 725 ou mais recente): pode não funcionar - Incompatível: o DroidDeck precisa de uma GPU Adreno (Snapdragon) + Não suportada: nenhum backend gráfico compatível está disponível para esta GPU O limite de requisições do GitHub foi atingido - tente mais tarde O GitHub respondeu HTTP %1$d diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 859638f0f..acb7d2aae 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -816,7 +816,7 @@ GPU neacceptat %1$s · %2$s %1$s: %2$s - Steam are nevoie de un driver Adreno; %1$s poate arăta un ecran negru + Nu este disponibil un backend grafic compatibil pentru %1$s. Steam poate afișa un ecran negru. Mediul de execuție Linux %1$s · %2$d%% Neinstalat · circa 3 GB, instalat la prima apăsare pe Play @@ -1414,8 +1414,8 @@ Depanarea wireless nu este disponibilă Comanda ADB copiată Alege folderul ROMs - Nu este un GPU Adreno - Turnip acceptă GPU-uri Adreno. Pe %1$s, Steam poate arăta un ecran negru. Descărcare: %2$.0f MB. + GPU neacceptat + Nu este disponibil un backend grafic compatibil pentru %1$s. Steam poate afișa un ecran negru. Descărcare: %2$.0f MB. Instalează oricum Elimină mediul de execuție Linux Aceasta șterge mediul de execuție, clientul Steam din interiorul lui și fiecare joc instalat acolo. @@ -2142,7 +2142,7 @@ Experimental: driverele sale sunt versiuni de test Suport experimental pentru Adreno 850: necesită drivere experimentale compatibile de afișaj și de jocuri; neverificat pe acest dispozitiv Sub hardware-ul testat (Adreno 725 și mai nou): poate să nu ruleze - Neacceptat: DroidDeck are nevoie de un GPU Adreno (Snapdragon) + Neacceptat: nu este disponibil un backend grafic compatibil pentru acest GPU S-a atins limita de solicitări a GitHub - încearcă din nou mai târziu GitHub a răspuns cu HTTP %1$d diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a710de925..dd904460a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -839,7 +839,7 @@ GPU через мост Mali %1$s · %2$s %1$s: %2$s - Steam требует драйвер Adreno; на %1$s возможен черный экран. + Совместимый графический бэкенд недоступен; на %1$s возможен черный экран. Среда выполнения Linux %1$s · %2$d%% Не установлено · около 3 ГБ, устанавливается при первом запуске игры @@ -1472,8 +1472,8 @@ Отладка по Wi-Fi недоступна Команда ADB скопирована Выберите папку ROM - GPU не Adreno - Turnip поддерживает GPU Adreno. На %1$s в Steam может быть черный экран. Загрузка: %2$.0f МБ. + Неподдерживаемый GPU + Для %1$s нет совместимого графического бэкенда. В Steam может отображаться чёрный экран. Загрузка: %2$.0f МБ. Все равно установить Удалить среду выполнения Linux Будут удалены среда выполнения, клиент Steam в ней и все установленные туда игры. @@ -2248,7 +2248,7 @@ Экспериментально: его драйверы — тестовые сборки Экспериментальная поддержка Adreno 850: требуются совместимые экспериментальные драйверы дисплея и игр; не проверено на этом устройстве Ниже проверенного оборудования (Adreno 725 и новее): может не работать - Не поддерживается: DroidDeck требует GPU Adreno (Snapdragon) + Не поддерживается: для этого GPU нет совместимого графического бэкенда Работает через мост Mali на собственном драйвере Vulkan телефона Не поддерживается без моста Mali: Turnip нужен GPU Adreno (Snapdragon) diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index b1546f900..c9d57f28e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -813,7 +813,7 @@ GPU stöds inte %1$s · %2$s %1$s: %2$s - Steam behöver en Adreno-drivrutin; %1$s kan visa en svart skärm + Ingen kompatibel grafikbackend är tillgänglig för %1$s. Steam kan visa en svart skärm. Linux-körmiljö %1$s · %2$d %% Inte installerad · cirka 3 GB, installeras första gången du trycker på Play @@ -1402,8 +1402,8 @@ Trådlös felsökning är inte tillgänglig ADB-kommandot har kopierats Välj ROM-mappen - Inte en Adreno-GPU - Turnip stöder Adreno-GPU:er. På %1$s kan Steam visa en svart skärm. Nedladdning: %2$.0f MB. + GPU stöds inte + Ingen kompatibel grafikbackend är tillgänglig för %1$s. Steam kan visa en svart skärm. Hämtning: %2$.0f MB. Installera trots det Ta bort Linux-körmiljön Det här tar bort körmiljön, Steam-klienten i den och alla spel som är installerade där. @@ -2117,7 +2117,7 @@ Experimentell: dess drivrutiner är testversioner Experimentellt stöd för Adreno 850: kräver kompatibla experimentella skärm- och speldrivrutiner; inte verifierat på den här enheten Under testad maskinvara (Adreno 725 och nyare): det kan hända att det inte fungerar - Stöds inte: DroidDeck behöver en Adreno-GPU (Snapdragon) + Stöds inte: ingen kompatibel grafikbackend är tillgänglig för denna GPU GitHubs hastighetsgräns nåddes - försök igen senare GitHub svarade HTTP %1$d diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index a28cdeeaf..fa9ab354b 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -810,7 +810,7 @@ ไม่รองรับ GPU นี้ %1$s · %2$s %1$s: %2$s - Steam ต้องใช้ไดรเวอร์ Adreno; %1$s อาจแสดงจอดำ + ไม่มีแบ็กเอนด์กราฟิกที่เข้ากันได้สำหรับ %1$s Steam อาจแสดงหน้าจอสีดำ รันไทม์ Linux %1$s · %2$d%% ยังไม่ติดตั้ง · ประมาณ 3 GB จะติดตั้งเมื่อกด Play ครั้งแรก @@ -1390,8 +1390,8 @@ Wireless debugging ไม่พร้อมใช้งาน คัดลอกคำสั่ง ADB แล้ว เลือกโฟลเดอร์ ROM - ไม่ใช่ GPU Adreno - Turnip รองรับ GPU Adreno บน %1$s Steam อาจแสดงจอดำ ดาวน์โหลด: %2$.0f MB + ไม่รองรับ GPU + ไม่มีแบ็กเอนด์กราฟิกที่เข้ากันได้สำหรับ %1$s Steam อาจแสดงหน้าจอสีดำ ดาวน์โหลด: %2$.0f MB ติดตั้งอยู่ดี ลบรันไทม์ Linux การลบนี้จะลบรันไทม์ ไคลเอนต์ Steam ภายใน และเกมทุกเกมที่ติดตั้งอยู่ในนั้น @@ -2092,7 +2092,7 @@ ทดลอง: ไดรเวอร์เป็นบิลด์ทดสอบ รองรับ Adreno 850 แบบทดลอง: ต้องใช้ไดรเวอร์จอแสดงผลและไดรเวอร์เกมแบบทดลองที่เข้ากันได้ ยังไม่ได้ตรวจสอบบนอุปกรณ์นี้ ต่ำกว่าฮาร์ดแวร์ที่ทดสอบแล้ว (Adreno 725 และรุ่นใหม่กว่า): อาจรันไม่ได้ - ไม่รองรับ: DroidDeck ต้องใช้ GPU Adreno (Snapdragon) + ไม่รองรับ: ไม่มีแบ็กเอนด์กราฟิกที่เข้ากันได้สำหรับ GPU นี้ ติดขีดจำกัดอัตราของ GitHub - ลองใหม่ในหลัง GitHub ตอบกลับด้วย HTTP %1$d diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 794f7225e..9015b0d89 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -813,7 +813,7 @@ GPU desteklenmiyor %1$s · %2$s %1$s: %2$s - Steam için Adreno sürücüsü gerekir; %1$s siyah ekran gösterebilir + %1$s için uyumlu bir grafik arka ucu yok. Steam siyah ekran gösterebilir. Linux çalışma zamanı %1$s · %%%2$d Yüklü değil · yaklaşık 3 GB, ilk kez Oyna\'ya basınca yüklenir @@ -1394,8 +1394,8 @@ Kablosuz hata ayıklama kullanılamıyor ADB komutu kopyalandı ROM klasörünü seçin - Adreno GPU değil - Turnip, Adreno GPU\'larını destekler. %1$s üzerinde Steam siyah ekran gösterebilir. İndirme: %2$.0f MB. + Desteklenmeyen GPU + %1$s için uyumlu bir grafik arka ucu yok. Steam siyah ekran gösterebilir. İndirme: %2$.0f MB. Yine de yükle Linux çalışma ortamını kaldır Bu işlem çalışma ortamını, içindeki Steam istemcisini ve oraya yüklenen tüm oyunları siler. @@ -2109,7 +2109,7 @@ Deneysel: sürücüleri test derlemeleri Deneysel Adreno 850 desteği: uyumlu deneysel ekran ve oyun sürücüleri gerekir; bu cihazda doğrulanmadı Test edilen donanımın altında (Adreno 725 ve daha yenisi): çalışmayabilir - Desteklenmiyor: DroidDeck bir Adreno (Snapdragon) GPU\'su gerektirir + Desteklenmiyor: bu GPU için uyumlu bir grafik arka ucu yok GitHub\'ın hız sınırına ulaşıldı - daha sonra yeniden deneyin GitHub HTTP %1$d yanıtı verdi diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 90cb9d7e7..7b9d82f35 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -819,7 +819,7 @@ GPU не підтримується %1$s · %2$s %1$s: %2$s - Steam потребує драйвера Adreno; %1$s може показувати чорний екран + Для %1$s немає сумісного графічного бекенда. Steam може показувати чорний екран. Середовище Linux %1$s · %2$d%% Не встановлено · приблизно 3 GB, встановлюється під час першого натискання «Грати» @@ -1426,8 +1426,8 @@ Бездротове налагодження недоступне Команду ADB скопійовано Виберіть папку ROMs - GPU не Adreno - Turnip підтримує GPU Adreno. На %1$s Steam може показувати чорний екран. Завантаження: %2$.0f MB. + Непідтримуваний GPU + Для %1$s немає сумісного графічного бекенда. Steam може показувати чорний екран. Завантаження: %2$.0f МБ. Усе одно встановити Видалити середовище Linux Буде видалено середовище, клієнт Steam у ньому та всі встановлені там ігри. @@ -2167,7 +2167,7 @@ Експериментально: його драйвери - тестові збірки Експериментальна підтримка Adreno 850: потрібні сумісні експериментальні драйвери дисплея та ігор; не перевірено на цьому пристрої Нижче за протестоване обладнання (Adreno 725 і новіші): може не запрацювати - Не підтримується: DroidDeck потребує GPU Adreno (Snapdragon) + Не підтримується: для цього GPU немає сумісного графічного бекенда Перевищено ліміт запитів GitHub - спробуйте пізніше GitHub відповів HTTP %1$d diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b4c2c9ec6..8541e9c79 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -810,7 +810,7 @@ GPU không được hỗ trợ %1$s · %2$s %1$s: %2$s - Steam cần driver Adreno; %1$s có thể hiện màn hình đen + Không có backend đồ họa tương thích cho %1$s. Steam có thể hiển thị màn hình đen. Runtime Linux %1$s · %2$d%% Chưa cài · khoảng 3 GB, được cài vào lần Play đầu tiên @@ -1390,8 +1390,8 @@ Gỡ lỗi qua mạng không dây không khả dụng Đã sao chép lệnh ADB Chọn thư mục ROM - Không phải GPU Adreno - 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. + GPU không được hỗ trợ + 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. Vẫn cài Gỡ runtime Linux Việc này xóa runtime, client Steam bên trong nó, và mọi game đã cài ở đó. @@ -2092,7 +2092,7 @@ Thử nghiệm: driver của nó là bản build thử nghiệm 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 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 - Không được hỗ trợ: DroidDeck cần GPU Adreno (Snapdragon) + Không được hỗ trợ: không có backend đồ họa tương thích cho GPU này Đã đạt giới hạn tốc độ của GitHub - hãy thử lại sau GitHub trả về HTTP %1$d diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ab4ea5242..3c64d969f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -811,7 +811,7 @@ GPU 通过 Mali 桥接 %1$s · %2$s %1$s:%2$s - Steam 需要 Adreno 驱动;%1$s 可能显示黑屏 + 没有可用的兼容图形后端;%1$s 可能显示黑屏 Linux 运行时 %1$s · %2$d%% 未安装 · 约 3 GB,首次点“游玩”时安装 @@ -1474,8 +1474,8 @@ 无线调试不可用 已复制 ADB 命令 选择 ROM 文件夹 - 不是 Adreno GPU - Turnip 支持 Adreno GPU。在 %1$s 上,Steam 可能显示黑屏。需下载:%2$.0f MB。 + 不受支持的 GPU + 没有适用于 %1$s 的兼容图形后端。Steam 可能显示黑屏。需下载:%2$.0f MB。 仍要安装 移除 Linux 运行时 这会删除运行时、其中的 Steam 客户端,以及安装在其中的所有游戏。 @@ -2171,7 +2171,7 @@ 实验性:其驱动为测试构建 Adreno 850 实验支持:需要配套的实验版显示驱动和游戏驱动;尚未在此设备上验证 低于已测试的硬件(Adreno 725 及更新):可能无法运行 - 不受支持:DroidDeck 需要 Adreno(骁龙)GPU + 不受支持:没有适用于此 GPU 的兼容图形后端 通过 Mali 桥接运行,使用手机自带的 Vulkan 驱动 关闭 Mali 桥接时不受支持:Turnip 需要 Adreno(骁龙)GPU diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index f555a538c..b864d4bf1 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -810,7 +810,7 @@ GPU 不受支援 %1$s · %2$s %1$s:%2$s - Steam 需要 Adreno 驅動程式;%1$s 可能會出現黑畫面 + 沒有可用的兼容圖形後端;%1$s 可能會出現黑畫面 Linux 運行環境 %1$s · %2$d%% 未安裝 · 約 3 GB,首次開啟 Steam 時安裝 @@ -1390,8 +1390,8 @@ 無法使用無線偵錯 已複製 ADB 指令 選擇 ROM 資料夾 - 不是 Adreno GPU - Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。 + 不受支援的 GPU + 沒有適用於 %1$s 的兼容圖形後端。Steam 可能會出現黑畫面。下載大小:%2$.0f MB。 仍要安裝 移除 Linux 運行環境 此操作會刪除運行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。 @@ -2092,7 +2092,7 @@ 實驗性:其驅動程式為測試版本 Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證 低於已測試的硬件(Adreno 725 或更新):可能無法執行 - 不受支援:DroidDeck 需要 Adreno (Snapdragon) GPU + 不受支援:沒有適用於此 GPU 的兼容圖形後端 已達 GitHub 的速率限制 - 請稍後再試 GitHub 回應 HTTP %1$d diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 6fe964d7f..e2988446e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -810,7 +810,7 @@ GPU 不受支援 %1$s · %2$s %1$s:%2$s - Steam 需要 Adreno 驅動程式;%1$s 可能出現黑畫面。 + 沒有可用的相容圖形後端;%1$s 可能出現黑畫面。 Linux 執行環境 %1$s · %2$d%% 未安裝 · 約 3 GB,首次點擊「執行」時安裝 @@ -1390,8 +1390,8 @@ 無法使用無線偵錯 已複製 ADB 指令 選擇 ROM 資料夾 - 不是 Adreno GPU - Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。 + 不受支援的 GPU + 沒有適用於 %1$s 的相容圖形後端。Steam 可能會出現黑畫面。下載大小:%2$.0f MB。 仍要安裝 移除 Linux 執行環境 這會刪除執行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。 @@ -2092,7 +2092,7 @@ 實驗性:其驅動程式為測試版本 Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證 低於已測試的硬體(Adreno 725 及更新機型):可能無法執行 - 不受支援:DroidDeck 需要 Adreno(Snapdragon)GPU + 不受支援:沒有適用於此 GPU 的相容圖形後端 已達 GitHub 速率限制 - 請稍後再試 GitHub 回應 HTTP %1$d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f82841a88..0bb1673a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -210,6 +210,7 @@ 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. DirectAudio could not be downloaded, so game audio uses the slower path this session. This program + 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. Couldn\'t download the Steam client. Your network or VPN may be blocking Steam\'s servers; check the connection and try again. Couldn\'t set up the Steam client. Check your connection and free storage, then try again. %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. @@ -888,7 +889,7 @@ GPU via Mali bridge %1$s · %2$s %1$s: %2$s - Steam needs an Adreno driver; %1$s may show a black screen + No compatible graphics backend is available; %1$s may show a black screen Linux runtime %1$s · %2$d%% Not installed · about 3 GB, installed on first Play @@ -927,6 +928,14 @@ Restart this session to save game debug logs Couldn\'t update logging. Check the switch and try again. Debugging tools + Detailed graphics logs + Steam interface: GPU drawing + 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. + built %1$s + %1$s updated to the newest build + The Mali bridge failed its self-test on %1$s last session (images or draws), so the session used the PanVK driver pair instead. + 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. + Each game\'s Proton, DXVK and driver logs in the session\'s logs. Always on when the GPU is not an Adreno. Latest session logs Saved session logs Keeps the newest %1$d @@ -1552,8 +1561,8 @@ Wireless debugging is unavailable ADB command copied Choose the ROMs folder - Not an Adreno GPU - Turnip supports Adreno GPUs. On %1$s, Steam may show a black screen. Download: %2$.0f MB. + Unsupported GPU + No compatible graphics backend is available for %1$s. Steam may show a black screen. Download: %2$.0f MB. Install anyway Remove Linux runtime This deletes the runtime, the Steam client inside it, and every game installed there. @@ -2310,11 +2319,27 @@ Toon outlines plus sharpening. Not an Adreno GPU + System PowerVR driver: compatibility varies by device + Experimental: RADV or the system Xclipse driver + System PowerVR + Uses the built-in drivers for this GPU + The system PowerVR driver is selected for the runtime and display. + No open driver covers this Mali yet: the Mali bridge is on. + 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. + 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. + Mali (Venus) + the Mali bridge + No open driver covers this Mali yet: %1$s is used, on the phone\'s own driver. + %1$s isn\'t published yet: %2$s is used until it is. + %1$s isn\'t available, and this phone\'s driver reports Vulkan %2$s (Venus needs 1.2): the Mali bridge is used. + %1$s isn\'t published yet: the Mali bridge is on until it is. + Queued: %1$s (waiting for a different runtime driver) + DXVK is provided by the system PowerVR runtime. Other DXVK packages can be stored but are not applied with this driver. Supported Experimental: its drivers are test builds Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device Below tested hardware (Adreno 725 and newer): it may not run - Not supported: DroidDeck needs an Adreno (Snapdragon) GPU + Not supported: no compatible graphics backend is available for this GPU Runs through the Mali bridge, on the phone\'s own Vulkan driver Not supported with the Mali bridge off: Turnip needs an Adreno (Snapdragon) GPU @@ -2869,6 +2894,13 @@ Enable GOG achievements in Stores settings Achievements unavailable; check session logs Achievement unlocked + Mali (Venus): off + Mali (Venus): on + 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. + Mali bridge: off + %1$s cannot use the Mali bridge (it needs a Mali GPU without a system-driver route), so this switch has no effect here. + Mali bridge: on + %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. Paste D-pad Left stick @@ -3644,6 +3676,11 @@ Syncthing Reload Syncthing isn\'t running. + Safe mode + Safe mode: on + Safe mode: off + Most compatible. Recommended for a new device. + Faster where this GPU passed its checks. 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. You can try Steam settings → Steam client → x86-64 (experimental). It needs an additional download and may run more slowly. The Linux graphics driver could not find a usable Vulkan device. Check that the selected Linux GPU driver supports this GPU. @@ -3723,19 +3760,11 @@ Cloud settings Xbox Cloud Gaming Game Pass - Mali bridge: off - Mali bridge: on - Safe mode - Safe mode: on - Safe mode: off - Most compatible. Recommended for a new device. - Faster where this GPU passed its checks. DXVK DXVK: automatic DXVK: Proton\'s own Uses an older DXVK where this GPU\'s Vulkan is too old for Proton\'s. Every Proton keeps its own DXVK, even where this GPU\'s Vulkan may be too old for it. - %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. Steam integration %1$d of %2$d on Set up diff --git a/app/src/test/java/com/droiddeck/launcher/core/DeviceSupportTest.kt b/app/src/test/java/com/droiddeck/launcher/core/DeviceSupportTest.kt new file mode 100644 index 000000000..633888998 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/core/DeviceSupportTest.kt @@ -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)) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt index 409637e0c..638533b12 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt @@ -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"))) } diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DeviceInfoTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DeviceInfoTest.kt new file mode 100644 index 000000000..808425f67 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DeviceInfoTest.kt @@ -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)) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt index 6082d3ddf..b9c0ce819 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt @@ -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) + } } diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairUpdateTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairUpdateTest.kt new file mode 100644 index 000000000..eb3a556de --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairUpdateTest.kt @@ -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))) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt index 6ff43cb12..514afac36 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt @@ -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())) + } } diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt index 7348210c9..cf4bfe76c 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt @@ -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) } diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/SystemVulkanPruningTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/SystemVulkanPruningTest.kt new file mode 100644 index 000000000..9f593658f --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/gpu/SystemVulkanPruningTest.kt @@ -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 + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/SystemVulkanSelectionTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/SystemVulkanSelectionTest.kt new file mode 100644 index 000000000..29a25037f --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/gpu/SystemVulkanSelectionTest.kt @@ -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" })) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt b/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt index 241d81545..966934ef0 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/ForcedPackageTest.kt @@ -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") diff --git a/app/src/test/java/com/droiddeck/launcher/session/GraphicsReportTest.kt b/app/src/test/java/com/droiddeck/launcher/session/GraphicsReportTest.kt new file mode 100644 index 000000000..0900617ae --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/GraphicsReportTest.kt @@ -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")) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/OrphanReaperProcessTest.kt b/app/src/test/java/com/droiddeck/launcher/session/OrphanReaperProcessTest.kt new file mode 100644 index 000000000..bb766e75c --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/OrphanReaperProcessTest.kt @@ -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 }) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/PowerVrAutoDriversTest.kt b/app/src/test/java/com/droiddeck/launcher/session/PowerVrAutoDriversTest.kt new file mode 100644 index 000000000..fe2f6a812 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/PowerVrAutoDriversTest.kt @@ -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)) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/PowerVrComponentsTest.kt b/app/src/test/java/com/droiddeck/launcher/session/PowerVrComponentsTest.kt new file mode 100644 index 000000000..3a768aee7 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/PowerVrComponentsTest.kt @@ -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)) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/PowerVrQueuedComponentsTest.kt b/app/src/test/java/com/droiddeck/launcher/session/PowerVrQueuedComponentsTest.kt new file mode 100644 index 000000000..66aaeb650 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/PowerVrQueuedComponentsTest.kt @@ -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) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionEndHintTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionEndHintTest.kt index c5f75297e..4bc4aef33 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionEndHintTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionEndHintTest.kt @@ -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(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" } } } diff --git a/app/src/test/java/com/droiddeck/launcher/ui/ComponentsPageProfileTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/ComponentsPageProfileTest.kt index 501438788..8d9d59f8d 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/ComponentsPageProfileTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/ComponentsPageProfileTest.kt @@ -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, + ) + } + } + } + } diff --git a/app/src/test/java/com/droiddeck/launcher/update/AppUpdatesTest.kt b/app/src/test/java/com/droiddeck/launcher/update/AppUpdatesTest.kt index 5bb78ff9c..1dad18e92 100644 --- a/app/src/test/java/com/droiddeck/launcher/update/AppUpdatesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/update/AppUpdatesTest.kt @@ -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) diff --git a/docs/development/american-nightmare-pixel-debug.md b/docs/development/american-nightmare-pixel-debug.md new file mode 100644 index 000000000..ef5992dd0 --- /dev/null +++ b/docs/development/american-nightmare-pixel-debug.md @@ -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 + at +`d0f7264ea8e402d30c868779198dc2fd74e7431c`. That proposal's attribution and existing +source/license provenance are retained. The semaphore correction originated in +. 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 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 +. 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. diff --git a/docs/development/gpu-pending-completion.md b/docs/development/gpu-pending-completion.md new file mode 100644 index 000000000..db036ba21 --- /dev/null +++ b/docs/development/gpu-pending-completion.md @@ -0,0 +1,88 @@ +# Compositor work whose completion is unknown + +A timed-out fence wait does not cancel submitted GPU work. Continuing after +that result can reset a command buffer or fence still in use, or destroy an +image and its memory while the GPU may still read them. Previously the finite +wait paths returned without putting the compositor into a terminal state, and +swapchain teardown ignored the result of `vkDeviceWaitIdle`. + +An unsuccessful completion check other than real `VK_ERROR_DEVICE_LOST` now +puts the compositor in a distinct completion-unknown state. It stops subsequent +presentation and retains potentially pending image/memory and swapchain +resources until the DroidDeck app process exits. Device loss continues through +the existing device-loss path. This is deliberate retention rather than an +automatic recovery; restarting a guest session cannot reset the in-process GPU +state, so the diagnostic asks for an app restart. + +Initialization checks the black-image submit and fence result. An unsuccessful +empty completion-marker submit no longer causes a wait on an unsignalled fence. +Surface-rebuild and effect-map paths stop when teardown cannot establish idle. +Acquire retries also stop: retaining the old swapchain is not permission to +acquire from it again. A later frame-generation acquire that ends in a terminal +state returns before the completion marker, fence wait or any further recording. +Real device loss on present is handled before attempting that marker, too. +Mapped SHM bytes are retained unchanged, including same-size `wl_shm` and +single-pixel commits; keeping only the allocation would still race a GPU read. +Off-screen pass copy/abort and the internal fence wait reject terminal states +without further Vulkan calls. Release fds are still consumed on rejected copies. +The normal frame fence still has its existing infinite timeout; this change is +not a general GPU hang watchdog. + +Externally owned DMA-BUF contents need the same protection. `vkp_completion_unknown()` +exposes the terminal state to compositor-thread lifecycle paths. Replacement, +surface/client destruction, cursor/idle-role releases, previously queued paced +releases and SurfaceFlinger callbacks no longer return a DMA-BUF to its client +after quarantine. The compositor retains DMA-BUF references/backing FDs and AHB +references until app process exit. A display fence alone does not establish that +the compositor's Vulkan reads completed. Queued releases detach destroyed surfaces +and remain held without repeatedly arming an expired timer. New terminal releases +require no allocation, so allocation failure cannot fall back to an unsafe release. +Copied SHM releases and healthy/genuine-device-loss cleanup keep their existing +behaviour. This intentionally prevents further client buffer reuse; restarting +the Android app process, not just the guest session, is required. + +## Regression coverage + +`python3 -m unittest discover -s tools/tests -p test_vk_pending_completion.py -v` +compiles the production wait/destructor and swapchain teardown functions with a +fake Vulkan dispatch table. The two cases cover success, timeout, host-memory +errors, genuine device loss, resource retention and suppressed repeated teardown. +No real GPU fault is injected. The harness matches main's Vulkan-image resources; +it does not assume the experimental PowerVR branch's additional AHB fields. + +`python3 -m unittest discover -s tools/tests -p test_vk_terminal_callers.py -v` +additionally compiles the **entire production `vk_present.c`**, with fake Vulkan +dispatch callbacks and stub external modules. The compositor's `take_shm` and +`take_pixel` callers are inserted verbatim, not rewritten. It needs the Android +NDK's Vulkan-Headers (`ANDROID_HOME`/`ANDROID_SDK_ROOT`/`NDK`), a host Vulkan +development package, or `DROIDDECK_VULKAN_INCLUDE`; absent headers are explicitly +reported as a skip. No Android runtime or GPU is required. + +The 12 scenarios include first/later OUT_OF_DATE and SURFACE_LOST followed by an +idle failure, a finite blit timeout followed by same-size SHM/single-pixel +uploads, normal presentation/frame generation, a healthy later acquire timeout +whose earlier work still needs draining, real acquire/present device loss, and +terminal off-screen pass entry. Every work-issuing fake callback asserts the +device is live. After quarantine, the tests verify retained handles and unchanged +mapped bytes, and probe the public render/blit/readback/import/pass entry points +for suppressed acquire/submit/wait/reset and destruction. Consumed fds are checked. +Against PR #560's `68dc1c45` the same harness compiles, but nine scenarios fail +(the three healthy controls pass); the corrected production unit passes all 12. + +The baseline production fence/destructor compiles but fails the timeout-state +oracle; the corrected functions pass both cases with `-Wall -Wextra -Werror`. +The change was also exercised in an independently integrated Pixel PowerVR Lab, +but its short game observations do not establish the cause of the earlier GNOC +reboot or sustained device stability. Main's native CI and APK build remain +separate validation. + +`python3 -m unittest discover -s tools/tests -p test_vk_dmabuf_quarantine.py -v` +compiles the production compositor/AHB release, reference and listener functions +verbatim, using the real Wayland list layouts/macros with fake IPC/timer/AHB +endpoints. It covers paced replacement, immediate surface cleanup, already queued +releases, a mixed DMA-BUF/SHM queue, client destruction, a display-fence callback, +healthy cleanup and genuine device loss. It checks withheld releases, retained +FDs/references/AHB ownership, removal of surface listeners and disarmed timers. +The same harness can test an old source directory via +`DROIDDECK_DMABUF_TEST_SOURCE`; it does not inject a real driver fault or prove +long-term device stability. diff --git a/docs/development/mali-venus.md b/docs/development/mali-venus.md new file mode 100644 index 000000000..8a722ff7e --- /dev/null +++ b/docs/development/mali-venus.md @@ -0,0 +1,269 @@ +# DroidDeck on Mali (Pixel 9 Pro) through Venus + +Venus implementation notes and historical prototype measurements from a Pixel 9 Pro +(Tensor G4, Mali-G715, Android 17). These October prototype results do not validate +the current DroidDeck integration. + +## Status (2026-10-08, night) + +Prototype 13 is the APK on the phone, a release build (not debuggable). +Prototypes 12 and 13 are published as GitHub prereleases. +Steam Big Picture shows a clean, live picture, and Slay the Spire 2 runs at about 40 fps. + +Direct3D 9-11 games run through DXVK. +Test game: Hollow Knight Silksong (Unity, D3D11), app id 1030300. +- Prototype 12: 35-40 fps in game, stable, with good audio. The phone was not throttling. +- Prototype 13 (cached frame readback, see Performance): 45-52 fps, averaging 47-52. + +Done and verified on the phone: +- The Mali compatibility layer (`tools/venus/layer`) loads and reports the six features DXVK requires. +- DXVK accepts the GPU and creates its device. D3D11 runs at feature level 11_0. +- Silksong loads and plays. +- The APK carries the layer and the Venus ICD; nothing on the phone is installed by hand any more. + +Fixed: a crash in the Venus guest driver, about 20 s after launch, with a black screen until then. +- Where: `vtest_vcmd_submit_cmd2` (`src/virtio/vulkan/vn_renderer_vtest.c`), on the `dxvk-submit` thread inside `vkQueueSubmit2`. +- What: it reads `batch->syncs[1]->sync_id` and `syncs[1]` is the pointer `0x1`. +- Cause: two threads submit to the same queue at once. + - Venus's async present thread (`vn_wsi`) makes the WSI submits. On vtest, `vn_wsi_fence_wait` drops the queue lock while it waits on a fence. + - It assumes one submit per swapchain. But `wsi_queue_submit2_unordered` makes a second, empty submit for DXVK's present fence. + - DXVK's thread passes `vn_wsi_flush` in that window and submits at the same time. + - Both use the per-queue scratch buffer `queue->storage`. The empty submit's `sync_vals[0] = 1` lands on DXVK's `syncs[1]`. + - gamescope and Zink use no present fences, so Steam and Slay the Spire 2 never hit it. +- Fix: `vn_queue_submit` holds a new per-queue `submit_mutex`. Verified with Silksong. + +Other open problems: +- Steam's UI blocks 30 s at a time on `IClientFriends::GetVoiceMicrophoneVolume`. A launch looks stuck on "Launching" even after the game died. +- The render server aborts ("pthread_mutex_lock called on a destroyed mutex") when a context dies after an error. Not fixed. +- The phone runs short of memory: about 1-2 GB free during a session. BC decoding makes textures 4-8x bigger. +- Prototype 13 is not debuggable, so private-storage access through `adb run-as` is unavailable. + - To debug again: add `debuggable true` to the `release` build type in `app/build.gradle`, rebuild, reinstall. Keep it out of commits. + - A debuggable build shows a harmless "not 16 KB-compatible" dialog. + +## Performance + +How a game frame reaches the screen (Silksong, 1280x720): +1. The game draws through Venus. gamescope's WSI layer turns its X11 window into a Wayland surface on gamescope. +2. Venus's present thread (`vn_wsi[0,0]`) runs WSI's software path (`MESA_VK_WSI_DEBUG=sw`). + - It blits the image into a host-visible buffer, waits for the GPU, and `memcpy`s the buffer into a `wl_shm` buffer (3.6 MB). +3. gamescope uploads that `wl_shm` buffer into a texture (patch 0927), composites, and hands a dma-buf to the app. +4. The app's compositor draws it at 60 Hz. Its own work is about 2-3 ms a frame (`wayland.log`, `perf` lines). + +Measured with `tools/venus/measure-present.patch`: + +| Per frame | Prototype 12 | Prototype 13 | +|---|---|---| +| Frame interval | 26-29 ms | ~20 ms | +| `memcpy` out of the readback buffer | 7-10 ms (~400 MB/s) | ~1.3 ms, cache sync included | +| Present thread total | 17-20 ms | ~12 ms | +| GPU fence waits (two per present) | ~4 ms | ~4.5 ms | +| Game blocked in `vn_wsi_flush` | ~0.4 ms | ~0.01 ms | + +Why the copy was slow: Venus hid Mali's cached memory type. +- Mali's native types (`memtypes` probe): 0 = device-local, host-visible, coherent (uncached); 1 = device-local, host-visible, cached (not coherent). +- `vn_physical_device_init_memory_properties` strips host-visible from cached types that are not coherent, and labels type 0 "cached" for app compatibility. +- So WSI's readback buffer landed on uncached memory, and the CPU read it at ~400 MB/s. + +The fix (prototype 13): +- Venus allocates WSI's readback buffer in the hidden cached type (`vn_wsi_memory_type_index`). Apps still never see that type. +- The vtest renderer keeps that buffer's dma-buf fd and implements flush and invalidate as `DMA_BUF_IOCTL_SYNC`. +- WSI calls `vkInvalidateMappedMemoryRanges` before every CPU read of a frame (Wayland and X11 software paths). +- The render server allocates cached memory types from the cached dma-buf heap (`/dev/dma_heap/system`). + +What is left on the present thread (~12 ms): +- ~4.5 ms in two GPU fence waits. WSI makes a second, empty submit for DXVK's present fence, and Venus waits on each. +- ~6 ms in those two submits and their Venus round trips. +- Thread sample (`top -H`, prototype 12): no thread at 100%. Game main thread 48%, `vn_wsi` 30%, `dxvk-frame` 30%, Unity render thread 25%. + - `dxvk-frame` mostly waits for presents; 30% CPU looks like Venus polling in its semaphore waits. + +## Repository entrypoints + +- `tools/venus/ci-build.sh`: guest ICD, compatibility layer and Android server build. +- `tools/venus/release.env`: Venus component pin consumed by APK builds. +- `.github/workflows/build-venus.yml`: Venus component artifact workflow. +- `tools/gamescope/build-in-arch.sh`: patched gamescope build. +- `tools/gamescope/release.env`: gamescope component pin consumed by APK builds. +- `.github/workflows/build.yml`: APK build and regression tests. + +## How it works + +The session's Linux processes (glibc) cannot load Mali's Android (bionic) Vulkan driver. +Venus bridges them: + +1. The guest loads Mesa's Venus ICD (`libvulkan_virtio.so`) with `VN_DEBUG=vtest`. +2. The ICD serializes Vulkan calls over a unix socket: `files/venus/vtest.sock`. +3. virglrenderer's `virgl_test_server --venus --no-virgl` runs on the Android side as an app child process. +4. Per client, it forks `virgl_render_server`, which replays the calls on Mali's system Vulkan driver. + +For Wine games, the Mali compatibility layer sits between DXVK (via winevulkan) and the Venus ICD. + +The app's own compositor uses Mali's system driver directly. + +## Mali compatibility layer (`tools/venus/layer`) + +Why: DXVK 3.1.1 refuses any GPU without six features, and Mali-G715 has none of them. +The list comes from `dxvk/dxvk_device_info.cpp`, `getFeatureList`, compared with `vulkaninfo` through Venus. +Everything else DXVK requires is present: Vulkan 1.4, geometry and tessellation shaders, maxPushConstantsSize 256. + +What it does, per feature: +- `fillModeNonSolid`: LINE and POINT polygon modes draw as FILL. +- `multiViewport`: reports maxViewports 16. Pipelines and `vkCmdSet{Viewport,Scissor}[WithCount]` keep only the first. +- `robustBufferAccess2`: the device gets `robustBufferAccess` (version 1) instead. +- `shaderClipDistance`: emulated per pipeline. + - The last pre-rasterization stage writes the distances to a free varying location. + - The fragment shader reads them and runs `OpKill` when one is negative. + - Only when that stage is the only one using distances and the fragment shader's code is known. + - Otherwise the distances are dropped. Depth-only passes lose clipping too. + - DXVK passes shader code inline (`VkShaderModuleCreateInfo` in the stage), so its pipelines qualify. + - Mali has no `graphicsPipelineLibraryIndependentInterpolationDecoration`, so DXVK compiles whole pipelines and the layer sees both stages. +- `shaderCullDistance`: dropped. +- `textureCompressionBC`: BC1-BC7 images are created as RGBA8, R8, RG8 or RGBA16F. + - Every upload into one is decoded on the CPU with `bcdec` while the copy is recorded. + - The source must be memory the application has mapped; DXVK's staging memory is. + - Decoded texels go through the layer's own staging buffers, recycled when the command buffer is reset. + - Copies from a BC image into a buffer, and between BC and uncompressed images, are skipped with a warning. + +Files: +- `layer.c`: loader plumbing, feature and limit reporting, device creation, the hook table. +- `pipeline.c`: pipelines, shader modules, viewports. +- `spirv.c`: the SPIR-V rewriter for clip and cull distances. +- `bc.c`: BC formats, images and uploads. +- `VkLayer_droiddeck_mali_compat.json`: implicit layer manifest. + +When it is active: +- Only in Wine processes (`WINEPREFIX` set), and only for features the GPU lacks. +- `DROIDDECK_MALI_COMPAT=1` forces it on, `0` off. `DROIDDECK_MALI_COMPAT_DISABLE=1` keeps the loader from loading it. +- Wine's own `explorer.exe` uses Zink, so it gets the layer too. +- It logs one line per device: `droiddeck-mali-compat: emulating …`. + +How it ships: +- `build-apk.sh` copies `layer/` into the APK's `assets/linuxfs/usr/local/…`. +- `SessionFiles.kt` stages both files into the guest at every session start (its `optional` list). + - Guest paths: `usr/local/lib/droiddeck-mali/` and `usr/local/share/vulkan/implicit_layer.d/`. + +Not done yet: +- Unit tests for the SPIR-V rewriter. It is untested beyond DXVK's shaders, and unknown whether Silksong's shaders used clip distances at all. +- Performance: BC decoding on the CPU makes loads slow. A compute-shader decoder would be the next step. + +## Changes on the branch + +App (Kotlin/C): +- `session/VenusComponent.kt` (new): on a non-Adreno GPU, starts the vtest server and stages the ICD to `files/venus/`. + - It sets the guest env: `VK_ICD_FILENAMES`, `VN_DEBUG=vtest`, `VTEST_SOCKET_NAME`, `MESA_VK_WSI_DEBUG=sw`. +- `session/SessionService.kt`: uses the Venus env instead of the Turnip ICD, and adds the component. +- `gpu/TurnipDriver.java`: the compositor uses the system Vulkan driver on non-Adreno. +- `cpp/waylandcomp/src/vk_present.c`: + - Enables only the dma-buf extensions the driver has. + - Logs the driver's full extension list. + - Gives a zero row pitch a packed 64-byte row (gamescope's 1x1 root buffer). +- `gpu/GpuInfo.kt`, `FrontEndActivity.kt`, `core/DeviceReport.kt`: wording, plus a "Venus" line in `device.txt`. +- `session/SessionFiles.kt`: stages the Mali compatibility layer when the APK carries it. +- `tools/linuxfs/preload/venus.c` (new, part of `libblsession`): unsets `LIBGL_KOPPER_DISABLE` when `VN_DEBUG` has `vtest`. + - Steam's `steamwebhelper.sh` sets it unconditionally. + - Without kopper and without DRI3, Zink gets no GL context, and the Steam UI crash-loops. + - Steam restores that script when it verifies its install, so it cannot be edited. + +`tools/venus/mesa-venus-vtest.patch` (Mesa 26.2.4, guest side): +- No `VK_EXT_physical_device_drm` under vtest. gamescope otherwise demanded a primary node. +- Sync-fd export for fences and semaphores under vtest: a CPU wait that returns -1 (already signaled). gamescope requires `VK_KHR_external_semaphore_fd`. +- WSI skips attaching a -1 sync file to a dma-buf. +- `vn_queue_submission_init_syncs` submits only the sync slots it filled. +- Timeline semaphores free all `queue_count + 1` renderer syncs. Upstream freed one too few and leaked a vtest sync each. +- `vn_queue_submit` runs under a per-queue `submit_mutex`. The async present thread raced the app's submits; see Status. +- WSI's CPU readback buffer goes to Mali's hidden cached memory type; see Performance. + - `vn_physical_device`: keeps the renderer's real memory flags, and picks the hidden type under vtest. + - `vn_renderer_vtest.c`: flush and invalidate of incoherent memory are `DMA_BUF_IOCTL_SYNC` on the bo's dma-buf. + - WSI common code: `vkInvalidateMappedMemoryRanges` before every CPU read of a frame. + +`tools/venus/measure-present.patch`: per-frame present timings on stderr, applied on top for `MEASURE=1` builds only. + +`tools/venus/virglrenderer-android.patch` (virglrenderer 1.3.0, host side): +- Mali on Android can import dma-bufs but cannot export any memory. + - Buffers report dma_buf features 0x4 (importable only); opaque fd reports nothing. + - virglrenderer's minigbm fallback does not exist on Android. +- On Android the server allocates the dma-buf itself and imports it. + - Source for coherent types: `/dev/dma_heap/system-uncached`, then `/dev/dma_heap/system`, then an `AHardwareBuffer` fd. + - Uncached because Mali here is not cache-coherent with the CPU, and nobody calls `DMA_BUF_IOCTL_SYNC` on coherent memory. + - The guest maps coherent memory without syncs, and on the cached heap frames tore at cache-line granularity: the streaks. + - Cached types (not coherent) come from `/dev/dma_heap/system`. Whoever maps them syncs through the dma-buf. + - The fd rides the existing udmabuf plumbing. +- Image and buffer queries report "importable dma-buf" as exportable too. gamescope keeps only exportable modifiers. +- Exports of any memory type go through that allocator. +- Logs what the driver reports, and failed allocations. + +`tools/gamescope/patches/0927-shm-buffers-upload-every-commit.patch`: +- Xwayland has no glamor here, so it reuses about three wl_shm buffers with new pixels. +- gamescope cached one texture per buffer, copied at first import. +- The screen cycled through three stale frames: the flicker. Input and audio still worked. +- The patch caches dma-buf textures only, and uploads shm buffers on every commit. +- See `tools/gamescope/PATCHES.md`. +- The session stages gamescope from the APK assets every start, so it needs an APK rebuild. + +## Build + +Run workflows on the branch being tested: + +1. Build the Venus component with `.github/workflows/build-venus.yml`. Its `venus` + artifact contains `venus.tzst` and its SHA256. For a local Linux build with Docker, + the Android NDK, Meson and Ninja, run: + `ANDROID_NDK_HOME=/path/to/ndk WORK="$PWD/venus-work" bash tools/venus/ci-build.sh`. +2. After gamescope patches change, run `.github/workflows/build-gamescope.yml`. + Leave its tag input empty for an artifact-only build. This runs + `tools/gamescope/build-in-arch.sh` in an Arch Linux ARM container and produces + `gamescope.tzst` and its SHA256. +3. Publish component bundles and update their repository, tag and SHA256 pins in + the corresponding `release.env` before building an APK that fetches them. + Building Venus alone does not rebuild gamescope or the APK. +4. Push the branch for `.github/workflows/build.yml` to prepare pinned components, + compile the APK and run Android, Python and native tests. For a configured + local environment, `tools/build_local.sh` fetches the same pinned inputs and + builds the APK. + +Release APKs remain non-debuggable. Private-storage debugging requires a separate +local development build; exported session logs work with standard builds. + +## Device validation + +Install a compatibly signed APK with `adb install -r /path/to/build.apk`. Start a +Steam session with Mali (Venus) selected in Setup. Check the exported session +folder under `/sdcard/Download/DroidDeck/`: + +- `session.log`: gamescope, Steam and game output. +- `venus.log`: vtest and render server output. +- `wayland.log`: compositor frames and windows. +- `events.jsonl`: `frame.first` records the first displayed frame. +- `crash.log`: crash diagnostics when available. + +Use `tools/droiddeckctl` for diagnostics on standard builds. The +`tools/venus/guest-vulkaninfo.sh` helper requires a configured development build. +Stop the session through DroidDeck to collect the final Steam logs. + +Validate Steam UI rendering and a game on real Mali hardware, then check Adreno +regressions. Historical prototype results do not replace testing a new APK. + +## Failure history (prototype → cause → fix) + +1. Official APK installed → Turnip ICD, no GPU → install the prototype. +2. gamescope: "physical device has no primary node" → Mesa: drop `EXT_physical_device_drm` under vtest. +3. gamescope: missing `VK_KHR_external_semaphore_fd` → Mesa: CPU-wait sync-fd export. +4. Hang on loading: server "minigbm_allocation is not enabled" → virgl: relax the export check. Not enough: Mali exports nothing. +5. Same hang → virgl: allocate a dma-buf (dma-heap or AHB) and import it. +6. gamescope assert `modifiers.size() > 0` → virgl: report importable dma-buf as exportable; route exports through the allocator. +7. Updater "zink: could not create swapchain"; Xwayland has no glamor, so no DRI3 → `MESA_VK_WSI_DEBUG=sw`. +8. steamwebhelper restart loop, "failed to acquire a gl context" → `venus.c` preload unsets `LIBGL_KOPPER_DISABLE`. +9. Frames reach the compositor but "0 windows open" and the overlay stays → compositor: fix stride 0 on gamescope's 1x1 root buffer. +10. Sign-in screen shows, with dash streaks across it → virgl: allocate from the uncached dma-heap. +11. Picture looks frozen or flickers between old frames, though clicks play sounds → gamescope patch 0927. +12. Steam sign-in and Slay the Spire 2 at about 40 fps. Silksong: "d3d11: failed to create factory"; DXVK skips the GPU for `fillModeNonSolid` → the Mali compatibility layer. +13. Silksong: D3D11 starts, black screen, then a crash in Venus's `vtest_vcmd_submit_cmd2` → Mesa: serialize queue submits against the async present thread. Silksong plays at 35-40 fps. +14. Silksong held at 35-40 fps; Venus's present thread spent half its time copying frames from uncached memory → cached readback buffer (prototype 13), 47-52 fps. + +## Next steps + +1. Performance, measured first with `MEASURE=1` (see Performance): + - The two GPU fence waits and two submits per present (~10 ms on the present thread). + - The `dxvk-frame` thread at 30% CPU, probably Venus polling in semaphore waits. +2. Silksong: check BC textures look right and loading time is bearable. +3. Test more games on different engines: GameMaker, FNA (Celeste, Stardew Valley), Unity D3D9, Godot. Fix shared gaps in the layer, never per game. +4. Direct3D 12 games use VKD3D-Proton, which has its own requirement list. Check it the same way. +5. Clean up: delete `droiddeck-env` when debugging is done. diff --git a/tools/build_local.sh b/tools/build_local.sh index 93ffd51fd..041d6b313 100755 --- a/tools/build_local.sh +++ b/tools/build_local.sh @@ -139,8 +139,15 @@ docker run --rm --platform "${docker_platform}" \ aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \ -o "$d/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$d/libssbs.so" + mkdir -p "$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/" mkdir -p "$d/usr/local/bin" external/MaliBridge/scripts/build-client-linux.sh "$d" aarch64-linux-gnu-gcc + 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" aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$d/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-clipboard" for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do @@ -417,6 +424,7 @@ if ! restore_step comet "${comet_key}" "${comet_out}"; then DROIDDECK_BUILD_IMAGE="${image_name}" bash tools/comet/build-local.sh save_step comet "${comet_key}" "${comet_out}" fi +python3 tools/powervr/fetch.py bash tools/packs/build.sh app/build/packs python3 tools/release/runtime_inputs.py record ./gradlew "${gradle_task}" --console=plain -PndkVersion="${ndk_version}" ${gradle_rust_arg} diff --git a/tools/docker_platform.sh b/tools/docker_platform.sh index 4d4744875..3360c81ec 100644 --- a/tools/docker_platform.sh +++ b/tools/docker_platform.sh @@ -4,7 +4,7 @@ if [[ -z "${docker_platform}" ]]; then fi case "${docker_platform}" in linux/arm64|linux/arm64/*) docker_platform=linux/arm64 ;; esac if [[ -z "${DROIDDECK_BUILD_IMAGE:-}" ]]; then - DROIDDECK_BUILD_IMAGE=droiddeck-local-cross:24.04-v3 + DROIDDECK_BUILD_IMAGE=droiddeck-local-cross:24.04-v4 [[ "${docker_platform}" == linux/arm64 ]] && DROIDDECK_BUILD_IMAGE+=-arm64 fi export DROIDDECK_DOCKER_PLATFORM=${docker_platform} DROIDDECK_BUILD_IMAGE diff --git a/tools/dxvk/build.sh b/tools/dxvk/build.sh new file mode 100755 index 000000000..d403aa050 --- /dev/null +++ b/tools/dxvk/build.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# CI component build; this does not change the APK or an installed Proton depot. +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +. "$root/tools/dxvk/source.env" +mkdir -p "${1:?output directory required}" +out=$(cd "$1" && pwd) +# Refuse to mix a pinned build with prior DLLs or an incomplete previous build. +# Include dotfiles in the check and leave all existing contents untouched. +shopt -s nullglob dotglob +existing=("$out"/*) +if ((${#existing[@]})); then + printf '%s\n' 'output directory must be empty; use a fresh build directory' >&2 + exit 1 +fi +shopt -u nullglob dotglob +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +git init -q "$work/source" +git -C "$work/source" fetch --depth=1 https://github.com/doitsujin/dxvk.git "$DXVK_REVISION" +git -C "$work/source" checkout -q --detach FETCH_HEAD +test "$(git -C "$work/source" rev-parse HEAD)" = "$DXVK_REVISION" +git -C "$work/source" submodule update --init --recursive --depth=1 +for patch in "$root"/tools/dxvk/patches/*.patch; do + git -C "$work/source" apply --check "$patch" + git -C "$work/source" apply "$patch" +done +python3 "$root/tools/dxvk/check_present_sync.py" "$work/source/src/dxvk/dxvk_presenter.cpp" +python3 "$root/tools/dxvk/check_hud_storage_format.py" "$work/source/src/dxvk/hud/shaders/hud_frame_time_eval.comp" +python3 "$root/tools/dxvk/check_d3d9_single_sample.py" "$work/source" +for arch in 32 64; do + meson setup "$work/build$arch" "$work/source" --cross-file "$work/source/build-win$arch.txt" \ + --buildtype release --prefix "$out" --bindir "x$arch" --libdir "x$arch" -Db_ndebug=if-release --strip + meson compile -C "$work/build$arch" -j 4 + meson install -C "$work/build$arch" +done +cp "$work/source/LICENSE" "$out/LICENSE" +printf '%s\n' "$DXVK_REVISION" > "$out/source-commit" +git -C "$work/source" submodule status --recursive > "$out/submodules.txt" +git -C "$work/source" diff > "$out/downstream.patch" +tar --exclude-vcs --zstd -cf "$out/source.tar.zst" -C "$work/source" . +(cd "$out" && find x32 x64 -name '*.dll' -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS) diff --git a/tools/dxvk/check_d3d9_single_sample.py b/tools/dxvk/check_d3d9_single_sample.py new file mode 100644 index 000000000..8f73a3f3a --- /dev/null +++ b/tools/dxvk/check_d3d9_single_sample.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Compile pinned DXVK's actual descriptor paths against a controlled backend. + +This checks CPU control flow, not GPU rendering. No Wine/game/device is started. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile + +CASES = ('forced_msaa_depth', 'default_msaa_depth', 'forced_nonmaskable', + 'default_nonmaskable', 'forced_already_single', 'forced_no_depth', + 'forced_unsupported', 'forced_invalid_type', 'forced_lockable_msaa', + 'forced_lockable_single', 'forced_allocation_failure', + 'default_allocation_failure', 'forced_recreate', 'ordinary_surface', + 'ordinary_invalid_lockable') + + +def function(text, signature): + start = text.index(signature) + opening = text.index('{', start) + # Ignore braces in comments and strings while preserving source positions. + stripped = re.sub(r'//[^\n]*|/\*.*?\*/|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'', + lambda m: ' ' * len(m.group()), text, flags=re.S) + depth = 0 + for index in range(opening, len(stripped)): + depth += (stripped[index] == '{') - (stripped[index] == '}') + if depth == 0: + return text[start:index + 1] + raise ValueError('Unterminated source function: ' + signature) + + +def production(source): + base = source / 'src/d3d9' + common = (base / 'd3d9_common_texture.cpp').read_text() + header = (base / 'd3d9_common_texture.h').read_text() + swap = (base / 'd3d9_swapchain.cpp').read_text() + device = (base / 'd3d9_device.cpp').read_text() + util = (base / 'd3d9_util.cpp').read_text() + pieces = { + 'decode': function(util, 'HRESULT DecodeMultiSampleType('), + 'normalize': function(common, 'HRESULT D3D9CommonTexture::NormalizeTextureProperties('), + 'backbuffers': function(swap, 'HRESULT D3D9SwapChainEx::CreateBackBuffers('), + 'uses_image': function(header, 'static bool TextureUsesImage('), + } + reset = function(device, 'HRESULT D3D9DeviceEx::ResetSwapChain(') + begin = reset.index('D3D9_COMMON_TEXTURE_DESC desc;', reset.index('if (pPresentationParameters->EnableAutoDepthStencil)')) + end = reset.index('m_autoDepthStencil =', begin) + pieces['auto_depth'] = reset[begin:end].rstrip() + framebuffer = function(device, 'void D3D9DeviceEx::BindFramebuffer(') + begin = framebuffer.index('const bool mismatch =') + end = framebuffer.index(';', framebuffer.index('const bool bindDS =', begin)) + 1 + pieces['depth_binding'] = framebuffer[begin:end] + return pieces + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('source', type=Path, help='Pinned DXVK tree with downstream patches applied') + parser.add_argument('--cxx', default=os.environ.get('CXX', 'c++')) + parser.add_argument('--sanitize', action='store_true', help='Enable ASan/UBSan on the CPU harness') + args = parser.parse_args() + pieces = production(args.source) + harness = (Path(__file__).parent / 'tests/d3d9_single_sample.cpp').read_text() + harness = harness.replace('/* PRODUCTION_USES_IMAGE */', pieces['uses_image']) + with tempfile.TemporaryDirectory(prefix='dxvk-d3d9-') as directory: + out = Path(directory) + (out / 'harness.cpp').write_text(harness) + include = '\n\n'.join(pieces[key] for key in ('decode', 'normalize', 'backbuffers')) + include += '\nHRESULT auto_depth(D3D9DeviceEx* device, D3DPRESENT_PARAMETERS* pPresentationParameters, D3D9_COMMON_TEXTURE_DESC* output) {\n' + include += 'auto* self = device;\n' + pieces['auto_depth'].replace('(this,', '(self,') + include += '\n*output = desc; return D3D_OK;\n}\n' + include += '\nbool depth_binds(const ImageInfo& color, const ImageInfo& dsInfo) {\n' + include += 'auto sampleCount = color.sampleCount; auto renderArea = color.extent;\n' + include += 'const bool noRT0Bound = false, ignoreRT0 = false;\n' + include += pieces['depth_binding'] + '\nreturn bindDS;\n}\n' + (out / 'd3d9_production.inc').write_text(include) + command = [args.cxx, '-std=c++17', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-parameter', str(out / 'harness.cpp'), '-o', str(out / 'check')] + # The original CreateBackBuffers does not use Flags; that known baseline + # warning must not turn a negative control into a compiler-fixture error. + if args.sanitize: + command[1:1] = ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] + compilation = subprocess.run(command, capture_output=True, text=True) + if compilation.returncode: + raise SystemExit(compilation.stderr) + results = [] + for case in CASES: + process = subprocess.run([str(out / 'check'), case], capture_output=True, + text=True, timeout=10, cwd=out, + env={'ASAN_OPTIONS': 'detect_leaks=' + + ('0' if sys.platform == 'darwin' else '1')}) + results.append({'case': case, 'passed': process.returncode == 0, + 'exit': process.returncode, 'detail': process.stderr.strip()[-800:]}) + result = {'cases': results, 'passed': sum(row['passed'] for row in results), + 'failed': sum(not row['passed'] for row in results), + 'production_sha256': {key: hashlib.sha256(value.encode()).hexdigest() + for key, value in pieces.items()}, + 'backend_and_adapter_mocked': True, 'GPU_test': False, + 'sanitizers': args.sanitize, + 'leak_detection': args.sanitize and sys.platform != 'darwin'} + print(json.dumps(result, indent=2)) + return bool(result['failed']) + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/dxvk/check_hud_storage_format.py b/tools/dxvk/check_hud_storage_format.py new file mode 100644 index 000000000..f376cad6b --- /dev/null +++ b/tools/dxvk/check_hud_storage_format.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Compile the actual HUD shader and verify its binding-3 R8_UINT contract. + +The frame-time HUD binds an R8_UINT texel-buffer view. An Unknown SPIR-V image +format requires STORAGE_WRITE_WITHOUT_FORMAT, which PowerVR does not report +for that buffer format. Static spirv-val alone accepts both declarations; +this check validates the shader against the known host-side binding contract. +""" +import argparse +from pathlib import Path +import shutil +import struct +import subprocess +import tempfile + + +def check(shader): + compiler = shutil.which('glslangValidator') + validator = shutil.which('spirv-val') + if not compiler or not validator: + raise RuntimeError('glslangValidator and spirv-val required') + with tempfile.TemporaryDirectory(prefix='dxvk-hud-contract-') as directory: + output = Path(directory) / 'hud.spv' + for command in ([compiler, '-V', '--target-env', 'vulkan1.3', str(shader), '-o', str(output)], + [validator, '--target-env', 'vulkan1.3', str(output)]): + process = subprocess.run(command, capture_output=True, text=True, timeout=30) + if process.returncode: + raise RuntimeError('HUD shader compilation/SPIR-V validation failed') + raw = output.read_bytes() + if len(raw) < 20 or len(raw) % 4: + raise RuntimeError('Malformed SPIR-V') + words = struct.unpack('<' + str(len(raw) // 4) + 'I', raw) + if words[0] != 0x07230203: + raise RuntimeError('Unexpected SPIR-V magic') + bindings, sets, variables, pointers, images = {}, {}, {}, {}, {} + offset = 5 + while offset < len(words): + size, opcode = words[offset] >> 16, words[offset] & 0xffff + if not size or size > len(words) - offset: + raise RuntimeError('Malformed SPIR-V instruction') + instruction = words[offset:offset + size] + if opcode == 71 and size == 4: # OpDecorate + if instruction[2] == 33: + bindings[instruction[1]] = instruction[3] + elif instruction[2] == 34: + sets[instruction[1]] = instruction[3] + elif opcode == 59 and size >= 4: # OpVariable + variables[instruction[2]] = instruction[1] + elif opcode == 32 and size == 4: # OpTypePointer + pointers[instruction[1]] = instruction[3] + elif opcode == 25 and size >= 9: # OpTypeImage + images[instruction[1]] = (instruction[3], instruction[7], instruction[8]) + offset += size + targets = [images.get(pointers.get(variables.get(identifier))) + for identifier, binding in bindings.items() + if binding == 3 and sets.get(identifier) == 0] + if targets != [(5, 2, 39)]: # DimBuffer, storage image, ImageFormatR8ui + raise RuntimeError('HUD binding 3 must compile to a storage texel buffer with ImageFormatR8ui') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('shader', type=Path) + args = parser.parse_args() + try: + check(args.shader) + except RuntimeError as error: + parser.exit(1, str(error) + '\n') + print('HUD binding 3: SPIR-V valid, explicit R8ui storage texel-buffer format') + + +if __name__ == '__main__': + main() diff --git a/tools/dxvk/check_present_sync.py b/tools/dxvk/check_present_sync.py new file mode 100644 index 000000000..ad47a8ade --- /dev/null +++ b/tools/dxvk/check_present_sync.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Replay asynchronous WSI ownership using the pinned presenter's actual selections. + +This checks semaphore selection, not the full Vulkan presenter. Real Vulkan +validation and device testing remain separate gates. Requires a native C++ compiler. +""" +import argparse +import os +from pathlib import Path +import re +import subprocess +import tempfile + + +def selection_code(source): + match = re.search( + r"// Return relevant Vulkan objects for the acquired image\s*" + r"(?P.*?)\s*m_presentPending = true;", source, re.S) + wait = re.search(r"info\.pWaitSemaphores\s*=.*?;", source, re.S) + if not match or not wait: + raise ValueError("unsupported pinned presenter layout") + return match["acquire"], wait[0] + + +HARNESS = r''' +#include +#include +#include +#include +using VkSemaphore = uint64_t; +struct Sync { VkSemaphore acquire, present; }; +struct Info { const VkSemaphore* pWaitSemaphores; }; + +struct Presenter { + std::vector m_semaphores; + std::vector m_images = {0, 1, 2, 3}; + unsigned m_frameIndex = 0, m_imageIndex = 0; + bool m_hasSwapchainMaintenance1; + explicit Presenter(bool maintenance): m_hasSwapchainMaintenance1(maintenance) { + for (unsigned i = 0; i < (maintenance ? 4u : 8u); ++i) + m_semaphores.push_back({100 + i, 1 + i}); + } + Sync acquire() { + Sync sync{}; unsigned image; + ACQUIRE_SELECTION + (void)image; + return sync; + } + VkSemaphore present() { + auto& currSync = m_semaphores.at(m_frameIndex); + Info info{}; + PRESENT_SELECTION + return *info.pWaitSemaphores; + } +}; + +int replay(bool maintenance) { + Presenter p(maintenance); + std::array outstandingByImage{}; + std::array outstandingByFence{}; + // Deliberately delay reacquisition of image 2 beyond a full frame-ring cycle. + const std::array imageOrder = { + 2,0,3,0,1,3,0,1,1,2,3,1,0,3,2,2,0,1,3,0,2 + }; + for (unsigned iteration = 0; iteration < 200; ++iteration) { + p.m_imageIndex = imageOrder[iteration % imageOrder.size()]; + if (maintenance) { + // The existing fence path waits this frame's previous present fence. + const auto completed = outstandingByFence.at(p.m_frameIndex); + for (auto& pending : outstandingByImage) + if (pending == completed) pending = 0; + } + // Acquisition and its GPU wait complete the last present of THIS image. + outstandingByImage.at(p.m_imageIndex) = 0; + const auto signal = p.acquire().present; + const auto wait = p.present(); + if (signal != wait) { + std::fprintf(stderr, "FAIL: render signal differs from present wait\n"); + return 2; + } + for (auto pending : outstandingByImage) { + if (pending == signal) { + std::fprintf(stderr, "FAIL: semaphore reused before presentation completion at frame %u\n", iteration); + return 2; + } + } + outstandingByImage.at(p.m_imageIndex) = signal; + if (maintenance) outstandingByFence.at(p.m_frameIndex) = signal; + p.m_frameIndex = (p.m_frameIndex + 1) % p.m_semaphores.size(); + } + std::printf("PASS: %s, 200 out-of-order acquisitions\n", maintenance ? "present-fence path" : "no-present-fence path"); + return 0; +} +int main() { + if (int status = replay(false)) return status; + return replay(true); +} +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path, help="pinned dxvk_presenter.cpp, after patching") + args = parser.parse_args() + acquire, present = selection_code(args.source.read_text()) + code = HARNESS.replace("ACQUIRE_SELECTION", acquire).replace("PRESENT_SELECTION", present) + with tempfile.TemporaryDirectory(prefix="dxvk-present-sync-") as directory: + cpp = Path(directory) / "replay.cpp" + exe = Path(directory) / "replay" + cpp.write_text(code) + subprocess.run([os.environ.get("HOST_CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", str(cpp), "-o", str(exe)], check=True) + return subprocess.run([str(exe)]).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/dxvk/patches/0001-check-direct3d-bc-formats.patch b/tools/dxvk/patches/0001-check-direct3d-bc-formats.patch new file mode 100644 index 000000000..db327f038 --- /dev/null +++ b/tools/dxvk/patches/0001-check-direct3d-bc-formats.patch @@ -0,0 +1,53 @@ +diff --git a/src/dxvk/dxvk_device_info.cpp b/src/dxvk/dxvk_device_info.cpp +index db09fba..8e9a76e 100644 +--- a/src/dxvk/dxvk_device_info.cpp ++++ b/src/dxvk/dxvk_device_info.cpp +@@ -118,6 +118,17 @@ namespace dxvk { + initQueueProperties(instance, adapter, deviceInfo); + initMemoryProperties(instance, adapter); + ++ m_hasBcFormats = true; ++ for (uint32_t value = VK_FORMAT_BC1_RGBA_UNORM_BLOCK; value <= VK_FORMAT_BC7_SRGB_BLOCK; value++) { ++ VkFormatProperties properties = { }; ++ vk->vkGetPhysicalDeviceFormatProperties(adapter, VkFormat(value), &properties); ++ constexpr VkFormatFeatureFlags required = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT ++ | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ++ | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT; ++ if ((properties.optimalTilingFeatures & required) != required) ++ m_hasBcFormats = false; ++ } ++ + disableUnusedFeatures(instance, safeMode); + + enableFeaturesAndExtensions(); +@@ -785,6 +796,9 @@ namespace dxvk { + } + } + ++ if (!m_hasBcFormats) ++ return std::string("Device does not support the BC formats required by Direct3D"); ++ + if (!m_featuresEnabled.extDescriptorHeap.descriptorHeap && m_properties.core.properties.limits.maxPushConstantsSize < MaxTotalPushDataSize) + return str::format("Device does not support ", MaxTotalPushDataSize, " of push data"); + +@@ -886,7 +900,7 @@ namespace dxvk { + ENABLE_FEATURE(core.features, shaderResourceResidency, false), + ENABLE_FEATURE(core.features, shaderResourceMinLod, false), + ENABLE_FEATURE(core.features, tessellationShader, false), +- ENABLE_FEATURE(core.features, textureCompressionBC, true), ++ ENABLE_FEATURE(core.features, textureCompressionBC, false), + ENABLE_FEATURE(core.features, variableMultisampleRate, false), + ENABLE_FEATURE(core.features, vertexPipelineStoresAndAtomics, false), + ENABLE_FEATURE(core.features, wideLines, false), +diff --git a/src/dxvk/dxvk_device_info.h b/src/dxvk/dxvk_device_info.h +index 0a55f90..8bbc770 100644 +--- a/src/dxvk/dxvk_device_info.h ++++ b/src/dxvk/dxvk_device_info.h +@@ -394,6 +394,7 @@ namespace dxvk { + + DxvkDeviceQueueMapping m_queueMapping = { }; + ++ bool m_hasBcFormats = false; + bool m_hasMeshShader = false; + bool m_hasFmask = false; + diff --git a/tools/dxvk/patches/0002-present-semaphore-per-swapchain-image.patch b/tools/dxvk/patches/0002-present-semaphore-per-swapchain-image.patch new file mode 100644 index 000000000..bd22bdc52 --- /dev/null +++ b/tools/dxvk/patches/0002-present-semaphore-per-swapchain-image.patch @@ -0,0 +1,26 @@ +diff --git a/src/dxvk/dxvk_presenter.cpp b/src/dxvk/dxvk_presenter.cpp +index 643f7c5..4dc8c4d 100644 +--- a/src/dxvk/dxvk_presenter.cpp ++++ b/src/dxvk/dxvk_presenter.cpp +@@ -161,6 +161,10 @@ namespace dxvk { + + // Return relevant Vulkan objects for the acquired image + sync = m_semaphores.at(m_frameIndex); ++ // Without a present fence, reacquiring this image is the only guarantee ++ // that its previous presentation has consumed the wait semaphore. ++ if (!m_hasSwapchainMaintenance1) ++ sync.present = m_semaphores.at(m_imageIndex).present; + image = m_images.at(m_imageIndex); + + m_presentPending = true; +@@ -239,7 +243,9 @@ namespace dxvk { + + VkPresentInfoKHR info = { VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + info.waitSemaphoreCount = 1; +- info.pWaitSemaphores = &currSync.present; ++ info.pWaitSemaphores = m_hasSwapchainMaintenance1 ++ ? &currSync.present ++ : &m_semaphores.at(m_imageIndex).present; + info.swapchainCount = 1; + info.pSwapchains = &m_swapchain; + info.pImageIndices = &m_imageIndex; diff --git a/tools/dxvk/patches/0003-hud-explicit-r8-texel-format.patch b/tools/dxvk/patches/0003-hud-explicit-r8-texel-format.patch new file mode 100644 index 000000000..85ed529e8 --- /dev/null +++ b/tools/dxvk/patches/0003-hud-explicit-r8-texel-format.patch @@ -0,0 +1,12 @@ +--- a/src/dxvk/hud/shaders/hud_frame_time_eval.comp ++++ b/src/dxvk/hud/shaders/hud_frame_time_eval.comp +@@ -41,7 +41,8 @@ + draw_info_t draw_infos[]; + }; + +-layout(binding = 3) ++// The bound text view is VK_FORMAT_R8_UINT; do not require formatless writes. ++layout(binding = 3, r8ui) + uniform writeonly uimageBuffer text_buffer; + + layout(push_constant) diff --git a/tools/dxvk/patches/0004-d3d9-force-single-sample.patch b/tools/dxvk/patches/0004-d3d9-force-single-sample.patch new file mode 100644 index 000000000..6ae7343c1 --- /dev/null +++ b/tools/dxvk/patches/0004-d3d9-force-single-sample.patch @@ -0,0 +1,86 @@ +diff --git a/dxvk.conf b/dxvk.conf +index ba4bc20..26d3127 100644 +--- a/dxvk.conf ++++ b/dxvk.conf +@@ -336,6 +336,15 @@ + + # d3d11.disableMsaa = False + ++# Forces D3D9 surfaces to single-sample after validating the original request. ++# The effective surface description reports D3DMULTISAMPLE_NONE; MSAA quality, ++# alpha-to-coverage and per-sample masks are consequently disabled. This is an ++# opt-in compatibility override for games with broken MSAA rendering. ++# ++# Supported values: True, False ++ ++# d3d9.forceSingleSample = False ++ + + # Forces insertion of memory and control barriers after writes to group-shared + # memory in compute shaders. This is only intended to be used as a workaround +diff --git a/src/d3d9/d3d9_common_texture.cpp b/src/d3d9/d3d9_common_texture.cpp +index 9d5fe38..629fea3 100644 +--- a/src/d3d9/d3d9_common_texture.cpp ++++ b/src/d3d9/d3d9_common_texture.cpp +@@ -212,6 +212,15 @@ namespace dxvk { + return D3DERR_INVALIDCALL; + } + ++ // Keep original request validation above. This opt-in override changes ++ // the effective surface description as well as the backing image. ++ // All surface descriptors and resolves then see the actual sample count. ++ if (ResourceType == D3DRTYPE_SURFACE && options->forceSingleSample ++ && sampleCount != VK_SAMPLE_COUNT_1_BIT) { ++ pDesc->MultiSample = D3DMULTISAMPLE_NONE; ++ pDesc->MultisampleQuality = 0; ++ } ++ + // Using MANAGED pool with DYNAMIC usage is illegal + if (IsPoolManaged(pDesc->Pool) && (pDesc->Usage & D3DUSAGE_DYNAMIC)) + return D3DERR_INVALIDCALL; +diff --git a/src/d3d9/d3d9_options.cpp b/src/d3d9/d3d9_options.cpp +index e46864e..0cab7dc 100644 +--- a/src/d3d9/d3d9_options.cpp ++++ b/src/d3d9/d3d9_options.cpp +@@ -59,6 +59,7 @@ namespace dxvk { + this->memoryTrackTest = config.getOption ("d3d9.memoryTrackTest", false); + this->forceSamplerTypeSpecConstants = config.getOption ("d3d9.forceSamplerTypeSpecConstants", false); + this->forceSampleRateShading = config.getOption ("d3d9.forceSampleRateShading", false); ++ this->forceSingleSample = config.getOption ("d3d9.forceSingleSample", false); + this->forceAspectRatio = config.getOption ("d3d9.forceAspectRatio", ""); + this->forceRefreshRate = config.getOption ("d3d9.forceRefreshRate", 0u); + this->modeCountCompatibility = config.getOption ("d3d9.modeCountCompatibility", false); +diff --git a/src/d3d9/d3d9_options.h b/src/d3d9/d3d9_options.h +index e69a39a..4478420 100644 +--- a/src/d3d9/d3d9_options.h ++++ b/src/d3d9/d3d9_options.h +@@ -113,6 +113,9 @@ namespace dxvk { + /// Forces sample rate shading + bool forceSampleRateShading; + ++ /// Optional compatibility override: report and render surfaces as single-sample. ++ bool forceSingleSample; ++ + /// Allow D3DLOCK_DISCARD + bool allowDiscard; + +diff --git a/src/d3d9/d3d9_swapchain.cpp b/src/d3d9/d3d9_swapchain.cpp +--- a/src/d3d9/d3d9_swapchain.cpp ++++ b/src/d3d9/d3d9_swapchain.cpp +@@ -1055,1 +1055,16 @@ namespace dxvk { ++ if (m_parent->GetOptions()->forceSingleSample) { ++ // Backbuffers bypass the normal surface-creation entry points. Validate ++ // the application's request, then apply the same effective sample count. ++ // IsLockable above is an internal GDI fallback requirement, not the ++ // application's lockability request used by MSAA validation. ++ auto requestedDesc = desc; ++ requestedDesc.IsLockable = (Flags & D3DPRESENTFLAG_LOCKABLE_BACKBUFFER) != 0; ++ HRESULT hr = D3D9CommonTexture::NormalizeTextureProperties( ++ m_parent, D3DRTYPE_SURFACE, &requestedDesc); ++ if (FAILED(hr)) ++ return hr; ++ desc.MultiSample = requestedDesc.MultiSample; ++ desc.MultisampleQuality = requestedDesc.MultisampleQuality; ++ } ++ + const bool isExtended = m_parent->IsD3DCompatibile(D3DCompatibility::D3D9Ex); diff --git a/tools/dxvk/source.env b/tools/dxvk/source.env new file mode 100644 index 000000000..7aac170c6 --- /dev/null +++ b/tools/dxvk/source.env @@ -0,0 +1 @@ +DXVK_REVISION=25ca63f17f34bdc05a39873ee63907d3cbbfa030 diff --git a/tools/dxvk/tests/d3d9_single_sample.cpp b/tools/dxvk/tests/d3d9_single_sample.cpp new file mode 100644 index 000000000..2c9782f92 --- /dev/null +++ b/tools/dxvk/tests/d3d9_single_sample.cpp @@ -0,0 +1,214 @@ +// Backend/adapter fixture for the verbatim DXVK functions in d3d9_production.inc. +#include +#include +#include +#include +#include +#include +#include + +using HRESULT = int32_t; +using DWORD = uint32_t; +using UINT = uint32_t; +using VkSampleCountFlagBits = uint32_t; +constexpr HRESULT D3D_OK = 0, D3DERR_INVALIDCALL = -1, + D3DERR_NOTAVAILABLE = -2, D3DERR_OUTOFVIDEOMEMORY = -3; +constexpr uint32_t VK_SAMPLE_COUNT_1_BIT = 1, VK_IMAGE_ASPECT_COLOR_BIT = 1; +constexpr bool FALSE = false, TRUE = true; +#define FAILED(x) ((x) < 0) +#define unlikely(x) (x) +enum D3DMULTISAMPLE_TYPE { D3DMULTISAMPLE_NONE, D3DMULTISAMPLE_NONMASKABLE, + D3DMULTISAMPLE_16_SAMPLES = 16 }; +enum D3DRESOURCETYPE { D3DRTYPE_SURFACE, D3DRTYPE_CUBETEXTURE, D3DRTYPE_VOLUMETEXTURE }; +enum D3DPOOL { D3DPOOL_DEFAULT, D3DPOOL_MANAGED, D3DPOOL_SYSTEMMEM, D3DPOOL_SCRATCH }; +enum class D3DCompatibility { D3D9Ex }; +enum class D3D9Format { X8R8G8B8, D24S8, A8, NULL_FORMAT, ATI2, + D32_LOCKABLE, D32F_LOCKABLE, D16_LOCKABLE, S8_LOCKABLE }; +constexpr DWORD D3DUSAGE_RENDERTARGET = 1, D3DUSAGE_DEPTHSTENCIL = 2, + D3DUSAGE_DYNAMIC = 4, D3DUSAGE_WRITEONLY = 8, D3DUSAGE_AUTOGENMIPMAP = 16; +constexpr DWORD D3DPRESENTFLAG_LOCKABLE_BACKBUFFER = 1, D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL = 2; +constexpr int D3DDEVTYPE_HAL = 1, VK_IMAGE_LAYOUT_UNDEFINED = 0; +struct Extent { uint32_t width, height, depth = 1; }; +namespace util { +uint32_t computeMipLevelCount(Extent e) { + uint32_t n = std::max({e.width, e.height, e.depth}), levels = 0; + do { ++levels; n >>= 1; } while (n); + return levels; +} +} +bool IsPoolManaged(D3DPOOL pool) { return pool == D3DPOOL_MANAGED; } +bool IsDXTFormat(D3D9Format) { return false; } +bool IsDepthStencilFormat(D3D9Format format) { return format == D3D9Format::D24S8; } +bool IsLockableDepthStencilFormat(D3D9Format) { return false; } +struct D3D9_FORMAT_BLOCK_SIZE { uint32_t Width, Height; }; +D3D9_FORMAT_BLOCK_SIZE GetFormatAlignedBlockSize(D3D9Format) { return {1, 1}; } +D3D9Format EnumerateFormat(D3D9Format format) { return format; } +struct D3D9_COMMON_TEXTURE_DESC { + uint32_t Width = 1280, Height = 720, Depth = 1, MipLevels = 1, ArraySize = 1; + D3D9Format Format = D3D9Format::X8R8G8B8; + D3DMULTISAMPLE_TYPE MultiSample = D3DMULTISAMPLE_NONE; + DWORD MultisampleQuality = 0; + D3DPOOL Pool = D3DPOOL_DEFAULT; + DWORD Usage = D3DUSAGE_RENDERTARGET; + bool Discard = false, IsBackBuffer = false, IsAttachmentOnly = false, IsLockable = false; +}; +struct D3DPRESENT_PARAMETERS { + uint32_t BackBufferWidth = 1280, BackBufferHeight = 720; + D3D9Format BackBufferFormat = D3D9Format::X8R8G8B8; + D3DMULTISAMPLE_TYPE MultiSampleType = D3DMULTISAMPLE_TYPE(4); + DWORD MultiSampleQuality = 0, Flags = 0; + D3D9Format AutoDepthStencilFormat = D3D9Format::D24S8; +}; +struct Options { + bool forceSingleSample = false, extraFrontbuffer = false, + disableA8RT = false, supportCubeDepthFormats = false; +}; +struct FormatMapping { + uint32_t Aspect; + bool IsValid() const { return true; } +}; +struct Adapter { + bool supported = true; + std::vector checked; + HRESULT CheckDeviceMultiSampleType(int, D3D9Format, bool, D3DMULTISAMPLE_TYPE samples, void*) { + checked.push_back(samples); + return supported ? D3D_OK : D3DERR_NOTAVAILABLE; + } +}; +struct ImageInfo { Extent extent; VkSampleCountFlagBits sampleCount; }; +struct DxvkImage { ImageInfo info; }; +template using Rc = std::shared_ptr; +template using small_vector = std::vector; +struct DxvkContext { + unsigned initialized = 0; + void initImage(const Rc&, int) { ++initialized; } +}; +struct D3D9DeviceEx { + Options options; + Adapter adapter; + DxvkContext context; + unsigned attempts = 0, failAt = 0, losable = 0, live = 0, injected = 0; + std::vector created; + Options* GetOptions() { return &options; } + Adapter* GetAdapter() { return &adapter; } + FormatMapping LookupFormat(D3D9Format format) { return {IsDepthStencilFormat(format) ? 2u : 1u}; } + struct Unsupported { unsigned elementSize = 0; }; + Unsupported unsupported; + const Unsupported* UnsupportedFormatInfo(D3D9Format) { return &unsupported; } + bool IsD3DCompatibile(D3DCompatibility) { return false; } + void IncrementLosableCounter() { ++losable; } + template void InjectCs(F function) { ++injected; function(&context); } +}; +HRESULT DecodeMultiSampleType(D3DMULTISAMPLE_TYPE, DWORD, VkSampleCountFlagBits*); +struct D3D9CommonTexture { + Rc image; + const Rc& GetImage() const { return image; } + static HRESULT NormalizeTextureProperties(D3D9DeviceEx*, D3DRESOURCETYPE, D3D9_COMMON_TEXTURE_DESC*); + /* PRODUCTION_USES_IMAGE */ +}; +class DxvkError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; + std::string message() const { return what(); } +}; +struct Logger { static void err(const std::string&) {} }; +struct D3D9SwapChainEx; +struct D3D9Surface { + D3D9CommonTexture texture; + D3D9DeviceEx* device; + D3D9Surface(D3D9DeviceEx* d, D3D9_COMMON_TEXTURE_DESC* desc, bool, D3D9SwapChainEx*, void*) : device(d) { + if (++d->attempts == d->failAt) throw DxvkError("controlled allocation failure"); + VkSampleCountFlagBits samples; + if (FAILED(DecodeMultiSampleType(desc->MultiSample, desc->MultisampleQuality, &samples))) + throw DxvkError("invalid backend sample count"); + texture.image = std::make_shared(DxvkImage{{{desc->Width, desc->Height}, samples}}); + d->created.push_back(*desc); ++d->live; + } + ~D3D9Surface() { --device->live; } + D3D9CommonTexture* GetCommonTexture() { return &texture; } +}; +struct D3D9SwapChainEx { + D3D9DeviceEx* m_parent; + D3DPRESENT_PARAMETERS m_presentParams; + std::vector> m_backBuffers; + bool flip = false; + unsigned destroys = 0; + bool SwapWithFrontBuffer() { return flip; } + void DestroyBackBuffers() { ++destroys; m_backBuffers.clear(); } + HRESULT CreateBackBuffers(uint32_t, DWORD); +}; +#include "d3d9_production.inc" + +void paired(bool enabled, D3DMULTISAMPLE_TYPE type, DWORD quality) { + D3D9DeviceEx device; device.options.forceSingleSample = enabled; + device.options.extraFrontbuffer = true; + D3D9SwapChainEx chain{&device, {}, {}}; + chain.m_presentParams.MultiSampleType = type; chain.m_presentParams.MultiSampleQuality = quality; + assert(chain.CreateBackBuffers(2, 0) == D3D_OK); + D3D9_COMMON_TEXTURE_DESC depth; + assert(auto_depth(&device, &chain.m_presentParams, &depth) == D3D_OK); + VkSampleCountFlagBits requested, dsSamples; + assert(DecodeMultiSampleType(type, quality, &requested) == D3D_OK); + assert(DecodeMultiSampleType(depth.MultiSample, depth.MultisampleQuality, &dsSamples) == D3D_OK); + assert(chain.m_backBuffers.size() == 3 && device.context.initialized == 3); + for (const auto& surface : chain.m_backBuffers) { + const auto& image = surface->GetCommonTexture()->GetImage()->info; + assert(image.sampleCount == (enabled ? 1u : requested)); + assert(depth_binds(image, {{depth.Width, depth.Height}, dsSamples})); + } + for (const auto& desc : device.created) { + assert(desc.IsLockable && desc.IsBackBuffer && !desc.IsAttachmentOnly); + assert(desc.MultiSample == (enabled ? D3DMULTISAMPLE_NONE : type)); + assert(desc.MultisampleQuality == (enabled ? 0u : quality)); + } + // Original presentation parameters remain available for reset/auto-depth validation. + assert(chain.m_presentParams.MultiSampleType == type && chain.m_presentParams.MultiSampleQuality == quality); + if (enabled && requested > 1) assert(device.adapter.checked.front() == D3DMULTISAMPLE_TYPE(requested)); +} + +int main(int argc, char** argv) { + assert(argc == 2); std::string name = argv[1]; + if (name == "forced_msaa_depth") paired(true, D3DMULTISAMPLE_TYPE(4), 0); + else if (name == "default_msaa_depth") paired(false, D3DMULTISAMPLE_TYPE(4), 0); + else if (name == "forced_nonmaskable") paired(true, D3DMULTISAMPLE_NONMASKABLE, 2); + else if (name == "default_nonmaskable") paired(false, D3DMULTISAMPLE_NONMASKABLE, 2); + else if (name == "forced_already_single") paired(true, D3DMULTISAMPLE_NONE, 0); + else if (name == "forced_no_depth" || name == "forced_lockable_single") { + D3D9DeviceEx d; d.options.forceSingleSample = true; + D3D9SwapChainEx c{&d, {}, {}}; + if (name == "forced_lockable_single") c.m_presentParams.MultiSampleType = D3DMULTISAMPLE_NONE; + assert(c.CreateBackBuffers(1, name == "forced_lockable_single" ? D3DPRESENTFLAG_LOCKABLE_BACKBUFFER : 0) == D3D_OK); + assert(c.m_backBuffers.size() == 1 && d.created[0].MultiSample == D3DMULTISAMPLE_NONE); + assert(d.created[0].IsLockable && d.context.initialized == 1); + } else if (name == "forced_unsupported" || name == "forced_invalid_type" || name == "forced_lockable_msaa") { + D3D9DeviceEx d; d.options.forceSingleSample = true; + D3D9SwapChainEx c{&d, {}, {}}; + if (name == "forced_unsupported") d.adapter.supported = false; + if (name == "forced_invalid_type") c.m_presentParams.MultiSampleType = D3DMULTISAMPLE_TYPE(17); + HRESULT expected = name == "forced_unsupported" ? D3DERR_NOTAVAILABLE : D3DERR_INVALIDCALL; + assert(c.CreateBackBuffers(2, name == "forced_lockable_msaa" ? D3DPRESENTFLAG_LOCKABLE_BACKBUFFER : 0) == expected); + assert(d.attempts == 0 && d.losable == 0 && d.injected == 0 && c.m_backBuffers.empty()); + } else if (name == "forced_allocation_failure" || name == "default_allocation_failure") { + D3D9DeviceEx d; d.options.forceSingleSample = name == "forced_allocation_failure"; d.failAt = 2; + D3D9SwapChainEx c{&d, {}, {}}; + assert(c.CreateBackBuffers(3, 0) == D3DERR_OUTOFVIDEOMEMORY); + assert(d.attempts == 2 && d.losable == 1 && d.live == 0 && d.injected == 0 && c.m_backBuffers.empty()); + } else if (name == "forced_recreate") { + D3D9DeviceEx d; d.options.forceSingleSample = true; + D3D9SwapChainEx c{&d, {}, {}}; + assert(c.CreateBackBuffers(3, 0) == D3D_OK); + c.m_presentParams.MultiSampleType = D3DMULTISAMPLE_TYPE(8); + assert(c.CreateBackBuffers(1, 0) == D3D_OK); + assert(d.live == 1 && c.m_backBuffers.size() == 1 && c.destroys == 2); + assert(d.adapter.checked.size() == 2 && d.adapter.checked.back() == D3DMULTISAMPLE_TYPE(8)); + assert(c.m_backBuffers[0]->GetCommonTexture()->GetImage()->info.sampleCount == 1); + } else if (name == "ordinary_surface" || name == "ordinary_invalid_lockable") { + D3D9DeviceEx d; d.options.forceSingleSample = true; + D3D9_COMMON_TEXTURE_DESC desc; desc.MultiSample = D3DMULTISAMPLE_TYPE(4); + desc.IsLockable = name == "ordinary_invalid_lockable"; + assert(D3D9CommonTexture::NormalizeTextureProperties(&d, D3DRTYPE_SURFACE, &desc) + == (desc.IsLockable ? D3DERR_INVALIDCALL : D3D_OK)); + if (!desc.IsLockable) assert(desc.MultiSample == D3DMULTISAMPLE_NONE && desc.MultisampleQuality == 0); + } else assert(false); + return 0; +} diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index fc3b1b481..4736d3232 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -64,14 +64,79 @@ library list, before anything is published. refresh output they now wait for it too, for at most `external_overlay_hold_cycles` refreshes (4, so 30 fps at 120 Hz still merges), and are painted on their own after that. +The non-Adreno test branch's own patches are numbered 0920-0935 (0120-0135 until main took +0121-0125), so they apply after DroidDeck's. Applied in either order the tree is the same. + +The system PowerVR driver uses the existing SDL/Wayland swapchain backend. Its additional patches are: + +- `0920-nested-swapchain-capability-fallback.patch`: negotiate the surface's usages, mutable-format + support, image count, alpha and extent; use offscreen composition and GPU copies when required. +- `0921-powervr-single-pixel-rcas.patch`: use the existing single-pixel RCAS shader on PowerVR, + where the quad variant fails compilation. +- `0922-powervr-linear-dmabuf-import.patch`: retain the explicit linear layout when PowerVR + rejects the mutable-format query, and intersect imported-image and FD memory-type masks. +- `0923-powervr-rgba-copy-surface.patch`: require an advertised RGBA SDR copy surface matching + the shader's `rgba8` storage view. The tested workaround leaves other vendors, HDR and direct + composition unchanged; it does not restrict game-facing BGRA formats. +- `0924-sdl-preserve-explicit-refresh.patch` and `0925-sdl-wait-before-present.patch`: preserve + configured refresh across SDL events and finish composition before presenting without a + render-finished semaphore. The build runs the focused capability, format and refresh tests. + Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has. +- `0926-explicit-sync-switch.patch`: `GAMESCOPE_DISABLE_EXPLICIT_SYNC=1` makes the nested + (Wayland and SDL) backends report no explicit sync, so gamescope does not offer + wp_linux_drm_syncobj_manager_v1 and clients use implicit sync. The app sets it on a GPU that is + not an Adreno when the kernel is older than 6.6 (no DRM_IOCTL_SYNCOBJ_EVENTFD: an Exynos 2400e + on 6.1 logged hundreds of "timeline: DRM_IOCTL_SYNCOBJ_EVENTFD failed" and a game hung at a + black screen) or when the render node is Mali's stand-in (/dev/mali0 has no DRM syncobjs). +- `0927-shm-buffers-upload-every-commit.patch` (David Cimo, for Mali through Venus; his `0114`): with + no DRM node, Xwayland has no glamor and commits a few wl_shm buffers over and over with new pixels. + gamescope memoizes one texture per `wlr_buffer`, and a shm buffer's texture is a copy taken at + import, so the screen cycled through the first few frames forever while Steam ran on behind them. + Only dma-buf textures are memoized now; a shm buffer is uploaded on every commit. Adreno never + takes this path, since its Xwayland hands over dma-bufs. Also what XWAYLAND_NO_GLAMOR sessions need. +- `0929-log-failing-compute-pipeline.patch`: when `vkCreateComputePipelines` fails, one extra line + per shader type names the pipeline (shader type, layer count, ycbcr/colorspace masks, fp16) and the + SPIR-V capabilities its shader declares. A Mali-G615 through Venus refused all 119 composite + pipelines with VK_ERROR_INITIALIZATION_FAILED and nothing said which feature it objected to. +- `0930-composite-without-immutable-ycbcr-samplers.patch`: one composite pipeline is compiled at + start-up; when the driver refuses it, the descriptor layout is rebuilt without binding 4's immutable + YCbCr samplers (that binding then takes the slot's ordinary sampler) and the probe tried again, with + both outcomes in the log. `GAMESCOPE_NO_YCBCR_SAMPLERS=1` forces it. A Mali-G615 through Venus + refused every composite shader (blit, blur, RCAS: SPIR-V capabilities Shader, Sampled1D, + ImageQuery, no Float16) while EASU, NIS and the NV12 converter, which share the layout but never + touch binding 4, compiled. +- `0931-memory-type-fallback-keep-probe.patch`: `findMemoryType` asked for DEVICE_LOCAL|HOST_VISIBLE + memory falls back to host-visible memory without DEVICE_LOCAL, and a failure logs the requested + properties, the type bits and every memory type. The Mali bridge models a unified-memory GPU like a + discrete one (host-visible types without DEVICE_LOCAL), so the upload buffer found nothing and the + backend failed. Also: 0930's probe pipeline is kept instead of destroyed, and skipped when + `GAMESCOPE_NO_YCBCR_SAMPLERS=1` (destroying a pipeline the Venus host had refused killed the + render server). +- `0932-name-hung-up-waitable.patch`: "IWaitable hung up. Aborting." now names the waitable's class, + its fd and what the fd is (`/proc/self/fd`), and the Wayland backend's input thread says why the + compositor connection ended (protocol error or errno) first. An S24 FE aborted there after 60/120 Hz + flips and after a resume with nothing else in the log. +- `0933-flippable-image-unexportable-modifiers.patch`: a flippable (dma-buf exported) image whose + modifiers the driver supports but does not report EXPORTABLE uses them anyway, and an image with no + usable modifier fails with a log line instead of asserting in `CVulkanTexture::BInit`. The Mali + bridge forwards the phone driver's answer, which leaves EXPORTABLE out, though it exports dma-bufs. +- `0934-flippable-image-no-backend-modifiers.patch`: when the nested backend offers no modifier at all + for a flippable image's format, `CVulkanTexture::BInit` tries linear and logs it instead of asserting + `!modifiers.empty()`. NieR Replicant ended the session there on a Pixel 9a under Venus. +- `0935-flippable-image-linear-when-driver-rejects-all.patch`: when the driver answers every offered + modifier with `VK_ERROR_FORMAT_NOT_SUPPORTED` and linear was among them, the flippable image tries + linear anyway; if it cannot be made, `BInit` fails as before. An r32p1 Mali-G610 (Vulkan 1.1) behind + the Mali bridge rejected both of the backend's modifiers for AR24, so the session ended at start + (0933's "no usable modifier"); the bridge makes such images without the handle type and exports + them through a dma-heap. -- `0116-release-cursor-shm-buffers.patch` - this app: pointer cursor surfaces have no - X11/xdg window association, so routing their commits through `g_PendingCommits` - retained every old SHM buffer until the surface was destroyed. Ignore cursor-role - commits before taking the buffer lock; X11 cursor images still follow the existing - MouseCursor path. Window commits awaiting association are unchanged. +The shared DroidDeck patch also includes `0116-release-cursor-shm-buffers.patch`: pointer cursor +surfaces have no X11/xdg window association, so routing their commits through `g_PendingCommits` +retained every old SHM buffer until the surface was destroyed. It ignores cursor-role commits before +taking the buffer lock; X11 cursor images still follow the existing MouseCursor path. Window commits +awaiting association are unchanged. The live regression probe is `tools/tests/test_gamescope_cursor_shm.py`. Run it inside an active Gamescope session with access to that process's `/proc` fd diff --git a/tools/gamescope/build-in-arch.sh b/tools/gamescope/build-in-arch.sh index dbf323136..cf56408bf 100755 --- a/tools/gamescope/build-in-arch.sh +++ b/tools/gamescope/build-in-arch.sh @@ -43,9 +43,12 @@ meson test -C src/build --no-rebuild --print-errorlogs cursor_routing wayland_sc ls -l *.pkg.tar.* " cd "$WORK" +g++ -std=c++17 -Wall -Wextra -Werror -UNDEBUG -I pkg/arch/src/gamescope/src \ + tools/gamescope/tests/nested-swapchain.cpp -o pkg/test-nested-swapchain +pkg/test-nested-swapchain rm -rf out && mkdir -p out/usr/local/bin PKG=$(ls pkg/arch/gamescope-*.pkg.tar.* | grep -v -- '-debug-' | head -1) -tar --use-compress-program=unzstd -xf "$PKG" -C out --strip-components=2 usr/bin/gamescope +tar -xf "$PKG" -C out --strip-components=2 usr/bin/gamescope mv out/gamescope out/usr/local/bin/gamescope chmod 755 out/usr/local/bin/gamescope strip --strip-unneeded out/usr/local/bin/gamescope || true diff --git a/tools/gamescope/patches/0920-nested-swapchain-capability-fallback.patch b/tools/gamescope/patches/0920-nested-swapchain-capability-fallback.patch new file mode 100644 index 000000000..c26c8f82b --- /dev/null +++ b/tools/gamescope/patches/0920-nested-swapchain-capability-fallback.patch @@ -0,0 +1,358 @@ +diff --git a/src/nested_swapchain.hpp b/src/nested_swapchain.hpp +new file mode 100644 +index 0000000..06fc987 +--- /dev/null ++++ b/src/nested_swapchain.hpp +@@ -0,0 +1,53 @@ ++#pragma once ++ ++#include ++#include ++ ++namespace gamescope ++{ ++struct NestedSwapchainConfig ++{ ++ bool copy = false; ++ uint32_t imageCount = 0; ++ VkImageUsageFlags usage = 0; ++ VkCompositeAlphaFlagBitsKHR alpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; ++ VkExtent2D extent = {}; ++}; ++ ++inline bool ChooseNestedSwapchainConfig( const VkSurfaceCapabilitiesKHR &caps, ++ bool mutableFormat, uint32_t width, uint32_t height, NestedSwapchainConfig &config ) ++{ ++ constexpr VkImageUsageFlags direct = VK_IMAGE_USAGE_SAMPLED_BIT | ++ VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; ++ config.copy = !mutableFormat || ( caps.supportedUsageFlags & direct ) != direct; ++ config.usage = config.copy ? VkImageUsageFlags{VK_IMAGE_USAGE_TRANSFER_DST_BIT} : direct; ++ if ( ( caps.supportedUsageFlags & config.usage ) != config.usage ) ++ return false; ++ ++ bool foundAlpha = false; ++ for ( auto alpha : { VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, ++ VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR, VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR } ) ++ { ++ if ( caps.supportedCompositeAlpha & alpha ) ++ { ++ config.alpha = alpha; ++ foundAlpha = true; ++ break; ++ } ++ } ++ if ( !foundAlpha || caps.maxImageExtent.width < caps.minImageExtent.width || ++ caps.maxImageExtent.height < caps.minImageExtent.height ) ++ return false; ++ ++ config.imageCount = caps.minImageCount + ( caps.minImageCount != UINT32_MAX ); ++ if ( caps.maxImageCount ) ++ config.imageCount = std::min( config.imageCount, caps.maxImageCount ); ++ if ( config.imageCount < caps.minImageCount ) ++ return false; ++ config.extent = caps.currentExtent.width == UINT32_MAX ++ ? VkExtent2D{ std::clamp( width, caps.minImageExtent.width, caps.maxImageExtent.width ), ++ std::clamp( height, caps.minImageExtent.height, caps.maxImageExtent.height ) } ++ : caps.currentExtent; ++ return config.extent.width && config.extent.height; ++} ++} +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index ffad695..ffc8e28 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -12,6 +12,7 @@ + #include + #include + #include "vulkan_include.h" ++#include "nested_swapchain.hpp" + #include "Utils/Algorithm.h" + + #if defined(__linux__) +@@ -450,6 +451,8 @@ bool CVulkanDevice::createDevice() + bool hasDrmProps = vulkan_has_drm_props(); + bool supportsForeignQueue = false; + bool supportsHDRMetadata = false; ++ bool presentIdExtension = false; ++ bool presentWaitExtension = false; + for (const auto& ext : m_supportedExts) { + if ( strcmp(ext.extensionName, VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME) == 0 ) + m_bSupportsModifiers = true; +@@ -457,6 +460,13 @@ bool CVulkanDevice::createDevice() + if ( strcmp(ext.extensionName, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME) == 0 ) + supportsForeignQueue = true; + ++ if ( strcmp(ext.extensionName, VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME) == 0 ) ++ m_bSupportsMutableSwapchain = true; ++ if ( strcmp(ext.extensionName, VK_KHR_PRESENT_ID_EXTENSION_NAME) == 0 ) ++ presentIdExtension = true; ++ if ( strcmp(ext.extensionName, VK_KHR_PRESENT_WAIT_EXTENSION_NAME) == 0 ) ++ presentWaitExtension = true; ++ + if ( strcmp(ext.extensionName, VK_EXT_HDR_METADATA_EXTENSION_NAME) == 0 ) + supportsHDRMetadata = true; + } +@@ -576,10 +586,24 @@ bool CVulkanDevice::createDevice() + if ( GetBackend()->UsesVulkanSwapchain() ) + { + enabledExtensions.push_back( VK_KHR_SWAPCHAIN_EXTENSION_NAME ); +- enabledExtensions.push_back( VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME ); ++ if ( m_bSupportsMutableSwapchain ) ++ enabledExtensions.push_back( VK_KHR_SWAPCHAIN_MUTABLE_FORMAT_EXTENSION_NAME ); + +- enabledExtensions.push_back( VK_KHR_PRESENT_ID_EXTENSION_NAME ); +- enabledExtensions.push_back( VK_KHR_PRESENT_WAIT_EXTENSION_NAME ); ++ if ( presentIdExtension && presentWaitExtension ) ++ { ++ VkPhysicalDevicePresentWaitFeaturesKHR wait = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR }; ++ VkPhysicalDevicePresentIdFeaturesKHR id = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR, .pNext = &wait }; ++ VkPhysicalDeviceFeatures2 features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &id }; ++ vk.GetPhysicalDeviceFeatures2( physDev(), &features ); ++ m_bSupportsPresentWait = id.presentId && wait.presentWait; ++ } ++ if ( m_bSupportsPresentWait ) ++ { ++ enabledExtensions.push_back( VK_KHR_PRESENT_ID_EXTENSION_NAME ); ++ enabledExtensions.push_back( VK_KHR_PRESENT_WAIT_EXTENSION_NAME ); ++ } ++ else ++ vk_log.infof( "Using FIFO acquire pacing; presentation timestamps unavailable" ); + + if ( supportsHDRMetadata ) + enabledExtensions.push_back( VK_EXT_HDR_METADATA_EXTENSION_NAME ); +@@ -654,7 +678,7 @@ bool CVulkanDevice::createDevice() + + VkPhysicalDeviceFeatures2 features2 = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, +- .pNext = &presentIdFeatures, ++ .pNext = m_bSupportsPresentWait ? static_cast( &presentIdFeatures ) : static_cast( &features13 ), + .features = { + .shaderInt16 = m_bSupportsFp16, + }, +@@ -2660,7 +2684,7 @@ bool CVulkanTexture::BInit( uint32_t width, uint32_t height, uint32_t depth, uin + return true; + } + +-bool CVulkanTexture::BInitFromSwapchain( VkImage image, uint32_t width, uint32_t height, VkFormat format ) ++bool CVulkanTexture::BInitFromSwapchain( VkImage image, uint32_t width, uint32_t height, VkFormat format, bool transferOnly ) + { + m_drmFormat = VulkanFormatToDRM( format ); + m_vkImage = image; +@@ -2672,6 +2696,12 @@ bool CVulkanTexture::BInitFromSwapchain( VkImage image, uint32_t width, uint32_t + m_contentWidth = width; + m_contentHeight = height; + m_bOutputImage = true; ++ if ( transferOnly ) ++ { ++ // A copy destination needs no views (and the swapchain need not be mutable). ++ m_bInitialized = true; ++ return true; ++ } + + VkImageViewCreateInfo createInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, +@@ -3094,7 +3124,7 @@ void vulkan_present_to_window( void ) + + VkPresentInfoKHR presentInfo = { + .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, +- .pNext = &presentIdInfo, ++ .pNext = g_device.supportsPresentWait() ? &presentIdInfo : nullptr, + .swapchainCount = 1, + .pSwapchains = &g_output.swapChain, + .pImageIndices = &g_output.nOutImage, +@@ -3102,14 +3132,22 @@ void vulkan_present_to_window( void ) + + if ( g_device.vk.QueuePresentKHR( g_device.queue(), &presentInfo ) == VK_SUCCESS ) + { +- g_currentPresentWaitId = presentId; +- g_currentPresentWaitId.notify_all(); ++ if ( g_device.supportsPresentWait() ) ++ { ++ g_currentPresentWaitId = presentId; ++ g_currentPresentWaitId.notify_all(); ++ } + } + else + vulkan_remake_swapchain(); + + while ( !acquire_next_image() ) + vulkan_remake_swapchain(); ++ if ( !g_device.supportsPresentWait() ) ++ { ++ // Estimated scheduling anchor after FIFO buffer acquisition, not presentation feedback. ++ GetVBlankTimer().MarkVBlank( get_time_in_nanos(), true ); ++ } + } + + gamescope::Rc vulkan_create_1d_lut(uint32_t size) +@@ -3219,7 +3257,19 @@ extern bool g_bOutputHDREnabled; + + bool vulkan_make_swapchain( VulkanOutput_t *pOutput ) + { +- uint32_t imageCount = pOutput->surfaceCaps.minImageCount + 1; ++ // Re-query on resize as well as initial creation. ++ if ( g_device.vk.GetPhysicalDeviceSurfaceCapabilitiesKHR( g_device.physDev(), pOutput->surface, &pOutput->surfaceCaps ) != VK_SUCCESS ) ++ return false; ++ gamescope::NestedSwapchainConfig config; ++ if ( !gamescope::ChooseNestedSwapchainConfig( pOutput->surfaceCaps, g_device.supportsMutableSwapchain(), g_nOutputWidth, g_nOutputHeight, config ) ) ++ { ++ vk_log.errorf( "Surface supports neither direct composition nor transfer presentation" ); ++ return false; ++ } ++ pOutput->copyToSwapchain = config.copy; ++ g_nOutputWidth = config.extent.width; ++ g_nOutputHeight = config.extent.height; ++ uint32_t imageCount = config.imageCount; + uint32_t formatCount = pOutput->surfaceFormats.size(); + uint32_t surfaceFormat = formatCount; + VkColorSpaceKHR preferredColorSpace = g_bOutputHDREnabled ? VK_COLOR_SPACE_HDR10_ST2084_EXT : VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; +@@ -3254,6 +3304,13 @@ bool vulkan_make_swapchain( VulkanOutput_t *pOutput ) + } + } + ++ if ( surfaceFormat == formatCount ) ++ { ++ for ( surfaceFormat = 0; surfaceFormat < formatCount; ++surfaceFormat ) ++ if ( pOutput->surfaceFormats[surfaceFormat].format == VK_FORMAT_R8G8B8A8_UNORM && ++ pOutput->surfaceFormats[surfaceFormat].colorSpace == preferredColorSpace ) ++ break; ++ } + if ( surfaceFormat == formatCount ) + return false; + +@@ -3276,8 +3333,8 @@ bool vulkan_make_swapchain( VulkanOutput_t *pOutput ) + + VkSwapchainCreateInfoKHR createInfo = { + .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, +- .pNext = formats[0] != formats[1] ? &usageListInfo : nullptr, +- .flags = formats[0] != formats[1] ? VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR : (VkSwapchainCreateFlagBitsKHR )0, ++ .pNext = !config.copy && formats[0] != formats[1] ? &usageListInfo : nullptr, ++ .flags = !config.copy && formats[0] != formats[1] ? VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR : (VkSwapchainCreateFlagBitsKHR )0, + .surface = pOutput->surface, + .minImageCount = imageCount, + .imageFormat = eVkFormat, +@@ -3287,10 +3344,10 @@ bool vulkan_make_swapchain( VulkanOutput_t *pOutput ) + .height = g_nOutputHeight, + }, + .imageArrayLayers = 1, +- .imageUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, ++ .imageUsage = config.usage, + .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, + .preTransform = pOutput->surfaceCaps.currentTransform, +- .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, ++ .compositeAlpha = config.alpha, + .presentMode = VK_PRESENT_MODE_FIFO_KHR, + .clipped = VK_TRUE, + }; +@@ -3304,12 +3361,30 @@ bool vulkan_make_swapchain( VulkanOutput_t *pOutput ) + g_device.vk.GetSwapchainImagesKHR( g_device.device(), pOutput->swapChain, &imageCount, swapchainImages.data() ); + + pOutput->outputImages.resize(imageCount); ++ pOutput->presentationImages.clear(); ++ if ( config.copy ) ++ { ++ vk_log.infof( "Nested swapchain: composing offscreen and copying to FIFO presentation images" ); ++ pOutput->presentationImages.resize(imageCount); ++ } + + for ( uint32_t i = 0; i < pOutput->outputImages.size(); i++ ) + { + pOutput->outputImages[i] = new CVulkanTexture(); + +- if ( !pOutput->outputImages[i]->BInitFromSwapchain(swapchainImages[i], g_nOutputWidth, g_nOutputHeight, eVkFormat)) ++ if ( config.copy ) ++ { ++ CVulkanTexture::createFlags flags; ++ flags.bStorage = true; ++ flags.bSampled = true; ++ flags.bTransferSrc = true; ++ if ( !pOutput->outputImages[i]->BInit( g_nOutputWidth, g_nOutputHeight, 1, pOutput->uOutputFormat, flags ) ) ++ return false; ++ pOutput->presentationImages[i] = new CVulkanTexture(); ++ if ( !pOutput->presentationImages[i]->BInitFromSwapchain( swapchainImages[i], g_nOutputWidth, g_nOutputHeight, eVkFormat, true ) ) ++ return false; ++ } ++ else if ( !pOutput->outputImages[i]->BInitFromSwapchain(swapchainImages[i], g_nOutputWidth, g_nOutputHeight, eVkFormat)) + return false; + } + +@@ -3335,6 +3410,10 @@ bool vulkan_remake_swapchain( void ) + g_device.vk.QueueWaitIdle( g_device.queue() ); + + pOutput->outputImages.clear(); ++ pOutput->presentationImages.clear(); ++ if ( pOutput->acquireFence ) ++ g_device.vk.DestroyFence( g_device.device(), pOutput->acquireFence, nullptr ); ++ pOutput->acquireFence = VK_NULL_HANDLE; + + g_device.vk.DestroySwapchainKHR( g_device.device(), pOutput->swapChain, nullptr ); + +@@ -3670,7 +3749,7 @@ bool vulkan_init( VkInstance instance, VkSurfaceKHR surface ) + if (!init_nis_data()) + return false; + +- if ( GetBackend()->UsesVulkanSwapchain() ) ++ if ( GetBackend()->UsesVulkanSwapchain() && g_device.supportsPresentWait() ) + { + std::thread present_wait_thread( present_wait_thread_func ); + present_wait_thread.detach(); +@@ -4579,6 +4658,9 @@ std::optional vulkan_composite( struct FrameInfo_t *frameInfo, gamesco + CompositeTimingEnd( cmdBuffer.get(), *oTimingSlot, pszKey, pszKeyPre, pszKeyPost ); + } + ++ if ( !pOutputOverride && g_output.copyToSwapchain ) ++ cmdBuffer->copyImage( compositeImage, g_output.presentationImages[g_output.nOutImage] ); ++ + uint64_t sequence = g_device.submit(std::move(cmdBuffer)); + if ( oTimingSlot ) + g_CompositeTiming.pending.back().ulSeqNo = sequence; +diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp +index fa519db..674a3b4 100644 +--- a/src/rendervulkan.hpp ++++ b/src/rendervulkan.hpp +@@ -162,7 +162,7 @@ public: + }; + + bool BInit( uint32_t width, uint32_t height, uint32_t depth, uint32_t drmFormat, createFlags flags, wlr_dmabuf_attributes *pDMA = nullptr, uint32_t contentWidth = 0, uint32_t contentHeight = 0, CVulkanTexture *pExistingImageToReuseMemory = nullptr, gamescope::OwningRc pBackendFb = nullptr ); +- bool BInitFromSwapchain( VkImage image, uint32_t width, uint32_t height, VkFormat format ); ++ bool BInitFromSwapchain( VkImage image, uint32_t width, uint32_t height, VkFormat format, bool transferOnly = false ); + + uint32_t IncRef(); + uint32_t DecRef(); +@@ -586,7 +586,9 @@ struct VulkanOutput_t + + std::shared_ptr swapchainHDRMetadata; + VkSwapchainKHR swapChain; +- VkFence acquireFence; ++ VkFence acquireFence = VK_NULL_HANDLE; ++ bool copyToSwapchain = false; ++ std::vector> presentationImages; + + uint32_t nOutImage; // swapchain index in nested mode, or ping/pong between two RTs + std::vector> outputImages; +@@ -756,6 +758,7 @@ static inline uint32_t div_roundup(uint32_t x, uint32_t y) + VK_FUNC(CreateDescriptorPool) \ + VK_FUNC(CreateDescriptorSetLayout) \ + VK_FUNC(CreateFence) \ ++ VK_FUNC(DestroyFence) \ + VK_FUNC(CreateGraphicsPipelines) \ + VK_FUNC(CreateImage) \ + VK_FUNC(CreateImageView) \ +@@ -862,6 +865,8 @@ public: + inline VkPipelineLayout pipelineLayout() {return m_pipelineLayout;} + inline int drmRenderFd() {return m_drmRendererFd;} + inline bool supportsModifiers() {return m_bSupportsModifiers;} ++ inline bool supportsMutableSwapchain() {return m_bSupportsMutableSwapchain;} ++ inline bool supportsPresentWait() {return m_bSupportsPresentWait;} + inline bool hasDrmPrimaryDevId() {return m_bHasDrmPrimaryDevId;} + inline dev_t primaryDevId() {return m_drmPrimaryDevId;} + inline bool supportsFp16() {return m_bSupportsFp16;} +@@ -930,6 +935,8 @@ protected: + uint32_t m_uVendorID = 0; + bool m_bHasDrmPrimaryDevId = false; + bool m_bSupportsModifiers = false; ++ bool m_bSupportsMutableSwapchain = false; ++ bool m_bSupportsPresentWait = false; + bool m_bInitialized = false; + + diff --git a/tools/gamescope/patches/0921-powervr-single-pixel-rcas.patch b/tools/gamescope/patches/0921-powervr-single-pixel-rcas.patch new file mode 100644 index 000000000..d2b3aafea --- /dev/null +++ b/tools/gamescope/patches/0921-powervr-single-pixel-rcas.patch @@ -0,0 +1,15 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -996,8 +996,9 @@ + SHADER(BLUR, cs_composite_blur); + SHADER(BLUR_COND, cs_composite_blur_cond); + SHADER(BLUR_FIRST_PASS, cs_gaussian_blur_horizontal); +- // The one pixel layout is only measured on Adreno, every other vendor keeps the quad swizzle. +- if (m_uVendorID == 0x5143) /* Qualcomm */ ++ // PowerVR rejects the quad-swizzled RCAS for 3+ layers with YCbCr mask 2. ++ // The existing one-pixel shader compiles those variants and covers the same 16x16 tile. ++ if (m_uVendorID == 0x5143 || m_uVendorID == 0x1010) /* Qualcomm or Imagination */ + { + SHADER(RCAS, cs_composite_rcas_1px); + } diff --git a/tools/gamescope/patches/0922-powervr-linear-dmabuf-import.patch b/tools/gamescope/patches/0922-powervr-linear-dmabuf-import.patch new file mode 100644 index 000000000..40a913560 --- /dev/null +++ b/tools/gamescope/patches/0922-powervr-linear-dmabuf-import.patch @@ -0,0 +1,156 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -13,6 +13,7 @@ + #include + #include "vulkan_include.h" + #include "nested_swapchain.hpp" ++#include "dmabuf_import.hpp" + #include "Utils/Algorithm.h" + + #if defined(__linux__) +@@ -2229,8 +2230,27 @@ + return false; + } + +- if ( res == VK_SUCCESS && +- ( externalImageProperties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT ) ) ++ bool importable = res == VK_SUCCESS && ++ ( externalImageProperties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT ); ++ if ( gamescope::RetryPowerVRLinearQuery( g_device.vendorID(), imageInfo.format, ++ pDMA->modifier, pDMA->n_planes, imageInfo.flags, res ) ) ++ { ++ // PowerVR's mutable-format query rejects this linear import even though ++ // explicit-layout imports and UNORM/sRGB GPU sampling work. Query the base ++ // format without changing the image we actually create or its supplied stride. ++ VkImageCreateInfo baseFormat = imageInfo; ++ baseFormat.flags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; ++ baseFormat.pNext = nullptr; ++ res = getModifierProps( &baseFormat, pDMA->modifier, &externalImageProperties ); ++ importable = res == VK_SUCCESS && ++ ( externalImageProperties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT ); ++ } ++ if ( !importable && g_device.vendorID() == 0x1010 ) ++ { ++ vk_log.errorf( "PowerVR DMA-BUF modifier is not importable; refusing a different tiling" ); ++ return false; ++ } ++ if ( importable ) + { + modifierInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT, +@@ -2348,6 +2368,22 @@ + + m_format = imageInfo.format; + ++ VkMemoryFdPropertiesKHR fdProperties = { ++ .sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR, ++ }; ++ if ( pDMA ) ++ { ++ if ( !g_device.vk.GetMemoryFdPropertiesKHR ) ++ return false; ++ res = g_device.vk.GetMemoryFdPropertiesKHR( g_device.device(), ++ VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, pDMA->fd[0], &fdProperties ); ++ if ( res != VK_SUCCESS ) ++ { ++ vk_errorf( res, "vkGetMemoryFdPropertiesKHR failed" ); ++ return false; ++ } ++ } ++ + res = g_device.vk.CreateImage(g_device.device(), &imageInfo, nullptr, &m_vkImage); + if (res != VK_SUCCESS) { + vk_errorf( res, "vkCreateImage failed" ); +@@ -2357,10 +2393,22 @@ + VkMemoryRequirements memRequirements; + g_device.vk.GetImageMemoryRequirements(g_device.device(), m_vkImage, &memRequirements); + ++ uint32_t memoryType = pDMA ++ ? gamescope::DmabufMemoryType( g_device.memoryProperties(), memRequirements.memoryTypeBits, ++ fdProperties.memoryTypeBits, properties ) ++ : uint32_t( g_device.findMemoryType( properties, memRequirements.memoryTypeBits ) ); ++ if ( memoryType == UINT32_MAX ) ++ { ++ vk_log.errorf( "No compatible image memory type (image=0x%x, fd=0x%x)", ++ memRequirements.memoryTypeBits, fdProperties.memoryTypeBits ); ++ g_device.vk.DestroyImage( g_device.device(), m_vkImage, nullptr ); ++ m_vkImage = VK_NULL_HANDLE; ++ return false; ++ } + VkMemoryAllocateInfo allocInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memRequirements.size, +- .memoryTypeIndex = uint32_t(g_device.findMemoryType(properties, memRequirements.memoryTypeBits)), ++ .memoryTypeIndex = memoryType, + }; + + m_size = allocInfo.allocationSize; +@@ -2453,6 +2501,13 @@ + if ( res != VK_SUCCESS ) + { + vk_errorf( res, "vkBindImageMemory failed" ); ++ if ( pDMA ) ++ vk_log.errorf( "DMA-BUF: %ux%u format=%u modifier=0x%" PRIx64 ++ " planes=%d stride=%u offset=%u tiling=%u allocation=%" PRIu64 ++ " memoryType=%u imageBits=0x%x fdBits=0x%x", width, height, imageInfo.format, ++ pDMA->modifier, pDMA->n_planes, pDMA->stride[0], pDMA->offset[0], imageInfo.tiling, ++ uint64_t( allocInfo.allocationSize ), allocInfo.memoryTypeIndex, ++ memRequirements.memoryTypeBits, fdProperties.memoryTypeBits ); + return false; + } + +diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp +--- a/src/rendervulkan.hpp ++++ b/src/rendervulkan.hpp +@@ -791,6 +791,7 @@ + VK_FUNC(GetImageMemoryRequirements) \ + VK_FUNC(GetImageSubresourceLayout) \ + VK_FUNC(GetMemoryFdKHR) \ ++ VK_FUNC(GetMemoryFdPropertiesKHR) \ + VK_FUNC(GetQueryPoolResults) \ + VK_FUNC(GetSemaphoreCounterValue) \ + VK_FUNC(GetSwapchainImagesKHR) \ +@@ -865,6 +866,8 @@ + inline VkPipelineLayout pipelineLayout() {return m_pipelineLayout;} + inline int drmRenderFd() {return m_drmRendererFd;} + inline bool supportsModifiers() {return m_bSupportsModifiers;} ++ inline uint32_t vendorID() const {return m_uVendorID;} ++ inline const VkPhysicalDeviceMemoryProperties &memoryProperties() const {return m_memoryProperties;} + inline bool supportsMutableSwapchain() {return m_bSupportsMutableSwapchain;} + inline bool supportsPresentWait() {return m_bSupportsPresentWait;} + inline bool hasDrmPrimaryDevId() {return m_bHasDrmPrimaryDevId;} +diff --git a/src/dmabuf_import.hpp b/src/dmabuf_import.hpp +new file mode 100644 +--- /dev/null ++++ b/src/dmabuf_import.hpp +@@ -0,0 +1,29 @@ ++#pragma once ++#include ++ ++namespace gamescope ++{ ++inline bool RetryPowerVRLinearQuery( uint32_t vendor, VkFormat format, uint64_t modifier, ++ uint32_t planes, VkImageCreateFlags flags, VkResult result ) ++{ ++ return vendor == 0x1010 && modifier == 0 && planes == 1 && ++ flags == VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT && result == VK_ERROR_FORMAT_NOT_SUPPORTED && ++ ( format == VK_FORMAT_R8G8B8A8_UNORM || format == VK_FORMAT_B8G8R8A8_UNORM ); ++} ++ ++inline uint32_t DmabufMemoryType( const VkPhysicalDeviceMemoryProperties &memory, ++ uint32_t imageBits, uint32_t fdBits, VkMemoryPropertyFlags preferred ) ++{ ++ uint32_t fallback = UINT32_MAX; ++ const uint32_t compatible = imageBits & fdBits; ++ for ( uint32_t i = 0; i < memory.memoryTypeCount; ++i ) ++ { ++ if ( !( compatible & ( 1u << i ) ) ) continue; ++ if ( fallback == UINT32_MAX ) fallback = i; ++ if ( ( memory.memoryTypes[i].propertyFlags & preferred ) == preferred ) return i; ++ } ++ // Device-local is a preference for an existing imported buffer. CPU mapping ++ // properties are requirements and must never be silently relaxed. ++ return preferred == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT ? fallback : UINT32_MAX; ++} ++} diff --git a/tools/gamescope/patches/0923-powervr-rgba-copy-surface.patch b/tools/gamescope/patches/0923-powervr-rgba-copy-surface.patch new file mode 100644 index 000000000..72344e6bc --- /dev/null +++ b/tools/gamescope/patches/0923-powervr-rgba-copy-surface.patch @@ -0,0 +1,61 @@ +diff --git a/src/nested_surface_format.hpp b/src/nested_surface_format.hpp +new file mode 100644 +--- /dev/null ++++ b/src/nested_surface_format.hpp +@@ -0,0 +1,31 @@ ++#pragma once ++ ++#include ++ ++namespace gamescope ++{ ++// The SDR composition shader declares rgba8, which requires an R8G8B8A8_UNORM ++// storage view. Binding a BGRA view is a format mismatch; a same-format copy ++// cannot correct it. Keep the workaround scoped to the PowerVR SDR copy path ++// where the channel reversal was reproduced, not game-facing texture formats. ++// On success, formatCount means the existing selector should choose the format. ++// On failure, no matching RGBA SDR surface was advertised for this scoped path. ++inline bool ChooseNestedSDRCopySurfaceFormat( uint32_t vendorId, bool copy, bool hdr, ++ const VkSurfaceFormatKHR *formats, uint32_t formatCount, uint32_t &surfaceFormat ) ++{ ++ surfaceFormat = formatCount; ++ if ( vendorId != 0x1010 || !copy || hdr ) ++ return true; ++ ++ for ( uint32_t i = 0; i < formatCount; ++i ) ++ { ++ if ( formats[i].format == VK_FORMAT_R8G8B8A8_UNORM && ++ formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR ) ++ { ++ surfaceFormat = i; ++ return true; ++ } ++ } ++ return false; ++} ++} +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -14,6 +14,7 @@ + #include "vulkan_include.h" + #include "nested_swapchain.hpp" + #include "dmabuf_import.hpp" ++#include "nested_surface_format.hpp" + #include "Utils/Algorithm.h" + + #if defined(__linux__) +@@ -3330,6 +3331,13 @@ + uint32_t surfaceFormat = formatCount; + VkColorSpaceKHR preferredColorSpace = g_bOutputHDREnabled ? VK_COLOR_SPACE_HDR10_ST2084_EXT : VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + ++ if ( !gamescope::ChooseNestedSDRCopySurfaceFormat( g_device.vendorID(), config.copy, ++ g_bOutputHDREnabled, pOutput->surfaceFormats.data(), formatCount, surfaceFormat ) ) ++ { ++ vk_log.errorf( "PowerVR copy composition requires an RGBA SDR surface" ); ++ return false; ++ } ++ + if ( surfaceFormat == formatCount ) + { + for ( surfaceFormat = 0; surfaceFormat < formatCount; surfaceFormat++ ) diff --git a/tools/gamescope/patches/0924-sdl-preserve-explicit-refresh.patch b/tools/gamescope/patches/0924-sdl-preserve-explicit-refresh.patch new file mode 100644 index 000000000..2ec1a18d2 --- /dev/null +++ b/tools/gamescope/patches/0924-sdl-preserve-explicit-refresh.patch @@ -0,0 +1,78 @@ +diff --git a/src/sdl_refresh.hpp b/src/sdl_refresh.hpp +new file mode 100644 +--- /dev/null ++++ b/src/sdl_refresh.hpp +@@ -0,0 +1,22 @@ ++#pragma once ++ ++#include ++ ++namespace gamescope ++{ ++struct SDLRefreshChoice ++{ ++ int32_t refresh = 0; ++ bool configured = false; ++}; ++ ++// SDL reports the desktop mode again when its window is shown, moved or resized. Keep an ++// explicit -r as the rate exposed by nested Gamescope; discover the desktop rate only when the ++// caller did not configure one. ++inline constexpr SDLRefreshChoice ChooseSDLRefresh( int32_t configuredRefresh, int32_t desktopRefresh ) ++{ ++ return configuredRefresh > 0 ++ ? SDLRefreshChoice{ configuredRefresh, true } ++ : SDLRefreshChoice{ desktopRefresh, false }; ++} ++} +diff --git a/src/Backends/SDLBackend.cpp b/src/Backends/SDLBackend.cpp +--- a/src/Backends/SDLBackend.cpp ++++ b/src/Backends/SDLBackend.cpp +@@ -18,14 +18,20 @@ + #include "steamcompmgr.hpp" + #include "Utils/Defer.h" + #include "refresh_rate.h" ++#include "log.hpp" ++#include "sdl_refresh.hpp" + + #include "sdlscancodetable.hpp" + + static int g_nOldNestedRefresh = 0; + static bool g_bWindowFocused = true; ++static int g_nLastLoggedConfiguredRefresh = -1; ++static int g_nLastLoggedDesktopRefresh = -1; ++static int g_nLastLoggedEffectiveRefresh = -1; + + static int g_nOutputWidthPts = 0; + static int g_nOutputHeightPts = 0; ++static LogScope sdl_log( "sdl_backend" ); + + extern bool g_bForceHDR10OutputDebug; + extern bool steamMode; +@@ -846,9 +851,25 @@ namespace gamescope + SDL_DisplayMode mode = { SDL_PIXELFORMAT_UNKNOWN, 0, 0, 0, 0 }; + + display_index = SDL_GetWindowDisplayIndex( m_Connector.GetSDLWindow() ); + if ( SDL_GetDesktopDisplayMode( display_index, &mode ) == 0 ) + { +- g_nOutputRefresh = ConvertHztomHz( mode.refresh_rate ); ++ const int nDesktopRefresh = ConvertHztomHz( mode.refresh_rate ); ++ const SDLRefreshChoice refresh = ChooseSDLRefresh( g_nOldNestedRefresh, nDesktopRefresh ); ++ g_nOutputRefresh = refresh.refresh; ++ ++ if ( g_nLastLoggedConfiguredRefresh != g_nOldNestedRefresh || ++ g_nLastLoggedDesktopRefresh != nDesktopRefresh || ++ g_nLastLoggedEffectiveRefresh != refresh.refresh ) ++ { ++ sdl_log.infof( "Refresh mode: configured=%.3fHz desktop=%.3fHz effective=%.3fHz source=%s", ++ ConvertmHzToHz( float( g_nOldNestedRefresh ) ), ++ ConvertmHzToHz( float( nDesktopRefresh ) ), ++ ConvertmHzToHz( float( refresh.refresh ) ), ++ refresh.configured ? "configured -r" : "SDL desktop" ); ++ g_nLastLoggedConfiguredRefresh = g_nOldNestedRefresh; ++ g_nLastLoggedDesktopRefresh = nDesktopRefresh; ++ g_nLastLoggedEffectiveRefresh = refresh.refresh; ++ } + } + } + break; diff --git a/tools/gamescope/patches/0925-sdl-wait-before-present.patch b/tools/gamescope/patches/0925-sdl-wait-before-present.patch new file mode 100644 index 000000000..d8037df9c --- /dev/null +++ b/tools/gamescope/patches/0925-sdl-wait-before-present.patch @@ -0,0 +1,23 @@ +diff --git a/src/Backends/SDLBackend.cpp b/src/Backends/SDLBackend.cpp +--- a/src/Backends/SDLBackend.cpp ++++ b/src/Backends/SDLBackend.cpp +@@ -351,14 +351,15 @@ namespace gamescope + std::optional oCompositeResult = vulkan_composite( (FrameInfo_t *)pFrameInfo, nullptr, false ); + if ( !oCompositeResult ) + return -EINVAL; + ++ // Present has no render-finished semaphore, so wait before the native consumer can read ++ // the old contents. This serializes composition and present; a future pipelined path must ++ // signal a binary render-finished semaphore and make vkQueuePresentKHR wait on it. ++ vulkan_wait( *oCompositeResult, true ); ++ + vulkan_present_to_window(); + + // TODO: Hook up PresentationFeedback. +- +- // Wait for the composite result on our side *after* we +- // commit the buffer to the compositor to avoid a bubble. +- vulkan_wait( *oCompositeResult, true ); + + GetVBlankTimer().UpdateWasCompositing( true ); + GetVBlankTimer().UpdateLastDrawTime( get_time_in_nanos() - g_SteamCompMgrVBlankTime.ulWakeupTime ); diff --git a/tools/gamescope/patches/0926-explicit-sync-switch.patch b/tools/gamescope/patches/0926-explicit-sync-switch.patch new file mode 100644 index 000000000..92d97dead --- /dev/null +++ b/tools/gamescope/patches/0926-explicit-sync-switch.patch @@ -0,0 +1,29 @@ +--- a/src/Backends/WaylandBackend.cpp ++++ b/src/Backends/WaylandBackend.cpp +@@ -2309,7 +2309,10 @@ + + bool CWaylandBackend::SupportsExplicitSync() const + { +- return true; ++ // GAMESCOPE_DISABLE_EXPLICIT_SYNC=1: a kernel without DRM_IOCTL_SYNCOBJ_EVENTFD (before ++ // Linux 6.6) cannot signal the timelines, so clients use implicit sync instead. ++ const char *pszDisable = getenv( "GAMESCOPE_DISABLE_EXPLICIT_SYNC" ); ++ return !( pszDisable && *pszDisable == '1' ); + } + + bool CWaylandBackend::IsPaused() const +--- a/src/Backends/SDLBackend.cpp ++++ b/src/Backends/SDLBackend.cpp +@@ -506,8 +506,10 @@ + + bool CSDLBackend::SupportsExplicitSync() const + { +- // We use a Vulkan swapchain, so yes. +- return true; ++ // We use a Vulkan swapchain, so yes - unless the kernel cannot signal the timelines ++ // (no DRM_IOCTL_SYNCOBJ_EVENTFD before Linux 6.6): GAMESCOPE_DISABLE_EXPLICIT_SYNC=1. ++ const char *pszDisable = getenv( "GAMESCOPE_DISABLE_EXPLICIT_SYNC" ); ++ return !( pszDisable && *pszDisable == '1' ); + } + + bool CSDLBackend::IsPaused() const diff --git a/tools/gamescope/patches/0927-shm-buffers-upload-every-commit.patch b/tools/gamescope/patches/0927-shm-buffers-upload-every-commit.patch new file mode 100644 index 000000000..5f382c15b --- /dev/null +++ b/tools/gamescope/patches/0927-shm-buffers-upload-every-commit.patch @@ -0,0 +1,44 @@ +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index 18b799e..b803ee0 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -1850,16 +1850,23 @@ import_commit ( + commit->fifo = false; + } + +- if ( gamescope::OwningRc pTexture = s_BufferMemos.LookupVulkanTexture( buf ) ) ++ struct wlr_dmabuf_attributes dmabuf = {0}; ++ const bool bDmabuf = wlr_buffer_get_dmabuf( buf, &dmabuf ); ++ ++ // Only a dma-buf's texture shows the buffer itself. A shm buffer's texture is a copy taken at ++ // import, and Xwayland without glamor commits the same shm buffers again with new pixels. ++ if ( bDmabuf ) + { +- // Going from OwningRc -> Rc now. +- commit->vulkanTex = pTexture; +- return commit; ++ if ( gamescope::OwningRc pTexture = s_BufferMemos.LookupVulkanTexture( buf ) ) ++ { ++ // Going from OwningRc -> Rc now. ++ commit->vulkanTex = pTexture; ++ return commit; ++ } + } + +- struct wlr_dmabuf_attributes dmabuf = {0}; + gamescope::OwningRc pBackendFb; +- if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) ++ if ( bDmabuf ) + { + pBackendFb = GetBackend()->ImportDmabufToBackend( &dmabuf ); + } +@@ -1873,7 +1880,8 @@ import_commit ( + + commit->vulkanTex = pOwnedTexture; + +- s_BufferMemos.MemoizeBuffer( buf, std::move( pOwnedTexture ) ); ++ if ( bDmabuf ) ++ s_BufferMemos.MemoizeBuffer( buf, std::move( pOwnedTexture ) ); + + return commit; + } diff --git a/tools/gamescope/patches/0928-robustness2-optional.patch b/tools/gamescope/patches/0928-robustness2-optional.patch new file mode 100644 index 000000000..35e8a7cd5 --- /dev/null +++ b/tools/gamescope/patches/0928-robustness2-optional.patch @@ -0,0 +1,96 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index 41294f7..42b2ec6 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -435,6 +435,9 @@ bool CVulkanDevice::selectPhysDev(VkSurfaceKHR surface) + return true; + } + ++// Stands in for VK_NULL_HANDLE in descriptor slots when the device has no nullDescriptor. ++static gamescope::OwningRc s_nullDescriptorFallback; ++ + bool CVulkanDevice::createDevice() + { + uint32_t supportedExtensionCount; +@@ -459,6 +462,9 @@ bool CVulkanDevice::createDevice() + if ( strcmp(ext.extensionName, VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME) == 0 ) + m_bSupportsModifiers = true; + ++ if ( strcmp(ext.extensionName, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) == 0 ) ++ m_bSupportsNullDescriptor = true; ++ + if ( strcmp(ext.extensionName, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME) == 0 ) + supportsForeignQueue = true; + +@@ -622,7 +628,10 @@ bool CVulkanDevice::createDevice() + + enabledExtensions.push_back( VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME ); + +- enabledExtensions.push_back( VK_EXT_ROBUSTNESS_2_EXTENSION_NAME ); ++ if ( m_bSupportsNullDescriptor ) ++ enabledExtensions.push_back( VK_EXT_ROBUSTNESS_2_EXTENSION_NAME ); ++ else ++ vk_log.infof( "no VK_EXT_robustness2: unused descriptor slots use a 1x1 texture" ); + #if 0 + enabledExtensions.push_back( VK_KHR_MAINTENANCE_5_EXTENSION_NAME ); + #endif +@@ -711,9 +720,10 @@ bool CVulkanDevice::createDevice() + + VkPhysicalDeviceRobustness2FeaturesEXT robustness2Features = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, +- .pNext = std::exchange(features2.pNext, &robustness2Features), + .nullDescriptor = VK_TRUE, + }; ++ if ( m_bSupportsNullDescriptor ) ++ robustness2Features.pNext = std::exchange(features2.pNext, &robustness2Features); + + VkResult res = vk.CreateDevice(physDev(), &deviceCreateInfo, nullptr, &m_device); + if ( res == VK_ERROR_NOT_PERMITTED_KHR && useRealtimeVulkanQueue ) +@@ -1861,6 +1871,15 @@ void CVulkanCmdBuffer::dispatch(uint32_t x, uint32_t y, uint32_t z) + lut3DDescriptor[i].imageView = m_lut3D[i] ? m_lut3D[i]->srgbView() : VK_NULL_HANDLE; + } + ++ if ( !m_device->supportsNullDescriptor() && s_nullDescriptorFallback != nullptr ) ++ { ++ VkImageView fallback = s_nullDescriptorFallback->srgbView(); ++ for (auto &d : imageDescriptors) if (d.imageView == VK_NULL_HANDLE) d.imageView = fallback; ++ for (auto &d : ycbcrImageDescriptors) if (d.imageView == VK_NULL_HANDLE) d.imageView = fallback; ++ for (auto &d : shaperLutDescriptor) if (d.imageView == VK_NULL_HANDLE) d.imageView = fallback; ++ for (auto &d : lut3DDescriptor) if (d.imageView == VK_NULL_HANDLE) d.imageView = fallback; ++ } ++ + if (!m_target->isYcbcr()) + { + targetDescriptors[0].imageView = m_target->srgbView(); +@@ -3546,6 +3565,9 @@ static bool vulkan_make_output_images( VulkanOutput_t *pOutput ) + // Oh no. + pOutput->temporaryHackyBlankImage = vulkan_create_debug_blank_texture(); + ++ if ( !g_device.supportsNullDescriptor() && s_nullDescriptorFallback == nullptr ) ++ s_nullDescriptorFallback = vulkan_create_flat_texture( 1, 1, 0, 0, 0, 0 ); ++ + if ( pOutput->uOutputFormatOverlay != VK_FORMAT_UNDEFINED && !kDisablePartialComposition ) + { + uint32_t uPartialDRMFormat = pOutput->uOutputFormatOverlay; +diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp +index 5ffec4a..901e6a6 100644 +--- a/src/rendervulkan.hpp ++++ b/src/rendervulkan.hpp +@@ -866,6 +866,9 @@ public: + inline VkPipelineLayout pipelineLayout() {return m_pipelineLayout;} + inline int drmRenderFd() {return m_drmRendererFd;} + inline bool supportsModifiers() {return m_bSupportsModifiers;} ++ // VK_EXT_robustness2's nullDescriptor. A Mali driver through Venus has no robustness2: unused ++ // descriptor slots then point at a 1x1 texture instead of VK_NULL_HANDLE. ++ inline bool supportsNullDescriptor() {return m_bSupportsNullDescriptor;} + inline uint32_t vendorID() const {return m_uVendorID;} + inline const VkPhysicalDeviceMemoryProperties &memoryProperties() const {return m_memoryProperties;} + inline bool supportsMutableSwapchain() {return m_bSupportsMutableSwapchain;} +@@ -938,6 +941,7 @@ protected: + uint32_t m_uVendorID = 0; + bool m_bHasDrmPrimaryDevId = false; + bool m_bSupportsModifiers = false; ++ bool m_bSupportsNullDescriptor = false; + bool m_bSupportsMutableSwapchain = false; + bool m_bSupportsPresentWait = false; + bool m_bInitialized = false; diff --git a/tools/gamescope/patches/0929-log-failing-compute-pipeline.patch b/tools/gamescope/patches/0929-log-failing-compute-pipeline.patch new file mode 100644 index 000000000..3d5ebc00f --- /dev/null +++ b/tools/gamescope/patches/0929-log-failing-compute-pipeline.patch @@ -0,0 +1,55 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index 42b2ec6..103baee 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -994,6 +994,8 @@ bool CVulkanDevice::createPools() + return true; + } + ++static std::string s_shaderCaps[SHADER_TYPE_COUNT]; ++ + bool CVulkanDevice::createShaders() + { + struct ShaderInfo_t +@@ -1040,6 +1042,25 @@ bool CVulkanDevice::createShaders() + .pCode = shaderInfos[i].spirv, + }; + ++ // The SPIR-V capabilities each shader declares, for the log when its pipeline is refused. ++ { ++ const uint32_t *pWords = shaderInfos[i].spirv; ++ size_t nWords = shaderInfos[i].size / sizeof(uint32_t); ++ std::string &sCaps = s_shaderCaps[i]; ++ sCaps.clear(); ++ for ( size_t w = 5; w < nWords; ) ++ { ++ uint32_t uLen = pWords[w] >> 16; ++ if ( !uLen ) ++ break; ++ if ( ( pWords[w] & 0xffffu ) == 17 /* OpCapability */ && w + 1 < nWords ) ++ sCaps += " " + std::to_string( pWords[w + 1] ); ++ else if ( ( pWords[w] & 0xffffu ) == 54 /* OpFunction: past the header */ ) ++ break; ++ w += uLen; ++ } ++ } ++ + VkResult res = vk.CreateShaderModule(device(), &shaderCreateInfo, nullptr, &m_shaderModules[i]); + if ( res != VK_SUCCESS ) + { +@@ -1240,6 +1261,15 @@ VkPipeline CVulkanDevice::compilePipeline(uint32_t layerCount, uint32_t ycbcrMas + VkResult res = vk.CreateComputePipelines(device(), VK_NULL_HANDLE, 1, &computePipelineCreateInfo, nullptr, &result); + if (res != VK_SUCCESS) { + vk_errorf( res, "vkCreateComputePipelines failed" ); ++ // Which pipeline, once per shader type: a vendor driver through Venus may refuse one variant only. ++ static uint32_t s_uLoggedTypes = 0; ++ if ( !( s_uLoggedTypes & ( 1u << ( (uint32_t)type & 31u ) ) ) ) ++ { ++ s_uLoggedTypes |= 1u << ( (uint32_t)type & 31u ); ++ vk_log.errorf( " failing compute pipeline: shader type %u, layers %u, ycbcr mask 0x%x, blur layers %u, colorspace mask 0x%x, output eotf %u, itm %u, fp16 %d, SPIR-V capabilities:%s", ++ (uint32_t)type, layerCount, ycbcrMask, blur_layer_count, colorspace_mask, (uint32_t)output_eotf, (uint32_t)itm_enable, m_bSupportsFp16 ? 1 : 0, ++ (uint32_t)type < SHADER_TYPE_COUNT ? s_shaderCaps[type].c_str() : "" ); ++ } + return VK_NULL_HANDLE; + } + diff --git a/tools/gamescope/patches/0930-composite-without-immutable-ycbcr-samplers.patch b/tools/gamescope/patches/0930-composite-without-immutable-ycbcr-samplers.patch new file mode 100644 index 000000000..edbf2ad76 --- /dev/null +++ b/tools/gamescope/patches/0930-composite-without-immutable-ycbcr-samplers.patch @@ -0,0 +1,78 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index 103baee..d344a99 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -319,6 +319,33 @@ bool CVulkanDevice::BInit(VkInstance instance, VkSurfaceKHR surface) + return false; + if (!createShaders()) + return false; ++ { ++ // One composite pipeline up front: a driver that refuses it with the immutable YCbCr ++ // samplers gets the layout without them (NV12 textures then sample without conversion). ++ if ( getenv( "GAMESCOPE_NO_YCBCR_SAMPLERS" ) && *getenv( "GAMESCOPE_NO_YCBCR_SAMPLERS" ) == '1' ) ++ { ++ vk_log.infof( "GAMESCOPE_NO_YCBCR_SAMPLERS=1: composite layout without immutable YCbCr samplers" ); ++ vk.DestroyPipelineLayout( device(), m_pipelineLayout, nullptr ); ++ vk.DestroyDescriptorSetLayout( device(), m_descriptorSetLayout, nullptr ); ++ m_bYcbcrImmutableSamplers = false; ++ if ( !createLayouts() ) ++ return false; ++ } ++ VkPipeline probe = compilePipeline( 1, 0, SHADER_TYPE_BLIT, 1, 0, 0, 0, false ); ++ if ( probe == VK_NULL_HANDLE && m_bYcbcrImmutableSamplers ) ++ { ++ vk_log.errorf( "composite pipeline refused; retrying without immutable YCbCr samplers" ); ++ vk.DestroyPipelineLayout( device(), m_pipelineLayout, nullptr ); ++ vk.DestroyDescriptorSetLayout( device(), m_descriptorSetLayout, nullptr ); ++ m_bYcbcrImmutableSamplers = false; ++ if ( !createLayouts() ) ++ return false; ++ probe = compilePipeline( 1, 0, SHADER_TYPE_BLIT, 1, 0, 0, 0, false ); ++ vk_log.infof( "composite pipeline without immutable YCbCr samplers: %s", probe ? "accepted" : "STILL refused" ); ++ } ++ if ( probe != VK_NULL_HANDLE ) ++ vk.DestroyPipeline( device(), probe, nullptr ); ++ } + if (!createScratchResources()) + return false; + +@@ -869,7 +896,7 @@ bool CVulkanDevice::createLayouts() + .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorCount = VKR_SAMPLER_SLOTS, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, +- .pImmutableSamplers = ycbcrSamplers.data(), ++ .pImmutableSamplers = m_bYcbcrImmutableSamplers ? ycbcrSamplers.data() : nullptr, + }, + VkDescriptorSetLayoutBinding { + .binding = 5, +@@ -1870,6 +1897,8 @@ void CVulkanCmdBuffer::dispatch(uint32_t x, uint32_t y, uint32_t z) + imageDescriptors[i].sampler = m_device->sampler(m_samplerState[i]); + imageDescriptors[i].imageLayout = VK_IMAGE_LAYOUT_GENERAL; + ycbcrImageDescriptors[i].imageLayout = VK_IMAGE_LAYOUT_GENERAL; ++ if ( !m_device->ycbcrImmutableSamplers() ) ++ ycbcrImageDescriptors[i].sampler = imageDescriptors[i].sampler; + if (m_boundTextures[i] == nullptr) + continue; + +diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp +index 901e6a6..e0ede11 100644 +--- a/src/rendervulkan.hpp ++++ b/src/rendervulkan.hpp +@@ -876,6 +876,7 @@ public: + inline bool hasDrmPrimaryDevId() {return m_bHasDrmPrimaryDevId;} + inline dev_t primaryDevId() {return m_drmPrimaryDevId;} + inline bool supportsFp16() {return m_bSupportsFp16;} ++ inline bool ycbcrImmutableSamplers() {return m_bYcbcrImmutableSamplers;} + inline std::vector& supportedExtensions() {return m_supportedExts;} + + inline std::pair uploadBufferData(uint32_t size) +@@ -925,6 +926,9 @@ protected: + VkQueue m_generalQueue = nullptr; + VkSamplerYcbcrConversion m_ycbcrConversion = VK_NULL_HANDLE; + VkSampler m_ycbcrSampler = VK_NULL_HANDLE; ++ // Binding 4 carries immutable YCbCr samplers; off when the driver refuses every composite ++ // pipeline with them (Mali-G615 through Venus: VK_ERROR_INITIALIZATION_FAILED). ++ bool m_bYcbcrImmutableSamplers = true; + VkDescriptorSetLayout m_descriptorSetLayout = VK_NULL_HANDLE; + VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; + VkDescriptorPool m_descriptorPool = VK_NULL_HANDLE; diff --git a/tools/gamescope/patches/0931-memory-type-fallback-keep-probe.patch b/tools/gamescope/patches/0931-memory-type-fallback-keep-probe.patch new file mode 100644 index 000000000..1f9954041 --- /dev/null +++ b/tools/gamescope/patches/0931-memory-type-fallback-keep-probe.patch @@ -0,0 +1,55 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index d344a99..e699c85 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -331,7 +331,10 @@ bool CVulkanDevice::BInit(VkInstance instance, VkSurfaceKHR surface) + if ( !createLayouts() ) + return false; + } +- VkPipeline probe = compilePipeline( 1, 0, SHADER_TYPE_BLIT, 1, 0, 0, 0, false ); ++ // Forced off: no probe. Through Venus a pipeline the host refused can still come back as a ++ // handle, and destroying it later is a command the render server rejects - it killed the ++ // whole vtest context (Mali-G615: "vkDestroyPipeline resulted in CS error", exit 137). ++ VkPipeline probe = m_bYcbcrImmutableSamplers ? compilePipeline( 1, 0, SHADER_TYPE_BLIT, 1, 0, 0, 0, false ) : VK_NULL_HANDLE; + if ( probe == VK_NULL_HANDLE && m_bYcbcrImmutableSamplers ) + { + vk_log.errorf( "composite pipeline refused; retrying without immutable YCbCr samplers" ); +@@ -343,8 +346,8 @@ bool CVulkanDevice::BInit(VkInstance instance, VkSurfaceKHR surface) + probe = compilePipeline( 1, 0, SHADER_TYPE_BLIT, 1, 0, 0, 0, false ); + vk_log.infof( "composite pipeline without immutable YCbCr samplers: %s", probe ? "accepted" : "STILL refused" ); + } +- if ( probe != VK_NULL_HANDLE ) +- vk.DestroyPipeline( device(), probe, nullptr ); ++ // The probe is kept, never destroyed (see above): one pipeline. ++ (void)probe; + } + if (!createScratchResources()) + return false; +@@ -1380,7 +1383,26 @@ int32_t CVulkanDevice::findMemoryType( VkMemoryPropertyFlags properties, uint32_ + + return i; + } +- ++ ++ // A device that lists its host-visible memory without DEVICE_LOCAL (the Mali bridge models a ++ // unified-memory GPU that way, as a discrete one would) still has it all on the GPU: settle for ++ // host-visible memory rather than fail ("findMemoryType failed", then no backend). ++ if ( properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT && properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT ) ++ { ++ int32_t nFallback = findMemoryType( properties & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, requiredTypeBits ); ++ static bool s_bLogged = false; ++ if ( !s_bLogged ) ++ { ++ s_bLogged = true; ++ vk_log.infof( "no memory type with properties 0x%x in type bits 0x%x; without DEVICE_LOCAL: %d", properties, requiredTypeBits, nFallback ); ++ } ++ if ( nFallback >= 0 ) ++ return nFallback; ++ } ++ ++ vk_log.errorf( "findMemoryType: no memory type with properties 0x%x in type bits 0x%x (%u types)", properties, requiredTypeBits, m_memoryProperties.memoryTypeCount ); ++ for ( uint32_t i = 0; i < m_memoryProperties.memoryTypeCount; i++ ) ++ vk_log.errorf( " memory type %u: flags 0x%x heap %u", i, m_memoryProperties.memoryTypes[ i ].propertyFlags, m_memoryProperties.memoryTypes[ i ].heapIndex ); + return -1; + } + diff --git a/tools/gamescope/patches/0932-name-hung-up-waitable.patch b/tools/gamescope/patches/0932-name-hung-up-waitable.patch new file mode 100644 index 000000000..51abe76eb --- /dev/null +++ b/tools/gamescope/patches/0932-name-hung-up-waitable.patch @@ -0,0 +1,57 @@ +diff --git a/src/Backends/WaylandBackend.cpp b/src/Backends/WaylandBackend.cpp +index a927f69..2c9bd73 100644 +--- a/src/Backends/WaylandBackend.cpp ++++ b/src/Backends/WaylandBackend.cpp +@@ -2882,7 +2882,22 @@ namespace gamescope + abort(); + } + +- CFunctionWaitable waitable( nFD ); ++ // The compositor's connection: a hang-up says why (the protocol error, if one was posted). ++ class CDisplayWaitable final : public IWaitable ++ { ++ public: ++ CDisplayWaitable( int nFD, wl_display *pDisplay ) : m_nFD{ nFD }, m_pDisplay{ pDisplay } {} ++ int GetFD() final { return m_nFD; } ++ void OnPollHangUp() final ++ { ++ LogDisplayError( "The Wayland compositor closed gamescope's connection (input thread)", m_pDisplay ); ++ IWaitable::OnPollHangUp(); ++ } ++ private: ++ int m_nFD; ++ wl_display *m_pDisplay; ++ }; ++ CDisplayWaitable waitable( nFD, m_pBackend->GetDisplay() ); + m_Waiter.AddWaitable( &waitable ); + + int nRet = 0; +diff --git a/src/waitable.h b/src/waitable.h +index be54d46..b9116fe 100644 +--- a/src/waitable.h ++++ b/src/waitable.h +@@ -9,6 +9,8 @@ + + #include + #include ++#include ++#include + + #include "log.hpp" + +@@ -30,7 +32,14 @@ namespace gamescope + virtual void OnPollPri() {} + virtual void OnPollHangUp() + { +- g_WaitableLog.errorf( "IWaitable hung up. Aborting." ); ++ // Which one: its class and what its descriptor is (a socket, a pipe, an eventfd...). ++ int nFD = GetFD(); ++ char szPath[64], szTarget[256] = "?"; ++ snprintf( szPath, sizeof( szPath ), "/proc/self/fd/%d", nFD ); ++ ssize_t nLen = nFD >= 0 ? readlink( szPath, szTarget, sizeof( szTarget ) - 1 ) : -1; ++ if ( nLen >= 0 ) ++ szTarget[nLen] = '\0'; ++ g_WaitableLog.errorf( "IWaitable hung up: %s, fd %d -> %s. Aborting.", typeid( *this ).name(), nFD, szTarget ); + abort(); + } + diff --git a/tools/gamescope/patches/0933-flippable-image-unexportable-modifiers.patch b/tools/gamescope/patches/0933-flippable-image-unexportable-modifiers.patch new file mode 100644 index 000000000..ab704f031 --- /dev/null +++ b/tools/gamescope/patches/0933-flippable-image-unexportable-modifiers.patch @@ -0,0 +1,44 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index e699c85..0ddd249 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -2372,6 +2372,7 @@ bool CVulkanTexture::BInit( uint32_t width, uint32_t height, uint32_t depth, uin + } + + std::vector modifiers = {}; ++ std::vector unexportable = {}; + // TODO(JoshA): Move this code to backend for making flippable image. + if ( GetBackend()->UsesModifiers() && flags.bFlippable && g_device.supportsModifiers() && !pDMA ) + { +@@ -2410,12 +2411,30 @@ bool CVulkanTexture::BInit( uint32_t width, uint32_t height, uint32_t depth, uin + } + + if ( !( externalFormatProps.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT ) ) ++ { ++ vk_log.infof( "flippable image: modifier 0x%" PRIx64 " for DRM format 0x%x supported but not reported exportable (features 0x%x)", ++ modifier, drmFormat, externalFormatProps.externalMemoryProperties.externalMemoryFeatures ); ++ unexportable.push_back( modifier ); + continue; ++ } + + modifiers.push_back( modifier ); + } + +- assert( modifiers.size() > 0 ); ++ // A driver that supports the modifier but leaves EXPORTABLE out (the Mali bridge forwards the ++ // phone's own driver's answer) can still export the memory as a dma-buf: the bridge's ++ // swapchains do exactly that. Try those rather than assert (rendervulkan.cpp, CVulkanTexture::BInit). ++ if ( modifiers.empty() && !unexportable.empty() ) ++ { ++ vk_log.errorf( "flippable image: no modifier reported exportable for DRM format 0x%x; trying %zu supported one(s) anyway", ++ drmFormat, unexportable.size() ); ++ modifiers = unexportable; ++ } ++ if ( modifiers.empty() ) ++ { ++ vk_log.errorf( "flippable image: no usable modifier for DRM format 0x%x (%zu offered by the backend)", drmFormat, numPossibleModifiers ); ++ return false; ++ } + + modifierListInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT, diff --git a/tools/gamescope/patches/0934-flippable-image-no-backend-modifiers.patch b/tools/gamescope/patches/0934-flippable-image-no-backend-modifiers.patch new file mode 100644 index 000000000..cd22f4484 --- /dev/null +++ b/tools/gamescope/patches/0934-flippable-image-no-backend-modifiers.patch @@ -0,0 +1,26 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -2235,9 +2235,19 @@ + else + { + std::span modifiers = GetBackend()->GetSupportedModifiers( drmFormat ); +- assert( !modifiers.empty() ); +- possibleModifiers = modifiers.data(); +- numPossibleModifiers = modifiers.size(); ++ if ( modifiers.empty() ) ++ { ++ // The nested backend offered no modifier for this format (a game's buffer on Venus, ++ // NieR Replicant on a Pixel 9a): linear instead of the assert that ended the session. ++ vk_log.errorf( "flippable image: the backend offers no modifier for DRM format 0x%x; trying linear", drmFormat ); ++ possibleModifiers = &linear; ++ numPossibleModifiers = 1; ++ } ++ else ++ { ++ possibleModifiers = modifiers.data(); ++ numPossibleModifiers = modifiers.size(); ++ } + } + + for ( size_t i = 0; i < numPossibleModifiers; i++ ) diff --git a/tools/gamescope/patches/0935-flippable-image-linear-when-driver-rejects-all.patch b/tools/gamescope/patches/0935-flippable-image-linear-when-driver-rejects-all.patch new file mode 100644 index 000000000..dd26e94d3 --- /dev/null +++ b/tools/gamescope/patches/0935-flippable-image-linear-when-driver-rejects-all.patch @@ -0,0 +1,38 @@ +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -2373,6 +2373,7 @@ + + std::vector modifiers = {}; + std::vector unexportable = {}; ++ bool linearRejected = false; + // TODO(JoshA): Move this code to backend for making flippable image. + if ( GetBackend()->UsesModifiers() && flags.bFlippable && g_device.supportsModifiers() && !pDMA ) + { +@@ -2414,7 +2415,10 @@ + }; + res = getModifierProps( &imageInfo, modifier, &externalFormatProps ); + if ( res == VK_ERROR_FORMAT_NOT_SUPPORTED ) ++ { ++ linearRejected |= modifier == DRM_FORMAT_MOD_LINEAR; + continue; ++ } + else if ( res != VK_SUCCESS ) { + vk_errorf( res, "getModifierProps failed" ); + return false; +@@ -2440,6 +2444,16 @@ + drmFormat, unexportable.size() ); + modifiers = unexportable; + } ++ // An r32p1 Mali driver (G610, Vulkan 1.1) behind the Mali bridge answers every modifier query ++ // with VK_ERROR_FORMAT_NOT_SUPPORTED, yet the bridge makes such an image without the handle type ++ // and exports it through a dma-heap. Linear, when the backend offered it, is tried rather than ++ // giving up; if the image cannot be made, BInit fails as before (Xiaomi 13T, AR24). ++ if ( modifiers.empty() && linearRejected ) ++ { ++ vk_log.errorf( "flippable image: the driver supports none of the %zu modifier(s) offered for DRM format 0x%x; trying linear anyway", ++ numPossibleModifiers, drmFormat ); ++ modifiers.push_back( DRM_FORMAT_MOD_LINEAR ); ++ } + if ( modifiers.empty() ) + { + vk_log.errorf( "flippable image: no usable modifier for DRM format 0x%x (%zu offered by the backend)", drmFormat, numPossibleModifiers ); diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index a5633d960..a8b4653dc 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,3 +1,3 @@ -GAMESCOPE_REPO=Droid-Deck/DroidDeck-Components -GAMESCOPE_TAG=gamescope-3.16.29-p21 -GAMESCOPE_SHA256=c34d2728eb31a5ded35e5cb874e68dd4932a7fba4bcce5f99edf654108f97018 +GAMESCOPE_REPO=Droid-Deck/DroidDeck +GAMESCOPE_TAG=gamescope-3.16.29-pr2029-22b50d182 +GAMESCOPE_SHA256=d6bf0fe2ebb98e1fc7c9a09fccce7d7916c9cc6a10016018040cda01a940b781 diff --git a/tools/gamescope/tests/nested-swapchain.cpp b/tools/gamescope/tests/nested-swapchain.cpp new file mode 100644 index 000000000..a2054aa74 --- /dev/null +++ b/tools/gamescope/tests/nested-swapchain.cpp @@ -0,0 +1,156 @@ +#include "nested_swapchain.hpp" +#include "nested_surface_format.hpp" +#include "dmabuf_import.hpp" +#include "sdl_refresh.hpp" +#include +#include + +int main() +{ + using gamescope::ChooseSDLRefresh; + constexpr int configured60 = 60'000; + constexpr int desktop120 = 120'000; + auto capped = ChooseSDLRefresh(configured60, desktop120); + assert(capped.configured && capped.refresh == configured60); + + auto discovered = ChooseSDLRefresh(0, desktop120); + assert(!discovered.configured && discovered.refresh == desktop120); + + auto configured120 = ChooseSDLRefresh(120'000, 60'000); + assert(configured120.configured && configured120.refresh == 120'000); + + VkSurfaceCapabilitiesKHR caps = {}; + caps.minImageCount = 2; + caps.maxImageCount = 3; + caps.currentExtent = {UINT32_MAX, UINT32_MAX}; + caps.minImageExtent = {16, 16}; + caps.maxImageExtent = {4096, 4096}; + caps.supportedCompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + caps.supportedUsageFlags = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + gamescope::NestedSwapchainConfig config; + + // Existing desktop drivers retain direct composition into mutable swapchains. + assert(gamescope::ChooseNestedSwapchainConfig(caps, true, 1280, 720, config)); + assert(!config.copy && config.imageCount == 3 && config.extent.width == 1280); + + // No mutable formats: compose offscreen and request only a copy destination. + assert(gamescope::ChooseNestedSwapchainConfig(caps, false, 1280, 720, config)); + assert(config.copy && config.usage == VK_IMAGE_USAGE_TRANSFER_DST_BIT); + + // The Android bridge exposes transfer usage and inherited alpha, but no storage. + caps.supportedUsageFlags &= ~VK_IMAGE_USAGE_STORAGE_BIT; + caps.supportedCompositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; + caps.maxImageCount = 2; + caps.currentExtent = {1602, 720}; + assert(gamescope::ChooseNestedSwapchainConfig(caps, true, 1920, 1080, config)); + assert(config.copy && config.alpha == VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR); + assert(config.imageCount == 2 && config.extent.width == 1602); + + // Fail rather than submitting unsupported image usage or alpha. + caps.supportedUsageFlags &= ~VK_IMAGE_USAGE_TRANSFER_DST_BIT; + assert(!gamescope::ChooseNestedSwapchainConfig(caps, true, 1280, 720, config)); + caps.supportedUsageFlags |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + caps.supportedCompositeAlpha = 0; + assert(!gamescope::ChooseNestedSwapchainConfig(caps, false, 1280, 720, config)); + caps.supportedCompositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; + + caps.currentExtent = {0, 0}; + assert(!gamescope::ChooseNestedSwapchainConfig(caps, false, 1280, 720, config)); + caps.currentExtent = {UINT32_MAX, UINT32_MAX}; + assert(gamescope::ChooseNestedSwapchainConfig(caps, false, 1, 9000, config)); + assert(config.extent.width == 16 && config.extent.height == 4096); + caps.maxImageCount = 1; + assert(!gamescope::ChooseNestedSwapchainConfig(caps, false, 1280, 720, config)); + + using gamescope::ChooseNestedSDRCopySurfaceFormat; + const VkSurfaceFormatKHR rgba = { + VK_FORMAT_R8G8B8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + const VkSurfaceFormatKHR bgra = { + VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + uint32_t surfaceFormat = UINT32_MAX; + + // PowerVR SDR copy output needs the RGBA view matching the rgba8 shader. + const VkSurfaceFormatKHR bgraFirst[] = {bgra, rgba, rgba}; + assert(ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + bgraFirst, 3, surfaceFormat)); + assert(surfaceFormat == 1); + const VkSurfaceFormatKHR rgbaFirst[] = {rgba, bgra}; + assert(ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + rgbaFirst, 2, surfaceFormat)); + assert(surfaceFormat == 0); + assert(ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + &rgba, 1, surfaceFormat)); + assert(surfaceFormat == 0); + + // Reusing a successful index must not hide a missing compatible format. + assert(!ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + &bgra, 1, surfaceFormat)); + assert(surfaceFormat == 1); + const VkSurfaceFormatKHR wrongColorSpace = { + VK_FORMAT_R8G8B8A8_UNORM, VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT}; + assert(!ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + &wrongColorSpace, 1, surfaceFormat)); + assert(surfaceFormat == 1); + const VkSurfaceFormatKHR srgbFormat = { + VK_FORMAT_R8G8B8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}; + assert(!ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + &srgbFormat, 1, surfaceFormat)); + assert(surfaceFormat == 1); + const VkSurfaceFormatKHR compatibleLast[] = {wrongColorSpace, srgbFormat, rgba}; + assert(ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + compatibleLast, 3, surfaceFormat)); + assert(surfaceFormat == 2); + assert(!ChooseNestedSDRCopySurfaceFormat(0x1010, true, false, + nullptr, 0, surfaceFormat)); + assert(surfaceFormat == 0); + + // The existing selector still owns HDR, direct output, and other vendors. + // Each call clears a stale index even when BGRA is the only advertised format. + surfaceFormat = 0; + assert(ChooseNestedSDRCopySurfaceFormat(0x1010, true, true, + &bgra, 1, surfaceFormat)); + assert(surfaceFormat == 1); + surfaceFormat = 0; + assert(ChooseNestedSDRCopySurfaceFormat(0x1010, false, false, + &bgra, 1, surfaceFormat)); + assert(surfaceFormat == 1); + surfaceFormat = 0; + assert(ChooseNestedSDRCopySurfaceFormat(0x5143, true, false, + &bgra, 1, surfaceFormat)); + assert(surfaceFormat == 1); + + // Retry only the device/format/layout/flag combination reproduced on PowerVR. + using gamescope::RetryPowerVRLinearQuery; + assert(RetryPowerVRLinearQuery(0x1010, VK_FORMAT_B8G8R8A8_UNORM, 0, 1, + VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_ERROR_FORMAT_NOT_SUPPORTED)); + assert(RetryPowerVRLinearQuery(0x1010, VK_FORMAT_R8G8B8A8_UNORM, 0, 1, + VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_ERROR_FORMAT_NOT_SUPPORTED)); + assert(!RetryPowerVRLinearQuery(0x5143, VK_FORMAT_B8G8R8A8_UNORM, 0, 1, + VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_ERROR_FORMAT_NOT_SUPPORTED)); + assert(!RetryPowerVRLinearQuery(0x1010, VK_FORMAT_B8G8R8A8_UNORM, 1, 1, + VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_ERROR_FORMAT_NOT_SUPPORTED)); + assert(!RetryPowerVRLinearQuery(0x1010, VK_FORMAT_B8G8R8A8_UNORM, 0, 2, + VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_ERROR_FORMAT_NOT_SUPPORTED)); + assert(!RetryPowerVRLinearQuery(0x1010, VK_FORMAT_R16G16B16A16_SFLOAT, 0, 1, + VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_ERROR_FORMAT_NOT_SUPPORTED)); + assert(!RetryPowerVRLinearQuery(0x1010, VK_FORMAT_B8G8R8A8_UNORM, 0, 1, + 0, VK_ERROR_FORMAT_NOT_SUPPORTED)); + assert(!RetryPowerVRLinearQuery(0x1010, VK_FORMAT_B8G8R8A8_UNORM, 0, 1, + VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, VK_ERROR_DEVICE_LOST)); + + VkPhysicalDeviceMemoryProperties memory = {}; + memory.memoryTypeCount = 3; + memory.memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + memory.memoryTypes[1].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + memory.memoryTypes[2].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; + using gamescope::DmabufMemoryType; + assert(DmabufMemoryType(memory, 7, 6, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == 1); + assert(DmabufMemoryType(memory, 7, 4, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == 2); + assert(DmabufMemoryType(memory, 1, 6, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == UINT32_MAX); + assert(DmabufMemoryType(memory, 7, 0, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == UINT32_MAX); + assert(DmabufMemoryType(memory, 7, 4, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == UINT32_MAX); + std::puts("Gamescope presentation and DMA-BUF import tests passed"); +} diff --git a/tools/hybris/build_runtime.sh b/tools/hybris/build_runtime.sh new file mode 100755 index 000000000..53c7d9481 --- /dev/null +++ b/tools/hybris/build_runtime.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# CI-only full libhybris build, using the upstream pinned cross-toolchain recipe. +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +out=${1:?output directory required} +mkdir -p "$out" +out=$(cd "$out" && pwd) +. "$root/tools/hybris/source.env" +work=$(mktemp -d "${TMPDIR:-/tmp}/droiddeck-hybris.XXXXXX") +trap 'rm -rf "$work"' EXIT +fetch() { + git init -q "$3" + git -C "$3" fetch --depth=1 "$1" "$2" + git -C "$3" checkout -q --detach FETCH_HEAD + test "$(git -C "$3" rev-parse HEAD)" = "$2" +} +fetch https://github.com/taowen/libhybris.git "$HYBRIS_REVISION" "$work/libhybris" +fetch https://github.com/taowen/arlinux-rootfs.git "$PROTOCOL_REVISION" "$work/rootfs" +git -C "$work/libhybris" apply --check "$root/tools/hybris/powervr-hal-version.patch" +git -C "$work/libhybris" apply "$root/tools/hybris/powervr-hal-version.patch" +for patch in "$root"/tools/hybris/patches/*.patch; do + git -C "$work/libhybris" apply --check "$patch" + git -C "$work/libhybris" apply "$patch" +done +export ARLINUX_WSI_PROTOCOL_DIR="$work/rootfs/graphics-protocols" +export CONTAINER_ENGINE=podman +"$work/libhybris/tools/build-aarch64.sh" --out "$work/build" + +mkdir -p "$work/package/lib" "$work/package/licenses" +cp -a "$work/build/install/usr/lib/hybris/." "$work/package/lib/" +find "$work/package/lib" -name '*.la' -delete +rm -rf "$work/package/lib/include" "$work/package/lib/pkgconfig" "$work/package/lib/bin" +# Resolve the wrapper's private dependencies without putting its EGL/Vulkan frontends +# on the session's LD_LIBRARY_PATH. The Linux runtime keeps owning its Mesa loader. +while IFS= read -r -d '' library; do + patchelf --set-rpath '$ORIGIN:$ORIGIN/../..' "$library" +done < <(find "$work/package/lib" -type f -name '*.so*' -print0) +for library in libhybris-vulkan-icd.so.0 libhybris-common.so.1 libVkLayer_hybris_compat.so libhybris/linker/q.so; do + test -s "$work/package/lib/$library" +done +cp "$work/libhybris"/LICENSE.* "$work/package/licenses/" +cp "$work/build/manifest.json" "$work/package/build-manifest.json" +printf '%s\n' "$HYBRIS_REVISION" > "$work/package/source-commit" +# Ship the exact patched source and protocol license with the runtime. +git -C "$work/libhybris" diff > "$work/package/licenses/powervr.patch" +git -C "$work/libhybris" archive HEAD | zstd -T0 -q -o "$work/package/licenses/libhybris-source.tar.zst" +cp "$ARLINUX_WSI_PROTOCOL_DIR/wayland-android.xml" "$work/package/licenses/" +(cd "$work/package" && find lib -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS) +tar --zstd -cf "$out/runtime.tzst" -C "$work/package" . +sha256sum "$out/runtime.tzst" | cut -d' ' -f1 > "$out/version.txt" +printf 'Built libhybris %s for Android HAL access from AArch64 Linux\n' "$HYBRIS_REVISION" diff --git a/tools/hybris/low32/README.md b/tools/hybris/low32/README.md new file mode 100644 index 000000000..43f158673 --- /dev/null +++ b/tools/hybris/low32/README.md @@ -0,0 +1,99 @@ +# Opt-in device-backed low mappings for Wine WoW64 + +On the tested Pixel PowerVR stack, Vulkan maps host-visible GPU allocations +above 4 GiB. Wine's 32-bit WoW64 Vulkan bridge rejects those pointers. This +caused American Nightmare (202750) to stop at D3D9 vertex-buffer creation even +after the separate PowerVR BC texture profile passed device initialization. + +`low32.c` is an experimental native Linux/glibc preload adapter. It is off by +default. It applies only to anonymous-address, non-executable `MAP_SHARED` +mappings of one explicitly selected character device. It retains the original +device FD, offset, protection, flags, and actual GPU backing, and searches low +address space with `MAP_FIXED_NOREPLACE`. It never truncates a pointer, copies +into shadow storage, replaces existing mappings, or disables Wine's guard. +Caller-specified/fixed mappings, regular files, private/anonymous memory and +other devices retain their original behavior. Failure to find a suitable low +hole fails with ENOMEM. The mmap and mmap64 preload chains are independently +resolved and called through their respective RTLD_NEXT symbols; +only resolver bootstrap uses the raw syscall. A compact atomic cursor avoids +wasting large address slots on thousands of small driver control allocations. +If the compact probe collides, a bounded address-range walk of +/proc/self/maps finds page-sized holes and uses NOREPLACE again to handle races. +The walk runs before coarse fallback, so small allocations do not fragment +large available ranges. A game load had 2.3 GB free below 4 GiB but no hole as large as 4 MiB; the +coarse-only version falsely failed even 4 KiB mappings. Mapping failures are +reported even after the ordinary 256-event trace limit. + +## Build and verify + +Use a native 64-bit Linux/glibc compiler, not the Android NDK or macOS compiler: + +```sh +tools/hybris/low32/build.sh /absolute/build-dir /path/to/Vulkan-Headers/include +``` + +The native library and unit test are always built. GPU probes are optional when +headers are provided; `gpu-copy-native` uses the selected native host compiler +and has the host's architecture, while the Windows x86 probe also needs +i686-w64-mingw32-gcc. Copy a native probe to a device only when their architectures match. +The unit test uses /dev/zero and proves CPU data integrity, collision handling, +concurrency, 2000 simultaneous small mappings, invalid offsets, forwarding and +opt-out. A fragmented-address-space regression reserves the low region, opens +64 off-grid page holes, and verifies all 64 mappings and their data. The old +coarse adapter fails that regression; the address-range fallback passes. An +additional density regression fills 64 existing page holes while preserving a +separate contiguous 16 MiB interval for a later complete data-readback map; the +prior gap-after-coarse implementation fails and the gap-first strategy passes. These +CPU tests do not by themselves prove GPU correctness. +The build also tests both preload orders with a second library that hooks mmap +and mmap64 independently. Opt-out, tracing and adaptation must each preserve +the selected symbol's chain, invalid-offset errno and file offsets above 4 GiB. + +`gpu-copy.c` tests actual Vulkan transfer operations on vendor 0x1010. It maps +two 32 MiB device allocations, runs four copies of full deterministic patterns, +including offset/unmap/remap, uses flush/invalidate and barriers/fences, and +compares every word. It can require each full CPU range to fit below 4 GiB: + +```sh +./gpu-copy-native 32 --require-low32 +# Through a separate Proton ARM64/WoW64 test prefix: +proton run gpu-copy-x86.exe 32 --require-low32 'Z:\path\probe-output.txt' +``` + +The Windows output file avoids Wine's detached console hiding probe results. +Run with Khronos synchronization validation. Keep raw Steam/account logs local; +export only controlled PROBE results, validation IDs and counts. The tested +memory type was device-local, host-visible and coherent; noncoherent physical +memory is not covered by that run. + +## Activation scope + +Set only the target game's overrides, after identifying the render device's +`st_rdev` and validating the installed library's hash: + +```text +LD_PRELOAD=/absolute/guest/path/libdroiddeck-low32.so +DROIDDECK_LOW32_MODE=low32 +DROIDDECK_LOW32_RDEV= +``` + +Preserve any existing target-game preload rather than replacing it. The tested +Pixel's /dev/dri/renderD128 was character device major 226/minor 128, st_rdev 57984. +Do not hardcode that value for other machines. BC emulation and the repaired +DXVK presentation component remain separate prerequisites; this adapter does +not implement them. No global Steam profile, managed graphics pack, Android +security setting or unrelated game's overrides should change. + +Before activating, back up the exact game-environment bytes and only the target +prefix's graphics DLLs, preserving symlinks. Rollback must check for concurrent +edits. Keep original backups in the app; do not copy account files or an entire +Steam prefix to a report. Removing these three overrides disables the adapter. + +## Compatibility limits + +This initial binary was built on Ubuntu 24.04 ARM64 and needs glibc 2.38. The +tested guest supports it. This is not yet a universal runtime component; other +glibc versions, page sizes, drivers, GPU families, native 32-bit processes and +general game stability require their own tests. Bounded native and Windows GPU +copies prove data integrity for those workloads, not unrestricted gameplay. +Never enable globally based on successful testing of one game. diff --git a/tools/hybris/low32/build.sh b/tools/hybris/low32/build.sh new file mode 100755 index 000000000..f92307066 --- /dev/null +++ b/tools/hybris/low32/build.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu +# Run on native 64-bit Linux/glibc. This is not an Android/Bionic library. +base=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +output=${1:?usage: build.sh /absolute/build-directory [Vulkan-header-directory]} +mkdir -p "$output" +cc=${CC:-cc} +"$cc" -std=gnu11 -Wall -Wextra -Werror -O2 -fPIC -shared -s \ + "$base/low32.c" -ldl -o "$output/libdroiddeck-low32.so" +"$cc" -std=gnu11 -Wall -Wextra -Werror -O2 "$base/test-low32.c" -pthread -o "$output/test-low32" +LD_PRELOAD="$output/libdroiddeck-low32.so" "$output/test-low32" +"$cc" -std=gnu11 -Wall -Wextra -Werror -O2 -fPIC -shared \ + "$base/test-chain-hook.c" -ldl -o "$output/test-chain-hook.so" +"$cc" -std=gnu11 -Wall -Wextra -Werror -O2 "$base/test-preload-chain.c" -ldl -o "$output/test-preload-chain" +for preloads in "$output/libdroiddeck-low32.so:$output/test-chain-hook.so" \ + "$output/test-chain-hook.so:$output/libdroiddeck-low32.so"; do + for mode in off trace low32; do + LD_PRELOAD="$preloads" "$output/test-preload-chain" "$mode" + done +done +if [ "$#" -ge 2 ]; then + "$cc" -std=gnu11 -Wall -Wextra -Werror -O2 -s -I"$2" "$base/gpu-copy.c" -ldl -o "$output/gpu-copy-native" + if command -v i686-w64-mingw32-gcc >/dev/null 2>&1; then + i686-w64-mingw32-gcc -std=gnu11 -Wall -Wextra -Werror -O2 -s -I"$2" \ + "$base/gpu-copy.c" -o "$output/gpu-copy-x86.exe" + fi +fi diff --git a/tools/hybris/low32/gpu-copy.c b/tools/hybris/low32/gpu-copy.c new file mode 100644 index 000000000..d59dbf677 --- /dev/null +++ b/tools/hybris/low32/gpu-copy.c @@ -0,0 +1,166 @@ +/* Same standalone GPU-copy probe built as Linux AArch64 and Windows x86. + * Tests actual Vulkan buffers, complete data comparison, noncoherent cache + * operations, remapping with an offset and queue completion. No Steam API. + */ +#define VK_NO_PROTOTYPES +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include + +#define IFNS(X) X(DestroyInstance) X(EnumeratePhysicalDevices) X(GetPhysicalDeviceProperties) \ + X(GetPhysicalDeviceMemoryProperties) X(GetPhysicalDeviceQueueFamilyProperties) X(CreateDevice) X(GetDeviceProcAddr) +#define DFNS(X) X(DestroyDevice) X(GetDeviceQueue) X(CreateBuffer) X(DestroyBuffer) \ + X(GetBufferMemoryRequirements) X(AllocateMemory) X(FreeMemory) X(BindBufferMemory) \ + X(MapMemory) X(UnmapMemory) X(FlushMappedMemoryRanges) X(InvalidateMappedMemoryRanges) \ + X(CreateCommandPool) X(DestroyCommandPool) X(AllocateCommandBuffers) X(ResetCommandBuffer) \ + X(BeginCommandBuffer) X(EndCommandBuffer) X(CmdCopyBuffer) X(CmdPipelineBarrier) \ + X(CreateFence) X(DestroyFence) X(ResetFences) X(QueueSubmit) X(WaitForFences) +#define DECL(n) static PFN_vk##n p##n; +IFNS(DECL) +DFNS(DECL) +#undef DECL +#define CHECK(x) do { VkResult rc=(x); if(rc!=VK_SUCCESS){printf("PROBE_FAIL stage=%s vk_result=%d\n",#x,rc);goto cleanup;} }while(0) +#define REQUIRE(x,msg) do {if(!(x)){puts("PROBE_FAIL " msg);goto cleanup;}}while(0) + +int main(int argc,char **argv) +{ +#ifdef _WIN32 + /* Wine/Proton can detach the Windows console from the Linux pipe. */ + if(argc>3 && !freopen(argv[3],"w",stdout)) return 4; +#endif + setvbuf(stdout,NULL,_IONBF,0); + puts("PROBE_STAGE main"); + unsigned mib=argc>1?(unsigned)strtoul(argv[1],NULL,10):32; + int require_low=argc>2&&!strcmp(argv[2],"--require-low32"); + if(mib<1||mib>256)return 2; + const VkDeviceSize bytes=(VkDeviceSize)mib*1024*1024; + VkInstance instance=VK_NULL_HANDLE;VkDevice device=VK_NULL_HANDLE; + VkBuffer buffers[2]={0};VkDeviceMemory memories[2]={0}; + VkDeviceSize allocated[2]={0};void *mapped[2]={0}; + VkCommandPool pool=VK_NULL_HANDLE;VkFence fence=VK_NULL_HANDLE; + int success=0,pending=0;uint64_t mismatches=0;unsigned iterations=0; + int low_all=1;uint32_t memory_flags[2]={0}; + PFN_vkGetInstanceProcAddr gipa; +#ifdef _WIN32 + HMODULE loader=LoadLibraryA("vulkan-1.dll"); + REQUIRE(loader,"load_vulkan_failed"); + gipa=(PFN_vkGetInstanceProcAddr)(void *)GetProcAddress(loader,"vkGetInstanceProcAddr"); +#else + void *loader=dlopen("libvulkan.so.1",RTLD_NOW|RTLD_LOCAL); + REQUIRE(loader,"load_vulkan_failed"); + gipa=(PFN_vkGetInstanceProcAddr)dlsym(loader,"vkGetInstanceProcAddr"); +#endif + REQUIRE(gipa,"missing_gipa"); + puts("PROBE_STAGE vulkan_loader"); + PFN_vkCreateInstance create=(PFN_vkCreateInstance)gipa(NULL,"vkCreateInstance"); + REQUIRE(create,"missing_create_instance"); + VkApplicationInfo app={.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pApplicationName="DroidDeck low32 buffer integrity probe",.apiVersion=VK_API_VERSION_1_1}; + VkInstanceCreateInfo ici={.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,.pApplicationInfo=&app}; + CHECK(create(&ici,NULL,&instance)); + puts("PROBE_STAGE instance_created"); +#define LOADI(n) p##n=(PFN_vk##n)gipa(instance,"vk" #n);REQUIRE(p##n,"missing_" #n); + IFNS(LOADI) +#undef LOADI + uint32_t count=0;CHECK(pEnumeratePhysicalDevices(instance,&count,NULL)); + puts("PROBE_STAGE physical_device_enumeration"); + REQUIRE(count&&count<=16,"physical_device_count"); + VkPhysicalDevice physicals[16],physical=VK_NULL_HANDLE; + CHECK(pEnumeratePhysicalDevices(instance,&count,physicals)); + VkPhysicalDeviceProperties properties; + for(uint32_t i=0;i +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef MAP_FIXED_NOREPLACE +#define MAP_FIXED_NOREPLACE 0x100000 +#endif + +static _Atomic uint64_t cursor; +static _Atomic unsigned logged; +typedef void *(*map_function)(void *,size_t,int,int,int,off_t); +typedef void *(*map64_function)(void *,size_t,int,int,int,off64_t); +static _Atomic(map_function) next_map; +static _Atomic(map64_function) next_map64; +_Static_assert(sizeof(off_t) == sizeof(off64_t) && sizeof(off_t) == 8, + "64-bit host required"); +static __thread int resolving __attribute__((tls_model("initial-exec"))); +static __thread int active __attribute__((tls_model("initial-exec"))); +static void *native_map(void *addr, size_t length, int prot, int flags, int fd, off_t offset) +{ + map_function next=atomic_load(&next_map); + if (!next && !resolving) { + resolving=1; + next=(map_function)dlsym(RTLD_NEXT,"mmap"); + atomic_store(&next_map,next); + resolving=0; + } + /* Bootstrap only while resolving; otherwise preserve existing preload hooks. */ + if (next) return next(addr,length,prot,flags,fd,offset); + return (void *)syscall(SYS_mmap, addr, length, prot, flags, fd, offset); +} + +static void *native_map64(void *addr, size_t length, int prot, int flags, int fd, off_t offset) +{ + map64_function next=atomic_load(&next_map64); + if (!next && !resolving) { + resolving=1; + next=(map64_function)dlsym(RTLD_NEXT,"mmap64"); + atomic_store(&next_map64,next); + resolving=0; + } + if (next) return next(addr,length,prot,flags,fd,(off64_t)offset); + return (void *)syscall(SYS_mmap, addr, length, prot, flags, fd, offset); +} + +static void trace_map(const char *kind, size_t n, int prot, int flags, int fd, + off_t off, const struct stat *st, void *p, int error) +{ + int saved = errno; + unsigned count = atomic_fetch_add(&logged, 1); + if (count < 256 || error != 0) { + char text[320]; + int nbytes = snprintf(text, sizeof(text), + "DD_LOW32 %s pid=%ld len=%zu prot=%x flags=%x fd=%d off=%jd mode=%o rdev=%ju ptr=%p errno=%d\n", + kind, (long)getpid(), n, prot, flags, fd, (intmax_t)off, st ? st->st_mode : 0, + st ? (uintmax_t)st->st_rdev : 0, p, error); + if (nbytes > 0) { + ssize_t written = write(STDERR_FILENO, text, + (size_t)nbytes < sizeof(text) ? (size_t)nbytes : sizeof(text)-1); + (void)written; + } + } + errno = saved; +} + +static int target_device(int fd, struct stat *st) +{ + const char *value = getenv("DROIDDECK_LOW32_RDEV"); + char *end = NULL; + if (!value || !*value || fd < 0) return 0; + int saved = errno; + errno = 0; + uintmax_t expected = strtoumax(value, &end, 10); + int valid = !errno && end && !*end && expected > 0 && + fstat(fd, st) == 0 && S_ISCHR(st->st_mode) && + (uintmax_t)st->st_rdev == expected; + errno = saved; + return valid; +} + +static void *adapted_map(map_function forward, void *addr, size_t length, int prot, + int flags, int fd, off_t offset) +{ + const char *mode = getenv("DROIDDECK_LOW32_MODE"); + struct stat st = {0}; + if (mode && !strcmp(mode, "trace")) { + void *p = forward(addr, length, prot, flags, fd, offset); + if (fd >= 0 && (prot & PROT_WRITE) && !(prot & PROT_EXEC) && + (flags & MAP_TYPE) == MAP_SHARED) { + int saved=errno; + (void)fstat(fd,&st); + errno=saved; + trace_map("trace", length, prot, flags, fd, offset, &st, p, + p == MAP_FAILED ? errno : 0); + } + return p; + } + if (!mode || strcmp(mode, "low32") || addr || !length || + (flags & (MAP_ANONYMOUS | MAP_FIXED | MAP_FIXED_NOREPLACE)) || + (prot & PROT_EXEC) || (flags & MAP_TYPE) != MAP_SHARED || + !target_device(fd, &st)) + return forward(addr, length, prot, flags, fd, offset); + + const uintptr_t lower = 0x10000000u, upper = 0xffff0000u; + const uintptr_t step = 0x00400000u; + if (length > upper-lower) { + errno = ENOMEM; + trace_map("too_large", length, prot, flags, fd, offset, &st, MAP_FAILED, errno); + return MAP_FAILED; + } + long page_value=sysconf(_SC_PAGESIZE); + uintptr_t page=page_value>0?(uintptr_t)page_value:4096; + uintptr_t rounded=(length+page-1)&~(page-1); + uintptr_t span=upper-lower; + uintptr_t compact=(uintptr_t)(atomic_fetch_add(&cursor,rounded)%span); + if (rounded>span-compact) compact=0; + unsigned slots = (unsigned)((span-length)/step)+1; + unsigned start = (unsigned)(compact/step)%slots; + int original_errno = errno; + for (unsigned i = 0; i < 1; i++) { + /* Compact first: hundreds of small driver mappings must not waste a + * 4MiB address slot each. Coarse search is only for existing Wine holes. */ + uintptr_t hint = lower + (i==0?compact:(uintptr_t)((start+i-1)%slots)*step); + void *p = forward((void *)hint, length, prot, + flags|MAP_FIXED_NOREPLACE, fd, offset); + if (p != MAP_FAILED) { + uintptr_t first = (uintptr_t)p; + if (first >= lower && first < upper && length <= upper-first) { + errno = original_errno; + trace_map("mapped", length, prot, flags, fd, offset, &st, p, 0); + return p; + } + /* Older kernels can ignore NOREPLACE; never return a high mapping. */ + int saved = errno; + (void)syscall(SYS_munmap, p, length); + errno = saved; + } else if (errno != EEXIST && errno != ENOMEM && errno != EAGAIN) { + trace_map("rejected", length, prot, flags, fd, offset, &st, p, errno); + return p; + } + } + /* Pack small allocations into address holes before coarse probing can + * fragment large holes. Walk + * this process's current mappings, then use NOREPLACE to tolerate races. + * No pointer paths or contents are read; only address ranges are used. */ + for (unsigned pass = 0; pass < 2; ++pass) { + FILE *maps = fopen("/proc/self/maps", "r"); + if (!maps) break; + char line[512]; + uintptr_t gap_start = lower; + unsigned lines = 0; + while (fgets(line, sizeof(line), maps) && ++lines < 65536) { + uintptr_t first, last; + if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR, &first, &last) != 2) continue; + if (last <= gap_start) continue; + uintptr_t gap_end = first < upper ? first : upper; + if (gap_end > gap_start && rounded <= gap_end - gap_start) { + void *p = forward((void *)gap_start, length, prot, + flags | MAP_FIXED_NOREPLACE, fd, offset); + if (p != MAP_FAILED) { + uintptr_t address = (uintptr_t)p; + if (address >= lower && address < upper && length <= upper - address) { + fclose(maps); + errno = original_errno; + trace_map("mapped_gap", length, prot, flags, fd, offset, &st, p, 0); + return p; + } + (void)syscall(SYS_munmap, p, length); + } else if (errno != EEXIST && errno != ENOMEM && errno != EAGAIN) { + int saved = errno; + fclose(maps); + errno = saved; + trace_map("rejected", length, prot, flags, fd, offset, &st, MAP_FAILED, errno); + return MAP_FAILED; + } + } + if (last > gap_start) gap_start = last; + if (gap_start >= upper) break; + } + if (gap_start < upper && rounded <= upper - gap_start) { + void *p = forward((void *)gap_start, length, prot, + flags | MAP_FIXED_NOREPLACE, fd, offset); + if (p != MAP_FAILED) { + uintptr_t address = (uintptr_t)p; + if (address >= lower && address < upper && length <= upper - address) { + fclose(maps); + errno = original_errno; + trace_map("mapped_gap", length, prot, flags, fd, offset, &st, p, 0); + return p; + } + (void)syscall(SYS_munmap, p, length); + } + } + fclose(maps); + } + for (unsigned i = 1; i <= slots; i++) { + /* Last resort if address walking was unavailable or raced. */ + uintptr_t hint = lower + (i==0?compact:(uintptr_t)((start+i-1)%slots)*step); + void *p = forward((void *)hint, length, prot, + flags|MAP_FIXED_NOREPLACE, fd, offset); + if (p != MAP_FAILED) { + uintptr_t first = (uintptr_t)p; + if (first >= lower && first < upper && length <= upper-first) { + errno = original_errno; + trace_map("mapped", length, prot, flags, fd, offset, &st, p, 0); + return p; + } + /* Older kernels can ignore NOREPLACE; never return a high mapping. */ + int saved = errno; + (void)syscall(SYS_munmap, p, length); + errno = saved; + } else if (errno != EEXIST && errno != ENOMEM && errno != EAGAIN) { + trace_map("rejected", length, prot, flags, fd, offset, &st, p, errno); + return p; + } + } + errno = ENOMEM; + trace_map("no_low_hole", length, prot, flags, fd, offset, &st, MAP_FAILED, errno); + return MAP_FAILED; +} + +static void *intercept_map(map_function forward, void *addr, size_t length, + int prot, int flags, int fd, off_t offset) +{ + if (active) return forward(addr,length,prot,flags,fd,offset); + active=1; + void *p=adapted_map(forward,addr,length,prot,flags,fd,offset); + active=0; + return p; +} + +void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) +{ + return intercept_map(native_map,addr,length,prot,flags,fd,offset); +} + +void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off64_t offset) +{ + return intercept_map(native_map64,addr,length,prot,flags,fd,(off_t)offset); +} diff --git a/tools/hybris/low32/test-chain-hook.c b/tools/hybris/low32/test-chain-hook.c new file mode 100644 index 000000000..b5e68a1e6 --- /dev/null +++ b/tools/hybris/low32/test-chain-hook.c @@ -0,0 +1,37 @@ +/* Controlled second preload: distinct mmap/mmap64 counters and 64-bit offsets. */ +#define _GNU_SOURCE +#define _LARGEFILE64_SOURCE +#include +#include +#include +#include + +static int selected_fd = -2; +static unsigned calls[2]; +static int64_t offsets[2]; + +void chain_clear(int fd) +{ + selected_fd = fd; + calls[0] = calls[1] = 0; + offsets[0] = offsets[1] = -1; +} + +unsigned chain_calls(unsigned channel) { return calls[channel]; } +int64_t chain_offset(unsigned channel) { return offsets[channel]; } + +void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off) +{ + static void *(*next)(void *, size_t, int, int, int, off_t); + if (!next) next = dlsym(RTLD_NEXT, "mmap"); + if (fd == selected_fd) { ++calls[0]; offsets[0] = off; } + return next(addr, len, prot, flags, fd, off); +} + +void *mmap64(void *addr, size_t len, int prot, int flags, int fd, off64_t off) +{ + static void *(*next)(void *, size_t, int, int, int, off64_t); + if (!next) next = dlsym(RTLD_NEXT, "mmap64"); + if (fd == selected_fd) { ++calls[1]; offsets[1] = off; } + return next(addr, len, prot, flags, fd, off); +} diff --git a/tools/hybris/low32/test-low32.c b/tools/hybris/low32/test-low32.c new file mode 100644 index 000000000..88c2b7130 --- /dev/null +++ b/tools/hybris/low32/test-low32.c @@ -0,0 +1,119 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define N (32u*1024u*1024u) +static int zero_fd; +static void *thread_test(void *arg) +{ + unsigned seed = (unsigned)(uintptr_t)arg; + uint32_t *p = mmap(NULL,N,PROT_READ|PROT_WRITE,MAP_SHARED,zero_fd,0); + assert(p != MAP_FAILED && (uintptr_t)p+N <= 0x100000000ull); + for (size_t i=0;i=0); + struct stat st; assert(!fstat(zero_fd,&st)); char value[48]; + snprintf(value,sizeof(value),"%ju",(uintmax_t)st.st_rdev); + assert(!setenv("DROIDDECK_LOW32_RDEV",value,1)); + assert(!setenv("DROIDDECK_LOW32_MODE","low32",1)); + /* Existing low-address memory must survive device mappings. */ + uint32_t *guard=mmap((void *)0x10000000,128u*1024u*1024u,PROT_READ|PROT_WRITE, + MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED_NOREPLACE,-1,0); + assert(guard==(void *)0x10000000);guard[0]=0xdeadbeef;guard[32767]=0xcafef00d; + pthread_t threads[8]; + for (uintptr_t i=0;i<8;i++) assert(!pthread_create(&threads[i],NULL,thread_test,(void *)i)); + for (unsigned i=0;i<8;i++) assert(!pthread_join(threads[i],NULL)); + assert(guard[0]==0xdeadbeef && guard[32767]==0xcafef00d); + puts("PASS concurrent 32MiB mappings, full pattern readback and occupied-memory preservation"); + assert(!munmap(guard,128u*1024u*1024u)); + /* Caller-specified address and MAP_FIXED semantics are preserved. */ + void *explicit=mmap((void *)0x30000000,4096,PROT_READ|PROT_WRITE, + MAP_SHARED|MAP_FIXED_NOREPLACE,zero_fd,0); + assert(explicit==(void *)0x30000000); assert(!munmap(explicit,4096)); + int mem=memfd_create("low32-nontarget",0);assert(mem>=0);assert(!ftruncate(mem,N)); + uint32_t *q=mmap((void *)0x500000000ull,N,PROT_READ|PROT_WRITE,MAP_SHARED,mem,0); + assert(q==(void *)0x500000000ull);q[0]=123;assert(q[0]==123);assert(!munmap(q,N));close(mem); + puts("PASS unrelated regular-file mapping and explicit-address forwarding"); + errno=0; + void *bad=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,zero_fd,1); + assert(bad==MAP_FAILED && errno==EINVAL); + puts("PASS invalid device offset retains EINVAL"); + /* Real game initialization creates many small control/buffer mappings. */ + void *small[2000]; + for(unsigned i=0;i<2000;i++) { + small[i]=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,zero_fd,0); + assert(small[i]!=MAP_FAILED && (uintptr_t)small[i]+4096<=0x100000000ull); + *(uint32_t *)small[i]=i; + } + for(unsigned i=0;i<2000;i++){ + assert(*(uint32_t *)small[i]==i);assert(!munmap(small[i],4096)); + } + puts("PASS 2000 simultaneous small mappings without address-slot exhaustion"); + /* A fragmented Wine address space has usable holes away from every + * 4MiB probe location. The mapping walk must find them without clobbering. */ + const uintptr_t lo=0x10000000u, hi=0xffff0000u; + void *reservation=mmap((void *)lo,hi-lo,PROT_NONE, + MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE|MAP_FIXED_NOREPLACE,-1,0); + assert(reservation==(void *)lo); + void *holes[64], *fragmented[64]; + for (unsigned i=0;i<64;i++) { + holes[i]=(void *)(lo+0x12345000u+(uintptr_t)i*0x10000u); + assert(!munmap(holes[i],4096)); + } + for (unsigned i=0;i<64;i++) { + fragmented[i]=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,zero_fd,0); + assert(fragmented[i]!=MAP_FAILED); + int found=0;for(unsigned j=0;j<64;j++)found|=fragmented[i]==holes[j];assert(found); + *(uint32_t *)fragmented[i]=i+77; + } + for (unsigned i=0;i<64;i++) assert(*(uint32_t *)fragmented[i]==i+77); + for (unsigned i=0;i<64;i++) assert(!munmap(fragmented[i],4096)); + assert(!munmap(reservation,hi-lo)); + puts("PASS fragmented low32 mappings find 64 page holes beyond coarse probes"); + /* Small allocations must consume existing page holes rather than punch + * coarse-grid holes into the only large interval needed by a later map. */ + reservation=mmap((void *)lo,hi-lo,PROT_NONE, + MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE|MAP_FIXED_NOREPLACE,-1,0); + assert(reservation==(void *)lo); + for (unsigned i=0;i<64;i++) assert(!munmap(holes[i],4096)); + const uintptr_t large_address=0x70000000u; + const size_t large_size=16u*1024u*1024u; + assert(!munmap((void *)large_address,large_size)); + for (unsigned i=0;i<64;i++) { + fragmented[i]=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,zero_fd,0); + assert(fragmented[i]!=MAP_FAILED); + int found=0;for(unsigned j=0;j<64;j++)found|=fragmented[i]==holes[j];assert(found); + *(uint32_t *)fragmented[i]=i+99; + } + uint32_t *large=mmap(NULL,large_size,PROT_READ|PROT_WRITE,MAP_SHARED,zero_fd,0); + assert(large==(void *)large_address); + for(size_t i=0;i +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + assert(argc == 2); + void (*clear)(int) = dlsym(RTLD_DEFAULT, "chain_clear"); + unsigned (*calls)(unsigned) = dlsym(RTLD_DEFAULT, "chain_calls"); + int64_t (*offset)(unsigned) = dlsym(RTLD_DEFAULT, "chain_offset"); + assert(clear && calls && offset); + if (strcmp(argv[1], "off")) assert(!setenv("DROIDDECK_LOW32_MODE", argv[1], 1)); + else assert(!unsetenv("DROIDDECK_LOW32_MODE")); + int fd = open("/dev/zero", O_RDWR | O_CLOEXEC); + assert(fd >= 0); + struct stat st; + assert(!fstat(fd, &st)); + char rdev[48]; + snprintf(rdev, sizeof(rdev), "%ju", (uintmax_t)st.st_rdev); + assert(!setenv("DROIDDECK_LOW32_RDEV", rdev, 1)); + for (unsigned channel = 0; channel < 2; ++channel) { + clear(fd); + void *p = channel ? mmap64(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0) + : mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + assert(p != MAP_FAILED); + assert(calls(channel) == 1 && calls(1 - channel) == 0 && offset(channel) == 0); + if (!strcmp(argv[1], "low32")) assert((uintptr_t)p + 4096 <= UINT64_C(0x100000000)); + *(unsigned *)p = 0xcafef00d; + assert(*(unsigned *)p == 0xcafef00d && !munmap(p, 4096)); + clear(fd); + errno = 0; + p = channel ? mmap64(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 1) + : mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 1); + assert(p == MAP_FAILED && errno == EINVAL); + assert(calls(channel) == 1 && calls(1 - channel) == 0 && offset(channel) == 1); + } + close(fd); + /* Regular-file forwarding must preserve an mmap64 offset above 4 GiB. */ + fd = memfd_create("preload-chain-test", 0); + const off64_t large_offset = INT64_C(0x200001000); + assert(fd >= 0 && !ftruncate64(fd, large_offset + 4096)); + unsigned value = 0x1234abcd; + assert(pwrite64(fd, &value, sizeof(value), large_offset) == (ssize_t)sizeof(value)); + clear(fd); + unsigned *p = mmap64(NULL, 4096, PROT_READ, MAP_SHARED, fd, large_offset); + assert(p != MAP_FAILED && *p == value); + assert(calls(1) == 1 && calls(0) == 0 && offset(1) == large_offset); + assert(!munmap(p, 4096)); + close(fd); + printf("PASS separate mmap/mmap64 chains (%s), EINVAL and >4GiB offset readback\n", argv[1]); + return 0; +} diff --git a/tools/hybris/patches/0001-dxvk-bc-profile.patch b/tools/hybris/patches/0001-dxvk-bc-profile.patch new file mode 100644 index 000000000..0076f53f8 --- /dev/null +++ b/tools/hybris/patches/0001-dxvk-bc-profile.patch @@ -0,0 +1,505 @@ +diff --git a/hybris/vulkan/compat/bc_commands.c b/hybris/vulkan/compat/bc_commands.c +index 90dac59..cf62d7d 100644 +--- a/hybris/vulkan/compat/bc_commands.c ++++ b/hybris/vulkan/compat/bc_commands.c +@@ -2,6 +2,7 @@ + #define VK_NO_PROTOTYPES + #include "bc_context.h" + #include ++#include + + #define PROC(name) PFN_vk##name name = (PFN_vk##name)device->resolver(device->handle, "vk" #name) + static VkResult VKAPI_CALL allocate_commands(VkDevice handle, const VkCommandBufferAllocateInfo *info, +@@ -76,6 +77,7 @@ static VkResult VKAPI_CALL end_command(VkCommandBuffer handle) + PROC(EndCommandBuffer); + VkResult result = EndCommandBuffer(handle); + VkResult recorded = command->state.error; ++ if (result != VK_SUCCESS || recorded != VK_SUCCESS) fprintf(stderr, "HYBRIS_BC end: native=%d recorded=%d\n", result, recorded); + if (recorded == VK_SUCCESS) return result; + if (recorded == VK_ERROR_OUT_OF_HOST_MEMORY || recorded == VK_ERROR_OUT_OF_DEVICE_MEMORY || + recorded == VK_ERROR_DEVICE_LOST) return recorded; +@@ -198,6 +200,37 @@ static void VKAPI_CALL push_constants(VkCommandBuffer handle, VkPipelineLayout l + CmdPushConstants(handle, layout, stages, offset, size, values); + if (device->format_mask) hybris_bc_save_push(&command->state, layout, stages, offset, size, values); + } ++static void bind_descriptors2(VkCommandBuffer handle, const VkBindDescriptorSetsInfo *info, const char *name) ++{ ++ struct hybris_bc_device *device; ++ struct hybris_bc_command *command = hybris_bc_command_find(handle, &device); ++ if (!command) return; ++ PFN_vkCmdBindDescriptorSets2 call = (void *)device->resolver(device->handle, name); ++ call(handle, info); ++ if (device->format_mask && (info->stageFlags & VK_SHADER_STAGE_COMPUTE_BIT)) ++ hybris_bc_save_descriptors(&command->state, VK_PIPELINE_BIND_POINT_COMPUTE, ++ info->layout, info->firstSet, info->descriptorSetCount, info->pDescriptorSets, ++ info->dynamicOffsetCount, info->pDynamicOffsets); ++} ++static void VKAPI_CALL bind_descriptors2_core(VkCommandBuffer handle, const VkBindDescriptorSetsInfo *info) ++{ bind_descriptors2(handle, info, "vkCmdBindDescriptorSets2"); } ++static void VKAPI_CALL bind_descriptors2_khr(VkCommandBuffer handle, const VkBindDescriptorSetsInfo *info) ++{ bind_descriptors2(handle, info, "vkCmdBindDescriptorSets2KHR"); } ++static void push_constants2(VkCommandBuffer handle, const VkPushConstantsInfo *info, const char *name) ++{ ++ struct hybris_bc_device *device; ++ struct hybris_bc_command *command = hybris_bc_command_find(handle, &device); ++ if (!command) return; ++ PFN_vkCmdPushConstants2 call = (void *)device->resolver(device->handle, name); ++ call(handle, info); ++ if (device->format_mask) ++ hybris_bc_save_push(&command->state, info->layout, info->stageFlags, ++ info->offset, info->size, info->pValues); ++} ++static void VKAPI_CALL push_constants2_core(VkCommandBuffer handle, const VkPushConstantsInfo *info) ++{ push_constants2(handle, info, "vkCmdPushConstants2"); } ++static void VKAPI_CALL push_constants2_khr(VkCommandBuffer handle, const VkPushConstantsInfo *info) ++{ push_constants2(handle, info, "vkCmdPushConstants2KHR"); } + PFN_vkVoidFunction hybris_bc_commands_proc(const char *name) + { + static const struct { const char *name; PFN_vkVoidFunction function; } commands[] = { +@@ -207,7 +240,9 @@ PFN_vkVoidFunction hybris_bc_commands_proc(const char *name) + ENTRY(ResetCommandBuffer, reset_command), ENTRY(ResetCommandPool, reset_pool), + ENTRY(DestroyCommandPool, destroy_pool), ENTRY(CreatePipelineLayout, create_layout), + ENTRY(DestroyPipelineLayout, destroy_layout), ENTRY(CmdBindPipeline, bind_pipeline), +- ENTRY(CmdBindDescriptorSets, bind_descriptors), ENTRY(CmdPushConstants, push_constants) ++ ENTRY(CmdBindDescriptorSets, bind_descriptors), ENTRY(CmdPushConstants, push_constants), ++ ENTRY(CmdBindDescriptorSets2, bind_descriptors2_core), ENTRY(CmdBindDescriptorSets2KHR, bind_descriptors2_khr), ++ ENTRY(CmdPushConstants2, push_constants2_core), ENTRY(CmdPushConstants2KHR, push_constants2_khr) + #undef ENTRY + }; + for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); ++i) +diff --git a/hybris/vulkan/compat/bc_image.c b/hybris/vulkan/compat/bc_image.c +index fe25344..44faaf6 100644 +--- a/hybris/vulkan/compat/bc_image.c ++++ b/hybris/vulkan/compat/bc_image.c +@@ -5,6 +5,19 @@ + + #define PROC(name) PFN_vk##name name = (PFN_vk##name)image->resolver(image->device, "vk" #name) + ++int hybris_bc_srgb_family(VkFormat format) ++{ ++ return (format >= VK_FORMAT_BC1_RGB_UNORM_BLOCK && format <= VK_FORMAT_BC3_SRGB_BLOCK) || ++ format == VK_FORMAT_BC7_UNORM_BLOCK || format == VK_FORMAT_BC7_SRGB_BLOCK; ++} ++int hybris_bc_view_compatible(VkFormat image, VkFormat view) ++{ ++ if (image == view) return 1; ++ return hybris_bc_srgb_family(image) && ++ ((image - VK_FORMAT_BC1_RGB_UNORM_BLOCK) / 2 == ++ (view - VK_FORMAT_BC1_RGB_UNORM_BLOCK) / 2) && hybris_bc_srgb_family(view); ++} ++ + VkFormat hybris_bc_image_format(VkFormat format, unsigned rgb_mask) + { + switch (format) { +@@ -71,19 +84,28 @@ VkResult hybris_bc_image_describe(const VkImageCreateInfo *info, struct hybris_b + info->imageType != VK_IMAGE_TYPE_2D || info->tiling != VK_IMAGE_TILING_OPTIMAL || + info->samples != VK_SAMPLE_COUNT_1_BIT || + (info->initialLayout != VK_IMAGE_LAYOUT_UNDEFINED && info->initialLayout != VK_IMAGE_LAYOUT_PREINITIALIZED) || +- (info->flags & ~VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) || ++ (info->flags & ~(VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT | VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT)) || ++ ((info->flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) && !hybris_bc_srgb_family(info->format)) || + !info->usage || + (info->usage & ~(VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) || + !info->extent.width || !info->extent.height || info->extent.depth != 1 || + !info->mipLevels || info->mipLevels > 32 || !info->arrayLayers) + return VK_ERROR_FORMAT_NOT_SUPPORTED; ++ image->flags = info->flags; ++ image->view_mask = (info->flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) ? 3 : 1; + /* These chains only constrain views or request no external/swapchain + * binding. The decoded image has the same view restriction in our wrapper. */ + for (const VkBaseInStructure *next = info->pNext; next; next = next->pNext) { + if (next->sType == VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO) { + const VkImageFormatListCreateInfo *list = (const void *)next; +- for (uint32_t i = 0; i < list->viewFormatCount; ++i) +- if (list->pViewFormats[i] != info->format) return VK_ERROR_FORMAT_NOT_SUPPORTED; ++ if (list->viewFormatCount) image->view_mask = 0; ++ for (uint32_t i = 0; i < list->viewFormatCount; ++i) { ++ VkFormat view = list->pViewFormats[i]; ++ if (!hybris_bc_view_compatible(info->format, view) || ++ (view != info->format && !(info->flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT))) ++ return VK_ERROR_FORMAT_NOT_SUPPORTED; ++ image->view_mask |= view == info->format ? 1u : 2u; ++ } + } else if (next->sType == VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO) { + if (((const VkExternalMemoryImageCreateInfo *)next)->handleTypes) + return VK_ERROR_FORMAT_NOT_SUPPORTED; +diff --git a/hybris/vulkan/compat/bc_image.h b/hybris/vulkan/compat/bc_image.h +index 9e62eb3..c950cc2 100644 +--- a/hybris/vulkan/compat/bc_image.h ++++ b/hybris/vulkan/compat/bc_image.h +@@ -12,10 +12,14 @@ struct hybris_bc_image { + VkMemoryRequirements requirements; + VkBool32 requires_dedicated, prefers_dedicated; + VkFormat format, decoded_format; ++ VkImageCreateFlags flags; ++ unsigned view_mask; + VkExtent3D extent; + uint32_t mip_levels, layers, block_bytes; + VkDeviceSize mip_offset[32], block_size; + }; ++int hybris_bc_srgb_family(VkFormat format); ++int hybris_bc_view_compatible(VkFormat image, VkFormat view); + VkFormat hybris_bc_image_format(VkFormat format, unsigned rgb_mask); + VkResult hybris_bc_image_describe(const VkImageCreateInfo *info, struct hybris_bc_image *image); + void hybris_bc_image_backing_info(const struct hybris_bc_image *image, +diff --git a/hybris/vulkan/compat/bc_policy.c b/hybris/vulkan/compat/bc_policy.c +index c5856f6..3124a25 100644 +--- a/hybris/vulkan/compat/bc_policy.c ++++ b/hybris/vulkan/compat/bc_policy.c +@@ -5,6 +5,7 @@ + #include "bc_context.h" + #include "../layer/layer.h" + #include ++#include + #include + #include + +@@ -16,12 +17,18 @@ static void initialize_policy(void) + const char *value = getenv("HYBRIS_BC_TEXTURES"); + if (!value || !strcmp(value, "missing") || !strcmp(value, "1")) policy = 1; + else if (value && !strcmp(value, "force")) policy = 2; ++ else if (value && !strcmp(value, "dxvk")) policy = 3; + } + int hybris_bc_enabled(void) + { + pthread_once(&policy_once, initialize_policy); + return policy != 0; + } ++static int dxvk_profile(void) ++{ ++ pthread_once(&policy_once, initialize_policy); ++ return policy == 3; ++} + #define PHYSICAL_PROC(name) PFN_vk##name name = (PFN_vk##name)context.resolver(context.instance, "vk" #name) + /* Preserve three-channel border replacement when the native image format + * supports the full sampled/transfer domain. Selection is per physical device; +@@ -52,7 +59,7 @@ unsigned hybris_bc_physical_mask(VkPhysicalDevice physical) + VkPhysicalDeviceProperties properties; + GetPhysicalDeviceProperties(physical, &properties); + /* Core 1.4 includes descriptor-state commands that are not intercepted yet. */ +- if (properties.apiVersion < VK_API_VERSION_1_1 || properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 4, 0) || ++ if (properties.apiVersion < VK_API_VERSION_1_1 || (!dxvk_profile() && properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 4, 0)) || + properties.limits.maxStorageBufferRange < 256 || + properties.limits.minStorageBufferOffsetAlignment >= properties.limits.maxStorageBufferRange) return 0; + uint32_t count = 0; +@@ -72,6 +79,9 @@ unsigned hybris_bc_physical_mask(VkPhysicalDevice physical) + unsigned mask = 0; + for (unsigned i = 0; i < 16; ++i) { + VkFormat format = VK_FORMAT_BC1_RGB_UNORM_BLOCK + i; ++ /* D3D uses BC1 RGBA. Keep RGB-only BC1 unadvertised until absent-alpha ++ * border semantics are implemented on devices without RGB8 images. */ ++ if (dxvk_profile() && i < 2) continue; + VkFormat backing = hybris_bc_image_format(format, physical_rgb_mask(physical)); + if (backing == VK_FORMAT_UNDEFINED) continue; + VkFormatProperties native, decoded; +@@ -84,6 +94,33 @@ unsigned hybris_bc_physical_mask(VkPhysicalDevice physical) + } + return mask; + } ++/* The emulation profile implements Vulkan 1.3 plus maintenance6. Do not expose ++ * 1.4 core host-copy/push-descriptor operations that bypass our state tracking. */ ++static void cap_properties(VkPhysicalDevice physical, VkPhysicalDeviceProperties *out) ++{ ++ if (dxvk_profile() && hybris_bc_physical_mask(physical) && out->apiVersion > VK_API_VERSION_1_3) ++ out->apiVersion = VK_API_VERSION_1_3; ++} ++static void VKAPI_CALL properties(VkPhysicalDevice physical, VkPhysicalDeviceProperties *out) ++{ ++ struct hybris_layer_physical context; ++ if (!hybris_layer_lookup_physical(physical, &context)) return; ++ PHYSICAL_PROC(GetPhysicalDeviceProperties); ++ GetPhysicalDeviceProperties(physical, out); ++ cap_properties(physical, out); ++} ++static void properties2(VkPhysicalDevice physical, VkPhysicalDeviceProperties2 *out, const char *name) ++{ ++ struct hybris_layer_physical context; ++ if (!hybris_layer_lookup_physical(physical, &context)) return; ++ PFN_vkGetPhysicalDeviceProperties2 query = (void *)context.resolver(context.instance, name); ++ query(physical, out); ++ cap_properties(physical, &out->properties); ++} ++static void VKAPI_CALL properties2_core(VkPhysicalDevice physical, VkPhysicalDeviceProperties2 *out) ++{ properties2(physical, out, "vkGetPhysicalDeviceProperties2"); } ++static void VKAPI_CALL properties2_khr(VkPhysicalDevice physical, VkPhysicalDeviceProperties2 *out) ++{ properties2(physical, out, "vkGetPhysicalDeviceProperties2KHR"); } + static int emulates(VkPhysicalDevice physical, VkFormat format) + { + return format >= VK_FORMAT_BC1_RGB_UNORM_BLOCK && format <= VK_FORMAT_BC7_SRGB_BLOCK && +@@ -109,10 +146,13 @@ int hybris_bc_extension_allowed(const char *name) + "VK_KHR_maintenance6", "VK_KHR_maintenance8", "VK_EXT_host_image_copy", + "VK_QCOM_rotated_copy_commands", "VK_EXT_device_generated_commands", + "VK_NV_device_generated_commands", "VK_NV_device_generated_commands_compute", +- "VK_NV_dedicated_allocation", "VK_NV_copy_memory_indirect" ++ "VK_NV_dedicated_allocation", "VK_NV_copy_memory_indirect", "VK_NV_per_stage_descriptor_set" + }; + for (size_t i = 0; i < sizeof(unsupported) / sizeof(unsupported[0]); ++i) +- if (!strcmp(name, unsupported[i])) return 0; ++ if (!strcmp(name, unsupported[i])) { ++ if (dxvk_profile() && !strcmp(name, "VK_KHR_maintenance6")) continue; ++ return 0; ++ } + return 1; + } + /* Extension feature queries must agree with the filtered extension list. Each +@@ -121,8 +161,12 @@ static unsigned unsupported_feature_count(VkStructureType type) + { + switch (type) { + case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_FEATURES_EXT: return 4; ++ case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_4_FEATURES: ++ return dxvk_profile() ? (offsetof(VkPhysicalDeviceVulkan14Features, pushDescriptor) - ++ offsetof(VkPhysicalDeviceVulkan14Features, globalPriorityQuery)) / sizeof(VkBool32) + 1 : 0; ++ case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_6_FEATURES_KHR: return dxvk_profile() ? 0 : 1; ++ case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PER_STAGE_DESCRIPTOR_SET_FEATURES_NV: return 2; + case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_OBJECT_FEATURES_EXT: +- case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_6_FEATURES_KHR: + case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES_EXT: + case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COPY_MEMORY_INDIRECT_FEATURES_NV: + case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEVICE_GENERATED_COMMANDS_FEATURES_NV: return 1; +@@ -252,11 +296,12 @@ static void VKAPI_CALL sparse_properties2_core(VkPhysicalDevice physical, const + static void VKAPI_CALL sparse_properties2_khr(VkPhysicalDevice physical, const VkPhysicalDeviceSparseImageFormatInfo2 *info, + uint32_t *count, VkSparseImageFormatProperties2 *out) + { sparse_properties2(physical, info, count, out, "vkGetPhysicalDeviceSparseImageFormatProperties2KHR"); } +-static int image_supported(VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags) ++static int image_supported(VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags) + { + return type == VK_IMAGE_TYPE_2D && tiling == VK_IMAGE_TILING_OPTIMAL && usage && + !(usage & ~(VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) && +- !(flags & ~VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT); ++ !(flags & ~(VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT | VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT)) && ++ (!(flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) || hybris_bc_srgb_family(format)); + } + static VkResult VKAPI_CALL image_properties(VkPhysicalDevice physical, VkFormat format, VkImageType type, + VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties *out) +@@ -265,7 +310,7 @@ static VkResult VKAPI_CALL image_properties(VkPhysicalDevice physical, VkFormat + if (!hybris_layer_lookup_physical(physical, &context)) return VK_ERROR_INITIALIZATION_FAILED; + PHYSICAL_PROC(GetPhysicalDeviceImageFormatProperties); + if (emulates(physical, format)) { +- if (!image_supported(type, tiling, usage, flags)) { *out = (VkImageFormatProperties){0}; return VK_ERROR_FORMAT_NOT_SUPPORTED; } ++ if (!image_supported(format, type, tiling, usage, flags)) { *out = (VkImageFormatProperties){0}; return VK_ERROR_FORMAT_NOT_SUPPORTED; } + VkResult result = GetPhysicalDeviceImageFormatProperties(physical, hybris_bc_image_format(format, physical_rgb_mask(physical)), type, + tiling, usage | VK_IMAGE_USAGE_TRANSFER_DST_BIT, flags, out); + if (result == VK_SUCCESS) out->sampleCounts &= VK_SAMPLE_COUNT_1_BIT; +@@ -282,14 +327,16 @@ static VkResult image_properties2(VkPhysicalDevice physical, const VkPhysicalDev + (PFN_vkGetPhysicalDeviceImageFormatProperties2)context.resolver(context.instance, name); + if (!emulates(physical, info->format)) return query(physical, info, out); + out->imageFormatProperties = (VkImageFormatProperties){0}; +- if (!image_supported(info->type, info->tiling, info->usage, info->flags)) return VK_ERROR_FORMAT_NOT_SUPPORTED; ++ if (!image_supported(info->format, info->type, info->tiling, info->usage, info->flags)) return VK_ERROR_FORMAT_NOT_SUPPORTED; + for (const VkBaseInStructure *node = info->pNext; node; node = node->pNext) { + if (node->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO) { + if (((const VkPhysicalDeviceExternalImageFormatInfo *)node)->handleType) return VK_ERROR_FORMAT_NOT_SUPPORTED; + } else if (node->sType == VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO) { + const VkImageFormatListCreateInfo *list = (const void *)node; + for (uint32_t i = 0; i < list->viewFormatCount; ++i) +- if (list->pViewFormats[i] != info->format) return VK_ERROR_FORMAT_NOT_SUPPORTED; ++ if (!hybris_bc_view_compatible(info->format, list->pViewFormats[i]) || ++ (list->pViewFormats[i] != info->format && !(info->flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT))) ++ return VK_ERROR_FORMAT_NOT_SUPPORTED; + } else if (node->sType != VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_VIEW_IMAGE_FORMAT_INFO_EXT) { + return VK_ERROR_FORMAT_NOT_SUPPORTED; + } +@@ -322,6 +369,9 @@ PFN_vkVoidFunction hybris_bc_policy_proc(const char *name) + if (!hybris_bc_enabled()) return NULL; + static const struct { const char *name; PFN_vkVoidFunction function; } commands[] = { + #define ENTRY(name, function) {"vk" #name, (PFN_vkVoidFunction)function} ++ ENTRY(GetPhysicalDeviceProperties, properties), ++ ENTRY(GetPhysicalDeviceProperties2, properties2_core), ++ ENTRY(GetPhysicalDeviceProperties2KHR, properties2_khr), + ENTRY(GetPhysicalDeviceQueueFamilyProperties, queue_properties), + ENTRY(GetPhysicalDeviceQueueFamilyProperties2, queue_properties2_core), + ENTRY(GetPhysicalDeviceQueueFamilyProperties2KHR, queue_properties2_khr), +diff --git a/hybris/vulkan/compat/bc_resources.c b/hybris/vulkan/compat/bc_resources.c +index 526c3f1..74cc0c9 100644 +--- a/hybris/vulkan/compat/bc_resources.c ++++ b/hybris/vulkan/compat/bc_resources.c +@@ -210,8 +210,10 @@ static VkResult VKAPI_CALL create_view(VkDevice handle, const VkImageViewCreateI + struct hybris_bc_resource *resource = hybris_bc_resource_find(device, info->image); + VkImageViewCreateInfo translated = *info; + if (resource && resource->emulated.image) { +- if (info->format != resource->format) return VK_ERROR_FORMAT_NOT_SUPPORTED; +- translated.format = resource->emulated.decoded_format; ++ unsigned bit = info->format == resource->format ? 1u : 2u; ++ if (!hybris_bc_view_compatible(resource->format, info->format) || ++ !(resource->emulated.view_mask & bit)) return VK_ERROR_FORMAT_NOT_SUPPORTED; ++ translated.format = hybris_bc_image_format(info->format, device->rgb_mask); + } + PROC(CreateImageView); + return CreateImageView(handle, &translated, allocator, view); +diff --git a/hybris/vulkan/compat/bc_transfer.c b/hybris/vulkan/compat/bc_transfer.c +index 6edb652..799fb62 100644 +--- a/hybris/vulkan/compat/bc_transfer.c ++++ b/hybris/vulkan/compat/bc_transfer.c +@@ -2,6 +2,7 @@ + #define VK_NO_PROTOTYPES + #include "bc_transfer.h" + #include ++#include + #include + + #define PROC(name) PFN_vk##name name = (PFN_vk##name)t->decoder->resolver(t->decoder->device, "vk" #name) +@@ -59,16 +60,29 @@ static VkResult scratch(struct hybris_bc_transfer *t) + if (type == t->memory->memoryTypeCount) type = i; + if (t->memory->memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) { type = i; break; } + } +- if (type == t->memory->memoryTypeCount) { result = VK_ERROR_OUT_OF_DEVICE_MEMORY; goto fail; } ++ if (type == t->memory->memoryTypeCount) { ++ fprintf(stderr, "HYBRIS_BC scratch: no memory type (bits=0x%x count=%u size=%llu)\n", ++ requirements.memoryRequirements.memoryTypeBits, t->memory->memoryTypeCount, ++ (unsigned long long)requirements.memoryRequirements.size); ++ result = VK_ERROR_OUT_OF_DEVICE_MEMORY; goto fail; ++ } + VkMemoryDedicatedAllocateInfo owner = { + .sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, .buffer = buffer}; + VkMemoryAllocateInfo allocation = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .pNext = dedicated.requiresDedicatedAllocation || dedicated.prefersDedicatedAllocation ? &owner : NULL, + .allocationSize = requirements.memoryRequirements.size, .memoryTypeIndex = type}; + result = AllocateMemory(device, &allocation, NULL, &memory); +- if (result != VK_SUCCESS) goto fail; ++ if (result != VK_SUCCESS) { ++ fprintf(stderr, "HYBRIS_BC scratch allocation: result=%d size=%llu type=%u bits=0x%x\n", ++ result, (unsigned long long)allocation.allocationSize, type, requirements.memoryRequirements.memoryTypeBits); ++ goto fail; ++ } + result = BindBufferMemory(device, buffer, memory, 0); +- if (result != VK_SUCCESS) goto fail; ++ if (result != VK_SUCCESS) { ++ fprintf(stderr, "HYBRIS_BC scratch bind: result=%d size=%llu type=%u\n", ++ result, (unsigned long long)allocation.allocationSize, type); ++ goto fail; ++ } + t->scratch = buffer; + t->scratch_memory = memory; + t->scratch_size = size; +@@ -193,7 +207,7 @@ VkResult hybris_bc_decode_image(struct hybris_bc_transfer *t, + buffer_barrier(t, command, image->blocks, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); + VkResult result = decode(t, command, image, layout, region); +- if (result != VK_SUCCESS) { state->error = result; return result; } ++ if (result != VK_SUCCESS) { fprintf(stderr, "HYBRIS_BC transfer failed: %d\n", result); state->error = result; return result; } + buffer_barrier(t, command, image->blocks, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); +diff --git a/hybris/vulkan/compat/shader_policy.c b/hybris/vulkan/compat/shader_policy.c +index a1dcf6d..21a4cc7 100644 +--- a/hybris/vulkan/compat/shader_policy.c ++++ b/hybris/vulkan/compat/shader_policy.c +@@ -92,7 +92,8 @@ static void properties2(VkPhysicalDevice physical, VkPhysicalDeviceProperties2 * + { + struct hybris_layer_physical context; + if (!hybris_layer_lookup_physical(physical, &context)) return; +- PFN_vkGetPhysicalDeviceProperties2 query = (PFN_vkGetPhysicalDeviceProperties2)context.resolver(context.instance, name); ++ PFN_vkGetPhysicalDeviceProperties2 query = (void *)hybris_bc_policy_proc(name); ++ if (!query) query = (void *)context.resolver(context.instance, name); + query(physical, out); + VkPhysicalDeviceVertexAttributeDivisorPropertiesEXT *legacy = NULL; + for (VkBaseOutStructure *node = out->pNext; node; node = node->pNext) +diff --git a/tests/baseline/probe_bc_images.c b/tests/baseline/probe_bc_images.c +index 85ba6e6..55e069c 100644 +--- a/tests/baseline/probe_bc_images.c ++++ b/tests/baseline/probe_bc_images.c +@@ -95,9 +95,10 @@ int bc_images_probe(int validate, int route) + } + free(queues); + if (family == UINT32_MAX) return 3; ++ unsigned first_format = getenv("BC_D3D_FORMATS") ? 2 : 0; + VkBool32 linear_filter[BC_IMAGE_FORMAT_COUNT] = {0}; + unsigned unsupported_formats = 0; +- for (unsigned f = 0; f < BC_IMAGE_FORMAT_COUNT; ++f) { ++ for (unsigned f = first_format; f < BC_IMAGE_FORMAT_COUNT; ++f) { + VkFormatProperties format; + p_vkGetPhysicalDeviceFormatProperties(physical, bc_formats[f], &format); + VkFormatProperties2 format2 = {.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2}; +@@ -164,19 +165,28 @@ int bc_images_probe(int validate, int route) + p_vkGetPhysicalDeviceFeatures2(physical, &queried); + maintenance4 = maintenance.maintenance4; + } +- synchronization.pNext = maintenance4 ? &maintenance : NULL; +- const char *device_extensions[4]; ++ int maintenance6 = getenv("BC_MAINTENANCE6") != NULL; ++ VkPhysicalDeviceMaintenance6FeaturesKHR maintenance_six = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_6_FEATURES_KHR}; ++ if (maintenance6) { ++ VkPhysicalDeviceFeatures2 q = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &maintenance_six}; ++ p_vkGetPhysicalDeviceFeatures2(physical, &q); ++ if (!maintenance_six.maintenance6) { puts("MISSING maintenance6"); return 3; } ++ } ++ maintenance.pNext = maintenance6 ? &maintenance_six : NULL; ++ synchronization.pNext = maintenance4 ? (void *)&maintenance : maintenance6 ? (void *)&maintenance_six : NULL; ++ const char *device_extensions[5]; + uint32_t device_extension_count = 0; + if (copies2) device_extensions[device_extension_count++] = VK_KHR_COPY_COMMANDS_2_EXTENSION_NAME; + if (sync2) device_extensions[device_extension_count++] = VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME; + if (maintenance4) device_extensions[device_extension_count++] = VK_KHR_MAINTENANCE_4_EXTENSION_NAME; + if (format_list) device_extensions[device_extension_count++] = VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME; ++ if (maintenance6) device_extensions[device_extension_count++] = VK_KHR_MAINTENANCE_6_EXTENSION_NAME; + float priority = 1; + VkDeviceQueueCreateInfo qc = {.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .queueFamilyIndex = family, .queueCount = 1, .pQueuePriorities = &priority}; + VkDeviceCreateInfo dc = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .queueCreateInfoCount = 1, .pQueueCreateInfos = &qc, +- .pNext = sync2 ? (void *)&synchronization : maintenance4 ? (void *)&maintenance : NULL, ++ .pNext = sync2 ? (void *)&synchronization : maintenance4 ? (void *)&maintenance : maintenance6 ? (void *)&maintenance_six : NULL, + .enabledExtensionCount = device_extension_count, .ppEnabledExtensionNames = device_extensions}; + VkDebugUtilsMessengerEXT messenger = VK_NULL_HANDLE; + PFN_vkDestroyDebugUtilsMessengerEXT destroy_messenger = NULL; +@@ -307,8 +317,12 @@ int bc_images_probe(int validate, int route) + VkFenceCreateInfo fence_info = {.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + VkFence fence; + CHECK(p_vkCreateFence(device, &fence_info, NULL, &fence)); ++ PFN_vkCmdBindDescriptorSets2KHR bind2 = (PFN_vkCmdBindDescriptorSets2KHR)p_vkGetDeviceProcAddr(device, "vkCmdBindDescriptorSets2KHR"); ++ PFN_vkCmdPushConstants2KHR push2 = (PFN_vkCmdPushConstants2KHR)p_vkGetDeviceProcAddr(device, "vkCmdPushConstants2KHR"); ++ if (maintenance6 && (!bind2 || !push2)) return 2; ++ printf("BC_EXTENDED maintenance6=%d mutable=%d\n", maintenance6, getenv("BC_MUTABLE") != NULL); + unsigned failures = 0, readbacks = 0, copies2_cases = 0, sync2_cases = 0; +- for (unsigned f = 0; f < BC_IMAGE_FORMAT_COUNT; ++f) { ++ for (unsigned f = first_format; f < BC_IMAGE_FORMAT_COUNT; ++f) { + for (unsigned shape_index = 0; shape_index < 2; ++shape_index) { + uint32_t base_width = shape_index ? 32 : 9, base_height = shape_index ? 32 : 7; + for (uint32_t mip = 0; mip < 4; ++mip) { +@@ -337,6 +351,13 @@ int bc_images_probe(int validate, int route) + (i != 2 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0), .sharingMode = VK_SHARING_MODE_EXCLUSIVE}; + VkImageFormatListCreateInfo formats = {.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO, + .viewFormatCount = 1, .pViewFormats = &image_info.format}; ++ VkFormat paired[2] = {bc_formats[f], bc_formats[f ^ 1]}; ++ if (getenv("BC_MUTABLE") && (f < 8 || (f >= 12 && f < 14)) && i < 2) { ++ image_info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; ++ image_info.format = paired[1]; ++ formats.viewFormatCount = 2; ++ formats.pViewFormats = paired; ++ } + if (format_list) image_info.pNext = &formats; + VkMemoryRequirements2 described = {.sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2}; + if (maintenance4) { +@@ -414,9 +435,19 @@ int bc_images_probe(int validate, int route) + VkCommandBufferBeginInfo begin = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + CHECK(p_vkBeginCommandBuffer(command, &begin)); + p_vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); +- p_vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, 0, 1, &descriptors, 1, &dynamic_offset); ++ if (maintenance6) { ++ VkBindDescriptorSetsInfoKHR bind = {.sType = VK_STRUCTURE_TYPE_BIND_DESCRIPTOR_SETS_INFO_KHR, ++ .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, .layout = pipeline_layout, ++ .descriptorSetCount = 1, .pDescriptorSets = &descriptors, ++ .dynamicOffsetCount = 1, .pDynamicOffsets = &dynamic_offset}; ++ bind2(command, &bind); ++ } else p_vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, 0, 1, &descriptors, 1, &dynamic_offset); + uint32_t shape[] = {width, height, LAYERS, f >= 14 ? 3 : f >= 8 && f < 12 ? 1 + (f & 1) : 0, linear_filter[f] != 0}; +- p_vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(shape), shape); ++ if (maintenance6) { ++ VkPushConstantsInfoKHR push = {.sType = VK_STRUCTURE_TYPE_PUSH_CONSTANTS_INFO_KHR, ++ .layout = pipeline_layout, .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, .size = sizeof(shape), .pValues = shape}; ++ push2(command, &push); ++ } else p_vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(shape), shape); + VkMemoryBarrier before = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, + .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; +@@ -614,7 +645,7 @@ int bc_images_probe(int validate, int route) + p_vkDestroyDevice(device, NULL); + if (destroy_messenger) destroy_messenger(instance, messenger, NULL); + p_vkDestroyInstance(instance, NULL); dlclose(h); +- printf("BC_IMAGES_SUMMARY formats=16 shapes=2 readbacks=%u copy2_cases=%u sync2_cases=%u maintenance4=%u format_list=%u failures=%u validation_errors=%u route=%d\n", +- readbacks, copies2_cases, sync2_cases, maintenance4, format_list, failures, validation.errors, route); +- return failures || validation.errors || readbacks != BC_IMAGE_FORMAT_COUNT * 2 * 4 * 3 ? 2 : 0; ++ printf("BC_IMAGES_SUMMARY formats=%u shapes=2 readbacks=%u copy2_cases=%u sync2_cases=%u maintenance4=%u format_list=%u failures=%u validation_errors=%u route=%d\n", ++ (unsigned)BC_IMAGE_FORMAT_COUNT - first_format, readbacks, copies2_cases, sync2_cases, maintenance4, format_list, failures, validation.errors, route); ++ return failures || validation.errors || readbacks != (BC_IMAGE_FORMAT_COUNT - first_format) * 2 * 4 * 3 ? 2 : 0; + } diff --git a/tools/hybris/powervr-hal-version.patch b/tools/hybris/powervr-hal-version.patch new file mode 100644 index 000000000..63b72c759 --- /dev/null +++ b/tools/hybris/powervr-hal-version.patch @@ -0,0 +1,12 @@ +--- a/hybris/vulkan/icd/icd.c ++++ b/hybris/vulkan/icd/icd.c +@@ -71,7 +71,8 @@ + int result = module->methods->open(module, HWVULKAN_DEVICE_0, + (hw_device_t **)&candidate); + if (result || !candidate || candidate->common.tag != HARDWARE_DEVICE_TAG || +- candidate->common.version != HWVULKAN_DEVICE_API_VERSION_0_1 || ++ (candidate->common.version != HWVULKAN_DEVICE_API_VERSION_0_1 && ++ candidate->common.version != HARDWARE_DEVICE_API_VERSION(0, 1)) || + !candidate->CreateInstance || !candidate->GetInstanceProcAddr || + !candidate->EnumerateInstanceExtensionProperties) { + fprintf(stderr, "hybris ICD: incompatible Vulkan HAL device (open=%d)\n", result); diff --git a/tools/hybris/run-proton.py b/tools/hybris/run-proton.py new file mode 100644 index 000000000..c8c1b0eb4 --- /dev/null +++ b/tools/hybris/run-proton.py @@ -0,0 +1,89 @@ +#!/usr/bin/python3 +"""Select a staged DXVK payload for one DroidDeck Proton game launch. + +Place this script beside dxvk/x32/ and dxvk/x64/, then select its absolute +path with DROIDDECK_PROTON_WRAPPER in the game's environment settings. +Run `python3 run-proton.py --check /path/to/proton` to check the payload and +Proton source layout without starting Proton or changing a game prefix. + +This wrapper does not install the graphics runtime, select Vulkan layers, +change synchronization settings, or provide game-specific workarounds. +""" + +import argparse +import os +from pathlib import Path +import sys + + +DLL_NAMES = ("d3d8.dll", "d3d9.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll") +PREFIX_MARKER = "droiddeck-dxvk-profile-v1" + + +def prepare(proton, dxvk): + """Validate all inputs and compile an in-memory copy of the Proton script.""" + for architecture in ("x32", "x64"): + for name in DLL_NAMES: + dll = dxvk / architecture / name + if not dll.is_file(): + raise ValueError(f"missing DXVK file: {dll}") + with dll.open("rb") as stream: + if not stream.read(1): + raise ValueError(f"empty DXVK file: {dll}") + + source = proton.read_text(encoding="utf-8") + for is32, architecture in (("False", "x64"), ("True", "x32")): + needle = f'g_proton.arch_pe_dir("wine/dxvk", {is32}) + f + ".dll"' + if source.count(needle) != 1: + raise ValueError(f"unsupported Proton layout: expected one {architecture} DXVK copy expression") + directory = str(dxvk / architecture) + "/" + source = source.replace(needle, repr(directory) + ' + f + ".dll"', 1) + + needle = " # check whether any prefix config has changed" + if source.count(needle) != 1: + raise ValueError("unsupported Proton layout: expected one prefix configuration marker") + # The supported Proton copies DXVK DLLs on every launch. This marker records + # profile activation; it does not need changing whenever the payload changes. + marker = repr("\n" + PREFIX_MARKER) + source = source.replace(needle, f" prefix_info += {marker}\n" + needle, 1) + return compile(source, str(proton), "exec") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="validate inputs without running Proton") + parser.add_argument("proton", help="selected Proton Python script") + parser.add_argument("proton_args", nargs=argparse.REMAINDER) + args = parser.parse_args() + if not args.check and (len(args.proton_args) < 2 or args.proton_args[0] != "waitforexitandrun"): + parser.error("expected: PROTON waitforexitandrun GAME [ARGUMENTS...]") + + proton = Path(args.proton) + # A sibling payload directory by name (dxvk-2.6.2 for Xclipse); never a path. + name = os.environ.get("DROIDDECK_HYBRIS_DXVK") or "dxvk" + if not name.startswith("dxvk") or "/" in name or name.startswith("."): + print(f"DroidDeck DXVK profile: invalid payload name {name!r}", file=sys.stderr) + return 1 + dxvk = Path(__file__).resolve().parent / name + try: + code = prepare(proton, dxvk) + except (OSError, ValueError, SyntaxError) as error: + print(f"DroidDeck DXVK profile: {error}", file=sys.stderr) + return 1 + + if args.check: + print(f"DroidDeck DXVK profile: payload present and Proton layout supported ({dxvk})") + return 0 + + print(f"DROIDDECK_GRAPHICS profile={PREFIX_MARKER} dxvk={dxvk}", flush=True) + # Preserve the selected Proton path, including symlinks, and its arguments. + # Proton uses argv[0] and sibling Python imports to locate its own depot. + sys.argv = [args.proton, *args.proton_args] + sys.path.insert(0, str(proton.absolute().parent)) + exec(code, {"__name__": "__main__", "__file__": args.proton, + "__package__": None, "__spec__": None}) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/hybris/source.env b/tools/hybris/source.env new file mode 100644 index 000000000..30dd70e1c --- /dev/null +++ b/tools/hybris/source.env @@ -0,0 +1,2 @@ +HYBRIS_REVISION=bc8cb3586b79d208fd21b925fa0209b3e432e008 +PROTOCOL_REVISION=c6ae3c22a804da3cda017f4c8eaa6c6a3da55457 diff --git a/tools/linuxfs/check-preloads.sh b/tools/linuxfs/check-preloads.sh index 30f94a96a..bc7ca42ca 100644 --- a/tools/linuxfs/check-preloads.sh +++ b/tools/linuxfs/check-preloads.sh @@ -26,5 +26,10 @@ SSBS=$(syms "$D/libssbs.so") for sym in sigaction signal bsd_signal sysv_signal; do grep -qx "$sym" <<<"$SSBS" || { echo "::error::libssbs.so does not export $sym"; exit 1; } done +XCLIPSE=$(syms "$D/usr/local/lib/droiddeck-xclipse/libVkLayer_droiddeck_xclipse.so") +for sym in vkNegotiateLoaderLayerInterfaceVersion vkGetInstanceProcAddr vkGetDeviceProcAddr; do + grep -qx "$sym" <<<"$XCLIPSE" || { echo "::error::the Xclipse layer does not export $sym"; exit 1; } +done +test -f "$D/usr/local/lib/droiddeck-xclipse/VkLayer_droiddeck_xclipse.json" test -f "$D/usr/local/bin/droiddeck-session" test -f "$D/usr/local/bin/droiddeck-proton-extra" diff --git a/tools/linuxfs/desktop/droiddeck-gpu b/tools/linuxfs/desktop/droiddeck-gpu index 886f0d0a0..012cd3062 100644 --- a/tools/linuxfs/desktop/droiddeck-gpu +++ b/tools/linuxfs/desktop/droiddeck-gpu @@ -6,13 +6,18 @@ if [ -n "${BL_DEBUG_DIR:-}" ]; then fi width=${BL_WIDTH:-1280} height=${BL_HEIGHT:-720} +backend=${BL_GAMESCOPE_BACKEND:-wayland} +case "$backend" in + wayland|sdl) ;; + *) echo "== unsupported gamescope backend: $backend" >&2; exit 64 ;; +esac window=0 -if [ "${BL_DESKTOP_GPU_WINDOW:-1}" != 0 ] && [ -n "${WAYLAND_DISPLAY:-}" ] && [ "$WAYLAND_DISPLAY" != wayland-0 ]; then +if [ "$backend" = wayland ] && [ "${BL_DESKTOP_GPU_WINDOW:-1}" != 0 ] && [ -n "${WAYLAND_DISPLAY:-}" ] && [ "$WAYLAND_DISPLAY" != wayland-0 ]; then window=1 parent=$WAYLAND_DISPLAY args=(--backend sdl --expose-wayland --follow-game-window-mode -W "$width" -H "$height" -w "$width" -h "$height") else - args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$width" -h "$height") + args=(--backend "$backend" --expose-wayland -f -W "$width" -H "$height" -w "$width" -h "$height") fi if [ "${BL_FPS:-0}" -gt 0 ]; then args+=(-r "$BL_FPS") @@ -27,6 +32,7 @@ else export WAYLAND_DISPLAY=wayland-0 fi export GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb SDL_VIDEODRIVER=x11 +[ "$backend" = sdl ] && export SDL_VIDEODRIVER=wayland unset LIBGL_ALWAYS_SOFTWARE QT_QUICK_BACKEND KWIN_COMPOSE wsi=",${MESA_VK_WSI_DEBUG:-}," while [[ $wsi == *,sw,* ]]; do wsi=${wsi//,sw,/,}; done @@ -40,7 +46,7 @@ if [ "$window" = 1 ]; then export BL_GPU_CHILD_WSI=${MESA_VK_WSI_DEBUG:-} SDL_VIDEODRIVER=wayland MESA_VK_WSI_DEBUG=sw gamescope "${args[@]}" -- /usr/local/bin/droiddeck-gpu-window "$@" else - gamescope "${args[@]}" -- /usr/local/bin/droiddeck-gpu-window "$@" + gamescope "${args[@]}" -- env SDL_VIDEODRIVER=x11 /usr/local/bin/droiddeck-gpu-window "$@" fi rc=$? echo "== droiddeck-gpu $(date -u +%H:%M:%S): exited $rc" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index a9dab23a7..9fb3c39c6 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -524,6 +524,15 @@ def apply_config(env, config, app_id, fixes=None): if dxvk: own = result.get("DXVK_CONFIG", "").strip().rstrip(";").strip() result["DXVK_CONFIG"] = own + "; " + dxvk if own else dxvk + # An open driver pair (PanVK, RADV) on a GPU that is not an Adreno: vkd3d-proton reports the + # driver's own feature level and shader model. The app-wide 12_2 / 6_9 over-promised what PanVK + # has, and an Unreal DX12 game took paths that crashed. A game's own setting still wins. + if env.get("BL_OPEN_DRIVER") == "1": + game = config.get("games", {}).get(app_id, {}) or {} + for name in ("VKD3D_FEATURE_LEVEL", "VKD3D_SHADER_MODEL"): + if name not in game and name in result: + result.pop(name) + result["DROIDDECK_VKD3D_CAPS"] = "driver" return result @@ -995,6 +1004,7 @@ def diagnostic_game_exit(app_id, raw_code, source=None, wrapper_status=None): def start(launch, launch_env, digest, after=None, diagnostic_app_id=None): if len(launch) > 1 and launch[1] == "waitforexitandrun": ubisoft_composition_off(launch[0], launch_env) + launch, launch_env = system_graphics_launch(launch, launch_env) if after or diagnostic_app_id is not None: process = subprocess.Popen(launch, env=launch_env) if diagnostic_app_id is not None: @@ -1022,6 +1032,110 @@ def start(launch, launch_env, digest, after=None, diagnostic_app_id=None): return os.execvpe(launch[0], launch, launch_env) +class SystemGraphicsError(ValueError): + pass + + +def system_graphics_launch(command, env): + """Use the APK's PowerVR compatibility files only for actual Proton game launches.""" + runtime = env.get("DROIDDECK_POWERVR_RUNTIME") + if not runtime or not game_id(env) or len(command) < 3 or command[1] != "waitforexitandrun": + return command, env + directory = Path(runtime) + if not directory.is_absolute(): + raise SystemGraphicsError("system Vulkan runtime path is not absolute") + # run-proton.py picks the same payload by name; Xclipse needs a DXVK within 128 bytes of push data. + payload = env.get("DROIDDECK_HYBRIS_DXVK") or "dxvk" + if not payload.startswith("dxvk") or "/" in payload: + raise SystemGraphicsError("invalid DXVK payload name: %s" % payload) + required = ( + "run-proton.py", "wsi/libVkLayer_window_system_integration.so", + "wsi/VkLayer_window_system_integration.json", "lib/libVkLayer_hybris_compat.so", + *("%s/%s/%s.dll" % (payload, arch, name) for arch in ("x32", "x64") + for name in ("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi")), + ) + try: + for name in required: + path = directory / name + if not path.is_file() or not path.stat().st_size: + raise SystemGraphicsError("missing system Vulkan component: %s" % name) + wrapper = directory / "run-proton.py" + if not os.access(wrapper, os.X_OK): + raise SystemGraphicsError("system Vulkan Proton wrapper is not executable") + except OSError as error: + raise SystemGraphicsError("system Vulkan component is unreadable: %s" % error) from error + result = dict(env) + layers = "VK_LAYER_window_system_integration:" + env.get("DROIDDECK_HYBRIS_LAYERS", "VK_LAYER_HYBRIS_compat") + result.update({ + "HYBRIS_BC_TEXTURES": "dxvk", + "VK_LAYER_PATH": ":".join(filter(None, (str(directory / "wsi"), str(directory / "lib"), + env.get("DROIDDECK_HYBRIS_LAYER_PATH")))), + "VK_INSTANCE_LAYERS": layers, + "ENABLE_GAMESCOPE_WSI": "0", + "VK_LOADER_LAYERS_DISABLE": "*gamescope*", + "DISABLE_VK_LAYER_VALVE_steam_fossilize_1": "1", + }) + for name in ("DISABLE_WSI_LAYER", "PROTON_USE_WINED3D", "ENABLE_VK_LAYER_VALVE_steam_fossilize_1"): + result.pop(name, None) + return [str(wrapper), *command], result + + +def game_logs(command, env, app_id, now=None): + """Detailed graphics logs for one game launch, in the session's log folder: Proton's own log, + DXVK's and vkd3d-proton's, and the Vulkan driver's startup messages. The app turns this on + (BL_GAME_LOGS=1) on a GPU that is not an Adreno, or with Setup's "Detailed graphics logs".""" + if env.get("BL_GAME_LOGS") != "1" or not env.get("BL_DEBUG_DIR") or len(command) < 3 \ + or command[1] != "waitforexitandrun": + return env + import time + stamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(now)) + name = app_id or re.sub(r"[^A-Za-z0-9._-]+", "_", Path(command[2]).stem)[:40] or "game" + parent = Path(env["BL_DEBUG_DIR"]) / "games" + try: + parent.mkdir(parents=True, exist_ok=True) + folder = Path(tempfile.mkdtemp(prefix=stamp + "-", suffix="-" + name, dir=parent)) + except OSError as error: + print("droiddeck-game-env: no game log folder (%s)" % error, file=sys.stderr) + return env + result = dict(env) + result.update({ + "PROTON_LOG": "1", + "PROTON_LOG_DIR": str(folder), + "DXVK_LOG_PATH": str(folder), + "DXVK_LOG_LEVEL": env.get("DXVK_LOG_LEVEL", "info"), + "VKD3D_LOG_FILE": str(folder / "vkd3d.log"), + "VKD3D_DEBUG": env.get("VKD3D_DEBUG", "warn"), + "VKD3D_SHADER_DEBUG": env.get("VKD3D_SHADER_DEBUG", "none"), + "MESA_LOG_FILE": str(folder / "mesa.log"), + "PANVK_DEBUG": env.get("PANVK_DEBUG", "startup"), + }) + try: + (folder / "launch.txt").write_text("\n".join([ + "app: %s" % (app_id or "-"), + "exe: %s" % command[2], + "proton: %s" % command[0], + "started: %s" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)), + ] + ["%s=%s" % (key, env[key]) for key in sorted(env) + if key.startswith(("VK_", "BL_VK", "BL_DRM", "BL_OPEN", "MESA_", "PANVK", "DXVK_", "VKD3D", "ZINK", "TU_", "DROIDDECK_VKD3D"))]) + "\n") + except OSError: + pass + # Whether the game's sound reaches PulseAudio: the streams 20 s and 60 s in, without waiting. + try: + subprocess.Popen(["sh", "-c", 'for t in 20 40; do sleep $t; { echo "== $(date +%T)"; ' + 'pactl list sink-inputs 2>&1 | head -80; } >> "$1/audio-streams.txt"; done', "audio", str(folder)], + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + start_new_session=True) + except OSError: + pass + return result + + +def exec_proton(command, env): + # Synchronization selection has already picked the actual Proton executable. + command, env = system_graphics_launch(command, env) + os.execvpe(command[0], command, env) + + def main(): command = sys.argv[1:] if not command: @@ -1089,6 +1203,7 @@ def main(): print("droiddeck-game-env: OpenGL does not wait for the screen refresh (vblank_mode=0)", file=sys.stderr) if app_id and len(command) > 1 and command[1] == "waitforexitandrun": command = offline_without_anticheat(command, env) + env = game_logs(command, env, app_id) if mic_enabled == "0": env["AUDIO_DIRECT_MIC"] = "0" env = directaudio_settings(env) @@ -1165,4 +1280,8 @@ def main(): if __name__ == "__main__": - sys.exit(main()) + try: + sys.exit(main()) + except SystemGraphicsError as error: + print("droiddeck-game-env: PowerVR graphics runtime unavailable (%s)" % error, file=sys.stderr) + sys.exit(1) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 3edc114c8..b8137ddc1 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -1,5 +1,15 @@ #!/bin/bash set -u +# A GPU that is not an Adreno (BL_RAISE_NOFILE=1): the open-files limit goes up to the hard limit +# for the whole session tree (gamescope, Steam, games). A Mali PanVK session ran out after ten +# minutes ("Too many open files" importing sync files and dma-bufs) and froze. +if [ "${BL_RAISE_NOFILE:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ]; then + bl_nofile_was="$(ulimit -Sn)/$(ulimit -Hn)" + ulimit -Sn "$(ulimit -Hn)" 2>/dev/null || true + echo "== open files: soft/hard $bl_nofile_was -> soft $(ulimit -Sn)" + [ -n "${BL_DEBUG_DIR:-}" ] && echo "soft/hard $bl_nofile_was -> soft $(ulimit -Sn) (session tree)" > "$BL_DEBUG_DIR/limits.txt" 2>/dev/null + unset bl_nofile_was +fi export HOME=${HOME:-/root} mode=${1:-desktop} shift || true @@ -260,18 +270,78 @@ if [ "${BL_VKBRIDGE:-0}" = 1 ]; then unset TU_DEBUG if [ -z "${BL_INSIDE:-}" ]; then step "vkbridge self-test" - timeout 60 /usr/local/bin/droiddeck-vkbridge-smoke 2>&1 | sed 's/^/== vkbridge: /' + bl_vkb_smoke=$(timeout 60 /usr/local/bin/droiddeck-vkbridge-smoke 2>&1) + printf '%s\n' "$bl_vkb_smoke" | sed 's/^/== vkbridge: /' + bl_vkb_why= + if printf '%s\n' "$bl_vkb_smoke" | grep -qE '^FAIL memory sharing'; then + bl_vkb_why=memory + elif printf '%s\n' "$bl_vkb_smoke" | grep -qE '^FAIL (image clear|draw)'; then + bl_vkb_why=draw + fi + if [ -n "$bl_vkb_why" ] && [ -r "${BL_VKBRIDGE_FALLBACK_ICD:-}" ]; then + export VK_DRIVER_FILES=$BL_VKBRIDGE_FALLBACK_ICD VK_ICD_FILENAMES=$BL_VKBRIDGE_FALLBACK_ICD BL_VKBRIDGE=0 + export BL_OPEN_DRIVER=1 BL_DRM_STANDIN=/dev/mali0 + [ "${BL_STEAM_GPU_COMPOSITING:-0}" = 1 ] || export BL_STEAM_SOFT_COMPOSITING=1 + if [ "$bl_vkb_why" = memory ]; then + echo "== vkbridge: memory sharing = none (no method passed the server's self-test); this session uses the PanVK pair instead ($BL_VKBRIDGE_FALLBACK_ICD)" + else + echo "== vkbridge: self-test failed images or draws; this session uses the PanVK pair instead ($BL_VKBRIDGE_FALLBACK_ICD)" + fi + [ -n "${BL_DEBUG_DIR:-}" ] && printf '%s\n%s\n' "$BL_VKBRIDGE_FALLBACK_ICD" "$bl_vkb_why" > "$BL_DEBUG_DIR/vkbridge-fallback" 2>/dev/null + elif [ -n "$bl_vkb_why" ]; then + echo "== vkbridge: self-test failed ($bl_vkb_why) and no PanVK pair is installed to fall back on; expect a black screen or a lost device - set a Mali driver pair in Setup" + fi + unset bl_vkb_why + unset bl_vkb_smoke if command -v vulkaninfo >/dev/null 2>&1; then timeout 30 vulkaninfo --summary 2>&1 | sed -n '/^Devices:/,$p' | head -40 | sed 's/^/== vulkaninfo: /' fi fi - echo "== vulkan driver: Mali bridge ($bl_vkb_icd, server at $VKBRIDGE_SOCKET)" + [ "${BL_VKBRIDGE:-0}" = 1 ] && echo "== vulkan driver: Mali bridge ($bl_vkb_icd, server at $VKBRIDGE_SOCKET)" else echo "== vulkan driver: Mali bridge requested but NOT usable (icd $bl_vkb_icd, socket ${VKBRIDGE_SOCKET:-unset}; missing:$bl_vkb_missing)" [ -n "${BL_INSIDE:-}" ] || exit 69 fi unset bl_vkb_icd bl_vkb_missing fi +if [ "${BL_VENUS:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ] && command -v vulkaninfo >/dev/null 2>&1; then + if bl_vn_info=$(timeout 30 vulkaninfo 2>/dev/null); then + case $(printf '%s\n' "$bl_vn_info" | grep -m1 'nullDescriptor') in + *true*) echo "== venus: nullDescriptor present (Zink keeps the GPU)" ;; + *) + export GALLIUM_DRIVER=llvmpipe LIBGL_ALWAYS_SOFTWARE=1 + unset MESA_LOADER_DRIVER_OVERRIDE LIBGL_KOPPER_DRI2 + echo "== venus: no nullDescriptor on this device even with the Mali compatibility layer; Steam's GL and Xwayland use llvmpipe (software)" + ;; + esac + else + echo "== venus: vulkaninfo failed or timed out; GL left on Zink" + fi + unset bl_vn_info +fi +if [ "${BL_GAME_LOGS:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ] && [ -n "${BL_DEBUG_DIR:-}" ]; then + ( + { + echo "== VK_DRIVER_FILES=${VK_DRIVER_FILES:-} VK_ICD_FILENAMES=${VK_ICD_FILENAMES:-} BL_DRM_STANDIN=${BL_DRM_STANDIN:-}" + if command -v vulkaninfo >/dev/null 2>&1; then + timeout 10 vulkaninfo --summary + echo "== vulkaninfo exit: $?" + if timeout 20 vulkaninfo > "$BL_DEBUG_DIR/vulkan-full.txt" 2>&1; then + echo "== device extensions (full list in vulkan-full.txt):" + sed -n '/^Device Extensions:/,/^$/p' "$BL_DEBUG_DIR/vulkan-full.txt" | grep -o 'VK_[A-Za-z0-9_]*' | sort -u | tr '\n' ' ' | fold -s -w 200 + echo + echo "== key features:" + grep -E '^[[:space:]]+(robustBufferAccess2?|robustImageAccess2?|nullDescriptor|logicOp|fillModeNonSolid|shaderClipDistance|shaderCullDistance|dualSrcBlend|geometryShader|scalarBlockLayout|timelineSemaphore|dynamicRendering|synchronization2|maintenance4|maintenance5|maintenance6|shaderDrawParameters|descriptorIndexing|bufferDeviceAddress|vulkanMemoryModel|hostQueryReset|imagelessFramebuffer|samplerYcbcrConversion|shaderInt64|shaderFloat64|shaderInt16|shaderFloat16|storageBuffer16BitAccess|extendedDynamicState[23]?|vertexInputDynamicState|customBorderColors|customBorderColorWithoutFormat|depthClipEnable|multiViewport|textureCompressionBC|indexTypeUint8|rectangularLines|bresenhamLines|transformFeedback|geometryStreams|graphicsPipelineLibrary|pushDescriptor|primitiveTopologyListRestart|dynamicRenderingLocalRead|shaderDemoteToHelperInvocation|subgroupSizeControl|computeFullSubgroups|inlineUniformBlock|pipelineCreationCacheControl) = ' "$BL_DEBUG_DIR/vulkan-full.txt" | sed 's/^[[:space:]]*/ /' | sort -u + else + echo "== full vulkaninfo failed or timed out (exit $?)" + fi + else + echo "== vulkaninfo is not in this runtime; GPU nodes:" + ls -l /dev/kgsl-3d0 /dev/mali0 /dev/dri 2>&1 + fi + } > "$BL_DEBUG_DIR/vulkan-summary.txt" 2>&1 + ) & +fi export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0} start_agent() { @@ -283,6 +353,38 @@ nap() { [ -n "${bl_nap_fd:-}" ] || exec {bl_nap_fd}<> <(:) read -r -t "$1" -u "$bl_nap_fd" _ 2>/dev/null || : } +# Which program a driver error came from: Mesa's lines ("radv/amdgpu: The CS has been rejected", +# "ZINK: vkQueueWaitIdle failed") carry no process name. For the first three minutes of an open +# driver pair's session, every process that appears goes into processes.txt with the session log's +# line count at that moment, so an error's line number in session.log points at what had just +# started. Pure bash over /proc (no execs: see nap). +watch_processes() { + local out=$BL_DEBUG_DIR/processes.txt log=$BL_DEBUG_DIR/session.log i p pid comm lines + local -a argv new + local -A seen=() + echo "# seconds session.log-lines pid name command line" > "$out" 2>/dev/null || return 0 + for ((i = 0; i < 360; i++)); do + new=() + for p in /proc/[0-9]*; do + pid=${p#/proc/} + [ -n "${seen[$pid]:-}" ] && continue + seen[$pid]=1 + read -r comm < "$p/comm" 2>/dev/null || continue + argv=() + mapfile -d '' -t argv < "$p/cmdline" 2>/dev/null + new+=("$pid"$'\t'"$comm"$'\t'"${argv[*]:0:8}") + done + if [ ${#new[@]} -gt 0 ]; then + lines=0 + [ -r "$log" ] && { mapfile -t argv < "$log"; lines=${#argv[@]}; argv=(); } + for p in "${new[@]}"; do printf '%s\t%s\t%.200s\n' "$SECONDS" "$lines" "$p"; done >> "$out" + fi + nap 0.5 + done +} +if [ "${BL_OPEN_DRIVER:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ] && [ -n "${BL_DEBUG_DIR:-}" ]; then + watch_processes & +fi export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} @@ -400,6 +502,12 @@ if [ -z "${BL_INSIDE:-}" ]; then echo "== gamescope: games at ${nested}x${nested_h} stretched to the ${width}x${height} output" fi fi + backend=${BL_GAMESCOPE_BACKEND:-wayland} + case "$backend" in + wayland) ;; + sdl) export SDL_VIDEODRIVER=wayland ;; + *) echo "== unsupported gamescope backend: $backend"; exit 64 ;; + esac if [ "$mode" = steam ] && [ "${BL_CURSOR_COMPOSITE:-0}" = 1 ]; then export gamescope_cursor_composite=2 fi @@ -410,7 +518,7 @@ if [ -z "${BL_INSIDE:-}" ]; then export gamescope_composite_force=1 echo "== gamescope: composites every frame (Mali bridge: the Steam UI flickers on separate planes)" fi - args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$nested_h") + args=(--backend "$backend" --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$nested_h") if [ "$nested" != "$width" ] || [ "$nested_h" != "$height" ]; then args+=(-S stretch) export BL_STRETCHED=1 @@ -1145,6 +1253,20 @@ case $mode in export STEAM_STEAMRT_RECURSION_GUARD=1 export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} export SDL_VIDEO_X11_DGAMOUSE=0 + bl_steam_path=$PATH + bl_steam_angle=vulkan + bl_steam_presentation=() + if [ "${BL_STEAM_GL_PRESENT:-}" = 1 ]; then + bl_steam_path="/usr/local/libexec/droiddeck-steam:$PATH" + export DISABLE_GAMESCOPE_WSI=1 LIBGL_KOPPER_DISABLE=true + bl_steam_angle=gl + bl_steam_presentation=(-cef-disable-gpu-compositing) + echo "== steam: system PowerVR uses OpenGL/DRI3 through Zink with software CEF compositing" + elif [ "${BL_STEAM_SOFT_COMPOSITING:-}" = 1 ]; then + bl_steam_presentation=(-cef-disable-gpu-compositing) + echo "== steam: open driver pair - software CEF compositing, GPU raster" + fi + _bl_cef=(-cef-force-gpu "${bl_steam_presentation[@]}" -cef-use-gl=angle "-cef-use-angle=$bl_steam_angle") decky_loader=$HOME/homebrew/services/PluginLoader if [ -f "$HOME/.droiddeck-decky-enabled" ] && [ -x "$decky_loader" ]; then decky_arch=$(cat "$HOME/homebrew/services/.droiddeck-decky-arch" 2>/dev/null || echo unknown) @@ -1194,7 +1316,9 @@ case $mode in fi _bl_ui=(-gamepadui) [ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=() - _bl_cef=(-cef-force-gpu -cef-use-gl=angle -cef-use-angle=vulkan) + # bl_steam_angle / bl_steam_presentation (set above) default to the Adreno values (vulkan, + # none); only the system-PowerVR path (BL_STEAM_GL_PRESENT=1) changes them. + _bl_cef=(-cef-force-gpu "${bl_steam_presentation[@]}" -cef-use-gl=angle "-cef-use-angle=${bl_steam_angle:-vulkan}") [ "${steam_cef_gpu:-1}" = 1 ] || _bl_cef=(-cef-disable-gpu) if [ "${BL_DESKTOP:-}" = 1 ] && [ "${BL_STEAM_UI:-}" = desktop ]; then _bl_cef=(-cef-disable-gpu) @@ -1547,6 +1671,36 @@ case $mode in done ) & stop_watcher=$! + if [ "${BL_VENUS:-0}" = 1 ] && [ -x /usr/local/bin/droiddeck-glprobe ] && [ "${GALLIUM_DRIVER:-}" = zink ]; then + LIBGL_DEBUG=verbose MESA_LOG_LEVEL=debug timeout 20 /usr/local/bin/droiddeck-glprobe 2>&1 | grep -v drirc | head -60 | sed 's/^/== glprobe: /' + if [ "${PIPESTATUS[0]}" = 1 ]; then + VK_LOADER_DEBUG=error,warn timeout 20 /usr/local/bin/droiddeck-glprobe 2>&1 | grep -v drirc | head -40 | sed 's/^/== glprobe (loader): /' + DROIDDECK_MALI_COMPAT_TRACE=1 MESA_DEBUG=1 MESA_LOG_LEVEL=debug LIBGL_DEBUG=verbose ZINK_DEBUG=mem VN_DEBUG=init,result,log_ctx_info \ + timeout 30 /usr/local/bin/droiddeck-glprobe > "${BL_DEBUG_DIR:-/tmp}/glprobe-trace.txt" 2>&1 + grep -v drirc "${BL_DEBUG_DIR:-/tmp}/glprobe-trace.txt" | head -120 | sed 's/^/== glprobe (trace): /' + echo "== glprobe (trace): last pre-device query before Zink gave up: $(grep 'droiddeck-mali-compat: trace:' "${BL_DEBUG_DIR:-/tmp}/glprobe-trace.txt" | grep -v 'trace: extensions:' | tail -1)" + command -v vulkaninfo >/dev/null 2>&1 && DROIDDECK_MALI_COMPAT_TRACE=1 timeout 20 vulkaninfo --summary 2>&1 | head -40 | sed 's/^/== glprobe (vulkaninfo): /' + bl_gl_ok= + for bl_variant in "ZINK_DEBUG=" "ZINK_DEBUG= ZINK_DESCRIPTORS=auto" "ZINK_DEBUG= LIBGL_KOPPER_DISABLE=true" "ZINK_DEBUG= DROIDDECK_MALI_COMPAT=0"; do + if env $bl_variant timeout 20 /usr/local/bin/droiddeck-glprobe 2>&1 | grep -v drirc | head -20 | sed "s/^/== glprobe ($bl_variant): /"; [ "${PIPESTATUS[0]}" = 0 ]; then + bl_gl_ok=$bl_variant + break + fi + done + if [ -n "$bl_gl_ok" ] && [ "$bl_gl_ok" != "ZINK_DEBUG= DROIDDECK_MALI_COMPAT=0" ]; then + for bl_kv in $bl_gl_ok; do + case $bl_kv in *=) unset "${bl_kv%=}" ;; *) export "$bl_kv" ;; esac + done + echo "== venus: GL through Zink starts with: $bl_gl_ok (the Steam client gets that)" + else + [ -n "$bl_gl_ok" ] && echo "== venus: Zink starts only without the Mali compatibility layer (then without nullDescriptor); not used" + export GALLIUM_DRIVER=llvmpipe LIBGL_ALWAYS_SOFTWARE=1 + unset MESA_LOADER_DRIVER_OVERRIDE LIBGL_KOPPER_DRI2 + echo "== venus: GL through Zink did not start (see glprobe above); the Steam client's GL uses llvmpipe (software)" + fi + unset bl_gl_ok bl_variant bl_kv + fi + fi rm -f "$HOME/.bl-restart-game" "$HOME/.bl-restarted-for" while :; do rm -f "$HOME/.bl-restart-once" "$HOME/.bl-restart-lost" @@ -1572,6 +1726,8 @@ case $mode in desktop_ui_pid=$! fi steam_started=$SECONDS + PATH="$bl_steam_path" \ + BL_STEAM_WEBHELPER="$steam_root/steamrtarm64/steamwebhelper" \ "${steam_client[@]}" "${_bl_ui[@]}" "${_bl_cdp[@]}" "${_bl_lang[@]}" -clientbeta "$steam_channel" \ -no-cef-sandbox -cef-ozone-platform=x11 "${_bl_cef[@]}" "$@" rc=$? diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-taskset b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-taskset new file mode 100644 index 000000000..8973fc7f5 --- /dev/null +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-taskset @@ -0,0 +1,17 @@ +#!/bin/bash +# Staged as taskset in a PATH directory used only by the Steam client. +# Valve's ARM64 helper uses `taskset 0x7c /path/to/steamwebhelper ...` and exits +# before starting the browser when Android denies sched_setaffinity. +set -u +if [ "$#" -ge 2 ] && [[ "$1" =~ ^(0[xX])?[0-9a-fA-F]+$ ]] && + [ -n "${BL_STEAM_WEBHELPER:-}" ] && [ "$2" -ef "$BL_STEAM_WEBHELPER" ]; then + mask=$1 + shift + # Pin this wrapper before exec; never retry a helper that has already run. + if ! /usr/bin/taskset -p "$mask" "$$" >/dev/null 2>&1; then + echo "droiddeck: Steam helper CPU pinning unavailable; using the allowed CPUs" >&2 + fi + exec -- "$@" +fi +# Queries, existing-process changes, and other programs retain taskset's behavior. +exec /usr/bin/taskset "$@" diff --git a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility index e37cddfbf..8247201a3 100755 --- a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility +++ b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility @@ -174,6 +174,9 @@ bl_components() { local -A cm_sums=() while IFS=$'\\t' read -r scope t want comp rel sum src; do [ -n "$scope" ] || continue + # The selected system PowerVR runtime supplies DXVK at launch. Keep saved selections + # for a later manual-driver session without rewriting this Proton's bundled files now. + [ "$comp" = dxvk ] && [ -n "${DROIDDECK_POWERVR_RUNTIME:-}" ] && continue cm_rows+=("$comp"$'\\t'"$rel"$'\\t'"$sum"$'\\t'"$src") [ "$src" != "-" ] && [ -f "$base/$rel" ] && cm_files+=("$base/$rel") done <<< "$cm_selected" diff --git a/tools/linuxfs/preload/drm.c b/tools/linuxfs/preload/drm.c index 37784c3d9..9c40a3929 100644 --- a/tools/linuxfs/preload/drm.c +++ b/tools/linuxfs/preload/drm.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,10 @@ #include #define MAX_HANDLES 4096 +#ifndef F_DUPFD_QUERY +#define F_DUPFD_QUERY 1027 +#endif +#define PVR_HANDLE_BIT UINT32_C(0x80000000) struct gem_close { uint32_t handle; @@ -23,11 +28,14 @@ struct handle_entry { uint32_t handle; int dma_fd; int drm_fd; + int owned_drm_fd; /* PowerVR: pin the open file description, not its reusable fd number. */ + int powervr; }; static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static struct handle_entry handle_entries[MAX_HANDLES]; static uint32_t next_handle = 1; +static uint32_t next_pvr_handle = PVR_HANDLE_BIT; static dev_t kgsl_dev; static int kgsl_state; @@ -37,12 +45,40 @@ static int is_kgsl(int fd) { if (kgsl_state == 0) { const char *standin = getenv("BL_DRM_STANDIN"); kgsl_state = stat("/dev/kgsl-3d0", &st) == 0 || (standin && *standin && stat(standin, &st) == 0) ? 1 : -1; - kgsl_dev = st.st_rdev; + if (kgsl_state > 0) kgsl_dev = st.st_rdev; } pthread_mutex_unlock(&lock); return kgsl_state > 0 && fstat(fd, &st) == 0 && S_ISCHR(st.st_mode) && st.st_rdev == kgsl_dev; } +/* Keep the opt-in separate from GPU detection: manual ICDs must retain native DRM behavior. */ +struct bl_drm_version { + int major, minor, patch; + size_t name_len; + char *name; + size_t date_len; + char *date; + size_t desc_len; + char *desc; +}; +#define BL_DRM_VERSION _IOWR('d', 0x00, struct bl_drm_version) + +static int is_powervr(int fd) { + const char *enabled = getenv("BL_POWERVR_DRM_HANDLES"); + if (!enabled || strcmp(enabled, "1")) return 0; + const char *path = getenv("BL_POWERVR_RENDER_NODE"); + if (!path || !*path) return 0; + struct stat node, candidate; + if (stat(path, &node) || fstat(fd, &candidate) || + !S_ISCHR(candidate.st_mode) || !S_ISCHR(node.st_mode) || + candidate.st_rdev != node.st_rdev) return 0; + char name[16] = {0}; + struct bl_drm_version version = {.name_len = sizeof(name) - 1, .name = name}; + /* Bypass the preload's ioctl dispatcher to avoid recursively entering this hook. */ + return syscall(SYS_ioctl, fd, BL_DRM_VERSION, &version) == 0 && + version.name_len == 3 && !memcmp(name, "pvr", 3); +} + static void *real(const char *name) { void *fn = dlsym(RTLD_NEXT, name); if (!fn) { @@ -89,21 +125,86 @@ static int same_file_description(int a, int b) { return ret; } +/* + * Android kernels before 6.10 have no F_DUPFD_QUERY, while the app seccomp policy blocks kcmp. + * F_SETOWN_EX updates the open file description, so a duplicate observes a temporary owner marker + * and an independent open of the same render node does not. Restore the original owner before + * returning; no signal is armed because these DRM descriptors do not have O_ASYNC set. + */ +static int same_owner_description(int owned, int fd) { + struct f_owner_ex original_owned, original_fd, observed; + if (fcntl(owned, F_GETOWN_EX, &original_owned) < 0 || + fcntl(fd, F_GETOWN_EX, &original_fd) < 0) { + return -1; + } + const struct f_owner_ex candidates[] = { + {.type = F_OWNER_TID, .pid = getpid()}, + {.type = F_OWNER_PID, .pid = getpid()}, + {.type = F_OWNER_PGRP, .pid = getpgrp()}, + }; + const struct f_owner_ex *marker = NULL; + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { + if (candidates[i].type != original_fd.type || candidates[i].pid != original_fd.pid) { + marker = &candidates[i]; + break; + } + } + if (!marker || fcntl(owned, F_SETOWN_EX, marker) < 0) return -1; + int observed_result = fcntl(fd, F_GETOWN_EX, &observed); + int observed_errno = errno; + int restore_result = fcntl(owned, F_SETOWN_EX, &original_owned); + int restore_errno = errno; + if (restore_result < 0) { + errno = restore_errno; + return -1; + } + if (observed_result < 0) { + errno = observed_errno; + return -1; + } + return observed.type == marker->type && observed.pid == marker->pid; +} + +static int same_owned_description(int owned, int fd) { + int saved_errno = errno; + int result = fcntl(owned, F_DUPFD_QUERY, fd); + if (result >= 0) { + errno = saved_errno; + return result == 1; + } +#ifdef SYS_kcmp + long compared = syscall(SYS_kcmp, getpid(), getpid(), 0 /* KCMP_FILE */, owned, fd); + if (compared >= 0) { + errno = saved_errno; + return compared == 0; + } +#endif + int owner_match = same_owner_description(owned, fd); + errno = saved_errno; + return owner_match == 1; +} + static int find_handle_locked(int fd, uint32_t handle) { for (int i = 0; i < MAX_HANDLES; i++) { if (handle_entries[i].handle == handle && - same_file_description(handle_entries[i].drm_fd, fd)) { + (handle_entries[i].powervr + ? same_owned_description(handle_entries[i].owned_drm_fd, fd) + : same_file_description(handle_entries[i].drm_fd, fd))) { return i; } } return -1; } -static uint32_t alloc_handle_locked(void) { +static uint32_t alloc_handle_locked(int powervr) { for (int tries = 0; tries <= MAX_HANDLES; tries++) { - uint32_t candidate = next_handle++; - if (next_handle == 0) { - next_handle = 1; + uint32_t candidate; + if (powervr) { + candidate = next_pvr_handle++; + if (!(next_pvr_handle & PVR_HANDLE_BIT)) next_pvr_handle = PVR_HANDLE_BIT; + } else { + candidate = next_handle++; + if (next_handle == PVR_HANDLE_BIT) next_handle = 1; } if (candidate == 0) { continue; @@ -123,25 +224,49 @@ static uint32_t alloc_handle_locked(void) { } int drmPrimeFDToHandle(int fd, int prime_fd, uint32_t *handle) { - if (!is_kgsl(fd)) { + int kgsl = is_kgsl(fd); + int powervr = !kgsl && is_powervr(fd); + if (!kgsl) { int (*fn)(int, int, uint32_t *) = (int (*)(int, int, uint32_t *)) real("drmPrimeFDToHandle"); - return fn ? fn(fd, prime_fd, handle) : -ENOSYS; + if (!fn) return -ENOSYS; + int result = fn(fd, prime_fd, handle); + if (!result || !powervr || errno != EINVAL) return result; + } + if (!handle) { errno = EINVAL; return -EINVAL; } + int owner = -1; + if (powervr) { + owner = fcntl(fd, F_DUPFD_CLOEXEC, 0); + if (owner < 0) return -errno; + /* The old-kernel owner probe mutates shared OFD state briefly; serialize it with lookups. */ + pthread_mutex_lock(&lock); + int owner_matches = same_owned_description(owner, fd); + pthread_mutex_unlock(&lock); + if (!owner_matches) { + close(owner); + errno = EOPNOTSUPP; + return -EOPNOTSUPP; + } } int dup_fd = fcntl(prime_fd, F_DUPFD_CLOEXEC, 0); if (dup_fd < 0) { - return -errno; + int error = errno; + if (owner >= 0) close(owner); + errno = error; + return -error; } int ret = -ENOMEM; pthread_mutex_lock(&lock); for (int i = 0; i < MAX_HANDLES; i++) { if (handle_entries[i].handle == 0) { - uint32_t token = alloc_handle_locked(); + uint32_t token = alloc_handle_locked(powervr); if (!token) { break; } handle_entries[i].handle = token; handle_entries[i].dma_fd = dup_fd; handle_entries[i].drm_fd = fd; + handle_entries[i].owned_drm_fd = owner; + handle_entries[i].powervr = powervr; *handle = token; ret = 0; break; @@ -150,24 +275,32 @@ int drmPrimeFDToHandle(int fd, int prime_fd, uint32_t *handle) { pthread_mutex_unlock(&lock); if (ret) { close(dup_fd); + if (owner >= 0) close(owner); + errno = -ret; } return ret; } int drmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, int *prime_fd) { - if (!is_kgsl(fd)) { - int (*fn)(int, uint32_t, uint32_t, int *) = (int (*)(int, uint32_t, uint32_t, int *)) real("drmPrimeHandleToFD"); - return fn ? fn(fd, handle, flags, prime_fd) : -ENOSYS; - } + int kgsl = is_kgsl(fd); int ret = -EINVAL; pthread_mutex_lock(&lock); int index = find_handle_locked(fd, handle); if (index >= 0) { - *prime_fd = fcntl(handle_entries[index].dma_fd, F_DUPFD_CLOEXEC, 0); - ret = *prime_fd < 0 ? -errno : 0; + if (!prime_fd || (handle_entries[index].powervr && (flags & ~(O_CLOEXEC | O_RDWR)))) { + errno = EINVAL; + } else { + int command = handle_entries[index].powervr && !(flags & O_CLOEXEC) ? F_DUPFD : F_DUPFD_CLOEXEC; + *prime_fd = fcntl(handle_entries[index].dma_fd, command, 0); + ret = *prime_fd < 0 ? -errno : 0; + } } pthread_mutex_unlock(&lock); - return ret; + if (index >= 0 || kgsl) return ret; + /* Unknown handles (including real PowerVR GEM handles) retain kernel semantics. */ + int (*fn)(int, uint32_t, uint32_t, int *) = + (int (*)(int, uint32_t, uint32_t, int *)) real("drmPrimeHandleToFD"); + return fn ? fn(fd, handle, flags, prime_fd) : -ENOSYS; } static int release_handle(int fd, uint32_t handle) { @@ -176,6 +309,7 @@ static int release_handle(int fd, uint32_t handle) { int index = find_handle_locked(fd, handle); if (index >= 0) { close(handle_entries[index].dma_fd); + if (handle_entries[index].powervr) close(handle_entries[index].owned_drm_fd); handle_entries[index] = (struct handle_entry){0}; found = 1; } @@ -184,22 +318,23 @@ static int release_handle(int fd, uint32_t handle) { } int drmCloseBufferHandle(int fd, uint32_t handle) { - if (!is_kgsl(fd)) { - int (*fn)(int, uint32_t) = (int (*)(int, uint32_t)) real("drmCloseBufferHandle"); - return fn ? fn(fd, handle) : -ENOSYS; - } - return release_handle(fd, handle) ? 0 : -EINVAL; + if (release_handle(fd, handle)) return 0; + if (is_kgsl(fd)) { errno = EINVAL; return -EINVAL; } + int (*fn)(int, uint32_t) = (int (*)(int, uint32_t)) real("drmCloseBufferHandle"); + return fn ? fn(fd, handle) : -ENOSYS; } __attribute__((visibility("hidden"))) int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) { - if (request != GEM_CLOSE || !arg || !is_kgsl(fd)) { + if (request != GEM_CLOSE || !arg) { return 0; } if (release_handle(fd, ((struct gem_close *) arg)->handle)) { *rc = 0; - } else { + } else if (is_kgsl(fd)) { errno = EINVAL; *rc = -1; + } else { + return 0; } return 1; } diff --git a/tools/linuxfs/preload/netif.c b/tools/linuxfs/preload/netif.c index 55da3c21e..d7f56e32a 100644 --- a/tools/linuxfs/preload/netif.c +++ b/tools/linuxfs/preload/netif.c @@ -596,6 +596,8 @@ static FILE *synthesize(const char *table) { int bl_status_without_tracer(const char *path, int flags, const void *caller) __attribute__((visibility("hidden"))); +const char *bl_oom_adj_redirect(const char *path, int writing) __attribute__((visibility("hidden"))); + static FILE *open_stream(fopen_fn real, const char *path, const char *mode, const void *caller) { int status = mode && mode[0] == 'r' && !strchr(mode, '+') ? bl_status_without_tracer(path, O_RDONLY | O_CLOEXEC, caller) : -1; @@ -604,6 +606,7 @@ static FILE *open_stream(fopen_fn real, const char *path, const char *mode, cons if (copy == NULL) close(status); return copy; } + path = bl_oom_adj_redirect(path, mode && (mode[0] == 'w' || mode[0] == 'a' || strchr(mode, '+'))); int table = path && mode && mode[0] == 'r' ? denied_table(path) : -1; FILE *f; if (table >= 0 && denied_recently(table, path) && (f = synthesize(net_tables[table])) != NULL) return f; diff --git a/tools/linuxfs/preload/oomadj.c b/tools/linuxfs/preload/oomadj.c new file mode 100644 index 000000000..19e5f4251 --- /dev/null +++ b/tools/linuxfs/preload/oomadj.c @@ -0,0 +1,47 @@ +/* + * Based on jdmg28's DroidDeck-Mali fork (commit 6be00e00, GPL-3.0): steam-launch-wrapper runs every + * game with --oom-score-adjust 900. Android's lmkd reads 900 as CACHED_APP_MIN_ADJ and kills the + * foreground game first when memory runs short; on Linux 6.1 the raise also becomes the process's + * permanent minimum. An app's processes cannot go below the score they inherit, so the only writes + * that succeed from in here are raises: every write-open of an oom_score_adj file goes to /dev/null. + */ +#define _GNU_SOURCE +#include +#include +#include + +static int is_oom_score_adj(const char *path) { + if (path == NULL || strncmp(path, "/proc/", 6) != 0) return 0; + const char *p = path + 6; + if (strncmp(p, "self/", 5) == 0) { + p += 5; + } else if (strncmp(p, "thread-self/", 12) == 0) { + p += 12; + } else { + const char *digits = p; + while (*p >= '0' && *p <= '9') p++; + if (p == digits || *p != '/') return 0; + p++; + if (strncmp(p, "task/", 5) == 0) { + p += 5; + digits = p; + while (*p >= '0' && *p <= '9') p++; + if (p == digits || *p != '/') return 0; + p++; + } + } + return strcmp(p, "oom_score_adj") == 0 || strcmp(p, "oom_adj") == 0; +} + +__attribute__((visibility("hidden"))) const char *bl_oom_adj_redirect(const char *path, int writing) { + static int logged; + if (!writing || !is_oom_score_adj(path)) return path; + if (!__atomic_exchange_n(&logged, 1, __ATOMIC_RELAXED)) { + static const char msg[] = "droiddeck: an oom_score_adj raise was dropped (Android would treat this program as a cached app)\n"; + int saved = errno; + ssize_t ignored = write(STDERR_FILENO, msg, sizeof(msg) - 1); + (void)ignored; + errno = saved; + } + return "/dev/null"; +} diff --git a/tools/linuxfs/preload/opens.c b/tools/linuxfs/preload/opens.c index 332f9d89a..4113062cd 100644 --- a/tools/linuxfs/preload/opens.c +++ b/tools/linuxfs/preload/opens.c @@ -14,6 +14,7 @@ int bl_status_without_tracer(const char *path, int flags, const void *caller) __attribute__((visibility("hidden"))); int bl_writable_retry(int dirfd, const char *path, int flags) __attribute__((visibility("hidden"))); int bl_ntsync_open(const char *path, int flags) __attribute__((visibility("hidden"))); +const char *bl_oom_adj_redirect(const char *path, int writing) __attribute__((visibility("hidden"))); int bl_venc_open(const char *path, int flags) __attribute__((visibility("hidden"))); int bl_vdec_open(const char *path, int flags) __attribute__((visibility("hidden"))); int bl_dircache_create(int dirfd, const char *path, int flags, mode_t mode, @@ -53,6 +54,7 @@ int open(const char *path, int flags, ...) { errno = EACCES; return -1; } + path = bl_oom_adj_redirect(path, (flags & O_ACCMODE) != O_RDONLY); if ((fd = bl_status_without_tracer(path, flags, __builtin_return_address(0))) >= 0) return fd; if ((fd = bl_ntsync_open(path, flags)) != -2) return fd; if ((fd = bl_venc_open(path, flags)) != -2) return fd; @@ -80,6 +82,7 @@ int openat(int dirfd, const char *path, int flags, ...) { errno = EACCES; return -1; } + path = bl_oom_adj_redirect(path, (flags & O_ACCMODE) != O_RDONLY); if ((fd = bl_status_without_tracer(path, flags, __builtin_return_address(0))) >= 0) return fd; if ((fd = bl_ntsync_open(path, flags)) != -2) return fd; if ((fd = bl_venc_open(path, flags)) != -2) return fd; diff --git a/tools/linuxfs/preload/venus.c b/tools/linuxfs/preload/venus.c new file mode 100644 index 000000000..4feb9ee03 --- /dev/null +++ b/tools/linuxfs/preload/venus.c @@ -0,0 +1,18 @@ +/* + * Zink keeps kopper when the session draws through Venus. + * + * On Mali the session reaches the GPU through Venus over vtest, and Xwayland has no DRM node to + * build glamor on, so it offers no DRI3. Without DRI3, Zink can only present through kopper (its + * Vulkan WSI path). Steam's steamwebhelper.sh exports LIBGL_KOPPER_DISABLE=true for the client + * interface unconditionally, so on Mali every GL context the interface asked for failed and the + * webhelper restarted until Steam gave up on GPU acceleration. Steam restores that script when + * it verifies its install, so the variable is dropped here instead, before Mesa reads it. + */ +#define _GNU_SOURCE 1 +#include +#include + +__attribute__((constructor)) static void bl_venus_keep_kopper(void) { + const char *debug = getenv("VN_DEBUG"); + if (debug && strstr(debug, "vtest")) unsetenv("LIBGL_KOPPER_DISABLE"); +} diff --git a/tools/linuxfs/xclipse-layer/VkLayer_droiddeck_xclipse.json b/tools/linuxfs/xclipse-layer/VkLayer_droiddeck_xclipse.json new file mode 100644 index 000000000..888d0a89d --- /dev/null +++ b/tools/linuxfs/xclipse-layer/VkLayer_droiddeck_xclipse.json @@ -0,0 +1,11 @@ +{ + "file_format_version": "1.2.0", + "layer": { + "name": "VK_LAYER_DROIDDECK_xclipse", + "type": "GLOBAL", + "library_path": "./libVkLayer_droiddeck_xclipse.so", + "api_version": "1.3.0", + "implementation_version": "1", + "description": "Workarounds for Samsung Xclipse drivers loaded through libhybris" + } +} diff --git a/tools/linuxfs/xclipse-layer/device_chain.inc b/tools/linuxfs/xclipse-layer/device_chain.inc new file mode 100644 index 000000000..e8a131411 --- /dev/null +++ b/tools/linuxfs/xclipse-layer/device_chain.inc @@ -0,0 +1,180 @@ +/* Generated by gen_device_chain.py from Vulkan-Headers 1.3.275; do not edit. */ +DEVICE_CHAIN(VK_STRUCTURE_TYPE_DEVICE_DEVICE_MEMORY_REPORT_CREATE_INFO_EXT, VkDeviceDeviceMemoryReportCreateInfoEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_DEVICE_DIAGNOSTICS_CONFIG_CREATE_INFO_NV, VkDeviceDiagnosticsConfigCreateInfoNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_DEVICE_GROUP_DEVICE_CREATE_INFO, VkDeviceGroupDeviceCreateInfo) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_DEVICE_MEMORY_OVERALLOCATION_CREATE_INFO_AMD, VkDeviceMemoryOverallocationCreateInfoAMD) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_DEVICE_PRIVATE_DATA_CREATE_INFO, VkDevicePrivateDataCreateInfo) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_DEVICE_QUEUE_SHADER_CORE_CONTROL_CREATE_INFO_ARM, VkDeviceQueueShaderCoreControlCreateInfoARM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES, VkPhysicalDevice16BitStorageFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_4444_FORMATS_FEATURES_EXT, VkPhysicalDevice4444FormatsFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES, VkPhysicalDevice8BitStorageFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR, VkPhysicalDeviceAccelerationStructureFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ADDRESS_BINDING_REPORT_FEATURES_EXT, VkPhysicalDeviceAddressBindingReportFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_AMIGO_PROFILING_FEATURES_SEC, VkPhysicalDeviceAmigoProfilingFeaturesSEC) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ASTC_DECODE_FEATURES_EXT, VkPhysicalDeviceASTCDecodeFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_DYNAMIC_STATE_FEATURES_EXT, VkPhysicalDeviceAttachmentFeedbackLoopDynamicStateFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_FEATURES_EXT, VkPhysicalDeviceAttachmentFeedbackLoopLayoutFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BLEND_OPERATION_ADVANCED_FEATURES_EXT, VkPhysicalDeviceBlendOperationAdvancedFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BORDER_COLOR_SWIZZLE_FEATURES_EXT, VkPhysicalDeviceBorderColorSwizzleFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES, VkPhysicalDeviceBufferDeviceAddressFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES_EXT, VkPhysicalDeviceBufferDeviceAddressFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CLUSTER_CULLING_SHADER_FEATURES_HUAWEI, VkPhysicalDeviceClusterCullingShaderFeaturesHUAWEI) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COHERENT_MEMORY_FEATURES_AMD, VkPhysicalDeviceCoherentMemoryFeaturesAMD) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COLOR_WRITE_ENABLE_FEATURES_EXT, VkPhysicalDeviceColorWriteEnableFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COMPUTE_SHADER_DERIVATIVES_FEATURES_NV, VkPhysicalDeviceComputeShaderDerivativesFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CONDITIONAL_RENDERING_FEATURES_EXT, VkPhysicalDeviceConditionalRenderingFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COOPERATIVE_MATRIX_FEATURES_KHR, VkPhysicalDeviceCooperativeMatrixFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COOPERATIVE_MATRIX_FEATURES_NV, VkPhysicalDeviceCooperativeMatrixFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COPY_MEMORY_INDIRECT_FEATURES_NV, VkPhysicalDeviceCopyMemoryIndirectFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CORNER_SAMPLED_IMAGE_FEATURES_NV, VkPhysicalDeviceCornerSampledImageFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COVERAGE_REDUCTION_MODE_FEATURES_NV, VkPhysicalDeviceCoverageReductionModeFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUBIC_CLAMP_FEATURES_QCOM, VkPhysicalDeviceCubicClampFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUBIC_WEIGHTS_FEATURES_QCOM, VkPhysicalDeviceCubicWeightsFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUDA_KERNEL_LAUNCH_FEATURES_NV, VkPhysicalDeviceCudaKernelLaunchFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CUSTOM_BORDER_COLOR_FEATURES_EXT, VkPhysicalDeviceCustomBorderColorFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEDICATED_ALLOCATION_IMAGE_ALIASING_FEATURES_NV, VkPhysicalDeviceDedicatedAllocationImageAliasingFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_BIAS_CONTROL_FEATURES_EXT, VkPhysicalDeviceDepthBiasControlFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLAMP_ZERO_ONE_FEATURES_EXT, VkPhysicalDeviceDepthClampZeroOneFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_CONTROL_FEATURES_EXT, VkPhysicalDeviceDepthClipControlFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_ENABLE_FEATURES_EXT, VkPhysicalDeviceDepthClipEnableFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_BUFFER_FEATURES_EXT, VkPhysicalDeviceDescriptorBufferFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES, VkPhysicalDeviceDescriptorIndexingFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_POOL_OVERALLOCATION_FEATURES_NV, VkPhysicalDeviceDescriptorPoolOverallocationFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_SET_HOST_MAPPING_FEATURES_VALVE, VkPhysicalDeviceDescriptorSetHostMappingFeaturesVALVE) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEVICE_GENERATED_COMMANDS_COMPUTE_FEATURES_NV, VkPhysicalDeviceDeviceGeneratedCommandsComputeFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEVICE_GENERATED_COMMANDS_FEATURES_NV, VkPhysicalDeviceDeviceGeneratedCommandsFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEVICE_MEMORY_REPORT_FEATURES_EXT, VkPhysicalDeviceDeviceMemoryReportFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DIAGNOSTICS_CONFIG_FEATURES_NV, VkPhysicalDeviceDiagnosticsConfigFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES, VkPhysicalDeviceDynamicRenderingFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_UNUSED_ATTACHMENTS_FEATURES_EXT, VkPhysicalDeviceDynamicRenderingUnusedAttachmentsFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXCLUSIVE_SCISSOR_FEATURES_NV, VkPhysicalDeviceExclusiveScissorFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_2_FEATURES_EXT, VkPhysicalDeviceExtendedDynamicState2FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_3_FEATURES_EXT, VkPhysicalDeviceExtendedDynamicState3FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, VkPhysicalDeviceExtendedDynamicStateFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_SPARSE_ADDRESS_SPACE_FEATURES_NV, VkPhysicalDeviceExtendedSparseAddressSpaceFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_RDMA_FEATURES_NV, VkPhysicalDeviceExternalMemoryRDMAFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT, VkPhysicalDeviceFaultFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, VkPhysicalDeviceFeatures2) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_2_FEATURES_EXT, VkPhysicalDeviceFragmentDensityMap2FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_FEATURES_EXT, VkPhysicalDeviceFragmentDensityMapFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_DENSITY_MAP_OFFSET_FEATURES_QCOM, VkPhysicalDeviceFragmentDensityMapOffsetFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR, VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT, VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_ENUMS_FEATURES_NV, VkPhysicalDeviceFragmentShadingRateEnumsFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADING_RATE_FEATURES_KHR, VkPhysicalDeviceFragmentShadingRateFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAME_BOUNDARY_FEATURES_EXT, VkPhysicalDeviceFrameBoundaryFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GLOBAL_PRIORITY_QUERY_FEATURES_KHR, VkPhysicalDeviceGlobalPriorityQueryFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_IMAGE_COPY_FEATURES_EXT, VkPhysicalDeviceHostImageCopyFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES, VkPhysicalDeviceHostQueryResetFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGELESS_FRAMEBUFFER_FEATURES, VkPhysicalDeviceImagelessFramebufferFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_2D_VIEW_OF_3D_FEATURES_EXT, VkPhysicalDeviceImage2DViewOf3DFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_FEATURES_EXT, VkPhysicalDeviceImageCompressionControlFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_COMPRESSION_CONTROL_SWAPCHAIN_FEATURES_EXT, VkPhysicalDeviceImageCompressionControlSwapchainFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_PROCESSING_2_FEATURES_QCOM, VkPhysicalDeviceImageProcessing2FeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_PROCESSING_FEATURES_QCOM, VkPhysicalDeviceImageProcessingFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES, VkPhysicalDeviceImageRobustnessFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_SLICED_VIEW_OF_3D_FEATURES_EXT, VkPhysicalDeviceImageSlicedViewOf3DFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_VIEW_MIN_LOD_FEATURES_EXT, VkPhysicalDeviceImageViewMinLodFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INDEX_TYPE_UINT8_FEATURES_EXT, VkPhysicalDeviceIndexTypeUint8FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INHERITED_VIEWPORT_SCISSOR_FEATURES_NV, VkPhysicalDeviceInheritedViewportScissorFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INLINE_UNIFORM_BLOCK_FEATURES, VkPhysicalDeviceInlineUniformBlockFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INVOCATION_MASK_FEATURES_HUAWEI, VkPhysicalDeviceInvocationMaskFeaturesHUAWEI) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_LEGACY_DITHERING_FEATURES_EXT, VkPhysicalDeviceLegacyDitheringFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_LINEAR_COLOR_ATTACHMENT_FEATURES_NV, VkPhysicalDeviceLinearColorAttachmentFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_LINE_RASTERIZATION_FEATURES_EXT, VkPhysicalDeviceLineRasterizationFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_4_FEATURES, VkPhysicalDeviceMaintenance4Features) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR, VkPhysicalDeviceMaintenance5FeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_6_FEATURES_KHR, VkPhysicalDeviceMaintenance6FeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_DECOMPRESSION_FEATURES_NV, VkPhysicalDeviceMemoryDecompressionFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PRIORITY_FEATURES_EXT, VkPhysicalDeviceMemoryPriorityFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT, VkPhysicalDeviceMeshShaderFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_NV, VkPhysicalDeviceMeshShaderFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTISAMPLED_RENDER_TO_SINGLE_SAMPLED_FEATURES_EXT, VkPhysicalDeviceMultisampledRenderToSingleSampledFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_FEATURES, VkPhysicalDeviceMultiviewFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_PER_VIEW_RENDER_AREAS_FEATURES_QCOM, VkPhysicalDeviceMultiviewPerViewRenderAreasFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTIVIEW_PER_VIEW_VIEWPORTS_FEATURES_QCOM, VkPhysicalDeviceMultiviewPerViewViewportsFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MULTI_DRAW_FEATURES_EXT, VkPhysicalDeviceMultiDrawFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MUTABLE_DESCRIPTOR_TYPE_FEATURES_EXT, VkPhysicalDeviceMutableDescriptorTypeFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NESTED_COMMAND_BUFFER_FEATURES_EXT, VkPhysicalDeviceNestedCommandBufferFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_NON_SEAMLESS_CUBE_MAP_FEATURES_EXT, VkPhysicalDeviceNonSeamlessCubeMapFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_OPACITY_MICROMAP_FEATURES_EXT, VkPhysicalDeviceOpacityMicromapFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_OPTICAL_FLOW_FEATURES_NV, VkPhysicalDeviceOpticalFlowFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PAGEABLE_DEVICE_LOCAL_MEMORY_FEATURES_EXT, VkPhysicalDevicePageableDeviceLocalMemoryFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PERFORMANCE_QUERY_FEATURES_KHR, VkPhysicalDevicePerformanceQueryFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PER_STAGE_DESCRIPTOR_SET_FEATURES_NV, VkPhysicalDevicePerStageDescriptorSetFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES, VkPhysicalDevicePipelineCreationCacheControlFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_EXECUTABLE_PROPERTIES_FEATURES_KHR, VkPhysicalDevicePipelineExecutablePropertiesFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_LIBRARY_GROUP_HANDLES_FEATURES_EXT, VkPhysicalDevicePipelineLibraryGroupHandlesFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_PROPERTIES_FEATURES_EXT, VkPhysicalDevicePipelinePropertiesFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_PROTECTED_ACCESS_FEATURES_EXT, VkPhysicalDevicePipelineProtectedAccessFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_ROBUSTNESS_FEATURES_EXT, VkPhysicalDevicePipelineRobustnessFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_BARRIER_FEATURES_NV, VkPhysicalDevicePresentBarrierFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR, VkPhysicalDevicePresentIdFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR, VkPhysicalDevicePresentWaitFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVES_GENERATED_QUERY_FEATURES_EXT, VkPhysicalDevicePrimitivesGeneratedQueryFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIMITIVE_TOPOLOGY_LIST_RESTART_FEATURES_EXT, VkPhysicalDevicePrimitiveTopologyListRestartFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRIVATE_DATA_FEATURES, VkPhysicalDevicePrivateDataFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROTECTED_MEMORY_FEATURES, VkPhysicalDeviceProtectedMemoryFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROVOKING_VERTEX_FEATURES_EXT, VkPhysicalDeviceProvokingVertexFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RASTERIZATION_ORDER_ATTACHMENT_ACCESS_FEATURES_EXT, VkPhysicalDeviceRasterizationOrderAttachmentAccessFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_QUERY_FEATURES_KHR, VkPhysicalDeviceRayQueryFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_INVOCATION_REORDER_FEATURES_NV, VkPhysicalDeviceRayTracingInvocationReorderFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_MAINTENANCE_1_FEATURES_KHR, VkPhysicalDeviceRayTracingMaintenance1FeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_MOTION_BLUR_FEATURES_NV, VkPhysicalDeviceRayTracingMotionBlurFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR, VkPhysicalDeviceRayTracingPipelineFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_POSITION_FETCH_FEATURES_KHR, VkPhysicalDeviceRayTracingPositionFetchFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RELAXED_LINE_RASTERIZATION_FEATURES_IMG, VkPhysicalDeviceRelaxedLineRasterizationFeaturesIMG) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RENDER_PASS_STRIPED_FEATURES_ARM, VkPhysicalDeviceRenderPassStripedFeaturesARM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_REPRESENTATIVE_FRAGMENT_TEST_FEATURES_NV, VkPhysicalDeviceRepresentativeFragmentTestFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RGBA10X6_FORMATS_FEATURES_EXT, VkPhysicalDeviceRGBA10X6FormatsFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, VkPhysicalDeviceRobustness2FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_YCBCR_CONVERSION_FEATURES, VkPhysicalDeviceSamplerYcbcrConversionFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCALAR_BLOCK_LAYOUT_FEATURES, VkPhysicalDeviceScalarBlockLayoutFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SCHEDULING_CONTROLS_FEATURES_ARM, VkPhysicalDeviceSchedulingControlsFeaturesARM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SEPARATE_DEPTH_STENCIL_LAYOUTS_FEATURES, VkPhysicalDeviceSeparateDepthStencilLayoutsFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_FLOAT_2_FEATURES_EXT, VkPhysicalDeviceShaderAtomicFloat2FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_FLOAT_FEATURES_EXT, VkPhysicalDeviceShaderAtomicFloatFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES, VkPhysicalDeviceShaderAtomicInt64Features) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR, VkPhysicalDeviceShaderClockFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CORE_BUILTINS_FEATURES_ARM, VkPhysicalDeviceShaderCoreBuiltinsFeaturesARM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES, VkPhysicalDeviceShaderDemoteToHelperInvocationFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, VkPhysicalDeviceShaderDrawParametersFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_EARLY_AND_LATE_FRAGMENT_TESTS_FEATURES_AMD, VkPhysicalDeviceShaderEarlyAndLateFragmentTestsFeaturesAMD) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES, VkPhysicalDeviceShaderFloat16Int8Features) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_IMAGE_ATOMIC_INT64_FEATURES_EXT, VkPhysicalDeviceShaderImageAtomicInt64FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_IMAGE_FOOTPRINT_FEATURES_NV, VkPhysicalDeviceShaderImageFootprintFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_INTEGER_DOT_PRODUCT_FEATURES, VkPhysicalDeviceShaderIntegerDotProductFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_INTEGER_FUNCTIONS_2_FEATURES_INTEL, VkPhysicalDeviceShaderIntegerFunctions2FeaturesINTEL) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_MODULE_IDENTIFIER_FEATURES_EXT, VkPhysicalDeviceShaderModuleIdentifierFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_OBJECT_FEATURES_EXT, VkPhysicalDeviceShaderObjectFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_FEATURES_NV, VkPhysicalDeviceShaderSMBuiltinsFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SUBGROUP_EXTENDED_TYPES_FEATURES, VkPhysicalDeviceShaderSubgroupExtendedTypesFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SUBGROUP_UNIFORM_CONTROL_FLOW_FEATURES_KHR, VkPhysicalDeviceShaderSubgroupUniformControlFlowFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES, VkPhysicalDeviceShaderTerminateInvocationFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TILE_IMAGE_FEATURES_EXT, VkPhysicalDeviceShaderTileImageFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADING_RATE_IMAGE_FEATURES_NV, VkPhysicalDeviceShadingRateImageFeaturesNV) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES, VkPhysicalDeviceSubgroupSizeControlFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBPASS_MERGE_FEEDBACK_FEATURES_EXT, VkPhysicalDeviceSubpassMergeFeedbackFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBPASS_SHADING_FEATURES_HUAWEI, VkPhysicalDeviceSubpassShadingFeaturesHUAWEI) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT, VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES, VkPhysicalDeviceSynchronization2Features) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TEXEL_BUFFER_ALIGNMENT_FEATURES_EXT, VkPhysicalDeviceTexelBufferAlignmentFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TEXTURE_COMPRESSION_ASTC_HDR_FEATURES, VkPhysicalDeviceTextureCompressionASTCHDRFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TILE_PROPERTIES_FEATURES_QCOM, VkPhysicalDeviceTilePropertiesFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES, VkPhysicalDeviceTimelineSemaphoreFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TRANSFORM_FEEDBACK_FEATURES_EXT, VkPhysicalDeviceTransformFeedbackFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_UNIFORM_BUFFER_STANDARD_LAYOUT_FEATURES, VkPhysicalDeviceUniformBufferStandardLayoutFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VARIABLE_POINTERS_FEATURES, VkPhysicalDeviceVariablePointersFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_FEATURES_KHR, VkPhysicalDeviceVertexAttributeDivisorFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_INPUT_DYNAMIC_STATE_FEATURES_EXT, VkPhysicalDeviceVertexInputDynamicStateFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VIDEO_MAINTENANCE_1_FEATURES_KHR, VkPhysicalDeviceVideoMaintenance1FeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_FEATURES, VkPhysicalDeviceVulkan11Features) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES, VkPhysicalDeviceVulkan12Features) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES, VkPhysicalDeviceVulkan13Features) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES, VkPhysicalDeviceVulkanMemoryModelFeatures) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_FEATURES_KHR, VkPhysicalDeviceWorkgroupMemoryExplicitLayoutFeaturesKHR) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_YCBCR_2_PLANE_444_FORMATS_FEATURES_EXT, VkPhysicalDeviceYcbcr2Plane444FormatsFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_YCBCR_DEGAMMA_FEATURES_QCOM, VkPhysicalDeviceYcbcrDegammaFeaturesQCOM) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_YCBCR_IMAGE_ARRAYS_FEATURES_EXT, VkPhysicalDeviceYcbcrImageArraysFeaturesEXT) +DEVICE_CHAIN(VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ZERO_INITIALIZE_WORKGROUP_MEMORY_FEATURES, VkPhysicalDeviceZeroInitializeWorkgroupMemoryFeatures) diff --git a/tools/linuxfs/xclipse-layer/gen_device_chain.py b/tools/linuxfs/xclipse-layer/gen_device_chain.py new file mode 100755 index 000000000..3ef47693c --- /dev/null +++ b/tools/linuxfs/xclipse-layer/gen_device_chain.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Generate device_chain.inc: the structures a VkDeviceCreateInfo chain may hold, with their sizes. + +The Xclipse layer copies the chain ahead of the VkPhysicalDeviceFeatures2 it edits, and copying a +structure needs its size. Run against the Vulkan-Headers release the build compiles with (Ubuntu +24.04's libvulkan-dev is 1.3.275): + + gen_device_chain.py Vulkan-Headers/registry/vk.xml Vulkan-Headers/include/vulkan/vulkan_core.h +""" +import re +import sys +import xml.etree.ElementTree as ElementTree +from pathlib import Path + + +def main(registry, core_header, output): + core = Path(core_header).read_text() + root = ElementTree.parse(registry).getroot() + entries = [] + for struct in root.iter("type"): + if struct.get("category") != "struct" or struct.get("alias"): + continue + if "VkDeviceCreateInfo" not in (struct.get("structextends") or "").split(","): + continue + name = struct.get("name") + stype = next((member.get("values") for member in struct.findall("member") + if member.findtext("name") == "sType"), None) + # Platform and provisional structures live outside vulkan_core.h; the layer never builds them. + if not stype or not re.search(r"\}\s*" + re.escape(name) + r"\s*;", core): + continue + entries.append((stype, name)) + version = re.search(r"#define VK_HEADER_VERSION (\d+)", core).group(1) + lines = ["/* Generated by gen_device_chain.py from Vulkan-Headers 1.3.%s; do not edit. */" % version] + lines += ["DEVICE_CHAIN(%s, %s)" % entry for entry in sorted(entries)] + Path(output).write_text("\n".join(lines) + "\n") + print("%d structures" % len(entries)) + + +if __name__ == "__main__": + if len(sys.argv) not in (3, 4): + sys.exit(__doc__) + main(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) == 4 else + Path(__file__).with_name("device_chain.inc")) diff --git a/tools/linuxfs/xclipse-layer/xclipse_layer.c b/tools/linuxfs/xclipse-layer/xclipse_layer.c new file mode 100644 index 000000000..68c3379dd --- /dev/null +++ b/tools/linuxfs/xclipse-layer/xclipse_layer.c @@ -0,0 +1,493 @@ +/* + * VK_LAYER_DROIDDECK_xclipse: workarounds for Samsung's Xclipse driver loaded through libhybris. + * + * 1. bionic environ. A glibc process never initializes bionic's `environ`; the driver's + * AMD-derived shader compiler walks it for AMD_SC_* settings and crashed on the NULL. + * Once instance creation has loaded the driver, point it at a snapshot of ours. + * 2. YCbCr immutable samplers in compute. Compute pipelines whose layout binds an array of + * immutable YCbCr-conversion samplers fail to compile (VK_ERROR_UNKNOWN); gamescope's + * composite shaders all bind one. Such bindings get a plain linear sampler instead, so only + * multi-planar (NV12) layers sample incorrectly. + * 3. Calibrated timestamps. The driver has only VK_EXT_calibrated_timestamps, but the bridge + * lists the KHR names too, and Wine calls those; the loader then aborts (it took down Wine's + * explorer.exe and with it every game window). The KHR entry points forward to the EXT ones. + * 4. BC4/BC5 filtering. The libhybris compat layer emulates BC textures; BC4/BC5 decode into + * R16/R16G16 images, for which this driver advertises no linear filtering, so DXVK would + * treat them as unfilterable. The layer reports linear filtering for BC4/BC5 where they are + * sampled at all (RDNA filters 16-bit normalized formats; at worst they sample unfiltered). + * 5. textureCompressionBC. The compat layer emulates BC formats but keeps the feature bit off; + * DXVK before 3.0 (the last that fits this driver's 128 bytes of push constants) requires + * it. The layer reports it while BC formats are sampleable and clears it at device creation. + * Everything else passes straight through. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#define EXPORT __attribute__((visibility("default"))) +#define MAX_OBJECTS 64 + +extern char **environ; + +/* The loader's dispatch table pointer heads every dispatchable object: an instance shares it + * with its physical devices, a device with its queues and command buffers. */ +typedef void *dispatch_key; +static dispatch_key key_of(const void *object) { return *(dispatch_key *)object; } + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static int debug; + +struct instance_data { + dispatch_key key; + VkInstance handle; + PFN_vkGetInstanceProcAddr next; + PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT time_domains; + PFN_vkGetPhysicalDeviceFeatures features; + PFN_vkGetPhysicalDeviceFeatures2 features2; + PFN_vkGetPhysicalDeviceFormatProperties format_properties; + PFN_vkGetPhysicalDeviceFormatProperties2 format_properties2; +}; +struct device_data { + dispatch_key key; + VkDevice handle; + PFN_vkGetDeviceProcAddr next; + PFN_vkCreateSampler create_sampler; + PFN_vkDestroySampler destroy_sampler; + PFN_vkCreateDescriptorSetLayout create_set_layout; + PFN_vkDestroyDevice destroy_device; + PFN_vkGetCalibratedTimestampsEXT timestamps; + VkSampler fallback; +}; +static struct instance_data instances[MAX_OBJECTS]; +static struct device_data devices[MAX_OBJECTS]; +/* The live YCbCr samplers each device created, so set layouts can recognise them. Guarded by lock. */ +static struct ycbcr_sampler { VkDevice device; VkSampler sampler; } *ycbcr_samplers; +static size_t ycbcr_count, ycbcr_capacity; + +static struct instance_data *instance_by_key(dispatch_key key) { + for (int i = 0; i < MAX_OBJECTS; i++) + if (instances[i].handle && instances[i].key == key) return &instances[i]; + return NULL; +} + +static struct device_data *device_by_key(dispatch_key key) { + for (int i = 0; i < MAX_OBJECTS; i++) + if (devices[i].handle && devices[i].key == key) return &devices[i]; + return NULL; +} + +/* Entries are never freed while their object lives, so a looked-up copy of the pointers is safe. */ +static struct instance_data instance_of(const void *object) { + struct instance_data copy = {0}; + pthread_mutex_lock(&lock); + struct instance_data *data = instance_by_key(key_of(object)); + if (data) copy = *data; + pthread_mutex_unlock(&lock); + return copy; +} + +static struct device_data device_of(const void *object) { + struct device_data copy = {0}; + pthread_mutex_lock(&lock); + struct device_data *data = device_by_key(key_of(object)); + if (data) copy = *data; + pthread_mutex_unlock(&lock); + return copy; +} + +static void fix_bionic_environ(void) { + void *common = dlopen("libhybris-common.so.1", RTLD_NOW | RTLD_NOLOAD); + if (!common) return; /* not a libhybris driver */ + void *(*h_dlopen)(const char *, int) = (void *(*)(const char *, int))dlsym(common, "hybris_dlopen"); + void *(*h_dlsym)(void *, const char *) = (void *(*)(void *, const char *))dlsym(common, "hybris_dlsym"); + if (!h_dlopen || !h_dlsym) return; + void *libc = h_dlopen("libc.so", RTLD_NOW); + char ***bionic_environ = libc ? (char ***)h_dlsym(libc, "environ") : NULL; + if (!bionic_environ || *bionic_environ) return; + size_t count = 0; + while (environ && environ[count]) count++; + char **snapshot = calloc(count + 1, sizeof(*snapshot)); + if (!snapshot) return; + for (size_t i = 0; i < count; i++) snapshot[i] = strdup(environ[i]); + *bionic_environ = snapshot; + if (debug) fprintf(stderr, "xclipse layer: bionic environ set (%zu entries)\n", count); +} + +/* ---- instance ---- */ + +static VkResult VKAPI_CALL create_instance(const VkInstanceCreateInfo *info, const VkAllocationCallbacks *allocator, + VkInstance *instance) { + VkLayerInstanceCreateInfo *chain = (VkLayerInstanceCreateInfo *)info->pNext; + while (chain && !(chain->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO && chain->function == VK_LAYER_LINK_INFO)) + chain = (VkLayerInstanceCreateInfo *)chain->pNext; + if (!chain || !chain->u.pLayerInfo) return VK_ERROR_INITIALIZATION_FAILED; + PFN_vkGetInstanceProcAddr next = chain->u.pLayerInfo->pfnNextGetInstanceProcAddr; + chain->u.pLayerInfo = chain->u.pLayerInfo->pNext; + PFN_vkCreateInstance next_create = (PFN_vkCreateInstance)next(NULL, "vkCreateInstance"); + if (!next_create) return VK_ERROR_INITIALIZATION_FAILED; + VkResult result = next_create(info, allocator, instance); + if (result != VK_SUCCESS) return result; + fix_bionic_environ(); + struct instance_data data = { + .key = key_of(*instance), + .handle = *instance, + .next = next, + .time_domains = (PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT) + next(*instance, "vkGetPhysicalDeviceCalibrateableTimeDomainsEXT"), + .features = (PFN_vkGetPhysicalDeviceFeatures)next(*instance, "vkGetPhysicalDeviceFeatures"), + .features2 = (PFN_vkGetPhysicalDeviceFeatures2)next(*instance, "vkGetPhysicalDeviceFeatures2"), + .format_properties = (PFN_vkGetPhysicalDeviceFormatProperties)next(*instance, "vkGetPhysicalDeviceFormatProperties"), + .format_properties2 = (PFN_vkGetPhysicalDeviceFormatProperties2)next(*instance, "vkGetPhysicalDeviceFormatProperties2"), + }; + pthread_mutex_lock(&lock); + for (int i = 0; i < MAX_OBJECTS; i++) + if (!instances[i].handle) { instances[i] = data; break; } + pthread_mutex_unlock(&lock); + return result; +} + +static void VKAPI_CALL destroy_instance(VkInstance instance, const VkAllocationCallbacks *allocator) { + PFN_vkGetInstanceProcAddr next = instance_of(instance).next; + pthread_mutex_lock(&lock); + struct instance_data *data = instance_by_key(key_of(instance)); + if (data) memset(data, 0, sizeof(*data)); + pthread_mutex_unlock(&lock); + PFN_vkDestroyInstance next_destroy = next ? (PFN_vkDestroyInstance)next(instance, "vkDestroyInstance") : NULL; + if (next_destroy) next_destroy(instance, allocator); +} + +static VkResult VKAPI_CALL time_domains_khr(VkPhysicalDevice physical, uint32_t *count, VkTimeDomainEXT *domains) { + PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT ext = instance_of(physical).time_domains; + if (!ext) { *count = 0; return VK_SUCCESS; } + return ext(physical, count, domains); +} + +/* BC formats sampleable below us although the driver's feature bit is off: the compat layer emulates them. */ +static int bc_emulated(VkPhysicalDevice physical) { + struct instance_data data = instance_of(physical); + if (!data.features || !data.format_properties) return 0; + VkPhysicalDeviceFeatures native; + data.features(physical, &native); + if (native.textureCompressionBC) return 0; + VkFormatProperties bc1, bc7; + data.format_properties(physical, VK_FORMAT_BC1_RGBA_UNORM_BLOCK, &bc1); + data.format_properties(physical, VK_FORMAT_BC7_UNORM_BLOCK, &bc7); + return (bc1.optimalTilingFeatures & bc7.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0; +} + +static void VKAPI_CALL get_features(VkPhysicalDevice physical, VkPhysicalDeviceFeatures *features) { + PFN_vkGetPhysicalDeviceFeatures next = instance_of(physical).features; + if (next) next(physical, features); + if (bc_emulated(physical)) features->textureCompressionBC = VK_TRUE; +} + +static void VKAPI_CALL get_features2(VkPhysicalDevice physical, VkPhysicalDeviceFeatures2 *features) { + PFN_vkGetPhysicalDeviceFeatures2 next = instance_of(physical).features2; + if (next) next(physical, features); + if (bc_emulated(physical)) features->features.textureCompressionBC = VK_TRUE; +} + +static int bc45(VkFormat format) { + return format >= VK_FORMAT_BC4_UNORM_BLOCK && format <= VK_FORMAT_BC5_SNORM_BLOCK; +} + +static void VKAPI_CALL get_format_properties(VkPhysicalDevice physical, VkFormat format, VkFormatProperties *out) { + PFN_vkGetPhysicalDeviceFormatProperties next = instance_of(physical).format_properties; + if (next) next(physical, format, out); + if (bc45(format) && (out->optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT)) + out->optimalTilingFeatures |= VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; +} + +static void VKAPI_CALL get_format_properties2(VkPhysicalDevice physical, VkFormat format, VkFormatProperties2 *out) { + PFN_vkGetPhysicalDeviceFormatProperties2 next = instance_of(physical).format_properties2; + if (next) next(physical, format, out); + if (!bc45(format)) return; + if (out->formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) + out->formatProperties.optimalTilingFeatures |= VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + for (VkBaseOutStructure *p = (VkBaseOutStructure *)out->pNext; p; p = p->pNext) { + if (p->sType != VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3) continue; + VkFormatProperties3 *f3 = (VkFormatProperties3 *)p; + if (f3->optimalTilingFeatures & VK_FORMAT_FEATURE_2_SAMPLED_IMAGE_BIT) + f3->optimalTilingFeatures |= VK_FORMAT_FEATURE_2_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + } +} + +/* ---- device ---- */ + +/* The size of each structure a VkDeviceCreateInfo chain may hold (device_chain.inc, generated from the + * Vulkan registry), plus the loader's own; 0 for anything else. */ +static size_t chain_size(VkStructureType type) { + switch (type) { +#define DEVICE_CHAIN(stype, name) case stype: return sizeof(name); +#include "device_chain.inc" +#undef DEVICE_CHAIN + case VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO: return sizeof(VkLayerDeviceCreateInfo); + default: return 0; + } +} + +struct chain_copy { void **nodes; size_t count; }; + +static void free_chain_copy(struct chain_copy *copy) { + for (size_t i = 0; i < copy->count; i++) free(copy->nodes[i]); + free(copy->nodes); + copy->nodes = NULL; + copy->count = 0; +} + +/* The chain with textureCompressionBC cleared in a copy of the VkPhysicalDeviceFeatures2 that enables + * it: every structure up to it is copied into [copy], the rest is shared. The caller's structures are + * never written. *head_out stays [head] when nothing needs clearing, or when a structure ahead of it + * is unknown to this build (newer than its Vulkan headers); the compat layer then refuses the bit. */ +static VkResult chain_without_bc(const void *head, struct chain_copy *copy, const void **head_out) { + *head_out = head; + const VkBaseInStructure *target = NULL; + size_t count = 0; + for (const VkBaseInStructure *p = head; p && !target; p = p->pNext) { + if (!chain_size(p->sType)) return VK_SUCCESS; + count++; + if (p->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2 && + ((const VkPhysicalDeviceFeatures2 *)p)->features.textureCompressionBC) target = p; + } + if (!target) return VK_SUCCESS; + if (!(copy->nodes = calloc(count, sizeof(*copy->nodes)))) return VK_ERROR_OUT_OF_HOST_MEMORY; + VkBaseOutStructure *first = NULL, *last = NULL; + for (const VkBaseInStructure *p = head; ; p = p->pNext) { + size_t size = chain_size(p->sType); + VkBaseOutStructure *node = malloc(size); + if (!node) { free_chain_copy(copy); return VK_ERROR_OUT_OF_HOST_MEMORY; } + copy->nodes[copy->count++] = node; + memcpy(node, p, size); + if (last) last->pNext = node; else first = node; + last = node; + if (p == target) { + ((VkPhysicalDeviceFeatures2 *)node)->features.textureCompressionBC = VK_FALSE; + *head_out = first; /* its pNext still leads to the caller's remaining structures */ + return VK_SUCCESS; + } + } +} + +static VkResult VKAPI_CALL create_device(VkPhysicalDevice physical, const VkDeviceCreateInfo *info, + const VkAllocationCallbacks *allocator, VkDevice *device) { + VkLayerDeviceCreateInfo *chain = (VkLayerDeviceCreateInfo *)info->pNext; + while (chain && !(chain->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && chain->function == VK_LAYER_LINK_INFO)) + chain = (VkLayerDeviceCreateInfo *)chain->pNext; + if (!chain || !chain->u.pLayerInfo) return VK_ERROR_INITIALIZATION_FAILED; + PFN_vkGetInstanceProcAddr next_gipa = chain->u.pLayerInfo->pfnNextGetInstanceProcAddr; + PFN_vkGetDeviceProcAddr next_gdpa = chain->u.pLayerInfo->pfnNextGetDeviceProcAddr; + chain->u.pLayerInfo = chain->u.pLayerInfo->pNext; + PFN_vkCreateDevice next_create = (PFN_vkCreateDevice)next_gipa(NULL, "vkCreateDevice"); + if (!next_create) return VK_ERROR_INITIALIZATION_FAILED; + + VkDeviceCreateInfo patched = *info; + /* An emulated textureCompressionBC never reaches the compat layer, which refuses the bit. */ + int strip_bc = bc_emulated(physical); + VkPhysicalDeviceFeatures plain_features; + if (strip_bc && info->pEnabledFeatures && info->pEnabledFeatures->textureCompressionBC) { + plain_features = *info->pEnabledFeatures; + plain_features.textureCompressionBC = VK_FALSE; + patched.pEnabledFeatures = &plain_features; + } + struct chain_copy copy = {0}; + if (strip_bc) { + VkResult copied = chain_without_bc(info->pNext, ©, &patched.pNext); + if (copied != VK_SUCCESS) return copied; + } + VkResult result = next_create(physical, &patched, allocator, device); + free_chain_copy(©); + if (result != VK_SUCCESS) return result; + + PFN_vkCreateSampler create_sampler = (PFN_vkCreateSampler)next_gdpa(*device, "vkCreateSampler"); + VkSamplerCreateInfo plain = { + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .magFilter = VK_FILTER_LINEAR, .minFilter = VK_FILTER_LINEAR, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK, + }; + VkSampler fallback = VK_NULL_HANDLE; + if (create_sampler) create_sampler(*device, &plain, NULL, &fallback); + struct device_data data = { + .key = key_of(*device), + .handle = *device, + .next = next_gdpa, + .create_sampler = create_sampler, + .destroy_sampler = (PFN_vkDestroySampler)next_gdpa(*device, "vkDestroySampler"), + .create_set_layout = (PFN_vkCreateDescriptorSetLayout)next_gdpa(*device, "vkCreateDescriptorSetLayout"), + .destroy_device = (PFN_vkDestroyDevice)next_gdpa(*device, "vkDestroyDevice"), + .timestamps = (PFN_vkGetCalibratedTimestampsEXT)next_gdpa(*device, "vkGetCalibratedTimestampsEXT"), + .fallback = fallback, + }; + pthread_mutex_lock(&lock); + for (int i = 0; i < MAX_OBJECTS; i++) + if (!devices[i].handle) { devices[i] = data; break; } + pthread_mutex_unlock(&lock); + return result; +} + +static void VKAPI_CALL destroy_device(VkDevice device, const VkAllocationCallbacks *allocator) { + struct device_data data = device_of(device); + pthread_mutex_lock(&lock); + struct device_data *entry = device_by_key(key_of(device)); + if (entry) memset(entry, 0, sizeof(*entry)); + for (size_t k = 0; k < ycbcr_count;) + if (ycbcr_samplers[k].device == device) ycbcr_samplers[k] = ycbcr_samplers[--ycbcr_count]; + else k++; + pthread_mutex_unlock(&lock); + if (data.fallback && data.destroy_sampler) data.destroy_sampler(device, data.fallback, NULL); + if (data.destroy_device) data.destroy_device(device, allocator); +} + +static VkResult VKAPI_CALL timestamps_khr(VkDevice device, uint32_t count, const VkCalibratedTimestampInfoEXT *infos, + uint64_t *timestamps, uint64_t *deviation) { + PFN_vkGetCalibratedTimestampsEXT ext = device_of(device).timestamps; + return ext ? ext(device, count, infos, timestamps, deviation) : VK_ERROR_FEATURE_NOT_PRESENT; +} + +static VkResult VKAPI_CALL create_sampler(VkDevice device, const VkSamplerCreateInfo *info, + const VkAllocationCallbacks *allocator, VkSampler *sampler) { + PFN_vkCreateSampler next = device_of(device).create_sampler; + if (!next) return VK_ERROR_INITIALIZATION_FAILED; + VkResult result = next(device, info, allocator, sampler); + if (result != VK_SUCCESS) return result; + int ycbcr = 0; + for (const VkBaseInStructure *p = info->pNext; p && !ycbcr; p = p->pNext) + ycbcr = p->sType == VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO; + if (!ycbcr) return result; + pthread_mutex_lock(&lock); + if (ycbcr_count == ycbcr_capacity) { + size_t capacity = ycbcr_capacity ? ycbcr_capacity * 2 : 16; + struct ycbcr_sampler *grown = realloc(ycbcr_samplers, capacity * sizeof(*grown)); + if (!grown) { + pthread_mutex_unlock(&lock); + /* An untracked YCbCr sampler would reach a compute layout unreplaced: fail instead. */ + PFN_vkDestroySampler destroy = device_of(device).destroy_sampler; + if (destroy) destroy(device, *sampler, allocator); + *sampler = VK_NULL_HANDLE; + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + ycbcr_samplers = grown; + ycbcr_capacity = capacity; + } + ycbcr_samplers[ycbcr_count++] = (struct ycbcr_sampler){device, *sampler}; + pthread_mutex_unlock(&lock); + return result; +} + +static void VKAPI_CALL destroy_sampler(VkDevice device, VkSampler sampler, const VkAllocationCallbacks *allocator) { + pthread_mutex_lock(&lock); + for (size_t k = 0; k < ycbcr_count; k++) + if (ycbcr_samplers[k].device == device && ycbcr_samplers[k].sampler == sampler) { + ycbcr_samplers[k] = ycbcr_samplers[--ycbcr_count]; + break; + } + pthread_mutex_unlock(&lock); + PFN_vkDestroySampler next = device_of(device).destroy_sampler; + if (next) next(device, sampler, allocator); +} + +/* Called with lock held. */ +static int is_ycbcr(VkDevice device, VkSampler sampler) { + for (size_t k = 0; sampler && k < ycbcr_count; k++) + if (ycbcr_samplers[k].device == device && ycbcr_samplers[k].sampler == sampler) return 1; + return 0; +} + +static VkResult VKAPI_CALL create_set_layout(VkDevice device, const VkDescriptorSetLayoutCreateInfo *info, + const VkAllocationCallbacks *allocator, VkDescriptorSetLayout *layout) { + struct device_data data = device_of(device); + if (!data.create_set_layout) return VK_ERROR_INITIALIZATION_FAILED; + int replace = 0; + pthread_mutex_lock(&lock); + for (uint32_t b = 0; data.fallback && b < info->bindingCount && !replace; b++) { + const VkDescriptorSetLayoutBinding *binding = &info->pBindings[b]; + if (!(binding->stageFlags & VK_SHADER_STAGE_COMPUTE_BIT) || !binding->pImmutableSamplers) continue; + for (uint32_t s = 0; s < binding->descriptorCount; s++) + if (is_ycbcr(device, binding->pImmutableSamplers[s])) { replace = 1; break; } + } + pthread_mutex_unlock(&lock); + if (!replace) return data.create_set_layout(device, info, allocator, layout); + + VkResult result = VK_ERROR_OUT_OF_HOST_MEMORY; + VkDescriptorSetLayoutBinding *bindings = calloc(info->bindingCount, sizeof(*bindings)); + VkSampler **arrays = calloc(info->bindingCount, sizeof(*arrays)); + if (!bindings || !arrays) goto done; + for (uint32_t b = 0; b < info->bindingCount; b++) { + bindings[b] = info->pBindings[b]; + if (!(bindings[b].stageFlags & VK_SHADER_STAGE_COMPUTE_BIT) || !bindings[b].pImmutableSamplers) continue; + /* Without the replacement the driver would see the samplers it cannot compile: fail instead. */ + if (!(arrays[b] = malloc(bindings[b].descriptorCount * sizeof(VkSampler)))) goto done; + pthread_mutex_lock(&lock); + for (uint32_t s = 0; s < bindings[b].descriptorCount; s++) { + VkSampler original = bindings[b].pImmutableSamplers[s]; + arrays[b][s] = is_ycbcr(device, original) ? data.fallback : original; + } + pthread_mutex_unlock(&lock); + bindings[b].pImmutableSamplers = arrays[b]; + } + VkDescriptorSetLayoutCreateInfo patched = *info; + patched.pBindings = bindings; + result = data.create_set_layout(device, &patched, allocator, layout); + if (debug) fprintf(stderr, "xclipse layer: compute YCbCr immutable samplers replaced (%d)\n", result); +done: + for (uint32_t b = 0; arrays && b < info->bindingCount; b++) free(arrays[b]); + free(arrays); + free(bindings); + return result; +} + +/* ---- dispatch ---- */ + +static PFN_vkVoidFunction device_hook(const char *name) { + if (!strcmp(name, "vkGetDeviceProcAddr")) return NULL; /* handled by the caller */ + if (!strcmp(name, "vkDestroyDevice")) return (PFN_vkVoidFunction)destroy_device; + if (!strcmp(name, "vkCreateSampler")) return (PFN_vkVoidFunction)create_sampler; + if (!strcmp(name, "vkDestroySampler")) return (PFN_vkVoidFunction)destroy_sampler; + if (!strcmp(name, "vkCreateDescriptorSetLayout")) return (PFN_vkVoidFunction)create_set_layout; + if (!strcmp(name, "vkGetCalibratedTimestampsKHR")) return (PFN_vkVoidFunction)timestamps_khr; + return NULL; +} + +EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device, const char *name) { + if (!strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)vkGetDeviceProcAddr; + PFN_vkVoidFunction hook = device_hook(name); + if (hook) return hook; + struct device_data data = device ? device_of(device) : (struct device_data){0}; + return data.next ? data.next(device, name) : NULL; +} + +EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance, const char *name) { + if (!strcmp(name, "vkGetInstanceProcAddr")) return (PFN_vkVoidFunction)vkGetInstanceProcAddr; + if (!strcmp(name, "vkCreateInstance")) return (PFN_vkVoidFunction)create_instance; + if (!strcmp(name, "vkDestroyInstance")) return (PFN_vkVoidFunction)destroy_instance; + if (!strcmp(name, "vkCreateDevice")) return (PFN_vkVoidFunction)create_device; + if (!strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)vkGetDeviceProcAddr; + if (!strcmp(name, "vkGetPhysicalDeviceFeatures")) return (PFN_vkVoidFunction)get_features; + if (!strcmp(name, "vkGetPhysicalDeviceFeatures2") || !strcmp(name, "vkGetPhysicalDeviceFeatures2KHR")) + return (PFN_vkVoidFunction)get_features2; + if (!strcmp(name, "vkGetPhysicalDeviceCalibrateableTimeDomainsKHR")) return (PFN_vkVoidFunction)time_domains_khr; + if (!strcmp(name, "vkGetPhysicalDeviceFormatProperties")) return (PFN_vkVoidFunction)get_format_properties; + if (!strcmp(name, "vkGetPhysicalDeviceFormatProperties2") || !strcmp(name, "vkGetPhysicalDeviceFormatProperties2KHR")) + return (PFN_vkVoidFunction)get_format_properties2; + PFN_vkVoidFunction hook = device_hook(name); + if (hook) return hook; + PFN_vkGetInstanceProcAddr next = instance ? instance_of(instance).next : NULL; + return next ? next(instance, name) : NULL; +} + +EXPORT VKAPI_ATTR VkResult VKAPI_CALL vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface *negotiate) { + if (!negotiate || negotiate->loaderLayerInterfaceVersion < 2) return VK_ERROR_INITIALIZATION_FAILED; + debug = getenv("DROIDDECK_XCLIPSE_LAYER_DEBUG") != NULL; + negotiate->loaderLayerInterfaceVersion = 2; + negotiate->pfnGetInstanceProcAddr = vkGetInstanceProcAddr; + negotiate->pfnGetDeviceProcAddr = vkGetDeviceProcAddr; + negotiate->pfnGetPhysicalDeviceProcAddr = NULL; + return VK_SUCCESS; +} diff --git a/tools/local-cross.Dockerfile b/tools/local-cross.Dockerfile index 653ad2b2c..badaaf5ef 100644 --- a/tools/local-cross.Dockerfile +++ b/tools/local-cross.Dockerfile @@ -3,7 +3,7 @@ FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ - && apt-get install -y binutils gcc g++ python3 zstd unzip \ + && apt-get install -y binutils gcc g++ libvulkan-dev python3 zstd unzip \ && if [ "$(uname -m)" = aarch64 ]; then \ for tool in gcc g++ strip readelf objdump ar; do ln -s "$(command -v $tool)" /usr/local/bin/aarch64-linux-gnu-$tool; done; \ else \ diff --git a/tools/mangoapp/mangoapp b/tools/mangoapp/mangoapp index 0a208fef3..755a7e44d 100644 --- a/tools/mangoapp/mangoapp +++ b/tools/mangoapp/mangoapp @@ -8,4 +8,11 @@ if [ "$(wc -l < "$starts")" -gt 5 ]; then echo "mangoapp: started $(wc -l < "$starts") times in 60 s; the performance overlay stays off for this session" >&2 exec sleep infinity fi +# The system PowerVR route (BL_POWERVR_RENDER_NODE): Zink could not make mangoapp a swapchain there +# ("zink: could not create swapchain", then GLXBadCurrentWindow) and it crashed at every start. The +# overlay is a few text lines: llvmpipe draws it. +if [ -n "${BL_POWERVR_RENDER_NODE:-}" ]; then + export GALLIUM_DRIVER=llvmpipe LIBGL_ALWAYS_SOFTWARE=1 + unset MESA_LOADER_DRIVER_OVERRIDE LIBGL_KOPPER_DRI2 +fi LD_LIBRARY_PATH=/usr/local/lib/mangoapp${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} exec /usr/local/lib/mangoapp/mangoapp "$@" diff --git a/tools/pixel_reset_diagnostics.py b/tools/pixel_reset_diagnostics.py new file mode 100644 index 000000000..462b9d460 --- /dev/null +++ b/tools/pixel_reset_diagnostics.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Read-only reboot projection. Never writes raw Android logs or account data. + +This is evidence collection, not a GPU fault injector or a root-cause oracle. +The optional kernel projection reads only SYSTEM_LAST_KMSG after user unlock. +It emits fixed categories and numeric hardware fields, never arbitrary log lines. +""" +import argparse +from datetime import datetime, timezone +import json +import os +from pathlib import Path +import re +import subprocess +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +ENTRY = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(SYSTEM_LAST_KMSG)\b', re.M) +CATEGORIES = ('GNOC', 'PVRSRV', 'watchdog', 'IOMMU', 'S2MPU', 'access control', 'Kernel panic', 'HWR', 'GPU fault') +PRIMARY_USER_STATES = frozenset(('RUNNING_LOCKED', 'RUNNING_UNLOCKING', 'RUNNING_UNLOCKED')) +# Exact canonical reasons plus the vendor reason observed in the bounded Pixel +# tests. Unrecognized/custom suffixes are not diagnostic output categories. +BOOT_REASONS = frozenset(('cold', 'hard', 'warm', 'reboot', 'shutdown', + 'recovery', 'bootloader', 'kernel_panic', 'watchdog', + 'reset,warm,gnoc_error')) + + +def primary_state(text): + match = re.search(r'Started users state: \[0=([A-Z_]+)(?=[,\]])', text) + return match.group(1) if match and match.group(1) in PRIMARY_USER_STATES else 'unknown' + + +def safe_reason(text): + value = text.strip() + return value if value in BOOT_REASONS else 'unknown' + + +def kernel_projection(text, device_timezone, after=None): + entries = list(ENTRY.finditer(text)) + result = {'metadata_dates': [m.group(1) for m in entries], 'raw_body_saved': False, + 'root_cause_proven': False} + if not entries: + result['status'] = 'no_accessible_entry' + return result + latest = max(enumerate(entries), key=lambda row: row[1].group(1)) + index, match = latest + body = text[match.end():entries[index + 1].start() if index + 1 < len(entries) else len(text)] + result['entry_date_device_local'] = match.group(1) + try: + naive = datetime.strptime(match.group(1), '%Y-%m-%d %H:%M:%S') + zone = ZoneInfo(device_timezone) + except (ValueError, ZoneInfoNotFoundError): + result['status'] = 'unknown_device_timezone' + return result + # Dropbox supplies a local wall clock without an offset. Round-trip both + # folds: a repeated hour has two possible instants; a skipped hour has none. + instants = {local.astimezone(timezone.utc) + for fold in (0, 1) + for local in (naive.replace(tzinfo=zone, fold=fold),) + if local.astimezone(timezone.utc).astimezone(zone).replace(tzinfo=None) == naive} + if len(instants) != 1: + result['status'] = ('ambiguous_device_local_time' if instants + else 'nonexistent_device_local_time') + return result + local = instants.pop() + result['entry_date_utc'] = local.isoformat() + if after is not None and local < after: + result['status'] = 'stale_entry_not_attributed_to_current_trial' + return result + result['status'] = 'projected_entry_not_a_root_cause' + result['categories'] = {key: len(re.findall(re.escape(key), body, re.I)) for key in CATEGORIES} + patterns = { + 'gnoc_addresses': r'Error from GNOC at addr (0x[0-9a-fA-F]+)', + 'gnoc_error_types': r'GNOC error type (0x[0-9a-fA-F]+)', + 'firmware_reboot_codes': r'Reboot reason:\s+(0x[0-9a-fA-F]+)', + 'access_control_addresses': r'Access Control Error \((0x[0-9a-fA-F]+)', + } + result['hardware_fields'] = {key: sorted(set(re.findall(pattern, body))) for key, pattern in patterns.items()} + # Read the complete value, not just a recognized-looking prefix. Apply the + # same fixed projection as getprop; no arbitrary kernel reason text escapes. + reasons = re.findall(r'Last boot reason:[ \t]*([^\r\n]*)', body) + result['last_boot_reasons'] = sorted({safe_reason(reason) for reason in reasons}) + return result + + +def collect(adb, serial, include_kernel=False, after=None): + def shell(*args): + try: + process = subprocess.run([adb, '-s', serial, 'shell', *args], capture_output=True, + text=True, timeout=20) + except (OSError, subprocess.TimeoutExpired): + return None, '' + # No command stderr is copied to output: it may contain unrelated data. + return process.returncode, process.stdout if process.returncode == 0 else '' + + result = {'at': datetime.now(timezone.utc).isoformat(), 'read_only': True, + 'raw_logs_saved': False, 'credentials_collected': False} + code, value = shell('settings', 'get', 'global', 'boot_count') + result['boot_count'] = int(value.strip()) if code == 0 and value.strip().isdigit() else None + code, value = shell('getprop', 'ro.boot.bootreason') + result['bootloader_reason'] = safe_reason(value) + code, value = shell('getprop', 'sys.boot.reason') + result['system_boot_reason'] = safe_reason(value) + code, value = shell('dumpsys', 'user') + result['primary_user_state'] = primary_state(value) + code, value = shell('getprop', 'persist.sys.timezone') + device_timezone = value.strip() + try: + ZoneInfo(device_timezone) + result['device_timezone'] = device_timezone + except (ValueError, ZoneInfoNotFoundError): + result['device_timezone'] = 'unknown' + code, value = shell('dumpsys', 'dropbox', 'SYSTEM_LAST_KMSG') + result['kernel_metadata_dates'] = [m.group(1) for m in ENTRY.finditer(value)] + if include_kernel: + if result['primary_user_state'] != 'RUNNING_UNLOCKED': + result['kernel'] = {'status': 'requires_user_unlock', 'raw_body_saved': False} + else: + code, value = shell('dumpsys', 'dropbox', '--print', 'SYSTEM_LAST_KMSG') + result['kernel'] = kernel_projection(value, result['device_timezone'], after) + result['kernel']['read_exit_code'] = code + return result + + +def write_private_output(path, output): + # Exclusive creation also rejects existing symlinks. Set the private mode + # before any bytes are written, rather than chmod after exposing the data. + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_NOFOLLOW', 0) + descriptor = os.open(path, flags, 0o600) + with os.fdopen(descriptor, 'w', encoding='utf-8') as stream: + stream.write(output) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--adb', default='adb') + parser.add_argument('--serial', required=True) + parser.add_argument('--kernel', action='store_true') + parser.add_argument('--after', help='Earliest relevant UTC ISO timestamp; excludes stale kernel entries') + parser.add_argument('--output', type=Path, help='New JSON file; refuses to overwrite') + args = parser.parse_args() + after = datetime.fromisoformat(args.after.replace('Z', '+00:00')) if args.after else None + if after is not None and after.tzinfo is None: + parser.error('--after must include a timezone') + result = collect(args.adb, args.serial, args.kernel, after) + output = json.dumps(result, indent=2) + '\n' + if args.output: + write_private_output(args.output, output) + print(output, end='') + + +if __name__ == '__main__': + main() diff --git a/tools/powervr/fetch.py b/tools/powervr/fetch.py new file mode 100644 index 000000000..75c8b539e --- /dev/null +++ b/tools/powervr/fetch.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Assemble the pinned PowerVR components into the APK's isolated Linux runtime. + +Like DirectAudio, APK builds download published components rather than compile +them. Their source, licenses and patches travel with the payload. Build recipes +remain in tools/{hybris,dxvk,vulkan-wsi}; release.json pins their reviewed inputs. +""" +import argparse +import hashlib +import io +import json +import os +from pathlib import Path, PurePosixPath +import posixpath +import re +import stat +import subprocess +import tarfile +import tempfile +from urllib.parse import urlparse +from urllib.request import urlopen +from zipfile import ZipFile + + +ROOT = Path(__file__).resolve().parents[2] +ASSETS = Path("app/src/main/assets/hybris") +REQUIRED_LIBRARIES = ( + "lib/libhybris-vulkan-icd.so.0", "lib/libhybris-common.so.1", + "lib/libhybris/linker/q.so", "lib/libVkLayer_hybris_compat.so", + "wsi/libVkLayer_window_system_integration.so", +) +MAX_UNPACKED = 256 * 1024 * 1024 +BUNDLED_LICENSES = { + 'tools/vulkan-wsi/licenses/Vulkan-Headers/' + name: 'licenses/Vulkan-Headers/' + name + for name in ('LICENSE.md', 'MIT.txt') +} + + +def sha256(data): + return hashlib.sha256(data).hexdigest() + + +def json_bytes(value): + return (json.dumps(value, sort_keys=True, indent=2) + "\n").encode() + + +def safe_name(name): + path = PurePosixPath(name) + if not name or path.is_absolute() or ".." in path.parts or "\\" in name: + raise ValueError("unsafe archive path: " + name) + return str(path) + + +def check_tree(entries): + # Never extract through a symlink, even one pointing inside the tree. + for name, entry in entries.items(): + for parent in PurePosixPath(name).parents: + if str(parent) in entries: + raise ValueError("archive member below a file or symlink: " + name) + if entry[0] == "symlink": + read(entries, name) + + +def read(entries, name, seen=()): + if name in seen or len(seen) > 16: + raise ValueError("runtime symlink cycle: " + name) + if name not in entries: + raise ValueError("missing runtime file: " + name) + kind, mode, data = entries[name] + if kind == "symlink": + if PurePosixPath(data).is_absolute() or "\\" in data: + raise ValueError("unsafe runtime symlink: " + name) + target = safe_name(posixpath.normpath(posixpath.join(posixpath.dirname(name), data))) + return read(entries, target, (*seen, name)) + return data + + +def read_tar(data): + unpacked = subprocess.run(["zstd", "-q", "-dc"], input=data, + capture_output=True, check=True).stdout + if len(unpacked) > MAX_UNPACKED: + raise ValueError("runtime archive exceeds size limit") + entries, seen = {}, set() + with tarfile.open(fileobj=io.BytesIO(unpacked)) as package: + for member in package: + name = safe_name(member.name) + if name in seen: + raise ValueError("duplicate archive path: " + name) + seen.add(name) + if member.isdir(): + continue + if name == ".": + raise ValueError("archive root must be a directory") + if member.issym(): + entries[name] = ("symlink", 0o777, member.linkname) + elif member.isfile(): + entries[name] = ("file", 0o755 if member.mode & 0o111 else 0o644, + package.extractfile(member).read()) + else: + raise ValueError("unsupported archive entry type: " + name) + check_tree(entries) + return entries + + +def tree_digest(entries): + return sha256(json_bytes({name: [kind, mode, sha256(data) if kind == "file" else data] + for name, (kind, mode, data) in sorted(entries.items())})) + + +def load_release(root): + release = json.loads((root / "tools/powervr/release.json").read_bytes()) + if release.get("format") != 1: + raise ValueError("unsupported PowerVR release pins") + # These identify the reviewed recipe, not a new attestation from the old + # producer. The original build metadata is preserved inside the runtime. + for component in [release["hybris"], *release["archives"]]: + for name, expected in component["recipe_inputs"].items(): + if sha256((root / name).read_bytes()) != expected: + raise ValueError("PowerVR component recipe changed; update its release pin: " + name) + return release + + +def assembly_inputs(root): + return {"format": 1, "inputs": { + name: sha256((root / name).read_bytes()) for name in + ("tools/powervr/release.json", "tools/powervr/fetch.py", "tools/hybris/run-proton.py", *BUNDLED_LICENSES)}} + + +def fetch(component, cache): + url = component["url"] + if urlparse(url).scheme != "https": + raise ValueError("component URL must use HTTPS") + cache.mkdir(parents=True, exist_ok=True) + target = cache / (component["sha256"] + "-" + PurePosixPath(urlparse(url).path).name) + + def valid(path): + return (path.is_file() and path.stat().st_size == component["size"] + and sha256(path.read_bytes()) == component["sha256"]) + + if valid(target): + return target.read_bytes() + # No unverified response is ever parsed or promoted into the cache. + with tempfile.NamedTemporaryFile(dir=cache, prefix=".download-", delete=False) as output: + temporary = Path(output.name) + try: + with urlopen(url, timeout=60) as response: + remaining = component["size"] + while block := response.read(min(1024 * 1024, remaining + 1)): + remaining -= len(block) + if remaining < 0: + raise ValueError("component download exceeds pinned size") + output.write(block) + output.flush() + if not valid(temporary): + raise ValueError("component download checksum mismatch: " + url) + os.replace(temporary, target) + finally: + temporary.unlink(missing_ok=True) + return target.read_bytes() + + +def read_zip(data, component): + entries, seen = {}, set() + expected = {item["name"]: item for item in component["files"]} + allowed = set(expected) | set(component["ignored"]) + total = 0 + with ZipFile(io.BytesIO(data)) as archive: + for member in archive.infolist(): + name = safe_name(member.filename) + if name in seen: + raise ValueError("duplicate archive path: " + name) + seen.add(name) + mode = member.external_attr >> 16 + kind = stat.S_IFMT(mode) + if member.is_dir() and kind in (0, stat.S_IFDIR): + continue + if kind not in (0, stat.S_IFREG) or name not in allowed: + raise ValueError("unexpected component archive entry: " + name) + total += member.file_size + if total > MAX_UNPACKED: + raise ValueError("component archive exceeds size limit") + if name not in expected: + continue + pin = expected[name] + content = archive.read(member) + if len(content) != pin["size"] or sha256(content) != pin["sha256"]: + raise ValueError("component member checksum mismatch: " + name) + path = safe_name(pin["path"]) + if path in entries: + raise ValueError("duplicate component destination: " + path) + entries[path] = ("file", pin["mode"], content) + if set(expected) - seen: + raise ValueError("component archive missing files: " + ", ".join(sorted(set(expected) - seen))) + check_tree(entries) + return entries + + +def read_tar_gz(data, component): + """An upstream release tarball, held to the same per-file pins as a CI archive.""" + entries, seen = {}, set() + expected = {item["name"]: item for item in component["files"]} + allowed = set(expected) | set(component["ignored"]) + total = 0 + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: + for member in archive: + name = safe_name(member.name) + if name in seen: + raise ValueError("duplicate archive path: " + name) + seen.add(name) + if member.isdir(): + continue + if not member.isfile() or name not in allowed: + raise ValueError("unexpected component archive entry: " + name) + total += member.size + if total > MAX_UNPACKED: + raise ValueError("component archive exceeds size limit") + if name not in expected: + continue + pin = expected[name] + content = archive.extractfile(member).read() + if len(content) != pin["size"] or sha256(content) != pin["sha256"]: + raise ValueError("component member checksum mismatch: " + name) + path = safe_name(pin["path"]) + if path in entries: + raise ValueError("duplicate component destination: " + path) + entries[path] = ("file", pin["mode"], content) + if set(expected) - seen: + raise ValueError("component archive missing files: " + ", ".join(sorted(set(expected) - seen))) + check_tree(entries) + return entries + + +def read_component(data, component): + kind = component.get("format", "zip") + if kind == "zip": + return read_zip(data, component) + if kind == "tar.gz": + return read_tar_gz(data, component) + raise ValueError("unsupported component archive format: " + kind) + + +def check_bundle(entries, release, root): + check_tree(entries) + if read(entries, "assembly-inputs.json") != json_bytes(assembly_inputs(root)): + raise ValueError("PowerVR assembly inputs changed; run tools/powervr/fetch.py") + if entries["run-proton.py"] != ("file", 0o755, (root / "tools/hybris/run-proton.py").read_bytes()): + raise ValueError("PowerVR Proton wrapper is stale or not executable") + compile(read(entries, "run-proton.py"), "run-proton.py", "exec") + bridge = dict(entries) + del bridge["assembly-inputs.json"], bridge["run-proton.py"] + for source, target in BUNDLED_LICENSES.items(): + if entries.get(target) != ("file", 0o644, (root / source).read_bytes()): + raise ValueError("Vulkan-Headers license missing or stale: " + target) + for component in release["archives"]: + for pin in component["files"]: + content = read(entries, pin["path"]) + if (entries[pin["path"]][:2] != ("file", pin["mode"]) + or len(content) != pin["size"] or sha256(content) != pin["sha256"]): + raise ValueError("PowerVR component mismatch: " + pin["path"]) + del bridge[pin["path"]] + for target in BUNDLED_LICENSES.values(): + bridge.pop(target, None) + if tree_digest(bridge) != release["hybris"]["tree_sha256"]: + raise ValueError("PowerVR bridge differs from the pinned archive") + for name in REQUIRED_LIBRARIES: + content = read(entries, name) + if content[:6] != b"\x7fELF\x02\x01" or content[18:20] != b"\xb7\x00": + raise ValueError("runtime library is not ELF64 AArch64: " + name) + # dxvk/ is the default payload; run-proton.py can select another dxvk-* sibling by name. + payloads = sorted({name.split("/")[0] for name in entries if re.fullmatch(r"dxvk[^/]*/x(32|64)/[^/]+\.dll", name)}) + if "dxvk" not in payloads: + raise ValueError("default DXVK payload missing") + for payload in payloads: + for architecture, machine, magic in (("x32", b"\x4c\x01", b"\x0b\x01"), + ("x64", b"\x64\x86", b"\x0b\x02")): + for name in ("d3d8.dll", "d3d9.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll"): + path = payload + "/" + architecture + "/" + name + content = read(entries, path) + pe = int.from_bytes(content[60:64], "little") + if (content[:2] != b"MZ" or pe < 64 or content[pe:pe + 4] != b"PE\0\0" + or content[pe + 4:pe + 6] != machine or content[pe + 24:pe + 26] != magic): + raise ValueError("DXVK library has wrong PE architecture: " + path) + + +def pack(entries): + stream = io.BytesIO() + directories = {str(parent) for name in entries for parent in PurePosixPath(name).parents + if str(parent) != "."} + with tarfile.open(fileobj=stream, mode="w", format=tarfile.USTAR_FORMAT) as archive: + for name in sorted(directories | set(entries)): + info = tarfile.TarInfo(name) + info.mtime = info.uid = info.gid = 0 + if name in directories: + info.type, info.mode = tarfile.DIRTYPE, 0o755 + archive.addfile(info) + continue + kind, info.mode, data = entries[name] + if kind == "symlink": + info.type, info.linkname = tarfile.SYMTYPE, data + archive.addfile(info) + else: + info.size = len(data) + archive.addfile(info, io.BytesIO(data)) + return subprocess.run(["zstd", "-q", "-10", "-T1", "-c"], input=stream.getvalue(), + capture_output=True, check=True).stdout + + +def verify_archive(archive, version, root=ROOT): + release = load_release(root) + if sha256(archive) != version.strip(): + raise ValueError("PowerVR runtime archive checksum mismatch") + check_bundle(read_tar(archive), release, root) + + +def verify(root=ROOT): + verify_archive((root / ASSETS / "runtime.tzst").read_bytes(), + (root / ASSETS / "version.txt").read_text(), root) + + +def assemble(root, cache): + release = load_release(root) + entries = read_tar(fetch(release["hybris"], cache)) + if tree_digest(entries) != release["hybris"]["tree_sha256"]: + raise ValueError("PowerVR bridge tree checksum mismatch") + for component in release["archives"]: + additions = read_component(fetch(component, cache), component) + if set(entries) & set(additions): + raise ValueError("component overwrites existing runtime files") + entries.update(additions) + for source, target in BUNDLED_LICENSES.items(): + license_entry = ("file", 0o644, (root / source).read_bytes()) + if target in entries and entries[target] != license_entry: + raise ValueError("bundled license overwrites component file: " + target) + entries[target] = license_entry + entries["run-proton.py"] = ("file", 0o755, (root / "tools/hybris/run-proton.py").read_bytes()) + entries["assembly-inputs.json"] = ("file", 0o644, json_bytes(assembly_inputs(root))) + check_bundle(entries, release, root) + archive = pack(entries) + assets = root / ASSETS + assets.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=assets.parent, prefix=".hybris-") as directory: + stage = Path(directory) + (stage / "runtime.tzst").write_bytes(archive) + (stage / "version.txt").write_text(sha256(archive) + "\n") + # version.txt is the readiness marker; an interrupted replacement fails + # closed instead of presenting a partially updated payload as ready. + (assets / "version.txt").unlink(missing_ok=True) + os.replace(stage / "runtime.tzst", assets / "runtime.tzst") + os.replace(stage / "version.txt", assets / "version.txt") + print("PowerVR runtime prepared: " + sha256(archive)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--verify", action="store_true") + parser.add_argument("--cache-dir", type=Path, default=Path(os.environ.get( + "DROIDDECK_BUILD_CACHE", str(Path.home() / ".cache/droiddeck-build")))) + args = parser.parse_args() + if args.verify: + verify() + else: + assemble(ROOT, args.cache_dir) + + +if __name__ == "__main__": + main() diff --git a/tools/powervr/release.json b/tools/powervr/release.json new file mode 100644 index 000000000..749eb2f73 --- /dev/null +++ b/tools/powervr/release.json @@ -0,0 +1,335 @@ +{ + "archives": [ + { + "files": [ + { + "mode": 493, + "name": "x32/d3d8.dll", + "path": "dxvk/x32/d3d8.dll", + "sha256": "c9612be6aac99390156cdcd3b22054fba62cf68bfc948d753cc39282d37f67ab", + "size": 1658894 + }, + { + "mode": 493, + "name": "x32/d3d9.dll", + "path": "dxvk/x32/d3d9.dll", + "sha256": "fbd0e7d7aae39d5d623757ead228930c4be78cb3b16db6f352796577831ce36e", + "size": 7598094 + }, + { + "mode": 493, + "name": "x32/d3d10core.dll", + "path": "dxvk/x32/d3d10core.dll", + "sha256": "cd6052b1f6a02ab94fea5bfa3519e556fb353f585da4b64e3c810ef4cc37c808", + "size": 192526 + }, + { + "mode": 493, + "name": "x32/d3d11.dll", + "path": "dxvk/x32/d3d11.dll", + "sha256": "9bf51b5ab913e1bd84513b2b1608586bc0a651788df9e664b8135840b8c8c54d", + "size": 7942158 + }, + { + "mode": 493, + "name": "x32/dxgi.dll", + "path": "dxvk/x32/dxgi.dll", + "sha256": "d3772dc86267fe94eee7afc30c993f44297fd43b50210f1691f3d03ebf4d4402", + "size": 5738510 + }, + { + "mode": 493, + "name": "x64/d3d8.dll", + "path": "dxvk/x64/d3d8.dll", + "sha256": "cd870a8f5cdbfef33e7ccf7884d6f1c442e2c8ec22d6adea60fea3dbe83c32e2", + "size": 1499136 + }, + { + "mode": 493, + "name": "x64/d3d9.dll", + "path": "dxvk/x64/d3d9.dll", + "sha256": "4f80af96991073bb3dc25f3b93ec2c07ba25fcabb8a6e30ffd5eaae38c8465c7", + "size": 6934528 + }, + { + "mode": 493, + "name": "x64/d3d10core.dll", + "path": "dxvk/x64/d3d10core.dll", + "sha256": "4950e952ca291b13c0645322f1831414e77df87183ed2f81d5dde02c1fd43ff9", + "size": 978944 + }, + { + "mode": 493, + "name": "x64/d3d11.dll", + "path": "dxvk/x64/d3d11.dll", + "sha256": "3497d5f7dc78fa7affdba6777d3ab4aac7ec744d7e2c795fbf07dbff54058e6c", + "size": 7143424 + }, + { + "mode": 493, + "name": "x64/dxgi.dll", + "path": "dxvk/x64/dxgi.dll", + "sha256": "d6e0ca242dba6cb8606106a7fcb65268225520f03c68e36a365f44d65f863e74", + "size": 5230592 + }, + { + "mode": 420, + "name": "LICENSE", + "path": "licenses/dxvk-LICENSE", + "sha256": "a5cb1a6ded7d2d7e92d550ba28edd21be2d1d4044662b399887351023e30ce64", + "size": 1074 + }, + { + "mode": 420, + "name": "source-commit", + "path": "provenance/dxvk-source-commit", + "sha256": "cb748e54f2fcdca516189435647ea001dbd14f97e2df5b36fd2f13fc185a5dbf", + "size": 41 + }, + { + "mode": 420, + "name": "SHA256SUMS", + "path": "provenance/dxvk-SHA256SUMS", + "sha256": "8a106cb90288052cc91768b569980ed7381c89591fb7cea3bc6d0312c6f30a47", + "size": 802 + }, + { + "mode": 420, + "name": "downstream.patch", + "path": "licenses/dxvk-downstream.patch", + "sha256": "e44b8d656a37c3b1059bbf3dcbcf11922a00634301f2509024fb263a45e9f50e", + "size": 3604 + }, + { + "mode": 420, + "name": "source.tar.zst", + "path": "licenses/dxvk-source.tar.zst", + "sha256": "dfdebe8aa71b85af638f2565c1d5abe31417c6db643a827bcf0988e29ded55e7", + "size": 6043001 + }, + { + "mode": 420, + "name": "submodules.txt", + "path": "provenance/dxvk-submodules.txt", + "sha256": "01e74c750ca473590e17f1ca33f204b8272f84898ee6e8a8509be9740919092c", + "size": 472 + } + ], + "id": "dxvk", + "ignored": [ + "x32/d3d10core.dll.a", + "x32/d3d11.dll.a", + "x32/d3d8.dll.a", + "x32/d3d9.dll.a", + "x32/dxgi.dll.a", + "x64/d3d10core.dll.a", + "x64/d3d11.dll.a", + "x64/d3d8.dll.a", + "x64/d3d9.dll.a", + "x64/dxgi.dll.a" + ], + "provenance": { + "ci_run": "https://github.com/wallentx/DroidDeck/actions/runs/38119192562", + "repository_commit": "aee0ebd43557ef9964ba7f6a11d7d30efaf983ac" + }, + "recipe_inputs": { + "tools/dxvk/build.sh": "88b656292a6a8c92974f1c014dd685e104818ef45af5fa4e62b729452c969d6e", + "tools/dxvk/check_present_sync.py": "42e2a590e17c1f53e5cd4100ef3d19a5bdcce31a6d7a18fbf9961f4ef27d5063", + "tools/dxvk/patches/0001-check-direct3d-bc-formats.patch": "d38fc4d773b7974498dcb377dbaa883c9bba36aefa3235b39bca69d6dd220c77", + "tools/dxvk/patches/0002-present-semaphore-per-swapchain-image.patch": "c802789a33099d3efc3413a297e2b752f8eb84c18cbfbdfc04e188f4090410d0", + "tools/dxvk/source.env": "e49b0757f203f88783360c06118f60945242fde8738419e0b27cf2531bf3a365" + }, + "sha256": "d08dd962ced3482883ad9e9eb4903a4d27a3756b4f989a97ecdfcccb53fbee66", + "size": 24063241, + "url": "https://github.com/wallentx/DroidDeck/releases/download/v0.3.2-progress.20261011.3/dxvk-aee0ebd.zip" + }, + { + "files": [ + { + "mode": 420, + "name": "libVkLayer_window_system_integration.so", + "path": "wsi/libVkLayer_window_system_integration.so", + "sha256": "d07b961181b9547fd48ad9f05cc6d3b808b824676eb5a186fe3feb8caa013f19", + "size": 436888 + }, + { + "mode": 420, + "name": "VkLayer_window_system_integration.json", + "path": "wsi/VkLayer_window_system_integration.json", + "sha256": "a1df8b1cee7315015d8fe82534451e373ae4d9cb2adfc232c68346ecbf2818ee", + "size": 3683 + }, + { + "mode": 420, + "name": "LICENSE", + "path": "licenses/wsi-LICENSE", + "sha256": "5eb8d2da52230d2d66ce078a89ae0173aac63e2e73bed5f907a34736d114ea30", + "size": 1086 + }, + { + "mode": 420, + "name": "source-commit", + "path": "provenance/wsi-source-commit", + "sha256": "8367150b20502af36c472651850694a15a3e2a1fd6b6beb1f6e12a85a97824f5", + "size": 41 + }, + { + "mode": 420, + "name": "SHA256SUMS", + "path": "provenance/wsi-SHA256SUMS", + "sha256": "fda6f61028659648801de0e3a6fe1676efd99e1551881a5db8630970eae86158", + "size": 108 + }, + { + "mode": 420, + "name": "downstream.patch", + "path": "licenses/wsi-downstream.patch", + "sha256": "e2ed113d22f05563248ef606c774c22658b184fbc05912229007cd5281027e78", + "size": 769 + }, + { + "mode": 420, + "name": "source.tar.zst", + "path": "licenses/wsi-source.tar.zst", + "sha256": "22ae7c2d8aa3dfffdda601c3442d1966ab53ad225acdfadee69ba38974d0c4c5", + "size": 209468 + }, + { + "mode": 420, + "name": "vulkan-headers-commit", + "path": "provenance/wsi-vulkan-headers-commit", + "sha256": "16706d6cdbeb788426a90d66574dfd71068c4d993d39e76ec5c281b1f30dc7a6", + "size": 41 + } + ], + "id": "wsi", + "ignored": [], + "provenance": { + "ci_run": "https://github.com/wallentx/DroidDeck/actions/runs/38047627963", + "repository_commit": "abe58618a5194f1f15499cbd5f9ec75503782556", + "vulkan_headers_licenses": { + "note": "Added from tracked assembly inputs; the original WSI archive and compiled library are unchanged.", + "revision": "8d6039a455a7ecc7d2a592ff97f62db4e59b70bf", + "source_urls": [ + "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/8d6039a455a7ecc7d2a592ff97f62db4e59b70bf/LICENSE.md", + "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/8d6039a455a7ecc7d2a592ff97f62db4e59b70bf/LICENSES/MIT.txt" + ] + } + }, + "recipe_inputs": { + "tools/vulkan-wsi/build.sh": "9e403151c58e6b2ee15241f525c4bacb2495f072191da6a63065e7fcc25b3b3c", + "tools/vulkan-wsi/licenses/Vulkan-Headers/LICENSE.md": "95ad366d23fadf701d355bc45fb8b82ae2d700239471d35d41286ac3b08ff903", + "tools/vulkan-wsi/licenses/Vulkan-Headers/MIT.txt": "1ca3502222d967f3be5751c55f6b7ee735b5383909c3b501495f54b216dbf227", + "tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch": "e2ed113d22f05563248ef606c774c22658b184fbc05912229007cd5281027e78", + "tools/vulkan-wsi/source.env": "71a88087265a09b4ce06e7c9b98ef6799c6a7657cc6ef9084de663e96f5cfe63" + }, + "sha256": "348f9ec42159c1ee6e2baba4312a5e845d26687238f29e3f4a1b0c3c97e13b3c", + "size": 362717, + "url": "https://github.com/wallentx/DroidDeck/releases/download/v0.3.2-progress.20261010.4/wsi-abe5861.zip" + }, + { + "files": [ + { + "mode": 493, + "name": "dxvk-2.6.2/x32/d3d10core.dll", + "path": "dxvk-2.6.2/x32/d3d10core.dll", + "sha256": "b34b479fa0bce29eba6dcff5b1f8584c2ae699d53a123668590f8a2b84e84094", + "size": 176142 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x32/d3d11.dll", + "path": "dxvk-2.6.2/x32/d3d11.dll", + "sha256": "858d1a848dd5023191cb16cd986e0f00008faf5df8a87b5c835b56cf4b646910", + "size": 4845582 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x32/d3d8.dll", + "path": "dxvk-2.6.2/x32/d3d8.dll", + "sha256": "617459c8d1239f98687f8157bdaec85738ee4699885ed7d1ff13deb32609c759", + "size": 1658894 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x32/d3d9.dll", + "path": "dxvk-2.6.2/x32/d3d9.dll", + "sha256": "00bc142e98ddad1e334a4d88fa630038e092b0758b3d79e295172b181a4c772d", + "size": 4460558 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x32/dxgi.dll", + "path": "dxvk-2.6.2/x32/dxgi.dll", + "sha256": "67d7ca4a4f8f66baabd381c73b52a8e85870083e868ed474a4bc58b7da486883", + "size": 3137550 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x64/d3d10core.dll", + "path": "dxvk-2.6.2/x64/d3d10core.dll", + "sha256": "565154a0763c6460843fe47e5aaec62e609163d9995a922d5e19e85027fe2f88", + "size": 155662 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x64/d3d11.dll", + "path": "dxvk-2.6.2/x64/d3d11.dll", + "sha256": "5b63515a432d19302f7dc81dd2ee5bf40043683b5064a2bce03a2b6d27dcce5d", + "size": 4399118 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x64/d3d8.dll", + "path": "dxvk-2.6.2/x64/d3d8.dll", + "sha256": "dfd837c3467eeb15f69a773e18bbc0550cb69c9e33d5d57b4ea42c5f63bfdc4b", + "size": 1544206 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x64/d3d9.dll", + "path": "dxvk-2.6.2/x64/d3d9.dll", + "sha256": "01e7ae2b0544b6031fe695fd16fbc76786ded57fdbe3f6bd11eee7c98d2c370d", + "size": 4108302 + }, + { + "mode": 493, + "name": "dxvk-2.6.2/x64/dxgi.dll", + "path": "dxvk-2.6.2/x64/dxgi.dll", + "sha256": "074616e1af172edf175f3fc6ed7ca30f6c685a70875eed3af10797a10840ed39", + "size": 2891790 + } + ], + "format": "tar.gz", + "id": "dxvk-xclipse", + "ignored": [], + "provenance": { + "note": "Unmodified upstream release, selected on Samsung Xclipse: its 128 bytes of push constants predate DXVK 3's 256-byte requirement. Covered by the zlib license already shipped as licenses/dxvk-LICENSE.", + "upstream_release": "https://github.com/doitsujin/dxvk/releases/tag/v2.6.2" + }, + "recipe_inputs": {}, + "sha256": "17761876556afd55736cb895d184f5a1c55d43350f1b1e3b129f8d28706d7992", + "size": 10107492, + "url": "https://github.com/doitsujin/dxvk/releases/download/v2.6.2/dxvk-2.6.2.tar.gz" + } + ], + "format": 1, + "hybris": { + "provenance": { + "apk_ci_run": "https://github.com/wallentx/DroidDeck/actions/runs/38123715308", + "extracted_from_apk": "https://github.com/wallentx/DroidDeck/releases/download/v0.3.2-progress.20261011.3/DroidDeck-0.3.2-1b6185b.apk", + "note": "APK build reused a cached bridge. Original source and compiler metadata are preserved in build-manifest.json; no APK commit is claimed as the bridge producer.", + "upstream_source_commit": "bc8cb3586b79d208fd21b925fa0209b3e432e008" + }, + "recipe_inputs": { + "tools/hybris/build_runtime.sh": "bc83472ecfd886841c8c2c436080cdee0a27ce0d860ffbffe45a75fbc6b2b21d", + "tools/hybris/patches/0001-dxvk-bc-profile.patch": "b90605e51256595de4453352865aa198e59bd2d392ac6b7c2a60f38c57727c9d", + "tools/hybris/powervr-hal-version.patch": "d3ed8ca03e5a874b9dc46651983117760c7490b96127335625d0480a2b85e66f", + "tools/hybris/source.env": "9ae7985f2052dbcc8b9666494bc8a711ec85013f6a27d6d72d9729c32b919dd8" + }, + "sha256": "f4f12c26ed21003c2c98a7cececd76c8a3b332d79ece9d1f963cf3b9e8943978", + "size": 5552486, + "tree_sha256": "de9cc2c4107bf396e4f963d7e91f936a966593fb1df680a0b5d43db0f9fef4e1", + "url": "https://github.com/wallentx/DroidDeck/releases/download/v0.3.2-progress.20261011.3/hybris-runtime-f4f12c26.tzst" + } +} diff --git a/tools/release/check_session_assets.py b/tools/release/check_session_assets.py index 2042e470f..af31d4c8c 100644 --- a/tools/release/check_session_assets.py +++ b/tools/release/check_session_assets.py @@ -1,4 +1,5 @@ from pathlib import Path +import importlib.util import re import sys from zipfile import ZipFile @@ -9,7 +10,10 @@ SESSION_FILES = OVERLAY.parents[2] / 'app/src/main/java/com/droiddeck/launcher/s def check_extraction(overlay, source): - deployed = set(re.findall(r'"(usr/local/bin/droiddeck-[^"]+)"\s+to\s+"usr/local/bin/[^"/]+"', source)) + # Extracted into usr/local/bin under any name, or (droiddeck-steam-taskset, which PATH puts in + # front of Steam's own taskset only for the client) into usr/local/libexec/droiddeck-steam. + deployed = set(re.findall( + r'"(usr/local/bin/droiddeck-[^"]+)"\s+to\s+"usr/local/(?:bin|libexec/droiddeck-steam)/[^"/]+"', source)) return ['not extracted at session start: ' + path.relative_to(overlay).as_posix() for path in sorted((overlay / 'usr/local/bin').glob('droiddeck-*')) if path.relative_to(overlay).as_posix() not in deployed] @@ -41,6 +45,16 @@ def check(apk, overlay=OVERLAY): continue if content != source.read_bytes(): errors.append('stale ' + asset) + if overlay == OVERLAY: + spec = importlib.util.spec_from_file_location('powervr_fetch', + OVERLAY.parents[1] / 'powervr/fetch.py') + runtime = importlib.util.module_from_spec(spec) + spec.loader.exec_module(runtime) + try: + runtime.verify_archive(package.read('assets/hybris/runtime.tzst'), + package.read('assets/hybris/version.txt').decode('ascii')) + except (KeyError, ValueError, OSError) as error: + errors.append('invalid PowerVR runtime: ' + str(error)) return errors diff --git a/tools/release/runtime_inputs.py b/tools/release/runtime_inputs.py index b03f5235d..b14f13eff 100644 --- a/tools/release/runtime_inputs.py +++ b/tools/release/runtime_inputs.py @@ -1,5 +1,6 @@ import hashlib import gzip +import importlib.util import json from pathlib import Path import subprocess @@ -22,6 +23,7 @@ SOURCE_PATHS = ['app/build.gradle', 'app/src/main/cpp/fakeinput_steam.cpp', 'app 'tools/linuxfs', 'tools/proot', 'tools/aaudio-sink', 'tools/directaudio', 'tools/packs', 'tools/gamescope/release.env', 'tools/droiddeck-esync/release.env', 'tools/mangoapp', 'tools/msitools', 'tools/pipewire', 'tools/comet', 'tools/xbox', 'tools/dirt-ws2', 'tools/components', 'tools/ci', 'tools/build_local.sh', + 'tools/powervr', 'tools/hybris', 'tools/dxvk', 'tools/vulkan-wsi', 'tools/release/runtime_inputs.py', 'app/src/main/assets/desktop-removal', 'tools/quest', 'tools/vr/src', 'tools/vr/driver_droiddeck', 'tools/vr/layer', 'tools/vr/build.sh', 'tools/gpuload', @@ -53,7 +55,7 @@ def sources(root): def outputs(root): result = {} - for tree in ['linuxfs', 'directaudio', 'droiddeck-esync', 'desktop-removal']: + for tree in ['linuxfs', 'directaudio', 'droiddeck-esync', 'desktop-removal', 'hybris']: base = root / 'app/src/main/assets' / tree for path in base.rglob('*'): if path.is_file() and not any(part.startswith('.') for part in path.relative_to(base).parts): @@ -85,6 +87,8 @@ def required(): paths += ['assets/linuxfs/i386/libfaultreport.so'] paths += ['assets/linuxfs/usr/local/lib/droiddeck-msitools/' + name for name in ['msiinfo', 'cabextract', '7z', '7z.so', 'libmsi-1.0.so.0', 'libgsf-1.so.114', 'libgcab-1.0.so.0']] + paths += ['assets/linuxfs/usr/local/lib/droiddeck-xclipse/' + name for name in + ['libVkLayer_droiddeck_xclipse.so', 'VkLayer_droiddeck_xclipse.json']] paths += ['assets/linuxfs/usr/local/lib/droiddeck-vr/' + name for name in ['droiddeck/bin/linuxarm64/driver_droiddeck.so', 'droiddeck/bin/linuxarm64/xr-display-mode', 'droiddeck/driver.vrdrivermanifest', @@ -115,9 +119,17 @@ def required(): paths += ['assets/desktop-removal/' + name for name in ['lxqt-r1.tsv.gzip', 'kde-remove.tsv.gzip', 'kde-keep.txt.gzip', 'emulators-keep.txt.gzip', 'versions.txt', 'sources.json']] + paths += ['assets/hybris/runtime.tzst', 'assets/hybris/version.txt'] return paths +def verify_powervr(root): + spec = importlib.util.spec_from_file_location('powervr_fetch', ROOT / 'tools/powervr/fetch.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.verify(root) + + def record(root=ROOT): if root == ROOT: from check_session_assets import source_assets @@ -129,6 +141,7 @@ def record(root=ROOT): missing = [name for name in required() if name not in paths or not (root / paths[name]).is_file()] if missing: raise ValueError('Missing runtime inputs:\n' + '\n'.join(missing)) + verify_powervr(root) for name, path in paths.items(): if name.startswith('assets/desktop-removal/') and name.endswith('.gzip'): try: diff --git a/tools/tests/drm_handles_test.c b/tools/tests/drm_handles_test.c new file mode 100644 index 000000000..08d337f55 --- /dev/null +++ b/tools/tests/drm_handles_test.c @@ -0,0 +1,343 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Only the device/driver boundary is simulated. Descriptors and their lifetimes are real. + * F_DUPFD_QUERY is modelled so this test also runs on older CI kernels; the device probe + * separately exercises the actual query on Android. */ +#define TEST_FD_LIMIT 65536 +static _Atomic unsigned identities[TEST_FD_LIMIT]; +static struct f_owner_ex owners[TEST_FD_LIMIT]; +static unsigned identity_serial = 1; +static int identity_denied, owner_denied, owner_probe_delay, kgsl_present; +static int kernel_error = EINVAL; +static int native_imports, native_exports, native_closes; +static const char *driver_name = "pvr"; +static const char *pvr_node = "/dev/dri/renderD129"; +static int fixture_stat(const char *path, struct stat *st); +static long fixture_syscall(long number, ...); +static int fixture_fcntl(int fd, int op, ...); +static void *fixture_dlsym(void *object, const char *name); +static int fixture_close(int fd); + +#define stat(path, st) fixture_stat(path, st) +#define syscall fixture_syscall +#define fcntl fixture_fcntl +#define dlsym fixture_dlsym +#define close fixture_close +#include "../linuxfs/preload/drm.c" +#undef stat +#undef syscall +#undef fcntl +#undef dlsym +#undef close + +static int test_open(const char *path) { + int fd = open(path, O_RDWR | O_CLOEXEC); + assert(fd >= 0 && fd < TEST_FD_LIMIT); + identities[fd] = identity_serial++; + return fd; +} + +static int fixture_close(int fd) { + if (fd >= 0 && fd < TEST_FD_LIMIT) identities[fd] = 0; + return close(fd); +} + +static int fixture_stat(const char *path, struct stat *st) { + if (!strcmp(path, "/dev/kgsl-3d0")) { + if (kgsl_present) return stat("/dev/zero", st); + errno = ENOENT; + return -1; + } + if (!strcmp(path, pvr_node)) return stat("/dev/null", st); + if (!strcmp(path, "/dev/dri/renderD128")) return stat("/dev/zero", st); + if (!strncmp(path, "/dev/dri/", 9)) { errno = ENOENT; return -1; } + return stat(path, st); +} + +static int fixture_fcntl(int fd, int op, ...) { + va_list ap; + va_start(ap, op); + if (op == F_GETOWN_EX || op == F_SETOWN_EX) { + struct f_owner_ex *owner = va_arg(ap, struct f_owner_ex *); + va_end(ap); + if (owner_denied) { errno = EINVAL; return -1; } + if (fd < 0 || fd >= TEST_FD_LIMIT || !identities[fd] || !owner) { + errno = EBADF; + return -1; + } + if (op == F_GETOWN_EX) { + *owner = owners[identities[fd]]; + } else { + owners[identities[fd]] = *owner; + if (owner_probe_delay && owner->pid) usleep(1000); + } + return 0; + } + int arg = va_arg(ap, int); + va_end(ap); + if (op == F_DUPFD_QUERY) { + if (identity_denied) { errno = EINVAL; return -1; } + if (fd < 0 || arg < 0 || fd >= TEST_FD_LIMIT || arg >= TEST_FD_LIMIT || + !identities[fd] || !identities[arg]) { errno = EBADF; return -1; } + return identities[fd] == identities[arg]; + } + int ret = fcntl(fd, op, arg); + if (ret >= 0 && (op == F_DUPFD || op == F_DUPFD_CLOEXEC)) { + assert(ret < TEST_FD_LIMIT); + identities[ret] = identities[fd]; + } + return ret; +} + +static long fixture_syscall(long number, ...) { + va_list ap; + va_start(ap, number); + if (number == SYS_ioctl) { + (void)va_arg(ap, int); + unsigned long request = va_arg(ap, unsigned long); + struct bl_drm_version *v = va_arg(ap, void *); + assert(request == BL_DRM_VERSION); + snprintf(v->name, v->name_len, "%s", driver_name); + v->name_len = strlen(driver_name); + va_end(ap); + return 0; + } + va_end(ap); + assert(number == SYS_kcmp); + errno = EPERM; + return -1; +} + +static int native_import(int fd, int dma, uint32_t *handle) { + (void)fd; + native_imports++; + if (fcntl(dma, F_GETFD) < 0) return -1; + if (!handle) { errno = EFAULT; return -1; } + if (kernel_error) { errno = kernel_error; return -1; } + *handle = 42; + return 0; +} +static int native_export(int fd, uint32_t handle, uint32_t flags, int *out) { + (void)flags; + native_exports++; + if (handle != 42 || !out) { errno = EINVAL; return -1; } + *out = fixture_fcntl(fd, F_DUPFD_CLOEXEC, 0); + return *out < 0 ? -1 : 0; +} +static int native_close(int fd, uint32_t handle) { + (void)fd; + native_closes++; + if (handle == 42) return 0; + errno = EINVAL; + return -1; +} +static void *fixture_dlsym(void *object, const char *name) { + assert(object == RTLD_NEXT); + if (!strcmp(name, "drmPrimeFDToHandle")) return native_import; + if (!strcmp(name, "drmPrimeHandleToFD")) return native_export; + if (!strcmp(name, "drmCloseBufferHandle")) return native_close; + abort(); +} + +static int count_fds(void) { + int count = 0; + for (int fd = 0; fd < TEST_FD_LIMIT; ++fd) count += identities[fd] != 0; + return count; +} +static void empty_table(void) { + for (int i = 0; i < MAX_HANDLES; ++i) assert(!handle_entries[i].handle); +} + +struct fallback_stress { + int drm; + int dma; + int failures; +}; + +static void *stress_fallback_imports(void *opaque) { + struct fallback_stress *stress = opaque; + for (int i = 0; i < 20; i++) { + uint32_t handle = 0; + if (drmPrimeFDToHandle(stress->drm, stress->dma, &handle) != 0 || + drmCloseBufferHandle(stress->drm, handle) != 0) { + stress->failures++; + } + } + return NULL; +} + +int main(void) { + int drm = test_open("/dev/null"), dma = test_open("/dev/zero"); + uint32_t handle = 0; + unsetenv("BL_POWERVR_DRM_HANDLES"); + setenv("BL_POWERVR_RENDER_NODE", pvr_node, 1); + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); + setenv("BL_POWERVR_DRM_HANDLES", "yes", 1); + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); + setenv("BL_POWERVR_DRM_HANDLES", "1", 1); + unsetenv("BL_POWERVR_RENDER_NODE"); + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); /* no selected node */ + setenv("BL_POWERVR_RENDER_NODE", "", 1); + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); + setenv("BL_POWERVR_RENDER_NODE", "/dev/dri/renderD128", 1); + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); /* another GPU owns 128 */ + setenv("BL_POWERVR_RENDER_NODE", "/dev/dri/renderD130", 1); + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); /* selected node is absent */ + setenv("BL_POWERVR_RENDER_NODE", __FILE__, 1); + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); /* regular files are not DRM nodes */ + setenv("BL_POWERVR_RENDER_NODE", pvr_node, 1); + driver_name = "other"; + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0); + driver_name = "pvr"; + assert(drmPrimeFDToHandle(dma, dma, &handle) != 0); /* wrong character device */ + kernel_error = EACCES; + assert(drmPrimeFDToHandle(drm, dma, &handle) != 0 && errno == EACCES); + kernel_error = EINVAL; + assert(drmPrimeFDToHandle(drm, -1, &handle) != 0 && errno == EBADF); + empty_table(); + + int baseline = count_fds(); + identity_denied = 1; + assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && (handle & PVR_HANDLE_BIT)); + int denied_alias = fixture_fcntl(drm, F_DUPFD_CLOEXEC, 0), denied_out = -1; + int denied_separate = test_open("/dev/null"); + assert(drmPrimeHandleToFD(denied_separate, handle, O_CLOEXEC, &denied_out) != 0); + assert(drmPrimeHandleToFD(denied_alias, handle, O_CLOEXEC, &denied_out) == 0); + fixture_close(denied_out); + assert(drmCloseBufferHandle(denied_alias, handle) == 0); + fixture_close(denied_alias); + fixture_close(denied_separate); + assert(count_fds() == baseline); + owner_denied = 1; + assert(drmPrimeFDToHandle(drm, dma, &handle) == -EOPNOTSUPP); + assert(count_fds() == baseline); + owner_denied = 0; + + /* Marker changes and restoration must be atomic across concurrent imports of one DRM OFD. */ + owner_probe_delay = 1; + struct fallback_stress stress[8]; + pthread_t workers[8]; + for (int i = 0; i < 8; i++) { + stress[i] = (struct fallback_stress){.drm = drm, .dma = dma}; + assert(pthread_create(&workers[i], NULL, stress_fallback_imports, &stress[i]) == 0); + } + for (int i = 0; i < 8; i++) { + assert(pthread_join(workers[i], NULL) == 0); + assert(stress[i].failures == 0); + } + owner_probe_delay = 0; + assert(owners[identities[drm]].pid == 0); + assert(count_fds() == baseline); + empty_table(); + identity_denied = 0; + + assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && (handle & PVR_HANDLE_BIT)); + assert(count_fds() == baseline + 2); + int alias = fixture_fcntl(drm, F_DUPFD_CLOEXEC, 0), out = -1; + int separate = test_open("/dev/null"); + assert(drmPrimeHandleToFD(separate, handle, O_CLOEXEC | O_RDWR, &out) != 0); + struct gem_close close_request = {.handle = handle}; + int rc = 123; + assert(bl_drm_gem_close(separate, GEM_CLOSE, &close_request, &rc) == 0 && rc == 123); + assert(drmPrimeHandleToFD(alias, handle, O_CLOEXEC | O_RDWR, &out) == 0); + assert(fcntl(out, F_GETFD) & FD_CLOEXEC); + struct stat src, dst; + assert(fstat(dma, &src) == 0 && fstat(out, &dst) == 0); + assert(src.st_dev == dst.st_dev && src.st_ino == dst.st_ino); + fixture_close(out); + assert(drmPrimeHandleToFD(alias, handle, 0, &out) == 0); + assert(!(fcntl(out, F_GETFD) & FD_CLOEXEC)); + fixture_close(out); + assert(drmPrimeHandleToFD(alias, handle, 0x40, &out) == -EINVAL); + + /* A closed-and-reused descriptor must not inherit the old open file's handles. */ + int old_number = drm; + fixture_close(drm); + drm = test_open("/dev/null"); + assert(drm == old_number); + assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) != 0); + assert(drmCloseBufferHandle(drm, handle) != 0); + assert(drmPrimeHandleToFD(alias, handle, O_CLOEXEC, &out) == 0); + fixture_close(out); + assert(bl_drm_gem_close(alias, GEM_CLOSE, &close_request, &rc) == 1 && rc == 0); + assert(drmPrimeHandleToFD(alias, handle, O_CLOEXEC, &out) != 0); + fixture_close(alias); + fixture_close(separate); + assert(count_fds() == baseline); + empty_table(); + + /* Real kernel handles and their direct GEM_CLOSE ioctls are never swallowed. */ + kernel_error = 0; + assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && handle == 42); + empty_table(); + assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) == 0); + fixture_close(out); + close_request.handle = handle; + assert(bl_drm_gem_close(drm, GEM_CLOSE, &close_request, &rc) == 0); + assert(drmCloseBufferHandle(drm, handle) == 0); + assert(native_imports && native_exports && native_closes); + kernel_error = EINVAL; + + /* Slot reuse must not reuse public tokens or leak either owned descriptor. */ + uint32_t previous = 0; + for (int n = 0; n < 1000; ++n) { + assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && handle != previous); + assert(drmCloseBufferHandle(drm, handle) == 0); + if (previous) assert(drmPrimeHandleToFD(drm, previous, O_CLOEXEC, &out) != 0); + previous = handle; + } + assert(count_fds() == baseline); + empty_table(); + + assert(drmPrimeFDToHandle(drm, dma, &handle) == 0); + pid_t child = fork(); + assert(child >= 0); + if (child == 0) { + assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) == 0); + fixture_close(out); + assert(drmCloseBufferHandle(drm, handle) == 0); + _exit(0); + } + int status; + assert(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0); + assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) == 0); + fixture_close(out); + assert(drmCloseBufferHandle(drm, handle) == 0); + + /* Preserve the original device layout when PowerVR owns renderD128. */ + pvr_node = "/dev/dri/renderD128"; + setenv("BL_POWERVR_RENDER_NODE", pvr_node, 1); + assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && (handle & PVR_HANDLE_BIT)); + assert(drmCloseBufferHandle(drm, handle) == 0); + + /* Retain the old KGSL path with the PowerVR opt-in unset. */ + unsetenv("BL_POWERVR_DRM_HANDLES"); + kgsl_present = 1; + kgsl_state = 0; + assert(drmPrimeFDToHandle(dma, drm, &handle) == 0 && !(handle & PVR_HANDLE_BIT)); + assert(drmPrimeHandleToFD(dma, handle, O_CLOEXEC | O_RDWR, &out) == 0); + fixture_close(out); + close_request.handle = handle; + assert(bl_drm_gem_close(dma, GEM_CLOSE, &close_request, &rc) == 1 && rc == 0); + assert(drmCloseBufferHandle(dma, handle) == -EINVAL); + empty_table(); + assert(count_fds() == baseline); + fixture_close(drm); + fixture_close(dma); + puts("DRM adapter scoping, ownership, reuse, fork and kernel-passthrough tests passed"); +} diff --git a/tools/tests/test_drm_handles.py b/tools/tests/test_drm_handles.py new file mode 100644 index 000000000..5f638a2b4 --- /dev/null +++ b/tools/tests/test_drm_handles.py @@ -0,0 +1,26 @@ +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +class DrmHandleTest(unittest.TestCase): + def test_handle_lifetime_and_device_scope(self): + compiler = shutil.which(os.environ.get("CC", "cc")) + if not compiler: + self.fail("A C compiler is required for the DRM adapter regression test") + source = Path(__file__).with_name("drm_handles_test.c") + with tempfile.TemporaryDirectory(prefix="droiddeck-drm-") as tmp: + program = Path(tmp) / "drm-handles" + built = subprocess.run([compiler, "-std=gnu11", "-Wall", "-Wextra", "-Werror", + "-UNDEBUG", "-pthread", str(source), "-ldl", "-o", str(program)], + capture_output=True, text=True) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + ran = subprocess.run([str(program)], capture_output=True, text=True, timeout=30) + self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_dxvk_build.py b/tools/tests/test_dxvk_build.py new file mode 100644 index 000000000..0038d0dc9 --- /dev/null +++ b/tools/tests/test_dxvk_build.py @@ -0,0 +1,121 @@ +"""Exercise output isolation with a synthetic toolchain; no network/DLL build.""" +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] + +# Substitute only external source/compiler/archive commands. The real build.sh +# owns output checks, architecture iteration and manifest inputs. +FAKE_TOOL = r''' +import hashlib, json, os +from pathlib import Path +import sys +name = Path(sys.argv[0]).name +args = sys.argv[1:] +with open(os.environ['TOOL_CALLS'], 'a') as stream: + stream.write(name + '\n') +if name == 'git': + if args[0] == 'init': + Path(args[-1]).mkdir() + (Path(args[-1]) / 'LICENSE').write_text('synthetic public license') + elif 'rev-parse' in args: + print(os.environ['TEST_REVISION']) +elif name == 'meson': + if args[0] == 'setup': + p = Path(args[1]); p.mkdir() + p.joinpath('config.json').write_text(json.dumps({ + 'out': args[args.index('--prefix') + 1], + 'arch': args[args.index('--bindir') + 1]})) + elif args[0] == 'install': + config = json.loads(Path(args[2], 'config.json').read_text()) + out = Path(config['out'], config['arch']); out.mkdir(exist_ok=True) + out.joinpath('current.dll').write_bytes(b'synthetic current pinned DLL') + if config['arch'] == os.environ.get('FAIL_INSTALL_ARCH'): + sys.exit(5) +elif name == 'tar': + Path(args[args.index('-cf') + 1]).write_bytes(b'synthetic source archive') +elif name == 'sha256sum': + for arg in args: + print(hashlib.sha256(Path(arg).read_bytes()).hexdigest() + ' ' + arg) +elif name != 'python3': + raise AssertionError(name) +''' + + +class DxvkBuildTest(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.base = Path(self.temporary.name) + self.bin = self.base / 'bin'; self.bin.mkdir() + executable = self.bin / 'fake-tool' + executable.write_text('#!' + sys.executable + '\n' + FAKE_TOOL) + executable.chmod(0o755) + for name in ('git', 'meson', 'tar', 'sha256sum', 'python3'): + (self.bin / name).symlink_to(executable) + self.calls = self.base / 'calls' + self.env = dict(os.environ, PATH=str(self.bin) + os.pathsep + os.environ['PATH'], + TOOL_CALLS=str(self.calls)) + source_env = ROOT / 'tools/dxvk/source.env' + self.env['TEST_REVISION'] = next(line.split('=', 1)[1].strip('"\'') + for line in source_env.read_text().splitlines() + if line.startswith('DXVK_REVISION=')) + self.output = self.base / 'component' + + def build(self): + return subprocess.run(['bash', str(ROOT / 'tools/dxvk/build.sh'), str(self.output)], + env=self.env, capture_output=True, text=True, timeout=10) + + def test_stale_dll_is_rejected_untouched_before_fetch(self): + stale = self.output / 'x32/stale.dll'; stale.parent.mkdir(parents=True) + stale.write_bytes(b'prior pinned build') + result = self.build() + self.assertNotEqual(result.returncode, 0) + self.assertIn('output directory must be empty', result.stderr) + self.assertEqual(stale.read_bytes(), b'prior pinned build') + self.assertFalse(self.calls.exists()) + self.assertFalse((self.output / 'SHA256SUMS').exists()) + + def test_hidden_contents_are_also_rejected(self): + self.output.mkdir(); (self.output / '.previous').write_text('retained') + self.assertNotEqual(self.build().returncode, 0) + self.assertFalse(self.calls.exists()) + self.assertEqual((self.output / '.previous').read_text(), 'retained') + + def test_empty_or_new_output_contains_only_current_dlls(self): + for existing in (False, True): + with self.subTest(existing=existing): + self.output = self.base / ('existing' if existing else 'new') + if existing: self.output.mkdir() + result = self.build() + self.assertEqual(result.returncode, 0, result.stderr) + rows = (self.output / 'SHA256SUMS').read_text().splitlines() + self.assertEqual([row.split(' ')[1] for row in rows], + ['x32/current.dll', 'x64/current.dll']) + for row in rows: + digest, path = row.split(' ') + self.assertEqual(digest, hashlib.sha256((self.output / path).read_bytes()).hexdigest()) + + def test_partial_build_is_not_reused_or_deleted(self): + self.env['FAIL_INSTALL_ARCH'] = 'x64' + result = self.build() + self.assertEqual(result.returncode, 5, result.stderr) + before = {str(p.relative_to(self.output)): p.read_bytes() + for p in self.output.rglob('*') if p.is_file()} + self.calls.unlink(); del self.env['FAIL_INSTALL_ARCH'] + result = self.build() + self.assertNotEqual(result.returncode, 0) + self.assertFalse(self.calls.exists()) + after = {str(p.relative_to(self.output)): p.read_bytes() + for p in self.output.rglob('*') if p.is_file()} + self.assertEqual(after, before) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tests/test_effects_source_resolution.py b/tools/tests/test_effects_source_resolution.py index 2a229812f..043f39212 100644 --- a/tools/tests/test_effects_source_resolution.py +++ b/tools/tests/test_effects_source_resolution.py @@ -26,6 +26,7 @@ class EffectsSourceResolutionTest(unittest.TestCase): header = (REPO / 'app/src/main/cpp/waylandcomp/src/vk_present.h').read_text() effects = (REPO / 'app/src/main/cpp/waylandcomp/src/effects_chain.c').read_text() functions = '\n'.join(function(source, signature) for signature in ( + 'static void release_external_client_image(', 'static void release_external_client_draws(', 'static int map_draw(', 'static int draw_to_blit(', 'static int draw_to_scene_blit(', 'static int effects_source_draw(', 'static void effects_source_note(', @@ -37,11 +38,13 @@ class EffectsSourceResolutionTest(unittest.TestCase): image = source[source.index('struct vkp_image {'):source.index('\n};', source.index('struct vkp_image {')) + 3] draw = header[header.index('struct vkp_draw {'):header.index('\n};', header.index('struct vkp_draw {')) + 3] hdr = header[header.index('struct vkp_hdr_frame {'):header.index('\n};', header.index('struct vkp_hdr_frame {')) + 3] - constants = sorted(set(re.findall(r'\bVK_[A-Z0-9_]+\b', functions + image)) - + sync = (REPO / 'app/src/main/cpp/waylandcomp/src/vk_external_image_sync.h').read_text() + sync = re.sub(r'^#(include|ifndef|define|endif).*$', '', sync, flags=re.M) + constants = sorted(set(re.findall(r'\bVK_[A-Z0-9_]+\b', functions + image + sync)) - {'VK_FORMAT_R8G8B8A8_UNORM'}) definitions = '\n'.join(f'#define {name} {0 if name in ("VK_SUCCESS", "VK_NULL_HANDLE") else i + 1}' for i, name in enumerate(constants)) - code = TYPES + definitions + '\n' + image + '\n' + draw + '\n' + hdr + STUBS + functions + CASES + code = TYPES + definitions + '\n' + sync + '\n' + image + '\n' + draw + '\n' + hdr + STUBS + functions + CASES cls.temp = tempfile.TemporaryDirectory(prefix='droiddeck-effects-resolution-') cls.addClassCleanup(cls.temp.cleanup) folder = Path(cls.temp.name) @@ -148,10 +151,12 @@ typedef struct { #define droiddeck_log(...) ((void)0) #define LOGE(...) ((void)0) struct droiddeck_color { uint32_t identity; }; +typedef struct AHardwareBuffer AHardwareBuffer; ''' STUBS = r''' +static void quarantine_pending_work(VkResult result, const char *where) { (void)result; (void)where; } static int noop(int handle, ...) { return VK_SUCCESS; } static VkImageMemoryBarrier recorded_barriers[64]; static int barrier_count; @@ -173,7 +178,7 @@ static struct { int (*EndCommandBuffer)(int, ...), (*ResetFences)(int, ...); int (*QueueSubmit)(int, ...), (*QueuePresentKHR)(int, ...); } g_vk = {noop, noop, note_barriers, noop, noop, noop, noop, noop}; -static int g_dev_state, g_window = 1, g_swapchain, g_swap_extra; +static int g_dev_state = 1, g_window = 1, g_swapchain, g_swap_extra; static int g_colorspace_ext, g_swap_hdr_unavailable, g_swap_is_hdr, g_swap_hdr_want; static int g_plain_frame, g_first_frame_done, g_base_black; static int g_qfam, g_queue, g_dev, g_fence, g_swap_fmt = SCENE_FMT; @@ -195,7 +200,7 @@ static VkImageBlit copied; static struct vkp_draw recorded_draws[16]; static VkImageBlit recorded_blits[16]; static VkImage recorded_targets[16]; -static int recorded_after_effects[16]; +static int recorded_after_effects[16], recorded_barrier_marks[16]; static void vkp_apply_window_request(void) {} static void sc_layer_hide_base(void) {} static void check_surface_changed(void) {} @@ -238,6 +243,7 @@ static void record_draw(int cmd, const struct vkp_draw *draw, const VkImageBlit assert(copy_count < 16); recorded_draws[copy_count] = *draw; recorded_blits[copy_count] = *blit; recorded_targets[copy_count] = target; recorded_after_effects[copy_count] = effects_count > 0; + recorded_barrier_marks[copy_count] = barrier_count; copy_count++; copied = *blit; copied_w = draw->dw; copied_h = draw->dh; target_w = w; target_h = h; } @@ -509,11 +515,16 @@ static void check_overlay_barriers(void) { reset(); struct vkp_draw layers[] = {background_draw(), fullscreen(), cursor_draw()}; run_path(offscreen, layers, 3, NULL); check_input(1280, 720); - int acquired = find_barrier(cursor.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + int acquired = find_barrier(cursor.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); assert(acquired >= 0 && recorded_barriers[acquired].srcQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT); int writable = find_barrier(4, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); int readable = find_barrier(4, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); assert(writable > acquired && readable > writable); + int released = find_barrier(cursor.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL); + assert(released > acquired && recorded_barriers[released].dstQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT); + int last_cursor_draw = -1; + for (int i = 0; i < copy_count; i++) if (recorded_draws[i].img == &cursor) last_cursor_draw = i; + assert(last_cursor_draw >= 0 && released >= recorded_barrier_marks[last_cursor_draw]); } } static void check_postprocessing(void) { diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index 4a9eb32fa..dfc65de1c 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -11,6 +11,7 @@ import sys import tempfile from unittest import mock import unittest +from unittest.mock import patch BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin" MODULE = runpy.run_path(str(BIN / "droiddeck-game-env")) @@ -410,6 +411,154 @@ class EgoCorePlanTest(unittest.TestCase): self.assertIn("added a copy of its 4-core plan", result.stderr) +class SystemGraphicsLaunchTest(unittest.TestCase): + def test_start_configures_ubisoft_with_proton_before_wrapping_the_game(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runtime, _ = self.runtime(root) + prefix = root / "42" / "pfx" + (prefix / MODULE["UBISOFT_LAUNCHER"]).mkdir(parents=True) + (prefix / "user.reg").write_text("WINE REGISTRY Version 2\n") + env = {"DROIDDECK_POWERVR_RUNTIME": str(runtime), "STEAM_COMPAT_DATA_PATH": str(prefix.parent)} + command = ["/selected/proton", "waitforexitandrun", "game.exe"] + with mock.patch("subprocess.run") as run, mock.patch("os.execvpe") as execute: + MODULE["start"](command, env, None) + self.assertEqual(run.call_count, 2) + for call in run.call_args_list: + self.assertEqual(call.args[0][:3], ["/selected/proton", "runinprefix", "reg.exe"]) + self.assertEqual(execute.call_args.args[:2], (str(runtime / "run-proton.py"), + [str(runtime / "run-proton.py"), *command])) + + def runtime(self, root): + runtime = root / "system_vulkan" / ("a" * 64) + files = ["run-proton.py", "lib/libVkLayer_hybris_compat.so", + "wsi/libVkLayer_window_system_integration.so", + "wsi/VkLayer_window_system_integration.json"] + files += ["dxvk/%s/%s.dll" % (arch, name) for arch in ("x32", "x64") + for name in ("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi")] + for name in files: + target = runtime / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(b"component") + (runtime / "run-proton.py").chmod(0o755) + return runtime, files + + def test_other_gpus_and_non_game_calls_keep_the_original_launch(self): + command = ["/proton", "waitforexitandrun", "game.exe"] + base = {"STEAM_COMPAT_DATA_PATH": "/compatdata/42", "VK_LAYER_PATH": "original"} + selected = dict(base, DROIDDECK_POWERVR_RUNTIME="/not-installed") + cases = [(command, base), (command, dict(selected, STEAM_COMPAT_DATA_PATH="/compatdata/0")), + (command, dict(selected, STEAM_COMPAT_DATA_PATH="/compatdata/invalid")), + (["/proton", "run", "iscriptevaluator.exe"], selected), + (["/proton", "getcompatpath", "x"], selected), (command[:2], selected)] + for args, env in cases: + with self.subTest(args=args, env=env): + actual, inherited = MODULE["system_graphics_launch"](args, env) + self.assertIs(actual, args) + self.assertIs(inherited, env) + + def test_system_graphics_follow_game_configuration_and_preserve_other_keys(self): + with tempfile.TemporaryDirectory() as tmp: + runtime, _ = self.runtime(Path(tmp)) + base = {"DROIDDECK_POWERVR_RUNTIME": str(runtime), "STEAM_COMPAT_DATA_PATH": "/compatdata/42"} + config = {"version": 1, "games": {"42": { + "PROTON_USE_WINED3D": "1", "DISABLE_WSI_LAYER": "1", + "ENABLE_VK_LAYER_VALVE_steam_fossilize_1": "1", "HYBRIS_BC_TEXTURES": "0", + "VK_LAYER_PATH": "old", "VK_INSTANCE_LAYERS": "old", "ENABLE_GAMESCOPE_WSI": "1", + "FEX_TSOENABLED": "1", "WINESERVER": "/selected/wineserver", "CUSTOM": "literal $value", + }}} + env = MODULE["apply_config"](base, config, "42") + command = ["/selected/proton", "waitforexitandrun", "game with spaces.exe"] + launch, result = MODULE["system_graphics_launch"](command, env) + self.assertEqual(launch, [str(runtime / "run-proton.py"), *command]) + self.assertEqual(result["HYBRIS_BC_TEXTURES"], "dxvk") + self.assertEqual(result["VK_LAYER_PATH"], f"{runtime}/wsi:{runtime}/lib") + self.assertEqual(result["VK_INSTANCE_LAYERS"], "VK_LAYER_window_system_integration:VK_LAYER_HYBRIS_compat") + self.assertEqual(result["VK_LOADER_LAYERS_DISABLE"], "*gamescope*") + self.assertEqual(result["ENABLE_GAMESCOPE_WSI"], "0") + self.assertEqual(result["DISABLE_VK_LAYER_VALVE_steam_fossilize_1"], "1") + for key in ("PROTON_USE_WINED3D", "DISABLE_WSI_LAYER", "ENABLE_VK_LAYER_VALVE_steam_fossilize_1"): + self.assertNotIn(key, result) + self.assertIn(key, env) + for key in ("FEX_TSOENABLED", "WINESERVER", "CUSTOM"): + self.assertEqual(result[key], env[key]) + + def test_xclipse_selection_adds_its_layers_layer_path_and_dxvk_payload(self): + with tempfile.TemporaryDirectory() as tmp: + runtime, _ = self.runtime(Path(tmp)) + env = {"DROIDDECK_POWERVR_RUNTIME": str(runtime), "STEAM_COMPAT_DATA_PATH": "/compatdata/42", + "DROIDDECK_HYBRIS_LAYERS": "VK_LAYER_DROIDDECK_xclipse:VK_LAYER_HYBRIS_compat", + "DROIDDECK_HYBRIS_LAYER_PATH": "/usr/local/lib/droiddeck-xclipse", + "DROIDDECK_HYBRIS_DXVK": "dxvk-2.6.2"} + command = ["/proton", "waitforexitandrun", "game.exe"] + # The selected payload is required like the default one. + with self.assertRaises(MODULE["SystemGraphicsError"]): + MODULE["system_graphics_launch"](command, env) + for arch in ("x32", "x64"): + for name in ("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi"): + target = runtime / "dxvk-2.6.2" / arch / (name + ".dll") + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(b"component") + launch, result = MODULE["system_graphics_launch"](command, env) + self.assertEqual(launch, [str(runtime / "run-proton.py"), *command]) + self.assertEqual(result["VK_INSTANCE_LAYERS"], + "VK_LAYER_window_system_integration:VK_LAYER_DROIDDECK_xclipse:VK_LAYER_HYBRIS_compat") + self.assertEqual(result["VK_LAYER_PATH"], f"{runtime}/wsi:{runtime}/lib:/usr/local/lib/droiddeck-xclipse") + self.assertEqual(result["DROIDDECK_HYBRIS_DXVK"], "dxvk-2.6.2") + for name in ("../dxvk", "other", "dxvk/x"): + with self.subTest(name=name), self.assertRaises(MODULE["SystemGraphicsError"]): + MODULE["system_graphics_launch"](command, dict(env, DROIDDECK_HYBRIS_DXVK=name)) + + def test_incomplete_runtime_fails_before_any_proton_exec(self): + with tempfile.TemporaryDirectory() as tmp: + runtime, files = self.runtime(Path(tmp)) + env = {"DROIDDECK_POWERVR_RUNTIME": str(runtime), "STEAM_COMPAT_DATA_PATH": "/compatdata/42"} + command = ["/proton", "waitforexitandrun", "game.exe"] + for name in files: + target = runtime / name + original = target.read_bytes() + for missing in (False, True): + with self.subTest(name=name, missing=missing): + target.unlink() if missing else target.write_bytes(b"") + with patch.object(os, "execvpe") as execute: + with self.assertRaises(MODULE["SystemGraphicsError"]): + MODULE["exec_proton"](command, env) + execute.assert_not_called() + target.write_bytes(original) + if name == "run-proton.py": + target.chmod(0o755) + (runtime / "run-proton.py").chmod(0o644) + with self.assertRaises(MODULE["SystemGraphicsError"]): + MODULE["system_graphics_launch"](command, env) + with self.assertRaises(MODULE["SystemGraphicsError"]): + MODULE["system_graphics_launch"](command, dict(env, DROIDDECK_POWERVR_RUNTIME="relative")) + + def test_wrapper_receives_the_sync_selected_proton(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runtime, _ = self.runtime(root) + env = {"HOME": tmp, "DROIDDECK_POWERVR_RUNTIME": str(runtime), + "STEAM_COMPAT_DATA_PATH": "/compatdata/42"} + command = ["/stock/proton", "waitforexitandrun", "game.exe"] + selected = ["/sync-pack/proton", *command[1:]] + selected_env = dict(env, WINESERVER="/sync-pack/wineserver", BL_SYNC_PACK="selected") + main = MODULE["main"] + with patch.dict(os.environ, env, clear=True), patch.object(sys, "argv", ["game-env", *command]), \ + patch.dict(main.__globals__, {"load_sync": lambda: {}, + "select_sync": lambda *args: (selected, selected_env), + "win_components": lambda *args: ""}), \ + patch.object(os, "execvpe", side_effect=SystemExit(23)) as execute: + with self.assertRaises(SystemExit) as stopped: + main() + self.assertEqual(stopped.exception.code, 23) + execute.assert_called_once() + path, args, actual_env = execute.call_args.args + self.assertEqual(path, str(runtime / "run-proton.py")) + self.assertEqual(args, [path, *selected]) + self.assertEqual(actual_env["WINESERVER"], "/sync-pack/wineserver") + self.assertEqual(actual_env["BL_SYNC_PACK"], "selected") + + class GameDebugLoggingTest(unittest.TestCase): def test_maintenance_precision_survives_run_with_performance_game_settings(self): with tempfile.TemporaryDirectory() as tmp: @@ -807,6 +956,55 @@ class GameEnvironmentTest(unittest.TestCase): subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True) self.assertEqual(target.read_text(), "general") + def test_system_powervr_preserves_dxvk_profiles_without_applying_them(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + home = root / "home" + component_dir = home / ".local/share/droiddeck-components" + component_dir.mkdir(parents=True) + base = root / "proton" + paths = { + "dxvk": "files/lib/wine/dxvk/aarch64-windows/d3d11.dll", + "fex": "files/lib/wine/aarch64-windows/libarm64ecfex.dll", + "vkd3d": "files/lib/wine/vkd3d-proton/aarch64-windows/d3d12.dll", + } + rows = [] + for comp, rel in paths.items(): + target = base / rel + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("bundled " + comp) + for scope in ("*", "42"): + source = root / (comp + ("-general" if scope == "*" else "-game")) + source.write_text(source.name) + rows.append("%s\tproton\tbuild-1\t%s\t%s\t%s\t%s\n" % ( + scope, comp, rel, hashlib.sha256(source.read_bytes()).hexdigest(), source, + )) + leftover = (base / paths["dxvk"]).with_name("d3d8.dll") + leftover.write_text("bundled d3d8") + rows.append("42\tproton\tbuild-1\tdxvk\t%s\t-\t-\n" % leftover.relative_to(base)) + (base / "version").write_text("1 build-1\n") + selection = component_dir / "desired.tsv" + selection.write_text("".join(rows)) + saved = selection.read_bytes() + script = COMPAT["BL_COMPONENTS_SETUP"] + '\nverb=waitforexitandrun; bl_components "$1"\n' + env = dict(os.environ, HOME=str(home), STEAM_COMPAT_DATA_PATH="/steam/compatdata/42", + DROIDDECK_POWERVR_RUNTIME="/system_vulkan/runtime") + + subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True) + + self.assertEqual((base / paths["dxvk"]).read_text(), "bundled dxvk") + self.assertEqual(leftover.read_text(), "bundled d3d8") + for comp in ("fex", "vkd3d"): + self.assertEqual((base / paths[comp]).read_text(), comp + "-game") + self.assertEqual(selection.read_bytes(), saved) + + # A subsequent manual-driver session still applies the saved per-game DXVK choice. + env.pop("DROIDDECK_POWERVR_RUNTIME") + subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True) + self.assertEqual((base / paths["dxvk"]).read_text(), "dxvk-game") + self.assertFalse(leftover.exists()) + self.assertEqual(selection.read_bytes(), saved) + def test_component_scope_matches_environment_scope_for_shortcut_ids(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) @@ -1294,6 +1492,49 @@ class GameEnvironmentTest(unittest.TestCase): with self.assertRaises(ValueError): MODULE["apply_config"]({}, {**config, "dxvkConfigGames": []}, "42") + def test_game_logs_only_when_asked_and_only_for_game_launches(self): + game_logs = MODULE["game_logs"] + with tempfile.TemporaryDirectory() as tmp: + command = ["/proton", "waitforexitandrun", "/games/Hades/Hades.exe"] + env = {"BL_DEBUG_DIR": tmp, "VK_DRIVER_FILES": "/icd.json"} + self.assertEqual(game_logs(command, env, "1145360", now=0), env) + on = dict(env, BL_GAME_LOGS="1") + self.assertEqual(game_logs(["/proton", "run", "x.exe"], on, "1145360", now=0), on) + result = game_logs(command, on, "1145360", now=0) + folder = Path(result["PROTON_LOG_DIR"]) + self.assertEqual(folder.parent, Path(tmp) / "games") + self.assertTrue(folder.name.endswith("-1145360")) + self.assertEqual(result["DXVK_LOG_PATH"], str(folder)) + self.assertEqual(result["PROTON_LOG"], "1") + launch = (folder / "launch.txt").read_text() + self.assertIn("VK_DRIVER_FILES=/icd.json", launch) + self.assertNotIn("PROTON_LOG", env) + named = game_logs(command, on, None, now=0) + self.assertTrue(Path(named["PROTON_LOG_DIR"]).name.endswith("-Hades")) + + def test_rapid_game_launches_keep_separate_diagnostics(self): + with tempfile.TemporaryDirectory() as tmp, mock.patch("subprocess.Popen"): + env = {"BL_GAME_LOGS": "1", "BL_DEBUG_DIR": tmp} + first = MODULE["game_logs"](["/proton", "waitforexitandrun", "first.exe"], env, "42", now=0) + second = MODULE["game_logs"](["/proton", "waitforexitandrun", "second.exe"], env, "42", now=0) + self.assertNotEqual(first["PROTON_LOG_DIR"], second["PROTON_LOG_DIR"]) + self.assertIn("exe: first.exe", (Path(first["PROTON_LOG_DIR"]) / "launch.txt").read_text()) + self.assertIn("exe: second.exe", (Path(second["PROTON_LOG_DIR"]) / "launch.txt").read_text()) + + def test_open_driver_pairs_report_the_drivers_own_vkd3d_caps(self): + apply = MODULE["apply_config"] + config = {"version": 1, "shared": {"VKD3D_FEATURE_LEVEL": "12_2", "VKD3D_SHADER_MODEL": "6_9"}, + "games": {"42": {"VKD3D_SHADER_MODEL": "6_6"}}} + adreno = apply({}, config, "1") + self.assertEqual(adreno["VKD3D_FEATURE_LEVEL"], "12_2") + pair = apply({"BL_OPEN_DRIVER": "1"}, config, "1") + self.assertNotIn("VKD3D_FEATURE_LEVEL", pair) + self.assertNotIn("VKD3D_SHADER_MODEL", pair) + self.assertEqual(pair["DROIDDECK_VKD3D_CAPS"], "driver") + own = apply({"BL_OPEN_DRIVER": "1"}, config, "42") + self.assertEqual(own["VKD3D_SHADER_MODEL"], "6_6") + self.assertNotIn("VKD3D_FEATURE_LEVEL", own) + def test_game_ids_and_probes(self): for prefix in ("", "/compatdata/0", "/compatdata/0-123", "/compatdata/nope", "/compatdata/4294967296"): self.assertIsNone(MODULE["game_id"]({"STEAM_COMPAT_DATA_PATH": prefix})) diff --git a/tools/tests/test_gamescope_backend.py b/tools/tests/test_gamescope_backend.py new file mode 100644 index 000000000..d9f91bf82 --- /dev/null +++ b/tools/tests/test_gamescope_backend.py @@ -0,0 +1,102 @@ +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-session" +DESKTOP_GPU = Path(__file__).resolve().parents[1] / "linuxfs/desktop/droiddeck-gpu" + + +class GamescopeBackendTest(unittest.TestCase): + def select(self, backend=None): + source = SESSION.read_text() + start = source.index(' backend=${BL_GAMESCOPE_BACKEND:-wayland}') + end = source.index('\n', source.index(' args=(--backend ', start)) + script = 'width=1280; height=720; nested=1280\n' + source[start:end] + script += '\nprintf "%s\\n" "${args[0]}" "${args[1]}" "${SDL_VIDEODRIVER:-unset}"\n' + env = dict(os.environ) + env.pop("BL_GAMESCOPE_BACKEND", None) + env.pop("SDL_VIDEODRIVER", None) + if backend is not None: + env["BL_GAMESCOPE_BACKEND"] = backend + return subprocess.run(["bash", "-c", script], env=env, text=True, + capture_output=True, timeout=5) + + def test_default_retains_the_wayland_backend(self): + result = self.select() + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(["--backend", "wayland", "unset"], result.stdout.splitlines()) + + def test_sdl_uses_the_host_wayland_vulkan_swapchain(self): + result = self.select("sdl") + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(["--backend", "sdl", "wayland"], result.stdout.splitlines()) + + def test_unknown_backend_fails_before_gamescope(self): + result = self.select("drm") + self.assertEqual(64, result.returncode) + self.assertIn("unsupported gamescope backend: drm", result.stdout) + + +class DesktopGamescopeBackendTest(unittest.TestCase): + def launch(self, backend=None): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + capture = root / "gamescope.json" + gamescope = root / "gamescope" + gamescope.write_text( + f"#!{sys.executable}\n" + "import json, os, sys\n" + "from pathlib import Path\n" + "Path(os.environ['GAMESCOPE_CAPTURE']).write_text(json.dumps({\n" + " 'args': sys.argv[1:],\n" + " 'sdl': os.environ.get('SDL_VIDEODRIVER'),\n" + " 'gdk': os.environ.get('GDK_BACKEND'),\n" + " 'qt': os.environ.get('QT_QPA_PLATFORM'),\n" + " 'wayland': os.environ.get('WAYLAND_DISPLAY'),\n" + "}))\n" + ) + gamescope.chmod(0o755) + env = dict(os.environ) + for name in ("BL_GAMESCOPE_BACKEND", "BL_DEBUG_DIR", "BL_FPS", "BL_REFRESH", "BL_HDR"): + env.pop(name, None) + env.update(PATH=f"{root}{os.pathsep}{env.get('PATH', '')}", + GAMESCOPE_CAPTURE=str(capture), WAYLAND_DISPLAY="desktop-0", BL_DESKTOP_GPU_WINDOW="0", + SDL_VIDEODRIVER="dummy", GDK_BACKEND="wayland", QT_QPA_PLATFORM="wayland") + if backend is not None: + env["BL_GAMESCOPE_BACKEND"] = backend + result = subprocess.run(["bash", str(DESKTOP_GPU), "demo", "one argument"], + env=env, text=True, capture_output=True, timeout=5) + return result, json.loads(capture.read_text()) if capture.exists() else None + + def assert_xwayland_application(self, capture): + args = capture["args"] + self.assertEqual(["env", "SDL_VIDEODRIVER=x11", "/usr/local/bin/droiddeck-gpu-window", + "demo", "one argument"], args[args.index("--") + 1:]) + self.assertEqual("x11", capture["gdk"]) + self.assertEqual("xcb", capture["qt"]) + self.assertEqual("wayland-0", capture["wayland"]) + + def test_default_keeps_wayland_and_xwayland_application(self): + result, capture = self.launch() + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(["--backend", "wayland"], capture["args"][:2]) + self.assertEqual("x11", capture["sdl"]) + self.assert_xwayland_application(capture) + + def test_sdl_uses_wayland_without_leaking_to_the_application(self): + result, capture = self.launch("sdl") + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(["--backend", "sdl"], capture["args"][:2]) + self.assertEqual("wayland", capture["sdl"]) + self.assert_xwayland_application(capture) + + def test_unknown_backend_fails_before_gamescope(self): + result, capture = self.launch("drm") + self.assertEqual(64, result.returncode) + self.assertIn("unsupported gamescope backend: drm", result.stderr) + self.assertIsNone(capture) diff --git a/tools/tests/test_oom_adj_preload.py b/tools/tests/test_oom_adj_preload.py new file mode 100644 index 000000000..f6fc25ce1 --- /dev/null +++ b/tools/tests/test_oom_adj_preload.py @@ -0,0 +1,47 @@ +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +SOURCE = Path(__file__).resolve().parents[1] / "linuxfs/preload/oomadj.c" + +PROGRAM = r""" +#include +const char *bl_oom_adj_redirect(const char *path, int writing); +int main(int argc, char **argv) { + for (int i = 1; i + 1 < argc; i += 2) printf("%s\n", bl_oom_adj_redirect(argv[i + 1], argv[i][0] == 'w')); + return 0; +} +""" + + +@unittest.skipUnless(shutil.which("cc"), "needs cc") +class OomAdjPreloadTest(unittest.TestCase): + def test_only_writes_to_oom_files_go_to_dev_null(self): + with tempfile.TemporaryDirectory() as tmp: + main = Path(tmp) / "main.c" + main.write_text(PROGRAM) + binary = Path(tmp) / "probe" + subprocess.run(["cc", "-o", str(binary), str(main), str(SOURCE)], check=True) + cases = [ + ("w", "/proc/self/oom_score_adj", "/dev/null"), + ("w", "/proc/123/oom_score_adj", "/dev/null"), + ("w", "/proc/thread-self/oom_score_adj", "/dev/null"), + ("w", "/proc/12/task/34/oom_score_adj", "/dev/null"), + ("w", "/proc/self/oom_adj", "/dev/null"), + ("r", "/proc/self/oom_score_adj", "/proc/self/oom_score_adj"), + ("w", "/proc/self/oom_score", "/proc/self/oom_score"), + ("w", "/proc/abc/oom_score_adj", "/proc/abc/oom_score_adj"), + ("w", "/tmp/oom_score_adj", "/tmp/oom_score_adj"), + ] + args = [str(binary)] + for mode, path, _ in cases: + args += [mode, path] + out = subprocess.run(args, capture_output=True, text=True, check=True) + self.assertEqual(out.stdout.split(), [want for _, _, want in cases]) + self.assertIn("oom_score_adj raise was dropped", out.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_pixel_reset_diagnostics.py b/tools/tests/test_pixel_reset_diagnostics.py new file mode 100644 index 000000000..8cb5ba1a0 --- /dev/null +++ b/tools/tests/test_pixel_reset_diagnostics.py @@ -0,0 +1,215 @@ +"""Test freshness, narrow output, and lock handling without accessing a device.""" +from datetime import datetime, timezone +import importlib.util +import json +import io +import os +from pathlib import Path +import stat +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location('reset_projection', Path(__file__).resolve().parents[1] / 'pixel_reset_diagnostics.py') +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +class ResetProjectionTest(unittest.TestCase): + def test_only_primary_state(self): + self.assertEqual(module.primary_state('Private space secret\nStarted users state: [0=RUNNING_UNLOCKED, 10=STOPPED]'), 'RUNNING_UNLOCKED') + self.assertEqual(module.primary_state('State: RUNNING_UNLOCKED'), 'unknown') + + def test_stale_body_not_used(self): + raw = '2026-10-10 20:10:09 SYSTEM_LAST_KMSG (text, 1024 bytes)\nGNOC error type 0x2000' + result = module.kernel_projection(raw, 'America/Toronto', datetime(2026, 10, 11, 21, tzinfo=timezone.utc)) + self.assertEqual(result['status'], 'stale_entry_not_attributed_to_current_trial') + self.assertNotIn('categories', result) + + def test_repeated_dst_hour_is_ambiguous_before_freshness(self): + raw = '2026-11-01 01:30:00 SYSTEM_LAST_KMSG\nGNOC error type 0x2000' + for after in (None, datetime(2026, 11, 1, 6, tzinfo=timezone.utc)): + with self.subTest(after=after): + result = module.kernel_projection(raw, 'America/Toronto', after) + self.assertEqual(result['status'], 'ambiguous_device_local_time') + self.assertNotIn('entry_date_utc', result) + self.assertNotIn('categories', result) + self.assertNotIn('hardware_fields', result) + + def test_skipped_dst_hour_is_not_a_real_instant(self): + raw = '2026-03-08 02:30:00 SYSTEM_LAST_KMSG\nGNOC error type 0x2000' + result = module.kernel_projection(raw, 'America/Toronto') + self.assertEqual(result['status'], 'nonexistent_device_local_time') + self.assertNotIn('entry_date_utc', result) + self.assertNotIn('categories', result) + + def test_unambiguous_times_keep_correct_offsets_and_cutoff(self): + for stamp, expected in (('2026-11-01 00:30:00', '2026-11-01T04:30:00+00:00'), + ('2026-11-01 02:30:00', '2026-11-01T07:30:00+00:00'), + ('2026-03-08 03:30:00', '2026-03-08T07:30:00+00:00')): + with self.subTest(stamp=stamp): + raw = stamp + ' SYSTEM_LAST_KMSG\nGNOC' + result = module.kernel_projection(raw, 'America/Toronto') + self.assertEqual(result['entry_date_utc'], expected) + self.assertEqual(result['status'], 'projected_entry_not_a_root_cause') + cutoff = datetime.fromisoformat(expected) + self.assertEqual(module.kernel_projection(raw, 'America/Toronto', cutoff)['status'], + 'projected_entry_not_a_root_cause') + + def test_latest_entry_and_no_arbitrary_body(self): + raw = ('2026-10-10 20:10:09 SYSTEM_LAST_KMSG (text)\nGNOC error type 0x1111\n' + '2026-10-11 17:35:01 SYSTEM_LAST_KMSG (text)\n' + 'PRIVATE_TEST_VALUE=a_secret_that_must_not_be_emitted\n' + 'Error from GNOC at addr 0x28220000\nGNOC error type 0x2000\n' + 'Reboot reason: 0xc005 - Secret arbitrary description\nLast boot reason: reset,warm,gnoc_error\n') + result = module.kernel_projection(raw, 'America/Toronto', datetime(2026, 10, 11, 21, tzinfo=timezone.utc)) + self.assertEqual(result['entry_date_utc'], '2026-10-11T21:35:01+00:00') + self.assertEqual(result['hardware_fields']['gnoc_error_types'], ['0x2000']) + self.assertNotIn('0x1111', json.dumps(result)) + self.assertNotIn('a_secret', json.dumps(result)) + self.assertNotIn('arbitrary description', json.dumps(result)) + self.assertFalse(result['root_cause_proven']) + + def test_missing_entry_and_timezone(self): + self.assertEqual(module.kernel_projection('Permission denied', 'America/Toronto')['status'], 'no_accessible_entry') + self.assertEqual(module.kernel_projection('2026-10-11 17:00:00 SYSTEM_LAST_KMSG\nGNOC', 'Unknown/Region')['status'], 'unknown_device_timezone') + + def test_reason_rejects_paths_and_urls(self): + self.assertEqual(module.safe_reason('reset,warm,gnoc_error\n'), 'reset,warm,gnoc_error') + self.assertEqual(module.safe_reason('https://private.invalid/login?secret=abc'), 'unknown') + self.assertEqual(module.safe_reason('/data/private'), 'unknown') + + def test_reason_only_recognized_exact_values(self): + for reason in ('cold', 'hard', 'warm', 'reboot', 'shutdown', 'recovery', + 'bootloader', 'kernel_panic', 'watchdog', 'reset,warm,gnoc_error'): + with self.subTest(reason=reason): + self.assertEqual(module.safe_reason(reason + '\n'), reason) + for reason in ('PRIVATE TEST VALUE', 'private_test_value', '123456', + 'reboot,private_test_value', 'reset,warm,gnoc_error,private_test_value', + 'reset,warm,gnoc_error trailing_private_test_value', ''): + with self.subTest(reason=reason): + self.assertEqual(module.safe_reason(reason), 'unknown') + + def test_kernel_reason_uses_same_fixed_projection(self): + raw = ('2026-10-11 17:35:01 SYSTEM_LAST_KMSG (text)\n' + 'Last boot reason: private_test_value\n' + 'Last boot reason: reset,warm,gnoc_error trailing_private_test_value\n' + 'Last boot reason: reset,warm,gnoc_error\n' + 'Last boot reason:\nPRIVATE TEST VALUE\n') + result = module.kernel_projection(raw, 'America/Toronto') + self.assertEqual(result['last_boot_reasons'], ['reset,warm,gnoc_error', 'unknown']) + self.assertNotIn('private_test_value', json.dumps(result)) + self.assertNotIn('PRIVATE TEST VALUE', json.dumps(result)) + + def test_primary_state_uses_fixed_categories(self): + for state in ('RUNNING_LOCKED', 'RUNNING_UNLOCKING', 'RUNNING_UNLOCKED'): + self.assertEqual(module.primary_state('Started users state: [0=' + state + ']'), state) + for state in ('RUNNING_PRIVATE_TEST_VALUE', 'RUNNING_UNLOCKED_PRIVATE', + 'RUNNING_UNLOCKED=private_test_value'): + self.assertEqual(module.primary_state('Started users state: [0=' + state + ']'), 'unknown') + + def test_property_reason_never_exports_custom_text(self): + def run(argv, **kwargs): + if argv[-2:] == ['getprop', 'ro.boot.bootreason']: + text = 'PRIVATE TEST VALUE' + elif argv[-2:] == ['getprop', 'sys.boot.reason']: + text = 'reset,warm,gnoc_error,private_test_value' + elif argv[-1] == 'persist.sys.timezone': + text = 'America/Toronto' + else: + text = '' + return subprocess.CompletedProcess(argv, 0, text, '') + with patch.object(module.subprocess, 'run', side_effect=run): + result = module.collect('adb', 'test-device') + self.assertEqual(result['bootloader_reason'], 'unknown') + self.assertEqual(result['system_boot_reason'], 'unknown') + self.assertNotIn('PRIVATE TEST VALUE', json.dumps(result)) + self.assertNotIn('private_test_value', json.dumps(result)) + + @unittest.skipUnless(os.name == 'posix', 'POSIX private file modes') + def test_cli_output_is_private_before_writing(self): + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / 'diagnostics.json' + saved_umask = os.umask(0) + original_fdopen = os.fdopen + original_path_open = Path.open + creation_modes = [] + def inspect_stream(stream): + try: + creation_modes.append(stat.S_IMODE(os.fstat(stream.fileno()).st_mode)) + self.assertEqual(os.fstat(stream.fileno()).st_size, 0) + self.assertEqual(creation_modes[-1], 0o600) + except BaseException: + stream.close() + raise + return stream + def inspect_fd(fd, *args, **kwargs): + return inspect_stream(original_fdopen(fd, *args, **kwargs)) + def inspect_path(path, *args, **kwargs): + stream = original_path_open(path, *args, **kwargs) + return inspect_stream(stream) if path == output else stream + try: + with patch.object(sys, 'argv', ['diagnostics', '--serial', 'test-device', + '--output', str(output)]), \ + patch.object(module, 'collect', return_value={'boot_count': 12}), \ + patch.object(os, 'fdopen', side_effect=inspect_fd), \ + patch.object(Path, 'open', new=inspect_path), \ + patch('sys.stdout', new_callable=io.StringIO): + module.main() + finally: + os.umask(saved_umask) + self.assertEqual(creation_modes, [0o600]) + self.assertEqual(stat.S_IMODE(output.stat().st_mode), 0o600) + self.assertEqual(json.loads(output.read_text()), {'boot_count': 12}) + + def test_output_refuses_existing_file(self): + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / 'diagnostics.json' + output.write_text('existing controlled test data') + with self.assertRaises(FileExistsError): + module.write_private_output(output, '{"replacement":true}\n') + self.assertEqual(output.read_text(), 'existing controlled test data') + + @unittest.skipUnless(os.name == 'posix', 'POSIX symlink behavior') + def test_output_refuses_symlink_without_touching_target(self): + with tempfile.TemporaryDirectory() as directory: + target = Path(directory) / 'target.json' + target.write_text('existing controlled test data') + output = Path(directory) / 'diagnostics.json' + output.symlink_to(target) + with self.assertRaises(FileExistsError): + module.write_private_output(output, '{"replacement":true}\n') + self.assertTrue(output.is_symlink()) + self.assertEqual(target.read_text(), 'existing controlled test data') + + def test_locked_skips_kernel_body(self): + commands = [] + def run(argv, **kwargs): + commands.append(argv) + if argv[-2:] == ['dumpsys', 'user']: + text = 'Started users state: [0=RUNNING_LOCKED]' + elif argv[-1] == 'persist.sys.timezone': + text = 'America/Toronto' + elif argv[-3:] == ['get', 'global', 'boot_count']: + text = '12' + else: + text = '' + return subprocess.CompletedProcess(argv, 0, text, 'PRIVATE_STDERR') + with patch.object(module.subprocess, 'run', side_effect=run): + result = module.collect('adb', 'test-device', True) + self.assertEqual(result['kernel']['status'], 'requires_user_unlock') + self.assertFalse(any('--print' in command for command in commands)) + self.assertNotIn('PRIVATE_STDERR', json.dumps(result)) + + def test_command_failure_does_not_export_stderr(self): + with patch.object(module.subprocess, 'run', return_value=subprocess.CompletedProcess([], 1, 'UNRELATED_DATA', 'PRIVATE_STDERR')): + result = module.collect('adb', 'test-device', True) + self.assertIsNone(result['boot_count']) + self.assertNotIn('UNRELATED_DATA', json.dumps(result)) + self.assertNotIn('PRIVATE_STDERR', json.dumps(result)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tests/test_powervr_runtime.py b/tools/tests/test_powervr_runtime.py new file mode 100644 index 000000000..475f89927 --- /dev/null +++ b/tools/tests/test_powervr_runtime.py @@ -0,0 +1,291 @@ +import importlib.util +import io +import json +from pathlib import Path +import subprocess +import tarfile +import tempfile +import unittest +from unittest.mock import patch +from zipfile import ZipFile, ZipInfo + + +spec = importlib.util.spec_from_file_location('powervr_fetch', + Path(__file__).resolve().parents[1] / 'powervr/fetch.py') +runtime = importlib.util.module_from_spec(spec) +spec.loader.exec_module(runtime) + + +def elf(): + content = bytearray(64) + content[:6], content[18:20] = b'\x7fELF\x02\x01', b'\xb7\x00' + return bytes(content) + + +def pe(architecture): + content = bytearray(128) + content[:2], content[60:64], content[64:68] = b'MZ', (64).to_bytes(4, 'little'), b'PE\0\0' + content[68:70] = b'\x4c\x01' if architecture == 'x32' else b'\x64\x86' + content[88:90] = b'\x0b\x01' if architecture == 'x32' else b'\x0b\x02' + return bytes(content) + + +def tar(entries): + stream = io.BytesIO() + with tarfile.open(fileobj=stream, mode='w') as archive: + for name, kind, content in entries: + info = tarfile.TarInfo(name) + info.type = kind + if kind in (tarfile.SYMTYPE, tarfile.LNKTYPE): + info.linkname = content + archive.addfile(info) + elif kind == tarfile.REGTYPE: + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + else: + archive.addfile(info) + return subprocess.run(['zstd', '-q', '-c'], input=stream.getvalue(), + capture_output=True, check=True).stdout + + +def tar_gz(entries): + stream = io.BytesIO() + with tarfile.open(fileobj=stream, mode='w:gz') as archive: + for name, kind, content in entries: + info = tarfile.TarInfo(name) + info.type = kind + if kind == tarfile.SYMTYPE: + info.linkname = content + archive.addfile(info) + elif kind == tarfile.REGTYPE: + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + else: + archive.addfile(info) + return stream.getvalue() + + +def upstream_dxvk(version, architecture_of=lambda arch: arch): + return {'dxvk-%s/%s/%s' % (version, arch, name): pe(architecture_of(arch)) + for arch in ('x32', 'x64') + for name in ('d3d8.dll', 'd3d9.dll', 'd3d10core.dll', 'd3d11.dll', 'dxgi.dll')} + + +class PowerVRRuntimeTest(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.cache = self.root / 'cache' + self.cache.mkdir() + for name, content in { + 'tools/powervr/fetch.py': b'fixture assembler', + 'tools/hybris/run-proton.py': b'print("fixture Proton wrapper")\n', + 'tools/hybris/source.env': b'HYBRIS_REVISION=fixture\n', + 'tools/vulkan-wsi/licenses/Vulkan-Headers/LICENSE.md': b'Khronos headers attribution\n', + 'tools/vulkan-wsi/licenses/Vulkan-Headers/MIT.txt': b'MIT license fixture\n', + }.items(): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + bridge = {name: ('file', 0o755, elf()) for name in runtime.REQUIRED_LIBRARIES if name.startswith('lib/')} + bridge['lib/libhybris-common.so.1.0.0'] = bridge.pop('lib/libhybris-common.so.1') + bridge['lib/libhybris-common.so.1'] = ('symlink', 0o777, 'libhybris-common.so.1.0.0') + data = runtime.pack(bridge) + self.release = {'format': 1, 'hybris': self.component('bridge.tzst', data), 'archives': []} + self.release['hybris'].update({ + 'tree_sha256': runtime.tree_digest(bridge), + 'recipe_inputs': {'tools/hybris/source.env': runtime.sha256((self.root / 'tools/hybris/source.env').read_bytes())}}) + files = {'dxvk/' + arch + '/' + name: pe(arch) + for arch in ('x32', 'x64') + for name in ('d3d8.dll', 'd3d9.dll', 'd3d10core.dll', 'd3d11.dll', 'dxgi.dll')} + files['wsi/libVkLayer_window_system_integration.so'] = elf() + stream = io.BytesIO() + with ZipFile(stream, 'w') as archive: + for name, content in files.items(): + archive.writestr(name, content) + component = self.component('graphics.zip', stream.getvalue()) + component.update({'ignored': [], 'files': [ + {'name': name, 'path': name, 'size': len(content), 'sha256': runtime.sha256(content), 'mode': 0o644} + for name, content in files.items()]}) + self.release['archives'].append(component) + self.pins = self.root / 'tools/powervr/release.json' + self.pins.write_bytes(runtime.json_bytes(self.release)) + + def component(self, name, data): + component = {'url': 'https://example.invalid/' + name, 'size': len(data), + 'sha256': runtime.sha256(data), 'recipe_inputs': {}} + (self.cache / (component['sha256'] + '-' + name)).write_bytes(data) + return component + + def add_upstream_dxvk(self, files): + top = next(iter(files)).split('/')[0] + data = tar_gz([(top, tarfile.DIRTYPE, None)] + + [(name, tarfile.REGTYPE, content) for name, content in files.items()]) + component = self.component('dxvk-upstream.tar.gz', data) + component.update({'format': 'tar.gz', 'ignored': [], 'files': [ + {'name': name, 'path': name, 'size': len(content), 'sha256': runtime.sha256(content), 'mode': 0o755} + for name, content in files.items()]}) + self.release['archives'].append(component) + self.pins.write_bytes(runtime.json_bytes(self.release)) + return component + + def assembled(self): + runtime.assemble(self.root, self.cache) + return runtime.read_tar((self.root / runtime.ASSETS / 'runtime.tzst').read_bytes()) + + def test_complete_payload_preserves_symlinks_and_is_reproducible(self): + self.assembled() + first = (self.root / runtime.ASSETS / 'runtime.tzst').read_bytes() + runtime.verify(self.root) + self.assembled() + self.assertEqual(first, (self.root / runtime.ASSETS / 'runtime.tzst').read_bytes()) + self.assertEqual(runtime.read_tar(first)['lib/libhybris-common.so.1'][0], 'symlink') + + def test_upstream_tarball_adds_a_second_dxvk_payload(self): + files = upstream_dxvk('2.6.2') + self.add_upstream_dxvk(files) + entries = self.assembled() + runtime.verify(self.root) + for name, content in files.items(): + self.assertEqual(entries[name], ('file', 0o755, content)) + self.assertIn('dxvk/x64/d3d9.dll', entries) + + def test_second_dxvk_payload_with_wrong_pe_architecture_is_rejected(self): + self.add_upstream_dxvk(upstream_dxvk('2.6.2', lambda arch: 'x64' if arch == 'x32' else 'x32')) + with self.assertRaisesRegex(ValueError, 'wrong PE architecture: dxvk-2.6.2/'): + self.assembled() + + def test_tar_gz_rejects_symlinks_unknown_members_and_changed_files(self): + pin = {'name': 'dxvk-1/x64/d3d9.dll', 'path': 'dxvk-1/x64/d3d9.dll', 'size': 4, + 'sha256': runtime.sha256(b'good'), 'mode': 0o755} + component = {'files': [pin], 'ignored': []} + cases = [ + [('dxvk-1/x64/d3d9.dll', tarfile.SYMTYPE, '../../outside')], + [('dxvk-1/x64/d3d9.dll', tarfile.REGTYPE, b'good'), ('dxvk-1/extra.dll', tarfile.REGTYPE, b'x')], + [('dxvk-1/x64/d3d9.dll', tarfile.REGTYPE, b'evil')], + [], + ] + for entries in cases: + with self.subTest(entries=[name for name, _, _ in entries]), self.assertRaises(ValueError): + runtime.read_tar_gz(tar_gz(entries), component) + self.assertEqual(runtime.read_tar_gz(tar_gz([('dxvk-1/x64/d3d9.dll', tarfile.REGTYPE, b'good')]), component), + {'dxvk-1/x64/d3d9.dll': ('file', 0o755, b'good')}) + with self.assertRaisesRegex(ValueError, 'unsupported component archive format'): + runtime.read_component(b'', {'format': 'rar'}) + + def test_stale_wrapper_cannot_pass_with_an_updated_archive_checksum(self): + self.assembled() + (self.root / 'tools/hybris/run-proton.py').write_text('print("new wrapper")\n') + with self.assertRaisesRegex(ValueError, 'assembly inputs changed'): + runtime.verify(self.root) + + def test_header_licenses_are_bundled_verbatim(self): + entries = self.assembled() + for source, target in runtime.BUNDLED_LICENSES.items(): + self.assertEqual(runtime.read(entries, target), (self.root / source).read_bytes()) + + def test_missing_or_modified_header_license_is_rejected(self): + original = self.assembled() + target = 'licenses/Vulkan-Headers/MIT.txt' + for replacement in (None, ('file', 0o644, b'stale license')): + with self.subTest(replacement=replacement): + entries = dict(original) + if replacement is None: + del entries[target] + else: + entries[target] = replacement + with self.assertRaisesRegex(ValueError, 'license missing or stale'): + runtime.check_bundle(entries, self.release, self.root) + + def test_changed_tracked_header_license_rejects_old_bundle(self): + self.assembled() + source = next(iter(runtime.BUNDLED_LICENSES)) + (self.root / source).write_bytes(b'changed license text') + with self.assertRaisesRegex(ValueError, 'assembly inputs changed'): + runtime.verify(self.root) + + def test_changed_component_recipe_requires_new_release_pin(self): + self.assembled() + (self.root / 'tools/hybris/source.env').write_text('HYBRIS_REVISION=new-source\n') + with self.assertRaisesRegex(ValueError, 'recipe changed'): + runtime.verify(self.root) + + def test_old_dxvk_cannot_pass_by_updating_only_the_bundle_checksum(self): + entries = self.assembled() + entries['dxvk/x64/d3d11.dll'] = ('file', 0o644, pe('x64') + b'old DLL') + archive = runtime.pack(entries) + with self.assertRaisesRegex(ValueError, 'component mismatch'): + runtime.verify_archive(archive, runtime.sha256(archive), self.root) + + def test_wrong_pe_architecture_rejected_even_with_matching_member_pin(self): + entries = self.assembled() + name = 'dxvk/x32/d3d11.dll' + entries[name] = ('file', 0o644, pe('x64')) + pin = next(item for item in self.release['archives'][0]['files'] if item['path'] == name) + pin['sha256'] = runtime.sha256(pe('x64')) + with self.assertRaisesRegex(ValueError, 'wrong PE architecture'): + runtime.check_bundle(entries, self.release, self.root) + + def test_unpinned_bridge_file_is_rejected(self): + entries = self.assembled() + entries['lib/extra-loader.so'] = ('file', 0o755, elf()) + with self.assertRaisesRegex(ValueError, 'bridge differs'): + runtime.check_bundle(entries, self.release, self.root) + + def test_failed_download_leaves_the_previous_payload_ready(self): + self.assembled() + before = (self.root / runtime.ASSETS / 'version.txt').read_bytes() + component = self.release['archives'][0] + (self.cache / (component['sha256'] + '-graphics.zip')).write_bytes(b'bad cache') + with patch.object(runtime, 'urlopen', return_value=io.BytesIO(b'corrupt response')): + with self.assertRaisesRegex(ValueError, 'checksum mismatch'): + runtime.assemble(self.root, self.cache) + self.assertEqual(before, (self.root / runtime.ASSETS / 'version.txt').read_bytes()) + runtime.verify(self.root) + + def test_tar_rejects_traversal_duplicate_special_files_and_symlink_escape(self): + cases = [ + [('../escape', tarfile.REGTYPE, b'x')], + [('file', tarfile.REGTYPE, b'x'), ('./file', tarfile.REGTYPE, b'y')], + [('device', tarfile.CHRTYPE, b'')], + [('file', tarfile.LNKTYPE, 'other')], + [('lib/link', tarfile.SYMTYPE, '../../outside')], + [('lib/link', tarfile.SYMTYPE, '/absolute')], + [('lib/a', tarfile.SYMTYPE, 'b'), ('lib/b', tarfile.SYMTYPE, 'a')], + [('lib', tarfile.SYMTYPE, 'target'), ('target', tarfile.REGTYPE, b'x'), + ('lib/child', tarfile.REGTYPE, b'x')], + ] + for members in cases: + with self.subTest(members=members): + with self.assertRaises(ValueError): + runtime.read_tar(tar(members)) + + def test_relative_symlink_that_stays_inside_runtime_is_preserved(self): + entries = runtime.read_tar(tar([ + ('lib/a', tarfile.REGTYPE, b'library'), ('lib/child/link', tarfile.SYMTYPE, '../a')])) + self.assertEqual(runtime.read(entries, 'lib/child/link'), b'library') + + def test_zip_rejects_symlink_even_if_name_is_allowed(self): + stream = io.BytesIO() + with ZipFile(stream, 'w') as archive: + info = ZipInfo('allowed') + info.create_system = 3 + info.external_attr = 0o120777 << 16 + archive.writestr(info, '../outside') + with self.assertRaisesRegex(ValueError, 'unexpected component archive entry'): + runtime.read_zip(stream.getvalue(), {'files': [], 'ignored': ['allowed']}) + + def test_zip_rejects_unknown_and_duplicate_members(self): + for names in (['unexpected'], ['allowed', 'allowed'], ['../escape']): + with self.subTest(names=names): + stream = io.BytesIO() + with ZipFile(stream, 'w') as archive: + for name in names: + archive.writestr(name, b'file') + with self.assertRaises(ValueError): + runtime.read_zip(stream.getvalue(), {'files': [], 'ignored': ['allowed']}) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tests/test_proton_wrapper.py b/tools/tests/test_proton_wrapper.py new file mode 100644 index 000000000..175d4600a --- /dev/null +++ b/tools/tests/test_proton_wrapper.py @@ -0,0 +1,228 @@ +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + + +TOOLS = Path(__file__).resolve().parents[1] +WRAPPER_SOURCE = TOOLS / "hybris/run-proton.py" +DLL_NAMES = ("d3d8.dll", "d3d9.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll") +DXVK_X64_ANCHOR = 'g_proton.arch_pe_dir("wine/dxvk", False) + f + ".dll"' +DXVK_X32_ANCHOR = 'g_proton.arch_pe_dir("wine/dxvk", True) + f + ".dll"' +PREFIX_ANCHOR = " # check whether any prefix config has changed" + + +PROTON_SOURCE = '''import json +import os +from pathlib import Path +import sys + +from neighbor import NEIGHBOR + + +class Proton: + def arch_pe_dir(self, directory, is32): + return str(Path(__file__).parent / "stock" / ("x32" if is32 else "x64")) + "/" + + +g_proton = Proton() + + +def prefix_marker(): + if True: + prefix_info = "stock" + # check whether any prefix config has changed + return prefix_info + + +def payloads(): + result = {"x64": {}, "x32": {}} + for f in ("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi"): + x64 = g_proton.arch_pe_dir("wine/dxvk", False) + f + ".dll" + x32 = g_proton.arch_pe_dir("wine/dxvk", True) + f + ".dll" + result["x64"][f + ".dll"] = Path(x64).read_text() + result["x32"][f + ".dll"] = Path(x32).read_text() + return result + + +def main(): + record = { + "argv0": sys.argv[0], + "argv": sys.argv[1:], + "file": __file__, + "neighbor": NEIGHBOR, + "prefix": prefix_marker(), + "payloads": payloads(), + "openvr": g_proton.arch_pe_dir("wine/dxvk", False) + "openvr_api_dxvk.dll", + } + Path(os.environ["PROBE"]).write_text(json.dumps(record)) + raise SystemExit(int(os.environ.get("PROTON_EXIT", "0"))) + + +main() +''' + + +class ProtonWrapperTest(unittest.TestCase): + def fixture(self, root): + wrapper_dir = root / 'DXVK profile "quotes" $dollar `backticks`' + wrapper_dir.mkdir(parents=True) + wrapper = wrapper_dir / "run-proton.py" + shutil.copy2(WRAPPER_SOURCE, wrapper) + + dxvk = wrapper_dir / "dxvk" + for architecture in ("x32", "x64"): + payload = dxvk / architecture + payload.mkdir(parents=True) + for name in DLL_NAMES: + (payload / name).write_text(f"{architecture}:{name}") + + depot = root / "Proton depot with spaces" + depot.mkdir() + proton = depot / "selected proton.py" + proton.write_text(PROTON_SOURCE) + (depot / "neighbor.py").write_text('NEIGHBOR = "imported from selected depot"\n') + return {"wrapper": wrapper, "dxvk": dxvk, "depot": depot, "proton": proton, + "probe": root / "proton-ran.json"} + + def run_wrapper(self, fixture, *proton_args, check=False, exit_status=0, env=None): + command = [sys.executable, str(fixture["wrapper"])] + if check: + command.append("--check") + command.extend((str(fixture["proton"]), *proton_args)) + return subprocess.run( + command, + cwd=fixture["wrapper"].parent, + env={"PATH": os.defpath, "PROBE": str(fixture["probe"]), + "PROTON_EXIT": str(exit_status), **(env or {})}, + text=True, + capture_output=True, + ) + + def test_launch_uses_both_staged_payloads_and_selected_proton_context(self): + with tempfile.TemporaryDirectory() as tmp: + fixture = self.fixture(Path(tmp)) + original_source = fixture["proton"].read_bytes() + arguments = ("waitforexitandrun", "game with spaces.exe", 'quoted "value"', + "$literal-dollar", "`literal-backticks`") + result = self.run_wrapper(fixture, *arguments) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(fixture["proton"].read_bytes(), original_source) + record = json.loads(fixture["probe"].read_text()) + self.assertEqual(record["argv0"], str(fixture["proton"])) + self.assertEqual(record["argv"], list(arguments)) + self.assertEqual(record["file"], str(fixture["proton"])) + self.assertEqual(record["neighbor"], "imported from selected depot") + self.assertEqual(record["prefix"], "stock\ndroiddeck-dxvk-profile-v1") + self.assertEqual(record["openvr"], str(fixture["depot"] / "stock/x64/openvr_api_dxvk.dll")) + for architecture in ("x32", "x64"): + self.assertEqual( + record["payloads"][architecture], + {name: f"{architecture}:{name}" for name in DLL_NAMES}, + ) + + def test_named_sibling_payload_replaces_the_default(self): + with tempfile.TemporaryDirectory() as tmp: + fixture = self.fixture(Path(tmp)) + for architecture in ("x32", "x64"): + payload = fixture["wrapper"].parent / "dxvk-2.6.2" / architecture + payload.mkdir(parents=True) + for name in DLL_NAMES: + (payload / name).write_text(f"2.6.2 {architecture}:{name}") + result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe", + env={"DROIDDECK_HYBRIS_DXVK": "dxvk-2.6.2"}) + + self.assertEqual(result.returncode, 0, result.stderr) + record = json.loads(fixture["probe"].read_text()) + for architecture in ("x32", "x64"): + self.assertEqual( + record["payloads"][architecture], + {name: f"2.6.2 {architecture}:{name}" for name in DLL_NAMES}, + ) + + def test_payload_names_outside_the_wrapper_directory_are_refused(self): + for name in ("../dxvk", "dxvk/../../x", "/tmp/dxvk", "other", ".dxvk"): + with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp: + fixture = self.fixture(Path(tmp)) + result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe", + env={"DROIDDECK_HYBRIS_DXVK": name}) + + self.assertEqual(result.returncode, 1) + self.assertIn("invalid payload name", result.stderr) + self.assertFalse(fixture["probe"].exists()) + + def test_check_validates_without_executing_proton(self): + with tempfile.TemporaryDirectory() as tmp: + fixture = self.fixture(Path(tmp)) + result = self.run_wrapper(fixture, check=True) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("payload present and Proton layout supported", result.stdout) + self.assertFalse(fixture["probe"].exists()) + + def test_check_rejects_malformed_source_before_its_top_level_code_runs(self): + with tempfile.TemporaryDirectory() as tmp: + fixture = self.fixture(Path(tmp)) + source = fixture["proton"].read_text() + source = source.replace( + "import sys\n", 'import sys\n\nPath(os.environ["PROBE"]).write_text("top-level")\n', 1) + fixture["proton"].write_text(source + "\nthis is not valid =\n") + result = self.run_wrapper(fixture, check=True) + + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("DroidDeck DXVK profile:", result.stderr) + self.assertFalse(fixture["probe"].exists()) + + def test_preflight_rejects_each_missing_or_empty_payload_before_proton_runs(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for state in ("missing", "empty"): + for architecture in ("x32", "x64"): + for name in DLL_NAMES: + with self.subTest(state=state, architecture=architecture, name=name): + fixture = self.fixture(root / f"{state}-{architecture}-{name}") + payload = fixture["dxvk"] / architecture / name + if state == "missing": + payload.unlink() + else: + payload.write_bytes(b"") + result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe") + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn(f"{state} DXVK file", result.stderr) + self.assertFalse(fixture["probe"].exists()) + + def test_preflight_rejects_changed_proton_layout_or_syntax_before_proton_runs(self): + mutations = [] + for label, anchor in (("x64", DXVK_X64_ANCHOR), ("x32", DXVK_X32_ANCHOR), + ("prefix", PREFIX_ANCHOR)): + mutations.extend(((f"missing-{label}", lambda text, anchor=anchor: text.replace(anchor, "missing_anchor", 1)), + (f"duplicate-{label}", lambda text, anchor=anchor: text + "\n# " + anchor + "\n"))) + mutations.append(("syntax-error", lambda text: text + "\nthis is not valid =\n")) + + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for label, mutate in mutations: + with self.subTest(layout=label): + fixture = self.fixture(root / label) + fixture["proton"].write_text(mutate(fixture["proton"].read_text())) + result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe") + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("DroidDeck DXVK profile:", result.stderr) + self.assertFalse(fixture["probe"].exists()) + + def test_proton_exit_status_is_propagated(self): + with tempfile.TemporaryDirectory() as tmp: + fixture = self.fixture(Path(tmp)) + result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe", exit_status=23) + + self.assertEqual(result.returncode, 23, result.stderr) + self.assertTrue(fixture["probe"].exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_runtime_inputs.py b/tools/tests/test_runtime_inputs.py index ebb6cf97e..e747f3320 100644 --- a/tools/tests/test_runtime_inputs.py +++ b/tools/tests/test_runtime_inputs.py @@ -6,6 +6,7 @@ import subprocess import tarfile import tempfile import unittest +from unittest.mock import patch from unittest import mock from zipfile import ZipFile @@ -20,6 +21,10 @@ class RuntimeInputsTest(unittest.TestCase): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) + # Component archive validation has its own binary/archive fixtures. + component_check = patch.object(runtime, 'verify_powervr') + component_check.start() + self.addCleanup(component_check.stop) paths = runtime.outputs(self.root) for name in runtime.required(): path = self.root / paths.get(name, 'app/src/main/' + name) @@ -115,6 +120,20 @@ class RuntimeInputsTest(unittest.TestCase): with self.assertRaisesRegex(ValueError, 'quest1.tzst'): runtime.record(self.root) + def test_changed_powervr_wrapper_rejects_old_outputs(self): + wrapper = self.root / 'tools/hybris/run-proton.py' + wrapper.parent.mkdir(parents=True) + wrapper.write_text('current wrapper') + runtime.record(self.root) + wrapper.write_text('changed wrapper') + with self.assertRaisesRegex(ValueError, 'sources changed'): + runtime.verify(self.root) + + def test_missing_powervr_bundle_cannot_be_attested(self): + (self.root / 'app/src/main/assets/hybris/runtime.tzst').unlink() + with self.assertRaisesRegex(ValueError, 'assets/hybris/runtime.tzst'): + runtime.record(self.root) + def test_modified_prebuilt_binary_is_rejected(self): runtime.record(self.root) (self.root / 'app/src/main/jniLibs/arm64-v8a/libproot.so').write_bytes(b'old APK binary') diff --git a/tools/tests/test_steam_helper_affinity.py b/tools/tests/test_steam_helper_affinity.py new file mode 100644 index 000000000..97549ac05 --- /dev/null +++ b/tools/tests/test_steam_helper_affinity.py @@ -0,0 +1,91 @@ +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tempfile +import unittest + +WRAPPER = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-steam-taskset" + + +class SteamHelperAffinityTest(unittest.TestCase): + def launch(self, status, form="helper", configured=True): + with tempfile.TemporaryDirectory(prefix="droiddeck-helper-") as temporary: + root = Path(temporary) + helper = root / "steamwebhelper" + helper.write_text(f"#!{sys.executable}\n" + + 'import json, os, sys\n' + 'with open(os.environ["STARTS"],"a") as f: f.write(json.dumps(sys.argv[1:])+"\\n")\n' + 'sys.exit(23)\n') + helper.chmod(0o755) + real = root / "real-taskset" + real.write_text(f"#!{sys.executable}\n" + + 'import json, os, sys\n' + 'with open(os.environ["TRACE"],"a") as f: f.write(json.dumps(sys.argv[1:])+"\\n")\n' + f'sys.exit({status})\n') + real.chmod(0o755) + wrapper = root / "taskset" + wrapper.write_text(WRAPPER.read_text().replace("/usr/bin/taskset", shlex.quote(str(real)))) + helper_path = helper + if form == "alias": + helper_path = root / "helper symlink" + helper_path.symlink_to(helper) + elif form == "other": + helper_path = root / "other/steamwebhelper" + helper_path.parent.mkdir() + helper_path.write_text(helper.read_text()) + helper_path.chmod(0o755) + args = ["0x7c", str(helper_path), "arg with spaces", "--literal=$HOME"] + if form == "query": + args = ["-p", "12345"] + elif form == "invalid": + args[0] = "not-a-mask" + elif form == "empty": + args = [] + env = dict(os.environ, TRACE=str(root / "trace"), STARTS=str(root / "starts")) + env.pop("BL_STEAM_WEBHELPER", None) + if configured: + env["BL_STEAM_WEBHELPER"] = str(helper) + result = subprocess.run(["bash", str(wrapper), *args], env=env, + capture_output=True, text=True, timeout=5) + trace = [json.loads(x) for x in (root / "trace").read_text().splitlines()] + starts = ([json.loads(x) for x in (root / "starts").read_text().splitlines()] + if (root / "starts").exists() else []) + return result, trace, starts, args + + def test_denied_pinning_still_executes_helper_once(self): + result, trace, starts, args = self.launch(1) + self.assertEqual(result.returncode, 23) + self.assertEqual(starts, [args[2:]]) + self.assertEqual(len(trace), 1) + self.assertEqual(trace[0][:2], ["-p", "0x7c"]) + self.assertRegex(trace[0][2], r"^[1-9][0-9]*$") + self.assertIn("using the allowed CPUs", result.stderr) + + def test_successful_pinning_preserves_helper_exit_status(self): + result, trace, starts, args = self.launch(0) + self.assertEqual(result.returncode, 23) + self.assertEqual(starts, [args[2:]]) + self.assertEqual(trace[0][:2], ["-p", "0x7c"]) + self.assertNotIn("pinning unavailable", result.stderr) + + def test_symlink_to_actual_helper_is_recognized(self): + result, _, starts, args = self.launch(1, "alias") + self.assertEqual(result.returncode, 23) + self.assertEqual(starts, [args[2:]]) + + def test_other_taskset_forms_and_targets_pass_through(self): + for form, configured in [("other", True), ("query", True), ("invalid", True), + ("empty", True), ("helper", False)]: + with self.subTest(form=form, configured=configured): + result, trace, starts, args = self.launch(47, form, configured) + self.assertEqual(result.returncode, 47) + self.assertEqual(trace, [args]) + self.assertEqual(starts, []) + self.assertNotIn("pinning unavailable", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_steam_presentation.py b/tools/tests/test_steam_presentation.py new file mode 100644 index 000000000..fc1b6fec3 --- /dev/null +++ b/tools/tests/test_steam_presentation.py @@ -0,0 +1,75 @@ +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-session" + + +class SteamPresentationTest(unittest.TestCase): + def launch(self, enabled=None, inherited=None): + source = SESSION.read_text() + begin = source.index(" bl_steam_path=$PATH") + end = source.index("\n decky_loader=", begin) + command = source.index(' PATH="$bl_steam_path"') + command_end = source.index("\n rc=$?", command) + script = (source[begin:end] + '\n_bl_ui=(-gamepadui); _bl_cdp=(); _bl_lang=()\n' + # 0.5.0 launches through steam_client (the arm64 client, or the x86-64 one under FEX). + + 'steam_client=("$steam_root/steamrtarm64/steam")\n' + + source[command:command_end]) + env = dict(os.environ) + for key in ("BL_STEAM_GL_PRESENT", "DISABLE_GAMESCOPE_WSI", "LIBGL_KOPPER_DISABLE"): + env.pop(key, None) + if enabled is not None: + env["BL_STEAM_GL_PRESENT"] = enabled + env.update(inherited or {}) + with tempfile.TemporaryDirectory(prefix="droiddeck-steam-gl-") as tmp: + program = Path(tmp) / "steamrtarm64/steam" + program.parent.mkdir() + program.write_text(f"#!{sys.executable}\n" + + 'import json, os, sys\nprint(json.dumps({"args":sys.argv[1:],' + '"disable":os.getenv("DISABLE_GAMESCOPE_WSI"),' + '"kopper":os.getenv("LIBGL_KOPPER_DISABLE"),' + '"path":os.getenv("PATH"),' + '"helper":os.getenv("BL_STEAM_WEBHELPER")}))\n') + program.chmod(0o755) + env.update(steam_root=tmp, steam_channel="steamdeck_publicbeta") + result = subprocess.run(["bash", "-c", script, "test", "steam://open/main"], + env=env, capture_output=True, text=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stderr) + launch = json.loads(result.stdout.splitlines()[-1]) + self.assertEqual(enabled == "1", launch["path"].startswith("/usr/local/libexec/droiddeck-steam:")) + self.assertEqual(launch["helper"], str(Path(tmp) / "steamrtarm64/steamwebhelper")) + return launch + + def test_default_keeps_vulkan(self): + result = self.launch() + self.assertIn("-cef-use-angle=vulkan", result["args"]) + self.assertNotIn("-cef-disable-gpu-compositing", result["args"]) + self.assertIsNone(result["disable"]) + self.assertIsNone(result["kopper"]) + + def test_system_powervr_keeps_gpu_gl_with_software_web_compositing(self): + result = self.launch("1") + self.assertIn("-cef-use-angle=gl", result["args"]) + self.assertIn("-cef-use-gl=angle", result["args"]) + self.assertIn("-cef-force-gpu", result["args"]) + self.assertIn("-cef-disable-gpu-compositing", result["args"]) + self.assertNotIn("-cef-disable-gpu", result["args"]) + self.assertEqual(result["disable"], "1") + self.assertEqual(result["kopper"], "true") + self.assertEqual(result["args"][-1], "steam://open/main") + + def test_disabled_option_preserves_existing_environment(self): + result = self.launch("0", {"DISABLE_GAMESCOPE_WSI": "0", "LIBGL_KOPPER_DISABLE": "false"}) + self.assertIn("-cef-use-angle=vulkan", result["args"]) + self.assertNotIn("-cef-disable-gpu-compositing", result["args"]) + self.assertEqual(result["disable"], "0") + self.assertEqual(result["kopper"], "false") + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_vk_dmabuf_quarantine.py b/tools/tests/test_vk_dmabuf_quarantine.py new file mode 100644 index 000000000..aeafada3e --- /dev/null +++ b/tools/tests/test_vk_dmabuf_quarantine.py @@ -0,0 +1,120 @@ +"""Exercise production DMA-BUF release/ownership paths without a GPU. + +Wayland resources and OS timer/Android AHB endpoints are fake; list macros and +the compositor/AHB lifecycle functions come directly from the repository. +""" +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +from test_vk_pending_completion import function + +ROOT = Path(__file__).resolve().parents[2] +SRC = ROOT / 'app/src/main/cpp/waylandcomp/src' + + +class VkDmabufQuarantineTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cc = shutil.which(os.environ.get('CC', 'cc')) + if not cc: + raise AssertionError('C compiler required') + # Optional old-head input allows a genuine negative control using the + # identical harness, rather than a model of the previous behaviour. + source_dir = Path(os.environ.get('DROIDDECK_DMABUF_TEST_SOURCE', SRC)) + comp = (source_dir / 'compositor.c').read_text() + ahb = (source_dir / 'ahb_swapchain.c').read_text() + vk = (source_dir / 'vk_present.c').read_text() + names = ['get_dmabuf', 'pending_release_free', + 'pending_releases_forget_surface', 'on_pending_release_buffer_destroyed', + 'arm_release_timer', 'on_release_timer', 'release_buffer', + 'dmabuf_buffer_unref', 'drop_dmabuf', 'on_dmabuf_destroyed', + 'droiddeck_dmabuf_ahb_slot', 'droiddeck_dmabuf_unref', + 'droiddeck_release_buffer'] + helpers = [] + if re.search(r'\bstatic int buffer_release_blocked\(', comp): + helpers = [function(comp, 'buffer_release_blocked')] + if re.search(r'\bstatic void send_buffer_release_at\(', comp): + # send_buffer_release is a macro over send_buffer_release_at (with buffer tracing). + helpers += ['static void buf_trace_release(struct wl_resource *b, int line) { (void)b; (void)line; }', + '#define send_buffer_release(b) send_buffer_release_at((b), __LINE__)', + function(comp, 'send_buffer_release_at')] + else: + helpers += [function(comp, 'send_buffer_release')] + else: + helpers = ['static int __attribute__((unused)) buffer_release_blocked(struct wl_resource *b) { (void)b; return 0; }'] + if re.search(r'\bstatic int buffer_shown_elsewhere\(', comp): + # One surface per buffer in these scenarios: never shown elsewhere. + helpers += ['static int buffer_shown_elsewhere(struct wl_resource *b, const struct surface *e) ' + '{ (void)b; (void)e; return 0; }'] + if re.search(r'\bvolatile int g_release_unpaced;', comp): + # Releases stay paced in these scenarios (fps limit -1 sends them at once). + helpers.insert(0, 'static int g_release_unpaced;') + query = function(vk, 'vkp_completion_unknown') if 'int vkp_completion_unknown(' in vk else \ + 'int vkp_completion_unknown(void) { return g_dev_state == -3; }' + comp_functions = [function(comp, names[0])] + helpers + [function(comp, n) for n in names[1:]] + ahb_functions = [function(ahb, n) for n in ('buf_free', 'maybe_free', + 'on_buffer_resource_destroyed', 'send_deferred_release', + 'on_fence_readable', 'ahb_swapchain_defer_release', + 'ahb_swapchain_surface_gone')] + structs = re.search(r'struct pending_release \{.*?\n\};', comp, re.S).group() + '\n' + \ + re.search(r'struct ahb_buf \{.*?\n\};', ahb, re.S).group() + harness = Path(__file__).with_name('vk_dmabuf_quarantine_test.c').read_text() + harness = harness.replace('/* PRODUCTION_STRUCTS */', structs) + harness = harness.replace('/* PRODUCTION_FUNCTIONS */', '\n'.join([query] + comp_functions + ahb_functions)) + cls.tmp = tempfile.TemporaryDirectory(prefix='droiddeck-dmabuf-') + cls.addClassCleanup(cls.tmp.cleanup) + probe = Path(cls.tmp.name) / 'probe.c' + probe.write_text(harness) + cls.binary = probe.with_suffix('') + built = subprocess.run([cc, '-std=c11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-parameter', '-UNDEBUG', + '-I', str(SRC.parent / 'prebuilt/include'), + str(probe), '-o', str(cls.binary)], capture_output=True, text=True) + if built.returncode: + raise AssertionError(built.stdout + built.stderr) + + def run_scenario(self, name): + result = subprocess.run([str(self.binary), name], capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_replacement_retains_reference_and_release(self): + self.run_scenario('replacement') + + def test_surface_destroy_retains_reference_and_release(self): + self.run_scenario('surface-gone') + + def test_queued_release_is_held_and_timer_disarmed(self): + self.run_scenario('queued') + + def test_mixed_queue_releases_only_copied_shm(self): + self.run_scenario('mixed') + + def test_client_destroy_retains_backing_fd(self): + self.run_scenario('client-gone') + + def test_ahb_callback_retains_ownership_and_release(self): + self.run_scenario('ahb') + + def test_active_client_destroy_retains_reference(self): + self.run_scenario('active-client-gone') + + def test_healthy_ahb_callback_and_cleanup(self): + self.run_scenario('ahb-healthy') + + def test_device_lost_ahb_callback_and_cleanup(self): + self.run_scenario('ahb-device-lost') + + def test_healthy_release_and_reference_cleanup(self): + self.run_scenario('healthy') + + def test_genuine_device_loss_keeps_existing_cleanup(self): + self.run_scenario('device-lost') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tests/test_vk_external_image_sync.py b/tools/tests/test_vk_external_image_sync.py new file mode 100644 index 000000000..a82dac25d --- /dev/null +++ b/tools/tests/test_vk_external_image_sync.py @@ -0,0 +1,63 @@ +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +def vulkan_include_args(): + candidates = [] + for name in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT"): + if os.environ.get(name): + candidates.append(Path(os.environ[name])) + + android_home = os.environ.get("ANDROID_HOME") + if android_home: + ndk_root = Path(android_home) / "ndk" + version = os.environ.get("NDK_VERSION") + if version: + candidates.append(ndk_root / version) + if ndk_root.is_dir(): + candidates.extend(sorted(ndk_root.iterdir(), reverse=True)) + + for ndk in candidates: + include = ndk / "toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include" + if (include / "vulkan/vulkan.h").is_file(): + return ["-idirafter", str(include)] + return [] + + +class VkExternalImageSyncTest(unittest.TestCase): + def test_client_barrier_pair(self): + compiler = shutil.which(os.environ.get("CC", "cc")) + if not compiler: + self.fail("A C compiler is required for the Vulkan image-sync regression test") + + tests = Path(__file__).resolve().parent + source = tests / "vk_external_image_sync_test.c" + includes = tests.parents[1] / "app/src/main/cpp/waylandcomp/src" + with tempfile.TemporaryDirectory(prefix="droiddeck-vk-sync-") as tmp: + program = Path(tmp) / "vk-external-image-sync" + command = [ + compiler, + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + "-UNDEBUG", + "-I", + str(includes), + *vulkan_include_args(), + str(source), + "-o", + str(program), + ] + built = subprocess.run(command, capture_output=True, text=True) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + ran = subprocess.run([str(program)], capture_output=True, text=True, timeout=30) + self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_vk_pending_completion.py b/tools/tests/test_vk_pending_completion.py new file mode 100644 index 000000000..d0a4ad594 --- /dev/null +++ b/tools/tests/test_vk_pending_completion.py @@ -0,0 +1,187 @@ +"""Fault-inject the production fence wait and imported-image destructor. + +No GPU is wedged: a fake dispatch table returns the same result a timed-out +submit would return. The compiled functions come from vk_present.c itself. +""" +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +SOURCE = Path(__file__).resolve().parents[2] / 'app/src/main/cpp/waylandcomp/src/vk_present.c' + + +def function(text, name): + match = re.search(r'^(?:static )?[^\n;]+\b' + name + r'\([^;]*?\)\s*\{', text, re.M) + if not match: + raise AssertionError('Production function not found: ' + name) + start = match.start() + pos = text.index('{', match.start()) + depth = 1 + end = pos + 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + return text[start:end] + + +HARNESS = r''' +#include +#include +#include +#include +typedef int VkResult; +typedef uintptr_t VkFence; +#define VK_TRUE 1 +#define VK_SUCCESS 0 +#define VK_TIMEOUT 2 +#define VK_ERROR_DEVICE_LOST -4 +#define VK_ERROR_OUT_OF_HOST_MEMORY -1 +#define VK_ERROR_UNKNOWN -13 +static int g_dev_state, g_base_black; +static uintptr_t g_dev = 1; +static struct { int64_t wait_ns, wait_max_ns; unsigned waits; } g_perf; +static VkResult answer; +static unsigned destroyed, unmapped, freed_memory, forgotten, errors; +static int64_t perf_now(void) { return 10; } +static void perf_add(int64_t *s, int64_t *m, unsigned *c, int64_t d) { *s+=d; *m=d; ++*c; } +static void __attribute__((unused)) droiddeck_log(const char *tag, const char *fmt, ...) { (void)tag; (void)fmt; ++errors; } +#define LOGE(...) droiddeck_log("error", __VA_ARGS__) +static VkResult wait_fn(uintptr_t dev, unsigned n, const VkFence *f, int all, uint64_t timeout) { + (void)dev; (void)n; (void)f; (void)all; (void)timeout; return answer; +} +static void unmap_fn(uintptr_t dev, uintptr_t mem) { (void)dev; (void)mem; ++unmapped; } +static void destroy_fn(uintptr_t dev, uintptr_t img, void *allocator) { (void)dev; (void)img; (void)allocator; ++destroyed; } +static void free_fn(uintptr_t dev, uintptr_t mem, void *allocator) { (void)dev; (void)mem; (void)allocator; ++freed_memory; } +static struct { + VkResult (*WaitForFences)(uintptr_t, unsigned, const VkFence *, int, uint64_t); + void (*UnmapMemory)(uintptr_t, uintptr_t); + void (*DestroyImage)(uintptr_t, uintptr_t, void *); + void (*FreeMemory)(uintptr_t, uintptr_t, void *); +} g_vk = {wait_fn, unmap_fn, destroy_fn, free_fn}; +typedef struct AHardwareBuffer AHardwareBuffer; +static int released_buffers; +static void AHardwareBuffer_release(AHardwareBuffer *b) { (void)b; ++released_buffers; } +struct vkp_image { uintptr_t image, mem; void *map; AHardwareBuffer *android_buffer; }; +static void blendp_forget_image(uintptr_t image) { (void)image; ++forgotten; } +/* PRODUCTION_FUNCTIONS */ +static struct vkp_image *image(void) { + struct vkp_image *p = calloc(1, sizeof(*p)); assert(p); + p->image=1; p->mem=2; p->map=p; return p; +} +int main(void) { + g_dev_state=1; answer=VK_SUCCESS; + assert(timed_wait(1, 1000)==VK_SUCCESS); + assert(g_dev_state==1); + vkp_image_destroy(image()); + assert(destroyed==1 && unmapped==1 && freed_memory==1 && forgotten==1); + + g_dev_state=1; g_base_black=1; answer=VK_TIMEOUT; + assert(timed_wait(1, 1000)==VK_TIMEOUT); + assert(g_dev_state!=1 && g_dev_state!=-2); /* pending work is not DEVICE_LOST */ + struct vkp_image *pending=image(); + vkp_image_destroy(pending); + assert(destroyed==1 && unmapped==1 && freed_memory==1 && forgotten==1); + unsigned first_errors=errors; + assert(first_errors>0); + assert(timed_wait(1, 1000)==VK_ERROR_UNKNOWN); + assert(errors==first_errors); /* one terminal diagnostic, no repeated recovery */ + free(pending); /* fake GPU exists only in this test */ + + g_dev_state=1; answer=VK_ERROR_OUT_OF_HOST_MEMORY; + assert(timed_wait(1, 1000)==VK_ERROR_OUT_OF_HOST_MEMORY); + assert(g_dev_state!=1 && g_dev_state!=-2); + pending=image(); vkp_image_destroy(pending); + assert(destroyed==1 && unmapped==1 && freed_memory==1 && forgotten==1); + free(pending); + + g_dev_state=1; answer=VK_ERROR_DEVICE_LOST; + assert(timed_wait(1, 1000)==VK_ERROR_DEVICE_LOST); + assert(g_dev_state==1); /* existing caller performs genuine device-loss cleanup */ + return 0; +} +''' + +SWAPCHAIN_HARNESS = r''' +#include +#include +#include +typedef int VkResult; +#define VK_SUCCESS 0 +#define VK_ERROR_DEVICE_LOST -4 +#define VK_ERROR_OUT_OF_HOST_MEMORY -1 +#define VK_NULL_HANDLE 0 +static int g_dev_state, g_base_black, g_swap_md_identity; +static uintptr_t g_dev=1, g_inst=1, g_swapchain, g_surface, *g_images; +static uint32_t g_nimg; +static unsigned resets, forgotten, destroyed, errors, lost; +static VkResult answer; +#define LOGE(...) (++errors) +static VkResult idle_fn(uintptr_t dev) { (void)dev; return answer; } +static void destroy_fn(uintptr_t dev, uintptr_t obj, void *alloc) { + (void)dev; (void)obj; (void)alloc; ++destroyed; +} +static struct { + VkResult (*DeviceWaitIdle)(uintptr_t); + void (*DestroySwapchainKHR)(uintptr_t, uintptr_t, void *); + void (*DestroySurfaceKHR)(uintptr_t, uintptr_t, void *); +} g_vk={idle_fn, destroy_fn, destroy_fn}; +static void reset_sync(void) { ++resets; } +static void blendp_forget_image(uintptr_t img) { (void)img; ++forgotten; } +static void device_lost(const char *where) { (void)where; ++lost; g_dev_state=-2; } +/* PRODUCTION_FUNCTIONS */ +static void setup(void) { + g_dev_state=1; g_base_black=1; g_swap_md_identity=1; + g_swapchain=2; g_surface=3; g_nimg=1; + g_images=calloc(1,sizeof(*g_images)); assert(g_images); *g_images=4; +} +int main(void) { + setup(); answer=VK_SUCCESS; destroy_swapchain(); + assert(resets==1 && destroyed==2 && forgotten==1); + assert(!g_swapchain && !g_surface && !g_images && !g_nimg && !g_swap_md_identity); + setup(); answer=VK_ERROR_OUT_OF_HOST_MEMORY; destroy_swapchain(); + assert(g_dev_state==-3 && errors==1); + assert(resets==1 && destroyed==2 && forgotten==1); + assert(g_swapchain && g_surface && g_images && g_nimg); + destroy_swapchain(); + assert(errors==1 && resets==1 && destroyed==2); + free(g_images); /* fake GPU resources only */ + setup(); answer=VK_ERROR_DEVICE_LOST; destroy_swapchain(); + assert(lost==1 && g_dev_state==-2); + assert(resets==1 && destroyed==2 && forgotten==1); + free(g_images); + return 0; +} +''' + + +class VkPendingCompletionTest(unittest.TestCase): + def compile_and_run(self, harness, names): + cc = shutil.which(os.environ.get('CC', 'cc')) + self.assertIsNotNone(cc, 'A C compiler is required') + text = SOURCE.read_text() + production = '\n'.join(function(text, n) for n in names) + with tempfile.TemporaryDirectory(prefix='droiddeck-pending-') as tmp: + source=Path(tmp)/'probe.c'; binary=Path(tmp)/'probe' + source.write_text(harness.replace('/* PRODUCTION_FUNCTIONS */', production)) + built=subprocess.run([cc,'-std=c11','-Wall','-Wextra','-Werror','-UNDEBUG',str(source),'-o',str(binary)],capture_output=True,text=True) + self.assertEqual(built.returncode,0,built.stdout+built.stderr) + ran=subprocess.run([str(binary)],capture_output=True,text=True,timeout=10) + self.assertEqual(ran.returncode,0,ran.stdout+ran.stderr) + + def test_unknown_completion_retains_gpu_resources(self): + self.compile_and_run(HARNESS, ['quarantine_pending_work', 'timed_wait', 'vkp_image_destroy']) + + def test_failed_device_idle_does_not_destroy_swapchain(self): + # LOGE's arguments must be evaluated as real production logging does. + harness = SWAPCHAIN_HARNESS.replace('#define LOGE(...) (++errors)', + 'static void log_error(const char *fmt, int result, const char *where) { ' + '(void)fmt; (void)result; (void)where; ++errors; }\n#define LOGE(...) log_error(__VA_ARGS__)') + self.compile_and_run(harness, ['quarantine_pending_work', 'destroy_swapchain']) + + +if __name__=='__main__': + unittest.main() diff --git a/tools/tests/test_vk_terminal_callers.py b/tools/tests/test_vk_terminal_callers.py new file mode 100644 index 000000000..f0c7294ca --- /dev/null +++ b/tools/tests/test_vk_terminal_callers.py @@ -0,0 +1,140 @@ +"""Fault-inject complete production render/blit/upload callers, without a GPU. + +Uses Vulkan-Headers from the Android NDK (as installed by APK CI), or the host +Vulkan development package. Only Android platform declarations and the external +effect/frame-generation modules are mocked; vk_present.c is compiled unmodified. +""" +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from test_vk_pending_completion import function + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / 'app/src/main/cpp/waylandcomp/src/vk_present.c' + + +def vulkan_include(): + candidates = [Path(p) for p in [os.environ.get('DROIDDECK_VULKAN_INCLUDE', ''), + '/usr/include', '/usr/local/include'] if p] + for name in ('ANDROID_HOME', 'ANDROID_SDK_ROOT', 'ANDROID_NDK_HOME', 'NDK'): + if os.environ.get(name): + root = Path(os.environ[name]) + candidates += sorted(root.glob('ndk/*/toolchains/llvm/prebuilt/*/sysroot/usr/include'), reverse=True) + candidates += list(root.glob('toolchains/llvm/prebuilt/*/sysroot/usr/include')) + return next((p for p in candidates if (p / 'vulkan/vulkan.h').is_file()), None) + + +class VkTerminalCallersTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + headers = vulkan_include() + if headers is None: + raise unittest.SkipTest('NDK or host Vulkan-Headers required for full production-C caller tests') + cc = shutil.which(os.environ.get('CC', 'cc')) + if not cc: + raise AssertionError('C compiler required') + cls.tmp = tempfile.TemporaryDirectory(prefix='droiddeck-callers-') + cls.addClassCleanup(cls.tmp.cleanup) + tmp = Path(cls.tmp.name) + (tmp / 'android').mkdir() + (tmp / 'android/native_window.h').write_text(''' +#pragma once +#include +typedef struct ANativeWindow ANativeWindow; +int32_t ANativeWindow_getWidth(ANativeWindow *); +int32_t ANativeWindow_getHeight(ANativeWindow *); +void ANativeWindow_release(ANativeWindow *); +''') + (tmp / 'android/hardware_buffer.h').write_text(''' +#pragma once +#include +typedef struct AHardwareBuffer AHardwareBuffer; +typedef struct AHardwareBuffer_Desc { + uint32_t width, height, layers, format; + uint64_t usage; + uint32_t stride, rfu0; + uint64_t rfu1; +} AHardwareBuffer_Desc; +#define AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE (1ULL << 8) +#define AHARDWAREBUFFER_USAGE_PROTECTED_CONTENT (1ULL << 14) +#define AHARDWAREBUFFER_USAGE_GPU_MIPMAP_COMPLETE (1ULL << 26) +static inline void AHardwareBuffer_describe(const AHardwareBuffer *b, AHardwareBuffer_Desc *d) { (void)b; *d = (AHardwareBuffer_Desc){0}; } +static inline void AHardwareBuffer_acquire(AHardwareBuffer *b) { (void)b; } +static inline void AHardwareBuffer_release(AHardwareBuffer *b) { (void)b; } +''') + (tmp / 'sys').mkdir() + (tmp / 'sys/system_properties.h').write_text(''' +#pragma once +#define PROP_VALUE_MAX 92 +static inline int __system_property_get(const char *name, char *value) { (void)name; value[0] = 0; return 0; } +''') + (tmp / 'android/log.h').write_text(''' +#pragma once +#define ANDROID_LOG_INFO 4 +int __android_log_print(int, const char *, const char *, ...); +''') + (tmp / 'wayland-server.h').write_text('#pragma once\nstruct wl_display; struct wl_resource; struct wl_client;\n') + compositor = (SOURCE.parent / 'compositor.c').read_text() + # send_buffer_release is a macro over send_buffer_release_at (with buffer tracing). + callers = ('static void buf_trace_release(struct wl_resource *b, int line) { (void)b; (void)line; }\n' + + '#define send_buffer_release(b) send_buffer_release_at((b), __LINE__)\n' + + '\n'.join(function(compositor, n) for n in ('buffer_release_blocked', 'send_buffer_release_at', 'take_shm', 'take_pixel'))) + harness = (Path(__file__).with_name('vk_terminal_callers_test.c')).read_text() + probe = tmp / 'probe.c' + probe.write_text(harness.replace('/* COMPOSITOR_CALLERS */', callers)) + cls.binary = tmp / 'probe' + built = subprocess.run([cc, '-std=c11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-parameter', '-Wno-missing-field-initializers', '-UNDEBUG', + '-I', str(tmp), '-idirafter', str(headers), '-I', str(SOURCE.parent), + str(probe), '-pthread', '-o', str(cls.binary)], + capture_output=True, text=True) + if built.returncode: + raise AssertionError(built.stdout + built.stderr) + + def run_scenario(self, name): + ran = subprocess.run([str(self.binary), name], capture_output=True, text=True, timeout=10) + self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + + def test_first_acquire_idle_failure(self): + self.run_scenario('first-out-of-date') + + def test_first_surface_lost_idle_failure(self): + self.run_scenario('first-surface-lost') + + def test_later_framegen_acquire_idle_failure(self): + self.run_scenario('later-out-of-date') + + def test_later_framegen_surface_lost_idle_failure(self): + self.run_scenario('later-surface-lost') + + def test_finite_blit_timeout_same_size_shm_upload(self): + self.run_scenario('shm-timeout') + + def test_finite_blit_timeout_same_size_pixel_upload(self): + self.run_scenario('pixel-timeout') + + def test_normal_present(self): + self.run_scenario('normal') + + def test_normal_framegen_present(self): + self.run_scenario('framegen') + + def test_healthy_later_acquire_timeout_drains_work(self): + self.run_scenario('later-timeout') + + def test_real_device_lost_on_acquire(self): + self.run_scenario('acquire-device-lost') + + def test_real_device_lost_on_present(self): + self.run_scenario('present-device-lost') + + def test_pass_copy_and_abort_terminal_entry(self): + self.run_scenario('pass-terminal') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tests/test_xclipse_layer.py b/tools/tests/test_xclipse_layer.py new file mode 100644 index 000000000..36042c9c7 --- /dev/null +++ b/tools/tests/test_xclipse_layer.py @@ -0,0 +1,47 @@ +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +TOOLS = Path(__file__).resolve().parents[1] +LAYER = TOOLS / "linuxfs/xclipse-layer" + + +class XclipseLayerTest(unittest.TestCase): + def test_layer_against_a_mock_loader_and_driver(self): + compiler = shutil.which(os.environ.get("CC", "cc")) + if not compiler: + self.fail("A C compiler is required for the Xclipse layer regression test") + # The build compiles the layer against the distribution's headers (libvulkan-dev); + # VULKAN_INCLUDE points elsewhere, e.g. a Vulkan-Headers checkout on macOS. + include = Path(os.environ.get("VULKAN_INCLUDE", "/usr/include")) + if not (include / "vulkan/vk_layer.h").is_file(): + self.fail("Vulkan headers are required: install libvulkan-dev or set VULKAN_INCLUDE") + with tempfile.TemporaryDirectory(prefix="droiddeck-xclipse-") as tmp: + program = Path(tmp) / "xclipse-layer" + built = subprocess.run([compiler, "-std=gnu11", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", + "-UNDEBUG", "-pthread", "-I", str(LAYER), "-I", str(include), + str(Path(__file__).with_name("xclipse_layer_test.c")), "-ldl", "-o", str(program)], + capture_output=True, text=True) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + ran = subprocess.run([str(program)], capture_output=True, text=True, timeout=30) + self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + self.assertIn("xclipse layer ok", ran.stdout) + + def test_device_chain_table_matches_its_generator(self): + registry = os.environ.get("VULKAN_REGISTRY") + if not registry: + self.skipTest("set VULKAN_REGISTRY to a Vulkan-Headers 1.3.275 checkout to recheck device_chain.inc") + with tempfile.TemporaryDirectory(prefix="droiddeck-chain-") as tmp: + output = Path(tmp) / "device_chain.inc" + subprocess.run(["python3", str(LAYER / "gen_device_chain.py"), str(Path(registry) / "registry/vk.xml"), + str(Path(registry) / "include/vulkan/vulkan_core.h"), str(output)], + check=True, capture_output=True) + self.assertEqual(output.read_text(), (LAYER / "device_chain.inc").read_text()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/vk_dmabuf_quarantine_test.c b/tools/tests/vk_dmabuf_quarantine_test.c new file mode 100644 index 000000000..cbcf724b1 --- /dev/null +++ b/tools/tests/vk_dmabuf_quarantine_test.c @@ -0,0 +1,199 @@ +/* Production lifecycle functions are inserted verbatim by Python. No GPU, + * Android app, external client or real timer is involved in this probe. */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef __linux__ +struct itimerspec { struct timespec it_interval, it_value; }; +#endif + +struct wl_listener { struct wl_list link; void (*notify)(struct wl_listener *, void *); }; +struct wl_resource { int dma; void *user_data; struct wl_list listeners; unsigned releases; }; +struct wl_client; +struct wl_event_source { int unused; }; +typedef struct AHardwareBuffer { int unused; } AHardwareBuffer; +struct vkp_image { int unused; }; +struct dmabuf_buffer { + int refs, n_planes, fd[4]; + struct vkp_image *img; + void *ahb_state; +}; +struct surface { + struct wl_resource *dmabuf; + struct dmabuf_buffer *dmabuf_buf; + struct wl_listener dmabuf_destroy; + int releases_pending; + int64_t next_release_ns; +}; +/* PRODUCTION_STRUCTS */ +static int g_dev_state, g_fps_limit, g_release_timer_fd; +static int64_t g_refresh_ns; +static struct wl_list g_pending_releases, g_bufs; +static void *g_display; +static unsigned image_destroys, ahb_releases, source_removes; +static struct itimerspec last_timer; +static const int wl_buffer_interface = 1, dbuf_buffer_impl = 2; +#define TFD_TIMER_ABSTIME 1 + +/* List operations use the real Wayland layouts/macros. OS/IPC endpoints alone + * are fakes, so no event is sent and no process or hardware is touched. */ +void wl_list_init(struct wl_list *l) { l->prev = l; l->next = l; } +void wl_list_insert(struct wl_list *l, struct wl_list *e) { + e->next = l->next; e->prev = l; l->next->prev = e; l->next = e; +} +void wl_list_remove(struct wl_list *e) { e->next->prev = e->prev; e->prev->next = e->next; } +static int wl_resource_instance_of(struct wl_resource *r, const void *i, const void *impl) { return r->dma; } +static void *wl_resource_get_user_data(struct wl_resource *r) { return r->user_data; } +static void wl_resource_add_destroy_listener(struct wl_resource *r, struct wl_listener *l) { + wl_list_insert(r->listeners.prev, &l->link); +} +static void wl_buffer_send_release(struct wl_resource *r) { ++r->releases; } +static void wl_display_flush_clients(void *d) {} +static int timerfd_settime(int fd, int flags, const struct itimerspec *v, void *old) { last_timer = *v; return 0; } +static void perf_note_release(int64_t since) {} +static int64_t now_ns(void) { return 1000000000LL; } +static void vkp_image_destroy(struct vkp_image *img) { ++image_destroys; } +static void AHardwareBuffer_release(AHardwareBuffer *ahb) { ++ahb_releases; } +static int wl_event_source_remove(struct wl_event_source *src) { ++source_removes; return 0; } +static struct dmabuf_buffer *get_dmabuf(struct wl_resource *r); +int ahb_swapchain_defer_release(struct dmabuf_buffer *, struct wl_resource *, struct surface *, int); +/* PRODUCTION_FUNCTIONS */ + +static void init_resource(struct wl_resource *r, struct dmabuf_buffer *b) { + *r = (struct wl_resource){.dma = b != NULL, .user_data = b}; + wl_list_init(&r->listeners); +} +static struct dmabuf_buffer *init_buffer(struct surface *s, struct wl_resource *r) { + struct dmabuf_buffer *b = calloc(1, sizeof(*b)); assert(b); + b->refs = 2; b->n_planes = 1; + int fds[2]; assert(pipe(fds) == 0); b->fd[0] = fds[0]; close(fds[1]); + init_resource(r, b); + assert(get_dmabuf(r) == b); + *s = (struct surface){.dmabuf = r, .dmabuf_buf = b}; + s->dmabuf_destroy.notify = on_dmabuf_destroyed; + wl_resource_add_destroy_listener(r, &s->dmabuf_destroy); + return b; +} +static void destroy_resource(struct wl_resource *r) { + while (r->listeners.next != &r->listeners) { + struct wl_listener *l = wl_container_of(r->listeners.next, l, link); + l->notify(l, r); + } + if (r->dma) dmabuf_buffer_unref(r->user_data); + r->user_data = NULL; +} +static void fire_timer(void) { + int fd[2]; uint64_t expirations = 1; + assert(pipe(fd) == 0 && write(fd[1], &expirations, sizeof(expirations)) == sizeof(expirations)); + on_release_timer(fd[0], 0, NULL); close(fd[0]); close(fd[1]); +} +static void assert_retained(struct dmabuf_buffer *b, int refs) { + assert(b->refs == refs && fcntl(b->fd[0], F_GETFD) >= 0 && !image_destroys); +} +static void test_drop(int paced) { + struct surface s; struct wl_resource r; + struct dmabuf_buffer *b = init_buffer(&s, &r); + g_dev_state = -3; drop_dmabuf(&s, paced); + assert(!s.dmabuf && !s.dmabuf_buf && !r.releases && !s.releases_pending); + assert(r.listeners.next == &r.listeners); /* no listener into a dead surface */ + assert_retained(b, 2); + destroy_resource(&r); assert_retained(b, 2); + close(b->fd[0]); free(b); /* fake GPU only */ +} +static void test_queue(int mixed, int client_gone) { + struct surface s; struct wl_resource r, shm; + struct dmabuf_buffer *b = init_buffer(&s, &r); + g_release_timer_fd = 123; g_refresh_ns = 100000; + drop_dmabuf(&s, 1); assert(s.releases_pending == 1 && !r.releases); + assert(last_timer.it_value.tv_sec || last_timer.it_value.tv_nsec); + if (mixed) { init_resource(&shm, NULL); release_buffer(&s, &shm, now_ns()); } + pending_releases_forget_surface(&s); + g_dev_state = -3; + fire_timer(); + assert(!r.releases && g_pending_releases.next != &g_pending_releases); + if (mixed) assert(shm.releases == 1); + assert(!last_timer.it_value.tv_sec && !last_timer.it_value.tv_nsec); + assert_retained(b, 1); + if (client_gone) { + destroy_resource(&r); + assert(g_pending_releases.next == &g_pending_releases); + assert_retained(b, 1); + } + close(b->fd[0]); free(b); /* fake GPU only */ +} +static void test_active_client_destroy(void) { + struct surface s; struct wl_resource r; + struct dmabuf_buffer *b = init_buffer(&s, &r); + g_dev_state = -3; + destroy_resource(&r); + assert(!s.dmabuf && s.dmabuf_buf == b && !r.releases); + drop_dmabuf(&s, 0); assert(!s.dmabuf_buf); + assert_retained(b, 2); + close(b->fd[0]); free(b); /* fake GPU only */ +} +static void test_ahb(int terminal, int lost) { + struct surface s; struct wl_resource r; + struct dmabuf_buffer *b = init_buffer(&s, &r); + struct ahb_buf *ab = calloc(1, sizeof(*ab)); assert(ab); + AHardwareBuffer ahb; + *ab = (struct ahb_buf){.b = b, .ahb = &ahb, .resource = &r, .on_layer = 1, .fence_fd = -1}; + b->ahb_state = ab; ++b->refs; + ab->resource_destroy.notify = on_buffer_resource_destroyed; + wl_resource_add_destroy_listener(&r, &ab->resource_destroy); + wl_list_insert(&g_bufs, &ab->link); + drop_dmabuf(&s, 1); assert(ab->release_pending && ab->surface == &s); + int backing_fd = b->fd[0]; + g_dev_state = terminal ? -3 : lost ? -2 : 1; + ahb_swapchain_surface_gone(&s); assert(!ab->surface); + /* Actual event-loop callback after the display's fence becomes readable. */ + struct wl_event_source source; int fds[2]; assert(pipe(fds) == 0); + ab->on_layer = 0; ab->fence_src = &source; ab->fence_fd = fds[0]; + on_fence_readable(fds[0], 0, ab); close(fds[1]); + assert(source_removes == 1 && ab->fence_fd == -1 && !ab->fence_src); + assert(r.releases == (terminal ? 0u : 1u)); + assert(ab->release_pending == terminal && !ahb_releases); + destroy_resource(&r); + if (terminal) { + assert(!ab->resource && b->ahb_state == ab && !ahb_releases); + assert_retained(b, 2); + close(b->fd[0]); free(ab); free(b); /* fake GPU only */ + } else { + assert(ahb_releases == 1 && image_destroys == 1); + errno = 0; assert(fcntl(backing_fd, F_GETFD) == -1 && errno == EBADF); + } +} +static void test_cleanup(int lost) { + struct surface s; struct wl_resource r; + struct dmabuf_buffer *b = init_buffer(&s, &r); + int fd = b->fd[0]; g_dev_state = lost ? -2 : 1; + assert(!vkp_completion_unknown()); + drop_dmabuf(&s, 0); + assert(r.releases == 1 && b->refs == 1 && !image_destroys); + destroy_resource(&r); assert(image_destroys == 1); + errno = 0; assert(fcntl(fd, F_GETFD) == -1 && errno == EBADF); +} +int main(int argc, char **argv) { + assert(argc == 2); + wl_list_init(&g_pending_releases); wl_list_init(&g_bufs); + g_dev_state = 1; g_release_timer_fd = -1; + if (!strcmp(argv[1], "replacement")) test_drop(1); + else if (!strcmp(argv[1], "surface-gone")) test_drop(0); + else if (!strcmp(argv[1], "queued")) test_queue(0, 0); + else if (!strcmp(argv[1], "mixed")) test_queue(1, 0); + else if (!strcmp(argv[1], "client-gone")) test_queue(0, 1); + else if (!strcmp(argv[1], "ahb")) test_ahb(1, 0); + else if (!strcmp(argv[1], "ahb-healthy")) test_ahb(0, 0); + else if (!strcmp(argv[1], "ahb-device-lost")) test_ahb(0, 1); + else if (!strcmp(argv[1], "active-client-gone")) test_active_client_destroy(); + else if (!strcmp(argv[1], "healthy")) test_cleanup(0); + else if (!strcmp(argv[1], "device-lost")) test_cleanup(1); + else assert(0 && "unknown scenario"); + return 0; +} diff --git a/tools/tests/vk_external_image_sync_test.c b/tools/tests/vk_external_image_sync_test.c new file mode 100644 index 000000000..c6114d257 --- /dev/null +++ b/tools/tests/vk_external_image_sync_test.c @@ -0,0 +1,63 @@ +#include +#include +#include + +#include "vk_external_image_sync.h" + +static void check_external_pair(const struct vkp_external_client_barrier_pair *pair, + VkImage image, uint32_t queue_family) { + assert(pair->acquire.sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER); + assert(pair->acquire.oldLayout == VK_IMAGE_LAYOUT_GENERAL); + assert(pair->acquire.newLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + assert(pair->acquire.srcQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT); + assert(pair->acquire.dstQueueFamilyIndex == queue_family); + assert(pair->acquire.image == image); + assert(pair->acquire.srcAccessMask == 0); + assert(pair->acquire.dstAccessMask == VK_ACCESS_TRANSFER_READ_BIT); + assert(pair->acquire.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT); + assert(pair->acquire.subresourceRange.baseMipLevel == 0); + assert(pair->acquire.subresourceRange.levelCount == 1); + assert(pair->acquire.subresourceRange.baseArrayLayer == 0); + assert(pair->acquire.subresourceRange.layerCount == 1); + + assert(pair->release.sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER); + assert(pair->release.oldLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + assert(pair->release.newLayout == VK_IMAGE_LAYOUT_GENERAL); + assert(pair->release.srcQueueFamilyIndex == queue_family); + assert(pair->release.dstQueueFamilyIndex == VK_QUEUE_FAMILY_FOREIGN_EXT); + assert(pair->release.image == image); + assert(pair->release.srcAccessMask == VK_ACCESS_MEMORY_READ_BIT); + assert(pair->release.dstAccessMask == 0); + assert(pair->release.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT); + assert(pair->release.subresourceRange.levelCount == 1); + assert(pair->release.subresourceRange.layerCount == 1); +} + +int main(void) { + const VkImage image = (VkImage)(uintptr_t)0x1234; + const VkImage ahb_image = (VkImage)(uintptr_t)0x5678; + const uint32_t queue_family = 7; + struct vkp_external_client_barrier_pair generic, ahb; + + assert(vkp_external_client_barrier_pair(image, queue_family, 1, 0, &generic)); + check_external_pair(&generic, image, queue_family); + + /* AHardwareBuffer imports deliberately use the same client flags and lifecycle. */ + assert(vkp_external_client_barrier_pair(ahb_image, queue_family, 1, 0, &ahb)); + check_external_pair(&ahb, ahb_image, queue_family); + + unsigned char untouched[sizeof(generic)]; + memset(&generic, 0xa5, sizeof(generic)); + memcpy(untouched, &generic, sizeof(generic)); + assert(!vkp_external_client_barrier_pair(image, queue_family, 0, 0, &generic)); + assert(memcmp(&generic, untouched, sizeof(generic)) == 0); + assert(!vkp_is_external_client_image(0, 0)); + + memset(&generic, 0x5a, sizeof(generic)); + memcpy(untouched, &generic, sizeof(generic)); + assert(!vkp_external_client_barrier_pair(image, queue_family, 1, 1, &generic)); + assert(memcmp(&generic, untouched, sizeof(generic)) == 0); + assert(!vkp_is_external_client_image(1, 1)); + assert(vkp_is_external_client_image(1, 0)); + return 0; +} diff --git a/tools/tests/vk_terminal_callers_test.c b/tools/tests/vk_terminal_callers_test.c new file mode 100644 index 000000000..b2764c0da --- /dev/null +++ b/tools/tests/vk_terminal_callers_test.c @@ -0,0 +1,290 @@ +/* Compiled with the complete, unmodified production translation unit. The + * compositor's take_shm/take_pixel functions are inserted verbatim by Python. */ +#include "vk_present.c" +#include + +struct vk_api g_vk; +static unsigned operations, acquires, submits, markers, waits, idle_calls, resets; +static unsigned destroys, releases, lost_notifications, presented_notifications; +static VkResult idle_answer, fence_answer, acquire_answer, present_answer; +static unsigned fail_acquire_at; +static int generated; + +/* Every work-issuing callback is an oracle: no GPU operation after a terminal + * result. Object destruction is counted separately for genuine device loss. */ +static void live(void) { assert(g_dev_state == 1); ++operations; } +static VkResult acquire(VkDevice d, VkSwapchainKHR s, uint64_t t, VkSemaphore a, VkFence f, uint32_t *i) { + live(); ++acquires; *i = 0; + return acquires == fail_acquire_at ? acquire_answer : VK_SUCCESS; +} +static VkResult idle(VkDevice d) { live(); ++idle_calls; return idle_answer; } +static VkResult wait_fences(VkDevice d, uint32_t n, const VkFence *f, VkBool32 all, uint64_t t) { + live(); ++waits; return fence_answer; +} +static VkResult submit(VkQueue q, uint32_t n, const VkSubmitInfo *s, VkFence f) { + live(); ++submits; if (!s->commandBufferCount) ++markers; return VK_SUCCESS; +} +static VkResult present(VkQueue q, const VkPresentInfoKHR *p) { live(); return present_answer; } +static VkResult reset_cmd(VkCommandBuffer c, VkCommandBufferResetFlags f) { live(); ++resets; return VK_SUCCESS; } +static VkResult reset_fence(VkDevice d, uint32_t n, const VkFence *f) { live(); ++resets; return VK_SUCCESS; } +static VkResult begin_cmd(VkCommandBuffer c, const VkCommandBufferBeginInfo *b) { live(); return VK_SUCCESS; } +static VkResult end_cmd(VkCommandBuffer c) { live(); return VK_SUCCESS; } +static void barrier(VkCommandBuffer c, VkPipelineStageFlags s, VkPipelineStageFlags d, VkDependencyFlags f, + uint32_t nm, const VkMemoryBarrier *m, uint32_t nb, const VkBufferMemoryBarrier *b, + uint32_t ni, const VkImageMemoryBarrier *i) { live(); } +static void clear(VkCommandBuffer c, VkImage i, VkImageLayout l, const VkClearColorValue *v, + uint32_t n, const VkImageSubresourceRange *r) { live(); } +static void blit(VkCommandBuffer c, VkImage s, VkImageLayout sl, VkImage d, VkImageLayout dl, + uint32_t n, const VkImageBlit *b, VkFilter f) { live(); } +static void destroy_swap(VkDevice d, VkSwapchainKHR s, const VkAllocationCallbacks *a) { ++destroys; } +static void destroy_surface(VkInstance i, VkSurfaceKHR s, const VkAllocationCallbacks *a) { ++destroys; } +static void destroy_sem(VkDevice d, VkSemaphore s, const VkAllocationCallbacks *a) { live(); ++destroys; } +static VkResult create_sem(VkDevice d, const VkSemaphoreCreateInfo *c, const VkAllocationCallbacks *a, VkSemaphore *s) { + live(); *s = (VkSemaphore)(uintptr_t)1; return VK_SUCCESS; +} +static void destroy_image(VkDevice d, VkImage i, const VkAllocationCallbacks *a) { ++destroys; } +static void free_memory(VkDevice d, VkDeviceMemory m, const VkAllocationCallbacks *a) { ++destroys; } +static void unmap(VkDevice d, VkDeviceMemory m) { ++destroys; } + +/* External modules are not the subject of this fault injection. Their declared + * signatures remain checked by the production headers. */ +int __android_log_print(int p, const char *t, const char *f, ...) { return 0; } +void droiddeck_log(const char *t, const char *f, ...) {} +void droiddeck_on_first_frame(void) {} +int32_t ANativeWindow_getWidth(ANativeWindow *w) { return 64; } +int32_t ANativeWindow_getHeight(ANativeWindow *w) { return 64; } +void ANativeWindow_release(ANativeWindow *w) {} +void sc_layer_window_gone(void) {} +int sc_layer_present_pass(const struct vkp_draw *d, int n, int w, int h) { return 0; } +void sc_layer_hide(void) {} +int sc_layer_base_black(int w, int h) { return 0; } +void sc_layer_hide_base(void) {} +void sc_layer_hide_overlay(void) {} +void vkp_cardboard_bind_device(VkDevice d, const VkPhysicalDeviceMemoryProperties *m) {} +int vkp_cardboard_active(void) { return 0; } +void vkp_cardboard_begin_frame(void) {} +VkImage vkp_cardboard_run(VkCommandBuffer c, VkImage s, int sw, int sh, int ow, int oh) { return s; } +int vk_loader_open(const char *a, const char *b, const char *c) { assert(0); return -1; } +void vk_loader_load_instance(VkInstance i) { assert(0); } +void vk_loader_load_device(VkDevice d) { assert(0); } +PFN_vkGetInstanceProcAddr vk_loader_gipa(void) { return NULL; } +void vkp_effects_bind_device(VkDevice d, VkPhysicalDevice p, const VkPhysicalDeviceMemoryProperties *m) {} +int vkp_effects_active(void) { return 0; } +int vkp_effects_upscaling(void) { return 0; } +int vkp_effects_postprocessing(void) { return 0; } +VkFilter vkp_effects_blit_filter(void) { return VK_FILTER_NEAREST; } +void vkp_effects_set_formats(VkFormat a, VkFormat b) {} +VkImage vkp_effects_run(VkCommandBuffer c, VkImage i, int w, int h, int ow, int oh, int *rw, int *rh) { + *rw = w; *rh = h; return i; +} +const void *vkp_framegen_device_features(VkInstance i, PFN_vkGetInstanceProcAddr p, VkPhysicalDevice d) { return NULL; } +void vkp_framegen_device_ready(VkDevice d, VkQueue q, uint32_t f, int e) {} +void vkp_framegen_device_lost(void) { ++lost_notifications; } +int vkp_framegen_active(void) { return generated != 0; } +int vkp_framegen_extra_images(void) { return generated; } +int vkp_framegen_format_ok(VkFormat f) { return 0; } +int vkp_framegen_run(VkCommandBuffer c, VkImage s, VkImageView v, int w, int h, + VkFormat f, VkImage images[VKP_FG_MAX_GENERATIONS]) { + for (int i = 0; i < generated; ++i) images[i] = s; + return generated; +} +void vkp_framegen_presented(int n) { ++presented_notifications; } +void vkp_gpu_busy_device(VkPhysicalDevice p, VkDevice d, uint32_t f) {} +void vkp_gpu_busy_device_lost(void) {} +void hdrc_bind_device(VkDevice d, const VkPhysicalDeviceMemoryProperties *m) {} +void hdrc_forget_image(VkImage i) {} +int hdrc_encode(VkCommandBuffer c, VkImage a, VkImage b, VkFormat f, int w, int h, const struct hdrc_params *p) { return -1; } +void blendp_bind_device(VkDevice d) {} +void blendp_forget_image(VkImage i) {} +void vkp_effects_forget_image(VkImage i) {} +void blendp_begin_frame(void) {} +int blendp_draw(VkCommandBuffer c, VkImage s, VkFormat sf, int sw, int sh, int sg, + VkImage d, VkFormat df, int dw, int dh, const VkImageBlit *b) { return -1; } +int droiddeck_color_requested(void) { return 0; } +float droiddeck_color_sdr_white(void) { return 203.0f; } + +enum surface_role { ROLE_NONE, ROLE_TOPLEVEL }; +struct damage_list { int n; }; +struct surface { + struct vkp_image *shm_img; + struct wl_resource *resource; + int buf_w, buf_h, buf_alpha, has_content; + /* Read by take_shm's game-frame detection; with no damage it stays off here. */ + int src_set; float src[4]; + struct damage_list damage_buf, damage_local; + struct surface *parent; enum surface_role role; int mapped; +}; +static void surface_size(const struct surface *s, int *w, int *h) { *w = s->buf_w; *h = s->buf_h; } +static void scene_size(int *w, int *h) { *w = *h = 0; } +static int64_t damage_area(const struct damage_list *d, int bound_w, int bound_h) { return 0; } +static int64_t now_ns(void) { return 0; } +static struct surface *g_hud_surface, *g_hud_shm; +static int64_t g_hud_shm_ns, g_hud_last_ns; +static int g_hud_new; +static void droiddeck_on_game_frame(void) {} +struct client_info { int asked_feedback, shm_frames; }; +struct wl_shm_buffer { int w, h, stride; const void *data; }; +#define WL_SHM_FORMAT_ARGB8888 0 +static unsigned g_stat_shm; +static int wl_shm_buffer_get_width(struct wl_shm_buffer *b) { return b->w; } +static int wl_shm_buffer_get_height(struct wl_shm_buffer *b) { return b->h; } +static int wl_shm_buffer_get_stride(struct wl_shm_buffer *b) { return b->stride; } +static int wl_shm_buffer_get_format(struct wl_shm_buffer *b) { return WL_SHM_FORMAT_ARGB8888; } +static const void *wl_shm_buffer_get_data(struct wl_shm_buffer *b) { return b->data; } +static void wl_shm_buffer_begin_access(struct wl_shm_buffer *b) {} +static void wl_shm_buffer_end_access(struct wl_shm_buffer *b) {} +struct wl_resource { int dmabuf; }; +static struct wl_resource *expected_release; +static void wl_buffer_send_release(struct wl_resource *b) { assert(b && b == expected_release); ++releases; } +static struct wl_client *wl_resource_get_client(struct wl_resource *r) { return NULL; } +static struct client_info *client_info_of(struct wl_client *c) { return NULL; } +struct dmabuf_buffer { int unused; }; +static struct dmabuf_buffer dmabuf_fixture; +static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer) { + assert(buffer); + return buffer->dmabuf ? &dmabuf_fixture : NULL; +} +/* COMPOSITOR_CALLERS */ + +static void setup(int extra) { + g_vk = (struct vk_api){.AcquireNextImageKHR = acquire, .DeviceWaitIdle = idle, + .WaitForFences = wait_fences, .QueueSubmit = submit, .QueuePresentKHR = present, + .ResetCommandBuffer = reset_cmd, .ResetFences = reset_fence, .BeginCommandBuffer = begin_cmd, + .EndCommandBuffer = end_cmd, .CmdPipelineBarrier = barrier, .CmdClearColorImage = clear, + .CmdBlitImage = blit, .DestroySwapchainKHR = destroy_swap, .DestroySurfaceKHR = destroy_surface, + .DestroySemaphore = destroy_sem, .CreateSemaphore = create_sem, + .DestroyImage = destroy_image, .FreeMemory = free_memory, .UnmapMemory = unmap}; + idle_answer = fence_answer = acquire_answer = present_answer = VK_SUCCESS; + generated = g_swap_extra = extra; + g_dev_state = 1; + g_dev = (VkDevice)(uintptr_t)1; g_inst = (VkInstance)(uintptr_t)1; + g_window = (ANativeWindow *)(uintptr_t)1; g_queue = (VkQueue)(uintptr_t)1; + g_swapchain = (VkSwapchainKHR)(uintptr_t)2; g_surface = (VkSurfaceKHR)(uintptr_t)3; + g_images = calloc(1, sizeof(*g_images)); assert(g_images); + *g_images = (VkImage)(uintptr_t)4; g_nimg = 1; + g_extent = g_caps_extent = (VkExtent2D){64, 64}; + g_surface_checked_ns = INT64_MAX; /* no simulated geometry change */ + g_scene.img = (VkImage)(uintptr_t)5; g_scene.mem = (VkDeviceMemory)(uintptr_t)6; + g_scene.w = g_scene.h = 64; + for (int k = 0; k < MAX_PRESENTS; ++k) { + g_cmds[k] = (VkCommandBuffer)(uintptr_t)(10 + k); + g_acqs[k] = (VkSemaphore)(uintptr_t)(20 + k); + g_rnds[k] = (VkSemaphore)(uintptr_t)(30 + k); + } + g_fence = (VkFence)(uintptr_t)40; +} + +/* Probe the public GPU entry points after a terminal result. This includes an + * active off-screen pass and consumed release fds, not just the render guard. */ +static void assert_stopped(void) { + unsigned before = operations, before_destroys = destroys; + struct vkp_image src = {.image = (VkImage)(uintptr_t)7, .w = 1, .h = 1}; + struct vkp_image dst = {.image = (VkImage)(uintptr_t)8, .w = 1, .h = 1, .blit_dst = 1}; + uint32_t pixel = 0; uint64_t mod = 0; + assert(vkp_render(64, 64, NULL, 0) == -1); + assert(vkp_render_plain(64, 64) == -1); + assert(vkp_render_hdr(64, 64, NULL, 0, NULL, NULL) == -1); + assert(vkp_base_black(64, 64) == -1); + assert(vkp_update_map(64, 64) == -1); + assert(vkp_ready() == -1); + assert(vkp_image_create_shm(1, 1) == NULL); + assert(vkp_image_import_dmabuf(-1, 0, VKP_MOD_LINEAR, 1, 1, 4, 0, 0) == NULL); + assert(vkp_dmabuf_modifiers(0, &mod, 1) == 0); + assert(vkp_image_readback(&src, &pixel, 1) == -1); + assert(vkp_pass_begin(64, 64, NULL, 0, NULL, NULL) == -1); + int fd[2]; assert(pipe(fd) == 0); + assert(vkp_blit_image(&src, &dst, fd[0]) == -1); + errno = 0; assert(close(fd[0]) == -1 && errno == EBADF); close(fd[1]); + g_pass.active = 1; + assert(pipe(fd) == 0); + assert(vkp_pass_copy_to(&dst, fd[0]) == -1 && !g_pass.active); + errno = 0; assert(close(fd[0]) == -1 && errno == EBADF); close(fd[1]); + g_pass.active = 1; vkp_pass_abort(); assert(!g_pass.active); + assert(timed_wait(g_fence, UINT64_MAX) != VK_SUCCESS); + assert(operations == before && destroys == before_destroys); +} + +static void acquire_failure(int later, VkResult result) { + setup(later ? 1 : 0); + fail_acquire_at = later ? 2 : 1; acquire_answer = result; + idle_answer = VK_ERROR_OUT_OF_HOST_MEMORY; + VkImage *retained = g_images; + assert(vkp_render(64, 64, NULL, 0) == -1); + assert(g_dev_state == -3 && vkp_completion_unknown() && !vkp_device_lost()); + assert(acquires == fail_acquire_at && idle_calls == 1); + assert(submits == (unsigned)later && markers == 0 && waits == 0); + assert(destroys == 0 && g_images == retained && g_nimg == 1); + assert(g_swapchain && g_surface && g_fence && g_scene.img && g_scene.mem); + assert_stopped(); + free(g_images); /* test-owned fake resources only */ +} + +static void upload_timeout(int single_pixel) { + setup(0); + uint8_t bytes[48], saved[48], data[16]; + memset(bytes, 0xa5, sizeof(bytes)); memset(data, 0x3c, sizeof(data)); + struct vkp_image *img = calloc(1, sizeof(*img)); assert(img); + *img = (struct vkp_image){.image = (VkImage)(uintptr_t)7, .mem = (VkDeviceMemory)(uintptr_t)8, + .map = bytes, .offset = 3, .row_pitch = 16, + .w = single_pixel ? 1 : 2, .h = single_pixel ? 1 : 2}; + struct surface s = {.shm_img = img}; + struct wl_shm_buffer b = {.w = img->w, .h = img->h, .stride = img->w * 4, .data = data}; + struct wl_resource resource = {0}; + expected_release = &resource; + if (single_pixel) take_pixel(&s, data, &resource); else take_shm(&s, &b, &resource); + assert(bytes[3] == 0x3c && bytes[0] == 0xa5 && releases == 1); + memcpy(saved, bytes, sizeof(saved)); + struct vkp_image dst = {.image = (VkImage)(uintptr_t)9, .blit_dst = 1, .w = img->w, .h = img->h}; + fence_answer = VK_TIMEOUT; + assert(vkp_blit_image(img, &dst, -1) == -1); + assert(g_dev_state == -3 && submits == 1 && waits == 1); + memset(data, 0x7e, sizeof(data)); + if (single_pixel) take_pixel(&s, data, &resource); else take_shm(&s, &b, &resource); + assert(s.shm_img == img && memcmp(saved, bytes, sizeof(saved)) == 0 && releases == 2); + struct wl_resource dmabuf = {.dmabuf = 1}; + expected_release = &dmabuf; + assert(buffer_release_blocked(&dmabuf)); + send_buffer_release(&dmabuf); + assert(releases == 2); + vkp_image_destroy(img); + assert(destroys == 0 && img->map == bytes && img->image && img->mem); + assert_stopped(); + g_dev_state = 1; + assert(!buffer_release_blocked(&dmabuf)); + send_buffer_release(&dmabuf); + assert(releases == 3); + free(img); free(g_images); /* fake resources only, not production cleanup */ +} + +int main(int argc, char **argv) { + assert(argc == 2); + const char *name = argv[1]; + if (!strcmp(name, "first-out-of-date")) acquire_failure(0, VK_ERROR_OUT_OF_DATE_KHR); + else if (!strcmp(name, "first-surface-lost")) acquire_failure(0, VK_ERROR_SURFACE_LOST_KHR); + else if (!strcmp(name, "later-out-of-date")) acquire_failure(1, VK_ERROR_OUT_OF_DATE_KHR); + else if (!strcmp(name, "later-surface-lost")) acquire_failure(1, VK_ERROR_SURFACE_LOST_KHR); + else if (!strcmp(name, "shm-timeout")) upload_timeout(0); + else if (!strcmp(name, "pixel-timeout")) upload_timeout(1); + else if (!strcmp(name, "pass-terminal")) { setup(0); g_dev_state = -3; assert_stopped(); free(g_images); } + else if (!strcmp(name, "normal") || !strcmp(name, "framegen")) { + int extra = !strcmp(name, "framegen"); setup(extra); + assert(vkp_render(64, 64, NULL, 0) == 0 && g_dev_state == 1); + assert(acquires == (unsigned)(1 + extra) && submits == (unsigned)(1 + extra)); + assert(waits == 1 && markers == 0 && idle_calls == 0 && destroys == 0); + assert(presented_notifications == (unsigned)extra); free(g_images); + } else if (!strcmp(name, "later-timeout")) { + setup(1); fail_acquire_at = 2; acquire_answer = VK_TIMEOUT; + assert(vkp_render(64, 64, NULL, 0) == -1 && g_dev_state == 1); + assert(acquires == 2 && submits == 2 && markers == 1 && waits == 1); free(g_images); + } else if (!strcmp(name, "acquire-device-lost") || !strcmp(name, "present-device-lost")) { + setup(1); + if (!strcmp(name, "acquire-device-lost")) { fail_acquire_at = 2; acquire_answer = VK_ERROR_DEVICE_LOST; } + else present_answer = VK_ERROR_DEVICE_LOST; + assert(vkp_render(64, 64, NULL, 0) == -1 && vkp_device_lost()); + assert(!vkp_completion_unknown()); + assert(g_dev_state == -2 && lost_notifications == 1 && markers == 0 && waits == 0); + assert(!g_images && !g_swapchain && !g_surface && destroys == 2); + assert_stopped(); + } else assert(0 && "unknown scenario"); + return 0; +} diff --git a/tools/tests/xclipse_layer_test.c b/tools/tests/xclipse_layer_test.c new file mode 100644 index 000000000..26105d69f --- /dev/null +++ b/tools/tests/xclipse_layer_test.c @@ -0,0 +1,217 @@ +/* The Xclipse layer between a simulated loader and a mock driver. The driver refuses what the compat + * layer and Samsung's driver refuse: textureCompressionBC, and a compute layout holding an immutable + * YCbCr sampler. Built by test_xclipse_layer.py with the layer source included directly. */ +#include "xclipse_layer.c" +#include + +struct object { void *dispatch; }; +static int instance_table, device_table[4]; +static struct object mock_instance = {&instance_table}, mock_physical = {&instance_table}; +static struct object mock_devices[4] = {{&device_table[0]}, {&device_table[1]}, {&device_table[2]}, {&device_table[3]}}; +static int devices_created; + +/* What the driver last saw. */ +static int driver_saw_bc, driver_saw_layout_sampler_count; +static VkSampler driver_layout_samplers[16]; +static uint64_t next_sampler = 0x100; +static VkSampler driver_ycbcr[16]; +static int driver_ycbcr_count; + +static VkResult VKAPI_CALL mock_create_instance(const VkInstanceCreateInfo *info, const VkAllocationCallbacks *allocator, + VkInstance *instance) { + *instance = (VkInstance)&mock_instance; + return VK_SUCCESS; +} + +static void VKAPI_CALL mock_features(VkPhysicalDevice physical, VkPhysicalDeviceFeatures *features) { + memset(features, 0, sizeof(*features)); +} + +static void VKAPI_CALL mock_features2(VkPhysicalDevice physical, VkPhysicalDeviceFeatures2 *features) { + mock_features(physical, &features->features); +} + +/* The compat layer's emulation: BC formats sample, the feature bit stays off. */ +static void VKAPI_CALL mock_format_properties(VkPhysicalDevice physical, VkFormat format, VkFormatProperties *out) { + memset(out, 0, sizeof(*out)); + if (format >= VK_FORMAT_BC1_RGB_UNORM_BLOCK && format <= VK_FORMAT_BC7_SRGB_BLOCK) + out->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; +} + +static void VKAPI_CALL mock_format_properties2(VkPhysicalDevice physical, VkFormat format, VkFormatProperties2 *out) { + mock_format_properties(physical, format, &out->formatProperties); +} + +static VkResult VKAPI_CALL mock_time_domains(VkPhysicalDevice physical, uint32_t *count, VkTimeDomainEXT *domains) { + if (domains) { domains[0] = VK_TIME_DOMAIN_DEVICE_EXT; domains[1] = VK_TIME_DOMAIN_CLOCK_MONOTONIC_EXT; } + *count = 2; + return VK_SUCCESS; +} + +static VkResult VKAPI_CALL mock_create_device(VkPhysicalDevice physical, const VkDeviceCreateInfo *info, + const VkAllocationCallbacks *allocator, VkDevice *device) { + driver_saw_bc = info->pEnabledFeatures && info->pEnabledFeatures->textureCompressionBC; + for (const VkBaseInStructure *p = info->pNext; p; p = p->pNext) + if (p->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2 && + ((const VkPhysicalDeviceFeatures2 *)p)->features.textureCompressionBC) driver_saw_bc = 1; + if (driver_saw_bc) return VK_ERROR_FEATURE_NOT_PRESENT; + assert(devices_created < 4); + *device = (VkDevice)&mock_devices[devices_created++]; + return VK_SUCCESS; +} + +static VkResult VKAPI_CALL mock_create_sampler(VkDevice device, const VkSamplerCreateInfo *info, + const VkAllocationCallbacks *allocator, VkSampler *sampler) { + *sampler = (VkSampler)next_sampler++; + for (const VkBaseInStructure *p = info->pNext; p; p = p->pNext) + if (p->sType == VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO) driver_ycbcr[driver_ycbcr_count++] = *sampler; + return VK_SUCCESS; +} + +static void VKAPI_CALL mock_destroy_sampler(VkDevice device, VkSampler sampler, const VkAllocationCallbacks *allocator) { + for (int i = 0; i < driver_ycbcr_count; i++) + if (driver_ycbcr[i] == sampler) driver_ycbcr[i] = driver_ycbcr[--driver_ycbcr_count]; +} + +static VkResult VKAPI_CALL mock_create_set_layout(VkDevice device, const VkDescriptorSetLayoutCreateInfo *info, + const VkAllocationCallbacks *allocator, VkDescriptorSetLayout *layout) { + driver_saw_layout_sampler_count = 0; + for (uint32_t b = 0; b < info->bindingCount; b++) { + const VkDescriptorSetLayoutBinding *binding = &info->pBindings[b]; + for (uint32_t s = 0; binding->pImmutableSamplers && s < binding->descriptorCount; s++) { + VkSampler sampler = binding->pImmutableSamplers[s]; + driver_layout_samplers[driver_saw_layout_sampler_count++] = sampler; + for (int i = 0; i < driver_ycbcr_count; i++) + if ((binding->stageFlags & VK_SHADER_STAGE_COMPUTE_BIT) && driver_ycbcr[i] == sampler) + return VK_ERROR_UNKNOWN; + } + } + *layout = (VkDescriptorSetLayout)0x5000; + return VK_SUCCESS; +} + +static void VKAPI_CALL mock_destroy_device(VkDevice device, const VkAllocationCallbacks *allocator) {} + +static VkResult VKAPI_CALL mock_timestamps(VkDevice device, uint32_t count, const VkCalibratedTimestampInfoEXT *infos, + uint64_t *timestamps, uint64_t *deviation) { + for (uint32_t i = 0; i < count; i++) timestamps[i] = 42; + *deviation = 1; + return VK_SUCCESS; +} + +#define MOCK(name, function) if (!strcmp(requested, name)) return (PFN_vkVoidFunction)function +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL mock_gipa(VkInstance instance, const char *requested) { + MOCK("vkCreateInstance", mock_create_instance); + MOCK("vkCreateDevice", mock_create_device); + MOCK("vkGetPhysicalDeviceFeatures", mock_features); + MOCK("vkGetPhysicalDeviceFeatures2", mock_features2); + MOCK("vkGetPhysicalDeviceFormatProperties", mock_format_properties); + MOCK("vkGetPhysicalDeviceFormatProperties2", mock_format_properties2); + MOCK("vkGetPhysicalDeviceCalibrateableTimeDomainsEXT", mock_time_domains); + return NULL; +} + +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL mock_gdpa(VkDevice device, const char *requested) { + MOCK("vkCreateSampler", mock_create_sampler); + MOCK("vkDestroySampler", mock_destroy_sampler); + MOCK("vkCreateDescriptorSetLayout", mock_create_set_layout); + MOCK("vkDestroyDevice", mock_destroy_device); + MOCK("vkGetCalibratedTimestampsEXT", mock_timestamps); + return NULL; +} + +/* The loader's link for one vkCreateDevice call, then the caller's own chain. */ +static VkResult create(const void *chain, const VkPhysicalDeviceFeatures *enabled, VkDevice *device) { + VkLayerDeviceLink link = {NULL, mock_gipa, mock_gdpa}; + VkLayerDeviceCreateInfo loader = {VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO, chain, VK_LAYER_LINK_INFO, {&link}}; + VkDeviceCreateInfo info = {VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &loader, 0, 0, NULL, 0, NULL, 0, NULL, enabled}; + PFN_vkCreateDevice create_device = (PFN_vkCreateDevice)vkGetInstanceProcAddr((VkInstance)&mock_instance, "vkCreateDevice"); + return create_device((VkPhysicalDevice)&mock_physical, &info, NULL, device); +} + +int main(void) { + VkNegotiateLayerInterface negotiate = {LAYER_NEGOTIATE_INTERFACE_STRUCT, NULL, 2, NULL, NULL, NULL}; + assert(vkNegotiateLoaderLayerInterfaceVersion(&negotiate) == VK_SUCCESS); + + VkLayerInstanceLink instance_link = {NULL, mock_gipa, NULL}; + VkLayerInstanceCreateInfo instance_loader = {VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO, NULL, VK_LAYER_LINK_INFO, + {&instance_link}}; + VkInstanceCreateInfo instance_info = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pNext = &instance_loader}; + VkInstance instance; + PFN_vkCreateInstance create_instance_fn = (PFN_vkCreateInstance)vkGetInstanceProcAddr(NULL, "vkCreateInstance"); + assert(create_instance_fn(&instance_info, NULL, &instance) == VK_SUCCESS); + + /* The emulated bit is reported... */ + VkPhysicalDeviceFeatures features; + ((PFN_vkGetPhysicalDeviceFeatures)vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceFeatures"))( + (VkPhysicalDevice)&mock_physical, &features); + assert(features.textureCompressionBC); + + /* ...and cleared before the driver, through pEnabledFeatures... */ + VkDevice device; + VkPhysicalDeviceFeatures legacy = {.textureCompressionBC = VK_TRUE}; + assert(create(NULL, &legacy, &device) == VK_SUCCESS && !driver_saw_bc && legacy.textureCompressionBC); + + /* ...and through a VkPhysicalDeviceFeatures2 behind another structure, leaving the caller's chain intact. */ + VkPhysicalDeviceVulkan12Features vk12 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES}; + VkPhysicalDeviceFeatures2 features2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &vk12, + {.textureCompressionBC = VK_TRUE}}; + VkPhysicalDeviceTimelineSemaphoreFeatures timeline = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES, + &features2, VK_TRUE}; + VkPhysicalDeviceFeatures2 features2_before = features2; + VkPhysicalDeviceTimelineSemaphoreFeatures timeline_before = timeline; + VkDevice chained; + assert(create(&timeline, NULL, &chained) == VK_SUCCESS && !driver_saw_bc); + assert(!memcmp(&features2, &features2_before, sizeof(features2))); + assert(!memcmp(&timeline, &timeline_before, sizeof(timeline))); + + /* A structure newer than the headers this was built with cannot be copied: the chain passes as it + * is and the driver refuses the bit, without the caller's structures being written. */ + struct { VkStructureType sType; const void *pNext; uint32_t payload; } future = {(VkStructureType)0x7ffffff0, &features2, 7}; + VkDevice unused; + assert(create(&future, NULL, &unused) == VK_ERROR_FEATURE_NOT_PRESENT); + assert(!memcmp(&features2, &features2_before, sizeof(features2)) && future.pNext == &features2); + + /* Compute layouts get a plain sampler in place of a YCbCr one. */ + PFN_vkCreateSampler create_sampler_fn = (PFN_vkCreateSampler)vkGetDeviceProcAddr(device, "vkCreateSampler"); + PFN_vkDestroySampler destroy_sampler_fn = (PFN_vkDestroySampler)vkGetDeviceProcAddr(device, "vkDestroySampler"); + PFN_vkCreateDescriptorSetLayout create_layout = (PFN_vkCreateDescriptorSetLayout)vkGetDeviceProcAddr(device, "vkCreateDescriptorSetLayout"); + VkSamplerYcbcrConversionInfo conversion = {VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO, NULL, (VkSamplerYcbcrConversion)1}; + VkSamplerCreateInfo ycbcr_info = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .pNext = &conversion}; + VkSamplerCreateInfo plain_info = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + VkSampler ycbcr; + assert(create_sampler_fn(device, &ycbcr_info, NULL, &ycbcr) == VK_SUCCESS); + VkSampler immutable[2] = {ycbcr, ycbcr}; + VkDescriptorSetLayoutBinding binding = {4, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2, VK_SHADER_STAGE_COMPUTE_BIT, immutable}; + VkDescriptorSetLayoutCreateInfo layout_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, NULL, 0, 1, &binding}; + VkDescriptorSetLayout layout; + assert(create_layout(device, &layout_info, NULL, &layout) == VK_SUCCESS); + assert(driver_layout_samplers[0] != ycbcr && immutable[0] == ycbcr); + + /* A destroyed YCbCr sampler's handle, reused for a plain sampler, is left alone. */ + destroy_sampler_fn(device, ycbcr, NULL); + next_sampler = (uint64_t)ycbcr; + VkSampler reused; + assert(create_sampler_fn(device, &plain_info, NULL, &reused) == VK_SUCCESS && reused == ycbcr); + immutable[0] = immutable[1] = reused; + assert(create_layout(device, &layout_info, NULL, &layout) == VK_SUCCESS && driver_layout_samplers[0] == reused); + + /* Registrations belong to their device and go with it. */ + VkSampler other; + assert(create_sampler_fn(chained, &ycbcr_info, NULL, &other) == VK_SUCCESS && ycbcr_count == 1); + ((PFN_vkDestroyDevice)vkGetDeviceProcAddr(chained, "vkDestroyDevice"))(chained, NULL); + assert(ycbcr_count == 0); + + /* The KHR calibrated-timestamp entry points reach the EXT ones. */ + uint32_t domains = 0; + PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT time_domains = (PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT) + vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceCalibrateableTimeDomainsKHR"); + assert(time_domains((VkPhysicalDevice)&mock_physical, &domains, NULL) == VK_SUCCESS && domains == 2); + VkCalibratedTimestampInfoEXT stamp = {VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_EXT, NULL, VK_TIME_DOMAIN_DEVICE_EXT}; + uint64_t value = 0, deviation = 0; + PFN_vkGetCalibratedTimestampsEXT timestamps = (PFN_vkGetCalibratedTimestampsEXT)vkGetDeviceProcAddr(device, "vkGetCalibratedTimestampsKHR"); + assert(timestamps(device, 1, &stamp, &value, &deviation) == VK_SUCCESS && value == 42); + + puts("xclipse layer ok"); + return 0; +} diff --git a/tools/venus/addr2line.sh b/tools/venus/addr2line.sh new file mode 100644 index 000000000..e9f6e5f52 --- /dev/null +++ b/tools/venus/addr2line.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Function, source line and disassembly at offsets into an aarch64 library, with the NDK's tools. +# Run in WSL: bash addr2line.sh ... +# The offset is the crash pc minus the library's start in the process's maps (tools/venus/watch-maps.sh). +LIB=${1:?library} +shift +B=/home/delo/android-sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/bin +md5sum "$LIB" +$B/llvm-addr2line -f -C -i -e "$LIB" "$@" +for a in "$@"; do + start=$(printf "0x%x" $((a - 48))) + stop=$(printf "0x%x" $((a + 16))) + $B/llvm-objdump -d --no-show-raw-insn --start-address=$start --stop-address=$stop "$LIB" | tail -n +7 +done diff --git a/tools/venus/ci-build.sh b/tools/venus/ci-build.sh new file mode 100755 index 000000000..a06f3b73b --- /dev/null +++ b/tools/venus/ci-build.sh @@ -0,0 +1,146 @@ +#!/bin/bash +# Builds the Mali (Venus) component in CI, from David Cimo's tools/venus recipes (build-venus-icd.sh, +# build-virgl-android.sh, build-layer.sh), into venus.tzst rooted at app/src/main: +# jniLibs/arm64-v8a/ libvirgl_test_server.so, libvirgl_render_server.so, libvirglrenderer.so, +# libepoxy.so (bionic: the vtest server the app runs over the phone's Vulkan) +# assets/venus/ libvulkan_virtio.so (glibc: Mesa's Venus ICD for the session) +# assets/linuxfs/usr/local/... the Mali compatibility layer and its manifest +# Needs: docker (Debian trixie for the glibc parts), the NDK in $ANDROID_NDK_HOME, meson, ninja. +set -euo pipefail +REPO=$(cd "$(dirname "$0")/../.." && pwd) +W=${WORK:-$PWD/venus-work} +OUT=$W/out +rm -rf "$OUT" && mkdir -p "$OUT/jniLibs/arm64-v8a" "$OUT/assets/venus" \ + "$OUT/assets/linuxfs/usr/local/lib/droiddeck-mali" "$OUT/assets/linuxfs/usr/local/share/vulkan/implicit_layer.d" + +# --- Linux side: Mesa 26.2.5's Venus ICD and the Mali compatibility layer (aarch64 glibc) --- +docker run --rm --platform linux/amd64 -v "$W":/w -v "$REPO/tools/venus":/venus:ro -w /tmp debian:trixie bash -c ' +set -euo pipefail +export DEBIAN_FRONTEND=noninteractive +dpkg --add-architecture arm64 +apt-get update -qq +apt-get install -y -qq --no-install-recommends git ca-certificates curl crossbuild-essential-arm64 meson ninja-build \ + pkg-config python3-mako python3-yaml python3-packaging bison flex glslang-tools libwayland-bin wayland-protocols \ + libdrm-dev:arm64 libwayland-dev:arm64 libx11-dev:arm64 libx11-xcb-dev:arm64 libxcb1-dev:arm64 \ + libxcb-dri3-dev:arm64 libxcb-present-dev:arm64 libxcb-randr0-dev:arm64 libxcb-shm0-dev:arm64 \ + libxcb-sync-dev:arm64 libxcb-xfixes0-dev:arm64 libxshmfence-dev:arm64 libxrandr-dev:arm64 \ + libexpat1-dev:arm64 zlib1g-dev:arm64 libzstd-dev:arm64 >/dev/null +cat > /tmp/arm64.ini </dev/null +ninja -C venus-build src/virtio/vulkan/libvulkan_virtio.so +aarch64-linux-gnu-strip --strip-unneeded -o /w/out/assets/venus/libvulkan_virtio.so venus-build/src/virtio/vulkan/libvulkan_virtio.so +echo "venus icd min glibc: $(aarch64-linux-gnu-objdump -T /w/out/assets/venus/libvulkan_virtio.so | grep -o "GLIBC_[0-9.]*" | sort -Vu | tail -1)" +SDK=vulkan-sdk-1.4.357.0 +git -c advice.detachedHead=false clone -q --depth 1 -b $SDK https://github.com/KhronosGroup/Vulkan-Headers.git /tmp/vulkan +git -c advice.detachedHead=false clone -q --depth 1 -b $SDK https://github.com/KhronosGroup/SPIRV-Headers.git /tmp/spirv +mkdir -p /tmp/bcdec +curl -sSfL -o /tmp/bcdec/bcdec.h https://raw.githubusercontent.com/iOrange/bcdec/80859ed3b7afb1c527a2a99d70c61457bea72d0c/bcdec.h +L=/w/out/assets/linuxfs/usr/local +aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wextra -Wno-unused-parameter -fvisibility=hidden -pthread \ + -I/tmp/vulkan/include -I/tmp/spirv/include -I/tmp/bcdec \ + -o $L/lib/droiddeck-mali/libVkLayer_droiddeck_mali_compat.so /venus/layer/*.c +aarch64-linux-gnu-strip --strip-unneeded $L/lib/droiddeck-mali/libVkLayer_droiddeck_mali_compat.so +cp /venus/layer/VkLayer_droiddeck_mali_compat.json $L/share/vulkan/implicit_layer.d/ +chmod -R a+rwX /w/out +' + +# --- Android side: libepoxy + virglrenderer 1.3.0 (venus, vtest) for arm64 with the NDK --- +NDK=${ANDROID_NDK_HOME:?} +TC=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin +API=29 +A=$W/android +AOUT=$A/out +mkdir -p "$A/cross" "$A/compat/log" "$A/compat/cutils" "$AOUT/lib/pkgconfig" +cd "$A" +git clone -q --depth 1 -b 1.5.10 https://github.com/anholt/libepoxy.git +git clone -q --depth 1 -b v1.4.365 https://github.com/KhronosGroup/Vulkan-Headers.git +git clone -q --depth 1 -b virglrenderer-1.3.0 https://gitlab.freedesktop.org/virgl/virglrenderer.git +git -C virglrenderer apply "$REPO/tools/venus/virglrenderer-android.patch" +git -C virglrenderer apply "$REPO/tools/venus/virglrenderer-destroy-unknown-pipeline.patch" +git -C virglrenderer apply "$REPO/tools/venus/virglrenderer-destroy-mutex-after-objects.patch" +cat > compat/log/log.h <<'EOC' +#pragma once +#include +#define LOG_PRI(prio, tag, ...) __android_log_print(prio, tag, __VA_ARGS__) +EOC +cat > compat/cutils/properties.h <<'EOC' +#pragma once +#include +#include +#define PROPERTY_VALUE_MAX PROP_VALUE_MAX +#define PROPERTY_KEY_MAX 32 +static inline int property_get(const char *key, char *value, const char *def) +{ + int len = __system_property_get(key, value); + if (len <= 0 && def) { strcpy(value, def); len = (int)strlen(def); } + return len; +} +EOC +cat > "$AOUT/lib/pkgconfig/vulkan.pc" < cross/android.ini </dev/null +ninja -C epoxy-build install >/dev/null +meson setup virgl-build virglrenderer --cross-file cross/android.ini --prefix="$AOUT" --libdir=lib \ + --buildtype=release -Dvenus=true -Dplatforms=auto -Drender-server-worker=thread \ + -Dvulkan-dload=true -Dcheck-gl-errors=false -Dtests=false >/dev/null +ninja -C virgl-build install >/dev/null +J=$OUT/jniLibs/arm64-v8a +cp "$AOUT/bin/virgl_test_server" "$J/libvirgl_test_server.so" +cp "$AOUT/libexec/virgl_render_server" "$J/libvirgl_render_server.so" +cp "$AOUT/lib/libvirglrenderer.so" "$AOUT/lib/libepoxy.so" "$J/" +for f in "$J"/*.so; do "$TC/llvm-strip" --strip-unneeded "$f"; done + +cd "$OUT" +find . -type f | sort +tar --owner=0 --group=0 --numeric-owner --sort=name -cf - . | zstd -19 -T0 -q -o "$W/venus.tzst" +sha256sum "$W/venus.tzst" | tee "$W/venus.tzst.sha256" diff --git a/tools/venus/glprobe.c b/tools/venus/glprobe.c new file mode 100644 index 000000000..da044311d --- /dev/null +++ b/tools/venus/glprobe.c @@ -0,0 +1,69 @@ +/* + * droiddeck-glprobe: does OpenGL through GLX start on this session's display, the way the Steam + * client's first window asks for it (glXChooseVisual, then a context)? Run once before the client + * on the Mali (Venus) route, where Zink failed on a Mali-G615 with nothing but "failed to load + * driver: zink" and the client gave up ("UpdateUI GL not available"). Exit 0 when a visual and a + * context come back, 1 when GL does not start, 2 when there is no X display or no libGL. + * libX11 and libGL are opened at run time, so this builds without their headers. + */ +#include +#include + +typedef void *(*open_display_fn)(const char *); +typedef int (*close_display_fn)(void *); +typedef int (*default_screen_fn)(void *); +typedef void *(*choose_visual_fn)(void *, int, int *); +typedef void *(*create_context_fn)(void *, void *, void *, int); +typedef void (*destroy_context_fn)(void *, void *); +typedef int (*free_fn)(void *); + +#define GLX_RGBA 4 +#define GLX_DOUBLEBUFFER 5 +#define GLX_RED_SIZE 8 +#define GLX_GREEN_SIZE 9 +#define GLX_BLUE_SIZE 10 + +int main(void) +{ + void *x11 = dlopen("libX11.so.6", RTLD_NOW | RTLD_GLOBAL); + void *gl = dlopen("libGL.so.1", RTLD_NOW | RTLD_GLOBAL); + if (!x11 || !gl) { + printf("glprobe: cannot open %s: %s\n", x11 ? "libGL.so.1" : "libX11.so.6", dlerror()); + return 2; + } + open_display_fn open_display = (open_display_fn)dlsym(x11, "XOpenDisplay"); + close_display_fn close_display = (close_display_fn)dlsym(x11, "XCloseDisplay"); + default_screen_fn default_screen = (default_screen_fn)dlsym(x11, "XDefaultScreen"); + free_fn x_free = (free_fn)dlsym(x11, "XFree"); + choose_visual_fn choose_visual = (choose_visual_fn)dlsym(gl, "glXChooseVisual"); + create_context_fn create_context = (create_context_fn)dlsym(gl, "glXCreateContext"); + destroy_context_fn destroy_context = (destroy_context_fn)dlsym(gl, "glXDestroyContext"); + if (!open_display || !close_display || !default_screen || !x_free || !choose_visual || !create_context || + !destroy_context) { + printf("glprobe: missing X11/GLX entry points\n"); + return 2; + } + void *dpy = open_display(NULL); + if (!dpy) { + printf("glprobe: no X display\n"); + return 2; + } + int attrs[] = {GLX_RGBA, GLX_DOUBLEBUFFER, GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, 0}; + void *visual = choose_visual(dpy, default_screen(dpy), attrs); + if (!visual) { + printf("glprobe: FAIL glXChooseVisual\n"); + close_display(dpy); + return 1; + } + void *ctx = create_context(dpy, visual, NULL, 1); + x_free(visual); + if (!ctx) { + printf("glprobe: FAIL glXCreateContext\n"); + close_display(dpy); + return 1; + } + destroy_context(dpy, ctx); + close_display(dpy); + printf("glprobe: OK (visual and context)\n"); + return 0; +} diff --git a/tools/venus/guest-vulkaninfo.sh b/tools/venus/guest-vulkaninfo.sh new file mode 100644 index 000000000..124c5fb26 --- /dev/null +++ b/tools/venus/guest-vulkaninfo.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# vulkaninfo as the session's Linux processes see the GPU: through Venus, and through the Mali +# compatibility layer when DROIDDECK_MALI_COMPAT is 1. Needs a running session (the vtest server). +# Run from Git Bash: MSYS_NO_PATHCONV=1 bash tools/venus/guest-vulkaninfo.sh [0|1] > vulkaninfo.txt +set -euo pipefail +COMPAT=${1:-1} +A=/c/Users/derab/tools/platform-tools/adb.exe +PKG=com.droiddeck.launcher +F=/data/user/0/$PKG/files +APK=$($A shell pm path $PKG | head -1 | tr -d '\r' | sed 's/^package://') +L=${APK%/base.apk}/lib/arm64 +$A exec-out "run-as $PKG sh -c 'PROOT_TMP_DIR=/data/user/0/$PKG/cache PROOT_LOADER=$L/libproot-loader.so \ + $L/libproot.so -r $F/linuxfs -w /root -b /dev -b /proc -b /sys -b $F /usr/bin/env -i HOME=/root \ + PATH=/usr/bin:/bin DROIDDECK_MALI_COMPAT=$COMPAT VK_ICD_FILENAMES=$F/venus/virtio_icd.json VN_DEBUG=vtest \ + VTEST_SOCKET_NAME=$F/venus/vtest.sock vulkaninfo 2>&1'" diff --git a/tools/venus/layer/VkLayer_droiddeck_mali_compat.json b/tools/venus/layer/VkLayer_droiddeck_mali_compat.json new file mode 100644 index 000000000..e1efa4c15 --- /dev/null +++ b/tools/venus/layer/VkLayer_droiddeck_mali_compat.json @@ -0,0 +1,17 @@ +{ + "file_format_version": "1.2.0", + "layer": { + "name": "VK_LAYER_DROIDDECK_mali_compat", + "type": "GLOBAL", + "library_path": "/usr/local/lib/droiddeck-mali/libVkLayer_droiddeck_mali_compat.so", + "api_version": "1.4.357", + "implementation_version": "1", + "description": "Features DXVK requires and Mali lacks, made up for", + "functions": { + "vkNegotiateLoaderLayerInterfaceVersion": "vkNegotiateLoaderLayerInterfaceVersion" + }, + "disable_environment": { + "DROIDDECK_MALI_COMPAT_DISABLE": "1" + } + } +} diff --git a/tools/venus/layer/bc.c b/tools/venus/layer/bc.c new file mode 100644 index 000000000..328b68169 --- /dev/null +++ b/tools/venus/layer/bc.c @@ -0,0 +1,869 @@ +/* + * BC1-BC7 textures on a GPU without them: each BC image is created in an uncompressed format, and + * every upload into one is decoded on the CPU while the copy is recorded. The decoded texels go + * through a staging buffer of the layer's own, which lives until the command buffer is reset. + * Uploads must come from memory the application has mapped, as DXVK's staging memory always is. + */ +#include "mali_compat.h" + +#include +#include + +#define BCDEC_IMPLEMENTATION +#define BCDEC_STATIC +#define BCDEC_BC4BC5_PRECISE +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#include "bcdec.h" +#pragma GCC diagnostic pop + +#define HANDLE(h) ((uint64_t)(uintptr_t)(h)) +#define STAGING_BLOCK_SIZE (16u << 20) +#define STAGING_KEPT (64u << 20) + +enum bc_kind { BC1, BC2, BC3, BC4, BC5, BC6H, BC7 }; + +struct bc_format { + VkFormat format; + VkFormat substitute; + enum bc_kind kind; + uint32_t block_bytes; + uint32_t texel_bytes; + bool is_signed; + /* BC1's RGB formats have no alpha; its 1-bit transparency must read as opaque. */ + bool opaque; +}; + +static const struct bc_format bc_formats[] = { + {VK_FORMAT_BC1_RGB_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, BC1, 8, 4, false, true}, + {VK_FORMAT_BC1_RGB_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, BC1, 8, 4, false, true}, + {VK_FORMAT_BC1_RGBA_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, BC1, 8, 4, false, false}, + {VK_FORMAT_BC1_RGBA_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, BC1, 8, 4, false, false}, + {VK_FORMAT_BC2_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, BC2, 16, 4, false, false}, + {VK_FORMAT_BC2_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, BC2, 16, 4, false, false}, + {VK_FORMAT_BC3_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, BC3, 16, 4, false, false}, + {VK_FORMAT_BC3_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, BC3, 16, 4, false, false}, + {VK_FORMAT_BC4_UNORM_BLOCK, VK_FORMAT_R8_UNORM, BC4, 8, 1, false, false}, + {VK_FORMAT_BC4_SNORM_BLOCK, VK_FORMAT_R8_SNORM, BC4, 8, 1, true, false}, + {VK_FORMAT_BC5_UNORM_BLOCK, VK_FORMAT_R8G8_UNORM, BC5, 16, 2, false, false}, + {VK_FORMAT_BC5_SNORM_BLOCK, VK_FORMAT_R8G8_SNORM, BC5, 16, 2, true, false}, + {VK_FORMAT_BC6H_UFLOAT_BLOCK, VK_FORMAT_R16G16B16A16_SFLOAT, BC6H, 16, 8, false, false}, + {VK_FORMAT_BC6H_SFLOAT_BLOCK, VK_FORMAT_R16G16B16A16_SFLOAT, BC6H, 16, 8, true, false}, + {VK_FORMAT_BC7_UNORM_BLOCK, VK_FORMAT_R8G8B8A8_UNORM, BC7, 16, 4, false, false}, + {VK_FORMAT_BC7_SRGB_BLOCK, VK_FORMAT_R8G8B8A8_SRGB, BC7, 16, 4, false, false}, +}; + +static const struct bc_format *find_bc(VkFormat format) +{ + for (size_t i = 0; i < sizeof(bc_formats) / sizeof(bc_formats[0]); i++) { + if (bc_formats[i].format == format) + return &bc_formats[i]; + } + return NULL; +} + +VkFormat bc_substitute(VkFormat format) +{ + const struct bc_format *bc = find_bc(format); + return bc ? bc->substitute : VK_FORMAT_UNDEFINED; +} + +/* What a real BC format allows: sampling and copies. */ +#define BC_FEATURES \ + (VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | \ + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | \ + VK_FORMAT_FEATURE_TRANSFER_DST_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT) +#define BC_USAGE (VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT) + +static const struct bc_format *physical_bc(VkPhysicalDevice physical_device, VkFormat format, struct instance **inst) +{ + *inst = instance_from_handle(physical_device); + if (!(physical_device_gaps(*inst, physical_device) & GAP_BC)) + return NULL; + return find_bc(format); +} + +VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceFormatProperties(VkPhysicalDevice physical_device, VkFormat format, + VkFormatProperties *properties) +{ + struct instance *inst; + const struct bc_format *bc = physical_bc(physical_device, format, &inst); + inst->GetPhysicalDeviceFormatProperties(physical_device, bc ? bc->substitute : format, properties); + if (bc) { + properties->optimalTilingFeatures &= BC_FEATURES; + properties->linearTilingFeatures = 0; + properties->bufferFeatures = 0; + } +} + +VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceFormatProperties2(VkPhysicalDevice physical_device, VkFormat format, + VkFormatProperties2 *properties) +{ + struct instance *inst; + const struct bc_format *bc = physical_bc(physical_device, format, &inst); + inst->GetPhysicalDeviceFormatProperties2(physical_device, bc ? bc->substitute : format, properties); + if (!bc) + return; + properties->formatProperties.optimalTilingFeatures &= BC_FEATURES; + properties->formatProperties.linearTilingFeatures = 0; + properties->formatProperties.bufferFeatures = 0; + for (VkBaseOutStructure *s = properties->pNext; s; s = s->pNext) { + if (s->sType == VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3) { + VkFormatProperties3 *p3 = (VkFormatProperties3 *)s; + p3->optimalTilingFeatures &= (VkFormatFeatureFlags2)BC_FEATURES; + p3->linearTilingFeatures = 0; + p3->bufferFeatures = 0; + } else if (s->sType == VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT) { + ((VkDrmFormatModifierPropertiesListEXT *)s)->drmFormatModifierCount = 0; + } else if (s->sType == VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_2_EXT) { + ((VkDrmFormatModifierPropertiesList2EXT *)s)->drmFormatModifierCount = 0; + } + } +} + +VKAPI_ATTR VkResult VKAPI_CALL GetPhysicalDeviceImageFormatProperties( + VkPhysicalDevice physical_device, VkFormat format, VkImageType type, VkImageTiling tiling, + VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties *properties) +{ + struct instance *inst; + const struct bc_format *bc = physical_bc(physical_device, format, &inst); + if (!bc) + return inst->GetPhysicalDeviceImageFormatProperties(physical_device, format, type, tiling, usage, flags, + properties); + if (tiling != VK_IMAGE_TILING_OPTIMAL || (usage & ~BC_USAGE)) + return VK_ERROR_FORMAT_NOT_SUPPORTED; + return inst->GetPhysicalDeviceImageFormatProperties(physical_device, bc->substitute, type, tiling, usage, + flags & ~VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT, + properties); +} + +/* A view format list naming BC formats, as one naming their substitutes. */ +struct format_list { + VkImageFormatListCreateInfo *list; + const VkFormat *original; + VkFormat formats[16]; +}; + +static void substitute_format_list(const void *chain, VkFormat substitute, struct format_list *saved) +{ + saved->list = NULL; + for (const VkBaseInStructure *s = chain; s; s = s->pNext) { + if (s->sType != VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO) + continue; + VkImageFormatListCreateInfo *list = (VkImageFormatListCreateInfo *)s; + uint32_t count = 0; + saved->formats[count++] = substitute; + for (uint32_t i = 0; i < list->viewFormatCount && count < 16; i++) { + VkFormat format = bc_substitute(list->pViewFormats[i]); + /* Uncompressed views of the compressed blocks have nothing to alias any more. */ + if (format == VK_FORMAT_UNDEFINED) + continue; + bool seen = false; + for (uint32_t k = 0; k < count; k++) + seen |= saved->formats[k] == format; + if (!seen) + saved->formats[count++] = format; + } + saved->list = list; + saved->original = list->pViewFormats; + list->pViewFormats = saved->formats; + list->viewFormatCount = count; + return; + } +} + +static void restore_format_list(struct format_list *saved, uint32_t original_count) +{ + if (saved->list) { + saved->list->pViewFormats = saved->original; + saved->list->viewFormatCount = original_count; + } +} + +static uint32_t format_list_count(const void *chain) +{ + for (const VkBaseInStructure *s = chain; s; s = s->pNext) { + if (s->sType == VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO) + return ((const VkImageFormatListCreateInfo *)s)->viewFormatCount; + } + return 0; +} + +VKAPI_ATTR VkResult VKAPI_CALL GetPhysicalDeviceImageFormatProperties2( + VkPhysicalDevice physical_device, const VkPhysicalDeviceImageFormatInfo2 *info, + VkImageFormatProperties2 *properties) +{ + struct instance *inst; + const struct bc_format *bc = physical_bc(physical_device, info->format, &inst); + if (!bc) + return inst->GetPhysicalDeviceImageFormatProperties2(physical_device, info, properties); + if (info->tiling != VK_IMAGE_TILING_OPTIMAL || (info->usage & ~BC_USAGE)) + return VK_ERROR_FORMAT_NOT_SUPPORTED; + VkPhysicalDeviceImageFormatInfo2 copy = *info; + copy.format = bc->substitute; + copy.flags &= ~VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT; + uint32_t list_count = format_list_count(info->pNext); + struct format_list saved; + substitute_format_list(info->pNext, bc->substitute, &saved); + VkResult result = inst->GetPhysicalDeviceImageFormatProperties2(physical_device, ©, properties); + restore_format_list(&saved, list_count); + return result; +} + +struct bc_image { + const struct bc_format *format; + uint32_t array_layers; +}; + +static void substitute_image_info(const struct bc_format *bc, VkImageCreateInfo *info) +{ + info->format = bc->substitute; + info->flags &= ~VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT; + info->usage &= BC_USAGE; +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateImage(VkDevice device, const VkImageCreateInfo *info, + const VkAllocationCallbacks *allocator, VkImage *image) +{ + struct device *dev = device_from_handle(device); + const struct bc_format *bc = find_bc(info->format); + if (!bc) + return dev->CreateImage(device, info, allocator, image); + + VkImageCreateInfo copy = *info; + substitute_image_info(bc, ©); + uint32_t list_count = format_list_count(info->pNext); + struct format_list saved; + substitute_format_list(info->pNext, bc->substitute, &saved); + VkResult result = dev->CreateImage(device, ©, allocator, image); + restore_format_list(&saved, list_count); + if (result != VK_SUCCESS) + return result; + + struct bc_image *tracked = malloc(sizeof(*tracked)); + tracked->format = bc; + tracked->array_layers = info->arrayLayers; + pthread_mutex_lock(&dev->lock); + map_put(&dev->images, HANDLE(*image), tracked); + pthread_mutex_unlock(&dev->lock); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL DestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks *allocator) +{ + struct device *dev = device_from_handle(device); + pthread_mutex_lock(&dev->lock); + free(map_remove(&dev->images, HANDLE(image))); + pthread_mutex_unlock(&dev->lock); + dev->DestroyImage(device, image, allocator); +} + +static const struct bc_image *tracked_image(struct device *dev, VkImage image) +{ + pthread_mutex_lock(&dev->lock); + const struct bc_image *tracked = map_get(&dev->images, HANDLE(image)); + pthread_mutex_unlock(&dev->lock); + return tracked; +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateImageView(VkDevice device, const VkImageViewCreateInfo *info, + const VkAllocationCallbacks *allocator, VkImageView *view) +{ + struct device *dev = device_from_handle(device); + const struct bc_image *tracked = tracked_image(dev, info->image); + if (!tracked) + return dev->CreateImageView(device, info, allocator, view); + VkImageViewCreateInfo copy = *info; + VkFormat format = bc_substitute(info->format); + copy.format = format != VK_FORMAT_UNDEFINED ? format : tracked->format->substitute; + return dev->CreateImageView(device, ©, allocator, view); +} + +VKAPI_ATTR void VKAPI_CALL GetDeviceImageMemoryRequirements(VkDevice device, + const VkDeviceImageMemoryRequirements *info, + VkMemoryRequirements2 *requirements) +{ + struct device *dev = device_from_handle(device); + const struct bc_format *bc = find_bc(info->pCreateInfo->format); + if (!bc) { + dev->GetDeviceImageMemoryRequirements(device, info, requirements); + return; + } + VkImageCreateInfo image = *info->pCreateInfo; + substitute_image_info(bc, &image); + VkDeviceImageMemoryRequirements copy = *info; + copy.pCreateInfo = ℑ + dev->GetDeviceImageMemoryRequirements(device, ©, requirements); +} + +VKAPI_ATTR void VKAPI_CALL GetDeviceImageSparseMemoryRequirements( + VkDevice device, const VkDeviceImageMemoryRequirements *info, uint32_t *count, + VkSparseImageMemoryRequirements2 *requirements) +{ + struct device *dev = device_from_handle(device); + const struct bc_format *bc = find_bc(info->pCreateInfo->format); + if (!bc) { + dev->GetDeviceImageSparseMemoryRequirements(device, info, count, requirements); + return; + } + VkImageCreateInfo image = *info->pCreateInfo; + substitute_image_info(bc, &image); + VkDeviceImageMemoryRequirements copy = *info; + copy.pCreateInfo = ℑ + dev->GetDeviceImageSparseMemoryRequirements(device, ©, count, requirements); +} + +VKAPI_ATTR void VKAPI_CALL GetDeviceImageSubresourceLayout(VkDevice device, + const VkDeviceImageSubresourceInfo *info, + VkSubresourceLayout2 *layout) +{ + struct device *dev = device_from_handle(device); + const struct bc_format *bc = find_bc(info->pCreateInfo->format); + if (!bc) { + dev->GetDeviceImageSubresourceLayout(device, info, layout); + return; + } + VkImageCreateInfo image = *info->pCreateInfo; + substitute_image_info(bc, &image); + VkDeviceImageSubresourceInfo copy = *info; + copy.pCreateInfo = ℑ + dev->GetDeviceImageSubresourceLayout(device, ©, layout); +} + +struct bound_buffer { + VkDeviceMemory memory; + VkDeviceSize offset; +}; + +static void track_binding(struct device *dev, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) +{ + struct bound_buffer *bound = malloc(sizeof(*bound)); + bound->memory = memory; + bound->offset = offset; + pthread_mutex_lock(&dev->lock); + free(map_remove(&dev->buffers, HANDLE(buffer))); + map_put(&dev->buffers, HANDLE(buffer), bound); + pthread_mutex_unlock(&dev->lock); +} + +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, + VkDeviceSize offset) +{ + struct device *dev = device_from_handle(device); + VkResult result = dev->BindBufferMemory(device, buffer, memory, offset); + if (result == VK_SUCCESS) + track_binding(dev, buffer, memory, offset); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory2(VkDevice device, uint32_t count, const VkBindBufferMemoryInfo *infos) +{ + struct device *dev = device_from_handle(device); + VkResult result = dev->BindBufferMemory2(device, count, infos); + if (result == VK_SUCCESS) { + for (uint32_t i = 0; i < count; i++) + track_binding(dev, infos[i].buffer, infos[i].memory, infos[i].memoryOffset); + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL DestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks *allocator) +{ + struct device *dev = device_from_handle(device); + pthread_mutex_lock(&dev->lock); + free(map_remove(&dev->buffers, HANDLE(buffer))); + pthread_mutex_unlock(&dev->lock); + dev->DestroyBuffer(device, buffer, allocator); +} + +/* The memory's start as the application sees it mapped. */ +static void track_mapping(struct device *dev, VkDeviceMemory memory, VkDeviceSize offset, void *data) +{ + pthread_mutex_lock(&dev->lock); + map_put(&dev->memories, HANDLE(memory), (uint8_t *)data - offset); + pthread_mutex_unlock(&dev->lock); +} + +static void forget_mapping(struct device *dev, VkDeviceMemory memory) +{ + pthread_mutex_lock(&dev->lock); + map_remove(&dev->memories, HANDLE(memory)); + pthread_mutex_unlock(&dev->lock); +} + +VKAPI_ATTR VkResult VKAPI_CALL MapMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, + VkDeviceSize size, VkMemoryMapFlags flags, void **data) +{ + struct device *dev = device_from_handle(device); + VkResult result = dev->MapMemory(device, memory, offset, size, flags, data); + if (result == VK_SUCCESS) + track_mapping(dev, memory, offset, *data); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL MapMemory2(VkDevice device, const VkMemoryMapInfo *info, void **data) +{ + struct device *dev = device_from_handle(device); + VkResult result = dev->MapMemory2(device, info, data); + if (result == VK_SUCCESS) + track_mapping(dev, info->memory, info->offset, *data); + return result; +} + +VKAPI_ATTR void VKAPI_CALL UnmapMemory(VkDevice device, VkDeviceMemory memory) +{ + struct device *dev = device_from_handle(device); + forget_mapping(dev, memory); + dev->UnmapMemory(device, memory); +} + +VKAPI_ATTR VkResult VKAPI_CALL UnmapMemory2(VkDevice device, const VkMemoryUnmapInfo *info) +{ + struct device *dev = device_from_handle(device); + forget_mapping(dev, info->memory); + return dev->UnmapMemory2(device, info); +} + +VKAPI_ATTR void VKAPI_CALL FreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks *allocator) +{ + struct device *dev = device_from_handle(device); + forget_mapping(dev, memory); + dev->FreeMemory(device, memory, allocator); +} + +struct staging_block { + VkBuffer buffer; + VkDeviceMemory memory; + uint8_t *data; + VkDeviceSize size; + VkDeviceSize used; + struct staging_block *next; +}; + +struct command_buffer { + VkCommandPool pool; + struct staging_block *staging; +}; + +static void destroy_block(struct device *dev, struct staging_block *block) +{ + dev->DestroyBuffer(dev->handle, block->buffer, NULL); + dev->FreeMemory(dev->handle, block->memory, NULL); + free(block); +} + +static struct staging_block *create_block(struct device *dev, VkDeviceSize size) +{ + struct staging_block *block = calloc(1, sizeof(*block)); + block->size = size; + VkBufferCreateInfo buffer_info = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = size, + .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }; + if (dev->CreateBuffer(dev->handle, &buffer_info, NULL, &block->buffer) != VK_SUCCESS) { + free(block); + return NULL; + } + VkMemoryRequirements requirements; + dev->GetBufferMemoryRequirements(dev->handle, block->buffer, &requirements); + const VkMemoryPropertyFlags wanted = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + uint32_t type = UINT32_MAX; + for (uint32_t i = 0; i < dev->memory_properties.memoryTypeCount && type == UINT32_MAX; i++) { + if ((requirements.memoryTypeBits & (1u << i)) && + (dev->memory_properties.memoryTypes[i].propertyFlags & wanted) == wanted) + type = i; + } + VkMemoryAllocateInfo allocate_info = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = requirements.size, + .memoryTypeIndex = type, + }; + void *data = NULL; + if (type == UINT32_MAX || dev->AllocateMemory(dev->handle, &allocate_info, NULL, &block->memory) != VK_SUCCESS) { + dev->DestroyBuffer(dev->handle, block->buffer, NULL); + free(block); + return NULL; + } + if (dev->BindBufferMemory(dev->handle, block->buffer, block->memory, 0) != VK_SUCCESS || + dev->MapMemory(dev->handle, block->memory, 0, VK_WHOLE_SIZE, 0, &data) != VK_SUCCESS) { + destroy_block(dev, block); + return NULL; + } + block->data = data; + return block; +} + +/* Called with the device lock held: the command buffer is done with its staging. */ +static void recycle_staging(struct device *dev, struct command_buffer *state) +{ + VkDeviceSize kept = 0; + for (struct staging_block *b = dev->free_staging; b; b = b->next) + kept += b->size; + while (state->staging) { + struct staging_block *block = state->staging; + state->staging = block->next; + if (kept + block->size > STAGING_KEPT) { + destroy_block(dev, block); + continue; + } + block->used = 0; + block->next = dev->free_staging; + dev->free_staging = block; + kept += block->size; + } +} + +/* Room for `size` bytes in a staging buffer that lives as long as the command buffer's recording. */ +static struct staging_block *staging_for(struct device *dev, VkCommandBuffer cmd, VkDeviceSize size, + VkDeviceSize *offset) +{ + pthread_mutex_lock(&dev->lock); + struct command_buffer *state = map_get(&dev->command_buffers, HANDLE(cmd)); + if (!state) { + state = calloc(1, sizeof(*state)); + map_put(&dev->command_buffers, HANDLE(cmd), state); + } + struct staging_block *block = state->staging; + VkDeviceSize start = block ? (block->used + 63) & ~(VkDeviceSize)63 : 0; + if (!block || start + size > block->size) { + struct staging_block **link = &dev->free_staging; + while (*link && (*link)->size < size) + link = &(*link)->next; + block = *link; + if (block) + *link = block->next; + else + block = create_block(dev, size > STAGING_BLOCK_SIZE ? size : STAGING_BLOCK_SIZE); + if (block) { + block->next = state->staging; + state->staging = block; + } + start = 0; + } + if (block) + block->used = start + size; + pthread_mutex_unlock(&dev->lock); + *offset = start; + return block; +} + +/* Where the application has the buffer's contents mapped, or NULL. */ +static const uint8_t *mapped_buffer(struct device *dev, VkBuffer buffer) +{ + pthread_mutex_lock(&dev->lock); + const struct bound_buffer *bound = map_get(&dev->buffers, HANDLE(buffer)); + const uint8_t *base = bound ? map_get(&dev->memories, HANDLE(bound->memory)) : NULL; + const uint8_t *data = base ? base + bound->offset : NULL; + pthread_mutex_unlock(&dev->lock); + return data; +} + +static void decode_block(const struct bc_format *bc, const uint8_t *block, uint8_t *texels) +{ + switch (bc->kind) { + case BC1: + bcdec_bc1(block, texels, 16); + if (bc->opaque) { + for (int i = 0; i < 16; i++) + texels[i * 4 + 3] = 0xff; + } + break; + case BC2: + bcdec_bc2(block, texels, 16); + break; + case BC3: + bcdec_bc3(block, texels, 16); + break; + case BC4: + bcdec_bc4(block, texels, 4, bc->is_signed); + break; + case BC5: + bcdec_bc5(block, texels, 8, bc->is_signed); + break; + case BC6H: { + uint16_t rgb[16 * 3]; + bcdec_bc6h_half(block, rgb, 12, bc->is_signed); + uint16_t *rgba = (uint16_t *)texels; + for (int i = 0; i < 16; i++) { + rgba[i * 4 + 0] = rgb[i * 3 + 0]; + rgba[i * 4 + 1] = rgb[i * 3 + 1]; + rgba[i * 4 + 2] = rgb[i * 3 + 2]; + rgba[i * 4 + 3] = 0x3c00; + } + break; + } + case BC7: + bcdec_bc7(block, texels, 16); + break; + } +} + +struct upload { + VkDeviceSize offset; + uint32_t row_length; + uint32_t image_height; + VkImageSubresourceLayers subresource; + VkOffset3D image_offset; + VkExtent3D extent; +}; + +static uint32_t warned; + +static void warn_once(uint32_t bit, const char *message) +{ + if (!(__atomic_fetch_or(&warned, bit, __ATOMIC_RELAXED) & bit)) + layer_log("%s", message); +} + +static void upload_region(struct device *dev, VkCommandBuffer cmd, const struct bc_image *image, + const uint8_t *source, VkImage dst, VkImageLayout layout, const struct upload *region) +{ + const struct bc_format *bc = image->format; + uint32_t width = region->extent.width, height = region->extent.height; + uint32_t layers = region->subresource.layerCount; + if (layers == VK_REMAINING_ARRAY_LAYERS) + layers = image->array_layers - region->subresource.baseArrayLayer; + uint32_t slices = region->extent.depth * layers; + uint32_t row_texels = region->row_length ? region->row_length : width; + uint32_t height_texels = region->image_height ? region->image_height : height; + size_t source_row = (size_t)((row_texels + 3) / 4) * bc->block_bytes; + size_t source_slice = (size_t)((height_texels + 3) / 4) * source_row; + size_t row_bytes = (size_t)width * bc->texel_bytes; + VkDeviceSize size = (VkDeviceSize)row_bytes * height * slices; + if (!size) + return; + + VkDeviceSize offset; + struct staging_block *block = staging_for(dev, cmd, size, &offset); + if (!block) { + warn_once(1u << 0, "out of staging memory for BC texture uploads; textures left empty"); + return; + } + uint8_t *out = block->data + offset; + const uint8_t *in = source + region->offset; + uint8_t texels[16 * 8]; + for (uint32_t s = 0; s < slices; s++) { + for (uint32_t by = 0; by < (height + 3) / 4; by++) { + for (uint32_t bx = 0; bx < (width + 3) / 4; bx++) { + decode_block(bc, in + s * source_slice + by * source_row + (size_t)bx * bc->block_bytes, texels); + for (uint32_t y = 0; y < 4 && by * 4 + y < height; y++) { + uint32_t columns = width - bx * 4 < 4 ? width - bx * 4 : 4; + memcpy(out + ((size_t)s * height + by * 4 + y) * row_bytes + (size_t)bx * 4 * bc->texel_bytes, + texels + (size_t)y * 4 * bc->texel_bytes, (size_t)columns * bc->texel_bytes); + } + } + } + } + + VkBufferImageCopy copy = { + .bufferOffset = offset, + .imageSubresource = region->subresource, + .imageOffset = region->image_offset, + .imageExtent = region->extent, + }; + copy.imageSubresource.layerCount = layers; + dev->CmdCopyBufferToImage(cmd, block->buffer, dst, layout, 1, ©); +} + +VKAPI_ATTR void VKAPI_CALL CmdCopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer, VkImage image, + VkImageLayout layout, uint32_t count, + const VkBufferImageCopy *regions) +{ + struct device *dev = device_from_handle(cmd); + const struct bc_image *tracked = tracked_image(dev, image); + if (!tracked) { + dev->CmdCopyBufferToImage(cmd, buffer, image, layout, count, regions); + return; + } + const uint8_t *source = mapped_buffer(dev, buffer); + if (!source) { + warn_once(1u << 1, "BC texture upload from memory that is not mapped; texture left empty"); + return; + } + for (uint32_t i = 0; i < count; i++) { + struct upload region = {regions[i].bufferOffset, regions[i].bufferRowLength, regions[i].bufferImageHeight, + regions[i].imageSubresource, regions[i].imageOffset, regions[i].imageExtent}; + upload_region(dev, cmd, tracked, source, image, layout, ®ion); + } +} + +VKAPI_ATTR void VKAPI_CALL CmdCopyBufferToImage2(VkCommandBuffer cmd, const VkCopyBufferToImageInfo2 *info) +{ + struct device *dev = device_from_handle(cmd); + const struct bc_image *tracked = tracked_image(dev, info->dstImage); + if (!tracked) { + dev->CmdCopyBufferToImage2(cmd, info); + return; + } + const uint8_t *source = mapped_buffer(dev, info->srcBuffer); + if (!source) { + warn_once(1u << 1, "BC texture upload from memory that is not mapped; texture left empty"); + return; + } + for (uint32_t i = 0; i < info->regionCount; i++) { + const VkBufferImageCopy2 *r = &info->pRegions[i]; + struct upload region = {r->bufferOffset, r->bufferRowLength, r->bufferImageHeight, + r->imageSubresource, r->imageOffset, r->imageExtent}; + upload_region(dev, cmd, tracked, source, info->dstImage, info->dstImageLayout, ®ion); + } +} + +/* Reading compressed blocks back would hand out decoded texels the buffer has no room for. */ +VKAPI_ATTR void VKAPI_CALL CmdCopyImageToBuffer(VkCommandBuffer cmd, VkImage image, VkImageLayout layout, + VkBuffer buffer, uint32_t count, const VkBufferImageCopy *regions) +{ + struct device *dev = device_from_handle(cmd); + if (tracked_image(dev, image)) { + warn_once(1u << 2, "BC texture read back into a buffer; skipped"); + return; + } + dev->CmdCopyImageToBuffer(cmd, image, layout, buffer, count, regions); +} + +VKAPI_ATTR void VKAPI_CALL CmdCopyImageToBuffer2(VkCommandBuffer cmd, const VkCopyImageToBufferInfo2 *info) +{ + struct device *dev = device_from_handle(cmd); + if (tracked_image(dev, info->srcImage)) { + warn_once(1u << 2, "BC texture read back into a buffer; skipped"); + return; + } + dev->CmdCopyImageToBuffer2(cmd, info); +} + +/* Between two BC images the texels line up; between BC and anything else they cannot. */ +VKAPI_ATTR void VKAPI_CALL CmdCopyImage(VkCommandBuffer cmd, VkImage src, VkImageLayout src_layout, VkImage dst, + VkImageLayout dst_layout, uint32_t count, const VkImageCopy *regions) +{ + struct device *dev = device_from_handle(cmd); + if (!tracked_image(dev, src) != !tracked_image(dev, dst)) { + warn_once(1u << 3, "copy between a BC texture and an uncompressed one; skipped"); + return; + } + dev->CmdCopyImage(cmd, src, src_layout, dst, dst_layout, count, regions); +} + +VKAPI_ATTR void VKAPI_CALL CmdCopyImage2(VkCommandBuffer cmd, const VkCopyImageInfo2 *info) +{ + struct device *dev = device_from_handle(cmd); + if (!tracked_image(dev, info->srcImage) != !tracked_image(dev, info->dstImage)) { + warn_once(1u << 3, "copy between a BC texture and an uncompressed one; skipped"); + return; + } + dev->CmdCopyImage2(cmd, info); +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateCommandBuffers(VkDevice device, const VkCommandBufferAllocateInfo *info, + VkCommandBuffer *buffers) +{ + struct device *dev = device_from_handle(device); + VkResult result = dev->AllocateCommandBuffers(device, info, buffers); + if (result != VK_SUCCESS) + return result; + pthread_mutex_lock(&dev->lock); + for (uint32_t i = 0; i < info->commandBufferCount; i++) { + struct command_buffer *state = calloc(1, sizeof(*state)); + state->pool = info->commandPool; + map_put(&dev->command_buffers, HANDLE(buffers[i]), state); + } + pthread_mutex_unlock(&dev->lock); + return result; +} + +VKAPI_ATTR void VKAPI_CALL FreeCommandBuffers(VkDevice device, VkCommandPool pool, uint32_t count, + const VkCommandBuffer *buffers) +{ + struct device *dev = device_from_handle(device); + pthread_mutex_lock(&dev->lock); + for (uint32_t i = 0; i < count; i++) { + struct command_buffer *state = buffers[i] ? map_remove(&dev->command_buffers, HANDLE(buffers[i])) : NULL; + if (state) { + recycle_staging(dev, state); + free(state); + } + } + pthread_mutex_unlock(&dev->lock); + dev->FreeCommandBuffers(device, pool, count, buffers); +} + +static void recycle_command_buffer(struct device *dev, VkCommandBuffer cmd) +{ + pthread_mutex_lock(&dev->lock); + struct command_buffer *state = map_get(&dev->command_buffers, HANDLE(cmd)); + if (state) + recycle_staging(dev, state); + pthread_mutex_unlock(&dev->lock); +} + +VKAPI_ATTR VkResult VKAPI_CALL BeginCommandBuffer(VkCommandBuffer cmd, const VkCommandBufferBeginInfo *info) +{ + struct device *dev = device_from_handle(cmd); + recycle_command_buffer(dev, cmd); + return dev->BeginCommandBuffer(cmd, info); +} + +VKAPI_ATTR VkResult VKAPI_CALL ResetCommandBuffer(VkCommandBuffer cmd, VkCommandBufferResetFlags flags) +{ + struct device *dev = device_from_handle(cmd); + recycle_command_buffer(dev, cmd); + return dev->ResetCommandBuffer(cmd, flags); +} + +/* Every command buffer of the pool is done with its staging; `forget` drops them as well. */ +static void recycle_pool(struct device *dev, VkCommandPool pool, bool forget) +{ + pthread_mutex_lock(&dev->lock); + struct handle_map *map = &dev->command_buffers; + for (uint32_t i = 0; i < map->capacity;) { + struct command_buffer *state = map->values[i]; + if (!map->keys[i] || state->pool != pool) { + i++; + continue; + } + recycle_staging(dev, state); + if (forget) { + /* Removal shifts a later entry into this slot, so look at it again. */ + free(map_remove(map, map->keys[i])); + continue; + } + i++; + } + pthread_mutex_unlock(&dev->lock); +} + +VKAPI_ATTR VkResult VKAPI_CALL ResetCommandPool(VkDevice device, VkCommandPool pool, VkCommandPoolResetFlags flags) +{ + struct device *dev = device_from_handle(device); + recycle_pool(dev, pool, false); + return dev->ResetCommandPool(device, pool, flags); +} + +VKAPI_ATTR void VKAPI_CALL DestroyCommandPool(VkDevice device, VkCommandPool pool, + const VkAllocationCallbacks *allocator) +{ + struct device *dev = device_from_handle(device); + recycle_pool(dev, pool, true); + dev->DestroyCommandPool(device, pool, allocator); +} + +void bc_destroy_device(struct device *dev) +{ + struct handle_map *maps[] = {&dev->images, &dev->buffers, &dev->command_buffers}; + for (size_t m = 0; m < sizeof(maps) / sizeof(maps[0]); m++) { + for (uint32_t i = 0; i < maps[m]->capacity; i++) { + if (!maps[m]->keys[i]) + continue; + if (maps[m] == &dev->command_buffers) + recycle_staging(dev, maps[m]->values[i]); + free(maps[m]->values[i]); + } + map_free(maps[m]); + } + map_free(&dev->memories); + while (dev->free_staging) { + struct staging_block *block = dev->free_staging; + dev->free_staging = block->next; + destroy_block(dev, block); + } +} diff --git a/tools/venus/layer/draw.c b/tools/venus/layer/draw.c new file mode 100644 index 000000000..f3621355b --- /dev/null +++ b/tools/venus/layer/draw.c @@ -0,0 +1,452 @@ +/* + * The Mali compatibility layer's draw and descriptor emulation (added to David Cimo's layer): + * + * - multiDrawIndirect: an indirect draw with drawCount > 1 is recorded as drawCount single draws + * at offset + i * stride. Mali-G57 (Bifrost/Valhall v9) has none, and DXVK refuses the device. + * - robustness2's nullDescriptor: VK_NULL_HANDLE buffers, image views and vertex buffers in + * descriptor writes (plain, template and push) and vertex-buffer binds are replaced with small + * stand-ins the layer owns. robustBufferAccess2 itself maps to robustBufferAccess (layer.c). + * A null texel-buffer view gets an RGBA8 (uniform) or R32_UINT (storage) view of the stand-in + * buffer: its format is not known here, but Zink leaves them unbound and never reads them. + */ +#include +#include + +#include "mali_compat.h" + +/* --- multiDrawIndirect --- */ + +VKAPI_ATTR void VKAPI_CALL CmdDrawIndirect(VkCommandBuffer cmd, VkBuffer buffer, VkDeviceSize offset, + uint32_t draw_count, uint32_t stride) +{ + struct device *dev = device_from_handle(cmd); + if (draw_count <= 1) { + dev->CmdDrawIndirect(cmd, buffer, offset, draw_count, stride); + return; + } + for (uint32_t i = 0; i < draw_count; i++) + dev->CmdDrawIndirect(cmd, buffer, offset + (VkDeviceSize)i * stride, 1, stride); +} + +VKAPI_ATTR void VKAPI_CALL CmdDrawIndexedIndirect(VkCommandBuffer cmd, VkBuffer buffer, VkDeviceSize offset, + uint32_t draw_count, uint32_t stride) +{ + struct device *dev = device_from_handle(cmd); + if (draw_count <= 1) { + dev->CmdDrawIndexedIndirect(cmd, buffer, offset, draw_count, stride); + return; + } + for (uint32_t i = 0; i < draw_count; i++) + dev->CmdDrawIndexedIndirect(cmd, buffer, offset + (VkDeviceSize)i * stride, 1, stride); +} + +/* --- nullDescriptor stand-ins --- */ + +/* A template's entries, to find the null handles in the data it is given. */ +struct template_info { + uint32_t count; + VkDescriptorUpdateTemplateEntry *entries; + size_t size; /* bytes the data spans */ +}; + +#define NULL_BUFFER_SIZE 65536 + +static uint32_t memory_type(const struct device *dev, uint32_t bits, VkMemoryPropertyFlags want) +{ + for (uint32_t i = 0; i < dev->memory_properties.memoryTypeCount; i++) + if ((bits & (1u << i)) && (dev->memory_properties.memoryTypes[i].propertyFlags & want) == want) + return i; + for (uint32_t i = 0; i < dev->memory_properties.memoryTypeCount; i++) + if (bits & (1u << i)) + return i; + return 0; +} + +/* Made once per device, under its lock, the first time a null descriptor is seen. */ +static bool null_resources(struct device *dev) +{ + if (dev->null_ready) + return dev->null_ready > 0; + dev->null_ready = -1; + + VkBufferCreateInfo binfo = { + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .size = NULL_BUFFER_SIZE, + .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | + VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + }; + if (dev->CreateBuffer(dev->handle, &binfo, NULL, &dev->null_buffer) != VK_SUCCESS) + return false; + VkMemoryRequirements breq; + dev->GetBufferMemoryRequirements(dev->handle, dev->null_buffer, &breq); + VkMemoryAllocateInfo balloc = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = breq.size, + .memoryTypeIndex = memory_type(dev, breq.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT), + }; + if (dev->AllocateMemory(dev->handle, &balloc, NULL, &dev->null_buffer_memory) != VK_SUCCESS) + return false; + dev->BindBufferMemory(dev->handle, dev->null_buffer, dev->null_buffer_memory, 0); + /* A null buffer reads as zeros. */ + void *data = NULL; + if ((dev->memory_properties.memoryTypes[balloc.memoryTypeIndex].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) && + dev->MapMemory(dev->handle, dev->null_buffer_memory, 0, VK_WHOLE_SIZE, 0, &data) == VK_SUCCESS) { + memset(data, 0, (size_t)breq.size); + dev->UnmapMemory(dev->handle, dev->null_buffer_memory); + } + + VkImageCreateInfo iinfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .extent = {1, 1, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + if (dev->CreateImage(dev->handle, &iinfo, NULL, &dev->null_image) != VK_SUCCESS) + return false; + VkMemoryRequirements ireq; + dev->GetImageMemoryRequirements(dev->handle, dev->null_image, &ireq); + VkMemoryAllocateInfo ialloc = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = ireq.size, + .memoryTypeIndex = memory_type(dev, ireq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT), + }; + if (dev->AllocateMemory(dev->handle, &ialloc, NULL, &dev->null_image_memory) != VK_SUCCESS) + return false; + dev->BindImageMemory(dev->handle, dev->null_image, dev->null_image_memory, 0); + VkImageViewCreateInfo vinfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = dev->null_image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, + }; + if (dev->CreateImageView(dev->handle, &vinfo, NULL, &dev->null_view) != VK_SUCCESS) + return false; + /* Texel views are optional: a write that needs one and has none keeps its null. */ + VkBufferViewCreateInfo tinfo = { + .sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO, + .buffer = dev->null_buffer, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .range = VK_WHOLE_SIZE, + }; + if (dev->CreateBufferView(dev->handle, &tinfo, NULL, &dev->null_uniform_texel) != VK_SUCCESS) + dev->null_uniform_texel = VK_NULL_HANDLE; + tinfo.format = VK_FORMAT_R32_UINT; + if (dev->CreateBufferView(dev->handle, &tinfo, NULL, &dev->null_storage_texel) != VK_SUCCESS) + dev->null_storage_texel = VK_NULL_HANDLE; + dev->null_ready = 1; + layer_log("nullDescriptor: stand-in buffer and 1x1 image ready"); + return true; +} + +void null_resources_destroy(struct device *dev) +{ + for (uint32_t i = 0; i < dev->templates.capacity; i++) { + struct template_info *t = dev->templates.values ? dev->templates.values[i] : NULL; + if (t) { + free(t->entries); + free(t); + } + } + map_free(&dev->templates); + if (dev->null_uniform_texel) dev->DestroyBufferView(dev->handle, dev->null_uniform_texel, NULL); + if (dev->null_storage_texel) dev->DestroyBufferView(dev->handle, dev->null_storage_texel, NULL); + if (dev->null_view) dev->DestroyImageView(dev->handle, dev->null_view, NULL); + if (dev->null_image) dev->DestroyImage(dev->handle, dev->null_image, NULL); + if (dev->null_image_memory) dev->FreeMemory(dev->handle, dev->null_image_memory, NULL); + if (dev->null_buffer) dev->DestroyBuffer(dev->handle, dev->null_buffer, NULL); + if (dev->null_buffer_memory) dev->FreeMemory(dev->handle, dev->null_buffer_memory, NULL); +} + +static bool is_buffer_type(VkDescriptorType t) +{ + return t == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER || t == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || + t == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC || t == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC; +} + +static bool is_image_type(VkDescriptorType t) +{ + return t == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || t == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE || + t == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER || t == VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT; +} + +static bool is_texel_type(VkDescriptorType t) +{ + return t == VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER || t == VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; +} + +static void fix_texel(struct device *dev, VkDescriptorType t, VkBufferView *view) +{ + if (*view != VK_NULL_HANDLE || !null_resources(dev)) + return; + *view = t == VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER ? dev->null_storage_texel : dev->null_uniform_texel; +} + +static void fix_buffer(struct device *dev, VkDescriptorBufferInfo *info) +{ + if (info->buffer != VK_NULL_HANDLE || !null_resources(dev)) + return; + info->buffer = dev->null_buffer; + info->offset = 0; + info->range = VK_WHOLE_SIZE; +} + +static void fix_image(struct device *dev, VkDescriptorImageInfo *info) +{ + if (info->imageView != VK_NULL_HANDLE || !null_resources(dev)) + return; + info->imageView = dev->null_view; +} + +/* Copies of the writes with stand-ins for null handles; NULL when nothing needed changing. */ +static VkWriteDescriptorSet *fix_writes(struct device *dev, uint32_t count, const VkWriteDescriptorSet *writes, + void ***scratch, uint32_t *scratch_count) +{ + VkWriteDescriptorSet *copies = NULL; + for (uint32_t i = 0; i < count; i++) { + const VkWriteDescriptorSet *w = &writes[i]; + bool null_found = false; + for (uint32_t j = 0; j < w->descriptorCount && !null_found; j++) { + if (is_buffer_type(w->descriptorType) && w->pBufferInfo && w->pBufferInfo[j].buffer == VK_NULL_HANDLE) + null_found = true; + if (is_image_type(w->descriptorType) && w->pImageInfo && w->pImageInfo[j].imageView == VK_NULL_HANDLE && + w->descriptorType != VK_DESCRIPTOR_TYPE_SAMPLER) + null_found = true; + if (is_texel_type(w->descriptorType) && w->pTexelBufferView && w->pTexelBufferView[j] == VK_NULL_HANDLE) + null_found = true; + } + if (!null_found) + continue; + if (!copies) { + copies = malloc(count * sizeof(*copies)); + memcpy(copies, writes, count * sizeof(*copies)); + *scratch = calloc(count, sizeof(void *)); + } + if (is_buffer_type(w->descriptorType)) { + VkDescriptorBufferInfo *b = malloc(w->descriptorCount * sizeof(*b)); + memcpy(b, w->pBufferInfo, w->descriptorCount * sizeof(*b)); + for (uint32_t j = 0; j < w->descriptorCount; j++) + fix_buffer(dev, &b[j]); + copies[i].pBufferInfo = b; + (*scratch)[(*scratch_count)++] = b; + } else if (is_texel_type(w->descriptorType)) { + VkBufferView *v = malloc(w->descriptorCount * sizeof(*v)); + memcpy(v, w->pTexelBufferView, w->descriptorCount * sizeof(*v)); + for (uint32_t j = 0; j < w->descriptorCount; j++) + fix_texel(dev, w->descriptorType, &v[j]); + copies[i].pTexelBufferView = v; + (*scratch)[(*scratch_count)++] = v; + } else { + VkDescriptorImageInfo *im = malloc(w->descriptorCount * sizeof(*im)); + memcpy(im, w->pImageInfo, w->descriptorCount * sizeof(*im)); + for (uint32_t j = 0; j < w->descriptorCount; j++) + fix_image(dev, &im[j]); + copies[i].pImageInfo = im; + (*scratch)[(*scratch_count)++] = im; + } + } + return copies; +} + +static void free_scratch(void **scratch, uint32_t count) +{ + for (uint32_t i = 0; i < count; i++) + free(scratch[i]); + free(scratch); +} + +VKAPI_ATTR void VKAPI_CALL UpdateDescriptorSets(VkDevice device, uint32_t write_count, const VkWriteDescriptorSet *writes, + uint32_t copy_count, const VkCopyDescriptorSet *copies) +{ + struct device *dev = device_from_handle(device); + pthread_mutex_lock(&dev->lock); + void **scratch = NULL; + uint32_t scratch_count = 0; + VkWriteDescriptorSet *fixed = fix_writes(dev, write_count, writes, &scratch, &scratch_count); + pthread_mutex_unlock(&dev->lock); + dev->UpdateDescriptorSets(device, write_count, fixed ? fixed : writes, copy_count, copies); + free(fixed); + free_scratch(scratch, scratch_count); +} + +VKAPI_ATTR void VKAPI_CALL CmdPushDescriptorSet(VkCommandBuffer cmd, VkPipelineBindPoint bind_point, + VkPipelineLayout layout, uint32_t set, uint32_t write_count, + const VkWriteDescriptorSet *writes) +{ + struct device *dev = device_from_handle(cmd); + pthread_mutex_lock(&dev->lock); + void **scratch = NULL; + uint32_t scratch_count = 0; + VkWriteDescriptorSet *fixed = fix_writes(dev, write_count, writes, &scratch, &scratch_count); + pthread_mutex_unlock(&dev->lock); + dev->CmdPushDescriptorSet(cmd, bind_point, layout, set, write_count, fixed ? fixed : writes); + free(fixed); + free_scratch(scratch, scratch_count); +} + +static size_t entry_size(VkDescriptorType t) +{ + if (is_buffer_type(t)) + return sizeof(VkDescriptorBufferInfo); + if (is_image_type(t) || t == VK_DESCRIPTOR_TYPE_SAMPLER) + return sizeof(VkDescriptorImageInfo); + if (t == VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK) + return 1; + return sizeof(uint64_t); +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateDescriptorUpdateTemplate(VkDevice device, + const VkDescriptorUpdateTemplateCreateInfo *info, + const VkAllocationCallbacks *allocator, + VkDescriptorUpdateTemplate *tmpl) +{ + struct device *dev = device_from_handle(device); + VkResult result = dev->CreateDescriptorUpdateTemplate(device, info, allocator, tmpl); + if (result != VK_SUCCESS) + return result; + struct template_info *t = calloc(1, sizeof(*t)); + t->count = info->descriptorUpdateEntryCount; + t->entries = malloc(t->count * sizeof(*t->entries)); + memcpy(t->entries, info->pDescriptorUpdateEntries, t->count * sizeof(*t->entries)); + for (uint32_t i = 0; i < t->count; i++) { + const VkDescriptorUpdateTemplateEntry *e = &t->entries[i]; + size_t end = e->descriptorType == VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK + ? e->offset + e->descriptorCount + : e->offset + (e->descriptorCount ? (e->descriptorCount - 1) * e->stride : 0) + entry_size(e->descriptorType); + if (end > t->size) + t->size = end; + } + pthread_mutex_lock(&dev->lock); + map_put(&dev->templates, (uint64_t)*tmpl, t); + pthread_mutex_unlock(&dev->lock); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL DestroyDescriptorUpdateTemplate(VkDevice device, VkDescriptorUpdateTemplate tmpl, + const VkAllocationCallbacks *allocator) +{ + struct device *dev = device_from_handle(device); + pthread_mutex_lock(&dev->lock); + struct template_info *t = map_remove(&dev->templates, (uint64_t)tmpl); + pthread_mutex_unlock(&dev->lock); + if (t) { + free(t->entries); + free(t); + } + dev->DestroyDescriptorUpdateTemplate(device, tmpl, allocator); +} + +/* A copy of the template's data with stand-ins for null handles; NULL when nothing changed. */ +static void *fix_template_data(struct device *dev, VkDescriptorUpdateTemplate tmpl, const void *data) +{ + pthread_mutex_lock(&dev->lock); + struct template_info *t = map_get(&dev->templates, (uint64_t)tmpl); + void *copy = NULL; + if (t && data) { + for (uint32_t i = 0; i < t->count; i++) { + const VkDescriptorUpdateTemplateEntry *e = &t->entries[i]; + bool buffer = is_buffer_type(e->descriptorType); + bool image = is_image_type(e->descriptorType); + bool texel = is_texel_type(e->descriptorType); + if (!buffer && !image && !texel) + continue; + for (uint32_t j = 0; j < e->descriptorCount; j++) { + size_t at = e->offset + (size_t)j * e->stride; + const char *src = (const char *)(copy ? copy : data) + at; + bool null_handle = buffer ? ((const VkDescriptorBufferInfo *)src)->buffer == VK_NULL_HANDLE + : texel ? *(const VkBufferView *)src == VK_NULL_HANDLE + : ((const VkDescriptorImageInfo *)src)->imageView == VK_NULL_HANDLE; + if (!null_handle) + continue; + if (!copy) { + copy = malloc(t->size); + memcpy(copy, data, t->size); + } + if (buffer) + fix_buffer(dev, (VkDescriptorBufferInfo *)((char *)copy + at)); + else if (texel) + fix_texel(dev, e->descriptorType, (VkBufferView *)((char *)copy + at)); + else + fix_image(dev, (VkDescriptorImageInfo *)((char *)copy + at)); + } + } + } + pthread_mutex_unlock(&dev->lock); + return copy; +} + +VKAPI_ATTR void VKAPI_CALL UpdateDescriptorSetWithTemplate(VkDevice device, VkDescriptorSet set, + VkDescriptorUpdateTemplate tmpl, const void *data) +{ + struct device *dev = device_from_handle(device); + void *fixed = fix_template_data(dev, tmpl, data); + dev->UpdateDescriptorSetWithTemplate(device, set, tmpl, fixed ? fixed : data); + free(fixed); +} + +VKAPI_ATTR void VKAPI_CALL CmdPushDescriptorSetWithTemplate(VkCommandBuffer cmd, VkDescriptorUpdateTemplate tmpl, + VkPipelineLayout layout, uint32_t set, const void *data) +{ + struct device *dev = device_from_handle(cmd); + void *fixed = fix_template_data(dev, tmpl, data); + dev->CmdPushDescriptorSetWithTemplate(cmd, tmpl, layout, set, fixed ? fixed : data); + free(fixed); +} + +/* Null vertex buffers read as zeros: bound to the stand-in at offset 0. */ +VKAPI_ATTR void VKAPI_CALL CmdBindVertexBuffers(VkCommandBuffer cmd, uint32_t first, uint32_t count, + const VkBuffer *buffers, const VkDeviceSize *offsets) +{ + struct device *dev = device_from_handle(cmd); + VkBuffer fixed[64]; + VkDeviceSize fixed_offsets[64]; + bool any = false; + for (uint32_t i = 0; i < count && i < 64; i++) any |= buffers[i] == VK_NULL_HANDLE; + if (!any || count > 64) { + dev->CmdBindVertexBuffers(cmd, first, count, buffers, offsets); + return; + } + pthread_mutex_lock(&dev->lock); + bool ready = null_resources(dev); + pthread_mutex_unlock(&dev->lock); + for (uint32_t i = 0; i < count; i++) { + fixed[i] = buffers[i] == VK_NULL_HANDLE && ready ? dev->null_buffer : buffers[i]; + fixed_offsets[i] = buffers[i] == VK_NULL_HANDLE ? 0 : offsets[i]; + } + dev->CmdBindVertexBuffers(cmd, first, count, fixed, fixed_offsets); +} + +VKAPI_ATTR void VKAPI_CALL CmdBindVertexBuffers2(VkCommandBuffer cmd, uint32_t first, uint32_t count, + const VkBuffer *buffers, const VkDeviceSize *offsets, + const VkDeviceSize *sizes, const VkDeviceSize *strides) +{ + struct device *dev = device_from_handle(cmd); + VkBuffer fixed[64]; + VkDeviceSize fixed_offsets[64], fixed_sizes[64]; + bool any = false; + for (uint32_t i = 0; i < count && i < 64; i++) any |= buffers[i] == VK_NULL_HANDLE; + if (!any || count > 64) { + dev->CmdBindVertexBuffers2(cmd, first, count, buffers, offsets, sizes, strides); + return; + } + pthread_mutex_lock(&dev->lock); + bool ready = null_resources(dev); + pthread_mutex_unlock(&dev->lock); + for (uint32_t i = 0; i < count; i++) { + bool null_buffer = buffers[i] == VK_NULL_HANDLE; + fixed[i] = null_buffer && ready ? dev->null_buffer : buffers[i]; + fixed_offsets[i] = null_buffer ? 0 : offsets[i]; + fixed_sizes[i] = sizes ? (null_buffer ? NULL_BUFFER_SIZE : sizes[i]) : 0; + } + dev->CmdBindVertexBuffers2(cmd, first, count, fixed, fixed_offsets, sizes ? fixed_sizes : NULL, strides); +} diff --git a/tools/venus/layer/layer.c b/tools/venus/layer/layer.c new file mode 100644 index 000000000..4df8a54ad --- /dev/null +++ b/tools/venus/layer/layer.c @@ -0,0 +1,812 @@ +#define _GNU_SOURCE +#include "mali_compat.h" + +#include +#include +#include +#include +#include + +#define LAYER_NAME "VK_LAYER_DROIDDECK_mali_compat" + +static pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER; +static struct instance *instances; +static struct device *devices; + +void layer_log(const char *format, ...) +{ + char line[512]; + va_list args; + va_start(args, format); + vsnprintf(line, sizeof(line), format, args); + va_end(args); + fprintf(stderr, "droiddeck-mali-compat: %s\n", line); +} + +/* The loader's dispatch table pointer: shared by an instance and its physical devices, and by a + * device and its queues and command buffers. */ +static void *dispatch_key(const void *dispatchable) +{ + return *(void *const *)dispatchable; +} + +struct instance *instance_from_handle(const void *dispatchable) +{ + void *key = dispatch_key(dispatchable); + pthread_mutex_lock(&global_lock); + struct instance *inst = instances; + while (inst && inst->key != key) + inst = inst->next; + pthread_mutex_unlock(&global_lock); + return inst; +} + +struct device *device_from_handle(const void *dispatchable) +{ + void *key = dispatch_key(dispatchable); + pthread_mutex_lock(&global_lock); + struct device *dev = devices; + while (dev && dev->key != key) + dev = dev->next; + pthread_mutex_unlock(&global_lock); + return dev; +} + +static uint64_t hash(uint64_t key) +{ + key ^= key >> 33; + key *= 0xff51afd7ed558ccdull; + key ^= key >> 33; + return key; +} + +void *map_get(const struct handle_map *map, uint64_t key) +{ + if (!map->capacity) + return NULL; + for (uint32_t i = hash(key) & (map->capacity - 1);; i = (i + 1) & (map->capacity - 1)) { + if (map->keys[i] == key) + return map->values[i]; + if (!map->keys[i]) + return NULL; + } +} + +static void map_grow(struct handle_map *map) +{ + struct handle_map bigger = { + .capacity = map->capacity ? map->capacity * 2 : 64, + }; + bigger.keys = calloc(bigger.capacity, sizeof(*bigger.keys)); + bigger.values = calloc(bigger.capacity, sizeof(*bigger.values)); + for (uint32_t i = 0; i < map->capacity; i++) { + if (map->keys[i]) + map_put(&bigger, map->keys[i], map->values[i]); + } + free(map->keys); + free(map->values); + *map = bigger; +} + +void map_put(struct handle_map *map, uint64_t key, void *value) +{ + if ((map->count + 1) * 4 > map->capacity * 3) + map_grow(map); + uint32_t i = hash(key) & (map->capacity - 1); + while (map->keys[i] && map->keys[i] != key) + i = (i + 1) & (map->capacity - 1); + if (!map->keys[i]) + map->count++; + map->keys[i] = key; + map->values[i] = value; +} + +void *map_remove(struct handle_map *map, uint64_t key) +{ + if (!map->capacity) + return NULL; + uint32_t mask = map->capacity - 1; + uint32_t i = hash(key) & mask; + while (map->keys[i] != key) { + if (!map->keys[i]) + return NULL; + i = (i + 1) & mask; + } + void *value = map->values[i]; + map->count--; + /* Backward-shift deletion keeps every remaining key reachable from its home slot. */ + for (uint32_t j = (i + 1) & mask; map->keys[j]; j = (j + 1) & mask) { + uint32_t home = hash(map->keys[j]) & mask; + if (((j - home) & mask) >= ((j - i) & mask)) { + map->keys[i] = map->keys[j]; + map->values[i] = map->values[j]; + i = j; + } + } + map->keys[i] = 0; + map->values[i] = NULL; + return value; +} + +void map_free(struct handle_map *map) +{ + free(map->keys); + free(map->values); + *map = (struct handle_map){0}; +} + +/* Wine processes by default. DROIDDECK_MALI_COMPAT=1 (the app's Venus route) turns it on everywhere + * but gamescope, so Zink in the Steam client and Xwayland gets robustness2's nullDescriptor, which it + * requires; gamescope runs fine on the driver's own features. =all includes gamescope too. */ +static bool wanted(void) +{ + const char *setting = getenv("DROIDDECK_MALI_COMPAT"); + if (setting) { + if (strcmp(setting, "0") == 0) + return false; + if (strcmp(setting, "all") == 0) + return true; + return strcmp(program_invocation_short_name, "gamescope") != 0; + } + return getenv("WINEPREFIX") != NULL; +} + +/* DROIDDECK_MALI_COMPAT_TRACE=1: what an application asks before it creates a device (Zink on a + * Mali-G615 through Venus gave up before vkCreateDevice without a word). */ +static bool tracing(void) +{ + static int on = -1; + if (on < 0) { + const char *v = getenv("DROIDDECK_MALI_COMPAT_TRACE"); + on = v && *v && strcmp(v, "0") != 0; + } + return on; +} + +static bool has_extension(struct instance *inst, VkPhysicalDevice physical_device, const char *name) +{ + uint32_t count = 0; + inst->EnumerateDeviceExtensionProperties(physical_device, NULL, &count, NULL); + VkExtensionProperties *extensions = calloc(count, sizeof(*extensions)); + inst->EnumerateDeviceExtensionProperties(physical_device, NULL, &count, extensions); + bool found = false; + for (uint32_t i = 0; i < count && !found; i++) + found = strcmp(extensions[i].extensionName, name) == 0; + free(extensions); + return found; +} + +uint32_t physical_device_gaps(struct instance *inst, VkPhysicalDevice physical_device) +{ + pthread_mutex_lock(&inst->lock); + for (uint32_t i = 0; i < inst->physical_device_count; i++) { + if (inst->physical_devices[i].handle == physical_device) { + uint32_t gaps = inst->physical_devices[i].gaps; + pthread_mutex_unlock(&inst->lock); + return gaps; + } + } + pthread_mutex_unlock(&inst->lock); + + bool robustness2 = has_extension(inst, physical_device, "VK_EXT_robustness2"); + VkPhysicalDeviceRobustness2FeaturesEXT robust = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, + }; + VkPhysicalDeviceFeatures2 features = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, + .pNext = robustness2 ? &robust : NULL, + }; + inst->GetPhysicalDeviceFeatures2(physical_device, &features); + uint32_t gaps = 0; + if (!features.features.fillModeNonSolid) + gaps |= GAP_FILL_MODE; + if (!features.features.multiViewport) + gaps |= GAP_MULTI_VIEWPORT; + if (!features.features.shaderClipDistance) + gaps |= GAP_CLIP_DISTANCE; + if (!features.features.shaderCullDistance) + gaps |= GAP_CULL_DISTANCE; + if (!features.features.textureCompressionBC) + gaps |= GAP_BC; + if (robustness2 && !robust.robustBufferAccess2) + gaps |= GAP_ROBUSTNESS2; + if (!features.features.multiDrawIndirect) + gaps |= GAP_MULTI_DRAW; + if (!has_extension(inst, physical_device, VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME)) + gaps |= GAP_DEPTH_CLIP; + /* No robustness2 at all: the extension is offered with robustBufferAccess2 (as robustBufferAccess) + * and nullDescriptor (stand-ins, draw.c). */ + if (!robustness2) + gaps |= GAP_ROBUSTNESS2 | GAP_NULL_DESCRIPTOR; + + pthread_mutex_lock(&inst->lock); + if (inst->physical_device_count < sizeof(inst->physical_devices) / sizeof(inst->physical_devices[0])) { + inst->physical_devices[inst->physical_device_count++] = + (struct physical_device){.handle = physical_device, .gaps = gaps}; + } + pthread_mutex_unlock(&inst->lock); + return gaps; +} + +/* Structures of extensions the driver does not have are taken out of a chain for the call down and + * put back after it: the driver (and Venus) must not see them. */ +struct unlinked { + VkBaseOutStructure *prev, *node; +}; + +static uint32_t unlink_types(void *head, const VkStructureType *types, uint32_t type_count, + struct unlinked *out, uint32_t max) +{ + uint32_t n = 0; + VkBaseOutStructure *prev = head; + while (prev && prev->pNext && n < max) { + VkBaseOutStructure *s = prev->pNext; + bool drop = false; + for (uint32_t i = 0; i < type_count; i++) + drop |= s->sType == types[i]; + if (drop) { + out[n++] = (struct unlinked){prev, s}; + prev->pNext = s->pNext; + } else { + prev = s; + } + } + return n; +} + +static void relink(struct unlinked *list, uint32_t n) +{ + while (n--) + list[n].prev->pNext = list[n].node; +} + +static uint32_t foreign_types(uint32_t gaps, VkStructureType *types) +{ + uint32_t n = 0; + if (gaps & GAP_DEPTH_CLIP) + types[n++] = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_ENABLE_FEATURES_EXT; + if (gaps & GAP_NULL_DESCRIPTOR) + types[n++] = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT; + return n; +} + +static void report_features(VkPhysicalDeviceFeatures *features, uint32_t gaps) +{ + if (gaps & GAP_FILL_MODE) + features->fillModeNonSolid = VK_TRUE; + if (gaps & GAP_MULTI_VIEWPORT) + features->multiViewport = VK_TRUE; + if (gaps & GAP_CLIP_DISTANCE) + features->shaderClipDistance = VK_TRUE; + if (gaps & GAP_CULL_DISTANCE) + features->shaderCullDistance = VK_TRUE; + if (gaps & GAP_BC) + features->textureCompressionBC = VK_TRUE; + if (gaps & GAP_MULTI_DRAW) + features->multiDrawIndirect = VK_TRUE; +} + +/* Which made-up features the application asked for, and off with them before the driver sees them. */ +static uint32_t take_features(VkPhysicalDeviceFeatures *features, uint32_t gaps) +{ + uint32_t requested = 0; + if ((gaps & GAP_FILL_MODE) && features->fillModeNonSolid) { + requested |= GAP_FILL_MODE; + features->fillModeNonSolid = VK_FALSE; + } + if ((gaps & GAP_MULTI_VIEWPORT) && features->multiViewport) { + requested |= GAP_MULTI_VIEWPORT; + features->multiViewport = VK_FALSE; + } + if ((gaps & GAP_CLIP_DISTANCE) && features->shaderClipDistance) { + requested |= GAP_CLIP_DISTANCE; + features->shaderClipDistance = VK_FALSE; + } + if ((gaps & GAP_CULL_DISTANCE) && features->shaderCullDistance) { + requested |= GAP_CULL_DISTANCE; + features->shaderCullDistance = VK_FALSE; + } + if ((gaps & GAP_BC) && features->textureCompressionBC) { + requested |= GAP_BC; + features->textureCompressionBC = VK_FALSE; + } + if ((gaps & GAP_MULTI_DRAW) && features->multiDrawIndirect) { + requested |= GAP_MULTI_DRAW; + features->multiDrawIndirect = VK_FALSE; + } + return requested; +} + +static void report_limits(VkPhysicalDeviceLimits *limits, uint32_t gaps) +{ + if (gaps & GAP_MULTI_VIEWPORT) + limits->maxViewports = 16; + if ((gaps & GAP_MULTI_DRAW) && limits->maxDrawIndirectCount < (1u << 30)) + limits->maxDrawIndirectCount = 1u << 30; + if (gaps & GAPS_SHADER) { + limits->maxClipDistances = 8; + limits->maxCullDistances = 8; + limits->maxCombinedClipAndCullDistances = 8; + } +} + +static VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceFeatures(VkPhysicalDevice physical_device, + VkPhysicalDeviceFeatures *features) +{ + struct instance *inst = instance_from_handle(physical_device); + inst->GetPhysicalDeviceFeatures(physical_device, features); + report_features(features, physical_device_gaps(inst, physical_device)); +} + +static VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceFeatures2(VkPhysicalDevice physical_device, + VkPhysicalDeviceFeatures2 *features) +{ + struct instance *inst = instance_from_handle(physical_device); + uint32_t gaps = physical_device_gaps(inst, physical_device); + VkStructureType types[4]; + struct unlinked dropped[8]; + uint32_t dropped_count = unlink_types(features, types, foreign_types(gaps, types), dropped, 8); + inst->GetPhysicalDeviceFeatures2(physical_device, features); + relink(dropped, dropped_count); + report_features(&features->features, gaps); + if (tracing()) { + char chain[512] = ""; + size_t used = 0; + for (VkBaseOutStructure *s = features->pNext; s && used + 16 < sizeof(chain); s = s->pNext) + used += (size_t)snprintf(chain + used, sizeof(chain) - used, " %d", (int)s->sType); + layer_log("trace: vkGetPhysicalDeviceFeatures2 (gaps 0x%x, %u structures held back), chain:%s", gaps, dropped_count, chain); + } + for (VkBaseOutStructure *s = features->pNext; s; s = s->pNext) { + if (s->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT && (gaps & GAP_ROBUSTNESS2)) { + ((VkPhysicalDeviceRobustness2FeaturesEXT *)s)->robustBufferAccess2 = VK_TRUE; + if (gaps & GAP_NULL_DESCRIPTOR) + ((VkPhysicalDeviceRobustness2FeaturesEXT *)s)->nullDescriptor = VK_TRUE; + } + if (s->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DEPTH_CLIP_ENABLE_FEATURES_EXT && (gaps & GAP_DEPTH_CLIP)) + ((VkPhysicalDeviceDepthClipEnableFeaturesEXT *)s)->depthClipEnable = VK_TRUE; + } +} + +static VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceProperties(VkPhysicalDevice physical_device, + VkPhysicalDeviceProperties *properties) +{ + struct instance *inst = instance_from_handle(physical_device); + inst->GetPhysicalDeviceProperties(physical_device, properties); + report_limits(&properties->limits, physical_device_gaps(inst, physical_device)); +} + +static VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceProperties2(VkPhysicalDevice physical_device, + VkPhysicalDeviceProperties2 *properties) +{ + struct instance *inst = instance_from_handle(physical_device); + inst->GetPhysicalDeviceProperties2(physical_device, properties); + report_limits(&properties->properties.limits, physical_device_gaps(inst, physical_device)); + if (tracing()) + layer_log("trace: vkGetPhysicalDeviceProperties2: %s, Vulkan %u.%u.%u, type %d", properties->properties.deviceName, + VK_API_VERSION_MAJOR(properties->properties.apiVersion), VK_API_VERSION_MINOR(properties->properties.apiVersion), + VK_API_VERSION_PATCH(properties->properties.apiVersion), (int)properties->properties.deviceType); +} + +/* The extensions the layer stands in for, added to the driver's list. */ +static VKAPI_ATTR VkResult VKAPI_CALL EnumerateDeviceExtensionProperties(VkPhysicalDevice physical_device, + const char *layer_name, uint32_t *count, + VkExtensionProperties *properties) +{ + struct instance *inst = instance_from_handle(physical_device); + if (layer_name) + return inst->EnumerateDeviceExtensionProperties(physical_device, layer_name, count, properties); + uint32_t gaps = physical_device_gaps(inst, physical_device); + VkExtensionProperties extra[2]; + uint32_t extra_count = 0; + if (gaps & GAP_DEPTH_CLIP) + extra[extra_count++] = (VkExtensionProperties){VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME, 1}; + if (gaps & GAP_NULL_DESCRIPTOR) + extra[extra_count++] = (VkExtensionProperties){VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, 1}; + uint32_t own = 0; + VkResult result = inst->EnumerateDeviceExtensionProperties(physical_device, NULL, &own, NULL); + if (result != VK_SUCCESS) + return result; + if (!properties) { + *count = own + extra_count; + return VK_SUCCESS; + } + VkExtensionProperties *all = calloc(own + extra_count, sizeof(*all)); + inst->EnumerateDeviceExtensionProperties(physical_device, NULL, &own, all); + memcpy(all + own, extra, extra_count * sizeof(*extra)); + uint32_t total = own + extra_count; + if (tracing()) { + layer_log("trace: vkEnumerateDeviceExtensionProperties: %u from the driver, %u added by the layer", own, extra_count); + trace_extension_names(all, total); + } + uint32_t n = *count < total ? *count : total; + memcpy(properties, all, n * sizeof(*all)); + free(all); + *count = n; + return n < total ? VK_INCOMPLETE : VK_SUCCESS; +} + +static VKAPI_ATTR VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *info, + const VkAllocationCallbacks *allocator, VkInstance *instance) +{ + VkLayerInstanceCreateInfo *link = (VkLayerInstanceCreateInfo *)info->pNext; + while (link && !(link->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO && link->function == VK_LAYER_LINK_INFO)) + link = (VkLayerInstanceCreateInfo *)link->pNext; + if (!link) + return VK_ERROR_INITIALIZATION_FAILED; + PFN_vkGetInstanceProcAddr gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr; + link->u.pLayerInfo = link->u.pLayerInfo->pNext; + + PFN_vkCreateInstance create = (PFN_vkCreateInstance)gipa(VK_NULL_HANDLE, "vkCreateInstance"); + VkResult result = create(info, allocator, instance); + if (tracing()) + layer_log("trace: vkCreateInstance for %s (API %u.%u): %d, layer %s", info->pApplicationInfo && info->pApplicationInfo->pApplicationName ? + info->pApplicationInfo->pApplicationName : "?", info->pApplicationInfo ? VK_API_VERSION_MAJOR(info->pApplicationInfo->apiVersion) : 0, + info->pApplicationInfo ? VK_API_VERSION_MINOR(info->pApplicationInfo->apiVersion) : 0, (int)result, wanted() ? "active" : "idle"); + if (result != VK_SUCCESS) + return result; + + struct instance *inst = calloc(1, sizeof(*inst)); + inst->key = dispatch_key(*instance); + inst->handle = *instance; + inst->active = wanted(); + inst->gipa = gipa; + pthread_mutex_init(&inst->lock, NULL); +#define LOAD(name) inst->name = (PFN_vk##name)gipa(*instance, "vk" #name); +#define LOAD_KHR(name) \ + if (!inst->name) \ + inst->name = (PFN_vk##name)gipa(*instance, "vk" #name "KHR"); + LOAD(DestroyInstance) + LOAD(EnumerateDeviceExtensionProperties) + LOAD(GetPhysicalDeviceFeatures) + LOAD(GetPhysicalDeviceFeatures2) + LOAD_KHR(GetPhysicalDeviceFeatures2) + LOAD(GetPhysicalDeviceProperties) + LOAD(GetPhysicalDeviceProperties2) + LOAD_KHR(GetPhysicalDeviceProperties2) + LOAD(GetPhysicalDeviceMemoryProperties) + LOAD(GetPhysicalDeviceFormatProperties) + LOAD(GetPhysicalDeviceFormatProperties2) + LOAD_KHR(GetPhysicalDeviceFormatProperties2) + LOAD(GetPhysicalDeviceImageFormatProperties) + LOAD(GetPhysicalDeviceImageFormatProperties2) + LOAD_KHR(GetPhysicalDeviceImageFormatProperties2) +#undef LOAD +#undef LOAD_KHR + if (tracing()) + trace_load(inst); + /* Without the Vulkan 1.1 queries there is no way to answer the chained ones. */ + if (!inst->GetPhysicalDeviceFeatures2 || !inst->GetPhysicalDeviceProperties2 || + !inst->GetPhysicalDeviceFormatProperties2 || !inst->GetPhysicalDeviceImageFormatProperties2) + inst->active = false; + + pthread_mutex_lock(&global_lock); + inst->next = instances; + instances = inst; + pthread_mutex_unlock(&global_lock); + return VK_SUCCESS; +} + +static VKAPI_ATTR void VKAPI_CALL DestroyInstance(VkInstance instance, const VkAllocationCallbacks *allocator) +{ + void *key = dispatch_key(instance); + pthread_mutex_lock(&global_lock); + struct instance **link = &instances; + while (*link && (*link)->key != key) + link = &(*link)->next; + struct instance *inst = *link; + if (inst) + *link = inst->next; + pthread_mutex_unlock(&global_lock); + if (!inst) + return; + inst->DestroyInstance(instance, allocator); + pthread_mutex_destroy(&inst->lock); + free(inst); +} + +static VKAPI_ATTR VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice physical_device, const VkDeviceCreateInfo *info, + const VkAllocationCallbacks *allocator, VkDevice *device) +{ + VkLayerDeviceCreateInfo *link = (VkLayerDeviceCreateInfo *)info->pNext; + while (link && !(link->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && link->function == VK_LAYER_LINK_INFO)) + link = (VkLayerDeviceCreateInfo *)link->pNext; + if (!link) + return VK_ERROR_INITIALIZATION_FAILED; + PFN_vkGetInstanceProcAddr gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr; + PFN_vkGetDeviceProcAddr gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr; + link->u.pLayerInfo = link->u.pLayerInfo->pNext; + + struct instance *inst = instance_from_handle(physical_device); + PFN_vkCreateDevice create = (PFN_vkCreateDevice)gipa(inst->handle, "vkCreateDevice"); + uint32_t gaps = inst->active ? physical_device_gaps(inst, physical_device) : 0; + + /* The application's own structures are put back after the call: DXVK reads its enabled + * features again later, and must keep seeing what it asked for. */ + VkDeviceCreateInfo copy = *info; + VkPhysicalDeviceFeatures features; + VkPhysicalDeviceFeatures2 *features2 = NULL; + VkPhysicalDeviceRobustness2FeaturesEXT *robust = NULL; + for (VkBaseOutStructure *s = (VkBaseOutStructure *)info->pNext; s; s = s->pNext) { + if (s->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2) + features2 = (VkPhysicalDeviceFeatures2 *)s; + else if (s->sType == VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT) + robust = (VkPhysicalDeviceRobustness2FeaturesEXT *)s; + } + VkPhysicalDeviceFeatures *enabled = NULL; + VkPhysicalDeviceFeatures saved_features = {0}; + if (info->pEnabledFeatures) { + features = *info->pEnabledFeatures; + copy.pEnabledFeatures = &features; + enabled = &features; + } else if (features2) { + saved_features = features2->features; + enabled = &features2->features; + } + uint32_t requested = enabled ? take_features(enabled, gaps) : 0; + VkBool32 saved_robust = robust ? robust->robustBufferAccess2 : VK_FALSE; + if (robust && (gaps & GAP_ROBUSTNESS2) && robust->robustBufferAccess2) { + requested |= GAP_ROBUSTNESS2; + robust->robustBufferAccess2 = VK_FALSE; + /* The first version of robust buffer access is the nearest the driver has. */ + if (enabled) + enabled->robustBufferAccess = VK_TRUE; + } + + /* The extensions the layer stands in for leave the list, and their structures the chain. */ + const char **names = NULL; + if (gaps & (GAP_DEPTH_CLIP | GAP_NULL_DESCRIPTOR)) { + names = malloc((info->enabledExtensionCount ? info->enabledExtensionCount : 1) * sizeof(*names)); + copy.enabledExtensionCount = 0; + for (uint32_t i = 0; i < info->enabledExtensionCount; i++) { + const char *name = info->ppEnabledExtensionNames[i]; + if ((gaps & GAP_DEPTH_CLIP) && !strcmp(name, VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME)) { + requested |= GAP_DEPTH_CLIP; + continue; + } + if ((gaps & GAP_NULL_DESCRIPTOR) && !strcmp(name, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) { + requested |= GAP_NULL_DESCRIPTOR; + if (enabled) + enabled->robustBufferAccess = VK_TRUE; + continue; + } + names[copy.enabledExtensionCount++] = name; + } + copy.ppEnabledExtensionNames = names; + } + VkStructureType foreign[4]; + struct unlinked dropped[8]; + uint32_t dropped_count = unlink_types(©, foreign, foreign_types(gaps, foreign), dropped, 8); + + VkResult result = create(physical_device, ©, allocator, device); + + relink(dropped, dropped_count); + free(names); + if (features2 && !info->pEnabledFeatures) + features2->features = saved_features; + if (robust) + robust->robustBufferAccess2 = saved_robust; + if (result != VK_SUCCESS) + return result; + + struct device *dev = calloc(1, sizeof(*dev)); + dev->key = dispatch_key(*device); + dev->handle = *device; + dev->gdpa = gdpa; + dev->emulated = requested; + pthread_mutex_init(&dev->lock, NULL); +#define LOAD(name) \ + dev->name = (PFN_vk##name)gdpa(*device, "vk" #name); \ + if (!dev->name) \ + dev->name = (PFN_vk##name)gdpa(*device, "vk" #name "KHR"); \ + if (!dev->name) \ + dev->name = (PFN_vk##name)gdpa(*device, "vk" #name "EXT"); + DEVICE_FUNCTIONS(LOAD) +#undef LOAD + inst->GetPhysicalDeviceMemoryProperties(physical_device, &dev->memory_properties); + VkPhysicalDeviceProperties properties; + inst->GetPhysicalDeviceProperties(physical_device, &properties); + uint32_t components = properties.limits.maxVertexOutputComponents; + if (properties.limits.maxFragmentInputComponents < components) + components = properties.limits.maxFragmentInputComponents; + dev->max_varying_locations = components / 4; + + pthread_mutex_lock(&global_lock); + dev->next = devices; + devices = dev; + pthread_mutex_unlock(&global_lock); + + if (requested) { + layer_log("emulating%s%s%s%s%s%s%s%s%s for %s", (requested & GAP_FILL_MODE) ? " fillModeNonSolid" : "", + (requested & GAP_MULTI_VIEWPORT) ? " multiViewport" : "", + (requested & GAP_CLIP_DISTANCE) ? " shaderClipDistance" : "", + (requested & GAP_CULL_DISTANCE) ? " shaderCullDistance" : "", + (requested & GAP_ROBUSTNESS2) ? " robustBufferAccess2" : "", + (requested & GAP_BC) ? " textureCompressionBC" : "", + (requested & GAP_MULTI_DRAW) ? " multiDrawIndirect" : "", + (requested & GAP_DEPTH_CLIP) ? " depthClipEnable" : "", + (requested & GAP_NULL_DESCRIPTOR) ? " nullDescriptor" : "", properties.deviceName); + } + return VK_SUCCESS; +} + +static VKAPI_ATTR void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallbacks *allocator) +{ + void *key = dispatch_key(device); + pthread_mutex_lock(&global_lock); + struct device **link = &devices; + while (*link && (*link)->key != key) + link = &(*link)->next; + struct device *dev = *link; + if (dev) + *link = dev->next; + pthread_mutex_unlock(&global_lock); + if (!dev) + return; + bc_destroy_device(dev); + null_resources_destroy(dev); + dev->DestroyDevice(device, allocator); + for (uint32_t i = 0; i < dev->shader_modules.capacity; i++) { + struct spirv_code *code = dev->shader_modules.values[i]; + if (code) { + free(code->words); + free(code); + } + } + map_free(&dev->shader_modules); + pthread_mutex_destroy(&dev->lock); + free(dev); +} + +struct hook { + const char *name; + PFN_vkVoidFunction function; + uint32_t gaps; +}; + +#define HOOK(name, gaps) {"vk" #name, (PFN_vkVoidFunction)name, gaps} +#define HOOK_ALIAS(name, alias, gaps) {"vk" #alias, (PFN_vkVoidFunction)name, gaps} + +static const struct hook device_hooks[] = { + HOOK(CreateShaderModule, GAPS_SHADER), + HOOK(DestroyShaderModule, GAPS_SHADER), + HOOK(CreateGraphicsPipelines, GAPS_PIPELINE), + HOOK(CmdSetViewport, GAP_MULTI_VIEWPORT), + HOOK(CmdSetScissor, GAP_MULTI_VIEWPORT), + HOOK(CmdSetViewportWithCount, GAP_MULTI_VIEWPORT), + HOOK_ALIAS(CmdSetViewportWithCount, CmdSetViewportWithCountEXT, GAP_MULTI_VIEWPORT), + HOOK(CmdSetScissorWithCount, GAP_MULTI_VIEWPORT), + HOOK_ALIAS(CmdSetScissorWithCount, CmdSetScissorWithCountEXT, GAP_MULTI_VIEWPORT), + HOOK(CreateImage, GAP_BC), + HOOK(DestroyImage, GAP_BC), + HOOK(CreateImageView, GAP_BC), + HOOK(GetDeviceImageMemoryRequirements, GAP_BC), + HOOK_ALIAS(GetDeviceImageMemoryRequirements, GetDeviceImageMemoryRequirementsKHR, GAP_BC), + HOOK(GetDeviceImageSparseMemoryRequirements, GAP_BC), + HOOK_ALIAS(GetDeviceImageSparseMemoryRequirements, GetDeviceImageSparseMemoryRequirementsKHR, GAP_BC), + HOOK(GetDeviceImageSubresourceLayout, GAP_BC), + HOOK_ALIAS(GetDeviceImageSubresourceLayout, GetDeviceImageSubresourceLayoutKHR, GAP_BC), + HOOK(CmdCopyBufferToImage, GAP_BC), + HOOK(CmdCopyBufferToImage2, GAP_BC), + HOOK_ALIAS(CmdCopyBufferToImage2, CmdCopyBufferToImage2KHR, GAP_BC), + HOOK(CmdCopyImageToBuffer, GAP_BC), + HOOK(CmdCopyImageToBuffer2, GAP_BC), + HOOK_ALIAS(CmdCopyImageToBuffer2, CmdCopyImageToBuffer2KHR, GAP_BC), + HOOK(CmdCopyImage, GAP_BC), + HOOK(CmdCopyImage2, GAP_BC), + HOOK_ALIAS(CmdCopyImage2, CmdCopyImage2KHR, GAP_BC), + HOOK(BindBufferMemory, GAP_BC), + HOOK(BindBufferMemory2, GAP_BC), + HOOK_ALIAS(BindBufferMemory2, BindBufferMemory2KHR, GAP_BC), + HOOK(DestroyBuffer, GAP_BC), + HOOK(FreeMemory, GAP_BC), + HOOK(MapMemory, GAP_BC), + HOOK(MapMemory2, GAP_BC), + HOOK_ALIAS(MapMemory2, MapMemory2KHR, GAP_BC), + HOOK(UnmapMemory, GAP_BC), + HOOK(UnmapMemory2, GAP_BC), + HOOK_ALIAS(UnmapMemory2, UnmapMemory2KHR, GAP_BC), + HOOK(AllocateCommandBuffers, GAP_BC), + HOOK(FreeCommandBuffers, GAP_BC), + HOOK(BeginCommandBuffer, GAP_BC), + HOOK(ResetCommandBuffer, GAP_BC), + HOOK(ResetCommandPool, GAP_BC), + HOOK(DestroyCommandPool, GAP_BC), + HOOK(CmdDrawIndirect, GAP_MULTI_DRAW), + HOOK(CmdDrawIndexedIndirect, GAP_MULTI_DRAW), + HOOK(UpdateDescriptorSets, GAP_NULL_DESCRIPTOR), + HOOK(CmdPushDescriptorSet, GAP_NULL_DESCRIPTOR), + HOOK_ALIAS(CmdPushDescriptorSet, CmdPushDescriptorSetKHR, GAP_NULL_DESCRIPTOR), + HOOK(CreateDescriptorUpdateTemplate, GAP_NULL_DESCRIPTOR), + HOOK_ALIAS(CreateDescriptorUpdateTemplate, CreateDescriptorUpdateTemplateKHR, GAP_NULL_DESCRIPTOR), + HOOK(DestroyDescriptorUpdateTemplate, GAP_NULL_DESCRIPTOR), + HOOK_ALIAS(DestroyDescriptorUpdateTemplate, DestroyDescriptorUpdateTemplateKHR, GAP_NULL_DESCRIPTOR), + HOOK(UpdateDescriptorSetWithTemplate, GAP_NULL_DESCRIPTOR), + HOOK_ALIAS(UpdateDescriptorSetWithTemplate, UpdateDescriptorSetWithTemplateKHR, GAP_NULL_DESCRIPTOR), + HOOK(CmdPushDescriptorSetWithTemplate, GAP_NULL_DESCRIPTOR), + HOOK_ALIAS(CmdPushDescriptorSetWithTemplate, CmdPushDescriptorSetWithTemplateKHR, GAP_NULL_DESCRIPTOR), + HOOK(CmdBindVertexBuffers, GAP_NULL_DESCRIPTOR), + HOOK(CmdBindVertexBuffers2, GAP_NULL_DESCRIPTOR), + HOOK_ALIAS(CmdBindVertexBuffers2, CmdBindVertexBuffers2EXT, GAP_NULL_DESCRIPTOR), +}; + +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL GetDeviceProcAddr(VkDevice device, const char *name); + +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL GetDeviceProcAddr(VkDevice device, const char *name) +{ + if (!strcmp(name, "vkGetDeviceProcAddr")) + return (PFN_vkVoidFunction)GetDeviceProcAddr; + struct device *dev = device_from_handle(device); + if (!dev) + return NULL; + if (!strcmp(name, "vkDestroyDevice")) + return (PFN_vkVoidFunction)DestroyDevice; + PFN_vkVoidFunction next = dev->gdpa(device, name); + if (!next) + return NULL; + for (size_t i = 0; i < sizeof(device_hooks) / sizeof(device_hooks[0]); i++) { + if ((dev->emulated & device_hooks[i].gaps) && !strcmp(name, device_hooks[i].name)) + return device_hooks[i].function; + } + return next; +} + +static const struct hook physical_device_hooks[] = { + HOOK(EnumerateDeviceExtensionProperties, 0), + HOOK(GetPhysicalDeviceFeatures, 0), + HOOK(GetPhysicalDeviceFeatures2, 0), + HOOK_ALIAS(GetPhysicalDeviceFeatures2, GetPhysicalDeviceFeatures2KHR, 0), + HOOK(GetPhysicalDeviceProperties, 0), + HOOK(GetPhysicalDeviceProperties2, 0), + HOOK_ALIAS(GetPhysicalDeviceProperties2, GetPhysicalDeviceProperties2KHR, 0), + HOOK(GetPhysicalDeviceFormatProperties, 0), + HOOK(GetPhysicalDeviceFormatProperties2, 0), + HOOK_ALIAS(GetPhysicalDeviceFormatProperties2, GetPhysicalDeviceFormatProperties2KHR, 0), + HOOK(GetPhysicalDeviceImageFormatProperties, 0), + HOOK(GetPhysicalDeviceImageFormatProperties2, 0), + HOOK_ALIAS(GetPhysicalDeviceImageFormatProperties2, GetPhysicalDeviceImageFormatProperties2KHR, 0), +}; + +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL GetInstanceProcAddr(VkInstance instance, const char *name) +{ + if (!strcmp(name, "vkGetInstanceProcAddr")) + return (PFN_vkVoidFunction)GetInstanceProcAddr; + if (!strcmp(name, "vkCreateInstance")) + return (PFN_vkVoidFunction)CreateInstance; + if (!strcmp(name, "vkGetDeviceProcAddr")) + return (PFN_vkVoidFunction)GetDeviceProcAddr; + if (!instance) + return NULL; + struct instance *inst = instance_from_handle(instance); + if (!inst) + return NULL; + if (!strcmp(name, "vkDestroyInstance")) + return (PFN_vkVoidFunction)DestroyInstance; + /* Always ours, even when inactive: each layer moves the device chain one link on. */ + if (!strcmp(name, "vkCreateDevice")) + return (PFN_vkVoidFunction)CreateDevice; + PFN_vkVoidFunction next = inst->gipa(instance, name); + if (next && tracing()) { + PFN_vkVoidFunction traced = trace_hook(inst, name); + if (traced) + return traced; + } + if (!next || !inst->active) + return next; + for (size_t i = 0; i < sizeof(physical_device_hooks) / sizeof(physical_device_hooks[0]); i++) { + if (!strcmp(name, physical_device_hooks[i].name)) + return physical_device_hooks[i].function; + } + return next; +} + +__attribute__((visibility("default"))) VKAPI_ATTR VkResult VKAPI_CALL +vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface *interface) +{ + if (interface->loaderLayerInterfaceVersion < 2) + return VK_ERROR_INITIALIZATION_FAILED; + interface->loaderLayerInterfaceVersion = 2; + interface->pfnGetInstanceProcAddr = GetInstanceProcAddr; + interface->pfnGetDeviceProcAddr = GetDeviceProcAddr; + interface->pfnGetPhysicalDeviceProcAddr = NULL; + return VK_SUCCESS; +} diff --git a/tools/venus/layer/mali_compat.h b/tools/venus/layer/mali_compat.h new file mode 100644 index 000000000..055034ab6 --- /dev/null +++ b/tools/venus/layer/mali_compat.h @@ -0,0 +1,301 @@ +/* + * VK_LAYER_DROIDDECK_mali_compat: makes up for what Mali lacks and DXVK requires. + * + * DXVK refuses any GPU without fillModeNonSolid, multiViewport, shaderClipDistance, + * shaderCullDistance, textureCompressionBC and robustBufferAccess2, and Mali has none of them. + * The layer reports each missing feature as supported, keeps it out of the driver's device, and + * handles what uses it: polygon modes draw filled, only the first viewport is kept, clip distances + * become a varying the fragment shader discards on, cull distances are dropped, and BC textures + * are decoded on the CPU into uncompressed images as they are uploaded. + */ +#ifndef MALI_COMPAT_H +#define MALI_COMPAT_H + +#include +#include +#include +#include +#include +#include + +enum gap { + GAP_FILL_MODE = 1u << 0, + GAP_MULTI_VIEWPORT = 1u << 1, + GAP_CLIP_DISTANCE = 1u << 2, + GAP_CULL_DISTANCE = 1u << 3, + GAP_ROBUSTNESS2 = 1u << 4, + GAP_BC = 1u << 5, + /* draw.c and pipeline.c (added for Mali-G57 and drivers without these, on top of David Cimo's six) */ + GAP_MULTI_DRAW = 1u << 6, /* multiDrawIndirect: indirect draws looped one by one */ + GAP_DEPTH_CLIP = 1u << 7, /* VK_EXT_depth_clip_enable: depthClipEnable off becomes depth clamp */ + GAP_NULL_DESCRIPTOR = 1u << 8, /* VK_EXT_robustness2 missing entirely: nullDescriptor stand-ins */ +}; + +#define GAPS_SHADER (GAP_CLIP_DISTANCE | GAP_CULL_DISTANCE) +#define GAPS_PIPELINE (GAP_FILL_MODE | GAP_MULTI_VIEWPORT | GAPS_SHADER | GAP_DEPTH_CLIP) + +void layer_log(const char *format, ...) __attribute__((format(printf, 1, 2))); + +/* Open addressing from a Vulkan handle to a pointer. Not locked: the owner holds its lock. */ +struct handle_map { + uint64_t *keys; + void **values; + uint32_t capacity; + uint32_t count; +}; + +void *map_get(const struct handle_map *map, uint64_t key); +void map_put(struct handle_map *map, uint64_t key, void *value); +void *map_remove(struct handle_map *map, uint64_t key); +void map_free(struct handle_map *map); + +struct physical_device { + VkPhysicalDevice handle; + uint32_t gaps; +}; + +struct instance { + void *key; + VkInstance handle; + bool active; + PFN_vkGetInstanceProcAddr gipa; + PFN_vkDestroyInstance DestroyInstance; + PFN_vkEnumerateDeviceExtensionProperties EnumerateDeviceExtensionProperties; + PFN_vkGetPhysicalDeviceFeatures GetPhysicalDeviceFeatures; + PFN_vkGetPhysicalDeviceFeatures2 GetPhysicalDeviceFeatures2; + PFN_vkGetPhysicalDeviceProperties GetPhysicalDeviceProperties; + PFN_vkGetPhysicalDeviceProperties2 GetPhysicalDeviceProperties2; + PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties; + PFN_vkGetPhysicalDeviceFormatProperties GetPhysicalDeviceFormatProperties; + PFN_vkGetPhysicalDeviceFormatProperties2 GetPhysicalDeviceFormatProperties2; + PFN_vkGetPhysicalDeviceImageFormatProperties GetPhysicalDeviceImageFormatProperties; + PFN_vkGetPhysicalDeviceImageFormatProperties2 GetPhysicalDeviceImageFormatProperties2; + PFN_vkEnumeratePhysicalDevices EnumeratePhysicalDevices; + PFN_vkGetPhysicalDeviceMemoryProperties2 GetPhysicalDeviceMemoryProperties2; + PFN_vkGetPhysicalDeviceQueueFamilyProperties GetPhysicalDeviceQueueFamilyProperties; + PFN_vkGetPhysicalDeviceQueueFamilyProperties2 GetPhysicalDeviceQueueFamilyProperties2; + PFN_vkGetPhysicalDeviceExternalBufferProperties GetPhysicalDeviceExternalBufferProperties; + PFN_vkGetPhysicalDeviceExternalSemaphoreProperties GetPhysicalDeviceExternalSemaphoreProperties; + PFN_vkGetPhysicalDeviceExternalFenceProperties GetPhysicalDeviceExternalFenceProperties; + pthread_mutex_t lock; + struct physical_device physical_devices[8]; + uint32_t physical_device_count; + struct instance *next; +}; + +#define DEVICE_FUNCTIONS(X) \ + X(DestroyDevice) \ + X(CreateShaderModule) \ + X(DestroyShaderModule) \ + X(CreateGraphicsPipelines) \ + X(CmdSetViewport) \ + X(CmdSetScissor) \ + X(CmdSetViewportWithCount) \ + X(CmdSetScissorWithCount) \ + X(CreateImage) \ + X(DestroyImage) \ + X(CreateImageView) \ + X(GetDeviceImageMemoryRequirements) \ + X(GetDeviceImageSparseMemoryRequirements) \ + X(GetDeviceImageSubresourceLayout) \ + X(CmdCopyBufferToImage) \ + X(CmdCopyBufferToImage2) \ + X(CmdCopyImageToBuffer) \ + X(CmdCopyImageToBuffer2) \ + X(CmdCopyImage) \ + X(CmdCopyImage2) \ + X(CreateBuffer) \ + X(DestroyBuffer) \ + X(GetBufferMemoryRequirements) \ + X(BindBufferMemory) \ + X(BindBufferMemory2) \ + X(AllocateMemory) \ + X(FreeMemory) \ + X(MapMemory) \ + X(MapMemory2) \ + X(UnmapMemory) \ + X(UnmapMemory2) \ + X(AllocateCommandBuffers) \ + X(FreeCommandBuffers) \ + X(BeginCommandBuffer) \ + X(ResetCommandBuffer) \ + X(ResetCommandPool) \ + X(DestroyCommandPool) \ + X(CmdDrawIndirect) \ + X(CmdDrawIndexedIndirect) \ + X(GetImageMemoryRequirements) \ + X(BindImageMemory) \ + X(DestroyImageView) \ + X(CreateBufferView) \ + X(DestroyBufferView) \ + X(UpdateDescriptorSets) \ + X(CmdPushDescriptorSet) \ + X(CreateDescriptorUpdateTemplate) \ + X(DestroyDescriptorUpdateTemplate) \ + X(UpdateDescriptorSetWithTemplate) \ + X(CmdPushDescriptorSetWithTemplate) \ + X(CmdBindVertexBuffers) \ + X(CmdBindVertexBuffers2) + +struct staging_block; + +struct device { + void *key; + VkDevice handle; + PFN_vkGetDeviceProcAddr gdpa; + /* The gaps the application enabled a made-up feature for. */ + uint32_t emulated; + /* Locations a clip distance varying may use, from the vertex output and fragment input limits. */ + uint32_t max_varying_locations; + VkPhysicalDeviceMemoryProperties memory_properties; +#define DECLARE(name) PFN_vk##name name; + DEVICE_FUNCTIONS(DECLARE) +#undef DECLARE + pthread_mutex_t lock; + struct handle_map shader_modules; /* VkShaderModule -> struct spirv_code, for modules with distances */ + struct handle_map images; /* VkImage -> struct bc_image */ + struct handle_map buffers; /* VkBuffer -> struct bound_buffer */ + struct handle_map memories; /* VkDeviceMemory -> struct mapped_memory */ + struct handle_map command_buffers; /* VkCommandBuffer -> struct command_buffer */ + struct staging_block *free_staging; + struct handle_map templates; /* VkDescriptorUpdateTemplate -> struct template_info (draw.c) */ + /* nullDescriptor stand-ins (draw.c): 0 not made yet, 1 ready, -1 failed. */ + int null_ready; + VkBuffer null_buffer; + VkDeviceMemory null_buffer_memory; + VkImage null_image; + VkDeviceMemory null_image_memory; + VkImageView null_view; + /* Texel-buffer stand-ins: RGBA8 for uniform (sampled) texel buffers, R32_UINT for storage. */ + VkBufferView null_uniform_texel; + VkBufferView null_storage_texel; + struct device *next; +}; + +struct device *device_from_handle(const void *dispatchable); + +struct spirv_code { + uint32_t *words; + size_t count; +}; + +/* spirv.c */ +bool spirv_uses_distances(const uint32_t *words, size_t count); +/* The module with clip and cull distances taken out; false when it cannot be rewritten. */ +bool spirv_strip_distances(const uint32_t *words, size_t count, struct spirv_code *out); + +enum clip_role { CLIP_STRIP, CLIP_PRODUCER, CLIP_CONSUMER }; + +struct clip_info { + bool has_clip; /* a standalone ClipDistance variable this stage could hand on */ + uint32_t clip_count; /* its array length */ + uint32_t locations_used; /* one past the highest varying location, outputs or inputs per role */ +}; + +void spirv_clip_info(const uint32_t *words, size_t count, enum clip_role role, struct clip_info *info); +/* + * Producer: the last stage before rasterization writes its clip distances to `location` instead. + * Consumer: the fragment shader reads them there and discards when any is negative. + * Both drop cull distances. False when the module cannot be rewritten. + */ +bool spirv_emulate_clip(const uint32_t *words, size_t count, enum clip_role role, uint32_t location, + uint32_t clip_count, struct spirv_code *out); + +/* pipeline.c */ +VKAPI_ATTR VkResult VKAPI_CALL CreateShaderModule(VkDevice device, const VkShaderModuleCreateInfo *info, + const VkAllocationCallbacks *allocator, VkShaderModule *module); +VKAPI_ATTR void VKAPI_CALL DestroyShaderModule(VkDevice device, VkShaderModule module, + const VkAllocationCallbacks *allocator); +VKAPI_ATTR VkResult VKAPI_CALL CreateGraphicsPipelines(VkDevice device, VkPipelineCache cache, uint32_t count, + const VkGraphicsPipelineCreateInfo *infos, + const VkAllocationCallbacks *allocator, VkPipeline *pipelines); +VKAPI_ATTR void VKAPI_CALL CmdSetViewport(VkCommandBuffer cmd, uint32_t first, uint32_t count, + const VkViewport *viewports); +VKAPI_ATTR void VKAPI_CALL CmdSetScissor(VkCommandBuffer cmd, uint32_t first, uint32_t count, + const VkRect2D *scissors); +VKAPI_ATTR void VKAPI_CALL CmdSetViewportWithCount(VkCommandBuffer cmd, uint32_t count, + const VkViewport *viewports); +VKAPI_ATTR void VKAPI_CALL CmdSetScissorWithCount(VkCommandBuffer cmd, uint32_t count, const VkRect2D *scissors); + +/* bc.c */ +VkFormat bc_substitute(VkFormat format); +VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceFormatProperties(VkPhysicalDevice physical_device, VkFormat format, + VkFormatProperties *properties); +VKAPI_ATTR void VKAPI_CALL GetPhysicalDeviceFormatProperties2(VkPhysicalDevice physical_device, VkFormat format, + VkFormatProperties2 *properties); +VKAPI_ATTR VkResult VKAPI_CALL GetPhysicalDeviceImageFormatProperties( + VkPhysicalDevice physical_device, VkFormat format, VkImageType type, VkImageTiling tiling, + VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties *properties); +VKAPI_ATTR VkResult VKAPI_CALL GetPhysicalDeviceImageFormatProperties2( + VkPhysicalDevice physical_device, const VkPhysicalDeviceImageFormatInfo2 *info, + VkImageFormatProperties2 *properties); +VKAPI_ATTR VkResult VKAPI_CALL CreateImage(VkDevice device, const VkImageCreateInfo *info, + const VkAllocationCallbacks *allocator, VkImage *image); +VKAPI_ATTR void VKAPI_CALL DestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks *allocator); +VKAPI_ATTR VkResult VKAPI_CALL CreateImageView(VkDevice device, const VkImageViewCreateInfo *info, + const VkAllocationCallbacks *allocator, VkImageView *view); +VKAPI_ATTR void VKAPI_CALL GetDeviceImageMemoryRequirements(VkDevice device, + const VkDeviceImageMemoryRequirements *info, + VkMemoryRequirements2 *requirements); +VKAPI_ATTR void VKAPI_CALL GetDeviceImageSparseMemoryRequirements( + VkDevice device, const VkDeviceImageMemoryRequirements *info, uint32_t *count, + VkSparseImageMemoryRequirements2 *requirements); +VKAPI_ATTR void VKAPI_CALL GetDeviceImageSubresourceLayout(VkDevice device, + const VkDeviceImageSubresourceInfo *info, + VkSubresourceLayout2 *layout); +VKAPI_ATTR void VKAPI_CALL CmdCopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer, VkImage image, + VkImageLayout layout, uint32_t count, + const VkBufferImageCopy *regions); +VKAPI_ATTR void VKAPI_CALL CmdCopyBufferToImage2(VkCommandBuffer cmd, const VkCopyBufferToImageInfo2 *info); +VKAPI_ATTR void VKAPI_CALL CmdCopyImageToBuffer(VkCommandBuffer cmd, VkImage image, VkImageLayout layout, + VkBuffer buffer, uint32_t count, const VkBufferImageCopy *regions); +VKAPI_ATTR void VKAPI_CALL CmdCopyImageToBuffer2(VkCommandBuffer cmd, const VkCopyImageToBufferInfo2 *info); +VKAPI_ATTR void VKAPI_CALL CmdCopyImage(VkCommandBuffer cmd, VkImage src, VkImageLayout src_layout, VkImage dst, + VkImageLayout dst_layout, uint32_t count, const VkImageCopy *regions); +VKAPI_ATTR void VKAPI_CALL CmdCopyImage2(VkCommandBuffer cmd, const VkCopyImageInfo2 *info); +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, + VkDeviceSize offset); +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory2(VkDevice device, uint32_t count, + const VkBindBufferMemoryInfo *infos); +VKAPI_ATTR void VKAPI_CALL DestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks *allocator); +VKAPI_ATTR void VKAPI_CALL FreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks *allocator); +VKAPI_ATTR VkResult VKAPI_CALL MapMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, + VkDeviceSize size, VkMemoryMapFlags flags, void **data); +VKAPI_ATTR VkResult VKAPI_CALL MapMemory2(VkDevice device, const VkMemoryMapInfo *info, void **data); +VKAPI_ATTR void VKAPI_CALL UnmapMemory(VkDevice device, VkDeviceMemory memory); +VKAPI_ATTR VkResult VKAPI_CALL UnmapMemory2(VkDevice device, const VkMemoryUnmapInfo *info); +VKAPI_ATTR VkResult VKAPI_CALL AllocateCommandBuffers(VkDevice device, const VkCommandBufferAllocateInfo *info, + VkCommandBuffer *buffers); +VKAPI_ATTR void VKAPI_CALL FreeCommandBuffers(VkDevice device, VkCommandPool pool, uint32_t count, + const VkCommandBuffer *buffers); +VKAPI_ATTR VkResult VKAPI_CALL BeginCommandBuffer(VkCommandBuffer cmd, const VkCommandBufferBeginInfo *info); +VKAPI_ATTR VkResult VKAPI_CALL ResetCommandBuffer(VkCommandBuffer cmd, VkCommandBufferResetFlags flags); +VKAPI_ATTR VkResult VKAPI_CALL ResetCommandPool(VkDevice device, VkCommandPool pool, VkCommandPoolResetFlags flags); +VKAPI_ATTR void VKAPI_CALL DestroyCommandPool(VkDevice device, VkCommandPool pool, + const VkAllocationCallbacks *allocator); +void bc_destroy_device(struct device *dev); + +/* layer.c */ +struct instance *instance_from_handle(const void *dispatchable); +/* What the GPU lacks of the features the layer can make up for. */ +uint32_t physical_device_gaps(struct instance *inst, VkPhysicalDevice physical_device); + +/* trace.c (DROIDDECK_MALI_COMPAT_TRACE=1) */ +void trace_load(struct instance *inst); +PFN_vkVoidFunction trace_hook(struct instance *inst, const char *name); +void trace_extension_names(const VkExtensionProperties *ext, uint32_t n); + + +/* draw.c */ +VKAPI_ATTR void VKAPI_CALL CmdDrawIndirect(VkCommandBuffer cmd, VkBuffer buffer, VkDeviceSize offset, uint32_t draw_count, uint32_t stride); +VKAPI_ATTR void VKAPI_CALL CmdDrawIndexedIndirect(VkCommandBuffer cmd, VkBuffer buffer, VkDeviceSize offset, uint32_t draw_count, uint32_t stride); +VKAPI_ATTR void VKAPI_CALL UpdateDescriptorSets(VkDevice device, uint32_t write_count, const VkWriteDescriptorSet *writes, uint32_t copy_count, const VkCopyDescriptorSet *copies); +VKAPI_ATTR void VKAPI_CALL CmdPushDescriptorSet(VkCommandBuffer cmd, VkPipelineBindPoint bind_point, VkPipelineLayout layout, uint32_t set, uint32_t write_count, const VkWriteDescriptorSet *writes); +VKAPI_ATTR VkResult VKAPI_CALL CreateDescriptorUpdateTemplate(VkDevice device, const VkDescriptorUpdateTemplateCreateInfo *info, const VkAllocationCallbacks *allocator, VkDescriptorUpdateTemplate *tmpl); +VKAPI_ATTR void VKAPI_CALL DestroyDescriptorUpdateTemplate(VkDevice device, VkDescriptorUpdateTemplate tmpl, const VkAllocationCallbacks *allocator); +VKAPI_ATTR void VKAPI_CALL UpdateDescriptorSetWithTemplate(VkDevice device, VkDescriptorSet set, VkDescriptorUpdateTemplate tmpl, const void *data); +VKAPI_ATTR void VKAPI_CALL CmdPushDescriptorSetWithTemplate(VkCommandBuffer cmd, VkDescriptorUpdateTemplate tmpl, VkPipelineLayout layout, uint32_t set, const void *data); +VKAPI_ATTR void VKAPI_CALL CmdBindVertexBuffers(VkCommandBuffer cmd, uint32_t first, uint32_t count, const VkBuffer *buffers, const VkDeviceSize *offsets); +VKAPI_ATTR void VKAPI_CALL CmdBindVertexBuffers2(VkCommandBuffer cmd, uint32_t first, uint32_t count, const VkBuffer *buffers, const VkDeviceSize *offsets, const VkDeviceSize *sizes, const VkDeviceSize *strides); +void null_resources_destroy(struct device *dev); +#endif diff --git a/tools/venus/layer/pipeline.c b/tools/venus/layer/pipeline.c new file mode 100644 index 000000000..a749c21d3 --- /dev/null +++ b/tools/venus/layer/pipeline.c @@ -0,0 +1,308 @@ +#include "mali_compat.h" + +#include +#include + +#define HANDLE(h) ((uint64_t)(uintptr_t)(h)) +#define MAX_STAGES 8 + +/* A module with distances goes to the driver without them; its original stays here, for the + * pipeline that can emulate its clip distances. */ +VKAPI_ATTR VkResult VKAPI_CALL CreateShaderModule(VkDevice device, const VkShaderModuleCreateInfo *info, + const VkAllocationCallbacks *allocator, VkShaderModule *module) +{ + struct device *dev = device_from_handle(device); + size_t count = info->codeSize / sizeof(uint32_t); + if (!spirv_uses_distances(info->pCode, count)) + return dev->CreateShaderModule(device, info, allocator, module); + + struct spirv_code stripped; + if (!spirv_strip_distances(info->pCode, count, &stripped)) { + layer_log("could not take the clip and cull distances out of a shader module"); + return dev->CreateShaderModule(device, info, allocator, module); + } + VkShaderModuleCreateInfo copy = *info; + copy.pCode = stripped.words; + copy.codeSize = stripped.count * sizeof(uint32_t); + VkResult result = dev->CreateShaderModule(device, ©, allocator, module); + free(stripped.words); + if (result != VK_SUCCESS) + return result; + + struct spirv_code *original = malloc(sizeof(*original)); + original->count = count; + original->words = malloc(info->codeSize); + memcpy(original->words, info->pCode, info->codeSize); + pthread_mutex_lock(&dev->lock); + map_put(&dev->shader_modules, HANDLE(*module), original); + pthread_mutex_unlock(&dev->lock); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL DestroyShaderModule(VkDevice device, VkShaderModule module, + const VkAllocationCallbacks *allocator) +{ + struct device *dev = device_from_handle(device); + pthread_mutex_lock(&dev->lock); + struct spirv_code *original = map_remove(&dev->shader_modules, HANDLE(module)); + pthread_mutex_unlock(&dev->lock); + if (original) { + free(original->words); + free(original); + } + dev->DestroyShaderModule(device, module, allocator); +} + +struct stage_code { + const uint32_t *words; + size_t count; + /* The application's own, when the code comes inline in the stage. */ + VkShaderModuleCreateInfo *inline_info; + /* A module whose distances were already taken out at creation. */ + bool stripped_module; +}; + +static void find_stage_code(struct device *dev, const VkPipelineShaderStageCreateInfo *stage, struct stage_code *code) +{ + memset(code, 0, sizeof(*code)); + for (const VkBaseInStructure *s = stage->pNext; s; s = s->pNext) { + if (s->sType == VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO) { + code->inline_info = (VkShaderModuleCreateInfo *)s; + code->words = code->inline_info->pCode; + code->count = code->inline_info->codeSize / sizeof(uint32_t); + return; + } + } + if (stage->module == VK_NULL_HANDLE) + return; + pthread_mutex_lock(&dev->lock); + struct spirv_code *original = map_get(&dev->shader_modules, HANDLE(stage->module)); + pthread_mutex_unlock(&dev->lock); + if (original) { + code->words = original->words; + code->count = original->count; + code->stripped_module = true; + } +} + +struct pipeline_rewrite { + VkPipelineRasterizationStateCreateInfo rasterization; + /* VK_EXT_depth_clip_enable's state, taken out of the application's chain for the call. */ + VkBaseOutStructure *clip_prev, *clip_node; + VkPipelineViewportStateCreateInfo viewport; + VkPipelineShaderStageCreateInfo stages[MAX_STAGES]; + VkShaderModuleCreateInfo modules[MAX_STAGES]; + struct spirv_code code[MAX_STAGES]; + VkShaderModule temporary[MAX_STAGES]; + /* Rewritten where they are, when they are not first in their stage's chain. */ + VkShaderModuleCreateInfo *in_place[MAX_STAGES]; + const uint32_t *in_place_code[MAX_STAGES]; + size_t in_place_size[MAX_STAGES]; +}; + +static void use_code(struct device *dev, VkGraphicsPipelineCreateInfo *info, struct pipeline_rewrite *rw, + const struct stage_code *code, uint32_t i) +{ + if (info->pStages != rw->stages) { + memcpy(rw->stages, info->pStages, info->stageCount * sizeof(*rw->stages)); + info->pStages = rw->stages; + } + const struct spirv_code *out = &rw->code[i]; + if (code->inline_info && rw->stages[i].pNext == code->inline_info) { + rw->modules[i] = *code->inline_info; + rw->modules[i].pCode = out->words; + rw->modules[i].codeSize = out->count * sizeof(uint32_t); + rw->stages[i].pNext = &rw->modules[i]; + } else if (code->inline_info) { + rw->in_place[i] = code->inline_info; + rw->in_place_code[i] = code->inline_info->pCode; + rw->in_place_size[i] = code->inline_info->codeSize; + code->inline_info->pCode = out->words; + code->inline_info->codeSize = out->count * sizeof(uint32_t); + } else { + VkShaderModuleCreateInfo module_info = { + .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .codeSize = out->count * sizeof(uint32_t), + .pCode = out->words, + }; + if (dev->CreateShaderModule(dev->handle, &module_info, NULL, &rw->temporary[i]) == VK_SUCCESS) + rw->stages[i].module = rw->temporary[i]; + } +} + +static int stage_index(const VkGraphicsPipelineCreateInfo *info, VkShaderStageFlagBits stage) +{ + for (uint32_t i = 0; i < info->stageCount; i++) { + if (info->pStages[i].stage == stage) + return (int)i; + } + return -1; +} + +/* + * Clip distances are emulated when the last stage before rasterization is the only one that + * hands them on: a vertex shader feeding a geometry shader would need a varying of its own. + * Anywhere else they are dropped, along with every cull distance. + */ +static void rewrite_shaders(struct device *dev, VkGraphicsPipelineCreateInfo *info, struct pipeline_rewrite *rw) +{ + uint32_t n = info->stageCount; + if (!n || n > MAX_STAGES || !info->pStages) + return; + struct stage_code code[MAX_STAGES]; + bool uses[MAX_STAGES]; + bool any = false; + for (uint32_t i = 0; i < n; i++) { + find_stage_code(dev, &info->pStages[i], &code[i]); + uses[i] = code[i].words && spirv_uses_distances(code[i].words, code[i].count); + any |= uses[i]; + } + if (!any) + return; + + int producer = stage_index(info, VK_SHADER_STAGE_GEOMETRY_BIT); + if (producer < 0) + producer = stage_index(info, VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT); + if (producer < 0) + producer = stage_index(info, VK_SHADER_STAGE_VERTEX_BIT); + int consumer = stage_index(info, VK_SHADER_STAGE_FRAGMENT_BIT); + bool emulate = (dev->emulated & GAP_CLIP_DISTANCE) && producer >= 0 && consumer >= 0 && uses[producer] && + code[consumer].words; + for (uint32_t i = 0; i < n && emulate; i++) { + if ((int)i != producer && (int)i != consumer && uses[i]) + emulate = false; + } + if (emulate) { + struct clip_info produced, consumed; + spirv_clip_info(code[producer].words, code[producer].count, CLIP_PRODUCER, &produced); + spirv_clip_info(code[consumer].words, code[consumer].count, CLIP_CONSUMER, &consumed); + uint32_t location = produced.locations_used > consumed.locations_used ? produced.locations_used + : consumed.locations_used; + emulate = produced.has_clip && location != UINT32_MAX && + location + produced.clip_count <= dev->max_varying_locations && + spirv_emulate_clip(code[producer].words, code[producer].count, CLIP_PRODUCER, location, + produced.clip_count, &rw->code[producer]) && + spirv_emulate_clip(code[consumer].words, code[consumer].count, CLIP_CONSUMER, location, + produced.clip_count, &rw->code[consumer]); + if (!emulate) { + free(rw->code[producer].words); + free(rw->code[consumer].words); + rw->code[producer] = rw->code[consumer] = (struct spirv_code){0}; + } + } + + for (uint32_t i = 0; i < n; i++) { + if (!rw->code[i].words && uses[i] && !code[i].stripped_module && + !spirv_strip_distances(code[i].words, code[i].count, &rw->code[i])) { + layer_log("could not take the clip and cull distances out of a pipeline's shader"); + } + if (rw->code[i].words) + use_code(dev, info, rw, &code[i], i); + } +} + +static void rewrite_pipeline(struct device *dev, VkGraphicsPipelineCreateInfo *info, struct pipeline_rewrite *rw) +{ + if ((dev->emulated & GAP_FILL_MODE) && info->pRasterizationState && + info->pRasterizationState->polygonMode != VK_POLYGON_MODE_FILL) { + rw->rasterization = *info->pRasterizationState; + rw->rasterization.polygonMode = VK_POLYGON_MODE_FILL; + info->pRasterizationState = &rw->rasterization; + } + /* No VK_EXT_depth_clip_enable: depth clipping off is the nearest the driver has as depth clamp. */ + if ((dev->emulated & GAP_DEPTH_CLIP) && info->pRasterizationState) { + VkBaseOutStructure *prev = (VkBaseOutStructure *)info->pRasterizationState; + while (prev->pNext && prev->pNext->sType != VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_DEPTH_CLIP_STATE_CREATE_INFO_EXT) + prev = prev->pNext; + VkBaseOutStructure *node = prev->pNext; + if (node) { + bool clip = ((VkPipelineRasterizationDepthClipStateCreateInfoEXT *)node)->depthClipEnable; + if (info->pRasterizationState != &rw->rasterization) { + rw->rasterization = *info->pRasterizationState; + if (prev == (VkBaseOutStructure *)info->pRasterizationState) + prev = (VkBaseOutStructure *)&rw->rasterization; + info->pRasterizationState = &rw->rasterization; + } + if (!clip) + rw->rasterization.depthClampEnable = VK_TRUE; + if (prev != (VkBaseOutStructure *)&rw->rasterization) { + rw->clip_prev = prev; + rw->clip_node = node; + } + prev->pNext = node->pNext; + } + } + if ((dev->emulated & GAP_MULTI_VIEWPORT) && info->pViewportState && + (info->pViewportState->viewportCount > 1 || info->pViewportState->scissorCount > 1)) { + rw->viewport = *info->pViewportState; + if (rw->viewport.viewportCount > 1) + rw->viewport.viewportCount = 1; + if (rw->viewport.scissorCount > 1) + rw->viewport.scissorCount = 1; + info->pViewportState = &rw->viewport; + } + if (dev->emulated & GAPS_SHADER) + rewrite_shaders(dev, info, rw); +} + +static void undo_rewrite(struct device *dev, struct pipeline_rewrite *rw) +{ + if (rw->clip_prev) + rw->clip_prev->pNext = rw->clip_node; + for (uint32_t i = 0; i < MAX_STAGES; i++) { + if (rw->in_place[i]) { + rw->in_place[i]->pCode = rw->in_place_code[i]; + rw->in_place[i]->codeSize = rw->in_place_size[i]; + } + if (rw->temporary[i] != VK_NULL_HANDLE) + dev->DestroyShaderModule(dev->handle, rw->temporary[i], NULL); + free(rw->code[i].words); + } +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateGraphicsPipelines(VkDevice device, VkPipelineCache cache, uint32_t count, + const VkGraphicsPipelineCreateInfo *infos, + const VkAllocationCallbacks *allocator, VkPipeline *pipelines) +{ + struct device *dev = device_from_handle(device); + VkGraphicsPipelineCreateInfo *copies = malloc(count * sizeof(*copies)); + struct pipeline_rewrite *rewrites = calloc(count, sizeof(*rewrites)); + for (uint32_t i = 0; i < count; i++) { + copies[i] = infos[i]; + rewrite_pipeline(dev, &copies[i], &rewrites[i]); + } + VkResult result = dev->CreateGraphicsPipelines(device, cache, count, copies, allocator, pipelines); + for (uint32_t i = 0; i < count; i++) + undo_rewrite(dev, &rewrites[i]); + free(rewrites); + free(copies); + return result; +} + +/* The driver has one viewport and one scissor; anything past the first is left out. */ +VKAPI_ATTR void VKAPI_CALL CmdSetViewport(VkCommandBuffer cmd, uint32_t first, uint32_t count, + const VkViewport *viewports) +{ + struct device *dev = device_from_handle(cmd); + if (first == 0 && count > 0) + dev->CmdSetViewport(cmd, 0, 1, viewports); +} + +VKAPI_ATTR void VKAPI_CALL CmdSetScissor(VkCommandBuffer cmd, uint32_t first, uint32_t count, const VkRect2D *scissors) +{ + struct device *dev = device_from_handle(cmd); + if (first == 0 && count > 0) + dev->CmdSetScissor(cmd, 0, 1, scissors); +} + +VKAPI_ATTR void VKAPI_CALL CmdSetViewportWithCount(VkCommandBuffer cmd, uint32_t count, const VkViewport *viewports) +{ + struct device *dev = device_from_handle(cmd); + dev->CmdSetViewportWithCount(cmd, count > 1 ? 1 : count, viewports); +} + +VKAPI_ATTR void VKAPI_CALL CmdSetScissorWithCount(VkCommandBuffer cmd, uint32_t count, const VkRect2D *scissors) +{ + struct device *dev = device_from_handle(cmd); + dev->CmdSetScissorWithCount(cmd, count > 1 ? 1 : count, scissors); +} diff --git a/tools/venus/layer/spirv.c b/tools/venus/layer/spirv.c new file mode 100644 index 000000000..734d5ce2d --- /dev/null +++ b/tools/venus/layer/spirv.c @@ -0,0 +1,833 @@ +/* + * Clip and cull distances out of SPIR-V, for a GPU without either. + * + * Stripping: every ClipDistance or CullDistance variable becomes a Private one, so the shader still + * writes and reads it but the rasterizer never sees it. Members of a gl_PerVertex block stay + * declared (as glslang declares them unused anyway) and their accesses move to a Private stand-in. + * Emulating: the last stage before rasterization writes its clip distances to a free varying + * location instead, and the fragment shader reads them there and discards when one is negative. + */ +#include "mali_compat.h" + +#include +#include +#include + +#define OPCODE(word) ((word) & 0xffffu) +#define LENGTH(word) ((word) >> 16) +#define VERSION_1_4 0x00010400u + +struct words { + uint32_t *data; + size_t count; + size_t capacity; +}; + +static void push(struct words *words, uint32_t word) +{ + if (words->count == words->capacity) { + words->capacity = words->capacity ? words->capacity * 2 : 64; + words->data = realloc(words->data, words->capacity * sizeof(uint32_t)); + } + words->data[words->count++] = word; +} + +static void emit(struct words *words, SpvOp op, size_t count, const uint32_t *operands) +{ + push(words, (uint32_t)(count + 1) << 16 | op); + for (size_t i = 0; i < count; i++) + push(words, operands[i]); +} + +#define EMIT(words, op, ...) \ + do { \ + const uint32_t operands_[] = {__VA_ARGS__}; \ + emit(words, op, sizeof(operands_) / sizeof(operands_[0]), operands_); \ + } while (0) + +enum place { BEFORE, INSTEAD, AFTER }; + +struct edit { + size_t offset; + enum place place; + struct words words; +}; + +struct new_type { + SpvOp op; + uint32_t operands[3]; + uint32_t count; + uint32_t id; +}; + +struct module { + uint32_t *w; + size_t count; + uint32_t version; + uint32_t original_bound; + uint32_t bound; + size_t *def; + size_t first_declaration; + size_t first_function; + struct edit *edits; + size_t edit_count; + struct new_type *new_types; + size_t new_type_count; + struct words globals; + uint8_t *private_ids; + uint32_t *interface_add; + size_t interface_add_count; + uint8_t *interface_remove; + bool failed; +}; + +static bool declares_result_first(uint32_t op) +{ + return (op >= SpvOpTypeVoid && op <= SpvOpTypePipe) || op == SpvOpLabel || op == SpvOpExtInstImport || + op == SpvOpString; +} + +static bool declares_result_second(uint32_t op) +{ + switch (op) { + case SpvOpConstantTrue: + case SpvOpConstantFalse: + case SpvOpConstant: + case SpvOpConstantComposite: + case SpvOpConstantNull: + case SpvOpSpecConstantTrue: + case SpvOpSpecConstantFalse: + case SpvOpSpecConstant: + case SpvOpSpecConstantComposite: + case SpvOpSpecConstantOp: + case SpvOpUndef: + case SpvOpVariable: + case SpvOpFunction: + return true; + default: + return false; + } +} + +static bool is_declaration(uint32_t op) +{ + return (op >= SpvOpTypeVoid && op <= SpvOpTypeForwardPointer) || declares_result_second(op); +} + +static void unload(struct module *m) +{ + free(m->w); + free(m->def); + for (size_t i = 0; i < m->edit_count; i++) + free(m->edits[i].words.data); + free(m->edits); + free(m->new_types); + free(m->globals.data); + free(m->private_ids); + free(m->interface_add); + free(m->interface_remove); +} + +static bool load(struct module *m, const uint32_t *words, size_t count) +{ + memset(m, 0, sizeof(*m)); + if (count < 5 || words[0] != SpvMagicNumber || words[3] == 0 || words[3] > (1u << 22)) + return false; + m->w = malloc(count * sizeof(uint32_t)); + memcpy(m->w, words, count * sizeof(uint32_t)); + m->count = count; + m->version = words[1]; + m->original_bound = m->bound = words[3]; + m->def = calloc(m->bound, sizeof(size_t)); + /* Room for every id the rewrite can add. */ + m->private_ids = calloc(m->bound + 4096, 1); + m->interface_remove = calloc(m->bound, 1); + for (size_t i = 5; i < count;) { + uint32_t length = LENGTH(m->w[i]); + uint32_t op = OPCODE(m->w[i]); + if (length == 0 || i + length > count) { + unload(m); + return false; + } + uint32_t result = 0; + if (declares_result_first(op) && length > 1) + result = m->w[i + 1]; + else if (declares_result_second(op) && length > 2) + result = m->w[i + 2]; + if (result && result < m->bound) + m->def[result] = i; + if (!m->first_declaration && is_declaration(op)) + m->first_declaration = i; + if (!m->first_function && op == SpvOpFunction) + m->first_function = i; + i += length; + } + if (!m->first_declaration || !m->first_function) { + unload(m); + return false; + } + return true; +} + +static struct words *edit(struct module *m, size_t offset, enum place place) +{ + m->edits = realloc(m->edits, (m->edit_count + 1) * sizeof(*m->edits)); + struct edit *e = &m->edits[m->edit_count++]; + *e = (struct edit){.offset = offset, .place = place}; + return &e->words; +} + +static uint32_t new_id(struct module *m) +{ + return m->bound++; +} + +static bool is_private(const struct module *m, uint32_t id) +{ + return id < m->original_bound + 4096 && m->private_ids[id]; +} + +static void mark_private(struct module *m, uint32_t id) +{ + if (id < m->original_bound + 4096) + m->private_ids[id] = 1; + else + m->failed = true; +} + +/* The defining instruction of an id from the original module, or 0. */ +static size_t definition(const struct module *m, uint32_t id) +{ + return id < m->original_bound ? m->def[id] : 0; +} + +static uint32_t find_type(struct module *m, SpvOp op, const uint32_t *operands, uint32_t count) +{ + for (size_t i = 5; i < m->first_function; i += LENGTH(m->w[i])) { + if (OPCODE(m->w[i]) == op && LENGTH(m->w[i]) == count + 2 && + !memcmp(&m->w[i + 2], operands, count * sizeof(uint32_t))) + return m->w[i + 1]; + } + for (size_t i = 0; i < m->new_type_count; i++) { + struct new_type *t = &m->new_types[i]; + if (t->op == op && t->count == count && !memcmp(t->operands, operands, count * sizeof(uint32_t))) + return t->id; + } + return 0; +} + +static uint32_t add_type(struct module *m, struct words *where, SpvOp op, const uint32_t *operands, uint32_t count) +{ + uint32_t id = new_id(m); + push(where, (count + 2) << 16 | op); + push(where, id); + for (uint32_t i = 0; i < count; i++) + push(where, operands[i]); + m->new_types = realloc(m->new_types, (m->new_type_count + 1) * sizeof(*m->new_types)); + struct new_type *t = &m->new_types[m->new_type_count++]; + *t = (struct new_type){.op = op, .count = count, .id = id}; + memcpy(t->operands, operands, count * sizeof(uint32_t)); + return id; +} + +/* Right after its pointee, so a global variable declared before the end of the types can use it. */ +static uint32_t pointer_type(struct module *m, uint32_t storage, uint32_t pointee) +{ + uint32_t operands[2] = {storage, pointee}; + uint32_t id = find_type(m, SpvOpTypePointer, operands, 2); + if (id) + return id; + size_t pointee_def = definition(m, pointee); + struct words *where = pointee_def ? edit(m, pointee_def, AFTER) : &m->globals; + return add_type(m, where, SpvOpTypePointer, operands, 2); +} + +static uint32_t scalar_type(struct module *m, SpvOp op, const uint32_t *operands, uint32_t count) +{ + uint32_t id = find_type(m, op, operands, count); + return id ? id : add_type(m, &m->globals, op, operands, count); +} + +static uint32_t float_type(struct module *m) +{ + const uint32_t width = 32; + return scalar_type(m, SpvOpTypeFloat, &width, 1); +} + +static uint32_t uint_type(struct module *m) +{ + const uint32_t operands[2] = {32, 0}; + return scalar_type(m, SpvOpTypeInt, operands, 2); +} + +static uint32_t bool_type(struct module *m) +{ + return scalar_type(m, SpvOpTypeBool, NULL, 0); +} + +static uint32_t constant(struct module *m, uint32_t type, uint32_t value) +{ + for (size_t i = 5; i < m->first_function; i += LENGTH(m->w[i])) { + if (OPCODE(m->w[i]) == SpvOpConstant && LENGTH(m->w[i]) == 4 && m->w[i + 1] == type && m->w[i + 3] == value) + return m->w[i + 2]; + } + uint32_t id = new_id(m); + EMIT(&m->globals, SpvOpConstant, type, id, value); + return id; +} + +static bool constant_value(const struct module *m, uint32_t id, uint32_t *value) +{ + size_t d = definition(m, id); + if (!d || OPCODE(m->w[d]) != SpvOpConstant || LENGTH(m->w[d]) != 4) + return false; + *value = m->w[d + 3]; + return true; +} + +/* What a pointer type points to, and where. */ +static bool pointer_parts(const struct module *m, uint32_t pointer, uint32_t *storage, uint32_t *pointee) +{ + size_t d = definition(m, pointer); + if (!d || OPCODE(m->w[d]) != SpvOpTypePointer) + return false; + *storage = m->w[d + 2]; + *pointee = m->w[d + 3]; + return true; +} + +static bool is_float32(const struct module *m, uint32_t type) +{ + size_t d = definition(m, type); + return d && OPCODE(m->w[d]) == SpvOpTypeFloat && m->w[d + 2] == 32; +} + +/* Varying locations a type takes up. */ +static uint32_t location_span(const struct module *m, uint32_t type, int depth) +{ + size_t d = definition(m, type); + if (!d || depth > 8) + return 1; + uint32_t value; + switch (OPCODE(m->w[d])) { + case SpvOpTypeArray: + return constant_value(m, m->w[d + 3], &value) ? value * location_span(m, m->w[d + 2], depth + 1) : 1; + case SpvOpTypeMatrix: + return m->w[d + 3] * location_span(m, m->w[d + 2], depth + 1); + case SpvOpTypeStruct: { + uint32_t span = 0; + for (uint32_t i = 2; i < LENGTH(m->w[d]); i++) + span += location_span(m, m->w[d + i], depth + 1); + return span; + } + case SpvOpTypeVector: { + size_t component = definition(m, m->w[d + 2]); + bool wide = component && m->w[component + 2] == 64; + return wide && m->w[d + 3] > 2 ? 2 : 1; + } + default: + return 1; + } +} + +struct distance_var { + uint32_t id; + uint32_t built_in; + uint32_t storage; + size_t decoration; +}; + +struct distance_member { + uint32_t structure; + uint32_t member; +}; + +struct analysis { + struct distance_var vars[8]; + uint32_t var_count; + struct distance_member members[8]; + uint32_t member_count; + bool member_locations; +}; + +static bool is_distance(uint32_t built_in) +{ + return built_in == SpvBuiltInClipDistance || built_in == SpvBuiltInCullDistance; +} + +static void analyze(struct module *m, struct analysis *a) +{ + memset(a, 0, sizeof(*a)); + for (size_t i = 5; i < m->first_declaration; i += LENGTH(m->w[i])) { + uint32_t op = OPCODE(m->w[i]); + if (op == SpvOpDecorate && LENGTH(m->w[i]) == 4 && m->w[i + 2] == SpvDecorationBuiltIn && + is_distance(m->w[i + 3])) { + uint32_t storage, pointee; + size_t d = definition(m, m->w[i + 1]); + if (a->var_count < 8 && d && OPCODE(m->w[d]) == SpvOpVariable && + pointer_parts(m, m->w[d + 1], &storage, &pointee)) { + a->vars[a->var_count++] = + (struct distance_var){.id = m->w[i + 1], .built_in = m->w[i + 3], .storage = storage, .decoration = i}; + } else { + m->failed = true; + } + } else if (op == SpvOpMemberDecorate && LENGTH(m->w[i]) == 5 && m->w[i + 3] == SpvDecorationBuiltIn && + is_distance(m->w[i + 4])) { + if (a->member_count < 8) + a->members[a->member_count++] = (struct distance_member){m->w[i + 1], m->w[i + 2]}; + else + m->failed = true; + } else if (op == SpvOpMemberDecorate && LENGTH(m->w[i]) >= 5 && m->w[i + 3] == SpvDecorationLocation) { + a->member_locations = true; + } + } +} + +static bool uses_distances(const uint32_t *words, size_t count) +{ + for (size_t i = 5; i + 1 < count && LENGTH(words[i]); i += LENGTH(words[i])) { + uint32_t op = OPCODE(words[i]); + if (op == SpvOpCapability && (words[i + 1] == SpvCapabilityClipDistance || words[i + 1] == SpvCapabilityCullDistance)) + return true; + if (op != SpvOpCapability && op != SpvOpExtension && op != SpvOpExtInstImport && op != SpvOpMemoryModel) + break; + } + return false; +} + +bool spirv_uses_distances(const uint32_t *words, size_t count) +{ + return count > 5 && words[0] == SpvMagicNumber && uses_distances(words, count); +} + +static void remove_capabilities(struct module *m) +{ + for (size_t i = 5; i < m->first_declaration; i += LENGTH(m->w[i])) { + if (OPCODE(m->w[i]) == SpvOpCapability && + (m->w[i + 1] == SpvCapabilityClipDistance || m->w[i + 1] == SpvCapabilityCullDistance)) + edit(m, i, INSTEAD); + } +} + +static void add_to_interface(struct module *m, uint32_t id) +{ + m->interface_add = realloc(m->interface_add, (m->interface_add_count + 1) * sizeof(uint32_t)); + m->interface_add[m->interface_add_count++] = id; +} + +/* A standalone distance variable becomes Private, and its decorations go. */ +static void privatize(struct module *m, const struct distance_var *var) +{ + size_t d = definition(m, var->id); + uint32_t storage, pointee; + pointer_parts(m, m->w[d + 1], &storage, &pointee); + m->w[d + 1] = pointer_type(m, SpvStorageClassPrivate, pointee); + m->w[d + 3] = SpvStorageClassPrivate; + mark_private(m, var->id); + /* Before SPIR-V 1.4 an entry point lists only its inputs and outputs. */ + if (m->version < VERSION_1_4) + m->interface_remove[var->id] = 1; + for (size_t i = 5; i < m->first_declaration; i += LENGTH(m->w[i])) { + if (OPCODE(m->w[i]) == SpvOpDecorate && m->w[i + 1] == var->id) + edit(m, i, INSTEAD); + } +} + +struct block_var { + uint32_t id; + uint32_t structure; + bool arrayed; + uint32_t array_length; +}; + +struct stand_in { + uint32_t block; + uint32_t member; + uint32_t variable; +}; + +/* Every variable of a block type with a distance member, and a Private stand-in per member. */ +static uint32_t make_stand_ins(struct module *m, const struct analysis *a, struct stand_in *stand_ins, uint32_t max) +{ + uint32_t count = 0; + for (size_t i = m->first_declaration; i < m->first_function; i += LENGTH(m->w[i])) { + if (OPCODE(m->w[i]) != SpvOpVariable) + continue; + uint32_t storage, pointee; + if (!pointer_parts(m, m->w[i + 1], &storage, &pointee)) + continue; + struct block_var var = {.id = m->w[i + 2], .structure = pointee}; + size_t d = definition(m, pointee); + if (d && OPCODE(m->w[d]) == SpvOpTypeArray) { + var.arrayed = true; + var.structure = m->w[d + 2]; + var.array_length = m->w[d + 3]; + } + for (uint32_t k = 0; k < a->member_count; k++) { + if (a->members[k].structure != var.structure) + continue; + if (count == max) { + m->failed = true; + return count; + } + size_t s = definition(m, var.structure); + uint32_t member_type = m->w[s + 2 + a->members[k].member]; + uint32_t type = member_type; + if (var.arrayed) { + const uint32_t operands[2] = {member_type, var.array_length}; + type = add_type(m, &m->globals, SpvOpTypeArray, operands, 2); + } + uint32_t variable = new_id(m); + EMIT(&m->globals, SpvOpVariable, pointer_type(m, SpvStorageClassPrivate, type), variable, + SpvStorageClassPrivate); + if (m->version >= VERSION_1_4) + add_to_interface(m, variable); + stand_ins[count++] = (struct stand_in){var.id, a->members[k].member, variable}; + } + } + return count; +} + +static const struct stand_in *find_stand_in(const struct module *m, const struct stand_in *stand_ins, uint32_t count, + uint32_t base, uint32_t member_index) +{ + uint32_t member; + if (!constant_value(m, member_index, &member)) + return NULL; + for (uint32_t i = 0; i < count; i++) { + if (stand_ins[i].block == base && stand_ins[i].member == member) + return &stand_ins[i]; + } + return NULL; +} + +static bool is_arrayed_block(const struct module *m, uint32_t variable) +{ + size_t d = definition(m, variable); + uint32_t storage, pointee; + if (!d || !pointer_parts(m, m->w[d + 1], &storage, &pointee)) + return false; + size_t p = definition(m, pointee); + return p && OPCODE(m->w[p]) == SpvOpTypeArray; +} + +/* + * One pass over the function bodies: accesses to a distance member go to its stand-in, and every + * pointer derived from a Private one becomes Private too. A Private pointer handed to a function + * or merged through a phi would need its types changed beyond this, so that fails the rewrite. + */ +static void retarget_accesses(struct module *m, const struct stand_in *stand_ins, uint32_t stand_in_count) +{ + for (size_t i = m->first_function; i < m->count; i += LENGTH(m->w[i])) { + uint32_t op = OPCODE(m->w[i]); + uint32_t length = LENGTH(m->w[i]); + switch (op) { + case SpvOpAccessChain: + case SpvOpInBoundsAccessChain: { + uint32_t base = m->w[i + 3]; + bool arrayed = is_arrayed_block(m, base); + uint32_t member_position = arrayed ? 5 : 4; + const struct stand_in *stand_in = + length > member_position + ? find_stand_in(m, stand_ins, stand_in_count, base, m->w[i + member_position]) + : NULL; + uint32_t storage, pointee; + if (stand_in) { + if (!pointer_parts(m, m->w[i + 1], &storage, &pointee)) { + m->failed = true; + break; + } + /* Before edit(): a new pointer type is an edit too, and would move the chain's buffer. */ + uint32_t type = pointer_type(m, SpvStorageClassPrivate, pointee); + struct words *chain = edit(m, i, INSTEAD); + push(chain, (length - 1) << 16 | op); + push(chain, type); + push(chain, m->w[i + 2]); + push(chain, stand_in->variable); + for (uint32_t k = 4; k < length; k++) { + if (k != member_position) + push(chain, m->w[i + k]); + } + mark_private(m, m->w[i + 2]); + } else if (is_private(m, base)) { + if (!pointer_parts(m, m->w[i + 1], &storage, &pointee)) { + m->failed = true; + break; + } + m->w[i + 1] = pointer_type(m, SpvStorageClassPrivate, pointee); + mark_private(m, m->w[i + 2]); + } + break; + } + case SpvOpPtrAccessChain: + case SpvOpCopyObject: + if (is_private(m, m->w[i + 3])) { + uint32_t storage, pointee; + if (!pointer_parts(m, m->w[i + 1], &storage, &pointee)) { + m->failed = true; + break; + } + m->w[i + 1] = pointer_type(m, SpvStorageClassPrivate, pointee); + mark_private(m, m->w[i + 2]); + } + break; + case SpvOpFunctionCall: + for (uint32_t k = 4; k < length; k++) { + if (is_private(m, m->w[i + k])) + m->failed = true; + } + break; + case SpvOpPhi: + case SpvOpSelect: + for (uint32_t k = 3; k < length; k++) { + if (is_private(m, m->w[i + k])) + m->failed = true; + } + break; + default: + break; + } + } +} + +static uint32_t string_words(const uint32_t *words, uint32_t available) +{ + for (uint32_t i = 0; i < available; i++) { + uint32_t word = words[i]; + if (!(word & 0xff) || !(word & 0xff00) || !(word & 0xff0000) || !(word & 0xff000000)) + return i + 1; + } + return available; +} + +static void rewrite_interfaces(struct module *m) +{ + for (size_t i = 5; i < m->first_declaration; i += LENGTH(m->w[i])) { + if (OPCODE(m->w[i]) != SpvOpEntryPoint) + continue; + uint32_t length = LENGTH(m->w[i]); + uint32_t first = 3 + string_words(&m->w[i + 3], length - 3); + struct words *entry = edit(m, i, INSTEAD); + push(entry, 0); + for (uint32_t k = 1; k < first; k++) + push(entry, m->w[i + k]); + for (uint32_t k = first; k < length; k++) { + uint32_t id = m->w[i + k]; + if (id >= m->original_bound || !m->interface_remove[id]) + push(entry, id); + } + for (size_t k = 0; k < m->interface_add_count; k++) + push(entry, m->interface_add[k]); + entry->data[0] = (uint32_t)entry->count << 16 | SpvOpEntryPoint; + } +} + +static void emit_edits(const struct module *m, struct words *out, size_t offset, enum place place, bool *replaced) +{ + for (size_t k = 0; k < m->edit_count; k++) { + const struct edit *e = &m->edits[k]; + if (e->offset != offset || e->place != place) + continue; + for (size_t j = 0; j < e->words.count; j++) + push(out, e->words.data[j]); + if (replaced) + *replaced = true; + } +} + +static bool finish(struct module *m, struct spirv_code *out) +{ + if (m->failed) + return false; + rewrite_interfaces(m); + struct words result = {0}; + for (int i = 0; i < 5; i++) + push(&result, m->w[i]); + result.data[3] = m->bound; + for (size_t i = 5; i < m->count; i += LENGTH(m->w[i])) { + if (i == m->first_function) { + for (size_t j = 0; j < m->globals.count; j++) + push(&result, m->globals.data[j]); + } + emit_edits(m, &result, i, BEFORE, NULL); + bool replaced = false; + emit_edits(m, &result, i, INSTEAD, &replaced); + if (!replaced) { + for (uint32_t j = 0; j < LENGTH(m->w[i]); j++) + push(&result, m->w[i + j]); + } + emit_edits(m, &result, i, AFTER, NULL); + } + out->words = result.data; + out->count = result.count; + return true; +} + +static void strip(struct module *m, const struct analysis *a, uint32_t keep) +{ + for (uint32_t i = 0; i < a->var_count; i++) { + if (a->vars[i].id != keep) + privatize(m, &a->vars[i]); + } + struct stand_in stand_ins[16]; + uint32_t count = make_stand_ins(m, a, stand_ins, 16); + retarget_accesses(m, stand_ins, count); + remove_capabilities(m); +} + +bool spirv_strip_distances(const uint32_t *words, size_t count, struct spirv_code *out) +{ + struct module m; + if (!load(&m, words, count)) + return false; + struct analysis a; + analyze(&m, &a); + strip(&m, &a, 0); + bool done = finish(&m, out); + unload(&m); + return done; +} + +static const struct distance_var *clip_output(const struct module *m, const struct analysis *a, uint32_t *length) +{ + for (uint32_t i = 0; i < a->var_count; i++) { + const struct distance_var *var = &a->vars[i]; + if (var->built_in != SpvBuiltInClipDistance || var->storage != SpvStorageClassOutput) + continue; + uint32_t storage, pointee; + pointer_parts(m, m->w[definition(m, var->id) + 1], &storage, &pointee); + size_t d = definition(m, pointee); + if (d && OPCODE(m->w[d]) == SpvOpTypeArray && is_float32(m, m->w[d + 2]) && + constant_value(m, m->w[d + 3], length)) + return var; + } + return NULL; +} + +void spirv_clip_info(const uint32_t *words, size_t count, enum clip_role role, struct clip_info *info) +{ + memset(info, 0, sizeof(*info)); + struct module m; + if (!load(&m, words, count)) { + info->locations_used = UINT32_MAX; + return; + } + struct analysis a; + analyze(&m, &a); + if (role == CLIP_PRODUCER) + info->has_clip = clip_output(&m, &a, &info->clip_count) != NULL; + uint32_t storage_wanted = role == CLIP_PRODUCER ? SpvStorageClassOutput : SpvStorageClassInput; + for (size_t i = 5; i < m.first_declaration; i += LENGTH(m.w[i])) { + if (OPCODE(m.w[i]) != SpvOpDecorate || LENGTH(m.w[i]) != 4 || m.w[i + 2] != SpvDecorationLocation) + continue; + size_t d = definition(&m, m.w[i + 1]); + uint32_t storage, pointee; + if (!d || OPCODE(m.w[d]) != SpvOpVariable || !pointer_parts(&m, m.w[d + 1], &storage, &pointee) || + storage != storage_wanted) + continue; + uint32_t end = m.w[i + 3] + location_span(&m, pointee, 0); + if (end > info->locations_used) + info->locations_used = end; + } + if (a.member_locations || m.failed) + info->locations_used = UINT32_MAX; + unload(&m); +} + +/* Fragment shader: read the clip distances at `location` and discard when any is negative. */ +static void add_clip_test(struct module *m, uint32_t location, uint32_t clip_count) +{ + uint32_t float_id = float_type(m); + uint32_t uint_id = uint_type(m); + uint32_t bool_id = bool_type(m); + const uint32_t array_operands[2] = {float_id, constant(m, uint_id, clip_count)}; + uint32_t array_id = add_type(m, &m->globals, SpvOpTypeArray, array_operands, 2); + uint32_t input_pointer = pointer_type(m, SpvStorageClassInput, array_id); + uint32_t element_pointer = pointer_type(m, SpvStorageClassInput, float_id); + uint32_t zero = constant(m, float_id, 0); + uint32_t indices[8]; + for (uint32_t i = 0; i < clip_count; i++) + indices[i] = constant(m, uint_id, i); + uint32_t input = new_id(m); + EMIT(&m->globals, SpvOpVariable, input_pointer, input, SpvStorageClassInput); + EMIT(edit(m, m->first_declaration, BEFORE), SpvOpDecorate, input, SpvDecorationLocation, location); + add_to_interface(m, input); + + for (size_t i = 5; i < m->first_declaration; i += LENGTH(m->w[i])) { + if (OPCODE(m->w[i]) != SpvOpEntryPoint || m->w[i + 1] != SpvExecutionModelFragment) + continue; + size_t function = definition(m, m->w[i + 2]); + if (!function) { + m->failed = true; + return; + } + size_t label = function + LENGTH(m->w[function]); + if (label >= m->count || OPCODE(m->w[label]) != SpvOpLabel) { + m->failed = true; + return; + } + /* Function variables must open the entry block, so the test goes after them. */ + size_t last = label; + for (size_t k = label + LENGTH(m->w[label]); k < m->count; k += LENGTH(m->w[k])) { + uint32_t op = OPCODE(m->w[k]); + if (op != SpvOpVariable && op != SpvOpLine && op != SpvOpNoLine) + break; + last = k; + } + uint32_t original_label = m->w[label + 1]; + uint32_t entry = new_id(m), kill = new_id(m), join = new_id(m); + EMIT(edit(m, label, INSTEAD), SpvOpLabel, entry); + struct words *test = edit(m, last, AFTER); + uint32_t any = 0; + for (uint32_t c = 0; c < clip_count; c++) { + uint32_t pointer = new_id(m), value = new_id(m), negative = new_id(m); + EMIT(test, SpvOpAccessChain, element_pointer, pointer, input, indices[c]); + EMIT(test, SpvOpLoad, float_id, value, pointer); + EMIT(test, SpvOpFOrdLessThan, bool_id, negative, value, zero); + if (any) { + uint32_t either = new_id(m); + EMIT(test, SpvOpLogicalOr, bool_id, either, any, negative); + any = either; + } else { + any = negative; + } + } + EMIT(test, SpvOpSelectionMerge, join, SpvSelectionControlMaskNone); + EMIT(test, SpvOpBranchConditional, any, kill, join); + EMIT(test, SpvOpLabel, kill); + push(test, 1u << 16 | SpvOpKill); + EMIT(test, SpvOpLabel, join); + EMIT(test, SpvOpBranch, original_label); + EMIT(test, SpvOpLabel, original_label); + } +} + +bool spirv_emulate_clip(const uint32_t *words, size_t count, enum clip_role role, uint32_t location, + uint32_t clip_count, struct spirv_code *out) +{ + if (clip_count == 0 || clip_count > 8) + return false; + struct module m; + if (!load(&m, words, count)) + return false; + struct analysis a; + analyze(&m, &a); + uint32_t keep = 0; + if (role == CLIP_PRODUCER) { + uint32_t length = 0; + const struct distance_var *clip = clip_output(&m, &a, &length); + if (!clip || length != clip_count) { + unload(&m); + return false; + } + keep = clip->id; + m.w[clip->decoration + 2] = SpvDecorationLocation; + m.w[clip->decoration + 3] = location; + } + strip(&m, &a, keep); + if (role == CLIP_CONSUMER) + add_clip_test(&m, location, clip_count); + bool done = finish(&m, out); + unload(&m); + return done; +} diff --git a/tools/venus/layer/trace.c b/tools/venus/layer/trace.c new file mode 100644 index 000000000..f02bab5c9 --- /dev/null +++ b/tools/venus/layer/trace.c @@ -0,0 +1,233 @@ +/* + * DROIDDECK_MALI_COMPAT_TRACE=1: the physical-device queries an application makes before + * vkCreateDevice, logged as they pass through. Zink on Venus over an older Mali driver gave up + * after vkGetPhysicalDeviceProperties2 without a word (its release builds print nothing there), + * so the last query in the log is where it stopped. + */ +#include "mali_compat.h" + +#include +#include +#include +#include + +static uint32_t format_queries; + +static bool format_worth_logging(uint32_t n, VkFormatFeatureFlags linear, VkFormatFeatureFlags optimal, VkFormatFeatureFlags buffer) +{ + return n == 1 || n % 25 == 0 || (!linear && !optimal && !buffer); +} + +static VKAPI_ATTR VkResult VKAPI_CALL TraceEnumeratePhysicalDevices(VkInstance instance, uint32_t *count, VkPhysicalDevice *devices) +{ + struct instance *inst = instance_from_handle(instance); + VkResult r = inst->EnumeratePhysicalDevices(instance, count, devices); + layer_log("trace: vkEnumeratePhysicalDevices (%s): %d, %u device(s)", devices ? "list" : "count", (int)r, count ? *count : 0); + return r; +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceMemoryProperties(VkPhysicalDevice pd, VkPhysicalDeviceMemoryProperties *p) +{ + struct instance *inst = instance_from_handle(pd); + inst->GetPhysicalDeviceMemoryProperties(pd, p); + char types[384] = ""; + size_t o = 0; + for (uint32_t i = 0; i < p->memoryTypeCount && o + 16 < sizeof(types); i++) + o += (size_t)snprintf(types + o, sizeof(types) - o, " %u:h%u/0x%x", i, p->memoryTypes[i].heapIndex, + (unsigned)p->memoryTypes[i].propertyFlags); + layer_log("trace: vkGetPhysicalDeviceMemoryProperties: %u heaps, %u types:%s", p->memoryHeapCount, p->memoryTypeCount, types); +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceMemoryProperties2(VkPhysicalDevice pd, VkPhysicalDeviceMemoryProperties2 *p) +{ + struct instance *inst = instance_from_handle(pd); + inst->GetPhysicalDeviceMemoryProperties2(pd, p); + char chain[160] = ""; + size_t o = 0; + for (VkBaseOutStructure *s = p->pNext; s && o + 16 < sizeof(chain); s = s->pNext) + o += (size_t)snprintf(chain + o, sizeof(chain) - o, " %d", (int)s->sType); + layer_log("trace: vkGetPhysicalDeviceMemoryProperties2: %u heaps, %u types, chain:%s", p->memoryProperties.memoryHeapCount, + p->memoryProperties.memoryTypeCount, chain); +} + +static void log_queue_families(const char *what, uint32_t n, const VkQueueFamilyProperties *(*at)(const void *, uint32_t), const void *arr) +{ + char fams[256] = ""; + size_t o = 0; + for (uint32_t i = 0; arr && i < n && o + 24 < sizeof(fams); i++) { + const VkQueueFamilyProperties *q = at(arr, i); + o += (size_t)snprintf(fams + o, sizeof(fams) - o, " %u:0x%x*%u", i, (unsigned)q->queueFlags, q->queueCount); + } + layer_log("trace: %s: %u famil%s%s", what, n, n == 1 ? "y" : "ies", fams); +} + +static const VkQueueFamilyProperties *qf1(const void *a, uint32_t i) { return &((const VkQueueFamilyProperties *)a)[i]; } +static const VkQueueFamilyProperties *qf2(const void *a, uint32_t i) { return &((const VkQueueFamilyProperties2 *)a)[i].queueFamilyProperties; } + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceQueueFamilyProperties(VkPhysicalDevice pd, uint32_t *n, VkQueueFamilyProperties *p) +{ + struct instance *inst = instance_from_handle(pd); + inst->GetPhysicalDeviceQueueFamilyProperties(pd, n, p); + log_queue_families("vkGetPhysicalDeviceQueueFamilyProperties", n ? *n : 0, qf1, p); +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceQueueFamilyProperties2(VkPhysicalDevice pd, uint32_t *n, VkQueueFamilyProperties2 *p) +{ + struct instance *inst = instance_from_handle(pd); + inst->GetPhysicalDeviceQueueFamilyProperties2(pd, n, p); + log_queue_families("vkGetPhysicalDeviceQueueFamilyProperties2", n ? *n : 0, qf2, p); +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceFormatProperties(VkPhysicalDevice pd, VkFormat format, VkFormatProperties *p) +{ + struct instance *inst = instance_from_handle(pd); + if (inst->active) GetPhysicalDeviceFormatProperties(pd, format, p); + else inst->GetPhysicalDeviceFormatProperties(pd, format, p); + uint32_t n = __atomic_add_fetch(&format_queries, 1, __ATOMIC_RELAXED); + if (format_worth_logging(n, p->linearTilingFeatures, p->optimalTilingFeatures, p->bufferFeatures)) + layer_log("trace: vkGetPhysicalDeviceFormatProperties #%u: format %d linear 0x%x optimal 0x%x buffer 0x%x", n, (int)format, + (unsigned)p->linearTilingFeatures, (unsigned)p->optimalTilingFeatures, (unsigned)p->bufferFeatures); +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceFormatProperties2(VkPhysicalDevice pd, VkFormat format, VkFormatProperties2 *p) +{ + struct instance *inst = instance_from_handle(pd); + if (inst->active) GetPhysicalDeviceFormatProperties2(pd, format, p); + else inst->GetPhysicalDeviceFormatProperties2(pd, format, p); + uint32_t n = __atomic_add_fetch(&format_queries, 1, __ATOMIC_RELAXED); + const VkFormatProperties *f = &p->formatProperties; + if (format_worth_logging(n, f->linearTilingFeatures, f->optimalTilingFeatures, f->bufferFeatures)) + layer_log("trace: vkGetPhysicalDeviceFormatProperties2 #%u: format %d linear 0x%x optimal 0x%x buffer 0x%x", n, (int)format, + (unsigned)f->linearTilingFeatures, (unsigned)f->optimalTilingFeatures, (unsigned)f->bufferFeatures); +} + +static VKAPI_ATTR VkResult VKAPI_CALL TraceGetPhysicalDeviceImageFormatProperties(VkPhysicalDevice pd, VkFormat format, VkImageType type, + VkImageTiling tiling, VkImageUsageFlags usage, + VkImageCreateFlags flags, VkImageFormatProperties *p) +{ + struct instance *inst = instance_from_handle(pd); + VkResult r = inst->active ? GetPhysicalDeviceImageFormatProperties(pd, format, type, tiling, usage, flags, p) + : inst->GetPhysicalDeviceImageFormatProperties(pd, format, type, tiling, usage, flags, p); + if (r != VK_SUCCESS) + layer_log("trace: vkGetPhysicalDeviceImageFormatProperties: format %d type %d tiling %d usage 0x%x flags 0x%x -> %d", (int)format, + (int)type, (int)tiling, (unsigned)usage, (unsigned)flags, (int)r); + return r; +} + +static VKAPI_ATTR VkResult VKAPI_CALL TraceGetPhysicalDeviceImageFormatProperties2(VkPhysicalDevice pd, const VkPhysicalDeviceImageFormatInfo2 *info, + VkImageFormatProperties2 *p) +{ + struct instance *inst = instance_from_handle(pd); + VkResult r = inst->active ? GetPhysicalDeviceImageFormatProperties2(pd, info, p) : inst->GetPhysicalDeviceImageFormatProperties2(pd, info, p); + if (r != VK_SUCCESS) + layer_log("trace: vkGetPhysicalDeviceImageFormatProperties2: format %d type %d tiling %d usage 0x%x flags 0x%x -> %d", (int)info->format, + (int)info->type, (int)info->tiling, (unsigned)info->usage, (unsigned)info->flags, (int)r); + return r; +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceExternalBufferProperties(VkPhysicalDevice pd, const VkPhysicalDeviceExternalBufferInfo *info, + VkExternalBufferProperties *p) +{ + struct instance *inst = instance_from_handle(pd); + inst->GetPhysicalDeviceExternalBufferProperties(pd, info, p); + layer_log("trace: vkGetPhysicalDeviceExternalBufferProperties: handle 0x%x usage 0x%x -> features 0x%x", (unsigned)info->handleType, + (unsigned)info->usage, (unsigned)p->externalMemoryProperties.externalMemoryFeatures); +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceExternalSemaphoreProperties(VkPhysicalDevice pd, + const VkPhysicalDeviceExternalSemaphoreInfo *info, + VkExternalSemaphoreProperties *p) +{ + struct instance *inst = instance_from_handle(pd); + inst->GetPhysicalDeviceExternalSemaphoreProperties(pd, info, p); + layer_log("trace: vkGetPhysicalDeviceExternalSemaphoreProperties: handle 0x%x -> features 0x%x", (unsigned)info->handleType, + (unsigned)p->externalSemaphoreFeatures); +} + +static VKAPI_ATTR void VKAPI_CALL TraceGetPhysicalDeviceExternalFenceProperties(VkPhysicalDevice pd, const VkPhysicalDeviceExternalFenceInfo *info, + VkExternalFenceProperties *p) +{ + struct instance *inst = instance_from_handle(pd); + inst->GetPhysicalDeviceExternalFenceProperties(pd, info, p); + layer_log("trace: vkGetPhysicalDeviceExternalFenceProperties: handle 0x%x -> features 0x%x", (unsigned)info->handleType, + (unsigned)p->externalFenceFeatures); +} + +void trace_extension_names(const VkExtensionProperties *ext, uint32_t n) +{ + char line[400]; + size_t o = 0; + line[0] = 0; + for (uint32_t i = 0; i < n; i++) { + size_t len = strlen(ext[i].extensionName); + if (o && o + len + 2 > sizeof(line)) { + layer_log("trace: extensions:%s", line); + o = 0; + line[0] = 0; + } + o += (size_t)snprintf(line + o, sizeof(line) - o, " %s", ext[i].extensionName); + } + if (o) layer_log("trace: extensions:%s", line); +} + +void trace_load(struct instance *inst) +{ +#define T_LOAD(name) inst->name = (PFN_vk##name)inst->gipa(inst->handle, "vk" #name); +#define T_LOAD_KHR(name) \ + if (!inst->name) \ + inst->name = (PFN_vk##name)inst->gipa(inst->handle, "vk" #name "KHR"); + T_LOAD(EnumeratePhysicalDevices) + T_LOAD(GetPhysicalDeviceMemoryProperties2) + T_LOAD_KHR(GetPhysicalDeviceMemoryProperties2) + T_LOAD(GetPhysicalDeviceQueueFamilyProperties) + T_LOAD(GetPhysicalDeviceQueueFamilyProperties2) + T_LOAD_KHR(GetPhysicalDeviceQueueFamilyProperties2) + T_LOAD(GetPhysicalDeviceExternalBufferProperties) + T_LOAD_KHR(GetPhysicalDeviceExternalBufferProperties) + T_LOAD(GetPhysicalDeviceExternalSemaphoreProperties) + T_LOAD_KHR(GetPhysicalDeviceExternalSemaphoreProperties) + T_LOAD(GetPhysicalDeviceExternalFenceProperties) + T_LOAD_KHR(GetPhysicalDeviceExternalFenceProperties) +#undef T_LOAD +#undef T_LOAD_KHR +} + +struct trace_hook { + const char *name; + PFN_vkVoidFunction function; + size_t next_offset; +}; + +#define TH(fn, field) {"vk" #fn, (PFN_vkVoidFunction)Trace##field, offsetof(struct instance, field)} + +static const struct trace_hook trace_hooks[] = { + TH(EnumeratePhysicalDevices, EnumeratePhysicalDevices), + TH(GetPhysicalDeviceMemoryProperties, GetPhysicalDeviceMemoryProperties), + TH(GetPhysicalDeviceMemoryProperties2, GetPhysicalDeviceMemoryProperties2), + TH(GetPhysicalDeviceMemoryProperties2KHR, GetPhysicalDeviceMemoryProperties2), + TH(GetPhysicalDeviceQueueFamilyProperties, GetPhysicalDeviceQueueFamilyProperties), + TH(GetPhysicalDeviceQueueFamilyProperties2, GetPhysicalDeviceQueueFamilyProperties2), + TH(GetPhysicalDeviceQueueFamilyProperties2KHR, GetPhysicalDeviceQueueFamilyProperties2), + TH(GetPhysicalDeviceFormatProperties, GetPhysicalDeviceFormatProperties), + TH(GetPhysicalDeviceFormatProperties2, GetPhysicalDeviceFormatProperties2), + TH(GetPhysicalDeviceFormatProperties2KHR, GetPhysicalDeviceFormatProperties2), + TH(GetPhysicalDeviceImageFormatProperties, GetPhysicalDeviceImageFormatProperties), + TH(GetPhysicalDeviceImageFormatProperties2, GetPhysicalDeviceImageFormatProperties2), + TH(GetPhysicalDeviceImageFormatProperties2KHR, GetPhysicalDeviceImageFormatProperties2), + TH(GetPhysicalDeviceExternalBufferProperties, GetPhysicalDeviceExternalBufferProperties), + TH(GetPhysicalDeviceExternalBufferPropertiesKHR, GetPhysicalDeviceExternalBufferProperties), + TH(GetPhysicalDeviceExternalSemaphoreProperties, GetPhysicalDeviceExternalSemaphoreProperties), + TH(GetPhysicalDeviceExternalSemaphorePropertiesKHR, GetPhysicalDeviceExternalSemaphoreProperties), + TH(GetPhysicalDeviceExternalFenceProperties, GetPhysicalDeviceExternalFenceProperties), + TH(GetPhysicalDeviceExternalFencePropertiesKHR, GetPhysicalDeviceExternalFenceProperties), +}; + +PFN_vkVoidFunction trace_hook(struct instance *inst, const char *name) +{ + for (size_t i = 0; i < sizeof(trace_hooks) / sizeof(trace_hooks[0]); i++) { + if (strcmp(name, trace_hooks[i].name)) continue; + void *next = *(void **)((char *)inst + trace_hooks[i].next_offset); + return next ? trace_hooks[i].function : NULL; + } + return NULL; +} diff --git a/tools/venus/measure-present.patch b/tools/venus/measure-present.patch new file mode 100644 index 000000000..5d8eb87cf --- /dev/null +++ b/tools/venus/measure-present.patch @@ -0,0 +1,153 @@ +diff --git a/src/virtio/vulkan/vn_wsi.c b/src/virtio/vulkan/vn_wsi.c +index f0b41b1..5c08312 100644 +--- a/src/virtio/vulkan/vn_wsi.c ++++ b/src/virtio/vulkan/vn_wsi.c +@@ -23,6 +23,33 @@ + #define DRM_FORMAT_MOD_LINEAR 0 + #endif + ++/* MEASURE: async present costs, logged every 120 presents. */ ++static struct { ++ uint32_t presents, fence_waits, flushes; ++ int64_t present_sum, present_max, fence_sum, fence_max, flush_sum, flush_max; ++} vn_measure; ++ ++#define VN_MEASURE_ADD(name, ns) \ ++ do { \ ++ vn_measure.name##_sum += (ns); \ ++ vn_measure.name##_max = MAX2(vn_measure.name##_max, (ns)); \ ++ } while (0) ++ ++static void ++vn_measure_present(int64_t ns) ++{ ++ VN_MEASURE_ADD(present, ns); ++ if (++vn_measure.presents < 120) ++ return; ++ fprintf(stderr, "MEASURE vn: present thread %.2f/%.2f ms | fence wait %.2f/%.2f ms (%u) | app blocked in flush %.2f/%.2f ms (%u)\n", ++ vn_measure.present_sum / 120 / 1e6, vn_measure.present_max / 1e6, ++ vn_measure.fence_waits ? vn_measure.fence_sum / vn_measure.fence_waits / 1e6 : 0.0, ++ vn_measure.fence_max / 1e6, vn_measure.fence_waits, ++ vn_measure.flushes ? vn_measure.flush_sum / vn_measure.flushes / 1e6 : 0.0, ++ vn_measure.flush_max / 1e6, vn_measure.flushes); ++ memset(&vn_measure, 0, sizeof(vn_measure)); ++} ++ + /* The common WSI support makes some assumptions about the driver. + * + * In wsi_device_init, it assumes VK_EXT_pci_bus_info is available. In +@@ -331,6 +358,7 @@ vn_wsi_memory_type_index(const struct vn_device *dev, + !(type_bits & BITFIELD_BIT(readback_type))) + return alloc_info->memoryTypeIndex; + ++ fprintf(stderr, "MEASURE vn: WSI readback buffer in memory type %d\n", readback_type); + return readback_type; + } + +@@ -479,8 +507,11 @@ vn_wsi_fence_wait(VkQueue queue_handle) + simple_mtx_unlock(&queue->async_present.queue_mutex); + vn_wsi_chains_unlock(dev, queue->async_present.info, /*all=*/false); + ++ int64_t measure_start = os_time_get_nano(); + result = vn_WaitForFences(dev_handle, 1, &queue->async_present.fence, true, + UINT64_MAX); ++ VN_MEASURE_ADD(fence, os_time_get_nano() - measure_start); ++ vn_measure.fence_waits++; + + vn_wsi_chains_lock(dev, queue->async_present.info, /*all=*/false); + if (can_unlock_queue) +@@ -537,8 +568,11 @@ vn_wsi_flush(VkQueue queue_handle) + /* Being able to acquire the lock ensures async present queue access + * has completed. + */ ++ int64_t measure_start = os_time_get_nano(); + simple_mtx_lock(&queue->async_present.queue_mutex); + simple_mtx_unlock(&queue->async_present.queue_mutex); ++ VN_MEASURE_ADD(flush, os_time_get_nano() - measure_start); ++ vn_measure.flushes++; + } + + static VkPresentInfoKHR * +@@ -811,9 +845,11 @@ vn_wsi_present_thread(void *data) + + queue->async_present.pending = false; + ++ int64_t measure_start = os_time_get_nano(); + queue->async_present.result = + wsi_common_queue_present(queue_vk->base.device->physical->wsi_device, + queue_vk, queue->async_present.info); ++ vn_measure_present(os_time_get_nano() - measure_start); + + vn_wsi_chains_unlock(dev, queue->async_present.info, /*all=*/true); + simple_mtx_unlock(&queue->async_present.queue_mutex); +diff --git a/src/vulkan/wsi/wsi_common_wayland.c b/src/vulkan/wsi/wsi_common_wayland.c +index 047c8b3..a153a39 100644 +--- a/src/vulkan/wsi/wsi_common_wayland.c ++++ b/src/vulkan/wsi/wsi_common_wayland.c +@@ -3277,6 +3277,38 @@ set_timestamp(struct wsi_wl_swapchain *chain, + return true; + } + ++/* MEASURE: per-frame cost of the shm copy, logged every 120 presents. */ ++static struct { ++ uint32_t frames; ++ int64_t last_present, interval_sum, interval_max, copy_sum, copy_max, total_sum, total_max; ++} wl_measure; ++ ++static void ++wl_measure_frame(struct wsi_wl_swapchain *chain, int64_t start, int64_t copy_ns, int64_t end) ++{ ++ if (wl_measure.last_present) { ++ int64_t interval = start - wl_measure.last_present; ++ wl_measure.interval_sum += interval; ++ wl_measure.interval_max = MAX2(wl_measure.interval_max, interval); ++ } ++ wl_measure.last_present = start; ++ wl_measure.copy_sum += copy_ns; ++ wl_measure.copy_max = MAX2(wl_measure.copy_max, copy_ns); ++ wl_measure.total_sum += end - start; ++ wl_measure.total_max = MAX2(wl_measure.total_max, end - start); ++ if (++wl_measure.frames < 120) ++ return; ++ fprintf(stderr, "MEASURE wl: %ux%u type %d | interval %.2f/%.2f ms | copy %.2f/%.2f ms (%u KB) | present %.2f/%.2f ms\n", ++ chain->extent.width, chain->extent.height, chain->buffer_type, ++ wl_measure.interval_sum / 119 / 1e6, wl_measure.interval_max / 1e6, ++ wl_measure.copy_sum / 120 / 1e6, wl_measure.copy_max / 1e6, ++ chain->images[0].base.row_pitches[0] * chain->extent.height / 1024, ++ wl_measure.total_sum / 120 / 1e6, wl_measure.total_max / 1e6); ++ int64_t last = wl_measure.last_present; ++ memset(&wl_measure, 0, sizeof(wl_measure)); ++ wl_measure.last_present = last; ++} ++ + static VkResult + wsi_wl_swapchain_queue_present(struct wsi_swapchain *wsi_chain, + uint32_t image_index, +@@ -3286,6 +3318,7 @@ wsi_wl_swapchain_queue_present(struct wsi_swapchain *wsi_chain, + struct wsi_wl_swapchain *chain = (struct wsi_wl_swapchain *)wsi_chain; + bool timestamped = false; + bool queue_dispatched = false; ++ int64_t measure_start = os_time_get_nano(), measure_copy = 0; + + MESA_TRACE_FUNC_FLOW(&chain->images[image_index].wayland_buffer.flow); + +@@ -3314,9 +3347,11 @@ wsi_wl_swapchain_queue_present(struct wsi_swapchain *wsi_chain, + + if (chain->buffer_type == WSI_WL_BUFFER_SHM_MEMCPY) { + struct wsi_wl_image *image = &chain->images[image_index]; ++ int64_t copy_start = os_time_get_nano(); + wsi_image_invalidate_cpu_map(&chain->base, &image->base); + memcpy(image->shm_ptr, image->base.cpu_map, + image->base.row_pitches[0] * chain->extent.height); ++ measure_copy = os_time_get_nano() - copy_start; + } + + VkResult ret = wsi_wl_swapchain_update_colorspace(chain); +@@ -3515,6 +3550,7 @@ wsi_wl_swapchain_queue_present(struct wsi_swapchain *wsi_chain, + + memset(&chain->timing_request, 0, sizeof(chain->timing_request)); + ++ wl_measure_frame(chain, measure_start, measure_copy, os_time_get_nano()); + return VK_SUCCESS; + } + diff --git a/tools/venus/memtypes.c b/tools/venus/memtypes.c new file mode 100644 index 000000000..39520298a --- /dev/null +++ b/tools/venus/memtypes.c @@ -0,0 +1,40 @@ +// Prints the system Vulkan driver's memory types and heaps, as Android apps see them; Venus +// rewrites them for the guest. Build in WSL and run over ADB (from Git Bash, MSYS_NO_PATHCONV=1): +// /home/delo/android-sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android31-clang \ +// -O1 -o memtypes memtypes.c -lvulkan +// adb push memtypes /data/local/tmp/ && adb shell "chmod 755 /data/local/tmp/memtypes && /data/local/tmp/memtypes" +#include +#include + +int main(void) +{ + VkApplicationInfo app = { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .apiVersion = VK_API_VERSION_1_1 }; + VkInstanceCreateInfo ici = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app }; + VkInstance inst; + if (vkCreateInstance(&ici, NULL, &inst) != VK_SUCCESS) { + printf("vkCreateInstance failed\n"); + return 1; + } + uint32_t count = 1; + VkPhysicalDevice pd; + vkEnumeratePhysicalDevices(inst, &count, &pd); + VkPhysicalDeviceProperties props; + vkGetPhysicalDeviceProperties(pd, &props); + printf("%s\n", props.deviceName); + VkPhysicalDeviceMemoryProperties mp; + vkGetPhysicalDeviceMemoryProperties(pd, &mp); + for (uint32_t i = 0; i < mp.memoryHeapCount; i++) + printf("heap %u: %llu MiB flags 0x%x\n", i, (unsigned long long)(mp.memoryHeaps[i].size >> 20), mp.memoryHeaps[i].flags); + for (uint32_t i = 0; i < mp.memoryTypeCount; i++) { + VkMemoryPropertyFlags f = mp.memoryTypes[i].propertyFlags; + printf("type %u: heap %u flags 0x%x%s%s%s%s%s%s\n", i, mp.memoryTypes[i].heapIndex, f, + f & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT ? " DEVICE_LOCAL" : "", + f & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT ? " HOST_VISIBLE" : "", + f & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT ? " HOST_COHERENT" : "", + f & VK_MEMORY_PROPERTY_HOST_CACHED_BIT ? " HOST_CACHED" : "", + f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT ? " LAZY" : "", + f & VK_MEMORY_PROPERTY_PROTECTED_BIT ? " PROTECTED" : ""); + } + vkDestroyInstance(inst, NULL); + return 0; +} diff --git a/tools/venus/mesa-venus-vtest.patch b/tools/venus/mesa-venus-vtest.patch new file mode 100644 index 000000000..e2b569975 --- /dev/null +++ b/tools/venus/mesa-venus-vtest.patch @@ -0,0 +1,541 @@ +diff --git a/src/virtio/vulkan/vn_device.c b/src/virtio/vulkan/vn_device.c +index f42b9f9..6ed3051 100644 +--- a/src/virtio/vulkan/vn_device.c ++++ b/src/virtio/vulkan/vn_device.c +@@ -45,6 +45,7 @@ vn_queue_fini(struct vn_queue *queue) + if (queue->wait_fence != VK_NULL_HANDLE) { + vn_DestroyFence(dev_handle, queue->wait_fence, NULL); + } ++ simple_mtx_destroy(&queue->submit_mutex); + vn_cached_storage_fini(&queue->storage); + vn_queue_base_fini(&queue->base); + } +@@ -65,6 +66,7 @@ vn_queue_init(struct vn_device *dev, + queue->index = array_index; + + vn_cached_storage_init(&queue->storage, &dev->base.vk.alloc); ++ simple_mtx_init(&queue->submit_mutex, mtx_plain); + + if (dev->physical_device->emulate_second_queue == + queue_info->queueFamilyIndex && +diff --git a/src/virtio/vulkan/vn_device_memory.c b/src/virtio/vulkan/vn_device_memory.c +index 8b6eb4c..be146b6 100644 +--- a/src/virtio/vulkan/vn_device_memory.c ++++ b/src/virtio/vulkan/vn_device_memory.c +@@ -93,11 +93,12 @@ vn_device_memory_bo_init(struct vn_device *dev, struct vn_device_memory *mem) + } + + const struct vk_device_memory *mem_vk = &mem->base.vk; +- const VkMemoryType *mem_type = &dev->physical_device->memory_properties +- .memoryTypes[mem_vk->memory_type_index]; ++ /* The renderer's flags: WSI maps a type the app sees as not mappable. */ ++ const VkMemoryPropertyFlags flags = ++ dev->physical_device->renderer_memory_flags[mem_vk->memory_type_index]; + return vn_renderer_bo_create_from_device_memory( +- dev->renderer, batch, mem_vk->size, mem->base.id, +- mem_type->propertyFlags, mem_vk->export_handle_types, &mem->base_bo); ++ dev->renderer, batch, mem_vk->size, mem->base.id, flags, ++ mem_vk->export_handle_types, &mem->base_bo); + } + + static inline void +@@ -374,6 +375,14 @@ vn_AllocateMemory(VkDevice device, + { + struct vn_device *dev = vn_device_from_handle(device); + ++ VkMemoryAllocateInfo local_alloc_info; ++ const uint32_t type_index = vn_wsi_memory_type_index(dev, pAllocateInfo); ++ if (type_index != pAllocateInfo->memoryTypeIndex) { ++ local_alloc_info = *pAllocateInfo; ++ local_alloc_info.memoryTypeIndex = type_index; ++ pAllocateInfo = &local_alloc_info; ++ } ++ + struct vn_device_memory *mem = vk_device_memory_create( + &dev->base.vk, pAllocateInfo, pAllocator, sizeof(*mem)); + if (!mem) +diff --git a/src/virtio/vulkan/vn_physical_device.c b/src/virtio/vulkan/vn_physical_device.c +index f3581aa..c9c50e5 100644 +--- a/src/virtio/vulkan/vn_physical_device.c ++++ b/src/virtio/vulkan/vn_physical_device.c +@@ -982,6 +982,7 @@ vn_physical_device_init_memory_properties( + ring, vn_physical_device_to_handle(physical_dev), &props2); + + physical_dev->memory_properties = props2.memoryProperties; ++ physical_dev->wsi_readback_memory_type = -1; + + /* Kernel makes every mapping coherent. If a memory type is truly + * incoherent, it's better to remove the host-visible flag than silently +@@ -996,12 +997,18 @@ vn_physical_device_init_memory_properties( + VkMemoryPropertyFlags *flags = &props->memoryTypes[i].propertyFlags; + const bool coherent = *flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + const bool cached = *flags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT; ++ physical_dev->renderer_memory_flags[i] = *flags; + if (coherent) { + if (first_coherent == VK_MAX_MEMORY_TYPES) + first_coherent = i; + if (cached) + has_coherent_cached = true; + } else if (cached) { ++ /* vtest maps the renderer's dma-buf itself and can sync the CPU ++ * cache around a read, which WSI does before copying a frame out. ++ */ ++ if (VN_DEBUG(VTEST) && physical_dev->wsi_readback_memory_type < 0) ++ physical_dev->wsi_readback_memory_type = i; + *flags &= ~(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_CACHED_BIT); + } +@@ -1060,7 +1067,9 @@ vn_physical_device_init_external_fence_handles( + { + physical_dev->external_fence_handles = 0; + +- if (physical_dev->instance->renderer->info.has_external_sync) { ++ /* Over vtest an export waits on the CPU and returns -1 (vn_sync.c). */ ++ if (physical_dev->instance->renderer->info.has_external_sync || ++ VN_DEBUG(VTEST)) { + #if !DETECT_OS_WINDOWS + physical_dev->external_fence_handles = + VK_EXTERNAL_FENCE_HANDLE_TYPE_SYNC_FD_BIT; +@@ -1095,7 +1104,8 @@ vn_physical_device_init_external_semaphore_handles( + physical_dev->external_binary_semaphore_handles = 0; + physical_dev->external_timeline_semaphore_handles = 0; + +- if (physical_dev->instance->renderer->info.has_external_sync) { ++ if (physical_dev->instance->renderer->info.has_external_sync || ++ VN_DEBUG(VTEST)) { + #if !DETECT_OS_WINDOWS + physical_dev->external_binary_semaphore_handles = + VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT; +@@ -1173,7 +1183,8 @@ vn_physical_device_get_native_extensions( + exts->KHR_map_memory2 = true; + #if !DETECT_OS_WINDOWS + exts->EXT_map_memory_placed = true; +- exts->EXT_physical_device_drm = true; ++ /* Over vtest there is no DRM device behind Venus. */ ++ exts->EXT_physical_device_drm = !VN_DEBUG(VTEST); + #endif + /* use common implementation */ + exts->EXT_tooling_info = true; +diff --git a/src/virtio/vulkan/vn_physical_device.h b/src/virtio/vulkan/vn_physical_device.h +index 2c2f4a9..c002a84 100644 +--- a/src/virtio/vulkan/vn_physical_device.h ++++ b/src/virtio/vulkan/vn_physical_device.h +@@ -112,6 +112,12 @@ struct vn_physical_device { + int emulate_second_queue; + + VkPhysicalDeviceMemoryProperties memory_properties; ++ /* The renderer's flags, before memory_properties hides the host-visible ++ * bit of cached types that are not coherent. ++ */ ++ VkMemoryPropertyFlags renderer_memory_flags[VK_MAX_MEMORY_TYPES]; ++ /* A hidden cached type for the CPU copy of WSI images, or -1. */ ++ int wsi_readback_memory_type; + + struct { + VkExternalMemoryHandleTypeFlagBits renderer_handle_type; +diff --git a/src/virtio/vulkan/vn_queue.c b/src/virtio/vulkan/vn_queue.c +index 057842a..dee1244 100644 +--- a/src/virtio/vulkan/vn_queue.c ++++ b/src/virtio/vulkan/vn_queue.c +@@ -1049,11 +1049,16 @@ vn_queue_submission_init_syncs(struct vn_queue_submission *submit) + assert(fence->payload->type == VN_SYNC_TYPE_SYNC); + + submit->temp.syncs[sync_index] = fence->payload->sync; +- submit->temp.sync_vals[sync_index] = 1; ++ submit->temp.sync_vals[sync_index++] = 1; + + /* fence is backed by renderer sync without renderer object */ + submit->fence_handle = VK_NULL_HANDLE; + } ++ ++ /* A signal semaphore counted for an imported sync_file has no renderer ++ * sync to fill in, and the slot would hand vtest stale storage. ++ */ ++ submit->sync_count = sync_index; + } + + static VkResult +@@ -1248,9 +1253,13 @@ vn_queue_submit(struct vn_queue_submission *submit) + struct vn_device *dev = vn_device_from_vk(queue->base.vk.base.device); + VkResult result; + ++ simple_mtx_lock(&queue->submit_mutex); ++ + result = vn_queue_submission_prepare_submit(submit); +- if (result != VK_SUCCESS) ++ if (result != VK_SUCCESS) { ++ simple_mtx_unlock(&queue->submit_mutex); + return vn_error(dev->instance, result); ++ } + + result = vn_queue_submission_do_submit(submit); + if (result != VK_SUCCESS) +@@ -1260,6 +1269,7 @@ vn_queue_submit(struct vn_queue_submission *submit) + + out_cleanup: + vn_queue_submission_cleanup(submit); ++ simple_mtx_unlock(&queue->submit_mutex); + return vn_result(dev->instance, result); + } + +diff --git a/src/virtio/vulkan/vn_queue.h b/src/virtio/vulkan/vn_queue.h +index 3ea6d9d..4160e56 100644 +--- a/src/virtio/vulkan/vn_queue.h ++++ b/src/virtio/vulkan/vn_queue.h +@@ -52,6 +52,11 @@ struct vn_queue { + /* for vn_queue_submission storage */ + struct vn_cached_storage storage; + ++ /* Serializes submissions. The async present thread submits to this queue ++ * while the application does, after vn_wsi_fence_wait drops queue_mutex. ++ */ ++ simple_mtx_t submit_mutex; ++ + /* for async queue present */ + struct { + /* Protects VkQueue host access except async present states. */ +diff --git a/src/virtio/vulkan/vn_renderer_vtest.c b/src/virtio/vulkan/vn_renderer_vtest.c +index fc59d32..c7b20f5 100644 +--- a/src/virtio/vulkan/vn_renderer_vtest.c ++++ b/src/virtio/vulkan/vn_renderer_vtest.c +@@ -13,9 +13,11 @@ + #include + #include + #include ++#include + #include + #include + ++#include "drm-uapi/dma-buf.h" + #include "drm-uapi/virtgpu_drm.h" + #include "util/os_file.h" + #include "util/os_misc.h" +@@ -38,8 +40,10 @@ struct vtest_bo { + struct vn_renderer_bo base; + + uint32_t blob_flags; +- /* might be closed after mmap */ ++ /* might be closed after mmap, but not when the memory is incoherent */ + int res_fd; ++ /* the CPU cache is synced through the dma-buf on flush and invalidate */ ++ bool incoherent; + }; + + struct vtest_sync { +@@ -607,13 +611,35 @@ vtest_sync_create(struct vn_renderer *renderer, + return VK_SUCCESS; + } + ++/* The guest maps the renderer's dma-buf, so a CPU cache sync on it covers the ++ * mapping. Coherent memory needs none. ++ */ ++static void ++vtest_bo_sync_cpu_cache(struct vtest *vtest, ++ struct vtest_bo *bo, ++ uint64_t direction) ++{ ++ if (!bo->incoherent || bo->res_fd < 0) ++ return; ++ ++ struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_START | direction }; ++ int ret = ioctl(bo->res_fd, DMA_BUF_IOCTL_SYNC, &sync); ++ if (!ret) { ++ sync.flags = DMA_BUF_SYNC_END | direction; ++ ret = ioctl(bo->res_fd, DMA_BUF_IOCTL_SYNC, &sync); ++ } ++ if (ret) ++ vn_log(vtest->instance, "dma-buf cache sync failed: %s", strerror(errno)); ++} ++ + static void + vtest_bo_invalidate(struct vn_renderer *renderer, + struct vn_renderer_bo *bo, + VkDeviceSize offset, + VkDeviceSize size) + { +- /* nop */ ++ vtest_bo_sync_cpu_cache((struct vtest *)renderer, (struct vtest_bo *)bo, ++ DMA_BUF_SYNC_READ); + } + + static void +@@ -622,7 +648,8 @@ vtest_bo_flush(struct vn_renderer *renderer, + VkDeviceSize offset, + VkDeviceSize size) + { +- /* nop */ ++ vtest_bo_sync_cpu_cache((struct vtest *)renderer, (struct vtest_bo *)bo, ++ DMA_BUF_SYNC_WRITE); + } + + static void * +@@ -651,7 +678,7 @@ vtest_bo_map(struct vn_renderer *renderer, + } else { + bo->base.mmap_ptr = ptr; + /* we don't need the fd anymore */ +- if (!shareable) { ++ if (!shareable && !bo->incoherent) { + close(bo->res_fd); + bo->res_fd = -1; + } +@@ -735,6 +762,8 @@ vtest_bo_create_from_device_memory( + }, + .res_fd = res_fd, + .blob_flags = blob_flags, ++ .incoherent = (flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) && ++ !(flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT), + }; + + *out_bo = &bo->base; +diff --git a/src/virtio/vulkan/vn_sync.c b/src/virtio/vulkan/vn_sync.c +index ee0a539..4687520 100644 +--- a/src/virtio/vulkan/vn_sync.c ++++ b/src/virtio/vulkan/vn_sync.c +@@ -26,7 +26,8 @@ vn_sync_payload_release_internal(struct vn_device *dev, + vn_renderer_sync_destroy(dev->renderer, payload->sync); + break; + case VN_SYNC_TYPE_TIMELINE_SYNC: +- for (uint32_t i = 0; i < dev->queue_count; i++) ++ /* One sync per queue plus one for host signals. */ ++ for (uint32_t i = 0; i <= dev->queue_count; i++) + vn_renderer_sync_destroy(dev->renderer, payload->syncs[i]); + vk_free2(&dev->base.vk.alloc, alloc, payload->syncs); + break; +@@ -49,6 +50,24 @@ vn_sync_payload_release(struct vn_device *dev, + vn_sync_payload_release_internal(dev, payload, NULL); + } + ++/* vtest cannot export a sync_file: wait for the payload on the CPU and ++ * return -1, the sync fd that stands for an already signaled payload. ++ */ ++static int ++vn_sync_export_signaled(struct vn_device *dev, struct vn_renderer_sync *sync) ++{ ++ uint64_t val = 1; ++ const struct vn_renderer_wait wait = { ++ .timeout = UINT64_MAX, ++ .syncs = &sync, ++ .sync_values = &val, ++ .sync_count = 1, ++ }; ++ vn_renderer_wait(dev->renderer, &wait); ++ vn_renderer_sync_reset(dev->renderer, sync); ++ return -1; ++} ++ + /* fence commands */ + + static VkResult +@@ -298,7 +317,10 @@ vn_GetFenceFdKHR(VkDevice device, + assert(sync_file); + + int fd = -1; +- if (payload->type == VN_SYNC_TYPE_SYNC) { ++ if (payload->type == VN_SYNC_TYPE_SYNC && ++ !dev->renderer->info.has_external_sync) { ++ fd = vn_sync_export_signaled(dev, payload->sync); ++ } else if (payload->type == VN_SYNC_TYPE_SYNC) { + fd = vn_renderer_sync_export_syncobj(dev->renderer, + fence->payload->sync, sync_file); + vn_renderer_sync_reset(dev->renderer, fence->payload->sync); +@@ -830,7 +852,11 @@ vn_GetSemaphoreFdKHR(VkDevice device, + assert(sync_file); + + int fd = -1; +- if (payload->type == VN_SYNC_TYPE_SYNC) { ++ if (payload->type == VN_SYNC_TYPE_SYNC && ++ !dev->renderer->info.has_external_sync) { ++ assert(sem->sync_fd_export); ++ fd = vn_sync_export_signaled(dev, payload->sync); ++ } else if (payload->type == VN_SYNC_TYPE_SYNC) { + assert(sem->sync_fd_export); + fd = vn_renderer_sync_export_syncobj(dev->renderer, payload->sync, + sync_file); +diff --git a/src/virtio/vulkan/vn_wsi.c b/src/virtio/vulkan/vn_wsi.c +index e73ab48..f0b41b1 100644 +--- a/src/virtio/vulkan/vn_wsi.c ++++ b/src/virtio/vulkan/vn_wsi.c +@@ -287,6 +287,53 @@ vn_wsi_memory_info_init(struct vn_device_memory *mem, + } + } + ++/* The buffer WSI copies each frame into for the CPU (software presentation) ++ * gets the hidden cached type: on vtest the host-visible type the app sees is ++ * uncached, and reading a frame from it took most of a present. WSI ++ * invalidates the buffer before each read. ++ */ ++uint32_t ++vn_wsi_memory_type_index(const struct vn_device *dev, ++ const VkMemoryAllocateInfo *alloc_info) ++{ ++ const struct vn_physical_device *physical_dev = dev->physical_device; ++ const int readback_type = physical_dev->wsi_readback_memory_type; ++ if (readback_type < 0 || !physical_dev->wsi_device.sw) ++ return alloc_info->memoryTypeIndex; ++ ++ const VkMemoryDedicatedAllocateInfo *dedicated_info = NULL; ++ bool wsi = false; ++ vk_foreach_struct_const(pnext, alloc_info->pNext) { ++ switch ((uint32_t)pnext->sType) { ++ case VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO: ++ dedicated_info = (const void *)pnext; ++ break; ++ case VK_STRUCTURE_TYPE_WSI_MEMORY_ALLOCATE_INFO_MESA: ++ wsi = true; ++ break; ++ case VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO: ++ case VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT: ++ return alloc_info->memoryTypeIndex; ++ default: ++ break; ++ } ++ } ++ if (!wsi || !dedicated_info || dedicated_info->buffer == VK_NULL_HANDLE) ++ return alloc_info->memoryTypeIndex; ++ ++ const VkMemoryPropertyFlags flags = ++ physical_dev->memory_properties.memoryTypes[alloc_info->memoryTypeIndex] ++ .propertyFlags; ++ const struct vn_buffer *buf = vn_buffer_from_handle(dedicated_info->buffer); ++ const uint32_t type_bits = ++ buf->requirements.memory.memoryRequirements.memoryTypeBits; ++ if (!(flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) || ++ !(type_bits & BITFIELD_BIT(readback_type))) ++ return alloc_info->memoryTypeIndex; ++ ++ return readback_type; ++} ++ + static uint32_t + vn_modifier_plane_count(struct vn_physical_device *physical_dev, + VkFormat format, +diff --git a/src/virtio/vulkan/vn_wsi.h b/src/virtio/vulkan/vn_wsi.h +index 2231556..1429feb 100644 +--- a/src/virtio/vulkan/vn_wsi.h ++++ b/src/virtio/vulkan/vn_wsi.h +@@ -34,6 +34,10 @@ void + vn_wsi_memory_info_init(struct vn_device_memory *mem, + const VkMemoryAllocateInfo *alloc_info); + ++uint32_t ++vn_wsi_memory_type_index(const struct vn_device *dev, ++ const VkMemoryAllocateInfo *alloc_info); ++ + bool + vn_wsi_validate_image_format_info( + struct vn_physical_device *physical_dev, +@@ -77,6 +81,13 @@ vn_wsi_memory_info_init(struct vn_device_memory *mem, + { + } + ++static inline uint32_t ++vn_wsi_memory_type_index(const struct vn_device *dev, ++ const VkMemoryAllocateInfo *alloc_info) ++{ ++ return alloc_info->memoryTypeIndex; ++} ++ + static inline bool + vn_wsi_validate_image_format_info(struct vn_physical_device *physical_dev, + const VkPhysicalDeviceImageFormatInfo2 *info) +diff --git a/src/vulkan/wsi/wsi_common.c b/src/vulkan/wsi/wsi_common.c +index 411ee3e..ae38115 100644 +--- a/src/vulkan/wsi/wsi_common.c ++++ b/src/vulkan/wsi/wsi_common.c +@@ -235,6 +235,7 @@ wsi_device_init(struct wsi_device *wsi, + WSI_GET_CB(WaitForFences); + WSI_GET_CB(MapMemory); + WSI_GET_CB(UnmapMemory); ++ WSI_GET_CB(InvalidateMappedMemoryRanges); + if (wsi->has_present_wait) + WSI_GET_CB(WaitSemaphores); + #undef WSI_GET_CB +@@ -3577,6 +3578,22 @@ wsi_create_cpu_linear_image_mem(const struct wsi_swapchain *chain, + return VK_SUCCESS; + } + ++void ++wsi_image_invalidate_cpu_map(const struct wsi_swapchain *chain, ++ const struct wsi_image *image) ++{ ++ /* A driver may back the CPU image with cached memory that is not ++ * coherent; for coherent memory this does nothing. ++ */ ++ const VkMappedMemoryRange range = { ++ .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, ++ .memory = image->blit.memory != VK_NULL_HANDLE ? image->blit.memory ++ : image->memory, ++ .size = VK_WHOLE_SIZE, ++ }; ++ chain->wsi->InvalidateMappedMemoryRanges(chain->device, 1, &range); ++} ++ + static VkResult + wsi_create_cpu_buffer_image_mem(const struct wsi_swapchain *chain, + const struct wsi_image_info *info, +diff --git a/src/vulkan/wsi/wsi_common.h b/src/vulkan/wsi/wsi_common.h +index 803d140..432ba43 100644 +--- a/src/vulkan/wsi/wsi_common.h ++++ b/src/vulkan/wsi/wsi_common.h +@@ -247,6 +247,7 @@ struct wsi_device { + WSI_CB(WaitForFences); + WSI_CB(MapMemory); + WSI_CB(UnmapMemory); ++ WSI_CB(InvalidateMappedMemoryRanges); + WSI_CB(WaitSemaphores); + #undef WSI_CB + +diff --git a/src/vulkan/wsi/wsi_common_drm.c b/src/vulkan/wsi/wsi_common_drm.c +index dbf65b2..2a6b701 100644 +--- a/src/vulkan/wsi/wsi_common_drm.c ++++ b/src/vulkan/wsi/wsi_common_drm.c +@@ -204,6 +204,10 @@ wsi_signal_dma_buf_from_semaphore(const struct wsi_swapchain *chain, + if (result != VK_SUCCESS) + return result; + ++ /* -1 is an already signaled payload: there is nothing to attach. */ ++ if (sync_file_fd < 0) ++ return VK_SUCCESS; ++ + result = wsi_dma_buf_import_sync_file(image->dma_buf_fd, sync_file_fd); + close(sync_file_fd); + return result; +diff --git a/src/vulkan/wsi/wsi_common_private.h b/src/vulkan/wsi/wsi_common_private.h +index 3d2cd7d..e469414 100644 +--- a/src/vulkan/wsi/wsi_common_private.h ++++ b/src/vulkan/wsi/wsi_common_private.h +@@ -410,6 +410,10 @@ wsi_configure_cpu_image(const struct wsi_swapchain *chain, + const struct wsi_cpu_image_params *params, + struct wsi_image_info *info); + ++void ++wsi_image_invalidate_cpu_map(const struct wsi_swapchain *chain, ++ const struct wsi_image *image); ++ + VkResult + wsi_create_buffer_blit_context(const struct wsi_swapchain *chain, + const struct wsi_image_info *info, +diff --git a/src/vulkan/wsi/wsi_common_wayland.c b/src/vulkan/wsi/wsi_common_wayland.c +index 33a4059..047c8b3 100644 +--- a/src/vulkan/wsi/wsi_common_wayland.c ++++ b/src/vulkan/wsi/wsi_common_wayland.c +@@ -3314,6 +3314,7 @@ wsi_wl_swapchain_queue_present(struct wsi_swapchain *wsi_chain, + + if (chain->buffer_type == WSI_WL_BUFFER_SHM_MEMCPY) { + struct wsi_wl_image *image = &chain->images[image_index]; ++ wsi_image_invalidate_cpu_map(&chain->base, &image->base); + memcpy(image->shm_ptr, image->base.cpu_map, + image->base.row_pitches[0] * chain->extent.height); + } +diff --git a/src/vulkan/wsi/wsi_common_x11.c b/src/vulkan/wsi/wsi_common_x11.c +index a077afe..579ba13 100644 +--- a/src/vulkan/wsi/wsi_common_x11.c ++++ b/src/vulkan/wsi/wsi_common_x11.c +@@ -1854,6 +1854,7 @@ x11_present_to_x11_sw(struct x11_swapchain *chain, uint32_t image_index) + xcb_get_geometry_cookie_t geom_cookie = xcb_get_geometry(chain->conn, chain->window); + + xcb_void_cookie_t cookie; ++ wsi_image_invalidate_cpu_map(&chain->base, &image->base); + void *myptr = image->base.cpu_map; + size_t hdr_len = sizeof(xcb_put_image_request_t); + int stride_b = image->base.row_pitches[0]; diff --git a/tools/venus/release.env b/tools/venus/release.env new file mode 100644 index 000000000..5da556c28 --- /dev/null +++ b/tools/venus/release.env @@ -0,0 +1,3 @@ +VENUS_REPO=The412Banner/droiddeck-nonadreno-drivers +VENUS_TAG=venus-proto13-e24ed8ab +VENUS_SHA256=25f236bbc32c8490fc61c20a6016b0bea4a8c3267a69de6b91496da2afe6bc04 diff --git a/tools/venus/virglrenderer-android.patch b/tools/venus/virglrenderer-android.patch new file mode 100644 index 000000000..9cf1a0248 --- /dev/null +++ b/tools/venus/virglrenderer-android.patch @@ -0,0 +1,322 @@ +diff --git a/src/venus/vkr_device_memory.c b/src/venus/vkr_device_memory.c +index ffe621b..01cd811 100644 +--- a/src/venus/vkr_device_memory.c ++++ b/src/venus/vkr_device_memory.c +@@ -227,6 +227,136 @@ vkr_gbm_get_fd_info_from_allocation_info(UNUSED struct vkr_physical_device *phys + + #endif /* ENABLE_GBM_ALLOCATION */ + ++#ifdef __ANDROID__ ++#include ++#include ++#include ++#include ++#include ++ ++static int vkr_dma_heap_fd = -1; ++static int vkr_dma_heap_cached_fd = -1; ++ ++/* Uncached for coherent memory types: nobody brackets CPU access with ++ * DMA_BUF_IOCTL_SYNC (the guest maps the memory as host-coherent), so on a ++ * cached heap the CPU and the GPU see each other's writes torn at cache-line ++ * granularity. Cached for cached types, which are not coherent: whoever maps ++ * them syncs the CPU cache through the dma-buf. ++ */ ++static void ++vkr_dma_heap_init_once(void) ++{ ++ vkr_dma_heap_cached_fd = open("/dev/dma_heap/system", O_RDONLY | O_CLOEXEC); ++ vkr_dma_heap_fd = open("/dev/dma_heap/system-uncached", O_RDONLY | O_CLOEXEC); ++ if (vkr_dma_heap_fd < 0) { ++ vkr_log("no uncached dma-buf heap (%s): using the cached one", strerror(errno)); ++ vkr_dma_heap_fd = vkr_dma_heap_cached_fd; ++ } ++ if (vkr_dma_heap_fd < 0) ++ vkr_log("no dma-buf heap (%s): allocating AHardwareBuffers", strerror(errno)); ++} ++ ++/* An AHardwareBuffer's dma-buf: the first fd of its native handle, which the ++ * NDK hands out only through a unix socket. ++ */ ++static int ++vkr_ahb_alloc_dma_buf(size_t size) ++{ ++ const AHardwareBuffer_Desc desc = { ++ .width = (uint32_t)size, ++ .height = 1, ++ .layers = 1, ++ .format = AHARDWAREBUFFER_FORMAT_BLOB, ++ .usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | ++ AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER, ++ }; ++ AHardwareBuffer *ahb = NULL; ++ if (AHardwareBuffer_allocate(&desc, &ahb)) { ++ vkr_log("AHardwareBuffer_allocate of %zu failed", size); ++ return -1; ++ } ++ ++ int fd = -1; ++ int sv[2]; ++ if (!socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv)) { ++ if (!AHardwareBuffer_sendHandleToUnixSocket(ahb, sv[0])) { ++ char data[4096]; ++ char control[CMSG_SPACE(sizeof(int) * 16)]; ++ struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) }; ++ struct msghdr msg = { ++ .msg_iov = &iov, ++ .msg_iovlen = 1, ++ .msg_control = control, ++ .msg_controllen = sizeof(control), ++ }; ++ if (recvmsg(sv[1], &msg, MSG_CMSG_CLOEXEC) > 0) { ++ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); ++ if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { ++ const int count = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int); ++ const int *fds = (const int *)CMSG_DATA(cmsg); ++ for (int i = 0; i < count; i++) { ++ if (i == 0) ++ fd = fds[i]; ++ else ++ close(fds[i]); ++ } ++ } ++ } ++ } ++ close(sv[0]); ++ close(sv[1]); ++ } ++ AHardwareBuffer_release(ahb); ++ if (fd < 0) ++ vkr_log("could not get the dma-buf of an AHardwareBuffer"); ++ return fd; ++} ++ ++static VkResult ++vkr_android_get_fd_info_from_allocation_info(const VkMemoryAllocateInfo *alloc_info, ++ bool cached, ++ int *out_dma_buf_fd, ++ VkImportMemoryFdInfoKHR *out_fd_info) ++{ ++ static once_flag dma_heap_once_flag = ONCE_FLAG_INIT; ++ call_once(&dma_heap_once_flag, vkr_dma_heap_init_once); ++ ++ const size_t size = align(alloc_info->allocationSize, getpagesize()); ++ const int heap_fd = ++ cached && vkr_dma_heap_cached_fd >= 0 ? vkr_dma_heap_cached_fd : vkr_dma_heap_fd; ++ int dma_buf_fd = -1; ++ if (heap_fd >= 0) { ++ struct dma_heap_allocation_data data = { ++ .len = size, ++ .fd_flags = O_RDWR | O_CLOEXEC, ++ }; ++ if (!ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data)) ++ dma_buf_fd = data.fd; ++ else ++ vkr_log("dma-buf heap allocation of %zu failed (%s)", size, strerror(errno)); ++ } ++ if (dma_buf_fd < 0) ++ dma_buf_fd = vkr_ahb_alloc_dma_buf(size); ++ if (dma_buf_fd < 0) ++ return VK_ERROR_OUT_OF_DEVICE_MEMORY; ++ ++ const int fd = os_dupfd_cloexec(dma_buf_fd); ++ if (fd < 0) { ++ close(dma_buf_fd); ++ return VK_ERROR_OUT_OF_HOST_MEMORY; ++ } ++ ++ *out_dma_buf_fd = dma_buf_fd; ++ *out_fd_info = (VkImportMemoryFdInfoKHR){ ++ .sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, ++ .pNext = alloc_info->pNext, ++ .fd = fd, ++ .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, ++ }; ++ return VK_SUCCESS; ++} ++#endif /* __ANDROID__ */ ++ + static void + vkr_dispatch_vkAllocateMemory(struct vn_dispatch_context *dispatch, + struct vn_command_vkAllocateMemory *args) +@@ -281,7 +411,14 @@ vkr_dispatch_vkAllocateMemory(struct vn_dispatch_context *dispatch, + int udmabuf_fd = -1; + void *gbm_bo = NULL; + VkExportMemoryAllocateInfo local_export_info; +- if ((property_flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) && !res_info) { ++ /* Android without driver export: a dma-buf export of any memory type is ++ * served by a dma-buf the server allocates and imports. ++ */ ++ const bool android_export = ++ physical_dev->android_dma_buf_import && export_info && ++ (export_info->handleTypes & VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT); ++ if (((property_flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) || android_export) && ++ !res_info) { + /* An implementation can support dma_buf import along with opaque fd export/import. + * If the client driver is using external memory and requesting dma_buf, without + * dma_buf fd export support, we must use gbm bo import path instead of forcing +@@ -346,6 +483,13 @@ vkr_dispatch_vkAllocateMemory(struct vn_dispatch_context *dispatch, + */ + args->ret = vkr_udmabuf_get_fd_info_from_allocation_info( + physical_dev, alloc_info, &udmabuf_fd, &local_import_info); ++#ifdef __ANDROID__ ++ } else if (physical_dev->android_dma_buf_import) { ++ /* Kept in udmabuf_fd: exported and closed the same way. */ ++ args->ret = vkr_android_get_fd_info_from_allocation_info( ++ alloc_info, property_flags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT, ++ &udmabuf_fd, &local_import_info); ++#endif + } else { + args->ret = vkr_gbm_get_fd_info_from_allocation_info( + physical_dev, alloc_info, &gbm_bo, &local_import_info); +@@ -367,6 +511,12 @@ vkr_dispatch_vkAllocateMemory(struct vn_dispatch_context *dispatch, + + struct vkr_device_memory *mem = vkr_device_memory_create_and_add(ctx, args); + if (!mem) { ++ vkr_log("vkAllocateMemory failed (%d): type %u flags 0x%x size %" PRIu64 "%s", ++ args->ret, mem_type_index, property_flags, ++ (uint64_t)alloc_info->allocationSize, ++ udmabuf_fd >= 0 ? ", imported dma-buf" : ""); ++ if (udmabuf_fd >= 0) ++ close(udmabuf_fd); + if (local_import_info.fd >= 0) + close(local_import_info.fd); + if (gbm_bo) +diff --git a/src/venus/vkr_physical_device.c b/src/venus/vkr_physical_device.c +index e2c3baa..69ec875 100644 +--- a/src/venus/vkr_physical_device.c ++++ b/src/venus/vkr_physical_device.c +@@ -207,31 +207,54 @@ vkr_physical_device_init_memory_properties(struct vkr_physical_device *physical_ + .sType = VK_STRUCTURE_TYPE_EXTERNAL_BUFFER_PROPERTIES, + }; + ++ /* Exportable and compatible with itself is enough. Requiring the type in ++ * exportFromImportedHandleTypes as well rules out drivers (Mali on Android) ++ * that export fine but do not re-export imported memory. ++ */ + if (physical_dev->EXT_external_memory_dma_buf) { + info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + vk->GetPhysicalDeviceExternalBufferProperties(handle, &info, &props); ++ const VkExternalMemoryProperties *p = &props.externalMemoryProperties; ++ vkr_log("dma_buf buffer: features 0x%x exportFrom 0x%x compatible 0x%x", ++ p->externalMemoryFeatures, p->exportFromImportedHandleTypes, ++ p->compatibleHandleTypes); + physical_dev->is_dma_buf_fd_export_supported = +- (props.externalMemoryProperties.externalMemoryFeatures & +- VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) && +- (props.externalMemoryProperties.exportFromImportedHandleTypes & ++ (p->externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) && ++ ((p->exportFromImportedHandleTypes | p->compatibleHandleTypes) & + VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT); + } + + if (physical_dev->KHR_external_memory_fd) { + info.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT, + vk->GetPhysicalDeviceExternalBufferProperties(handle, &info, &props); ++ const VkExternalMemoryProperties *p = &props.externalMemoryProperties; ++ vkr_log("opaque_fd buffer: features 0x%x exportFrom 0x%x compatible 0x%x", ++ p->externalMemoryFeatures, p->exportFromImportedHandleTypes, ++ p->compatibleHandleTypes); + physical_dev->is_opaque_fd_export_supported = +- (props.externalMemoryProperties.externalMemoryFeatures & +- VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) && +- (props.externalMemoryProperties.exportFromImportedHandleTypes & ++ (p->externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) && ++ ((p->exportFromImportedHandleTypes | p->compatibleHandleTypes) & + VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT); + } ++ vkr_log("memory export: dma_buf %d opaque_fd %d", ++ physical_dev->is_dma_buf_fd_export_supported, ++ physical_dev->is_opaque_fd_export_supported); + + /* fallback to gbm allocation with dma-buf import */ ++ physical_dev->android_dma_buf_import = false; ++#ifdef __ANDROID__ ++ if (!physical_dev->is_dma_buf_fd_export_supported && ++ !physical_dev->is_opaque_fd_export_supported && ++ physical_dev->EXT_external_memory_dma_buf) { ++ physical_dev->android_dma_buf_import = true; ++ vkr_log("memory export: none; allocating dma-bufs and importing them"); ++ } ++#else + if (!physical_dev->is_dma_buf_fd_export_supported && + !physical_dev->is_opaque_fd_export_supported && + physical_dev->EXT_external_memory_dma_buf) + physical_dev->gbm_device = vkr_physical_device_get_gbm_device(); ++#endif + + physical_dev->udmabuf_dev_fd = -1; + if (VKR_DEBUG(UDMABUF)) { +@@ -703,6 +726,25 @@ vkr_dispatch_vkGetPhysicalDeviceFormatProperties2( + args->pFormatProperties); + } + ++/* Android without driver export: the server allocates a dma-buf and imports ++ * it (vkr_device_memory.c), so a dma-buf the driver can import, the guest ++ * can export. ++ */ ++static void ++vkr_android_dma_buf_export_fixup(const struct vkr_physical_device *physical_dev, ++ VkExternalMemoryHandleTypeFlagBits handle_type, ++ VkExternalMemoryProperties *props) ++{ ++ if (!physical_dev->android_dma_buf_import || ++ handle_type != VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT || ++ !(props->externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT)) ++ return; ++ ++ props->externalMemoryFeatures |= VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT; ++ props->exportFromImportedHandleTypes |= VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; ++ props->compatibleHandleTypes |= VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT; ++} ++ + static void + vkr_dispatch_vkGetPhysicalDeviceImageFormatProperties2( + UNUSED struct vn_dispatch_context *dispatch, +@@ -715,6 +757,20 @@ vkr_dispatch_vkGetPhysicalDeviceImageFormatProperties2( + vn_replace_vkGetPhysicalDeviceImageFormatProperties2_args_handle(args); + args->ret = vk->GetPhysicalDeviceImageFormatProperties2( + args->physicalDevice, args->pImageFormatInfo, args->pImageFormatProperties); ++ ++ const VkPhysicalDeviceExternalImageFormatInfo *external_info = vkr_find_struct( ++ args->pImageFormatInfo->pNext, ++ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO); ++ VkExternalImageFormatProperties *external_props = vkr_find_struct( ++ args->pImageFormatProperties->pNext, VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES); ++ if (physical_dev->android_dma_buf_import && external_info && external_props) { ++ if (args->ret == VK_SUCCESS) ++ vkr_android_dma_buf_export_fixup(physical_dev, external_info->handleType, ++ &external_props->externalMemoryProperties); ++ else ++ vkr_log("image format %d tiling %d: no external memory (%d)", ++ args->pImageFormatInfo->format, args->pImageFormatInfo->tiling, args->ret); ++ } + } + + static void +@@ -743,6 +799,10 @@ vkr_dispatch_vkGetPhysicalDeviceExternalBufferProperties( + vn_replace_vkGetPhysicalDeviceExternalBufferProperties_args_handle(args); + vk->GetPhysicalDeviceExternalBufferProperties( + args->physicalDevice, args->pExternalBufferInfo, args->pExternalBufferProperties); ++ ++ vkr_android_dma_buf_export_fixup( ++ physical_dev, args->pExternalBufferInfo->handleType, ++ &args->pExternalBufferProperties->externalMemoryProperties); + } + + static void +diff --git a/src/venus/vkr_physical_device.h b/src/venus/vkr_physical_device.h +index b0b6691..28ba0b7 100644 +--- a/src/venus/vkr_physical_device.h ++++ b/src/venus/vkr_physical_device.h +@@ -33,6 +33,10 @@ struct vkr_physical_device { + bool is_opaque_fd_export_supported; + void *gbm_device; + int udmabuf_dev_fd; ++ /* Android: host-visible memory is a dma-buf the server allocates (dma-buf ++ * heap, else AHardwareBuffer) and imports, since the driver cannot export. ++ */ ++ bool android_dma_buf_import; + + VkQueueFamilyProperties *queue_family_properties; + uint32_t queue_family_property_count; diff --git a/tools/venus/virglrenderer-destroy-mutex-after-objects.patch b/tools/venus/virglrenderer-destroy-mutex-after-objects.patch new file mode 100644 index 000000000..a36fb6e80 --- /dev/null +++ b/tools/venus/virglrenderer-destroy-mutex-after-objects.patch @@ -0,0 +1,30 @@ +venus: destroy a device's object mutex after the objects that lock it + +From BattleTempest's DroidDeck-Pixel (kevinbryant551-ux/DroidDeck-Pixel, GPL-3.0), +tools/venus/patches/0103-destroy-object-mutex-after-objects.patch. Bionic aborts on locking a +destroyed mutex ("pthread_mutex_lock called on a destroyed mutex", seen on a Tab S9 FE G68 +and a Pixel 9a), so a client that exited holding objects took the render worker down. + +diff --git a/src/venus/vkr_device.c b/src/venus/vkr_device.c +index 9144180..7b7c904 100644 +--- a/src/venus/vkr_device.c ++++ b/src/venus/vkr_device.c +@@ -324,13 +324,16 @@ vkr_device_destroy(struct vkr_context *ctx, struct vkr_device *dev, bool destroy + vkr_log("vkDeviceWaitIdle(%p) failed(%d)", dev, (int32_t)result); + } + +- mtx_destroy(&dev->object_mutex); +- ++ /* The leftover objects are removed under object_mutex: destroy it after them. glibc lets a ++ * destroyed mutex be locked; bionic aborts ("pthread_mutex_lock called on a destroyed mutex"), ++ * so every client that exited holding objects took its render worker down. */ + if (!list_is_empty(&dev->objects)) { + list_for_each_entry_safe (struct vkr_object, obj, &dev->objects, track_head) + vkr_device_object_destroy(ctx, dev, obj); + } + ++ mtx_destroy(&dev->object_mutex); ++ + list_for_each_entry_safe (struct vkr_queue, queue, &dev->queues, base.track_head) + vkr_queue_destroy(ctx, queue); + diff --git a/tools/venus/virglrenderer-destroy-unknown-pipeline.patch b/tools/venus/virglrenderer-destroy-unknown-pipeline.patch new file mode 100644 index 000000000..60c8ead7a --- /dev/null +++ b/tools/venus/virglrenderer-destroy-unknown-pipeline.patch @@ -0,0 +1,54 @@ +diff --git a/src/venus/venus-protocol/vn_protocol_renderer_pipeline.h b/src/venus/venus-protocol/vn_protocol_renderer_pipeline.h +index ce82e59..8798de9 100644 +--- a/src/venus/venus-protocol/vn_protocol_renderer_pipeline.h ++++ b/src/venus/venus-protocol/vn_protocol_renderer_pipeline.h +@@ -3290,7 +3290,11 @@ static inline void vn_encode_vkCreateComputePipelines_reply(struct vn_cs_encoder + static inline void vn_decode_vkDestroyPipeline_args_temp(struct vn_cs_decoder *dec, struct vn_command_vkDestroyPipeline *args) + { + vn_decode_VkDevice_lookup(dec, &args->device); +- vn_decode_VkPipeline_lookup(dec, &args->pipeline); ++ { ++ uint64_t id; ++ vn_decode_uint64_t(dec, &id); ++ args->pipeline = (VkPipeline)(uintptr_t)vkr_cs_decoder_lookup_object_for_destroy((const struct vkr_cs_decoder *)dec, id, VK_OBJECT_TYPE_PIPELINE); ++ } + if (vn_decode_simple_pointer(dec)) { + vn_cs_decoder_set_fatal(dec); + } else { +diff --git a/src/venus/vkr_cs.h b/src/venus/vkr_cs.h +index c72c5cc..b45061f 100644 +--- a/src/venus/vkr_cs.h ++++ b/src/venus/vkr_cs.h +@@ -324,6 +324,32 @@ vkr_cs_decoder_lookup_object(const struct vkr_cs_decoder *dec, + return obj; + } + ++/* For a destroy command only: an id the renderer never created (the driver refused the create, ++ * which the guest may not have waited for) is logged and destroyed as VK_NULL_HANDLE, a no-op, ++ * instead of marking the whole context fatal - that took the vtest server down (exit 137) when ++ * gamescope destroyed a compute pipeline a Mali-G615 had refused. */ ++static inline struct vkr_object * ++vkr_cs_decoder_lookup_object_for_destroy(const struct vkr_cs_decoder *dec, ++ vkr_object_id id, ++ VkObjectType type) ++{ ++ struct vkr_object *obj; ++ ++ if (!id) ++ return NULL; ++ ++ mtx_lock(dec->object_mutex); ++ const struct hash_entry *entry = ++ _mesa_hash_table_search((struct hash_table *)dec->object_table, &id); ++ obj = likely(entry) ? entry->data : NULL; ++ mtx_unlock(dec->object_mutex); ++ if (unlikely(!obj || obj->type != type)) { ++ vkr_log("destroy of unknown object %" PRIu64 " of type %d ignored", id, type); ++ return NULL; ++ } ++ return obj; ++} ++ + static inline void + vkr_cs_decoder_reset_temp_pool(struct vkr_cs_decoder *dec) + { diff --git a/tools/venus/watch-maps.sh b/tools/venus/watch-maps.sh new file mode 100644 index 000000000..2850147b1 --- /dev/null +++ b/tools/venus/watch-maps.sh @@ -0,0 +1,19 @@ +#!/system/bin/sh +# Keeps the memory map of a running game process in the app's cache directory, so a crash address +# in Wine's log (handle_syscall_fault pc=...) can be matched to a library afterwards. +# On the phone, as the app (from Git Bash, with MSYS_NO_PATHCONV=1): +# adb push tools/venus/watch-maps.sh /data/local/tmp/ +# adb shell "run-as com.droiddeck.launcher sh /data/local/tmp/watch-maps.sh 'Hollow Knight Silksong.exe'" +# Runs 15 minutes. Then: adb exec-out "run-as com.droiddeck.launcher cat cache/game-maps.txt" > maps.txt +name=${1:?process name, as ps shows it} +cd /data/user/0/com.droiddeck.launcher || exit 1 +rm -f cache/game-maps.txt +i=0 +while [ $i -lt 900 ]; do + p=$(ps -A -o PID,NAME | grep -F "$name" | grep -v Crash | head -1 | awk '{print $1}') + if [ -n "$p" ]; then + cat /proc/$p/maps > cache/game-maps.tmp 2>/dev/null && mv cache/game-maps.tmp cache/game-maps.txt + fi + sleep 1 + i=$((i + 1)) +done diff --git a/tools/vulkan-wsi/build.sh b/tools/vulkan-wsi/build.sh new file mode 100755 index 000000000..cf5a1c356 --- /dev/null +++ b/tools/vulkan-wsi/build.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# CI build of a vendor-neutral X11 surface/swapchain layer. +set -euo pipefail +root=$(cd "$(dirname "$0")/../.." && pwd) +. "$root/tools/vulkan-wsi/source.env" +mkdir -p "${1:?output directory required}" +out=$(cd "$1" && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +fetch() { + git init -q "$3" + git -C "$3" fetch --depth=1 "$1" "$2" + git -C "$3" checkout -q --detach FETCH_HEAD + test "$(git -C "$3" rev-parse HEAD)" = "$2" +} +fetch https://github.com/ginkage/vulkan-wsi-layer.git "$WSI_REVISION" "$work/source" +fetch https://github.com/KhronosGroup/Vulkan-Headers.git "$VULKAN_HEADERS_REVISION" "$work/headers" +git -C "$work/source" apply --check "$root/tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch" +git -C "$work/source" apply "$root/tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch" +cmake -S "$work/source" -B "$work/build" -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr -DVULKAN_CXX_INCLUDE="$work/headers/include" \ + -DBUILD_WSI_HEADLESS=OFF -DBUILD_WSI_WAYLAND=OFF -DBUILD_WSI_DISPLAY=OFF -DBUILD_WSI_X11=ON \ + -DSELECT_EXTERNAL_ALLOCATOR=dma_buf_heaps -DWSIALLOC_MEMORY_HEAP_NAME=system -DKERNEL_HEADER_DIR=/usr/include +cmake --build "$work/build" --parallel 4 +DESTDIR="$work/stage" cmake --install "$work/build" +cp "$work/stage/usr/share/vulkan/implicit_layer.d/"* "$out/" +cp "$root/tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch" "$out/downstream.patch" +cp "$work/source/LICENSE" "$out/LICENSE" +mkdir -p "$out/licenses/Vulkan-Headers" +cp "$root/tools/vulkan-wsi/licenses/Vulkan-Headers/"* "$out/licenses/Vulkan-Headers/" +printf '%s\n' "$WSI_REVISION" > "$out/source-commit" +printf '%s\n' "$VULKAN_HEADERS_REVISION" > "$out/vulkan-headers-commit" +tar --exclude-vcs --zstd -cf "$out/source.tar.zst" -C "$work/source" . +(cd "$out" && sha256sum ./*.so > SHA256SUMS) diff --git a/tools/vulkan-wsi/licenses/Vulkan-Headers/LICENSE.md b/tools/vulkan-wsi/licenses/Vulkan-Headers/LICENSE.md new file mode 100644 index 000000000..17cf4a17d --- /dev/null +++ b/tools/vulkan-wsi/licenses/Vulkan-Headers/LICENSE.md @@ -0,0 +1,19 @@ +Copyright 2015-2026 The Khronos Group Inc. + +Files in this repository fall under one of these licenses: + +- `Apache-2.0` +- `MIT` + +Note: With the exception of `parse_dependency.py`, which is based on an +external file, files using the `MIT` license also fall under `Apache-2.0`. +Example: + +``` +SPDX-License-Identifier: Apache-2.0 OR MIT +``` + +Full license text of these licenses is available at: + + * Apache-2.0: https://opensource.org/licenses/Apache-2.0 + * MIT: https://opensource.org/licenses/MIT diff --git a/tools/vulkan-wsi/licenses/Vulkan-Headers/MIT.txt b/tools/vulkan-wsi/licenses/Vulkan-Headers/MIT.txt new file mode 100644 index 000000000..8495fe9e9 --- /dev/null +++ b/tools/vulkan-wsi/licenses/Vulkan-Headers/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2015-2023 The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch b/tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch new file mode 100644 index 000000000..da605feaa --- /dev/null +++ b/tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch @@ -0,0 +1,16 @@ +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -421,6 +421,13 @@ + cp ${PROJECT_SOURCE_DIR}/layer/VkLayer_window_system_integration.json ${CMAKE_CURRENT_BINARY_DIR} + ${JSON_COMMANDS}) + ++# X11 DRI3 uses these shared sources even when Wayland and display are disabled. ++if(BUILD_WSI_X11 AND NOT BUILD_WSI_WAYLAND AND NOT BUILD_WSI_DISPLAY) ++ target_sources(${PROJECT_NAME} PRIVATE ++ wsi/extensions/external_memory_extension.cpp ++ wsi/image_backing_memory_external.cpp) ++endif() ++ + # Install the layer and its manifest into a Vulkan implicit layer directory. Upstream leaves this to + # manual copying; the fork adds install rules so the Debian package picks up the .so and JSON. + install(TARGETS ${PROJECT_NAME} DESTINATION share/vulkan/implicit_layer.d/) diff --git a/tools/vulkan-wsi/source.env b/tools/vulkan-wsi/source.env new file mode 100644 index 000000000..4c6e9f7a3 --- /dev/null +++ b/tools/vulkan-wsi/source.env @@ -0,0 +1,2 @@ +WSI_REVISION=40cfec09d0e2d42c5e1f2642cf4cba11e7008112 +VULKAN_HEADERS_REVISION=8d6039a455a7ecc7d2a592ff97f62db4e59b70bf