From 7dc00d9b235c2ef0bbe55b99c5d4a2c8ff0afd82 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Wed, 23 Sep 2026 20:59:45 -0400 Subject: [PATCH] Desktop GPU: patched wlroots with a Vulkan DMA-BUF allocator; new windows take the keys wlroots 0.20.2 (Arch's wlroots0.20, what the desktop's labwc links) gains an allocator that exports Vulkan images as DMA-BUFs for a GPU node that is not a DRM device - the Adreno KGSL stand-in - where gbm and the DRM lease path fail ('unable to create allocator'). linux-dmabuf skips its GEM import check on such a node so client buffers are not all refused. Built by build-wlroots.yml. Compositor: without a Wine desktop, a window that opens on top takes keyboard focus (a gamescope started from the Linux desktop). --- .github/workflows/build-wlroots.yml | 46 ++ app/src/main/cpp/waylandcomp/src/compositor.c | 4 + tools/wlroots/build-in-arch.sh | 58 ++ ...lkan-allocator-for-non-drm-gpu-nodes.patch | 549 ++++++++++++++++++ 4 files changed, 657 insertions(+) create mode 100644 .github/workflows/build-wlroots.yml create mode 100755 tools/wlroots/build-in-arch.sh create mode 100644 tools/wlroots/patches/0001-vulkan-allocator-for-non-drm-gpu-nodes.patch diff --git a/.github/workflows/build-wlroots.yml b/.github/workflows/build-wlroots.yml new file mode 100644 index 00000000..2bee0ac7 --- /dev/null +++ b/.github/workflows/build-wlroots.yml @@ -0,0 +1,46 @@ +name: Build wlroots (patched) + +# The desktop's wlroots (Arch Linux ARM's wlroots0.20, what the desktop package's labwc links) +# rebuilt with tools/wlroots/patches: a Vulkan DMA-BUF allocator for the Adreno node, which is not +# a DRM device, so labwc's vulkan and gles2 renderers can start. Built in an Arch Linux ARM +# container on an arm64 runner and published as a release asset the apk build fetches +# (tools/wlroots/release.env names the tag and sha). The hosted desktop package is never changed. +on: + push: + paths: + - 'tools/wlroots/**' + - '.github/workflows/build-wlroots.yml' + workflow_dispatch: + inputs: + tag: + description: 'Release tag to publish as (e.g. wlroots-0.20.2-p1); empty = artifact only' + required: false + default: '' + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-24.04-arm + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - name: Build in Arch Linux ARM + run: | + set -euo pipefail + docker run --rm -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/wlroots/build-in-arch.sh + sudo chown -R "$(id -u):$(id -g)" . + - uses: actions/upload-artifact@v4 + with: + name: wlroots-patched + path: | + wlroots.tzst + wlroots.tzst.sha256 + - name: Publish + if: github.event_name == 'workflow_dispatch' && inputs.tag != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "wlroots 0.20.2 with tools/wlroots/patches (Vulkan DMA-BUF allocator for non-DRM GPU nodes); staged by the apk at /usr/local/lib/droiddeck-wlroots for the desktop's GPU renderers. sha256 $(cut -d' ' -f1 wlroots.tzst.sha256)" wlroots.tzst wlroots.tzst.sha256 || gh release upload "${{ inputs.tag }}" wlroots.tzst wlroots.tzst.sha256 --clobber diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index dbf4551b..4efe9c83 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -589,6 +589,10 @@ static void map_toplevel(struct surface *s) { s->placed ? "" : " (no desktop position yet)"); } wl_list_insert(g_toplevels.prev, &s->toplevel_link); /* new windows start on top */ + /* Without a Wine desktop the clients are whole programs (the Linux desktop, a gamescope a + * program on it started over it): a window that opens on top takes the keys too, as it would + * on any desktop, rather than the one clicked last that is now underneath it. */ + if (!g_desktop) g_key_target = s; apply_zorder(); } diff --git a/tools/wlroots/build-in-arch.sh b/tools/wlroots/build-in-arch.sh new file mode 100755 index 00000000..8d39c5df --- /dev/null +++ b/tools/wlroots/build-in-arch.sh @@ -0,0 +1,58 @@ +#!/usr/bin/bash +# Runs INSIDE an Arch Linux ARM container (menci/archlinuxarm:base-devel) on an arm64 runner. +# Builds Arch's own wlroots0.20 package - the wlroots the desktop package's labwc links - with the +# patches in ./patches added, keeps only the library, and packs it as wlroots.tzst +# (usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so) for the apk to stage. The desktop launcher +# puts that directory first on labwc's library path for the GPU renderers only. +set -euxo pipefail +VERSION=${WLROOTS_VERSION:-0.20.2} +WORK=/work +cd "$WORK" +# Same container workarounds as tools/gamescope/build-in-arch.sh. +grep -q '^DisableSandbox' /etc/pacman.conf || sed -i 's/^\[options\]/[options]\nDisableSandbox/' /etc/pacman.conf +{ for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done; cat /etc/pacman.d/mirrorlist; } > /etc/pacman.d/mirrorlist.new +mv /etc/pacman.d/mirrorlist.new /etc/pacman.d/mirrorlist +pacman -Syu --noconfirm --needed git sudo zstd binutils +id builder >/dev/null 2>&1 || useradd -m builder +echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder +rm -rf pkg && mkdir pkg && cp -r tools/wlroots/patches pkg/ && chown -R builder pkg +sudo -u builder bash -c " +set -euxo pipefail +cd pkg +for rel in 1 2 3 4; do + if git clone -q --depth 1 --branch ${VERSION}-\$rel https://gitlab.archlinux.org/archlinux/packaging/packages/wlroots0.20.git arch 2>/dev/null; then break; fi +done +test -f arch/PKGBUILD +cd arch +grep -q '^pkgver=${VERSION}$' PKGBUILD +cp ../patches/*.patch . +PATCHES=\$(ls *.patch | sort) +{ echo; echo 'source+=('; for p in \$PATCHES; do echo \" \$p\"; done; echo ')'; for p in \$PATCHES; do echo \"sha256sums+=('SKIP')\"; done; } >> PKGBUILD +# Arch's PKGBUILD has no prepare(); ours applies the patches to the checkout. +cat >> PKGBUILD <<'PREP' + +prepare() { + cd \"\$srcdir/\$pkgname\" + for p in \$(ls \"\$srcdir\"/*.patch | sort); do + echo \"applying \$(basename \"\$p\")\" + patch -p1 --no-backup-if-mismatch < \"\$p\" + done +} +PREP +makepkg -A -s --noconfirm --skipchecksums --skippgpcheck +ls -l *.pkg.tar.* +" +cd "$WORK" +rm -rf out && mkdir -p out/usr/local/lib/droiddeck-wlroots +PKG=$(ls pkg/arch/wlroots0.20-*.pkg.tar.* | grep -v -- '-debug-' | head -1) +mkdir -p x && tar --use-compress-program=unzstd -xf "$PKG" -C x +SO=x/usr/lib/libwlroots-0.20.so +test -f "$SO" +cp -L "$SO" out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so +strip --strip-unneeded out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so || true +# The patch is in: the allocator's symbol and its log line. +grep -q 'Created Vulkan DMA-BUF allocator' out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so +readelf -d out/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so | grep -E 'NEEDED|SONAME' +(cd out && tar --use-compress-program='zstd -19' -cf ../wlroots.tzst usr) +sha256sum wlroots.tzst | tee wlroots.tzst.sha256 +ls -l wlroots.tzst out/usr/local/lib/droiddeck-wlroots/ diff --git a/tools/wlroots/patches/0001-vulkan-allocator-for-non-drm-gpu-nodes.patch b/tools/wlroots/patches/0001-vulkan-allocator-for-non-drm-gpu-nodes.patch new file mode 100644 index 00000000..658233f4 --- /dev/null +++ b/tools/wlroots/patches/0001-vulkan-allocator-for-non-drm-gpu-nodes.patch @@ -0,0 +1,549 @@ +diff --git a/include/render/vulkan.h b/include/render/vulkan.h +index f96619f2..9f945f87 100644 +--- a/include/render/vulkan.h ++++ b/include/render/vulkan.h +@@ -461,6 +461,10 @@ struct wlr_vk_descriptor_pool *vulkan_alloc_blend_ds( + // Frees the given descriptor set from the pool its pool. + void vulkan_free_ds(struct wlr_vk_renderer *renderer, + struct wlr_vk_descriptor_pool *pool, VkDescriptorSet ds); ++// A DMA-BUF allocator on a Vulkan device of its own, for a DRM fd gbm cannot use (see ++// render/vulkan/allocator.c). ++struct wlr_allocator *vulkan_allocator_create(int drm_fd); ++ + struct wlr_vk_format_props *vulkan_format_props_from_drm( + struct wlr_vk_device *dev, uint32_t drm_format); + struct wlr_vk_renderer *vulkan_get_renderer(struct wlr_renderer *r); +diff --git a/render/allocator/allocator.c b/render/allocator/allocator.c +index b7dbf3d6..f3162d47 100644 +--- a/render/allocator/allocator.c ++++ b/render/allocator/allocator.c +@@ -1,6 +1,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -13,6 +14,10 @@ + #include "render/allocator/shm.h" + #include "render/wlr_renderer.h" + ++#if WLR_HAS_VULKAN_RENDERER ++#include "render/vulkan.h" ++#endif ++ + #if WLR_HAS_GBM_ALLOCATOR + #include "render/allocator/gbm.h" + #endif +@@ -32,6 +37,29 @@ void wlr_allocator_init(struct wlr_allocator *alloc, + wl_signal_init(&alloc->events.destroy); + } + ++/* Whether the fd is a DRM device at all. The GPU node on Android (KGSL, for Adreno) stands in for ++ * a render node so that Vulkan and libdrm's device lookup can name the GPU, but it answers no DRM ++ * ioctl: drmIsMaster() takes that for mastership, and gbm cannot allocate on it. */ ++static bool is_drm_device(int fd) { ++ drmVersion *version = drmGetVersion(fd); ++ if (version == NULL) { ++ return false; ++ } ++ drmFreeVersion(version); ++ return true; ++} ++ ++#if WLR_HAS_VULKAN_RENDERER ++static struct wlr_allocator *try_vulkan_allocator(int drm_fd) { ++ wlr_log(WLR_DEBUG, "Trying to create Vulkan allocator"); ++ struct wlr_allocator *alloc = vulkan_allocator_create(drm_fd); ++ if (alloc == NULL) { ++ wlr_log(WLR_DEBUG, "Failed to create Vulkan allocator"); ++ } ++ return alloc; ++} ++#endif ++ + /* Re-open the DRM node to avoid GEM handle ref'counting issues. See: + * https://gitlab.freedesktop.org/mesa/drm/-/merge_requests/110 + */ +@@ -109,8 +137,20 @@ struct wlr_allocator *wlr_allocator_autocreate(struct wlr_backend *backend, + struct wlr_allocator *alloc = NULL; + + uint32_t gbm_caps = WLR_BUFFER_CAP_DMABUF; ++ bool drm_device = drm_fd >= 0 && is_drm_device(drm_fd); ++ const char *forced = getenv("WLR_ALLOCATOR"); ++ bool vk_first = forced != NULL && strcmp(forced, "vulkan") == 0; ++#if WLR_HAS_VULKAN_RENDERER ++ // A GPU node that is not a DRM device (and WLR_ALLOCATOR=vulkan): buffers come from Vulkan. ++ if ((backend_caps & gbm_caps) && (renderer_caps & gbm_caps) && drm_fd >= 0 ++ && (vk_first || !drm_device)) { ++ if ((alloc = try_vulkan_allocator(drm_fd)) != NULL) { ++ return alloc; ++ } ++ } ++#endif + if ((backend_caps & gbm_caps) && (renderer_caps & gbm_caps) +- && drm_fd >= 0) { ++ && drm_fd >= 0 && drm_device) { + #if WLR_HAS_GBM_ALLOCATOR + wlr_log(WLR_DEBUG, "Trying to create gbm allocator"); + int gbm_fd = reopen_drm_node(drm_fd, true); +@@ -124,6 +164,11 @@ struct wlr_allocator *wlr_allocator_autocreate(struct wlr_backend *backend, + wlr_log(WLR_DEBUG, "Failed to create gbm allocator"); + #else + wlr_log(WLR_DEBUG, "Skipping gbm allocator: disabled at compile-time"); ++#endif ++#if WLR_HAS_VULKAN_RENDERER ++ if (!vk_first && (alloc = try_vulkan_allocator(drm_fd)) != NULL) { ++ return alloc; ++ } + #endif + } + +@@ -138,7 +183,7 @@ struct wlr_allocator *wlr_allocator_autocreate(struct wlr_backend *backend, + + uint32_t drm_caps = WLR_BUFFER_CAP_DMABUF | WLR_BUFFER_CAP_DATA_PTR; + if ((backend_caps & drm_caps) && (renderer_caps & drm_caps) +- && drm_fd >= 0 && drmIsMaster(drm_fd)) { ++ && drm_fd >= 0 && drm_device && drmIsMaster(drm_fd)) { + wlr_log(WLR_DEBUG, "Trying to create drm dumb allocator"); + int dumb_fd = reopen_drm_node(drm_fd, false); + if (dumb_fd < 0) { +diff --git a/render/vulkan/allocator.c b/render/vulkan/allocator.c +new file mode 100644 +index 00000000..4c00d361 +--- /dev/null ++++ b/render/vulkan/allocator.c +@@ -0,0 +1,392 @@ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include "render/vulkan.h" ++ ++/* ++ * DMA-BUFs allocated by Vulkan, for a GPU whose device node is not a DRM device and so cannot be ++ * given to gbm - Adreno through KGSL on Android, where the node Turnip opens stands in for a ++ * render node. Each buffer is an exportable image with an explicit DRM format modifier, created ++ * the way vulkan_import_dmabuf() imports one for rendering, so the renderer and the parent ++ * compositor see the same layout. The device is our own, on the physical device the DRM fd names: ++ * this works under the gles2 renderer too, as long as its EGL can import the result. ++ */ ++ ++struct wlr_vk_allocator { ++ struct wlr_allocator base; ++ struct wlr_vk_device *dev; ++ PFN_vkGetMemoryFdKHR get_memory_fd; ++ PFN_vkGetImageDrmFormatModifierPropertiesEXT get_modifier_props; ++ struct wl_list buffers; // wlr_vk_allocator_buffer.link ++}; ++ ++struct wlr_vk_allocator_buffer { ++ struct wlr_buffer base; ++ struct wlr_vk_allocator *alloc; // NULL once the allocator is gone ++ struct wl_list link; ++ VkImage image; ++ VkDeviceMemory memory; ++ struct wlr_dmabuf_attributes dmabuf; ++}; ++ ++static const struct wlr_buffer_impl buffer_impl; ++ ++static struct wlr_vk_allocator_buffer *buffer_from_buffer(struct wlr_buffer *wlr_buffer) { ++ assert(wlr_buffer->impl == &buffer_impl); ++ struct wlr_vk_allocator_buffer *buffer = wl_container_of(wlr_buffer, buffer, base); ++ return buffer; ++} ++ ++static void buffer_release_vk(struct wlr_vk_allocator_buffer *buffer) { ++ if (buffer->alloc == NULL) { ++ return; ++ } ++ VkDevice dev = buffer->alloc->dev->dev; ++ // The exported DMA-BUF keeps the memory alive for whoever imported it. ++ if (buffer->image != VK_NULL_HANDLE) { ++ vkDestroyImage(dev, buffer->image, NULL); ++ } ++ if (buffer->memory != VK_NULL_HANDLE) { ++ vkFreeMemory(dev, buffer->memory, NULL); ++ } ++ buffer->image = VK_NULL_HANDLE; ++ buffer->memory = VK_NULL_HANDLE; ++ wl_list_remove(&buffer->link); ++ buffer->alloc = NULL; ++} ++ ++static void buffer_destroy(struct wlr_buffer *wlr_buffer) { ++ struct wlr_vk_allocator_buffer *buffer = buffer_from_buffer(wlr_buffer); ++ wlr_buffer_finish(wlr_buffer); ++ buffer_release_vk(buffer); ++ wlr_dmabuf_attributes_finish(&buffer->dmabuf); ++ free(buffer); ++} ++ ++static bool buffer_get_dmabuf(struct wlr_buffer *wlr_buffer, ++ struct wlr_dmabuf_attributes *dmabuf) { ++ struct wlr_vk_allocator_buffer *buffer = buffer_from_buffer(wlr_buffer); ++ *dmabuf = buffer->dmabuf; ++ return true; ++} ++ ++static const struct wlr_buffer_impl buffer_impl = { ++ .destroy = buffer_destroy, ++ .get_dmabuf = buffer_get_dmabuf, ++}; ++ ++// One vkCreateImage over the modifiers that share the same mutable-sRGB setting, the one thing ++// in the import that depends on the modifier. The driver picks the modifier. ++static VkImage create_image(struct wlr_vk_allocator *alloc, ++ const struct wlr_vk_format_props *props, int width, int height, ++ const uint64_t *mods, uint32_t mods_len, bool mutable_srgb) { ++ VkImageCreateInfo img_info = { ++ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, ++ .imageType = VK_IMAGE_TYPE_2D, ++ .format = props->format.vk, ++ .mipLevels = 1, ++ .arrayLayers = 1, ++ .samples = VK_SAMPLE_COUNT_1_BIT, ++ .sharingMode = VK_SHARING_MODE_EXCLUSIVE, ++ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, ++ .extent = (VkExtent3D){ width, height, 1 }, ++ .usage = vulkan_render_usage, ++ .tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT, ++ }; ++ if (mutable_srgb) { ++ img_info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; ++ } ++ ++ VkExternalMemoryImageCreateInfo eimg = { ++ .sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, ++ .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, ++ }; ++ img_info.pNext = &eimg; ++ ++ VkImageDrmFormatModifierListCreateInfoEXT mod_list = { ++ .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT, ++ .drmFormatModifierCount = mods_len, ++ .pDrmFormatModifiers = mods, ++ }; ++ eimg.pNext = &mod_list; ++ ++ VkFormat view_formats[] = { ++ props->format.vk, ++ props->format.vk_srgb, ++ }; ++ VkImageFormatListCreateInfoKHR list_info = { ++ .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO_KHR, ++ .pViewFormats = view_formats, ++ .viewFormatCount = sizeof(view_formats) / sizeof(view_formats[0]), ++ }; ++ if (mutable_srgb) { ++ mod_list.pNext = &list_info; ++ } ++ ++ VkImage image = VK_NULL_HANDLE; ++ VkResult res = vkCreateImage(alloc->dev->dev, &img_info, NULL, &image); ++ if (res != VK_SUCCESS) { ++ wlr_vk_error("vkCreateImage", res); ++ return VK_NULL_HANDLE; ++ } ++ return image; ++} ++ ++static struct wlr_buffer *allocator_create_buffer(struct wlr_allocator *wlr_allocator, ++ int width, int height, const struct wlr_drm_format *format) { ++ struct wlr_vk_allocator *alloc = wl_container_of(wlr_allocator, alloc, base); ++ VkDevice dev = alloc->dev->dev; ++ ++ const struct wlr_vk_format_props *props = ++ vulkan_format_props_from_drm(alloc->dev, format->format); ++ if (props == NULL) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: format 0x%08"PRIX32" not supported", ++ format->format); ++ return NULL; ++ } ++ ++ // The modifiers asked for that this device can render to at this size, split by whether the ++ // import will make the image mutable-format. INVALID (implicit) is not something Vulkan can ++ // allocate; LINEAR stands in for it when it is all there is. ++ uint64_t *mutable_mods = calloc(format->len + 1, sizeof(uint64_t)); ++ uint64_t *plain_mods = calloc(format->len + 1, sizeof(uint64_t)); ++ if (mutable_mods == NULL || plain_mods == NULL) { ++ free(mutable_mods); ++ free(plain_mods); ++ return NULL; ++ } ++ uint32_t mutable_len = 0, plain_len = 0; ++ bool implicit_only = format->len > 0; ++ for (size_t i = 0; i <= format->len; i++) { ++ uint64_t mod; ++ if (i < format->len) { ++ mod = format->modifiers[i]; ++ if (mod == DRM_FORMAT_MOD_INVALID) { ++ continue; ++ } ++ implicit_only = false; ++ } else if (implicit_only) { ++ mod = DRM_FORMAT_MOD_LINEAR; ++ } else { ++ break; ++ } ++ const struct wlr_vk_format_modifier_props *mp = ++ vulkan_format_props_find_modifier(props, mod, true); ++ if (mp == NULL || (uint32_t)width > mp->max_extent.width || ++ (uint32_t)height > mp->max_extent.height) { ++ continue; ++ } ++ if (mp->has_mutable_srgb) { ++ mutable_mods[mutable_len++] = mod; ++ } else { ++ plain_mods[plain_len++] = mod; ++ } ++ } ++ ++ VkImage image = VK_NULL_HANDLE; ++ if (mutable_len > 0) { ++ image = create_image(alloc, props, width, height, mutable_mods, mutable_len, true); ++ } ++ if (image == VK_NULL_HANDLE && plain_len > 0) { ++ image = create_image(alloc, props, width, height, plain_mods, plain_len, false); ++ } ++ free(mutable_mods); ++ free(plain_mods); ++ if (image == VK_NULL_HANDLE) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: no usable modifier for %dx%d format 0x%08"PRIX32, ++ width, height, format->format); ++ return NULL; ++ } ++ ++ struct wlr_vk_allocator_buffer *buffer = calloc(1, sizeof(*buffer)); ++ if (buffer == NULL) { ++ vkDestroyImage(dev, image, NULL); ++ return NULL; ++ } ++ wlr_buffer_init(&buffer->base, &buffer_impl, width, height); ++ buffer->alloc = alloc; ++ buffer->image = image; ++ wl_list_insert(&alloc->buffers, &buffer->link); ++ for (int i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { ++ buffer->dmabuf.fd[i] = -1; ++ } ++ ++ VkMemoryRequirements reqs; ++ vkGetImageMemoryRequirements(dev, image, &reqs); ++ int mem_type = vulkan_find_mem_type(alloc->dev, ++ VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, reqs.memoryTypeBits); ++ if (mem_type < 0) { ++ mem_type = vulkan_find_mem_type(alloc->dev, 0, reqs.memoryTypeBits); ++ } ++ if (mem_type < 0) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: no memory type for the image"); ++ goto error; ++ } ++ ++ VkMemoryDedicatedAllocateInfo dedicated = { ++ .sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO, ++ .image = image, ++ }; ++ VkExportMemoryAllocateInfo export = { ++ .sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO, ++ .pNext = &dedicated, ++ .handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, ++ }; ++ VkMemoryAllocateInfo mem_info = { ++ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, ++ .pNext = &export, ++ .allocationSize = reqs.size, ++ .memoryTypeIndex = mem_type, ++ }; ++ VkResult res = vkAllocateMemory(dev, &mem_info, NULL, &buffer->memory); ++ if (res != VK_SUCCESS) { ++ wlr_vk_error("vkAllocateMemory", res); ++ goto error; ++ } ++ res = vkBindImageMemory(dev, image, buffer->memory, 0); ++ if (res != VK_SUCCESS) { ++ wlr_vk_error("vkBindImageMemory", res); ++ goto error; ++ } ++ ++ VkImageDrmFormatModifierPropertiesEXT mod_props = { ++ .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT, ++ }; ++ res = alloc->get_modifier_props(dev, image, &mod_props); ++ if (res != VK_SUCCESS) { ++ wlr_vk_error("vkGetImageDrmFormatModifierPropertiesEXT", res); ++ goto error; ++ } ++ const struct wlr_vk_format_modifier_props *mp = ++ vulkan_format_props_find_modifier(props, mod_props.drmFormatModifier, true); ++ if (mp == NULL) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: driver chose modifier 0x%016"PRIX64 ++ " which it cannot render to", mod_props.drmFormatModifier); ++ goto error; ++ } ++ uint32_t n_planes = mp->props.drmFormatModifierPlaneCount; ++ if (n_planes == 0 || n_planes > WLR_DMABUF_MAX_PLANES) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: %"PRIu32" memory planes", n_planes); ++ goto error; ++ } ++ ++ VkMemoryGetFdInfoKHR fd_info = { ++ .sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR, ++ .memory = buffer->memory, ++ .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, ++ }; ++ int fd = -1; ++ res = alloc->get_memory_fd(dev, &fd_info, &fd); ++ if (res != VK_SUCCESS || fd < 0) { ++ wlr_vk_error("vkGetMemoryFdKHR", res); ++ goto error; ++ } ++ ++ buffer->dmabuf = (struct wlr_dmabuf_attributes){ ++ .width = width, ++ .height = height, ++ .format = format->format, ++ .modifier = mod_props.drmFormatModifier, ++ .n_planes = n_planes, ++ }; ++ for (int i = 0; i < WLR_DMABUF_MAX_PLANES; i++) { ++ buffer->dmabuf.fd[i] = -1; ++ } ++ static const VkImageAspectFlagBits plane_aspects[] = { ++ VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT, ++ VK_IMAGE_ASPECT_MEMORY_PLANE_1_BIT_EXT, ++ VK_IMAGE_ASPECT_MEMORY_PLANE_2_BIT_EXT, ++ VK_IMAGE_ASPECT_MEMORY_PLANE_3_BIT_EXT, ++ }; ++ for (uint32_t i = 0; i < n_planes; i++) { ++ VkImageSubresource sub = { .aspectMask = plane_aspects[i] }; ++ VkSubresourceLayout layout; ++ vkGetImageSubresourceLayout(dev, image, &sub, &layout); ++ buffer->dmabuf.offset[i] = layout.offset; ++ buffer->dmabuf.stride[i] = layout.rowPitch; ++ // Every plane lives in the one allocation; each gets its own fd, as ++ // wlr_dmabuf_attributes_finish() closes them one by one. ++ buffer->dmabuf.fd[i] = i == 0 ? fd : fcntl(fd, F_DUPFD_CLOEXEC, 0); ++ if (buffer->dmabuf.fd[i] < 0) { ++ wlr_log_errno(WLR_ERROR, "Vulkan allocator: dup failed"); ++ goto error; ++ } ++ } ++ ++ wlr_log(WLR_DEBUG, "Vulkan allocator: %dx%d format 0x%08"PRIX32" modifier 0x%016"PRIX64 ++ ", %"PRIu32" plane(s)", width, height, format->format, mod_props.drmFormatModifier, n_planes); ++ return &buffer->base; ++ ++error: ++ buffer_destroy(&buffer->base); ++ return NULL; ++} ++ ++static void allocator_destroy(struct wlr_allocator *wlr_allocator) { ++ struct wlr_vk_allocator *alloc = wl_container_of(wlr_allocator, alloc, base); ++ // Buffers may outlive their allocator; their DMA-BUF fds stay valid without the device. ++ struct wlr_vk_allocator_buffer *buffer, *tmp; ++ wl_list_for_each_safe(buffer, tmp, &alloc->buffers, link) { ++ buffer_release_vk(buffer); ++ } ++ struct wlr_vk_instance *ini = alloc->dev->instance; ++ vulkan_device_destroy(alloc->dev); ++ vulkan_instance_destroy(ini); ++ free(alloc); ++} ++ ++static const struct wlr_allocator_interface allocator_impl = { ++ .destroy = allocator_destroy, ++ .create_buffer = allocator_create_buffer, ++}; ++ ++struct wlr_allocator *vulkan_allocator_create(int drm_fd) { ++ struct wlr_vk_instance *ini = vulkan_instance_create(false); ++ if (ini == NULL) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: could not create an instance"); ++ return NULL; ++ } ++ VkPhysicalDevice phdev = vulkan_find_drm_phdev(ini, drm_fd); ++ if (phdev == VK_NULL_HANDLE) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: no Vulkan device for the DRM fd"); ++ vulkan_instance_destroy(ini); ++ return NULL; ++ } ++ struct wlr_vk_device *dev = vulkan_device_create(ini, phdev); ++ if (dev == NULL) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: could not create a device"); ++ vulkan_instance_destroy(ini); ++ return NULL; ++ } ++ ++ struct wlr_vk_allocator *alloc = calloc(1, sizeof(*alloc)); ++ if (alloc == NULL) { ++ vulkan_device_destroy(dev); ++ vulkan_instance_destroy(ini); ++ return NULL; ++ } ++ alloc->dev = dev; ++ wl_list_init(&alloc->buffers); ++ alloc->get_memory_fd = (PFN_vkGetMemoryFdKHR) ++ vkGetDeviceProcAddr(dev->dev, "vkGetMemoryFdKHR"); ++ alloc->get_modifier_props = (PFN_vkGetImageDrmFormatModifierPropertiesEXT) ++ vkGetDeviceProcAddr(dev->dev, "vkGetImageDrmFormatModifierPropertiesEXT"); ++ if (alloc->get_memory_fd == NULL || alloc->get_modifier_props == NULL) { ++ wlr_log(WLR_ERROR, "Vulkan allocator: the device lacks DMA-BUF export"); ++ free(alloc); ++ vulkan_device_destroy(dev); ++ vulkan_instance_destroy(ini); ++ return NULL; ++ } ++ ++ wlr_allocator_init(&alloc->base, &allocator_impl, WLR_BUFFER_CAP_DMABUF); ++ wlr_log(WLR_INFO, "Created Vulkan DMA-BUF allocator"); ++ return &alloc->base; ++} +diff --git a/render/vulkan/meson.build b/render/vulkan/meson.build +index 36a822f9..abeb5337 100644 +--- a/render/vulkan/meson.build ++++ b/render/vulkan/meson.build +@@ -37,6 +37,7 @@ glslang_version_info = run_command(glslang, '--version', check: true).stdout() + glslang_version = glslang_version_info.split('\n')[0].split(':')[-1] + + wlr_files += files( ++ 'allocator.c', + 'pass.c', + 'renderer.c', + 'texture.c', +diff --git a/types/wlr_linux_dmabuf_v1.c b/types/wlr_linux_dmabuf_v1.c +index 3165e880..a7fe060c 100644 +--- a/types/wlr_linux_dmabuf_v1.c ++++ b/types/wlr_linux_dmabuf_v1.c +@@ -901,11 +901,22 @@ static bool set_default_feedback(struct wlr_linux_dmabuf_v1 *linux_dmabuf, + if (device->available_nodes & (1 << DRM_NODE_RENDER)) { + const char *name = device->nodes[DRM_NODE_RENDER]; + main_device_fd = open(name, O_RDWR | O_CLOEXEC); +- drmFreeDevice(&device); + if (main_device_fd < 0) { + wlr_log_errno(WLR_ERROR, "Failed to open DRM device %s", name); ++ drmFreeDevice(&device); + goto error_compiled; + } ++ // A GPU node that stands in for a render node without being a DRM device (KGSL on ++ // Android) cannot import a buffer as a GEM handle; every client's would be refused. ++ drmVersion *version = drmGetVersion(main_device_fd); ++ if (version == NULL) { ++ wlr_log(WLR_INFO, "%s is not a DRM device, skipping DMA-BUF import checks", name); ++ close(main_device_fd); ++ main_device_fd = -1; ++ } else { ++ drmFreeVersion(version); ++ } ++ drmFreeDevice(&device); + } else { + // Likely a split display/render setup. Unfortunately we have no way to + // get back the proper render node used by the renderer under-the-hood.